From 95ddf88d18377095733a649ea6b3c8e365e996a2 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 28 Oct 2025 10:46:04 +0000 Subject: [PATCH] feat: Add iam.googleapis.com/ServiceAccount converter This change introduces a new converter for the `iam.googleapis.com/ServiceAccount` asset type. The converter maps the following fields from the `google_service_account` Terraform resource to the CAI asset: - `account_id` -> `name` - `display_name` -> `displayName` - `description` -> `description` - `project` -> `projectId` - `email` -> `email` New test cases have been added to validate the converter's functionality. --- mmv1/provider/terraform_tgc.go | 1 + .../tgc/resource_converters.go.tmpl | 2 +- .../tgc/services/iam/iam_service_account.go | 124 ++++++++++++++++++ .../tgc/tests/data/iam_service_account.json | 53 ++++++++ .../tgc/tests/data/iam_service_account.tf | 27 ++++ 5 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 mmv1/third_party/tgc/services/iam/iam_service_account.go create mode 100644 mmv1/third_party/tgc/tests/data/iam_service_account.json create mode 100644 mmv1/third_party/tgc/tests/data/iam_service_account.tf diff --git a/mmv1/provider/terraform_tgc.go b/mmv1/provider/terraform_tgc.go index 9489d077015d..61fdae672e67 100644 --- a/mmv1/provider/terraform_tgc.go +++ b/mmv1/provider/terraform_tgc.go @@ -399,6 +399,7 @@ func (tgc TerraformGoogleConversion) CopyCommonFiles(outputFolder string, genera "converters/google/resources/services/compute/compute_security_policy.go": "third_party/tgc/services/compute/compute_security_policy.go", "converters/google/resources/services/eventarc/eventarc_utils.go": "third_party/terraform/services/eventarc/eventarc_utils.go", "converters/google/resources/services/kms/kms_key_ring_iam.go": "third_party/tgc/services/kms/kms_key_ring_iam.go", + "converters/google/resources/services/iam/iam_service_account.go": "third_party/tgc/services/iam/iam_service_account.go", "converters/google/resources/services/kms/kms_crypto_key_iam.go": "third_party/tgc/services/kms/kms_crypto_key_iam.go", "converters/google/resources/services/resourcemanager/project_iam_custom_role.go": "third_party/tgc/services/resourcemanager/project_iam_custom_role.go", "converters/google/resources/services/resourcemanager/organization_iam_custom_role.go": "third_party/tgc/services/resourcemanager/organization_iam_custom_role.go", diff --git a/mmv1/third_party/tgc/resource_converters.go.tmpl b/mmv1/third_party/tgc/resource_converters.go.tmpl index bfaafa3512a4..aa95650ad0af 100644 --- a/mmv1/third_party/tgc/resource_converters.go.tmpl +++ b/mmv1/third_party/tgc/resource_converters.go.tmpl @@ -143,6 +143,7 @@ func ResourceConverters() map[string][]cai.ResourceConverter { "google_pubsub_topic": {pubsub.ResourceConverterPubsubTopic()}, "google_kms_crypto_key": {kms.ResourceConverterKMSCryptoKey()}, "google_kms_key_ring": {kms.ResourceConverterKMSKeyRing()}, + "google_service_account": {iam.ResourceConverterIamServiceAccount()}, "google_filestore_instance": {filestore.ResourceConverterFilestoreInstance()}, "google_access_context_manager_service_perimeter": {accesscontextmanager.ResourceConverterAccessContextManagerServicePerimeter()}, "google_access_context_manager_access_policy": {accesscontextmanager.ResourceConverterAccessContextManagerAccessPolicy()}, @@ -192,7 +193,6 @@ func ResourceConverters() map[string][]cai.ResourceConverter { "google_organization_iam_custom_role": {resourcemanager.ResourceConverterOrganizationIAMCustomRole()}, "google_vpc_access_connector": {vpcaccess.ResourceConverterVPCAccessConnector()}, "google_logging_metric": {logging.ResourceConverterLoggingMetric()}, - "google_service_account": {resourcemanager.ResourceConverterServiceAccount()}, "google_service_account_key": {resourcemanager.ResourceConverterServiceAccountKey()}, } diff --git a/mmv1/third_party/tgc/services/iam/iam_service_account.go b/mmv1/third_party/tgc/services/iam/iam_service_account.go new file mode 100644 index 000000000000..2ef61a4ed557 --- /dev/null +++ b/mmv1/third_party/tgc/services/iam/iam_service_account.go @@ -0,0 +1,124 @@ +package iam + +import ( + "fmt" + "reflect" + + "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" +) + +const IamServiceAccountAssetType string = "iam.googleapis.com/ServiceAccount" + +// ResourceConverterIamServiceAccount returns the ResourceConverter object for iam service account. +func ResourceConverterIamServiceAccount() cai.ResourceConverter { + return cai.ResourceConverter{ + AssetType: IamServiceAccountAssetType, + Convert: GetIamServiceAccountCaiObject, + } +} + +// GetIamServiceAccountCaiObject converts the Terraform plan data into a CAI object. +func GetIamServiceAccountCaiObject(d tpgresource.TerraformResourceData, config *transport_tpg.Config) ([]cai.Asset, error) { + name, err := cai.AssetName(d, config, "//iam.googleapis.com/projects/{{project}}/serviceAccounts/{{account_id}}@{{project}}.iam.gserviceaccount.com") + if err != nil { + return []cai.Asset{}, err + } + if obj, err := GetIamServiceAccountApiObject(d, config); err == nil { + return []cai.Asset{{ + Name: name, + Type: IamServiceAccountAssetType, + Resource: &cai.AssetResource{ + Version: "v1", + DiscoveryDocumentURI: "https://iam.googleapis.com/$discovery/rest?version=v1", + DiscoveryName: "ServiceAccount", + Data: obj, + }, + }}, nil + } else { + return []cai.Asset{}, err + } +} + +// GetIamServiceAccountApiObject returns the CAI resource object. +func GetIamServiceAccountApiObject(d tpgresource.TerraformResourceData, config *transport_tpg.Config) (map[string]interface{}, error) { + obj := make(map[string]interface{}) + + nameProp, err := expandIamServiceAccountName(d, config) + if err != nil { + return nil, err + } else if v, ok := d.GetOkExists("account_id"); !tpgresource.IsEmptyValue(reflect.ValueOf(nameProp)) && (ok || !reflect.DeepEqual(v, nameProp)) { + obj["name"] = nameProp + } + + emailProp, err := expandIamServiceAccountEmail(d, config) + if err != nil { + return nil, err + } else if v, ok := d.GetOkExists("email"); !tpgresource.IsEmptyValue(reflect.ValueOf(emailProp)) && (ok || !reflect.DeepEqual(v, emailProp)) { + obj["email"] = emailProp + } + + displayNameProp, err := expandIamServiceAccountDisplayName(d.Get("display_name"), d, config) + if err != nil { + return nil, err + } else if v, ok := d.GetOkExists("display_name"); !tpgresource.IsEmptyValue(reflect.ValueOf(displayNameProp)) && (ok || !reflect.DeepEqual(v, displayNameProp)) { + obj["displayName"] = displayNameProp + } + + descriptionProp, err := expandIamServiceAccountDescription(d.Get("description"), d, config) + if err != nil { + return nil, err + } else if v, ok := d.GetOkExists("description"); !tpgresource.IsEmptyValue(reflect.ValueOf(descriptionProp)) && (ok || !reflect.DeepEqual(v, descriptionProp)) { + obj["description"] = descriptionProp + } + + projectProp, err := expandIamServiceAccountProject(d, config) + if err != nil { + return nil, err + } else if v, ok := d.GetOkExists("project"); !tpgresource.IsEmptyValue(reflect.ValueOf(projectProp)) && (ok || !reflect.DeepEqual(v, projectProp)) { + obj["projectId"] = projectProp + } + + return obj, nil +} + +func expandIamServiceAccountName(d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { + project, err := tpgresource.GetProject(d, config) + if err != nil { + return nil, err + } + + if accountId, ok := d.GetOk("account_id"); ok { + return fmt.Sprintf("projects/%s/serviceAccounts/%s@%s.iam.gserviceaccount.com", project, accountId, project), nil + } + return nil, nil +} + +func expandIamServiceAccountEmail(d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { + if email, ok := d.GetOk("email"); ok && email != "" { + return email, nil + } + + project, err := tpgresource.GetProject(d, config) + if err != nil { + return nil, err + } + + if accountId, ok := d.GetOk("account_id"); ok { + return fmt.Sprintf("%s@%s.iam.gserviceaccount.com", accountId, project), nil + } + return nil, nil +} + +func expandIamServiceAccountDisplayName(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { + return v, nil +} + +func expandIamServiceAccountDescription(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { + return v, nil +} + +func expandIamServiceAccountProject(d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { + return tpgresource.GetProject(d, config) +} diff --git a/mmv1/third_party/tgc/tests/data/iam_service_account.json b/mmv1/third_party/tgc/tests/data/iam_service_account.json new file mode 100644 index 000000000000..ad082b0428f3 --- /dev/null +++ b/mmv1/third_party/tgc/tests/data/iam_service_account.json @@ -0,0 +1,53 @@ +[ + { + "name": "//iam.googleapis.com/projects/{{.Provider.project}}/serviceAccounts/gg-asset-34338-63e0@{{.Provider.project}}.iam.gserviceaccount.com", + "asset_type": "iam.googleapis.com/ServiceAccount", + "ancestry_path": "{{.Ancestry}}/project/{{.Provider.project}}", + "resource": { + "version": "v1", + "discovery_document_uri": "https://iam.googleapis.com/$discovery/rest?version=v1", + "discovery_name": "ServiceAccount", + "parent": "//cloudresourcemanager.googleapis.com/projects/{{.Provider.project}}", + "data": { + "name": "projects/{{.Provider.project}}/serviceAccounts/gg-asset-34338-63e0@{{.Provider.project}}.iam.gserviceaccount.com", + "email": "gg-asset-34338-63e0@{{.Provider.project}}.iam.gserviceaccount.com", + "displayName": "Test Service Account 1", + "description": "A test service account.", + "projectId": "{{.Provider.project}}" + } + } + }, + { + "name": "//iam.googleapis.com/projects/{{.Provider.project}}/serviceAccounts/gg-asset-34872-33bd@{{.Provider.project}}.iam.gserviceaccount.com", + "asset_type": "iam.googleapis.com/ServiceAccount", + "ancestry_path": "{{.Ancestry}}/project/{{.Provider.project}}", + "resource": { + "version": "v1", + "discovery_document_uri": "https://iam.googleapis.com/$discovery/rest?version=v1", + "discovery_name": "ServiceAccount", + "parent": "//cloudresourcemanager.googleapis.com/projects/{{.Provider.project}}", + "data": { + "name": "projects/{{.Provider.project}}/serviceAccounts/gg-asset-34872-33bd@{{.Provider.project}}.iam.gserviceaccount.com", + "email": "gg-asset-34872-33bd@{{.Provider.project}}.iam.gserviceaccount.com", + "displayName": "Test Service Account 2", + "projectId": "{{.Provider.project}}" + } + } + }, + { + "name": "//iam.googleapis.com/projects/{{.Provider.project}}/serviceAccounts/gg-asset-34952-20de@{{.Provider.project}}.iam.gserviceaccount.com", + "asset_type": "iam.googleapis.com/ServiceAccount", + "ancestry_path": "{{.Ancestry}}/project/{{.Provider.project}}", + "resource": { + "version": "v1", + "discovery_document_uri": "https://iam.googleapis.com/$discovery/rest?version=v1", + "discovery_name": "ServiceAccount", + "parent": "//cloudresourcemanager.googleapis.com/projects/{{.Provider.project}}", + "data": { + "name": "projects/{{.Provider.project}}/serviceAccounts/gg-asset-34952-20de@{{.Provider.project}}.iam.gserviceaccount.com", + "email": "gg-asset-34952-20de@{{.Provider.project}}.iam.gserviceaccount.com", + "projectId": "{{.Provider.project}}" + } + } + } +] diff --git a/mmv1/third_party/tgc/tests/data/iam_service_account.tf b/mmv1/third_party/tgc/tests/data/iam_service_account.tf new file mode 100644 index 000000000000..0073883f410c --- /dev/null +++ b/mmv1/third_party/tgc/tests/data/iam_service_account.tf @@ -0,0 +1,27 @@ +terraform { + required_providers { + google = { + source = "hashicorp/google" + version = ">= 4.54.0" + } + } +} + +provider "google" { + project = "{{.Provider.project}}" +} + +resource "google_service_account" "test_account_1" { + account_id = "gg-asset-34338-63e0" + display_name = "Test Service Account 1" + description = "A test service account." +} + +resource "google_service_account" "test_account_2" { + account_id = "gg-asset-34872-33bd" + display_name = "Test Service Account 2" +} + +resource "google_service_account" "test_account_3" { + account_id = "gg-asset-34952-20de" +}