//go:build integration package store import ( "context" "strings" "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/protocol" ) const sluskRepo = "gitea.shcizo.se/shcizo/slusk" // Digests of the fixture services; only well-formed ones are recorded as versions. var ( digD1 = "sha256:" + strings.Repeat("d", 63) + "1" digGitea = "sha256:" + strings.Repeat("a", 63) + "1" digPg = "sha256:" + strings.Repeat("b", 63) + "1" ) func versionsOf(t *testing.T, s *Store, repo string) []model.ImageVersion { t.Helper() got, err := s.RecentVersions(context.Background(), repo, 100) require.NoError(t, err) return got } func TestSetAvailable_RecordsVersion(t *testing.T) { s := openTestStore(t) ctx := context.Background() _, svc := jobFixture(t, s, "a") require.NoError(t, s.SetAvailable(ctx, svc, model.Available{Ref: sluskRepo + ":1.5.0", Digest: "sha256:n1", Source: model.SourcePoll, At: t0})) got, err := s.GetService(ctx, svc) require.NoError(t, err) assert.Equal(t, sluskRepo+":1.5.0", got.AvailableRef) assert.Equal(t, "sha256:n1", got.AvailableDigest) assert.Equal(t, model.SourcePoll, got.AvailableSource) vs := versionsOf(t, s, sluskRepo) var found model.ImageVersion for _, v := range vs { if v.Digest == "sha256:n1" { found = v } } require.NotZero(t, found.ID) assert.Equal(t, "1.5.0", found.Tag) assert.Equal(t, model.SourcePoll, found.Source) // Seeing the same version again keeps the first sighting. require.NoError(t, s.SetAvailable(ctx, svc, model.Available{Ref: sluskRepo + ":1.5.0", Digest: "sha256:n1", Source: model.SourceWebhook, At: t0.Add(time.Hour)})) again, err := s.GetImageVersion(ctx, found.ID) require.NoError(t, err) assert.Equal(t, model.SourcePoll, again.Source) assert.True(t, t0.Equal(again.FirstSeenAt)) assert.Len(t, versionsOf(t, s, sluskRepo), len(vs)) } func TestSnapshotRecordsRunningVersions(t *testing.T) { s := openTestStore(t) ctx := context.Background() id := newAgent(t, s, "node-2") tagless := protocol.Service{Kind: protocol.KindSwarm, SwarmService: "g_g", Image: "gitea/gitea@" + digGitea, Digest: digGitea, Status: "running"} bare := protocol.Service{Kind: protocol.KindContainer, ContainerName: "pg", Image: "postgres", Digest: digPg, Status: "running"} nodigest := protocol.Service{Kind: protocol.KindContainer, ContainerName: "nd", Image: "nodigest:1", Status: "running"} hello := protocol.Hello{Hostname: "node-2", SwarmManager: true, SwarmClusterID: "c1"} svcs := []protocol.Service{slusk, tagless, bare, nodigest} require.NoError(t, s.ApplySnapshot(ctx, id, hello, svcs, t0)) require.NoError(t, s.ApplySnapshot(ctx, id, hello, svcs, t0.Add(time.Hour))) got := versionsOf(t, s, sluskRepo) require.Len(t, got, 1) assert.Equal(t, "1.4.2", got[0].Tag) assert.Equal(t, digD1, got[0].Digest) assert.Equal(t, model.SourceRunning, got[0].Source) assert.True(t, t0.Equal(got[0].FirstSeenAt), "re-snapshot does not bump first_seen_at") tl := versionsOf(t, s, "gitea/gitea") require.Len(t, tl, 1) assert.Equal(t, "", tl[0].Tag) assert.Equal(t, digGitea, tl[0].Digest) b := versionsOf(t, s, "postgres") require.Len(t, b, 1) assert.Equal(t, "latest", b[0].Tag, "a bare image runs latest, like the followed ref") assert.Empty(t, versionsOf(t, s, "nodigest")) } func TestSnapshotRecordsOnlyWellFormedDigests(t *testing.T) { s := openTestStore(t) id := newAgent(t, s, "node-2") bad := protocol.Service{Kind: protocol.KindContainer, ContainerName: "bad", Image: "bad:1", Digest: "sha256:short", Status: "running"} upper := protocol.Service{Kind: protocol.KindContainer, ContainerName: "up", Image: "up:1", Digest: "sha256:" + strings.Repeat("A", 64), Status: "running"} require.NoError(t, s.ApplySnapshot(context.Background(), id, protocol.Hello{Hostname: "node-2"}, []protocol.Service{bad, upper}, t0)) assert.Empty(t, versionsOf(t, s, "bad")) assert.Empty(t, versionsOf(t, s, "up")) } func TestRecentVersions(t *testing.T) { ctx := context.Background() t.Run("order, limit and repo filter", func(t *testing.T) { s := openTestStore(t) _, svc := jobFixture(t, s, "a") require.NoError(t, s.SetAvailable(ctx, svc, model.Available{Ref: "other/repo:x", Digest: "sha256:ox", Source: model.SourcePoll, At: t0})) // 12 versions of the service's repo, one per minute. for i := 0; i < 12; i++ { tag := "t" + string(rune('a'+i)) require.NoError(t, s.SetAvailable(ctx, svc, model.Available{Ref: sluskRepo + ":" + tag, Digest: "sha256:" + tag, Source: model.SourcePoll, At: t0.Add(time.Duration(i+1) * time.Minute)})) } got, err := s.RecentVersions(ctx, sluskRepo, 10) require.NoError(t, err) require.Len(t, got, 10) assert.Equal(t, "tl", got[0].Tag, "newest first") assert.Equal(t, "tc", got[9].Tag) for _, v := range got { assert.Equal(t, sluskRepo, v.Repo) } }) t.Run("digest-less row is hidden once its tag has a digest", func(t *testing.T) { s := openTestStore(t) _, svc := jobFixture(t, s, "a") add := func(tag, digest string, at time.Time) { require.NoError(t, s.SetAvailable(ctx, svc, model.Available{Ref: sluskRepo + ":" + tag, Digest: digest, Source: model.SourceWebhook, At: at})) } add("main", "", t0.Add(time.Minute)) add("solo", "", t0.Add(2*time.Minute)) require.Len(t, versionsOf(t, s, sluskRepo), 3, "running 1.4.2 plus two digest-less rows") add("main", "sha256:m1", t0.Add(3*time.Minute)) var refs []string for _, v := range versionsOf(t, s, sluskRepo) { refs = append(refs, v.Ref()) } assert.Equal(t, []string{sluskRepo + ":main@sha256:m1", sluskRepo + ":solo", sluskRepo + ":1.4.2@" + digD1}, refs) }) t.Run("digest-less row newer than the digest row of its tag is shown", func(t *testing.T) { s := openTestStore(t) _, svc := jobFixture(t, s, "a") add := func(tag, digest string, at time.Time) { require.NoError(t, s.SetAvailable(ctx, svc, model.Available{Ref: sluskRepo + ":" + tag, Digest: digest, Source: model.SourceWebhook, At: at})) } add("main", "sha256:m1", t0.Add(time.Minute)) add("main", "", t0.Add(2*time.Minute)) var refs []string for _, v := range versionsOf(t, s, sluskRepo) { refs = append(refs, v.Ref()) } assert.Equal(t, []string{sluskRepo + ":main", sluskRepo + ":main@sha256:m1", sluskRepo + ":1.4.2@" + digD1}, refs) }) } func TestGetImageVersion_NotFound(t *testing.T) { s := openTestStore(t) _, err := s.GetImageVersion(context.Background(), 99999) assert.ErrorIs(t, err, model.ErrNotFound) } func TestSetPollError(t *testing.T) { s := openTestStore(t) ctx := context.Background() agent, svc := jobFixture(t, s, "a") require.NoError(t, s.SetAvailable(ctx, svc, model.Available{Ref: sluskRepo + ":1.5.0", Digest: "sha256:n1", Source: model.SourcePoll, At: t0})) require.NoError(t, s.SetPollError(ctx, []int64{svc}, "boom")) require.NoError(t, s.SetPollError(ctx, nil, "ignored")) got, err := s.GetService(ctx, svc) require.NoError(t, err) assert.Equal(t, "boom", got.LastPollError) require.NoError(t, s.UpsertService(ctx, agent, "", slusk, t0.Add(time.Hour))) got, err = s.GetService(ctx, svc) require.NoError(t, err) assert.Equal(t, "boom", got.LastPollError, "inventory writes do not touch the poll error") assert.Equal(t, "sha256:n1", got.AvailableDigest, "nor the suggestion") require.NoError(t, s.SetPollError(ctx, []int64{svc}, "")) got, err = s.GetService(ctx, svc) require.NoError(t, err) assert.Empty(t, got.LastPollError) } func TestSetLastAutoDigest(t *testing.T) { s := openTestStore(t) ctx := context.Background() agent, svc := jobFixture(t, s, "a") got, err := s.GetService(ctx, svc) require.NoError(t, err) assert.Empty(t, got.LastAutoDigest) require.NoError(t, s.SetLastAutoDigest(ctx, svc, "sha256:n1")) require.NoError(t, s.UpsertService(ctx, agent, "", slusk, t0.Add(time.Hour))) got, err = s.GetService(ctx, svc) require.NoError(t, err) assert.Equal(t, "sha256:n1", got.LastAutoDigest, "inventory writes do not touch it") list, err := s.ListServices(ctx) require.NoError(t, err) require.Len(t, list, 1) assert.Equal(t, "sha256:n1", list[0].LastAutoDigest) }