feat(store): drift state, move candidates and service merge (#10)

Adds services.awaiting_inventory, Service.Drifted/RunsRef/RunningRef, AcceptRunning, MoveCandidates, MergeSources and MergeService, and maps a job for a deleted service to ErrNotFound.
This commit is contained in:
2026-10-13 10:28:34 +02:00
parent 307e46e4c7
commit 061243f5da
11 changed files with 1036 additions and 14 deletions
+79
View File
@@ -2,6 +2,7 @@ package model
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
@@ -58,6 +59,84 @@ func TestImageVersionRef(t *testing.T) {
}
}
const (
dA = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
dB = "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
)
func TestRunsRef(t *testing.T) {
cases := []struct {
name string
desired, actualRef, actualDig string
want bool
}{
{"same string", "repo:v1@" + dA, "repo:v1@" + dA, "", true},
{"tag and digest, actual reports it separately", "repo:tag@" + dA, "repo:tag", dA, true},
{"bare image reports latest digest", "postgres:latest@" + dA, "postgres", dA, true},
{"tagless actual with the same digest", "repo:tag@" + dA, "repo@" + dA, "", true},
{"same digest under another tag", "repo:v1@" + dA, "repo:v2@" + dA, "", true},
{"same digest, actual tag differs, digest reported", "repo:v1@" + dA, "repo:v2", dA, true},
{"same tag, actual digest unknown", "repo:tag@" + dA, "repo:tag", "", true},
{"desired without digest, same tag", "repo:tag", "repo:tag@" + dA, "", true},
{"bare desired, latest actual", "repo", "repo:latest", "", true},
{"registry with port", "localhost:5000/foo:v1@" + dA, "localhost:5000/foo:v1", dA, true},
{"registry with port, other digest", "localhost:5000/foo:v1@" + dA, "localhost:5000/foo:v1", dB, false},
{"same tag, other digest", "repo:tag@" + dA, "repo:tag", dB, false},
{"other digest in the ref", "repo:tag@" + dA, "repo:tag@" + dB, "", false},
{"other tag, digest unknown", "repo:v1@" + dA, "repo:v2", "", false},
{"tagless desired, tagged actual, digest unknown", "repo@" + dA, "repo:v1", "", false},
{"other repo", "repo:tag@" + dA, "other:tag", dA, false},
{"no docker.io normalisation", "nginx:1@" + dA, "docker.io/library/nginx:1", dA, false},
{"case-sensitive repo", "Repo:1@" + dA, "repo:1", dA, false},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
assert.Equal(t, c.want, RunsRef(c.desired, c.actualRef, c.actualDig))
})
}
}
func TestDrifted(t *testing.T) {
now := time.Now()
drifted := Service{DesiredRef: "repo:tag@" + dA, ActualRef: "repo:tag", ActualDigest: dB}
cases := []struct {
name string
mutate func(*Service)
want bool
}{
{"runs another digest", func(*Service) {}, true},
{"never updated", func(s *Service) { s.DesiredRef = "" }, false},
{"removed", func(s *Service) { s.RemovedAt = &now }, false},
{"awaiting inventory", func(s *Service) { s.AwaitingInventory = true }, false},
{"runs the desired digest", func(s *Service) { s.ActualDigest = dA }, false},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
svc := drifted
c.mutate(&svc)
assert.Equal(t, c.want, svc.Drifted())
})
}
}
func TestRunningRef(t *testing.T) {
cases := []struct {
name, actualRef, actualDigest, want string
}{
{"ref with digest is kept", "repo:v1@" + dB, dA, "repo:v1@" + dB},
{"tag plus reported digest", "repo:v1", dA, "repo:v1@" + dA},
{"bare image becomes latest", "postgres", dA, "postgres:latest@" + dA},
{"registry with port", "localhost:5000/foo:v1", dA, "localhost:5000/foo:v1@" + dA},
{"no digest known", "repo:v1", "", "repo:v1"},
{"malformed digest", "repo:v1", "sha256:abc", "repo:v1"},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
assert.Equal(t, c.want, RunningRef(c.actualRef, c.actualDigest))
})
}
}
func TestJob_Active(t *testing.T) {
cases := []struct {
status JobStatus