mirror of
https://github.com/coder/coder.git
synced 2025-07-08 11:39:50 +00:00
feat: add new required slug property to coder_app, use in URLs (#4573)
This commit is contained in:
@ -331,8 +331,9 @@ func NewAuthTester(ctx context.Context, t *testing.T, client *codersdk.Client, a
|
||||
Id: "something",
|
||||
Auth: &proto.Agent_Token{},
|
||||
Apps: []*proto.App{{
|
||||
Name: "testapp",
|
||||
Url: "http://localhost:3000",
|
||||
Slug: "testapp",
|
||||
DisplayName: "testapp",
|
||||
Url: "http://localhost:3000",
|
||||
}},
|
||||
}},
|
||||
}},
|
||||
@ -372,7 +373,7 @@ func NewAuthTester(ctx context.Context, t *testing.T, client *codersdk.Client, a
|
||||
"{template}": template.ID.String(),
|
||||
"{fileID}": file.ID.String(),
|
||||
"{workspaceresource}": workspace.LatestBuild.Resources[0].ID.String(),
|
||||
"{workspaceapp}": workspace.LatestBuild.Resources[0].Agents[0].Apps[0].Name,
|
||||
"{workspaceapp}": workspace.LatestBuild.Resources[0].Agents[0].Apps[0].Slug,
|
||||
"{templateversion}": version.ID.String(),
|
||||
"{jobID}": templateVersionDryRun.ID.String(),
|
||||
"{templatename}": template.Name,
|
||||
|
@ -1861,7 +1861,7 @@ func (q *fakeQuerier) GetWorkspaceAgentsCreatedAfter(_ context.Context, after ti
|
||||
return workspaceAgents, nil
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) GetWorkspaceAppByAgentIDAndName(_ context.Context, arg database.GetWorkspaceAppByAgentIDAndNameParams) (database.WorkspaceApp, error) {
|
||||
func (q *fakeQuerier) GetWorkspaceAppByAgentIDAndSlug(_ context.Context, arg database.GetWorkspaceAppByAgentIDAndSlugParams) (database.WorkspaceApp, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
@ -1869,7 +1869,7 @@ func (q *fakeQuerier) GetWorkspaceAppByAgentIDAndName(_ context.Context, arg dat
|
||||
if app.AgentID != arg.AgentID {
|
||||
continue
|
||||
}
|
||||
if app.Name != arg.Name {
|
||||
if app.Slug != arg.Slug {
|
||||
continue
|
||||
}
|
||||
return app, nil
|
||||
@ -2522,7 +2522,8 @@ func (q *fakeQuerier) InsertWorkspaceApp(_ context.Context, arg database.InsertW
|
||||
ID: arg.ID,
|
||||
AgentID: arg.AgentID,
|
||||
CreatedAt: arg.CreatedAt,
|
||||
Name: arg.Name,
|
||||
Slug: arg.Slug,
|
||||
DisplayName: arg.DisplayName,
|
||||
Icon: arg.Icon,
|
||||
Command: arg.Command,
|
||||
Url: arg.Url,
|
||||
|
7
coderd/database/dump.sql
generated
7
coderd/database/dump.sql
generated
@ -399,7 +399,7 @@ 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,
|
||||
display_name character varying(64) NOT NULL,
|
||||
icon character varying(256) NOT NULL,
|
||||
command character varying(65534),
|
||||
url character varying(65534),
|
||||
@ -408,7 +408,8 @@ CREATE TABLE workspace_apps (
|
||||
healthcheck_threshold integer DEFAULT 0 NOT NULL,
|
||||
health workspace_app_health DEFAULT 'disabled'::public.workspace_app_health NOT NULL,
|
||||
subdomain boolean DEFAULT false NOT NULL,
|
||||
sharing_level app_sharing_level DEFAULT 'owner'::public.app_sharing_level NOT NULL
|
||||
sharing_level app_sharing_level DEFAULT 'owner'::public.app_sharing_level NOT NULL,
|
||||
slug text NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE workspace_builds (
|
||||
@ -548,7 +549,7 @@ 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);
|
||||
ADD CONSTRAINT workspace_apps_agent_id_slug_idx UNIQUE (agent_id, slug);
|
||||
|
||||
ALTER TABLE ONLY workspace_apps
|
||||
ADD CONSTRAINT workspace_apps_pkey PRIMARY KEY (id);
|
||||
|
5
coderd/database/migrations/000066_app_slug.down.sql
Normal file
5
coderd/database/migrations/000066_app_slug.down.sql
Normal file
@ -0,0 +1,5 @@
|
||||
-- drop unique index on "slug" column
|
||||
ALTER TABLE "workspace_apps" DROP CONSTRAINT IF EXISTS "workspace_apps_agent_id_slug_idx";
|
||||
|
||||
-- drop "slug" column from "workspace_apps" table
|
||||
ALTER TABLE "workspace_apps" DROP COLUMN "slug";
|
16
coderd/database/migrations/000066_app_slug.up.sql
Normal file
16
coderd/database/migrations/000066_app_slug.up.sql
Normal file
@ -0,0 +1,16 @@
|
||||
BEGIN;
|
||||
|
||||
-- add "slug" column to "workspace_apps" table
|
||||
ALTER TABLE "workspace_apps" ADD COLUMN "slug" text DEFAULT '';
|
||||
|
||||
-- copy the "name" column for each workspace app to the "slug" column
|
||||
UPDATE "workspace_apps" SET "slug" = "name";
|
||||
|
||||
-- make "slug" column not nullable and remove default
|
||||
ALTER TABLE "workspace_apps" ALTER COLUMN "slug" SET NOT NULL;
|
||||
ALTER TABLE "workspace_apps" ALTER COLUMN "slug" DROP DEFAULT;
|
||||
|
||||
-- add unique index on "slug" column
|
||||
ALTER TABLE "workspace_apps" ADD CONSTRAINT "workspace_apps_agent_id_slug_idx" UNIQUE ("agent_id", "slug");
|
||||
|
||||
COMMIT;
|
34
coderd/database/migrations/000067_app_display_name.down.sql
Normal file
34
coderd/database/migrations/000067_app_display_name.down.sql
Normal file
@ -0,0 +1,34 @@
|
||||
BEGIN;
|
||||
|
||||
-- Select all apps with an extra "row_number" column that determines the "rank"
|
||||
-- of the display name against other display names in the same agent.
|
||||
WITH row_numbers AS (
|
||||
SELECT
|
||||
*,
|
||||
row_number() OVER (PARTITION BY agent_id, display_name ORDER BY display_name ASC) AS row_number
|
||||
FROM
|
||||
workspace_apps
|
||||
)
|
||||
|
||||
-- Update any app with a "row_number" greater than 1 to have the row number
|
||||
-- appended to the display name. This effectively means that all lowercase
|
||||
-- display names remain untouched, while non-unique mixed case usernames are
|
||||
-- appended with a unique number. If you had three apps called all called asdf,
|
||||
-- they would then be renamed to e.g. asdf, asdf1234, and asdf5678.
|
||||
UPDATE
|
||||
workspace_apps
|
||||
SET
|
||||
display_name = workspace_apps.display_name || floor(random() * 10000)::text
|
||||
FROM
|
||||
row_numbers
|
||||
WHERE
|
||||
workspace_apps.id = row_numbers.id AND
|
||||
row_numbers.row_number > 1;
|
||||
|
||||
-- rename column "display_name" to "name" on "workspace_apps"
|
||||
ALTER TABLE "workspace_apps" RENAME COLUMN "display_name" TO "name";
|
||||
|
||||
-- restore unique index on "workspace_apps" table
|
||||
ALTER TABLE workspace_apps ADD CONSTRAINT workspace_apps_agent_id_name_key UNIQUE ("agent_id", "name");
|
||||
|
||||
COMMIT;
|
@ -0,0 +1,9 @@
|
||||
BEGIN;
|
||||
|
||||
-- rename column "name" to "display_name" on "workspace_apps"
|
||||
ALTER TABLE "workspace_apps" RENAME COLUMN "name" TO "display_name";
|
||||
|
||||
-- drop constraint "workspace_apps_agent_id_name_key" on "workspace_apps".
|
||||
ALTER TABLE ONLY workspace_apps DROP CONSTRAINT IF EXISTS workspace_apps_agent_id_name_key;
|
||||
|
||||
COMMIT;
|
@ -667,7 +667,7 @@ 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"`
|
||||
DisplayName string `db:"display_name" json:"display_name"`
|
||||
Icon string `db:"icon" json:"icon"`
|
||||
Command sql.NullString `db:"command" json:"command"`
|
||||
Url sql.NullString `db:"url" json:"url"`
|
||||
@ -677,6 +677,7 @@ type WorkspaceApp struct {
|
||||
Health WorkspaceAppHealth `db:"health" json:"health"`
|
||||
Subdomain bool `db:"subdomain" json:"subdomain"`
|
||||
SharingLevel AppSharingLevel `db:"sharing_level" json:"sharing_level"`
|
||||
Slug string `db:"slug" json:"slug"`
|
||||
}
|
||||
|
||||
type WorkspaceBuild struct {
|
||||
|
@ -100,7 +100,7 @@ type sqlcQuerier interface {
|
||||
GetWorkspaceAgentByInstanceID(ctx context.Context, authInstanceID string) (WorkspaceAgent, error)
|
||||
GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgent, error)
|
||||
GetWorkspaceAgentsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceAgent, error)
|
||||
GetWorkspaceAppByAgentIDAndName(ctx context.Context, arg GetWorkspaceAppByAgentIDAndNameParams) (WorkspaceApp, error)
|
||||
GetWorkspaceAppByAgentIDAndSlug(ctx context.Context, arg GetWorkspaceAppByAgentIDAndSlugParams) (WorkspaceApp, error)
|
||||
GetWorkspaceAppsByAgentID(ctx context.Context, agentID uuid.UUID) ([]WorkspaceApp, error)
|
||||
GetWorkspaceAppsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceApp, error)
|
||||
GetWorkspaceAppsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceApp, error)
|
||||
|
@ -973,8 +973,8 @@ func (q *sqlQuerier) UpdateGitSSHKey(ctx context.Context, arg UpdateGitSSHKeyPar
|
||||
}
|
||||
|
||||
const deleteGroupByID = `-- name: DeleteGroupByID :exec
|
||||
DELETE FROM
|
||||
groups
|
||||
DELETE FROM
|
||||
groups
|
||||
WHERE
|
||||
id = $1
|
||||
`
|
||||
@ -985,8 +985,8 @@ func (q *sqlQuerier) DeleteGroupByID(ctx context.Context, id uuid.UUID) error {
|
||||
}
|
||||
|
||||
const deleteGroupMember = `-- name: DeleteGroupMember :exec
|
||||
DELETE FROM
|
||||
group_members
|
||||
DELETE FROM
|
||||
group_members
|
||||
WHERE
|
||||
user_id = $1
|
||||
`
|
||||
@ -4773,23 +4773,23 @@ func (q *sqlQuerier) UpdateWorkspaceAgentVersionByID(ctx context.Context, arg Up
|
||||
return err
|
||||
}
|
||||
|
||||
const getWorkspaceAppByAgentIDAndName = `-- name: GetWorkspaceAppByAgentIDAndName :one
|
||||
SELECT id, created_at, agent_id, name, icon, command, url, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, subdomain, sharing_level FROM workspace_apps WHERE agent_id = $1 AND name = $2
|
||||
const getWorkspaceAppByAgentIDAndSlug = `-- name: GetWorkspaceAppByAgentIDAndSlug :one
|
||||
SELECT id, created_at, agent_id, display_name, icon, command, url, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, subdomain, sharing_level, slug FROM workspace_apps WHERE agent_id = $1 AND slug = $2
|
||||
`
|
||||
|
||||
type GetWorkspaceAppByAgentIDAndNameParams struct {
|
||||
type GetWorkspaceAppByAgentIDAndSlugParams struct {
|
||||
AgentID uuid.UUID `db:"agent_id" json:"agent_id"`
|
||||
Name string `db:"name" json:"name"`
|
||||
Slug string `db:"slug" json:"slug"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) GetWorkspaceAppByAgentIDAndName(ctx context.Context, arg GetWorkspaceAppByAgentIDAndNameParams) (WorkspaceApp, error) {
|
||||
row := q.db.QueryRowContext(ctx, getWorkspaceAppByAgentIDAndName, arg.AgentID, arg.Name)
|
||||
func (q *sqlQuerier) GetWorkspaceAppByAgentIDAndSlug(ctx context.Context, arg GetWorkspaceAppByAgentIDAndSlugParams) (WorkspaceApp, error) {
|
||||
row := q.db.QueryRowContext(ctx, getWorkspaceAppByAgentIDAndSlug, arg.AgentID, arg.Slug)
|
||||
var i WorkspaceApp
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.CreatedAt,
|
||||
&i.AgentID,
|
||||
&i.Name,
|
||||
&i.DisplayName,
|
||||
&i.Icon,
|
||||
&i.Command,
|
||||
&i.Url,
|
||||
@ -4799,12 +4799,13 @@ func (q *sqlQuerier) GetWorkspaceAppByAgentIDAndName(ctx context.Context, arg Ge
|
||||
&i.Health,
|
||||
&i.Subdomain,
|
||||
&i.SharingLevel,
|
||||
&i.Slug,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getWorkspaceAppsByAgentID = `-- name: GetWorkspaceAppsByAgentID :many
|
||||
SELECT id, created_at, agent_id, name, icon, command, url, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, subdomain, sharing_level FROM workspace_apps WHERE agent_id = $1 ORDER BY name ASC
|
||||
SELECT id, created_at, agent_id, display_name, icon, command, url, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, subdomain, sharing_level, slug FROM workspace_apps WHERE agent_id = $1 ORDER BY slug ASC
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetWorkspaceAppsByAgentID(ctx context.Context, agentID uuid.UUID) ([]WorkspaceApp, error) {
|
||||
@ -4820,7 +4821,7 @@ func (q *sqlQuerier) GetWorkspaceAppsByAgentID(ctx context.Context, agentID uuid
|
||||
&i.ID,
|
||||
&i.CreatedAt,
|
||||
&i.AgentID,
|
||||
&i.Name,
|
||||
&i.DisplayName,
|
||||
&i.Icon,
|
||||
&i.Command,
|
||||
&i.Url,
|
||||
@ -4830,6 +4831,7 @@ func (q *sqlQuerier) GetWorkspaceAppsByAgentID(ctx context.Context, agentID uuid
|
||||
&i.Health,
|
||||
&i.Subdomain,
|
||||
&i.SharingLevel,
|
||||
&i.Slug,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -4845,7 +4847,7 @@ func (q *sqlQuerier) GetWorkspaceAppsByAgentID(ctx context.Context, agentID uuid
|
||||
}
|
||||
|
||||
const getWorkspaceAppsByAgentIDs = `-- name: GetWorkspaceAppsByAgentIDs :many
|
||||
SELECT id, created_at, agent_id, name, icon, command, url, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, subdomain, sharing_level FROM workspace_apps WHERE agent_id = ANY($1 :: uuid [ ]) ORDER BY name ASC
|
||||
SELECT id, created_at, agent_id, display_name, icon, command, url, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, subdomain, sharing_level, slug FROM workspace_apps WHERE agent_id = ANY($1 :: uuid [ ]) ORDER BY slug ASC
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetWorkspaceAppsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceApp, error) {
|
||||
@ -4861,7 +4863,7 @@ func (q *sqlQuerier) GetWorkspaceAppsByAgentIDs(ctx context.Context, ids []uuid.
|
||||
&i.ID,
|
||||
&i.CreatedAt,
|
||||
&i.AgentID,
|
||||
&i.Name,
|
||||
&i.DisplayName,
|
||||
&i.Icon,
|
||||
&i.Command,
|
||||
&i.Url,
|
||||
@ -4871,6 +4873,7 @@ func (q *sqlQuerier) GetWorkspaceAppsByAgentIDs(ctx context.Context, ids []uuid.
|
||||
&i.Health,
|
||||
&i.Subdomain,
|
||||
&i.SharingLevel,
|
||||
&i.Slug,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -4886,7 +4889,7 @@ func (q *sqlQuerier) GetWorkspaceAppsByAgentIDs(ctx context.Context, ids []uuid.
|
||||
}
|
||||
|
||||
const getWorkspaceAppsCreatedAfter = `-- name: GetWorkspaceAppsCreatedAfter :many
|
||||
SELECT id, created_at, agent_id, name, icon, command, url, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, subdomain, sharing_level FROM workspace_apps WHERE created_at > $1 ORDER BY name ASC
|
||||
SELECT id, created_at, agent_id, display_name, icon, command, url, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, subdomain, sharing_level, slug FROM workspace_apps WHERE created_at > $1 ORDER BY slug ASC
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetWorkspaceAppsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceApp, error) {
|
||||
@ -4902,7 +4905,7 @@ func (q *sqlQuerier) GetWorkspaceAppsCreatedAfter(ctx context.Context, createdAt
|
||||
&i.ID,
|
||||
&i.CreatedAt,
|
||||
&i.AgentID,
|
||||
&i.Name,
|
||||
&i.DisplayName,
|
||||
&i.Icon,
|
||||
&i.Command,
|
||||
&i.Url,
|
||||
@ -4912,6 +4915,7 @@ func (q *sqlQuerier) GetWorkspaceAppsCreatedAfter(ctx context.Context, createdAt
|
||||
&i.Health,
|
||||
&i.Subdomain,
|
||||
&i.SharingLevel,
|
||||
&i.Slug,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -4932,7 +4936,8 @@ INSERT INTO
|
||||
id,
|
||||
created_at,
|
||||
agent_id,
|
||||
name,
|
||||
slug,
|
||||
display_name,
|
||||
icon,
|
||||
command,
|
||||
url,
|
||||
@ -4944,14 +4949,15 @@ INSERT INTO
|
||||
health
|
||||
)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) RETURNING id, created_at, agent_id, name, icon, command, url, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, subdomain, sharing_level
|
||||
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) RETURNING id, created_at, agent_id, display_name, icon, command, url, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, subdomain, sharing_level, slug
|
||||
`
|
||||
|
||||
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"`
|
||||
Slug string `db:"slug" json:"slug"`
|
||||
DisplayName string `db:"display_name" json:"display_name"`
|
||||
Icon string `db:"icon" json:"icon"`
|
||||
Command sql.NullString `db:"command" json:"command"`
|
||||
Url sql.NullString `db:"url" json:"url"`
|
||||
@ -4968,7 +4974,8 @@ func (q *sqlQuerier) InsertWorkspaceApp(ctx context.Context, arg InsertWorkspace
|
||||
arg.ID,
|
||||
arg.CreatedAt,
|
||||
arg.AgentID,
|
||||
arg.Name,
|
||||
arg.Slug,
|
||||
arg.DisplayName,
|
||||
arg.Icon,
|
||||
arg.Command,
|
||||
arg.Url,
|
||||
@ -4984,7 +4991,7 @@ func (q *sqlQuerier) InsertWorkspaceApp(ctx context.Context, arg InsertWorkspace
|
||||
&i.ID,
|
||||
&i.CreatedAt,
|
||||
&i.AgentID,
|
||||
&i.Name,
|
||||
&i.DisplayName,
|
||||
&i.Icon,
|
||||
&i.Command,
|
||||
&i.Url,
|
||||
@ -4994,6 +5001,7 @@ func (q *sqlQuerier) InsertWorkspaceApp(ctx context.Context, arg InsertWorkspace
|
||||
&i.Health,
|
||||
&i.Subdomain,
|
||||
&i.SharingLevel,
|
||||
&i.Slug,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
@ -81,7 +81,7 @@ VALUES
|
||||
( $1, $2, $3, $4) RETURNING *;
|
||||
|
||||
-- We use the organization_id as the id
|
||||
-- for simplicity since all users is
|
||||
-- for simplicity since all users is
|
||||
-- every member of the org.
|
||||
-- name: InsertAllUsersGroup :one
|
||||
INSERT INTO groups (
|
||||
@ -110,14 +110,14 @@ INSERT INTO group_members (
|
||||
VALUES ( $1, $2);
|
||||
|
||||
-- name: DeleteGroupMember :exec
|
||||
DELETE FROM
|
||||
group_members
|
||||
DELETE FROM
|
||||
group_members
|
||||
WHERE
|
||||
user_id = $1;
|
||||
|
||||
-- name: DeleteGroupByID :exec
|
||||
DELETE FROM
|
||||
groups
|
||||
DELETE FROM
|
||||
groups
|
||||
WHERE
|
||||
id = $1;
|
||||
|
||||
|
@ -1,14 +1,14 @@
|
||||
-- name: GetWorkspaceAppsByAgentID :many
|
||||
SELECT * FROM workspace_apps WHERE agent_id = $1 ORDER BY name ASC;
|
||||
SELECT * FROM workspace_apps WHERE agent_id = $1 ORDER BY slug ASC;
|
||||
|
||||
-- name: GetWorkspaceAppsByAgentIDs :many
|
||||
SELECT * FROM workspace_apps WHERE agent_id = ANY(@ids :: uuid [ ]) ORDER BY name ASC;
|
||||
SELECT * FROM workspace_apps WHERE agent_id = ANY(@ids :: uuid [ ]) ORDER BY slug ASC;
|
||||
|
||||
-- name: GetWorkspaceAppByAgentIDAndName :one
|
||||
SELECT * FROM workspace_apps WHERE agent_id = $1 AND name = $2;
|
||||
-- name: GetWorkspaceAppByAgentIDAndSlug :one
|
||||
SELECT * FROM workspace_apps WHERE agent_id = $1 AND slug = $2;
|
||||
|
||||
-- name: GetWorkspaceAppsCreatedAfter :many
|
||||
SELECT * FROM workspace_apps WHERE created_at > $1 ORDER BY name ASC;
|
||||
SELECT * FROM workspace_apps WHERE created_at > $1 ORDER BY slug ASC;
|
||||
|
||||
-- name: InsertWorkspaceApp :one
|
||||
INSERT INTO
|
||||
@ -16,7 +16,8 @@ INSERT INTO
|
||||
id,
|
||||
created_at,
|
||||
agent_id,
|
||||
name,
|
||||
slug,
|
||||
display_name,
|
||||
icon,
|
||||
command,
|
||||
url,
|
||||
@ -28,7 +29,7 @@ INSERT INTO
|
||||
health
|
||||
)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) RETURNING *;
|
||||
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) RETURNING *;
|
||||
|
||||
-- name: UpdateWorkspaceAppHealthByID :exec
|
||||
UPDATE
|
||||
|
@ -16,7 +16,7 @@ const (
|
||||
UniqueProvisionerDaemonsNameKey UniqueConstraint = "provisioner_daemons_name_key" // ALTER TABLE ONLY provisioner_daemons ADD CONSTRAINT provisioner_daemons_name_key UNIQUE (name);
|
||||
UniqueSiteConfigsKeyKey UniqueConstraint = "site_configs_key_key" // ALTER TABLE ONLY site_configs ADD CONSTRAINT site_configs_key_key UNIQUE (key);
|
||||
UniqueTemplateVersionsTemplateIDNameKey UniqueConstraint = "template_versions_template_id_name_key" // ALTER TABLE ONLY template_versions ADD CONSTRAINT template_versions_template_id_name_key UNIQUE (template_id, name);
|
||||
UniqueWorkspaceAppsAgentIDNameKey UniqueConstraint = "workspace_apps_agent_id_name_key" // ALTER TABLE ONLY workspace_apps ADD CONSTRAINT workspace_apps_agent_id_name_key UNIQUE (agent_id, name);
|
||||
UniqueWorkspaceAppsAgentIDSlugIndex UniqueConstraint = "workspace_apps_agent_id_slug_idx" // ALTER TABLE ONLY workspace_apps ADD CONSTRAINT workspace_apps_agent_id_slug_idx UNIQUE (agent_id, slug);
|
||||
UniqueWorkspaceBuildsJobIDKey UniqueConstraint = "workspace_builds_job_id_key" // ALTER TABLE ONLY workspace_builds ADD CONSTRAINT workspace_builds_job_id_key UNIQUE (job_id);
|
||||
UniqueWorkspaceBuildsWorkspaceIDBuildNumberKey UniqueConstraint = "workspace_builds_workspace_id_build_number_key" // ALTER TABLE ONLY workspace_builds ADD CONSTRAINT workspace_builds_workspace_id_build_number_key UNIQUE (workspace_id, build_number);
|
||||
UniqueIndexOrganizationName UniqueConstraint = "idx_organization_name" // CREATE UNIQUE INDEX idx_organization_name ON organizations USING btree (name);
|
||||
|
@ -14,8 +14,8 @@ var (
|
||||
// Remove the "starts with" and "ends with" regex components.
|
||||
nameRegex = strings.Trim(UsernameValidRegex.String(), "^$")
|
||||
appURL = regexp.MustCompile(fmt.Sprintf(
|
||||
// {PORT/APP_NAME}--{AGENT_NAME}--{WORKSPACE_NAME}--{USERNAME}
|
||||
`^(?P<AppName>%[1]s)--(?P<AgentName>%[1]s)--(?P<WorkspaceName>%[1]s)--(?P<Username>%[1]s)$`,
|
||||
// {PORT/APP_SLUG}--{AGENT_NAME}--{WORKSPACE_NAME}--{USERNAME}
|
||||
`^(?P<AppSlug>%[1]s)--(?P<AgentName>%[1]s)--(?P<WorkspaceName>%[1]s)--(?P<Username>%[1]s)$`,
|
||||
nameRegex))
|
||||
|
||||
validHostnameLabelRegex = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`)
|
||||
@ -23,8 +23,8 @@ var (
|
||||
|
||||
// ApplicationURL is a parsed application URL hostname.
|
||||
type ApplicationURL struct {
|
||||
// Only one of AppName or Port will be set.
|
||||
AppName string
|
||||
// Only one of AppSlug or Port will be set.
|
||||
AppSlug string
|
||||
Port uint16
|
||||
AgentName string
|
||||
WorkspaceName string
|
||||
@ -34,12 +34,12 @@ type ApplicationURL struct {
|
||||
// String returns the application URL hostname without scheme. You will likely
|
||||
// want to append a period and the base hostname.
|
||||
func (a ApplicationURL) String() string {
|
||||
appNameOrPort := a.AppName
|
||||
appSlugOrPort := a.AppSlug
|
||||
if a.Port != 0 {
|
||||
appNameOrPort = strconv.Itoa(int(a.Port))
|
||||
appSlugOrPort = strconv.Itoa(int(a.Port))
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s--%s--%s--%s", appNameOrPort, a.AgentName, a.WorkspaceName, a.Username)
|
||||
return fmt.Sprintf("%s--%s--%s--%s", appSlugOrPort, a.AgentName, a.WorkspaceName, a.Username)
|
||||
}
|
||||
|
||||
// ParseSubdomainAppURL parses an ApplicationURL from the given subdomain. If
|
||||
@ -51,7 +51,7 @@ func (a ApplicationURL) String() string {
|
||||
//
|
||||
// Subdomains should be in the form:
|
||||
//
|
||||
// {PORT/APP_NAME}--{AGENT_NAME}--{WORKSPACE_NAME}--{USERNAME}
|
||||
// {PORT/APP_SLUG}--{AGENT_NAME}--{WORKSPACE_NAME}--{USERNAME}
|
||||
// (eg. https://8080--main--dev--dean.hi.c8s.io)
|
||||
func ParseSubdomainAppURL(subdomain string) (ApplicationURL, error) {
|
||||
matches := appURL.FindAllStringSubmatch(subdomain, -1)
|
||||
@ -60,9 +60,9 @@ func ParseSubdomainAppURL(subdomain string) (ApplicationURL, error) {
|
||||
}
|
||||
matchGroup := matches[0]
|
||||
|
||||
appName, port := AppNameOrPort(matchGroup[appURL.SubexpIndex("AppName")])
|
||||
appSlug, port := AppSlugOrPort(matchGroup[appURL.SubexpIndex("AppSlug")])
|
||||
return ApplicationURL{
|
||||
AppName: appName,
|
||||
AppSlug: appSlug,
|
||||
Port: port,
|
||||
AgentName: matchGroup[appURL.SubexpIndex("AgentName")],
|
||||
WorkspaceName: matchGroup[appURL.SubexpIndex("WorkspaceName")],
|
||||
@ -70,9 +70,9 @@ func ParseSubdomainAppURL(subdomain string) (ApplicationURL, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// AppNameOrPort takes a string and returns either the input string or a port
|
||||
// AppSlugOrPort takes a string and returns either the input string or a port
|
||||
// number.
|
||||
func AppNameOrPort(val string) (string, uint16) {
|
||||
func AppSlugOrPort(val string) (string, uint16) {
|
||||
port, err := strconv.ParseUint(val, 10, 16)
|
||||
if err != nil || port == 0 {
|
||||
port = 0
|
||||
|
@ -25,7 +25,7 @@ func TestApplicationURLString(t *testing.T) {
|
||||
{
|
||||
Name: "AppName",
|
||||
URL: httpapi.ApplicationURL{
|
||||
AppName: "app",
|
||||
AppSlug: "app",
|
||||
Port: 0,
|
||||
AgentName: "agent",
|
||||
WorkspaceName: "workspace",
|
||||
@ -36,7 +36,7 @@ func TestApplicationURLString(t *testing.T) {
|
||||
{
|
||||
Name: "Port",
|
||||
URL: httpapi.ApplicationURL{
|
||||
AppName: "",
|
||||
AppSlug: "",
|
||||
Port: 8080,
|
||||
AgentName: "agent",
|
||||
WorkspaceName: "workspace",
|
||||
@ -47,7 +47,7 @@ func TestApplicationURLString(t *testing.T) {
|
||||
{
|
||||
Name: "Both",
|
||||
URL: httpapi.ApplicationURL{
|
||||
AppName: "app",
|
||||
AppSlug: "app",
|
||||
Port: 8080,
|
||||
AgentName: "agent",
|
||||
WorkspaceName: "workspace",
|
||||
@ -111,7 +111,7 @@ func TestParseSubdomainAppURL(t *testing.T) {
|
||||
Name: "AppName--Agent--Workspace--User",
|
||||
Subdomain: "app--agent--workspace--user",
|
||||
Expected: httpapi.ApplicationURL{
|
||||
AppName: "app",
|
||||
AppSlug: "app",
|
||||
Port: 0,
|
||||
AgentName: "agent",
|
||||
WorkspaceName: "workspace",
|
||||
@ -122,7 +122,7 @@ func TestParseSubdomainAppURL(t *testing.T) {
|
||||
Name: "Port--Agent--Workspace--User",
|
||||
Subdomain: "8080--agent--workspace--user",
|
||||
Expected: httpapi.ApplicationURL{
|
||||
AppName: "",
|
||||
AppSlug: "",
|
||||
Port: 8080,
|
||||
AgentName: "agent",
|
||||
WorkspaceName: "workspace",
|
||||
@ -131,9 +131,9 @@ func TestParseSubdomainAppURL(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Name: "HyphenatedNames",
|
||||
Subdomain: "app-name--agent-name--workspace-name--user-name",
|
||||
Subdomain: "app-slug--agent-name--workspace-name--user-name",
|
||||
Expected: httpapi.ApplicationURL{
|
||||
AppName: "app-name",
|
||||
AppSlug: "app-slug",
|
||||
Port: 0,
|
||||
AgentName: "agent-name",
|
||||
WorkspaceName: "workspace-name",
|
||||
|
@ -28,6 +28,7 @@ import (
|
||||
"github.com/coder/coder/coderd/rbac"
|
||||
"github.com/coder/coder/coderd/telemetry"
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/provisioner"
|
||||
"github.com/coder/coder/provisionerd/proto"
|
||||
"github.com/coder/coder/provisionersdk"
|
||||
sdkproto "github.com/coder/coder/provisionersdk/proto"
|
||||
@ -755,6 +756,7 @@ func insertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid.
|
||||
}
|
||||
snapshot.WorkspaceResources = append(snapshot.WorkspaceResources, telemetry.ConvertWorkspaceResource(resource))
|
||||
|
||||
var appSlugs = make(map[string]struct{})
|
||||
for _, prAgent := range protoResource.Agents {
|
||||
var instanceID sql.NullString
|
||||
if prAgent.GetInstanceId() != "" {
|
||||
@ -806,6 +808,18 @@ func insertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid.
|
||||
snapshot.WorkspaceAgents = append(snapshot.WorkspaceAgents, telemetry.ConvertWorkspaceAgent(dbAgent))
|
||||
|
||||
for _, app := range prAgent.Apps {
|
||||
slug := app.Slug
|
||||
if slug == "" {
|
||||
return xerrors.Errorf("app must have a slug or name set")
|
||||
}
|
||||
if !provisioner.AppSlugRegex.MatchString(slug) {
|
||||
return xerrors.Errorf("app slug %q does not match regex %q", slug, provisioner.AppSlugRegex.String())
|
||||
}
|
||||
if _, exists := appSlugs[slug]; exists {
|
||||
return xerrors.Errorf("duplicate app slug, must be unique per template: %q", slug)
|
||||
}
|
||||
appSlugs[slug] = struct{}{}
|
||||
|
||||
health := database.WorkspaceAppHealthDisabled
|
||||
if app.Healthcheck == nil {
|
||||
app.Healthcheck = &sdkproto.Healthcheck{}
|
||||
@ -823,11 +837,12 @@ func insertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid.
|
||||
}
|
||||
|
||||
dbApp, err := db.InsertWorkspaceApp(ctx, database.InsertWorkspaceAppParams{
|
||||
ID: uuid.New(),
|
||||
CreatedAt: database.Now(),
|
||||
AgentID: dbAgent.ID,
|
||||
Name: app.Name,
|
||||
Icon: app.Icon,
|
||||
ID: uuid.New(),
|
||||
CreatedAt: database.Now(),
|
||||
AgentID: dbAgent.ID,
|
||||
Slug: slug,
|
||||
DisplayName: app.DisplayName,
|
||||
Icon: app.Icon,
|
||||
Command: sql.NullString{
|
||||
String: app.Command,
|
||||
Valid: app.Command != "",
|
||||
|
@ -596,7 +596,8 @@ func convertApps(dbApps []database.WorkspaceApp) []codersdk.WorkspaceApp {
|
||||
for _, dbApp := range dbApps {
|
||||
apps = append(apps, codersdk.WorkspaceApp{
|
||||
ID: dbApp.ID,
|
||||
Name: dbApp.Name,
|
||||
Slug: dbApp.Slug,
|
||||
DisplayName: dbApp.DisplayName,
|
||||
Command: dbApp.Command.String,
|
||||
Icon: dbApp.Icon,
|
||||
Subdomain: dbApp.Subdomain,
|
||||
@ -868,7 +869,7 @@ func (api *API) postWorkspaceAppHealth(rw http.ResponseWriter, r *http.Request)
|
||||
for name, newHealth := range req.Healths {
|
||||
old := func() *database.WorkspaceApp {
|
||||
for _, app := range apps {
|
||||
if app.Name == name {
|
||||
if app.DisplayName == name {
|
||||
return &app
|
||||
}
|
||||
}
|
||||
|
@ -555,8 +555,9 @@ func TestWorkspaceAgentListeningPorts(t *testing.T) {
|
||||
// should not exist in the response.
|
||||
_, appLPort := generateUnfilteredPort(t)
|
||||
app := &proto.App{
|
||||
Name: "test-app",
|
||||
Url: fmt.Sprintf("http://localhost:%d", appLPort),
|
||||
Slug: "test-app",
|
||||
DisplayName: "test-app",
|
||||
Url: fmt.Sprintf("http://localhost:%d", appLPort),
|
||||
}
|
||||
|
||||
// Generate a filtered port that should not exist in the response.
|
||||
@ -623,16 +624,18 @@ func TestWorkspaceAgentAppHealth(t *testing.T) {
|
||||
authToken := uuid.NewString()
|
||||
apps := []*proto.App{
|
||||
{
|
||||
Name: "code-server",
|
||||
Command: "some-command",
|
||||
Url: "http://localhost:3000",
|
||||
Icon: "/code.svg",
|
||||
Slug: "code-server",
|
||||
DisplayName: "code-server",
|
||||
Command: "some-command",
|
||||
Url: "http://localhost:3000",
|
||||
Icon: "/code.svg",
|
||||
},
|
||||
{
|
||||
Name: "code-server-2",
|
||||
Command: "some-command",
|
||||
Url: "http://localhost:3000",
|
||||
Icon: "/code.svg",
|
||||
Slug: "code-server-2",
|
||||
DisplayName: "code-server-2",
|
||||
Command: "some-command",
|
||||
Url: "http://localhost:3000",
|
||||
Icon: "/code.svg",
|
||||
Healthcheck: &proto.Healthcheck{
|
||||
Url: "http://localhost:3000",
|
||||
Interval: 5,
|
||||
|
@ -51,9 +51,9 @@ func (api *API) workspaceAppsProxyPath(rw http.ResponseWriter, r *http.Request)
|
||||
workspace := httpmw.WorkspaceParam(r)
|
||||
agent := httpmw.WorkspaceAgentParam(r)
|
||||
|
||||
// We do not support port proxying on paths, so lookup the app by name.
|
||||
appName := chi.URLParam(r, "workspaceapp")
|
||||
app, ok := api.lookupWorkspaceApp(rw, r, agent.ID, appName)
|
||||
// We do not support port proxying on paths, so lookup the app by slug.
|
||||
appSlug := chi.URLParam(r, "workspaceapp")
|
||||
app, ok := api.lookupWorkspaceApp(rw, r, agent.ID, appSlug)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
@ -180,8 +180,8 @@ func (api *API) handleSubdomainApplications(middlewares ...func(http.Handler) ht
|
||||
agent := httpmw.WorkspaceAgentParam(r)
|
||||
|
||||
var workspaceAppPtr *database.WorkspaceApp
|
||||
if app.AppName != "" {
|
||||
workspaceApp, ok := api.lookupWorkspaceApp(rw, r, agent.ID, app.AppName)
|
||||
if app.AppSlug != "" {
|
||||
workspaceApp, ok := api.lookupWorkspaceApp(rw, r, agent.ID, app.AppSlug)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
@ -251,14 +251,14 @@ func (api *API) parseWorkspaceApplicationHostname(rw http.ResponseWriter, r *htt
|
||||
return app, true
|
||||
}
|
||||
|
||||
// lookupWorkspaceApp looks up the workspace application by name in the given
|
||||
// lookupWorkspaceApp looks up the workspace application by slug in the given
|
||||
// agent and returns it. If the application is not found or there was a server
|
||||
// error while looking it up, an HTML error page is returned and false is
|
||||
// returned so the caller can return early.
|
||||
func (api *API) lookupWorkspaceApp(rw http.ResponseWriter, r *http.Request, agentID uuid.UUID, appName string) (database.WorkspaceApp, bool) {
|
||||
app, err := api.Database.GetWorkspaceAppByAgentIDAndName(r.Context(), database.GetWorkspaceAppByAgentIDAndNameParams{
|
||||
func (api *API) lookupWorkspaceApp(rw http.ResponseWriter, r *http.Request, agentID uuid.UUID, appSlug string) (database.WorkspaceApp, bool) {
|
||||
app, err := api.Database.GetWorkspaceAppByAgentIDAndSlug(r.Context(), database.GetWorkspaceAppByAgentIDAndSlugParams{
|
||||
AgentID: agentID,
|
||||
Name: appName,
|
||||
Slug: appSlug,
|
||||
})
|
||||
if xerrors.Is(err, sql.ErrNoRows) {
|
||||
renderApplicationNotFound(rw, r, api.AccessURL)
|
||||
@ -402,12 +402,28 @@ func (api *API) verifyWorkspaceApplicationSubdomainAuth(rw http.ResponseWriter,
|
||||
return false
|
||||
}
|
||||
|
||||
hostSplit := strings.SplitN(api.AppHostname, ".", 2)
|
||||
if len(hostSplit) != 2 {
|
||||
// This should be impossible as we verify the app hostname on
|
||||
// startup, but we'll check anyways.
|
||||
api.Logger.Error(r.Context(), "could not split invalid app hostname", slog.F("hostname", api.AppHostname))
|
||||
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{
|
||||
Status: http.StatusInternalServerError,
|
||||
Title: "Internal Server Error",
|
||||
Description: "The app is configured with an invalid app wildcard hostname. Please contact an administrator.",
|
||||
RetryEnabled: false,
|
||||
DashboardURL: api.AccessURL.String(),
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
// Set the app cookie for all subdomains of api.AppHostname. This cookie
|
||||
// is handled properly by the ExtractAPIKey middleware.
|
||||
cookieHost := "." + hostSplit[1]
|
||||
http.SetCookie(rw, &http.Cookie{
|
||||
Name: httpmw.DevURLSessionTokenCookie,
|
||||
Value: apiKey,
|
||||
Domain: "." + api.AppHostname,
|
||||
Domain: cookieHost,
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
@ -589,21 +605,18 @@ func (api *API) proxyWorkspaceApplication(proxyApp proxyApplication, rw http.Res
|
||||
return
|
||||
}
|
||||
|
||||
// If the app does not exist, but the app name is a port number, then
|
||||
// route to the port as an "anonymous app". We only support HTTP for
|
||||
// port-based URLs.
|
||||
// If the app does not exist, but the app slug is a port number, then route
|
||||
// to the port as an "anonymous app". We only support HTTP for port-based
|
||||
// URLs.
|
||||
//
|
||||
// This is only supported for subdomain-based applications.
|
||||
internalURL := fmt.Sprintf("http://127.0.0.1:%d", proxyApp.Port)
|
||||
|
||||
// If the app name was used instead, fetch the app from the database so we
|
||||
// can get the internal URL.
|
||||
if proxyApp.App != nil {
|
||||
if !proxyApp.App.Url.Valid {
|
||||
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{
|
||||
Status: http.StatusBadRequest,
|
||||
Title: "Bad Request",
|
||||
Description: fmt.Sprintf("Application %q does not have a URL set.", proxyApp.App.Name),
|
||||
Description: fmt.Sprintf("Application %q does not have a URL set.", proxyApp.App.Slug),
|
||||
RetryEnabled: true,
|
||||
DashboardURL: api.AccessURL.String(),
|
||||
})
|
||||
|
@ -160,23 +160,27 @@ func createWorkspaceWithApps(t *testing.T, client *codersdk.Client, orgID uuid.U
|
||||
},
|
||||
Apps: []*proto.App{
|
||||
{
|
||||
Name: proxyTestAppNameFake,
|
||||
Slug: proxyTestAppNameFake,
|
||||
DisplayName: proxyTestAppNameFake,
|
||||
SharingLevel: proto.AppSharingLevel_OWNER,
|
||||
// Hopefully this IP and port doesn't exist.
|
||||
Url: "http://127.1.0.1:65535",
|
||||
},
|
||||
{
|
||||
Name: proxyTestAppNameOwner,
|
||||
Slug: proxyTestAppNameOwner,
|
||||
DisplayName: proxyTestAppNameOwner,
|
||||
SharingLevel: proto.AppSharingLevel_OWNER,
|
||||
Url: appURL,
|
||||
},
|
||||
{
|
||||
Name: proxyTestAppNameAuthenticated,
|
||||
Slug: proxyTestAppNameAuthenticated,
|
||||
DisplayName: proxyTestAppNameAuthenticated,
|
||||
SharingLevel: proto.AppSharingLevel_AUTHENTICATED,
|
||||
Url: appURL,
|
||||
},
|
||||
{
|
||||
Name: proxyTestAppNamePublic,
|
||||
Slug: proxyTestAppNamePublic,
|
||||
DisplayName: proxyTestAppNamePublic,
|
||||
SharingLevel: proto.AppSharingLevel_PUBLIC,
|
||||
Url: appURL,
|
||||
},
|
||||
@ -624,7 +628,7 @@ func TestWorkspaceAppsProxySubdomain(t *testing.T) {
|
||||
require.NoError(t, err, "get app host")
|
||||
|
||||
subdomain := httpapi.ApplicationURL{
|
||||
AppName: appName,
|
||||
AppSlug: appName,
|
||||
Port: port,
|
||||
AgentName: proxyTestAgentName,
|
||||
WorkspaceName: workspaces[0].Name,
|
||||
@ -855,7 +859,7 @@ func TestAppSharing(t *testing.T) {
|
||||
proxyTestAppNamePublic: codersdk.WorkspaceAppSharingLevelPublic,
|
||||
}
|
||||
for _, app := range agnt.Apps {
|
||||
found[app.Name] = app.SharingLevel
|
||||
found[app.DisplayName] = app.SharingLevel
|
||||
}
|
||||
require.Equal(t, expected, found, "apps have incorrect sharing levels")
|
||||
|
||||
|
@ -1435,16 +1435,18 @@ func TestWorkspaceResource(t *testing.T) {
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
apps := []*proto.App{
|
||||
{
|
||||
Name: "code-server",
|
||||
Command: "some-command",
|
||||
Url: "http://localhost:3000",
|
||||
Icon: "/code.svg",
|
||||
Slug: "code-server",
|
||||
DisplayName: "code-server",
|
||||
Command: "some-command",
|
||||
Url: "http://localhost:3000",
|
||||
Icon: "/code.svg",
|
||||
},
|
||||
{
|
||||
Name: "code-server-2",
|
||||
Command: "some-command",
|
||||
Url: "http://localhost:3000",
|
||||
Icon: "/code.svg",
|
||||
Slug: "code-server-2",
|
||||
DisplayName: "code-server-2",
|
||||
Command: "some-command",
|
||||
Url: "http://localhost:3000",
|
||||
Icon: "/code.svg",
|
||||
Healthcheck: &proto.Healthcheck{
|
||||
Url: "http://localhost:3000",
|
||||
Interval: 5,
|
||||
@ -1487,7 +1489,7 @@ func TestWorkspaceResource(t *testing.T) {
|
||||
app := apps[0]
|
||||
require.EqualValues(t, app.Command, got.Command)
|
||||
require.EqualValues(t, app.Icon, got.Icon)
|
||||
require.EqualValues(t, app.Name, got.Name)
|
||||
require.EqualValues(t, app.DisplayName, got.DisplayName)
|
||||
require.EqualValues(t, codersdk.WorkspaceAppHealthDisabled, got.Health)
|
||||
require.EqualValues(t, "", got.Healthcheck.URL)
|
||||
require.EqualValues(t, 0, got.Healthcheck.Interval)
|
||||
@ -1496,7 +1498,7 @@ func TestWorkspaceResource(t *testing.T) {
|
||||
app = apps[1]
|
||||
require.EqualValues(t, app.Command, got.Command)
|
||||
require.EqualValues(t, app.Icon, got.Icon)
|
||||
require.EqualValues(t, app.Name, got.Name)
|
||||
require.EqualValues(t, app.DisplayName, got.DisplayName)
|
||||
require.EqualValues(t, codersdk.WorkspaceAppHealthInitializing, got.Health)
|
||||
require.EqualValues(t, app.Healthcheck.Url, got.Healthcheck.URL)
|
||||
require.EqualValues(t, app.Healthcheck.Interval, got.Healthcheck.Interval)
|
||||
|
Reference in New Issue
Block a user