94 lines
3.4 KiB
Go
94 lines
3.4 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"
|
|
"github.com/shcizo/package-updater/internal/protocol"
|
|
)
|
|
|
|
// 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 protocol.JobStart, 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
|
|
}
|