Skip to content

Commit 8a5ab3d

Browse files
committed
PS-1530: Viper -> Kong, rework controller entrypoint
1 parent 87c51da commit 8a5ab3d

File tree

11 files changed

+697
-624
lines changed

11 files changed

+697
-624
lines changed

cmd/controller/controller.go

Lines changed: 103 additions & 289 deletions
Large diffs are not rendered by default.

cmd/controller/controller_test.go

Lines changed: 239 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
package controller_test
22

33
import (
4+
"os"
45
"testing"
56
"time"
67

78
"github.com/buildkite/agent-stack-k8s/v2/cmd/controller"
89
"github.com/buildkite/agent-stack-k8s/v2/internal/controller/config"
910
"github.com/google/go-cmp/cmp"
10-
"github.com/spf13/cobra"
11+
"github.com/stretchr/testify/assert"
1112
"github.com/stretchr/testify/require"
1213
corev1 "k8s.io/api/core/v1"
1314
"k8s.io/apimachinery/pkg/api/resource"
@@ -164,22 +165,9 @@ func TestReadAndParseConfig(t *testing.T) {
164165
},
165166
}
166167

167-
// These need to be unset to as it is set in CI which pollutes the test environment
168-
t.Setenv("INTEGRATION_TEST_BUILDKITE_TOKEN", "")
169-
t.Setenv("IMAGE", "")
170-
t.Setenv("NAMESPACE", "")
171-
t.Setenv("AGENT_TOKEN_SECRET", "")
168+
cleanTestEnv(t)
172169

173-
cmd := &cobra.Command{}
174-
controller.AddConfigFlags(cmd)
175-
v, err := controller.ReadConfigFromFileArgsAndEnv(cmd, []string{})
176-
require.NoError(t, err)
177-
178-
// We need to read the config file from the test
179-
v.SetConfigFile("../../examples/config.yaml")
180-
require.NoError(t, v.ReadInConfig())
181-
182-
actual, err := controller.ParseAndValidateConfig(v)
170+
actual, err := controller.BuildConfigFromArgs([]string{"--config=../../examples/config.yaml"})
183171
require.NoError(t, err)
184172

