diff --git a/mmv1/provider/terraform_tgc_next.go b/mmv1/provider/terraform_tgc_next.go index efc093d7bd58..0ef4fd7359b7 100644 --- a/mmv1/provider/terraform_tgc_next.go +++ b/mmv1/provider/terraform_tgc_next.go @@ -63,7 +63,7 @@ type ResourceIdentifier struct { AliasName string // It can be "Default" or the same with ResourceName CaiAssetNameFormat string ImportFormats []string - IdentityParam string + IdentityParams []string } func NewTerraformGoogleConversionNext(product *api.Product, versionName string, startTime time.Time, templateFS fs.FS) TerraformGoogleConversionNext { @@ -485,12 +485,14 @@ func (tgc *TerraformGoogleConversionNext) generateResourcesForVersion(products [ } } -// Analyzes a list of CAI asset names and finds the single path segment -// (by index) that contains different values across all names. -// Example: -// "folders/{{folder}}/feeds/{{feed_id}}" -> folders -// "organizations/{{org_id}}/feeds/{{feed_id}} -> organizations -// "projects/{{project}}/feeds/{{feed_id}}" -> projects +// Analyzes a list of CAI asset names and finds all path segments +// that contain different values across all names, dropping only the segments +// that are identical across the entire group. This robustly retains identifying +// combinations of segments (e.g., ["projects", "global"] vs ["locations", "global"]). +// Example (simplest case, single unique segment): +// "folders/{{folder}}/feeds/{{feed_id}}" -> ["folders"] +// "organizations/{{org_id}}/feeds/{{feed_id}}" -> ["organizations"] +// "projects/{{project}}/feeds/{{feed_id}}" -> ["projects"] func FindIdentityParams(rids []ResourceIdentifier) []ResourceIdentifier { segmentsList := make([][]string, len(rids)) for i, rid := range rids { @@ -500,22 +502,19 @@ func FindIdentityParams(rids []ResourceIdentifier) []ResourceIdentifier { segmentsList = removeSharedElements(segmentsList) for i, segments := range segmentsList { - if len(segments) == 0 { - rids[i].IdentityParam = "" - } else { - rids[i].IdentityParam = segments[0] - } + rids[i].IdentityParams = segments } - // Check if we have multiple resources with the same IdentityParam - identityParams := make(map[string]int) + // Check if we have multiple resources with the same IdentityParams + identityParamsCounts := make(map[string]int) for _, rid := range rids { - identityParams[rid.IdentityParam]++ + key := strings.Join(rid.IdentityParams, "|") + identityParamsCounts[key]++ } // If we have collisions or empty params, try using ImportFormats hasCollision := false - for _, count := range identityParams { + for _, count := range identityParamsCounts { if count > 1 { hasCollision = true break @@ -523,40 +522,33 @@ func FindIdentityParams(rids []ResourceIdentifier) []ResourceIdentifier { } if hasCollision { - // Reset segmentsList using ImportFormats + // Reset segmentsList using ImportFormats where available, else CaiAssetNameFormat for i, rid := range rids { if len(rid.ImportFormats) > 0 { segmentsList[i] = processPathIntoSegments(rid.ImportFormats[0]) } else { - // If no import format, fallback to previous empty list or keep as is? - // For now let's assume if we are falling back, we want fresh segments. - segmentsList[i] = []string{} + segmentsList[i] = processPathIntoSegments(rid.CaiAssetNameFormat) } } segmentsList = removeSharedElements(segmentsList) for i, segments := range segmentsList { - if len(segments) == 0 { - rids[i].IdentityParam = "" - } else { - rids[i].IdentityParam = segments[0] - } + rids[i].IdentityParams = segments } } - // Move the id with empty IdentityParam to the end of the list - for i, ids := range rids { - if ids.IdentityParam == "" { - temp := ids - lastIndex := len(rids) - 1 - if i != lastIndex { - rids[i] = rids[lastIndex] - rids[lastIndex] = temp - } - break + // Move the ids with empty IdentityParams to the end of the list + var withParam []ResourceIdentifier + var withoutParam []ResourceIdentifier + for _, ids := range rids { + if len(ids.IdentityParams) == 0 { + withoutParam = append(withoutParam, ids) + } else { + withParam = append(withParam, ids) } } + rids = append(withParam, withoutParam...) return rids } diff --git a/mmv1/provider/terraform_tgc_next_test.go b/mmv1/provider/terraform_tgc_next_test.go new file mode 100644 index 000000000000..e0f73ec5d540 --- /dev/null +++ b/mmv1/provider/terraform_tgc_next_test.go @@ -0,0 +1,127 @@ +// Copyright 2026 Google Inc. +// 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 provider + +import ( + "reflect" + "testing" +) + +func TestFindIdentityParams(t *testing.T) { + cases := []struct { + name string + input []ResourceIdentifier + expected [][]string // Comparing IdentityParams slices in return sequence + }{ + { + name: "single resource with single distinct segment", + input: []ResourceIdentifier{ + { + CaiAssetNameFormat: "folders/{{folder}}/feeds/{{name}}", + }, + { + CaiAssetNameFormat: "organizations/{{org_id}}/feeds/{{name}}", + }, + { + CaiAssetNameFormat: "projects/{{project}}/feeds/{{name}}", + }, + }, + expected: [][]string{ + {"folders"}, + {"organizations"}, + {"projects"}, + }, + }, + { + name: "complex multi-segment firewall collision logic", + input: []ResourceIdentifier{ + { + // NetworkFirewallPolicy + CaiAssetNameFormat: "projects/{{project}}/global/firewallPolicies/{{name}}", + }, + { + // RegionNetworkFirewallPolicy + CaiAssetNameFormat: "projects/{{project}}/regions/{{region}}/firewallPolicies/{{name}}", + }, + { + // FirewallPolicy + CaiAssetNameFormat: "locations/global/firewallPolicies/{{name}}", + }, + }, + expected: [][]string{ + {"projects", "global"}, + {"projects", "regions"}, + {"locations", "global"}, + }, + }, + { + name: "fallback to import formats when there is a collision", + input: []ResourceIdentifier{ + { + CaiAssetNameFormat: "projects/{{project}}/regions/{{region}}/forwardingRules/{{name}}", + ImportFormats: []string{"projects/{{project}}/regions/{{region}}/forwardingRules/{{name}}"}, + }, + { + // Forced collision through CaiAssetNameFormat identical structure + CaiAssetNameFormat: "projects/{{project}}/regions/{{region}}/forwardingRules/{{name}}", + ImportFormats: []string{"projects/{{project}}/global/forwardingRules/{{name}}"}, + }, + }, + expected: [][]string{ + {"regions"}, + {"global"}, + }, + }, + { + name: "empty identify params grouped at end", + input: []ResourceIdentifier{ + { + CaiAssetNameFormat: "projects/{{project}}/global/backendServices/{{name}}", + ImportFormats: []string{"projects/{{project}}/global/backendServices/{{name}}"}, + }, + { + CaiAssetNameFormat: "projects/{{project}}/global/backendServices/{{name}}", + ImportFormats: []string{"projects/{{project}}/global/backendServices/{{name}}"}, + }, + }, + expected: [][]string{ + nil, + nil, + }, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + // FindIdentityParams modifies the input array directly and returns it + inputCopy := make([]ResourceIdentifier, len(c.input)) + copy(inputCopy, c.input) + + result := FindIdentityParams(inputCopy) + if len(result) != len(c.expected) { + t.Fatalf("expected length %d, got %d", len(c.expected), len(result)) + } + + for i, exp := range c.expected { + if len(result[i].IdentityParams) == 0 && len(exp) == 0 { + continue // Both represent an empty/nil slice successfully + } + + if !reflect.DeepEqual(result[i].IdentityParams, exp) { + t.Errorf("at index %d: expected IdentityParams %v, got %v", i, exp, result[i].IdentityParams) + } + } + }) + } +} diff --git a/mmv1/templates/tgc_next/cai2hcl/convert_resource.go.tmpl b/mmv1/templates/tgc_next/cai2hcl/convert_resource.go.tmpl index 420f14280d6e..36637dc940fc 100644 --- a/mmv1/templates/tgc_next/cai2hcl/convert_resource.go.tmpl +++ b/mmv1/templates/tgc_next/cai2hcl/convert_resource.go.tmpl @@ -40,11 +40,9 @@ func ConvertResource(asset caiasset.Asset) ([]*models.TerraformResourceBlock, er case "{{ $resourceType }}": {{- range $i, $object := $resources }} {{- if eq $i 0 }} - if strings.Contains(asset.Name, "{{$object.IdentityParam}}") { - {{- else if $object.IdentityParam }} - } else if strings.Contains(asset.Name, "{{$object.IdentityParam}}") { + if true {{- range $param := $object.IdentityParams }} && strings.Contains(asset.Name, "{{$param}}"){{- end }} { {{- else }} - } else { + } else if true {{- range $param := $object.IdentityParams }} && strings.Contains(asset.Name, "{{$param}}"){{- end }} { {{- end }} converter = ConverterMap[asset.Type]["{{ $object.AliasName }}"] {{- if eq $i (sub (len $resources) 1)}}