internal/services/resource.go (256 lines of code) (raw):

package services import ( "context" "encoding/json" "fmt" "log" "strings" "github.com/Azure/terraform-provider-azapi/internal/azure" "github.com/Azure/terraform-provider-azapi/internal/azure/identity" aztypes "github.com/Azure/terraform-provider-azapi/internal/azure/types" "github.com/Azure/terraform-provider-azapi/internal/services/dynamic" "github.com/Azure/terraform-provider-azapi/utils" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" "github.com/hashicorp/terraform-plugin-log/tflog" ) func schemaValidate(config *AzapiResourceModel) error { if config == nil { return nil } azureResourceType, apiVersion, err := utils.GetAzureResourceTypeApiVersion(config.Type.ValueString()) if err != nil { return fmt.Errorf(`the argument "type" is invalid: %s`, err.Error()) } resourceDef, _ := azure.GetResourceDefinition(azureResourceType, apiVersion) log.Printf("[INFO] prepare validation for resource type: %s, api-version: %s", azureResourceType, apiVersion) versions := azure.GetApiVersions(azureResourceType) if len(versions) == 0 { return schemaValidationError(fmt.Sprintf("the argument \"type\" is invalid.\n resource type %s can't be found.\n", azureResourceType)) } isVersionValid := false for _, version := range versions { if version == apiVersion { isVersionValid = true break } } if !isVersionValid { return schemaValidationError(fmt.Sprintf("the argument \"type\"'s api-version is invalid.\n The supported versions are [%s].\n", strings.Join(versions, ", "))) } if resourceDef == nil { return nil } var bodyToValidate attr.Value if !config.Body.IsNull() && !config.Body.IsUnknown() && !config.Body.IsNull() && !config.Body.IsUnderlyingValueUnknown() { if v, ok := config.Body.UnderlyingValue().(types.Object); ok { attributes := v.Attributes() attributeTypes := v.AttributeTypes(context.Background()) attributes["name"] = config.Name attributeTypes["name"] = types.StringType if !config.Location.IsNull() { attributes["location"] = config.Location attributeTypes["location"] = types.StringType } if !config.Tags.IsNull() { attributes["tags"] = config.Tags attributeTypes["tags"] = types.MapType{ElemType: types.StringType} } if !config.Identity.IsNull() { identityAttributeTypes := map[string]attr.Type{ "type": types.StringType, } identityModel := identity.FromList(config.Identity) if len(identityModel.IdentityIDs.Elements()) != 0 { identityAttributeTypes["userAssignedIdentities"] = types.MapType{ElemType: types.DynamicType} elements := make(map[string]attr.Value) identityIds := identityModel.IdentityIDs.Elements() for _, identityId := range identityIds { elements[identityId.(types.String).ValueString()] = types.DynamicNull() } attributes["identity"] = types.ObjectValueMust(identityAttributeTypes, map[string]attr.Value{ "type": identityModel.Type, "userAssignedIdentities": types.MapValueMust(types.DynamicType, elements), }) } else { attributes["identity"] = types.ObjectValueMust(identityAttributeTypes, map[string]attr.Value{ "type": identityModel.Type, }) } attributeTypes["identity"] = types.ObjectType{AttrTypes: identityAttributeTypes} } bodyToValidate = types.ObjectValueMust(attributeTypes, attributes) } } else { bodyToValidate = config.Body } bodyToValidate, err = dynamic.MergeDynamic(types.DynamicValue(bodyToValidate), config.SensitiveBody) if err != nil { return fmt.Errorf("failed to merge write-only body: %s", err) } validateErrors := (*resourceDef).Validate(bodyToValidate, "") errors := make([]error, 0) // skip error that location is not in the body, because user might use the default location feature // same for tags for _, err := range validateErrors { if strings.Contains(err.Error(), "`location` is required") || strings.Contains(err.Error(), "`tags` is required") { continue } errors = append(errors, err) } if len(errors) != 0 { errorMsg := "the argument \"body\" is invalid:\n" for _, err := range errors { errorMsg += fmt.Sprintf("%s\n", err.Error()) } return schemaValidationError(errorMsg) } return nil } func schemaValidationError(detail string) error { return fmt.Errorf("embedded schema validation failed: %s You can try to update `azapi` provider to "+ "the latest version or disable the validation using the feature flag `schema_validation_enabled = false` "+ "within the resource block", detail) } func canResourceHaveProperty(resourceDef *aztypes.ResourceType, property string) bool { if resourceDef == nil || resourceDef.Body == nil || resourceDef.Body.Type == nil { return false } objectType, ok := (*resourceDef.Body.Type).(*aztypes.ObjectType) if !ok { return false } if prop, ok := objectType.Properties[property]; ok { if !prop.IsReadOnly() { return true } } return false } func flattenBody(responseBody interface{}, resourceDef *aztypes.ResourceType) (types.Dynamic, error) { body := utils.NormalizeObject(responseBody) if resourceDef != nil { SensitiveBody := (*resourceDef).GetWriteOnly(body) if bodyMap, ok := SensitiveBody.(map[string]interface{}); ok { delete(bodyMap, "location") delete(bodyMap, "tags") delete(bodyMap, "name") delete(bodyMap, "identity") SensitiveBody = bodyMap } body = SensitiveBody } data, err := json.Marshal(body) if err != nil { return types.DynamicNull(), err } return dynamic.FromJSONImplied(data) } func flattenOutput(responseBody interface{}, paths []string) attr.Value { for _, path := range paths { if path == "*" { if v, ok := responseBody.(string); ok { return basetypes.NewStringValue(v) } data, err := json.Marshal(responseBody) if err != nil { return nil } out, err := dynamic.FromJSONImplied(data) if err != nil { return nil } return out } } var output interface{} output = make(map[string]interface{}) for _, path := range paths { part := utils.ExtractObject(responseBody, path) if part == nil { continue } output = utils.MergeObject(output, part) } data, err := json.Marshal(output) if err != nil { return nil } out, err := dynamic.FromJSONImplied(data) if err != nil { return nil } return out } func flattenOutputJMES(responseBody interface{}, paths map[string]string) attr.Value { var output interface{} output = make(map[string]interface{}) for pathKey, path := range paths { part := utils.ExtractObjectJMES(responseBody, pathKey, path) if part == nil { continue } output = utils.MergeObject(output, part) } data, err := json.Marshal(output) if err != nil { return nil } out, err := dynamic.FromJSONImplied(data) if err != nil { return nil } return out } func AsStringList(input types.List) []string { var result []string diags := input.ElementsAs(context.Background(), &result, false) if diags.HasError() { tflog.Warn(context.Background(), fmt.Sprintf("failed to convert list to string list: %s", diags)) } return result } func AsMapOfString(input types.Map) map[string]string { result := make(map[string]string) diags := input.ElementsAs(context.Background(), &result, false) if diags.HasError() { tflog.Warn(context.Background(), fmt.Sprintf("failed to convert input to map of strings: %s", diags)) } return result } func AsMapOfLists(input types.Map) map[string][]string { result := make(map[string][]string) diags := input.ElementsAs(context.Background(), &result, false) if diags.HasError() { tflog.Warn(context.Background(), fmt.Sprintf("failed to convert input to map of lists: %s", diags)) } return result } func unmarshalBody(input types.Dynamic, out interface{}) error { if input.IsNull() || input.IsUnknown() || input.IsUnderlyingValueUnknown() { return nil } data, err := dynamic.ToJSON(input) if err != nil { return fmt.Errorf(`invalid dynamic value: value: %s, err: %+v`, input.String(), err) } if err = json.Unmarshal(data, &out); err != nil { return fmt.Errorf(`unmarshaling failed: value: %s, err: %+v`, string(data), err) } return nil } // ephemeralBodyChangeInPlan checks if the sensitive_body has changed in the plan modify phase. func ephemeralBodyChangeInPlan(ctx context.Context, d PrivateData, ephemeralBody types.Dynamic) (ok bool, diags diag.Diagnostics) { tflog.Warn(ctx, fmt.Sprintf("sensitive_bodyChangeInPlan: sensitive_body: %s", ephemeralBody.String())) // 1. sensitive_body is unknown (e.g. referencing an knonw-after-apply value) if !dynamic.IsFullyKnown(ephemeralBody) { return true, nil } // 2. sensitive_body is known in the config, but has different hash than the private data eb, err := dynamic.ToJSON(ephemeralBody) if err != nil { diags.AddError( `Error to marshal "sensitive_body"`, err.Error(), ) return } return ephemeralBodyPrivateMgr.Diff(ctx, d, eb) }