34865c9fd0
Adds the Swarm-mode counterpart to ComposeExecutor: updates a Swarm service's image directly through the Docker SDK (ServiceInspectWithRaw + ServiceUpdate with QueryRegistry=true so floating tags resolve to a fresh digest), gated by the same Refused guard used in Compose mode. Satisfies the existing Executor interface unchanged, so Queue needs no changes.
100 lines
3.3 KiB
Go
100 lines
3.3 KiB
Go
package updater_test
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
|
|
"github.com/docker/docker/api/types/swarm"
|
|
"github.com/shcizo/package-updater/internal/discovery"
|
|
"github.com/shcizo/package-updater/internal/updater"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
type fakeSwarmDockerClient struct {
|
|
inspectService swarm.Service
|
|
inspectErr error
|
|
updateErr error
|
|
|
|
inspectCalled bool
|
|
updateCalled bool
|
|
updateCalledServiceID string
|
|
updateCalledVersion swarm.Version
|
|
updateCalledSpec swarm.ServiceSpec
|
|
updateCalledOpts swarm.ServiceUpdateOptions
|
|
}
|
|
|
|
func (f *fakeSwarmDockerClient) ServiceInspectWithRaw(_ context.Context, _ string, _ swarm.ServiceInspectOptions) (swarm.Service, []byte, error) {
|
|
f.inspectCalled = true
|
|
return f.inspectService, nil, f.inspectErr
|
|
}
|
|
|
|
func (f *fakeSwarmDockerClient) ServiceUpdate(_ context.Context, serviceID string, version swarm.Version, spec swarm.ServiceSpec, opts swarm.ServiceUpdateOptions) (swarm.ServiceUpdateResponse, error) {
|
|
f.updateCalled = true
|
|
f.updateCalledServiceID = serviceID
|
|
f.updateCalledVersion = version
|
|
f.updateCalledSpec = spec
|
|
f.updateCalledOpts = opts
|
|
return swarm.ServiceUpdateResponse{}, f.updateErr
|
|
}
|
|
|
|
func mkInspectService(version uint64, image string) swarm.Service {
|
|
return swarm.Service{
|
|
ID: "svc-myapp",
|
|
Meta: swarm.Meta{Version: swarm.Version{Index: version}},
|
|
Spec: swarm.ServiceSpec{
|
|
Annotations: swarm.Annotations{Name: "myapp_web"},
|
|
TaskTemplate: swarm.TaskSpec{ContainerSpec: &swarm.ContainerSpec{Image: image}},
|
|
},
|
|
}
|
|
}
|
|
|
|
func TestSwarmExecutor_UpdatesImageAndVersion(t *testing.T) {
|
|
fake := &fakeSwarmDockerClient{inspectService: mkInspectService(7, "registry.example.com/myapp:v1")}
|
|
e := updater.NewSwarmExecutor(fake)
|
|
|
|
job := discovery.Job{Service: "myapp_web", ServiceID: "svc-myapp", Image: "registry.example.com/myapp:v2"}
|
|
err := e.Execute(context.Background(), job)
|
|
|
|
require.NoError(t, err)
|
|
require.True(t, fake.updateCalled)
|
|
require.Equal(t, "svc-myapp", fake.updateCalledServiceID)
|
|
require.Equal(t, uint64(7), fake.updateCalledVersion.Index)
|
|
require.Equal(t, "registry.example.com/myapp:v2", fake.updateCalledSpec.TaskTemplate.ContainerSpec.Image)
|
|
require.True(t, fake.updateCalledOpts.QueryRegistry)
|
|
}
|
|
|
|
func TestSwarmExecutor_InspectError(t *testing.T) {
|
|
fake := &fakeSwarmDockerClient{inspectErr: errors.New("service not found")}
|
|
e := updater.NewSwarmExecutor(fake)
|
|
|
|
err := e.Execute(context.Background(), discovery.Job{ServiceID: "svc-missing", Image: "x:v2"})
|
|
|
|
require.Error(t, err)
|
|
require.False(t, fake.updateCalled)
|
|
}
|
|
|
|
func TestSwarmExecutor_UpdateError(t *testing.T) {
|
|
fake := &fakeSwarmDockerClient{
|
|
inspectService: mkInspectService(1, "registry.example.com/myapp:v1"),
|
|
updateErr: errors.New("update rejected"),
|
|
}
|
|
e := updater.NewSwarmExecutor(fake)
|
|
|
|
err := e.Execute(context.Background(), discovery.Job{ServiceID: "svc-myapp", Image: "registry.example.com/myapp:v2"})
|
|
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestSwarmExecutor_RefusedJobIsNotExecuted(t *testing.T) {
|
|
fake := &fakeSwarmDockerClient{}
|
|
e := updater.NewSwarmExecutor(fake)
|
|
job := discovery.Job{ServiceID: "svc-myapp", Image: "x:v2", Refused: true, RefusedReason: "not opted in"}
|
|
|
|
err := e.Execute(context.Background(), job)
|
|
|
|
require.Error(t, err)
|
|
require.False(t, fake.inspectCalled)
|
|
require.False(t, fake.updateCalled)
|
|
}
|