feat: Add backend API support for resource metadata (#3242)

* Initial support for metadata in provisioner API and Terraform provisioner

* add support for nullable metadata fields

* handle metadata fields in provisionerd and API
This commit is contained in:
David Wahler
2022-08-01 16:53:05 -05:00
committed by GitHub
parent 877519232c
commit 8a2811210a
24 changed files with 1231 additions and 214 deletions

View File

@ -26,21 +26,22 @@ func New() database.Store {
organizations: make([]database.Organization, 0),
users: make([]database.User, 0),
auditLogs: make([]database.AuditLog, 0),
files: make([]database.File, 0),
gitSSHKey: make([]database.GitSSHKey, 0),
parameterSchemas: make([]database.ParameterSchema, 0),
parameterValues: make([]database.ParameterValue, 0),
provisionerDaemons: make([]database.ProvisionerDaemon, 0),
provisionerJobAgents: make([]database.WorkspaceAgent, 0),
provisionerJobLogs: make([]database.ProvisionerJobLog, 0),
provisionerJobResources: make([]database.WorkspaceResource, 0),
provisionerJobs: make([]database.ProvisionerJob, 0),
templateVersions: make([]database.TemplateVersion, 0),
templates: make([]database.Template, 0),
workspaceBuilds: make([]database.WorkspaceBuild, 0),
workspaceApps: make([]database.WorkspaceApp, 0),
workspaces: make([]database.Workspace, 0),
auditLogs: make([]database.AuditLog, 0),
files: make([]database.File, 0),
gitSSHKey: make([]database.GitSSHKey, 0),
parameterSchemas: make([]database.ParameterSchema, 0),
parameterValues: make([]database.ParameterValue, 0),
provisionerDaemons: make([]database.ProvisionerDaemon, 0),
provisionerJobAgents: make([]database.WorkspaceAgent, 0),
provisionerJobLogs: make([]database.ProvisionerJobLog, 0),
provisionerJobResources: make([]database.WorkspaceResource, 0),
provisionerJobResourceMetadata: make([]database.WorkspaceResourceMetadatum, 0),
provisionerJobs: make([]database.ProvisionerJob, 0),
templateVersions: make([]database.TemplateVersion, 0),
templates: make([]database.Template, 0),
workspaceBuilds: make([]database.WorkspaceBuild, 0),
workspaceApps: make([]database.WorkspaceApp, 0),
workspaces: make([]database.Workspace, 0),
},
}
}
@ -74,21 +75,22 @@ type data struct {
users []database.User
// New tables
auditLogs []database.AuditLog
files []database.File
gitSSHKey []database.GitSSHKey
parameterSchemas []database.ParameterSchema
parameterValues []database.ParameterValue
provisionerDaemons []database.ProvisionerDaemon
provisionerJobAgents []database.WorkspaceAgent
provisionerJobLogs []database.ProvisionerJobLog
provisionerJobResources []database.WorkspaceResource
provisionerJobs []database.ProvisionerJob
templateVersions []database.TemplateVersion
templates []database.Template
workspaceBuilds []database.WorkspaceBuild
workspaceApps []database.WorkspaceApp
workspaces []database.Workspace
auditLogs []database.AuditLog
files []database.File
gitSSHKey []database.GitSSHKey
parameterSchemas []database.ParameterSchema
parameterValues []database.ParameterValue
provisionerDaemons []database.ProvisionerDaemon
provisionerJobAgents []database.WorkspaceAgent
provisionerJobLogs []database.ProvisionerJobLog
provisionerJobResources []database.WorkspaceResource
provisionerJobResourceMetadata []database.WorkspaceResourceMetadatum
provisionerJobs []database.ProvisionerJob
templateVersions []database.TemplateVersion
templates []database.Template
workspaceBuilds []database.WorkspaceBuild
workspaceApps []database.WorkspaceApp
workspaces []database.Workspace
deploymentID string
}
@ -1331,6 +1333,34 @@ func (q *fakeQuerier) GetWorkspaceResourcesCreatedAfter(_ context.Context, after
return resources, nil
}
func (q *fakeQuerier) GetWorkspaceResourceMetadataByResourceID(_ context.Context, id uuid.UUID) ([]database.WorkspaceResourceMetadatum, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
metadata := make([]database.WorkspaceResourceMetadatum, 0)
for _, metadatum := range q.provisionerJobResourceMetadata {
if metadatum.WorkspaceResourceID.String() == id.String() {
metadata = append(metadata, metadatum)
}
}
return metadata, nil
}
func (q *fakeQuerier) GetWorkspaceResourceMetadataByResourceIDs(_ context.Context, ids []uuid.UUID) ([]database.WorkspaceResourceMetadatum, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
metadata := make([]database.WorkspaceResourceMetadatum, 0)
for _, metadatum := range q.provisionerJobResourceMetadata {
for _, id := range ids {
if metadatum.WorkspaceResourceID.String() == id.String() {
metadata = append(metadata, metadatum)
}
}
}
return metadata, nil
}
func (q *fakeQuerier) GetProvisionerJobsByIDs(_ context.Context, ids []uuid.UUID) ([]database.ProvisionerJob, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
@ -1659,6 +1689,21 @@ func (q *fakeQuerier) InsertWorkspaceResource(_ context.Context, arg database.In
return resource, nil
}
func (q *fakeQuerier) InsertWorkspaceResourceMetadata(_ context.Context, arg database.InsertWorkspaceResourceMetadataParams) (database.WorkspaceResourceMetadatum, error) {
q.mutex.Lock()
defer q.mutex.Unlock()
//nolint:gosimple
metadatum := database.WorkspaceResourceMetadatum{
WorkspaceResourceID: arg.WorkspaceResourceID,
Key: arg.Key,
Value: arg.Value,
Sensitive: arg.Sensitive,
}
q.provisionerJobResourceMetadata = append(q.provisionerJobResourceMetadata, metadatum)
return metadatum, nil
}
func (q *fakeQuerier) InsertUser(_ context.Context, arg database.InsertUserParams) (database.User, error) {
q.mutex.Lock()
defer q.mutex.Unlock()

View File

@ -328,6 +328,13 @@ CREATE TABLE workspace_builds (
reason build_reason DEFAULT 'initiator'::public.build_reason NOT NULL
);
CREATE TABLE workspace_resource_metadata (
workspace_resource_id uuid NOT NULL,
key character varying(1024) NOT NULL,
value character varying(65536),
sensitive boolean NOT NULL
);
CREATE TABLE workspace_resources (
id uuid NOT NULL,
created_at timestamp with time zone NOT NULL,
@ -433,6 +440,9 @@ ALTER TABLE ONLY workspace_builds
ALTER TABLE ONLY workspace_builds
ADD CONSTRAINT workspace_builds_workspace_id_name_key UNIQUE (workspace_id, name);
ALTER TABLE ONLY workspace_resource_metadata
ADD CONSTRAINT workspace_resource_metadata_pkey PRIMARY KEY (workspace_resource_id, key);
ALTER TABLE ONLY workspace_resources
ADD CONSTRAINT workspace_resources_pkey PRIMARY KEY (id);
@ -518,6 +528,9 @@ ALTER TABLE ONLY workspace_builds
ALTER TABLE ONLY workspace_builds
ADD CONSTRAINT workspace_builds_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE;
ALTER TABLE ONLY workspace_resource_metadata
ADD CONSTRAINT workspace_resource_metadata_workspace_resource_id_fkey FOREIGN KEY (workspace_resource_id) REFERENCES workspace_resources(id) ON DELETE CASCADE;
ALTER TABLE ONLY workspace_resources
ADD CONSTRAINT workspace_resources_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_jobs(id) ON DELETE CASCADE;

View File

@ -0,0 +1 @@
DROP TABLE IF EXISTS workspace_resource_metadata;

View File

@ -0,0 +1,8 @@
CREATE TABLE IF NOT EXISTS workspace_resource_metadata (
workspace_resource_id uuid NOT NULL,
key varchar(1024) NOT NULL,
value varchar(65536),
sensitive boolean NOT NULL,
PRIMARY KEY (workspace_resource_id, key),
FOREIGN KEY (workspace_resource_id) REFERENCES workspace_resources (id) ON DELETE CASCADE
);

View File

@ -564,3 +564,10 @@ type WorkspaceResource struct {
Type string `db:"type" json:"type"`
Name string `db:"name" json:"name"`
}
type WorkspaceResourceMetadatum struct {
WorkspaceResourceID uuid.UUID `db:"workspace_resource_id" json:"workspace_resource_id"`
Key string `db:"key" json:"key"`
Value sql.NullString `db:"value" json:"value"`
Sensitive bool `db:"sensitive" json:"sensitive"`
}

View File

@ -84,6 +84,8 @@ type querier interface {
GetWorkspaceByOwnerIDAndName(ctx context.Context, arg GetWorkspaceByOwnerIDAndNameParams) (Workspace, error)
GetWorkspaceOwnerCountsByTemplateIDs(ctx context.Context, ids []uuid.UUID) ([]GetWorkspaceOwnerCountsByTemplateIDsRow, error)
GetWorkspaceResourceByID(ctx context.Context, id uuid.UUID) (WorkspaceResource, error)
GetWorkspaceResourceMetadataByResourceID(ctx context.Context, workspaceResourceID uuid.UUID) ([]WorkspaceResourceMetadatum, error)
GetWorkspaceResourceMetadataByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceResourceMetadatum, error)
GetWorkspaceResourcesByJobID(ctx context.Context, jobID uuid.UUID) ([]WorkspaceResource, error)
GetWorkspaceResourcesCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceResource, error)
GetWorkspaces(ctx context.Context, arg GetWorkspacesParams) ([]Workspace, error)
@ -108,6 +110,7 @@ type querier interface {
InsertWorkspaceApp(ctx context.Context, arg InsertWorkspaceAppParams) (WorkspaceApp, error)
InsertWorkspaceBuild(ctx context.Context, arg InsertWorkspaceBuildParams) (WorkspaceBuild, error)
InsertWorkspaceResource(ctx context.Context, arg InsertWorkspaceResourceParams) (WorkspaceResource, error)
InsertWorkspaceResourceMetadata(ctx context.Context, arg InsertWorkspaceResourceMetadataParams) (WorkspaceResourceMetadatum, error)
ParameterValue(ctx context.Context, id uuid.UUID) (ParameterValue, error)
ParameterValues(ctx context.Context, arg ParameterValuesParams) ([]ParameterValue, error)
UpdateAPIKeyByID(ctx context.Context, arg UpdateAPIKeyByIDParams) error

View File

@ -3895,6 +3895,80 @@ func (q *sqlQuerier) GetWorkspaceResourceByID(ctx context.Context, id uuid.UUID)
return i, err
}
const getWorkspaceResourceMetadataByResourceID = `-- name: GetWorkspaceResourceMetadataByResourceID :many
SELECT
workspace_resource_id, key, value, sensitive
FROM
workspace_resource_metadata
WHERE
workspace_resource_id = $1
`
func (q *sqlQuerier) GetWorkspaceResourceMetadataByResourceID(ctx context.Context, workspaceResourceID uuid.UUID) ([]WorkspaceResourceMetadatum, error) {
rows, err := q.db.QueryContext(ctx, getWorkspaceResourceMetadataByResourceID, workspaceResourceID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceResourceMetadatum
for rows.Next() {
var i WorkspaceResourceMetadatum
if err := rows.Scan(
&i.WorkspaceResourceID,
&i.Key,
&i.Value,
&i.Sensitive,
); 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 getWorkspaceResourceMetadataByResourceIDs = `-- name: GetWorkspaceResourceMetadataByResourceIDs :many
SELECT
workspace_resource_id, key, value, sensitive
FROM
workspace_resource_metadata
WHERE
workspace_resource_id = ANY($1 :: uuid [ ])
`
func (q *sqlQuerier) GetWorkspaceResourceMetadataByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceResourceMetadatum, error) {
rows, err := q.db.QueryContext(ctx, getWorkspaceResourceMetadataByResourceIDs, pq.Array(ids))
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceResourceMetadatum
for rows.Next() {
var i WorkspaceResourceMetadatum
if err := rows.Scan(
&i.WorkspaceResourceID,
&i.Key,
&i.Value,
&i.Sensitive,
); 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 getWorkspaceResourcesByJobID = `-- name: GetWorkspaceResourcesByJobID :many
SELECT
id, created_at, job_id, transition, type, name
@ -4005,6 +4079,37 @@ func (q *sqlQuerier) InsertWorkspaceResource(ctx context.Context, arg InsertWork
return i, err
}
const insertWorkspaceResourceMetadata = `-- name: InsertWorkspaceResourceMetadata :one
INSERT INTO
workspace_resource_metadata (workspace_resource_id, key, value, sensitive)
VALUES
($1, $2, $3, $4) RETURNING workspace_resource_id, key, value, sensitive
`
type InsertWorkspaceResourceMetadataParams struct {
WorkspaceResourceID uuid.UUID `db:"workspace_resource_id" json:"workspace_resource_id"`
Key string `db:"key" json:"key"`
Value sql.NullString `db:"value" json:"value"`
Sensitive bool `db:"sensitive" json:"sensitive"`
}
func (q *sqlQuerier) InsertWorkspaceResourceMetadata(ctx context.Context, arg InsertWorkspaceResourceMetadataParams) (WorkspaceResourceMetadatum, error) {
row := q.db.QueryRowContext(ctx, insertWorkspaceResourceMetadata,
arg.WorkspaceResourceID,
arg.Key,
arg.Value,
arg.Sensitive,
)
var i WorkspaceResourceMetadatum
err := row.Scan(
&i.WorkspaceResourceID,
&i.Key,
&i.Value,
&i.Sensitive,
)
return i, err
}
const getWorkspaceByID = `-- name: GetWorkspaceByID :one
SELECT
id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl

View File

@ -22,3 +22,25 @@ INSERT INTO
workspace_resources (id, created_at, job_id, transition, type, name)
VALUES
($1, $2, $3, $4, $5, $6) RETURNING *;
-- name: GetWorkspaceResourceMetadataByResourceID :many
SELECT
*
FROM
workspace_resource_metadata
WHERE
workspace_resource_id = $1;
-- name: GetWorkspaceResourceMetadataByResourceIDs :many
SELECT
*
FROM
workspace_resource_metadata
WHERE
workspace_resource_id = ANY(@ids :: uuid [ ]);
-- name: InsertWorkspaceResourceMetadata :one
INSERT INTO
workspace_resource_metadata (workspace_resource_id, key, value, sensitive)
VALUES
($1, $2, $3, $4) RETURNING *;

View File

@ -829,6 +829,25 @@ func insertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid.
snapshot.WorkspaceApps = append(snapshot.WorkspaceApps, telemetry.ConvertWorkspaceApp(dbApp))
}
}
for _, metadatum := range protoResource.Metadata {
var value sql.NullString
if !metadatum.IsNull {
value.String = metadatum.Value
value.Valid = true
}
_, err := db.InsertWorkspaceResourceMetadata(ctx, database.InsertWorkspaceResourceMetadataParams{
WorkspaceResourceID: resource.ID,
Key: metadatum.Key,
Value: value,
Sensitive: metadatum.Sensitive,
})
if err != nil {
return xerrors.Errorf("insert metadata: %w", err)
}
}
return nil
}

View File

@ -241,6 +241,14 @@ func (api *API) provisionerJobResources(rw http.ResponseWriter, r *http.Request,
})
return
}
resourceMetadata, err := api.Database.GetWorkspaceResourceMetadataByResourceIDs(r.Context(), resourceIDs)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching workspace metadata.",
Detail: err.Error(),
})
return
}
apiResources := make([]codersdk.WorkspaceResource, 0)
for _, resource := range resources {
@ -266,7 +274,13 @@ func (api *API) provisionerJobResources(rw http.ResponseWriter, r *http.Request,
}
agents = append(agents, apiAgent)
}
apiResources = append(apiResources, convertWorkspaceResource(resource, agents))
metadata := make([]database.WorkspaceResourceMetadatum, 0)
for _, field := range resourceMetadata {
if field.WorkspaceResourceID == resource.ID {
metadata = append(metadata, field)
}
}
apiResources = append(apiResources, convertWorkspaceResource(resource, agents, metadata))
}
httpapi.Write(rw, http.StatusOK, apiResources)

