diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index bf199aac9f..12a7321189 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -3054,6 +3054,43 @@ const docTemplate = `{ } } }, + "/organizations/{organization}/provisionerkeys/daemons": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Enterprise" + ], + "summary": "List provisioner key daemons", + "operationId": "list-provisioner-key-daemons", + "parameters": [ + { + "type": "string", + "description": "Organization ID", + "name": "organization", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.ProvisionerKeyDaemons" + } + } + } + } + } + }, "/organizations/{organization}/provisionerkeys/{provisionerkey}": { "delete": { "security": [ @@ -11531,6 +11568,10 @@ const docTemplate = `{ "type": "string", "format": "uuid" }, + "key_id": { + "type": "string", + "format": "uuid" + }, "last_seen_at": { "type": "string", "format": "date-time" @@ -11714,6 +11755,20 @@ const docTemplate = `{ } } }, + "codersdk.ProvisionerKeyDaemons": { + "type": "object", + "properties": { + "daemons": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.ProvisionerDaemon" + } + }, + "key": { + "$ref": "#/definitions/codersdk.ProvisionerKey" + } + } + }, "codersdk.ProvisionerLogLevel": { "type": "string", "enum": [ diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 8338dcbec6..23a1f369c5 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -2682,6 +2682,39 @@ } } }, + "/organizations/{organization}/provisionerkeys/daemons": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": ["application/json"], + "tags": ["Enterprise"], + "summary": "List provisioner key daemons", + "operationId": "list-provisioner-key-daemons", + "parameters": [ + { + "type": "string", + "description": "Organization ID", + "name": "organization", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.ProvisionerKeyDaemons" + } + } + } + } + } + }, "/organizations/{organization}/provisionerkeys/{provisionerkey}": { "delete": { "security": [ @@ -10401,6 +10434,10 @@ "type": "string", "format": "uuid" }, + "key_id": { + "type": "string", + "format": "uuid" + }, "last_seen_at": { "type": "string", "format": "date-time" @@ -10576,6 +10613,20 @@ } } }, + "codersdk.ProvisionerKeyDaemons": { + "type": "object", + "properties": { + "daemons": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.ProvisionerDaemon" + } + }, + "key": { + "$ref": "#/definitions/codersdk.ProvisionerKey" + } + } + }, "codersdk.ProvisionerLogLevel": { "type": "string", "enum": ["debug"], diff --git a/coderd/coderd.go b/coderd/coderd.go index de6d098c42..57015b0c7d 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -1491,6 +1491,11 @@ func (api *API) CreateInMemoryTaggedProvisionerDaemon(dialCtx context.Context, n dbTypes = append(dbTypes, database.ProvisionerType(tp)) } + keyID, err := uuid.Parse(string(codersdk.ProvisionerKeyIDBuiltIn)) + if err != nil { + return nil, xerrors.Errorf("failed to parse built-in provisioner key ID: %w", err) + } + //nolint:gocritic // in-memory provisioners are owned by system daemon, err := api.Database.UpsertProvisionerDaemon(dbauthz.AsSystemRestricted(dialCtx), database.UpsertProvisionerDaemonParams{ Name: name, @@ -1501,6 +1506,7 @@ func (api *API) CreateInMemoryTaggedProvisionerDaemon(dialCtx context.Context, n LastSeenAt: sql.NullTime{Time: dbtime.Now(), Valid: true}, Version: buildinfo.Version(), APIVersion: proto.CurrentVersion.String(), + KeyID: keyID, }) if err != nil { return nil, xerrors.Errorf("failed to create in-memory provisioner daemon: %w", err) diff --git a/coderd/database/db2sdk/db2sdk.go b/coderd/database/db2sdk/db2sdk.go index 134b4e6cd8..a8e2c6cb93 100644 --- a/coderd/database/db2sdk/db2sdk.go +++ b/coderd/database/db2sdk/db2sdk.go @@ -533,6 +533,7 @@ func ProvisionerDaemon(dbDaemon database.ProvisionerDaemon) codersdk.Provisioner Tags: dbDaemon.Tags, Version: dbDaemon.Version, APIVersion: dbDaemon.APIVersion, + KeyID: dbDaemon.KeyID, } for _, provisionerType := range dbDaemon.Provisioners { result.Provisioners = append(result.Provisioners, codersdk.ProvisionerType(provisionerType)) @@ -540,6 +541,30 @@ func ProvisionerDaemon(dbDaemon database.ProvisionerDaemon) codersdk.Provisioner return result } +func RecentProvisionerDaemons(now time.Time, staleInterval time.Duration, daemons []database.ProvisionerDaemon) []codersdk.ProvisionerDaemon { + results := []codersdk.ProvisionerDaemon{} + + for _, daemon := range daemons { + // Daemon never connected, skip. + if !daemon.LastSeenAt.Valid { + continue + } + // Daemon has gone away, skip. + if now.Sub(daemon.LastSeenAt.Time) > staleInterval { + continue + } + + results = append(results, ProvisionerDaemon(daemon)) + } + + // Ensure stable order for display and for tests + sort.Slice(results, func(i, j int) bool { + return results[i].Name < results[j].Name + }) + + return results +} + func SlimRole(role rbac.Role) codersdk.SlimRole { orgID := "" if role.Identifier.OrganizationID != uuid.Nil { diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 9ae60bec90..0e3b0fe134 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -3066,6 +3066,10 @@ func (q *querier) ListProvisionerKeysByOrganization(ctx context.Context, organiz return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.ListProvisionerKeysByOrganization)(ctx, organizationID) } +func (q *querier) ListProvisionerKeysByOrganizationExcludeReserved(ctx context.Context, organizationID uuid.UUID) ([]database.ProvisionerKey, error) { + return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.ListProvisionerKeysByOrganizationExcludeReserved)(ctx, organizationID) +} + func (q *querier) ListWorkspaceAgentPortShares(ctx context.Context, workspaceID uuid.UUID) ([]database.WorkspaceAgentPortShare, error) { workspace, err := q.db.GetWorkspaceByID(ctx, workspaceID) if err != nil { diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index ebe4674be7..3e0c602949 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -2020,6 +2020,19 @@ func (s *MethodTestSuite) TestProvisionerKeys() { } check.Args(org.ID).Asserts(pk, policy.ActionRead).Returns(pks) })) + s.Run("ListProvisionerKeysByOrganizationExcludeReserved", s.Subtest(func(db database.Store, check *expects) { + org := dbgen.Organization(s.T(), db, database.Organization{}) + pk := dbgen.ProvisionerKey(s.T(), db, database.ProvisionerKey{OrganizationID: org.ID}) + pks := []database.ProvisionerKey{ + { + ID: pk.ID, + CreatedAt: pk.CreatedAt, + OrganizationID: pk.OrganizationID, + Name: pk.Name, + }, + } + check.Args(org.ID).Asserts(pk, policy.ActionRead).Returns(pks) + })) s.Run("DeleteProvisionerKey", s.Subtest(func(db database.Store, check *expects) { org := dbgen.Organization(s.T(), db, database.Organization{}) pk := dbgen.ProvisionerKey(s.T(), db, database.ProvisionerKey{OrganizationID: org.ID}) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index d2dce3c3ba..23430a953f 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -68,6 +68,7 @@ func New() database.Store { notificationPreferences: make([]database.NotificationPreference, 0), parameterSchemas: make([]database.ParameterSchema, 0), provisionerDaemons: make([]database.ProvisionerDaemon, 0), + provisionerKeys: make([]database.ProvisionerKey, 0), workspaceAgents: make([]database.WorkspaceAgent, 0), provisionerJobLogs: make([]database.ProvisionerJobLog, 0), workspaceResources: make([]database.WorkspaceResource, 0), @@ -108,6 +109,41 @@ func New() database.Store { q.defaultProxyDisplayName = "Default" q.defaultProxyIconURL = "/emojis/1f3e1.png" + + _, err = q.InsertProvisionerKey(context.Background(), database.InsertProvisionerKeyParams{ + ID: uuid.MustParse(codersdk.ProvisionerKeyIDBuiltIn), + OrganizationID: defaultOrg.ID, + CreatedAt: dbtime.Now(), + HashedSecret: []byte{}, + Name: codersdk.ProvisionerKeyNameBuiltIn, + Tags: map[string]string{}, + }) + if err != nil { + panic(xerrors.Errorf("failed to create built-in provisioner key: %w", err)) + } + _, err = q.InsertProvisionerKey(context.Background(), database.InsertProvisionerKeyParams{ + ID: uuid.MustParse(codersdk.ProvisionerKeyIDUserAuth), + OrganizationID: defaultOrg.ID, + CreatedAt: dbtime.Now(), + HashedSecret: []byte{}, + Name: codersdk.ProvisionerKeyNameUserAuth, + Tags: map[string]string{}, + }) + if err != nil { + panic(xerrors.Errorf("failed to create user-auth provisioner key: %w", err)) + } + _, err = q.InsertProvisionerKey(context.Background(), database.InsertProvisionerKeyParams{ + ID: uuid.MustParse(codersdk.ProvisionerKeyIDPSK), + OrganizationID: defaultOrg.ID, + CreatedAt: dbtime.Now(), + HashedSecret: []byte{}, + Name: codersdk.ProvisionerKeyNamePSK, + Tags: map[string]string{}, + }) + if err != nil { + panic(xerrors.Errorf("failed to create psk provisioner key: %w", err)) + } + return q } @@ -7582,6 +7618,25 @@ func (q *FakeQuerier) ListProvisionerKeysByOrganization(_ context.Context, organ return keys, nil } +func (q *FakeQuerier) ListProvisionerKeysByOrganizationExcludeReserved(_ context.Context, organizationID uuid.UUID) ([]database.ProvisionerKey, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + keys := make([]database.ProvisionerKey, 0) + for _, key := range q.provisionerKeys { + if key.ID.String() == codersdk.ProvisionerKeyIDBuiltIn || + key.ID.String() == codersdk.ProvisionerKeyIDUserAuth || + key.ID.String() == codersdk.ProvisionerKeyIDPSK { + continue + } + if key.OrganizationID == organizationID { + keys = append(keys, key) + } + } + + return keys, nil +} + func (q *FakeQuerier) ListWorkspaceAgentPortShares(_ context.Context, workspaceID uuid.UUID) ([]database.WorkspaceAgentPortShare, error) { q.mutex.Lock() defer q.mutex.Unlock() @@ -9311,6 +9366,7 @@ func (q *FakeQuerier) UpsertProvisionerDaemon(_ context.Context, arg database.Up Version: arg.Version, APIVersion: arg.APIVersion, OrganizationID: arg.OrganizationID, + KeyID: arg.KeyID, } q.provisionerDaemons = append(q.provisionerDaemons, d) return d, nil diff --git a/coderd/database/dbmetrics/dbmetrics.go b/coderd/database/dbmetrics/dbmetrics.go index 56dd9dbf09..e720a906be 100644 --- a/coderd/database/dbmetrics/dbmetrics.go +++ b/coderd/database/dbmetrics/dbmetrics.go @@ -1922,6 +1922,13 @@ func (m metricsStore) ListProvisionerKeysByOrganization(ctx context.Context, org return r0, r1 } +func (m metricsStore) ListProvisionerKeysByOrganizationExcludeReserved(ctx context.Context, organizationID uuid.UUID) ([]database.ProvisionerKey, error) { + start := time.Now() + r0, r1 := m.s.ListProvisionerKeysByOrganizationExcludeReserved(ctx, organizationID) + m.queryLatencies.WithLabelValues("ListProvisionerKeysByOrganizationExcludeReserved").Observe(time.Since(start).Seconds()) + return r0, r1 +} + func (m metricsStore) ListWorkspaceAgentPortShares(ctx context.Context, workspaceID uuid.UUID) ([]database.WorkspaceAgentPortShare, error) { start := time.Now() r0, r1 := m.s.ListWorkspaceAgentPortShares(ctx, workspaceID) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index 634d02f7a9..018d2e9cfd 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -4045,6 +4045,21 @@ func (mr *MockStoreMockRecorder) ListProvisionerKeysByOrganization(arg0, arg1 an return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListProvisionerKeysByOrganization", reflect.TypeOf((*MockStore)(nil).ListProvisionerKeysByOrganization), arg0, arg1) } +// ListProvisionerKeysByOrganizationExcludeReserved mocks base method. +func (m *MockStore) ListProvisionerKeysByOrganizationExcludeReserved(arg0 context.Context, arg1 uuid.UUID) ([]database.ProvisionerKey, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListProvisionerKeysByOrganizationExcludeReserved", arg0, arg1) + ret0, _ := ret[0].([]database.ProvisionerKey) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListProvisionerKeysByOrganizationExcludeReserved indicates an expected call of ListProvisionerKeysByOrganizationExcludeReserved. +func (mr *MockStoreMockRecorder) ListProvisionerKeysByOrganizationExcludeReserved(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListProvisionerKeysByOrganizationExcludeReserved", reflect.TypeOf((*MockStore)(nil).ListProvisionerKeysByOrganizationExcludeReserved), arg0, arg1) +} + // ListWorkspaceAgentPortShares mocks base method. func (m *MockStore) ListWorkspaceAgentPortShares(arg0 context.Context, arg1 uuid.UUID) ([]database.WorkspaceAgentPortShare, error) { m.ctrl.T.Helper() diff --git a/coderd/database/dbpurge/dbpurge_test.go b/coderd/database/dbpurge/dbpurge_test.go index c4fdb27eb1..8353a1cbdc 100644 --- a/coderd/database/dbpurge/dbpurge_test.go +++ b/coderd/database/dbpurge/dbpurge_test.go @@ -26,6 +26,7 @@ import ( "github.com/coder/coder/v2/coderd/database/dbrollup" "github.com/coder/coder/v2/coderd/database/dbtestutil" "github.com/coder/coder/v2/coderd/database/dbtime" + "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/provisionerd/proto" "github.com/coder/coder/v2/provisionersdk" "github.com/coder/coder/v2/testutil" @@ -412,6 +413,7 @@ func TestDeleteOldProvisionerDaemons(t *testing.T) { Version: "1.0.0", APIVersion: proto.CurrentVersion.String(), OrganizationID: defaultOrg.ID, + KeyID: uuid.MustParse(codersdk.ProvisionerKeyIDBuiltIn), }) require.NoError(t, err) _, err = db.UpsertProvisionerDaemon(ctx, database.UpsertProvisionerDaemonParams{ @@ -424,6 +426,7 @@ func TestDeleteOldProvisionerDaemons(t *testing.T) { Version: "1.0.0", APIVersion: proto.CurrentVersion.String(), OrganizationID: defaultOrg.ID, + KeyID: uuid.MustParse(codersdk.ProvisionerKeyIDBuiltIn), }) require.NoError(t, err) _, err = db.UpsertProvisionerDaemon(ctx, database.UpsertProvisionerDaemonParams{ @@ -438,6 +441,7 @@ func TestDeleteOldProvisionerDaemons(t *testing.T) { Version: "1.0.0", APIVersion: proto.CurrentVersion.String(), OrganizationID: defaultOrg.ID, + KeyID: uuid.MustParse(codersdk.ProvisionerKeyIDBuiltIn), }) require.NoError(t, err) _, err = db.UpsertProvisionerDaemon(ctx, database.UpsertProvisionerDaemonParams{ @@ -453,6 +457,7 @@ func TestDeleteOldProvisionerDaemons(t *testing.T) { Version: "1.0.0", APIVersion: proto.CurrentVersion.String(), OrganizationID: defaultOrg.ID, + KeyID: uuid.MustParse(codersdk.ProvisionerKeyIDBuiltIn), }) require.NoError(t, err) diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 6638d52745..4bf3aab10a 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -832,7 +832,8 @@ CREATE TABLE provisioner_daemons ( last_seen_at timestamp with time zone, version text DEFAULT ''::text NOT NULL, api_version text DEFAULT '1.0'::text NOT NULL, - organization_id uuid NOT NULL + organization_id uuid NOT NULL, + key_id uuid NOT NULL ); COMMENT ON COLUMN provisioner_daemons.api_version IS 'The API version of the provisioner daemon'; @@ -2095,6 +2096,9 @@ ALTER TABLE ONLY organization_members ALTER TABLE ONLY parameter_schemas ADD CONSTRAINT parameter_schemas_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_jobs(id) ON DELETE CASCADE; +ALTER TABLE ONLY provisioner_daemons + ADD CONSTRAINT provisioner_daemons_key_id_fkey FOREIGN KEY (key_id) REFERENCES provisioner_keys(id) ON DELETE CASCADE; + ALTER TABLE ONLY provisioner_daemons ADD CONSTRAINT provisioner_daemons_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; diff --git a/coderd/database/foreign_key_constraint.go b/coderd/database/foreign_key_constraint.go index 0c578255f0..7c9f3f55c7 100644 --- a/coderd/database/foreign_key_constraint.go +++ b/coderd/database/foreign_key_constraint.go @@ -27,6 +27,7 @@ const ( ForeignKeyOrganizationMembersOrganizationIDUUID ForeignKeyConstraint = "organization_members_organization_id_uuid_fkey" // ALTER TABLE ONLY organization_members ADD CONSTRAINT organization_members_organization_id_uuid_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; ForeignKeyOrganizationMembersUserIDUUID ForeignKeyConstraint = "organization_members_user_id_uuid_fkey" // ALTER TABLE ONLY organization_members ADD CONSTRAINT organization_members_user_id_uuid_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; ForeignKeyParameterSchemasJobID ForeignKeyConstraint = "parameter_schemas_job_id_fkey" // ALTER TABLE ONLY parameter_schemas ADD CONSTRAINT parameter_schemas_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_jobs(id) ON DELETE CASCADE; + ForeignKeyProvisionerDaemonsKeyID ForeignKeyConstraint = "provisioner_daemons_key_id_fkey" // ALTER TABLE ONLY provisioner_daemons ADD CONSTRAINT provisioner_daemons_key_id_fkey FOREIGN KEY (key_id) REFERENCES provisioner_keys(id) ON DELETE CASCADE; ForeignKeyProvisionerDaemonsOrganizationID ForeignKeyConstraint = "provisioner_daemons_organization_id_fkey" // ALTER TABLE ONLY provisioner_daemons ADD CONSTRAINT provisioner_daemons_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; ForeignKeyProvisionerJobLogsJobID ForeignKeyConstraint = "provisioner_job_logs_job_id_fkey" // ALTER TABLE ONLY provisioner_job_logs ADD CONSTRAINT provisioner_job_logs_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_jobs(id) ON DELETE CASCADE; ForeignKeyProvisionerJobTimingsJobID ForeignKeyConstraint = "provisioner_job_timings_job_id_fkey" // ALTER TABLE ONLY provisioner_job_timings ADD CONSTRAINT provisioner_job_timings_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_jobs(id) ON DELETE CASCADE; diff --git a/coderd/database/migrations/000250_built_in_psk_provisioner_keys.down.sql b/coderd/database/migrations/000250_built_in_psk_provisioner_keys.down.sql new file mode 100644 index 0000000000..9d20666194 --- /dev/null +++ b/coderd/database/migrations/000250_built_in_psk_provisioner_keys.down.sql @@ -0,0 +1,5 @@ +ALTER TABLE provisioner_daemons DROP COLUMN key_id; + +DELETE FROM provisioner_keys WHERE name = 'built-in'; +DELETE FROM provisioner_keys WHERE name = 'psk'; +DELETE FROM provisioner_keys WHERE name = 'user-auth'; diff --git a/coderd/database/migrations/000250_built_in_psk_provisioner_keys.up.sql b/coderd/database/migrations/000250_built_in_psk_provisioner_keys.up.sql new file mode 100644 index 0000000000..61660b5cf1 --- /dev/null +++ b/coderd/database/migrations/000250_built_in_psk_provisioner_keys.up.sql @@ -0,0 +1,6 @@ +INSERT INTO provisioner_keys (id, created_at, organization_id, name, hashed_secret, tags) VALUES ('00000000-0000-0000-0000-000000000001'::uuid, NOW(), (SELECT id FROM organizations WHERE is_default = true), 'built-in', ''::bytea, '{}'); +INSERT INTO provisioner_keys (id, created_at, organization_id, name, hashed_secret, tags) VALUES ('00000000-0000-0000-0000-000000000002'::uuid, NOW(), (SELECT id FROM organizations WHERE is_default = true), 'user-auth', ''::bytea, '{}'); +INSERT INTO provisioner_keys (id, created_at, organization_id, name, hashed_secret, tags) VALUES ('00000000-0000-0000-0000-000000000003'::uuid, NOW(), (SELECT id FROM organizations WHERE is_default = true), 'psk', ''::bytea, '{}'); + +ALTER TABLE provisioner_daemons ADD COLUMN key_id UUID REFERENCES provisioner_keys(id) ON DELETE CASCADE DEFAULT '00000000-0000-0000-0000-000000000001'::uuid NOT NULL; +ALTER TABLE provisioner_daemons ALTER COLUMN key_id DROP DEFAULT; diff --git a/coderd/database/models.go b/coderd/database/models.go index 9e0283ba85..4ee2f82818 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -2311,6 +2311,7 @@ type ProvisionerDaemon struct { // The API version of the provisioner daemon APIVersion string `db:"api_version" json:"api_version"` OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` + KeyID uuid.UUID `db:"key_id" json:"key_id"` } type ProvisionerJob struct { diff --git a/coderd/database/querier.go b/coderd/database/querier.go index b6a1eb5e15..c8f6449be3 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -391,6 +391,7 @@ type sqlcQuerier interface { InsertWorkspaceResource(ctx context.Context, arg InsertWorkspaceResourceParams) (WorkspaceResource, error) InsertWorkspaceResourceMetadata(ctx context.Context, arg InsertWorkspaceResourceMetadataParams) ([]WorkspaceResourceMetadatum, error) ListProvisionerKeysByOrganization(ctx context.Context, organizationID uuid.UUID) ([]ProvisionerKey, error) + ListProvisionerKeysByOrganizationExcludeReserved(ctx context.Context, organizationID uuid.UUID) ([]ProvisionerKey, error) ListWorkspaceAgentPortShares(ctx context.Context, workspaceID uuid.UUID) ([]WorkspaceAgentPortShare, error) // Arguments are optional with uuid.Nil to ignore. // - Use just 'organization_id' to get all members of an org diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 44219c4500..173448dd93 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -4971,7 +4971,7 @@ func (q *sqlQuerier) DeleteOldProvisionerDaemons(ctx context.Context) error { const getProvisionerDaemons = `-- name: GetProvisionerDaemons :many SELECT - id, created_at, name, provisioners, replica_id, tags, last_seen_at, version, api_version, organization_id + id, created_at, name, provisioners, replica_id, tags, last_seen_at, version, api_version, organization_id, key_id FROM provisioner_daemons ` @@ -4996,6 +4996,7 @@ func (q *sqlQuerier) GetProvisionerDaemons(ctx context.Context) ([]ProvisionerDa &i.Version, &i.APIVersion, &i.OrganizationID, + &i.KeyID, ); err != nil { return nil, err } @@ -5012,7 +5013,7 @@ func (q *sqlQuerier) GetProvisionerDaemons(ctx context.Context) ([]ProvisionerDa const getProvisionerDaemonsByOrganization = `-- name: GetProvisionerDaemonsByOrganization :many SELECT - id, created_at, name, provisioners, replica_id, tags, last_seen_at, version, api_version, organization_id + id, created_at, name, provisioners, replica_id, tags, last_seen_at, version, api_version, organization_id, key_id FROM provisioner_daemons WHERE @@ -5039,6 +5040,7 @@ func (q *sqlQuerier) GetProvisionerDaemonsByOrganization(ctx context.Context, or &i.Version, &i.APIVersion, &i.OrganizationID, + &i.KeyID, ); err != nil { return nil, err } @@ -5084,7 +5086,8 @@ INSERT INTO last_seen_at, "version", organization_id, - api_version + api_version, + key_id ) VALUES ( gen_random_uuid(), @@ -5095,15 +5098,17 @@ VALUES ( $5, $6, $7, - $8 + $8, + $9 ) ON CONFLICT("organization_id", "name", LOWER(COALESCE(tags ->> 'owner'::text, ''::text))) DO UPDATE SET provisioners = $3, tags = $4, last_seen_at = $5, "version" = $6, api_version = $8, - organization_id = $7 -RETURNING id, created_at, name, provisioners, replica_id, tags, last_seen_at, version, api_version, organization_id + organization_id = $7, + key_id = $9 +RETURNING id, created_at, name, provisioners, replica_id, tags, last_seen_at, version, api_version, organization_id, key_id ` type UpsertProvisionerDaemonParams struct { @@ -5115,6 +5120,7 @@ type UpsertProvisionerDaemonParams struct { Version string `db:"version" json:"version"` OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` APIVersion string `db:"api_version" json:"api_version"` + KeyID uuid.UUID `db:"key_id" json:"key_id"` } func (q *sqlQuerier) UpsertProvisionerDaemon(ctx context.Context, arg UpsertProvisionerDaemonParams) (ProvisionerDaemon, error) { @@ -5127,6 +5133,7 @@ func (q *sqlQuerier) UpsertProvisionerDaemon(ctx context.Context, arg UpsertProv arg.Version, arg.OrganizationID, arg.APIVersion, + arg.KeyID, ) var i ProvisionerDaemon err := row.Scan( @@ -5140,6 +5147,7 @@ func (q *sqlQuerier) UpsertProvisionerDaemon(ctx context.Context, arg UpsertProv &i.Version, &i.APIVersion, &i.OrganizationID, + &i.KeyID, ) return i, err } @@ -6021,6 +6029,54 @@ func (q *sqlQuerier) ListProvisionerKeysByOrganization(ctx context.Context, orga return items, nil } +const listProvisionerKeysByOrganizationExcludeReserved = `-- name: ListProvisionerKeysByOrganizationExcludeReserved :many +SELECT + id, created_at, organization_id, name, hashed_secret, tags +FROM + provisioner_keys +WHERE + organization_id = $1 +AND + -- exclude reserved built-in key + id != '00000000-0000-0000-0000-000000000001'::uuid +AND + -- exclude reserved user-auth key + id != '00000000-0000-0000-0000-000000000002'::uuid +AND + -- exclude reserved psk key + id != '00000000-0000-0000-0000-000000000003'::uuid +` + +func (q *sqlQuerier) ListProvisionerKeysByOrganizationExcludeReserved(ctx context.Context, organizationID uuid.UUID) ([]ProvisionerKey, error) { + rows, err := q.db.QueryContext(ctx, listProvisionerKeysByOrganizationExcludeReserved, organizationID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ProvisionerKey + for rows.Next() { + var i ProvisionerKey + if err := rows.Scan( + &i.ID, + &i.CreatedAt, + &i.OrganizationID, + &i.Name, + &i.HashedSecret, + &i.Tags, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const getWorkspaceProxies = `-- name: GetWorkspaceProxies :many SELECT id, name, display_name, icon, url, wildcard_hostname, created_at, updated_at, deleted, token_hashed_secret, region_id, derp_enabled, derp_only, version diff --git a/coderd/database/queries/provisionerdaemons.sql b/coderd/database/queries/provisionerdaemons.sql index f7c974b3fb..bee1c6e92f 100644 --- a/coderd/database/queries/provisionerdaemons.sql +++ b/coderd/database/queries/provisionerdaemons.sql @@ -33,7 +33,8 @@ INSERT INTO last_seen_at, "version", organization_id, - api_version + api_version, + key_id ) VALUES ( gen_random_uuid(), @@ -44,14 +45,16 @@ VALUES ( @last_seen_at, @version, @organization_id, - @api_version + @api_version, + @key_id ) ON CONFLICT("organization_id", "name", LOWER(COALESCE(tags ->> 'owner'::text, ''::text))) DO UPDATE SET provisioners = @provisioners, tags = @tags, last_seen_at = @last_seen_at, "version" = @version, api_version = @api_version, - organization_id = @organization_id + organization_id = @organization_id, + key_id = @key_id RETURNING *; -- name: UpdateProvisionerDaemonLastSeenAt :exec diff --git a/coderd/database/queries/provisionerkeys.sql b/coderd/database/queries/provisionerkeys.sql index cb4c763f10..3fb05a8d0f 100644 --- a/coderd/database/queries/provisionerkeys.sql +++ b/coderd/database/queries/provisionerkeys.sql @@ -37,6 +37,23 @@ WHERE AND lower(name) = lower(@name); +-- name: ListProvisionerKeysByOrganizationExcludeReserved :many +SELECT + * +FROM + provisioner_keys +WHERE + organization_id = $1 +AND + -- exclude reserved built-in key + id != '00000000-0000-0000-0000-000000000001'::uuid +AND + -- exclude reserved user-auth key + id != '00000000-0000-0000-0000-000000000002'::uuid +AND + -- exclude reserved psk key + id != '00000000-0000-0000-0000-000000000003'::uuid; + -- name: ListProvisionerKeysByOrganization :many SELECT * diff --git a/coderd/healthcheck/provisioner.go b/coderd/healthcheck/provisioner.go index b15899dc09..370a5ad04d 100644 --- a/coderd/healthcheck/provisioner.go +++ b/coderd/healthcheck/provisioner.go @@ -2,7 +2,6 @@ package healthcheck import ( "context" - "sort" "time" "golang.org/x/mod/semver" @@ -80,23 +79,10 @@ func (r *ProvisionerDaemonsReport) Run(ctx context.Context, opts *ProvisionerDae return } - // Ensure stable order for display and for tests - sort.Slice(daemons, func(i, j int) bool { - return daemons[i].Name < daemons[j].Name - }) - - for _, daemon := range daemons { - // Daemon never connected, skip. - if !daemon.LastSeenAt.Valid { - continue - } - // Daemon has gone away, skip. - if now.Sub(daemon.LastSeenAt.Time) > (opts.StaleInterval) { - continue - } - + recentDaemons := db2sdk.RecentProvisionerDaemons(now, opts.StaleInterval, daemons) + for _, daemon := range recentDaemons { it := healthsdk.ProvisionerDaemonsReportItem{ - ProvisionerDaemon: db2sdk.ProvisionerDaemon(daemon), + ProvisionerDaemon: daemon, Warnings: make([]health.Message, 0), } diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go index 9028af0be3..d429ae1a1b 100644 --- a/coderd/provisionerdserver/provisionerdserver_test.go +++ b/coderd/provisionerdserver/provisionerdserver_test.go @@ -1956,6 +1956,7 @@ func setup(t *testing.T, ignoreLogErrors bool, ov *overrides) (proto.DRPCProvisi Version: buildinfo.Version(), APIVersion: proto.CurrentVersion.String(), OrganizationID: defOrg.ID, + KeyID: uuid.MustParse(codersdk.ProvisionerKeyIDBuiltIn), }) require.NoError(t, err) diff --git a/codersdk/provisionerdaemons.go b/codersdk/provisionerdaemons.go index 0dd111ea1b..77d5cba8c8 100644 --- a/codersdk/provisionerdaemons.go +++ b/codersdk/provisionerdaemons.go @@ -38,6 +38,7 @@ const ( type ProvisionerDaemon struct { ID uuid.UUID `json:"id" format:"uuid"` OrganizationID uuid.UUID `json:"organization_id" format:"uuid"` + KeyID uuid.UUID `json:"key_id" format:"uuid"` CreatedAt time.Time `json:"created_at" format:"date-time"` LastSeenAt NullTime `json:"last_seen_at,omitempty" format:"date-time"` Name string `json:"name"` @@ -282,6 +283,31 @@ type ProvisionerKey struct { // HashedSecret - never include the access token in the API response } +type ProvisionerKeyDaemons struct { + Key ProvisionerKey `json:"key"` + Daemons []ProvisionerDaemon `json:"daemons"` +} + +const ( + ProvisionerKeyIDBuiltIn = "00000000-0000-0000-0000-000000000001" + ProvisionerKeyIDUserAuth = "00000000-0000-0000-0000-000000000002" + ProvisionerKeyIDPSK = "00000000-0000-0000-0000-000000000003" +) + +const ( + ProvisionerKeyNameBuiltIn = "built-in" + ProvisionerKeyNameUserAuth = "user-auth" + ProvisionerKeyNamePSK = "psk" +) + +func ReservedProvisionerKeyNames() []string { + return []string{ + ProvisionerKeyNameBuiltIn, + ProvisionerKeyNameUserAuth, + ProvisionerKeyNamePSK, + } +} + type CreateProvisionerKeyRequest struct { Name string `json:"name"` Tags map[string]string `json:"tags"` @@ -327,6 +353,24 @@ func (c *Client) ListProvisionerKeys(ctx context.Context, organizationID uuid.UU return resp, json.NewDecoder(res.Body).Decode(&resp) } +// ListProvisionerKeyDaemons lists all provisioner keys with their associated daemons for an organization. +func (c *Client) ListProvisionerKeyDaemons(ctx context.Context, organizationID uuid.UUID) ([]ProvisionerKeyDaemons, error) { + res, err := c.Request(ctx, http.MethodGet, + fmt.Sprintf("/api/v2/organizations/%s/provisionerkeys/daemons", organizationID.String()), + nil, + ) + if err != nil { + return nil, xerrors.Errorf("make request: %w", err) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return nil, ReadBodyAsError(res) + } + var resp []ProvisionerKeyDaemons + return resp, json.NewDecoder(res.Body).Decode(&resp) +} + // DeleteProvisionerKey deletes a provisioner key. func (c *Client) DeleteProvisionerKey(ctx context.Context, organizationID uuid.UUID, name string) error { res, err := c.Request(ctx, http.MethodDelete, diff --git a/docs/reference/api/debug.md b/docs/reference/api/debug.md index 9ca6379316..630e07510c 100644 --- a/docs/reference/api/debug.md +++ b/docs/reference/api/debug.md @@ -290,6 +290,7 @@ curl -X GET http://coder-server:8080/api/v2/debug/health \ "api_version": "string", "created_at": "2019-08-24T14:15:22Z", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "key_id": "1e779c8a-6786-4c89-b7c3-a6666f5fd6b5", "last_seen_at": "2019-08-24T14:15:22Z", "name": "string", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", diff --git a/docs/reference/api/enterprise.md b/docs/reference/api/enterprise.md index ea6f2d96d7..555acc4af8 100644 --- a/docs/reference/api/enterprise.md +++ b/docs/reference/api/enterprise.md @@ -1493,6 +1493,7 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/provisi "api_version": "string", "created_at": "2019-08-24T14:15:22Z", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "key_id": "1e779c8a-6786-4c89-b7c3-a6666f5fd6b5", "last_seen_at": "2019-08-24T14:15:22Z", "name": "string", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", @@ -1522,6 +1523,7 @@ Status Code **200** | `» api_version` | string | false | | | | `» created_at` | string(date-time) | false | | | | `» id` | string(uuid) | false | | | +| `» key_id` | string(uuid) | false | | | | `» last_seen_at` | string(date-time) | false | | | | `» name` | string | false | | | | `» organization_id` | string(uuid) | false | | | @@ -1655,6 +1657,98 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/provis To perform this operation, you must be authenticated. [Learn more](authentication.md). +## List provisioner key daemons + +### Code samples + +```shell +# Example request using curl +curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/provisionerkeys/daemons \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`GET /organizations/{organization}/provisionerkeys/daemons` + +### Parameters + +| Name | In | Type | Required | Description | +| -------------- | ---- | ------ | -------- | --------------- | +| `organization` | path | string | true | Organization ID | + +### Example responses + +> 200 Response + +```json +[ + { + "daemons": [ + { + "api_version": "string", + "created_at": "2019-08-24T14:15:22Z", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "key_id": "1e779c8a-6786-4c89-b7c3-a6666f5fd6b5", + "last_seen_at": "2019-08-24T14:15:22Z", + "name": "string", + "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", + "provisioners": ["string"], + "tags": { + "property1": "string", + "property2": "string" + }, + "version": "string" + } + ], + "key": { + "created_at": "2019-08-24T14:15:22Z", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "string", + "organization": "452c1a86-a0af-475b-b03f-724878b0f387", + "tags": { + "property1": "string", + "property2": "string" + } + } + } +] +``` + +### Responses + +| Status | Meaning | Description | Schema | +| ------ | ------------------------------------------------------- | ----------- | ----------------------------------------------------------------------------------- | +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | array of [codersdk.ProvisionerKeyDaemons](schemas.md#codersdkprovisionerkeydaemons) | + +