diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 430dbf15ca6da..db36b9e320db1 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -22,6 +22,7 @@ import ( "github.com/coder/coder/v2/coderd/httpapi/httpapiconstraints" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/util/slice" + "github.com/coder/coder/v2/provisionersdk" ) var _ database.Store = (*querier)(nil) @@ -2155,14 +2156,6 @@ func (q *querier) InsertOrganizationMember(ctx context.Context, arg database.Ins return insert(q.log, q.auth, obj, q.db.InsertOrganizationMember)(ctx, arg) } -// TODO: We need to create a ProvisionerDaemon resource type -func (q *querier) InsertProvisionerDaemon(ctx context.Context, arg database.InsertProvisionerDaemonParams) (database.ProvisionerDaemon, error) { - // if err := q.authorizeContext(ctx, rbac.ActionCreate, rbac.ResourceSystem); err != nil { - // return database.ProvisionerDaemon{}, err - // } - return q.db.InsertProvisionerDaemon(ctx, arg) -} - // TODO: We need to create a ProvisionerJob resource type func (q *querier) InsertProvisionerJob(ctx context.Context, arg database.InsertProvisionerJobParams) (database.ProvisionerJob, error) { // if err := q.authorizeContext(ctx, rbac.ActionCreate, rbac.ResourceSystem); err != nil { @@ -3063,6 +3056,17 @@ func (q *querier) UpsertOAuthSigningKey(ctx context.Context, value string) error return q.db.UpsertOAuthSigningKey(ctx, value) } +func (q *querier) UpsertProvisionerDaemon(ctx context.Context, arg database.UpsertProvisionerDaemonParams) (database.ProvisionerDaemon, error) { + res := rbac.ResourceProvisionerDaemon.All() + if arg.Tags[provisionersdk.TagScope] == provisionersdk.ScopeUser { + res.Owner = arg.Tags[provisionersdk.TagOwner] + } + if err := q.authorizeContext(ctx, rbac.ActionCreate, res); err != nil { + return database.ProvisionerDaemon{}, err + } + return q.db.UpsertProvisionerDaemon(ctx, arg) +} + func (q *querier) UpsertServiceBanner(ctx context.Context, value string) error { if err := q.authorizeContext(ctx, rbac.ActionCreate, rbac.ResourceDeploymentValues); err != nil { return err diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 1c439f5a6f6f3..c210aa00daf63 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -22,6 +22,7 @@ import ( "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/util/slice" + "github.com/coder/coder/v2/provisionersdk" "github.com/coder/coder/v2/testutil" ) @@ -1370,8 +1371,10 @@ func (s *MethodTestSuite) TestWorkspace() { func (s *MethodTestSuite) TestExtraMethods() { s.Run("GetProvisionerDaemons", s.Subtest(func(db database.Store, check *expects) { - d, err := db.InsertProvisionerDaemon(context.Background(), database.InsertProvisionerDaemonParams{ - ID: uuid.New(), + d, err := db.UpsertProvisionerDaemon(context.Background(), database.UpsertProvisionerDaemonParams{ + Tags: database.StringMap(map[string]string{ + provisionersdk.TagScope: provisionersdk.ScopeOrganization, + }), }) s.NoError(err, "insert provisioner daemon") check.Args().Asserts(d, rbac.ActionRead) @@ -1650,11 +1653,19 @@ func (s *MethodTestSuite) TestSystemFunctions() { JobID: j.ID, }).Asserts( /*rbac.ResourceSystem, rbac.ActionCreate*/ ) })) - s.Run("InsertProvisionerDaemon", s.Subtest(func(db database.Store, check *expects) { - // TODO: we need to create a ProvisionerDaemon resource - check.Args(database.InsertProvisionerDaemonParams{ - ID: uuid.New(), - }).Asserts( /*rbac.ResourceSystem, rbac.ActionCreate*/ ) + s.Run("UpsertProvisionerDaemon", s.Subtest(func(db database.Store, check *expects) { + pd := rbac.ResourceProvisionerDaemon.All() + check.Args(database.UpsertProvisionerDaemonParams{ + Tags: database.StringMap(map[string]string{ + provisionersdk.TagScope: provisionersdk.ScopeOrganization, + }), + }).Asserts(pd, rbac.ActionCreate) + check.Args(database.UpsertProvisionerDaemonParams{ + Tags: database.StringMap(map[string]string{ + provisionersdk.TagScope: provisionersdk.ScopeUser, + provisionersdk.TagOwner: "11111111-1111-1111-1111-111111111111", + }), + }).Asserts(pd.WithOwner("11111111-1111-1111-1111-111111111111"), rbac.ActionCreate) })) s.Run("InsertTemplateVersionParameter", s.Subtest(func(db database.Store, check *expects) { v := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{}) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 5f285814ebd4b..f733bdfd1b8e3 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -26,6 +26,7 @@ import ( "github.com/coder/coder/v2/coderd/rbac/regosql" "github.com/coder/coder/v2/coderd/util/slice" "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/provisionersdk" ) var validProxyByHostnameRegex = regexp.MustCompile(`^[a-zA-Z0-9._-]+$`) @@ -4936,25 +4937,6 @@ func (q *FakeQuerier) InsertOrganizationMember(_ context.Context, arg database.I return organizationMember, nil } -func (q *FakeQuerier) InsertProvisionerDaemon(_ context.Context, arg database.InsertProvisionerDaemonParams) (database.ProvisionerDaemon, error) { - if err := validateDatabaseType(arg); err != nil { - return database.ProvisionerDaemon{}, err - } - - q.mutex.Lock() - defer q.mutex.Unlock() - - daemon := database.ProvisionerDaemon{ - ID: arg.ID, - Name: arg.Name, - Provisioners: arg.Provisioners, - Tags: arg.Tags, - LastSeenAt: arg.LastSeenAt, - } - q.provisionerDaemons = append(q.provisionerDaemons, daemon) - return daemon, nil -} - func (q *FakeQuerier) InsertProvisionerJob(_ context.Context, arg database.InsertProvisionerJobParams) (database.ProvisionerJob, error) { if err := validateDatabaseType(arg); err != nil { return database.ProvisionerJob{}, err @@ -6961,6 +6943,43 @@ func (q *FakeQuerier) UpsertOAuthSigningKey(_ context.Context, value string) err return nil } +func (q *FakeQuerier) UpsertProvisionerDaemon(_ context.Context, arg database.UpsertProvisionerDaemonParams) (database.ProvisionerDaemon, error) { + err := validateDatabaseType(arg) + if err != nil { + return database.ProvisionerDaemon{}, err + } + + q.mutex.Lock() + defer q.mutex.Unlock() + for _, d := range q.provisionerDaemons { + if d.Name == arg.Name { + if d.Tags[provisionersdk.TagScope] == provisionersdk.ScopeOrganization && arg.Tags[provisionersdk.TagOwner] != "" { + continue + } + if d.Tags[provisionersdk.TagScope] == provisionersdk.ScopeUser && arg.Tags[provisionersdk.TagOwner] != d.Tags[provisionersdk.TagOwner] { + continue + } + d.Provisioners = arg.Provisioners + d.Tags = arg.Tags + d.Version = arg.Version + d.LastSeenAt = arg.LastSeenAt + return d, nil + } + } + d := database.ProvisionerDaemon{ + ID: uuid.New(), + CreatedAt: arg.CreatedAt, + Name: arg.Name, + Provisioners: arg.Provisioners, + Tags: arg.Tags, + ReplicaID: uuid.NullUUID{}, + LastSeenAt: arg.LastSeenAt, + Version: arg.Version, + } + q.provisionerDaemons = append(q.provisionerDaemons, d) + return d, nil +} + func (q *FakeQuerier) UpsertServiceBanner(_ context.Context, data string) error { q.mutex.RLock() defer q.mutex.RUnlock() diff --git a/coderd/database/dbmetrics/dbmetrics.go b/coderd/database/dbmetrics/dbmetrics.go index 248b82665fda0..55145d352958e 100644 --- a/coderd/database/dbmetrics/dbmetrics.go +++ b/coderd/database/dbmetrics/dbmetrics.go @@ -1348,13 +1348,6 @@ func (m metricsStore) InsertOrganizationMember(ctx context.Context, arg database return member, err } -func (m metricsStore) InsertProvisionerDaemon(ctx context.Context, arg database.InsertProvisionerDaemonParams) (database.ProvisionerDaemon, error) { - start := time.Now() - daemon, err := m.s.InsertProvisionerDaemon(ctx, arg) - m.queryLatencies.WithLabelValues("InsertProvisionerDaemon").Observe(time.Since(start).Seconds()) - return daemon, err -} - func (m metricsStore) InsertProvisionerJob(ctx context.Context, arg database.InsertProvisionerJobParams) (database.ProvisionerJob, error) { start := time.Now() job, err := m.s.InsertProvisionerJob(ctx, arg) @@ -1950,6 +1943,13 @@ func (m metricsStore) UpsertOAuthSigningKey(ctx context.Context, value string) e return r0 } +func (m metricsStore) UpsertProvisionerDaemon(ctx context.Context, arg database.UpsertProvisionerDaemonParams) (database.ProvisionerDaemon, error) { + start := time.Now() + r0, r1 := m.s.UpsertProvisionerDaemon(ctx, arg) + m.queryLatencies.WithLabelValues("UpsertProvisionerDaemon").Observe(time.Since(start).Seconds()) + return r0, r1 +} + func (m metricsStore) UpsertServiceBanner(ctx context.Context, value string) error { start := time.Now() r0 := m.s.UpsertServiceBanner(ctx, value) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index 7ae5a1653d4b1..8c2b8283705ff 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -2833,21 +2833,6 @@ func (mr *MockStoreMockRecorder) InsertOrganizationMember(arg0, arg1 interface{} return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertOrganizationMember", reflect.TypeOf((*MockStore)(nil).InsertOrganizationMember), arg0, arg1) } -// InsertProvisionerDaemon mocks base method. -func (m *MockStore) InsertProvisionerDaemon(arg0 context.Context, arg1 database.InsertProvisionerDaemonParams) (database.ProvisionerDaemon, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "InsertProvisionerDaemon", arg0, arg1) - ret0, _ := ret[0].(database.ProvisionerDaemon) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// InsertProvisionerDaemon indicates an expected call of InsertProvisionerDaemon. -func (mr *MockStoreMockRecorder) InsertProvisionerDaemon(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertProvisionerDaemon", reflect.TypeOf((*MockStore)(nil).InsertProvisionerDaemon), arg0, arg1) -} - // InsertProvisionerJob mocks base method. func (m *MockStore) InsertProvisionerJob(arg0 context.Context, arg1 database.InsertProvisionerJobParams) (database.ProvisionerJob, error) { m.ctrl.T.Helper() @@ -4089,6 +4074,21 @@ func (mr *MockStoreMockRecorder) UpsertOAuthSigningKey(arg0, arg1 interface{}) * return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertOAuthSigningKey", reflect.TypeOf((*MockStore)(nil).UpsertOAuthSigningKey), arg0, arg1) } +// UpsertProvisionerDaemon mocks base method. +func (m *MockStore) UpsertProvisionerDaemon(arg0 context.Context, arg1 database.UpsertProvisionerDaemonParams) (database.ProvisionerDaemon, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpsertProvisionerDaemon", arg0, arg1) + ret0, _ := ret[0].(database.ProvisionerDaemon) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpsertProvisionerDaemon indicates an expected call of UpsertProvisionerDaemon. +func (mr *MockStoreMockRecorder) UpsertProvisionerDaemon(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertProvisionerDaemon", reflect.TypeOf((*MockStore)(nil).UpsertProvisionerDaemon), arg0, arg1) +} + // UpsertServiceBanner mocks base method. func (m *MockStore) UpsertServiceBanner(arg0 context.Context, arg1 string) error { m.ctrl.T.Helper() diff --git a/coderd/database/dbpurge/dbpurge_test.go b/coderd/database/dbpurge/dbpurge_test.go index 07ebcc5955a5b..0cff0b140fd19 100644 --- a/coderd/database/dbpurge/dbpurge_test.go +++ b/coderd/database/dbpurge/dbpurge_test.go @@ -19,6 +19,7 @@ import ( "github.com/coder/coder/v2/coderd/database/dbpurge" "github.com/coder/coder/v2/coderd/database/dbtestutil" "github.com/coder/coder/v2/coderd/database/dbtime" + "github.com/coder/coder/v2/provisionersdk" "github.com/coder/coder/v2/testutil" ) @@ -209,39 +210,45 @@ func TestDeleteOldProvisionerDaemons(t *testing.T) { now := dbtime.Now() // given - _, err := db.InsertProvisionerDaemon(ctx, database.InsertProvisionerDaemonParams{ + _, err := db.UpsertProvisionerDaemon(ctx, database.UpsertProvisionerDaemonParams{ // Provisioner daemon created 14 days ago, and checked in just before 7 days deadline. - ID: uuid.New(), Name: "external-0", Provisioners: []database.ProvisionerType{"echo"}, + Tags: database.StringMap{provisionersdk.TagScope: provisionersdk.ScopeOrganization}, CreatedAt: now.Add(-14 * 24 * time.Hour), LastSeenAt: sql.NullTime{Valid: true, Time: now.Add(-7 * 24 * time.Hour).Add(time.Minute)}, }) require.NoError(t, err) - _, err = db.InsertProvisionerDaemon(ctx, database.InsertProvisionerDaemonParams{ + _, err = db.UpsertProvisionerDaemon(ctx, database.UpsertProvisionerDaemonParams{ // Provisioner daemon created 8 days ago, and checked in last time an hour after creation. - ID: uuid.New(), Name: "external-1", Provisioners: []database.ProvisionerType{"echo"}, + Tags: database.StringMap{provisionersdk.TagScope: provisionersdk.ScopeOrganization}, CreatedAt: now.Add(-8 * 24 * time.Hour), LastSeenAt: sql.NullTime{Valid: true, Time: now.Add(-8 * 24 * time.Hour).Add(time.Hour)}, }) require.NoError(t, err) - _, err = db.InsertProvisionerDaemon(ctx, database.InsertProvisionerDaemonParams{ + _, err = db.UpsertProvisionerDaemon(ctx, database.UpsertProvisionerDaemonParams{ // Provisioner daemon created 9 days ago, and never checked in. - ID: uuid.New(), - Name: "external-2", + Name: "alice-provisioner", Provisioners: []database.ProvisionerType{"echo"}, - CreatedAt: now.Add(-9 * 24 * time.Hour), + Tags: database.StringMap{ + provisionersdk.TagScope: provisionersdk.ScopeUser, + provisionersdk.TagOwner: uuid.NewString(), + }, + CreatedAt: now.Add(-9 * 24 * time.Hour), }) require.NoError(t, err) - _, err = db.InsertProvisionerDaemon(ctx, database.InsertProvisionerDaemonParams{ + _, err = db.UpsertProvisionerDaemon(ctx, database.UpsertProvisionerDaemonParams{ // Provisioner daemon created 6 days ago, and never checked in. - ID: uuid.New(), - Name: "external-3", + Name: "bob-provisioner", Provisioners: []database.ProvisionerType{"echo"}, - CreatedAt: now.Add(-6 * 24 * time.Hour), - LastSeenAt: sql.NullTime{Valid: true, Time: now.Add(-6 * 24 * time.Hour)}, + Tags: database.StringMap{ + provisionersdk.TagScope: provisionersdk.ScopeUser, + provisionersdk.TagOwner: uuid.NewString(), + }, + CreatedAt: now.Add(-6 * 24 * time.Hour), + LastSeenAt: sql.NullTime{Valid: true, Time: now.Add(-6 * 24 * time.Hour)}, }) require.NoError(t, err) @@ -257,8 +264,8 @@ func TestDeleteOldProvisionerDaemons(t *testing.T) { } return containsProvisionerDaemon(daemons, "external-0") && !containsProvisionerDaemon(daemons, "external-1") && - !containsProvisionerDaemon(daemons, "external-2") && - containsProvisionerDaemon(daemons, "external-3") + !containsProvisionerDaemon(daemons, "alice-provisioner") && + containsProvisionerDaemon(daemons, "bob-provisioner") }, testutil.WaitShort, testutil.IntervalFast) } diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 20c800fee9a50..a1cf4b08477d2 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -1283,9 +1283,6 @@ ALTER TABLE ONLY parameter_values ALTER TABLE ONLY parameter_values ADD CONSTRAINT parameter_values_scope_id_name_key UNIQUE (scope_id, name); -ALTER TABLE ONLY provisioner_daemons - ADD CONSTRAINT provisioner_daemons_name_key UNIQUE (name); - ALTER TABLE ONLY provisioner_daemons ADD CONSTRAINT provisioner_daemons_pkey PRIMARY KEY (id); @@ -1415,6 +1412,10 @@ CREATE UNIQUE INDEX idx_organization_name ON organizations USING btree (name); CREATE UNIQUE INDEX idx_organization_name_lower ON organizations USING btree (lower(name)); +CREATE UNIQUE INDEX idx_provisioner_daemons_name_owner_key ON provisioner_daemons USING btree (name, lower((tags ->> 'owner'::text))); + +COMMENT ON INDEX idx_provisioner_daemons_name_owner_key IS 'Relax uniqueness constraint for provisioner daemon names'; + CREATE INDEX idx_tailnet_agents_coordinator ON tailnet_agents USING btree (coordinator_id); CREATE INDEX idx_tailnet_clients_coordinator ON tailnet_clients USING btree (coordinator_id); diff --git a/coderd/database/migrations/000178_provisioner_daemon_idx_owner.down.sql b/coderd/database/migrations/000178_provisioner_daemon_idx_owner.down.sql new file mode 100644 index 0000000000000..375e9b53428b6 --- /dev/null +++ b/coderd/database/migrations/000178_provisioner_daemon_idx_owner.down.sql @@ -0,0 +1,4 @@ +DROP INDEX IF EXISTS idx_provisioner_daemons_name_owner_key; + +ALTER TABLE ONLY provisioner_daemons + ADD CONSTRAINT provisioner_daemons_name_key UNIQUE (name); diff --git a/coderd/database/migrations/000178_provisioner_daemon_idx_owner.up.sql b/coderd/database/migrations/000178_provisioner_daemon_idx_owner.up.sql new file mode 100644 index 0000000000000..0e14991c0bd6e --- /dev/null +++ b/coderd/database/migrations/000178_provisioner_daemon_idx_owner.up.sql @@ -0,0 +1,10 @@ +ALTER TABLE ONLY provisioner_daemons + DROP CONSTRAINT IF EXISTS provisioner_daemons_name_key; + +CREATE UNIQUE INDEX IF NOT EXISTS idx_provisioner_daemons_name_owner_key + ON provisioner_daemons + USING btree (name, lower((tags->>'owner')::text)); + +COMMENT ON INDEX idx_provisioner_daemons_name_owner_key + IS 'Relax uniqueness constraint for provisioner daemon names'; + diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 08d76ba3d9726..73638c8206e70 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -277,7 +277,6 @@ type sqlcQuerier interface { InsertMissingGroups(ctx context.Context, arg InsertMissingGroupsParams) ([]Group, error) InsertOrganization(ctx context.Context, arg InsertOrganizationParams) (Organization, error) InsertOrganizationMember(ctx context.Context, arg InsertOrganizationMemberParams) (OrganizationMember, error) - InsertProvisionerDaemon(ctx context.Context, arg InsertProvisionerDaemonParams) (ProvisionerDaemon, error) InsertProvisionerJob(ctx context.Context, arg InsertProvisionerJobParams) (ProvisionerJob, error) InsertProvisionerJobLogs(ctx context.Context, arg InsertProvisionerJobLogsParams) ([]ProvisionerJobLog, error) InsertReplica(ctx context.Context, arg InsertReplicaParams) (Replica, error) @@ -373,6 +372,7 @@ type sqlcQuerier interface { UpsertLastUpdateCheck(ctx context.Context, value string) error UpsertLogoURL(ctx context.Context, value string) error UpsertOAuthSigningKey(ctx context.Context, value string) error + UpsertProvisionerDaemon(ctx context.Context, arg UpsertProvisionerDaemonParams) (ProvisionerDaemon, error) UpsertServiceBanner(ctx context.Context, value string) error UpsertTailnetAgent(ctx context.Context, arg UpsertTailnetAgentParams) (TailnetAgent, error) UpsertTailnetClient(ctx context.Context, arg UpsertTailnetClientParams) (TailnetClient, error) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index e360977a82294..16d3de793a83f 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -3056,7 +3056,7 @@ func (q *sqlQuerier) GetProvisionerDaemons(ctx context.Context) ([]ProvisionerDa return items, nil } -const insertProvisionerDaemon = `-- name: InsertProvisionerDaemon :one +const upsertProvisionerDaemon = `-- name: UpsertProvisionerDaemon :one INSERT INTO provisioner_daemons ( id, @@ -3064,29 +3064,45 @@ INSERT INTO "name", provisioners, tags, - last_seen_at - ) -VALUES - ($1, $2, $3, $4, $5, $6) RETURNING id, created_at, name, provisioners, replica_id, tags, last_seen_at, version -` - -type InsertProvisionerDaemonParams struct { - ID uuid.UUID `db:"id" json:"id"` + last_seen_at, + "version" + ) +VALUES ( + gen_random_uuid(), + $1, + $2, + $3, + $4, + $5, + $6 +) ON CONFLICT("name", lower((tags ->> 'owner'::text))) DO UPDATE SET + provisioners = $3, + tags = $4, + last_seen_at = $5, + "version" = $6 +WHERE + -- Only ones with the same tags are allowed clobber + provisioner_daemons.tags <@ $4 :: jsonb +RETURNING id, created_at, name, provisioners, replica_id, tags, last_seen_at, version +` + +type UpsertProvisionerDaemonParams struct { CreatedAt time.Time `db:"created_at" json:"created_at"` Name string `db:"name" json:"name"` Provisioners []ProvisionerType `db:"provisioners" json:"provisioners"` Tags StringMap `db:"tags" json:"tags"` LastSeenAt sql.NullTime `db:"last_seen_at" json:"last_seen_at"` + Version string `db:"version" json:"version"` } -func (q *sqlQuerier) InsertProvisionerDaemon(ctx context.Context, arg InsertProvisionerDaemonParams) (ProvisionerDaemon, error) { - row := q.db.QueryRowContext(ctx, insertProvisionerDaemon, - arg.ID, +func (q *sqlQuerier) UpsertProvisionerDaemon(ctx context.Context, arg UpsertProvisionerDaemonParams) (ProvisionerDaemon, error) { + row := q.db.QueryRowContext(ctx, upsertProvisionerDaemon, arg.CreatedAt, arg.Name, pq.Array(arg.Provisioners), arg.Tags, arg.LastSeenAt, + arg.Version, ) var i ProvisionerDaemon err := row.Scan( diff --git a/coderd/database/queries/provisionerdaemons.sql b/coderd/database/queries/provisionerdaemons.sql index 310dc09bef92e..7cc37bdc40faa 100644 --- a/coderd/database/queries/provisionerdaemons.sql +++ b/coderd/database/queries/provisionerdaemons.sql @@ -4,19 +4,6 @@ SELECT FROM provisioner_daemons; --- name: InsertProvisionerDaemon :one -INSERT INTO - provisioner_daemons ( - id, - created_at, - "name", - provisioners, - tags, - last_seen_at - ) -VALUES - ($1, $2, $3, $4, $5, $6) RETURNING *; - -- name: DeleteOldProvisionerDaemons :exec -- Delete provisioner daemons that have been created at least a week ago -- and have not connected to coderd since a week. @@ -26,3 +13,32 @@ DELETE FROM provisioner_daemons WHERE ( (created_at < (NOW() - INTERVAL '7 days') AND last_seen_at IS NULL) OR (last_seen_at IS NOT NULL AND last_seen_at < (NOW() - INTERVAL '7 days')) ); + +-- name: UpsertProvisionerDaemon :one +INSERT INTO + provisioner_daemons ( + id, + created_at, + "name", + provisioners, + tags, + last_seen_at, + "version" + ) +VALUES ( + gen_random_uuid(), + @created_at, + @name, + @provisioners, + @tags, + @last_seen_at, + @version +) ON CONFLICT("name", lower((tags ->> 'owner'::text))) DO UPDATE SET + provisioners = @provisioners, + tags = @tags, + last_seen_at = @last_seen_at, + "version" = @version +WHERE + -- Only ones with the same tags are allowed clobber + provisioner_daemons.tags <@ @tags :: jsonb +RETURNING *; diff --git a/coderd/database/unique_constraint.go b/coderd/database/unique_constraint.go index da5877ad0f071..e69ed46614c5f 100644 --- a/coderd/database/unique_constraint.go +++ b/coderd/database/unique_constraint.go @@ -27,7 +27,6 @@ const ( UniqueParameterSchemasPkey UniqueConstraint = "parameter_schemas_pkey" // ALTER TABLE ONLY parameter_schemas ADD CONSTRAINT parameter_schemas_pkey PRIMARY KEY (id); UniqueParameterValuesPkey UniqueConstraint = "parameter_values_pkey" // ALTER TABLE ONLY parameter_values ADD CONSTRAINT parameter_values_pkey PRIMARY KEY (id); UniqueParameterValuesScopeIDNameKey UniqueConstraint = "parameter_values_scope_id_name_key" // ALTER TABLE ONLY parameter_values ADD CONSTRAINT parameter_values_scope_id_name_key UNIQUE (scope_id, name); - UniqueProvisionerDaemonsNameKey UniqueConstraint = "provisioner_daemons_name_key" // ALTER TABLE ONLY provisioner_daemons ADD CONSTRAINT provisioner_daemons_name_key UNIQUE (name); UniqueProvisionerDaemonsPkey UniqueConstraint = "provisioner_daemons_pkey" // ALTER TABLE ONLY provisioner_daemons ADD CONSTRAINT provisioner_daemons_pkey PRIMARY KEY (id); UniqueProvisionerJobLogsPkey UniqueConstraint = "provisioner_job_logs_pkey" // ALTER TABLE ONLY provisioner_job_logs ADD CONSTRAINT provisioner_job_logs_pkey PRIMARY KEY (id); UniqueProvisionerJobsPkey UniqueConstraint = "provisioner_jobs_pkey" // ALTER TABLE ONLY provisioner_jobs ADD CONSTRAINT provisioner_jobs_pkey PRIMARY KEY (id); @@ -66,6 +65,7 @@ const ( UniqueIndexAPIKeyName UniqueConstraint = "idx_api_key_name" // CREATE UNIQUE INDEX idx_api_key_name ON api_keys USING btree (user_id, token_name) WHERE (login_type = 'token'::login_type); UniqueIndexOrganizationName UniqueConstraint = "idx_organization_name" // CREATE UNIQUE INDEX idx_organization_name ON organizations USING btree (name); UniqueIndexOrganizationNameLower UniqueConstraint = "idx_organization_name_lower" // CREATE UNIQUE INDEX idx_organization_name_lower ON organizations USING btree (lower(name)); + UniqueIndexProvisionerDaemonsNameOwnerKey UniqueConstraint = "idx_provisioner_daemons_name_owner_key" // CREATE UNIQUE INDEX idx_provisioner_daemons_name_owner_key ON provisioner_daemons USING btree (name, lower((tags ->> 'owner'::text))); UniqueIndexUsersEmail UniqueConstraint = "idx_users_email" // CREATE UNIQUE INDEX idx_users_email ON users USING btree (email) WHERE (deleted = false); UniqueIndexUsersUsername UniqueConstraint = "idx_users_username" // CREATE UNIQUE INDEX idx_users_username ON users USING btree (username) WHERE (deleted = false); UniqueTemplatesOrganizationIDNameIndex UniqueConstraint = "templates_organization_id_name_idx" // CREATE UNIQUE INDEX templates_organization_id_name_idx ON templates USING btree (organization_id, lower((name)::text)) WHERE (deleted = false); diff --git a/provisionersdk/provisionertags.go b/provisionersdk/provisionertags.go index 3f64d10c5e61f..970cf2094dd74 100644 --- a/provisionersdk/provisionertags.go +++ b/provisionersdk/provisionertags.go @@ -14,6 +14,7 @@ const ( // If the scope is "user", the "owner" is changed to the user ID. // This is for user-scoped provisioner daemons, where users should // own their own operations. +// Otherwise, the "owner" tag is always empty. func MutateTags(userID uuid.UUID, tags map[string]string) map[string]string { if tags == nil { tags = map[string]string{} @@ -21,11 +22,13 @@ func MutateTags(userID uuid.UUID, tags map[string]string) map[string]string { _, ok := tags[TagScope] if !ok { tags[TagScope] = ScopeOrganization + delete(tags, TagOwner) } switch tags[TagScope] { case ScopeUser: tags[TagOwner] = userID.String() case ScopeOrganization: + delete(tags, TagOwner) default: tags[TagScope] = ScopeOrganization } diff --git a/provisionersdk/provisionertags_test.go b/provisionersdk/provisionertags_test.go index bc9d86c7991f5..26ecc1d12b24a 100644 --- a/provisionersdk/provisionertags_test.go +++ b/provisionersdk/provisionertags_test.go @@ -54,6 +54,27 @@ func TestMutateTags(t *testing.T) { provisionersdk.TagScope: provisionersdk.ScopeOrganization, }, }, + { + name: "organization scope with owner", + tags: map[string]string{ + provisionersdk.TagScope: provisionersdk.ScopeOrganization, + provisionersdk.TagOwner: testUserID.String(), + }, + userID: uuid.Nil, + want: map[string]string{ + provisionersdk.TagScope: provisionersdk.ScopeOrganization, + }, + }, + { + name: "owner tag with no other context", + tags: map[string]string{ + provisionersdk.TagOwner: testUserID.String(), + }, + userID: uuid.Nil, + want: map[string]string{ + provisionersdk.TagScope: provisionersdk.ScopeOrganization, + }, + }, { name: "invalid scope", tags: map[string]string{provisionersdk.TagScope: "360noscope"},