Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 27 additions & 35 deletions mmv1/provider/terraform_tgc_next.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -500,63 +502,53 @@ 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
}
}

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
}
Expand Down
127 changes: 127 additions & 0 deletions mmv1/provider/terraform_tgc_next_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
})
}
}
6 changes: 2 additions & 4 deletions mmv1/templates/tgc_next/cai2hcl/convert_resource.go.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -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)}}
Expand Down
Loading