internal/provider/architecture_data_source.go (537 lines of code) (raw):

package provider import ( "context" "errors" "fmt" "math/big" "time" "github.com/Azure/alzlib/deployment" "github.com/Azure/alzlib/to" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armpolicy" "github.com/Azure/terraform-provider-alz/internal/provider/gen" "github.com/Azure/terraform-provider-alz/internal/typehelper" "github.com/Azure/terraform-provider-alz/internal/typehelper/frameworktype" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" ) var _ datasource.DataSource = (*architectureDataSource)(nil) var _ datasource.DataSourceWithConfigure = (*architectureDataSource)(nil) func NewArchitectureDataSource() datasource.DataSource { return &architectureDataSource{} } type architectureDataSource struct { data *alzProviderData } func (d *architectureDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { resp.TypeName = req.ProviderTypeName + "_architecture" } func (d *architectureDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { resp.Schema = gen.ArchitectureDataSourceSchema(ctx) } func (d *architectureDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { // Prevent panic if the provider has not been configured. if req.ProviderData == nil { return } data, ok := req.ProviderData.(*alzProviderData) if !ok { resp.Diagnostics.AddError( "architectureDataSource.Configure() Unexpected type", fmt.Sprintf("Expected *alzProviderData, got: %T. Please report this issue to the provider developers.", req.ProviderData), ) return } d.data = data } func (d *architectureDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { var data gen.ArchitectureModel // Read Terraform configuration data into the model resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) if resp.Diagnostics.HasError() { return } readTimeout, diags := data.Timeouts.Read(ctx, 10*time.Minute) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } ctx, cancel := context.WithTimeout(ctx, readTimeout) defer cancel() if d.data == nil { resp.Diagnostics.AddError( "architectureDataSource.Read() Provider not configured", "The provider has not been configured. Please see the provider documentation for configuration instructions.", ) return } // Use alzlib to create the hierarchy from the supplied architecture depl := deployment.NewHierarchy(d.data.AlzLib) if err := depl.FromArchitecture(ctx, data.Name.ValueString(), data.RootManagementGroupId.ValueString(), data.Location.ValueString()); err != nil { resp.Diagnostics.AddError( fmt.Sprintf("architectureDataSource.Read() Error creating architecture %s", data.Name.ValueString()), err.Error(), ) return } // Process assignPermissions overrides setting the values in the alzlib assignPermissionsSetValues := []gen.OverridePolicyDefinitionParameterAssignPermissionsSetValue{} resp.Diagnostics.Append(data.OverridePolicyDefinitionParameterAssignPermissionsSet.ElementsAs( ctx, &assignPermissionsSetValues, false, )...) if resp.Diagnostics.HasError() { return } for _, assignPermissionsSetValue := range assignPermissionsSetValues { if assignPermissionsSetValue.DefinitionName.IsUnknown() || assignPermissionsSetValue.ParameterName.IsUnknown() { continue } d.data.SetAssignPermissionsOnDefinitionParameter( assignPermissionsSetValue.DefinitionName.ValueString(), assignPermissionsSetValue.ParameterName.ValueString(), ) } // Process assignPermissions overrides unsetting the values in the alzlib assignPermissionsUnsetValues := []gen.OverridePolicyDefinitionParameterAssignPermissionsUnsetValue{} resp.Diagnostics.Append(data.OverridePolicyDefinitionParameterAssignPermissionsUnset.ElementsAs( ctx, &assignPermissionsUnsetValues, false, )...) if resp.Diagnostics.HasError() { return } for _, assignPermissionsUnsetValue := range assignPermissionsUnsetValues { if assignPermissionsUnsetValue.DefinitionName.IsUnknown() || assignPermissionsUnsetValue.ParameterName.IsUnknown() { continue } d.data.UnsetAssignPermissionsOnDefinitionParameter( assignPermissionsUnsetValue.DefinitionName.ValueString(), assignPermissionsUnsetValue.ParameterName.ValueString(), ) } // Set policy assignment defaults defaultsMap := convertPolicyAssignmentParametersMapToSdkType(data.PolicyDefaultValues, resp) if resp.Diagnostics.HasError() { return } for defName, paramVal := range defaultsMap { if err := depl.AddDefaultPolicyAssignmentValue(ctx, defName, paramVal); err != nil { resp.Diagnostics.AddError( fmt.Sprintf("architectureDataSource.Read() Error applying policy assignment default `%s`", defName), err.Error(), ) return } } // Modify policy assignments modifyPolicyAssignments(ctx, depl, data, resp) if resp.Diagnostics.HasError() { return } // Generate policy role assignments policyRoleAssignments, err := depl.PolicyRoleAssignments(ctx) if err != nil { var praErr *deployment.PolicyRoleAssignmentErrors as := errors.As(err, &praErr) if !as { resp.Diagnostics.AddError( "architectureDataSource.Read() Error generating policy role assignments", err.Error(), ) return } if !d.data.suppressWarningPolicyRoleAssignments { resp.Diagnostics.AddWarning( "architectureDataSource.Read() External role assignment creation required for Azure Policy assignments.", fmt.Sprintf("This is a known limitation, please do not raise GitHub issues!\nTo suppress this message see the provider flag: `suppress_warning_policy_role_assignments`\n\nSee `https://github.com/Azure/alzlib/issues/189`\n\n%s", praErr.Error()), ) } } policyRoleAssignmentsVal, diags := policyRoleAssignmentsSetToProviderType(ctx, policyRoleAssignments.ToSlice()) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } data.PolicyRoleAssignments = policyRoleAssignmentsVal // Set computed values mgNames := depl.ManagementGroupNames() mgVals := make([]gen.ManagementGroupsValue, len(mgNames)) for i, mgName := range mgNames { mgVal, diags := alzMgToProviderType(ctx, depl.ManagementGroup(mgName)) resp.Diagnostics.Append(diags...) mgVals[i] = mgVal } mgs, diags := types.ListValueFrom(ctx, gen.NewManagementGroupsValueNull().Type(ctx), &mgVals) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } data.ManagementGroups = mgs // Set the id to keep ACC tests happy data.Id = data.Name // Save data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } func modifyPolicyAssignments(ctx context.Context, depl *deployment.Hierarchy, data gen.ArchitectureModel, resp *datasource.ReadResponse) { for mgName, pa2modValue := range data.PolicyAssignmentsToModify.Elements() { mg := depl.ManagementGroup(mgName) if mg == nil { resp.Diagnostics.AddWarning( "architectureDataSource.Read() Warning modifying policy assignments", fmt.Sprintf("Management group `%s` not found in hierarchy", mgName), ) continue } pa2mod, ok := pa2modValue.(gen.PolicyAssignmentsToModifyValue) if !ok { resp.Diagnostics.AddError( "architectureDataSource.Read() Error converting policy assignments to modify", "Error converting policy assignments to modify element to `gen.PolicyAssignmentsToModifyValue`", ) return } for paName, modValue := range pa2mod.PolicyAssignments.Elements() { mod, ok := modValue.(gen.PolicyAssignmentsValue) if !ok { resp.Diagnostics.AddError( "architectureDataSource.Read() Error converting policy assignment to modify", "Error converting policy assignments element to `gen.PolicyAssignmentsValue`", ) return } enf, ident, noncompl, params, resourceSel, overrides := policyAssignmentType2ArmPolicyValues(ctx, mod, resp) if resp.Diagnostics.HasError() { resp.Diagnostics.AddError( "architectureDataSource.Read() Error converting policy assignment values to Azure SDK types", fmt.Sprintf("Error modifying policy assignment values for `%s` at mg `%s`", paName, mgName), ) return } if err := mg.ModifyPolicyAssignment(paName, params, enf, noncompl, ident, resourceSel, overrides); err != nil { resp.Diagnostics.AddError( "architectureDataSource.Read() Error modifying policy assignment values in alzlib", fmt.Sprintf("Error modifying policy assignment values for `%s` at mg `%s`: %s", paName, mgName, err.Error()), ) return } } } } func policyRoleAssignmentsSetToProviderType(ctx context.Context, input []deployment.PolicyRoleAssignment) (basetypes.SetValue, diag.Diagnostics) { var diags diag.Diagnostics praSlice := make([]gen.PolicyRoleAssignmentsValue, 0, len(input)) for _, v := range input { pra, diag := policyRoleAssignmentToProviderType(ctx, v) diags.Append(diag...) praSlice = append(praSlice, pra) } if diags.HasError() { return types.SetNull(gen.NewPolicyRoleAssignmentsValueNull().Type(ctx)), diags } return types.SetValueFrom(ctx, gen.NewPolicyRoleAssignmentsValueNull().Type(ctx), &praSlice) } func policyRoleAssignmentToProviderType(ctx context.Context, input deployment.PolicyRoleAssignment) (gen.PolicyRoleAssignmentsValue, diag.Diagnostics) { return gen.NewPolicyRoleAssignmentsValue( gen.NewPolicyRoleAssignmentsValueNull().AttributeTypes(ctx), map[string]attr.Value{ "role_definition_id": types.StringValue(input.RoleDefinitionId), "scope": types.StringValue(input.Scope), "policy_assignment_name": types.StringValue(input.AssignmentName), "management_group_id": types.StringValue(input.ManagementGroupId), }, ) } func alzMgToProviderType(ctx context.Context, mg *deployment.HierarchyManagementGroup) (gen.ManagementGroupsValue, diag.Diagnostics) { var respDiags diag.Diagnostics policyAssignments, diags := typehelper.ConvertAlzMapToFrameworkType(mg.PolicyAssignmentMap()) respDiags.Append(diags...) policyDefinitions, diags := typehelper.ConvertAlzMapToFrameworkType(mg.PolicyDefinitionsMap()) respDiags.Append(diags...) policySetDefinitions, diags := typehelper.ConvertAlzMapToFrameworkType(mg.PolicySetDefinitionsMap()) respDiags.Append(diags...) roleDefinitions, diags := typehelper.ConvertAlzMapToFrameworkType(mg.RoleDefinitionsMap()) respDiags.Append(diags...) if respDiags.HasError() { return gen.NewManagementGroupsValueNull(), respDiags } return gen.NewManagementGroupsValue( gen.NewManagementGroupsValueNull().AttributeTypes(ctx), map[string]attr.Value{ "id": types.StringValue(mg.Name()), "parent_id": types.StringValue(mg.ParentId()), "display_name": types.StringValue(mg.DisplayName()), "exists": types.BoolValue(mg.Exists()), "level": types.NumberValue(big.NewFloat(float64(mg.Level()))), "policy_assignments": policyAssignments, "policy_definitions": policyDefinitions, "policy_set_definitions": policySetDefinitions, "role_definitions": roleDefinitions, }, ) } // policyAssignmentType2ArmPolicyValues returns a set of Azure Go SDK values from a PolicyAssignmentType. // This is used to modify existing policy assignments. func policyAssignmentType2ArmPolicyValues(ctx context.Context, pa gen.PolicyAssignmentsValue, resp *datasource.ReadResponse) ( enforcementMode *armpolicy.EnforcementMode, identity *armpolicy.Identity, nonComplianceMessages []*armpolicy.NonComplianceMessage, parameters map[string]*armpolicy.ParameterValuesValue, resourceSelectors []*armpolicy.ResourceSelector, overrides []*armpolicy.Override) { // Set enforcement mode. enforcementMode = convertPolicyAssignmentEnforcementModeToSdkType(pa.EnforcementMode) // set identity identity = convertPolicyAssignmentIdentityToSdkType(pa.Identity, pa.IdentityIds, resp) if resp.Diagnostics.HasError() { return nil, nil, nil, nil, nil, nil } // set non-compliance message if isKnown(pa.NonComplianceMessages) { nonCompl := make([]gen.NonComplianceMessagesValue, len(pa.NonComplianceMessages.Elements())) for i, msg := range pa.NonComplianceMessages.Elements() { frameworkMsg, ok := msg.(gen.NonComplianceMessagesValue) if !ok { resp.Diagnostics.AddError( "policyAssignmentType2ArmPolicyValues: error", "unable to convert non-compliance message attr.Value to concrete type", ) } nonCompl[i] = frameworkMsg } if resp.Diagnostics.HasError() { return nil, nil, nil, nil, nil, nil } nonComplianceMessages = convertPolicyAssignmentNonComplianceMessagesToSdkType(nonCompl) } // set parameters parameters = convertPolicyAssignmentParametersMapToSdkType(pa.Parameters, resp) if resp.Diagnostics.HasError() { return nil, nil, nil, nil, nil, nil } // set resource selectors if isKnown(pa.ResourceSelectors) { rS := make([]gen.ResourceSelectorsValue, len(pa.ResourceSelectors.Elements())) resp.Diagnostics.Append(pa.ResourceSelectors.ElementsAs(ctx, &rS, false)...) resourceSelectors = convertPolicyAssignmentResourceSelectorsToSdkType(ctx, rS, resp) if resp.Diagnostics.HasError() { return nil, nil, nil, nil, nil, nil } } // set overrides if isKnown(pa.Overrides) { ovr := make([]gen.OverridesValue, len(pa.Overrides.Elements())) resp.Diagnostics.Append(pa.Overrides.ElementsAs(ctx, &ovr, false)...) overrides = convertPolicyAssignmentOverridesToSdkType(ctx, ovr, resp) if resp.Diagnostics.HasError() { return nil, nil, nil, nil, nil, nil } } return enforcementMode, identity, nonComplianceMessages, parameters, resourceSelectors, overrides } func convertPolicyAssignmentOverridesToSdkType(ctx context.Context, input []gen.OverridesValue, resp *datasource.ReadResponse) []*armpolicy.Override { if len(input) == 0 { return nil } res := make([]*armpolicy.Override, len(input)) for i, o := range input { selectors := make([]*armpolicy.Selector, len(o.OverrideSelectors.Elements())) for j, s := range o.OverrideSelectors.Elements() { osv, ok := s.(gen.OverrideSelectorsValue) if !ok { resp.Diagnostics.AddError( "convertPolicyAssignmentOverridesToSdkType: error", "unable to convert override selectors attr.Value to concrete type", ) } // Convert In to a go slice, start off from an uninitialized slice so that the value is nil if the input is empty. var in []*string if len(osv.In.Elements()) != 0 { var err error in, err = frameworktype.SliceOfPrimitiveToGo[string](ctx, osv.In.Elements()) if err != nil { resp.Diagnostics.AddError( "convertPolicyAssignmentOverridesToSdkType: error", fmt.Sprintf("unable to convert OverrideSelctorsValue.In elements to Go slice: %s", err.Error()), ) return nil } } // Convert NotIn to a go slice, start off from an uninitialized slice so that the value is nil if the input is empty. var notIn []*string if len(osv.NotIn.Elements()) != 0 { var err error notIn, err = frameworktype.SliceOfPrimitiveToGo[string](ctx, osv.NotIn.Elements()) if err != nil { resp.Diagnostics.AddError( "convertPolicyAssignmentOverridesToSdkType: error", fmt.Sprintf("unable to convert OverrideSelctorsValue.NotIn elements to Go slice: %s", err.Error()), ) return nil } } selectors[j] = &armpolicy.Selector{ Kind: to.Ptr(armpolicy.SelectorKind(osv.Kind.ValueString())), In: in, NotIn: notIn, } } res[i] = &armpolicy.Override{ Kind: to.Ptr(armpolicy.OverrideKind(o.Kind.ValueString())), Value: to.Ptr(o.Value.ValueString()), Selectors: selectors, } } return res } func convertPolicyAssignmentResourceSelectorsToSdkType(ctx context.Context, input []gen.ResourceSelectorsValue, resp *datasource.ReadResponse) []*armpolicy.ResourceSelector { if len(input) == 0 { return nil } res := make([]*armpolicy.ResourceSelector, len(input)) for i, rs := range input { selectors := make([]*armpolicy.Selector, len(rs.ResourceSelectorSelectors.Elements())) for j, s := range rs.ResourceSelectorSelectors.Elements() { rssv, ok := s.(gen.ResourceSelectorSelectorsValue) if !ok { resp.Diagnostics.AddError( "convertPolicyAssignmentResourceSelectorsToSdkType: error", "unable to convert resource selector selectors attr.Value to concrete type", ) } // Convert In to a go slice, start off from an uninitialized slice so that the value is nil if the input is empty. var in []*string if len(rssv.In.Elements()) != 0 { var err error in, err = frameworktype.SliceOfPrimitiveToGo[string](ctx, rssv.In.Elements()) if err != nil { resp.Diagnostics.AddError( "convertPolicyAssignmentResourceSelectorsToSdkType: error", fmt.Sprintf("unable to convert ResourceSelectorSelectorsValue.In elements to Go slice: %s", err.Error()), ) return nil } } // Convert NotIn to a go slice, start off from an uninitialized slice so that the value is nil if the input is empty. var notIn []*string if len(rssv.NotIn.Elements()) != 0 { var err error notIn, err = frameworktype.SliceOfPrimitiveToGo[string](ctx, rssv.NotIn.Elements()) if err != nil { resp.Diagnostics.AddError( "convertPolicyAssignmentResourceSelectorsToSdkType: error", fmt.Sprintf("unable to convert ResourceSelectorSelectorsValue.NotIn elements to Go slice: %s", err.Error()), ) return nil } } selectors[j] = &armpolicy.Selector{ Kind: to.Ptr(armpolicy.SelectorKind(rssv.Kind.ValueString())), In: in, NotIn: notIn, } } res[i] = &armpolicy.ResourceSelector{ Name: to.Ptr(rs.Name.ValueString()), Selectors: selectors, } } return res } func convertPolicyAssignmentEnforcementModeToSdkType(src types.String) *armpolicy.EnforcementMode { if !isKnown(src) { return nil } switch src.ValueString() { case "DoNotEnforce": return to.Ptr(armpolicy.EnforcementModeDoNotEnforce) case "Default": return to.Ptr(armpolicy.EnforcementModeDefault) } return nil } func convertPolicyAssignmentNonComplianceMessagesToSdkType(src []gen.NonComplianceMessagesValue) []*armpolicy.NonComplianceMessage { if len(src) == 0 { return nil } res := make([]*armpolicy.NonComplianceMessage, len(src)) for i, msg := range src { res[i] = &armpolicy.NonComplianceMessage{ Message: msg.Message.ValueStringPointer(), } if isKnown(msg.PolicyDefinitionReferenceId) { res[i].PolicyDefinitionReferenceID = to.Ptr(msg.PolicyDefinitionReferenceId.ValueString()) } } return res } func convertPolicyAssignmentIdentityToSdkType(typ types.String, ids types.Set, resp *datasource.ReadResponse) *armpolicy.Identity { if !isKnown(typ) { return nil } var identity *armpolicy.Identity switch typ.ValueString() { case "SystemAssigned": identity = to.Ptr(armpolicy.Identity{ Type: to.Ptr(armpolicy.ResourceIdentityTypeSystemAssigned), }) case "UserAssigned": if ids.IsUnknown() { return nil } var id string if len(ids.Elements()) != 1 { resp.Diagnostics.AddError( "convertPolicyAssignmentIdentityToSdkType: error", "one (and only one) identity id is required for user assigned identity", ) return nil } idStr, ok := ids.Elements()[0].(types.String) if !ok { resp.Diagnostics.AddError( "convertPolicyAssignmentIdentityToSdkType: error", "unable to convert identity id to string", ) return nil } id = idStr.ValueString() identity = to.Ptr(armpolicy.Identity{ Type: to.Ptr(armpolicy.ResourceIdentityTypeUserAssigned), UserAssignedIdentities: map[string]*armpolicy.UserAssignedIdentitiesValue{id: {}}, }) default: resp.Diagnostics.AddError( "convertPolicyAssignmentIdentityToSdkType: error", fmt.Sprintf("unknown identity type: %s", typ.ValueString()), ) return nil } return identity } // convertPolicyAssignmentParametersMapToSdkType converts a map with a JSON string value to a map[string]*armpolicy.ParameterValuesValue. func convertPolicyAssignmentParametersMapToSdkType(src types.Map, resp *datasource.ReadResponse) map[string]*armpolicy.ParameterValuesValue { if !isKnown(src) { return nil } result := make(map[string]*armpolicy.ParameterValuesValue) for k, v := range src.Elements() { // Even thought he schema type is identical, from policy assignments to modify we receive basetypes.String values, // but from policy default values we receive jsontypes.Noprmalized. // We convert to Terraform value to get the string representation. vTf, err := v.ToTerraformValue(context.Background()) if err != nil { resp.Diagnostics.AddError( "convertPolicyAssignmentParametersMapToSdkType: error", "unable to convert parameter value to Terraform value", ) return nil } var vStr string err = vTf.Copy().As(&vStr) if err != nil { resp.Diagnostics.AddError( "convertPolicyAssignmentParametersMapToSdkType: error", "unable to convert parameter value to string", ) return nil } var pv armpolicy.ParameterValuesValue if err := pv.UnmarshalJSON([]byte(vStr)); err != nil { resp.Diagnostics.AddError( "convertPolicyAssignmentParametersMapToSdkType: error", fmt.Sprintf("unable to unmarshal policy parameter value: %s", err.Error()), ) return nil } if pv.Value == nil { resp.Diagnostics.AddError( "convertPolicyAssignmentParametersMapToSdkType: error", fmt.Sprintf("policy parameter `%s` value is nil, make sure to supply parameter value as follows: `jsonencode({ value = \"foo\" })`, or `jsonencode({ value = 1 })`", k), ) return nil } result[k] = &pv } return result } func isKnown(val attr.Value) bool { return !val.IsNull() && !val.IsUnknown() }