fix: address review findings for self-update (#15)
This commit is contained in:
@@ -22,12 +22,14 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
labelProject = "com.docker.compose.project"
|
||||
labelService = "com.docker.compose.service"
|
||||
labelWorkingDir = "com.docker.compose.project.working_dir"
|
||||
labelConfigFiles = "com.docker.compose.project.config_files"
|
||||
labelEnvFile = "com.docker.compose.project.environment_file"
|
||||
labelSwarmTask = "com.docker.swarm.service.id"
|
||||
labelProject = "com.docker.compose.project"
|
||||
labelService = "com.docker.compose.service"
|
||||
labelWorkingDir = "com.docker.compose.project.working_dir"
|
||||
labelConfigFiles = "com.docker.compose.project.config_files"
|
||||
labelEnvFile = "com.docker.compose.project.environment_file"
|
||||
labelSwarmTask = "com.docker.swarm.service.id"
|
||||
labelSwarmName = "com.docker.swarm.service.name"
|
||||
labelSelfUpdateJob = "se.shcizo.package-updater.self-update.job"
|
||||
)
|
||||
|
||||
// DockerClient is the subset of the Docker SDK the collector needs.
|
||||
@@ -61,6 +63,7 @@ type Collector struct {
|
||||
now func() time.Time
|
||||
|
||||
mu sync.Mutex
|
||||
self *selfInfo // the agent's own container, once inspected
|
||||
cache map[string]buildEntry // by Compose project
|
||||
warned map[string]bool // logged warnings: "<project>" (check failed) and "<project>/<service>" (service not in config)
|
||||
}
|
||||
@@ -77,8 +80,15 @@ type buildEntry struct {
|
||||
// stackLabels are the Compose labels a build check needs.
|
||||
type stackLabels struct{ workingDir, configFiles, envFile string }
|
||||
|
||||
// New returns a Collector. selfID is a prefix of the agent's own container
|
||||
// ID (inside a container, the hostname); that container is marked Self.
|
||||
// selfInfo is what the inspect of the agent's own container tells.
|
||||
type selfInfo struct {
|
||||
id string // full container ID
|
||||
swarmService string // name of the Swarm service the container is a task of, if any
|
||||
}
|
||||
|
||||
// New returns a Collector. selfID is the agent's own container: a prefix of
|
||||
// its ID (inside a container, the hostname) or its name. That container is
|
||||
// marked Self, and so is the Swarm service it is a task of.
|
||||
func New(cli DockerClient, selfID string) *Collector {
|
||||
return &Collector{cli: cli, selfID: selfID, now: time.Now, cache: map[string]buildEntry{}, warned: map[string]bool{}}
|
||||
}
|
||||
@@ -92,6 +102,29 @@ func (c *Collector) WithBuildCheck(check BuildCheck, log *slog.Logger) *Collecto
|
||||
return c
|
||||
}
|
||||
|
||||
// resolveSelf inspects the agent's own container once and remembers the
|
||||
// answer. It returns nil while that fails (or selfID is empty), and the
|
||||
// caller then compares container IDs with the selfID prefix.
|
||||
func (c *Collector) resolveSelf(ctx context.Context) *selfInfo {
|
||||
if c.selfID == "" {
|
||||
return nil
|
||||
}
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.self != nil {
|
||||
return c.self
|
||||
}
|
||||
insp, err := c.cli.ContainerInspect(ctx, c.selfID)
|
||||
if err != nil || insp.ContainerJSONBase == nil || insp.ID == "" {
|
||||
return nil
|
||||
}
|
||||
c.self = &selfInfo{id: insp.ID}
|
||||
if insp.Config != nil {
|
||||
c.self.swarmService = insp.Config.Labels[labelSwarmName]
|
||||
}
|
||||
return c.self
|
||||
}
|
||||
|
||||
// Hello returns the machine facts sent on connect. AgentVersion is left for
|
||||
// the caller to fill in.
|
||||
func (c *Collector) Hello(ctx context.Context) (protocol.Hello, error) {
|
||||
@@ -125,20 +158,21 @@ func (c *Collector) Snapshot(ctx context.Context) ([]protocol.Service, error) {
|
||||
repoDigests[img.ID] = img.RepoDigests
|
||||
}
|
||||
|
||||
self := c.resolveSelf(ctx)
|
||||
byKey := make(map[string]protocol.Service)
|
||||
stacks := make(map[string]stackLabels)
|
||||
for _, ct := range containers {
|
||||
if _, isTask := ct.Labels[labelSwarmTask]; isTask {
|
||||
continue
|
||||
}
|
||||
if _, isHelper := ct.Labels[protocol.LabelSelfUpdateJob]; isHelper {
|
||||
if _, isHelper := ct.Labels[labelSelfUpdateJob]; isHelper {
|
||||
continue
|
||||
}
|
||||
ref, err := c.imageRef(ctx, ct)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
svc := c.fromContainer(ct, ref, repoDigests[ct.ImageID])
|
||||
svc := c.fromContainer(ct, ref, repoDigests[ct.ImageID], self)
|
||||
if svc.Kind == protocol.KindCompose {
|
||||
if _, ok := stacks[svc.ComposeProject]; !ok {
|
||||
stacks[svc.ComposeProject] = stackLabels{svc.WorkingDir, svc.ConfigFiles, ct.Labels[labelEnvFile]}
|
||||
@@ -162,6 +196,7 @@ func (c *Collector) Snapshot(ctx context.Context) ([]protocol.Service, error) {
|
||||
continue
|
||||
}
|
||||
svc := fromSwarmService(s)
|
||||
svc.Self = self != nil && self.swarmService != "" && svc.SwarmService == self.swarmService
|
||||
byKey[svc.Key()] = svc
|
||||
}
|
||||
}
|
||||
@@ -192,12 +227,12 @@ func (c *Collector) imageRef(ctx context.Context, ct container.Summary) (string,
|
||||
return insp.Config.Image, nil
|
||||
}
|
||||
|
||||
func (c *Collector) fromContainer(ct container.Summary, ref string, repoDigests []string) protocol.Service {
|
||||
func (c *Collector) fromContainer(ct container.Summary, ref string, repoDigests []string, self *selfInfo) protocol.Service {
|
||||
svc := protocol.Service{
|
||||
Image: ref,
|
||||
Digest: digestFor(ref, repoDigests),
|
||||
Status: containerStatus(ct.State),
|
||||
Self: c.selfID != "" && strings.HasPrefix(ct.ID, c.selfID),
|
||||
Self: c.isSelf(ct.ID, self),
|
||||
}
|
||||
project, service := ct.Labels[labelProject], ct.Labels[labelService]
|
||||
if project != "" && service != "" {
|
||||
@@ -216,6 +251,15 @@ func (c *Collector) fromContainer(ct container.Summary, ref string, repoDigests
|
||||
return svc
|
||||
}
|
||||
|
||||
// isSelf reports whether id is the agent's own container: by the inspected
|
||||
// ID when there is one, else by the selfID prefix.
|
||||
func (c *Collector) isSelf(id string, self *selfInfo) bool {
|
||||
if self != nil {
|
||||
return id == self.id
|
||||
}
|
||||
return c.selfID != "" && strings.HasPrefix(id, c.selfID)
|
||||
}
|
||||
|
||||
func fromSwarmService(s swarm.Service) protocol.Service {
|
||||
ref := s.Spec.TaskTemplate.ContainerSpec.Image
|
||||
return protocol.Service{
|
||||
|
||||
@@ -138,7 +138,7 @@ func TestSnapshot_PlainContainerAndSwarmTaskSkipped(t *testing.T) {
|
||||
func TestSnapshot_SelfUpdateHelperSkipped(t *testing.T) {
|
||||
cli := &fakeDocker{containers: []container.Summary{
|
||||
{ID: "h1", Names: []string{"/quirky_helper"}, Image: "sha256:agentimage", State: "running",
|
||||
Labels: map[string]string{protocol.LabelSelfUpdateJob: "7"}},
|
||||
Labels: map[string]string{labelSelfUpdateJob: "7"}},
|
||||
{ID: "p1", Names: []string{"/adhoc"}, Image: "redis:alpine", State: "exited"},
|
||||
}}
|
||||
got, err := New(cli, "").Snapshot(context.Background())
|
||||
@@ -158,6 +158,63 @@ func TestSnapshot_SelfFlag(t *testing.T) {
|
||||
assert.True(t, got[0].Self)
|
||||
}
|
||||
|
||||
func TestSnapshot_SelfFlagByContainerName(t *testing.T) {
|
||||
cli := &fakeDocker{
|
||||
containers: []container.Summary{
|
||||
composeContainer("0123456789abcdef", "updater-agent", "agent", "pu:1", "sha256:i", "running"),
|
||||
composeContainer("fedcba9876543210", "slusk", "web", "slusk:1", "sha256:j", "running"),
|
||||
},
|
||||
inspect: map[string]container.InspectResponse{
|
||||
"updater-agent-agent-1": {ContainerJSONBase: &container.ContainerJSONBase{ID: "0123456789abcdef"}},
|
||||
},
|
||||
}
|
||||
got, err := New(cli, "updater-agent-agent-1").Snapshot(context.Background())
|
||||
require.NoError(t, err)
|
||||
require.Len(t, got, 2)
|
||||
assert.False(t, got[0].Self, "slusk")
|
||||
assert.True(t, got[1].Self, "updater-agent")
|
||||
}
|
||||
|
||||
func TestSnapshot_SelfFlagFallsBackToIDPrefixWhenInspectFails(t *testing.T) {
|
||||
cli := &fakeDocker{
|
||||
containers: []container.Summary{composeContainer("0123456789abcdef", "updater-agent", "agent", "pu:1", "sha256:i", "running")},
|
||||
inspectErr: errors.New("boom"),
|
||||
}
|
||||
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_AgentsOwnSwarmServiceIsSelf(t *testing.T) {
|
||||
swarmSvc := func(name string) swarm.Service {
|
||||
return swarm.Service{
|
||||
Spec: swarm.ServiceSpec{
|
||||
Annotations: swarm.Annotations{Name: name},
|
||||
TaskTemplate: swarm.TaskSpec{ContainerSpec: &swarm.ContainerSpec{Image: "pu:1"}},
|
||||
},
|
||||
ServiceStatus: &swarm.ServiceStatus{RunningTasks: 1, DesiredTasks: 1},
|
||||
}
|
||||
}
|
||||
cli := &fakeDocker{
|
||||
info: managerInfo(),
|
||||
services: []swarm.Service{swarmSvc("pu_agent"), swarmSvc("pu_master")},
|
||||
inspect: map[string]container.InspectResponse{
|
||||
"abc123": {
|
||||
ContainerJSONBase: &container.ContainerJSONBase{ID: "abc123full"},
|
||||
Config: &container.Config{Labels: map[string]string{labelSwarmName: "pu_agent"}},
|
||||
},
|
||||
},
|
||||
}
|
||||
got, err := New(cli, "abc123").Snapshot(context.Background())
|
||||
require.NoError(t, err)
|
||||
require.Len(t, got, 2)
|
||||
assert.Equal(t, "pu_agent", got[0].SwarmService)
|
||||
assert.True(t, got[0].Self)
|
||||
assert.Equal(t, "pu_master", got[1].SwarmService)
|
||||
assert.False(t, got[1].Self)
|
||||
}
|
||||
|
||||
func TestSnapshot_SwarmServicesOnlyOnManager(t *testing.T) {
|
||||
svc := swarm.Service{
|
||||
Spec: swarm.ServiceSpec{
|
||||
|
||||
Reference in New Issue
Block a user