feat(master): suggest newer version tags for third-party images (#11)
This commit is contained in:
@@ -275,6 +275,7 @@ func TestMergeService_MovesHistoryModeAndDesiredToTheNewLocation(t *testing.T) {
|
||||
require.NoError(t, s.SetLastTagDigest(ctx, f.to, "sha256:fromto"))
|
||||
require.NoError(t, s.SetAvailable(ctx, f.to, model.Available{Ref: sluskDrift + ":3", Digest: digNew, Source: model.SourcePoll, At: t0}))
|
||||
require.NoError(t, s.SetPollError(ctx, []int64{f.to}, "registry down"))
|
||||
require.NoError(t, s.SetNewer(ctx, f.to, sluskDrift+":3.1", t0))
|
||||
// A job on the old row is finished, and awaiting_inventory is set on both.
|
||||
before, err := s.GetService(ctx, f.from)
|
||||
require.NoError(t, err)
|
||||
@@ -297,6 +298,9 @@ func TestMergeService_MovesHistoryModeAndDesiredToTheNewLocation(t *testing.T) {
|
||||
assert.Equal(t, sluskDrift+":3", got.AvailableRef)
|
||||
assert.Equal(t, digNew, got.AvailableDigest)
|
||||
assert.Equal(t, "registry down", got.LastPollError)
|
||||
assert.Equal(t, sluskDrift+":3.1", got.NewerRef, "newer tag from the duplicate")
|
||||
require.NotNil(t, got.NewerAt)
|
||||
assert.True(t, t0.Equal(*got.NewerAt))
|
||||
assert.Equal(t, before.LastTagDigest, got.LastTagDigest, "an existing last_tag_digest wins")
|
||||
|
||||
_, err = s.GetService(ctx, f.to)
|
||||
|
||||
@@ -106,8 +106,8 @@ type mergeRow struct {
|
||||
|
||||
// MergeService moves the live service toID onto the removed service fromID:
|
||||
// the old row keeps its id, first_seen_at, update mode and desired ref, takes
|
||||
// the location and inventory of the live row (and its available suggestion),
|
||||
// becomes live, and the live row is deleted. The jobs of the live row move to
|
||||
// the location and inventory of the live row (and its available and newer-tag
|
||||
// suggestions), becomes live, and the live row is deleted. The jobs of the live row move to
|
||||
// the old one. This is the only place a services row is deleted.
|
||||
//
|
||||
// It returns *model.MergeRefusedError when the pair cannot be merged,
|
||||
@@ -195,12 +195,12 @@ func (s *Store) MergeService(ctx context.Context, fromID, toID int64) error {
|
||||
SELECT agent_id, location_key, compose_project, compose_service, working_dir, config_files, built,
|
||||
swarm_cluster_id, swarm_service, container_name, image_repo, actual_ref, actual_digest, status, self,
|
||||
updated_at, available_ref, available_digest, available_source, available_at, last_poll_error,
|
||||
last_tag_digest
|
||||
last_tag_digest, newer_ref, newer_at
|
||||
FROM services WHERE id = $1`, toID).Scan(
|
||||
&t.AgentID, &t.LocationKey, &t.ComposeProject, &t.ComposeService, &t.WorkingDir, &t.ConfigFiles, &t.Built,
|
||||
&t.SwarmClusterID, &t.SwarmService, &t.ContainerName, &t.ImageRepo, &t.ActualRef, &t.ActualDigest, &t.Status, &t.Self,
|
||||
&t.UpdatedAt, &t.AvailableRef, &t.AvailableDigest, &t.AvailableSource, &t.AvailableAt, &t.LastPollError,
|
||||
&lastTagDigest)
|
||||
&lastTagDigest, &t.NewerRef, &t.NewerAt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("merge: read new service: %w", err)
|
||||
}
|
||||
@@ -213,7 +213,7 @@ func (s *Store) MergeService(ctx context.Context, fromID, toID int64) error {
|
||||
config_files = $7, built = $8, swarm_cluster_id = $9, swarm_service = $10, container_name = $11,
|
||||
image_repo = $12, actual_ref = $13, actual_digest = $14, status = $15, self = $16, updated_at = $17,
|
||||
available_ref = $18, available_digest = $19, available_source = $20, available_at = $21,
|
||||
last_poll_error = $22,
|
||||
last_poll_error = $22, newer_ref = $24, newer_at = $25,
|
||||
last_tag_digest = CASE WHEN last_tag_digest <> '' THEN last_tag_digest ELSE $23 END,
|
||||
removed_at = NULL, awaiting_inventory = false
|
||||
WHERE id = $1`,
|
||||
@@ -221,7 +221,7 @@ func (s *Store) MergeService(ctx context.Context, fromID, toID int64) error {
|
||||
t.ConfigFiles, t.Built, t.SwarmClusterID, t.SwarmService, t.ContainerName,
|
||||
t.ImageRepo, t.ActualRef, t.ActualDigest, t.Status, t.Self, t.UpdatedAt,
|
||||
t.AvailableRef, t.AvailableDigest, t.AvailableSource, t.AvailableAt,
|
||||
t.LastPollError, lastTagDigest); err != nil {
|
||||
t.LastPollError, lastTagDigest, t.NewerRef, t.NewerAt); err != nil {
|
||||
return fmt.Errorf("merge: update old service: %w", err)
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
-- Newer version tag of the followed tag, found in the registry's tag list.
|
||||
-- A suggestion only: nothing deploys it automatically.
|
||||
ALTER TABLE services ADD COLUMN newer_ref text NOT NULL DEFAULT '';
|
||||
ALTER TABLE services ADD COLUMN newer_at timestamptz;
|
||||
|
||||
-- A suggested tag is recorded in image_versions with source 'tags'.
|
||||
ALTER TABLE image_versions DROP CONSTRAINT image_versions_source_check;
|
||||
ALTER TABLE image_versions ADD CONSTRAINT image_versions_source_check
|
||||
CHECK (source IN ('webhook', 'poll', 'running', 'tags'));
|
||||
@@ -175,7 +175,7 @@ const serviceColumns = `s.id, s.kind, s.location_key, s.agent_id, COALESCE(a.nam
|
||||
s.compose_project, s.compose_service, s.working_dir, s.config_files, s.built, s.swarm_cluster_id,
|
||||
s.swarm_service, s.container_name, s.image_repo, s.desired_ref, s.actual_ref, s.actual_digest,
|
||||
s.status, s.self, s.update_mode, s.available_ref, s.available_digest, s.available_source, s.available_at,
|
||||
s.last_poll_error, s.last_tag_digest, s.awaiting_inventory, s.first_seen_at, s.updated_at, s.removed_at`
|
||||
s.last_poll_error, s.newer_ref, s.newer_at, s.last_tag_digest, s.awaiting_inventory, s.first_seen_at, s.updated_at, s.removed_at`
|
||||
|
||||
func scanService(row pgx.Row) (model.Service, error) {
|
||||
var m model.Service
|
||||
@@ -184,7 +184,7 @@ func scanService(row pgx.Row) (model.Service, error) {
|
||||
&m.ComposeProject, &m.ComposeService, &m.WorkingDir, &m.ConfigFiles, &m.Built, &m.SwarmClusterID,
|
||||
&m.SwarmService, &m.ContainerName, &m.ImageRepo, &m.DesiredRef, &m.ActualRef, &m.ActualDigest,
|
||||
&m.Status, &m.Self, &m.UpdateMode, &m.AvailableRef, &m.AvailableDigest, &m.AvailableSource, &m.AvailableAt,
|
||||
&m.LastPollError, &m.LastTagDigest, &m.AwaitingInventory, &m.FirstSeenAt, &m.UpdatedAt, &m.RemovedAt)
|
||||
&m.LastPollError, &m.NewerRef, &m.NewerAt, &m.LastTagDigest, &m.AwaitingInventory, &m.FirstSeenAt, &m.UpdatedAt, &m.RemovedAt)
|
||||
m.Kind = protocol.Kind(kind)
|
||||
return m, err
|
||||
}
|
||||
@@ -246,6 +246,33 @@ func (s *Store) SetAvailable(ctx context.Context, id int64, a model.Available) e
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetNewer sets the service's newer-tag suggestion (model.Service.NewerRef);
|
||||
// ref "" clears it. A non-empty ref is also recorded in image_versions with
|
||||
// source "tags", in the same transaction, unless that (repo, tag) is already
|
||||
// there. The digest is unknown, so the row has none.
|
||||
func (s *Store) SetNewer(ctx context.Context, id int64, ref string, at time.Time) error {
|
||||
tx, err := s.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("set newer: begin: %w", err)
|
||||
}
|
||||
defer func() { _ = tx.Rollback(ctx) }()
|
||||
if _, err := tx.Exec(ctx, `
|
||||
UPDATE services SET newer_ref = $2, newer_at = CASE WHEN $2 = '' THEN NULL ELSE $3::timestamptz END
|
||||
WHERE id = $1`, id, ref, at); err != nil {
|
||||
return fmt.Errorf("set newer: %w", err)
|
||||
}
|
||||
if ref != "" {
|
||||
p := imageref.Parse(ref)
|
||||
if err := insertVersion(ctx, tx, p.Repo, p.Tag, "", model.SourceTags, at); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return fmt.Errorf("set newer: commit: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetPollError sets last_poll_error on the given services; msg "" clears it.
|
||||
func (s *Store) SetPollError(ctx context.Context, ids []int64, msg string) error {
|
||||
if len(ids) == 0 {
|
||||
@@ -313,6 +340,23 @@ func (s *Store) GetImageVersion(ctx context.Context, id int64) (model.ImageVersi
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// GetTagVersion returns the digest-less version row with an empty digest, which
|
||||
// SetNewer and the webhook create. Returns model.ErrNotFound if there is none.
|
||||
func (s *Store) GetTagVersion(ctx context.Context, repo, tag string) (model.ImageVersion, error) {
|
||||
var v model.ImageVersion
|
||||
err := s.pool.QueryRow(ctx, `
|
||||
SELECT id, repo, tag, digest, source, first_seen_at FROM image_versions
|
||||
WHERE repo = $1 AND tag = $2 AND digest = ''`, repo, tag,
|
||||
).Scan(&v.ID, &v.Repo, &v.Tag, &v.Digest, &v.Source, &v.FirstSeenAt)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return model.ImageVersion{}, model.ErrNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return model.ImageVersion{}, fmt.Errorf("get tag version: %w", err)
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// SetUpdateMode sets the service's update mode (model.ModeManual or
|
||||
// model.ModeAuto). Returns model.ErrNotFound if the service does not exist
|
||||
// or is marked removed.
|
||||
|
||||
@@ -214,3 +214,79 @@ func TestSetLastTagDigest(t *testing.T) {
|
||||
require.Len(t, list, 1)
|
||||
assert.Equal(t, "sha256:n1", list[0].LastTagDigest)
|
||||
}
|
||||
|
||||
func TestSetNewer_SetsAndClears(t *testing.T) {
|
||||
s := openTestStore(t)
|
||||
ctx := context.Background()
|
||||
_, svc := jobFixture(t, s, "a")
|
||||
|
||||
require.NoError(t, s.SetNewer(ctx, svc, sluskRepo+":1.5.0", t0))
|
||||
|
||||
got, err := s.GetService(ctx, svc)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, sluskRepo+":1.5.0", got.NewerRef)
|
||||
require.NotNil(t, got.NewerAt)
|
||||
assert.True(t, t0.Equal(*got.NewerAt))
|
||||
v, err := s.GetTagVersion(ctx, sluskRepo, "1.5.0")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, model.SourceTags, v.Source)
|
||||
assert.Empty(t, v.Digest)
|
||||
assert.True(t, t0.Equal(v.FirstSeenAt))
|
||||
|
||||
require.NoError(t, s.SetNewer(ctx, svc, "", t0.Add(time.Hour)))
|
||||
|
||||
got, err = s.GetService(ctx, svc)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, got.NewerRef)
|
||||
assert.Nil(t, got.NewerAt)
|
||||
_, err = s.GetTagVersion(ctx, sluskRepo, "1.5.0")
|
||||
assert.NoError(t, err, "clearing the suggestion keeps the version")
|
||||
}
|
||||
|
||||
func TestSetNewer_KeepsEarlierVersionSource(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", Source: model.SourceWebhook, At: t0}))
|
||||
|
||||
require.NoError(t, s.SetNewer(ctx, svc, sluskRepo+":1.5.0", t0.Add(time.Hour)))
|
||||
|
||||
v, err := s.GetTagVersion(ctx, sluskRepo, "1.5.0")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, model.SourceWebhook, v.Source)
|
||||
assert.True(t, t0.Equal(v.FirstSeenAt))
|
||||
}
|
||||
|
||||
func TestRecentVersions_TagsRowHiddenOnceDigestKnown(t *testing.T) {
|
||||
s := openTestStore(t)
|
||||
ctx := context.Background()
|
||||
_, svc := jobFixture(t, s, "a")
|
||||
require.NoError(t, s.SetNewer(ctx, svc, sluskRepo+":1.5.0", t0.Add(time.Minute)))
|
||||
require.Len(t, versionsOf(t, s, sluskRepo), 2, "running 1.4.2 and the tags row")
|
||||
|
||||
require.NoError(t, s.SetAvailable(ctx, svc, model.Available{Ref: sluskRepo + ":1.5.0", Digest: "sha256:n1", Source: model.SourcePoll, At: 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 + ":1.5.0@sha256:n1", sluskRepo + ":1.4.2@" + digD1}, refs)
|
||||
}
|
||||
|
||||
func TestGetTagVersion(t *testing.T) {
|
||||
s := openTestStore(t)
|
||||
ctx := context.Background()
|
||||
_, svc := jobFixture(t, s, "a")
|
||||
require.NoError(t, s.SetNewer(ctx, svc, sluskRepo+":1.5.0", t0))
|
||||
|
||||
got, err := s.GetTagVersion(ctx, sluskRepo, "1.5.0")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, sluskRepo+":1.5.0", got.Ref())
|
||||
|
||||
_, err = s.GetTagVersion(ctx, sluskRepo, "9.9.9")
|
||||
assert.ErrorIs(t, err, model.ErrNotFound)
|
||||
_, err = s.GetTagVersion(ctx, "other/repo", "1.5.0")
|
||||
assert.ErrorIs(t, err, model.ErrNotFound)
|
||||
_, err = s.GetTagVersion(ctx, sluskRepo, "1.4.2")
|
||||
assert.ErrorIs(t, err, model.ErrNotFound, "a row with a digest is not a tag version")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user