fix(master): say why a pinned pick fails on a protocol 2 agent (#9)

This commit is contained in:
2026-10-13 08:25:21 +02:00
parent 2e87d98012
commit 92e7646221
5 changed files with 26 additions and 6 deletions
+5 -1
View File
@@ -16,7 +16,8 @@ type candidate struct {
// Route picks the connection a job for svc goes to, among agents that speak // Route picks the connection a job for svc goes to, among agents that speak
// at least protocol version minProtocol. It returns model.ErrAgentOffline or // at least protocol version minProtocol. It returns model.ErrAgentOffline or
// model.ErrAgentTooOld when there is none. // model.ErrAgentTooOld (model.ErrAgentTooOldForPinned when minProtocol asks
// for a pinned ref) when there is none.
func (h *Hub) Route(svc model.Service, minProtocol int) (model.Route, error) { func (h *Hub) Route(svc model.Service, minProtocol int) (model.Route, error) {
h.mu.Lock() h.mu.Lock()
conns := make([]candidate, 0, len(h.conns)) conns := make([]candidate, 0, len(h.conns))
@@ -64,6 +65,9 @@ func route(svc model.Service, conns []candidate, minProtocol int) (model.Route,
} }
} }
if tooOld { if tooOld {
if minProtocol >= protocol.MinPinnedRefVersion {
return model.Route{}, model.ErrAgentTooOldForPinned
}
return model.Route{}, model.ErrAgentTooOld return model.Route{}, model.ErrAgentTooOld
} }
return model.Route{}, model.ErrAgentOffline return model.Route{}, model.ErrAgentOffline
+2 -2
View File
@@ -30,7 +30,7 @@ func TestRoute(t *testing.T) {
{"compose, own agent too old", composeSvc, []candidate{{2, 20, protocol.Hello{}}}, jobs, model.Route{}, model.ErrAgentTooOld}, {"compose, own agent too old", composeSvc, []candidate{{2, 20, protocol.Hello{}}}, jobs, model.Route{}, model.ErrAgentTooOld},
{"compose, own agent missing", composeSvc, nil, jobs, model.Route{}, model.ErrAgentOffline}, {"compose, own agent missing", composeSvc, nil, jobs, model.Route{}, model.ErrAgentOffline},
{"compose, only another agent online", composeSvc, []candidate{{5, 50, v2}}, jobs, model.Route{}, model.ErrAgentOffline}, {"compose, only another agent online", composeSvc, []candidate{{5, 50, v2}}, jobs, model.Route{}, model.ErrAgentOffline},
{"compose, v2 agent and a pinned ref", composeSvc, []candidate{{2, 20, v2}}, pinned, model.Route{}, model.ErrAgentTooOld}, {"compose, v2 agent and a pinned ref", composeSvc, []candidate{{2, 20, v2}}, pinned, model.Route{}, model.ErrAgentTooOldForPinned},
{"compose, v3 agent and a pinned ref", composeSvc, []candidate{{2, 20, v3}}, pinned, model.Route{AgentID: 2, ConnID: 20}, nil}, {"compose, v3 agent and a pinned ref", composeSvc, []candidate{{2, 20, v3}}, pinned, model.Route{AgentID: 2, ConnID: 20}, nil},
{"swarm on reporting agent", swarmSvc, []candidate{{1, 10, mgrV2}, {3, 30, mgrV2}}, jobs, model.Route{AgentID: 1, ConnID: 10}, nil}, {"swarm on reporting agent", swarmSvc, []candidate{{1, 10, mgrV2}, {3, 30, mgrV2}}, jobs, model.Route{AgentID: 1, ConnID: 10}, nil},
{"swarm, reporting agent offline, other manager", swarmSvc, []candidate{{3, 30, mgrV2}}, jobs, model.Route{AgentID: 3, ConnID: 30}, nil}, {"swarm, reporting agent offline, other manager", swarmSvc, []candidate{{3, 30, mgrV2}}, jobs, model.Route{AgentID: 3, ConnID: 30}, nil},
@@ -47,7 +47,7 @@ func TestRoute(t *testing.T) {
[]candidate{{1, 10, protocol.Hello{ProtocolVersion: 2, SwarmManager: true}}}, jobs, model.Route{}, model.ErrAgentOffline}, []candidate{{1, 10, protocol.Hello{ProtocolVersion: 2, SwarmManager: true}}}, jobs, model.Route{}, model.ErrAgentOffline},
{"swarm, pinned ref falls back to a v3 manager when the reporting one is v2", swarmSvc, {"swarm, pinned ref falls back to a v3 manager when the reporting one is v2", swarmSvc,
[]candidate{{1, 10, mgrV2}, {3, 30, mgrV3}}, pinned, model.Route{AgentID: 3, ConnID: 30}, nil}, []candidate{{1, 10, mgrV2}, {3, 30, mgrV3}}, pinned, model.Route{AgentID: 3, ConnID: 30}, nil},
{"swarm, pinned ref and only v2 managers", swarmSvc, []candidate{{1, 10, mgrV2}}, pinned, model.Route{}, model.ErrAgentTooOld}, {"swarm, pinned ref and only v2 managers", swarmSvc, []candidate{{1, 10, mgrV2}}, pinned, model.Route{}, model.ErrAgentTooOldForPinned},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
+3 -2
View File
@@ -156,8 +156,9 @@ func (s *Service) TriggerVersion(ctx context.Context, serviceID, versionID int64
} }
// Start starts an update of svc. It returns the job, which may already be // Start starts an update of svc. It returns the job, which may already be
// failed ("agent offline", "agent too old for updates", the latter also when // failed ("agent offline", "agent too old for updates", or "agent too old to
// the ref has a digest or no tag and the agent speaks protocol version 2); a // deploy a pinned version" when the ref has a digest or no tag and the agent
// speaks protocol version 2); a
// *model.NotUpdatableError when the service cannot be updated (no job // *model.NotUpdatableError when the service cannot be updated (no job
// created); the active job and model.ErrJobActive when one is queued or // created); the active job and model.ErrJobActive when one is queued or
// running. // running.
+4 -1
View File
@@ -83,6 +83,9 @@ type fakeHub struct {
func (h *fakeHub) Route(_ model.Service, minProtocol int) (model.Route, error) { func (h *fakeHub) Route(_ model.Service, minProtocol int) (model.Route, error) {
h.mins = append(h.mins, minProtocol) h.mins = append(h.mins, minProtocol)
if h.agentProtocol != 0 && minProtocol > h.agentProtocol { if h.agentProtocol != 0 && minProtocol > h.agentProtocol {
if minProtocol >= protocol.MinPinnedRefVersion {
return model.Route{}, model.ErrAgentTooOldForPinned
}
return model.Route{}, model.ErrAgentTooOld return model.Route{}, model.ErrAgentTooOld
} }
return h.route, h.routeErr return h.route, h.routeErr
@@ -481,7 +484,7 @@ func TestTriggerVersion_RequiresPinnedProtocol(t *testing.T) {
pinned, err := s.TriggerVersion(context.Background(), 1, 10, "sub-1") pinned, err := s.TriggerVersion(context.Background(), 1, 10, "sub-1")
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, model.JobFailed, pinned.Status) assert.Equal(t, model.JobFailed, pinned.Status)
assert.Equal(t, "agent too old for updates", pinned.Error) assert.Equal(t, "agent too old to deploy a pinned version (needs protocol 3)", pinned.Error)
assert.Empty(t, h.starts, "nothing sent to the old agent") assert.Empty(t, h.starts, "nothing sent to the old agent")
plain, err := s.TriggerVersion(context.Background(), 1, 11, "sub-1") plain, err := s.TriggerVersion(context.Background(), 1, 11, "sub-1")
+12
View File
@@ -15,8 +15,20 @@ var (
// ErrAgentTooOld means the only agent that could run the job speaks a // ErrAgentTooOld means the only agent that could run the job speaks a
// protocol version without jobs. // protocol version without jobs.
ErrAgentTooOld = errors.New("agent too old for updates") ErrAgentTooOld = errors.New("agent too old for updates")
// ErrAgentTooOldForPinned means the only agent that could run the job
// speaks protocol version 2, which cannot deploy a digest-pinned or
// tagless ref. errors.Is(err, ErrAgentTooOld) is true for it as well.
ErrAgentTooOldForPinned error = agentTooOldForPinned{}
) )
type agentTooOldForPinned struct{}
func (agentTooOldForPinned) Error() string {
return "agent too old to deploy a pinned version (needs protocol 3)"
}
func (agentTooOldForPinned) Is(target error) bool { return target == ErrAgentTooOld }
// NotUpdatableError says why a service cannot be updated. Reason is shown // NotUpdatableError says why a service cannot be updated. Reason is shown
// in the UI as is. // in the UI as is.
type NotUpdatableError struct { type NotUpdatableError struct {