Skip to content

Commit e8b3b0b

Browse files
authored
feat(conditional component): allow components to be included conditional (#536)
Use case: A client has a component that is deployed to multiple regions. However, some components need to be rolled out in a single region. Supporting count is the most flexible option I could think of. This was AI generated, so happy to clean up any stylistic changes or consider alternatives.
2 parents 7ef4e1d + bde25b2 commit e8b3b0b

File tree

6 files changed

+177
-2
lines changed

6 files changed

+177
-2
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
kind: Added
2+
body: Allow count on components for specific module exclusion
3+
time: 2025-09-08T15:22:14.965735+02:00

internal/config/schemas/schema-1.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,8 @@ definitions:
175175
properties:
176176
name:
177177
type: string
178+
count:
179+
type: string
178180
variables:
179181
$ref: "#/definitions/MachComposerVariables"
180182
secrets:

internal/config/site_component.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ type SiteComponentConfig struct {
2424
Variables variable.VariablesMap `yaml:"variables"`
2525
Secrets variable.VariablesMap `yaml:"secrets"`
2626
Deployment *Deployment `yaml:"deployment"`
27+
Count string `yaml:"count"`
2728

2829
DependsOn []string `yaml:"depends_on"`
2930
}

internal/generator/component.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ type componentContext struct {
1919
ComponentHash string
2020
ComponentVariables string
2121
ComponentSecrets string
22+
ComponentCount string
2223
SiteName string
2324
Environment string
2425
SourceType string
@@ -155,6 +156,7 @@ func renderComponentModule(_ context.Context, cfg *config.MachConfig, n *graph.S
155156
tc := componentContext{
156157
ComponentName: n.SiteComponentConfig.Name,
157158
ComponentVersion: n.SiteComponentConfig.Definition.Version,
159+
ComponentCount: n.SiteComponentConfig.Count,
158160
SiteName: n.SiteConfig.Identifier,
159161
Environment: cfg.Global.Environment,
160162
Version: n.SiteComponentConfig.Definition.Version,

internal/generator/component_test.go

Lines changed: 164 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
package generator
22

33
import (
4+
"context"
5+
"strings"
6+
"testing"
7+
48
"github.com/mach-composer/mach-composer-cli/internal/config"
59
"github.com/mach-composer/mach-composer-cli/internal/config/variable"
10+
"github.com/mach-composer/mach-composer-cli/internal/graph"
11+
"github.com/mach-composer/mach-composer-cli/internal/plugins"
612
"github.com/mach-composer/mach-composer-cli/internal/state"
713
"github.com/stretchr/testify/assert"
814
"github.com/stretchr/testify/mock"
9-
"testing"
1015
)
1116

1217
type mockRenderer struct {
@@ -65,3 +70,161 @@ func TestRenderRemoteSourcesCompactSources(t *testing.T) {
6570
assert.NoError(t, err)
6671
assert.Equal(t, "remote-state\nremote-state-2", resp)
6772
}
73+
74+
func TestRenderComponentModuleWithCount(t *testing.T) {
75+
ctx := context.Background()
76+
77+
// Create mock state renderer
78+
rr := new(mockRenderer)
79+
rr.On("Identifier").Return("test-site/test-component")
80+
rr.On("StateKey").Return("test-component")
81+
rr.On("Backend").Return("backend {}", nil)
82+
83+
// Create state repository
84+
r := state.NewRepository()
85+
_ = r.Add(rr)
86+
87+
// Create plugin repository
88+
pr := plugins.NewPluginRepository()
89+
90+
// Create test configuration
91+
cfg := &config.MachConfig{
92+
Global: config.GlobalConfig{
93+
Environment: "test",
94+
},
95+
Plugins: pr,
96+
StateRepository: r,
97+
}
98+
99+
// Create component definition
100+
source := config.Source("./modules/test-component")
101+
componentDef := &config.ComponentConfig{
102+
Name: "test-component",
103+
Version: "1.0.0",
104+
Source: source,
105+
}
106+
107+
// Create site component with count
108+
siteComponent := config.SiteComponentConfig{
109+
Name: "test-component",
110+
Definition: componentDef,
111+
Count: "data.sops_external.variables.data[\"region\"] == \"foo\" ? 1 : 0",
112+
Variables: make(variable.VariablesMap),
113+
Secrets: make(variable.VariablesMap),
114+
Deployment: &config.Deployment{
115+
Type: config.DeploymentSiteComponent,
116+
},
117+
}
118+
119+
// Create site config
120+
siteConfig := config.SiteConfig{
121+
Identifier: "test-site",
122+
Variables: make(variable.VariablesMap),
123+
Secrets: make(variable.VariablesMap),
124+
}
125+
126+
// Create project config
127+
projectConfig := config.MachConfig{
128+
Global: config.GlobalConfig{
129+
Variables: make(variable.VariablesMap),
130+
Secrets: make(variable.VariablesMap),
131+
},
132+
}
133+
134+
// Create graph node
135+
node := &graph.SiteComponent{
136+
SiteComponentConfig: siteComponent,
137+
SiteConfig: siteConfig,
138+
ProjectConfig: projectConfig,
139+
}
140+
141+
// Render the component module
142+
result, err := renderComponentModule(ctx, cfg, node)
143+
assert.NoError(t, err)
144+
145+
// Check that count is included in the output
146+
assert.Contains(t, result, "count = data.sops_external.variables.data[\"region\"] == \"foo\" ? 1 : 0")
147+
assert.Contains(t, result, "module.test-component[*]")
148+
}
149+
150+
func TestRenderComponentModuleWithoutCount(t *testing.T) {
151+
ctx := context.Background()
152+
153+
// Create mock state renderer
154+
rr := new(mockRenderer)
155+
rr.On("Identifier").Return("test-site/test-component")
156+
rr.On("StateKey").Return("test-component")
157+
rr.On("Backend").Return("backend {}", nil)
158+
159+
// Create state repository
160+
r := state.NewRepository()
161+
_ = r.Add(rr)
162+
163+
// Create plugin repository
164+
pr := plugins.NewPluginRepository()
165+
166+
// Create test configuration
167+
cfg := &config.MachConfig{
168+
Global: config.GlobalConfig{
169+
Environment: "test",
170+
},
171+
Plugins: pr,
172+
StateRepository: r,
173+
}
174+
175+
// Create component definition
176+
source := config.Source("./modules/test-component")
177+
componentDef := &config.ComponentConfig{
178+
Name: "test-component",
179+
Version: "1.0.0",
180+
Source: source,
181+
}
182+
183+
// Create site component without count
184+
siteComponent := config.SiteComponentConfig{
185+
Name: "test-component",
186+
Definition: componentDef,
187+
Count: "", // No count specified
188+
Variables: make(variable.VariablesMap),
189+
Secrets: make(variable.VariablesMap),
190+
Deployment: &config.Deployment{
191+
Type: config.DeploymentSiteComponent,
192+
},
193+
}
194+
195+
// Create site config
196+
siteConfig := config.SiteConfig{
197+
Identifier: "test-site",
198+
Variables: make(variable.VariablesMap),
199+
Secrets: make(variable.VariablesMap),
200+
}
201+
202+
// Create project config
203+
projectConfig := config.MachConfig{
204+
Global: config.GlobalConfig{
205+
Variables: make(variable.VariablesMap),
206+
Secrets: make(variable.VariablesMap),
207+
},
208+
}
209+
210+
// Create graph node
211+
node := &graph.SiteComponent{
212+
SiteComponentConfig: siteComponent,
213+
SiteConfig: siteConfig,
214+
ProjectConfig: projectConfig,
215+
}
216+
217+
// Render the component module
218+
result, err := renderComponentModule(ctx, cfg, node)
219+
assert.NoError(t, err)
220+
221+
// Check that count is NOT included in the output
222+
assert.NotContains(t, result, "count =")
223+
// Check that output doesn't have [*] suffix
224+
lines := strings.Split(result, "\n")
225+
for _, line := range lines {
226+
if strings.Contains(line, "value =") && strings.Contains(line, "module.test-component") {
227+
assert.NotContains(t, line, "[*]")
228+
}
229+
}
230+
}

internal/generator/templates/site_component.tmpl

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ module "{{ .ComponentName }}" {
1010
version = "{{ .Version }}"
1111
{{ end }}
1212

13+
{{ if .ComponentCount }}
14+
count = {{ .ComponentCount }}
15+
{{ end }}
16+
1317

1418
{{ if .ComponentVariables }}
1519
{{ .ComponentVariables }}
@@ -50,5 +54,5 @@ module "{{ .ComponentName }}" {
5054
output "{{ .ComponentName }}" {
5155
description = "The module outputs for {{ .ComponentName }}"
5256
sensitive = true
53-
value = module.{{ .ComponentName }}
57+
value = module.{{ .ComponentName }}{{ if .ComponentCount }}[*]{{ end }}
5458
}

0 commit comments

Comments
 (0)