package updater import ( "context" "fmt" "github.com/docker/docker/api/types/swarm" "github.com/shcizo/package-updater/internal/discovery" ) // SwarmDockerClient is the subset of the Docker SDK SwarmExecutor depends // on. Defined as an interface so tests can supply a fake. type SwarmDockerClient interface { ServiceInspectWithRaw(ctx context.Context, serviceID string, opts swarm.ServiceInspectOptions) (swarm.Service, []byte, error) ServiceUpdate(ctx context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, opts swarm.ServiceUpdateOptions) (swarm.ServiceUpdateResponse, error) } // SwarmExecutor updates a Swarm service's image via the Docker API, // equivalent to `docker service update --image`. type SwarmExecutor struct { cli SwarmDockerClient } // NewSwarmExecutor returns an executor that drives Swarm service updates // through the Docker API. func NewSwarmExecutor(cli SwarmDockerClient) *SwarmExecutor { return &SwarmExecutor{cli: cli} } // Execute inspects the service to get its current spec and version (required // by the Docker API as an optimistic-concurrency token), sets the new image // on the container spec, and calls ServiceUpdate. QueryRegistry is set so a // floating tag (e.g. ":latest") resolves to a fresh digest and actually // triggers a rolling update instead of being treated as unchanged. func (e *SwarmExecutor) Execute(ctx context.Context, job discovery.Job) error { if job.Refused { return fmt.Errorf("refused: %s", job.RefusedReason) } svc, _, err := e.cli.ServiceInspectWithRaw(ctx, job.ServiceID, swarm.ServiceInspectOptions{}) if err != nil { return fmt.Errorf("inspect service %s: %w", job.ServiceID, err) } spec := svc.Spec spec.TaskTemplate.ContainerSpec.Image = job.Image if _, err := e.cli.ServiceUpdate(ctx, job.ServiceID, svc.Version, spec, swarm.ServiceUpdateOptions{QueryRegistry: true}); err != nil { return fmt.Errorf("update service %s: %w", job.ServiceID, err) } return nil }