package updater import ( "bytes" "context" "errors" "testing" "github.com/docker/docker/api/types/registry" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/api/types/system" "github.com/opencontainers/go-digest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/shcizo/package-updater/internal/protocol" ) type fakeSwarm struct { info system.Info svc swarm.Service inspectErr error updateErr error warnings []string inspectCalls int updateCalls int updID string updVersion swarm.Version updSpec swarm.ServiceSpec updOpts swarm.ServiceUpdateOptions } func (f *fakeSwarm) Info(context.Context) (system.Info, error) { return f.info, nil } func (f *fakeSwarm) ServiceInspectWithRaw(context.Context, string, swarm.ServiceInspectOptions) (swarm.Service, []byte, error) { f.inspectCalls++ return f.svc, nil, f.inspectErr } func (f *fakeSwarm) ServiceUpdate(_ context.Context, id string, v swarm.Version, spec swarm.ServiceSpec, opts swarm.ServiceUpdateOptions) (swarm.ServiceUpdateResponse, error) { f.updateCalls++ f.updID, f.updVersion, f.updSpec, f.updOpts = id, v, spec, opts return swarm.ServiceUpdateResponse{Warnings: f.warnings}, f.updateErr } type fakeDist struct { digest string err error refs []string auths []string } func (f *fakeDist) DistributionInspect(_ context.Context, ref, auth string) (registry.DistributionInspect, error) { f.refs = append(f.refs, ref) f.auths = append(f.auths, auth) var d registry.DistributionInspect d.Descriptor.Digest = digest.Digest(f.digest) return d, f.err } func managerOf(cluster string) system.Info { var info system.Info info.Swarm.ControlAvailable = true info.Swarm.Cluster = &swarm.ClusterInfo{ID: cluster} return info } func swarmSvc(name string) swarm.Service { return swarm.Service{ ID: "svc-id", Meta: swarm.Meta{Version: swarm.Version{Index: 7}}, Spec: swarm.ServiceSpec{ Annotations: swarm.Annotations{Name: name}, TaskTemplate: swarm.TaskSpec{ContainerSpec: &swarm.ContainerSpec{Image: "gitea/gitea:1.26"}}, }, } } func TestSwarmUsesPinnedRef(t *testing.T) { const ref = "gitea/gitea:1.26@sha256:abababababababababababababababababababababababababababababababab" job := Job{JobStart: protocol.JobStart{Kind: protocol.KindSwarm, SwarmClusterID: "c1", SwarmService: "gitea_gitea", Ref: ref}} fake := &fakeSwarm{info: managerOf("c1"), svc: swarmSvc("gitea_gitea")} dist := &fakeDist{digest: "sha256:other"} var out bytes.Buffer got, err := NewSwarmExecutor(fake, NewResolver(dist, nil), nil).Execute(context.Background(), job, &out) require.NoError(t, err) assert.Equal(t, ref, got) assert.Equal(t, ref, fake.updSpec.TaskTemplate.ContainerSpec.Image) assert.Empty(t, dist.refs, "no registry lookup") assert.Contains(t, out.String(), "pinned "+ref) } func TestSwarmExecutor(t *testing.T) { job := Job{JobStart: protocol.JobStart{Kind: protocol.KindSwarm, SwarmClusterID: "c1", SwarmService: "gitea_gitea", Ref: "gitea/gitea:1.27"}} workerInfo := managerOf("c1") workerInfo.Swarm.ControlAvailable = false noContainer := swarmSvc("gitea_gitea") noContainer.Spec.TaskTemplate.ContainerSpec = nil cases := []struct { name string fake *fakeSwarm auth *RegistryAuth wantErr string wantUpdate bool wantEncAuth bool }{ {name: "happy path, no auth", fake: &fakeSwarm{info: managerOf("c1"), svc: swarmSvc("gitea_gitea")}, wantUpdate: true}, {name: "happy path, auth known", fake: &fakeSwarm{info: managerOf("c1"), svc: swarmSvc("gitea_gitea"), warnings: []string{"w1"}}, auth: &RegistryAuth{byHost: map[string]string{"docker.io": "enc"}}, wantUpdate: true, wantEncAuth: true}, {name: "cluster mismatch", fake: &fakeSwarm{info: managerOf("other"), svc: swarmSvc("gitea_gitea")}, wantErr: "refused: job is for swarm cluster c1, this daemon is in other"}, {name: "worker node", fake: &fakeSwarm{info: workerInfo, svc: swarmSvc("gitea_gitea")}, wantErr: "not a swarm manager"}, {name: "other service returned", fake: &fakeSwarm{info: managerOf("c1"), svc: swarmSvc("gitea_other")}, wantErr: "daemon returned service"}, {name: "no container spec", fake: &fakeSwarm{info: managerOf("c1"), svc: noContainer}, wantErr: "no container spec"}, {name: "inspect fails", fake: &fakeSwarm{info: managerOf("c1"), inspectErr: errors.New("gone")}, wantErr: "gone"}, {name: "update fails", fake: &fakeSwarm{info: managerOf("c1"), svc: swarmSvc("gitea_gitea"), updateErr: errors.New("rejected")}, wantErr: "rejected", wantUpdate: true}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { dist := &fakeDist{digest: "sha256:" + string(bytes.Repeat([]byte("a"), 64))} e := NewSwarmExecutor(c.fake, NewResolver(dist, c.auth), c.auth) var out bytes.Buffer ref, err := e.Execute(context.Background(), job, &out) if c.wantErr != "" { assert.ErrorContains(t, err, c.wantErr) } else { require.NoError(t, err) assert.Equal(t, "gitea/gitea:1.27@"+dist.digest, ref) assert.Contains(t, out.String(), "resolved "+ref) } if c.wantErr == "refused: job is for swarm cluster c1, this daemon is in other" || c.wantErr == "not a swarm manager" { assert.Zero(t, c.fake.inspectCalls) } if !c.wantUpdate { assert.Zero(t, c.fake.updateCalls) return } assert.Equal(t, 1, c.fake.updateCalls) assert.Equal(t, "svc-id", c.fake.updID) assert.Equal(t, uint64(7), c.fake.updVersion.Index) assert.Equal(t, "gitea/gitea:1.27@"+dist.digest, c.fake.updSpec.TaskTemplate.ContainerSpec.Image) assert.True(t, c.fake.updOpts.QueryRegistry) if c.wantEncAuth { assert.Equal(t, "enc", c.fake.updOpts.EncodedRegistryAuth) assert.Empty(t, c.fake.updOpts.RegistryAuthFrom) assert.Contains(t, out.String(), "warning: w1") } else { assert.Empty(t, c.fake.updOpts.EncodedRegistryAuth) assert.Equal(t, swarm.RegistryAuthFromSpec, c.fake.updOpts.RegistryAuthFrom) } }) } }