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 // 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, 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, ",") }