package inventory import ( "context" "errors" "testing" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/image" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/api/types/system" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/shcizo/package-updater/internal/protocol" ) type fakeDocker struct { containers []container.Summary images []image.Summary services []swarm.Service info system.Info containerErr error serviceListCalls int inspect map[string]container.InspectResponse inspectErr error } func (f *fakeDocker) ContainerList(context.Context, container.ListOptions) ([]container.Summary, error) { return f.containers, f.containerErr } func (f *fakeDocker) ContainerInspect(_ context.Context, id string) (container.InspectResponse, error) { return f.inspect[id], f.inspectErr } func (f *fakeDocker) ImageList(context.Context, image.ListOptions) ([]image.Summary, error) { return f.images, nil } func (f *fakeDocker) ServiceList(context.Context, swarm.ServiceListOptions) ([]swarm.Service, error) { f.serviceListCalls++ return f.services, nil } func (f *fakeDocker) Info(context.Context) (system.Info, error) { return f.info, nil } func composeContainer(id, project, service, img, imageID, state string) container.Summary { return container.Summary{ ID: id, Image: img, ImageID: imageID, State: state, Names: []string{"/" + project + "-" + service + "-1"}, Labels: map[string]string{ labelProject: project, labelService: service, labelWorkingDir: "/home/shcizo/self-hosted/" + project, }, } } func managerInfo() system.Info { var info system.Info info.Name = "swarm-mgr" info.Swarm.ControlAvailable = true info.Swarm.Cluster = &swarm.ClusterInfo{ID: "cluster-1"} return info } func TestSnapshot_ComposeContainerWithRepoDigest(t *testing.T) { cli := &fakeDocker{ containers: []container.Summary{composeContainer("abc123", "slusk", "web", "gitea.shcizo.se/shcizo/slusk:1.4.2", "sha256:img1", "running")}, images: []image.Summary{{ID: "sha256:img1", RepoDigests: []string{"gitea.shcizo.se/shcizo/slusk@sha256:d1"}}}, } got, err := New(cli, "").Snapshot(context.Background()) require.NoError(t, err) assert.Equal(t, []protocol.Service{{ Kind: protocol.KindCompose, ComposeProject: "slusk", ComposeService: "web", WorkingDir: "/home/shcizo/self-hosted/slusk", Image: "gitea.shcizo.se/shcizo/slusk:1.4.2", Digest: "sha256:d1", Status: "running", }}, got) } func TestSnapshot_MovedTagUsesConfiguredReference(t *testing.T) { ct := composeContainer("abc123", "slusk", "web", "sha256:img1", "sha256:img1", "running") cli := &fakeDocker{ containers: []container.Summary{ct}, images: []image.Summary{{ID: "sha256:img1", RepoDigests: []string{"gitea.shcizo.se/shcizo/slusk@sha256:d1"}}}, inspect: map[string]container.InspectResponse{ "abc123": {Config: &container.Config{Image: "gitea.shcizo.se/shcizo/slusk:1.4.2"}}, }, } got, err := New(cli, "").Snapshot(context.Background()) require.NoError(t, err) require.Len(t, got, 1) assert.Equal(t, "gitea.shcizo.se/shcizo/slusk:1.4.2", got[0].Image) assert.Equal(t, "sha256:d1", got[0].Digest) } func TestSnapshot_InspectErrorPropagates(t *testing.T) { cli := &fakeDocker{ containers: []container.Summary{composeContainer("abc123", "slusk", "web", "sha256:img1", "sha256:img1", "running")}, inspectErr: errors.New("gone"), } _, err := New(cli, "").Snapshot(context.Background()) require.Error(t, err) assert.Contains(t, err.Error(), "docker container inspect") } func TestSnapshot_ReplicasMergeAndRunningWins(t *testing.T) { cli := &fakeDocker{containers: []container.Summary{ composeContainer("a1", "slusk", "worker", "slusk:1", "sha256:i", "exited"), composeContainer("a2", "slusk", "worker", "slusk:1", "sha256:i", "running"), }} got, err := New(cli, "").Snapshot(context.Background()) require.NoError(t, err) require.Len(t, got, 1) assert.Equal(t, "running", got[0].Status) } func TestSnapshot_PlainContainerAndSwarmTaskSkipped(t *testing.T) { cli := &fakeDocker{containers: []container.Summary{ {ID: "p1", Names: []string{"/adhoc"}, Image: "redis:alpine", State: "exited"}, {ID: "t1", Names: []string{"/gitea_gitea.1.x"}, Image: "gitea/gitea:1.27.3", State: "running", Labels: map[string]string{labelSwarmTask: "svc1"}}, }} got, err := New(cli, "").Snapshot(context.Background()) require.NoError(t, err) assert.Equal(t, []protocol.Service{{ Kind: protocol.KindContainer, ContainerName: "adhoc", Image: "redis:alpine", Status: "stopped", }}, got) } func TestSnapshot_SelfFlag(t *testing.T) { cli := &fakeDocker{containers: []container.Summary{ composeContainer("0123456789abcdef", "updater-agent", "agent", "pu:1", "sha256:i", "running"), }} got, err := New(cli, "0123456789ab").Snapshot(context.Background()) require.NoError(t, err) require.Len(t, got, 1) assert.True(t, got[0].Self) } func TestSnapshot_SwarmServicesOnlyOnManager(t *testing.T) { svc := swarm.Service{ Spec: swarm.ServiceSpec{ Annotations: swarm.Annotations{Name: "gitea_gitea"}, TaskTemplate: swarm.TaskSpec{ContainerSpec: &swarm.ContainerSpec{Image: "gitea/gitea:1.27.3@sha256:87a6"}}, }, ServiceStatus: &swarm.ServiceStatus{RunningTasks: 1, DesiredTasks: 1}, } worker := &fakeDocker{services: []swarm.Service{svc}} got, err := New(worker, "").Snapshot(context.Background()) require.NoError(t, err) assert.Empty(t, got) assert.Equal(t, 0, worker.serviceListCalls, "workers must not call the manager-only API") manager := &fakeDocker{services: []swarm.Service{svc}, info: managerInfo()} got, err = New(manager, "").Snapshot(context.Background()) require.NoError(t, err) assert.Equal(t, []protocol.Service{{ Kind: protocol.KindSwarm, SwarmService: "gitea_gitea", Image: "gitea/gitea:1.27.3@sha256:87a6", Digest: "sha256:87a6", Status: "running", }}, got) } func TestSwarmStatus(t *testing.T) { cases := []struct { name string in *swarm.ServiceStatus want string }{ {"no status", nil, "unknown"}, {"scaled to zero", &swarm.ServiceStatus{DesiredTasks: 0}, "stopped"}, {"all running", &swarm.ServiceStatus{RunningTasks: 2, DesiredTasks: 2}, "running"}, {"some running", &swarm.ServiceStatus{RunningTasks: 1, DesiredTasks: 2}, "degraded"}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { assert.Equal(t, c.want, swarmStatus(c.in)) }) } } func TestSnapshot_SortedByKey(t *testing.T) { cli := &fakeDocker{containers: []container.Summary{ {ID: "2", Names: []string{"/zeta"}, Image: "z", State: "running"}, {ID: "1", Names: []string{"/alpha"}, Image: "a", State: "running"}, }} got, err := New(cli, "").Snapshot(context.Background()) require.NoError(t, err) require.Len(t, got, 2) assert.Equal(t, "alpha", got[0].ContainerName) } func TestSnapshot_PropagatesDockerErrors(t *testing.T) { cli := &fakeDocker{containerErr: errors.New("daemon down")} _, err := New(cli, "").Snapshot(context.Background()) assert.ErrorContains(t, err, "daemon down") } func TestHello(t *testing.T) { got, err := New(&fakeDocker{info: managerInfo()}, "").Hello(context.Background()) require.NoError(t, err) assert.Equal(t, protocol.Hello{Hostname: "swarm-mgr", SwarmManager: true, SwarmClusterID: "cluster-1"}, got) }