diff --git a/mmv1/api/resource.go b/mmv1/api/resource.go index 7502a04da2f6..79be14b68c74 100644 --- a/mmv1/api/resource.go +++ b/mmv1/api/resource.go @@ -1192,8 +1192,7 @@ func (r Resource) ExtractIdentifiers(url string) []string { return result } -// For example, "projects/{{project}}/schemas/{{name}}", "{{project}}/{{name}}", "{{name}}" -func (r Resource) RawImportIdFormatsFromIam() []string { +func (r Resource) IamImportFormats() []string { var importFormat []string if r.IamPolicy != nil { @@ -1202,8 +1201,12 @@ func (r Resource) RawImportIdFormatsFromIam() []string { if len(importFormat) == 0 { importFormat = r.ImportFormat } + return importFormat +} - return ImportIdFormats(importFormat, r.Identity, r.BaseUrl) +// For example, "projects/{{project}}/schemas/{{name}}", "{{project}}/{{name}}", "{{name}}" +func (r Resource) RawImportIdFormatsFromIam() []string { + return ImportIdFormats(r.IamImportFormats(), r.Identity, r.BaseUrl) } // For example, projects/(?P[^/]+)/schemas/(?P[^/]+)", "(?P[^/]+)/(?P[^/]+)", "(?P[^/]+) @@ -1668,3 +1671,34 @@ func (r Resource) CaiApiVersion(productBackendName, caiProductBaseUrl string) st } return "" } + +// For example: the uri "projects/{{project}}/schemas/{{name}}" +// The paramerter is "schema" as "project" is not returned. +func (r Resource) CaiIamResourceParams() []string { + resourceUri := strings.ReplaceAll(r.IamResourceUri(), "{{name}}", fmt.Sprintf("{{%s}}", r.IamParentResourceName())) + + return google.Reject(r.ExtractIdentifiers(resourceUri), func(param string) bool { + return param == "project" + }) +} + +// Gets the Cai IAM asset name template +// For example: //monitoring.googleapis.com/v3/projects/{{project}}/services/{{service_id}} +func (r Resource) CaiIamAssetNameTemplate(productBackendName string) string { + iamImportFormat := r.IamImportFormats() + if len(iamImportFormat) > 0 { + name := strings.ReplaceAll(iamImportFormat[0], "{{name}}", fmt.Sprintf("{{%s}}", r.IamParentResourceName())) + name = strings.ReplaceAll(name, "%", "") + return fmt.Sprintf("//%s.googleapis.com/%s", productBackendName, name) + } + + caiBaseUrl := r.CaiBaseUrl + + if caiBaseUrl == "" { + caiBaseUrl = r.SelfLink + } + if caiBaseUrl == "" { + caiBaseUrl = r.BaseUrl + } + return fmt.Sprintf("//%s.googleapis.com/%s/{{%s}}", productBackendName, caiBaseUrl, r.IamParentResourceName()) +} diff --git a/mmv1/google/slice_utils.go b/mmv1/google/slice_utils.go index 75ad22de3b7d..20219c7c85e1 100644 --- a/mmv1/google/slice_utils.go +++ b/mmv1/google/slice_utils.go @@ -39,3 +39,19 @@ func Reject[T any](S []T, test func(T) bool) (ret []T) { func Concat[T any](S1 []T, S2 []T) (ret []T) { return append(S1, S2...) } + +// difference returns the elements in `S1` that aren't in `S2`. +func Diff(S1, S2 []string) []string { + var ret []string + mb := make(map[string]bool, len(S2)) + for _, x := range S2 { + mb[x] = true + } + + for _, x := range S1 { + if _, found := mb[x]; !found { + ret = append(ret, x) + } + } + return ret +} diff --git a/mmv1/google/template_utils.go b/mmv1/google/template_utils.go index 78eb2dea26ac..9a8c5480cde9 100644 --- a/mmv1/google/template_utils.go +++ b/mmv1/google/template_utils.go @@ -65,6 +65,7 @@ var TemplateFunctions = template.FuncMap{ "join": strings.Join, "lower": strings.ToLower, "upper": strings.ToUpper, + "hasSuffix": strings.HasSuffix, "dict": wrapMultipleParams, "format2regex": Format2Regex, "hasPrefix": strings.HasPrefix, diff --git a/mmv1/provider/template_data.go b/mmv1/provider/template_data.go index c8e8fa5b599b..a578201d9895 100644 --- a/mmv1/provider/template_data.go +++ b/mmv1/provider/template_data.go @@ -177,6 +177,14 @@ func (td *TemplateData) GenerateTGCResourceFile(filePath string, resource api.Re td.GenerateFile(filePath, templatePath, resource, true, templates...) } +func (td *TemplateData) GenerateTGCIamResourceFile(filePath string, resource api.Resource) { + templatePath := "templates/tgc/resource_converter_iam.go.tmpl" + templates := []string{ + templatePath, + } + td.GenerateFile(filePath, templatePath, resource, true, templates...) +} + func (td *TemplateData) GenerateFile(filePath, templatePath string, input any, goFormat bool, templates ...string) { // log.Printf("Generating %s", filePath) diff --git a/mmv1/provider/terraform_tgc.go b/mmv1/provider/terraform_tgc.go index c1f9fe325b18..007c20c05595 100644 --- a/mmv1/provider/terraform_tgc.go +++ b/mmv1/provider/terraform_tgc.go @@ -17,18 +17,28 @@ package provider import ( "fmt" + "io/ioutil" "log" "os" "path" + "path/filepath" + "regexp" + "strings" "time" + "golang.org/x/exp/slices" + "github.com/GoogleCloudPlatform/magic-modules/mmv1/api" "github.com/GoogleCloudPlatform/magic-modules/mmv1/api/product" "github.com/GoogleCloudPlatform/magic-modules/mmv1/google" ) type TerraformGoogleConversion struct { - ResourcesForVersion []map[string]string + IamResources []map[string]string + + NonDefinedTests []string + + Tests []string TargetVersionName string @@ -56,11 +66,6 @@ func NewTerraformGoogleConversion(product *api.Product, versionName string, star return t } -func (tgc TerraformGoogleConversion) generatingHashicorpRepo() bool { - // This code is not used when generating TPG/TPGB - return false -} - func (tgc TerraformGoogleConversion) Generate(outputFolder, productPath, resourceToGenerate string, generateCode, generateDocs bool) { // Temporary shim to generate the missing resources directory. Can be removed // once the folder exists downstream. @@ -106,7 +111,7 @@ func (tgc TerraformGoogleConversion) GenerateObject(object api.Resource, outputF return } - // tgc.GenerateIamPolicy(object, *templateData, outputFolder, generateCode, generateDocs) + tgc.GenerateIamPolicy(object, *templateData, outputFolder, generateCode, generateDocs) } func (tgc TerraformGoogleConversion) GenerateResource(object api.Resource, templateData TemplateData, outputFolder string, generateCode, generateDocs bool) { @@ -120,8 +125,216 @@ func (tgc TerraformGoogleConversion) GenerateResource(object api.Resource, templ templateData.GenerateTGCResourceFile(targetFilePath, object) } +// Generate the IAM policy for this object. This is used to query and test +// IAM policies separately from the resource itself +// Docs are generated for the terraform provider, not here. +func (tgc TerraformGoogleConversion) GenerateIamPolicy(object api.Resource, templateData TemplateData, outputFolder string, generateCode, generateDocs bool) { + if !generateCode || object.IamPolicy.ExcludeTgc { + return + } + + productName := tgc.Product.ApiName + targetFolder := path.Join(outputFolder, "converters/google/resources/services", productName) + if err := os.MkdirAll(targetFolder, os.ModePerm); err != nil { + log.Println(fmt.Errorf("error creating parent directory %v: %v", targetFolder, err)) + } + + name := object.FilenameOverride + if name == "" { + name = google.Underscore(object.Name) + } + + targetFilePath := path.Join(targetFolder, fmt.Sprintf("%s_%s_iam.go", productName, name)) + templateData.GenerateTGCIamResourceFile(targetFilePath, object) + + targetFilePath = path.Join(targetFolder, fmt.Sprintf("iam_%s_%s.go", productName, name)) + templateData.GenerateIamPolicyFile(targetFilePath, object) + + // Don't generate tests - we can rely on the terraform provider + // to test these. +} + +// Generates the list of resources +func (tgc TerraformGoogleConversion) generateCaiIamResources(products []*api.Product) { + for _, productDefinition := range products { + service := strings.ToLower(productDefinition.Name) + for _, object := range productDefinition.Objects { + if object.MinVersionObj().Name != "ga" || object.Exclude || object.ExcludeTgc { + continue + } + + var iamClassName string + iamPolicy := object.IamPolicy + if iamPolicy != nil && !iamPolicy.Exclude && !iamPolicy.ExcludeTgc { + + iamClassName = fmt.Sprintf("%s.ResourceConverter%s", service, object.ResourceName()) + + tgc.IamResources = append(tgc.IamResources, map[string]string{ + "TerraformName": object.TerraformName(), + "IamClassName": iamClassName, + }) + } + } + } +} + func (tgc TerraformGoogleConversion) CompileCommonFiles(outputFolder string, products []*api.Product, overridePath string) { + log.Printf("Compiling common files.") + + tgc.generateCaiIamResources(products) + tgc.NonDefinedTests = retrieveFullManifestOfNonDefinedTests() + + files := retrieveFullListOfTestFiles() + for _, file := range files { + tgc.Tests = append(tgc.Tests, strings.Split(file, ".")[0]) + } + tgc.Tests = slices.Compact(tgc.Tests) + testSource := make(map[string]string) + for target, source := range retrieveTestSourceCodeWithLocation(".tmpl") { + target := strings.Replace(target, "go.tmpl", "go", 1) + testSource[target] = source + } + + templateData := NewTemplateData(outputFolder, tgc.TargetVersionName) + tgc.CompileFileList(outputFolder, testSource, *templateData, products) + + // compile_file_list( + // output_folder, + // [ + // ['converters/google/resources/services/compute/compute_instance_helpers.go', + // 'third_party/terraform/services/compute/compute_instance_helpers.go.erb'], + // ['converters/google/resources/resource_converters.go', + // 'templates/tgc/resource_converters.go.erb'], + // ['converters/google/resources/services/kms/iam_kms_key_ring.go', + // 'third_party/terraform/services/kms/iam_kms_key_ring.go.erb'], + // ['converters/google/resources/services/kms/iam_kms_crypto_key.go', + // 'third_party/terraform/services/kms/iam_kms_crypto_key.go.erb'], + // ['converters/google/resources/services/compute/metadata.go', + // 'third_party/terraform/services/compute/metadata.go.erb'], + // ['converters/google/resources/services/compute/compute_instance.go', + // 'third_party/tgc/compute_instance.go.erb'] + // ], + // file_template + // ) +} + +func (tgc TerraformGoogleConversion) CompileFileList(outputFolder string, files map[string]string, fileTemplate TemplateData, products []*api.Product) { + if err := os.MkdirAll(outputFolder, os.ModePerm); err != nil { + log.Println(fmt.Errorf("error creating output directory %v: %v", outputFolder, err)) + } + + for target, source := range files { + targetFile := filepath.Join(outputFolder, target) + targetDir := filepath.Dir(targetFile) + if err := os.MkdirAll(targetDir, os.ModePerm); err != nil { + log.Println(fmt.Errorf("error creating output directory %v: %v", targetDir, err)) + } + + templates := []string{ + source, + } + + formatFile := filepath.Ext(targetFile) == ".go" + + fileTemplate.GenerateFile(targetFile, source, tgc, formatFile, templates...) + // tgc.replaceImportPath(outputFolder, target) + } +} + +func retrieveFullManifestOfNonDefinedTests() []string { + var tests []string + fileMap := make(map[string]bool) + + files := retrieveFullListOfTestFiles() + for _, file := range files { + tests = append(tests, strings.Split(file, ".")[0]) + fileMap[file] = true + } + tests = slices.Compact(tests) + + nonDefinedTests := google.Diff(tests, retrieveListOfManuallyDefinedTests()) + nonDefinedTests = google.Reject(nonDefinedTests, func(file string) bool { + return strings.HasSuffix(file, "_without_default_project") + }) + + for _, test := range nonDefinedTests { + _, ok := fileMap[fmt.Sprintf("%s.json", test)] + if !ok { + log.Fatalf("test file named %s.json expected but found none", test) + } + + _, ok = fileMap[fmt.Sprintf("%s.tf", test)] + if !ok { + log.Fatalf("test file named %s.tf expected but found none", test) + } + } + + return nonDefinedTests +} + +// Gets all of the test files in the folder third_party/tgc/tests/data +func retrieveFullListOfTestFiles() []string { + var testFiles []string + + files, err := ioutil.ReadDir("third_party/tgc/tests/data") + if err != nil { + log.Fatal(err) + } + for _, file := range files { + testFiles = append(testFiles, file.Name()) + } + slices.Sort(testFiles) + + return testFiles +} + +func retrieveTestSourceCodeWithLocation(suffix string) map[string]string { + var fileNames []string + path := "third_party/tgc/tests/source/go" + files, err := ioutil.ReadDir(path) + if err != nil { + log.Fatal(err) + } + + for _, file := range files { + log.Printf("ext %s", filepath.Ext(file.Name())) + if filepath.Ext(file.Name()) == suffix { + fileNames = append(fileNames, file.Name()) + } + } + + slices.Sort(fileNames) + + testSource := make(map[string]string) + for _, file := range fileNames { + target := fmt.Sprintf("test/%s", file) + source := fmt.Sprintf("%s/%s", path, file) + testSource[target] = source + } + return testSource +} + +func retrieveListOfManuallyDefinedTests() []string { + m1 := retrieveListOfManuallyDefinedTestsFromFile("third_party/tgc/tests/source/go/cli_test.go.tmpl") + m2 := retrieveListOfManuallyDefinedTestsFromFile("third_party/tgc/tests/source/go/read_test.go.tmpl") + return google.Concat(m1, m2) +} + +// Reads the content of the file and then finds all of the tests in the contents +func retrieveListOfManuallyDefinedTestsFromFile(file string) []string { + data, err := os.ReadFile(file) + if err != nil { + log.Fatalf("Cannot open the file: %v", file) + } + + var tests []string + testsReg := regexp.MustCompile(`\s*name\s*:\s*"([^,]+)"`) + matches := testsReg.FindAllStringSubmatch(string(data), -1) + for _, testWithName := range matches { + tests = append(tests, testWithName[1]) + } + return tests } func (tgc TerraformGoogleConversion) CopyCommonFiles(outputFolder string, generateCode, generateDocs bool) { diff --git a/mmv1/templates/terraform/iam_policy.go.tmpl b/mmv1/templates/terraform/iam_policy.go.tmpl index 8e58a35c4ad6..58c83fad4b99 100644 --- a/mmv1/templates/terraform/iam_policy.go.tmpl +++ b/mmv1/templates/terraform/iam_policy.go.tmpl @@ -12,10 +12,10 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -}} -{{/* <% if hc_downstream */}} +{{- if ne $.Compiler "terraformgoogleconversion-codegen" }} // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 - +{{- end }} // ---------------------------------------------------------------------------- // // *** AUTO GENERATED CODE *** Type: MMv1 *** diff --git a/mmv1/templates/terraform/operation.go.tmpl b/mmv1/templates/terraform/operation.go.tmpl index 3a778b8f8e2d..f3690006ef57 100644 --- a/mmv1/templates/terraform/operation.go.tmpl +++ b/mmv1/templates/terraform/operation.go.tmpl @@ -1,7 +1,7 @@ -{{/* TODO rewrite: if hc_downstream */ -}} +{{- if ne $.Compiler "terraformgoogleconversion-codegen" }} // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 - +{{- end }} // ---------------------------------------------------------------------------- // // *** AUTO GENERATED CODE *** Type: MMv1 *** diff --git a/mmv1/templates/terraform/sweeper_file.go.tmpl b/mmv1/templates/terraform/sweeper_file.go.tmpl index f5085436c048..f9cb80ccb29a 100644 --- a/mmv1/templates/terraform/sweeper_file.go.tmpl +++ b/mmv1/templates/terraform/sweeper_file.go.tmpl @@ -1,7 +1,7 @@ -{{/* <% if hc_downstream */ -}} +{{- if ne $.Compiler "terraformgoogleconversion-codegen" }} // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 - +{{- end }} // ---------------------------------------------------------------------------- // // *** AUTO GENERATED CODE *** Type: MMv1 *** diff --git a/mmv1/templates/tgc/resource_converter.go.tmpl b/mmv1/templates/tgc/resource_converter.go.tmpl index 6b27838c72f0..f99fc72656f6 100644 --- a/mmv1/templates/tgc/resource_converter.go.tmpl +++ b/mmv1/templates/tgc/resource_converter.go.tmpl @@ -1,3 +1,17 @@ +{{/* The license inside this block applies to this file + Copyright 2024 Google LLC. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. */ -}} // ---------------------------------------------------------------------------- // // *** AUTO GENERATED CODE *** Type: MMv1 *** diff --git a/mmv1/templates/tgc/resource_converter_iam.go.tmpl b/mmv1/templates/tgc/resource_converter_iam.go.tmpl new file mode 100644 index 000000000000..a73eb330d311 --- /dev/null +++ b/mmv1/templates/tgc/resource_converter_iam.go.tmpl @@ -0,0 +1,145 @@ +{{/* The license inside this block applies to this file + Copyright 2024 Google LLC. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. */ -}} +// ---------------------------------------------------------------------------- +// +// *** AUTO GENERATED CODE *** Type: MMv1 *** +// +// ---------------------------------------------------------------------------- +// +// This file is automatically generated by Magic Modules and manual +// changes will be clobbered when the file is regenerated. +// +// Please read more about how to change this file in +// .github/CONTRIBUTING.md. +// +// ---------------------------------------------------------------------------- + +{{- $productBackendName := $.CaiProductBackendName $.CaiProductBaseUrl }} +{{- $assetNameTemplate := $.CaiIamAssetNameTemplate $productBackendName }} + +package {{ lower $.ProductMetadata.Name }} + +import ( + "fmt" + + "github.com/GoogleCloudPlatform/terraform-google-conversion/v5/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 {{ $.ResourceName -}}IAMAssetType string = "{{ $productBackendName }}.googleapis.com/{{$.Name}}" + +func ResourceConverter{{ $.ResourceName -}}IamPolicy() cai.ResourceConverter { + return cai.ResourceConverter{ + AssetType: {{ $.ResourceName -}}IAMAssetType, + Convert: Get{{ $.ResourceName -}}IamPolicyCaiObject, + MergeCreateUpdate: Merge{{ $.ResourceName -}}IamPolicy, + } +} + +func ResourceConverter{{ $.ResourceName -}}IamBinding() cai.ResourceConverter { + return cai.ResourceConverter{ + AssetType: {{ $.ResourceName -}}IAMAssetType, + Convert: Get{{ $.ResourceName -}}IamBindingCaiObject, + FetchFullResource: Fetch{{ $.ResourceName -}}IamPolicy, + MergeCreateUpdate: Merge{{ $.ResourceName -}}IamBinding, + MergeDelete: Merge{{ $.ResourceName -}}IamBindingDelete, + } +} + +func ResourceConverter{{ $.ResourceName -}}IamMember() cai.ResourceConverter { + return cai.ResourceConverter{ + AssetType: {{ $.ResourceName -}}IAMAssetType, + Convert: Get{{ $.ResourceName -}}IamMemberCaiObject, + FetchFullResource: Fetch{{ $.ResourceName -}}IamPolicy, + MergeCreateUpdate: Merge{{ $.ResourceName -}}IamMember, + MergeDelete: Merge{{ $.ResourceName -}}IamMemberDelete, + } +} + +func Get{{ $.ResourceName -}}IamPolicyCaiObject(d tpgresource.TerraformResourceData, config *transport_tpg.Config) ([]cai.Asset, error) { + return new{{ $.ResourceName -}}IamAsset(d, config, cai.ExpandIamPolicyBindings) +} + +func Get{{ $.ResourceName -}}IamBindingCaiObject(d tpgresource.TerraformResourceData, config *transport_tpg.Config) ([]cai.Asset, error) { + return new{{ $.ResourceName -}}IamAsset(d, config, cai.ExpandIamRoleBindings) +} + +func Get{{ $.ResourceName -}}IamMemberCaiObject(d tpgresource.TerraformResourceData, config *transport_tpg.Config) ([]cai.Asset, error) { + return new{{ $.ResourceName -}}IamAsset(d, config, cai.ExpandIamMemberBindings) +} + +func Merge{{ $.ResourceName -}}IamPolicy(existing, incoming cai.Asset) cai.Asset { + existing.IAMPolicy = incoming.IAMPolicy + return existing +} + +func Merge{{ $.ResourceName -}}IamBinding(existing, incoming cai.Asset) cai.Asset { + return cai.MergeIamAssets(existing, incoming, cai.MergeAuthoritativeBindings) +} + +func Merge{{ $.ResourceName -}}IamBindingDelete(existing, incoming cai.Asset) cai.Asset { + return cai.MergeDeleteIamAssets(existing, incoming, cai.MergeDeleteAuthoritativeBindings) +} + +func Merge{{ $.ResourceName -}}IamMember(existing, incoming cai.Asset) cai.Asset { + return cai.MergeIamAssets(existing, incoming, cai.MergeAdditiveBindings) +} + +func Merge{{ $.ResourceName -}}IamMemberDelete(existing, incoming cai.Asset) cai.Asset { + return cai.MergeDeleteIamAssets(existing, incoming, cai.MergeDeleteAdditiveBindings) +} + +func new{{ $.ResourceName -}}IamAsset( + 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, "{{ $assetNameTemplate }}") + if err != nil { + return []cai.Asset{}, err + } + + return []cai.Asset{{"{{"}} + Name: name, + Type: {{ $.ResourceName -}}IAMAssetType, + IAMPolicy: &cai.IAMPolicy{ + Bindings: bindings, + }, + {{"}}"}}, nil +} + +func Fetch{{ $.ResourceName -}}IamPolicy(d tpgresource.TerraformResourceData, config *transport_tpg.Config) (cai.Asset, error) { + // Check if the identity field returns a value + {{- range $param := $.CaiIamResourceParams }} + if _, ok := d.GetOk("{{ $param }}"); !ok { + return cai.Asset{}, cai.ErrEmptyIdentityField + } + {{- end}} + + return cai.FetchIamPolicy( + {{ $.ResourceName -}}IamUpdaterProducer, + d, + config, + "{{ $assetNameTemplate }}", + {{ $.ResourceName -}}IAMAssetType, + ) +} diff --git a/mmv1/templates/tgc/resource_converters.go.tmpl b/mmv1/templates/tgc/resource_converters.go.tmpl new file mode 100644 index 000000000000..8c8e5baf6beb --- /dev/null +++ b/mmv1/templates/tgc/resource_converters.go.tmpl @@ -0,0 +1,192 @@ +{{/* The license inside this block applies to this file + Copyright 2024 Google LLC. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. */ -}} +// ---------------------------------------------------------------------------- +// +// *** AUTO GENERATED CODE *** Type: MMv1 *** +// +// ---------------------------------------------------------------------------- +// +// This file is automatically generated by Magic Modules and manual +// changes will be clobbered when the file is regenerated. +// +// Please read more about how to change this file in +// .github/CONTRIBUTING.md. +// +// ---------------------------------------------------------------------------- +package google + +import ( + "sort" + "github.com/GoogleCloudPlatform/terraform-google-conversion/v5/tfplan2cai/converters/google/resources/services/compute" + "github.com/GoogleCloudPlatform/terraform-google-conversion/v5/tfplan2cai/converters/google/resources/services/resourcemanager" + "github.com/GoogleCloudPlatform/terraform-google-conversion/v5/tfplan2cai/converters/google/resources/services/spanner" + "github.com/hashicorp/terraform-provider-google-beta/google-beta/tpgresource" +) + + +// ResourceConverter returns a map of terraform resource types (i.e. `google_project`) +// to a slice of ResourceConverters. +// +// Modelling of relationships: +// terraform resources to CAI assets as []cai.ResourceConverter: +// 1:1 = [ResourceConverter{Convert: convertAbc}] (len=1) +// 1:N = [ResourceConverter{Convert: convertAbc}, ...] (len=N) +// N:1 = [ResourceConverter{Convert: convertAbc, merge: mergeAbc}] (len=1) +func ResourceConverters() map[string][]cai.ResourceConverter { + return map[string][]cai.ResourceConverter{ + "google_artifact_registry_repository": {artifactregistry.ResourceConverterArtifactRegistryRepository()}, + "google_app_engine_application": {resourceConverterAppEngineApplication()}, + "google_alloydb_cluster": {alloydb.ResourceConverterAlloydbCluster()}, + "google_alloydb_instance": {alloydb.ResourceConverterAlloydbInstance()}, + "google_apikeys_key": {resourceConverterApikeysKey()}, + "google_compute_address": {compute.ResourceConverterComputeAddress()}, + "google_compute_autoscaler": {compute.ResourceConverterComputeAutoscaler()}, + "google_compute_firewall": {compute.ResourceConverterComputeFirewall()}, + "google_compute_disk": {compute.ResourceConverterComputeDisk()}, + "google_compute_forwarding_rule": {compute.ResourceConverterComputeForwardingRule()}, + "google_gke_hub_membership": {gkehub.ResourceConverterGKEHubMembership()}, + "google_compute_global_address": {compute.ResourceConverterComputeGlobalAddress()}, + "google_compute_global_forwarding_rule": {compute.ResourceConverterComputeGlobalForwardingRule()}, + "google_compute_health_check": {compute.ResourceConverterComputeHealthCheck()}, + "google_compute_instance": {compute.ResourceConverterComputeInstance()}, + "google_compute_instance_group": {resourceConverterComputeInstanceGroup()}, + "google_compute_network": {compute.ResourceConverterComputeNetwork()}, + "google_compute_node_template": {compute.ResourceConverterComputeNodeTemplate()}, + "google_compute_route": {compute.ResourceConverterComputeRoute()}, + "google_compute_router": {compute.ResourceConverterComputeRouter()}, + "google_compute_vpn_tunnel": {compute.ResourceConverterComputeVpnTunnel()}, + "google_compute_resource_policy": {compute.ResourceConverterComputeResourcePolicy()}, + "google_compute_security_policy": {resourceConverterComputeSecurityPolicy()}, + "google_compute_snapshot": {compute.ResourceConverterComputeSnapshot()}, + "google_compute_subnetwork": {compute.ResourceConverterComputeSubnetwork()}, + "google_compute_ssl_policy": {compute.ResourceConverterComputeSslPolicy()}, + "google_compute_ssl_certificate": {compute.ResourceConverterComputeSslCertificate()}, + "google_compute_url_map": {compute.ResourceConverterComputeUrlMap()}, + "google_compute_target_http_proxy": {compute.ResourceConverterComputeTargetHttpProxy()}, + "google_compute_target_https_proxy": {compute.ResourceConverterComputeTargetHttpsProxy()}, + "google_compute_target_ssl_proxy": {compute.ResourceConverterComputeTargetSslProxy()}, + "google_compute_target_pool": {resourceConverterComputeTargetPool()}, + "google_composer_environment": {resourceConverterComposerEnvironment()}, + "google_compute_region_commitment": {resourceConverterCommitment()}, + "google_dataflow_job": {resourceDataflowJob()}, + "google_dataproc_autoscaling_policy": {dataproc.ResourceConverterDataprocAutoscalingPolicy()}, + "google_dataproc_cluster": {resourceConverterDataprocCluster()}, + "google_dns_managed_zone": {dns.ResourceConverterDNSManagedZone()}, + "google_dns_policy": {dns.ResourceConverterDNSPolicy()}, + "google_kms_key_ring_import_job": {kms.ResourceConverterKMSKeyRingImportJob()}, + "google_gke_hub_feature": {gkehub2.ResourceConverterGKEHub2Feature()}, + "google_storage_bucket": {resourceConverterStorageBucket()}, + "google_sql_database_instance": {resourceConverterSQLDatabaseInstance()}, + "google_sql_database": {sql.ResourceConverterSQLDatabase()}, + "google_container_cluster": {resourceConverterContainerCluster()}, + "google_container_node_pool": {resourceConverterContainerNodePool()}, + "google_bigquery_dataset": {bigquery.ResourceConverterBigQueryDataset()}, + "google_bigquery_dataset_iam_policy": {bigquery.ResourceConverterBigqueryDatasetIamPolicy()}, + "google_bigquery_dataset_iam_binding": {bigquery.ResourceConverterBigqueryDatasetIamBinding()}, + "google_bigquery_dataset_iam_member": {bigquery.ResourceConverterBigqueryDatasetIamMember()}, + "google_bigquery_table": {resourceConverterBigQueryTable()}, + "google_datastream_connection_profile": {datastream.ResourceConverterDatastreamConnectionProfile()}, + "google_datastream_private_connection": {datastream.ResourceConverterDatastreamPrivateConnection()}, + "google_datastream_stream": {datastream.ResourceConverterDatastreamStream()}, + "google_firebase_project": {resourceConverterFirebaseProject()}, + "google_org_policy_policy": {resourceConverterOrgPolicyPolicy()}, + "google_redis_instance": {redis.ResourceConverterRedisInstance()}, + "google_spanner_database": {spanner.ResourceConverterSpannerDatabase()}, + "google_spanner_database_iam_policy": {spanner.ResourceConverterSpannerDatabaseIamPolicy()}, + "google_spanner_database_iam_binding": {spanner.ResourceConverterSpannerDatabaseIamBinding()}, + "google_spanner_database_iam_member": {spanner.ResourceConverterSpannerDatabaseIamMember()}, + "google_spanner_instance": {spanner.ResourceConverterSpannerInstance()}, + "google_spanner_instance_iam_policy": {spanner.ResourceConverterSpannerInstanceIamPolicy()}, + "google_spanner_instance_iam_binding": {spanner.ResourceConverterSpannerInstanceIamBinding()}, + "google_spanner_instance_iam_member": {spanner.ResourceConverterSpannerInstanceIamMember()}, + "google_project_service": {resourceConverterServiceUsage()}, + "google_secret_manager_secret_version": {secretmanager.ResourceConverterSecretManagerSecretVersion()}, + "google_pubsub_lite_reservation": {pubsublite.ResourceConverterPubsubLiteReservation()}, + "google_pubsub_lite_subscription": {pubsublite.ResourceConverterPubsubLiteSubscription()}, + "google_pubsub_lite_topic": {pubsublite.ResourceConverterPubsubLiteTopic()}, + "google_pubsub_schema": {pubsub.ResourceConverterPubsubSchema()}, + "google_pubsub_subscription": {pubsub.ResourceConverterPubsubSubscription()}, + "google_pubsub_subscription_iam_policy": {pubsub.ResourceConverterPubsubSubscriptionIamPolicy()}, + "google_pubsub_subscription_iam_binding": {pubsub.ResourceConverterPubsubSubscriptionIamBinding()}, + "google_pubsub_subscription_iam_member": {pubsub.ResourceConverterPubsubSubscriptionIamMember()}, + "google_storage_bucket_iam_policy": {resourceConverterStorageBucketIamPolicy()}, + "google_storage_bucket_iam_binding": {resourceConverterStorageBucketIamBinding()}, + "google_storage_bucket_iam_member": {resourceConverterStorageBucketIamMember()}, + "google_compute_node_group": {compute.ResourceConverterComputeNodeGroup()}, + "google_logging_folder_bucket_config": {resourceConverterLogFolderBucket()}, + "google_app_engine_standard_app_version": {resourceAppEngineStandardAppVersion()}, + "google_logging_organization_bucket_config": {resourceConverterLogOrganizationBucket()}, + "google_logging_project_bucket_config": {resourceConverterLogProjectBucket()}, + "google_logging_billing_account_bucket_config": {resourceConverterLogBillingAccountBucket()}, + "google_cloud_tasks_queue": {cloudtasks.ResourceConverterCloudTasksQueue()}, + "google_pubsub_topic": {pubsub.ResourceConverterPubsubTopic()}, + "google_kms_crypto_key": {kms.ResourceConverterKMSCryptoKey()}, + "google_kms_key_ring": {kms.ResourceConverterKMSKeyRing()}, + "google_filestore_instance": {filestore.ResourceConverterFilestoreInstance()}, + "google_access_context_manager_service_perimeter": {accesscontextmanager.ResourceConverterAccessContextManagerServicePerimeter()}, + "google_access_context_manager_access_policy": {accesscontextmanager.ResourceConverterAccessContextManagerAccessPolicy()}, + "google_cloud_run_service": {cloudrun.ResourceConverterCloudRunService()}, + "google_cloud_run_domain_mapping": {cloudrun.ResourceConverterCloudRunDomainMapping()}, + "google_cloud_run_v2_job": {cloudrunv2.ResourceConverterCloudRunV2Job()}, + "google_cloudfunctions_function": {resourceConverterCloudFunctionsCloudFunction()}, + "google_monitoring_notification_channel": {monitoring.ResourceConverterMonitoringNotificationChannel()}, + "google_monitoring_alert_policy": {monitoring.ResourceConverterMonitoringAlertPolicy()}, + "google_vertex_ai_dataset": {vertexai.ResourceConverterVertexAIDataset()}, + {{- range $object := $.ResourcesForVersion }} + {{- if $object.ResourceName }} + "{{ $object.TerraformName }}": {{ $object.ResourceName }}(), + {{- end }} + {{- if $object.IamClassName }} + "{{ $object.TerraformName }}_iam_binding": tpgiamresource.ResourceIamBinding({{ $object.IamClassName }}IamSchema, {{ $object.IamClassName }}IamUpdaterProducer, {{ $object.IamClassName }}IdParseFunc), + "{{ $object.TerraformName }}_iam_member": tpgiamresource.ResourceIamMember({{ $object.IamClassName }}IamSchema, {{ $object.IamClassName }}IamUpdaterProducer, {{ $object.IamClassName }}IdParseFunc), + "{{ $object.TerraformName }}_iam_policy": tpgiamresource.ResourceIamPolicy({{ $object.IamClassName }}IamSchema, {{ $object.IamClassName }}IamUpdaterProducer, {{ $object.IamClassName }}IdParseFunc), + {{- end }} + {{- end }} + "google_project": { + resourceConverterProject(), + resourceConverterProjectBillingInfo(), + }, + "google_bigtable_instance": { + resourceConverterBigtableInstance(), + resourceConverterBigtableCluster(), + }, + "google_organization_iam_policy": {resourcemanager.ResourceConverterOrganizationIamPolicy()}, + "google_organization_iam_binding": {resourcemanager.ResourceConverterOrganizationIamBinding()}, + "google_organization_iam_member": {resourcemanager.ResourceConverterOrganizationIamMember()}, + "google_organization_policy": {resourceConverterOrganizationPolicy()}, + "google_project_organization_policy": {resourceConverterProjectOrgPolicy()}, + "google_folder": {resourceConverterFolder()}, + "google_folder_iam_policy": {resourcemanager.ResourceConverterFolderIamPolicy()}, + "google_folder_iam_binding": {resourcemanager.ResourceConverterFolderIamBinding()}, + "google_folder_iam_member": {resourcemanager.ResourceConverterFolderIamMember()}, + "google_folder_organization_policy": {resourceConverterFolderOrgPolicy()}, + "google_kms_crypto_key_iam_policy": {resourceConverterKmsCryptoKeyIamPolicy()}, + "google_kms_crypto_key_iam_binding": {resourceConverterKmsCryptoKeyIamBinding()}, + "google_kms_crypto_key_iam_member": {resourceConverterKmsCryptoKeyIamMember()}, + "google_kms_key_ring_iam_policy": {resourceConverterKmsKeyRingIamPolicy()}, + "google_kms_key_ring_iam_binding": {resourceConverterKmsKeyRingIamBinding()}, + "google_kms_key_ring_iam_member": {resourceConverterKmsKeyRingIamMember()}, + "google_project_iam_policy": {resourcemanager.ResourceConverterProjectIamPolicy()}, + "google_project_iam_binding": {resourcemanager.ResourceConverterProjectIamBinding()}, + "google_project_iam_member": {resourcemanager.ResourceConverterProjectIamMember()}, + "google_project_iam_custom_role": {resourceConverterProjectIAMCustomRole()}, + "google_organization_iam_custom_role": {resourceConverterOrganizationIAMCustomRole()}, + "google_vpc_access_connector": {vpcaccess.ResourceConverterVPCAccessConnector()}, + "google_logging_metric": {logging.ResourceConverterLoggingMetric()}, + "google_service_account": {resourceConverterServiceAccount()}, + "google_service_account_key": {resourceConverterServiceAccountKey()}, + + } +} diff --git a/mmv1/third_party/tgc/tests/source/cli_test.go.erb b/mmv1/third_party/tgc/tests/source/cli_test.go.erb index a1a648bdb899..aa9ba3db86ef 100644 --- a/mmv1/third_party/tgc/tests/source/cli_test.go.erb +++ b/mmv1/third_party/tgc/tests/source/cli_test.go.erb @@ -1,18 +1,4 @@ <% autogen_exception -%> -// Copyright 2019 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - package test import ( diff --git a/mmv1/third_party/tgc/tests/source/go/cli_test.go.tmpl b/mmv1/third_party/tgc/tests/source/go/cli_test.go.tmpl new file mode 100644 index 000000000000..f4dcfe211199 --- /dev/null +++ b/mmv1/third_party/tgc/tests/source/go/cli_test.go.tmpl @@ -0,0 +1,169 @@ +package test + +import ( + "bytes" + "encoding/json" + "fmt" + "log" + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/GoogleCloudPlatform/terraform-google-conversion/v5/caiasset" +) + +// TestCLI tests the "convert" and "validate" subcommand against a generated .tfplan file. +func TestCLI(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test in short mode.") + return + } + + // Test cases for each type of resource is defined here. + cases := []struct { + name string + compareConvertOutput compareConvertOutputFunc + }{ + {name: "example_project_iam_binding", compareConvertOutput: compareMergedIamBindingOutput}, + {name: "example_project_iam_member", compareConvertOutput: compareMergedIamMemberOutput}, + } + + for i := range cases { + // Allocate a variable to make sure test can run in parallel. + c := cases[i] + + // Add default convert comparison func if not set + if c.compareConvertOutput == nil { + c.compareConvertOutput = compareUnmergedConvertOutput + } + + // Test both offline and online mode. + for _, offline := range []bool{true, false} { + offline := offline + t.Run(fmt.Sprintf("tf=%s/offline=%t", c.name, offline), func(t *testing.T) { + t.Parallel() + // Create a temporary directory for running terraform. + dir, err := os.MkdirTemp(tmpDir, "terraform") + if err != nil { + log.Fatal(err) + } + defer os.RemoveAll(dir) + + // Generate the .tf and _assets.json files into the temporary directory. + generateTestFiles(t, "../testdata/templates", dir, c.name+".tf") + generateTestFiles(t, "../testdata/templates", dir, c.name+".json") + + // Uses glob matching to match generateTestFiles internals. + tfstateMatches, err := filepath.Glob(filepath.Join("../testdata/templates", c.name+".tfstate")) + if err != nil { + t.Fatalf("malformed glob: %v", err) + } + if tfstateMatches != nil { + generateTestFiles(t, "../testdata/templates", dir, c.name+".tfstate") + err = os.Rename( + filepath.Join(dir, c.name+".tfstate"), + filepath.Join(dir, "terraform.tfstate"), + ) + if err != nil { + t.Fatalf("renaming tfstate: %v", err) + } + } + + terraformWorkflow(t, dir, c.name) + + t.Run("cmd=convert", func(t *testing.T) { + testConvertCommand(t, dir, c.name, c.name, offline, true, c.compareConvertOutput) + }) + }) + } + } +} + +type compareConvertOutputFunc func(t *testing.T, expected []caiasset.Asset, actual []caiasset.Asset, offline bool) + +func compareUnmergedConvertOutput(t *testing.T, expected []caiasset.Asset, actual []caiasset.Asset, offline bool) { + expectedAssets := normalizeAssets(t, expected, offline) + actualAssets := normalizeAssets(t, actual, offline) + if diff := cmp.Diff(expectedAssets, actualAssets); diff != "" { + t.Errorf("%v diff(-want, +got):\n%s", t.Name(), diff) + } +} + +// For merged IAM members, only consider whether the expected members are present. +func compareMergedIamMemberOutput(t *testing.T, expected []caiasset.Asset, actual []caiasset.Asset, offline bool) { + var normalizedActual []caiasset.Asset + for i := range expected { + expectedAsset := expected[i] + actualAsset := actual[i] + + // Copy actualAsset + normalizedActualAsset := actualAsset + + expectedBindings := map[string]map[string]struct{}{} + for _, binding := range expectedAsset.IAMPolicy.Bindings { + expectedBindings[binding.Role] = map[string]struct{}{} + for _, member := range binding.Members { + expectedBindings[binding.Role][member] = struct{}{} + } + } + + iamPolicy := caiasset.IAMPolicy{} + for _, binding := range actualAsset.IAMPolicy.Bindings { + if expectedMembers, exists := expectedBindings[binding.Role]; exists { + iamBinding := caiasset.IAMBinding{ + Role: binding.Role, + } + for _, member := range binding.Members { + if _, exists := expectedMembers[member]; exists { + iamBinding.Members = append(iamBinding.Members, member) + } + } + iamPolicy.Bindings = append(iamPolicy.Bindings, iamBinding) + } + } + normalizedActualAsset.IAMPolicy = &iamPolicy + normalizedActual = append(normalizedActual, normalizedActualAsset) + } + + expectedAssets := normalizeAssets(t, expected, offline) + actualAssets := normalizeAssets(t, normalizedActual, offline) + if diff := cmp.Diff(expectedAssets, actualAssets); diff != "" { + t.Errorf("%v diff(-want, +got):\n%s", t.Name(), diff) + } +} + +// For merged IAM bindings, only consider whether the expected bindings are as expected. +func compareMergedIamBindingOutput(t *testing.T, expected []caiasset.Asset, actual []caiasset.Asset, offline bool) { + var normalizedActual []caiasset.Asset + for i := range expected { + expectedAsset := expected[i] + actualAsset := actual[i] + + // Copy actualAsset + normalizedActualAsset := actualAsset + + expectedBindings := map[string]struct{}{} + for _, binding := range expectedAsset.IAMPolicy.Bindings { + expectedBindings[binding.Role] = struct{}{} + } + + iamPolicy := caiasset.IAMPolicy{} + for _, binding := range actualAsset.IAMPolicy.Bindings { + if _, exists := expectedBindings[binding.Role]; exists { + iamPolicy.Bindings = append(iamPolicy.Bindings, binding) + } + } + normalizedActualAsset.IAMPolicy = &iamPolicy + normalizedActual = append(normalizedActual, normalizedActualAsset) + } + + expectedAssets := normalizeAssets(t, expected, offline) + actualAssets := normalizeAssets(t, normalizedActual, offline) + if diff := cmp.Diff(expectedAssets, actualAssets); diff != "" { + t.Errorf("%v diff(-want, +got):\n%s", t.Name(), diff) + } +} diff --git a/mmv1/third_party/tgc/tests/source/go/iam_test.go.tmpl b/mmv1/third_party/tgc/tests/source/go/iam_test.go.tmpl new file mode 100644 index 000000000000..0d64bb921d09 --- /dev/null +++ b/mmv1/third_party/tgc/tests/source/go/iam_test.go.tmpl @@ -0,0 +1,106 @@ +package test + +import ( + "fmt" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + + crmv1 "google.golang.org/api/cloudresourcemanager/v1" + + resources "github.com/GoogleCloudPlatform/terraform-google-conversion/v5/tfplan2cai/converters/google/resources" + "github.com/GoogleCloudPlatform/terraform-google-conversion/v5/tfplan2cai/tfdata" + "github.com/GoogleCloudPlatform/terraform-google-conversion/v5/tfplan2cai/tfplan" + provider "github.com/hashicorp/terraform-provider-google-beta/google-beta/provider" +) + +func TestIAMFetchFullResource(t *testing.T) { + cases := []struct { + name string + }{ +{{- range $test := $.Tests }} +{{- if or (hasSuffix $test "iam_binding") (hasSuffix $test "iam_member") }} + {name: "{{ $test }}"}, +{{- end }} +{{- end }} + } + + converters := resources.ResourceConverters() + schema := provider.Provider() + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var payload []byte + var err error + if strings.Contains(r.URL.String(), "dataset") { + obj := map[string][]interface{}{ + "access": {&crmv1.Policy{}}, + } + payload, err = json.Marshal(obj) + } else { + payload, err = (&crmv1.Policy{}).MarshalJSON() + } + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(fmt.Sprintf("failed to MarshalJSON: %s", err))) + return + } + w.Write(payload) + })) + + // Using Cleanup instead of defer because t.Parallel() does not block t.Run. + t.Cleanup(func() { + server.Close() + }) + + cfg := newTestConfig(server) + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + t.Parallel() + + // Create a temporary directory for generating tfplan.json from template. + dir, err := os.MkdirTemp(tmpDir, "terraform") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + generateTestFiles(t, "../testdata/templates", dir, c.name+".tf") + + // Run terraform init and terraform apply to generate tfplan.json files + terraformWorkflow(t, dir, c.name) + + path := filepath.Join(dir, c.name+".tfplan.json") + + data, err := os.ReadFile(path) + if err != nil { + t.Fatalf("opening JSON plan file: %s", err) + } + changes, err := tfplan.ReadResourceChanges(data) + if err != nil { + t.Fatalf("ReadResourceChanges failed: %s", err) + } + + for _, rc := range changes { + resource := schema.ResourcesMap[rc.Type] + rd := tfdata.NewFakeResourceData( + rc.Type, + resource.Schema, + rc.Change.After.(map[string]interface{}), + ) + for _, converter := range converters[rd.Kind()] { + if converter.FetchFullResource == nil { + continue + } + _, err := converter.FetchFullResource(rd, cfg) + if err != nil { + t.Errorf("FetchFullResource() = %s, want = nil", err) + } + } + } + + }) + } +} diff --git a/mmv1/third_party/tgc/tests/source/go/read_test.go.tmpl b/mmv1/third_party/tgc/tests/source/go/read_test.go.tmpl new file mode 100644 index 000000000000..38bde4fb590d --- /dev/null +++ b/mmv1/third_party/tgc/tests/source/go/read_test.go.tmpl @@ -0,0 +1,175 @@ +package test + +import ( + "context" + "encoding/json" + "log" + "os" + "path/filepath" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/GoogleCloudPlatform/terraform-google-conversion/v5/tfplan2cai" + "go.uber.org/zap/zaptest" +) + +func TestReadPlannedAssetsCoverage(t *testing.T) { + cases := []struct { + name string + }{ + // read-only, the following tests are not in cli_test or + // have unique parameters that separate them + {name: "example_folder_iam_binding"}, + {name: "example_folder_iam_member"}, + {name: "example_project_create"}, + {name: "example_project_update"}, + {name: "example_project_iam_binding"}, + {name: "example_project_iam_member"}, + {name: "example_storage_bucket"}, + {name: "example_storage_bucket_empty_project_id"}, + {name: "example_storage_bucket_iam_binding"}, + {name: "example_storage_bucket_iam_member"}, + {name: "example_project_create_empty_project_id"}, + {name: "example_project_iam_member_empty_project"}, + // auto inserted tests that are not in list above or manually inserted in cli_test.go + {{- range $test := $.NonDefinedTests }} + {name: "{{- $test }}"}, + {{- end }} + } + for i := range cases { + // Allocate a variable to make sure test can run in parallel. + c := cases[i] + t.Run(c.name, func(t *testing.T) { + t.Parallel() + // Create a temporary directory for running terraform. + dir, err := os.MkdirTemp(tmpDir, "terraform") + if err != nil { + log.Fatal(err) + } + defer os.RemoveAll(dir) + + generateTestFiles(t, "../testdata/templates", dir, c.name+".json") + generateTestFiles(t, "../testdata/templates", dir, c.name+".tf") + + // tfstate files are for cases testing updates, eg. project update. + // Uses glob matching to match generateTestFiles internals. + tfstateMatches, err := filepath.Glob(filepath.Join("../testdata/templates", c.name+".tfstate")) + if err != nil { + t.Fatalf("malformed glob: %v", err) + } + if tfstateMatches != nil { + generateTestFiles(t, "../testdata/templates", dir, c.name+".tfstate") + err = os.Rename( + filepath.Join(dir, c.name+".tfstate"), + filepath.Join(dir, "terraform.tfstate"), + ) + if err != nil { + t.Fatalf("renaming tfstate: %v", err) + } + } + + // Run terraform init and terraform apply to generate tfplan.json files + terraformWorkflow(t, dir, c.name) + + // Unmarshal payload from testfile into `want` variable. + f := filepath.Join(dir, c.name+".json") + want, err := readExpectedTestFile(f) + if err != nil { + t.Fatal(err) + } + + planfile := filepath.Join(dir, c.name+".tfplan.json") + ctx := context.Background() + ancestryCache := map[string]string{ + data.Provider["project"]: data.Ancestry, + } + + jsonPlan, err := os.ReadFile(planfile) + if err != nil { + t.Fatalf("Error parsing %s: %s", f, err) + } + got, err := tfplan2cai.Convert(ctx, jsonPlan, &tfplan2cai.Options{ + ConvertUnchanged: false, + ErrorLogger: zaptest.NewLogger(t), + Offline: true, + DefaultProject: data.Provider["project"], + DefaultRegion: "", + DefaultZone: "", + UserAgent: "", + AncestryCache: ancestryCache, + }) + if err != nil { + t.Fatalf("Convert(%s, %s, \"\", \"\", %s, offline): %v", planfile, data.Provider["project"], ancestryCache, err) + } + expectedAssets := normalizeAssets(t, want, true) + actualAssets := normalizeAssets(t, got, true) + if diff := cmp.Diff(expectedAssets, actualAssets); diff != "" { + t.Errorf("%v diff(-want, +got):\n%s", t.Name(), diff) + } + }) + } +} + + + +func TestReadPlannedAssetsCoverage_WithoutDefaultProject(t *testing.T) { + cases := []struct { + name string + }{ + {name: "example_project_create_empty_project_id"}, + {name: "example_storage_bucket_empty_project_id"}, + {name: "example_project_iam_member_empty_project"}, + } + for i := range cases { + // Allocate a variable to make sure test can run in parallel. + c := cases[i] + t.Run(c.name, func(t *testing.T) { + t.Parallel() + // Create a temporary directory for running terraform. + dir, err := os.MkdirTemp(tmpDir, "terraform") + if err != nil { + log.Fatal(err) + } + defer os.RemoveAll(dir) + + generateTestFiles(t, "../testdata/templates", dir, c.name+"_without_default_project.json") + generateTestFiles(t, "../testdata/templates", dir, c.name+".tf") + + // Run terraform init and terraform plan to generate tfplan.json files + terraformWorkflow(t, dir, c.name) + + // Unmarshal payload from testfile into `want` variable. + f := filepath.Join(dir, c.name+"_without_default_project.json") + want, err := readExpectedTestFile(f) + if err != nil { + t.Fatal(err) + } + + planfile := filepath.Join(dir, c.name+".tfplan.json") + ctx := context.Background() + + jsonPlan, err := os.ReadFile(planfile) + if err != nil { + t.Fatalf("Error parsing %s: %s", f, err) + } + got, err := tfplan2cai.Convert(ctx, jsonPlan, &tfplan2cai.Options{ + ConvertUnchanged: false, + ErrorLogger: zaptest.NewLogger(t), + Offline: true, + DefaultProject: "", + DefaultRegion: "", + DefaultZone: "", + UserAgent: "", + AncestryCache: map[string]string{}, + }) + if err != nil { + t.Fatalf("WithoutProject: Convert(%s, offline): %v", planfile, err) + } + expectedAssets := normalizeAssets(t, want, true) + actualAssets := normalizeAssets(t, got, true) + if diff := cmp.Diff(expectedAssets, actualAssets); diff != "" { + t.Errorf("%v diff(-want, +got):\n%s", t.Name(), diff) + } + }) + } +}