feat(agent): report and refuse Compose services built from source; honour --env-file (#7)
This commit is contained in:
@@ -26,6 +26,20 @@ func (f fakeCompose) ContainerList(context.Context, container.ListOptions) ([]co
|
||||
return f.containers, nil
|
||||
}
|
||||
|
||||
// outRunner is a fake OutputRunner for `docker compose config`.
|
||||
type outRunner struct {
|
||||
json string
|
||||
err error
|
||||
cmds []ranCmd
|
||||
}
|
||||
|
||||
func (o *outRunner) run(_ context.Context, dir string, args ...string) ([]byte, error) {
|
||||
o.cmds = append(o.cmds, ranCmd{dir, args})
|
||||
return []byte(o.json), o.err
|
||||
}
|
||||
|
||||
const plainConfig = `{"services":{"web":{"image":"x"},"db":{"image":"y"}}}`
|
||||
|
||||
type ranCmd struct {
|
||||
dir string
|
||||
args []string
|
||||
@@ -71,8 +85,10 @@ func TestComposeExecutor(t *testing.T) {
|
||||
overridePath := filepath.Join(stack, OverrideName)
|
||||
cleanOverride := func() { os.Remove(overridePath) }
|
||||
|
||||
outr := &outRunner{json: plainConfig}
|
||||
setup := func(cli ComposeDocker, dist *fakeDist, rec *recorder) *ComposeExecutor {
|
||||
return NewComposeExecutor(cli, NewResolver(dist, nil), "me", rec.run)
|
||||
*outr = outRunner{json: plainConfig}
|
||||
return NewComposeExecutor(cli, NewResolver(dist, nil), "me", rec.run, outr.run)
|
||||
}
|
||||
|
||||
t.Run("happy path", func(t *testing.T) {
|
||||
@@ -92,6 +108,80 @@ func TestComposeExecutor(t *testing.T) {
|
||||
assert.Equal(t, pinned, serviceImage(doc, "web"))
|
||||
assert.Contains(t, out.String(), "resolved "+pinned)
|
||||
})
|
||||
t.Run("built from source is refused before anything is written", func(t *testing.T) {
|
||||
t.Cleanup(cleanOverride)
|
||||
rec := &recorder{}
|
||||
e := setup(fakeCompose{rw, []container.Summary{ct}}, &fakeDist{digest: d}, rec)
|
||||
outr.json = `{"services":{"web":{"image":"nginx","build":{"context":"."}}}}`
|
||||
_, err := e.Execute(context.Background(), job, io.Discard)
|
||||
assert.EqualError(t, err, "service web is built from source (build:), not updated")
|
||||
assert.Empty(t, rec.cmds, "no pull/up")
|
||||
assert.NoFileExists(t, overridePath)
|
||||
})
|
||||
t.Run("build check uses the job's files without the override", func(t *testing.T) {
|
||||
t.Cleanup(cleanOverride)
|
||||
rec := &recorder{}
|
||||
_, 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])
|
||||
})
|
||||
t.Run("build check failure stops the job", func(t *testing.T) {
|
||||
t.Cleanup(cleanOverride)
|
||||
rec := &recorder{}
|
||||
e := setup(fakeCompose{rw, []container.Summary{ct}}, &fakeDist{digest: d}, rec)
|
||||
outr.err = errors.New("invalid compose file")
|
||||
_, err := e.Execute(context.Background(), job, io.Discard)
|
||||
assert.ErrorContains(t, err, "invalid compose file")
|
||||
assert.Empty(t, rec.cmds)
|
||||
assert.NoFileExists(t, overridePath)
|
||||
})
|
||||
t.Run("env files are passed to every compose call", func(t *testing.T) {
|
||||
t.Cleanup(cleanOverride)
|
||||
envFile := filepath.Join(stack, ".env.prod")
|
||||
require.NoError(t, os.WriteFile(envFile, []byte("A=1\n"), 0o644))
|
||||
withEnv := container.Summary{Labels: map[string]string{
|
||||
labelWorkingDir: stack, labelConfigFiles: filepath.Join(stack, "compose.yaml"), labelEnvFile: envFile,
|
||||
}}
|
||||
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}
|
||||
require.Len(t, outr.cmds, 1)
|
||||
assert.Equal(t, prefix, outr.cmds[0].args[:len(prefix)])
|
||||
require.Len(t, rec.cmds, 2)
|
||||
assert.Equal(t, prefix, rec.cmds[0].args[:len(prefix)])
|
||||
assert.Equal(t, prefix, rec.cmds[1].args[:len(prefix)])
|
||||
})
|
||||
t.Run("env file outside the roots is refused", func(t *testing.T) {
|
||||
t.Cleanup(cleanOverride)
|
||||
outside := filepath.Join(t.TempDir(), "secrets.env")
|
||||
require.NoError(t, os.WriteFile(outside, []byte("A=1\n"), 0o644))
|
||||
bad := container.Summary{Labels: map[string]string{
|
||||
labelWorkingDir: stack, labelConfigFiles: filepath.Join(stack, "compose.yaml"), labelEnvFile: outside,
|
||||
}}
|
||||
rec := &recorder{}
|
||||
_, err := setup(fakeCompose{rw, []container.Summary{bad}}, &fakeDist{digest: d}, rec).Execute(context.Background(), job, io.Discard)
|
||||
assert.ErrorContains(t, err, "env file")
|
||||
assert.Empty(t, rec.cmds)
|
||||
assert.Empty(t, outr.cmds)
|
||||
assert.NoFileExists(t, overridePath)
|
||||
})
|
||||
t.Run("env file symlinked outside the roots is refused", func(t *testing.T) {
|
||||
t.Cleanup(cleanOverride)
|
||||
outside := filepath.Join(t.TempDir(), "secrets.env")
|
||||
require.NoError(t, os.WriteFile(outside, []byte("A=1\n"), 0o644))
|
||||
link := filepath.Join(stack, ".env.link")
|
||||
require.NoError(t, os.Symlink(outside, link))
|
||||
t.Cleanup(func() { os.Remove(link) })
|
||||
bad := container.Summary{Labels: map[string]string{
|
||||
labelWorkingDir: stack, labelConfigFiles: filepath.Join(stack, "compose.yaml"), labelEnvFile: link,
|
||||
}}
|
||||
rec := &recorder{}
|
||||
_, err := setup(fakeCompose{rw, []container.Summary{bad}}, &fakeDist{digest: d}, rec).Execute(context.Background(), job, io.Discard)
|
||||
assert.ErrorContains(t, err, "env file")
|
||||
assert.Empty(t, rec.cmds)
|
||||
})
|
||||
t.Run("no matching container", func(t *testing.T) {
|
||||
rec := &recorder{}
|
||||
_, err := setup(fakeCompose{rw, nil}, &fakeDist{digest: d}, rec).Execute(context.Background(), job, io.Discard)
|
||||
@@ -163,3 +253,21 @@ func TestDockerEnv(t *testing.T) {
|
||||
assert.False(t, strings.HasPrefix(e, "DOCKER_CERT_PATH="), "unset variables are not passed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecDocker_UsesMinimalEnv(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "docker"), []byte("#!/bin/sh\nenv\n"), 0o755))
|
||||
t.Setenv("PATH", dir+":/usr/bin:/bin")
|
||||
t.Setenv("AGENT_TOKEN", "secret")
|
||||
t.Setenv("DOCKER_CONFIG", "/docker-config")
|
||||
|
||||
var out strings.Builder
|
||||
require.NoError(t, ExecDocker(context.Background(), dir, &out, "compose", "ps"))
|
||||
assert.NotContains(t, out.String(), "AGENT_TOKEN")
|
||||
assert.Contains(t, out.String(), "DOCKER_CONFIG=/docker-config")
|
||||
|
||||
raw, err := ExecDockerOutput(context.Background(), dir, "compose", "config")
|
||||
require.NoError(t, err)
|
||||
assert.NotContains(t, string(raw), "AGENT_TOKEN")
|
||||
assert.Contains(t, string(raw), "DOCKER_CONFIG=/docker-config")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user