From bdfa61aa0df44a9725cf13ee8c47b42619248f49 Mon Sep 17 00:00:00 2001 From: kylecarbs Date: Fri, 6 May 2022 03:12:32 +0000 Subject: [PATCH 01/26] feat: Add app support This adds apps as a property to a workspace agent. The resource is added to the Terraform provider here: https://github.com/coder/terraform-provider-coder/pull/17 Apps will be opened in the dashboard or via the CLI with `coder open `. If `command` is specified, a terminal will appear locally and in the web. If `target` is specified, the browser will open to an exposed instance of that target. --- .vscode/settings.json | 1 + coderd/database/databasefake/databasefake.go | 54 ++ coderd/database/dump.sql | 19 + .../migrations/000011_workspace_apps.down.sql | 1 + .../migrations/000011_workspace_apps.up.sql | 13 + coderd/database/models.go | 10 + coderd/database/querier.go | 3 + coderd/database/queries.sql.go | 118 +++++ coderd/database/queries/workspaceapps.sql | 19 + coderd/provisionerdaemons.go | 23 +- coderd/provisionerjobs.go | 23 +- coderd/rbac/README.md | 2 +- coderd/rbac/object.go | 2 +- coderd/workspaceagents.go | 32 +- coderd/workspaceresources.go | 24 +- coderd/workspaceresources_test.go | 39 ++ codersdk/workspaceapps.go | 11 + codersdk/workspaceresources.go | 1 + provisioner/terraform/provision.go | 38 +- provisioner/terraform/provision_test.go | 48 +- provisionersdk/proto/provisioner.pb.go | 497 +++++++++++------- provisionersdk/proto/provisioner.proto | 13 +- site/src/api/typesGenerated.ts | 14 +- 23 files changed, 791 insertions(+), 214 deletions(-) create mode 100644 coderd/database/migrations/000011_workspace_apps.down.sql create mode 100644 coderd/database/migrations/000011_workspace_apps.up.sql create mode 100644 coderd/database/queries/workspaceapps.sql create mode 100644 codersdk/workspaceapps.go diff --git a/.vscode/settings.json b/.vscode/settings.json index 6f7ea5c69fce3..ccc535fd1ae8a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,6 +7,7 @@ "coderdtest", "codersdk", "devel", + "apps", "drpc", "drpcconn", "drpcmux", diff --git a/coderd/database/databasefake/databasefake.go b/coderd/database/databasefake/databasefake.go index 5726c9f7befe5..1a2b1c7cdc346 100644 --- a/coderd/database/databasefake/databasefake.go +++ b/coderd/database/databasefake/databasefake.go @@ -34,6 +34,7 @@ func New() database.Store { 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), } } @@ -62,6 +63,7 @@ type fakeQuerier struct { templateVersions []database.TemplateVersion templates []database.Template workspaceBuilds []database.WorkspaceBuild + workspaceApps []database.WorkspaceApp workspaces []database.Workspace } @@ -328,6 +330,41 @@ func (q *fakeQuerier) GetWorkspaceByOwnerIDAndName(_ context.Context, arg databa return database.Workspace{}, sql.ErrNoRows } +func (q *fakeQuerier) GetWorkspaceAppsByAgentID(_ context.Context, id uuid.UUID) ([]database.WorkspaceApp, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + apps := make([]database.WorkspaceApp, 0) + for _, app := range q.workspaceApps { + if app.AgentID == id { + apps = append(apps, app) + } + } + if len(apps) == 0 { + return nil, sql.ErrNoRows + } + return apps, nil +} + +func (q *fakeQuerier) GetWorkspaceAppsByAgentIDs(_ context.Context, ids []uuid.UUID) ([]database.WorkspaceApp, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + apps := make([]database.WorkspaceApp, 0) + for _, app := range q.workspaceApps { + for _, id := range ids { + if app.AgentID.String() == id.String() { + apps = append(apps, app) + break + } + } + } + if len(apps) == 0 { + return nil, sql.ErrNoRows + } + return apps, nil +} + func (q *fakeQuerier) GetWorkspaceOwnerCountsByTemplateIDs(_ context.Context, templateIDs []uuid.UUID) ([]database.GetWorkspaceOwnerCountsByTemplateIDsRow, error) { q.mutex.RLock() defer q.mutex.RUnlock() @@ -1353,6 +1390,23 @@ func (q *fakeQuerier) InsertWorkspaceBuild(_ context.Context, arg database.Inser return workspaceBuild, nil } +func (q *fakeQuerier) InsertWorkspaceApp(_ context.Context, arg database.InsertWorkspaceAppParams) (database.WorkspaceApp, error) { + q.mutex.Lock() + defer q.mutex.Unlock() + + workspaceApp := database.WorkspaceApp{ + ID: arg.ID, + AgentID: arg.AgentID, + CreatedAt: arg.CreatedAt, + Name: arg.Name, + Icon: arg.Icon, + Command: arg.Command, + Target: arg.Target, + } + q.workspaceApps = append(q.workspaceApps, workspaceApp) + return workspaceApp, nil +} + func (q *fakeQuerier) UpdateAPIKeyByID(_ context.Context, arg database.UpdateAPIKeyByIDParams) error { q.mutex.Lock() defer q.mutex.Unlock() diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index dd18186ea761b..c59e70db04675 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -280,6 +280,16 @@ CREATE TABLE workspace_agents ( directory character varying(4096) DEFAULT ''::character varying NOT NULL ); +CREATE TABLE workspace_apps ( + id uuid NOT NULL, + created_at timestamp with time zone NOT NULL, + agent_id uuid NOT NULL, + name character varying(64) NOT NULL, + icon character varying(256) NOT NULL, + command character varying(65534), + target character varying(65534) +); + CREATE TABLE workspace_builds ( id uuid NOT NULL, created_at timestamp with time zone NOT NULL, @@ -382,6 +392,12 @@ ALTER TABLE ONLY users ALTER TABLE ONLY workspace_agents ADD CONSTRAINT workspace_agents_pkey PRIMARY KEY (id); +ALTER TABLE ONLY workspace_apps + ADD CONSTRAINT workspace_apps_agent_id_name_key UNIQUE (agent_id, name); + +ALTER TABLE ONLY workspace_apps + ADD CONSTRAINT workspace_apps_pkey PRIMARY KEY (id); + ALTER TABLE ONLY workspace_builds ADD CONSTRAINT workspace_builds_job_id_key UNIQUE (job_id); @@ -460,6 +476,9 @@ ALTER TABLE ONLY templates ALTER TABLE ONLY workspace_agents ADD CONSTRAINT workspace_agents_resource_id_fkey FOREIGN KEY (resource_id) REFERENCES workspace_resources(id) ON DELETE CASCADE; +ALTER TABLE ONLY workspace_apps + ADD CONSTRAINT workspace_apps_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; + ALTER TABLE ONLY workspace_builds ADD CONSTRAINT workspace_builds_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_jobs(id) ON DELETE CASCADE; diff --git a/coderd/database/migrations/000011_workspace_apps.down.sql b/coderd/database/migrations/000011_workspace_apps.down.sql new file mode 100644 index 0000000000000..c1c0dabd478da --- /dev/null +++ b/coderd/database/migrations/000011_workspace_apps.down.sql @@ -0,0 +1 @@ +DROP TABLE workspace_apps; diff --git a/coderd/database/migrations/000011_workspace_apps.up.sql b/coderd/database/migrations/000011_workspace_apps.up.sql new file mode 100644 index 0000000000000..bea1278a2c5fa --- /dev/null +++ b/coderd/database/migrations/000011_workspace_apps.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE workspace_apps ( + id uuid NOT NULL, + created_at timestamp with time zone NOT NULL, + agent_id uuid NOT NULL REFERENCES workspace_agents (id) ON DELETE CASCADE, + name varchar(64) NOT NULL, + icon varchar(256) NOT NULL, + -- A command to run when opened. + command varchar(65534), + -- A URL or port to target. + target varchar(65534), + PRIMARY KEY (id), + UNIQUE(agent_id, name) +); diff --git a/coderd/database/models.go b/coderd/database/models.go index 0fd6e4ca28270..26928c83937f7 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -493,6 +493,16 @@ type WorkspaceAgent struct { Directory string `db:"directory" json:"directory"` } +type WorkspaceApp struct { + ID uuid.UUID `db:"id" json:"id"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + AgentID uuid.UUID `db:"agent_id" json:"agent_id"` + Name string `db:"name" json:"name"` + Icon string `db:"icon" json:"icon"` + Command sql.NullString `db:"command" json:"command"` + Target sql.NullString `db:"target" json:"target"` +} + type WorkspaceBuild struct { ID uuid.UUID `db:"id" json:"id"` CreatedAt time.Time `db:"created_at" json:"created_at"` diff --git a/coderd/database/querier.go b/coderd/database/querier.go index db2f3f0d49987..73057cc01085f 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -58,6 +58,8 @@ type querier interface { GetWorkspaceAgentByID(ctx context.Context, id uuid.UUID) (WorkspaceAgent, error) GetWorkspaceAgentByInstanceID(ctx context.Context, authInstanceID string) (WorkspaceAgent, error) GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgent, error) + GetWorkspaceAppsByAgentID(ctx context.Context, agentID uuid.UUID) ([]WorkspaceApp, error) + GetWorkspaceAppsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceApp, error) GetWorkspaceBuildByID(ctx context.Context, id uuid.UUID) (WorkspaceBuild, error) GetWorkspaceBuildByJobID(ctx context.Context, jobID uuid.UUID) (WorkspaceBuild, error) GetWorkspaceBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) ([]WorkspaceBuild, error) @@ -88,6 +90,7 @@ type querier interface { InsertUser(ctx context.Context, arg InsertUserParams) (User, error) InsertWorkspace(ctx context.Context, arg InsertWorkspaceParams) (Workspace, error) InsertWorkspaceAgent(ctx context.Context, arg InsertWorkspaceAgentParams) (WorkspaceAgent, error) + InsertWorkspaceApp(ctx context.Context, arg InsertWorkspaceAppParams) (WorkspaceApp, error) InsertWorkspaceBuild(ctx context.Context, arg InsertWorkspaceBuildParams) (WorkspaceBuild, error) InsertWorkspaceResource(ctx context.Context, arg InsertWorkspaceResourceParams) (WorkspaceResource, error) UpdateAPIKeyByID(ctx context.Context, arg UpdateAPIKeyByIDParams) error diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index f91070915c097..7ff9b54102898 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -2630,6 +2630,124 @@ func (q *sqlQuerier) UpdateWorkspaceAgentConnectionByID(ctx context.Context, arg return err } +const getWorkspaceAppsByAgentID = `-- name: GetWorkspaceAppsByAgentID :many +SELECT id, created_at, agent_id, name, icon, command, target FROM workspace_apps WHERE agent_id = $1 +` + +func (q *sqlQuerier) GetWorkspaceAppsByAgentID(ctx context.Context, agentID uuid.UUID) ([]WorkspaceApp, error) { + rows, err := q.db.QueryContext(ctx, getWorkspaceAppsByAgentID, agentID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []WorkspaceApp + for rows.Next() { + var i WorkspaceApp + if err := rows.Scan( + &i.ID, + &i.CreatedAt, + &i.AgentID, + &i.Name, + &i.Icon, + &i.Command, + &i.Target, + ); 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 getWorkspaceAppsByAgentIDs = `-- name: GetWorkspaceAppsByAgentIDs :many +SELECT id, created_at, agent_id, name, icon, command, target FROM workspace_apps WHERE agent_id = ANY($1 :: uuid [ ]) +` + +func (q *sqlQuerier) GetWorkspaceAppsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceApp, error) { + rows, err := q.db.QueryContext(ctx, getWorkspaceAppsByAgentIDs, pq.Array(ids)) + if err != nil { + return nil, err + } + defer rows.Close() + var items []WorkspaceApp + for rows.Next() { + var i WorkspaceApp + if err := rows.Scan( + &i.ID, + &i.CreatedAt, + &i.AgentID, + &i.Name, + &i.Icon, + &i.Command, + &i.Target, + ); 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 insertWorkspaceApp = `-- name: InsertWorkspaceApp :one +INSERT INTO + workspace_apps ( + id, + created_at, + agent_id, + name, + icon, + command, + target + ) +VALUES + ($1, $2, $3, $4, $5, $6, $7) RETURNING id, created_at, agent_id, name, icon, command, target +` + +type InsertWorkspaceAppParams struct { + ID uuid.UUID `db:"id" json:"id"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + AgentID uuid.UUID `db:"agent_id" json:"agent_id"` + Name string `db:"name" json:"name"` + Icon string `db:"icon" json:"icon"` + Command sql.NullString `db:"command" json:"command"` + Target sql.NullString `db:"target" json:"target"` +} + +func (q *sqlQuerier) InsertWorkspaceApp(ctx context.Context, arg InsertWorkspaceAppParams) (WorkspaceApp, error) { + row := q.db.QueryRowContext(ctx, insertWorkspaceApp, + arg.ID, + arg.CreatedAt, + arg.AgentID, + arg.Name, + arg.Icon, + arg.Command, + arg.Target, + ) + var i WorkspaceApp + err := row.Scan( + &i.ID, + &i.CreatedAt, + &i.AgentID, + &i.Name, + &i.Icon, + &i.Command, + &i.Target, + ) + return i, err +} + const getWorkspaceBuildByID = `-- name: GetWorkspaceBuildByID :one SELECT id, created_at, updated_at, workspace_id, template_version_id, name, before_id, after_id, transition, initiator_id, provisioner_state, job_id diff --git a/coderd/database/queries/workspaceapps.sql b/coderd/database/queries/workspaceapps.sql new file mode 100644 index 0000000000000..1364fd3b59c93 --- /dev/null +++ b/coderd/database/queries/workspaceapps.sql @@ -0,0 +1,19 @@ +-- name: GetWorkspaceAppsByAgentID :many +SELECT * FROM workspace_apps WHERE agent_id = $1; + +-- name: GetWorkspaceAppsByAgentIDs :many +SELECT * FROM workspace_apps WHERE agent_id = ANY(@ids :: uuid [ ]); + +-- name: InsertWorkspaceApp :one +INSERT INTO + workspace_apps ( + id, + created_at, + agent_id, + name, + icon, + command, + target + ) +VALUES + ($1, $2, $3, $4, $5, $6, $7) RETURNING *; diff --git a/coderd/provisionerdaemons.go b/coderd/provisionerdaemons.go index a7639e6b76c3c..e7128833985de 100644 --- a/coderd/provisionerdaemons.go +++ b/coderd/provisionerdaemons.go @@ -625,7 +625,7 @@ func insertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid. } } - _, err := db.InsertWorkspaceAgent(ctx, database.InsertWorkspaceAgentParams{ + dbAgent, err := db.InsertWorkspaceAgent(ctx, database.InsertWorkspaceAgentParams{ ID: uuid.New(), CreatedAt: database.Now(), UpdatedAt: database.Now(), @@ -645,6 +645,27 @@ func insertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid. if err != nil { return xerrors.Errorf("insert agent: %w", err) } + + for _, app := range agent.Apps { + _, err := db.InsertWorkspaceApp(ctx, database.InsertWorkspaceAppParams{ + ID: uuid.New(), + CreatedAt: database.Now(), + AgentID: dbAgent.ID, + Name: app.Name, + Icon: app.Icon, + Command: sql.NullString{ + String: app.Command, + Valid: app.Command != "", + }, + Target: sql.NullString{ + String: app.Target, + Valid: app.Target != "", + }, + }) + if err != nil { + return xerrors.Errorf("insert app: %w", err) + } + } } return nil } diff --git a/coderd/provisionerjobs.go b/coderd/provisionerjobs.go index 5dd57211cbfe2..53d947116a4d2 100644 --- a/coderd/provisionerjobs.go +++ b/coderd/provisionerjobs.go @@ -209,6 +209,20 @@ func (api *api) provisionerJobResources(rw http.ResponseWriter, r *http.Request, }) return } + resourceAgentIDs := make([]uuid.UUID, 0) + for _, agent := range resourceAgents { + resourceAgentIDs = append(resourceAgentIDs, agent.ID) + } + apps, err := api.Database.GetWorkspaceAppsByAgentIDs(r.Context(), resourceAgentIDs) + if errors.Is(err, sql.ErrNoRows) { + err = nil + } + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get workspace apps: %s", err), + }) + return + } apiResources := make([]codersdk.WorkspaceResource, 0) for _, resource := range resources { @@ -217,7 +231,14 @@ func (api *api) provisionerJobResources(rw http.ResponseWriter, r *http.Request, if agent.ResourceID != resource.ID { continue } - apiAgent, err := convertWorkspaceAgent(agent, api.AgentConnectionUpdateFrequency) + dbApps := make([]database.WorkspaceApp, 0) + for _, app := range apps { + if app.AgentID == agent.ID { + dbApps = append(dbApps, app) + } + } + + apiAgent, err := convertWorkspaceAgent(agent, convertApps(dbApps), api.AgentConnectionUpdateFrequency) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ Message: fmt.Sprintf("convert provisioner job agent: %s", err), diff --git a/coderd/rbac/README.md b/coderd/rbac/README.md index 69ba5e9f925aa..f2e1283206822 100644 --- a/coderd/rbac/README.md +++ b/coderd/rbac/README.md @@ -51,7 +51,7 @@ This can be represented by the following truth table, where Y represents *positi ## Example Permissions -- `+site.*.*.read`: allowed to perform the `read` action against all objects of type `devurl` in a given Coder deployment. +- `+site.*.*.read`: allowed to perform the `read` action against all objects of type `app` in a given Coder deployment. - `-user.workspace.*.create`: user is not allowed to create workspaces. ## Roles diff --git a/coderd/rbac/object.go b/coderd/rbac/object.go index a4be9b1edab5b..8f994a5dd3b58 100644 --- a/coderd/rbac/object.go +++ b/coderd/rbac/object.go @@ -41,7 +41,7 @@ type Object struct { // OrgID specifies which org the object is a part of. OrgID string `json:"org_owner"` - // Type is "workspace", "project", "devurl", etc + // Type is "workspace", "project", "app", etc Type string `json:"type"` // TODO: SharedUsers? } diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index d2a7baeab560d..608fcd80f1c06 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -30,7 +30,14 @@ import ( func (api *api) workspaceAgent(rw http.ResponseWriter, r *http.Request) { workspaceAgent := httpmw.WorkspaceAgentParam(r) - apiAgent, err := convertWorkspaceAgent(workspaceAgent, api.AgentConnectionUpdateFrequency) + dbApps, err := api.Database.GetWorkspaceAppsByAgentID(r.Context(), workspaceAgent.ID) + if err != nil && !xerrors.Is(err, sql.ErrNoRows) { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get workspace agent apps: %s", err), + }) + return + } + apiAgent, err := convertWorkspaceAgent(workspaceAgent, convertApps(dbApps), api.AgentConnectionUpdateFrequency) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ Message: fmt.Sprintf("convert workspace agent: %s", err), @@ -48,7 +55,7 @@ func (api *api) workspaceAgentDial(rw http.ResponseWriter, r *http.Request) { defer api.websocketWaitGroup.Done() workspaceAgent := httpmw.WorkspaceAgentParam(r) - apiAgent, err := convertWorkspaceAgent(workspaceAgent, api.AgentConnectionUpdateFrequency) + apiAgent, err := convertWorkspaceAgent(workspaceAgent, nil, api.AgentConnectionUpdateFrequency) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ Message: fmt.Sprintf("convert workspace agent: %s", err), @@ -92,7 +99,7 @@ func (api *api) workspaceAgentDial(rw http.ResponseWriter, r *http.Request) { func (api *api) workspaceAgentMetadata(rw http.ResponseWriter, r *http.Request) { workspaceAgent := httpmw.WorkspaceAgent(r) - apiAgent, err := convertWorkspaceAgent(workspaceAgent, api.AgentConnectionUpdateFrequency) + apiAgent, err := convertWorkspaceAgent(workspaceAgent, nil, api.AgentConnectionUpdateFrequency) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ Message: fmt.Sprintf("convert workspace agent: %s", err), @@ -331,7 +338,7 @@ func (api *api) workspaceAgentPTY(rw http.ResponseWriter, r *http.Request) { defer api.websocketWaitGroup.Done() workspaceAgent := httpmw.WorkspaceAgentParam(r) - apiAgent, err := convertWorkspaceAgent(workspaceAgent, api.AgentConnectionUpdateFrequency) + apiAgent, err := convertWorkspaceAgent(workspaceAgent, nil, api.AgentConnectionUpdateFrequency) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ Message: fmt.Sprintf("convert workspace agent: %s", err), @@ -451,7 +458,21 @@ func (api *api) dialWorkspaceAgent(r *http.Request, agentID uuid.UUID) (*agent.C }, nil } -func convertWorkspaceAgent(dbAgent database.WorkspaceAgent, agentUpdateFrequency time.Duration) (codersdk.WorkspaceAgent, error) { +func convertApps(dbApps []database.WorkspaceApp) []codersdk.WorkspaceApp { + apps := make([]codersdk.WorkspaceApp, 0) + for _, dbApp := range dbApps { + apps = append(apps, codersdk.WorkspaceApp{ + ID: dbApp.ID, + Name: dbApp.Name, + Command: dbApp.Command.String, + Target: dbApp.Target.String, + Icon: dbApp.Icon, + }) + } + return apps +} + +func convertWorkspaceAgent(dbAgent database.WorkspaceAgent, apps []codersdk.WorkspaceApp, agentUpdateFrequency time.Duration) (codersdk.WorkspaceAgent, error) { var envs map[string]string if dbAgent.EnvironmentVariables.Valid { err := json.Unmarshal(dbAgent.EnvironmentVariables.RawMessage, &envs) @@ -471,6 +492,7 @@ func convertWorkspaceAgent(dbAgent database.WorkspaceAgent, agentUpdateFrequency StartupScript: dbAgent.StartupScript.String, EnvironmentVariables: envs, Directory: dbAgent.Directory, + Apps: apps, } if dbAgent.FirstConnectedAt.Valid { workspaceAgent.FirstConnectedAt = &dbAgent.FirstConnectedAt.Time diff --git a/coderd/workspaceresources.go b/coderd/workspaceresources.go index a337b3828f7ee..4595a95491d19 100644 --- a/coderd/workspaceresources.go +++ b/coderd/workspaceresources.go @@ -8,6 +8,7 @@ import ( "github.com/google/uuid" + "github.com/coder/coder/coderd/database" "github.com/coder/coder/coderd/httpapi" "github.com/coder/coder/coderd/httpmw" "github.com/coder/coder/codersdk" @@ -39,9 +40,30 @@ func (api *api) workspaceResource(rw http.ResponseWriter, r *http.Request) { }) return } + agentIDs := make([]uuid.UUID, 0) + for _, agent := range agents { + agentIDs = append(agentIDs, agent.ID) + } + apps, err := api.Database.GetWorkspaceAppsByAgentIDs(r.Context(), agentIDs) + if errors.Is(err, sql.ErrNoRows) { + err = nil + } + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get workspace apps: %s", err), + }) + return + } apiAgents := make([]codersdk.WorkspaceAgent, 0) for _, agent := range agents { - convertedAgent, err := convertWorkspaceAgent(agent, api.AgentConnectionUpdateFrequency) + dbApps := make([]database.WorkspaceApp, 0) + for _, app := range apps { + if app.AgentID == agent.ID { + dbApps = append(dbApps, app) + } + } + + convertedAgent, err := convertWorkspaceAgent(agent, convertApps(dbApps), api.AgentConnectionUpdateFrequency) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ Message: fmt.Sprintf("convert provisioner job agent: %s", err), diff --git a/coderd/workspaceresources_test.go b/coderd/workspaceresources_test.go index 8b4a846cfa258..2ba772c61a9ac 100644 --- a/coderd/workspaceresources_test.go +++ b/coderd/workspaceresources_test.go @@ -44,4 +44,43 @@ func TestWorkspaceResource(t *testing.T) { _, err = client.WorkspaceResource(context.Background(), resources[0].ID) require.NoError(t, err) }) + + t.Run("Apps", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + coderdtest.NewProvisionerDaemon(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{}, + Apps: []*proto.App{{ + Name: "code-server", + Command: "code-server", + }}, + }}, + }}, + }, + }, + }}, + }) + 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) + require.Len(t, resource.Agents, 1) + agent := resource.Agents[0] + require.Len(t, agent.Apps, 1) + }) } diff --git a/codersdk/workspaceapps.go b/codersdk/workspaceapps.go new file mode 100644 index 0000000000000..faee514dba349 --- /dev/null +++ b/codersdk/workspaceapps.go @@ -0,0 +1,11 @@ +package codersdk + +import "github.com/google/uuid" + +type WorkspaceApp struct { + ID uuid.UUID `json:"id"` + Name string `json:"name"` + Command string `json:"command,omitempty"` + Target string `json:"target,omitempty"` + Icon string `json:"icon"` +} diff --git a/codersdk/workspaceresources.go b/codersdk/workspaceresources.go index b21451bbc63ea..1d11788857dd1 100644 --- a/codersdk/workspaceresources.go +++ b/codersdk/workspaceresources.go @@ -46,6 +46,7 @@ type WorkspaceAgent struct { OperatingSystem string `json:"operating_system"` StartupScript string `json:"startup_script,omitempty"` Directory string `json:"directory,omitempty"` + Apps []WorkspaceApp `json:"apps"` } type WorkspaceAgentResourceMetadata struct { diff --git a/provisioner/terraform/provision.go b/provisioner/terraform/provision.go index 4e9865e829344..8a9c64d8b2497 100644 --- a/provisioner/terraform/provision.go +++ b/provisioner/terraform/provision.go @@ -344,7 +344,7 @@ func parseTerraformPlan(ctx context.Context, terraform *tfexec.Terraform, planfi if resource.Mode == tfjson.DataResourceMode { continue } - if resource.Type == "coder_agent" || resource.Type == "coder_agent_instance" { + if resource.Type == "coder_agent" || resource.Type == "coder_agent_instance" || resource.Type == "coder_app" { continue } resourceKey := strings.Join([]string{resource.Type, resource.Name}, ".") @@ -471,11 +471,45 @@ func parseTerraformApply(ctx context.Context, terraform *tfexec.Terraform, state } } + type appAttributes struct { + AgentID string `mapstructure:"agent_id"` + Name string `mapstructure:"name"` + Icon string `mapstructure:"icon"` + Target string `mapstructure:"target"` + Command string `mapstructure:"command"` + } + // Associate Apps with agents. + for _, resource := range state.Values.RootModule.Resources { + if resource.Type != "coder_app" { + continue + } + var attrs appAttributes + err = mapstructure.Decode(resource.AttributeValues, &attrs) + if err != nil { + return nil, xerrors.Errorf("decode app attributes: %w", err) + } + if attrs.Name == "" { + // Default to the resource name if none is set! + attrs.Name = resource.Name + } + for _, agent := range agents { + if agent.Id != attrs.AgentID { + continue + } + agent.Apps = append(agent.Apps, &proto.App{ + Name: attrs.Name, + Command: attrs.Command, + Target: attrs.Target, + Icon: attrs.Icon, + }) + } + } + for _, resource := range state.Values.RootModule.Resources { if resource.Mode == tfjson.DataResourceMode { continue } - if resource.Type == "coder_agent" || resource.Type == "coder_agent_instance" { + if resource.Type == "coder_agent" || resource.Type == "coder_agent_instance" || resource.Type == "coder_app" { continue } resourceKey := strings.Join([]string{resource.Type, resource.Name}, ".") diff --git a/provisioner/terraform/provision_test.go b/provisioner/terraform/provision_test.go index 6169f8e5f0bc4..ff6e6a1493dab 100644 --- a/provisioner/terraform/provision_test.go +++ b/provisioner/terraform/provision_test.go @@ -28,7 +28,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.3.4" + version = "0.4.0" } } } @@ -432,6 +432,52 @@ provider "coder" { }, }, }, + }, { + Name: "agent-with-app", + Files: map[string]string{ + "main.tf": provider + ` + resource "coder_agent" "A" { + os = "darwin" + arch = "amd64" + } + resource "null_resource" "A" { + depends_on = [ + coder_agent.A + ] + } + resource "coder_app" "A" { + agent_id = coder_agent.A.id + command = "vim" + } + `, + }, + Request: &proto.Provision_Request{ + Type: &proto.Provision_Request_Start{ + Start: &proto.Provision_Start{ + Metadata: &proto.Provision_Metadata{}, + }, + }, + }, + Response: &proto.Provision_Response{ + Type: &proto.Provision_Response_Complete{ + Complete: &proto.Provision_Complete{ + Resources: []*proto.Resource{{ + Name: "A", + Type: "null_resource", + Agents: []*proto.Agent{{ + Name: "A", + OperatingSystem: "darwin", + Architecture: "amd64", + Auth: &proto.Agent_Token{}, + Apps: []*proto.App{{ + Name: "A", + Command: "vim", + }}, + }}, + }}, + }, + }, + }, }} { testCase := testCase t.Run(testCase.Name, func(t *testing.T) { diff --git a/provisionersdk/proto/provisioner.pb.go b/provisionersdk/proto/provisioner.pb.go index 01d68d25a8619..2dc5c9d16f60c 100644 --- a/provisionersdk/proto/provisioner.pb.go +++ b/provisionersdk/proto/provisioner.pb.go @@ -711,6 +711,7 @@ type Agent struct { OperatingSystem string `protobuf:"bytes,5,opt,name=operating_system,json=operatingSystem,proto3" json:"operating_system,omitempty"` Architecture string `protobuf:"bytes,6,opt,name=architecture,proto3" json:"architecture,omitempty"` Directory string `protobuf:"bytes,7,opt,name=directory,proto3" json:"directory,omitempty"` + Apps []*App `protobuf:"bytes,8,rep,name=apps,proto3" json:"apps,omitempty"` // Types that are assignable to Auth: // *Agent_Token // *Agent_InstanceId @@ -798,6 +799,13 @@ func (x *Agent) GetDirectory() string { return "" } +func (x *Agent) GetApps() []*App { + if x != nil { + return x.Apps + } + return nil +} + func (m *Agent) GetAuth() isAgent_Auth { if m != nil { return m.Auth @@ -824,17 +832,89 @@ type isAgent_Auth interface { } type Agent_Token struct { - Token string `protobuf:"bytes,8,opt,name=token,proto3,oneof"` + Token string `protobuf:"bytes,9,opt,name=token,proto3,oneof"` } type Agent_InstanceId struct { - InstanceId string `protobuf:"bytes,9,opt,name=instance_id,json=instanceId,proto3,oneof"` + InstanceId string `protobuf:"bytes,10,opt,name=instance_id,json=instanceId,proto3,oneof"` } func (*Agent_Token) isAgent_Auth() {} func (*Agent_InstanceId) isAgent_Auth() {} +// App represents a dev-accessible application on the workspace. +type App struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Command string `protobuf:"bytes,2,opt,name=command,proto3" json:"command,omitempty"` + Target string `protobuf:"bytes,3,opt,name=target,proto3" json:"target,omitempty"` + Icon string `protobuf:"bytes,4,opt,name=icon,proto3" json:"icon,omitempty"` +} + +func (x *App) Reset() { + *x = App{} + if protoimpl.UnsafeEnabled { + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *App) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*App) ProtoMessage() {} + +func (x *App) ProtoReflect() protoreflect.Message { + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[8] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use App.ProtoReflect.Descriptor instead. +func (*App) Descriptor() ([]byte, []int) { + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{8} +} + +func (x *App) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *App) GetCommand() string { + if x != nil { + return x.Command + } + return "" +} + +func (x *App) GetTarget() string { + if x != nil { + return x.Target + } + return "" +} + +func (x *App) GetIcon() string { + if x != nil { + return x.Icon + } + return "" +} + // Resource represents created infrastructure. type Resource struct { state protoimpl.MessageState @@ -849,7 +929,7 @@ type Resource struct { func (x *Resource) Reset() { *x = Resource{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[8] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -862,7 +942,7 @@ func (x *Resource) String() string { func (*Resource) ProtoMessage() {} func (x *Resource) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[8] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[9] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -875,7 +955,7 @@ func (x *Resource) ProtoReflect() protoreflect.Message { // Deprecated: Use Resource.ProtoReflect.Descriptor instead. func (*Resource) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{8} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{9} } func (x *Resource) GetName() string { @@ -909,7 +989,7 @@ type Parse struct { func (x *Parse) Reset() { *x = Parse{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[9] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -922,7 +1002,7 @@ func (x *Parse) String() string { func (*Parse) ProtoMessage() {} func (x *Parse) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[9] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[10] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -935,7 +1015,7 @@ func (x *Parse) ProtoReflect() protoreflect.Message { // Deprecated: Use Parse.ProtoReflect.Descriptor instead. func (*Parse) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{9} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{10} } // Provision consumes source-code from a directory to produce resources. @@ -948,7 +1028,7 @@ type Provision struct { func (x *Provision) Reset() { *x = Provision{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[10] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -961,7 +1041,7 @@ func (x *Provision) String() string { func (*Provision) ProtoMessage() {} func (x *Provision) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[10] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[11] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -974,7 +1054,7 @@ func (x *Provision) ProtoReflect() protoreflect.Message { // Deprecated: Use Provision.ProtoReflect.Descriptor instead. func (*Provision) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{10} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{11} } type Parse_Request struct { @@ -988,7 +1068,7 @@ type Parse_Request struct { func (x *Parse_Request) Reset() { *x = Parse_Request{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[12] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1001,7 +1081,7 @@ func (x *Parse_Request) String() string { func (*Parse_Request) ProtoMessage() {} func (x *Parse_Request) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[12] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[13] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1014,7 +1094,7 @@ func (x *Parse_Request) ProtoReflect() protoreflect.Message { // Deprecated: Use Parse_Request.ProtoReflect.Descriptor instead. func (*Parse_Request) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{9, 0} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{10, 0} } func (x *Parse_Request) GetDirectory() string { @@ -1035,7 +1115,7 @@ type Parse_Complete struct { func (x *Parse_Complete) Reset() { *x = Parse_Complete{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[13] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1048,7 +1128,7 @@ func (x *Parse_Complete) String() string { func (*Parse_Complete) ProtoMessage() {} func (x *Parse_Complete) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[13] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[14] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1061,7 +1141,7 @@ func (x *Parse_Complete) ProtoReflect() protoreflect.Message { // Deprecated: Use Parse_Complete.ProtoReflect.Descriptor instead. func (*Parse_Complete) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{9, 1} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{10, 1} } func (x *Parse_Complete) GetParameterSchemas() []*ParameterSchema { @@ -1085,7 +1165,7 @@ type Parse_Response struct { func (x *Parse_Response) Reset() { *x = Parse_Response{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[14] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1098,7 +1178,7 @@ func (x *Parse_Response) String() string { func (*Parse_Response) ProtoMessage() {} func (x *Parse_Response) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[14] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[15] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1111,7 +1191,7 @@ func (x *Parse_Response) ProtoReflect() protoreflect.Message { // Deprecated: Use Parse_Response.ProtoReflect.Descriptor instead. func (*Parse_Response) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{9, 2} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{10, 2} } func (m *Parse_Response) GetType() isParse_Response_Type { @@ -1167,7 +1247,7 @@ type Provision_Metadata struct { func (x *Provision_Metadata) Reset() { *x = Provision_Metadata{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[15] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1180,7 +1260,7 @@ func (x *Provision_Metadata) String() string { func (*Provision_Metadata) ProtoMessage() {} func (x *Provision_Metadata) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[15] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[16] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1193,7 +1273,7 @@ func (x *Provision_Metadata) ProtoReflect() protoreflect.Message { // Deprecated: Use Provision_Metadata.ProtoReflect.Descriptor instead. func (*Provision_Metadata) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{10, 0} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{11, 0} } func (x *Provision_Metadata) GetCoderUrl() string { @@ -1253,7 +1333,7 @@ type Provision_Start struct { func (x *Provision_Start) Reset() { *x = Provision_Start{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[16] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1266,7 +1346,7 @@ func (x *Provision_Start) String() string { func (*Provision_Start) ProtoMessage() {} func (x *Provision_Start) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[16] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[17] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1279,7 +1359,7 @@ func (x *Provision_Start) ProtoReflect() protoreflect.Message { // Deprecated: Use Provision_Start.ProtoReflect.Descriptor instead. func (*Provision_Start) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{10, 1} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{11, 1} } func (x *Provision_Start) GetDirectory() string { @@ -1326,7 +1406,7 @@ type Provision_Cancel struct { func (x *Provision_Cancel) Reset() { *x = Provision_Cancel{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[17] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1339,7 +1419,7 @@ func (x *Provision_Cancel) String() string { func (*Provision_Cancel) ProtoMessage() {} func (x *Provision_Cancel) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[17] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[18] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1352,7 +1432,7 @@ func (x *Provision_Cancel) ProtoReflect() protoreflect.Message { // Deprecated: Use Provision_Cancel.ProtoReflect.Descriptor instead. func (*Provision_Cancel) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{10, 2} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{11, 2} } type Provision_Request struct { @@ -1369,7 +1449,7 @@ type Provision_Request struct { func (x *Provision_Request) Reset() { *x = Provision_Request{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[18] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1382,7 +1462,7 @@ func (x *Provision_Request) String() string { func (*Provision_Request) ProtoMessage() {} func (x *Provision_Request) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[18] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[19] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1395,7 +1475,7 @@ func (x *Provision_Request) ProtoReflect() protoreflect.Message { // Deprecated: Use Provision_Request.ProtoReflect.Descriptor instead. func (*Provision_Request) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{10, 3} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{11, 3} } func (m *Provision_Request) GetType() isProvision_Request_Type { @@ -1448,7 +1528,7 @@ type Provision_Complete struct { func (x *Provision_Complete) Reset() { *x = Provision_Complete{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[19] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1461,7 +1541,7 @@ func (x *Provision_Complete) String() string { func (*Provision_Complete) ProtoMessage() {} func (x *Provision_Complete) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[19] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[20] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1474,7 +1554,7 @@ func (x *Provision_Complete) ProtoReflect() protoreflect.Message { // Deprecated: Use Provision_Complete.ProtoReflect.Descriptor instead. func (*Provision_Complete) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{10, 4} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{11, 4} } func (x *Provision_Complete) GetState() []byte { @@ -1512,7 +1592,7 @@ type Provision_Response struct { func (x *Provision_Response) Reset() { *x = Provision_Response{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[20] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1525,7 +1605,7 @@ func (x *Provision_Response) String() string { func (*Provision_Response) ProtoMessage() {} func (x *Provision_Response) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[20] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[21] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1538,7 +1618,7 @@ func (x *Provision_Response) ProtoReflect() protoreflect.Message { // Deprecated: Use Provision_Response.ProtoReflect.Descriptor instead. func (*Provision_Response) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{10, 5} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{11, 5} } func (m *Provision_Response) GetType() isProvision_Response_Type { @@ -1660,7 +1740,7 @@ var file_provisionersdk_proto_provisioner_proto_rawDesc = []byte{ 0x70, 0x75, 0x74, 0x22, 0x37, 0x0a, 0x14, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x41, 0x75, 0x74, 0x68, 0x12, 0x1f, 0x0a, 0x0b, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0a, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x64, 0x22, 0xe9, 0x02, 0x0a, + 0x52, 0x0a, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x64, 0x22, 0x98, 0x03, 0x0a, 0x05, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2d, 0x0a, 0x03, 0x65, 0x6e, @@ -1675,114 +1755,123 @@ var file_provisionersdk_proto_provisioner_proto_rawDesc = []byte{ 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x61, 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x07, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x16, 0x0a, - 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x05, - 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x21, 0x0a, 0x0b, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, - 0x65, 0x5f, 0x69, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0a, 0x69, 0x6e, - 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x64, 0x1a, 0x36, 0x0a, 0x08, 0x45, 0x6e, 0x76, 0x45, - 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, - 0x42, 0x06, 0x0a, 0x04, 0x61, 0x75, 0x74, 0x68, 0x22, 0x5e, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x2a, 0x0a, 0x06, - 0x61, 0x67, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x70, - 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x41, 0x67, 0x65, 0x6e, 0x74, - 0x52, 0x06, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x73, 0x22, 0xfc, 0x01, 0x0a, 0x05, 0x50, 0x61, 0x72, - 0x73, 0x65, 0x1a, 0x27, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, - 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x1a, 0x55, 0x0a, 0x08, 0x43, - 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x49, 0x0a, 0x11, 0x70, 0x61, 0x72, 0x61, 0x6d, - 0x65, 0x74, 0x65, 0x72, 0x5f, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x73, 0x18, 0x02, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, - 0x2e, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, - 0x52, 0x10, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x53, 0x63, 0x68, 0x65, 0x6d, - 0x61, 0x73, 0x1a, 0x73, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, - 0x0a, 0x03, 0x6c, 0x6f, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x72, - 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4c, 0x6f, 0x67, 0x48, 0x00, 0x52, - 0x03, 0x6c, 0x6f, 0x67, 0x12, 0x39, 0x0a, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, - 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, + 0x28, 0x09, 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x2d, 0x0a, + 0x07, 0x64, 0x65, 0x76, 0x75, 0x72, 0x6c, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, + 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x44, 0x65, 0x76, + 0x75, 0x72, 0x6c, 0x52, 0x07, 0x64, 0x65, 0x76, 0x75, 0x72, 0x6c, 0x73, 0x12, 0x16, 0x0a, 0x05, + 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x05, 0x74, + 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x21, 0x0a, 0x0b, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, + 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0a, 0x69, 0x6e, 0x73, + 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x64, 0x1a, 0x36, 0x0a, 0x08, 0x45, 0x6e, 0x76, 0x45, 0x6e, + 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, + 0x06, 0x0a, 0x04, 0x61, 0x75, 0x74, 0x68, 0x22, 0x62, 0x0a, 0x06, 0x44, 0x65, 0x76, 0x75, 0x72, + 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, + 0x16, 0x0a, 0x06, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x06, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x69, 0x63, 0x6f, 0x6e, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x69, 0x63, 0x6f, 0x6e, 0x22, 0x5e, 0x0a, 0x08, 0x52, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, + 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, + 0x2a, 0x0a, 0x06, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x12, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x41, 0x67, + 0x65, 0x6e, 0x74, 0x52, 0x06, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x73, 0x22, 0xfc, 0x01, 0x0a, 0x05, + 0x50, 0x61, 0x72, 0x73, 0x65, 0x1a, 0x27, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x1c, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x1a, 0x55, + 0x0a, 0x08, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x49, 0x0a, 0x11, 0x70, 0x61, + 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x5f, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x73, 0x18, + 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, + 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x53, 0x63, 0x68, + 0x65, 0x6d, 0x61, 0x52, 0x10, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x53, 0x63, + 0x68, 0x65, 0x6d, 0x61, 0x73, 0x1a, 0x73, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x24, 0x0a, 0x03, 0x6c, 0x6f, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, + 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4c, 0x6f, 0x67, + 0x48, 0x00, 0x52, 0x03, 0x6c, 0x6f, 0x67, 0x12, 0x39, 0x0a, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6c, + 0x65, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x76, + 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x2e, 0x43, 0x6f, + 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x48, 0x00, 0x52, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, + 0x74, 0x65, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0xfa, 0x06, 0x0a, 0x09, 0x50, + 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x1a, 0x9d, 0x02, 0x0a, 0x08, 0x4d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x5f, 0x75, + 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x55, + 0x72, 0x6c, 0x12, 0x53, 0x0a, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, + 0x74, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, + 0x32, 0x20, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x57, + 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, + 0x6f, 0x6e, 0x52, 0x13, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, + 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x25, 0x0a, 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, + 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0d, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x27, + 0x0a, 0x0f, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, + 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x12, 0x21, 0x0a, 0x0c, 0x77, 0x6f, 0x72, 0x6b, 0x73, + 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x77, + 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, 0x2c, 0x0a, 0x12, 0x77, 0x6f, + 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x69, 0x64, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, + 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x49, 0x64, 0x1a, 0xd9, 0x01, 0x0a, 0x05, 0x53, 0x74, 0x61, + 0x72, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, + 0x12, 0x46, 0x0a, 0x10, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x5f, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x72, 0x6f, + 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, + 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, + 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x3b, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x72, 0x6f, + 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, + 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x64, + 0x72, 0x79, 0x5f, 0x72, 0x75, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x64, 0x72, + 0x79, 0x52, 0x75, 0x6e, 0x1a, 0x08, 0x0a, 0x06, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x1a, 0x80, + 0x01, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x34, 0x0a, 0x05, 0x73, 0x74, + 0x61, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x76, + 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, + 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x48, 0x00, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, + 0x12, 0x37, 0x0a, 0x06, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, + 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x48, + 0x00, 0x52, 0x06, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, + 0x65, 0x1a, 0x6b, 0x0a, 0x08, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x14, 0x0a, + 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, + 0x61, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x33, 0x0a, 0x09, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, + 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x1a, 0x77, + 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x03, 0x6c, 0x6f, + 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, + 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4c, 0x6f, 0x67, 0x48, 0x00, 0x52, 0x03, 0x6c, 0x6f, 0x67, + 0x12, 0x3d, 0x0a, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, + 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x48, 0x00, 0x52, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x42, - 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0xfa, 0x06, 0x0a, 0x09, 0x50, 0x72, 0x6f, 0x76, - 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x1a, 0x9d, 0x02, 0x0a, 0x08, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, - 0x74, 0x61, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x5f, 0x75, 0x72, 0x6c, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x55, 0x72, 0x6c, 0x12, - 0x53, 0x0a, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x74, 0x72, 0x61, - 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x20, 0x2e, - 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x57, 0x6f, 0x72, 0x6b, - 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, - 0x13, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, - 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x25, 0x0a, 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, - 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x77, 0x6f, - 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x77, - 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, - 0x77, 0x6e, 0x65, 0x72, 0x12, 0x21, 0x0a, 0x0c, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, - 0x65, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x77, 0x6f, 0x72, 0x6b, - 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, 0x2c, 0x0a, 0x12, 0x77, 0x6f, 0x72, 0x6b, 0x73, - 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x10, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, - 0x6e, 0x65, 0x72, 0x49, 0x64, 0x1a, 0xd9, 0x01, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x72, 0x74, 0x12, - 0x1c, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x46, 0x0a, - 0x10, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, - 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, - 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, - 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x3b, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, - 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, - 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, - 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, - 0x74, 0x61, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x64, 0x72, 0x79, 0x5f, - 0x72, 0x75, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x64, 0x72, 0x79, 0x52, 0x75, - 0x6e, 0x1a, 0x08, 0x0a, 0x06, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x1a, 0x80, 0x01, 0x0a, 0x07, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x34, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, - 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x53, - 0x74, 0x61, 0x72, 0x74, 0x48, 0x00, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, 0x37, 0x0a, - 0x06, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, - 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, - 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x48, 0x00, 0x52, 0x06, - 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x1a, 0x6b, - 0x0a, 0x08, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, - 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, - 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x33, 0x0a, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, - 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x52, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x1a, 0x77, 0x0a, 0x08, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x03, 0x6c, 0x6f, 0x67, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, - 0x65, 0x72, 0x2e, 0x4c, 0x6f, 0x67, 0x48, 0x00, 0x52, 0x03, 0x6c, 0x6f, 0x67, 0x12, 0x3d, 0x0a, - 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, - 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, - 0x48, 0x00, 0x52, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x42, 0x06, 0x0a, 0x04, - 0x74, 0x79, 0x70, 0x65, 0x2a, 0x3f, 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, - 0x12, 0x09, 0x0a, 0x05, 0x54, 0x52, 0x41, 0x43, 0x45, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x44, - 0x45, 0x42, 0x55, 0x47, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x02, - 0x12, 0x08, 0x0a, 0x04, 0x57, 0x41, 0x52, 0x4e, 0x10, 0x03, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, - 0x52, 0x4f, 0x52, 0x10, 0x04, 0x2a, 0x37, 0x0a, 0x13, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, - 0x63, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x09, 0x0a, 0x05, - 0x53, 0x54, 0x41, 0x52, 0x54, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x53, 0x54, 0x4f, 0x50, 0x10, - 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x53, 0x54, 0x52, 0x4f, 0x59, 0x10, 0x02, 0x32, 0xa3, - 0x01, 0x0a, 0x0b, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x12, 0x42, - 0x0a, 0x05, 0x50, 0x61, 0x72, 0x73, 0x65, 0x12, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, - 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x2e, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, - 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x30, 0x01, 0x12, 0x50, 0x0a, 0x09, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x12, - 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, - 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, - 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x28, 0x01, 0x30, 0x01, 0x42, 0x2d, 0x5a, 0x2b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, - 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x70, - 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x73, 0x64, 0x6b, 0x2f, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x2a, 0x3f, 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x4c, 0x65, + 0x76, 0x65, 0x6c, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x52, 0x41, 0x43, 0x45, 0x10, 0x00, 0x12, 0x09, + 0x0a, 0x05, 0x44, 0x45, 0x42, 0x55, 0x47, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, + 0x4f, 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x57, 0x41, 0x52, 0x4e, 0x10, 0x03, 0x12, 0x09, 0x0a, + 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x04, 0x2a, 0x37, 0x0a, 0x13, 0x57, 0x6f, 0x72, 0x6b, + 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, + 0x09, 0x0a, 0x05, 0x53, 0x54, 0x41, 0x52, 0x54, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x53, 0x54, + 0x4f, 0x50, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x53, 0x54, 0x52, 0x4f, 0x59, 0x10, + 0x02, 0x32, 0xa3, 0x01, 0x0a, 0x0b, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, + 0x72, 0x12, 0x42, 0x0a, 0x05, 0x50, 0x61, 0x72, 0x73, 0x65, 0x12, 0x1a, 0x2e, 0x70, 0x72, 0x6f, + 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x2e, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, + 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x50, 0x0a, 0x09, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, + 0x6f, 0x6e, 0x12, 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, + 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, + 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x28, 0x01, 0x30, 0x01, 0x42, 0x2d, 0x5a, 0x2b, 0x67, 0x69, 0x74, 0x68, 0x75, + 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, + 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x73, 0x64, 0x6b, + 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -1798,7 +1887,7 @@ func file_provisionersdk_proto_provisioner_proto_rawDescGZIP() []byte { } var file_provisionersdk_proto_provisioner_proto_enumTypes = make([]protoimpl.EnumInfo, 5) -var file_provisionersdk_proto_provisioner_proto_msgTypes = make([]protoimpl.MessageInfo, 21) +var file_provisionersdk_proto_provisioner_proto_msgTypes = make([]protoimpl.MessageInfo, 22) var file_provisionersdk_proto_provisioner_proto_goTypes = []interface{}{ (LogLevel)(0), // 0: provisioner.LogLevel (WorkspaceTransition)(0), // 1: provisioner.WorkspaceTransition @@ -1813,19 +1902,20 @@ var file_provisionersdk_proto_provisioner_proto_goTypes = []interface{}{ (*Log)(nil), // 10: provisioner.Log (*InstanceIdentityAuth)(nil), // 11: provisioner.InstanceIdentityAuth (*Agent)(nil), // 12: provisioner.Agent - (*Resource)(nil), // 13: provisioner.Resource - (*Parse)(nil), // 14: provisioner.Parse - (*Provision)(nil), // 15: provisioner.Provision - nil, // 16: provisioner.Agent.EnvEntry - (*Parse_Request)(nil), // 17: provisioner.Parse.Request - (*Parse_Complete)(nil), // 18: provisioner.Parse.Complete - (*Parse_Response)(nil), // 19: provisioner.Parse.Response - (*Provision_Metadata)(nil), // 20: provisioner.Provision.Metadata - (*Provision_Start)(nil), // 21: provisioner.Provision.Start - (*Provision_Cancel)(nil), // 22: provisioner.Provision.Cancel - (*Provision_Request)(nil), // 23: provisioner.Provision.Request - (*Provision_Complete)(nil), // 24: provisioner.Provision.Complete - (*Provision_Response)(nil), // 25: provisioner.Provision.Response + (*App)(nil), // 13: provisioner.App + (*Resource)(nil), // 14: provisioner.Resource + (*Parse)(nil), // 15: provisioner.Parse + (*Provision)(nil), // 16: provisioner.Provision + nil, // 17: provisioner.Agent.EnvEntry + (*Parse_Request)(nil), // 18: provisioner.Parse.Request + (*Parse_Complete)(nil), // 19: provisioner.Parse.Complete + (*Parse_Response)(nil), // 20: provisioner.Parse.Response + (*Provision_Metadata)(nil), // 21: provisioner.Provision.Metadata + (*Provision_Start)(nil), // 22: provisioner.Provision.Start + (*Provision_Cancel)(nil), // 23: provisioner.Provision.Cancel + (*Provision_Request)(nil), // 24: provisioner.Provision.Request + (*Provision_Complete)(nil), // 25: provisioner.Provision.Complete + (*Provision_Response)(nil), // 26: provisioner.Provision.Response } var file_provisionersdk_proto_provisioner_proto_depIdxs = []int32{ 2, // 0: provisioner.ParameterSource.scheme:type_name -> provisioner.ParameterSource.Scheme @@ -1835,28 +1925,29 @@ var file_provisionersdk_proto_provisioner_proto_depIdxs = []int32{ 7, // 4: provisioner.ParameterSchema.default_destination:type_name -> provisioner.ParameterDestination 4, // 5: provisioner.ParameterSchema.validation_type_system:type_name -> provisioner.ParameterSchema.TypeSystem 0, // 6: provisioner.Log.level:type_name -> provisioner.LogLevel - 16, // 7: provisioner.Agent.env:type_name -> provisioner.Agent.EnvEntry - 12, // 8: provisioner.Resource.agents:type_name -> provisioner.Agent - 9, // 9: provisioner.Parse.Complete.parameter_schemas:type_name -> provisioner.ParameterSchema - 10, // 10: provisioner.Parse.Response.log:type_name -> provisioner.Log - 18, // 11: provisioner.Parse.Response.complete:type_name -> provisioner.Parse.Complete - 1, // 12: provisioner.Provision.Metadata.workspace_transition:type_name -> provisioner.WorkspaceTransition - 8, // 13: provisioner.Provision.Start.parameter_values:type_name -> provisioner.ParameterValue - 20, // 14: provisioner.Provision.Start.metadata:type_name -> provisioner.Provision.Metadata - 21, // 15: provisioner.Provision.Request.start:type_name -> provisioner.Provision.Start - 22, // 16: provisioner.Provision.Request.cancel:type_name -> provisioner.Provision.Cancel - 13, // 17: provisioner.Provision.Complete.resources:type_name -> provisioner.Resource - 10, // 18: provisioner.Provision.Response.log:type_name -> provisioner.Log - 24, // 19: provisioner.Provision.Response.complete:type_name -> provisioner.Provision.Complete - 17, // 20: provisioner.Provisioner.Parse:input_type -> provisioner.Parse.Request - 23, // 21: provisioner.Provisioner.Provision:input_type -> provisioner.Provision.Request - 19, // 22: provisioner.Provisioner.Parse:output_type -> provisioner.Parse.Response - 25, // 23: provisioner.Provisioner.Provision:output_type -> provisioner.Provision.Response - 22, // [22:24] is the sub-list for method output_type - 20, // [20:22] is the sub-list for method input_type - 20, // [20:20] is the sub-list for extension type_name - 20, // [20:20] is the sub-list for extension extendee - 0, // [0:20] is the sub-list for field type_name + 17, // 7: provisioner.Agent.env:type_name -> provisioner.Agent.EnvEntry + 13, // 8: provisioner.Agent.apps:type_name -> provisioner.App + 12, // 9: provisioner.Resource.agents:type_name -> provisioner.Agent + 9, // 10: provisioner.Parse.Complete.parameter_schemas:type_name -> provisioner.ParameterSchema + 10, // 11: provisioner.Parse.Response.log:type_name -> provisioner.Log + 19, // 12: provisioner.Parse.Response.complete:type_name -> provisioner.Parse.Complete + 1, // 13: provisioner.Provision.Metadata.workspace_transition:type_name -> provisioner.WorkspaceTransition + 8, // 14: provisioner.Provision.Start.parameter_values:type_name -> provisioner.ParameterValue + 21, // 15: provisioner.Provision.Start.metadata:type_name -> provisioner.Provision.Metadata + 22, // 16: provisioner.Provision.Request.start:type_name -> provisioner.Provision.Start + 23, // 17: provisioner.Provision.Request.cancel:type_name -> provisioner.Provision.Cancel + 14, // 18: provisioner.Provision.Complete.resources:type_name -> provisioner.Resource + 10, // 19: provisioner.Provision.Response.log:type_name -> provisioner.Log + 25, // 20: provisioner.Provision.Response.complete:type_name -> provisioner.Provision.Complete + 18, // 21: provisioner.Provisioner.Parse:input_type -> provisioner.Parse.Request + 24, // 22: provisioner.Provisioner.Provision:input_type -> provisioner.Provision.Request + 20, // 23: provisioner.Provisioner.Parse:output_type -> provisioner.Parse.Response + 26, // 24: provisioner.Provisioner.Provision:output_type -> provisioner.Provision.Response + 23, // [23:25] is the sub-list for method output_type + 21, // [21:23] is the sub-list for method input_type + 21, // [21:21] is the sub-list for extension type_name + 21, // [21:21] is the sub-list for extension extendee + 0, // [0:21] is the sub-list for field type_name } func init() { file_provisionersdk_proto_provisioner_proto_init() } @@ -1962,7 +2053,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Resource); i { + switch v := v.(*App); i { case 0: return &v.state case 1: @@ -1974,7 +2065,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Parse); i { + switch v := v.(*Resource); i { case 0: return &v.state case 1: @@ -1986,6 +2077,18 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Parse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_provisionersdk_proto_provisioner_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Provision); i { case 0: return &v.state @@ -1997,7 +2100,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { return nil } } - file_provisionersdk_proto_provisioner_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { + file_provisionersdk_proto_provisioner_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Parse_Request); i { case 0: return &v.state @@ -2009,7 +2112,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { return nil } } - file_provisionersdk_proto_provisioner_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { + file_provisionersdk_proto_provisioner_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Parse_Complete); i { case 0: return &v.state @@ -2021,7 +2124,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { return nil } } - file_provisionersdk_proto_provisioner_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { + file_provisionersdk_proto_provisioner_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Parse_Response); i { case 0: return &v.state @@ -2033,7 +2136,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { return nil } } - file_provisionersdk_proto_provisioner_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { + file_provisionersdk_proto_provisioner_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Provision_Metadata); i { case 0: return &v.state @@ -2045,7 +2148,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { return nil } } - file_provisionersdk_proto_provisioner_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { + file_provisionersdk_proto_provisioner_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Provision_Start); i { case 0: return &v.state @@ -2057,7 +2160,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { return nil } } - file_provisionersdk_proto_provisioner_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { + file_provisionersdk_proto_provisioner_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Provision_Cancel); i { case 0: return &v.state @@ -2069,7 +2172,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { return nil } } - file_provisionersdk_proto_provisioner_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { + file_provisionersdk_proto_provisioner_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Provision_Request); i { case 0: return &v.state @@ -2081,7 +2184,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { return nil } } - file_provisionersdk_proto_provisioner_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { + file_provisionersdk_proto_provisioner_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Provision_Complete); i { case 0: return &v.state @@ -2093,7 +2196,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { return nil } } - file_provisionersdk_proto_provisioner_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { + file_provisionersdk_proto_provisioner_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Provision_Response); i { case 0: return &v.state @@ -2110,15 +2213,15 @@ func file_provisionersdk_proto_provisioner_proto_init() { (*Agent_Token)(nil), (*Agent_InstanceId)(nil), } - file_provisionersdk_proto_provisioner_proto_msgTypes[14].OneofWrappers = []interface{}{ + file_provisionersdk_proto_provisioner_proto_msgTypes[15].OneofWrappers = []interface{}{ (*Parse_Response_Log)(nil), (*Parse_Response_Complete)(nil), } - file_provisionersdk_proto_provisioner_proto_msgTypes[18].OneofWrappers = []interface{}{ + file_provisionersdk_proto_provisioner_proto_msgTypes[19].OneofWrappers = []interface{}{ (*Provision_Request_Start)(nil), (*Provision_Request_Cancel)(nil), } - file_provisionersdk_proto_provisioner_proto_msgTypes[20].OneofWrappers = []interface{}{ + file_provisionersdk_proto_provisioner_proto_msgTypes[21].OneofWrappers = []interface{}{ (*Provision_Response_Log)(nil), (*Provision_Response_Complete)(nil), } @@ -2128,7 +2231,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_provisionersdk_proto_provisioner_proto_rawDesc, NumEnums: 5, - NumMessages: 21, + NumMessages: 22, NumExtensions: 0, NumServices: 1, }, diff --git a/provisionersdk/proto/provisioner.proto b/provisionersdk/proto/provisioner.proto index 66bfee737a00a..738875d66c7d8 100644 --- a/provisionersdk/proto/provisioner.proto +++ b/provisionersdk/proto/provisioner.proto @@ -80,12 +80,21 @@ message Agent { string operating_system = 5; string architecture = 6; string directory = 7; + repeated App apps = 8; oneof auth { - string token = 8; - string instance_id = 9; + string token = 9; + string instance_id = 10; } } +// App represents a dev-accessible application on the workspace. +message App { + string name = 1; + string command = 2; + string target = 3; + string icon = 4; +} + // Resource represents created infrastructure. message Resource { string name = 1; diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index ee4add928bb11..1cac7fa9e783f 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -338,6 +338,7 @@ export interface WorkspaceAgent { readonly operating_system: string readonly startup_script: string readonly directory: string + readonly apps: WorkspaceApp[] } // From codersdk/workspaceagents.go:47:6 @@ -345,7 +346,7 @@ export interface WorkspaceAgentAuthenticateResponse { readonly session_token: string } -// From codersdk/workspaceresources.go:59:6 +// From codersdk/workspaceresources.go:60:6 export interface WorkspaceAgentInstanceMetadata { readonly jail_orchestrator: string readonly operating_system: string @@ -358,7 +359,7 @@ export interface WorkspaceAgentInstanceMetadata { readonly vnc: boolean } -// From codersdk/workspaceresources.go:51:6 +// From codersdk/workspaceresources.go:52:6 export interface WorkspaceAgentResourceMetadata { readonly memory_total: number readonly disk_total: number @@ -367,6 +368,15 @@ export interface WorkspaceAgentResourceMetadata { readonly cpu_mhz: number } +// From codersdk/workspacedevurls.go:5:6 +export interface WorkspaceApp { + readonly id: string + readonly name: string + readonly command: string + readonly target: string + readonly icon: string +} + // From codersdk/workspacebuilds.go:18:6 export interface WorkspaceBuild { readonly id: string From e3ff8adbfebe20512cfd3cfe7a87e2e511afefff Mon Sep 17 00:00:00 2001 From: kylecarbs Date: Tue, 24 May 2022 15:35:11 +0000 Subject: [PATCH 02/26] Compare fields in apps test --- coderd/rbac/authz.go | 1 + coderd/workspaceresources_test.go | 16 ++++++++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/coderd/rbac/authz.go b/coderd/rbac/authz.go index 5e566496093b2..b9f9e1f825e9e 100644 --- a/coderd/rbac/authz.go +++ b/coderd/rbac/authz.go @@ -3,6 +3,7 @@ package rbac import ( "context" _ "embed" + "golang.org/x/xerrors" "github.com/open-policy-agent/opa/rego" diff --git a/coderd/workspaceresources_test.go b/coderd/workspaceresources_test.go index c579ab25eab80..1759a36a55e93 100644 --- a/coderd/workspaceresources_test.go +++ b/coderd/workspaceresources_test.go @@ -49,6 +49,12 @@ func TestWorkspaceResource(t *testing.T) { _, client, coderd := coderdtest.NewWithServer(t, nil) user := coderdtest.CreateFirstUser(t, client) coderdtest.NewProvisionerDaemon(t, coderd) + app := &proto.App{ + Name: "code-server", + Command: "some-command", + Target: "http://localhost:3000", + Icon: "/code.svg", + } version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, Provision: []*proto.Provision_Response{{ @@ -60,10 +66,7 @@ func TestWorkspaceResource(t *testing.T) { Agents: []*proto.Agent{{ Id: "something", Auth: &proto.Agent_Token{}, - Apps: []*proto.App{{ - Name: "code-server", - Command: "code-server", - }}, + Apps: []*proto.App{app}, }}, }}, }, @@ -81,5 +84,10 @@ func TestWorkspaceResource(t *testing.T) { require.Len(t, resource.Agents, 1) agent := resource.Agents[0] require.Len(t, agent.Apps, 1) + got := agent.Apps[0] + require.Equal(t, app.Command, got.Command) + require.Equal(t, app.Icon, got.Icon) + require.Equal(t, app.Name, got.Name) + require.Equal(t, app.Target, got.Target) }) } From b6e1ea6b39ee2c4d91096d66605f085419deea9d Mon Sep 17 00:00:00 2001 From: kylecarbs Date: Wed, 25 May 2022 14:27:53 +0000 Subject: [PATCH 03/26] Update Terraform provider to use relative path --- coderd/coderd.go | 9 +- coderd/database/databasefake/databasefake.go | 15 +- coderd/database/dump.sql | 3 +- ...own.sql => 000014_workspace_apps.down.sql} | 0 ...ps.up.sql => 000014_workspace_apps.up.sql} | 5 +- coderd/database/models.go | 15 +- coderd/database/queries.sql.go | 36 ++- coderd/database/queries/workspaceapps.sql | 5 +- coderd/provisionerdaemons.go | 7 +- coderd/workspaceagents.go | 10 +- coderd/workspaceresources_test.go | 4 +- codersdk/workspaceapps.go | 15 +- provisioner/terraform/provision.go | 20 +- provisioner/terraform/provision_test.go | 2 +- provisionersdk/proto/provisioner.pb.go | 258 +++++++++--------- provisionersdk/proto/provisioner.proto | 3 +- 16 files changed, 218 insertions(+), 189 deletions(-) rename coderd/database/migrations/{000011_workspace_apps.down.sql => 000014_workspace_apps.down.sql} (100%) rename coderd/database/migrations/{000011_workspace_apps.up.sql => 000014_workspace_apps.up.sql} (77%) diff --git a/coderd/coderd.go b/coderd/coderd.go index cde85eec1c978..e10cd62828d0b 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -34,10 +34,11 @@ import ( // Options are requires parameters for Coder to start. type Options struct { - AccessURL *url.URL - Logger slog.Logger - Database database.Store - Pubsub database.Pubsub + AccessURL *url.URL + WildcardURL *url.URL + Logger slog.Logger + Database database.Store + Pubsub database.Pubsub AgentConnectionUpdateFrequency time.Duration // APIRateLimit is the minutely throughput rate limit per user or ip. diff --git a/coderd/database/databasefake/databasefake.go b/coderd/database/databasefake/databasefake.go index d8d3302a110c1..8ebbb9eaf62f0 100644 --- a/coderd/database/databasefake/databasefake.go +++ b/coderd/database/databasefake/databasefake.go @@ -1533,13 +1533,14 @@ func (q *fakeQuerier) InsertWorkspaceApp(_ context.Context, arg database.InsertW defer q.mutex.Unlock() workspaceApp := database.WorkspaceApp{ - ID: arg.ID, - AgentID: arg.AgentID, - CreatedAt: arg.CreatedAt, - Name: arg.Name, - Icon: arg.Icon, - Command: arg.Command, - Target: arg.Target, + ID: arg.ID, + AgentID: arg.AgentID, + CreatedAt: arg.CreatedAt, + Name: arg.Name, + Icon: arg.Icon, + Command: arg.Command, + Url: arg.Url, + RelativePath: arg.RelativePath, } q.workspaceApps = append(q.workspaceApps, workspaceApp) return workspaceApp, nil diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index c53f66e8f4fe3..4bf028644535f 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -288,7 +288,8 @@ CREATE TABLE workspace_apps ( name character varying(64) NOT NULL, icon character varying(256) NOT NULL, command character varying(65534), - target character varying(65534) + url character varying(65534), + relative_path boolean DEFAULT false NOT NULL ); CREATE TABLE workspace_builds ( diff --git a/coderd/database/migrations/000011_workspace_apps.down.sql b/coderd/database/migrations/000014_workspace_apps.down.sql similarity index 100% rename from coderd/database/migrations/000011_workspace_apps.down.sql rename to coderd/database/migrations/000014_workspace_apps.down.sql diff --git a/coderd/database/migrations/000011_workspace_apps.up.sql b/coderd/database/migrations/000014_workspace_apps.up.sql similarity index 77% rename from coderd/database/migrations/000011_workspace_apps.up.sql rename to coderd/database/migrations/000014_workspace_apps.up.sql index bea1278a2c5fa..7a991c179ee13 100644 --- a/coderd/database/migrations/000011_workspace_apps.up.sql +++ b/coderd/database/migrations/000014_workspace_apps.up.sql @@ -4,10 +4,9 @@ CREATE TABLE workspace_apps ( agent_id uuid NOT NULL REFERENCES workspace_agents (id) ON DELETE CASCADE, name varchar(64) NOT NULL, icon varchar(256) NOT NULL, - -- A command to run when opened. command varchar(65534), - -- A URL or port to target. - target varchar(65534), + url varchar(65534), + relative_path boolean NOT NULL DEFAULT false, PRIMARY KEY (id), UNIQUE(agent_id, name) ); diff --git a/coderd/database/models.go b/coderd/database/models.go index ec5a815e73ce9..4253103011acf 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -495,13 +495,14 @@ type WorkspaceAgent struct { } type WorkspaceApp struct { - ID uuid.UUID `db:"id" json:"id"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - AgentID uuid.UUID `db:"agent_id" json:"agent_id"` - Name string `db:"name" json:"name"` - Icon string `db:"icon" json:"icon"` - Command sql.NullString `db:"command" json:"command"` - Target sql.NullString `db:"target" json:"target"` + ID uuid.UUID `db:"id" json:"id"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + AgentID uuid.UUID `db:"agent_id" json:"agent_id"` + Name string `db:"name" json:"name"` + Icon string `db:"icon" json:"icon"` + Command sql.NullString `db:"command" json:"command"` + Url sql.NullString `db:"url" json:"url"` + RelativePath bool `db:"relative_path" json:"relative_path"` } type WorkspaceBuild struct { diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 207fd39e2cd62..26d6a3f78ffc6 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -2750,7 +2750,7 @@ func (q *sqlQuerier) UpdateWorkspaceAgentConnectionByID(ctx context.Context, arg } const getWorkspaceAppsByAgentID = `-- name: GetWorkspaceAppsByAgentID :many -SELECT id, created_at, agent_id, name, icon, command, target FROM workspace_apps WHERE agent_id = $1 +SELECT id, created_at, agent_id, name, icon, command, url, relative_path FROM workspace_apps WHERE agent_id = $1 ` func (q *sqlQuerier) GetWorkspaceAppsByAgentID(ctx context.Context, agentID uuid.UUID) ([]WorkspaceApp, error) { @@ -2769,7 +2769,8 @@ func (q *sqlQuerier) GetWorkspaceAppsByAgentID(ctx context.Context, agentID uuid &i.Name, &i.Icon, &i.Command, - &i.Target, + &i.Url, + &i.RelativePath, ); err != nil { return nil, err } @@ -2785,7 +2786,7 @@ func (q *sqlQuerier) GetWorkspaceAppsByAgentID(ctx context.Context, agentID uuid } const getWorkspaceAppsByAgentIDs = `-- name: GetWorkspaceAppsByAgentIDs :many -SELECT id, created_at, agent_id, name, icon, command, target FROM workspace_apps WHERE agent_id = ANY($1 :: uuid [ ]) +SELECT id, created_at, agent_id, name, icon, command, url, relative_path FROM workspace_apps WHERE agent_id = ANY($1 :: uuid [ ]) ` func (q *sqlQuerier) GetWorkspaceAppsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceApp, error) { @@ -2804,7 +2805,8 @@ func (q *sqlQuerier) GetWorkspaceAppsByAgentIDs(ctx context.Context, ids []uuid. &i.Name, &i.Icon, &i.Command, - &i.Target, + &i.Url, + &i.RelativePath, ); err != nil { return nil, err } @@ -2828,20 +2830,22 @@ INSERT INTO name, icon, command, - target + url, + relative_path ) VALUES - ($1, $2, $3, $4, $5, $6, $7) RETURNING id, created_at, agent_id, name, icon, command, target + ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id, created_at, agent_id, name, icon, command, url, relative_path ` type InsertWorkspaceAppParams struct { - ID uuid.UUID `db:"id" json:"id"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - AgentID uuid.UUID `db:"agent_id" json:"agent_id"` - Name string `db:"name" json:"name"` - Icon string `db:"icon" json:"icon"` - Command sql.NullString `db:"command" json:"command"` - Target sql.NullString `db:"target" json:"target"` + ID uuid.UUID `db:"id" json:"id"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + AgentID uuid.UUID `db:"agent_id" json:"agent_id"` + Name string `db:"name" json:"name"` + Icon string `db:"icon" json:"icon"` + Command sql.NullString `db:"command" json:"command"` + Url sql.NullString `db:"url" json:"url"` + RelativePath bool `db:"relative_path" json:"relative_path"` } func (q *sqlQuerier) InsertWorkspaceApp(ctx context.Context, arg InsertWorkspaceAppParams) (WorkspaceApp, error) { @@ -2852,7 +2856,8 @@ func (q *sqlQuerier) InsertWorkspaceApp(ctx context.Context, arg InsertWorkspace arg.Name, arg.Icon, arg.Command, - arg.Target, + arg.Url, + arg.RelativePath, ) var i WorkspaceApp err := row.Scan( @@ -2862,7 +2867,8 @@ func (q *sqlQuerier) InsertWorkspaceApp(ctx context.Context, arg InsertWorkspace &i.Name, &i.Icon, &i.Command, - &i.Target, + &i.Url, + &i.RelativePath, ) return i, err } diff --git a/coderd/database/queries/workspaceapps.sql b/coderd/database/queries/workspaceapps.sql index 1364fd3b59c93..4b63ba1be5580 100644 --- a/coderd/database/queries/workspaceapps.sql +++ b/coderd/database/queries/workspaceapps.sql @@ -13,7 +13,8 @@ INSERT INTO name, icon, command, - target + url, + relative_path ) VALUES - ($1, $2, $3, $4, $5, $6, $7) RETURNING *; + ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *; diff --git a/coderd/provisionerdaemons.go b/coderd/provisionerdaemons.go index aac427599d30a..891c7bc51c036 100644 --- a/coderd/provisionerdaemons.go +++ b/coderd/provisionerdaemons.go @@ -665,10 +665,11 @@ func insertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid. String: app.Command, Valid: app.Command != "", }, - Target: sql.NullString{ - String: app.Target, - Valid: app.Target != "", + Url: sql.NullString{ + String: app.Url, + Valid: app.Url != "", }, + RelativePath: app.RelativePath, }) if err != nil { return xerrors.Errorf("insert app: %w", err) diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index 875d5a7314c22..765714b6f510d 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -462,11 +462,11 @@ func convertApps(dbApps []database.WorkspaceApp) []codersdk.WorkspaceApp { apps := make([]codersdk.WorkspaceApp, 0) for _, dbApp := range dbApps { apps = append(apps, codersdk.WorkspaceApp{ - ID: dbApp.ID, - Name: dbApp.Name, - Command: dbApp.Command.String, - Target: dbApp.Target.String, - Icon: dbApp.Icon, + ID: dbApp.ID, + Name: dbApp.Name, + Command: dbApp.Command.String, + AccessURL: dbApp.Url.String, + Icon: dbApp.Icon, }) } return apps diff --git a/coderd/workspaceresources_test.go b/coderd/workspaceresources_test.go index 1759a36a55e93..f448c99e51fbd 100644 --- a/coderd/workspaceresources_test.go +++ b/coderd/workspaceresources_test.go @@ -52,7 +52,7 @@ func TestWorkspaceResource(t *testing.T) { app := &proto.App{ Name: "code-server", Command: "some-command", - Target: "http://localhost:3000", + Url: "http://localhost:3000", Icon: "/code.svg", } version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ @@ -88,6 +88,6 @@ func TestWorkspaceResource(t *testing.T) { require.Equal(t, app.Command, got.Command) require.Equal(t, app.Icon, got.Icon) require.Equal(t, app.Name, got.Name) - require.Equal(t, app.Target, got.Target) + require.Equal(t, app.Url, got.AccessURL) }) } diff --git a/codersdk/workspaceapps.go b/codersdk/workspaceapps.go index faee514dba349..ca46e83a33e64 100644 --- a/codersdk/workspaceapps.go +++ b/codersdk/workspaceapps.go @@ -3,9 +3,14 @@ package codersdk import "github.com/google/uuid" type WorkspaceApp struct { - ID uuid.UUID `json:"id"` - Name string `json:"name"` - Command string `json:"command,omitempty"` - Target string `json:"target,omitempty"` - Icon string `json:"icon"` + ID uuid.UUID `json:"id"` + // Name is a unique identifier attached to an agent. + Name string `json:"name"` + Command string `json:"command,omitempty"` + // AccessURL is an address used to access the application. + // If command is specified, this will be omitted. + AccessURL string `json:"access_url,omitempty"` + // Icon is a relative path or external URL that specifies + // an icon to be displayed in the dashboard. + Icon string `json:"icon"` } diff --git a/provisioner/terraform/provision.go b/provisioner/terraform/provision.go index 3839e1f1f9e79..b08b46725eea0 100644 --- a/provisioner/terraform/provision.go +++ b/provisioner/terraform/provision.go @@ -523,11 +523,12 @@ func parseTerraformApply(ctx context.Context, terraform *tfexec.Terraform, state } type appAttributes struct { - AgentID string `mapstructure:"agent_id"` - Name string `mapstructure:"name"` - Icon string `mapstructure:"icon"` - Target string `mapstructure:"target"` - Command string `mapstructure:"command"` + AgentID string `mapstructure:"agent_id"` + Name string `mapstructure:"name"` + Icon string `mapstructure:"icon"` + URL string `mapstructure:"url"` + Command string `mapstructure:"command"` + RelativePath bool `mapstructure:"relative_path"` } // Associate Apps with agents. for _, resource := range state.Values.RootModule.Resources { @@ -548,10 +549,11 @@ func parseTerraformApply(ctx context.Context, terraform *tfexec.Terraform, state continue } agent.Apps = append(agent.Apps, &proto.App{ - Name: attrs.Name, - Command: attrs.Command, - Target: attrs.Target, - Icon: attrs.Icon, + Name: attrs.Name, + Command: attrs.Command, + Url: attrs.URL, + Icon: attrs.Icon, + RelativePath: attrs.RelativePath, }) } } diff --git a/provisioner/terraform/provision_test.go b/provisioner/terraform/provision_test.go index c32b37cfee17f..77f1e8023c63b 100644 --- a/provisioner/terraform/provision_test.go +++ b/provisioner/terraform/provision_test.go @@ -30,7 +30,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.4.0" + version = "0.4.2" } } } diff --git a/provisionersdk/proto/provisioner.pb.go b/provisionersdk/proto/provisioner.pb.go index 2dc5c9d16f60c..eee976e770aa9 100644 --- a/provisionersdk/proto/provisioner.pb.go +++ b/provisionersdk/proto/provisioner.pb.go @@ -711,7 +711,7 @@ type Agent struct { OperatingSystem string `protobuf:"bytes,5,opt,name=operating_system,json=operatingSystem,proto3" json:"operating_system,omitempty"` Architecture string `protobuf:"bytes,6,opt,name=architecture,proto3" json:"architecture,omitempty"` Directory string `protobuf:"bytes,7,opt,name=directory,proto3" json:"directory,omitempty"` - Apps []*App `protobuf:"bytes,8,rep,name=apps,proto3" json:"apps,omitempty"` + Apps []*App `protobuf:"bytes,8,rep,name=apps,proto3" json:"apps,omitempty"` // Types that are assignable to Auth: // *Agent_Token // *Agent_InstanceId @@ -849,10 +849,11 @@ type App struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - Command string `protobuf:"bytes,2,opt,name=command,proto3" json:"command,omitempty"` - Target string `protobuf:"bytes,3,opt,name=target,proto3" json:"target,omitempty"` - Icon string `protobuf:"bytes,4,opt,name=icon,proto3" json:"icon,omitempty"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Command string `protobuf:"bytes,2,opt,name=command,proto3" json:"command,omitempty"` + Url string `protobuf:"bytes,3,opt,name=url,proto3" json:"url,omitempty"` + Icon string `protobuf:"bytes,4,opt,name=icon,proto3" json:"icon,omitempty"` + RelativePath bool `protobuf:"varint,5,opt,name=relative_path,json=relativePath,proto3" json:"relative_path,omitempty"` } func (x *App) Reset() { @@ -901,9 +902,9 @@ func (x *App) GetCommand() string { return "" } -func (x *App) GetTarget() string { +func (x *App) GetUrl() string { if x != nil { - return x.Target + return x.Url } return "" } @@ -915,6 +916,13 @@ func (x *App) GetIcon() string { return "" } +func (x *App) GetRelativePath() bool { + if x != nil { + return x.RelativePath + } + return false +} + // Resource represents created infrastructure. type Resource struct { state protoimpl.MessageState @@ -1740,7 +1748,7 @@ var file_provisionersdk_proto_provisioner_proto_rawDesc = []byte{ 0x70, 0x75, 0x74, 0x22, 0x37, 0x0a, 0x14, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x41, 0x75, 0x74, 0x68, 0x12, 0x1f, 0x0a, 0x0b, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0a, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x64, 0x22, 0x98, 0x03, 0x0a, + 0x52, 0x0a, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x64, 0x22, 0x8f, 0x03, 0x0a, 0x05, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2d, 0x0a, 0x03, 0x65, 0x6e, @@ -1755,123 +1763,125 @@ var file_provisionersdk_proto_provisioner_proto_rawDesc = []byte{ 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x61, 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x07, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x2d, 0x0a, - 0x07, 0x64, 0x65, 0x76, 0x75, 0x72, 0x6c, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, - 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x44, 0x65, 0x76, - 0x75, 0x72, 0x6c, 0x52, 0x07, 0x64, 0x65, 0x76, 0x75, 0x72, 0x6c, 0x73, 0x12, 0x16, 0x0a, 0x05, - 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x05, 0x74, - 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x21, 0x0a, 0x0b, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, - 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0a, 0x69, 0x6e, 0x73, - 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x64, 0x1a, 0x36, 0x0a, 0x08, 0x45, 0x6e, 0x76, 0x45, 0x6e, - 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, - 0x06, 0x0a, 0x04, 0x61, 0x75, 0x74, 0x68, 0x22, 0x62, 0x0a, 0x06, 0x44, 0x65, 0x76, 0x75, 0x72, - 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, - 0x16, 0x0a, 0x06, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x06, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x69, 0x63, 0x6f, 0x6e, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x69, 0x63, 0x6f, 0x6e, 0x22, 0x5e, 0x0a, 0x08, 0x52, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, - 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, - 0x2a, 0x0a, 0x06, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x12, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x41, 0x67, - 0x65, 0x6e, 0x74, 0x52, 0x06, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x73, 0x22, 0xfc, 0x01, 0x0a, 0x05, - 0x50, 0x61, 0x72, 0x73, 0x65, 0x1a, 0x27, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x1c, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x1a, 0x55, - 0x0a, 0x08, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x49, 0x0a, 0x11, 0x70, 0x61, - 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x5f, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x73, 0x18, - 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, - 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x53, 0x63, 0x68, - 0x65, 0x6d, 0x61, 0x52, 0x10, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x53, 0x63, - 0x68, 0x65, 0x6d, 0x61, 0x73, 0x1a, 0x73, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x24, 0x0a, 0x03, 0x6c, 0x6f, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, - 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4c, 0x6f, 0x67, - 0x48, 0x00, 0x52, 0x03, 0x6c, 0x6f, 0x67, 0x12, 0x39, 0x0a, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6c, - 0x65, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x76, - 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x2e, 0x43, 0x6f, + 0x28, 0x09, 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x24, 0x0a, + 0x04, 0x61, 0x70, 0x70, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x72, + 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x41, 0x70, 0x70, 0x52, 0x04, 0x61, + 0x70, 0x70, 0x73, 0x12, 0x16, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x09, 0x20, 0x01, + 0x28, 0x09, 0x48, 0x00, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x21, 0x0a, 0x0b, 0x69, + 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, + 0x48, 0x00, 0x52, 0x0a, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x64, 0x1a, 0x36, + 0x0a, 0x08, 0x45, 0x6e, 0x76, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, + 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x06, 0x0a, 0x04, 0x61, 0x75, 0x74, 0x68, 0x22, 0x7e, + 0x0a, 0x03, 0x41, 0x70, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6d, + 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, + 0x61, 0x6e, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x69, 0x63, 0x6f, 0x6e, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x69, 0x63, 0x6f, 0x6e, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x6c, + 0x61, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x0c, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x76, 0x65, 0x50, 0x61, 0x74, 0x68, 0x22, 0x5e, + 0x0a, 0x08, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, + 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, + 0x70, 0x65, 0x12, 0x2a, 0x0a, 0x06, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x03, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, + 0x2e, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x52, 0x06, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x73, 0x22, 0xfc, + 0x01, 0x0a, 0x05, 0x50, 0x61, 0x72, 0x73, 0x65, 0x1a, 0x27, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, + 0x79, 0x1a, 0x55, 0x0a, 0x08, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x49, 0x0a, + 0x11, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x5f, 0x73, 0x63, 0x68, 0x65, 0x6d, + 0x61, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, + 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, + 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x52, 0x10, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, + 0x72, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x73, 0x1a, 0x73, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x03, 0x6c, 0x6f, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, + 0x4c, 0x6f, 0x67, 0x48, 0x00, 0x52, 0x03, 0x6c, 0x6f, 0x67, 0x12, 0x39, 0x0a, 0x08, 0x63, 0x6f, + 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, + 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, + 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x48, 0x00, 0x52, 0x08, 0x63, 0x6f, 0x6d, + 0x70, 0x6c, 0x65, 0x74, 0x65, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0xfa, 0x06, + 0x0a, 0x09, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x1a, 0x9d, 0x02, 0x0a, 0x08, + 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6f, 0x64, 0x65, + 0x72, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6f, 0x64, + 0x65, 0x72, 0x55, 0x72, 0x6c, 0x12, 0x53, 0x0a, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0e, 0x32, 0x20, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, + 0x72, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, + 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x13, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, + 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x25, 0x0a, 0x0e, 0x77, 0x6f, + 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0d, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, + 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, + 0x77, 0x6e, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x77, 0x6f, 0x72, 0x6b, + 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x12, 0x21, 0x0a, 0x0c, 0x77, 0x6f, + 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0b, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, 0x2c, 0x0a, + 0x12, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, + 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x77, 0x6f, 0x72, 0x6b, 0x73, + 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x49, 0x64, 0x1a, 0xd9, 0x01, 0x0a, 0x05, + 0x53, 0x74, 0x61, 0x72, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, + 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, + 0x6f, 0x72, 0x79, 0x12, 0x46, 0x0a, 0x10, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, + 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, + 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x61, + 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0f, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x3b, 0x0a, 0x08, 0x6d, + 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, + 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, + 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, + 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, + 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x17, + 0x0a, 0x07, 0x64, 0x72, 0x79, 0x5f, 0x72, 0x75, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x06, 0x64, 0x72, 0x79, 0x52, 0x75, 0x6e, 0x1a, 0x08, 0x0a, 0x06, 0x43, 0x61, 0x6e, 0x63, 0x65, + 0x6c, 0x1a, 0x80, 0x01, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x34, 0x0a, + 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, + 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, + 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x48, 0x00, 0x52, 0x05, 0x73, 0x74, + 0x61, 0x72, 0x74, 0x12, 0x37, 0x0a, 0x06, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, + 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x43, 0x61, 0x6e, 0x63, + 0x65, 0x6c, 0x48, 0x00, 0x52, 0x06, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x42, 0x06, 0x0a, 0x04, + 0x74, 0x79, 0x70, 0x65, 0x1a, 0x6b, 0x0a, 0x08, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, + 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, + 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x33, 0x0a, 0x09, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x73, 0x1a, 0x77, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, + 0x03, 0x6c, 0x6f, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x72, 0x6f, + 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4c, 0x6f, 0x67, 0x48, 0x00, 0x52, 0x03, + 0x6c, 0x6f, 0x67, 0x12, 0x3d, 0x0a, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, + 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x48, 0x00, 0x52, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, - 0x74, 0x65, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0xfa, 0x06, 0x0a, 0x09, 0x50, - 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x1a, 0x9d, 0x02, 0x0a, 0x08, 0x4d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x5f, 0x75, - 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x55, - 0x72, 0x6c, 0x12, 0x53, 0x0a, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, - 0x74, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, - 0x32, 0x20, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x57, + 0x74, 0x65, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x2a, 0x3f, 0x0a, 0x08, 0x4c, 0x6f, + 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x52, 0x41, 0x43, 0x45, 0x10, + 0x00, 0x12, 0x09, 0x0a, 0x05, 0x44, 0x45, 0x42, 0x55, 0x47, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, + 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x57, 0x41, 0x52, 0x4e, 0x10, 0x03, + 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x04, 0x2a, 0x37, 0x0a, 0x13, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, - 0x6f, 0x6e, 0x52, 0x13, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, - 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x25, 0x0a, 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, - 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0d, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x27, - 0x0a, 0x0f, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, - 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, - 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x12, 0x21, 0x0a, 0x0c, 0x77, 0x6f, 0x72, 0x6b, 0x73, - 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x77, - 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, 0x2c, 0x0a, 0x12, 0x77, 0x6f, - 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x69, 0x64, - 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, - 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x49, 0x64, 0x1a, 0xd9, 0x01, 0x0a, 0x05, 0x53, 0x74, 0x61, - 0x72, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, - 0x12, 0x46, 0x0a, 0x10, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x5f, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x72, 0x6f, - 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, - 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, - 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x3b, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, - 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x72, 0x6f, - 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, - 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x64, - 0x72, 0x79, 0x5f, 0x72, 0x75, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x64, 0x72, - 0x79, 0x52, 0x75, 0x6e, 0x1a, 0x08, 0x0a, 0x06, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x1a, 0x80, - 0x01, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x34, 0x0a, 0x05, 0x73, 0x74, - 0x61, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x76, - 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, - 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x48, 0x00, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, - 0x12, 0x37, 0x0a, 0x06, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, - 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x48, - 0x00, 0x52, 0x06, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, - 0x65, 0x1a, 0x6b, 0x0a, 0x08, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x14, 0x0a, - 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, - 0x61, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x33, 0x0a, 0x09, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, - 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x1a, 0x77, - 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x03, 0x6c, 0x6f, - 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, - 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4c, 0x6f, 0x67, 0x48, 0x00, 0x52, 0x03, 0x6c, 0x6f, 0x67, - 0x12, 0x3d, 0x0a, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, - 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, - 0x65, 0x74, 0x65, 0x48, 0x00, 0x52, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x42, - 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x2a, 0x3f, 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x4c, 0x65, - 0x76, 0x65, 0x6c, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x52, 0x41, 0x43, 0x45, 0x10, 0x00, 0x12, 0x09, - 0x0a, 0x05, 0x44, 0x45, 0x42, 0x55, 0x47, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, - 0x4f, 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x57, 0x41, 0x52, 0x4e, 0x10, 0x03, 0x12, 0x09, 0x0a, - 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x04, 0x2a, 0x37, 0x0a, 0x13, 0x57, 0x6f, 0x72, 0x6b, - 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, - 0x09, 0x0a, 0x05, 0x53, 0x54, 0x41, 0x52, 0x54, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x53, 0x54, - 0x4f, 0x50, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x53, 0x54, 0x52, 0x4f, 0x59, 0x10, - 0x02, 0x32, 0xa3, 0x01, 0x0a, 0x0b, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, - 0x72, 0x12, 0x42, 0x0a, 0x05, 0x50, 0x61, 0x72, 0x73, 0x65, 0x12, 0x1a, 0x2e, 0x70, 0x72, 0x6f, - 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x2e, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, - 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x50, 0x0a, 0x09, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, - 0x6f, 0x6e, 0x12, 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, - 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, - 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x28, 0x01, 0x30, 0x01, 0x42, 0x2d, 0x5a, 0x2b, 0x67, 0x69, 0x74, 0x68, 0x75, - 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, - 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x73, 0x64, 0x6b, - 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x6f, 0x6e, 0x12, 0x09, 0x0a, 0x05, 0x53, 0x54, 0x41, 0x52, 0x54, 0x10, 0x00, 0x12, 0x08, 0x0a, + 0x04, 0x53, 0x54, 0x4f, 0x50, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x53, 0x54, 0x52, + 0x4f, 0x59, 0x10, 0x02, 0x32, 0xa3, 0x01, 0x0a, 0x0b, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, + 0x6f, 0x6e, 0x65, 0x72, 0x12, 0x42, 0x0a, 0x05, 0x50, 0x61, 0x72, 0x73, 0x65, 0x12, 0x1a, 0x2e, + 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, + 0x65, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x76, + 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x2e, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x50, 0x0a, 0x09, 0x50, 0x72, 0x6f, 0x76, + 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, + 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, + 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x30, 0x01, 0x42, 0x2d, 0x5a, 0x2b, 0x67, 0x69, + 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, + 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, + 0x73, 0x64, 0x6b, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x33, } var ( @@ -1902,7 +1912,7 @@ var file_provisionersdk_proto_provisioner_proto_goTypes = []interface{}{ (*Log)(nil), // 10: provisioner.Log (*InstanceIdentityAuth)(nil), // 11: provisioner.InstanceIdentityAuth (*Agent)(nil), // 12: provisioner.Agent - (*App)(nil), // 13: provisioner.App + (*App)(nil), // 13: provisioner.App (*Resource)(nil), // 14: provisioner.Resource (*Parse)(nil), // 15: provisioner.Parse (*Provision)(nil), // 16: provisioner.Provision diff --git a/provisionersdk/proto/provisioner.proto b/provisionersdk/proto/provisioner.proto index 738875d66c7d8..86e61ca8d9884 100644 --- a/provisionersdk/proto/provisioner.proto +++ b/provisionersdk/proto/provisioner.proto @@ -91,8 +91,9 @@ message Agent { message App { string name = 1; string command = 2; - string target = 3; + string url = 3; string icon = 4; + bool relative_path = 5; } // Resource represents created infrastructure. From 430cfe73f610b29011416569e936452f4a460d0d Mon Sep 17 00:00:00 2001 From: kylecarbs Date: Thu, 26 May 2022 02:14:40 +0000 Subject: [PATCH 04/26] Add some basic structure for routing --- coderd/coderd.go | 14 +++++++++++++ coderd/httpmw/apikey.go | 2 +- coderd/httpmw/wildcard.go | 24 +++++++++++++++++++++ coderd/httpmw/wildcard_test.go | 38 ++++++++++++++++++++++++++++++++++ coderd/workspaceapps.go | 9 ++++++++ coderd/workspaceapps_test.go | 1 + go.mod | 2 ++ go.sum | 2 ++ 8 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 coderd/httpmw/wildcard.go create mode 100644 coderd/httpmw/wildcard_test.go create mode 100644 coderd/workspaceapps.go create mode 100644 coderd/workspaceapps_test.go diff --git a/coderd/coderd.go b/coderd/coderd.go index e10cd62828d0b..b97cbc2965127 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -15,6 +15,8 @@ import ( "golang.org/x/xerrors" "google.golang.org/api/idtoken" + "github.com/go-chi/cors" + sdktrace "go.opentelemetry.io/otel/sdk/trace" "cdr.dev/slog" @@ -107,6 +109,15 @@ func newRouter(options *Options, a *api) chi.Router { tracing.HTTPMW(a.TracerProvider, "coderd.http"), ) + r.Route("/{user}/{workspaceagent}/{application}", func(r chi.Router) { + r.Use( + httpmw.RateLimitPerMinute(options.APIRateLimit), + apiKeyMiddleware, + httpmw.ExtractUserParam(a.Database), + authRolesMiddleware, + ) + }) + r.Route("/api/v2", func(r chi.Router) { r.NotFound(func(rw http.ResponseWriter, r *http.Request) { httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ @@ -320,6 +331,9 @@ func newRouter(options *Options, a *api) chi.Router { r.Get("/watch", a.watchWorkspace) }) }) + r.Route("/wildcardauth", func(r chi.Router) { + r.Use(cors.Handler(cors.Options{})) + }) r.Route("/workspacebuilds/{workspacebuild}", func(r chi.Router) { r.Use( apiKeyMiddleware, diff --git a/coderd/httpmw/apikey.go b/coderd/httpmw/apikey.go index abf55128d55d7..77a4b00af01be 100644 --- a/coderd/httpmw/apikey.go +++ b/coderd/httpmw/apikey.go @@ -17,7 +17,7 @@ import ( "github.com/coder/coder/coderd/httpapi" ) -// SessionTokenKey represents the name of the cookie or query paramater the API key is stored in. +// SessionTokenKey represents the name of the cookie or query parameter the API key is stored in. const SessionTokenKey = "session_token" type apiKeyContextKey struct{} diff --git a/coderd/httpmw/wildcard.go b/coderd/httpmw/wildcard.go new file mode 100644 index 0000000000000..c2cc5be1f451c --- /dev/null +++ b/coderd/httpmw/wildcard.go @@ -0,0 +1,24 @@ +package httpmw + +import ( + "net/http" + "strings" +) + +// Wildcard routes to the provided handler if the request host has the suffix of hostname. +func Wildcard(hostname string, handler http.HandlerFunc) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var ( + ctx = r.Context() + ) + + if !strings.HasSuffix(r.Host, hostname) { + next.ServeHTTP(w, r) + return + } + + handler(w, r.WithContext(ctx)) + }) + } +} diff --git a/coderd/httpmw/wildcard_test.go b/coderd/httpmw/wildcard_test.go new file mode 100644 index 0000000000000..ba0a7ebd607ca --- /dev/null +++ b/coderd/httpmw/wildcard_test.go @@ -0,0 +1,38 @@ +package httpmw_test + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/coder/coder/coderd/httpmw" +) + +func TestWildcard(t *testing.T) { + t.Parallel() + t.Run("Match", func(t *testing.T) { + t.Parallel() + req := httptest.NewRequest("GET", "http://frogs.bananas.org", nil) + res := httptest.NewRecorder() + httpmw.Wildcard("bananas.org", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusForbidden) + })).ServeHTTP(res, req) + require.Equal(t, http.StatusOK, res.Result().StatusCode) + }) + + t.Run("Passthrough", func(t *testing.T) { + t.Parallel() + req := httptest.NewRequest("GET", "http://frogs.apples.org", nil) + res := httptest.NewRecorder() + httpmw.Wildcard("bananas.org", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusForbidden) + })).ServeHTTP(res, req) + require.Equal(t, http.StatusForbidden, res.Result().StatusCode) + }) +} diff --git a/coderd/workspaceapps.go b/coderd/workspaceapps.go new file mode 100644 index 0000000000000..280710a2123de --- /dev/null +++ b/coderd/workspaceapps.go @@ -0,0 +1,9 @@ +package coderd + +import ( + "net/http" +) + +func (api *api) proxyPath(rw http.ResponseWriter, r *http.Request) { + +} diff --git a/coderd/workspaceapps_test.go b/coderd/workspaceapps_test.go new file mode 100644 index 0000000000000..a1bbc0bfabd3f --- /dev/null +++ b/coderd/workspaceapps_test.go @@ -0,0 +1 @@ +package coderd_test diff --git a/go.mod b/go.mod index 1271d4bdc0e0b..38ea6b634112d 100644 --- a/go.mod +++ b/go.mod @@ -126,6 +126,8 @@ require ( storj.io/drpc v0.0.30 ) +require github.com/go-chi/cors v1.2.1 + require ( github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c // indirect diff --git a/go.sum b/go.sum index 5c8ba9c18ffb4..d21ccb4729924 100644 --- a/go.sum +++ b/go.sum @@ -571,6 +571,8 @@ github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs= github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg= github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8= github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= +github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= github.com/go-chi/httprate v0.5.3 h1:5HPWb0N6ymIiuotMtCfOGpQKiKeqXVzMexHh1W1yXPc= github.com/go-chi/httprate v0.5.3/go.mod h1:kYR4lorHX3It9tTh4eTdHhcF2bzrYnCrRNlv5+IBm2M= github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8= From 6ef781cca36030834c64d94a0bc8af0692625113 Mon Sep 17 00:00:00 2001 From: kylecarbs Date: Thu, 26 May 2022 02:29:06 +0000 Subject: [PATCH 05/26] chore: Remove interface from coderd and lift API surface Abstracting coderd into an interface added misdirection because the interface was never intended to be fulfilled outside of a single implementation. This lifts the abstraction, and attaches all handlers to a root struct named `*coderd.API`. --- cli/server.go | 10 +- coderd/authorize.go | 4 +- coderd/coderd.go | 227 +++++++++++++++----------------- coderd/coderdtest/coderdtest.go | 8 +- coderd/files.go | 4 +- coderd/gitsshkey.go | 6 +- coderd/members.go | 4 +- coderd/organizations.go | 2 +- coderd/parameters.go | 6 +- coderd/provisionerdaemons.go | 18 +-- coderd/provisionerjobs.go | 4 +- coderd/roles.go | 6 +- coderd/templates.go | 10 +- coderd/templateversions.go | 20 +-- coderd/userauth.go | 4 +- coderd/users.go | 40 +++--- coderd/workspaceagents.go | 16 +-- coderd/workspaceapps.go | 9 ++ coderd/workspacebuilds.go | 16 +-- coderd/workspaceresourceauth.go | 8 +- coderd/workspaceresources.go | 2 +- coderd/workspaces.go | 18 +-- 22 files changed, 215 insertions(+), 227 deletions(-) create mode 100644 coderd/workspaceapps.go diff --git a/cli/server.go b/cli/server.go index c778e0a220235..4ccec67810a63 100644 --- a/cli/server.go +++ b/cli/server.go @@ -319,7 +319,7 @@ func server() *cobra.Command { // These errors are typically noise like "TLS: EOF". Vault does similar: // https://github.com/hashicorp/vault/blob/e2490059d0711635e529a4efcbaa1b26998d6e1c/command/server.go#L2714 ErrorLog: log.New(io.Discard, "", 0), - Handler: coderDaemon.Handler(), + Handler: coderDaemon, BaseContext: func(_ net.Listener) context.Context { return shutdownConnsCtx }, @@ -387,7 +387,7 @@ func server() *cobra.Command { signal.Notify(stopChan, os.Interrupt) select { case <-cmd.Context().Done(): - coderDaemon.CloseWait() + coderDaemon.Close() return cmd.Context().Err() case err := <-tunnelErrChan: if err != nil { @@ -395,7 +395,7 @@ func server() *cobra.Command { } case err := <-errCh: shutdownConns() - coderDaemon.CloseWait() + coderDaemon.Close() return err case <-stopChan: } @@ -459,7 +459,7 @@ func server() *cobra.Command { _, _ = fmt.Fprintf(cmd.OutOrStdout(), cliui.Styles.Prompt.String()+"Waiting for WebSocket connections to close...\n") shutdownConns() - coderDaemon.CloseWait() + coderDaemon.Close() return nil }, } @@ -555,7 +555,7 @@ func createFirstUser(cmd *cobra.Command, client *codersdk.Client, cfg config.Roo } // nolint:revive -func newProvisionerDaemon(ctx context.Context, coderDaemon coderd.CoderD, +func newProvisionerDaemon(ctx context.Context, coderDaemon *coderd.API, logger slog.Logger, cacheDir string, errChan chan error, dev bool) (*provisionerd.Server, error) { err := os.MkdirAll(cacheDir, 0700) if err != nil { diff --git a/coderd/authorize.go b/coderd/authorize.go index 13374344fae1f..b98ad5e20f83c 100644 --- a/coderd/authorize.go +++ b/coderd/authorize.go @@ -12,12 +12,12 @@ import ( "github.com/coder/coder/coderd/rbac" ) -func AuthorizeFilter[O rbac.Objecter](api *api, r *http.Request, action rbac.Action, objects []O) []O { +func AuthorizeFilter[O rbac.Objecter](api *API, r *http.Request, action rbac.Action, objects []O) []O { roles := httpmw.UserRoles(r) return rbac.Filter(r.Context(), api.Authorizer, roles.ID.String(), roles.Roles, action, objects) } -func (api *api) Authorize(rw http.ResponseWriter, r *http.Request, action rbac.Action, object rbac.Objecter) bool { +func (api *API) Authorize(rw http.ResponseWriter, r *http.Request, action rbac.Action, object rbac.Objecter) bool { roles := httpmw.UserRoles(r) err := api.Authorizer.ByRoleName(r.Context(), roles.ID.String(), roles.Roles, action, object.RBACObject()) if err != nil { diff --git a/coderd/coderd.go b/coderd/coderd.go index c474aeb785215..fb042d8b65045 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -28,7 +28,6 @@ import ( "github.com/coder/coder/coderd/tracing" "github.com/coder/coder/coderd/turnconn" "github.com/coder/coder/codersdk" - "github.com/coder/coder/provisionerd/proto" "github.com/coder/coder/site" ) @@ -56,22 +55,14 @@ type Options struct { TracerProvider *sdktrace.TracerProvider } -type CoderD interface { - Handler() http.Handler - CloseWait() - - // An in-process provisionerd connection. - ListenProvisionerDaemon(ctx context.Context) (proto.DRPCProvisionerDaemonClient, error) -} - -type coderD struct { - api *api - router chi.Router - options *Options -} +// New constructs a Coder API handler. +func New(options *Options) *API { + r := chi.NewRouter() + api := &API{ + Options: options, + router: r, + } -// newRouter constructs the Chi Router for the given API. -func newRouter(options *Options, a *api) chi.Router { if options.AgentConnectionUpdateFrequency == 0 { options.AgentConnectionUpdateFrequency = 3 * time.Second } @@ -90,12 +81,9 @@ func newRouter(options *Options, a *api) chi.Router { apiKeyMiddleware := httpmw.ExtractAPIKey(options.Database, &httpmw.OAuth2Configs{ Github: options.GithubOAuth2Config, }) - // TODO: @emyrk we should just move this into 'ExtractAPIKey'. authRolesMiddleware := httpmw.ExtractUserRoles(options.Database) - r := chi.NewRouter() - r.Use( func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -103,7 +91,7 @@ func newRouter(options *Options, a *api) chi.Router { }) }, httpmw.Prometheus, - tracing.HTTPMW(a.TracerProvider, "coderd.http"), + tracing.HTTPMW(api.TracerProvider, "coderd.http"), ) r.Route("/api/v2", func(r chi.Router) { @@ -116,7 +104,7 @@ func newRouter(options *Options, a *api) chi.Router { r.Use( // Specific routes can specify smaller limits. httpmw.RateLimitPerMinute(options.APIRateLimit), - debugLogRequest(a.Logger), + debugLogRequest(api.Logger), ) r.Get("/", func(w http.ResponseWriter, r *http.Request) { httpapi.Write(w, http.StatusOK, httpapi.Response{ @@ -139,8 +127,8 @@ func newRouter(options *Options, a *api) chi.Router { // file content is expensive so it should be small. httpmw.RateLimitPerMinute(12), ) - r.Get("/{hash}", a.fileByHash) - r.Post("/", a.postFile) + r.Get("/{hash}", api.fileByHash) + r.Post("/", api.postFile) }) r.Route("/organizations/{organization}", func(r chi.Router) { r.Use( @@ -148,40 +136,40 @@ func newRouter(options *Options, a *api) chi.Router { httpmw.ExtractOrganizationParam(options.Database), authRolesMiddleware, ) - r.Get("/", a.organization) - r.Get("/provisionerdaemons", a.provisionerDaemonsByOrganization) - r.Post("/templateversions", a.postTemplateVersionsByOrganization) + r.Get("/", api.organization) + r.Get("/provisionerdaemons", api.provisionerDaemonsByOrganization) + r.Post("/templateversions", api.postTemplateVersionsByOrganization) r.Route("/templates", func(r chi.Router) { - r.Post("/", a.postTemplateByOrganization) - r.Get("/", a.templatesByOrganization) - r.Get("/{templatename}", a.templateByOrganizationAndName) + r.Post("/", api.postTemplateByOrganization) + r.Get("/", api.templatesByOrganization) + r.Get("/{templatename}", api.templateByOrganizationAndName) }) r.Route("/workspaces", func(r chi.Router) { - r.Post("/", a.postWorkspacesByOrganization) - r.Get("/", a.workspacesByOrganization) + r.Post("/", api.postWorkspacesByOrganization) + r.Get("/", api.workspacesByOrganization) r.Route("/{user}", func(r chi.Router) { r.Use(httpmw.ExtractUserParam(options.Database)) - r.Get("/{workspacename}", a.workspaceByOwnerAndName) - r.Get("/", a.workspacesByOwner) + r.Get("/{workspacename}", api.workspaceByOwnerAndName) + r.Get("/", api.workspacesByOwner) }) }) r.Route("/members", func(r chi.Router) { - r.Get("/roles", a.assignableOrgRoles) + r.Get("/roles", api.assignableOrgRoles) r.Route("/{user}", func(r chi.Router) { r.Use( httpmw.ExtractUserParam(options.Database), httpmw.ExtractOrganizationMemberParam(options.Database), ) - r.Put("/roles", a.putMemberRoles) + r.Put("/roles", api.putMemberRoles) }) }) }) r.Route("/parameters/{scope}/{id}", func(r chi.Router) { r.Use(apiKeyMiddleware) - r.Post("/", a.postParameter) - r.Get("/", a.parameters) + r.Post("/", api.postParameter) + r.Get("/", api.parameters) r.Route("/{name}", func(r chi.Router) { - r.Delete("/", a.deleteParameter) + r.Delete("/", api.deleteParameter) }) }) r.Route("/templates/{template}", func(r chi.Router) { @@ -191,12 +179,12 @@ func newRouter(options *Options, a *api) chi.Router { httpmw.ExtractTemplateParam(options.Database), ) - r.Get("/", a.template) - r.Delete("/", a.deleteTemplate) + r.Get("/", api.template) + r.Delete("/", api.deleteTemplate) r.Route("/versions", func(r chi.Router) { - r.Get("/", a.templateVersionsByTemplate) - r.Patch("/", a.patchActiveTemplateVersion) - r.Get("/{templateversionname}", a.templateVersionByName) + r.Get("/", api.templateVersionsByTemplate) + r.Patch("/", api.patchActiveTemplateVersion) + r.Get("/{templateversionname}", api.templateVersionByName) }) }) r.Route("/templateversions/{templateversion}", func(r chi.Router) { @@ -206,23 +194,23 @@ func newRouter(options *Options, a *api) chi.Router { httpmw.ExtractTemplateVersionParam(options.Database), ) - r.Get("/", a.templateVersion) - r.Patch("/cancel", a.patchCancelTemplateVersion) - r.Get("/schema", a.templateVersionSchema) - r.Get("/parameters", a.templateVersionParameters) - r.Get("/resources", a.templateVersionResources) - r.Get("/logs", a.templateVersionLogs) + r.Get("/", api.templateVersion) + r.Patch("/cancel", api.patchCancelTemplateVersion) + r.Get("/schema", api.templateVersionSchema) + r.Get("/parameters", api.templateVersionParameters) + r.Get("/resources", api.templateVersionResources) + r.Get("/logs", api.templateVersionLogs) }) r.Route("/users", func(r chi.Router) { - r.Get("/first", a.firstUser) - r.Post("/first", a.postFirstUser) - r.Post("/login", a.postLogin) - r.Post("/logout", a.postLogout) - r.Get("/authmethods", a.userAuthMethods) + r.Get("/first", api.firstUser) + r.Post("/first", api.postFirstUser) + r.Post("/login", api.postLogin) + r.Post("/logout", api.postLogout) + r.Get("/authmethods", api.userAuthMethods) r.Route("/oauth2", func(r chi.Router) { r.Route("/github", func(r chi.Router) { r.Use(httpmw.ExtractOAuth2(options.GithubOAuth2Config)) - r.Get("/callback", a.userOAuth2Github) + r.Get("/callback", api.userOAuth2Github) }) }) r.Group(func(r chi.Router) { @@ -230,62 +218,62 @@ func newRouter(options *Options, a *api) chi.Router { apiKeyMiddleware, authRolesMiddleware, ) - r.Post("/", a.postUser) - r.Get("/", a.users) + r.Post("/", api.postUser) + r.Get("/", api.users) // These routes query information about site wide roles. r.Route("/roles", func(r chi.Router) { - r.Get("/", a.assignableSiteRoles) + r.Get("/", api.assignableSiteRoles) }) r.Route("/{user}", func(r chi.Router) { r.Use(httpmw.ExtractUserParam(options.Database)) - r.Get("/", a.userByName) - r.Put("/profile", a.putUserProfile) + r.Get("/", api.userByName) + r.Put("/profile", api.putUserProfile) r.Route("/status", func(r chi.Router) { - r.Put("/suspend", a.putUserStatus(database.UserStatusSuspended)) - r.Put("/active", a.putUserStatus(database.UserStatusActive)) + r.Put("/suspend", api.putUserStatus(database.UserStatusSuspended)) + r.Put("/active", api.putUserStatus(database.UserStatusActive)) }) r.Route("/password", func(r chi.Router) { - r.Put("/", a.putUserPassword) + r.Put("/", api.putUserPassword) }) // These roles apply to the site wide permissions. - r.Put("/roles", a.putUserRoles) - r.Get("/roles", a.userRoles) + r.Put("/roles", api.putUserRoles) + r.Get("/roles", api.userRoles) - r.Post("/authorization", a.checkPermissions) + r.Post("/authorization", api.checkPermissions) - r.Post("/keys", a.postAPIKey) + r.Post("/keys", api.postAPIKey) r.Route("/organizations", func(r chi.Router) { - r.Post("/", a.postOrganizationsByUser) - r.Get("/", a.organizationsByUser) - r.Get("/{organizationname}", a.organizationByUserAndName) + r.Post("/", api.postOrganizationsByUser) + r.Get("/", api.organizationsByUser) + r.Get("/{organizationname}", api.organizationByUserAndName) }) - r.Get("/gitsshkey", a.gitSSHKey) - r.Put("/gitsshkey", a.regenerateGitSSHKey) + r.Get("/gitsshkey", api.gitSSHKey) + r.Put("/gitsshkey", api.regenerateGitSSHKey) }) }) }) r.Route("/workspaceagents", func(r chi.Router) { - r.Post("/azure-instance-identity", a.postWorkspaceAuthAzureInstanceIdentity) - r.Post("/aws-instance-identity", a.postWorkspaceAuthAWSInstanceIdentity) - r.Post("/google-instance-identity", a.postWorkspaceAuthGoogleInstanceIdentity) + r.Post("/azure-instance-identity", api.postWorkspaceAuthAzureInstanceIdentity) + r.Post("/aws-instance-identity", api.postWorkspaceAuthAWSInstanceIdentity) + r.Post("/google-instance-identity", api.postWorkspaceAuthGoogleInstanceIdentity) r.Route("/me", func(r chi.Router) { r.Use(httpmw.ExtractWorkspaceAgent(options.Database)) - r.Get("/metadata", a.workspaceAgentMetadata) - r.Get("/listen", a.workspaceAgentListen) - r.Get("/gitsshkey", a.agentGitSSHKey) - r.Get("/turn", a.workspaceAgentTurn) - r.Get("/iceservers", a.workspaceAgentICEServers) + r.Get("/metadata", api.workspaceAgentMetadata) + r.Get("/listen", api.workspaceAgentListen) + r.Get("/gitsshkey", api.agentGitSSHKey) + r.Get("/turn", api.workspaceAgentTurn) + r.Get("/iceservers", api.workspaceAgentICEServers) }) r.Route("/{workspaceagent}", func(r chi.Router) { r.Use( apiKeyMiddleware, httpmw.ExtractWorkspaceAgentParam(options.Database), ) - r.Get("/", a.workspaceAgent) - r.Get("/dial", a.workspaceAgentDial) - r.Get("/turn", a.workspaceAgentTurn) - r.Get("/pty", a.workspaceAgentPTY) - r.Get("/iceservers", a.workspaceAgentICEServers) + r.Get("/", api.workspaceAgent) + r.Get("/dial", api.workspaceAgentDial) + r.Get("/turn", api.workspaceAgentTurn) + r.Get("/pty", api.workspaceAgentPTY) + r.Get("/iceservers", api.workspaceAgentICEServers) }) }) r.Route("/workspaceresources/{workspaceresource}", func(r chi.Router) { @@ -295,31 +283,31 @@ func newRouter(options *Options, a *api) chi.Router { httpmw.ExtractWorkspaceResourceParam(options.Database), httpmw.ExtractWorkspaceParam(options.Database), ) - r.Get("/", a.workspaceResource) + r.Get("/", api.workspaceResource) }) r.Route("/workspaces", func(r chi.Router) { r.Use( apiKeyMiddleware, authRolesMiddleware, ) - r.Get("/", a.workspaces) + r.Get("/", api.workspaces) r.Route("/{workspace}", func(r chi.Router) { r.Use( httpmw.ExtractWorkspaceParam(options.Database), ) - r.Get("/", a.workspace) + r.Get("/", api.workspace) r.Route("/builds", func(r chi.Router) { - r.Get("/", a.workspaceBuilds) - r.Post("/", a.postWorkspaceBuilds) - r.Get("/{workspacebuildname}", a.workspaceBuildByName) + r.Get("/", api.workspaceBuilds) + r.Post("/", api.postWorkspaceBuilds) + r.Get("/{workspacebuildname}", api.workspaceBuildByName) }) r.Route("/autostart", func(r chi.Router) { - r.Put("/", a.putWorkspaceAutostart) + r.Put("/", api.putWorkspaceAutostart) }) r.Route("/ttl", func(r chi.Router) { - r.Put("/", a.putWorkspaceTTL) + r.Put("/", api.putWorkspaceTTL) }) - r.Get("/watch", a.watchWorkspace) + r.Get("/watch", api.watchWorkspace) }) }) r.Route("/workspacebuilds/{workspacebuild}", func(r chi.Router) { @@ -329,48 +317,39 @@ func newRouter(options *Options, a *api) chi.Router { httpmw.ExtractWorkspaceBuildParam(options.Database), httpmw.ExtractWorkspaceParam(options.Database), ) - r.Get("/", a.workspaceBuild) - r.Patch("/cancel", a.patchCancelWorkspaceBuild) - r.Get("/logs", a.workspaceBuildLogs) - r.Get("/resources", a.workspaceBuildResources) - r.Get("/state", a.workspaceBuildState) + r.Get("/", api.workspaceBuild) + r.Patch("/cancel", api.patchCancelWorkspaceBuild) + r.Get("/logs", api.workspaceBuildLogs) + r.Get("/resources", api.workspaceBuildResources) + r.Get("/state", api.workspaceBuildState) }) }) - - var _ = xerrors.New("test") - r.NotFound(site.DefaultHandler().ServeHTTP) - return r -} - -func New(options *Options) CoderD { - a := &api{Options: options} - return &coderD{ - api: a, - router: newRouter(options, a), - options: options, - } -} - -func (c *coderD) CloseWait() { - c.api.websocketWaitMutex.Lock() - c.api.websocketWaitGroup.Wait() - c.api.websocketWaitMutex.Unlock() -} -func (c *coderD) Handler() http.Handler { - return c.router + return api } -// API contains all route handlers. Only HTTP handlers should -// be added to this struct for code clarity. -type api struct { +type API struct { *Options + router chi.Router websocketWaitMutex sync.Mutex websocketWaitGroup sync.WaitGroup } +// ServeHTTP fulfills the http.Handler interface to allow for the API +// to be mounted without exposing the router. +func (api *API) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + api.router.ServeHTTP(rw, r) +} + +// Close waits for all WebSocket connections to drain before returning. +func (api *API) Close() { + api.websocketWaitMutex.Lock() + api.websocketWaitGroup.Wait() + api.websocketWaitMutex.Unlock() +} + func debugLogRequest(log slog.Logger) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index 7ee5a2bc4dc9d..244c3640a9f28 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -78,7 +78,7 @@ func New(t *testing.T, options *Options) *codersdk.Client { // NewWithServer returns an in-memory coderd instance and // the HTTP server it started with. -func NewWithServer(t *testing.T, options *Options) (*httptest.Server, *codersdk.Client, coderd.CoderD) { +func NewWithServer(t *testing.T, options *Options) (*httptest.Server, *codersdk.Client, *coderd.API) { if options == nil { options = &Options{} } @@ -160,7 +160,7 @@ func NewWithServer(t *testing.T, options *Options) (*httptest.Server, *codersdk. APIRateLimit: options.APIRateLimit, Authorizer: options.Authorizer, }) - srv.Config.Handler = coderDaemon.Handler() + srv.Config.Handler = coderDaemon if options.IncludeProvisionerD { _ = NewProvisionerDaemon(t, coderDaemon) } @@ -168,7 +168,7 @@ func NewWithServer(t *testing.T, options *Options) (*httptest.Server, *codersdk. cancelFunc() _ = turnServer.Close() srv.Close() - coderDaemon.CloseWait() + coderDaemon.Close() }) return srv, codersdk.New(serverURL), coderDaemon @@ -177,7 +177,7 @@ func NewWithServer(t *testing.T, options *Options) (*httptest.Server, *codersdk. // NewProvisionerDaemon launches a provisionerd instance configured to work // well with coderd testing. It registers the "echo" provisioner for // quick testing. -func NewProvisionerDaemon(t *testing.T, coderDaemon coderd.CoderD) io.Closer { +func NewProvisionerDaemon(t *testing.T, coderDaemon *coderd.API) io.Closer { echoClient, echoServer := provisionersdk.TransportPipe() ctx, cancelFunc := context.WithCancel(context.Background()) t.Cleanup(func() { diff --git a/coderd/files.go b/coderd/files.go index 74b4b56f26f02..5e63889496ae8 100644 --- a/coderd/files.go +++ b/coderd/files.go @@ -18,7 +18,7 @@ import ( "github.com/coder/coder/codersdk" ) -func (api *api) postFile(rw http.ResponseWriter, r *http.Request) { +func (api *API) postFile(rw http.ResponseWriter, r *http.Request) { apiKey := httpmw.APIKey(r) // This requires the site wide action to create files. // Once created, a user can read their own files uploaded @@ -74,7 +74,7 @@ func (api *api) postFile(rw http.ResponseWriter, r *http.Request) { }) } -func (api *api) fileByHash(rw http.ResponseWriter, r *http.Request) { +func (api *API) fileByHash(rw http.ResponseWriter, r *http.Request) { hash := chi.URLParam(r, "hash") if hash == "" { httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ diff --git a/coderd/gitsshkey.go b/coderd/gitsshkey.go index d5b2b049f892c..a80144fee2683 100644 --- a/coderd/gitsshkey.go +++ b/coderd/gitsshkey.go @@ -12,7 +12,7 @@ import ( "github.com/coder/coder/codersdk" ) -func (api *api) regenerateGitSSHKey(rw http.ResponseWriter, r *http.Request) { +func (api *API) regenerateGitSSHKey(rw http.ResponseWriter, r *http.Request) { user := httpmw.UserParam(r) if !api.Authorize(rw, r, rbac.ActionUpdate, rbac.ResourceUserData.WithOwner(user.ID.String())) { @@ -57,7 +57,7 @@ func (api *api) regenerateGitSSHKey(rw http.ResponseWriter, r *http.Request) { }) } -func (api *api) gitSSHKey(rw http.ResponseWriter, r *http.Request) { +func (api *API) gitSSHKey(rw http.ResponseWriter, r *http.Request) { user := httpmw.UserParam(r) if !api.Authorize(rw, r, rbac.ActionRead, rbac.ResourceUserData.WithOwner(user.ID.String())) { @@ -81,7 +81,7 @@ func (api *api) gitSSHKey(rw http.ResponseWriter, r *http.Request) { }) } -func (api *api) agentGitSSHKey(rw http.ResponseWriter, r *http.Request) { +func (api *API) agentGitSSHKey(rw http.ResponseWriter, r *http.Request) { agent := httpmw.WorkspaceAgent(r) resource, err := api.Database.GetWorkspaceResourceByID(r.Context(), agent.ResourceID) if err != nil { diff --git a/coderd/members.go b/coderd/members.go index 4c716c537af50..78f44294ae87f 100644 --- a/coderd/members.go +++ b/coderd/members.go @@ -16,7 +16,7 @@ import ( "github.com/coder/coder/codersdk" ) -func (api *api) putMemberRoles(rw http.ResponseWriter, r *http.Request) { +func (api *API) putMemberRoles(rw http.ResponseWriter, r *http.Request) { user := httpmw.UserParam(r) organization := httpmw.OrganizationParam(r) member := httpmw.OrganizationMemberParam(r) @@ -55,7 +55,7 @@ func (api *api) putMemberRoles(rw http.ResponseWriter, r *http.Request) { httpapi.Write(rw, http.StatusOK, convertOrganizationMember(updatedUser)) } -func (api *api) updateOrganizationMemberRoles(ctx context.Context, args database.UpdateMemberRolesParams) (database.OrganizationMember, error) { +func (api *API) updateOrganizationMemberRoles(ctx context.Context, args database.UpdateMemberRolesParams) (database.OrganizationMember, error) { // Enforce only site wide roles for _, r := range args.GrantedRoles { // Must be an org role for the org in the args diff --git a/coderd/organizations.go b/coderd/organizations.go index b0b57f748ccd6..a5e3a958817dc 100644 --- a/coderd/organizations.go +++ b/coderd/organizations.go @@ -10,7 +10,7 @@ import ( "github.com/coder/coder/codersdk" ) -func (api *api) organization(rw http.ResponseWriter, r *http.Request) { +func (api *API) organization(rw http.ResponseWriter, r *http.Request) { organization := httpmw.OrganizationParam(r) if !api.Authorize(rw, r, rbac.ActionRead, rbac.ResourceOrganization. diff --git a/coderd/parameters.go b/coderd/parameters.go index 0f8afcbb2da60..9e719798083f2 100644 --- a/coderd/parameters.go +++ b/coderd/parameters.go @@ -16,7 +16,7 @@ import ( "github.com/coder/coder/codersdk" ) -func (api *api) postParameter(rw http.ResponseWriter, r *http.Request) { +func (api *API) postParameter(rw http.ResponseWriter, r *http.Request) { var createRequest codersdk.CreateParameterRequest if !httpapi.Read(rw, r, &createRequest) { return @@ -63,7 +63,7 @@ func (api *api) postParameter(rw http.ResponseWriter, r *http.Request) { httpapi.Write(rw, http.StatusCreated, convertParameterValue(parameterValue)) } -func (api *api) parameters(rw http.ResponseWriter, r *http.Request) { +func (api *API) parameters(rw http.ResponseWriter, r *http.Request) { scope, scopeID, valid := readScopeAndID(rw, r) if !valid { return @@ -89,7 +89,7 @@ func (api *api) parameters(rw http.ResponseWriter, r *http.Request) { httpapi.Write(rw, http.StatusOK, apiParameterValues) } -func (api *api) deleteParameter(rw http.ResponseWriter, r *http.Request) { +func (api *API) deleteParameter(rw http.ResponseWriter, r *http.Request) { scope, scopeID, valid := readScopeAndID(rw, r) if !valid { return diff --git a/coderd/provisionerdaemons.go b/coderd/provisionerdaemons.go index bb3fa21361bdd..590f3e56264f4 100644 --- a/coderd/provisionerdaemons.go +++ b/coderd/provisionerdaemons.go @@ -30,7 +30,7 @@ import ( sdkproto "github.com/coder/coder/provisionersdk/proto" ) -func (api *api) provisionerDaemonsByOrganization(rw http.ResponseWriter, r *http.Request) { +func (api *API) provisionerDaemonsByOrganization(rw http.ResponseWriter, r *http.Request) { daemons, err := api.Database.GetProvisionerDaemons(r.Context()) if errors.Is(err, sql.ErrNoRows) { err = nil @@ -49,7 +49,7 @@ func (api *api) provisionerDaemonsByOrganization(rw http.ResponseWriter, r *http // ListenProvisionerDaemon is an in-memory connection to a provisionerd. Useful when starting coderd and provisionerd // in the same process. -func (c *coderD) ListenProvisionerDaemon(ctx context.Context) (client proto.DRPCProvisionerDaemonClient, err error) { +func (api *API) ListenProvisionerDaemon(ctx context.Context) (client proto.DRPCProvisionerDaemonClient, err error) { clientSession, serverSession := provisionersdk.TransportPipe() defer func() { if err != nil { @@ -58,7 +58,7 @@ func (c *coderD) ListenProvisionerDaemon(ctx context.Context) (client proto.DRPC } }() - daemon, err := c.api.Database.InsertProvisionerDaemon(ctx, database.InsertProvisionerDaemonParams{ + daemon, err := api.Database.InsertProvisionerDaemon(ctx, database.InsertProvisionerDaemonParams{ ID: uuid.New(), CreatedAt: database.Now(), Name: namesgenerator.GetRandomName(1), @@ -70,12 +70,12 @@ func (c *coderD) ListenProvisionerDaemon(ctx context.Context) (client proto.DRPC mux := drpcmux.New() err = proto.DRPCRegisterProvisionerDaemon(mux, &provisionerdServer{ - AccessURL: c.options.AccessURL, + AccessURL: api.AccessURL, ID: daemon.ID, - Database: c.options.Database, - Pubsub: c.options.Pubsub, + Database: api.Database, + Pubsub: api.Pubsub, Provisioners: daemon.Provisioners, - Logger: c.options.Logger.Named(fmt.Sprintf("provisionerd-%s", daemon.Name)), + Logger: api.Logger.Named(fmt.Sprintf("provisionerd-%s", daemon.Name)), }) if err != nil { return nil, err @@ -85,13 +85,13 @@ func (c *coderD) ListenProvisionerDaemon(ctx context.Context) (client proto.DRPC if xerrors.Is(err, io.EOF) { return } - c.options.Logger.Debug(ctx, "drpc server error", slog.Error(err)) + api.Logger.Debug(ctx, "drpc server error", slog.Error(err)) }, }) go func() { err = server.Serve(ctx, serverSession) if err != nil && !xerrors.Is(err, io.EOF) { - c.options.Logger.Debug(ctx, "provisioner daemon disconnected", slog.Error(err)) + api.Logger.Debug(ctx, "provisioner daemon disconnected", slog.Error(err)) } // close the sessions so we don't leak goroutines serving them. _ = clientSession.Close() diff --git a/coderd/provisionerjobs.go b/coderd/provisionerjobs.go index 8c8ded3620f2f..a47cc15541994 100644 --- a/coderd/provisionerjobs.go +++ b/coderd/provisionerjobs.go @@ -26,7 +26,7 @@ import ( // 2. GET /logs?after=&follow // The combination of these responses should provide all current logs // to the consumer, and future logs are streamed in the follow request. -func (api *api) provisionerJobLogs(rw http.ResponseWriter, r *http.Request, job database.ProvisionerJob) { +func (api *API) provisionerJobLogs(rw http.ResponseWriter, r *http.Request, job database.ProvisionerJob) { follow := r.URL.Query().Has("follow") afterRaw := r.URL.Query().Get("after") beforeRaw := r.URL.Query().Get("before") @@ -178,7 +178,7 @@ func (api *api) provisionerJobLogs(rw http.ResponseWriter, r *http.Request, job } } -func (api *api) provisionerJobResources(rw http.ResponseWriter, r *http.Request, job database.ProvisionerJob) { +func (api *API) provisionerJobResources(rw http.ResponseWriter, r *http.Request, job database.ProvisionerJob) { if !job.CompletedAt.Valid { httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{ Message: "Job hasn't completed!", diff --git a/coderd/roles.go b/coderd/roles.go index 58d7ea96c6d14..3843124fbc45d 100644 --- a/coderd/roles.go +++ b/coderd/roles.go @@ -11,7 +11,7 @@ import ( ) // assignableSiteRoles returns all site wide roles that can be assigned. -func (api *api) assignableSiteRoles(rw http.ResponseWriter, r *http.Request) { +func (api *API) assignableSiteRoles(rw http.ResponseWriter, r *http.Request) { // TODO: @emyrk in the future, allow granular subsets of roles to be returned based on the // role of the user. @@ -24,7 +24,7 @@ func (api *api) assignableSiteRoles(rw http.ResponseWriter, r *http.Request) { } // assignableSiteRoles returns all site wide roles that can be assigned. -func (api *api) assignableOrgRoles(rw http.ResponseWriter, r *http.Request) { +func (api *API) assignableOrgRoles(rw http.ResponseWriter, r *http.Request) { // TODO: @emyrk in the future, allow granular subsets of roles to be returned based on the // role of the user. organization := httpmw.OrganizationParam(r) @@ -37,7 +37,7 @@ func (api *api) assignableOrgRoles(rw http.ResponseWriter, r *http.Request) { httpapi.Write(rw, http.StatusOK, convertRoles(roles)) } -func (api *api) checkPermissions(rw http.ResponseWriter, r *http.Request) { +func (api *API) checkPermissions(rw http.ResponseWriter, r *http.Request) { user := httpmw.UserParam(r) if !api.Authorize(rw, r, rbac.ActionRead, rbac.ResourceUser.WithOwner(user.ID.String())) { diff --git a/coderd/templates.go b/coderd/templates.go index 44850f817f623..185836e293335 100644 --- a/coderd/templates.go +++ b/coderd/templates.go @@ -18,7 +18,7 @@ import ( ) // Returns a single template. -func (api *api) template(rw http.ResponseWriter, r *http.Request) { +func (api *API) template(rw http.ResponseWriter, r *http.Request) { template := httpmw.TemplateParam(r) workspaceCounts, err := api.Database.GetWorkspaceOwnerCountsByTemplateIDs(r.Context(), []uuid.UUID{template.ID}) @@ -44,7 +44,7 @@ func (api *api) template(rw http.ResponseWriter, r *http.Request) { httpapi.Write(rw, http.StatusOK, convertTemplate(template, count)) } -func (api *api) deleteTemplate(rw http.ResponseWriter, r *http.Request) { +func (api *API) deleteTemplate(rw http.ResponseWriter, r *http.Request) { template := httpmw.TemplateParam(r) if !api.Authorize(rw, r, rbac.ActionDelete, template) { return @@ -84,7 +84,7 @@ func (api *api) deleteTemplate(rw http.ResponseWriter, r *http.Request) { } // Create a new template in an organization. -func (api *api) postTemplateByOrganization(rw http.ResponseWriter, r *http.Request) { +func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Request) { var createTemplate codersdk.CreateTemplateRequest organization := httpmw.OrganizationParam(r) if !api.Authorize(rw, r, rbac.ActionCreate, rbac.ResourceTemplate.InOrg(organization.ID)) { @@ -193,7 +193,7 @@ func (api *api) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque httpapi.Write(rw, http.StatusCreated, template) } -func (api *api) templatesByOrganization(rw http.ResponseWriter, r *http.Request) { +func (api *API) templatesByOrganization(rw http.ResponseWriter, r *http.Request) { organization := httpmw.OrganizationParam(r) templates, err := api.Database.GetTemplatesByOrganization(r.Context(), database.GetTemplatesByOrganizationParams{ OrganizationID: organization.ID, @@ -230,7 +230,7 @@ func (api *api) templatesByOrganization(rw http.ResponseWriter, r *http.Request) httpapi.Write(rw, http.StatusOK, convertTemplates(templates, workspaceCounts)) } -func (api *api) templateByOrganizationAndName(rw http.ResponseWriter, r *http.Request) { +func (api *API) templateByOrganizationAndName(rw http.ResponseWriter, r *http.Request) { organization := httpmw.OrganizationParam(r) templateName := chi.URLParam(r, "templatename") template, err := api.Database.GetTemplateByOrganizationAndName(r.Context(), database.GetTemplateByOrganizationAndNameParams{ diff --git a/coderd/templateversions.go b/coderd/templateversions.go index bf8438d32238b..0c1c1d3f7de83 100644 --- a/coderd/templateversions.go +++ b/coderd/templateversions.go @@ -19,7 +19,7 @@ import ( "github.com/coder/coder/codersdk" ) -func (api *api) templateVersion(rw http.ResponseWriter, r *http.Request) { +func (api *API) templateVersion(rw http.ResponseWriter, r *http.Request) { templateVersion := httpmw.TemplateVersionParam(r) if !api.Authorize(rw, r, rbac.ActionRead, templateVersion) { return @@ -36,7 +36,7 @@ func (api *api) templateVersion(rw http.ResponseWriter, r *http.Request) { httpapi.Write(rw, http.StatusOK, convertTemplateVersion(templateVersion, convertProvisionerJob(job))) } -func (api *api) patchCancelTemplateVersion(rw http.ResponseWriter, r *http.Request) { +func (api *API) patchCancelTemplateVersion(rw http.ResponseWriter, r *http.Request) { templateVersion := httpmw.TemplateVersionParam(r) if !api.Authorize(rw, r, rbac.ActionUpdate, templateVersion) { return @@ -79,7 +79,7 @@ func (api *api) patchCancelTemplateVersion(rw http.ResponseWriter, r *http.Reque }) } -func (api *api) templateVersionSchema(rw http.ResponseWriter, r *http.Request) { +func (api *API) templateVersionSchema(rw http.ResponseWriter, r *http.Request) { templateVersion := httpmw.TemplateVersionParam(r) if !api.Authorize(rw, r, rbac.ActionRead, templateVersion) { return @@ -122,7 +122,7 @@ func (api *api) templateVersionSchema(rw http.ResponseWriter, r *http.Request) { httpapi.Write(rw, http.StatusOK, apiSchemas) } -func (api *api) templateVersionParameters(rw http.ResponseWriter, r *http.Request) { +func (api *API) templateVersionParameters(rw http.ResponseWriter, r *http.Request) { apiKey := httpmw.APIKey(r) templateVersion := httpmw.TemplateVersionParam(r) if !api.Authorize(rw, r, rbac.ActionRead, templateVersion) { @@ -163,7 +163,7 @@ func (api *api) templateVersionParameters(rw http.ResponseWriter, r *http.Reques httpapi.Write(rw, http.StatusOK, values) } -func (api *api) templateVersionsByTemplate(rw http.ResponseWriter, r *http.Request) { +func (api *API) templateVersionsByTemplate(rw http.ResponseWriter, r *http.Request) { template := httpmw.TemplateParam(r) if !api.Authorize(rw, r, rbac.ActionRead, template) { return @@ -221,7 +221,7 @@ func (api *api) templateVersionsByTemplate(rw http.ResponseWriter, r *http.Reque httpapi.Write(rw, http.StatusOK, apiVersion) } -func (api *api) templateVersionByName(rw http.ResponseWriter, r *http.Request) { +func (api *API) templateVersionByName(rw http.ResponseWriter, r *http.Request) { template := httpmw.TemplateParam(r) if !api.Authorize(rw, r, rbac.ActionRead, template) { return @@ -258,7 +258,7 @@ func (api *api) templateVersionByName(rw http.ResponseWriter, r *http.Request) { httpapi.Write(rw, http.StatusOK, convertTemplateVersion(templateVersion, convertProvisionerJob(job))) } -func (api *api) patchActiveTemplateVersion(rw http.ResponseWriter, r *http.Request) { +func (api *API) patchActiveTemplateVersion(rw http.ResponseWriter, r *http.Request) { template := httpmw.TemplateParam(r) if !api.Authorize(rw, r, rbac.ActionUpdate, template) { return @@ -303,7 +303,7 @@ func (api *api) patchActiveTemplateVersion(rw http.ResponseWriter, r *http.Reque } // Creates a new version of a template. An import job is queued to parse the storage method provided. -func (api *api) postTemplateVersionsByOrganization(rw http.ResponseWriter, r *http.Request) { +func (api *API) postTemplateVersionsByOrganization(rw http.ResponseWriter, r *http.Request) { apiKey := httpmw.APIKey(r) organization := httpmw.OrganizationParam(r) var req codersdk.CreateTemplateVersionRequest @@ -415,7 +415,7 @@ func (api *api) postTemplateVersionsByOrganization(rw http.ResponseWriter, r *ht // provisioned, each resource can have an agent that dials back to coderd. // The agents returned are informative of the template version, and do not // return agents associated with any particular workspace. -func (api *api) templateVersionResources(rw http.ResponseWriter, r *http.Request) { +func (api *API) templateVersionResources(rw http.ResponseWriter, r *http.Request) { templateVersion := httpmw.TemplateVersionParam(r) if !api.Authorize(rw, r, rbac.ActionRead, templateVersion) { return @@ -435,7 +435,7 @@ func (api *api) templateVersionResources(rw http.ResponseWriter, r *http.Request // template version. These logs are only associated with the template version, // and not any build logs for a workspace. // Eg: Logs returned from 'terraform plan' when uploading a new terraform file. -func (api *api) templateVersionLogs(rw http.ResponseWriter, r *http.Request) { +func (api *API) templateVersionLogs(rw http.ResponseWriter, r *http.Request) { templateVersion := httpmw.TemplateVersionParam(r) if !api.Authorize(rw, r, rbac.ActionRead, templateVersion) { return diff --git a/coderd/userauth.go b/coderd/userauth.go index e2b52329b37a7..5838f933e2571 100644 --- a/coderd/userauth.go +++ b/coderd/userauth.go @@ -28,14 +28,14 @@ type GithubOAuth2Config struct { AllowOrganizations []string } -func (api *api) userAuthMethods(rw http.ResponseWriter, _ *http.Request) { +func (api *API) userAuthMethods(rw http.ResponseWriter, _ *http.Request) { httpapi.Write(rw, http.StatusOK, codersdk.AuthMethods{ Password: true, Github: api.GithubOAuth2Config != nil, }) } -func (api *api) userOAuth2Github(rw http.ResponseWriter, r *http.Request) { +func (api *API) userOAuth2Github(rw http.ResponseWriter, r *http.Request) { state := httpmw.OAuth2(r) oauthClient := oauth2.NewClient(r.Context(), oauth2.StaticTokenSource(state.Token)) diff --git a/coderd/users.go b/coderd/users.go index 0e0ce9bda22b8..8e927a9466d7b 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -25,7 +25,7 @@ import ( ) // Returns whether the initial user has been created or not. -func (api *api) firstUser(rw http.ResponseWriter, r *http.Request) { +func (api *API) firstUser(rw http.ResponseWriter, r *http.Request) { userCount, err := api.Database.GetUserCount(r.Context()) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ @@ -47,7 +47,7 @@ func (api *api) firstUser(rw http.ResponseWriter, r *http.Request) { } // Creates the initial user for a Coder deployment. -func (api *api) postFirstUser(rw http.ResponseWriter, r *http.Request) { +func (api *API) postFirstUser(rw http.ResponseWriter, r *http.Request) { var createUser codersdk.CreateFirstUserRequest if !httpapi.Read(rw, r, &createUser) { return @@ -103,7 +103,7 @@ func (api *api) postFirstUser(rw http.ResponseWriter, r *http.Request) { }) } -func (api *api) users(rw http.ResponseWriter, r *http.Request) { +func (api *API) users(rw http.ResponseWriter, r *http.Request) { var ( searchName = r.URL.Query().Get("search") statusFilter = r.URL.Query().Get("status") @@ -161,7 +161,7 @@ func (api *api) users(rw http.ResponseWriter, r *http.Request) { } // Creates a new user. -func (api *api) postUser(rw http.ResponseWriter, r *http.Request) { +func (api *API) postUser(rw http.ResponseWriter, r *http.Request) { // Create the user on the site if !api.Authorize(rw, r, rbac.ActionCreate, rbac.ResourceUser) { return @@ -224,7 +224,7 @@ func (api *api) postUser(rw http.ResponseWriter, r *http.Request) { // Returns the parameterized user requested. All validation // is completed in the middleware for this route. -func (api *api) userByName(rw http.ResponseWriter, r *http.Request) { +func (api *API) userByName(rw http.ResponseWriter, r *http.Request) { user := httpmw.UserParam(r) organizationIDs, err := userOrganizationIDs(r.Context(), api, user) @@ -242,7 +242,7 @@ func (api *api) userByName(rw http.ResponseWriter, r *http.Request) { httpapi.Write(rw, http.StatusOK, convertUser(user, organizationIDs)) } -func (api *api) putUserProfile(rw http.ResponseWriter, r *http.Request) { +func (api *API) putUserProfile(rw http.ResponseWriter, r *http.Request) { user := httpmw.UserParam(r) if !api.Authorize(rw, r, rbac.ActionUpdate, rbac.ResourceUser.WithID(user.ID.String())) { @@ -311,7 +311,7 @@ func (api *api) putUserProfile(rw http.ResponseWriter, r *http.Request) { httpapi.Write(rw, http.StatusOK, convertUser(updatedUserProfile, organizationIDs)) } -func (api *api) putUserStatus(status database.UserStatus) func(rw http.ResponseWriter, r *http.Request) { +func (api *API) putUserStatus(status database.UserStatus) func(rw http.ResponseWriter, r *http.Request) { return func(rw http.ResponseWriter, r *http.Request) { user := httpmw.UserParam(r) apiKey := httpmw.APIKey(r) @@ -352,7 +352,7 @@ func (api *api) putUserStatus(status database.UserStatus) func(rw http.ResponseW } } -func (api *api) putUserPassword(rw http.ResponseWriter, r *http.Request) { +func (api *API) putUserPassword(rw http.ResponseWriter, r *http.Request) { var ( user = httpmw.UserParam(r) params codersdk.UpdateUserPasswordRequest @@ -387,7 +387,7 @@ func (api *api) putUserPassword(rw http.ResponseWriter, r *http.Request) { httpapi.Write(rw, http.StatusNoContent, nil) } -func (api *api) userRoles(rw http.ResponseWriter, r *http.Request) { +func (api *API) userRoles(rw http.ResponseWriter, r *http.Request) { user := httpmw.UserParam(r) if !api.Authorize(rw, r, rbac.ActionRead, rbac.ResourceUserData. @@ -421,7 +421,7 @@ func (api *api) userRoles(rw http.ResponseWriter, r *http.Request) { httpapi.Write(rw, http.StatusOK, resp) } -func (api *api) putUserRoles(rw http.ResponseWriter, r *http.Request) { +func (api *API) putUserRoles(rw http.ResponseWriter, r *http.Request) { // User is the user to modify user := httpmw.UserParam(r) roles := httpmw.UserRoles(r) @@ -469,7 +469,7 @@ func (api *api) putUserRoles(rw http.ResponseWriter, r *http.Request) { // updateSiteUserRoles will ensure only site wide roles are passed in as arguments. // If an organization role is included, an error is returned. -func (api *api) updateSiteUserRoles(ctx context.Context, args database.UpdateUserRolesParams) (database.User, error) { +func (api *API) updateSiteUserRoles(ctx context.Context, args database.UpdateUserRolesParams) (database.User, error) { // Enforce only site wide roles for _, r := range args.GrantedRoles { if _, ok := rbac.IsOrgRole(r); ok { @@ -489,7 +489,7 @@ func (api *api) updateSiteUserRoles(ctx context.Context, args database.UpdateUse } // Returns organizations the parameterized user has access to. -func (api *api) organizationsByUser(rw http.ResponseWriter, r *http.Request) { +func (api *API) organizationsByUser(rw http.ResponseWriter, r *http.Request) { user := httpmw.UserParam(r) organizations, err := api.Database.GetOrganizationsByUserID(r.Context(), user.ID) @@ -515,7 +515,7 @@ func (api *api) organizationsByUser(rw http.ResponseWriter, r *http.Request) { httpapi.Write(rw, http.StatusOK, publicOrganizations) } -func (api *api) organizationByUserAndName(rw http.ResponseWriter, r *http.Request) { +func (api *API) organizationByUserAndName(rw http.ResponseWriter, r *http.Request) { organizationName := chi.URLParam(r, "organizationname") organization, err := api.Database.GetOrganizationByName(r.Context(), organizationName) if errors.Is(err, sql.ErrNoRows) { @@ -539,7 +539,7 @@ func (api *api) organizationByUserAndName(rw http.ResponseWriter, r *http.Reques httpapi.Write(rw, http.StatusOK, convertOrganization(organization)) } -func (api *api) postOrganizationsByUser(rw http.ResponseWriter, r *http.Request) { +func (api *API) postOrganizationsByUser(rw http.ResponseWriter, r *http.Request) { user := httpmw.UserParam(r) var req codersdk.CreateOrganizationRequest if !httpapi.Read(rw, r, &req) { @@ -605,7 +605,7 @@ func (api *api) postOrganizationsByUser(rw http.ResponseWriter, r *http.Request) } // Authenticates the user with an email and password. -func (api *api) postLogin(rw http.ResponseWriter, r *http.Request) { +func (api *API) postLogin(rw http.ResponseWriter, r *http.Request) { var loginWithPassword codersdk.LoginWithPasswordRequest if !httpapi.Read(rw, r, &loginWithPassword) { return @@ -651,7 +651,7 @@ func (api *api) postLogin(rw http.ResponseWriter, r *http.Request) { } // Creates a new session key, used for logging in via the CLI -func (api *api) postAPIKey(rw http.ResponseWriter, r *http.Request) { +func (api *API) postAPIKey(rw http.ResponseWriter, r *http.Request) { user := httpmw.UserParam(r) if !api.Authorize(rw, r, rbac.ActionCreate, rbac.ResourceAPIKey.WithOwner(user.ID.String())) { @@ -670,7 +670,7 @@ func (api *api) postAPIKey(rw http.ResponseWriter, r *http.Request) { } // Clear the user's session cookie -func (*api) postLogout(rw http.ResponseWriter, _ *http.Request) { +func (*API) postLogout(rw http.ResponseWriter, _ *http.Request) { // Get a blank token cookie cookie := &http.Cookie{ // MaxAge < 0 means to delete the cookie now @@ -700,7 +700,7 @@ func generateAPIKeyIDSecret() (id string, secret string, err error) { return id, secret, nil } -func (api *api) createAPIKey(rw http.ResponseWriter, r *http.Request, params database.InsertAPIKeyParams) (string, bool) { +func (api *API) createAPIKey(rw http.ResponseWriter, r *http.Request, params database.InsertAPIKeyParams) (string, bool) { keyID, keySecret, err := generateAPIKeyIDSecret() if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ @@ -743,7 +743,7 @@ func (api *api) createAPIKey(rw http.ResponseWriter, r *http.Request, params dat return sessionToken, true } -func (api *api) createUser(ctx context.Context, req codersdk.CreateUserRequest) (database.User, uuid.UUID, error) { +func (api *API) createUser(ctx context.Context, req codersdk.CreateUserRequest) (database.User, uuid.UUID, error) { var user database.User return user, req.OrganizationID, api.Database.InTx(func(db database.Store) error { var orgRoles []string @@ -845,7 +845,7 @@ func convertUsers(users []database.User, organizationIDsByUserID map[uuid.UUID][ return converted } -func userOrganizationIDs(ctx context.Context, api *api, user database.User) ([]uuid.UUID, error) { +func userOrganizationIDs(ctx context.Context, api *API, user database.User) ([]uuid.UUID, error) { organizationIDsByMemberIDsRows, err := api.Database.GetOrganizationIDsByMemberIDs(ctx, []uuid.UUID{user.ID}) if errors.Is(err, sql.ErrNoRows) || len(organizationIDsByMemberIDsRows) == 0 { return []uuid.UUID{}, nil diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index ec26001b99ff1..bbda257eb01d5 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -28,7 +28,7 @@ import ( "github.com/coder/coder/provisionersdk" ) -func (api *api) workspaceAgent(rw http.ResponseWriter, r *http.Request) { +func (api *API) workspaceAgent(rw http.ResponseWriter, r *http.Request) { workspaceAgent := httpmw.WorkspaceAgentParam(r) apiAgent, err := convertWorkspaceAgent(workspaceAgent, api.AgentConnectionUpdateFrequency) if err != nil { @@ -41,7 +41,7 @@ func (api *api) workspaceAgent(rw http.ResponseWriter, r *http.Request) { httpapi.Write(rw, http.StatusOK, apiAgent) } -func (api *api) workspaceAgentDial(rw http.ResponseWriter, r *http.Request) { +func (api *API) workspaceAgentDial(rw http.ResponseWriter, r *http.Request) { api.websocketWaitMutex.Lock() api.websocketWaitGroup.Add(1) api.websocketWaitMutex.Unlock() @@ -90,7 +90,7 @@ func (api *api) workspaceAgentDial(rw http.ResponseWriter, r *http.Request) { } } -func (api *api) workspaceAgentMetadata(rw http.ResponseWriter, r *http.Request) { +func (api *API) workspaceAgentMetadata(rw http.ResponseWriter, r *http.Request) { workspaceAgent := httpmw.WorkspaceAgent(r) apiAgent, err := convertWorkspaceAgent(workspaceAgent, api.AgentConnectionUpdateFrequency) if err != nil { @@ -136,7 +136,7 @@ func (api *api) workspaceAgentMetadata(rw http.ResponseWriter, r *http.Request) }) } -func (api *api) workspaceAgentListen(rw http.ResponseWriter, r *http.Request) { +func (api *API) workspaceAgentListen(rw http.ResponseWriter, r *http.Request) { api.websocketWaitMutex.Lock() api.websocketWaitGroup.Add(1) api.websocketWaitMutex.Unlock() @@ -269,12 +269,12 @@ func (api *api) workspaceAgentListen(rw http.ResponseWriter, r *http.Request) { } } -func (api *api) workspaceAgentICEServers(rw http.ResponseWriter, _ *http.Request) { +func (api *API) workspaceAgentICEServers(rw http.ResponseWriter, _ *http.Request) { httpapi.Write(rw, http.StatusOK, api.ICEServers) } // workspaceAgentTurn proxies a WebSocket connection to the TURN server. -func (api *api) workspaceAgentTurn(rw http.ResponseWriter, r *http.Request) { +func (api *API) workspaceAgentTurn(rw http.ResponseWriter, r *http.Request) { api.websocketWaitMutex.Lock() api.websocketWaitGroup.Add(1) api.websocketWaitMutex.Unlock() @@ -324,7 +324,7 @@ func (api *api) workspaceAgentTurn(rw http.ResponseWriter, r *http.Request) { // workspaceAgentPTY spawns a PTY and pipes it over a WebSocket. // This is used for the web terminal. -func (api *api) workspaceAgentPTY(rw http.ResponseWriter, r *http.Request) { +func (api *API) workspaceAgentPTY(rw http.ResponseWriter, r *http.Request) { api.websocketWaitMutex.Lock() api.websocketWaitGroup.Add(1) api.websocketWaitMutex.Unlock() @@ -395,7 +395,7 @@ func (api *api) workspaceAgentPTY(rw http.ResponseWriter, r *http.Request) { } // dialWorkspaceAgent connects to a workspace agent by ID. -func (api *api) dialWorkspaceAgent(r *http.Request, agentID uuid.UUID) (*agent.Conn, error) { +func (api *API) dialWorkspaceAgent(r *http.Request, agentID uuid.UUID) (*agent.Conn, error) { client, server := provisionersdk.TransportPipe() go func() { _ = peerbroker.ProxyListen(r.Context(), server, peerbroker.ProxyOptions{ diff --git a/coderd/workspaceapps.go b/coderd/workspaceapps.go new file mode 100644 index 0000000000000..0c2aa8f342bf7 --- /dev/null +++ b/coderd/workspaceapps.go @@ -0,0 +1,9 @@ +package coderd + +import ( + "net/http" +) + +func (api *API) proxyPath(rw http.ResponseWriter, r *http.Request) { + +} diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index d142ea4221c8c..b07f38b7f068e 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -19,7 +19,7 @@ import ( "github.com/coder/coder/codersdk" ) -func (api *api) workspaceBuild(rw http.ResponseWriter, r *http.Request) { +func (api *API) workspaceBuild(rw http.ResponseWriter, r *http.Request) { workspaceBuild := httpmw.WorkspaceBuildParam(r) workspace, err := api.Database.GetWorkspaceByID(r.Context(), workspaceBuild.WorkspaceID) if err != nil { @@ -45,7 +45,7 @@ func (api *api) workspaceBuild(rw http.ResponseWriter, r *http.Request) { httpapi.Write(rw, http.StatusOK, convertWorkspaceBuild(workspaceBuild, convertProvisionerJob(job))) } -func (api *api) workspaceBuilds(rw http.ResponseWriter, r *http.Request) { +func (api *API) workspaceBuilds(rw http.ResponseWriter, r *http.Request) { workspace := httpmw.WorkspaceParam(r) if !api.Authorize(rw, r, rbac.ActionRead, rbac.ResourceWorkspace. @@ -107,7 +107,7 @@ func (api *api) workspaceBuilds(rw http.ResponseWriter, r *http.Request) { httpapi.Write(rw, http.StatusOK, apiBuilds) } -func (api *api) workspaceBuildByName(rw http.ResponseWriter, r *http.Request) { +func (api *API) workspaceBuildByName(rw http.ResponseWriter, r *http.Request) { workspace := httpmw.WorkspaceParam(r) if !api.Authorize(rw, r, rbac.ActionRead, rbac.ResourceWorkspace. InOrg(workspace.OrganizationID).WithOwner(workspace.OwnerID.String()).WithID(workspace.ID.String())) { @@ -142,7 +142,7 @@ func (api *api) workspaceBuildByName(rw http.ResponseWriter, r *http.Request) { httpapi.Write(rw, http.StatusOK, convertWorkspaceBuild(workspaceBuild, convertProvisionerJob(job))) } -func (api *api) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { +func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { apiKey := httpmw.APIKey(r) workspace := httpmw.WorkspaceParam(r) var createBuild codersdk.CreateWorkspaceBuildRequest @@ -310,7 +310,7 @@ func (api *api) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { httpapi.Write(rw, http.StatusCreated, convertWorkspaceBuild(workspaceBuild, convertProvisionerJob(provisionerJob))) } -func (api *api) patchCancelWorkspaceBuild(rw http.ResponseWriter, r *http.Request) { +func (api *API) patchCancelWorkspaceBuild(rw http.ResponseWriter, r *http.Request) { workspaceBuild := httpmw.WorkspaceBuildParam(r) workspace, err := api.Database.GetWorkspaceByID(r.Context(), workspaceBuild.WorkspaceID) if err != nil { @@ -362,7 +362,7 @@ func (api *api) patchCancelWorkspaceBuild(rw http.ResponseWriter, r *http.Reques }) } -func (api *api) workspaceBuildResources(rw http.ResponseWriter, r *http.Request) { +func (api *API) workspaceBuildResources(rw http.ResponseWriter, r *http.Request) { workspaceBuild := httpmw.WorkspaceBuildParam(r) workspace, err := api.Database.GetWorkspaceByID(r.Context(), workspaceBuild.WorkspaceID) if err != nil { @@ -387,7 +387,7 @@ func (api *api) workspaceBuildResources(rw http.ResponseWriter, r *http.Request) api.provisionerJobResources(rw, r, job) } -func (api *api) workspaceBuildLogs(rw http.ResponseWriter, r *http.Request) { +func (api *API) workspaceBuildLogs(rw http.ResponseWriter, r *http.Request) { workspaceBuild := httpmw.WorkspaceBuildParam(r) workspace, err := api.Database.GetWorkspaceByID(r.Context(), workspaceBuild.WorkspaceID) if err != nil { @@ -412,7 +412,7 @@ func (api *api) workspaceBuildLogs(rw http.ResponseWriter, r *http.Request) { api.provisionerJobLogs(rw, r, job) } -func (api *api) workspaceBuildState(rw http.ResponseWriter, r *http.Request) { +func (api *API) workspaceBuildState(rw http.ResponseWriter, r *http.Request) { workspaceBuild := httpmw.WorkspaceBuildParam(r) workspace, err := api.Database.GetWorkspaceByID(r.Context(), workspaceBuild.WorkspaceID) if err != nil { diff --git a/coderd/workspaceresourceauth.go b/coderd/workspaceresourceauth.go index 17a713b651be5..e403271e01645 100644 --- a/coderd/workspaceresourceauth.go +++ b/coderd/workspaceresourceauth.go @@ -18,7 +18,7 @@ import ( // Azure supports instance identity verification: // https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service?tabs=linux#tabgroup_14 -func (api *api) postWorkspaceAuthAzureInstanceIdentity(rw http.ResponseWriter, r *http.Request) { +func (api *API) postWorkspaceAuthAzureInstanceIdentity(rw http.ResponseWriter, r *http.Request) { var req codersdk.AzureInstanceIdentityToken if !httpapi.Read(rw, r, &req) { return @@ -36,7 +36,7 @@ func (api *api) postWorkspaceAuthAzureInstanceIdentity(rw http.ResponseWriter, r // AWS supports instance identity verification: // https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html // Using this, we can exchange a signed instance payload for an agent token. -func (api *api) postWorkspaceAuthAWSInstanceIdentity(rw http.ResponseWriter, r *http.Request) { +func (api *API) postWorkspaceAuthAWSInstanceIdentity(rw http.ResponseWriter, r *http.Request) { var req codersdk.AWSInstanceIdentityToken if !httpapi.Read(rw, r, &req) { return @@ -54,7 +54,7 @@ func (api *api) postWorkspaceAuthAWSInstanceIdentity(rw http.ResponseWriter, r * // Google Compute Engine supports instance identity verification: // https://cloud.google.com/compute/docs/instances/verifying-instance-identity // Using this, we can exchange a signed instance payload for an agent token. -func (api *api) postWorkspaceAuthGoogleInstanceIdentity(rw http.ResponseWriter, r *http.Request) { +func (api *API) postWorkspaceAuthGoogleInstanceIdentity(rw http.ResponseWriter, r *http.Request) { var req codersdk.GoogleInstanceIdentityToken if !httpapi.Read(rw, r, &req) { return @@ -85,7 +85,7 @@ func (api *api) postWorkspaceAuthGoogleInstanceIdentity(rw http.ResponseWriter, api.handleAuthInstanceID(rw, r, claims.Google.ComputeEngine.InstanceID) } -func (api *api) handleAuthInstanceID(rw http.ResponseWriter, r *http.Request, instanceID string) { +func (api *API) handleAuthInstanceID(rw http.ResponseWriter, r *http.Request, instanceID string) { agent, err := api.Database.GetWorkspaceAgentByInstanceID(r.Context(), instanceID) if errors.Is(err, sql.ErrNoRows) { httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ diff --git a/coderd/workspaceresources.go b/coderd/workspaceresources.go index 1bdf1f83e94b9..a08861971e60e 100644 --- a/coderd/workspaceresources.go +++ b/coderd/workspaceresources.go @@ -14,7 +14,7 @@ import ( "github.com/coder/coder/codersdk" ) -func (api *api) workspaceResource(rw http.ResponseWriter, r *http.Request) { +func (api *API) workspaceResource(rw http.ResponseWriter, r *http.Request) { workspaceBuild := httpmw.WorkspaceBuildParam(r) workspaceResource := httpmw.WorkspaceResourceParam(r) workspace := httpmw.WorkspaceParam(r) diff --git a/coderd/workspaces.go b/coderd/workspaces.go index a30534a03e24d..340697a436baa 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -28,7 +28,7 @@ import ( "github.com/coder/coder/codersdk" ) -func (api *api) workspace(rw http.ResponseWriter, r *http.Request) { +func (api *API) workspace(rw http.ResponseWriter, r *http.Request) { workspace := httpmw.WorkspaceParam(r) if !api.Authorize(rw, r, rbac.ActionRead, workspace) { return @@ -100,7 +100,7 @@ func (api *api) workspace(rw http.ResponseWriter, r *http.Request) { convertWorkspace(workspace, convertWorkspaceBuild(build, convertProvisionerJob(job)), template, owner)) } -func (api *api) workspacesByOrganization(rw http.ResponseWriter, r *http.Request) { +func (api *API) workspacesByOrganization(rw http.ResponseWriter, r *http.Request) { organization := httpmw.OrganizationParam(r) workspaces, err := api.Database.GetWorkspacesWithFilter(r.Context(), database.GetWorkspacesWithFilterParams{ OrganizationID: organization.ID, @@ -131,7 +131,7 @@ func (api *api) workspacesByOrganization(rw http.ResponseWriter, r *http.Request // workspaces returns all workspaces a user can read. // Optional filters with query params -func (api *api) workspaces(rw http.ResponseWriter, r *http.Request) { +func (api *API) workspaces(rw http.ResponseWriter, r *http.Request) { apiKey := httpmw.APIKey(r) // Empty strings mean no filter @@ -192,7 +192,7 @@ func (api *api) workspaces(rw http.ResponseWriter, r *http.Request) { httpapi.Write(rw, http.StatusOK, apiWorkspaces) } -func (api *api) workspacesByOwner(rw http.ResponseWriter, r *http.Request) { +func (api *API) workspacesByOwner(rw http.ResponseWriter, r *http.Request) { owner := httpmw.UserParam(r) workspaces, err := api.Database.GetWorkspacesWithFilter(r.Context(), database.GetWorkspacesWithFilterParams{ OwnerID: owner.ID, @@ -221,7 +221,7 @@ func (api *api) workspacesByOwner(rw http.ResponseWriter, r *http.Request) { httpapi.Write(rw, http.StatusOK, apiWorkspaces) } -func (api *api) workspaceByOwnerAndName(rw http.ResponseWriter, r *http.Request) { +func (api *API) workspaceByOwnerAndName(rw http.ResponseWriter, r *http.Request) { owner := httpmw.UserParam(r) organization := httpmw.OrganizationParam(r) workspaceName := chi.URLParam(r, "workspacename") @@ -280,7 +280,7 @@ func (api *api) workspaceByOwnerAndName(rw http.ResponseWriter, r *http.Request) } // Create a new workspace for the currently authenticated user. -func (api *api) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Request) { +func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Request) { var createWorkspace codersdk.CreateWorkspaceRequest if !httpapi.Read(rw, r, &createWorkspace) { return @@ -502,7 +502,7 @@ func (api *api) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req convertWorkspaceBuild(workspaceBuild, convertProvisionerJob(templateVersionJob)), template, user)) } -func (api *api) putWorkspaceAutostart(rw http.ResponseWriter, r *http.Request) { +func (api *API) putWorkspaceAutostart(rw http.ResponseWriter, r *http.Request) { workspace := httpmw.WorkspaceParam(r) if !api.Authorize(rw, r, rbac.ActionUpdate, rbac.ResourceWorkspace. InOrg(workspace.OrganizationID).WithOwner(workspace.OwnerID.String()).WithID(workspace.ID.String())) { @@ -539,7 +539,7 @@ func (api *api) putWorkspaceAutostart(rw http.ResponseWriter, r *http.Request) { } } -func (api *api) putWorkspaceTTL(rw http.ResponseWriter, r *http.Request) { +func (api *API) putWorkspaceTTL(rw http.ResponseWriter, r *http.Request) { workspace := httpmw.WorkspaceParam(r) if !api.Authorize(rw, r, rbac.ActionUpdate, rbac.ResourceWorkspace. InOrg(workspace.OrganizationID).WithOwner(workspace.OwnerID.String()).WithID(workspace.ID.String())) { @@ -570,7 +570,7 @@ func (api *api) putWorkspaceTTL(rw http.ResponseWriter, r *http.Request) { } } -func (api *api) watchWorkspace(rw http.ResponseWriter, r *http.Request) { +func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) { workspace := httpmw.WorkspaceParam(r) c, err := websocket.Accept(rw, r, &websocket.AcceptOptions{ From 934b1ffd4909e07ad8af02e3b1e4b2f26f0767d6 Mon Sep 17 00:00:00 2001 From: kylecarbs Date: Thu, 26 May 2022 15:52:30 +0000 Subject: [PATCH 06/26] Add basic proxy logic --- .vscode/settings.json | 3 +- coderd/coderd.go | 8 ++- coderd/database/databasefake/databasefake.go | 16 ++++++ coderd/database/querier.go | 1 + coderd/database/queries.sql.go | 25 +++++++++ coderd/database/queries/workspaceapps.sql | 3 + coderd/workspaceapps.go | 26 ++++++++- coderd/workspaceapps_test.go | 58 ++++++++++++++++++++ codersdk/workspaceapps.go | 10 +++- 9 files changed, 146 insertions(+), 4 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 888ee89c245b6..0d122f9656cc0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,6 @@ { "cSpell.words": [ + "apps", "buildname", "circbuf", "cliflag", @@ -9,7 +10,6 @@ "codersdk", "cronstrue", "devel", - "apps", "drpc", "drpcconn", "drpcmux", @@ -72,6 +72,7 @@ "VMID", "weblinks", "webrtc", + "workspaceapps", "xerrors", "xstate", "yamux" diff --git a/coderd/coderd.go b/coderd/coderd.go index b9115ecf8869f..6366e145373e5 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -2,6 +2,7 @@ package coderd import ( "context" + "crypto/cipher" "crypto/x509" "fmt" "net/http" @@ -47,6 +48,7 @@ type Options struct { // app. Specific routes may have their own limiters. APIRateLimit int AWSCertificates awsidentity.Certificates + Authorizer rbac.Authorizer AzureCertificates x509.VerifyOptions GoogleTokenValidator *idtoken.Validator GithubOAuth2Config *GithubOAuth2Config @@ -54,8 +56,10 @@ type Options struct { SecureAuthCookie bool SSHKeygenAlgorithm gitsshkey.Algorithm TURNServer *turnconn.Server - Authorizer rbac.Authorizer TracerProvider *sdktrace.TracerProvider + // WildcardCipher is used to encrypt session tokens so that authentication + // can be securely transferred to the wildcard host. + WildcardCipher cipher.AEAD } // New constructs a Coder API handler. @@ -342,6 +346,8 @@ func New(options *Options) *API { }) r.NotFound(site.DefaultHandler().ServeHTTP) + // /workspaceapps/auth + return api } diff --git a/coderd/database/databasefake/databasefake.go b/coderd/database/databasefake/databasefake.go index 8ebbb9eaf62f0..d388291474c1d 100644 --- a/coderd/database/databasefake/databasefake.go +++ b/coderd/database/databasefake/databasefake.go @@ -1043,6 +1043,22 @@ func (q *fakeQuerier) GetWorkspaceAgentsByResourceIDs(_ context.Context, resourc return workspaceAgents, nil } +func (q *fakeQuerier) GetWorkspaceAppByAgentIDAndName(ctx context.Context, arg database.GetWorkspaceAppByAgentIDAndNameParams) (database.WorkspaceApp, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + for _, app := range q.workspaceApps { + if app.AgentID != arg.AgentID { + continue + } + if app.Name != arg.Name { + continue + } + return app, nil + } + return database.WorkspaceApp{}, sql.ErrNoRows +} + func (q *fakeQuerier) GetProvisionerDaemonByID(_ context.Context, id uuid.UUID) (database.ProvisionerDaemon, error) { q.mutex.RLock() defer q.mutex.RUnlock() diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 852e9d27570b6..98e03401d19d3 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -61,6 +61,7 @@ type querier interface { GetWorkspaceAgentByID(ctx context.Context, id uuid.UUID) (WorkspaceAgent, error) GetWorkspaceAgentByInstanceID(ctx context.Context, authInstanceID string) (WorkspaceAgent, error) GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgent, error) + GetWorkspaceAppByAgentIDAndName(ctx context.Context, arg GetWorkspaceAppByAgentIDAndNameParams) (WorkspaceApp, error) GetWorkspaceAppsByAgentID(ctx context.Context, agentID uuid.UUID) ([]WorkspaceApp, error) GetWorkspaceAppsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceApp, error) GetWorkspaceBuildByID(ctx context.Context, id uuid.UUID) (WorkspaceBuild, error) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 26d6a3f78ffc6..40f05ace10705 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -2749,6 +2749,31 @@ func (q *sqlQuerier) UpdateWorkspaceAgentConnectionByID(ctx context.Context, arg return err } +const getWorkspaceAppByAgentIDAndName = `-- name: GetWorkspaceAppByAgentIDAndName :one +SELECT id, created_at, agent_id, name, icon, command, url, relative_path FROM workspace_apps WHERE agent_id = $1 AND name = $2 +` + +type GetWorkspaceAppByAgentIDAndNameParams struct { + AgentID uuid.UUID `db:"agent_id" json:"agent_id"` + Name string `db:"name" json:"name"` +} + +func (q *sqlQuerier) GetWorkspaceAppByAgentIDAndName(ctx context.Context, arg GetWorkspaceAppByAgentIDAndNameParams) (WorkspaceApp, error) { + row := q.db.QueryRowContext(ctx, getWorkspaceAppByAgentIDAndName, arg.AgentID, arg.Name) + var i WorkspaceApp + err := row.Scan( + &i.ID, + &i.CreatedAt, + &i.AgentID, + &i.Name, + &i.Icon, + &i.Command, + &i.Url, + &i.RelativePath, + ) + return i, err +} + const getWorkspaceAppsByAgentID = `-- name: GetWorkspaceAppsByAgentID :many SELECT id, created_at, agent_id, name, icon, command, url, relative_path FROM workspace_apps WHERE agent_id = $1 ` diff --git a/coderd/database/queries/workspaceapps.sql b/coderd/database/queries/workspaceapps.sql index 4b63ba1be5580..61898d1caf56c 100644 --- a/coderd/database/queries/workspaceapps.sql +++ b/coderd/database/queries/workspaceapps.sql @@ -4,6 +4,9 @@ SELECT * FROM workspace_apps WHERE agent_id = $1; -- name: GetWorkspaceAppsByAgentIDs :many SELECT * FROM workspace_apps WHERE agent_id = ANY(@ids :: uuid [ ]); +-- name: GetWorkspaceAppByAgentIDAndName :one +SELECT * FROM workspace_apps WHERE agent_id = $1 AND name = $2; + -- name: InsertWorkspaceApp :one INSERT INTO workspace_apps ( diff --git a/coderd/workspaceapps.go b/coderd/workspaceapps.go index 0c2aa8f342bf7..4ee8a84320de2 100644 --- a/coderd/workspaceapps.go +++ b/coderd/workspaceapps.go @@ -2,8 +2,32 @@ package coderd import ( "net/http" + + "github.com/coder/coder/coderd/database" + "github.com/google/uuid" ) -func (api *API) proxyPath(rw http.ResponseWriter, r *http.Request) { +// workspaceAppsAuthWildcard authenticates the wildcard domain. +func (api *API) workspaceAppsAuthWildcard(rw http.ResponseWriter, r *http.Request) { + // r.URL.Query().Get("redirect") + +} + +func (api *API) workspaceAppsProxyWildcard(rw http.ResponseWriter, r *http.Request) { + +} +func (api *API) workspaceAppsProxyPath(rw http.ResponseWriter, r *http.Request) { + conn, err := api.dialWorkspaceAgent(r, uuid.Nil) + if err != nil { + return + } + app, err := api.Database.GetWorkspaceAppByAgentIDAndName(r.Context(), database.GetWorkspaceAppByAgentIDAndNameParams{ + AgentID: uuid.Nil, + Name: "something", + }) + if err != nil { + return + } + conn.DialContext(r.Context(), "tcp", "localhost:3000") } diff --git a/coderd/workspaceapps_test.go b/coderd/workspaceapps_test.go index a1bbc0bfabd3f..7c0d68b8934c1 100644 --- a/coderd/workspaceapps_test.go +++ b/coderd/workspaceapps_test.go @@ -1 +1,59 @@ package coderd_test + +import ( + "testing" + + "cdr.dev/slog/sloggers/slogtest" + "github.com/coder/coder/agent" + "github.com/coder/coder/coderd/coderdtest" + "github.com/coder/coder/codersdk" + "github.com/coder/coder/provisioner/echo" + "github.com/coder/coder/provisionersdk/proto" + "github.com/google/uuid" +) + +func TestWorkspaceAppsProxyPath(t *testing.T) { + t.Parallel() + t.Run("Proxies", func(t *testing.T) { + t.Parallel() + client, coderAPI := coderdtest.NewWithAPI(t, nil) + user := coderdtest.CreateFirstUser(t, client) + daemonCloser := coderdtest.NewProvisionerDaemon(t, coderAPI) + authToken := uuid.NewString() + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ + Parse: echo.ParseComplete, + ProvisionDryRun: echo.ProvisionComplete, + Provision: []*proto.Provision_Response{{ + Type: &proto.Provision_Response_Complete{ + Complete: &proto.Provision_Complete{ + Resources: []*proto.Resource{{ + Name: "example", + Type: "aws_instance", + Agents: []*proto.Agent{{ + Id: uuid.NewString(), + Auth: &proto.Agent_Token{ + Token: authToken, + }, + }}, + }}, + }, + }, + }}, + }) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + coderdtest.AwaitTemplateVersionJob(t, client, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) + coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) + daemonCloser.Close() + + agentClient := codersdk.New(client.URL) + agentClient.SessionToken = authToken + agentCloser := agent.New(agentClient.ListenWorkspaceAgent, &agent.Options{ + Logger: slogtest.Make(t, nil), + }) + t.Cleanup(func() { + _ = agentCloser.Close() + }) + resources := coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID) + }) +} diff --git a/codersdk/workspaceapps.go b/codersdk/workspaceapps.go index ca46e83a33e64..c6e10514a9b6c 100644 --- a/codersdk/workspaceapps.go +++ b/codersdk/workspaceapps.go @@ -1,6 +1,10 @@ package codersdk -import "github.com/google/uuid" +import ( + "context" + + "github.com/google/uuid" +) type WorkspaceApp struct { ID uuid.UUID `json:"id"` @@ -14,3 +18,7 @@ type WorkspaceApp struct { // an icon to be displayed in the dashboard. Icon string `json:"icon"` } + +func (c *Client) ProxyWorkspaceApplication(ctx context.Context) { + +} From 866eeed5970424939bec2b1618ce25a573ff7ad3 Mon Sep 17 00:00:00 2001 From: kylecarbs Date: Fri, 27 May 2022 13:29:04 +0000 Subject: [PATCH 07/26] Add proxying based on path --- coderd/coderd.go | 14 ++-- coderd/httpmw/wildcard.go | 24 ------ coderd/httpmw/wildcard_test.go | 38 --------- coderd/workspaceagents.go | 9 +-- coderd/workspaceapps.go | 126 +++++++++++++++++++++++++++--- coderd/workspaceapps_test.go | 37 ++++++++- coderd/workspaceresources_test.go | 2 +- 7 files changed, 158 insertions(+), 92 deletions(-) delete mode 100644 coderd/httpmw/wildcard.go delete mode 100644 coderd/httpmw/wildcard_test.go diff --git a/coderd/coderd.go b/coderd/coderd.go index 6366e145373e5..fcce5319ed2b2 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -2,7 +2,6 @@ package coderd import ( "context" - "crypto/cipher" "crypto/x509" "fmt" "net/http" @@ -36,11 +35,10 @@ import ( // Options are requires parameters for Coder to start. type Options struct { - AccessURL *url.URL - WildcardURL *url.URL - Logger slog.Logger - Database database.Store - Pubsub database.Pubsub + AccessURL *url.URL + Logger slog.Logger + Database database.Store + Pubsub database.Pubsub AgentConnectionUpdateFrequency time.Duration // APIRateLimit is the minutely throughput rate limit per user or ip. @@ -57,9 +55,6 @@ type Options struct { SSHKeygenAlgorithm gitsshkey.Algorithm TURNServer *turnconn.Server TracerProvider *sdktrace.TracerProvider - // WildcardCipher is used to encrypt session tokens so that authentication - // can be securely transferred to the wildcard host. - WildcardCipher cipher.AEAD } // New constructs a Coder API handler. @@ -109,6 +104,7 @@ func New(options *Options) *API { httpmw.ExtractUserParam(api.Database), authRolesMiddleware, ) + r.Get("/", api.workspaceAppsProxyPath) }) r.Route("/api/v2", func(r chi.Router) { diff --git a/coderd/httpmw/wildcard.go b/coderd/httpmw/wildcard.go deleted file mode 100644 index c2cc5be1f451c..0000000000000 --- a/coderd/httpmw/wildcard.go +++ /dev/null @@ -1,24 +0,0 @@ -package httpmw - -import ( - "net/http" - "strings" -) - -// Wildcard routes to the provided handler if the request host has the suffix of hostname. -func Wildcard(hostname string, handler http.HandlerFunc) func(http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - var ( - ctx = r.Context() - ) - - if !strings.HasSuffix(r.Host, hostname) { - next.ServeHTTP(w, r) - return - } - - handler(w, r.WithContext(ctx)) - }) - } -} diff --git a/coderd/httpmw/wildcard_test.go b/coderd/httpmw/wildcard_test.go deleted file mode 100644 index ba0a7ebd607ca..0000000000000 --- a/coderd/httpmw/wildcard_test.go +++ /dev/null @@ -1,38 +0,0 @@ -package httpmw_test - -import ( - "net/http" - "net/http/httptest" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/coder/coder/coderd/httpmw" -) - -func TestWildcard(t *testing.T) { - t.Parallel() - t.Run("Match", func(t *testing.T) { - t.Parallel() - req := httptest.NewRequest("GET", "http://frogs.bananas.org", nil) - res := httptest.NewRecorder() - httpmw.Wildcard("bananas.org", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - })(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusForbidden) - })).ServeHTTP(res, req) - require.Equal(t, http.StatusOK, res.Result().StatusCode) - }) - - t.Run("Passthrough", func(t *testing.T) { - t.Parallel() - req := httptest.NewRequest("GET", "http://frogs.apples.org", nil) - res := httptest.NewRecorder() - httpmw.Wildcard("bananas.org", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - })(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusForbidden) - })).ServeHTTP(res, req) - require.Equal(t, http.StatusForbidden, res.Result().StatusCode) - }) -} diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index 88e0298d8fb79..2ce79c298f23a 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -462,11 +462,10 @@ func convertApps(dbApps []database.WorkspaceApp) []codersdk.WorkspaceApp { apps := make([]codersdk.WorkspaceApp, 0) for _, dbApp := range dbApps { apps = append(apps, codersdk.WorkspaceApp{ - ID: dbApp.ID, - Name: dbApp.Name, - Command: dbApp.Command.String, - AccessURL: dbApp.Url.String, - Icon: dbApp.Icon, + ID: dbApp.ID, + Name: dbApp.Name, + Command: dbApp.Command.String, + Icon: dbApp.Icon, }) } return apps diff --git a/coderd/workspaceapps.go b/coderd/workspaceapps.go index 4ee8a84320de2..9c3a464f7ab16 100644 --- a/coderd/workspaceapps.go +++ b/coderd/workspaceapps.go @@ -1,33 +1,133 @@ package coderd import ( + "database/sql" + "errors" + "fmt" "net/http" + "net/http/httputil" + "net/url" + "strings" - "github.com/coder/coder/coderd/database" + "github.com/go-chi/chi/v5" "github.com/google/uuid" -) -// workspaceAppsAuthWildcard authenticates the wildcard domain. -func (api *API) workspaceAppsAuthWildcard(rw http.ResponseWriter, r *http.Request) { - // r.URL.Query().Get("redirect") + "github.com/coder/coder/coderd/database" + "github.com/coder/coder/coderd/httpapi" + "github.com/coder/coder/coderd/httpmw" +) -} +func (api *API) workspaceAppsProxyPath(rw http.ResponseWriter, r *http.Request) { + user := httpmw.UserParam(r) + // This can be in the form of: ".[workspace-agent]" or "" + workspaceWithAgent := chi.URLParam(r, "workspaceagent") + workspaceParts := strings.Split(workspaceWithAgent, ".") -func (api *API) workspaceAppsProxyWildcard(rw http.ResponseWriter, r *http.Request) { + workspace, err := api.Database.GetWorkspaceByOwnerIDAndName(r.Context(), database.GetWorkspaceByOwnerIDAndNameParams{ + OwnerID: user.ID, + Name: workspaceParts[0], + }) + if errors.Is(err, sql.ErrNoRows) { + httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ + Message: "workspace not found", + }) + return + } + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get workspace: %s", err), + }) + return + } -} + build, err := api.Database.GetLatestWorkspaceBuildByWorkspaceID(r.Context(), workspace.ID) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get workspace build: %s", err), + }) + return + } -func (api *API) workspaceAppsProxyPath(rw http.ResponseWriter, r *http.Request) { - conn, err := api.dialWorkspaceAgent(r, uuid.Nil) + resources, err := api.Database.GetWorkspaceResourcesByJobID(r.Context(), build.JobID) if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get workspace resources: %s", err), + }) return } + resourceIDs := make([]uuid.UUID, 0) + for _, resource := range resources { + resourceIDs = append(resourceIDs, resource.ID) + } + agents, err := api.Database.GetWorkspaceAgentsByResourceIDs(r.Context(), resourceIDs) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get workspace agents: %s", err), + }) + return + } + if len(agents) == 0 { + httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ + Message: "no agents exist", + }) + } + + agent := agents[0] + if len(workspaceParts) > 1 { + for _, otherAgent := range agents { + if otherAgent.Name == workspaceParts[1] { + agent = otherAgent + break + } + } + } + app, err := api.Database.GetWorkspaceAppByAgentIDAndName(r.Context(), database.GetWorkspaceAppByAgentIDAndNameParams{ - AgentID: uuid.Nil, - Name: "something", + AgentID: agent.ID, + Name: chi.URLParam(r, "application"), }) + if errors.Is(err, sql.ErrNoRows) { + httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ + Message: "application not found", + }) + return + } if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get workspace app: %s", err), + }) return } - conn.DialContext(r.Context(), "tcp", "localhost:3000") + if !app.Url.Valid { + httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ + Message: fmt.Sprintf("application does not have a url: %s", err), + }) + return + } + + appURL, err := url.Parse(app.Url.String) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("parse app url: %s", err), + }) + return + } + + conn, err := api.dialWorkspaceAgent(r, agent.ID) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("dial workspace agent: %s", err), + }) + return + } + + proxy := httputil.NewSingleHostReverseProxy(appURL) + defaultTransport, valid := http.DefaultTransport.(*http.Transport) + if !valid { + panic("dev error: default transport isn't a transport") + } + transport := defaultTransport.Clone() + transport.DialContext = conn.DialContext + proxy.Transport = transport + proxy.ServeHTTP(rw, r) } diff --git a/coderd/workspaceapps_test.go b/coderd/workspaceapps_test.go index 7c0d68b8934c1..c3e6c344f1f39 100644 --- a/coderd/workspaceapps_test.go +++ b/coderd/workspaceapps_test.go @@ -1,21 +1,43 @@ package coderd_test import ( + "context" + "fmt" + "io" + "net" + "net/http" "testing" + "github.com/google/uuid" + "github.com/stretchr/testify/require" + "cdr.dev/slog/sloggers/slogtest" "github.com/coder/coder/agent" "github.com/coder/coder/coderd/coderdtest" "github.com/coder/coder/codersdk" "github.com/coder/coder/provisioner/echo" "github.com/coder/coder/provisionersdk/proto" - "github.com/google/uuid" ) func TestWorkspaceAppsProxyPath(t *testing.T) { t.Parallel() t.Run("Proxies", func(t *testing.T) { t.Parallel() + // #nosec + ln, err := net.Listen("tcp", ":0") + require.NoError(t, err) + server := http.Server{ + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }), + } + t.Cleanup(func() { + _ = server.Close() + _ = ln.Close() + }) + go server.Serve(ln) + tcpAddr, _ := ln.Addr().(*net.TCPAddr) + client, coderAPI := coderdtest.NewWithAPI(t, nil) user := coderdtest.CreateFirstUser(t, client) daemonCloser := coderdtest.NewProvisionerDaemon(t, coderAPI) @@ -34,6 +56,10 @@ func TestWorkspaceAppsProxyPath(t *testing.T) { Auth: &proto.Agent_Token{ Token: authToken, }, + Apps: []*proto.App{{ + Name: "example", + Url: fmt.Sprintf("http://127.0.0.1:%d", tcpAddr.Port), + }}, }}, }}, }, @@ -54,6 +80,13 @@ func TestWorkspaceAppsProxyPath(t *testing.T) { t.Cleanup(func() { _ = agentCloser.Close() }) - resources := coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID) + coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID) + + resp, err := client.Request(context.Background(), http.MethodGet, "/me/"+workspace.Name+"/example", nil) + require.NoError(t, err) + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Equal(t, "", string(body)) + require.Equal(t, http.StatusOK, resp.StatusCode) }) } diff --git a/coderd/workspaceresources_test.go b/coderd/workspaceresources_test.go index f448c99e51fbd..9a1343b6ca03f 100644 --- a/coderd/workspaceresources_test.go +++ b/coderd/workspaceresources_test.go @@ -46,7 +46,7 @@ func TestWorkspaceResource(t *testing.T) { t.Run("Apps", func(t *testing.T) { t.Parallel() - _, client, coderd := coderdtest.NewWithServer(t, nil) + client, coderd := coderdtest.NewWithAPI(t, nil) user := coderdtest.CreateFirstUser(t, client) coderdtest.NewProvisionerDaemon(t, coderd) app := &proto.App{ From b4f961563f0ad185bb7b8fcfc0e8a29028c6ac47 Mon Sep 17 00:00:00 2001 From: kylecarbs Date: Fri, 27 May 2022 14:01:20 +0000 Subject: [PATCH 08/26] Add app proxying for wildcards --- coderd/coderd.go | 2 +- coderd/workspaceapps.go | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/coderd/coderd.go b/coderd/coderd.go index 9e371e80403e8..0cbb5f99c6878 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -104,7 +104,7 @@ func New(options *Options) *API { httpmw.ExtractUserParam(api.Database), authRolesMiddleware, ) - r.Get("/", api.workspaceAppsProxyPath) + r.Get("/*", api.workspaceAppsProxyPath) }) r.Route("/api/v2", func(r chi.Router) { diff --git a/coderd/workspaceapps.go b/coderd/workspaceapps.go index 9c3a464f7ab16..9b408dd200355 100644 --- a/coderd/workspaceapps.go +++ b/coderd/workspaceapps.go @@ -126,8 +126,10 @@ func (api *API) workspaceAppsProxyPath(rw http.ResponseWriter, r *http.Request) if !valid { panic("dev error: default transport isn't a transport") } + transport := defaultTransport.Clone() transport.DialContext = conn.DialContext proxy.Transport = transport + r.URL.Path = chi.URLParam(r, "*") proxy.ServeHTTP(rw, r) } From c88df46ee2dc761a586aa2399c65b14af328bfe6 Mon Sep 17 00:00:00 2001 From: kylecarbs Date: Tue, 31 May 2022 14:33:28 +0000 Subject: [PATCH 09/26] Add wsconncache --- .vscode/settings.json | 8 ++ coderd/coderd.go | 12 +- coderd/workspaceagents.go | 4 +- coderd/workspaceapps.go | 12 +- coderd/wsconncache/wsconncache.go | 128 ++++++++++++++++++++ coderd/wsconncache/wsconncache_test.go | 154 +++++++++++++++++++++++++ peer/channel.go | 5 + 7 files changed, 307 insertions(+), 16 deletions(-) create mode 100644 coderd/wsconncache/wsconncache.go create mode 100644 coderd/wsconncache/wsconncache_test.go diff --git a/.vscode/settings.json b/.vscode/settings.json index 623cf47b8ea89..57e86d8300bcd 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,8 @@ { "cSpell.words": [ "apps", + "awsidentity", + "buildinfo", "buildname", "circbuf", "cliflag", @@ -17,6 +19,7 @@ "Dsts", "fatih", "Formik", + "gitsshkey", "goarch", "gographviz", "goleak", @@ -31,6 +34,7 @@ "incpatch", "isatty", "Jobf", + "Keygen", "kirsle", "ldflags", "manifoldco", @@ -55,6 +59,7 @@ "retrier", "rpty", "sdkproto", + "sdktrace", "Signup", "sourcemapped", "stretchr", @@ -67,13 +72,16 @@ "tfjson", "tfstate", "trimprefix", + "turnconn", "typegen", "unconvert", "Untar", "VMID", "weblinks", "webrtc", + "workspaceagent", "workspaceapps", + "wsconncache", "xerrors", "xstate", "yamux" diff --git a/coderd/coderd.go b/coderd/coderd.go index 0cbb5f99c6878..932c20328c579 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -29,6 +29,7 @@ import ( "github.com/coder/coder/coderd/rbac" "github.com/coder/coder/coderd/tracing" "github.com/coder/coder/coderd/turnconn" + "github.com/coder/coder/coderd/wsconncache" "github.com/coder/coder/codersdk" "github.com/coder/coder/site" ) @@ -80,6 +81,7 @@ func New(options *Options) *API { Options: options, Handler: r, } + api.workspaceAgentCache = wsconncache.New(api.dialWorkspaceAgent, 0) apiKeyMiddleware := httpmw.ExtractAPIKey(options.Database, &httpmw.OAuth2Configs{ Github: options.GithubOAuth2Config, @@ -348,18 +350,16 @@ func New(options *Options) *API { }) }) r.NotFound(site.DefaultHandler().ServeHTTP) - - // /workspaceapps/auth - return api } type API struct { *Options - Handler chi.Router - websocketWaitMutex sync.Mutex - websocketWaitGroup sync.WaitGroup + Handler chi.Router + websocketWaitMutex sync.Mutex + websocketWaitGroup sync.WaitGroup + workspaceAgentCache *wsconncache.Cache } // Close waits for all WebSocket connections to drain before returning. diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index 2ce79c298f23a..48a74ebeef39f 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -419,7 +419,9 @@ func (api *API) dialWorkspaceAgent(r *http.Request, agentID uuid.UUID) (*agent.C if err != nil { return nil, xerrors.Errorf("negotiate: %w", err) } - options := &peer.ConnOptions{} + options := &peer.ConnOptions{ + Logger: api.Logger.Named("agent-dialer"), + } options.SettingEngine.SetSrflxAcceptanceMinWait(0) options.SettingEngine.SetRelayAcceptanceMinWait(0) // Use the ProxyDialer for the TURN server. diff --git a/coderd/workspaceapps.go b/coderd/workspaceapps.go index 9b408dd200355..009f71e18c45f 100644 --- a/coderd/workspaceapps.go +++ b/coderd/workspaceapps.go @@ -113,23 +113,17 @@ func (api *API) workspaceAppsProxyPath(rw http.ResponseWriter, r *http.Request) return } - conn, err := api.dialWorkspaceAgent(r, agent.ID) + conn, release, err := api.workspaceAgentCache.Acquire(r, agent.ID) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ Message: fmt.Sprintf("dial workspace agent: %s", err), }) return } + defer release() proxy := httputil.NewSingleHostReverseProxy(appURL) - defaultTransport, valid := http.DefaultTransport.(*http.Transport) - if !valid { - panic("dev error: default transport isn't a transport") - } - - transport := defaultTransport.Clone() - transport.DialContext = conn.DialContext - proxy.Transport = transport + proxy.Transport = conn.HTTPTransport() r.URL.Path = chi.URLParam(r, "*") proxy.ServeHTTP(rw, r) } diff --git a/coderd/wsconncache/wsconncache.go b/coderd/wsconncache/wsconncache.go new file mode 100644 index 0000000000000..7299f33b5bb72 --- /dev/null +++ b/coderd/wsconncache/wsconncache.go @@ -0,0 +1,128 @@ +// Package wsconncache caches workspace agent connections by UUID. +package wsconncache + +import ( + "context" + "net/http" + "sync" + "time" + + "github.com/google/uuid" + "go.uber.org/atomic" + "golang.org/x/xerrors" + + "github.com/coder/coder/agent" +) + +// New creates a new workspace connection cache that closes +// connections after the inactive timeout provided. +// +// Agent connections are cached due to WebRTC negotiation +// taking a few hundred milliseconds. +func New(dialer Dialer, inactiveTimeout time.Duration) *Cache { + if inactiveTimeout == 0 { + inactiveTimeout = 5 * time.Minute + } + return &Cache{ + conns: make(map[uuid.UUID]*Conn), + dialer: dialer, + inactiveTimeout: inactiveTimeout, + } +} + +// Dialer creates a new agent connection by ID. +type Dialer func(r *http.Request, id uuid.UUID) (*agent.Conn, error) + +// Conn wraps an agent connection with a reusable HTTP transport. +type Conn struct { + *agent.Conn + + locks atomic.Uint64 + timeoutMutex sync.Mutex + timeout *time.Timer + timeoutCancel context.CancelFunc + transport *http.Transport +} + +func (c *Conn) HTTPTransport() *http.Transport { + return c.transport +} + +// Close ends the HTTP transport if exists, and closes the agent. +func (c *Conn) Close() error { + if c.transport != nil { + c.transport.CloseIdleConnections() + } + if c.timeout != nil { + c.timeout.Stop() + } + return c.Conn.Close() +} + +type Cache struct { + connMutex sync.RWMutex + conns map[uuid.UUID]*Conn + dialer Dialer + inactiveTimeout time.Duration +} + +func (c *Cache) Acquire(r *http.Request, id uuid.UUID) (*Conn, func(), error) { + c.connMutex.RLock() + conn, exists := c.conns[id] + c.connMutex.RUnlock() + if !exists { + agentConn, err := c.dialer(r, id) + if err != nil { + return nil, nil, xerrors.Errorf("dial: %w", err) + } + timeoutCtx, timeoutCancelFunc := context.WithCancel(context.Background()) + defaultTransport, valid := http.DefaultTransport.(*http.Transport) + if !valid { + panic("dev error: default transport is the wrong type") + } + transport := defaultTransport.Clone() + transport.DialContext = agentConn.DialContext + conn = &Conn{ + Conn: agentConn, + timeoutCancel: timeoutCancelFunc, + transport: transport, + } + go func() { + select { + case <-timeoutCtx.Done(): + _ = conn.CloseWithError(xerrors.New("cache timeout")) + case <-conn.Closed(): + } + c.connMutex.Lock() + delete(c.conns, id) + c.connMutex.Unlock() + }() + c.connMutex.Lock() + c.conns[id] = conn + c.connMutex.Unlock() + } + conn.timeoutMutex.Lock() + defer conn.timeoutMutex.Unlock() + if conn.timeout != nil { + conn.timeout.Stop() + } + conn.locks.Inc() + return conn, func() { + conn.locks.Dec() + conn.timeoutMutex.Lock() + defer conn.timeoutMutex.Unlock() + if conn.timeout != nil { + conn.timeout.Stop() + } + conn.timeout = time.AfterFunc(c.inactiveTimeout, conn.timeoutCancel) + }, nil +} + +func (c *Cache) Close() error { + c.connMutex.Lock() + defer c.connMutex.Unlock() + for _, conn := range c.conns { + _ = conn.Close() + } + return nil +} diff --git a/coderd/wsconncache/wsconncache_test.go b/coderd/wsconncache/wsconncache_test.go new file mode 100644 index 0000000000000..7f4300a25f744 --- /dev/null +++ b/coderd/wsconncache/wsconncache_test.go @@ -0,0 +1,154 @@ +package wsconncache_test + +import ( + "context" + "fmt" + "net" + "net/http" + "net/http/httptest" + "net/http/httputil" + "net/url" + "testing" + "time" + + "github.com/google/uuid" + "github.com/pion/webrtc/v3" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/atomic" + "go.uber.org/goleak" + + "cdr.dev/slog" + "cdr.dev/slog/sloggers/slogtest" + "github.com/coder/coder/agent" + "github.com/coder/coder/coderd/wsconncache" + "github.com/coder/coder/peer" + "github.com/coder/coder/peerbroker" + "github.com/coder/coder/peerbroker/proto" + "github.com/coder/coder/provisionersdk" +) + +func TestMain(m *testing.M) { + goleak.VerifyTestMain(m) +} + +func TestCache(t *testing.T) { + t.Parallel() + t.Run("Cache", func(t *testing.T) { + t.Parallel() + cache := wsconncache.New(func(r *http.Request, id uuid.UUID) (*agent.Conn, error) { + return setupAgent(t, agent.Metadata{}, 0), nil + }, 0) + t.Cleanup(func() { + _ = cache.Close() + }) + _, _, err := cache.Acquire(httptest.NewRequest(http.MethodGet, "/", nil), uuid.Nil) + require.NoError(t, err) + }) + t.Run("Expire", func(t *testing.T) { + t.Parallel() + called := atomic.NewInt32(0) + cache := wsconncache.New(func(r *http.Request, id uuid.UUID) (*agent.Conn, error) { + called.Add(1) + return setupAgent(t, agent.Metadata{}, 0), nil + }, time.Microsecond) + t.Cleanup(func() { + _ = cache.Close() + }) + conn, release, err := cache.Acquire(httptest.NewRequest(http.MethodGet, "/", nil), uuid.Nil) + require.NoError(t, err) + release() + <-conn.Closed() + conn, release, err = cache.Acquire(httptest.NewRequest(http.MethodGet, "/", nil), uuid.Nil) + require.NoError(t, err) + release() + <-conn.Closed() + require.Equal(t, int32(2), called.Load()) + }) + t.Run("NoExpireWhenLocked", func(t *testing.T) { + t.Parallel() + cache := wsconncache.New(func(r *http.Request, id uuid.UUID) (*agent.Conn, error) { + return setupAgent(t, agent.Metadata{}, 0), nil + }, time.Microsecond) + t.Cleanup(func() { + _ = cache.Close() + }) + conn, release, err := cache.Acquire(httptest.NewRequest(http.MethodGet, "/", nil), uuid.Nil) + require.NoError(t, err) + time.Sleep(time.Millisecond) + release() + <-conn.Closed() + }) + t.Run("HTTPTransport", func(t *testing.T) { + t.Parallel() + random, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + t.Cleanup(func() { + _ = random.Close() + }) + tcpAddr, valid := random.Addr().(*net.TCPAddr) + require.True(t, valid) + + server := &http.Server{ + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }), + } + go server.Serve(random) + + cache := wsconncache.New(func(r *http.Request, id uuid.UUID) (*agent.Conn, error) { + return setupAgent(t, agent.Metadata{}, 0), nil + }, time.Microsecond) + t.Cleanup(func() { + _ = cache.Close() + }) + req := httptest.NewRequest(http.MethodGet, "/", nil) + conn, release, err := cache.Acquire(req, uuid.Nil) + require.NoError(t, err) + t.Cleanup(release) + proxy := httputil.NewSingleHostReverseProxy(&url.URL{ + Scheme: "http", + Host: fmt.Sprintf("127.0.0.1:%d", tcpAddr.Port), + Path: "/", + }) + proxy.Transport = conn.HTTPTransport() + res := httptest.NewRecorder() + proxy.ServeHTTP(res, req) + require.Equal(t, http.StatusOK, res.Result().StatusCode) + req = httptest.NewRequest(http.MethodGet, "/", nil) + res = httptest.NewRecorder() + proxy.ServeHTTP(res, req) + require.Equal(t, http.StatusOK, res.Result().StatusCode) + }) +} + +func setupAgent(t *testing.T, metadata agent.Metadata, ptyTimeout time.Duration) *agent.Conn { + client, server := provisionersdk.TransportPipe() + closer := agent.New(func(ctx context.Context, logger slog.Logger) (agent.Metadata, *peerbroker.Listener, error) { + listener, err := peerbroker.Listen(server, nil) + return metadata, listener, err + }, &agent.Options{ + Logger: slogtest.Make(t, nil).Leveled(slog.LevelDebug), + ReconnectingPTYTimeout: ptyTimeout, + }) + t.Cleanup(func() { + _ = client.Close() + _ = server.Close() + _ = closer.Close() + }) + api := proto.NewDRPCPeerBrokerClient(provisionersdk.Conn(client)) + stream, err := api.NegotiateConnection(context.Background()) + assert.NoError(t, err) + conn, err := peerbroker.Dial(stream, []webrtc.ICEServer{}, &peer.ConnOptions{ + Logger: slogtest.Make(t, nil), + }) + require.NoError(t, err) + t.Cleanup(func() { + _ = conn.Close() + }) + + return &agent.Conn{ + Negotiator: api, + Conn: conn, + } +} diff --git a/peer/channel.go b/peer/channel.go index 7db76d984f815..787fe739b6133 100644 --- a/peer/channel.go +++ b/peer/channel.go @@ -257,6 +257,11 @@ func (c *Channel) Label() string { return c.dc.Label() } +// ID returns the label of the underlying DataChannel. +func (c *Channel) ID() uint16 { + return *c.dc.ID() +} + // Protocol returns the protocol of the underlying DataChannel. func (c *Channel) Protocol() string { return c.dc.Protocol() From d327df7f4283354eb62aaaf2e791f07a7cda5ee2 Mon Sep 17 00:00:00 2001 From: kylecarbs Date: Tue, 31 May 2022 14:39:24 +0000 Subject: [PATCH 10/26] fix: Race when writing to a closed pipe This is such an intermittent race it's difficult to track, but regardless this is an improvement to the code. --- provisionersdk/serve.go | 6 +++--- provisionersdk/transport.go | 18 +++++++++++++----- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/provisionersdk/serve.go b/provisionersdk/serve.go index 1fbe50d506850..fdb277fc98a07 100644 --- a/provisionersdk/serve.go +++ b/provisionersdk/serve.go @@ -31,9 +31,9 @@ func Serve(ctx context.Context, server proto.DRPCProvisionerServer, options *Ser if options.Listener == nil { config := yamux.DefaultConfig() config.LogOutput = io.Discard - stdio, err := yamux.Server(readWriteCloser{ - ReadCloser: os.Stdin, - Writer: os.Stdout, + stdio, err := yamux.Server(&readWriteCloser{ + ReadCloser: os.Stdin, + WriteCloser: os.Stdout, }, config) if err != nil { return xerrors.Errorf("create yamux: %w", err) diff --git a/provisionersdk/transport.go b/provisionersdk/transport.go index 8e1a0069cf17a..a5eb43db07a36 100644 --- a/provisionersdk/transport.go +++ b/provisionersdk/transport.go @@ -22,16 +22,16 @@ func TransportPipe() (*yamux.Session, *yamux.Session) { yamuxConfig := yamux.DefaultConfig() yamuxConfig.LogOutput = io.Discard client, err := yamux.Client(&readWriteCloser{ - ReadCloser: clientReader, - Writer: serverWriter, + ReadCloser: clientReader, + WriteCloser: serverWriter, }, yamuxConfig) if err != nil { panic(err) } server, err := yamux.Server(&readWriteCloser{ - ReadCloser: serverReader, - Writer: clientWriter, + ReadCloser: serverReader, + WriteCloser: clientWriter, }, yamuxConfig) if err != nil { panic(err) @@ -46,7 +46,15 @@ func Conn(session *yamux.Session) drpc.Conn { type readWriteCloser struct { io.ReadCloser - io.Writer + io.WriteCloser +} + +func (c *readWriteCloser) Close() error { + err := c.ReadCloser.Close() + if err != nil { + return err + } + return c.WriteCloser.Close() } // Allows concurrent requests on a single dRPC connection. From cec2de3291b2dcd179920e00edbb77f40b58c976 Mon Sep 17 00:00:00 2001 From: kylecarbs Date: Tue, 31 May 2022 14:39:24 +0000 Subject: [PATCH 11/26] fix: Race when writing to a closed pipe This is such an intermittent race it's difficult to track, but regardless this is an improvement to the code. --- provisionersdk/serve.go | 9 ++++++--- provisionersdk/transport.go | 18 +++++++++++++----- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/provisionersdk/serve.go b/provisionersdk/serve.go index 1fbe50d506850..557ace241da67 100644 --- a/provisionersdk/serve.go +++ b/provisionersdk/serve.go @@ -31,9 +31,9 @@ func Serve(ctx context.Context, server proto.DRPCProvisionerServer, options *Ser if options.Listener == nil { config := yamux.DefaultConfig() config.LogOutput = io.Discard - stdio, err := yamux.Server(readWriteCloser{ - ReadCloser: os.Stdin, - Writer: os.Stdout, + stdio, err := yamux.Server(&readWriteCloser{ + ReadCloser: os.Stdin, + WriteCloser: os.Stdout, }, config) if err != nil { return xerrors.Errorf("create yamux: %w", err) @@ -54,6 +54,9 @@ func Serve(ctx context.Context, server proto.DRPCProvisionerServer, options *Ser // short-lived processes that can be executed concurrently. err = srv.Serve(ctx, options.Listener) if err != nil { + if errors.Is(err, io.EOF) { + return nil + } if errors.Is(err, context.Canceled) { return nil } diff --git a/provisionersdk/transport.go b/provisionersdk/transport.go index 8e1a0069cf17a..a5eb43db07a36 100644 --- a/provisionersdk/transport.go +++ b/provisionersdk/transport.go @@ -22,16 +22,16 @@ func TransportPipe() (*yamux.Session, *yamux.Session) { yamuxConfig := yamux.DefaultConfig() yamuxConfig.LogOutput = io.Discard client, err := yamux.Client(&readWriteCloser{ - ReadCloser: clientReader, - Writer: serverWriter, + ReadCloser: clientReader, + WriteCloser: serverWriter, }, yamuxConfig) if err != nil { panic(err) } server, err := yamux.Server(&readWriteCloser{ - ReadCloser: serverReader, - Writer: clientWriter, + ReadCloser: serverReader, + WriteCloser: clientWriter, }, yamuxConfig) if err != nil { panic(err) @@ -46,7 +46,15 @@ func Conn(session *yamux.Session) drpc.Conn { type readWriteCloser struct { io.ReadCloser - io.Writer + io.WriteCloser +} + +func (c *readWriteCloser) Close() error { + err := c.ReadCloser.Close() + if err != nil { + return err + } + return c.WriteCloser.Close() } // Allows concurrent requests on a single dRPC connection. From 8e61caced3d9ceabdb078df8722456ed095e76a0 Mon Sep 17 00:00:00 2001 From: kylecarbs Date: Tue, 31 May 2022 14:39:24 +0000 Subject: [PATCH 12/26] fix: Race when writing to a closed pipe This is such an intermittent race it's difficult to track, but regardless this is an improvement to the code. --- provisionersdk/serve.go | 9 ++++++--- provisionersdk/transport.go | 22 +++++++++++++++++----- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/provisionersdk/serve.go b/provisionersdk/serve.go index 1fbe50d506850..557ace241da67 100644 --- a/provisionersdk/serve.go +++ b/provisionersdk/serve.go @@ -31,9 +31,9 @@ func Serve(ctx context.Context, server proto.DRPCProvisionerServer, options *Ser if options.Listener == nil { config := yamux.DefaultConfig() config.LogOutput = io.Discard - stdio, err := yamux.Server(readWriteCloser{ - ReadCloser: os.Stdin, - Writer: os.Stdout, + stdio, err := yamux.Server(&readWriteCloser{ + ReadCloser: os.Stdin, + WriteCloser: os.Stdout, }, config) if err != nil { return xerrors.Errorf("create yamux: %w", err) @@ -54,6 +54,9 @@ func Serve(ctx context.Context, server proto.DRPCProvisionerServer, options *Ser // short-lived processes that can be executed concurrently. err = srv.Serve(ctx, options.Listener) if err != nil { + if errors.Is(err, io.EOF) { + return nil + } if errors.Is(err, context.Canceled) { return nil } diff --git a/provisionersdk/transport.go b/provisionersdk/transport.go index 8e1a0069cf17a..074f93f92fcc7 100644 --- a/provisionersdk/transport.go +++ b/provisionersdk/transport.go @@ -3,6 +3,7 @@ package provisionersdk import ( "context" "io" + "sync" "github.com/hashicorp/yamux" "storj.io/drpc" @@ -22,16 +23,16 @@ func TransportPipe() (*yamux.Session, *yamux.Session) { yamuxConfig := yamux.DefaultConfig() yamuxConfig.LogOutput = io.Discard client, err := yamux.Client(&readWriteCloser{ - ReadCloser: clientReader, - Writer: serverWriter, + ReadCloser: clientReader, + WriteCloser: serverWriter, }, yamuxConfig) if err != nil { panic(err) } server, err := yamux.Server(&readWriteCloser{ - ReadCloser: serverReader, - Writer: clientWriter, + ReadCloser: serverReader, + WriteCloser: clientWriter, }, yamuxConfig) if err != nil { panic(err) @@ -45,8 +46,19 @@ func Conn(session *yamux.Session) drpc.Conn { } type readWriteCloser struct { + closeMutex sync.Mutex io.ReadCloser - io.Writer + io.WriteCloser +} + +func (c *readWriteCloser) Close() error { + c.closeMutex.Lock() + defer c.closeMutex.Unlock() + err := c.ReadCloser.Close() + if err != nil { + return err + } + return c.WriteCloser.Close() } // Allows concurrent requests on a single dRPC connection. From 46b24f78ab537fba6d5e43ce6ee9c5dbbb3e73bb Mon Sep 17 00:00:00 2001 From: kylecarbs Date: Tue, 31 May 2022 14:39:24 +0000 Subject: [PATCH 13/26] fix: Race when writing to a closed pipe This is such an intermittent race it's difficult to track, but regardless this is an improvement to the code. --- provisionersdk/serve.go | 10 +++++++++- provisionersdk/transport.go | 20 ++++---------------- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/provisionersdk/serve.go b/provisionersdk/serve.go index 1fbe50d506850..0ecbf4d841eec 100644 --- a/provisionersdk/serve.go +++ b/provisionersdk/serve.go @@ -31,7 +31,7 @@ func Serve(ctx context.Context, server proto.DRPCProvisionerServer, options *Ser if options.Listener == nil { config := yamux.DefaultConfig() config.LogOutput = io.Discard - stdio, err := yamux.Server(readWriteCloser{ + stdio, err := yamux.Server(&readWriteCloser{ ReadCloser: os.Stdin, Writer: os.Stdout, }, config) @@ -54,6 +54,9 @@ func Serve(ctx context.Context, server proto.DRPCProvisionerServer, options *Ser // short-lived processes that can be executed concurrently. err = srv.Serve(ctx, options.Listener) if err != nil { + if errors.Is(err, io.EOF) { + return nil + } if errors.Is(err, context.Canceled) { return nil } @@ -67,3 +70,8 @@ func Serve(ctx context.Context, server proto.DRPCProvisionerServer, options *Ser } return nil } + +type readWriteCloser struct { + io.ReadCloser + io.Writer +} diff --git a/provisionersdk/transport.go b/provisionersdk/transport.go index 8e1a0069cf17a..39246622e6ce7 100644 --- a/provisionersdk/transport.go +++ b/provisionersdk/transport.go @@ -3,6 +3,7 @@ package provisionersdk import ( "context" "io" + "net" "github.com/hashicorp/yamux" "storj.io/drpc" @@ -17,22 +18,14 @@ const ( // TransportPipe creates an in-memory pipe for dRPC transport. func TransportPipe() (*yamux.Session, *yamux.Session) { - clientReader, clientWriter := io.Pipe() - serverReader, serverWriter := io.Pipe() + c1, c2 := net.Pipe() yamuxConfig := yamux.DefaultConfig() yamuxConfig.LogOutput = io.Discard - client, err := yamux.Client(&readWriteCloser{ - ReadCloser: clientReader, - Writer: serverWriter, - }, yamuxConfig) + client, err := yamux.Client(c1, yamuxConfig) if err != nil { panic(err) } - - server, err := yamux.Server(&readWriteCloser{ - ReadCloser: serverReader, - Writer: clientWriter, - }, yamuxConfig) + server, err := yamux.Server(c2, yamuxConfig) if err != nil { panic(err) } @@ -44,11 +37,6 @@ func Conn(session *yamux.Session) drpc.Conn { return &multiplexedDRPC{session} } -type readWriteCloser struct { - io.ReadCloser - io.Writer -} - // Allows concurrent requests on a single dRPC connection. // Required for calling functions concurrently. type multiplexedDRPC struct { From e9b74631893ca0721ee08f2d5cddffd6e5437223 Mon Sep 17 00:00:00 2001 From: kylecarbs Date: Fri, 3 Jun 2022 02:46:34 +0000 Subject: [PATCH 14/26] Add workspace route proxying endpoint - Makes the workspace conn cache concurrency-safe - Reduces unnecessary open checks in `peer.Channel` - Fixes the use of a temporary context when dialing a workspace agent --- agent/conn.go | 2 +- coderd/coderd.go | 11 +- coderd/coderdtest/coderdtest.go | 2 +- coderd/database/databasefake/databasefake.go | 2 +- coderd/workspaceagents.go | 16 ++- coderd/workspaceapps.go | 17 ++- coderd/workspaceapps_test.go | 124 ++++++++++--------- coderd/wsconncache/wsconncache.go | 120 +++++++++++------- coderd/wsconncache/wsconncache_test.go | 65 ++++++---- codersdk/workspaceapps.go | 6 - peer/channel.go | 44 ++----- peer/conn.go | 6 + site/src/api/typesGenerated.ts | 14 ++- 13 files changed, 247 insertions(+), 182 deletions(-) diff --git a/agent/conn.go b/agent/conn.go index d44d6d0c0b0d8..0be45bc05c33e 100644 --- a/agent/conn.go +++ b/agent/conn.go @@ -102,7 +102,7 @@ func (c *Conn) DialContext(ctx context.Context, network string, addr string) (ne var res dialResponse err = dec.Decode(&res) if err != nil { - return nil, xerrors.Errorf("failed to decode initial packet: %w", err) + return nil, xerrors.Errorf("decode agent dial response: %w", err) } if res.Error != "" { _ = channel.Close() diff --git a/coderd/coderd.go b/coderd/coderd.go index 5016fa4ba9007..fa7ac5d8d7bd5 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -15,8 +15,6 @@ import ( "golang.org/x/xerrors" "google.golang.org/api/idtoken" - "github.com/go-chi/cors" - sdktrace "go.opentelemetry.io/otel/sdk/trace" "cdr.dev/slog" @@ -97,7 +95,7 @@ func New(options *Options) *API { tracing.HTTPMW(api.TracerProvider, "coderd.http"), ) - r.Route("/{user}/{workspaceagent}/{application}", func(r chi.Router) { + r.Route("/@{user}/{workspaceagent}/apps/{application}", func(r chi.Router) { r.Use( httpmw.RateLimitPerMinute(options.APIRateLimit), apiKeyMiddleware, @@ -327,9 +325,6 @@ func New(options *Options) *API { r.Put("/extend", api.putExtendWorkspace) }) }) - r.Route("/wildcardauth", func(r chi.Router) { - r.Use(cors.Handler(cors.Options{})) - }) r.Route("/workspacebuilds/{workspacebuild}", func(r chi.Router) { r.Use( apiKeyMiddleware, @@ -357,10 +352,12 @@ type API struct { } // Close waits for all WebSocket connections to drain before returning. -func (api *API) Close() { +func (api *API) Close() error { api.websocketWaitMutex.Lock() api.websocketWaitGroup.Wait() api.websocketWaitMutex.Unlock() + + return api.workspaceAgentCache.Close() } func debugLogRequest(log slog.Logger) func(http.Handler) http.Handler { diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index 48fe3b81deb72..29624b196954d 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -172,7 +172,7 @@ func NewWithAPI(t *testing.T, options *Options) (*codersdk.Client, *coderd.API) cancelFunc() _ = turnServer.Close() srv.Close() - coderAPI.Close() + _ = coderAPI.Close() }) return codersdk.New(serverURL), coderAPI diff --git a/coderd/database/databasefake/databasefake.go b/coderd/database/databasefake/databasefake.go index 52b82807d0d3b..c90e6612e77b0 100644 --- a/coderd/database/databasefake/databasefake.go +++ b/coderd/database/databasefake/databasefake.go @@ -1061,7 +1061,7 @@ func (q *fakeQuerier) GetWorkspaceAgentsByResourceIDs(_ context.Context, resourc return workspaceAgents, nil } -func (q *fakeQuerier) GetWorkspaceAppByAgentIDAndName(ctx context.Context, arg database.GetWorkspaceAppByAgentIDAndNameParams) (database.WorkspaceApp, error) { +func (q *fakeQuerier) GetWorkspaceAppByAgentIDAndName(_ context.Context, arg database.GetWorkspaceAppByAgentIDAndNameParams) (database.WorkspaceApp, error) { q.mutex.RLock() defer q.mutex.RUnlock() diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index 771d10316099b..ccb7ffdaa4e6a 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -1,6 +1,7 @@ package coderd import ( + "context" "database/sql" "encoding/json" "fmt" @@ -382,12 +383,12 @@ func (api *API) workspaceAgentPTY(rw http.ResponseWriter, r *http.Request) { }() // Accept text connections, because it's more developer friendly. wsNetConn := websocket.NetConn(r.Context(), conn, websocket.MessageBinary) - agentConn, err := api.dialWorkspaceAgent(r, workspaceAgent.ID) + agentConn, release, err := api.workspaceAgentCache.Acquire(r, workspaceAgent.ID) if err != nil { _ = conn.Close(websocket.StatusInternalError, httpapi.WebsocketCloseSprintf("dial workspace agent: %s", err)) return } - defer agentConn.Close() + defer release() ptNetConn, err := agentConn.ReconnectingPTY(reconnect.String(), uint16(height), uint16(width), "") if err != nil { _ = conn.Close(websocket.StatusInternalError, httpapi.WebsocketCloseSprintf("dial: %s", err)) @@ -404,8 +405,9 @@ func (api *API) workspaceAgentPTY(rw http.ResponseWriter, r *http.Request) { // dialWorkspaceAgent connects to a workspace agent by ID. func (api *API) dialWorkspaceAgent(r *http.Request, agentID uuid.UUID) (*agent.Conn, error) { client, server := provisionersdk.TransportPipe() + ctx, cancelFunc := context.WithCancel(context.Background()) go func() { - _ = peerbroker.ProxyListen(r.Context(), server, peerbroker.ProxyOptions{ + _ = peerbroker.ProxyListen(ctx, server, peerbroker.ProxyOptions{ ChannelID: agentID.String(), Logger: api.Logger.Named("peerbroker-proxy-dial"), Pubsub: api.Pubsub, @@ -415,8 +417,9 @@ func (api *API) dialWorkspaceAgent(r *http.Request, agentID uuid.UUID) (*agent.C }() peerClient := proto.NewDRPCPeerBrokerClient(provisionersdk.Conn(client)) - stream, err := peerClient.NegotiateConnection(r.Context()) + stream, err := peerClient.NegotiateConnection(ctx) if err != nil { + cancelFunc() return nil, xerrors.Errorf("negotiate: %w", err) } options := &peer.ConnOptions{ @@ -452,8 +455,13 @@ func (api *API) dialWorkspaceAgent(r *http.Request, agentID uuid.UUID) (*agent.C })) peerConn, err := peerbroker.Dial(stream, append(api.ICEServers, turnconn.Proxy), options) if err != nil { + cancelFunc() return nil, xerrors.Errorf("dial: %w", err) } + go func() { + <-peerConn.Closed() + cancelFunc() + }() return &agent.Conn{ Negotiator: peerClient, Conn: peerConn, diff --git a/coderd/workspaceapps.go b/coderd/workspaceapps.go index 009f71e18c45f..7aec86e27fd82 100644 --- a/coderd/workspaceapps.go +++ b/coderd/workspaceapps.go @@ -123,7 +123,22 @@ func (api *API) workspaceAppsProxyPath(rw http.ResponseWriter, r *http.Request) defer release() proxy := httputil.NewSingleHostReverseProxy(appURL) + // Write the error directly using our format! + proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) { + httpapi.Write(w, http.StatusBadGateway, httpapi.Response{ + Message: err.Error(), + }) + } proxy.Transport = conn.HTTPTransport() - r.URL.Path = chi.URLParam(r, "*") + path := chi.URLParam(r, "*") + if !strings.HasSuffix(r.URL.Path, "/") && path == "" { + // Web applications typically request paths relative to the + // root URL. This allows for routing behind a proxy or subpath. + // See https://github.com/coder/code-server/issues/241 for examples. + r.URL.Path += "/" + http.Redirect(rw, r, r.URL.String(), http.StatusTemporaryRedirect) + return + } + r.URL.Path = path proxy.ServeHTTP(rw, r) } diff --git a/coderd/workspaceapps_test.go b/coderd/workspaceapps_test.go index c3e6c344f1f39..dc06573b51189 100644 --- a/coderd/workspaceapps_test.go +++ b/coderd/workspaceapps_test.go @@ -21,69 +21,81 @@ import ( func TestWorkspaceAppsProxyPath(t *testing.T) { t.Parallel() - t.Run("Proxies", func(t *testing.T) { - t.Parallel() - // #nosec - ln, err := net.Listen("tcp", ":0") - require.NoError(t, err) - server := http.Server{ - Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - }), - } - t.Cleanup(func() { - _ = server.Close() - _ = ln.Close() - }) - go server.Serve(ln) - tcpAddr, _ := ln.Addr().(*net.TCPAddr) + // #nosec + ln, err := net.Listen("tcp", ":0") + require.NoError(t, err) + server := http.Server{ + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }), + } + t.Cleanup(func() { + _ = server.Close() + _ = ln.Close() + }) + go server.Serve(ln) + tcpAddr, _ := ln.Addr().(*net.TCPAddr) - client, coderAPI := coderdtest.NewWithAPI(t, nil) - user := coderdtest.CreateFirstUser(t, client) - daemonCloser := coderdtest.NewProvisionerDaemon(t, coderAPI) - authToken := uuid.NewString() - version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ - Parse: echo.ParseComplete, - ProvisionDryRun: echo.ProvisionComplete, - Provision: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ - Resources: []*proto.Resource{{ - Name: "example", - Type: "aws_instance", - Agents: []*proto.Agent{{ - Id: uuid.NewString(), - Auth: &proto.Agent_Token{ - Token: authToken, - }, - Apps: []*proto.App{{ - Name: "example", - Url: fmt.Sprintf("http://127.0.0.1:%d", tcpAddr.Port), - }}, + client, coderAPI := coderdtest.NewWithAPI(t, nil) + user := coderdtest.CreateFirstUser(t, client) + daemonCloser := coderdtest.NewProvisionerDaemon(t, coderAPI) + authToken := uuid.NewString() + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ + Parse: echo.ParseComplete, + ProvisionDryRun: echo.ProvisionComplete, + Provision: []*proto.Provision_Response{{ + Type: &proto.Provision_Response_Complete{ + Complete: &proto.Provision_Complete{ + Resources: []*proto.Resource{{ + Name: "example", + Type: "aws_instance", + Agents: []*proto.Agent{{ + Id: uuid.NewString(), + Auth: &proto.Agent_Token{ + Token: authToken, + }, + Apps: []*proto.App{{ + Name: "example", + Url: fmt.Sprintf("http://127.0.0.1:%d", tcpAddr.Port), }}, }}, - }, + }}, }, - }}, - }) - template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) - coderdtest.AwaitTemplateVersionJob(t, client, version.ID) - workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) - coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) - daemonCloser.Close() + }, + }}, + }) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + coderdtest.AwaitTemplateVersionJob(t, client, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) + coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) + daemonCloser.Close() - agentClient := codersdk.New(client.URL) - agentClient.SessionToken = authToken - agentCloser := agent.New(agentClient.ListenWorkspaceAgent, &agent.Options{ - Logger: slogtest.Make(t, nil), - }) - t.Cleanup(func() { - _ = agentCloser.Close() - }) - coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID) + agentClient := codersdk.New(client.URL) + agentClient.SessionToken = authToken + agentCloser := agent.New(agentClient.ListenWorkspaceAgent, &agent.Options{ + Logger: slogtest.Make(t, nil), + }) + t.Cleanup(func() { + _ = agentCloser.Close() + }) + coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID) + client.HTTPClient.CheckRedirect = func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + } - resp, err := client.Request(context.Background(), http.MethodGet, "/me/"+workspace.Name+"/example", nil) + t.Run("RedirectsWithSlash", func(t *testing.T) { + t.Parallel() + resp, err := client.Request(context.Background(), http.MethodGet, "/@me/"+workspace.Name+"/apps/example", nil) + require.NoError(t, err) + defer resp.Body.Close() + require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode) + }) + + t.Run("Proxies", func(t *testing.T) { + t.Parallel() + resp, err := client.Request(context.Background(), http.MethodGet, "/@me/"+workspace.Name+"/apps/example/", nil) require.NoError(t, err) + defer resp.Body.Close() body, err := io.ReadAll(resp.Body) require.NoError(t, err) require.Equal(t, "", string(body)) diff --git a/coderd/wsconncache/wsconncache.go b/coderd/wsconncache/wsconncache.go index aad1f514fe207..f87c3b044f5b7 100644 --- a/coderd/wsconncache/wsconncache.go +++ b/coderd/wsconncache/wsconncache.go @@ -9,6 +9,7 @@ import ( "github.com/google/uuid" "go.uber.org/atomic" + "golang.org/x/sync/singleflight" "golang.org/x/xerrors" "github.com/coder/coder/agent" @@ -24,7 +25,7 @@ func New(dialer Dialer, inactiveTimeout time.Duration) *Cache { inactiveTimeout = 5 * time.Minute } return &Cache{ - conns: make(map[uuid.UUID]*Conn), + closed: make(chan struct{}), dialer: dialer, inactiveTimeout: inactiveTimeout, } @@ -48,61 +49,84 @@ func (c *Conn) HTTPTransport() *http.Transport { return c.transport } -// Close ends the HTTP transport if exists, and closes the agent. -func (c *Conn) Close() error { +// CloseWithError ends the HTTP transport if exists, and closes the agent. +func (c *Conn) CloseWithError(err error) error { if c.transport != nil { c.transport.CloseIdleConnections() } if c.timeout != nil { c.timeout.Stop() } - return c.Conn.Close() + return c.Conn.CloseWithError(err) } type Cache struct { - connMutex sync.RWMutex - conns map[uuid.UUID]*Conn + closed chan struct{} + closeMutex sync.Mutex + closeGroup sync.WaitGroup + connGroup singleflight.Group + connMap sync.Map dialer Dialer inactiveTimeout time.Duration } +// Acquire gets or establishes a connection with the dialer using the ID provided. +// If a connection is in-progress, that connection or error will be returned. +// +// The returned function is used to release a lock on the connection. Once zero +// locks exist on a connection, the inactive timeout will begin to tick down. +// After the time expires, the connection will be cleared from the cache. func (c *Cache) Acquire(r *http.Request, id uuid.UUID) (*Conn, func(), error) { - c.connMutex.RLock() - conn, exists := c.conns[id] - c.connMutex.RUnlock() - if !exists { - agentConn, err := c.dialer(r, id) + rawConn, found := c.connMap.Load(id.String()) + // If the connection isn't found, establish a new one! + if !found { + var err error + // A singleflight group is used to allow for concurrent requests to the + // same identifier to resolve. + rawConn, err, _ = c.connGroup.Do(id.String(), func() (interface{}, error) { + agentConn, err := c.dialer(r, id) + if err != nil { + return nil, xerrors.Errorf("dial: %w", err) + } + timeoutCtx, timeoutCancelFunc := context.WithCancel(context.Background()) + defaultTransport, valid := http.DefaultTransport.(*http.Transport) + if !valid { + panic("dev error: default transport is the wrong type") + } + transport := defaultTransport.Clone() + transport.DialContext = agentConn.DialContext + conn := &Conn{ + Conn: agentConn, + timeoutCancel: timeoutCancelFunc, + transport: transport, + } + c.closeMutex.Lock() + c.closeGroup.Add(1) + c.closeMutex.Unlock() + go func() { + defer c.closeGroup.Done() + var err error + select { + case <-timeoutCtx.Done(): + err = xerrors.New("cache timeout") + case <-c.closed: + err = xerrors.New("cache closed") + case <-conn.Closed(): + } + + c.connMap.Delete(id.String()) + c.connGroup.Forget(id.String()) + _ = conn.CloseWithError(err) + }() + return conn, nil + }) if err != nil { - return nil, nil, xerrors.Errorf("dial: %w", err) - } - timeoutCtx, timeoutCancelFunc := context.WithCancel(context.Background()) - defaultTransport, valid := http.DefaultTransport.(*http.Transport) - if !valid { - panic("dev error: default transport is the wrong type") + return nil, nil, err } - transport := defaultTransport.Clone() - transport.DialContext = agentConn.DialContext - conn = &Conn{ - Conn: agentConn, - timeoutCancel: timeoutCancelFunc, - transport: transport, - } - go func() { - select { - case <-timeoutCtx.Done(): - case <-conn.Closed(): - } - c.connMutex.Lock() - delete(c.conns, id) - c.connMutex.Unlock() - // This should close after the delete so callers - // can check the `Closed()` channel for this to be expired. - _ = conn.CloseWithError(xerrors.New("cache timeout")) - }() - c.connMutex.Lock() - c.conns[id] = conn - c.connMutex.Unlock() + c.connMap.Store(id.String(), rawConn) } + + conn, _ := rawConn.(*Conn) conn.timeoutMutex.Lock() defer conn.timeoutMutex.Unlock() if conn.timeout != nil { @@ -110,21 +134,27 @@ func (c *Cache) Acquire(r *http.Request, id uuid.UUID) (*Conn, func(), error) { } conn.locks.Inc() return conn, func() { - conn.locks.Dec() conn.timeoutMutex.Lock() defer conn.timeoutMutex.Unlock() if conn.timeout != nil { conn.timeout.Stop() } - conn.timeout = time.AfterFunc(c.inactiveTimeout, conn.timeoutCancel) + conn.locks.Dec() + if conn.locks.Load() == 0 { + conn.timeout = time.AfterFunc(c.inactiveTimeout, conn.timeoutCancel) + } }, nil } func (c *Cache) Close() error { - c.connMutex.Lock() - defer c.connMutex.Unlock() - for _, conn := range c.conns { - _ = conn.Close() + c.closeMutex.Lock() + defer c.closeMutex.Unlock() + select { + case <-c.closed: + return nil + default: } + close(c.closed) + c.closeGroup.Wait() return nil } diff --git a/coderd/wsconncache/wsconncache_test.go b/coderd/wsconncache/wsconncache_test.go index 7f4300a25f744..34ce39e20b86d 100644 --- a/coderd/wsconncache/wsconncache_test.go +++ b/coderd/wsconncache/wsconncache_test.go @@ -8,6 +8,7 @@ import ( "net/http/httptest" "net/http/httputil" "net/url" + "sync" "testing" "time" @@ -34,7 +35,7 @@ func TestMain(m *testing.M) { func TestCache(t *testing.T) { t.Parallel() - t.Run("Cache", func(t *testing.T) { + t.Run("Same", func(t *testing.T) { t.Parallel() cache := wsconncache.New(func(r *http.Request, id uuid.UUID) (*agent.Conn, error) { return setupAgent(t, agent.Metadata{}, 0), nil @@ -42,8 +43,11 @@ func TestCache(t *testing.T) { t.Cleanup(func() { _ = cache.Close() }) - _, _, err := cache.Acquire(httptest.NewRequest(http.MethodGet, "/", nil), uuid.Nil) + conn1, _, err := cache.Acquire(httptest.NewRequest(http.MethodGet, "/", nil), uuid.Nil) require.NoError(t, err) + conn2, _, err := cache.Acquire(httptest.NewRequest(http.MethodGet, "/", nil), uuid.Nil) + require.NoError(t, err) + require.True(t, conn1 == conn2) }) t.Run("Expire", func(t *testing.T) { t.Parallel() @@ -94,6 +98,9 @@ func TestCache(t *testing.T) { w.WriteHeader(http.StatusOK) }), } + t.Cleanup(func() { + _ = server.Close() + }) go server.Serve(random) cache := wsconncache.New(func(r *http.Request, id uuid.UUID) (*agent.Conn, error) { @@ -102,33 +109,47 @@ func TestCache(t *testing.T) { t.Cleanup(func() { _ = cache.Close() }) - req := httptest.NewRequest(http.MethodGet, "/", nil) - conn, release, err := cache.Acquire(req, uuid.Nil) - require.NoError(t, err) - t.Cleanup(release) - proxy := httputil.NewSingleHostReverseProxy(&url.URL{ - Scheme: "http", - Host: fmt.Sprintf("127.0.0.1:%d", tcpAddr.Port), - Path: "/", - }) - proxy.Transport = conn.HTTPTransport() - res := httptest.NewRecorder() - proxy.ServeHTTP(res, req) - require.Equal(t, http.StatusOK, res.Result().StatusCode) - req = httptest.NewRequest(http.MethodGet, "/", nil) - res = httptest.NewRecorder() - proxy.ServeHTTP(res, req) - require.Equal(t, http.StatusOK, res.Result().StatusCode) + + var wg sync.WaitGroup + // Perform many requests in parallel to simulate + // simultaneous HTTP requests. + for i := 0; i < 50; i++ { + wg.Add(1) + go func() { + defer wg.Done() + proxy := httputil.NewSingleHostReverseProxy(&url.URL{ + Scheme: "http", + Host: fmt.Sprintf("127.0.0.1:%d", tcpAddr.Port), + Path: "/", + }) + req := httptest.NewRequest(http.MethodGet, "/", nil) + conn, release, err := cache.Acquire(req, uuid.Nil) + if !assert.NoError(t, err) { + return + } + defer release() + proxy.Transport = conn.HTTPTransport() + res := httptest.NewRecorder() + proxy.ServeHTTP(res, req) + res.Result().Body.Close() + require.Equal(t, http.StatusOK, res.Result().StatusCode) + }() + } + wg.Wait() }) } func setupAgent(t *testing.T, metadata agent.Metadata, ptyTimeout time.Duration) *agent.Conn { client, server := provisionersdk.TransportPipe() closer := agent.New(func(ctx context.Context, logger slog.Logger) (agent.Metadata, *peerbroker.Listener, error) { - listener, err := peerbroker.Listen(server, nil) + listener, err := peerbroker.Listen(server, func(ctx context.Context) ([]webrtc.ICEServer, *peer.ConnOptions, error) { + return nil, &peer.ConnOptions{ + Logger: slogtest.Make(t, nil).Named("server").Leveled(slog.LevelDebug), + }, nil + }) return metadata, listener, err }, &agent.Options{ - Logger: slogtest.Make(t, nil).Leveled(slog.LevelDebug), + Logger: slogtest.Make(t, nil).Named("agent").Leveled(slog.LevelDebug), ReconnectingPTYTimeout: ptyTimeout, }) t.Cleanup(func() { @@ -140,7 +161,7 @@ func setupAgent(t *testing.T, metadata agent.Metadata, ptyTimeout time.Duration) stream, err := api.NegotiateConnection(context.Background()) assert.NoError(t, err) conn, err := peerbroker.Dial(stream, []webrtc.ICEServer{}, &peer.ConnOptions{ - Logger: slogtest.Make(t, nil), + Logger: slogtest.Make(t, nil).Named("client").Leveled(slog.LevelDebug), }) require.NoError(t, err) t.Cleanup(func() { diff --git a/codersdk/workspaceapps.go b/codersdk/workspaceapps.go index c6e10514a9b6c..431b986cd41a5 100644 --- a/codersdk/workspaceapps.go +++ b/codersdk/workspaceapps.go @@ -1,8 +1,6 @@ package codersdk import ( - "context" - "github.com/google/uuid" ) @@ -18,7 +16,3 @@ type WorkspaceApp struct { // an icon to be displayed in the dashboard. Icon string `json:"icon"` } - -func (c *Client) ProxyWorkspaceApplication(ctx context.Context) { - -} diff --git a/peer/channel.go b/peer/channel.go index a6102a648c8ae..c0415e50baa1a 100644 --- a/peer/channel.go +++ b/peer/channel.go @@ -6,7 +6,6 @@ import ( "io" "net" "sync" - "time" "github.com/pion/datachannel" "github.com/pion/webrtc/v3" @@ -136,12 +135,12 @@ func (c *Channel) init() { // is triggerred with a buffer size less than the chunks written. // // This makes sense when considering UDP connections, because - // bufferring of data that has no transmit guarantees is likely + // buffering of data that has no transmit guarantees is likely // to cause unexpected behavior. // // When ordered, this adds a bufio.Reader. This ensures additional // data on TCP-like connections can be read in parts, while still - // being bufferred. + // being buffered. if c.opts.Unordered { c.reader = c.rwc } else { @@ -193,11 +192,9 @@ func (c *Channel) Read(bytes []byte) (int, error) { if c.isClosed() { return 0, c.closeError } - if !c.isOpened() { - err := c.waitOpened() - if err != nil { - return 0, err - } + err := c.waitOpened() + if err != nil { + return 0, err } bytesRead, err := c.reader.Read(bytes) @@ -211,7 +208,6 @@ func (c *Channel) Read(bytes []byte) (int, error) { if xerrors.Is(err, io.EOF) { err = c.closeWithError(ErrClosed) } - return bytesRead, err } return bytesRead, err } @@ -235,24 +231,14 @@ func (c *Channel) Write(bytes []byte) (n int, err error) { if c.isClosed() { return 0, c.closeWithError(nil) } - if !c.isOpened() { - err := c.waitOpened() - if err != nil { - return 0, err - } + err = c.waitOpened() + if err != nil { + return 0, err } - if c.dc.BufferedAmount()+uint64(len(bytes)) >= maxBufferedAmount { <-c.sendMore } - // There's an obvious race-condition here. This is an edge-case, as - // most-frequently data won't be pooled so synchronously, but is - // definitely possible. - // - // See: https://github.com/pion/sctp/issues/181 - time.Sleep(time.Microsecond) - return c.rwc.Write(bytes) } @@ -266,11 +252,6 @@ func (c *Channel) Label() string { return c.dc.Label() } -// ID returns the label of the underlying DataChannel. -func (c *Channel) ID() uint16 { - return *c.dc.ID() -} - // Protocol returns the protocol of the underlying DataChannel. func (c *Channel) Protocol() string { return c.dc.Protocol() @@ -324,15 +305,6 @@ func (c *Channel) isClosed() bool { } } -func (c *Channel) isOpened() bool { - select { - case <-c.opened: - return true - default: - return false - } -} - func (c *Channel) waitOpened() error { select { case <-c.opened: diff --git a/peer/conn.go b/peer/conn.go index ed2f8dc746a8d..8eae101ccdbbe 100644 --- a/peer/conn.go +++ b/peer/conn.go @@ -114,6 +114,7 @@ type Conn struct { closeMutex sync.Mutex closeError error + dcCreateMutex sync.Mutex dcOpenChannel chan *webrtc.DataChannel dcDisconnectChannel chan struct{} dcDisconnectListeners atomic.Uint32 @@ -483,6 +484,11 @@ func (c *Conn) CreateChannel(ctx context.Context, label string, opts *ChannelOpt } func (c *Conn) dialChannel(ctx context.Context, label string, opts *ChannelOptions) (*Channel, error) { + // pion/webrtc is slower when opening multiple channels + // in parallel than it is sequentially. + c.dcCreateMutex.Lock() + defer c.dcCreateMutex.Unlock() + c.logger().Debug(ctx, "creating data channel", slog.F("label", label), slog.F("opts", opts)) var id *uint16 if opts.ID != 0 { diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 82890515e3044..1ae337ebeb146 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -387,6 +387,7 @@ export interface WorkspaceAgent { readonly operating_system: string readonly startup_script?: string readonly directory?: string + readonly apps: WorkspaceApp[] } // From codersdk/workspaceagents.go:47:6 @@ -394,7 +395,7 @@ export interface WorkspaceAgentAuthenticateResponse { readonly session_token: string } -// From codersdk/workspaceresources.go:57:6 +// From codersdk/workspaceresources.go:58:6 export interface WorkspaceAgentInstanceMetadata { readonly jail_orchestrator: string readonly operating_system: string @@ -407,7 +408,7 @@ export interface WorkspaceAgentInstanceMetadata { readonly vnc: boolean } -// From codersdk/workspaceresources.go:49:6 +// From codersdk/workspaceresources.go:50:6 export interface WorkspaceAgentResourceMetadata { readonly memory_total: number readonly disk_total: number @@ -416,6 +417,15 @@ export interface WorkspaceAgentResourceMetadata { readonly cpu_mhz: number } +// From codersdk/workspaceapps.go:7:6 +export interface WorkspaceApp { + readonly id: string + readonly name: string + readonly command?: string + readonly access_url?: string + readonly icon: string +} + // From codersdk/workspacebuilds.go:24:6 export interface WorkspaceBuild { readonly id: string From 80b5600a3d5c527254d5dd9eaa0ff297c5c4b907 Mon Sep 17 00:00:00 2001 From: kylecarbs Date: Fri, 3 Jun 2022 04:00:38 +0000 Subject: [PATCH 15/26] Add embed errors --- coderd/coderd.go | 17 ++++--- coderd/workspaceapps.go | 42 +++++++++++------ coderd/workspaceapps_test.go | 18 ++++++-- .../templates/docker-code-server/README.md | 11 +++++ examples/templates/docker-code-server/main.tf | 45 +++++++++++++++++++ site/embed.go | 31 ++++++++++--- site/embed_slim.go | 11 ++++- site/embed_test.go | 42 ++++++++++++++++- site/htmlTemplates/index.html | 1 + 9 files changed, 187 insertions(+), 31 deletions(-) create mode 100644 examples/templates/docker-code-server/README.md create mode 100644 examples/templates/docker-code-server/main.tf diff --git a/coderd/coderd.go b/coderd/coderd.go index fa7ac5d8d7bd5..df88edcb56ff3 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -76,8 +76,9 @@ func New(options *Options) *API { r := chi.NewRouter() api := &API{ - Options: options, - Handler: r, + Options: options, + Handler: r, + siteHandler: site.Handler(), } api.workspaceAgentCache = wsconncache.New(api.dialWorkspaceAgent, 0) @@ -95,14 +96,19 @@ func New(options *Options) *API { tracing.HTTPMW(api.TracerProvider, "coderd.http"), ) - r.Route("/@{user}/{workspaceagent}/apps/{application}", func(r chi.Router) { + apps := func(r chi.Router) { r.Use( httpmw.RateLimitPerMinute(options.APIRateLimit), apiKeyMiddleware, httpmw.ExtractUserParam(api.Database), ) r.Get("/*", api.workspaceAppsProxyPath) - }) + } + // %40 is the encoded character of the @ symbol. VS Code Web does + // not handle character encoding properly, so it's safe to assume + // other applications might not as well. + r.Route("/%40{user}/{workspaceagent}/apps/{application}", apps) + r.Route("/@{user}/{workspaceagent}/apps/{application}", apps) r.Route("/api/v2", func(r chi.Router) { r.NotFound(func(rw http.ResponseWriter, r *http.Request) { @@ -338,7 +344,7 @@ func New(options *Options) *API { r.Get("/state", api.workspaceBuildState) }) }) - r.NotFound(site.DefaultHandler().ServeHTTP) + r.NotFound(api.siteHandler.ServeHTTP) return api } @@ -346,6 +352,7 @@ type API struct { *Options Handler chi.Router + siteHandler http.Handler websocketWaitMutex sync.Mutex websocketWaitGroup sync.WaitGroup workspaceAgentCache *wsconncache.Cache diff --git a/coderd/workspaceapps.go b/coderd/workspaceapps.go index 7aec86e27fd82..4b420b8f50c67 100644 --- a/coderd/workspaceapps.go +++ b/coderd/workspaceapps.go @@ -15,6 +15,7 @@ import ( "github.com/coder/coder/coderd/database" "github.com/coder/coder/coderd/httpapi" "github.com/coder/coder/coderd/httpmw" + "github.com/coder/coder/site" ) func (api *API) workspaceAppsProxyPath(rw http.ResponseWriter, r *http.Request) { @@ -113,23 +114,15 @@ func (api *API) workspaceAppsProxyPath(rw http.ResponseWriter, r *http.Request) return } - conn, release, err := api.workspaceAgentCache.Acquire(r, agent.ID) - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("dial workspace agent: %s", err), - }) - return - } - defer release() - proxy := httputil.NewSingleHostReverseProxy(appURL) - // Write the error directly using our format! + // Write an error using our embed handler proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) { - httpapi.Write(w, http.StatusBadGateway, httpapi.Response{ - Message: err.Error(), - }) + r = r.WithContext(site.WithAPIResponse(r.Context(), site.APIResponse{ + StatusCode: http.StatusBadGateway, + Message: err.Error(), + })) + api.siteHandler.ServeHTTP(w, r) } - proxy.Transport = conn.HTTPTransport() path := chi.URLParam(r, "*") if !strings.HasSuffix(r.URL.Path, "/") && path == "" { // Web applications typically request paths relative to the @@ -139,6 +132,27 @@ func (api *API) workspaceAppsProxyPath(rw http.ResponseWriter, r *http.Request) http.Redirect(rw, r, r.URL.String(), http.StatusTemporaryRedirect) return } + if r.URL.RawQuery == "" && appURL.RawQuery != "" { + // If the application defines a default set of query parameters, + // we should always respect them. The reverse proxy will merge + // query parameters for server-side requests, but sometimes + // client-side applications require the query parameters to render + // properly. With code-server, this is the "folder" param. + r.URL.RawQuery = appURL.RawQuery + http.Redirect(rw, r, r.URL.String(), http.StatusTemporaryRedirect) + return + } r.URL.Path = path + + conn, release, err := api.workspaceAgentCache.Acquire(r, agent.ID) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("dial workspace agent: %s", err), + }) + return + } + defer release() + + proxy.Transport = conn.HTTPTransport() proxy.ServeHTTP(rw, r) } diff --git a/coderd/workspaceapps_test.go b/coderd/workspaceapps_test.go index dc06573b51189..ac28829ba79cd 100644 --- a/coderd/workspaceapps_test.go +++ b/coderd/workspaceapps_test.go @@ -38,7 +38,7 @@ func TestWorkspaceAppsProxyPath(t *testing.T) { client, coderAPI := coderdtest.NewWithAPI(t, nil) user := coderdtest.CreateFirstUser(t, client) - daemonCloser := coderdtest.NewProvisionerDaemon(t, coderAPI) + coderdtest.NewProvisionerDaemon(t, coderAPI) authToken := uuid.NewString() version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, @@ -56,7 +56,7 @@ func TestWorkspaceAppsProxyPath(t *testing.T) { }, Apps: []*proto.App{{ Name: "example", - Url: fmt.Sprintf("http://127.0.0.1:%d", tcpAddr.Port), + Url: fmt.Sprintf("http://127.0.0.1:%d?query=true", tcpAddr.Port), }}, }}, }}, @@ -68,7 +68,6 @@ func TestWorkspaceAppsProxyPath(t *testing.T) { coderdtest.AwaitTemplateVersionJob(t, client, version.ID) workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) - daemonCloser.Close() agentClient := codersdk.New(client.URL) agentClient.SessionToken = authToken @@ -91,11 +90,22 @@ func TestWorkspaceAppsProxyPath(t *testing.T) { require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode) }) - t.Run("Proxies", func(t *testing.T) { + t.Run("RedirectsWithQuery", func(t *testing.T) { t.Parallel() resp, err := client.Request(context.Background(), http.MethodGet, "/@me/"+workspace.Name+"/apps/example/", nil) require.NoError(t, err) defer resp.Body.Close() + require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode) + loc, err := resp.Location() + require.NoError(t, err) + require.Equal(t, "query=true", loc.RawQuery) + }) + + t.Run("Proxies", func(t *testing.T) { + t.Parallel() + resp, err := client.Request(context.Background(), http.MethodGet, "/@me/"+workspace.Name+"/apps/example/?query=true", nil) + require.NoError(t, err) + defer resp.Body.Close() body, err := io.ReadAll(resp.Body) require.NoError(t, err) require.Equal(t, "", string(body)) diff --git a/examples/templates/docker-code-server/README.md b/examples/templates/docker-code-server/README.md new file mode 100644 index 0000000000000..995773a1b66ab --- /dev/null +++ b/examples/templates/docker-code-server/README.md @@ -0,0 +1,11 @@ +--- +name: Develop code-server in Docker +description: Run code-server in a Docker development environment +tags: [local, docker] +--- + +# code-server in Docker + +## Getting started + +Run `coder templates init` and select this template. Follow the instructions that appear. diff --git a/examples/templates/docker-code-server/main.tf b/examples/templates/docker-code-server/main.tf new file mode 100644 index 0000000000000..d46ab3b3eaa04 --- /dev/null +++ b/examples/templates/docker-code-server/main.tf @@ -0,0 +1,45 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + version = "0.4.2" + } + docker = { + source = "kreuzwerker/docker" + version = "~> 2.16.0" + } + } +} + +provider "coder" { +} + +data "coder_workspace" "me" { +} + +resource "coder_agent" "dev" { + arch = "amd64" + os = "linux" + startup_script = "code-server --auth none" +} + +resource "coder_app" "code-server" { + agent_id = coder_agent.dev.id + url = "http://localhost:8080/?folder=/home/coder" +} + +resource "docker_container" "workspace" { + count = data.coder_workspace.me.start_count + image = "codercom/code-server:latest" + # Uses lower() to avoid Docker restriction on container names. + name = "coder-${data.coder_workspace.me.owner}-${lower(data.coder_workspace.me.name)}" + hostname = lower(data.coder_workspace.me.name) + dns = ["1.1.1.1"] + # Use the docker gateway if the access URL is 127.0.0.1 + entrypoint = ["sh", "-c", replace(coder_agent.dev.init_script, "127.0.0.1", "host.docker.internal")] + env = ["CODER_AGENT_TOKEN=${coder_agent.dev.token}"] + host { + host = "host.docker.internal" + ip = "host-gateway" + } +} diff --git a/site/embed.go b/site/embed.go index b78ed3a7674c7..c1f0fe9dabee6 100644 --- a/site/embed.go +++ b/site/embed.go @@ -5,6 +5,7 @@ package site import ( "bytes" + "context" "embed" "fmt" "io" @@ -28,7 +29,16 @@ import ( //go:embed out/bin/* var site embed.FS -func DefaultHandler() http.Handler { +type apiResponseContextKey struct{} + +// WithAPIResponse returns a context with the APIResponse value attached. +// This is used to inject API response data to the index.html for additional +// metadata in error pages. +func WithAPIResponse(ctx context.Context, apiResponse APIResponse) context.Context { + return context.WithValue(ctx, apiResponseContextKey{}, apiResponse) +} + +func Handler() http.Handler { // the out directory is where webpack builds are created. It is in the same // directory as this file (package site). siteFS, err := fs.Sub(site, "out") @@ -38,11 +48,11 @@ func DefaultHandler() http.Handler { panic(err) } - return Handler(siteFS) + return HandlerWithFS(siteFS) } // Handler returns an HTTP handler for serving the static site. -func Handler(fileSystem fs.FS) http.Handler { +func HandlerWithFS(fileSystem fs.FS) http.Handler { // html files are handled by a text/template. Non-html files // are served by the default file server. // @@ -90,8 +100,14 @@ func (h *handler) exists(filePath string) bool { } type htmlState struct { - CSP cspState - CSRF csrfState + APIResponse APIResponse + CSP cspState + CSRF csrfState +} + +type APIResponse struct { + StatusCode int + Message string } type cspState struct { @@ -139,6 +155,11 @@ func (h *handler) ServeHTTP(resp http.ResponseWriter, req *http.Request) { CSRF: csrfState{Token: nosurf.Token(req)}, } + apiResponseRaw := req.Context().Value(apiResponseContextKey{}) + if apiResponseRaw != nil { + state.APIResponse = apiResponseRaw.(APIResponse) + } + // First check if it's a file we have in our templates if h.serveHTML(resp, req, reqFile, state) { return diff --git a/site/embed_slim.go b/site/embed_slim.go index 489963b994322..04933de1e73b0 100644 --- a/site/embed_slim.go +++ b/site/embed_slim.go @@ -7,6 +7,15 @@ import ( "net/http" ) -func DefaultHandler() http.Handler { +type APIResponse struct { + StatusCode int + Message string +} + +func Handler() http.Handler { return http.NotFoundHandler() } + +func WithAPIResponse(ctx context.Context, _ APIResponse) context.Context { + return ctx +} diff --git a/site/embed_test.go b/site/embed_test.go index 9235f80c898d6..59ca137d36a9e 100644 --- a/site/embed_test.go +++ b/site/embed_test.go @@ -5,6 +5,7 @@ package site_test import ( "context" + "encoding/json" "fmt" "io" "net/http" @@ -39,7 +40,7 @@ func TestCaching(t *testing.T) { }, } - srv := httptest.NewServer(site.Handler(rootFS)) + srv := httptest.NewServer(site.HandlerWithFS(rootFS)) defer srv.Close() // Create a context @@ -98,7 +99,7 @@ func TestServingFiles(t *testing.T) { }, } - srv := httptest.NewServer(site.Handler(rootFS)) + srv := httptest.NewServer(site.HandlerWithFS(rootFS)) defer srv.Close() // Create a context @@ -172,3 +173,40 @@ func TestShouldCacheFile(t *testing.T) { require.Equal(t, testCase.expected, got, fmt.Sprintf("Expected ShouldCacheFile(%s) to be %t", testCase.reqFile, testCase.expected)) } } + +func TestServeAPIResponse(t *testing.T) { + t.Parallel() + + // Create a test server + rootFS := fstest.MapFS{ + "index.html": &fstest.MapFile{ + Data: []byte(`{"code":{{ .APIResponse.StatusCode }},"message":"{{ .APIResponse.Message }}"}`), + }, + } + + apiResponse := site.APIResponse{ + StatusCode: http.StatusBadGateway, + Message: "This could be an error message!", + } + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + r = r.WithContext(site.WithAPIResponse(r.Context(), apiResponse)) + site.HandlerWithFS(rootFS).ServeHTTP(w, r) + })) + defer srv.Close() + + req, err := http.NewRequestWithContext(context.Background(), "GET", srv.URL, nil) + require.NoError(t, err) + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + var body struct { + Code int `json:"code"` + Message string `json:"message"` + } + data, err := io.ReadAll(resp.Body) + require.NoError(t, err) + t.Logf("resp: %q", data) + err = json.Unmarshal(data, &body) + require.NoError(t, err) + require.Equal(t, apiResponse.StatusCode, body.Code) + require.Equal(t, apiResponse.Message, body.Message) +} diff --git a/site/htmlTemplates/index.html b/site/htmlTemplates/index.html index cc6a0e5e151a5..6574c66eeaaa6 100644 --- a/site/htmlTemplates/index.html +++ b/site/htmlTemplates/index.html @@ -17,6 +17,7 @@ + From 8b81c35c31a297231041b69d9265935e6490d1a3 Mon Sep 17 00:00:00 2001 From: kylecarbs Date: Fri, 3 Jun 2022 04:07:39 +0000 Subject: [PATCH 16/26] chore: Refactor site to improve testing It was difficult to develop this package due to the embed build tag being mandatory on the tests. The logic to test doesn't require any embedded files. --- coderd/coderd.go | 2 +- site/embed_slim.go | 12 ------------ site/{embed.go => site.go} | 24 ------------------------ site/site_embed.go | 24 ++++++++++++++++++++++++ site/site_slim.go | 15 +++++++++++++++ site/{embed_test.go => site_test.go} | 3 --- 6 files changed, 40 insertions(+), 40 deletions(-) delete mode 100644 site/embed_slim.go rename site/{embed.go => site.go} (94%) create mode 100644 site/site_embed.go create mode 100644 site/site_slim.go rename site/{embed_test.go => site_test.go} (99%) diff --git a/coderd/coderd.go b/coderd/coderd.go index 8c25c1208bd09..d42e3e931a05b 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -334,7 +334,7 @@ func New(options *Options) *API { r.Get("/state", api.workspaceBuildState) }) }) - r.NotFound(site.DefaultHandler().ServeHTTP) + r.NotFound(site.Handler(site.FS()).ServeHTTP) return api } diff --git a/site/embed_slim.go b/site/embed_slim.go deleted file mode 100644 index 489963b994322..0000000000000 --- a/site/embed_slim.go +++ /dev/null @@ -1,12 +0,0 @@ -//go:build !embed -// +build !embed - -package site - -import ( - "net/http" -) - -func DefaultHandler() http.Handler { - return http.NotFoundHandler() -} diff --git a/site/embed.go b/site/site.go similarity index 94% rename from site/embed.go rename to site/site.go index b78ed3a7674c7..25ac15db91339 100644 --- a/site/embed.go +++ b/site/site.go @@ -1,11 +1,7 @@ -//go:build embed -// +build embed - package site import ( "bytes" - "embed" "fmt" "io" "io/fs" @@ -21,26 +17,6 @@ import ( "golang.org/x/xerrors" ) -// The `embed` package ignores recursively including directories -// that prefix with `_`. Wildcarding nested is janky, but seems to -// work quite well for edge-cases. -//go:embed out -//go:embed out/bin/* -var site embed.FS - -func DefaultHandler() http.Handler { - // the out directory is where webpack builds are created. It is in the same - // directory as this file (package site). - siteFS, err := fs.Sub(site, "out") - - if err != nil { - // This can't happen... Go would throw a compilation error. - panic(err) - } - - return Handler(siteFS) -} - // Handler returns an HTTP handler for serving the static site. func Handler(fileSystem fs.FS) http.Handler { // html files are handled by a text/template. Non-html files diff --git a/site/site_embed.go b/site/site_embed.go new file mode 100644 index 0000000000000..f07ddb99441d0 --- /dev/null +++ b/site/site_embed.go @@ -0,0 +1,24 @@ +//go:build embed +// +build embed + +package site + +import ( + "embed" + "io/fs" +) + +//go:embed out +//go:embed out/bin/* +var site embed.FS + +func FS() fs.FS { + // the out directory is where webpack builds are created. It is in the same + // directory as this file (package site). + out, err := fs.Sub(site, "out") + if err != nil { + // This can't happen... Go would throw a compilation error. + panic(err) + } + return out +} diff --git a/site/site_slim.go b/site/site_slim.go new file mode 100644 index 0000000000000..414da032fc26e --- /dev/null +++ b/site/site_slim.go @@ -0,0 +1,15 @@ +//go:build !embed +// +build !embed + +package site + +import ( + "embed" + "io/fs" +) + +var slim embed.FS + +func FS() fs.FS { + return slim +} diff --git a/site/embed_test.go b/site/site_test.go similarity index 99% rename from site/embed_test.go rename to site/site_test.go index 9235f80c898d6..0008d404c66c3 100644 --- a/site/embed_test.go +++ b/site/site_test.go @@ -1,6 +1,3 @@ -//go:build embed -// +build embed - package site_test import ( From 0a63becfd653d519fdc534c09b5ca41507750c28 Mon Sep 17 00:00:00 2001 From: kylecarbs Date: Fri, 3 Jun 2022 04:20:38 +0000 Subject: [PATCH 17/26] Add test for error handler --- coderd/workspaceapps.go | 7 ++++++- coderd/workspaceapps_test.go | 11 +++++++++++ site/site.go | 6 +++++- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/coderd/workspaceapps.go b/coderd/workspaceapps.go index 4b420b8f50c67..f6c03d693c30a 100644 --- a/coderd/workspaceapps.go +++ b/coderd/workspaceapps.go @@ -18,6 +18,8 @@ import ( "github.com/coder/coder/site" ) +// workspaceAppsProxyPath proxies requests to a workspace application +// through a relative URL path. func (api *API) workspaceAppsProxyPath(rw http.ResponseWriter, r *http.Request) { user := httpmw.UserParam(r) // This can be in the form of: ".[workspace-agent]" or "" @@ -115,12 +117,15 @@ func (api *API) workspaceAppsProxyPath(rw http.ResponseWriter, r *http.Request) } proxy := httputil.NewSingleHostReverseProxy(appURL) - // Write an error using our embed handler proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) { + // This is a browser-facing route so JSON responses are not viable here. + // To pass friendly errors to the frontend, special meta tags are overridden + // in the index.html with the content passed here. r = r.WithContext(site.WithAPIResponse(r.Context(), site.APIResponse{ StatusCode: http.StatusBadGateway, Message: err.Error(), })) + w.WriteHeader(http.StatusBadGateway) api.siteHandler.ServeHTTP(w, r) } path := chi.URLParam(r, "*") diff --git a/coderd/workspaceapps_test.go b/coderd/workspaceapps_test.go index ac28829ba79cd..1466866735ca1 100644 --- a/coderd/workspaceapps_test.go +++ b/coderd/workspaceapps_test.go @@ -57,6 +57,9 @@ func TestWorkspaceAppsProxyPath(t *testing.T) { Apps: []*proto.App{{ Name: "example", Url: fmt.Sprintf("http://127.0.0.1:%d?query=true", tcpAddr.Port), + }, { + Name: "fake", + Url: "http://127.0.0.2", }}, }}, }}, @@ -111,4 +114,12 @@ func TestWorkspaceAppsProxyPath(t *testing.T) { require.Equal(t, "", string(body)) require.Equal(t, http.StatusOK, resp.StatusCode) }) + + t.Run("ProxyError", func(t *testing.T) { + t.Parallel() + resp, err := client.Request(context.Background(), http.MethodGet, "/@me/"+workspace.Name+"/apps/fake/", nil) + require.NoError(t, err) + defer resp.Body.Close() + require.Equal(t, http.StatusBadGateway, resp.StatusCode) + }) } diff --git a/site/site.go b/site/site.go index 8d41ff4373a8d..af302f997b814 100644 --- a/site/site.go +++ b/site/site.go @@ -134,7 +134,11 @@ func (h *handler) ServeHTTP(resp http.ResponseWriter, req *http.Request) { apiResponseRaw := req.Context().Value(apiResponseContextKey{}) if apiResponseRaw != nil { - state.APIResponse = apiResponseRaw.(APIResponse) + apiResponse, ok := apiResponseRaw.(APIResponse) + if !ok { + panic("dev error: api response in context isn't the correct type") + } + state.APIResponse = apiResponse } // First check if it's a file we have in our templates From d3b9ab5f72f629305914735349ce3bf8a1749cdd Mon Sep 17 00:00:00 2001 From: kylecarbs Date: Fri, 3 Jun 2022 04:42:59 +0000 Subject: [PATCH 18/26] Remove unused access url --- coderd/workspaceresources_test.go | 1 - codersdk/workspaceapps.go | 3 --- 2 files changed, 4 deletions(-) diff --git a/coderd/workspaceresources_test.go b/coderd/workspaceresources_test.go index 9a1343b6ca03f..a1a941f355386 100644 --- a/coderd/workspaceresources_test.go +++ b/coderd/workspaceresources_test.go @@ -88,6 +88,5 @@ func TestWorkspaceResource(t *testing.T) { require.Equal(t, app.Command, got.Command) require.Equal(t, app.Icon, got.Icon) require.Equal(t, app.Name, got.Name) - require.Equal(t, app.Url, got.AccessURL) }) } diff --git a/codersdk/workspaceapps.go b/codersdk/workspaceapps.go index 431b986cd41a5..4b74ff2250fee 100644 --- a/codersdk/workspaceapps.go +++ b/codersdk/workspaceapps.go @@ -9,9 +9,6 @@ type WorkspaceApp struct { // Name is a unique identifier attached to an agent. Name string `json:"name"` Command string `json:"command,omitempty"` - // AccessURL is an address used to access the application. - // If command is specified, this will be omitted. - AccessURL string `json:"access_url,omitempty"` // Icon is a relative path or external URL that specifies // an icon to be displayed in the dashboard. Icon string `json:"icon"` From 7a1ae1551228ba5048495d35ef4ec6ed0c9b6a0a Mon Sep 17 00:00:00 2001 From: kylecarbs Date: Fri, 3 Jun 2022 05:08:40 +0000 Subject: [PATCH 19/26] Add RBAC tests --- .vscode/settings.json | 1 + coderd/coderd.go | 4 ++-- coderd/coderd_test.go | 14 ++++++++++++++ coderd/workspaceagents.go | 2 +- coderd/workspaceagents_test.go | 2 +- coderd/workspaceapps.go | 8 ++++++-- codersdk/workspaceagents.go | 4 ++-- 7 files changed, 27 insertions(+), 8 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 57e86d8300bcd..961b7dcd1d7a7 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -80,6 +80,7 @@ "weblinks", "webrtc", "workspaceagent", + "workspaceapp", "workspaceapps", "wsconncache", "xerrors", diff --git a/coderd/coderd.go b/coderd/coderd.go index 598a6286b0fd6..8012aeaf32e53 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -107,8 +107,8 @@ func New(options *Options) *API { // %40 is the encoded character of the @ symbol. VS Code Web does // not handle character encoding properly, so it's safe to assume // other applications might not as well. - r.Route("/%40{user}/{workspaceagent}/apps/{application}", apps) - r.Route("/@{user}/{workspaceagent}/apps/{application}", apps) + r.Route("/%40{user}/{workspacename}/apps/{workspaceapp}", apps) + r.Route("/@{user}/{workspacename}/apps/{workspaceapp}", apps) r.Route("/api/v2", func(r chi.Router) { r.NotFound(func(rw http.ResponseWriter, r *http.Request) { diff --git a/coderd/coderd_test.go b/coderd/coderd_test.go index a3135ebc9249d..703143b92231f 100644 --- a/coderd/coderd_test.go +++ b/coderd/coderd_test.go @@ -82,6 +82,10 @@ func TestAuthorizeAllEndpoints(t *testing.T) { Agents: []*proto.Agent{{ Id: "something", Auth: &proto.Agent_Token{}, + Apps: []*proto.App{{ + Name: "app", + Url: "http://localhost:3000", + }}, }}, }}, }, @@ -128,6 +132,15 @@ func TestAuthorizeAllEndpoints(t *testing.T) { "GET:/api/v2/users/authmethods": {NoAuthorize: true}, "POST:/api/v2/csp/reports": {NoAuthorize: true}, + "GET:/%40{user}/{workspacename}/apps/{application}/*": { + AssertAction: rbac.ActionRead, + AssertObject: workspaceRBACObj, + }, + "GET:/@{user}/{workspacename}/apps/{application}/*": { + AssertAction: rbac.ActionRead, + AssertObject: workspaceRBACObj, + }, + // Has it's own auth "GET:/api/v2/users/oauth2/github/callback": {NoAuthorize: true}, @@ -374,6 +387,7 @@ func TestAuthorizeAllEndpoints(t *testing.T) { route = strings.ReplaceAll(route, "{template}", template.ID.String()) route = strings.ReplaceAll(route, "{hash}", file.Hash) route = strings.ReplaceAll(route, "{workspaceresource}", workspaceResources[0].ID.String()) + route = strings.ReplaceAll(route, "{workspaceapp}", workspaceResources[0].Agents[0].Apps[0].Name) route = strings.ReplaceAll(route, "{templateversion}", version.ID.String()) route = strings.ReplaceAll(route, "{templateversiondryrun}", templateVersionDryRun.ID.String()) route = strings.ReplaceAll(route, "{templatename}", template.Name) diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index cfdc886db1fc4..f7694cf0294db 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -403,7 +403,7 @@ func (api *API) workspaceAgentPTY(rw http.ResponseWriter, r *http.Request) { return } defer release() - ptNetConn, err := agentConn.ReconnectingPTY(reconnect.String(), uint16(height), uint16(width), "") + ptNetConn, err := agentConn.ReconnectingPTY(reconnect.String(), uint16(height), uint16(width), r.URL.Query().Get("command")) if err != nil { _ = conn.Close(websocket.StatusInternalError, httpapi.WebsocketCloseSprintf("dial: %s", err)) return diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go index 360abb3431156..c658791f93941 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -297,7 +297,7 @@ func TestWorkspaceAgentPTY(t *testing.T) { }) resources := coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID) - conn, err := client.WorkspaceAgentReconnectingPTY(context.Background(), resources[0].Agents[0].ID, uuid.New(), 80, 80) + conn, err := client.WorkspaceAgentReconnectingPTY(context.Background(), resources[0].Agents[0].ID, uuid.New(), 80, 80, "/bin/bash") require.NoError(t, err) defer conn.Close() diff --git a/coderd/workspaceapps.go b/coderd/workspaceapps.go index f6c03d693c30a..1042fc9ef627a 100644 --- a/coderd/workspaceapps.go +++ b/coderd/workspaceapps.go @@ -15,6 +15,7 @@ import ( "github.com/coder/coder/coderd/database" "github.com/coder/coder/coderd/httpapi" "github.com/coder/coder/coderd/httpmw" + "github.com/coder/coder/coderd/rbac" "github.com/coder/coder/site" ) @@ -23,7 +24,7 @@ import ( func (api *API) workspaceAppsProxyPath(rw http.ResponseWriter, r *http.Request) { user := httpmw.UserParam(r) // This can be in the form of: ".[workspace-agent]" or "" - workspaceWithAgent := chi.URLParam(r, "workspaceagent") + workspaceWithAgent := chi.URLParam(r, "workspacename") workspaceParts := strings.Split(workspaceWithAgent, ".") workspace, err := api.Database.GetWorkspaceByOwnerIDAndName(r.Context(), database.GetWorkspaceByOwnerIDAndNameParams{ @@ -42,6 +43,9 @@ func (api *API) workspaceAppsProxyPath(rw http.ResponseWriter, r *http.Request) }) return } + if !api.Authorize(rw, r, rbac.ActionRead, workspace) { + return + } build, err := api.Database.GetLatestWorkspaceBuildByWorkspaceID(r.Context(), workspace.ID) if err != nil { @@ -87,7 +91,7 @@ func (api *API) workspaceAppsProxyPath(rw http.ResponseWriter, r *http.Request) app, err := api.Database.GetWorkspaceAppByAgentIDAndName(r.Context(), database.GetWorkspaceAppByAgentIDAndNameParams{ AgentID: agent.ID, - Name: chi.URLParam(r, "application"), + Name: chi.URLParam(r, "workspaceapp"), }) if errors.Is(err, sql.ErrNoRows) { httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ diff --git a/codersdk/workspaceagents.go b/codersdk/workspaceagents.go index c634b1de7ea2a..f31ed07dd010a 100644 --- a/codersdk/workspaceagents.go +++ b/codersdk/workspaceagents.go @@ -341,8 +341,8 @@ func (c *Client) WorkspaceAgent(ctx context.Context, id uuid.UUID) (WorkspaceAge // WorkspaceAgentReconnectingPTY spawns a PTY that reconnects using the token provided. // It communicates using `agent.ReconnectingPTYRequest` marshaled as JSON. // Responses are PTY output that can be rendered. -func (c *Client) WorkspaceAgentReconnectingPTY(ctx context.Context, agentID, reconnect uuid.UUID, height, width int) (net.Conn, error) { - serverURL, err := c.URL.Parse(fmt.Sprintf("/api/v2/workspaceagents/%s/pty?reconnect=%s&height=%d&width=%d", agentID, reconnect, height, width)) +func (c *Client) WorkspaceAgentReconnectingPTY(ctx context.Context, agentID, reconnect uuid.UUID, height, width int, command string) (net.Conn, error) { + serverURL, err := c.URL.Parse(fmt.Sprintf("/api/v2/workspaceagents/%s/pty?reconnect=%s&height=%d&width=%d&command=%s", agentID, reconnect, height, width, command)) if err != nil { return nil, xerrors.Errorf("parse url: %w", err) } From b056400f44676a7416ae17b879301e518a5dff52 Mon Sep 17 00:00:00 2001 From: kylecarbs Date: Fri, 3 Jun 2022 15:58:33 +0000 Subject: [PATCH 20/26] Fix dial agent syntax --- coderd/workspaceagents.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index 1284c4ea7cec9..d1036eaaf1b78 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -419,7 +419,7 @@ func (api *API) workspaceAgentPTY(rw http.ResponseWriter, r *http.Request) { // dialWorkspaceAgent connects to a workspace agent by ID. Only rely on // r.Context() for cancellation if it's use is safe or r.Hijack() has // not been performed. -func (api *API) dialWorkspaceAgent(ctx context.Context, r *http.Request, agentID uuid.UUID) (*agent.Conn, error) { +func (api *API) dialWorkspaceAgent(r *http.Request, agentID uuid.UUID) (*agent.Conn, error) { client, server := provisionersdk.TransportPipe() ctx, cancelFunc := context.WithCancel(context.Background()) go func() { From 2018cdc34193085845f887342aaad8529cc74c9d Mon Sep 17 00:00:00 2001 From: kylecarbs Date: Fri, 3 Jun 2022 16:44:29 +0000 Subject: [PATCH 21/26] Fix linting errors --- coderd/database/databasefake/databasefake.go | 1 + ...4_workspace_apps.down.sql => 000019_workspace_apps.down.sql} | 0 ...00014_workspace_apps.up.sql => 000019_workspace_apps.up.sql} | 0 site/htmlTemplates/index.html | 2 +- 4 files changed, 2 insertions(+), 1 deletion(-) rename coderd/database/migrations/{000014_workspace_apps.down.sql => 000019_workspace_apps.down.sql} (100%) rename coderd/database/migrations/{000014_workspace_apps.up.sql => 000019_workspace_apps.up.sql} (100%) diff --git a/coderd/database/databasefake/databasefake.go b/coderd/database/databasefake/databasefake.go index d5ad8a310e9a4..9e02f4b55042b 100644 --- a/coderd/database/databasefake/databasefake.go +++ b/coderd/database/databasefake/databasefake.go @@ -1575,6 +1575,7 @@ func (q *fakeQuerier) InsertWorkspaceApp(_ context.Context, arg database.InsertW q.mutex.Lock() defer q.mutex.Unlock() + // nolint:gosimple workspaceApp := database.WorkspaceApp{ ID: arg.ID, AgentID: arg.AgentID, diff --git a/coderd/database/migrations/000014_workspace_apps.down.sql b/coderd/database/migrations/000019_workspace_apps.down.sql similarity index 100% rename from coderd/database/migrations/000014_workspace_apps.down.sql rename to coderd/database/migrations/000019_workspace_apps.down.sql diff --git a/coderd/database/migrations/000014_workspace_apps.up.sql b/coderd/database/migrations/000019_workspace_apps.up.sql similarity index 100% rename from coderd/database/migrations/000014_workspace_apps.up.sql rename to coderd/database/migrations/000019_workspace_apps.up.sql diff --git a/site/htmlTemplates/index.html b/site/htmlTemplates/index.html index 6574c66eeaaa6..78099f88007dd 100644 --- a/site/htmlTemplates/index.html +++ b/site/htmlTemplates/index.html @@ -17,7 +17,7 @@ - + From 2d5261f9a3db1df142da14524734c4159544e18a Mon Sep 17 00:00:00 2001 From: kylecarbs Date: Fri, 3 Jun 2022 16:55:13 +0000 Subject: [PATCH 22/26] Fix gen --- coderd/wsconncache/wsconncache.go | 2 ++ site/src/api/typesGenerated.ts | 1 - site/src/testHelpers/entities.ts | 7 +++++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/coderd/wsconncache/wsconncache.go b/coderd/wsconncache/wsconncache.go index f87c3b044f5b7..7d3b741a63b7e 100644 --- a/coderd/wsconncache/wsconncache.go +++ b/coderd/wsconncache/wsconncache.go @@ -54,6 +54,8 @@ func (c *Conn) CloseWithError(err error) error { if c.transport != nil { c.transport.CloseIdleConnections() } + c.timeoutMutex.Lock() + defer c.timeoutMutex.Unlock() if c.timeout != nil { c.timeout.Stop() } diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 621216a7d14fd..d3650aa62e64a 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -426,7 +426,6 @@ export interface WorkspaceApp { readonly id: string readonly name: string readonly command?: string - readonly access_url?: string readonly icon: string } diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index a73a1b14a9077..2c06cb214020d 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -210,7 +210,14 @@ export const MockDeletedWorkspace: TypesGen.Workspace = { ...MockWorkspace, late export const MockOutdatedWorkspace: TypesGen.Workspace = { ...MockFailedWorkspace, outdated: true } +export const MockWorkspaceApp: TypesGen.WorkspaceApp = { + id: "test-app", + name: "test-app", + icon: "", +} + export const MockWorkspaceAgent: TypesGen.WorkspaceAgent = { + apps: [MockWorkspaceApp], architecture: "amd64", created_at: "", environment_variables: {}, From 856f17d8f5bcd747866409dd18dcfc88ffd1d4b7 Mon Sep 17 00:00:00 2001 From: kylecarbs Date: Fri, 3 Jun 2022 17:17:55 +0000 Subject: [PATCH 23/26] Fix icon required --- codersdk/workspaceapps.go | 2 +- site/src/api/typesGenerated.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/codersdk/workspaceapps.go b/codersdk/workspaceapps.go index 4b74ff2250fee..d993a4dcf49ba 100644 --- a/codersdk/workspaceapps.go +++ b/codersdk/workspaceapps.go @@ -11,5 +11,5 @@ type WorkspaceApp struct { Command string `json:"command,omitempty"` // Icon is a relative path or external URL that specifies // an icon to be displayed in the dashboard. - Icon string `json:"icon"` + Icon string `json:"icon,omitempty"` } diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index d3650aa62e64a..4e3e7b9959aee 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -426,7 +426,7 @@ export interface WorkspaceApp { readonly id: string readonly name: string readonly command?: string - readonly icon: string + readonly icon?: string } // From codersdk/workspacebuilds.go:24:6 From ad90bcb4f7f87b67359de5876146c01d621bb5d1 Mon Sep 17 00:00:00 2001 From: kylecarbs Date: Fri, 3 Jun 2022 19:45:21 +0000 Subject: [PATCH 24/26] Adjust migration number --- ...019_workspace_apps.down.sql => 000020_workspace_apps.down.sql} | 0 ...{000019_workspace_apps.up.sql => 000020_workspace_apps.up.sql} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename coderd/database/migrations/{000019_workspace_apps.down.sql => 000020_workspace_apps.down.sql} (100%) rename coderd/database/migrations/{000019_workspace_apps.up.sql => 000020_workspace_apps.up.sql} (100%) diff --git a/coderd/database/migrations/000019_workspace_apps.down.sql b/coderd/database/migrations/000020_workspace_apps.down.sql similarity index 100% rename from coderd/database/migrations/000019_workspace_apps.down.sql rename to coderd/database/migrations/000020_workspace_apps.down.sql diff --git a/coderd/database/migrations/000019_workspace_apps.up.sql b/coderd/database/migrations/000020_workspace_apps.up.sql similarity index 100% rename from coderd/database/migrations/000019_workspace_apps.up.sql rename to coderd/database/migrations/000020_workspace_apps.up.sql From 38abbb5970a8999e5dafb9913edc23147dd93425 Mon Sep 17 00:00:00 2001 From: kylecarbs Date: Sat, 4 Jun 2022 17:05:17 +0000 Subject: [PATCH 25/26] Fix proxy error status code --- coderd/parameters_test.go | 5 +++-- coderd/workspaceapps.go | 1 - coderd/workspaceapps_test.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/coderd/parameters_test.go b/coderd/parameters_test.go index eb2857f2cb150..b0e519b938847 100644 --- a/coderd/parameters_test.go +++ b/coderd/parameters_test.go @@ -2,11 +2,12 @@ package coderd_test import ( "context" - "github.com/coder/coder/provisioner/echo" - "github.com/coder/coder/provisionersdk/proto" "net/http" "testing" + "github.com/coder/coder/provisioner/echo" + "github.com/coder/coder/provisionersdk/proto" + "github.com/stretchr/testify/require" "github.com/coder/coder/coderd/coderdtest" diff --git a/coderd/workspaceapps.go b/coderd/workspaceapps.go index 1042fc9ef627a..299400116c8dd 100644 --- a/coderd/workspaceapps.go +++ b/coderd/workspaceapps.go @@ -129,7 +129,6 @@ func (api *API) workspaceAppsProxyPath(rw http.ResponseWriter, r *http.Request) StatusCode: http.StatusBadGateway, Message: err.Error(), })) - w.WriteHeader(http.StatusBadGateway) api.siteHandler.ServeHTTP(w, r) } path := chi.URLParam(r, "*") diff --git a/coderd/workspaceapps_test.go b/coderd/workspaceapps_test.go index 1466866735ca1..399b1874dc6aa 100644 --- a/coderd/workspaceapps_test.go +++ b/coderd/workspaceapps_test.go @@ -120,6 +120,6 @@ func TestWorkspaceAppsProxyPath(t *testing.T) { resp, err := client.Request(context.Background(), http.MethodGet, "/@me/"+workspace.Name+"/apps/fake/", nil) require.NoError(t, err) defer resp.Body.Close() - require.Equal(t, http.StatusBadGateway, resp.StatusCode) + require.Equal(t, http.StatusOK, resp.StatusCode) }) } From 4f8964228eaadb1444d7b61e42bdd1ff2961f365 Mon Sep 17 00:00:00 2001 From: kylecarbs Date: Sat, 4 Jun 2022 18:06:00 +0000 Subject: [PATCH 26/26] Fix empty db lookup --- coderd/database/databasefake/databasefake.go | 3 --- coderd/workspaceresources.go | 3 --- 2 files changed, 6 deletions(-) diff --git a/coderd/database/databasefake/databasefake.go b/coderd/database/databasefake/databasefake.go index b6c725a3e94a6..da3a739c32629 100644 --- a/coderd/database/databasefake/databasefake.go +++ b/coderd/database/databasefake/databasefake.go @@ -419,9 +419,6 @@ func (q *fakeQuerier) GetWorkspaceAppsByAgentIDs(_ context.Context, ids []uuid.U } } } - if len(apps) == 0 { - return nil, sql.ErrNoRows - } return apps, nil } diff --git a/coderd/workspaceresources.go b/coderd/workspaceresources.go index c72f6add504dc..3f7e713ef63da 100644 --- a/coderd/workspaceresources.go +++ b/coderd/workspaceresources.go @@ -51,9 +51,6 @@ func (api *API) workspaceResource(rw http.ResponseWriter, r *http.Request) { agentIDs = append(agentIDs, agent.ID) } apps, err := api.Database.GetWorkspaceAppsByAgentIDs(r.Context(), agentIDs) - if errors.Is(err, sql.ErrNoRows) { - err = nil - } if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ Message: fmt.Sprintf("get workspace apps: %s", err),