fix(agent): list all compose profiles and fail closed on unknown services (#7)
This commit is contained in:
@@ -49,7 +49,9 @@ func NewBuildChecker(cli SelfInspector, selfID string, run OutputRunner) *BuildC
|
||||
return &BuildChecker{cli: cli, selfID: selfID, run: run}
|
||||
}
|
||||
|
||||
// BuiltServices returns the services of project that have a build: section.
|
||||
// BuiltServices returns every service of project (all profiles) mapped to
|
||||
// whether it has a build: section. A service that is not a key is not in the
|
||||
// project's configuration.
|
||||
// The arguments are the labels of one of the project's containers. Files that
|
||||
// are not inside the agent's read-write same-path mounts are refused.
|
||||
func (b *BuildChecker) BuiltServices(ctx context.Context, project, workingDir, configFilesLabel, envFileLabel string) (map[string]bool, error) {
|
||||
@@ -77,7 +79,9 @@ func (b *BuildChecker) BuiltServices(ctx context.Context, project, workingDir, c
|
||||
// already checked.
|
||||
func (b *BuildChecker) builtServices(ctx context.Context, project, workingDir string, files, envs []string) (map[string]bool, error) {
|
||||
args := composeBase(project, workingDir, files, envs)
|
||||
args = append(args, "config", "--format", "json")
|
||||
// Without --profile * compose omits services whose profile is not active,
|
||||
// and a built service behind a profile would read as absent.
|
||||
args = append(args, "--profile", "*", "config", "--format", "json")
|
||||
raw, err := b.run(ctx, workingDir, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("docker compose config: %w", err)
|
||||
|
||||
@@ -27,7 +27,7 @@ func TestBuildChecker_BuiltServices(t *testing.T) {
|
||||
assert.Equal(t, map[string]bool{"web": true, "db": false, "x": false}, got)
|
||||
require.Len(t, o.cmds, 1)
|
||||
assert.Equal(t, stack, o.cmds[0].dir)
|
||||
assert.Equal(t, []string{"compose", "-p", "app", "--project-directory", stack, "-f", filepath.Join(stack, "compose.yaml"), "config", "--format", "json"}, o.cmds[0].args)
|
||||
assert.Equal(t, []string{"compose", "-p", "app", "--project-directory", stack, "-f", filepath.Join(stack, "compose.yaml"), "--profile", "*", "config", "--format", "json"}, o.cmds[0].args)
|
||||
})
|
||||
t.Run("files outside the roots are refused without running docker", func(t *testing.T) {
|
||||
o := &outRunner{json: plainConfig}
|
||||
|
||||
@@ -138,7 +138,11 @@ func (e *ComposeExecutor) Execute(ctx context.Context, job protocol.JobStart, ou
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if built[service] {
|
||||
isBuilt, known := built[service]
|
||||
if !known {
|
||||
return "", fmt.Errorf("service %s not found in compose config", service)
|
||||
}
|
||||
if isBuilt {
|
||||
return "", fmt.Errorf("service %s is built from source (build:), not updated", service)
|
||||
}
|
||||
|
||||
|
||||
@@ -124,7 +124,20 @@ func TestComposeExecutor(t *testing.T) {
|
||||
_, err := setup(fakeCompose{rw, []container.Summary{ct}}, &fakeDist{digest: d}, rec).Execute(context.Background(), job, io.Discard)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, outr.cmds, 1)
|
||||
assert.Equal(t, ranCmd{stack, []string{"compose", "-p", "slusk", "--project-directory", stack, "-f", filepath.Join(stack, "compose.yaml"), "config", "--format", "json"}}, outr.cmds[0])
|
||||
assert.Equal(t, ranCmd{stack, []string{"compose", "-p", "slusk", "--project-directory", stack, "-f", filepath.Join(stack, "compose.yaml"), "--profile", "*", "config", "--format", "json"}}, outr.cmds[0])
|
||||
})
|
||||
t.Run("service missing from the config is refused", func(t *testing.T) {
|
||||
t.Cleanup(cleanOverride)
|
||||
rec := &recorder{}
|
||||
e := setup(fakeCompose{rw, []container.Summary{ct}}, &fakeDist{digest: d}, rec)
|
||||
outr.json = `{"services":{"db":{"image":"y"}}}`
|
||||
dist := &fakeDist{digest: d}
|
||||
e.resolve = NewResolver(dist, nil)
|
||||
_, err := e.Execute(context.Background(), job, io.Discard)
|
||||
assert.EqualError(t, err, "service web not found in compose config")
|
||||
assert.Empty(t, rec.cmds)
|
||||
assert.Empty(t, dist.refs, "nothing resolved")
|
||||
assert.NoFileExists(t, overridePath)
|
||||
})
|
||||
t.Run("build check failure stops the job", func(t *testing.T) {
|
||||
t.Cleanup(cleanOverride)
|
||||
@@ -140,13 +153,16 @@ func TestComposeExecutor(t *testing.T) {
|
||||
t.Cleanup(cleanOverride)
|
||||
envFile := filepath.Join(stack, ".env.prod")
|
||||
require.NoError(t, os.WriteFile(envFile, []byte("A=1\n"), 0o644))
|
||||
t.Cleanup(func() { os.Remove(envFile); os.Remove(filepath.Join(stack, ".env.local")) })
|
||||
envFile2 := filepath.Join(stack, ".env.local")
|
||||
require.NoError(t, os.WriteFile(envFile2, []byte("B=2\n"), 0o644))
|
||||
withEnv := container.Summary{Labels: map[string]string{
|
||||
labelWorkingDir: stack, labelConfigFiles: filepath.Join(stack, "compose.yaml"), labelEnvFile: envFile,
|
||||
labelWorkingDir: stack, labelConfigFiles: filepath.Join(stack, "compose.yaml"), labelEnvFile: envFile + "," + envFile2,
|
||||
}}
|
||||
rec := &recorder{}
|
||||
_, err := setup(fakeCompose{rw, []container.Summary{withEnv}}, &fakeDist{digest: d}, rec).Execute(context.Background(), job, io.Discard)
|
||||
require.NoError(t, err)
|
||||
prefix := []string{"compose", "-p", "slusk", "--project-directory", stack, "--env-file", envFile}
|
||||
prefix := []string{"compose", "-p", "slusk", "--project-directory", stack, "--env-file", envFile, "--env-file", envFile2}
|
||||
require.Len(t, outr.cmds, 1)
|
||||
assert.Equal(t, prefix, outr.cmds[0].args[:len(prefix)])
|
||||
require.Len(t, rec.cmds, 2)
|
||||
|
||||
Reference in New Issue
Block a user