diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index ef2b7b9d18b91..3cc557331d4a5 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -1808,9 +1808,12 @@ func (q *querier) InsertReplica(ctx context.Context, arg database.InsertReplicaP return q.db.InsertReplica(ctx, arg) } -func (q *querier) InsertTemplate(ctx context.Context, arg database.InsertTemplateParams) (database.Template, error) { +func (q *querier) InsertTemplate(ctx context.Context, arg database.InsertTemplateParams) error { obj := rbac.ResourceTemplate.InOrg(arg.OrganizationID) - return insert(q.log, q.auth, obj, q.db.InsertTemplate)(ctx, arg) + if err := q.authorizeContext(ctx, rbac.ActionCreate, obj); err != nil { + return err + } + return q.db.InsertTemplate(ctx, arg) } func (q *querier) InsertTemplateVersion(ctx context.Context, arg database.InsertTemplateVersionParams) (database.TemplateVersion, error) { @@ -2134,13 +2137,13 @@ func (q *querier) UpdateReplica(ctx context.Context, arg database.UpdateReplicaP return q.db.UpdateReplica(ctx, arg) } -func (q *querier) UpdateTemplateACLByID(ctx context.Context, arg database.UpdateTemplateACLByIDParams) (database.Template, error) { - // UpdateTemplateACL uses the ActionCreate action. Only users that can create the template - // may update the ACL. +func (q *querier) UpdateTemplateACLByID(ctx context.Context, arg database.UpdateTemplateACLByIDParams) error { fetch := func(ctx context.Context, arg database.UpdateTemplateACLByIDParams) (database.Template, error) { return q.db.GetTemplateByID(ctx, arg.ID) } - return fetchAndQuery(q.log, q.auth, rbac.ActionCreate, fetch, q.db.UpdateTemplateACLByID)(ctx, arg) + // UpdateTemplateACL uses the ActionCreate action. Only users that can create the template + // may update the ACL. + return fetchAndExec(q.log, q.auth, rbac.ActionCreate, fetch, q.db.UpdateTemplateACLByID)(ctx, arg) } func (q *querier) UpdateTemplateActiveVersionByID(ctx context.Context, arg database.UpdateTemplateActiveVersionByIDParams) error { @@ -2155,18 +2158,18 @@ func (q *querier) UpdateTemplateDeletedByID(ctx context.Context, arg database.Up return q.SoftDeleteTemplateByID(ctx, arg.ID) } -func (q *querier) UpdateTemplateMetaByID(ctx context.Context, arg database.UpdateTemplateMetaByIDParams) (database.Template, error) { +func (q *querier) UpdateTemplateMetaByID(ctx context.Context, arg database.UpdateTemplateMetaByIDParams) error { fetch := func(ctx context.Context, arg database.UpdateTemplateMetaByIDParams) (database.Template, error) { return q.db.GetTemplateByID(ctx, arg.ID) } - return updateWithReturn(q.log, q.auth, fetch, q.db.UpdateTemplateMetaByID)(ctx, arg) + return update(q.log, q.auth, fetch, q.db.UpdateTemplateMetaByID)(ctx, arg) } -func (q *querier) UpdateTemplateScheduleByID(ctx context.Context, arg database.UpdateTemplateScheduleByIDParams) (database.Template, error) { +func (q *querier) UpdateTemplateScheduleByID(ctx context.Context, arg database.UpdateTemplateScheduleByIDParams) error { fetch := func(ctx context.Context, arg database.UpdateTemplateScheduleByIDParams) (database.Template, error) { return q.db.GetTemplateByID(ctx, arg.ID) } - return updateWithReturn(q.log, q.auth, fetch, q.db.UpdateTemplateScheduleByID)(ctx, arg) + return update(q.log, q.auth, fetch, q.db.UpdateTemplateScheduleByID)(ctx, arg) } func (q *querier) UpdateTemplateVersionByID(ctx context.Context, arg database.UpdateTemplateVersionByIDParams) (database.TemplateVersion, error) { diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index b9e42f632bd6c..6a4f09260b7ab 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -772,7 +772,7 @@ func (s *MethodTestSuite) TestTemplate() { t1 := dbgen.Template(s.T(), db, database.Template{}) check.Args(database.UpdateTemplateACLByIDParams{ ID: t1.ID, - }).Asserts(t1, rbac.ActionCreate).Returns(t1) + }).Asserts(t1, rbac.ActionCreate) })) s.Run("UpdateTemplateActiveVersionByID", s.Subtest(func(db database.Store, check *expects) { t1 := dbgen.Template(s.T(), db, database.Template{ diff --git a/coderd/database/dbfake/dbfake.go b/coderd/database/dbfake/dbfake.go index d68d0d08af199..3813e535a8e57 100644 --- a/coderd/database/dbfake/dbfake.go +++ b/coderd/database/dbfake/dbfake.go @@ -58,7 +58,7 @@ func New() database.Store { workspaceResourceMetadata: make([]database.WorkspaceResourceMetadatum, 0), provisionerJobs: make([]database.ProvisionerJob, 0), templateVersions: make([]database.TemplateVersion, 0), - templates: make([]database.Template, 0), + templates: make([]database.TemplateTable, 0), workspaceAgentStats: make([]database.WorkspaceAgentStat, 0), workspaceAgentLogs: make([]database.WorkspaceAgentStartupLog, 0), workspaceBuilds: make([]database.WorkspaceBuild, 0), @@ -130,7 +130,7 @@ type data struct { templateVersions []database.TemplateVersion templateVersionParameters []database.TemplateVersionParameter templateVersionVariables []database.TemplateVersionVariable - templates []database.Template + templates []database.TemplateTable workspaceAgents []database.WorkspaceAgent workspaceAgentMetadata []database.WorkspaceAgentMetadatum workspaceAgentLogs []database.WorkspaceAgentStartupLog @@ -446,12 +446,37 @@ func (q *FakeQuerier) getLatestWorkspaceBuildByWorkspaceIDNoLock(_ context.Conte func (q *FakeQuerier) getTemplateByIDNoLock(_ context.Context, id uuid.UUID) (database.Template, error) { for _, template := range q.templates { if template.ID == id { - return template.DeepCopy(), nil + return q.templateWithUserNoLock(template), nil } } return database.Template{}, sql.ErrNoRows } +func (q *FakeQuerier) templatesWithUserNoLock(tpl []database.TemplateTable) []database.Template { + cpy := make([]database.Template, 0, len(tpl)) + for _, t := range tpl { + cpy = append(cpy, q.templateWithUserNoLock(t)) + } + return cpy +} + +func (q *FakeQuerier) templateWithUserNoLock(tpl database.TemplateTable) database.Template { + var user database.User + for _, _user := range q.users { + if _user.ID == tpl.CreatedBy { + user = _user + break + } + } + var withUser database.Template + // This is a cheeky way to copy the fields over without explicitly listing them all. + d, _ := json.Marshal(tpl) + _ = json.Unmarshal(d, &withUser) + withUser.CreatedByUsername = user.Username + withUser.CreatedByAvatarURL = user.AvatarURL.String + return withUser +} + func (q *FakeQuerier) getTemplateVersionByIDNoLock(_ context.Context, templateVersionID uuid.UUID) (database.TemplateVersion, error) { for _, templateVersion := range q.templateVersions { if templateVersion.ID != templateVersionID { @@ -1869,7 +1894,7 @@ func (q *FakeQuerier) GetTemplateByOrganizationAndName(_ context.Context, arg da if template.Deleted != arg.Deleted { continue } - return template.DeepCopy(), nil + return q.templateWithUserNoLock(template), nil } return database.Template{}, sql.ErrNoRows } @@ -2092,17 +2117,14 @@ func (q *FakeQuerier) GetTemplates(_ context.Context) ([]database.Template, erro defer q.mutex.RUnlock() templates := slices.Clone(q.templates) - for i := range templates { - templates[i] = templates[i].DeepCopy() - } - slices.SortFunc(templates, func(i, j database.Template) bool { + slices.SortFunc(templates, func(i, j database.TemplateTable) bool { if i.Name != j.Name { return i.Name < j.Name } return i.ID.String() < j.ID.String() }) - return templates, nil + return q.templatesWithUserNoLock(templates), nil } func (q *FakeQuerier) GetTemplatesWithFilter(ctx context.Context, arg database.GetTemplatesWithFilterParams) ([]database.Template, error) { @@ -3436,16 +3458,16 @@ func (q *FakeQuerier) InsertReplica(_ context.Context, arg database.InsertReplic return replica, nil } -func (q *FakeQuerier) InsertTemplate(_ context.Context, arg database.InsertTemplateParams) (database.Template, error) { +func (q *FakeQuerier) InsertTemplate(_ context.Context, arg database.InsertTemplateParams) error { if err := validateDatabaseType(arg); err != nil { - return database.Template{}, err + return err } q.mutex.Lock() defer q.mutex.Unlock() //nolint:gosimple - template := database.Template{ + template := database.TemplateTable{ ID: arg.ID, CreatedAt: arg.CreatedAt, UpdatedAt: arg.UpdatedAt, @@ -3464,7 +3486,7 @@ func (q *FakeQuerier) InsertTemplate(_ context.Context, arg database.InsertTempl AllowUserAutostop: true, } q.templates = append(q.templates, template) - return template.DeepCopy(), nil + return nil } func (q *FakeQuerier) InsertTemplateVersion(_ context.Context, arg database.InsertTemplateVersionParams) (database.TemplateVersion, error) { @@ -4172,9 +4194,9 @@ func (q *FakeQuerier) UpdateReplica(_ context.Context, arg database.UpdateReplic return database.Replica{}, sql.ErrNoRows } -func (q *FakeQuerier) UpdateTemplateACLByID(_ context.Context, arg database.UpdateTemplateACLByIDParams) (database.Template, error) { +func (q *FakeQuerier) UpdateTemplateACLByID(_ context.Context, arg database.UpdateTemplateACLByIDParams) error { if err := validateDatabaseType(arg); err != nil { - return database.Template{}, err + return err } q.mutex.Lock() @@ -4186,11 +4208,11 @@ func (q *FakeQuerier) UpdateTemplateACLByID(_ context.Context, arg database.Upda template.UserACL = arg.UserACL q.templates[i] = template - return template.DeepCopy(), nil + return nil } } - return database.Template{}, sql.ErrNoRows + return sql.ErrNoRows } func (q *FakeQuerier) UpdateTemplateActiveVersionByID(_ context.Context, arg database.UpdateTemplateActiveVersionByIDParams) error { @@ -4233,9 +4255,9 @@ func (q *FakeQuerier) UpdateTemplateDeletedByID(_ context.Context, arg database. return sql.ErrNoRows } -func (q *FakeQuerier) UpdateTemplateMetaByID(_ context.Context, arg database.UpdateTemplateMetaByIDParams) (database.Template, error) { +func (q *FakeQuerier) UpdateTemplateMetaByID(_ context.Context, arg database.UpdateTemplateMetaByIDParams) error { if err := validateDatabaseType(arg); err != nil { - return database.Template{}, err + return err } q.mutex.Lock() @@ -4251,15 +4273,15 @@ func (q *FakeQuerier) UpdateTemplateMetaByID(_ context.Context, arg database.Upd tpl.Description = arg.Description tpl.Icon = arg.Icon q.templates[idx] = tpl - return tpl.DeepCopy(), nil + return nil } - return database.Template{}, sql.ErrNoRows + return sql.ErrNoRows } -func (q *FakeQuerier) UpdateTemplateScheduleByID(_ context.Context, arg database.UpdateTemplateScheduleByIDParams) (database.Template, error) { +func (q *FakeQuerier) UpdateTemplateScheduleByID(_ context.Context, arg database.UpdateTemplateScheduleByIDParams) error { if err := validateDatabaseType(arg); err != nil { - return database.Template{}, err + return err } q.mutex.Lock() @@ -4278,10 +4300,10 @@ func (q *FakeQuerier) UpdateTemplateScheduleByID(_ context.Context, arg database tpl.InactivityTTL = arg.InactivityTTL tpl.LockedTTL = arg.LockedTTL q.templates[idx] = tpl - return tpl.DeepCopy(), nil + return nil } - return database.Template{}, sql.ErrNoRows + return sql.ErrNoRows } func (q *FakeQuerier) UpdateTemplateVersionByID(_ context.Context, arg database.UpdateTemplateVersionByIDParams) (database.TemplateVersion, error) { @@ -4984,7 +5006,8 @@ func (q *FakeQuerier) GetAuthorizedTemplates(ctx context.Context, arg database.G } var templates []database.Template - for _, template := range q.templates { + for _, templateTable := range q.templates { + template := q.templateWithUserNoLock(templateTable) if prepared != nil && prepared.Authorize(ctx, template.RBACObject()) != nil { continue } @@ -5012,7 +5035,7 @@ func (q *FakeQuerier) GetAuthorizedTemplates(ctx context.Context, arg database.G continue } } - templates = append(templates, template.DeepCopy()) + templates = append(templates, template) } if len(templates) > 0 { slices.SortFunc(templates, func(i, j database.Template) bool { @@ -5031,7 +5054,7 @@ func (q *FakeQuerier) GetTemplateGroupRoles(_ context.Context, id uuid.UUID) ([] q.mutex.RLock() defer q.mutex.RUnlock() - var template database.Template + var template database.TemplateTable for _, t := range q.templates { if t.ID == id { template = t @@ -5068,7 +5091,7 @@ func (q *FakeQuerier) GetTemplateUserRoles(_ context.Context, id uuid.UUID) ([]d q.mutex.RLock() defer q.mutex.RUnlock() - var template database.Template + var template database.TemplateTable for _, t := range q.templates { if t.ID == id { template = t diff --git a/coderd/database/dbgen/dbgen.go b/coderd/database/dbgen/dbgen.go index 23ccfe71ac61a..383e4b2fe5c19 100644 --- a/coderd/database/dbgen/dbgen.go +++ b/coderd/database/dbgen/dbgen.go @@ -62,8 +62,9 @@ func AuditLog(t testing.TB, db database.Store, seed database.AuditLog) database. } func Template(t testing.TB, db database.Store, seed database.Template) database.Template { - template, err := db.InsertTemplate(genCtx, database.InsertTemplateParams{ - ID: takeFirst(seed.ID, uuid.New()), + id := takeFirst(seed.ID, uuid.New()) + err := db.InsertTemplate(genCtx, database.InsertTemplateParams{ + ID: id, CreatedAt: takeFirst(seed.CreatedAt, database.Now()), UpdatedAt: takeFirst(seed.UpdatedAt, database.Now()), OrganizationID: takeFirst(seed.OrganizationID, uuid.New()), @@ -79,6 +80,9 @@ func Template(t testing.TB, db database.Store, seed database.Template) database. AllowUserCancelWorkspaceJobs: seed.AllowUserCancelWorkspaceJobs, }) require.NoError(t, err, "insert template") + + template, err := db.GetTemplateByID(context.Background(), id) + require.NoError(t, err, "get template") return template } diff --git a/coderd/database/dbmetrics/dbmetrics.go b/coderd/database/dbmetrics/dbmetrics.go index 12f03ea8c75fd..934f27ebcbbb6 100644 --- a/coderd/database/dbmetrics/dbmetrics.go +++ b/coderd/database/dbmetrics/dbmetrics.go @@ -1103,11 +1103,11 @@ func (m metricsStore) InsertReplica(ctx context.Context, arg database.InsertRepl return replica, err } -func (m metricsStore) InsertTemplate(ctx context.Context, arg database.InsertTemplateParams) (database.Template, error) { +func (m metricsStore) InsertTemplate(ctx context.Context, arg database.InsertTemplateParams) error { start := time.Now() - template, err := m.s.InsertTemplate(ctx, arg) + err := m.s.InsertTemplate(ctx, arg) m.queryLatencies.WithLabelValues("InsertTemplate").Observe(time.Since(start).Seconds()) - return template, err + return err } func (m metricsStore) InsertTemplateVersion(ctx context.Context, arg database.InsertTemplateVersionParams) (database.TemplateVersion, error) { @@ -1306,11 +1306,11 @@ func (m metricsStore) UpdateReplica(ctx context.Context, arg database.UpdateRepl return replica, err } -func (m metricsStore) UpdateTemplateACLByID(ctx context.Context, arg database.UpdateTemplateACLByIDParams) (database.Template, error) { +func (m metricsStore) UpdateTemplateACLByID(ctx context.Context, arg database.UpdateTemplateACLByIDParams) error { start := time.Now() - template, err := m.s.UpdateTemplateACLByID(ctx, arg) + err := m.s.UpdateTemplateACLByID(ctx, arg) m.queryLatencies.WithLabelValues("UpdateTemplateACLByID").Observe(time.Since(start).Seconds()) - return template, err + return err } func (m metricsStore) UpdateTemplateActiveVersionByID(ctx context.Context, arg database.UpdateTemplateActiveVersionByIDParams) error { @@ -1327,18 +1327,18 @@ func (m metricsStore) UpdateTemplateDeletedByID(ctx context.Context, arg databas return err } -func (m metricsStore) UpdateTemplateMetaByID(ctx context.Context, arg database.UpdateTemplateMetaByIDParams) (database.Template, error) { +func (m metricsStore) UpdateTemplateMetaByID(ctx context.Context, arg database.UpdateTemplateMetaByIDParams) error { start := time.Now() - template, err := m.s.UpdateTemplateMetaByID(ctx, arg) + err := m.s.UpdateTemplateMetaByID(ctx, arg) m.queryLatencies.WithLabelValues("UpdateTemplateMetaByID").Observe(time.Since(start).Seconds()) - return template, err + return err } -func (m metricsStore) UpdateTemplateScheduleByID(ctx context.Context, arg database.UpdateTemplateScheduleByIDParams) (database.Template, error) { +func (m metricsStore) UpdateTemplateScheduleByID(ctx context.Context, arg database.UpdateTemplateScheduleByIDParams) error { start := time.Now() - template, err := m.s.UpdateTemplateScheduleByID(ctx, arg) + err := m.s.UpdateTemplateScheduleByID(ctx, arg) m.queryLatencies.WithLabelValues("UpdateTemplateScheduleByID").Observe(time.Since(start).Seconds()) - return template, err + return err } func (m metricsStore) UpdateTemplateVersionByID(ctx context.Context, arg database.UpdateTemplateVersionByIDParams) (database.TemplateVersion, error) { diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index f672a5e5dfc61..fde51b9fead0d 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -2318,12 +2318,11 @@ func (mr *MockStoreMockRecorder) InsertReplica(arg0, arg1 interface{}) *gomock.C } // InsertTemplate mocks base method. -func (m *MockStore) InsertTemplate(arg0 context.Context, arg1 database.InsertTemplateParams) (database.Template, error) { +func (m *MockStore) InsertTemplate(arg0 context.Context, arg1 database.InsertTemplateParams) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "InsertTemplate", arg0, arg1) - ret0, _ := ret[0].(database.Template) - ret1, _ := ret[1].(error) - return ret0, ret1 + ret0, _ := ret[0].(error) + return ret0 } // InsertTemplate indicates an expected call of InsertTemplate. @@ -2761,12 +2760,11 @@ func (mr *MockStoreMockRecorder) UpdateReplica(arg0, arg1 interface{}) *gomock.C } // UpdateTemplateACLByID mocks base method. -func (m *MockStore) UpdateTemplateACLByID(arg0 context.Context, arg1 database.UpdateTemplateACLByIDParams) (database.Template, error) { +func (m *MockStore) UpdateTemplateACLByID(arg0 context.Context, arg1 database.UpdateTemplateACLByIDParams) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "UpdateTemplateACLByID", arg0, arg1) - ret0, _ := ret[0].(database.Template) - ret1, _ := ret[1].(error) - return ret0, ret1 + ret0, _ := ret[0].(error) + return ret0 } // UpdateTemplateACLByID indicates an expected call of UpdateTemplateACLByID. @@ -2804,12 +2802,11 @@ func (mr *MockStoreMockRecorder) UpdateTemplateDeletedByID(arg0, arg1 interface{ } // UpdateTemplateMetaByID mocks base method. -func (m *MockStore) UpdateTemplateMetaByID(arg0 context.Context, arg1 database.UpdateTemplateMetaByIDParams) (database.Template, error) { +func (m *MockStore) UpdateTemplateMetaByID(arg0 context.Context, arg1 database.UpdateTemplateMetaByIDParams) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "UpdateTemplateMetaByID", arg0, arg1) - ret0, _ := ret[0].(database.Template) - ret1, _ := ret[1].(error) - return ret0, ret1 + ret0, _ := ret[0].(error) + return ret0 } // UpdateTemplateMetaByID indicates an expected call of UpdateTemplateMetaByID. @@ -2819,12 +2816,11 @@ func (mr *MockStoreMockRecorder) UpdateTemplateMetaByID(arg0, arg1 interface{}) } // UpdateTemplateScheduleByID mocks base method. -func (m *MockStore) UpdateTemplateScheduleByID(arg0 context.Context, arg1 database.UpdateTemplateScheduleByIDParams) (database.Template, error) { +func (m *MockStore) UpdateTemplateScheduleByID(arg0 context.Context, arg1 database.UpdateTemplateScheduleByIDParams) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "UpdateTemplateScheduleByID", arg0, arg1) - ret0, _ := ret[0].(database.Template) - ret1, _ := ret[1].(error) - return ret0, ret1 + ret0, _ := ret[0].(error) + return ret0 } // UpdateTemplateScheduleByID indicates an expected call of UpdateTemplateScheduleByID. diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 322de586cfa31..11d4bcb0caa8d 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -579,15 +579,6 @@ COMMENT ON COLUMN templates.allow_user_autostart IS 'Allow users to specify an a COMMENT ON COLUMN templates.allow_user_autostop IS 'Allow users to specify custom autostop values for workspaces (enterprise).'; -CREATE TABLE user_links ( - user_id uuid NOT NULL, - login_type login_type NOT NULL, - linked_id text DEFAULT ''::text NOT NULL, - oauth_access_token text DEFAULT ''::text NOT NULL, - oauth_refresh_token text DEFAULT ''::text NOT NULL, - oauth_expiry timestamp with time zone DEFAULT '0001-01-01 00:00:00+00'::timestamp with time zone NOT NULL -); - CREATE TABLE users ( id uuid NOT NULL, email text NOT NULL, @@ -603,6 +594,53 @@ CREATE TABLE users ( last_seen_at timestamp without time zone DEFAULT '0001-01-01 00:00:00'::timestamp without time zone NOT NULL ); +CREATE VIEW visible_users AS + SELECT users.id, + users.username, + users.avatar_url + FROM users; + +COMMENT ON VIEW visible_users IS 'Visible fields of users are allowed to be joined with other tables for including context of other resources.'; + +CREATE VIEW template_with_users AS + SELECT templates.id, + templates.created_at, + templates.updated_at, + templates.organization_id, + templates.deleted, + templates.name, + templates.provisioner, + templates.active_version_id, + templates.description, + templates.default_ttl, + templates.created_by, + templates.icon, + templates.user_acl, + templates.group_acl, + templates.display_name, + templates.allow_user_cancel_workspace_jobs, + templates.max_ttl, + templates.allow_user_autostart, + templates.allow_user_autostop, + templates.failure_ttl, + templates.inactivity_ttl, + templates.locked_ttl, + COALESCE(visible_users.avatar_url, ''::text) AS created_by_avatar_url, + COALESCE(visible_users.username, ''::text) AS created_by_username + FROM (public.templates + LEFT JOIN visible_users ON ((templates.created_by = visible_users.id))); + +COMMENT ON VIEW template_with_users IS 'Joins in the username + avatar url of the created by user.'; + +CREATE TABLE user_links ( + user_id uuid NOT NULL, + login_type login_type NOT NULL, + linked_id text DEFAULT ''::text NOT NULL, + oauth_access_token text DEFAULT ''::text NOT NULL, + oauth_refresh_token text DEFAULT ''::text NOT NULL, + oauth_expiry timestamp with time zone DEFAULT '0001-01-01 00:00:00+00'::timestamp with time zone NOT NULL +); + CREATE UNLOGGED TABLE workspace_agent_metadata ( workspace_agent_id uuid NOT NULL, display_name character varying(127) NOT NULL, diff --git a/coderd/database/generate.sh b/coderd/database/generate.sh index ffbf2909490c3..e8777a036a3cf 100755 --- a/coderd/database/generate.sh +++ b/coderd/database/generate.sh @@ -57,4 +57,6 @@ SCRIPT_DIR=$(dirname "${BASH_SOURCE[0]}") go run golang.org/x/tools/cmd/goimports@latest -w queries.sql.go go run ../../scripts/dbgen/main.go + # This will error if a view is broken. + go test -run=TestViewSubset ) diff --git a/coderd/database/migrations/000138_join_users.down.sql b/coderd/database/migrations/000138_join_users.down.sql new file mode 100644 index 0000000000000..754574f7b5abd --- /dev/null +++ b/coderd/database/migrations/000138_join_users.down.sql @@ -0,0 +1,6 @@ +BEGIN; + +DROP VIEW template_with_users; +DROP VIEW visible_users; + +COMMIT; diff --git a/coderd/database/migrations/000138_join_users.up.sql b/coderd/database/migrations/000138_join_users.up.sql new file mode 100644 index 0000000000000..198dd55edf1d2 --- /dev/null +++ b/coderd/database/migrations/000138_join_users.up.sql @@ -0,0 +1,30 @@ +BEGIN; + +CREATE VIEW + visible_users +AS +SELECT + id, username, avatar_url +FROM + users; + +COMMENT ON VIEW visible_users IS 'Visible fields of users are allowed to be joined with other tables for including context of other resources.'; + +-- If you need to update this view, put 'DROP VIEW template_with_users;' before this. +CREATE VIEW + template_with_users +AS + SELECT + templates.*, + coalesce(visible_users.avatar_url, '') AS created_by_avatar_url, + coalesce(visible_users.username, '') AS created_by_username + FROM + templates + LEFT JOIN + visible_users + ON + templates.created_by = visible_users.id; + +COMMENT ON VIEW template_with_users IS 'Joins in the username + avatar url of the created by user.'; + +COMMIT; diff --git a/coderd/database/modelqueries.go b/coderd/database/modelqueries.go index a7f186b668b0a..fd47cd8aaff18 100644 --- a/coderd/database/modelqueries.go +++ b/coderd/database/modelqueries.go @@ -54,7 +54,7 @@ func (q *sqlQuerier) GetAuthorizedTemplates(ctx context.Context, arg GetTemplate pq.Array(arg.IDs), ) if err != nil { - return nil, xerrors.Errorf("query context: %w", err) + return nil, err } defer rows.Close() var items []Template @@ -83,16 +83,18 @@ func (q *sqlQuerier) GetAuthorizedTemplates(ctx context.Context, arg GetTemplate &i.FailureTTL, &i.InactivityTTL, &i.LockedTTL, + &i.CreatedByAvatarURL, + &i.CreatedByUsername, ); err != nil { - return nil, xerrors.Errorf("scan: %w", err) + return nil, err } items = append(items, i) } if err := rows.Close(); err != nil { - return nil, xerrors.Errorf("close: %w", err) + return nil, err } if err := rows.Err(); err != nil { - return nil, xerrors.Errorf("rows err: %w", err) + return nil, err } return items, nil } diff --git a/coderd/database/models.go b/coderd/database/models.go index 72041fb6c8a52..22704fd549fe8 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -1567,7 +1567,35 @@ type TailnetCoordinator struct { HeartbeatAt time.Time `db:"heartbeat_at" json:"heartbeat_at"` } +// Joins in the username + avatar url of the created by user. type Template struct { + ID uuid.UUID `db:"id" json:"id"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` + Deleted bool `db:"deleted" json:"deleted"` + Name string `db:"name" json:"name"` + Provisioner ProvisionerType `db:"provisioner" json:"provisioner"` + ActiveVersionID uuid.UUID `db:"active_version_id" json:"active_version_id"` + Description string `db:"description" json:"description"` + DefaultTTL int64 `db:"default_ttl" json:"default_ttl"` + CreatedBy uuid.UUID `db:"created_by" json:"created_by"` + Icon string `db:"icon" json:"icon"` + UserACL TemplateACL `db:"user_acl" json:"user_acl"` + GroupACL TemplateACL `db:"group_acl" json:"group_acl"` + DisplayName string `db:"display_name" json:"display_name"` + AllowUserCancelWorkspaceJobs bool `db:"allow_user_cancel_workspace_jobs" json:"allow_user_cancel_workspace_jobs"` + MaxTTL int64 `db:"max_ttl" json:"max_ttl"` + AllowUserAutostart bool `db:"allow_user_autostart" json:"allow_user_autostart"` + AllowUserAutostop bool `db:"allow_user_autostop" json:"allow_user_autostop"` + FailureTTL int64 `db:"failure_ttl" json:"failure_ttl"` + InactivityTTL int64 `db:"inactivity_ttl" json:"inactivity_ttl"` + LockedTTL int64 `db:"locked_ttl" json:"locked_ttl"` + CreatedByAvatarURL string `db:"created_by_avatar_url" json:"created_by_avatar_url"` + CreatedByUsername string `db:"created_by_username" json:"created_by_username"` +} + +type TemplateTable struct { ID uuid.UUID `db:"id" json:"id"` CreatedAt time.Time `db:"created_at" json:"created_at"` UpdatedAt time.Time `db:"updated_at" json:"updated_at"` @@ -1691,6 +1719,13 @@ type UserLink struct { OAuthExpiry time.Time `db:"oauth_expiry" json:"oauth_expiry"` } +// Visible fields of users are allowed to be joined with other tables for including context of other resources. +type VisibleUser struct { + ID uuid.UUID `db:"id" json:"id"` + Username string `db:"username" json:"username"` + AvatarURL sql.NullString `db:"avatar_url" json:"avatar_url"` +} + type Workspace struct { ID uuid.UUID `db:"id" json:"id"` CreatedAt time.Time `db:"created_at" json:"created_at"` diff --git a/coderd/database/models_test.go b/coderd/database/models_test.go new file mode 100644 index 0000000000000..54d11373f9253 --- /dev/null +++ b/coderd/database/models_test.go @@ -0,0 +1,46 @@ +package database_test + +import ( + "reflect" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/coder/coder/coderd/database" +) + +// TestViewSubsetTemplate ensures TemplateTable is a subset of Template +func TestViewSubsetTemplate(t *testing.T) { + t.Parallel() + table := reflect.TypeOf(database.TemplateTable{}) + joined := reflect.TypeOf(database.Template{}) + + tableFields := allFields(table) + joinedFields := allFields(joined) + if !assert.Subset(t, fieldNames(joinedFields), fieldNames(tableFields), "table is not subset") { + t.Log("Some fields were added to the Template Table without updating the 'template_with_users' view.") + t.Log("See migration 000138_join_users_up.sql to create the view.") + } +} + +func fieldNames(fields []reflect.StructField) []string { + names := make([]string, len(fields)) + for i, field := range fields { + names[i] = field.Name + } + return names +} + +func allFields(rt reflect.Type) []reflect.StructField { + fields := make([]reflect.StructField, 0, rt.NumField()) + for i := 0; i < rt.NumField(); i++ { + field := rt.Field(i) + if field.Anonymous && field.Type.Kind() == reflect.Struct { + // Recurse into anonymous struct fields. + fields = append(fields, allFields(field.Type)...) + continue + } + fields = append(fields, rt.Field(i)) + } + return fields +} diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 9fe7a61b7e6c6..31c5537cf862b 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -191,7 +191,7 @@ type sqlcQuerier interface { InsertProvisionerJob(ctx context.Context, arg InsertProvisionerJobParams) (ProvisionerJob, error) InsertProvisionerJobLogs(ctx context.Context, arg InsertProvisionerJobLogsParams) ([]ProvisionerJobLog, error) InsertReplica(ctx context.Context, arg InsertReplicaParams) (Replica, error) - InsertTemplate(ctx context.Context, arg InsertTemplateParams) (Template, error) + InsertTemplate(ctx context.Context, arg InsertTemplateParams) error InsertTemplateVersion(ctx context.Context, arg InsertTemplateVersionParams) (TemplateVersion, error) InsertTemplateVersionParameter(ctx context.Context, arg InsertTemplateVersionParameterParams) (TemplateVersionParameter, error) InsertTemplateVersionVariable(ctx context.Context, arg InsertTemplateVersionVariableParams) (TemplateVersionVariable, error) @@ -225,11 +225,11 @@ type sqlcQuerier interface { UpdateProvisionerJobWithCancelByID(ctx context.Context, arg UpdateProvisionerJobWithCancelByIDParams) error UpdateProvisionerJobWithCompleteByID(ctx context.Context, arg UpdateProvisionerJobWithCompleteByIDParams) error UpdateReplica(ctx context.Context, arg UpdateReplicaParams) (Replica, error) - UpdateTemplateACLByID(ctx context.Context, arg UpdateTemplateACLByIDParams) (Template, error) + UpdateTemplateACLByID(ctx context.Context, arg UpdateTemplateACLByIDParams) error UpdateTemplateActiveVersionByID(ctx context.Context, arg UpdateTemplateActiveVersionByIDParams) error UpdateTemplateDeletedByID(ctx context.Context, arg UpdateTemplateDeletedByIDParams) error - UpdateTemplateMetaByID(ctx context.Context, arg UpdateTemplateMetaByIDParams) (Template, error) - UpdateTemplateScheduleByID(ctx context.Context, arg UpdateTemplateScheduleByIDParams) (Template, error) + UpdateTemplateMetaByID(ctx context.Context, arg UpdateTemplateMetaByIDParams) error + UpdateTemplateScheduleByID(ctx context.Context, arg UpdateTemplateScheduleByIDParams) error UpdateTemplateVersionByID(ctx context.Context, arg UpdateTemplateVersionByIDParams) (TemplateVersion, error) UpdateTemplateVersionDescriptionByJobID(ctx context.Context, arg UpdateTemplateVersionDescriptionByJobIDParams) error UpdateTemplateVersionGitAuthProvidersByJobID(ctx context.Context, arg UpdateTemplateVersionGitAuthProvidersByJobIDParams) error diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 484a6ff4d6491..b656ab3182c23 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -3637,9 +3637,9 @@ func (q *sqlQuerier) GetTemplateAverageBuildTime(ctx context.Context, arg GetTem const getTemplateByID = `-- name: GetTemplateByID :one SELECT - id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, inactivity_ttl, locked_ttl + id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, inactivity_ttl, locked_ttl, created_by_avatar_url, created_by_username FROM - templates + template_with_users WHERE id = $1 LIMIT @@ -3672,15 +3672,17 @@ func (q *sqlQuerier) GetTemplateByID(ctx context.Context, id uuid.UUID) (Templat &i.FailureTTL, &i.InactivityTTL, &i.LockedTTL, + &i.CreatedByAvatarURL, + &i.CreatedByUsername, ) return i, err } const getTemplateByOrganizationAndName = `-- name: GetTemplateByOrganizationAndName :one SELECT - id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, inactivity_ttl, locked_ttl + id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, inactivity_ttl, locked_ttl, created_by_avatar_url, created_by_username FROM - templates + template_with_users AS templates WHERE organization_id = $1 AND deleted = $2 @@ -3721,12 +3723,14 @@ func (q *sqlQuerier) GetTemplateByOrganizationAndName(ctx context.Context, arg G &i.FailureTTL, &i.InactivityTTL, &i.LockedTTL, + &i.CreatedByAvatarURL, + &i.CreatedByUsername, ) return i, err } const getTemplates = `-- name: GetTemplates :many -SELECT id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, inactivity_ttl, locked_ttl FROM templates +SELECT id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, inactivity_ttl, locked_ttl, created_by_avatar_url, created_by_username FROM template_with_users AS templates ORDER BY (name, id) ASC ` @@ -3762,6 +3766,8 @@ func (q *sqlQuerier) GetTemplates(ctx context.Context) ([]Template, error) { &i.FailureTTL, &i.InactivityTTL, &i.LockedTTL, + &i.CreatedByAvatarURL, + &i.CreatedByUsername, ); err != nil { return nil, err } @@ -3778,9 +3784,9 @@ func (q *sqlQuerier) GetTemplates(ctx context.Context) ([]Template, error) { const getTemplatesWithFilter = `-- name: GetTemplatesWithFilter :many SELECT - id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, inactivity_ttl, locked_ttl + id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, inactivity_ttl, locked_ttl, created_by_avatar_url, created_by_username FROM - templates + template_with_users AS templates WHERE -- Optionally include deleted templates templates.deleted = $1 @@ -3851,6 +3857,8 @@ func (q *sqlQuerier) GetTemplatesWithFilter(ctx context.Context, arg GetTemplate &i.FailureTTL, &i.InactivityTTL, &i.LockedTTL, + &i.CreatedByAvatarURL, + &i.CreatedByUsername, ); err != nil { return nil, err } @@ -3865,7 +3873,7 @@ func (q *sqlQuerier) GetTemplatesWithFilter(ctx context.Context, arg GetTemplate return items, nil } -const insertTemplate = `-- name: InsertTemplate :one +const insertTemplate = `-- name: InsertTemplate :exec INSERT INTO templates ( id, @@ -3884,7 +3892,7 @@ INSERT INTO allow_user_cancel_workspace_jobs ) VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) RETURNING id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, inactivity_ttl, locked_ttl + ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) ` type InsertTemplateParams struct { @@ -3904,8 +3912,8 @@ type InsertTemplateParams struct { AllowUserCancelWorkspaceJobs bool `db:"allow_user_cancel_workspace_jobs" json:"allow_user_cancel_workspace_jobs"` } -func (q *sqlQuerier) InsertTemplate(ctx context.Context, arg InsertTemplateParams) (Template, error) { - row := q.db.QueryRowContext(ctx, insertTemplate, +func (q *sqlQuerier) InsertTemplate(ctx context.Context, arg InsertTemplateParams) error { + _, err := q.db.ExecContext(ctx, insertTemplate, arg.ID, arg.CreatedAt, arg.UpdatedAt, @@ -3921,35 +3929,10 @@ func (q *sqlQuerier) InsertTemplate(ctx context.Context, arg InsertTemplateParam arg.DisplayName, arg.AllowUserCancelWorkspaceJobs, ) - var i Template - err := row.Scan( - &i.ID, - &i.CreatedAt, - &i.UpdatedAt, - &i.OrganizationID, - &i.Deleted, - &i.Name, - &i.Provisioner, - &i.ActiveVersionID, - &i.Description, - &i.DefaultTTL, - &i.CreatedBy, - &i.Icon, - &i.UserACL, - &i.GroupACL, - &i.DisplayName, - &i.AllowUserCancelWorkspaceJobs, - &i.MaxTTL, - &i.AllowUserAutostart, - &i.AllowUserAutostop, - &i.FailureTTL, - &i.InactivityTTL, - &i.LockedTTL, - ) - return i, err + return err } -const updateTemplateACLByID = `-- name: UpdateTemplateACLByID :one +const updateTemplateACLByID = `-- name: UpdateTemplateACLByID :exec UPDATE templates SET @@ -3957,8 +3940,6 @@ SET user_acl = $2 WHERE id = $3 -RETURNING - id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, inactivity_ttl, locked_ttl ` type UpdateTemplateACLByIDParams struct { @@ -3967,34 +3948,9 @@ type UpdateTemplateACLByIDParams struct { ID uuid.UUID `db:"id" json:"id"` } -func (q *sqlQuerier) UpdateTemplateACLByID(ctx context.Context, arg UpdateTemplateACLByIDParams) (Template, error) { - row := q.db.QueryRowContext(ctx, updateTemplateACLByID, arg.GroupACL, arg.UserACL, arg.ID) - var i Template - err := row.Scan( - &i.ID, - &i.CreatedAt, - &i.UpdatedAt, - &i.OrganizationID, - &i.Deleted, - &i.Name, - &i.Provisioner, - &i.ActiveVersionID, - &i.Description, - &i.DefaultTTL, - &i.CreatedBy, - &i.Icon, - &i.UserACL, - &i.GroupACL, - &i.DisplayName, - &i.AllowUserCancelWorkspaceJobs, - &i.MaxTTL, - &i.AllowUserAutostart, - &i.AllowUserAutostop, - &i.FailureTTL, - &i.InactivityTTL, - &i.LockedTTL, - ) - return i, err +func (q *sqlQuerier) UpdateTemplateACLByID(ctx context.Context, arg UpdateTemplateACLByIDParams) error { + _, err := q.db.ExecContext(ctx, updateTemplateACLByID, arg.GroupACL, arg.UserACL, arg.ID) + return err } const updateTemplateActiveVersionByID = `-- name: UpdateTemplateActiveVersionByID :exec @@ -4039,7 +3995,7 @@ func (q *sqlQuerier) UpdateTemplateDeletedByID(ctx context.Context, arg UpdateTe return err } -const updateTemplateMetaByID = `-- name: UpdateTemplateMetaByID :one +const updateTemplateMetaByID = `-- name: UpdateTemplateMetaByID :exec UPDATE templates SET @@ -4051,8 +4007,6 @@ SET allow_user_cancel_workspace_jobs = $7 WHERE id = $1 -RETURNING - id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, inactivity_ttl, locked_ttl ` type UpdateTemplateMetaByIDParams struct { @@ -4065,8 +4019,8 @@ type UpdateTemplateMetaByIDParams struct { AllowUserCancelWorkspaceJobs bool `db:"allow_user_cancel_workspace_jobs" json:"allow_user_cancel_workspace_jobs"` } -func (q *sqlQuerier) UpdateTemplateMetaByID(ctx context.Context, arg UpdateTemplateMetaByIDParams) (Template, error) { - row := q.db.QueryRowContext(ctx, updateTemplateMetaByID, +func (q *sqlQuerier) UpdateTemplateMetaByID(ctx context.Context, arg UpdateTemplateMetaByIDParams) error { + _, err := q.db.ExecContext(ctx, updateTemplateMetaByID, arg.ID, arg.UpdatedAt, arg.Description, @@ -4075,35 +4029,10 @@ func (q *sqlQuerier) UpdateTemplateMetaByID(ctx context.Context, arg UpdateTempl arg.DisplayName, arg.AllowUserCancelWorkspaceJobs, ) - var i Template - err := row.Scan( - &i.ID, - &i.CreatedAt, - &i.UpdatedAt, - &i.OrganizationID, - &i.Deleted, - &i.Name, - &i.Provisioner, - &i.ActiveVersionID, - &i.Description, - &i.DefaultTTL, - &i.CreatedBy, - &i.Icon, - &i.UserACL, - &i.GroupACL, - &i.DisplayName, - &i.AllowUserCancelWorkspaceJobs, - &i.MaxTTL, - &i.AllowUserAutostart, - &i.AllowUserAutostop, - &i.FailureTTL, - &i.InactivityTTL, - &i.LockedTTL, - ) - return i, err + return err } -const updateTemplateScheduleByID = `-- name: UpdateTemplateScheduleByID :one +const updateTemplateScheduleByID = `-- name: UpdateTemplateScheduleByID :exec UPDATE templates SET @@ -4117,8 +4046,6 @@ SET locked_ttl = $9 WHERE id = $1 -RETURNING - id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, inactivity_ttl, locked_ttl ` type UpdateTemplateScheduleByIDParams struct { @@ -4133,8 +4060,8 @@ type UpdateTemplateScheduleByIDParams struct { LockedTTL int64 `db:"locked_ttl" json:"locked_ttl"` } -func (q *sqlQuerier) UpdateTemplateScheduleByID(ctx context.Context, arg UpdateTemplateScheduleByIDParams) (Template, error) { - row := q.db.QueryRowContext(ctx, updateTemplateScheduleByID, +func (q *sqlQuerier) UpdateTemplateScheduleByID(ctx context.Context, arg UpdateTemplateScheduleByIDParams) error { + _, err := q.db.ExecContext(ctx, updateTemplateScheduleByID, arg.ID, arg.UpdatedAt, arg.AllowUserAutostart, @@ -4145,32 +4072,7 @@ func (q *sqlQuerier) UpdateTemplateScheduleByID(ctx context.Context, arg UpdateT arg.InactivityTTL, arg.LockedTTL, ) - var i Template - err := row.Scan( - &i.ID, - &i.CreatedAt, - &i.UpdatedAt, - &i.OrganizationID, - &i.Deleted, - &i.Name, - &i.Provisioner, - &i.ActiveVersionID, - &i.Description, - &i.DefaultTTL, - &i.CreatedBy, - &i.Icon, - &i.UserACL, - &i.GroupACL, - &i.DisplayName, - &i.AllowUserCancelWorkspaceJobs, - &i.MaxTTL, - &i.AllowUserAutostart, - &i.AllowUserAutostop, - &i.FailureTTL, - &i.InactivityTTL, - &i.LockedTTL, - ) - return i, err + return err } const getTemplateVersionParameters = `-- name: GetTemplateVersionParameters :many diff --git a/coderd/database/queries/templates.sql b/coderd/database/queries/templates.sql index 3750b2fa76fd7..54ad458ce2f36 100644 --- a/coderd/database/queries/templates.sql +++ b/coderd/database/queries/templates.sql @@ -2,7 +2,7 @@ SELECT * FROM - templates + template_with_users WHERE id = $1 LIMIT @@ -12,7 +12,7 @@ LIMIT SELECT * FROM - templates + template_with_users AS templates WHERE -- Optionally include deleted templates templates.deleted = @deleted @@ -43,7 +43,7 @@ ORDER BY (name, id) ASC SELECT * FROM - templates + template_with_users AS templates WHERE organization_id = @organization_id AND deleted = @deleted @@ -52,11 +52,11 @@ LIMIT 1; -- name: GetTemplates :many -SELECT * FROM templates +SELECT * FROM template_with_users AS templates ORDER BY (name, id) ASC ; --- name: InsertTemplate :one +-- name: InsertTemplate :exec INSERT INTO templates ( id, @@ -75,7 +75,7 @@ INSERT INTO allow_user_cancel_workspace_jobs ) VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) RETURNING *; + ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14); -- name: UpdateTemplateActiveVersionByID :exec UPDATE @@ -95,7 +95,7 @@ SET WHERE id = $1; --- name: UpdateTemplateMetaByID :one +-- name: UpdateTemplateMetaByID :exec UPDATE templates SET @@ -107,10 +107,9 @@ SET allow_user_cancel_workspace_jobs = $7 WHERE id = $1 -RETURNING - *; +; --- name: UpdateTemplateScheduleByID :one +-- name: UpdateTemplateScheduleByID :exec UPDATE templates SET @@ -124,10 +123,9 @@ SET locked_ttl = $9 WHERE id = $1 -RETURNING - *; +; --- name: UpdateTemplateACLByID :one +-- name: UpdateTemplateACLByID :exec UPDATE templates SET @@ -135,8 +133,7 @@ SET user_acl = $2 WHERE id = $3 -RETURNING - *; +; -- name: GetTemplateAverageBuildTime :one WITH build_times AS ( diff --git a/coderd/database/sqlc.yaml b/coderd/database/sqlc.yaml index 964706cf0ad06..2bb3adea0980d 100644 --- a/coderd/database/sqlc.yaml +++ b/coderd/database/sqlc.yaml @@ -21,12 +21,24 @@ overrides: - column: "templates.group_acl" go_type: type: "TemplateACL" + - column: "template_with_users.user_acl" + go_type: + type: "TemplateACL" + - column: "template_with_users.group_acl" + go_type: + type: "TemplateACL" + - column: "template_with_users.created_by_avatar_url" + go_type: + type: "string" rename: + template: TemplateTable + template_with_user: Template api_key: APIKey api_key_scope: APIKeyScope api_key_scope_all: APIKeyScopeAll api_key_scope_application_connect: APIKeyScopeApplicationConnect avatar_url: AvatarURL + created_by_avatar_url: CreatedByAvatarURL session_count_vscode: SessionCountVSCode session_count_jetbrains: SessionCountJetBrains session_count_reconnecting_pty: SessionCountReconnectingPTY diff --git a/coderd/metricscache/metricscache_test.go b/coderd/metricscache/metricscache_test.go index 5cbe9b10203e1..d2a1a2a25f4ef 100644 --- a/coderd/metricscache/metricscache_test.go +++ b/coderd/metricscache/metricscache_test.go @@ -349,11 +349,14 @@ func TestCache_BuildTime(t *testing.T) { defer cache.Close() - template, err := db.InsertTemplate(ctx, database.InsertTemplateParams{ - ID: uuid.New(), + id := uuid.New() + err := db.InsertTemplate(ctx, database.InsertTemplateParams{ + ID: id, Provisioner: database.ProvisionerTypeEcho, }) require.NoError(t, err) + template, err := db.GetTemplateByID(ctx, id) + require.NoError(t, err) templateVersion, err := db.InsertTemplateVersion(ctx, database.InsertTemplateVersionParams{ ID: uuid.New(), diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go index 6b881210b3f6a..b0183fd3b1604 100644 --- a/coderd/provisionerdserver/provisionerdserver_test.go +++ b/coderd/provisionerdserver/provisionerdserver_test.go @@ -1025,7 +1025,7 @@ func TestCompleteJob(t *testing.T) { Name: "template", Provisioner: database.ProvisionerTypeEcho, }) - template, err := srv.Database.UpdateTemplateScheduleByID(ctx, database.UpdateTemplateScheduleByIDParams{ + err := srv.Database.UpdateTemplateScheduleByID(ctx, database.UpdateTemplateScheduleByIDParams{ ID: template.ID, UpdatedAt: database.Now(), AllowUserAutostart: c.templateAllowAutostop, diff --git a/coderd/schedule/template.go b/coderd/schedule/template.go index 12e87aac16527..d1782871fea44 100644 --- a/coderd/schedule/template.go +++ b/coderd/schedule/template.go @@ -4,6 +4,8 @@ import ( "context" "time" + "golang.org/x/xerrors" + "github.com/google/uuid" "github.com/coder/coder/coderd/database" @@ -68,17 +70,35 @@ func (*agplTemplateScheduleStore) SetTemplateScheduleOptions(ctx context.Context return tpl, nil } - return db.UpdateTemplateScheduleByID(ctx, database.UpdateTemplateScheduleByIDParams{ - ID: tpl.ID, - UpdatedAt: database.Now(), - DefaultTTL: int64(opts.DefaultTTL), - // Don't allow changing it, but keep the value in the DB (to avoid - // clearing settings if the license has an issue). - AllowUserAutostart: tpl.AllowUserAutostart, - AllowUserAutostop: tpl.AllowUserAutostop, - MaxTTL: tpl.MaxTTL, - FailureTTL: tpl.FailureTTL, - InactivityTTL: tpl.InactivityTTL, - LockedTTL: tpl.LockedTTL, - }) + var template database.Template + err := db.InTx(func(db database.Store) error { + err := db.UpdateTemplateScheduleByID(ctx, database.UpdateTemplateScheduleByIDParams{ + ID: tpl.ID, + UpdatedAt: database.Now(), + DefaultTTL: int64(opts.DefaultTTL), + // Don't allow changing it, but keep the value in the DB (to avoid + // clearing settings if the license has an issue). + AllowUserAutostart: tpl.AllowUserAutostart, + AllowUserAutostop: tpl.AllowUserAutostop, + MaxTTL: tpl.MaxTTL, + FailureTTL: tpl.FailureTTL, + InactivityTTL: tpl.InactivityTTL, + LockedTTL: tpl.LockedTTL, + }) + if err != nil { + return xerrors.Errorf("update template schedule: %w", err) + } + + template, err = db.GetTemplateByID(ctx, tpl.ID) + if err != nil { + return xerrors.Errorf("fetch updated template: %w", err) + } + + return nil + }, nil) + if err != nil { + return database.Template{}, err + } + + return template, err } diff --git a/coderd/templates.go b/coderd/templates.go index 3404a3ee16677..1238303aa04f5 100644 --- a/coderd/templates.go +++ b/coderd/templates.go @@ -1,7 +1,6 @@ package coderd import ( - "context" "database/sql" "errors" "fmt" @@ -40,16 +39,7 @@ func (api *API) template(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() template := httpmw.TemplateParam(r) - createdByNameMap, err := getCreatedByNamesByTemplateIDs(ctx, api.Database, []database.Template{template}) - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching creator name.", - Detail: err.Error(), - }) - return - } - - httpapi.Write(ctx, rw, http.StatusOK, api.convertTemplate(template, createdByNameMap[template.ID.String()])) + httpapi.Write(ctx, rw, http.StatusOK, api.convertTemplate(template)) } // @Summary Delete template by ID @@ -290,8 +280,9 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque } err = api.Database.InTx(func(tx database.Store) error { now := database.Now() - dbTemplate, err = tx.InsertTemplate(ctx, database.InsertTemplateParams{ - ID: uuid.New(), + id := uuid.New() + err = tx.InsertTemplate(ctx, database.InsertTemplateParams{ + ID: id, CreatedAt: now, UpdatedAt: now, OrganizationID: organization.ID, @@ -310,6 +301,11 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque return xerrors.Errorf("insert template: %s", err) } + dbTemplate, err = tx.GetTemplateByID(ctx, id) + if err != nil { + return xerrors.Errorf("get template by id: %s", err) + } + dbTemplate, err = (*api.TemplateScheduleStore.Load()).SetTemplateScheduleOptions(ctx, tx, dbTemplate, schedule.TemplateScheduleOptions{ UserAutostartEnabled: allowUserAutostart, UserAutostopEnabled: allowUserAutostop, @@ -348,12 +344,7 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque } templateVersionAudit.New = newTemplateVersion - createdByNameMap, err := getCreatedByNamesByTemplateIDs(ctx, tx, []database.Template{dbTemplate}) - if err != nil { - return xerrors.Errorf("get creator name: %w", err) - } - - template = api.convertTemplate(dbTemplate, createdByNameMap[dbTemplate.ID.String()]) + template = api.convertTemplate(dbTemplate) return nil }, nil) if err != nil { @@ -409,16 +400,7 @@ func (api *API) templatesByOrganization(rw http.ResponseWriter, r *http.Request) return } - createdByNameMap, err := getCreatedByNamesByTemplateIDs(ctx, api.Database, templates) - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching creator names.", - Detail: err.Error(), - }) - return - } - - httpapi.Write(ctx, rw, http.StatusOK, api.convertTemplates(templates, createdByNameMap)) + httpapi.Write(ctx, rw, http.StatusOK, api.convertTemplates(templates)) } // @Summary Get templates by organization and template name @@ -451,16 +433,7 @@ func (api *API) templateByOrganizationAndName(rw http.ResponseWriter, r *http.Re return } - createdByNameMap, err := getCreatedByNamesByTemplateIDs(ctx, api.Database, []database.Template{template}) - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching creator name.", - Detail: err.Error(), - }) - return - } - - httpapi.Write(ctx, rw, http.StatusOK, api.convertTemplate(template, createdByNameMap[template.ID.String()])) + httpapi.Write(ctx, rw, http.StatusOK, api.convertTemplate(template)) } // @Summary Update template metadata by ID @@ -546,7 +519,7 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) { } var err error - updated, err = tx.UpdateTemplateMetaByID(ctx, database.UpdateTemplateMetaByIDParams{ + err = tx.UpdateTemplateMetaByID(ctx, database.UpdateTemplateMetaByIDParams{ ID: template.ID, UpdatedAt: database.Now(), Name: name, @@ -559,6 +532,11 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) { return xerrors.Errorf("update template metadata: %w", err) } + updated, err = tx.GetTemplateByID(ctx, template.ID) + if err != nil { + return xerrors.Errorf("fetch updated template metadata: %w", err) + } + defaultTTL := time.Duration(req.DefaultTTLMillis) * time.Millisecond maxTTL := time.Duration(req.MaxTTLMillis) * time.Millisecond failureTTL := time.Duration(req.FailureTTLMillis) * time.Millisecond @@ -603,16 +581,7 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) { } aReq.New = updated - createdByNameMap, err := getCreatedByNamesByTemplateIDs(ctx, api.Database, []database.Template{updated}) - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching creator name.", - Detail: err.Error(), - }) - return - } - - httpapi.Write(ctx, rw, http.StatusOK, api.convertTemplate(updated, createdByNameMap[updated.ID.String()])) + httpapi.Write(ctx, rw, http.StatusOK, api.convertTemplate(updated)) } // @Summary Get template DAUs by ID @@ -680,23 +649,11 @@ func (api *API) templateExamples(rw http.ResponseWriter, r *http.Request) { httpapi.Write(ctx, rw, http.StatusOK, ex) } -func getCreatedByNamesByTemplateIDs(ctx context.Context, db database.Store, templates []database.Template) (map[string]string, error) { - creators := make(map[string]string, len(templates)) - for _, template := range templates { - creator, err := db.GetUserByID(ctx, template.CreatedBy) - if err != nil { - return map[string]string{}, err - } - creators[template.ID.String()] = creator.Username - } - return creators, nil -} - -func (api *API) convertTemplates(templates []database.Template, createdByNameMap map[string]string) []codersdk.Template { +func (api *API) convertTemplates(templates []database.Template) []codersdk.Template { apiTemplates := make([]codersdk.Template, 0, len(templates)) for _, template := range templates { - apiTemplates = append(apiTemplates, api.convertTemplate(template, createdByNameMap[template.ID.String()])) + apiTemplates = append(apiTemplates, api.convertTemplate(template)) } // Sort templates by ActiveUserCount DESC @@ -708,7 +665,7 @@ func (api *API) convertTemplates(templates []database.Template, createdByNameMap } func (api *API) convertTemplate( - template database.Template, createdByName string, + template database.Template, ) codersdk.Template { activeCount, _ := api.metricsCache.TemplateUniqueUsers(template.ID) @@ -730,7 +687,7 @@ func (api *API) convertTemplate( DefaultTTLMillis: time.Duration(template.DefaultTTL).Milliseconds(), MaxTTLMillis: time.Duration(template.MaxTTL).Milliseconds(), CreatedByID: template.CreatedBy, - CreatedByName: createdByName, + CreatedByName: template.CreatedByUsername, AllowUserAutostart: template.AllowUserAutostart, AllowUserAutostop: template.AllowUserAutostop, AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs, diff --git a/docs/admin/audit-logs.md b/docs/admin/audit-logs.md index 454d041225385..868a926946e00 100644 --- a/docs/admin/audit-logs.md +++ b/docs/admin/audit-logs.md @@ -9,19 +9,19 @@ We track the following resources: -| Resource | | -| -------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| APIKey
login, logout, register, create, delete |
FieldTracked
created_attrue
expires_attrue
hashed_secretfalse
idfalse
ip_addressfalse
last_usedtrue
lifetime_secondsfalse
login_typefalse
scopefalse
token_namefalse
updated_atfalse
user_idtrue
| -| AuditOAuthConvertState
|
FieldTracked
created_attrue
expires_attrue
from_login_typetrue
to_login_typetrue
user_idtrue
| -| Group
create, write, delete |
FieldTracked
avatar_urltrue
idtrue
memberstrue
nametrue
organization_idfalse
quota_allowancetrue
| -| GitSSHKey
create |
FieldTracked
created_atfalse
private_keytrue
public_keytrue
updated_atfalse
user_idtrue
| -| License
create, delete |
FieldTracked
exptrue
idfalse
jwtfalse
uploaded_attrue
uuidtrue
| -| Template
write, delete |
FieldTracked
active_version_idtrue
allow_user_autostarttrue
allow_user_autostoptrue
allow_user_cancel_workspace_jobstrue
created_atfalse
created_bytrue
default_ttltrue
deletedfalse
descriptiontrue
display_nametrue
failure_ttltrue
group_acltrue
icontrue
idtrue
inactivity_ttltrue
locked_ttltrue
max_ttltrue
nametrue
organization_idfalse
provisionertrue
updated_atfalse
user_acltrue
| -| TemplateVersion
create, write |
FieldTracked
created_atfalse
created_bytrue
git_auth_providersfalse
idtrue
job_idfalse
messagefalse
nametrue
organization_idfalse
readmetrue
template_idtrue
updated_atfalse
| -| User
create, write, delete |
FieldTracked
avatar_urlfalse
created_atfalse
deletedtrue
emailtrue
hashed_passwordtrue
idtrue
last_seen_atfalse
login_typetrue
rbac_rolestrue
statustrue
updated_atfalse
usernametrue
| -| Workspace
create, write, delete |
FieldTracked
autostart_scheduletrue
created_atfalse
deletedfalse
idtrue
last_used_atfalse
locked_attrue
nametrue
organization_idfalse
owner_idtrue
template_idtrue
ttltrue
updated_atfalse
| -| WorkspaceBuild
start, stop |
FieldTracked
build_numberfalse
created_atfalse
daily_costfalse
deadlinefalse
idfalse
initiator_idfalse
job_idfalse
max_deadlinefalse
provisioner_statefalse
reasonfalse
template_version_idtrue
transitionfalse
updated_atfalse
workspace_idfalse
| -| WorkspaceProxy
|
FieldTracked
created_attrue
deletedfalse
display_nametrue
icontrue
idtrue
nametrue
token_hashed_secrettrue
updated_atfalse
urltrue
wildcard_hostnametrue
| +| Resource | | +| -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| APIKey
login, logout, register, create, delete |
FieldTracked
created_attrue
expires_attrue
hashed_secretfalse
idfalse
ip_addressfalse
last_usedtrue
lifetime_secondsfalse
login_typefalse
scopefalse
token_namefalse
updated_atfalse
user_idtrue
| +| AuditOAuthConvertState
|
FieldTracked
created_attrue
expires_attrue
from_login_typetrue
to_login_typetrue
user_idtrue
| +| Group
create, write, delete |
FieldTracked
avatar_urltrue
idtrue
memberstrue
nametrue
organization_idfalse
quota_allowancetrue
| +| GitSSHKey
create |
FieldTracked
created_atfalse
private_keytrue
public_keytrue
updated_atfalse
user_idtrue
| +| License
create, delete |
FieldTracked
exptrue
idfalse
jwtfalse
uploaded_attrue
uuidtrue
| +| Template
write, delete |
FieldTracked
active_version_idtrue
allow_user_autostarttrue
allow_user_autostoptrue
allow_user_cancel_workspace_jobstrue
created_atfalse
created_bytrue
created_by_avatar_urlfalse
created_by_usernamefalse
default_ttltrue
deletedfalse
descriptiontrue
display_nametrue
failure_ttltrue
group_acltrue
icontrue
idtrue
inactivity_ttltrue
locked_ttltrue
max_ttltrue
nametrue
organization_idfalse
provisionertrue
updated_atfalse
user_acltrue
| +| TemplateVersion
create, write |
FieldTracked
created_atfalse
created_bytrue
git_auth_providersfalse
idtrue
job_idfalse
messagefalse
nametrue
organization_idfalse
readmetrue
template_idtrue
updated_atfalse
| +| User
create, write, delete |
FieldTracked
avatar_urlfalse
created_atfalse
deletedtrue
emailtrue
hashed_passwordtrue
idtrue
last_seen_atfalse
login_typetrue
rbac_rolestrue
statustrue
updated_atfalse
usernametrue
| +| Workspace
create, write, delete |
FieldTracked
autostart_scheduletrue
created_atfalse
deletedfalse
idtrue
last_used_atfalse
locked_attrue
nametrue
organization_idfalse
owner_idtrue
template_idtrue
ttltrue
updated_atfalse
| +| WorkspaceBuild
start, stop |
FieldTracked
build_numberfalse
created_atfalse
daily_costfalse
deadlinefalse
idfalse
initiator_idfalse
job_idfalse
max_deadlinefalse
provisioner_statefalse
reasonfalse
template_version_idtrue
transitionfalse
updated_atfalse
workspace_idfalse
| +| WorkspaceProxy
|
FieldTracked
created_attrue
deletedfalse
display_nametrue
icontrue
idtrue
nametrue
token_hashed_secrettrue
updated_atfalse
urltrue
wildcard_hostnametrue
| diff --git a/enterprise/audit/table.go b/enterprise/audit/table.go index 4a2df1e64194b..d75776a437183 100644 --- a/enterprise/audit/table.go +++ b/enterprise/audit/table.go @@ -71,6 +71,8 @@ var auditableResourcesTypes = map[any]map[string]Action{ "icon": ActionTrack, "default_ttl": ActionTrack, "created_by": ActionTrack, + "created_by_username": ActionIgnore, + "created_by_avatar_url": ActionIgnore, "group_acl": ActionTrack, "user_acl": ActionTrack, "allow_user_autostart": ActionTrack, diff --git a/enterprise/coderd/provisionerdaemons.go b/enterprise/coderd/provisionerdaemons.go index 5bb35bdbd3b22..c02d5a1779fe6 100644 --- a/enterprise/coderd/provisionerdaemons.go +++ b/enterprise/coderd/provisionerdaemons.go @@ -343,39 +343,52 @@ func (*EnterpriseTemplateScheduleStore) SetTemplateScheduleOptions(ctx context.C return tpl, nil } - template, err := db.UpdateTemplateScheduleByID(ctx, database.UpdateTemplateScheduleByIDParams{ - ID: tpl.ID, - UpdatedAt: database.Now(), - AllowUserAutostart: opts.UserAutostartEnabled, - AllowUserAutostop: opts.UserAutostopEnabled, - DefaultTTL: int64(opts.DefaultTTL), - MaxTTL: int64(opts.MaxTTL), - FailureTTL: int64(opts.FailureTTL), - InactivityTTL: int64(opts.InactivityTTL), - LockedTTL: int64(opts.LockedTTL), - }) - if err != nil { - return database.Template{}, xerrors.Errorf("update template schedule: %w", err) - } - - // Update all workspaces using the template to set the user defined schedule - // to be within the new bounds. This essentially does the following for each - // workspace using the template. - // if (template.ttl != NULL) { - // workspace.ttl = min(workspace.ttl, template.ttl) - // } - // - // NOTE: this does not apply to currently running workspaces as their - // schedule information is committed to the workspace_build during start. - // This limitation is displayed to the user while editing the template. - if opts.MaxTTL > 0 { - err = db.UpdateWorkspaceTTLToBeWithinTemplateMax(ctx, database.UpdateWorkspaceTTLToBeWithinTemplateMaxParams{ - TemplateID: template.ID, - TemplateMaxTTL: int64(opts.MaxTTL), + var template database.Template + err := db.InTx(func(db database.Store) error { + err := db.UpdateTemplateScheduleByID(ctx, database.UpdateTemplateScheduleByIDParams{ + ID: tpl.ID, + UpdatedAt: database.Now(), + AllowUserAutostart: opts.UserAutostartEnabled, + AllowUserAutostop: opts.UserAutostopEnabled, + DefaultTTL: int64(opts.DefaultTTL), + MaxTTL: int64(opts.MaxTTL), + FailureTTL: int64(opts.FailureTTL), + InactivityTTL: int64(opts.InactivityTTL), + LockedTTL: int64(opts.LockedTTL), }) if err != nil { - return database.Template{}, xerrors.Errorf("update TTL of all workspaces on template to be within new template max TTL: %w", err) + return xerrors.Errorf("update template schedule: %w", err) } + + // Update all workspaces using the template to set the user defined schedule + // to be within the new bounds. This essentially does the following for each + // workspace using the template. + // if (template.ttl != NULL) { + // workspace.ttl = min(workspace.ttl, template.ttl) + // } + // + // NOTE: this does not apply to currently running workspaces as their + // schedule information is committed to the workspace_build during start. + // This limitation is displayed to the user while editing the template. + if opts.MaxTTL > 0 { + err = db.UpdateWorkspaceTTLToBeWithinTemplateMax(ctx, database.UpdateWorkspaceTTLToBeWithinTemplateMaxParams{ + TemplateID: tpl.ID, + TemplateMaxTTL: int64(opts.MaxTTL), + }) + if err != nil { + return xerrors.Errorf("update TTL of all workspaces on template to be within new template max TTL: %w", err) + } + } + + template, err = db.GetTemplateByID(ctx, tpl.ID) + if err != nil { + return xerrors.Errorf("get updated template schedule: %w", err) + } + + return nil + }, nil) + if err != nil { + return database.Template{}, err } return template, nil diff --git a/enterprise/coderd/templates.go b/enterprise/coderd/templates.go index 0ae433787526b..dec2c1c9a6f14 100644 --- a/enterprise/coderd/templates.go +++ b/enterprise/coderd/templates.go @@ -163,7 +163,7 @@ func (api *API) patchTemplateACL(rw http.ResponseWriter, r *http.Request) { } } - template, err = tx.UpdateTemplateACLByID(ctx, database.UpdateTemplateACLByIDParams{ + err = tx.UpdateTemplateACLByID(ctx, database.UpdateTemplateACLByIDParams{ ID: template.ID, UserACL: template.UserACL, GroupACL: template.GroupACL, @@ -171,6 +171,10 @@ func (api *API) patchTemplateACL(rw http.ResponseWriter, r *http.Request) { if err != nil { return xerrors.Errorf("update template ACL by ID: %w", err) } + template, err = tx.GetTemplateByID(ctx, template.ID) + if err != nil { + return xerrors.Errorf("get updated template by ID: %w", err) + } return nil }, nil) if err != nil {