diff --git a/go.mod b/go.mod index e145703979..6fbb194f0b 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/hashicorp/hcl/v2 v2.24.0 github.com/hashicorp/terraform-json v0.27.2 github.com/hashicorp/terraform-plugin-sdk/v2 v2.40.1 - github.com/hashicorp/terraform-provider-google-beta v1.20.1-0.20260602172209-fb50b62c5fbc + github.com/hashicorp/terraform-provider-google-beta v1.20.1-0.20260602184719-11c414cc2f63 github.com/mitchellh/go-homedir v1.1.0 github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.11.1 diff --git a/go.sum b/go.sum index 6b878cc899..2d06b302dc 100644 --- a/go.sum +++ b/go.sum @@ -184,8 +184,8 @@ github.com/hashicorp/terraform-plugin-sdk/v2 v2.40.1 h1:2yPUd7esMOpuTaG3y1iEla1i github.com/hashicorp/terraform-plugin-sdk/v2 v2.40.1/go.mod h1:sq8qsxh+PwdvTQFcd17kfCoBgQo46ADNMvCpKE7t/gY= github.com/hashicorp/terraform-plugin-testing v1.15.0 h1:/fimKyl0YgD7aAtJkuuAZjwBASXhCIwWqMbDLnKLMe4= github.com/hashicorp/terraform-plugin-testing v1.15.0/go.mod h1:bGXMw7bE95EiZhSBV3rM2W8TiffaPTDuLS+HFI/lIYs= -github.com/hashicorp/terraform-provider-google-beta v1.20.1-0.20260602172209-fb50b62c5fbc h1:dp7XKxjGr09k9d9Pzi6dggVm1wBnBwuRlsfOoGj+p/Y= -github.com/hashicorp/terraform-provider-google-beta v1.20.1-0.20260602172209-fb50b62c5fbc/go.mod h1:L2DsgkalAwUunCiZtPSaSYjTkGeYqjlRufNNrWRgg0s= +github.com/hashicorp/terraform-provider-google-beta v1.20.1-0.20260602184719-11c414cc2f63 h1:ih459GDoWgmJOxxYcMsdwagIBV/eAHS+gj8EmZSiYNU= +github.com/hashicorp/terraform-provider-google-beta v1.20.1-0.20260602184719-11c414cc2f63/go.mod h1:L2DsgkalAwUunCiZtPSaSYjTkGeYqjlRufNNrWRgg0s= github.com/hashicorp/terraform-registry-address v0.4.0 h1:S1yCGomj30Sao4l5BMPjTGZmCNzuv7/GDTDX99E9gTk= github.com/hashicorp/terraform-registry-address v0.4.0/go.mod h1:LRS1Ay0+mAiRkUyltGT+UHWkIqTFvigGn/LbMshfflE= github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ= diff --git a/tfplan2cai/converters/google/resources/services/dataplex/dataplex_data_product.go b/tfplan2cai/converters/google/resources/services/dataplex/dataplex_data_product.go index ca2e92122f..212b1ef139 100644 --- a/tfplan2cai/converters/google/resources/services/dataplex/dataplex_data_product.go +++ b/tfplan2cai/converters/google/resources/services/dataplex/dataplex_data_product.go @@ -110,6 +110,12 @@ func GetDataplexDataProductCaiObject(d tpgresource.TerraformResourceData, config func GetDataplexDataProductApiObject(d tpgresource.TerraformResourceData, config *transport_tpg.Config) (map[string]interface{}, error) { obj := make(map[string]interface{}) + accessApprovalConfigProp, err := expandDataplexDataProductAccessApprovalConfig(d.Get("access_approval_config"), d, config) + if err != nil { + return nil, err + } else if v, ok := d.GetOkExists("access_approval_config"); !tpgresource.IsEmptyValue(reflect.ValueOf(accessApprovalConfigProp)) && (ok || !reflect.DeepEqual(v, accessApprovalConfigProp)) { + obj["accessApprovalConfig"] = accessApprovalConfigProp + } displayNameProp, err := expandDataplexDataProductDisplayName(d.Get("display_name"), d, config) if err != nil { return nil, err @@ -144,6 +150,32 @@ func GetDataplexDataProductApiObject(d tpgresource.TerraformResourceData, config return obj, nil } +func expandDataplexDataProductAccessApprovalConfig(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { + if v == nil { + return nil, nil + } + l := v.([]interface{}) + if len(l) == 0 || l[0] == nil { + return nil, nil + } + raw := l[0] + original := raw.(map[string]interface{}) + transformed := make(map[string]interface{}) + + transformedApproverEmails, err := expandDataplexDataProductAccessApprovalConfigApproverEmails(original["approver_emails"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedApproverEmails); val.IsValid() && !tpgresource.IsEmptyValue(val) { + transformed["approverEmails"] = transformedApproverEmails + } + + return transformed, nil +} + +func expandDataplexDataProductAccessApprovalConfigApproverEmails(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { + return v, nil +} + func expandDataplexDataProductDisplayName(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { return v, nil } diff --git a/tfplan2cai/converters/google/resources/services/dataplex/dataplex_data_product_iam.go b/tfplan2cai/converters/google/resources/services/dataplex/dataplex_data_product_iam.go new file mode 100644 index 0000000000..bed9e39a23 --- /dev/null +++ b/tfplan2cai/converters/google/resources/services/dataplex/dataplex_data_product_iam.go @@ -0,0 +1,131 @@ +// ---------------------------------------------------------------------------- +// +// *** AUTO GENERATED CODE *** Type: MMv1 *** +// +// ---------------------------------------------------------------------------- +// +// This code is generated by Magic Modules using the following: +// +// Configuration: https://github.com/GoogleCloudPlatform/magic-modules/tree/main/mmv1/products/dataplex/DataProduct.yaml +// Template: https://github.com/GoogleCloudPlatform/magic-modules/tree/main/mmv1/templates/tgc/resource_converter_iam.go.tmpl +// +// DO NOT EDIT this file directly. Any changes made to this file will be +// overwritten during the next generation cycle. +// +// ---------------------------------------------------------------------------- + +package dataplex + +import ( + "fmt" + + "github.com/GoogleCloudPlatform/terraform-google-conversion/v7/tfplan2cai/converters/google/resources/cai" + "github.com/hashicorp/terraform-provider-google-beta/google-beta/tpgresource" + transport_tpg "github.com/hashicorp/terraform-provider-google-beta/google-beta/transport" +) + +// Provide a separate asset type constant so we don't have to worry about name conflicts between IAM and non-IAM converter files +const DataplexDataProductIAMAssetType string = "dataplex.googleapis.com/DataProduct" + +func ResourceConverterDataplexDataProductIamPolicy() cai.ResourceConverter { + return cai.ResourceConverter{ + AssetType: DataplexDataProductIAMAssetType, + Convert: GetDataplexDataProductIamPolicyCaiObject, + MergeCreateUpdate: MergeDataplexDataProductIamPolicy, + } +} + +func ResourceConverterDataplexDataProductIamBinding() cai.ResourceConverter { + return cai.ResourceConverter{ + AssetType: DataplexDataProductIAMAssetType, + Convert: GetDataplexDataProductIamBindingCaiObject, + FetchFullResource: FetchDataplexDataProductIamPolicy, + MergeCreateUpdate: MergeDataplexDataProductIamBinding, + MergeDelete: MergeDataplexDataProductIamBindingDelete, + } +} + +func ResourceConverterDataplexDataProductIamMember() cai.ResourceConverter { + return cai.ResourceConverter{ + AssetType: DataplexDataProductIAMAssetType, + Convert: GetDataplexDataProductIamMemberCaiObject, + FetchFullResource: FetchDataplexDataProductIamPolicy, + MergeCreateUpdate: MergeDataplexDataProductIamMember, + MergeDelete: MergeDataplexDataProductIamMemberDelete, + } +} + +func GetDataplexDataProductIamPolicyCaiObject(d tpgresource.TerraformResourceData, config *transport_tpg.Config) ([]cai.Asset, error) { + return newDataplexDataProductIamAsset(d, config, cai.ExpandIamPolicyBindings) +} + +func GetDataplexDataProductIamBindingCaiObject(d tpgresource.TerraformResourceData, config *transport_tpg.Config) ([]cai.Asset, error) { + return newDataplexDataProductIamAsset(d, config, cai.ExpandIamRoleBindings) +} + +func GetDataplexDataProductIamMemberCaiObject(d tpgresource.TerraformResourceData, config *transport_tpg.Config) ([]cai.Asset, error) { + return newDataplexDataProductIamAsset(d, config, cai.ExpandIamMemberBindings) +} + +func MergeDataplexDataProductIamPolicy(existing, incoming cai.Asset) cai.Asset { + existing.IAMPolicy = incoming.IAMPolicy + return existing +} + +func MergeDataplexDataProductIamBinding(existing, incoming cai.Asset) cai.Asset { + return cai.MergeIamAssets(existing, incoming, cai.MergeAuthoritativeBindings) +} + +func MergeDataplexDataProductIamBindingDelete(existing, incoming cai.Asset) cai.Asset { + return cai.MergeDeleteIamAssets(existing, incoming, cai.MergeDeleteAuthoritativeBindings) +} + +func MergeDataplexDataProductIamMember(existing, incoming cai.Asset) cai.Asset { + return cai.MergeIamAssets(existing, incoming, cai.MergeAdditiveBindings) +} + +func MergeDataplexDataProductIamMemberDelete(existing, incoming cai.Asset) cai.Asset { + return cai.MergeDeleteIamAssets(existing, incoming, cai.MergeDeleteAdditiveBindings) +} + +func newDataplexDataProductIamAsset( + d tpgresource.TerraformResourceData, + config *transport_tpg.Config, + expandBindings func(d tpgresource.TerraformResourceData) ([]cai.IAMBinding, error), +) ([]cai.Asset, error) { + bindings, err := expandBindings(d) + if err != nil { + return []cai.Asset{}, fmt.Errorf("expanding bindings: %v", err) + } + + name, err := cai.AssetName(d, config, "//dataplex.googleapis.com/projects/{{project}}/locations/{{location}}/dataProducts/{{data_product_id}}") + if err != nil { + return []cai.Asset{}, err + } + + return []cai.Asset{{ + Name: name, + Type: DataplexDataProductIAMAssetType, + IAMPolicy: &cai.IAMPolicy{ + Bindings: bindings, + }, + }}, nil +} + +func FetchDataplexDataProductIamPolicy(d tpgresource.TerraformResourceData, config *transport_tpg.Config) (cai.Asset, error) { + // Check if the identity field returns a value + if _, ok := d.GetOk("location"); !ok { + return cai.Asset{}, cai.ErrEmptyIdentityField + } + if _, ok := d.GetOk("data_product_id"); !ok { + return cai.Asset{}, cai.ErrEmptyIdentityField + } + + return cai.FetchIamPolicy( + DataplexDataProductIamUpdaterProducer, + d, + config, + "//dataplex.googleapis.com/projects/{{project}}/locations/{{location}}/dataProducts/{{data_product_id}}", + DataplexDataProductIAMAssetType, + ) +} diff --git a/tfplan2cai/converters/google/resources/services/dataplex/iam_dataplex_data_product.go b/tfplan2cai/converters/google/resources/services/dataplex/iam_dataplex_data_product.go new file mode 100644 index 0000000000..19f3669431 --- /dev/null +++ b/tfplan2cai/converters/google/resources/services/dataplex/iam_dataplex_data_product.go @@ -0,0 +1,264 @@ +// ---------------------------------------------------------------------------- +// +// *** AUTO GENERATED CODE *** Type: MMv1 *** +// +// ---------------------------------------------------------------------------- +// +// This code is generated by Magic Modules using the following: +// +// Configuration: https://github.com/GoogleCloudPlatform/magic-modules/tree/main/mmv1/products/dataplex/DataProduct.yaml +// Template: https://github.com/GoogleCloudPlatform/magic-modules/tree/main/mmv1/templates/terraform/iam_policy.go.tmpl +// +// DO NOT EDIT this file directly. Any changes made to this file will be +// overwritten during the next generation cycle. +// +// ---------------------------------------------------------------------------- + +package dataplex + +import ( + "fmt" + "regexp" + "strings" + + "github.com/hashicorp/errwrap" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "google.golang.org/api/cloudresourcemanager/v1" + + "github.com/hashicorp/terraform-provider-google-beta/google-beta/tpgiamresource" + "github.com/hashicorp/terraform-provider-google-beta/google-beta/tpgresource" + transport_tpg "github.com/hashicorp/terraform-provider-google-beta/google-beta/transport" +) + +var ( + _ = regexp.Match + _ = strings.Trim + _ = errwrap.Wrap + _ = schema.Noop +) + +var DataplexDataProductIamSchema = map[string]*schema.Schema{ + "project": { + Type: schema.TypeString, + Computed: true, + Optional: true, + ForceNew: true, + }, + "location": { + Type: schema.TypeString, + Computed: true, + Optional: true, + ForceNew: true, + }, + "data_product_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + DiffSuppressFunc: tpgresource.CompareSelfLinkOrResourceName, + }, +} + +type DataplexDataProductIamUpdater struct { + project string + location string + dataProductId string + d tpgresource.TerraformResourceData + Config *transport_tpg.Config +} + +func DataplexDataProductIamUpdaterProducer(d tpgresource.TerraformResourceData, config *transport_tpg.Config) (tpgiamresource.ResourceIamUpdater, error) { + values := make(map[string]string) + + project, _ := tpgresource.GetProject(d, config) + if project != "" { + if err := d.Set("project", project); err != nil { + return nil, fmt.Errorf("Error setting project: %s", err) + } + } + values["project"] = project + location, _ := tpgresource.GetLocation(d, config) + if location != "" { + if err := d.Set("location", location); err != nil { + return nil, fmt.Errorf("Error setting location: %s", err) + } + } + values["location"] = location + if v, ok := d.GetOk("data_product_id"); ok { + values["data_product_id"] = v.(string) + } + + // We may have gotten either a long or short name, so attempt to parse long name if possible + m, err := tpgresource.GetImportIdQualifiers([]string{"projects/(?P[^/]+)/locations/(?P[^/]+)/dataProducts/(?P[^/]+)", "(?P[^/]+)/(?P[^/]+)/(?P[^/]+)", "(?P[^/]+)/(?P[^/]+)", "(?P[^/]+)"}, d, config, d.Get("data_product_id").(string)) + if err != nil { + return nil, err + } + + for k, v := range m { + values[k] = v + } + + u := &DataplexDataProductIamUpdater{ + project: values["project"], + location: values["location"], + dataProductId: values["data_product_id"], + d: d, + Config: config, + } + + if err := d.Set("project", u.project); err != nil { + return nil, fmt.Errorf("Error setting project: %s", err) + } + if err := d.Set("location", u.location); err != nil { + return nil, fmt.Errorf("Error setting location: %s", err) + } + if err := d.Set("data_product_id", u.GetResourceId()); err != nil { + return nil, fmt.Errorf("Error setting data_product_id: %s", err) + } + + return u, nil +} + +func DataplexDataProductIdParseFunc(d *schema.ResourceData, config *transport_tpg.Config) error { + values := make(map[string]string) + + project, _ := tpgresource.GetProject(d, config) + if project != "" { + values["project"] = project + } + + location, _ := tpgresource.GetLocation(d, config) + if location != "" { + values["location"] = location + } + + m, err := tpgresource.GetImportIdQualifiers([]string{"projects/(?P[^/]+)/locations/(?P[^/]+)/dataProducts/(?P[^/]+)", "(?P[^/]+)/(?P[^/]+)/(?P[^/]+)", "(?P[^/]+)/(?P[^/]+)", "(?P[^/]+)"}, d, config, d.Id()) + if err != nil { + return err + } + + for k, v := range m { + values[k] = v + } + + u := &DataplexDataProductIamUpdater{ + project: values["project"], + location: values["location"], + dataProductId: values["data_product_id"], + d: d, + Config: config, + } + if err := d.Set("data_product_id", u.GetResourceId()); err != nil { + return fmt.Errorf("Error setting data_product_id: %s", err) + } + d.SetId(u.GetResourceId()) + return nil +} + +func (u *DataplexDataProductIamUpdater) GetResourceIamPolicy() (*cloudresourcemanager.Policy, error) { + url, err := u.qualifyDataProductUrl("getIamPolicy") + if err != nil { + return nil, err + } + + project, err := tpgresource.GetProject(u.d, u.Config) + if err != nil { + return nil, err + } + var obj map[string]interface{} + + userAgent, err := tpgresource.GenerateUserAgentString(u.d, u.Config.UserAgent) + if err != nil { + return nil, err + } + + policy, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: u.Config, + Method: "GET", + Project: project, + RawURL: url, + UserAgent: userAgent, + Body: obj, + }) + if err != nil { + return nil, fmt.Errorf("Error retrieving IAM policy for %s: %w", u.DescribeResource(), err) + } + + out := &cloudresourcemanager.Policy{} + err = tpgresource.Convert(policy, out) + if err != nil { + return nil, fmt.Errorf("Cannot convert a policy to a resource manager policy: %w", err) + } + + return out, nil +} + +func (u *DataplexDataProductIamUpdater) SetResourceIamPolicy(policy *cloudresourcemanager.Policy) error { + json, err := tpgresource.ConvertToMap(policy) + if err != nil { + return err + } + + obj := make(map[string]interface{}) + obj["policy"] = json + + url, err := u.qualifyDataProductUrl("setIamPolicy") + if err != nil { + return err + } + project, err := tpgresource.GetProject(u.d, u.Config) + if err != nil { + return err + } + + userAgent, err := tpgresource.GenerateUserAgentString(u.d, u.Config.UserAgent) + if err != nil { + return err + } + + _, err = transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: u.Config, + Method: "POST", + Project: project, + RawURL: url, + UserAgent: userAgent, + Body: obj, + Timeout: u.d.Timeout(schema.TimeoutCreate), + }) + if err != nil { + return fmt.Errorf("Error setting IAM policy for %s: %w", u.DescribeResource(), err) + } + + return nil +} + +func (u *DataplexDataProductIamUpdater) qualifyDataProductUrl(methodIdentifier string) (string, error) { + urlTemplate := fmt.Sprintf("{{DataplexBasePath}}%s:%s", fmt.Sprintf("projects/%s/locations/%s/dataProducts/%s", u.project, u.location, u.dataProductId), methodIdentifier) + url, err := tpgresource.ReplaceVars(u.d, u.Config, urlTemplate) + if err != nil { + return "", err + } + return url, nil +} + +func (u *DataplexDataProductIamUpdater) GetResourceId() string { + return fmt.Sprintf("projects/%s/locations/%s/dataProducts/%s", u.project, u.location, u.dataProductId) +} + +func DataplexDataProductIamParentParentResourceIdentityParser(d *schema.ResourceData, identity *schema.IdentityData, transportConfig *transport_tpg.Config) (string, error) { + return tpgiamresource.ParseIamResourceIdentity(d, identity, transportConfig, tpgiamresource.IamResourceIdentityConfig{ + Params: []tpgiamresource.IamIdentityParam{ + {Key: "project", IdentityKey: "project"}, + {Key: "location", IdentityKey: "location"}, + {Key: "dataProductId", IdentityKey: "data_product_id"}, + }, + UriFormat: "projects/%s/locations/%s/dataProducts/%s", + }) +} + +func (u *DataplexDataProductIamUpdater) GetMutexKey() string { + return fmt.Sprintf("iam-dataplex-dataproduct-%s", u.GetResourceId()) +} + +func (u *DataplexDataProductIamUpdater) DescribeResource() string { + return fmt.Sprintf("dataplex dataproduct %q", u.GetResourceId()) +}