feat: add keys to organization provision daemons (#14627)

This commit is contained in:
Garrett Delfosse
2024-09-16 20:02:08 +00:00
committed by GitHub
parent 4afce19fb7
commit 335eb05223
32 changed files with 728 additions and 72 deletions
+55
View File
@@ -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": [
+51
View File
@@ -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"],
+6
View File
@@ -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)
+25
View File
@@ -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 {
+4
View File
@@ -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 {
+13
View File
@@ -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})
+56
View File
@@ -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
+7
View File
@@ -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)
+15
View File
@@ -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()
+5
View File
@@ -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)
+5 -1
View File
@@ -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;
@@ -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;
@@ -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';
@@ -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;
+1
View File
@@ -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 {
+1
View File
@@ -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
+62 -6
View File
@@ -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
@@ -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
@@ -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
*
+3 -17
View File
@@ -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),
}
@@ -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)
+44
View File
@@ -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,
+1
View File
@@ -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",
+94
View File
@@ -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) |
<h3 id="list-provisioner-key-daemons-responseschema">Response Schema</h3>
Status Code **200**
| Name | Type | Required | Restrictions | Description |
| -------------------- | ------------------------------------------------------------ | -------- | ------------ | ----------- |
| `[array item]` | array | false | | |
| `» daemons` | array | false | | |
| `»» 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 | | |
| `»» provisioners` | array | false | | |
| `»» tags` | object | false | | |
| `»»» [any property]` | string | false | | |
| `»» version` | string | false | | |
| `» key` | [codersdk.ProvisionerKey](schemas.md#codersdkprovisionerkey) | false | | |
| `»» created_at` | string(date-time) | false | | |
| `»» id` | string(uuid) | false | | |
| `»» name` | string | false | | |
| `»» organization` | string(uuid) | false | | |
| `»» tags` | object | false | | |
| `»»» [any property]` | string | false | | |
To perform this operation, you must be authenticated. [Learn more](authentication.md).
## Delete provisioner key
### Code samples
+46
View File
@@ -3988,6 +3988,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
"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",
@@ -4007,6 +4008,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
| `api_version` | string | false | | |
| `created_at` | string | false | | |
| `id` | string | false | | |
| `key_id` | string | false | | |
| `last_seen_at` | string | false | | |
| `name` | string | false | | |
| `organization_id` | string | false | | |
@@ -4149,6 +4151,47 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
| `tags` | object | false | | |
| » `[any property]` | string | false | | |
## codersdk.ProvisionerKeyDaemons
```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"
}
}
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
| --------- | ----------------------------------------------------------------- | -------- | ------------ | ----------- |
| `daemons` | array of [codersdk.ProvisionerDaemon](#codersdkprovisionerdaemon) | false | | |
| `key` | [codersdk.ProvisionerKey](#codersdkprovisionerkey) | false | | |
## codersdk.ProvisionerLogLevel
```json
@@ -8585,6 +8628,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
"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",
@@ -8704,6 +8748,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
"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",
@@ -8758,6 +8803,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
"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",
+1
View File
@@ -331,6 +331,7 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
)
r.Get("/", api.provisionerKeys)
r.Post("/", api.postProvisionerKey)
r.Get("/daemons", api.provisionerKeyDaemons)
r.Route("/{provisionerkey}", func(r chi.Router) {
r.Use(
httpmw.ExtractProvisionerKeyParam(options.Database),
+47 -25
View File
@@ -82,56 +82,77 @@ type provisionerDaemonAuth struct {
// authorize returns mutated tags if the given HTTP request is authorized to access the provisioner daemon
// protobuf API, and returns nil, err otherwise.
func (p *provisionerDaemonAuth) authorize(r *http.Request, orgID uuid.UUID, tags map[string]string) (map[string]string, error) {
func (p *provisionerDaemonAuth) authorize(r *http.Request, orgID uuid.UUID, tags map[string]string) (uuid.UUID, map[string]string, error) {
ctx := r.Context()
apiKey, apiKeyOK := httpmw.APIKeyOptional(r)
pk, pkOK := httpmw.ProvisionerKeyAuthOptional(r)
provAuth := httpmw.ProvisionerDaemonAuthenticated(r)
if !provAuth && !apiKeyOK {
return nil, xerrors.New("no API key or provisioner key provided")
return uuid.Nil, nil, xerrors.New("no API key or provisioner key provided")
}
if apiKeyOK && pkOK {
return nil, xerrors.New("Both API key and provisioner key authentication provided. Only one is allowed.")
return uuid.Nil, nil, xerrors.New("Both API key and provisioner key authentication provided. Only one is allowed.")
}
// Provisioner Key Auth
if pkOK {
if pk.OrganizationID != orgID {
return nil, xerrors.New("provisioner key unauthorized")
return uuid.Nil, nil, xerrors.New("provisioner key unauthorized")
}
if tags != nil && !maps.Equal(tags, map[string]string{}) {
return nil, xerrors.New("tags are not allowed when using a provisioner key")
return uuid.Nil, nil, xerrors.New("tags are not allowed when using a provisioner key")
}
// If using provisioner key / PSK auth, the daemon is, by definition, scoped to the organization.
// Use the provisioner key tags here.
tags = provisionersdk.MutateTags(uuid.Nil, pk.Tags)
return tags, nil
return pk.ID, tags, nil
}
// User Auth
tags = provisionersdk.MutateTags(apiKey.UserID, tags)
if tags[provisionersdk.TagScope] == provisionersdk.ScopeUser {
// Any authenticated user can create provisioner daemons scoped
// for jobs that they own,
return tags, nil
}
ua := httpmw.UserAuthorization(r)
err := p.authorizer.Authorize(ctx, ua, policy.ActionCreate, rbac.ResourceProvisionerDaemon.InOrg(orgID))
if err != nil {
if !provAuth {
return nil, xerrors.New("user unauthorized")
if apiKeyOK {
userKey, err := uuid.Parse(codersdk.ProvisionerKeyIDUserAuth)
if err != nil {
return uuid.Nil, nil, xerrors.Errorf("parse user provisioner key id: %w", err)
}
// Allow fallback to PSK auth if the user is not allowed to create provisioner daemons.
// This is to preserve backwards compatibility with existing user provisioner daemons.
// If using PSK auth, the daemon is, by definition, scoped to the organization.
tags = provisionersdk.MutateTags(uuid.Nil, tags)
return tags, nil
tags = provisionersdk.MutateTags(apiKey.UserID, tags)
if tags[provisionersdk.TagScope] == provisionersdk.ScopeUser {
// Any authenticated user can create provisioner daemons scoped
// for jobs that they own,
return userKey, tags, nil
}
ua := httpmw.UserAuthorization(r)
err = p.authorizer.Authorize(ctx, ua, policy.ActionCreate, rbac.ResourceProvisionerDaemon.InOrg(orgID))
if err != nil {
if !provAuth {
return uuid.Nil, nil, xerrors.New("user unauthorized")
}
pskKey, err := uuid.Parse(codersdk.ProvisionerKeyIDPSK)
if err != nil {
return uuid.Nil, nil, xerrors.Errorf("parse psk provisioner key id: %w", err)
}
// Allow fallback to PSK auth if the user is not allowed to create provisioner daemons.
// This is to preserve backwards compatibility with existing user provisioner daemons.
// If using PSK auth, the daemon is, by definition, scoped to the organization.
tags = provisionersdk.MutateTags(uuid.Nil, tags)
return pskKey, tags, nil
}
return userKey, tags, nil
}
// User is allowed to create provisioner daemons
return tags, nil
// PSK Auth
pskKey, err := uuid.Parse(codersdk.ProvisionerKeyIDPSK)
if err != nil {
return uuid.Nil, nil, xerrors.Errorf("parse psk provisioner key id: %w", err)
}
tags = provisionersdk.MutateTags(uuid.Nil, tags)
return pskKey, tags, nil
}
// Serves the provisioner daemon protobuf API over a WebSocket.
@@ -194,7 +215,7 @@ func (api *API) provisionerDaemonServe(rw http.ResponseWriter, r *http.Request)
api.Logger.Warn(ctx, "unnamed provisioner daemon")
}
tags, err := api.provisionerDaemonAuth.authorize(r, organization.ID, tags)
keyID, tags, err := api.provisionerDaemonAuth.authorize(r, organization.ID, tags)
if err != nil {
api.Logger.Warn(ctx, "unauthorized provisioner daemon serve request", slog.F("tags", tags), slog.Error(err))
httpapi.Write(ctx, rw, http.StatusForbidden,
@@ -267,6 +288,7 @@ func (api *API) provisionerDaemonServe(rw http.ResponseWriter, r *http.Request)
Version: versionHdrVal,
APIVersion: apiVersion,
OrganizationID: organization.ID,
KeyID: keyID,
})
if err != nil {
if !xerrors.Is(err, context.Canceled) {
+32 -6
View File
@@ -743,6 +743,7 @@ func TestGetProvisionerDaemons(t *testing.T) {
Options: &coderdtest.Options{
DeploymentValues: dv,
},
ProvisionerDaemonPSK: "provisionersftw",
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureExternalProvisionerDaemons: 1,
@@ -752,6 +753,16 @@ func TestGetProvisionerDaemons(t *testing.T) {
})
org := coderdenttest.CreateOrganization(t, client, coderdenttest.CreateOrganizationOptions{})
orgAdmin, _ := coderdtest.CreateAnotherUser(t, client, org.ID, rbac.ScopedRoleOrgAdmin(org.ID))
res, err := orgAdmin.CreateProvisionerKey(context.Background(), org.ID, codersdk.CreateProvisionerKeyRequest{
Name: "my-key",
})
require.NoError(t, err)
keys, err := orgAdmin.ListProvisionerKeys(context.Background(), org.ID)
require.NoError(t, err)
require.Len(t, keys, 1)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
daemonName := testutil.MustRandString(t, 63)
@@ -762,17 +773,32 @@ func TestGetProvisionerDaemons(t *testing.T) {
Provisioners: []codersdk.ProvisionerType{
codersdk.ProvisionerTypeEcho,
},
Tags: map[string]string{},
Tags: map[string]string{},
ProvisionerKey: res.Key,
})
require.NoError(t, err)
srv.DRPCConn().Close()
daemons, err := orgAdmin.OrganizationProvisionerDaemons(ctx, org.ID)
require.NoError(t, err)
if assert.Len(t, daemons, 1) {
assert.Equal(t, daemonName, daemons[0].Name)
assert.Equal(t, buildinfo.Version(), daemons[0].Version)
assert.Equal(t, proto.CurrentVersion.String(), daemons[0].APIVersion)
}
require.Len(t, daemons, 1)
assert.Equal(t, daemonName, daemons[0].Name)
assert.Equal(t, buildinfo.Version(), daemons[0].Version)
assert.Equal(t, proto.CurrentVersion.String(), daemons[0].APIVersion)
assert.Equal(t, keys[0].ID, daemons[0].KeyID)
pkDaemons, err := orgAdmin.ListProvisionerKeyDaemons(ctx, org.ID)
require.NoError(t, err)
require.Len(t, pkDaemons, 1)
require.Len(t, pkDaemons[0].Daemons, 1)
assert.Equal(t, keys[0].ID, pkDaemons[0].Key.ID)
assert.Equal(t, keys[0].Name, pkDaemons[0].Key.Name)
assert.Equal(t, daemonName, pkDaemons[0].Daemons[0].Name)
assert.Equal(t, buildinfo.Version(), pkDaemons[0].Daemons[0].Version)
assert.Equal(t, proto.CurrentVersion.String(), pkDaemons[0].Daemons[0].APIVersion)
assert.Equal(t, keys[0].ID, pkDaemons[0].Daemons[0].KeyID)
})
}
+76 -14
View File
@@ -3,10 +3,15 @@ package coderd
import (
"fmt"
"net/http"
"slices"
"strings"
"time"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/db2sdk"
"github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/coderd/httpmw"
"github.com/coder/coder/v2/coderd/provisionerdserver"
"github.com/coder/coder/v2/coderd/provisionerkey"
"github.com/coder/coder/v2/codersdk"
)
@@ -54,6 +59,21 @@ func (api *API) postProvisionerKey(rw http.ResponseWriter, r *http.Request) {
return
}
if slices.ContainsFunc(codersdk.ReservedProvisionerKeyNames(), func(s string) bool {
return strings.EqualFold(req.Name, s)
}) {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: fmt.Sprintf("Name cannot be reserved name '%s'", req.Name),
Validations: []codersdk.ValidationError{
{
Field: "name",
Detail: fmt.Sprintf("Name cannot be reserved name '%s'", req.Name),
},
},
})
return
}
params, token, err := provisionerkey.New(organization.ID, req.Name, req.Tags)
if err != nil {
httpapi.InternalServerError(rw, err)
@@ -89,7 +109,7 @@ func (api *API) provisionerKeys(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
organization := httpmw.OrganizationParam(r)
pks, err := api.Database.ListProvisionerKeysByOrganization(ctx, organization.ID)
pks, err := api.Database.ListProvisionerKeysByOrganizationExcludeReserved(ctx, organization.ID)
if err != nil {
httpapi.InternalServerError(rw, err)
return
@@ -98,6 +118,54 @@ func (api *API) provisionerKeys(rw http.ResponseWriter, r *http.Request) {
httpapi.Write(ctx, rw, http.StatusOK, convertProvisionerKeys(pks))
}
// @Summary List provisioner key daemons
// @ID list-provisioner-key-daemons
// @Security CoderSessionToken
// @Produce json
// @Tags Enterprise
// @Param organization path string true "Organization ID"
// @Success 200 {object} []codersdk.ProvisionerKeyDaemons
// @Router /organizations/{organization}/provisionerkeys/daemons [get]
func (api *API) provisionerKeyDaemons(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
organization := httpmw.OrganizationParam(r)
pks, err := api.Database.ListProvisionerKeysByOrganization(ctx, organization.ID)
if err != nil {
httpapi.InternalServerError(rw, err)
return
}
sdkKeys := convertProvisionerKeys(pks)
daemons, err := api.Database.GetProvisionerDaemonsByOrganization(ctx, organization.ID)
if err != nil {
httpapi.InternalServerError(rw, err)
return
}
// provisionerdserver.DefaultHeartbeatInterval*3 matches the healthcheck report staleInterval.
recentDaemons := db2sdk.RecentProvisionerDaemons(time.Now(), provisionerdserver.DefaultHeartbeatInterval*3, daemons)
pkDaemons := []codersdk.ProvisionerKeyDaemons{}
for _, key := range sdkKeys {
// currently we exclude user-auth from this list
if key.ID.String() == codersdk.ProvisionerKeyIDUserAuth {
continue
}
daemons := []codersdk.ProvisionerDaemon{}
for _, daemon := range recentDaemons {
if daemon.KeyID == key.ID {
daemons = append(daemons, daemon)
}
}
pkDaemons = append(pkDaemons, codersdk.ProvisionerKeyDaemons{
Key: key,
Daemons: daemons,
})
}
httpapi.Write(ctx, rw, http.StatusOK, pkDaemons)
}
// @Summary Delete provisioner key
// @ID delete-provisioner-key
// @Security CoderSessionToken
@@ -108,24 +176,18 @@ func (api *API) provisionerKeys(rw http.ResponseWriter, r *http.Request) {
// @Router /organizations/{organization}/provisionerkeys/{provisionerkey} [delete]
func (api *API) deleteProvisionerKey(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
organization := httpmw.OrganizationParam(r)
provisionerKey := httpmw.ProvisionerKeyParam(r)
pk, err := api.Database.GetProvisionerKeyByName(ctx, database.GetProvisionerKeyByNameParams{
OrganizationID: organization.ID,
Name: provisionerKey.Name,
})
if err != nil {
if httpapi.Is404Error(err) {
httpapi.ResourceNotFound(rw)
return
}
httpapi.InternalServerError(rw, err)
if provisionerKey.ID.String() == codersdk.ProvisionerKeyIDBuiltIn ||
provisionerKey.ID.String() == codersdk.ProvisionerKeyIDUserAuth ||
provisionerKey.ID.String() == codersdk.ProvisionerKeyIDPSK {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: fmt.Sprintf("Cannot delete reserved '%s' provisioner key", provisionerKey.Name),
})
return
}
err = api.Database.DeleteProvisionerKey(ctx, pk.ID)
err := api.Database.DeleteProvisionerKey(ctx, provisionerKey.ID)
if err != nil {
httpapi.InternalServerError(rw, err)
return
+22
View File
@@ -64,6 +64,20 @@ func TestProvisionerKeys(t *testing.T) {
err = outsideOrgAdmin.DeleteProvisionerKey(ctx, owner.OrganizationID, "key")
require.ErrorContains(t, err, "Resource not found")
// org admin cannot create reserved provisioner keys
_, err = orgAdmin.CreateProvisionerKey(ctx, owner.OrganizationID, codersdk.CreateProvisionerKeyRequest{
Name: codersdk.ProvisionerKeyNameBuiltIn,
})
require.ErrorContains(t, err, "reserved")
_, err = orgAdmin.CreateProvisionerKey(ctx, owner.OrganizationID, codersdk.CreateProvisionerKeyRequest{
Name: codersdk.ProvisionerKeyNameUserAuth,
})
require.ErrorContains(t, err, "reserved")
_, err = orgAdmin.CreateProvisionerKey(ctx, owner.OrganizationID, codersdk.CreateProvisionerKeyRequest{
Name: codersdk.ProvisionerKeyNamePSK,
})
require.ErrorContains(t, err, "reserved")
// org admin can list provisioner keys and get an empty list
keys, err := orgAdmin.ListProvisionerKeys(ctx, owner.OrganizationID)
require.NoError(t, err, "org admin list provisioner keys")
@@ -111,4 +125,12 @@ func TestProvisionerKeys(t *testing.T) {
// org admin cannot delete a provisioner key that doesn't exist
err = orgAdmin.DeleteProvisionerKey(ctx, owner.OrganizationID, "key")
require.ErrorContains(t, err, "Resource not found")
// org admin cannot delete reserved provisioner keys
err = orgAdmin.DeleteProvisionerKey(ctx, owner.OrganizationID, codersdk.ProvisionerKeyNameBuiltIn)
require.ErrorContains(t, err, "reserved")
err = orgAdmin.DeleteProvisionerKey(ctx, owner.OrganizationID, codersdk.ProvisionerKeyNameUserAuth)
require.ErrorContains(t, err, "reserved")
err = orgAdmin.DeleteProvisionerKey(ctx, owner.OrganizationID, codersdk.ProvisionerKeyNamePSK)
require.ErrorContains(t, err, "reserved")
}
+7
View File
@@ -1016,6 +1016,7 @@ export interface ProvisionerConfig {
export interface ProvisionerDaemon {
readonly id: string;
readonly organization_id: string;
readonly key_id: string;
readonly created_at: string;
readonly last_seen_at?: string;
readonly name: string;
@@ -1061,6 +1062,12 @@ export interface ProvisionerKey {
readonly tags: Record<string, string>;
}
// From codersdk/provisionerdaemons.go
export interface ProvisionerKeyDaemons {
readonly key: ProvisionerKey;
readonly daemons: Readonly<Array<ProvisionerDaemon>>;
}
// From codersdk/workspaces.go
export interface ProvisionerTiming {
readonly job_id: string;
+13
View File
@@ -567,6 +567,14 @@ export const MockOrganizationMember2: TypesGen.OrganizationMemberWithUserData =
roles: [],
};
export const MockProvisionerKey: TypesGen.ProvisionerKey = {
id: "test-provisioner-key",
organization: MockOrganization.id,
created_at: "2022-05-17T17:39:01.382927298Z",
name: "test-name",
tags: { scope: "organization" },
};
export const MockProvisioner: TypesGen.ProvisionerDaemon = {
created_at: "2022-05-17T17:39:01.382927298Z",
id: "test-provisioner",
@@ -576,6 +584,7 @@ export const MockProvisioner: TypesGen.ProvisionerDaemon = {
tags: { scope: "organization" },
version: MockBuildInfo.version,
api_version: MockBuildInfo.provisioner_api_version,
key_id: MockProvisionerKey.id,
};
export const MockUserProvisioner: TypesGen.ProvisionerDaemon = {
@@ -3528,6 +3537,7 @@ export const MockHealth: TypesGen.HealthcheckReport = {
provisioner_daemon: {
id: "e455b582-ac04-4323-9ad6-ab71301fa006",
organization_id: MockOrganization.id,
key_id: MockProvisionerKey.id,
created_at: "2024-01-04T15:53:03.21563Z",
last_seen_at: "2024-01-04T16:05:03.967551Z",
name: "ok",
@@ -3549,6 +3559,7 @@ export const MockHealth: TypesGen.HealthcheckReport = {
provisioner_daemon: {
id: "00000000-0000-0000-000000000000",
organization_id: MockOrganization.id,
key_id: MockProvisionerKey.id,
created_at: "2024-01-04T15:53:03.21563Z",
last_seen_at: "2024-01-04T16:05:03.967551Z",
name: "user-scoped",
@@ -3570,6 +3581,7 @@ export const MockHealth: TypesGen.HealthcheckReport = {
provisioner_daemon: {
id: "e455b582-ac04-4323-9ad6-ab71301fa006",
organization_id: MockOrganization.id,
key_id: MockProvisionerKey.id,
created_at: "2024-01-04T15:53:03.21563Z",
last_seen_at: "2024-01-04T16:05:03.967551Z",
name: "unhappy",
@@ -3726,6 +3738,7 @@ export const DeploymentHealthUnhealthy: TypesGen.HealthcheckReport = {
provisioner_daemon: {
id: "e455b582-ac04-4323-9ad6-ab71301fa006",
organization_id: MockOrganization.id,
key_id: MockProvisionerKey.id,
created_at: "2024-01-04T15:53:03.21563Z",
last_seen_at: "2024-01-04T16:05:03.967551Z",
name: "vvuurrkk-2",