Skip to content

Commit 65e60ce

Browse files
omerdemirokactions-user
authored andcommitted
deduplicate and unify gcp configs in case of multiple GCP providers in Terraform files (#2665)
This prevents creating duplicating GCP sources in case of a multi Google provider in TF files with overlapping projectIDs. GitOrigin-RevId: 9d9498f5385c300896de0d9f9afab65026036378
1 parent 938b37a commit 65e60ce

File tree

2 files changed

+266
-0
lines changed

2 files changed

+266
-0
lines changed

cmd/explore.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,8 @@ For more information about Terraform configuration, visit: https://developer.has
256256
statusArea.Println(fmt.Sprintf("Using GCP provider in %s with project %s%s.", p.FilePath, config.ProjectID, aliasInfo))
257257
}
258258

259+
gcpConfigs = unifiedGCPConfigs(gcpConfigs)
260+
259261
// Fallback to default GCP config if no terraform providers found
260262
if len(gcpConfigs) == 0 && failOverToDefaultLoginCfg {
261263
statusArea.Println("No GCP terraform providers found. Attempting to use GCP Application Default Credentials for configuration.")
@@ -403,3 +405,38 @@ func init() {
403405

404406
addAPIFlags(exploreCmd)
405407
}
408+
409+
// unifiedGCPConfigs collates the given GCP configs by project ID.
410+
// If there are multiple configs for the same project ID, the configs are merged.
411+
func unifiedGCPConfigs(gcpConfigs []*gcpproc.GCPConfig) []*gcpproc.GCPConfig {
412+
unified := make(map[string]*gcpproc.GCPConfig)
413+
for _, config := range gcpConfigs {
414+
if _, ok := unified[config.ProjectID]; !ok {
415+
unified[config.ProjectID] = config
416+
} else {
417+
unified[config.ProjectID].Regions = append(unified[config.ProjectID].Regions, config.Regions...)
418+
unified[config.ProjectID].Zones = append(unified[config.ProjectID].Zones, config.Zones...)
419+
}
420+
}
421+
422+
unifiedConfigs := make([]*gcpproc.GCPConfig, 0, len(unified))
423+
for _, config := range unified {
424+
var deDuplicatedRegions []string
425+
var deDuplicatedZones []string
426+
for _, region := range config.Regions {
427+
if !slices.Contains(deDuplicatedRegions, region) {
428+
deDuplicatedRegions = append(deDuplicatedRegions, region)
429+
}
430+
}
431+
for _, zone := range config.Zones {
432+
if !slices.Contains(deDuplicatedZones, zone) {
433+
deDuplicatedZones = append(deDuplicatedZones, zone)
434+
}
435+
}
436+
config.Regions = deDuplicatedRegions
437+
config.Zones = deDuplicatedZones
438+
unifiedConfigs = append(unifiedConfigs, config)
439+
}
440+
441+
return unifiedConfigs
442+
}

cmd/explore_test.go

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
package cmd
2+
3+
import (
4+
"reflect"
5+
"testing"
6+
7+
gcpproc "github.com/overmindtech/cli/sources/gcp/proc"
8+
)
9+
10+
func TestUnifiedGCPConfigs(t *testing.T) {
11+
t.Run("Multiple configs with different project IDs - no unification", func(t *testing.T) {
12+
configs := []*gcpproc.GCPConfig{
13+
{
14+
ProjectID: "project-1",
15+
Regions: []string{"us-central1", "us-east1"},
16+
Zones: []string{"us-central1-a", "us-east1-a"},
17+
},
18+
{
19+
ProjectID: "project-2",
20+
Regions: []string{"us-central1", "us-east1"},
21+
Zones: []string{"us-central1-a", "us-east1-a"},
22+
},
23+
{
24+
ProjectID: "project-3",
25+
Regions: []string{"europe-west1"},
26+
Zones: []string{"europe-west1-b"},
27+
},
28+
}
29+
30+
result := unifiedGCPConfigs(configs)
31+
32+
// Should have 3 configs (no unification since all project IDs are different)
33+
if len(result) != 3 {
34+
t.Fatalf("Expected 3 configs, got %d", len(result))
35+
}
36+
37+
// Verify each project ID appears exactly once
38+
projectIDs := make(map[string]int)
39+
for _, config := range result {
40+
projectIDs[config.ProjectID]++
41+
}
42+
43+
expectedProjects := []string{"project-1", "project-2", "project-3"}
44+
for _, projectID := range expectedProjects {
45+
if count, exists := projectIDs[projectID]; !exists || count != 1 {
46+
t.Fatalf("Expected project %s to appear exactly once, got %d", projectID, count)
47+
}
48+
}
49+
50+
// Find and verify each config maintains its original regions and zones
51+
for _, originalConfig := range configs {
52+
var foundConfig *gcpproc.GCPConfig
53+
for _, resultConfig := range result {
54+
if resultConfig.ProjectID == originalConfig.ProjectID {
55+
foundConfig = resultConfig
56+
break
57+
}
58+
}
59+
60+
if foundConfig == nil {
61+
t.Fatalf("Could not find config for project %s in result", originalConfig.ProjectID)
62+
}
63+
64+
if !reflect.DeepEqual(foundConfig.Regions, originalConfig.Regions) {
65+
t.Fatalf("Regions for project %s don't match. Expected %v, got %v",
66+
originalConfig.ProjectID, originalConfig.Regions, foundConfig.Regions)
67+
}
68+
69+
if !reflect.DeepEqual(foundConfig.Zones, originalConfig.Zones) {
70+
t.Fatalf("Zones for project %s don't match. Expected %v, got %v",
71+
originalConfig.ProjectID, originalConfig.Zones, foundConfig.Zones)
72+
}
73+
}
74+
})
75+
76+
t.Run("Same project ID with different regions - unification", func(t *testing.T) {
77+
configs := []*gcpproc.GCPConfig{
78+
{
79+
ProjectID: "unified-project",
80+
Regions: []string{"us-central1", "us-east1"},
81+
Zones: []string{"us-central1-a"},
82+
},
83+
{
84+
ProjectID: "unified-project",
85+
Regions: []string{"europe-west1", "asia-east1"},
86+
Zones: []string{"europe-west1-b"},
87+
},
88+
{
89+
ProjectID: "different-project",
90+
Regions: []string{"us-west1"},
91+
Zones: []string{"us-west1-a"},
92+
},
93+
}
94+
95+
result := unifiedGCPConfigs(configs)
96+
97+
// Should have 2 configs (unified-project configs merged)
98+
if len(result) != 2 {
99+
t.Fatalf("Expected 2 configs, got %d", len(result))
100+
}
101+
102+
// Find the unified config
103+
var unifiedConfig *gcpproc.GCPConfig
104+
var differentConfig *gcpproc.GCPConfig
105+
106+
for _, config := range result {
107+
switch config.ProjectID {
108+
case "unified-project":
109+
unifiedConfig = config
110+
case "different-project":
111+
differentConfig = config
112+
}
113+
}
114+
115+
if unifiedConfig == nil {
116+
t.Fatal("Could not find unified-project config in result")
117+
}
118+
if differentConfig == nil {
119+
t.Fatal("Could not find different-project config in result")
120+
}
121+
122+
// Verify unified config has all regions
123+
expectedRegions := []string{"us-central1", "us-east1", "europe-west1", "asia-east1"}
124+
125+
if !reflect.DeepEqual(unifiedConfig.Regions, expectedRegions) {
126+
t.Fatalf("Unified regions don't match. Expected %v, got %v", expectedRegions, unifiedConfig.Regions)
127+
}
128+
129+
// Verify unified config has all zones
130+
expectedZones := []string{"us-central1-a", "europe-west1-b"}
131+
132+
if !reflect.DeepEqual(unifiedConfig.Zones, expectedZones) {
133+
t.Fatalf("Unified zones don't match. Expected %v, got %v", expectedZones, unifiedConfig.Zones)
134+
}
135+
136+
// Verify different-project config is unchanged
137+
if !reflect.DeepEqual(differentConfig.Regions, []string{"us-west1"}) {
138+
t.Fatalf("Different project regions changed. Expected [us-west1], got %v", differentConfig.Regions)
139+
}
140+
if !reflect.DeepEqual(differentConfig.Zones, []string{"us-west1-a"}) {
141+
t.Fatalf("Different project zones changed. Expected [us-west1-a], got %v", differentConfig.Zones)
142+
}
143+
})
144+
145+
t.Run("Same project ID with different zones and regions - unification", func(t *testing.T) {
146+
configs := []*gcpproc.GCPConfig{
147+
{
148+
ProjectID: "zone-project",
149+
Regions: []string{"us-central1"},
150+
Zones: []string{"us-central1-a", "us-central1-b"},
151+
},
152+
{
153+
ProjectID: "zone-project",
154+
Regions: []string{"us-east1"},
155+
Zones: []string{"us-east1-a", "us-east1-c"},
156+
},
157+
}
158+
159+
result := unifiedGCPConfigs(configs)
160+
161+
// Should have 1 config (both configs merged)
162+
if len(result) != 1 {
163+
t.Fatalf("Expected 1 config, got %d", len(result))
164+
}
165+
166+
unifiedConfig := result[0]
167+
if unifiedConfig.ProjectID != "zone-project" {
168+
t.Fatalf("Expected project ID 'zone-project', got %s", unifiedConfig.ProjectID)
169+
}
170+
171+
// Verify unified config has all regions
172+
expectedRegions := []string{"us-central1", "us-east1"}
173+
174+
if !reflect.DeepEqual(unifiedConfig.Regions, expectedRegions) {
175+
t.Fatalf("Unified regions don't match. Expected %v, got %v", expectedRegions, unifiedConfig.Regions)
176+
}
177+
178+
// Verify unified config has all zones
179+
expectedZones := []string{"us-central1-a", "us-central1-b", "us-east1-a", "us-east1-c"}
180+
181+
if !reflect.DeepEqual(unifiedConfig.Zones, expectedZones) {
182+
t.Fatalf("Unified zones don't match. Expected %v, got %v", expectedZones, unifiedConfig.Zones)
183+
}
184+
})
185+
186+
t.Run("Same project ID with overlapping regions and zones - proper unification", func(t *testing.T) {
187+
configs := []*gcpproc.GCPConfig{
188+
{
189+
ProjectID: "overlap-project",
190+
Regions: []string{"us-central1", "us-east1", "europe-west1"},
191+
Zones: []string{"us-central1-a", "us-central1-b", "europe-west1-a"},
192+
},
193+
{
194+
ProjectID: "overlap-project",
195+
Regions: []string{"us-central1", "asia-east1"}, // us-central1 overlaps
196+
Zones: []string{"us-central1-a", "asia-east1-a"}, // us-central1-a overlaps
197+
},
198+
{
199+
ProjectID: "overlap-project",
200+
Regions: []string{"europe-west1", "us-west1"}, // europe-west1 overlaps
201+
Zones: []string{"europe-west1-a", "us-west1-b"}, // europe-west1-a overlaps
202+
},
203+
}
204+
205+
result := unifiedGCPConfigs(configs)
206+
207+
// Should have 1 config (all configs merged)
208+
if len(result) != 1 {
209+
t.Fatalf("Expected 1 config, got %d", len(result))
210+
}
211+
212+
unifiedConfig := result[0]
213+
if unifiedConfig.ProjectID != "overlap-project" {
214+
t.Fatalf("Expected project ID 'overlap-project', got %s", unifiedConfig.ProjectID)
215+
}
216+
217+
expectedRegions := []string{"us-central1", "us-east1", "europe-west1", "asia-east1", "us-west1"}
218+
219+
if !reflect.DeepEqual(unifiedConfig.Regions, expectedRegions) {
220+
t.Fatalf("Unified regions don't match. Expected %v, got %v", expectedRegions, unifiedConfig.Regions)
221+
}
222+
223+
expectedZones := []string{"us-central1-a", "us-central1-b", "europe-west1-a", "asia-east1-a", "us-west1-b"}
224+
225+
if !reflect.DeepEqual(unifiedConfig.Zones, expectedZones) {
226+
t.Fatalf("Unified zones don't match. Expected %v, got %v", expectedZones, unifiedConfig.Zones)
227+
}
228+
})
229+
}

0 commit comments

Comments
 (0)