feat(agent): job runner, compose override executor and swarm executor (#7)
This commit is contained in:
@@ -3,14 +3,19 @@ package updater
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/shcizo/package-updater/internal/discovery"
|
||||
"github.com/docker/docker/api/types/system"
|
||||
|
||||
"github.com/shcizo/package-updater/internal/imageref"
|
||||
"github.com/shcizo/package-updater/internal/protocol"
|
||||
)
|
||||
|
||||
// SwarmDockerClient is the subset of the Docker SDK SwarmExecutor depends
|
||||
// on. Defined as an interface so tests can supply a fake.
|
||||
type SwarmDockerClient interface {
|
||||
// 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)
|
||||
}
|
||||
@@ -18,35 +23,71 @@ type SwarmDockerClient interface {
|
||||
// SwarmExecutor updates a Swarm service's image via the Docker API,
|
||||
// equivalent to `docker service update --image`.
|
||||
type SwarmExecutor struct {
|
||||
cli SwarmDockerClient
|
||||
cli SwarmClient
|
||||
resolve *Resolver
|
||||
auth *RegistryAuth
|
||||
}
|
||||
|
||||
// NewSwarmExecutor returns an executor that drives Swarm service updates
|
||||
// through the Docker API.
|
||||
func NewSwarmExecutor(cli SwarmDockerClient) *SwarmExecutor {
|
||||
return &SwarmExecutor{cli: cli}
|
||||
// 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 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)
|
||||
// Execute checks that the local daemon is a manager of the job's cluster,
|
||||
// pins job.Ref to its current digest 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.ServiceID, swarm.ServiceInspectOptions{})
|
||||
svc, _, err := e.cli.ServiceInspectWithRaw(ctx, job.SwarmService, swarm.ServiceInspectOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("inspect service %s: %w", job.ServiceID, err)
|
||||
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.Fprintf(out, "resolved %s\n", pinned)
|
||||
|
||||
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)
|
||||
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
|
||||
}
|
||||
return nil
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user