mirror of https://github.com/coder/coder.git
chore: add archive column to template versions (#10178)
* chore: add archive column to template versions
This commit is contained in:
parent
c11f241622
commit
69d13f1676
|
@ -673,6 +673,17 @@ func (q *querier) AllUserIDs(ctx context.Context) ([]uuid.UUID, error) {
|
|||
return q.db.AllUserIDs(ctx)
|
||||
}
|
||||
|
||||
func (q *querier) ArchiveUnusedTemplateVersions(ctx context.Context, arg database.ArchiveUnusedTemplateVersionsParams) ([]uuid.UUID, error) {
|
||||
tpl, err := q.db.GetTemplateByID(ctx, arg.TemplateID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := q.authorizeContext(ctx, rbac.ActionUpdate, tpl); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return q.db.ArchiveUnusedTemplateVersions(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) CleanTailnetCoordinators(ctx context.Context) error {
|
||||
if err := q.authorizeContext(ctx, rbac.ActionDelete, rbac.ResourceTailnetCoordinator); err != nil {
|
||||
return err
|
||||
|
@ -2260,6 +2271,22 @@ func (q *querier) TryAcquireLock(ctx context.Context, id int64) (bool, error) {
|
|||
return q.db.TryAcquireLock(ctx, id)
|
||||
}
|
||||
|
||||
func (q *querier) UnarchiveTemplateVersion(ctx context.Context, arg database.UnarchiveTemplateVersionParams) error {
|
||||
v, err := q.db.GetTemplateVersionByID(ctx, arg.TemplateVersionID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tpl, err := q.db.GetTemplateByID(ctx, v.TemplateID.UUID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := q.authorizeContext(ctx, rbac.ActionUpdate, tpl); err != nil {
|
||||
return err
|
||||
}
|
||||
return q.db.UnarchiveTemplateVersion(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateAPIKeyByID(ctx context.Context, arg database.UpdateAPIKeyByIDParams) error {
|
||||
fetch := func(ctx context.Context, arg database.UpdateAPIKeyByIDParams) (database.APIKey, error) {
|
||||
return q.db.GetAPIKeyByID(ctx, arg.ID)
|
||||
|
|
|
@ -342,6 +342,41 @@ func (s *MethodTestSuite) TestGroup() {
|
|||
}
|
||||
|
||||
func (s *MethodTestSuite) TestProvsionerJob() {
|
||||
s.Run("ArchiveUnusedTemplateVersions", s.Subtest(func(db database.Store, check *expects) {
|
||||
j := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{
|
||||
Type: database.ProvisionerJobTypeTemplateVersionImport,
|
||||
Error: sql.NullString{
|
||||
String: "failed",
|
||||
Valid: true,
|
||||
},
|
||||
})
|
||||
tpl := dbgen.Template(s.T(), db, database.Template{})
|
||||
v := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{
|
||||
TemplateID: uuid.NullUUID{UUID: tpl.ID, Valid: true},
|
||||
JobID: j.ID,
|
||||
})
|
||||
check.Args(database.ArchiveUnusedTemplateVersionsParams{
|
||||
UpdatedAt: dbtime.Now(),
|
||||
TemplateID: tpl.ID,
|
||||
TemplateVersionID: uuid.Nil,
|
||||
JobStatus: database.NullProvisionerJobStatus{},
|
||||
}).Asserts(v.RBACObject(tpl), rbac.ActionUpdate)
|
||||
}))
|
||||
s.Run("UnarchiveTemplateVersion", s.Subtest(func(db database.Store, check *expects) {
|
||||
j := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{
|
||||
Type: database.ProvisionerJobTypeTemplateVersionImport,
|
||||
})
|
||||
tpl := dbgen.Template(s.T(), db, database.Template{})
|
||||
v := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{
|
||||
TemplateID: uuid.NullUUID{UUID: tpl.ID, Valid: true},
|
||||
JobID: j.ID,
|
||||
Archived: true,
|
||||
})
|
||||
check.Args(database.UnarchiveTemplateVersionParams{
|
||||
UpdatedAt: dbtime.Now(),
|
||||
TemplateVersionID: v.ID,
|
||||
}).Asserts(v.RBACObject(tpl), rbac.ActionUpdate)
|
||||
}))
|
||||
s.Run("Build/GetProvisionerJobByID", s.Subtest(func(db database.Store, check *expects) {
|
||||
w := dbgen.Workspace(s.T(), db, database.Workspace{})
|
||||
j := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{
|
||||
|
|
|
@ -846,6 +846,82 @@ func (q *FakeQuerier) AllUserIDs(_ context.Context) ([]uuid.UUID, error) {
|
|||
return userIDs, nil
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) ArchiveUnusedTemplateVersions(_ context.Context, arg database.ArchiveUnusedTemplateVersionsParams) ([]uuid.UUID, error) {
|
||||
err := validateDatabaseType(arg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
type latestBuild struct {
|
||||
Number int32
|
||||
Version uuid.UUID
|
||||
}
|
||||
latest := make(map[uuid.UUID]latestBuild)
|
||||
|
||||
for _, b := range q.workspaceBuilds {
|
||||
v, ok := latest[b.WorkspaceID]
|
||||
if ok || b.BuildNumber < v.Number {
|
||||
// Not the latest
|
||||
continue
|
||||
}
|
||||
// Ignore deleted workspaces.
|
||||
if b.Transition == database.WorkspaceTransitionDelete {
|
||||
continue
|
||||
}
|
||||
latest[b.WorkspaceID] = latestBuild{
|
||||
Number: b.BuildNumber,
|
||||
Version: b.TemplateVersionID,
|
||||
}
|
||||
}
|
||||
|
||||
usedVersions := make(map[uuid.UUID]bool)
|
||||
for _, l := range latest {
|
||||
usedVersions[l.Version] = true
|
||||
}
|
||||
for _, tpl := range q.templates {
|
||||
usedVersions[tpl.ActiveVersionID] = true
|
||||
}
|
||||
|
||||
var archived []uuid.UUID
|
||||
for i, v := range q.templateVersions {
|
||||
if arg.TemplateVersionID != uuid.Nil {
|
||||
if v.ID != arg.TemplateVersionID {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if v.Archived {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := usedVersions[v.ID]; !ok {
|
||||
var job *database.ProvisionerJob
|
||||
for i, j := range q.provisionerJobs {
|
||||
if v.JobID == j.ID {
|
||||
job = &q.provisionerJobs[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if arg.JobStatus.Valid {
|
||||
if job.JobStatus != arg.JobStatus.ProvisionerJobStatus {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if job.JobStatus == database.ProvisionerJobStatusRunning || job.JobStatus == database.ProvisionerJobStatusPending {
|
||||
continue
|
||||
}
|
||||
|
||||
v.Archived = true
|
||||
q.templateVersions[i] = v
|
||||
archived = append(archived, v.ID)
|
||||
}
|
||||
}
|
||||
|
||||
return archived, nil
|
||||
}
|
||||
|
||||
func (*FakeQuerier) CleanTailnetCoordinators(_ context.Context) error {
|
||||
return ErrUnimplemented
|
||||
}
|
||||
|
@ -2759,6 +2835,9 @@ func (q *FakeQuerier) GetTemplateVersionsByTemplateID(_ context.Context, arg dat
|
|||
if templateVersion.TemplateID.UUID != arg.TemplateID {
|
||||
continue
|
||||
}
|
||||
if arg.Archived.Valid && arg.Archived.Bool != templateVersion.Archived {
|
||||
continue
|
||||
}
|
||||
version = append(version, q.templateVersionWithUserNoLock(templateVersion))
|
||||
}
|
||||
|
||||
|
@ -5261,6 +5340,24 @@ func (*FakeQuerier) TryAcquireLock(_ context.Context, _ int64) (bool, error) {
|
|||
return false, xerrors.New("TryAcquireLock must only be called within a transaction")
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) UnarchiveTemplateVersion(_ context.Context, arg database.UnarchiveTemplateVersionParams) error {
|
||||
err := validateDatabaseType(arg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i, v := range q.data.templateVersions {
|
||||
if v.ID == arg.TemplateVersionID {
|
||||
v.Archived = false
|
||||
v.UpdatedAt = arg.UpdatedAt
|
||||
q.data.templateVersions[i] = v
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) UpdateAPIKeyByID(_ context.Context, arg database.UpdateAPIKeyByIDParams) error {
|
||||
if err := validateDatabaseType(arg); err != nil {
|
||||
return err
|
||||
|
|
|
@ -107,6 +107,13 @@ func (m metricsStore) AllUserIDs(ctx context.Context) ([]uuid.UUID, error) {
|
|||
return r0, r1
|
||||
}
|
||||
|
||||
func (m metricsStore) ArchiveUnusedTemplateVersions(ctx context.Context, arg database.ArchiveUnusedTemplateVersionsParams) ([]uuid.UUID, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.ArchiveUnusedTemplateVersions(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("ArchiveUnusedTemplateVersions").Observe(time.Since(start).Seconds())
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m metricsStore) CleanTailnetCoordinators(ctx context.Context) error {
|
||||
start := time.Now()
|
||||
err := m.s.CleanTailnetCoordinators(ctx)
|
||||
|
@ -1432,6 +1439,13 @@ func (m metricsStore) TryAcquireLock(ctx context.Context, pgTryAdvisoryXactLock
|
|||
return ok, err
|
||||
}
|
||||
|
||||
func (m metricsStore) UnarchiveTemplateVersion(ctx context.Context, arg database.UnarchiveTemplateVersionParams) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.UnarchiveTemplateVersion(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("UnarchiveTemplateVersion").Observe(time.Since(start).Seconds())
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m metricsStore) UpdateAPIKeyByID(ctx context.Context, arg database.UpdateAPIKeyByIDParams) error {
|
||||
start := time.Now()
|
||||
err := m.s.UpdateAPIKeyByID(ctx, arg)
|
||||
|
|
|
@ -97,6 +97,21 @@ func (mr *MockStoreMockRecorder) AllUserIDs(arg0 interface{}) *gomock.Call {
|
|||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AllUserIDs", reflect.TypeOf((*MockStore)(nil).AllUserIDs), arg0)
|
||||
}
|
||||
|
||||
// ArchiveUnusedTemplateVersions mocks base method.
|
||||
func (m *MockStore) ArchiveUnusedTemplateVersions(arg0 context.Context, arg1 database.ArchiveUnusedTemplateVersionsParams) ([]uuid.UUID, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ArchiveUnusedTemplateVersions", arg0, arg1)
|
||||
ret0, _ := ret[0].([]uuid.UUID)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// ArchiveUnusedTemplateVersions indicates an expected call of ArchiveUnusedTemplateVersions.
|
||||
func (mr *MockStoreMockRecorder) ArchiveUnusedTemplateVersions(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ArchiveUnusedTemplateVersions", reflect.TypeOf((*MockStore)(nil).ArchiveUnusedTemplateVersions), arg0, arg1)
|
||||
}
|
||||
|
||||
// CleanTailnetCoordinators mocks base method.
|
||||
func (m *MockStore) CleanTailnetCoordinators(arg0 context.Context) error {
|
||||
m.ctrl.T.Helper()
|
||||
|
@ -3024,6 +3039,20 @@ func (mr *MockStoreMockRecorder) TryAcquireLock(arg0, arg1 interface{}) *gomock.
|
|||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TryAcquireLock", reflect.TypeOf((*MockStore)(nil).TryAcquireLock), arg0, arg1)
|
||||
}
|
||||
|
||||
// UnarchiveTemplateVersion mocks base method.
|
||||
func (m *MockStore) UnarchiveTemplateVersion(arg0 context.Context, arg1 database.UnarchiveTemplateVersionParams) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UnarchiveTemplateVersion", arg0, arg1)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// UnarchiveTemplateVersion indicates an expected call of UnarchiveTemplateVersion.
|
||||
func (mr *MockStoreMockRecorder) UnarchiveTemplateVersion(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnarchiveTemplateVersion", reflect.TypeOf((*MockStore)(nil).UnarchiveTemplateVersion), arg0, arg1)
|
||||
}
|
||||
|
||||
// UpdateAPIKeyByID mocks base method.
|
||||
func (m *MockStore) UpdateAPIKeyByID(arg0 context.Context, arg1 database.UpdateAPIKeyByIDParams) error {
|
||||
m.ctrl.T.Helper()
|
||||
|
|
|
@ -676,7 +676,8 @@ CREATE TABLE template_versions (
|
|||
job_id uuid NOT NULL,
|
||||
created_by uuid NOT NULL,
|
||||
external_auth_providers text[],
|
||||
message character varying(1048576) DEFAULT ''::character varying NOT NULL
|
||||
message character varying(1048576) DEFAULT ''::character varying NOT NULL,
|
||||
archived boolean DEFAULT false NOT NULL
|
||||
);
|
||||
|
||||
COMMENT ON COLUMN template_versions.external_auth_providers IS 'IDs of External auth providers for a specific template version';
|
||||
|
@ -721,6 +722,7 @@ CREATE VIEW template_version_with_user AS
|
|||
template_versions.created_by,
|
||||
template_versions.external_auth_providers,
|
||||
template_versions.message,
|
||||
template_versions.archived,
|
||||
COALESCE(visible_users.avatar_url, ''::text) AS created_by_avatar_url,
|
||||
COALESCE(visible_users.username, ''::text) AS created_by_username
|
||||
FROM (public.template_versions
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
BEGIN;
|
||||
|
||||
-- The view will be rebuilt with the new column
|
||||
DROP VIEW template_version_with_user;
|
||||
|
||||
ALTER TABLE template_versions
|
||||
DROP COLUMN archived;
|
||||
|
||||
-- Restore the old version of the template_version_with_user view.
|
||||
CREATE VIEW
|
||||
template_version_with_user
|
||||
AS
|
||||
SELECT
|
||||
template_versions.*,
|
||||
coalesce(visible_users.avatar_url, '') AS created_by_avatar_url,
|
||||
coalesce(visible_users.username, '') AS created_by_username
|
||||
FROM
|
||||
template_versions
|
||||
LEFT JOIN
|
||||
visible_users
|
||||
ON
|
||||
template_versions.created_by = visible_users.id;
|
||||
|
||||
COMMENT ON VIEW template_version_with_user IS 'Joins in the username + avatar url of the created by user.';
|
||||
|
||||
COMMIT;
|
|
@ -0,0 +1,27 @@
|
|||
BEGIN;
|
||||
|
||||
-- The view will be rebuilt with the new column
|
||||
DROP VIEW template_version_with_user;
|
||||
|
||||
-- Archived template versions are not visible or usable by default.
|
||||
ALTER TABLE template_versions
|
||||
ADD COLUMN archived BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
|
||||
-- Restore the old version of the template_version_with_user view.
|
||||
CREATE VIEW
|
||||
template_version_with_user
|
||||
AS
|
||||
SELECT
|
||||
template_versions.*,
|
||||
coalesce(visible_users.avatar_url, '') AS created_by_avatar_url,
|
||||
coalesce(visible_users.username, '') AS created_by_username
|
||||
FROM
|
||||
template_versions
|
||||
LEFT JOIN
|
||||
visible_users
|
||||
ON
|
||||
template_versions.created_by = visible_users.id;
|
||||
|
||||
COMMENT ON VIEW template_version_with_user IS 'Joins in the username + avatar url of the created by user.';
|
||||
|
||||
COMMIT;
|
|
@ -1942,6 +1942,7 @@ type TemplateVersion struct {
|
|||
CreatedBy uuid.UUID `db:"created_by" json:"created_by"`
|
||||
ExternalAuthProviders []string `db:"external_auth_providers" json:"external_auth_providers"`
|
||||
Message string `db:"message" json:"message"`
|
||||
Archived bool `db:"archived" json:"archived"`
|
||||
CreatedByAvatarURL sql.NullString `db:"created_by_avatar_url" json:"created_by_avatar_url"`
|
||||
CreatedByUsername string `db:"created_by_username" json:"created_by_username"`
|
||||
}
|
||||
|
@ -1995,7 +1996,8 @@ type TemplateVersionTable struct {
|
|||
// IDs of External auth providers for a specific template version
|
||||
ExternalAuthProviders []string `db:"external_auth_providers" json:"external_auth_providers"`
|
||||
// Message describing the changes in this version of the template, similar to a Git commit message. Like a commit message, this should be a short, high-level description of the changes in this version of the template. This message is immutable and should not be updated after the fact.
|
||||
Message string `db:"message" json:"message"`
|
||||
Message string `db:"message" json:"message"`
|
||||
Archived bool `db:"archived" json:"archived"`
|
||||
}
|
||||
|
||||
type TemplateVersionVariable struct {
|
||||
|
|
|
@ -33,6 +33,12 @@ type sqlcQuerier interface {
|
|||
ActivityBumpWorkspace(ctx context.Context, workspaceID uuid.UUID) error
|
||||
// AllUserIDs returns all UserIDs regardless of user status or deletion.
|
||||
AllUserIDs(ctx context.Context) ([]uuid.UUID, error)
|
||||
// Archiving templates is a soft delete action, so is reversible.
|
||||
// Archiving prevents the version from being used and discovered
|
||||
// by listing.
|
||||
// Only unused template versions will be archived, which are any versions not
|
||||
// referenced by the latest build of a workspace.
|
||||
ArchiveUnusedTemplateVersions(ctx context.Context, arg ArchiveUnusedTemplateVersionsParams) ([]uuid.UUID, error)
|
||||
CleanTailnetCoordinators(ctx context.Context) error
|
||||
DeleteAPIKeyByID(ctx context.Context, id string) error
|
||||
DeleteAPIKeysByUserID(ctx context.Context, userID uuid.UUID) error
|
||||
|
@ -281,6 +287,8 @@ type sqlcQuerier interface {
|
|||
// This must be called from within a transaction. The lock will be automatically
|
||||
// released when the transaction ends.
|
||||
TryAcquireLock(ctx context.Context, pgTryAdvisoryXactLock int64) (bool, error)
|
||||
// This will always work regardless of the current state of the template version.
|
||||
UnarchiveTemplateVersion(ctx context.Context, arg UnarchiveTemplateVersionParams) error
|
||||
UpdateAPIKeyByID(ctx context.Context, arg UpdateAPIKeyByIDParams) error
|
||||
UpdateExternalAuthLink(ctx context.Context, arg UpdateExternalAuthLinkParams) (ExternalAuthLink, error)
|
||||
UpdateGitSSHKey(ctx context.Context, arg UpdateGitSSHKeyParams) (GitSSHKey, error)
|
||||
|
|
|
@ -494,6 +494,165 @@ func TestUserChangeLoginType(t *testing.T) {
|
|||
require.Equal(t, bobExpPass, bob.HashedPassword, "hashed password should not change")
|
||||
}
|
||||
|
||||
type tvArgs struct {
|
||||
Status database.ProvisionerJobStatus
|
||||
// CreateWorkspace is true if we should create a workspace for the template version
|
||||
CreateWorkspace bool
|
||||
WorkspaceTransition database.WorkspaceTransition
|
||||
}
|
||||
|
||||
// createTemplateVersion is a helper function to create a version with its dependencies.
|
||||
func createTemplateVersion(t testing.TB, db database.Store, tpl database.Template, args tvArgs) database.TemplateVersion {
|
||||
t.Helper()
|
||||
version := dbgen.TemplateVersion(t, db, database.TemplateVersion{
|
||||
TemplateID: uuid.NullUUID{
|
||||
UUID: tpl.ID,
|
||||
Valid: true,
|
||||
},
|
||||
OrganizationID: tpl.OrganizationID,
|
||||
CreatedAt: dbtime.Now(),
|
||||
UpdatedAt: dbtime.Now(),
|
||||
CreatedBy: tpl.CreatedBy,
|
||||
})
|
||||
|
||||
earlier := sql.NullTime{
|
||||
Time: dbtime.Now().Add(time.Second * -30),
|
||||
Valid: true,
|
||||
}
|
||||
now := sql.NullTime{
|
||||
Time: dbtime.Now(),
|
||||
Valid: true,
|
||||
}
|
||||
j := database.ProvisionerJob{
|
||||
ID: version.JobID,
|
||||
CreatedAt: earlier.Time,
|
||||
UpdatedAt: earlier.Time,
|
||||
Error: sql.NullString{},
|
||||
OrganizationID: tpl.OrganizationID,
|
||||
InitiatorID: tpl.CreatedBy,
|
||||
Type: database.ProvisionerJobTypeTemplateVersionImport,
|
||||
}
|
||||
|
||||
switch args.Status {
|
||||
case database.ProvisionerJobStatusRunning:
|
||||
j.StartedAt = earlier
|
||||
case database.ProvisionerJobStatusPending:
|
||||
case database.ProvisionerJobStatusFailed:
|
||||
j.StartedAt = earlier
|
||||
j.CompletedAt = now
|
||||
j.Error = sql.NullString{
|
||||
String: "failed",
|
||||
Valid: true,
|
||||
}
|
||||
j.ErrorCode = sql.NullString{
|
||||
String: "failed",
|
||||
Valid: true,
|
||||
}
|
||||
case database.ProvisionerJobStatusSucceeded:
|
||||
j.StartedAt = earlier
|
||||
j.CompletedAt = now
|
||||
default:
|
||||
t.Fatalf("invalid status: %s", args.Status)
|
||||
}
|
||||
|
||||
dbgen.ProvisionerJob(t, db, nil, j)
|
||||
if args.CreateWorkspace {
|
||||
wrk := dbgen.Workspace(t, db, database.Workspace{
|
||||
CreatedAt: time.Time{},
|
||||
UpdatedAt: time.Time{},
|
||||
OwnerID: tpl.CreatedBy,
|
||||
OrganizationID: tpl.OrganizationID,
|
||||
TemplateID: tpl.ID,
|
||||
})
|
||||
trans := database.WorkspaceTransitionStart
|
||||
if args.WorkspaceTransition != "" {
|
||||
trans = args.WorkspaceTransition
|
||||
}
|
||||
buildJob := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
|
||||
Type: database.ProvisionerJobTypeWorkspaceBuild,
|
||||
CompletedAt: now,
|
||||
InitiatorID: tpl.CreatedBy,
|
||||
OrganizationID: tpl.OrganizationID,
|
||||
})
|
||||
dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
|
||||
WorkspaceID: wrk.ID,
|
||||
TemplateVersionID: version.ID,
|
||||
BuildNumber: 1,
|
||||
Transition: trans,
|
||||
InitiatorID: tpl.CreatedBy,
|
||||
JobID: buildJob.ID,
|
||||
})
|
||||
}
|
||||
return version
|
||||
}
|
||||
|
||||
func TestArchiveVersions(t *testing.T) {
|
||||
t.Parallel()
|
||||
if testing.Short() {
|
||||
t.SkipNow()
|
||||
}
|
||||
|
||||
t.Run("ArchiveFailedVersions", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
sqlDB := testSQLDB(t)
|
||||
err := migrations.Up(sqlDB)
|
||||
require.NoError(t, err)
|
||||
db := database.New(sqlDB)
|
||||
ctx := context.Background()
|
||||
|
||||
org := dbgen.Organization(t, db, database.Organization{})
|
||||
user := dbgen.User(t, db, database.User{})
|
||||
tpl := dbgen.Template(t, db, database.Template{
|
||||
OrganizationID: org.ID,
|
||||
CreatedBy: user.ID,
|
||||
})
|
||||
// Create some versions
|
||||
failed := createTemplateVersion(t, db, tpl, tvArgs{
|
||||
Status: database.ProvisionerJobStatusFailed,
|
||||
CreateWorkspace: false,
|
||||
})
|
||||
unused := createTemplateVersion(t, db, tpl, tvArgs{
|
||||
Status: database.ProvisionerJobStatusSucceeded,
|
||||
CreateWorkspace: false,
|
||||
})
|
||||
createTemplateVersion(t, db, tpl, tvArgs{
|
||||
Status: database.ProvisionerJobStatusSucceeded,
|
||||
CreateWorkspace: true,
|
||||
})
|
||||
deleted := createTemplateVersion(t, db, tpl, tvArgs{
|
||||
Status: database.ProvisionerJobStatusSucceeded,
|
||||
CreateWorkspace: true,
|
||||
WorkspaceTransition: database.WorkspaceTransitionDelete,
|
||||
})
|
||||
|
||||
// Now archive failed versions
|
||||
archived, err := db.ArchiveUnusedTemplateVersions(ctx, database.ArchiveUnusedTemplateVersionsParams{
|
||||
UpdatedAt: dbtime.Now(),
|
||||
TemplateID: tpl.ID,
|
||||
// All versions
|
||||
TemplateVersionID: uuid.Nil,
|
||||
JobStatus: database.NullProvisionerJobStatus{
|
||||
ProvisionerJobStatus: database.ProvisionerJobStatusFailed,
|
||||
Valid: true,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err, "archive failed versions")
|
||||
require.Len(t, archived, 1, "should only archive one version")
|
||||
require.Equal(t, failed.ID, archived[0], "should archive failed version")
|
||||
|
||||
// Archive all unused versions
|
||||
archived, err = db.ArchiveUnusedTemplateVersions(ctx, database.ArchiveUnusedTemplateVersionsParams{
|
||||
UpdatedAt: dbtime.Now(),
|
||||
TemplateID: tpl.ID,
|
||||
// All versions
|
||||
TemplateVersionID: uuid.Nil,
|
||||
})
|
||||
require.NoError(t, err, "archive failed versions")
|
||||
require.Len(t, archived, 2)
|
||||
require.ElementsMatch(t, []uuid.UUID{deleted.ID, unused.ID}, archived, "should archive unused versions")
|
||||
})
|
||||
}
|
||||
|
||||
func requireUsersMatch(t testing.TB, expected []database.User, found []database.GetUsersRow, msg string) {
|
||||
t.Helper()
|
||||
require.ElementsMatch(t, expected, database.ConvertUserRows(found), msg)
|
||||
|
|
|
@ -5319,9 +5319,123 @@ func (q *sqlQuerier) InsertTemplateVersionParameter(ctx context.Context, arg Ins
|
|||
return i, err
|
||||
}
|
||||
|
||||
const archiveUnusedTemplateVersions = `-- name: ArchiveUnusedTemplateVersions :many
|
||||
UPDATE
|
||||
template_versions
|
||||
SET
|
||||
archived = true,
|
||||
updated_at = $1
|
||||
FROM
|
||||
-- Archive all versions that are returned from this query.
|
||||
(
|
||||
SELECT
|
||||
scoped_template_versions.id
|
||||
FROM
|
||||
-- Scope an archive to a single template and ignore already archived template versions
|
||||
(
|
||||
SELECT
|
||||
id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by, external_auth_providers, message, archived
|
||||
FROM
|
||||
template_versions
|
||||
WHERE
|
||||
template_versions.template_id = $2 :: uuid
|
||||
AND
|
||||
archived = false
|
||||
AND
|
||||
-- This allows archiving a specific template version.
|
||||
CASE
|
||||
WHEN $3::uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
|
||||
template_versions.id = $3 :: uuid
|
||||
ELSE
|
||||
true
|
||||
END
|
||||
) AS scoped_template_versions
|
||||
LEFT JOIN
|
||||
provisioner_jobs ON scoped_template_versions.job_id = provisioner_jobs.id
|
||||
LEFT JOIN
|
||||
templates ON scoped_template_versions.template_id = templates.id
|
||||
WHERE
|
||||
-- Actively used template versions (meaning the latest build is using
|
||||
-- the version) are never archived. A "restart" command on the workspace,
|
||||
-- even if failed, would use the version. So it cannot be archived until
|
||||
-- the build is outdated.
|
||||
NOT EXISTS (
|
||||
-- Return all "used" versions, where "used" is defined as being
|
||||
-- used by a latest workspace build.
|
||||
SELECT template_version_id FROM (
|
||||
SELECT
|
||||
DISTINCT ON (workspace_id) template_version_id, transition
|
||||
FROM
|
||||
workspace_builds
|
||||
ORDER BY workspace_id, build_number DESC
|
||||
) AS used_versions
|
||||
WHERE
|
||||
used_versions.transition != 'delete'
|
||||
AND
|
||||
scoped_template_versions.id = used_versions.template_version_id
|
||||
)
|
||||
-- Also never archive the active template version
|
||||
AND active_version_id != scoped_template_versions.id
|
||||
AND CASE
|
||||
-- Optionally, only archive versions that match a given
|
||||
-- job status like 'failed'.
|
||||
WHEN $4 :: provisioner_job_status IS NOT NULL THEN
|
||||
provisioner_jobs.job_status = $4 :: provisioner_job_status
|
||||
ELSE
|
||||
true
|
||||
END
|
||||
-- Pending or running jobs should not be archived, as they are "in progress"
|
||||
AND provisioner_jobs.job_status != 'running'
|
||||
AND provisioner_jobs.job_status != 'pending'
|
||||
) AS archived_versions
|
||||
WHERE
|
||||
template_versions.id IN (archived_versions.id)
|
||||
RETURNING template_versions.id
|
||||
`
|
||||
|
||||
type ArchiveUnusedTemplateVersionsParams struct {
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
|
||||
TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
|
||||
JobStatus NullProvisionerJobStatus `db:"job_status" json:"job_status"`
|
||||
}
|
||||
|
||||
// Archiving templates is a soft delete action, so is reversible.
|
||||
// Archiving prevents the version from being used and discovered
|
||||
// by listing.
|
||||
// Only unused template versions will be archived, which are any versions not
|
||||
// referenced by the latest build of a workspace.
|
||||
func (q *sqlQuerier) ArchiveUnusedTemplateVersions(ctx context.Context, arg ArchiveUnusedTemplateVersionsParams) ([]uuid.UUID, error) {
|
||||
rows, err := q.db.QueryContext(ctx, archiveUnusedTemplateVersions,
|
||||
arg.UpdatedAt,
|
||||
arg.TemplateID,
|
||||
arg.TemplateVersionID,
|
||||
arg.JobStatus,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []uuid.UUID
|
||||
for rows.Next() {
|
||||
var id uuid.UUID
|
||||
if err := rows.Scan(&id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, id)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getPreviousTemplateVersion = `-- name: GetPreviousTemplateVersion :one
|
||||
SELECT
|
||||
id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by, external_auth_providers, message, created_by_avatar_url, created_by_username
|
||||
id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by, external_auth_providers, message, archived, created_by_avatar_url, created_by_username
|
||||
FROM
|
||||
template_version_with_user AS template_versions
|
||||
WHERE
|
||||
|
@ -5357,6 +5471,7 @@ func (q *sqlQuerier) GetPreviousTemplateVersion(ctx context.Context, arg GetPrev
|
|||
&i.CreatedBy,
|
||||
pq.Array(&i.ExternalAuthProviders),
|
||||
&i.Message,
|
||||
&i.Archived,
|
||||
&i.CreatedByAvatarURL,
|
||||
&i.CreatedByUsername,
|
||||
)
|
||||
|
@ -5365,7 +5480,7 @@ func (q *sqlQuerier) GetPreviousTemplateVersion(ctx context.Context, arg GetPrev
|
|||
|
||||
const getTemplateVersionByID = `-- name: GetTemplateVersionByID :one
|
||||
SELECT
|
||||
id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by, external_auth_providers, message, created_by_avatar_url, created_by_username
|
||||
id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by, external_auth_providers, message, archived, created_by_avatar_url, created_by_username
|
||||
FROM
|
||||
template_version_with_user AS template_versions
|
||||
WHERE
|
||||
|
@ -5387,6 +5502,7 @@ func (q *sqlQuerier) GetTemplateVersionByID(ctx context.Context, id uuid.UUID) (
|
|||
&i.CreatedBy,
|
||||
pq.Array(&i.ExternalAuthProviders),
|
||||
&i.Message,
|
||||
&i.Archived,
|
||||
&i.CreatedByAvatarURL,
|
||||
&i.CreatedByUsername,
|
||||
)
|
||||
|
@ -5395,7 +5511,7 @@ func (q *sqlQuerier) GetTemplateVersionByID(ctx context.Context, id uuid.UUID) (
|
|||
|
||||
const getTemplateVersionByJobID = `-- name: GetTemplateVersionByJobID :one
|
||||
SELECT
|
||||
id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by, external_auth_providers, message, created_by_avatar_url, created_by_username
|
||||
id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by, external_auth_providers, message, archived, created_by_avatar_url, created_by_username
|
||||
FROM
|
||||
template_version_with_user AS template_versions
|
||||
WHERE
|
||||
|
@ -5417,6 +5533,7 @@ func (q *sqlQuerier) GetTemplateVersionByJobID(ctx context.Context, jobID uuid.U
|
|||
&i.CreatedBy,
|
||||
pq.Array(&i.ExternalAuthProviders),
|
||||
&i.Message,
|
||||
&i.Archived,
|
||||
&i.CreatedByAvatarURL,
|
||||
&i.CreatedByUsername,
|
||||
)
|
||||
|
@ -5425,7 +5542,7 @@ func (q *sqlQuerier) GetTemplateVersionByJobID(ctx context.Context, jobID uuid.U
|
|||
|
||||
const getTemplateVersionByTemplateIDAndName = `-- name: GetTemplateVersionByTemplateIDAndName :one
|
||||
SELECT
|
||||
id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by, external_auth_providers, message, created_by_avatar_url, created_by_username
|
||||
id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by, external_auth_providers, message, archived, created_by_avatar_url, created_by_username
|
||||
FROM
|
||||
template_version_with_user AS template_versions
|
||||
WHERE
|
||||
|
@ -5453,6 +5570,7 @@ func (q *sqlQuerier) GetTemplateVersionByTemplateIDAndName(ctx context.Context,
|
|||
&i.CreatedBy,
|
||||
pq.Array(&i.ExternalAuthProviders),
|
||||
&i.Message,
|
||||
&i.Archived,
|
||||
&i.CreatedByAvatarURL,
|
||||
&i.CreatedByUsername,
|
||||
)
|
||||
|
@ -5461,7 +5579,7 @@ func (q *sqlQuerier) GetTemplateVersionByTemplateIDAndName(ctx context.Context,
|
|||
|
||||
const getTemplateVersionsByIDs = `-- name: GetTemplateVersionsByIDs :many
|
||||
SELECT
|
||||
id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by, external_auth_providers, message, created_by_avatar_url, created_by_username
|
||||
id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by, external_auth_providers, message, archived, created_by_avatar_url, created_by_username
|
||||
FROM
|
||||
template_version_with_user AS template_versions
|
||||
WHERE
|
||||
|
@ -5489,6 +5607,7 @@ func (q *sqlQuerier) GetTemplateVersionsByIDs(ctx context.Context, ids []uuid.UU
|
|||
&i.CreatedBy,
|
||||
pq.Array(&i.ExternalAuthProviders),
|
||||
&i.Message,
|
||||
&i.Archived,
|
||||
&i.CreatedByAvatarURL,
|
||||
&i.CreatedByUsername,
|
||||
); err != nil {
|
||||
|
@ -5507,16 +5626,23 @@ func (q *sqlQuerier) GetTemplateVersionsByIDs(ctx context.Context, ids []uuid.UU
|
|||
|
||||
const getTemplateVersionsByTemplateID = `-- name: GetTemplateVersionsByTemplateID :many
|
||||
SELECT
|
||||
id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by, external_auth_providers, message, created_by_avatar_url, created_by_username
|
||||
id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by, external_auth_providers, message, archived, created_by_avatar_url, created_by_username
|
||||
FROM
|
||||
template_version_with_user AS template_versions
|
||||
WHERE
|
||||
template_id = $1 :: uuid
|
||||
AND CASE
|
||||
-- If no filter is provided, default to returning ALL template versions.
|
||||
-- The called should always provide a filter if they want to omit
|
||||
-- archived versions.
|
||||
WHEN $2 :: boolean IS NULL THEN true
|
||||
ELSE template_versions.archived = $2 :: boolean
|
||||
END
|
||||
AND CASE
|
||||
-- This allows using the last element on a page as effectively a cursor.
|
||||
-- This is an important option for scripts that need to paginate without
|
||||
-- duplicating or missing data.
|
||||
WHEN $2 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN (
|
||||
WHEN $3 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN (
|
||||
-- The pagination cursor is the last ID of the previous page.
|
||||
-- The query is ordered by the created_at field, so select all
|
||||
-- rows after the cursor.
|
||||
|
@ -5526,7 +5652,7 @@ WHERE
|
|||
FROM
|
||||
template_versions
|
||||
WHERE
|
||||
id = $2
|
||||
id = $3
|
||||
)
|
||||
)
|
||||
ELSE true
|
||||
|
@ -5534,22 +5660,24 @@ WHERE
|
|||
ORDER BY
|
||||
-- Deterministic and consistent ordering of all rows, even if they share
|
||||
-- a timestamp. This is to ensure consistent pagination.
|
||||
(created_at, id) ASC OFFSET $3
|
||||
(created_at, id) ASC OFFSET $4
|
||||
LIMIT
|
||||
-- A null limit means "no limit", so 0 means return all
|
||||
NULLIF($4 :: int, 0)
|
||||
NULLIF($5 :: int, 0)
|
||||
`
|
||||
|
||||
type GetTemplateVersionsByTemplateIDParams struct {
|
||||
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
|
||||
AfterID uuid.UUID `db:"after_id" json:"after_id"`
|
||||
OffsetOpt int32 `db:"offset_opt" json:"offset_opt"`
|
||||
LimitOpt int32 `db:"limit_opt" json:"limit_opt"`
|
||||
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
|
||||
Archived sql.NullBool `db:"archived" json:"archived"`
|
||||
AfterID uuid.UUID `db:"after_id" json:"after_id"`
|
||||
OffsetOpt int32 `db:"offset_opt" json:"offset_opt"`
|
||||
LimitOpt int32 `db:"limit_opt" json:"limit_opt"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) GetTemplateVersionsByTemplateID(ctx context.Context, arg GetTemplateVersionsByTemplateIDParams) ([]TemplateVersion, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getTemplateVersionsByTemplateID,
|
||||
arg.TemplateID,
|
||||
arg.Archived,
|
||||
arg.AfterID,
|
||||
arg.OffsetOpt,
|
||||
arg.LimitOpt,
|
||||
|
@ -5573,6 +5701,7 @@ func (q *sqlQuerier) GetTemplateVersionsByTemplateID(ctx context.Context, arg Ge
|
|||
&i.CreatedBy,
|
||||
pq.Array(&i.ExternalAuthProviders),
|
||||
&i.Message,
|
||||
&i.Archived,
|
||||
&i.CreatedByAvatarURL,
|
||||
&i.CreatedByUsername,
|
||||
); err != nil {
|
||||
|
@ -5590,7 +5719,7 @@ func (q *sqlQuerier) GetTemplateVersionsByTemplateID(ctx context.Context, arg Ge
|
|||
}
|
||||
|
||||
const getTemplateVersionsCreatedAfter = `-- name: GetTemplateVersionsCreatedAfter :many
|
||||
SELECT id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by, external_auth_providers, message, created_by_avatar_url, created_by_username FROM template_version_with_user AS template_versions WHERE created_at > $1
|
||||
SELECT id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by, external_auth_providers, message, archived, created_by_avatar_url, created_by_username FROM template_version_with_user AS template_versions WHERE created_at > $1
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetTemplateVersionsCreatedAfter(ctx context.Context, createdAt time.Time) ([]TemplateVersion, error) {
|
||||
|
@ -5614,6 +5743,7 @@ func (q *sqlQuerier) GetTemplateVersionsCreatedAfter(ctx context.Context, create
|
|||
&i.CreatedBy,
|
||||
pq.Array(&i.ExternalAuthProviders),
|
||||
&i.Message,
|
||||
&i.Archived,
|
||||
&i.CreatedByAvatarURL,
|
||||
&i.CreatedByUsername,
|
||||
); err != nil {
|
||||
|
@ -5677,6 +5807,27 @@ func (q *sqlQuerier) InsertTemplateVersion(ctx context.Context, arg InsertTempla
|
|||
return err
|
||||
}
|
||||
|
||||
const unarchiveTemplateVersion = `-- name: UnarchiveTemplateVersion :exec
|
||||
UPDATE
|
||||
template_versions
|
||||
SET
|
||||
archived = false,
|
||||
updated_at = $1
|
||||
WHERE
|
||||
id = $2
|
||||
`
|
||||
|
||||
type UnarchiveTemplateVersionParams struct {
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
|
||||
}
|
||||
|
||||
// This will always work regardless of the current state of the template version.
|
||||
func (q *sqlQuerier) UnarchiveTemplateVersion(ctx context.Context, arg UnarchiveTemplateVersionParams) error {
|
||||
_, err := q.db.ExecContext(ctx, unarchiveTemplateVersion, arg.UpdatedAt, arg.TemplateVersionID)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateTemplateVersionByID = `-- name: UpdateTemplateVersionByID :exec
|
||||
UPDATE
|
||||
template_versions
|
||||
|
|
|
@ -5,6 +5,13 @@ FROM
|
|||
template_version_with_user AS template_versions
|
||||
WHERE
|
||||
template_id = @template_id :: uuid
|
||||
AND CASE
|
||||
-- If no filter is provided, default to returning ALL template versions.
|
||||
-- The called should always provide a filter if they want to omit
|
||||
-- archived versions.
|
||||
WHEN sqlc.narg('archived') :: boolean IS NULL THEN true
|
||||
ELSE template_versions.archived = sqlc.narg('archived') :: boolean
|
||||
END
|
||||
AND CASE
|
||||
-- This allows using the last element on a page as effectively a cursor.
|
||||
-- This is an important option for scripts that need to paginate without
|
||||
|
@ -129,3 +136,91 @@ WHERE
|
|||
AND template_id = $3
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1;
|
||||
|
||||
-- name: UnarchiveTemplateVersion :exec
|
||||
-- This will always work regardless of the current state of the template version.
|
||||
UPDATE
|
||||
template_versions
|
||||
SET
|
||||
archived = false,
|
||||
updated_at = sqlc.arg('updated_at')
|
||||
WHERE
|
||||
id = sqlc.arg('template_version_id');
|
||||
|
||||
-- name: ArchiveUnusedTemplateVersions :many
|
||||
-- Archiving templates is a soft delete action, so is reversible.
|
||||
-- Archiving prevents the version from being used and discovered
|
||||
-- by listing.
|
||||
-- Only unused template versions will be archived, which are any versions not
|
||||
-- referenced by the latest build of a workspace.
|
||||
UPDATE
|
||||
template_versions
|
||||
SET
|
||||
archived = true,
|
||||
updated_at = sqlc.arg('updated_at')
|
||||
FROM
|
||||
-- Archive all versions that are returned from this query.
|
||||
(
|
||||
SELECT
|
||||
scoped_template_versions.id
|
||||
FROM
|
||||
-- Scope an archive to a single template and ignore already archived template versions
|
||||
(
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
template_versions
|
||||
WHERE
|
||||
template_versions.template_id = sqlc.arg('template_id') :: uuid
|
||||
AND
|
||||
archived = false
|
||||
AND
|
||||
-- This allows archiving a specific template version.
|
||||
CASE
|
||||
WHEN sqlc.arg('template_version_id')::uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
|
||||
template_versions.id = sqlc.arg('template_version_id') :: uuid
|
||||
ELSE
|
||||
true
|
||||
END
|
||||
) AS scoped_template_versions
|
||||
LEFT JOIN
|
||||
provisioner_jobs ON scoped_template_versions.job_id = provisioner_jobs.id
|
||||
LEFT JOIN
|
||||
templates ON scoped_template_versions.template_id = templates.id
|
||||
WHERE
|
||||
-- Actively used template versions (meaning the latest build is using
|
||||
-- the version) are never archived. A "restart" command on the workspace,
|
||||
-- even if failed, would use the version. So it cannot be archived until
|
||||
-- the build is outdated.
|
||||
NOT EXISTS (
|
||||
-- Return all "used" versions, where "used" is defined as being
|
||||
-- used by a latest workspace build.
|
||||
SELECT template_version_id FROM (
|
||||
SELECT
|
||||
DISTINCT ON (workspace_id) template_version_id, transition
|
||||
FROM
|
||||
workspace_builds
|
||||
ORDER BY workspace_id, build_number DESC
|
||||
) AS used_versions
|
||||
WHERE
|
||||
used_versions.transition != 'delete'
|
||||
AND
|
||||
scoped_template_versions.id = used_versions.template_version_id
|
||||
)
|
||||
-- Also never archive the active template version
|
||||
AND active_version_id != scoped_template_versions.id
|
||||
AND CASE
|
||||
-- Optionally, only archive versions that match a given
|
||||
-- job status like 'failed'.
|
||||
WHEN sqlc.narg('job_status') :: provisioner_job_status IS NOT NULL THEN
|
||||
provisioner_jobs.job_status = sqlc.narg('job_status') :: provisioner_job_status
|
||||
ELSE
|
||||
true
|
||||
END
|
||||
-- Pending or running jobs should not be archived, as they are "in progress"
|
||||
AND provisioner_jobs.job_status != 'running'
|
||||
AND provisioner_jobs.job_status != 'pending'
|
||||
) AS archived_versions
|
||||
WHERE
|
||||
template_versions.id IN (archived_versions.id)
|
||||
RETURNING template_versions.id;
|
||||
|
|
|
@ -16,7 +16,7 @@ We track the following resources:
|
|||
| GitSSHKey<br><i>create</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>false</td></tr><tr><td>private_key</td><td>true</td></tr><tr><td>public_key</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
|
||||
| License<br><i>create, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>exp</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>jwt</td><td>false</td></tr><tr><td>uploaded_at</td><td>true</td></tr><tr><td>uuid</td><td>true</td></tr></tbody></table> |
|
||||
| Template<br><i>write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>active_version_id</td><td>true</td></tr><tr><td>allow_user_autostart</td><td>true</td></tr><tr><td>allow_user_autostop</td><td>true</td></tr><tr><td>allow_user_cancel_workspace_jobs</td><td>true</td></tr><tr><td>autostop_requirement_days_of_week</td><td>true</td></tr><tr><td>autostop_requirement_weeks</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>created_by_avatar_url</td><td>false</td></tr><tr><td>created_by_username</td><td>false</td></tr><tr><td>default_ttl</td><td>true</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>description</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>failure_ttl</td><td>true</td></tr><tr><td>group_acl</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>max_ttl</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>provisioner</td><td>true</td></tr><tr><td>time_til_dormant</td><td>true</td></tr><tr><td>time_til_dormant_autodelete</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_acl</td><td>true</td></tr></tbody></table> |
|
||||
| TemplateVersion<br><i>create, write</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>created_by_avatar_url</td><td>false</td></tr><tr><td>created_by_username</td><td>false</td></tr><tr><td>external_auth_providers</td><td>false</td></tr><tr><td>id</td><td>true</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>message</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>readme</td><td>true</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
|
||||
| TemplateVersion<br><i>create, write</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>archived</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>created_by_avatar_url</td><td>false</td></tr><tr><td>created_by_username</td><td>false</td></tr><tr><td>external_auth_providers</td><td>false</td></tr><tr><td>id</td><td>true</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>message</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>readme</td><td>true</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
|
||||
| User<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>avatar_url</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>true</td></tr><tr><td>email</td><td>true</td></tr><tr><td>hashed_password</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>last_seen_at</td><td>false</td></tr><tr><td>login_type</td><td>true</td></tr><tr><td>quiet_hours_schedule</td><td>true</td></tr><tr><td>rbac_roles</td><td>true</td></tr><tr><td>status</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>username</td><td>true</td></tr></tbody></table> |
|
||||
| Workspace<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>automatic_updates</td><td>true</td></tr><tr><td>autostart_schedule</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>deleting_at</td><td>true</td></tr><tr><td>dormant_at</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>last_used_at</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>owner_id</td><td>true</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>ttl</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
|
||||
| WorkspaceBuild<br><i>start, stop</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>build_number</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>daily_cost</td><td>false</td></tr><tr><td>deadline</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>initiator_by_avatar_url</td><td>false</td></tr><tr><td>initiator_by_username</td><td>false</td></tr><tr><td>initiator_id</td><td>false</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>max_deadline</td><td>false</td></tr><tr><td>provisioner_state</td><td>false</td></tr><tr><td>reason</td><td>false</td></tr><tr><td>template_version_id</td><td>true</td></tr><tr><td>transition</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>workspace_id</td><td>false</td></tr></tbody></table> |
|
||||
|
|
|
@ -99,6 +99,7 @@ var auditableResourcesTypes = map[any]map[string]Action{
|
|||
"external_auth_providers": ActionIgnore, // Not helpful because this can only change when new versions are added.
|
||||
"created_by_avatar_url": ActionIgnore,
|
||||
"created_by_username": ActionIgnore,
|
||||
"archived": ActionTrack,
|
||||
},
|
||||
&database.User{}: {
|
||||
"id": ActionTrack,
|
||||
|
|
Loading…
Reference in New Issue