97 lines
2.5 KiB
Go
97 lines
2.5 KiB
Go
package discovery
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/docker/docker/api/types/container"
|
|
)
|
|
|
|
// Job describes a single (project, service, config_files) update to execute.
|
|
type Job struct {
|
|
Project string
|
|
Service string
|
|
WorkingDir string
|
|
ConfigFiles []string
|
|
// Image is the full image reference the update request asked for.
|
|
// Compose jobs ignore it (compose.yml already pins the reference to
|
|
// pull); Swarm jobs need it to know what to set on the service spec.
|
|
Image string
|
|
// ServiceID is the Swarm service ID. Empty for Compose jobs.
|
|
ServiceID string
|
|
// Refused is true when the WorkingDir falls outside STACKS_ROOT.
|
|
// The job is returned so the caller can surface a per-job "refused"
|
|
// result, but it MUST NOT be executed.
|
|
Refused bool
|
|
RefusedReason string
|
|
}
|
|
|
|
// Discovery orchestrates "given an image, what jobs should we enqueue?".
|
|
type Discovery struct {
|
|
cli DockerClient
|
|
stacksRoot string
|
|
optInLabel string
|
|
}
|
|
|
|
// New returns a Discovery bound to the given Docker client and config.
|
|
func New(cli DockerClient, stacksRoot, optInLabel string) *Discovery {
|
|
return &Discovery{cli: cli, stacksRoot: stacksRoot, optInLabel: optInLabel}
|
|
}
|
|
|
|
// FindJobs lists running containers, filters by image match + opt-in label,
|
|
// extracts Compose info, applies the path safety check, and deduplicates.
|
|
func (d *Discovery) FindJobs(ctx context.Context, image string) ([]Job, error) {
|
|
all, err := d.cli.ContainerList(ctx, container.ListOptions{All: true})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("docker container list: %w", err)
|
|
}
|
|
|
|
seen := make(map[string]struct{})
|
|
var jobs []Job
|
|
|
|
for _, c := range all {
|
|
if !ImagesMatch(image, c.Image) {
|
|
continue
|
|
}
|
|
if !HasOptIn(c.Labels, d.optInLabel) {
|
|
continue
|
|
}
|
|
cl, err := ParseComposeLabels(c.Labels)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
refused := !IsInsideRoot(d.stacksRoot, cl.WorkingDir)
|
|
reason := ""
|
|
if refused {
|
|
reason = fmt.Sprintf("working_dir %q outside STACKS_ROOT %q", cl.WorkingDir, d.stacksRoot)
|
|
}
|
|
|
|
key := dedupKey(cl)
|
|
if _, dup := seen[key]; dup {
|
|
continue
|
|
}
|
|
seen[key] = struct{}{}
|
|
|
|
jobs = append(jobs, Job{
|
|
Project: cl.Project,
|
|
Service: cl.Service,
|
|
WorkingDir: cl.WorkingDir,
|
|
ConfigFiles: cl.ConfigFiles,
|
|
Image: image,
|
|
Refused: refused,
|
|
RefusedReason: reason,
|
|
})
|
|
}
|
|
|
|
return jobs, nil
|
|
}
|
|
|
|
func dedupKey(cl ComposeLabels) string {
|
|
files := append([]string(nil), cl.ConfigFiles...)
|
|
sort.Strings(files)
|
|
return cl.Project + "|" + cl.Service + "|" + strings.Join(files, ",")
|
|
}
|