mirror of
https://github.com/coder/coder.git
synced 2025-07-13 21:36:50 +00:00
feat: Add workspace application support (#1773)
* 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 <name>`. 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. * Compare fields in apps test * Update Terraform provider to use relative path * Add some basic structure for routing * 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`. * Add basic proxy logic * Add proxying based on path * Add app proxying for wildcards * Add wsconncache * 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. * 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. * 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. * 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. * 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 * Add embed errors * 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. * Add test for error handler * Remove unused access url * Add RBAC tests * Fix dial agent syntax * Fix linting errors * Fix gen * Fix icon required * Adjust migration number * Fix proxy error status code * Fix empty db lookup
This commit is contained in:
@ -35,6 +35,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),
|
||||
}
|
||||
}
|
||||
@ -63,6 +64,7 @@ type fakeQuerier struct {
|
||||
templateVersions []database.TemplateVersion
|
||||
templates []database.Template
|
||||
workspaceBuilds []database.WorkspaceBuild
|
||||
workspaceApps []database.WorkspaceApp
|
||||
workspaces []database.Workspace
|
||||
}
|
||||
|
||||
@ -388,6 +390,38 @@ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
return apps, nil
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) GetWorkspacesAutostart(_ context.Context) ([]database.Workspace, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
@ -1031,6 +1065,22 @@ func (q *fakeQuerier) GetWorkspaceAgentsByResourceIDs(_ context.Context, resourc
|
||||
return workspaceAgents, nil
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) GetWorkspaceAppByAgentIDAndName(_ 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()
|
||||
@ -1521,6 +1571,25 @@ 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()
|
||||
|
||||
// nolint:gosimple
|
||||
workspaceApp := database.WorkspaceApp{
|
||||
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
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) UpdateAPIKeyByID(_ context.Context, arg database.UpdateAPIKeyByIDParams) error {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
|
20
coderd/database/dump.sql
generated
20
coderd/database/dump.sql
generated
@ -280,6 +280,17 @@ 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),
|
||||
url character varying(65534),
|
||||
relative_path boolean DEFAULT false NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE workspace_builds (
|
||||
id uuid NOT NULL,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
@ -382,6 +393,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);
|
||||
|
||||
@ -463,6 +480,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;
|
||||
|
||||
|
@ -0,0 +1 @@
|
||||
DROP TABLE workspace_apps;
|
12
coderd/database/migrations/000020_workspace_apps.up.sql
Normal file
12
coderd/database/migrations/000020_workspace_apps.up.sql
Normal file
@ -0,0 +1,12 @@
|
||||
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,
|
||||
command varchar(65534),
|
||||
url varchar(65534),
|
||||
relative_path boolean NOT NULL DEFAULT false,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE(agent_id, name)
|
||||
);
|
@ -493,6 +493,17 @@ 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"`
|
||||
Url sql.NullString `db:"url" json:"url"`
|
||||
RelativePath bool `db:"relative_path" json:"relative_path"`
|
||||
}
|
||||
|
||||
type WorkspaceBuild struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
|
@ -64,6 +64,9 @@ 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)
|
||||
GetWorkspaceBuildByJobID(ctx context.Context, jobID uuid.UUID) (WorkspaceBuild, error)
|
||||
GetWorkspaceBuildByWorkspaceID(ctx context.Context, arg GetWorkspaceBuildByWorkspaceIDParams) ([]WorkspaceBuild, error)
|
||||
@ -93,6 +96,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
|
||||
|
@ -2785,6 +2785,155 @@ 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
|
||||
`
|
||||
|
||||
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.Url,
|
||||
&i.RelativePath,
|
||||
); 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, url, relative_path 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.Url,
|
||||
&i.RelativePath,
|
||||
); 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,
|
||||
url,
|
||||
relative_path
|
||||
)
|
||||
VALUES
|
||||
($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"`
|
||||
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) {
|
||||
row := q.db.QueryRowContext(ctx, insertWorkspaceApp,
|
||||
arg.ID,
|
||||
arg.CreatedAt,
|
||||
arg.AgentID,
|
||||
arg.Name,
|
||||
arg.Icon,
|
||||
arg.Command,
|
||||
arg.Url,
|
||||
arg.RelativePath,
|
||||
)
|
||||
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 getLatestWorkspaceBuildByWorkspaceID = `-- name: GetLatestWorkspaceBuildByWorkspaceID :one
|
||||
SELECT
|
||||
id, created_at, updated_at, workspace_id, template_version_id, name, build_number, transition, initiator_id, provisioner_state, job_id, deadline
|
||||
|
23
coderd/database/queries/workspaceapps.sql
Normal file
23
coderd/database/queries/workspaceapps.sql
Normal file
@ -0,0 +1,23 @@
|
||||
-- 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: GetWorkspaceAppByAgentIDAndName :one
|
||||
SELECT * FROM workspace_apps WHERE agent_id = $1 AND name = $2;
|
||||
|
||||
-- name: InsertWorkspaceApp :one
|
||||
INSERT INTO
|
||||
workspace_apps (
|
||||
id,
|
||||
created_at,
|
||||
agent_id,
|
||||
name,
|
||||
icon,
|
||||
command,
|
||||
url,
|
||||
relative_path
|
||||
)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *;
|
Reference in New Issue
Block a user