fix: address final review findings for master/agent phase 1 (#6)
- inventory: resolve the image reference via ContainerInspect when a moved tag makes ContainerList report the image ID - store: allow reusing a revoked agent's name (partial unique index) and mark the revoked agent's compose/container services removed - web: publish a services event on revoke; no-store on the one-time token page - webauth: reject empty OIDC state; accept the CSRF form token from the body only - agent conn: bound the WebSocket handshake to 30s - hub: log agent_replaced when a connection replaces an older one
This commit is contained in:
@@ -22,6 +22,10 @@ import (
|
||||
// is too small for the master's future job messages; matches the master.
|
||||
const readLimit = 1 << 20
|
||||
|
||||
// dialTimeout bounds the TLS + WebSocket handshake so a master that accepts
|
||||
// TCP but never answers does not hang the agent forever.
|
||||
const dialTimeout = 30 * time.Second
|
||||
|
||||
// Source supplies what the agent reports. *inventory.Collector satisfies it.
|
||||
type Source interface {
|
||||
Hello(ctx context.Context) (protocol.Hello, error)
|
||||
@@ -92,10 +96,14 @@ func (a *Agent) Run(ctx context.Context) error {
|
||||
// hello was delivered, so Run resets its backoff only after real progress.
|
||||
func (a *Agent) session(parent context.Context) (connected bool, err error) {
|
||||
httpClient := &http.Client{Transport: &http.Transport{TLSClientConfig: pin.ClientConfig(a.cfg.Fingerprint)}}
|
||||
c, _, err := websocket.Dial(parent, a.cfg.URL, &websocket.DialOptions{
|
||||
// The dial context only bounds the handshake; coder/websocket does not
|
||||
// tie the established connection to it, so cancelling afterwards is safe.
|
||||
dialCtx, cancelDial := context.WithTimeout(parent, dialTimeout)
|
||||
c, _, err := websocket.Dial(dialCtx, a.cfg.URL, &websocket.DialOptions{
|
||||
HTTPClient: httpClient,
|
||||
HTTPHeader: http.Header{"Authorization": []string{"Bearer " + a.cfg.Token}},
|
||||
})
|
||||
cancelDial()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("dial master: %w", err)
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ const (
|
||||
// *client.Client satisfies it.
|
||||
type DockerClient interface {
|
||||
ContainerList(ctx context.Context, opts container.ListOptions) ([]container.Summary, error)
|
||||
ContainerInspect(ctx context.Context, containerID string) (container.InspectResponse, error)
|
||||
ImageList(ctx context.Context, opts image.ListOptions) ([]image.Summary, error)
|
||||
ServiceList(ctx context.Context, opts swarm.ServiceListOptions) ([]swarm.Service, error)
|
||||
Info(ctx context.Context) (system.Info, error)
|
||||
@@ -83,7 +84,11 @@ func (c *Collector) Snapshot(ctx context.Context) ([]protocol.Service, error) {
|
||||
if _, isTask := ct.Labels[labelSwarmTask]; isTask {
|
||||
continue
|
||||
}
|
||||
svc := c.fromContainer(ct, repoDigests[ct.ImageID])
|
||||
ref, err := c.imageRef(ctx, ct)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
svc := c.fromContainer(ct, ref, repoDigests[ct.ImageID])
|
||||
if prev, ok := byKey[svc.Key()]; ok {
|
||||
svc = mergeReplicas(prev, svc)
|
||||
}
|
||||
@@ -112,10 +117,28 @@ func (c *Collector) Snapshot(ctx context.Context) ([]protocol.Service, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *Collector) fromContainer(ct container.Summary, repoDigests []string) protocol.Service {
|
||||
// imageRef returns the image reference the container was created from.
|
||||
// When the tag has since moved to another image, ContainerList reports the
|
||||
// image ID ("sha256:...") instead, so the reference is read from the
|
||||
// container's config.
|
||||
func (c *Collector) imageRef(ctx context.Context, ct container.Summary) (string, error) {
|
||||
if !strings.HasPrefix(ct.Image, "sha256:") {
|
||||
return ct.Image, nil
|
||||
}
|
||||
insp, err := c.cli.ContainerInspect(ctx, ct.ID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("docker container inspect %s: %w", ct.ID, err)
|
||||
}
|
||||
if insp.Config == nil {
|
||||
return ct.Image, nil
|
||||
}
|
||||
return insp.Config.Image, nil
|
||||
}
|
||||
|
||||
func (c *Collector) fromContainer(ct container.Summary, ref string, repoDigests []string) protocol.Service {
|
||||
svc := protocol.Service{
|
||||
Image: ct.Image,
|
||||
Digest: digestFor(ct.Image, repoDigests),
|
||||
Image: ref,
|
||||
Digest: digestFor(ref, repoDigests),
|
||||
Status: containerStatus(ct.State),
|
||||
Self: c.selfID != "" && strings.HasPrefix(ct.ID, c.selfID),
|
||||
}
|
||||
|
||||
@@ -22,12 +22,18 @@ type fakeDocker struct {
|
||||
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
|
||||
}
|
||||
@@ -72,6 +78,32 @@ func TestSnapshot_ComposeContainerWithRepoDigest(t *testing.T) {
|
||||
}}, 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"),
|
||||
|
||||
Reference in New Issue
Block a user