114 lines
3.7 KiB
Go
114 lines
3.7 KiB
Go
package updater
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os/exec"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// OutputRunner runs "docker <args>" in dir and returns its stdout. A failure
|
|
// carries the tail of stderr in the error.
|
|
type OutputRunner func(ctx context.Context, dir string, args ...string) ([]byte, error)
|
|
|
|
// ExecDockerOutput is the real OutputRunner. It uses the same minimal
|
|
// environment as ExecDocker.
|
|
func ExecDockerOutput(ctx context.Context, dir string, args ...string) ([]byte, error) {
|
|
cmd := exec.CommandContext(ctx, "docker", args...)
|
|
cmd.Dir = dir
|
|
cmd.Env = dockerEnv()
|
|
cmd.WaitDelay = 10 * time.Second
|
|
var stdout, stderr bytes.Buffer
|
|
cmd.Stdout = &stdout
|
|
cmd.Stderr = &stderr
|
|
if err := cmd.Run(); err != nil {
|
|
tail := &tailBuffer{max: errorOutputTail}
|
|
tail.Write(stderr.Bytes())
|
|
return nil, fmt.Errorf("%w (output: %s)", err, strings.TrimSpace(tail.String()))
|
|
}
|
|
return stdout.Bytes(), nil
|
|
}
|
|
|
|
// BuildChecker finds out which services of a Compose project are built from
|
|
// source, by asking `docker compose config`. Such a service has a local
|
|
// image that an update would replace with the upstream one, so it is never
|
|
// updated.
|
|
type BuildChecker struct {
|
|
cli SelfInspector
|
|
selfID string
|
|
run OutputRunner
|
|
}
|
|
|
|
// NewBuildChecker returns a BuildChecker. cli and selfID name the agent's own
|
|
// container, whose mounts decide which files may be read; run is
|
|
// ExecDockerOutput outside tests.
|
|
func NewBuildChecker(cli SelfInspector, selfID string, run OutputRunner) *BuildChecker {
|
|
return &BuildChecker{cli: cli, selfID: selfID, run: run}
|
|
}
|
|
|
|
// BuiltServices returns the services of project that have a build: section.
|
|
// 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) {
|
|
roots, err := MountRoots(ctx, b.cli, b.selfID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
files, dir, err := stackFiles(roots, configFilesLabel)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if workingDir == "" {
|
|
workingDir = dir
|
|
} else if !insideAny(roots, workingDir) {
|
|
return nil, notMounted(workingDir)
|
|
}
|
|
envs, err := envFiles(roots, envFileLabel)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return b.builtServices(ctx, project, workingDir, files, envs)
|
|
}
|
|
|
|
// builtServices runs `docker compose config --format json` on files that are
|
|
// 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")
|
|
raw, err := b.run(ctx, workingDir, args...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("docker compose config: %w", err)
|
|
}
|
|
var cfg struct {
|
|
Services map[string]struct {
|
|
Build json.RawMessage `json:"build"`
|
|
} `json:"services"`
|
|
}
|
|
if err := json.Unmarshal(raw, &cfg); err != nil {
|
|
return nil, fmt.Errorf("parse docker compose config: %w", err)
|
|
}
|
|
built := make(map[string]bool, len(cfg.Services))
|
|
for name, svc := range cfg.Services {
|
|
built[name] = len(svc.Build) > 0 && string(svc.Build) != "null"
|
|
}
|
|
return built, nil
|
|
}
|
|
|
|
// composeBase returns the leading `docker compose` arguments every call
|
|
// shares. --project-directory is explicit because, with -f, compose would
|
|
// take it from the first file, which differs for stacks started with the
|
|
// flag.
|
|
func composeBase(project, workingDir string, files, envs []string) []string {
|
|
args := []string{"compose", "-p", project, "--project-directory", workingDir}
|
|
for _, e := range envs {
|
|
args = append(args, "--env-file", e)
|
|
}
|
|
for _, f := range files {
|
|
args = append(args, "-f", f)
|
|
}
|
|
return args
|
|
}
|