View File

@ -11,6 +11,7 @@ import (
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"github.com/moby/moby/pkg/namesgenerator"
"golang.org/x/exp/slices"
"golang.org/x/xerrors"
"github.com/coder/coder/coderd/database"
@ -681,7 +682,35 @@ func convertWorkspaceBuild(
}
}
func convertWorkspaceResource(resource database.WorkspaceResource, agents []codersdk.WorkspaceAgent) codersdk.WorkspaceResource {
func convertWorkspaceResource(resource database.WorkspaceResource, agents []codersdk.WorkspaceAgent, metadata []database.WorkspaceResourceMetadatum) codersdk.WorkspaceResource {
metadataMap := map[string]database.WorkspaceResourceMetadatum{}
// implicit metadata fields come first
metadataMap["type"] = database.WorkspaceResourceMetadatum{
Key: "type",
Value: sql.NullString{String: resource.Type, Valid: true},
Sensitive: false,
}
// explicit metadata fields come afterward, and can override implicit ones
for _, field := range metadata {
metadataMap[field.Key] = field
}
var convertedMetadata []codersdk.WorkspaceResourceMetadata
for _, field := range metadataMap {
if field.Value.Valid {
convertedField := codersdk.WorkspaceResourceMetadata{
Key: field.Key,
Value: field.Value.String,
Sensitive: field.Sensitive,
}
convertedMetadata = append(convertedMetadata, convertedField)
}
}
slices.SortFunc(convertedMetadata, func(a, b codersdk.WorkspaceResourceMetadata) bool {
return a.Key < b.Key
})
return codersdk.WorkspaceResource{
ID: resource.ID,
CreatedAt: resource.CreatedAt,
@ -690,5 +719,6 @@ func convertWorkspaceResource(resource database.WorkspaceResource, agents []code
Type: resource.Type,
Name: resource.Name,
Agents: agents,
Metadata: convertedMetadata,
}
}

View File

@ -80,5 +80,14 @@ func (api *API) workspaceResource(rw http.ResponseWriter, r *http.Request) {
apiAgents = append(apiAgents, convertedAgent)
}
httpapi.Write(rw, http.StatusOK, convertWorkspaceResource(workspaceResource, apiAgents))
metadata, err := api.Database.GetWorkspaceResourceMetadataByResourceID(r.Context(), workspaceResource.ID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching workspace resource metadata.",
Detail: err.Error(),
})
return
}
httpapi.Write(rw, http.StatusOK, convertWorkspaceResource(workspaceResource, apiAgents, metadata))
}

