Files

93 lines
3.3 KiB
Go

package updater
import (
"context"
"fmt"
"io"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/api/types/system"
"github.com/shcizo/package-updater/internal/imageref"
)
// SwarmClient is the subset of the Docker SDK SwarmExecutor depends on.
// Defined as an interface so tests can supply a fake.
type SwarmClient interface {
Info(ctx context.Context) (system.Info, error)
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 SwarmClient
resolve *Resolver
auth *RegistryAuth
}
// NewSwarmExecutor returns an executor that drives Swarm service updates
// through the Docker API. auth may be nil (anonymous registry access).
func NewSwarmExecutor(cli SwarmClient, resolve *Resolver, auth *RegistryAuth) *SwarmExecutor {
return &SwarmExecutor{cli: cli, resolve: resolve, auth: auth}
}
// Execute checks that the local daemon is a manager of the job's cluster,
// pins job.Ref to its current digest (a ref that has one is used as is) and
// updates the service to it. The update is addressed by the ID and version of
// the service that was inspected, so it changes exactly that object. Success
// means the daemon accepted the new spec, not that the tasks have converged.
// It returns the pinned ref.
func (e *SwarmExecutor) Execute(ctx context.Context, job Job, out io.Writer) (string, error) {
info, err := e.cli.Info(ctx)
if err != nil {
return "", fmt.Errorf("docker info: %w", err)
}
if !info.Swarm.ControlAvailable {
return "", fmt.Errorf("refused: this agent's daemon is not a swarm manager")
}
actual := ""
if info.Swarm.Cluster != nil {
actual = info.Swarm.Cluster.ID
}
if actual != job.SwarmClusterID {
return "", fmt.Errorf("refused: job is for swarm cluster %s, this daemon is in %s", job.SwarmClusterID, actual)
}
svc, _, err := e.cli.ServiceInspectWithRaw(ctx, job.SwarmService, swarm.ServiceInspectOptions{})
if err != nil {
return "", fmt.Errorf("inspect service %s: %w", job.SwarmService, err)
}
if svc.Spec.Name != job.SwarmService {
return "", fmt.Errorf("inspect service %s: daemon returned service %q", job.SwarmService, svc.Spec.Name)
}
if svc.Spec.TaskTemplate.ContainerSpec == nil {
return "", fmt.Errorf("service %s has no container spec", job.SwarmService)
}
pinned, err := e.resolve.Resolve(ctx, job.Ref)
if err != nil {
return "", err
}
fmt.Fprintln(out, resolveNote(job.Ref, pinned))
spec := svc.Spec
spec.TaskTemplate.ContainerSpec.Image = pinned
opts := swarm.ServiceUpdateOptions{QueryRegistry: true}
if enc := e.auth.For(imageref.Parse(job.Ref).Repo); enc != "" {
opts.EncodedRegistryAuth = enc
} else {
opts.RegistryAuthFrom = swarm.RegistryAuthFromSpec
}
resp, err := e.cli.ServiceUpdate(ctx, svc.ID, svc.Version, spec, opts)
if err != nil {
return "", fmt.Errorf("update service %s: %w", job.SwarmService, err)
}
for _, w := range resp.Warnings {
fmt.Fprintf(out, "warning: %s\n", w)
}
fmt.Fprintf(out, "service %s updated\n", job.SwarmService)
return pinned, nil
}