feat(agent): collect compose, swarm and container inventory from docker
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
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
|
||||
}
|
||||
|
||||
func (f *fakeDocker) ContainerList(context.Context, container.ListOptions) ([]container.Summary, error) {
|
||||
return f.containers, f.containerErr
|
||||
}
|
||||
|
||||
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_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)
|
||||
}
|
||||
Reference in New Issue
Block a user