View File

@ -7,6 +7,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/provisioner/echo"
"github.com/coder/coder/provisionersdk/proto"
)
@ -90,4 +91,64 @@ func TestWorkspaceResource(t *testing.T) {
require.Equal(t, app.Icon, got.Icon)
require.Equal(t, app.Name, got.Name)
})
t.Run("Metadata", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{
IncludeProvisionerD: true,
})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
Provision: []*proto.Provision_Response{{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
Resources: []*proto.Resource{{
Name: "some",
Type: "example",
Agents: []*proto.Agent{{
Id: "something",
Auth: &proto.Agent_Token{},
}},
Metadata: []*proto.Resource_Metadata{{
Key: "foo",
Value: "bar",
}, {
Key: "null",
IsNull: true,
}, {
Key: "empty",
}, {
Key: "secret",
Value: "squirrel",
Sensitive: true,
}},
}},
},
},
}},
})
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
resources, err := client.WorkspaceResourcesByBuild(context.Background(), workspace.LatestBuild.ID)
require.NoError(t, err)
resource, err := client.WorkspaceResource(context.Background(), resources[0].ID)
require.NoError(t, err)
metadata := resource.Metadata
require.Equal(t, []codersdk.WorkspaceResourceMetadata{{
Key: "empty",
}, {
Key: "foo",
Value: "bar",
}, {
Key: "secret",
Value: "squirrel",
Sensitive: true,
}, {
Key: "type",
Value: "example",
}}, metadata)
})
}