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
170 lines
5.3 KiB
Go
170 lines
5.3 KiB
Go
//go:build integration
|
|
|
|
package store
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/shcizo/package-updater/internal/master/model"
|
|
"github.com/shcizo/package-updater/internal/pin"
|
|
"github.com/shcizo/package-updater/internal/protocol"
|
|
)
|
|
|
|
func TestIdentity_CreatedOnceThenReused(t *testing.T) {
|
|
s := openTestStore(t)
|
|
ctx := context.Background()
|
|
k1, err := s.LoadOrCreateIdentity(ctx, pin.GenerateKey)
|
|
require.NoError(t, err)
|
|
k2, err := s.LoadOrCreateIdentity(ctx, pin.GenerateKey)
|
|
require.NoError(t, err)
|
|
assert.True(t, k1.Equal(k2), "a restarted master must keep its key or every agent breaks")
|
|
}
|
|
|
|
func TestMigrationsAreIdempotent(t *testing.T) {
|
|
s := openTestStore(t)
|
|
require.NoError(t, migrate(context.Background(), s.pool))
|
|
}
|
|
|
|
func TestAgents_CreateLookupRevoke(t *testing.T) {
|
|
s := openTestStore(t)
|
|
ctx := context.Background()
|
|
|
|
created, err := s.CreateAgent(ctx, "node-2", "hash-2")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "node-2", created.Name)
|
|
assert.Nil(t, created.RevokedAt)
|
|
|
|
found, err := s.AgentByTokenHash(ctx, "hash-2")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, created.ID, found.ID)
|
|
|
|
_, err = s.AgentByTokenHash(ctx, "nope")
|
|
assert.ErrorIs(t, err, model.ErrNotFound)
|
|
|
|
_, err = s.RevokeAgent(ctx, created.ID, time.Now())
|
|
require.NoError(t, err)
|
|
found, err = s.AgentByTokenHash(ctx, "hash-2")
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, found.RevokedAt)
|
|
}
|
|
|
|
func TestAgents_DuplicateNameIsConflict(t *testing.T) {
|
|
s := openTestStore(t)
|
|
ctx := context.Background()
|
|
_, err := s.CreateAgent(ctx, "node-2", "h1")
|
|
require.NoError(t, err)
|
|
_, err = s.CreateAgent(ctx, "node-2", "h2")
|
|
assert.ErrorIs(t, err, model.ErrConflict)
|
|
}
|
|
|
|
func TestAgents_RecordHelloAndMarkSeen(t *testing.T) {
|
|
s := openTestStore(t)
|
|
ctx := context.Background()
|
|
a, err := s.CreateAgent(ctx, "swarm-mgr", "h")
|
|
require.NoError(t, err)
|
|
|
|
at := time.Date(2026, 10, 12, 12, 0, 0, 0, time.UTC)
|
|
require.NoError(t, s.RecordHello(ctx, a.ID, protocol.Hello{
|
|
Hostname: "swarm-mgr", AgentVersion: "v1", SwarmManager: true, SwarmClusterID: "c1",
|
|
}, at))
|
|
require.NoError(t, s.MarkSeen(ctx, a.ID, at.Add(time.Minute)))
|
|
|
|
agents, err := s.ListAgents(ctx)
|
|
require.NoError(t, err)
|
|
require.Len(t, agents, 1)
|
|
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)))
|
|
}
|
|
|
|
func TestAgents_RevokedNameCanBeReused(t *testing.T) {
|
|
s := openTestStore(t)
|
|
ctx := context.Background()
|
|
old, err := s.CreateAgent(ctx, "node-2", "h1")
|
|
require.NoError(t, err)
|
|
_, err = s.RevokeAgent(ctx, old.ID, time.Now())
|
|
require.NoError(t, err)
|
|
|
|
fresh, err := s.CreateAgent(ctx, "node-2", "h2")
|
|
require.NoError(t, err)
|
|
assert.NotEqual(t, old.ID, fresh.ID)
|
|
|
|
_, err = s.CreateAgent(ctx, "node-2", "h3")
|
|
assert.ErrorIs(t, err, model.ErrConflict, "a second active agent with the name is still a conflict")
|
|
}
|
|
|
|
func TestAgents_RevokeRemovesOwnComposeAndContainerServices(t *testing.T) {
|
|
s := openTestStore(t)
|
|
ctx := context.Background()
|
|
id := newAgent(t, s, "swarm-mgr")
|
|
require.NoError(t, s.ApplySnapshot(ctx, id, manager, []protocol.Service{slusk, adhoc, gitea}, t0))
|
|
|
|
_, err := s.RevokeAgent(ctx, id, t0.Add(time.Hour))
|
|
require.NoError(t, err)
|
|
|
|
got, err := s.ListServices(ctx)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []string{"gitea_gitea"}, names(got), "swarm rows are shared and must survive a revoke")
|
|
}
|
|
|
|
func TestRevokedAgent_InventoryWritesRefused(t *testing.T) {
|
|
s := openTestStore(t)
|
|
ctx := context.Background()
|
|
id := newAgent(t, s, "node-2")
|
|
_, err := s.RevokeAgent(ctx, id, t0)
|
|
require.NoError(t, err)
|
|
|
|
assert.ErrorIs(t, s.ApplySnapshot(ctx, id, worker, []protocol.Service{slusk}, t0.Add(time.Minute)), model.ErrRevoked)
|
|
assert.ErrorIs(t, s.UpsertService(ctx, id, "", adhoc, t0.Add(time.Minute)), model.ErrRevoked)
|
|
|
|
got, err := s.ListServices(ctx)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, got, "a revoked agent must not write inventory")
|
|
}
|
|
|
|
func TestRevokedAgent_UpsertDoesNotResurrectRemovedRow(t *testing.T) {
|
|
s := openTestStore(t)
|
|
ctx := context.Background()
|
|
id := newAgent(t, s, "node-2")
|
|
require.NoError(t, s.ApplySnapshot(ctx, id, worker, []protocol.Service{slusk}, t0))
|
|
_, err := s.RevokeAgent(ctx, id, t0.Add(time.Hour))
|
|
require.NoError(t, err)
|
|
|
|
assert.ErrorIs(t, s.UpsertService(ctx, id, "", slusk, t0.Add(2*time.Hour)), model.ErrRevoked)
|
|
|
|
got, err := s.ListServices(ctx)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, got, "the revoked agent's removed row must stay removed")
|
|
}
|
|
|
|
func TestRevokeAgent_FailsActiveJobs(t *testing.T) {
|
|
s := openTestStore(t)
|
|
ctx := context.Background()
|
|
a, svcA := jobFixture(t, s, "a")
|
|
b, svcB := jobFixture(t, s, "b")
|
|
ja, err := s.CreateJob(ctx, newJob(svcA, a))
|
|
require.NoError(t, err)
|
|
jb, err := s.CreateJob(ctx, newJob(svcB, b))
|
|
require.NoError(t, err)
|
|
|
|
failed, err := s.RevokeAgent(ctx, a, t0)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []int64{ja.ID}, failed, "the failed job IDs are returned so the UI can signal them")
|
|
|
|
got, err := s.GetJob(ctx, ja.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, model.JobFailed, got.Status)
|
|
assert.Equal(t, "agent revoked", got.Error)
|
|
got, err = s.GetJob(ctx, jb.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, model.JobQueued, got.Status)
|
|
}
|