185173
if diff := cmp.Diff(*actual, expected); diff != "" {
@@ -189,55 +177,257 @@ func TestReadAndParseConfig(t *testing.T) {
189177

190178
func TestParseAndValidateConfig_DefaultResourceClassValidation(t *testing.T) {
191179
tests := []struct {
192-
name string
193-
config map[string]any
194-
wantErr string
180+
name string
181+
configYAML string
182+
wantErr string
195183
}{
196184
{
197185
name: "default references non-existent resource class",
198-
config: map[string]any{
199-
"agent-token-secret": "test",
200-
"image": "test:latest",
201-
"job-active-deadline-seconds": 3600,
202-
"namespace": "default",
203-
"default-resource-class-name": "nonexistent",
204-
"resource-classes": map[string]any{
205-
"small": map[string]any{
206-
"resource": map[string]any{
207-
"requests": map[string]any{"cpu": "100m"},
208-
},
209-
},
210-
},
211-
},
186+
configYAML: `
187+
default-resource-class-name: nonexistent
188+
resource-classes:
189+
small:
190+
resource:
191+
requests:
192+
cpu: "100m"
193+
`,
212194
wantErr: `default-resource-class-name "nonexistent" not found in resource-classes`,
213195
},
214196
{
215197
name: "default specified but no resource classes defined",
216-
config: map[string]any{
217-
"agent-token-secret": "test",
218-
"image": "test:latest",
219-
"job-active-deadline-seconds": 3600,
220-
"namespace": "default",
221-
"default-resource-class-name": "small",
222-
},
198+
configYAML: `
199+
default-resource-class-name: small
200+
`,
223201
wantErr: `default-resource-class-name "small" specified but no resource-classes defined`,
224202
},
225203
}
226204

227205
for _, tt := range tests {
228206
t.Run(tt.name, func(t *testing.T) {
229-
cmd := &cobra.Command{}
230-
controller.AddConfigFlags(cmd)
231-
v, err := controller.ReadConfigFromFileArgsAndEnv(cmd, []string{})
232-
require.NoError(t, err)
207+
cleanTestEnv(t)
208+
configFile := createTempConfigFile(t, tt.configYAML)
233209

234-
for k, val := range tt.config {
235-
v.Set(k, val)
236-
}
237-
238-
_, err = controller.ParseAndValidateConfig(v)
210+
_, err := controller.BuildConfigFromArgs([]string{"--config=" + configFile})
239211
require.Error(t, err)
240212
require.Contains(t, err.Error(), tt.wantErr)
241213
})
242214
}
243215
}
216+
217+
func TestConfigPrecedence(t *testing.T) {
218+
// Helper to build config with optional config file
219+
buildConfig := func(t *testing.T, args []string, configFile string) (*config.Config, error) {
220+
t.Helper()
221+
if configFile != "" {
222+
args = append([]string{"--config=" + configFile}, args...)
223+
}
224+
return controller.BuildConfigFromArgs(args)
225+
}
226+
227+
t.Run("defaults applied when nothing set", func(t *testing.T) {
228+
cleanTestEnv(t)
229+
230+
cfg, err := buildConfig(t, []string{}, "")
231+
require.NoError(t, err)
232+
233+
assert.Equal(t, 25, cfg.MaxInFlight)
234+
assert.Equal(t, "default", cfg.Namespace)
235+
assert.Equal(t, 10*time.Minute, cfg.JobTTL)
236+
assert.False(t, cfg.Debug)
237+
})
238+
239+
t.Run("config file overrides defaults", func(t *testing.T) {
240+
cleanTestEnv(t)
241+
configFile := createTempConfigFile(t, `
242+
max-in-flight: 100
243+
namespace: from-file
244+
debug: true
245+
`)
246+
247+
cfg, err := buildConfig(t, []string{}, configFile)
248+
require.NoError(t, err)
249+
250+
assert.Equal(t, 100, cfg.MaxInFlight)
251+
assert.Equal(t, "from-file", cfg.Namespace)
252+
assert.True(t, cfg.Debug)
253+
})
254+
255+
t.Run("config file can set zero value", func(t *testing.T) {
256+
cleanTestEnv(t)
257+
configFile := createTempConfigFile(t, `max-in-flight: 0`)
258+
259+
cfg, err := buildConfig(t, []string{}, configFile)
260+
require.NoError(t, err)
261+
262+
assert.Equal(t, 0, cfg.MaxInFlight)
263+
})
264+
265+
t.Run("config file can set false", func(t *testing.T) {
266+
cleanTestEnv(t)
267+
configFile := createTempConfigFile(t, `debug: false`)
268+
269+
cfg, err := buildConfig(t, []string{}, configFile)
270+
require.NoError(t, err)
271+
272+
assert.False(t, cfg.Debug)
273+
})
274+
275+
t.Run("env var overrides config file", func(t *testing.T) {
276+
cleanTestEnv(t)
277+
configFile := createTempConfigFile(t, `
278+
max-in-flight: 100
279+
namespace: from-file
280+
`)
281+
t.Setenv("MAX_IN_FLIGHT", "50")
282+
t.Setenv("NAMESPACE", "from-env")
283+
284+
cfg, err := buildConfig(t, []string{}, configFile)
285+
require.NoError(t, err)
286+
287+
assert.Equal(t, 50, cfg.MaxInFlight)
288+
assert.Equal(t, "from-env", cfg.Namespace)
289+
})
290+
291+
t.Run("env var can override config file with zero", func(t *testing.T) {
292+
cleanTestEnv(t)
293+
configFile := createTempConfigFile(t, `max-in-flight: 100`)
294+
t.Setenv("MAX_IN_FLIGHT", "0")
295+
296+
cfg, err := buildConfig(t, []string{}, configFile)
297+
require.NoError(t, err)
298+
299+
assert.Equal(t, 0, cfg.MaxInFlight)
300+
})
301+
302+
t.Run("env var can override config file with false", func(t *testing.T) {
303+
cleanTestEnv(t)
304+
configFile := createTempConfigFile(t, `debug: true`)
305+
t.Setenv("DEBUG", "false")
306+
307+
cfg, err := buildConfig(t, []string{}, configFile)
308+
require.NoError(t, err)
309+
310+
assert.False(t, cfg.Debug)
311+
})
312+
313+
t.Run("CLI overrides env var", func(t *testing.T) {
314+
cleanTestEnv(t)
315+
t.Setenv("MAX_IN_FLIGHT", "50")
316+
t.Setenv("NAMESPACE", "from-env")
317+
318+
cfg, err := buildConfig(t, []string{
319+
"--max-in-flight=200",
320+
"--namespace=from-cli",
321+
}, "")
322+
require.NoError(t, err)
323+
324+
assert.Equal(t, 200, cfg.MaxInFlight)
325+
assert.Equal(t, "from-cli", cfg.Namespace)
326+
})
327+
328+
t.Run("CLI can override env var with zero", func(t *testing.T) {
329+
cleanTestEnv(t)
330+
t.Setenv("MAX_IN_FLIGHT", "50")
331+
332+
cfg, err := buildConfig(t, []string{"--max-in-flight=0"}, "")
333+
require.NoError(t, err)
334+
335+
assert.Equal(t, 0, cfg.MaxInFlight)
336+
})
337+
338+
t.Run("CLI can override env var with false", func(t *testing.T) {
339+
cleanTestEnv(t)
340+
t.Setenv("DEBUG", "true")
341+
342+
cfg, err := buildConfig(t, []string{"--debug=false"}, "")
343+
require.NoError(t, err)
344+
345+
assert.False(t, cfg.Debug)
346+
})
347+
348+
t.Run("full precedence chain", func(t *testing.T) {
349+
cleanTestEnv(t)
350+
configFile := createTempConfigFile(t, `
351+
max-in-flight: 100
352+
namespace: from-file
353+
job-ttl: 5m
354+
`)
355+
t.Setenv("NAMESPACE", "from-env")
356+
t.Setenv("JOB_TTL", "15m")
357+
358+
cfg, err := buildConfig(t, []string{"--namespace=from-cli"}, configFile)
359+
require.NoError(t, err)
360+
361+
// max-in-flight: only in file
362+
assert.Equal(t, 100, cfg.MaxInFlight)
363+
// namespace: file -> env -> cli (cli wins)
364+
assert.Equal(t, "from-cli", cfg.Namespace)
365+
// job-ttl: file -> env (env wins)
366+
assert.Equal(t, 15*time.Minute, cfg.JobTTL)
367+
})
368+
369+
t.Run("duration fields work correctly", func(t *testing.T) {
370+
cleanTestEnv(t)
371+
configFile := createTempConfigFile(t, `
372+
job-ttl: 30m
373+
poll-interval: 5s
374+
`)
375+
376+
cfg, err := buildConfig(t, []string{}, configFile)
377+
require.NoError(t, err)
378+
379+
assert.Equal(t, 30*time.Minute, cfg.JobTTL)
380+
assert.Equal(t, 5*time.Second, cfg.PollInterval)
381+
})
382+
383+
t.Run("config file with unknown field is rejected", func(t *testing.T) {
384+
cleanTestEnv(t)
385+
configFile := createTempConfigFile(t, `
386+
namespace: my-namespace
387+
unknown-field: some-value
388+
`)
389+
390+
_, err := buildConfig(t, []string{}, configFile)
391+
require.Error(t, err)
392+
assert.Contains(t, err.Error(), "unknown-field")
393+
})
394+
}
395+
396+
// cleanTestEnv unsets environment variables that might be set in CI or .envrc
397+
// which could pollute the test environment.
398+
func cleanTestEnv(t *testing.T) {
399+
t.Helper()
400+
for _, env := range []string{
401+
"BUILDKITE_TOKEN",
402+
"INTEGRATION_TEST_BUILDKITE_TOKEN",
403+
"IMAGE",
404+
"NAMESPACE",
405+
"AGENT_TOKEN_SECRET",
406+
"IMAGE_PULL_BACKOFF_GRACE_PERIOD",
407+
"CONFIG",
408+
"MAX_IN_FLIGHT",
409+
"DEBUG",
410+
"JOB_TTL",
411+
} {
412+
t.Setenv(env, "")
413+
os.Unsetenv(env)
414+
}
415+
}
416+
417+
// createTempConfigFile creates a temporary config file with the given content
418+
// and returns its path. The file is automatically cleaned up after the test.
419+
func createTempConfigFile(t *testing.T, content string) string {
420+
t.Helper()
421+
f, err := os.CreateTemp("", "config-*.yaml")
422+
if err != nil {
423+
t.Fatalf("failed to create temp config file: %v", err)
424+
}
425+
if _, err := f.WriteString(content); err != nil {
426+
t.Fatalf("failed to write temp config file: %v", err)
427+
}
428+
if err := f.Close(); err != nil {
429+
t.Fatalf("failed to close temp config file: %v", err)
430+
}
431+
t.Cleanup(func() { os.Remove(f.Name()) })
432+
return f.Name()
433+
}

0 commit comments

Comments
 (0)