From 9d5f24b1257e39869c1fd7e083f4e96499624801 Mon Sep 17 00:00:00 2001 From: Samuel Enocsson Date: Tue, 13 Oct 2026 10:04:58 +0200 Subject: [PATCH] feat(store): persist the Swarm cluster id of each agent The Ledger UI groups Swarm services under their cluster and lists the cluster's managers, so the master needs to know which cluster an agent belongs to. RecordHello now stores hello.SwarmClusterID in the new agents.swarm_cluster_id column. Claude-Session: https://claude.ai/code/session_01GAZsp3P7Qto6JnhipxWUJQ --- internal/master/model/model.go | 9 ++++++--- internal/master/store/agents.go | 8 ++++---- internal/master/store/agents_integration_test.go | 1 + internal/master/store/migrations/0005_agent_cluster.sql | 1 + 4 files changed, 12 insertions(+), 7 deletions(-) create mode 100644 internal/master/store/migrations/0005_agent_cluster.sql diff --git a/internal/master/model/model.go b/internal/master/model/model.go index 7ca11b7..73af03f 100644 --- a/internal/master/model/model.go +++ b/internal/master/model/model.go @@ -27,9 +27,12 @@ type Agent struct { Hostname string AgentVersion string SwarmManager bool - LastSeenAt *time.Time - RevokedAt *time.Time - CreatedAt time.Time + // SwarmClusterID is the Swarm cluster the agent's daemon was in when it last + // connected; "" outside a swarm. + SwarmClusterID string + LastSeenAt *time.Time + RevokedAt *time.Time + CreatedAt time.Time } // Service is one service as last reported by an agent. diff --git a/internal/master/store/agents.go b/internal/master/store/agents.go index 79ff91f..4f9708f 100644 --- a/internal/master/store/agents.go +++ b/internal/master/store/agents.go @@ -13,11 +13,11 @@ import ( "github.com/shcizo/package-updater/internal/protocol" ) -const agentColumns = `id, name, hostname, agent_version, swarm_manager, last_seen_at, revoked_at, created_at` +const agentColumns = `id, name, hostname, agent_version, swarm_manager, swarm_cluster_id, last_seen_at, revoked_at, created_at` func scanAgent(row pgx.Row) (model.Agent, error) { var a model.Agent - err := row.Scan(&a.ID, &a.Name, &a.Hostname, &a.AgentVersion, &a.SwarmManager, &a.LastSeenAt, &a.RevokedAt, &a.CreatedAt) + err := row.Scan(&a.ID, &a.Name, &a.Hostname, &a.AgentVersion, &a.SwarmManager, &a.SwarmClusterID, &a.LastSeenAt, &a.RevokedAt, &a.CreatedAt) if errors.Is(err, pgx.ErrNoRows) { return model.Agent{}, model.ErrNotFound } @@ -101,8 +101,8 @@ func (s *Store) RevokeAgent(ctx context.Context, id int64, at time.Time) ([]int6 // RecordHello stores what the agent reported about itself on connect. func (s *Store) RecordHello(ctx context.Context, id int64, h protocol.Hello, at time.Time) error { if _, err := s.pool.Exec(ctx, - `UPDATE agents SET hostname = $2, agent_version = $3, swarm_manager = $4, last_seen_at = $5 WHERE id = $1`, - id, h.Hostname, h.AgentVersion, h.SwarmManager, at); err != nil { + `UPDATE agents SET hostname = $2, agent_version = $3, swarm_manager = $4, swarm_cluster_id = $6, last_seen_at = $5 WHERE id = $1`, + id, h.Hostname, h.AgentVersion, h.SwarmManager, at, h.SwarmClusterID); err != nil { return fmt.Errorf("record hello: %w", err) } return nil diff --git a/internal/master/store/agents_integration_test.go b/internal/master/store/agents_integration_test.go index 9cc22b9..93f4383 100644 --- a/internal/master/store/agents_integration_test.go +++ b/internal/master/store/agents_integration_test.go @@ -80,6 +80,7 @@ func TestAgents_RecordHelloAndMarkSeen(t *testing.T) { assert.Equal(t, "swarm-mgr", agents[0].Hostname) assert.Equal(t, "v1", agents[0].AgentVersion) assert.True(t, agents[0].SwarmManager) + assert.Equal(t, "c1", agents[0].SwarmClusterID) require.NotNil(t, agents[0].LastSeenAt) assert.True(t, agents[0].LastSeenAt.Equal(at.Add(time.Minute))) } diff --git a/internal/master/store/migrations/0005_agent_cluster.sql b/internal/master/store/migrations/0005_agent_cluster.sql new file mode 100644 index 0000000..14dba3b --- /dev/null +++ b/internal/master/store/migrations/0005_agent_cluster.sql @@ -0,0 +1 @@ +ALTER TABLE agents ADD COLUMN swarm_cluster_id text NOT NULL DEFAULT '';