feat(agent): report and refuse Compose services built from source; honour --env-file (#7)

This commit is contained in:
2026-10-12 19:32:05 +02:00
parent 69ef55837e
commit 27c4200dda
8 changed files with 534 additions and 24 deletions
+21 -9
View File
@@ -20,6 +20,7 @@ const (
labelComposeService = "com.docker.compose.service"
labelWorkingDir = "com.docker.compose.project.working_dir"
labelConfigFiles = "com.docker.compose.project.config_files"
labelEnvFile = "com.docker.compose.project.environment_file"
)
// errorOutputTail is how much of a failed command's output goes into the
@@ -80,13 +81,14 @@ type ComposeExecutor struct {
resolve *Resolver
selfID string
run CommandRunner
build *BuildChecker
}
// NewComposeExecutor returns an executor for Compose jobs. selfID names the
// agent's own container, whose mounts decide which directories may be
// touched; run is ExecDocker outside tests.
func NewComposeExecutor(cli ComposeDocker, resolve *Resolver, selfID string, run CommandRunner) *ComposeExecutor {
return &ComposeExecutor{cli: cli, resolve: resolve, selfID: selfID, run: run}
// touched; run and runOut are ExecDocker and ExecDockerOutput outside tests.
func NewComposeExecutor(cli ComposeDocker, resolve *Resolver, selfID string, run CommandRunner, runOut OutputRunner) *ComposeExecutor {
return &ComposeExecutor{cli: cli, resolve: resolve, selfID: selfID, run: run, build: NewBuildChecker(cli, selfID, runOut)}
}
// Execute pins job.Ref to its current digest, writes it to the stack's
@@ -110,6 +112,7 @@ func (e *ComposeExecutor) Execute(ctx context.Context, job protocol.JobStart, ou
}
workingDir := cts[0].Labels[labelWorkingDir]
configFiles := cts[0].Labels[labelConfigFiles]
envFileLabel := cts[0].Labels[labelEnvFile]
roots, err := MountRoots(ctx, e.cli, e.selfID)
if err != nil {
@@ -125,6 +128,20 @@ func (e *ComposeExecutor) Execute(ctx context.Context, job protocol.JobStart, ou
return "", notMounted(workingDir)
}
envs, err := envFiles(roots, envFileLabel)
if err != nil {
return "", err
}
// Fresh check, not the inventory's cache: a build: section may have been
// added since. Nothing is written for such a service.
built, err := e.build.builtServices(ctx, project, workingDir, files[:len(files)-1], envs)
if err != nil {
return "", err
}
if built[service] {
return "", fmt.Errorf("service %s is built from source (build:), not updated", service)
}
pinned, err := e.resolve.Resolve(ctx, job.Ref)
if err != nil {
return "", err
@@ -135,12 +152,7 @@ func (e *ComposeExecutor) Execute(ctx context.Context, job protocol.JobStart, ou
return "", err
}
// --project-directory is explicit because, with -f, compose would take it
// from the first file, which differs for stacks started with the flag.
base := []string{"compose", "-p", project, "--project-directory", workingDir}
for _, f := range files {
base = append(base, "-f", f)
}
base := composeBase(project, workingDir, files, envs)
for _, step := range [][]string{{"pull", service}, {"up", "-d", service}} {
args := append(append([]string(nil), base...), step...)
fmt.Fprintf(out, "$ docker %s\n", strings.Join(args, " "))