feat: add new required slug property to coder_app, use in URLs (#4573)

This commit is contained in:
Dean Sheather 2022-10-29 03:41:31 +10:00 committed by GitHub
parent 90f77a3415
commit 10df2fd4fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
75 changed files with 852 additions and 460 deletions

View File

@ -33,7 +33,7 @@ func NewWorkspaceAppHealthReporter(logger slog.Logger, apps []codersdk.Workspace
hasHealthchecksEnabled := false
health := make(map[string]codersdk.WorkspaceAppHealth, 0)
for _, app := range apps {
health[app.Name] = app.Health
health[app.DisplayName] = app.Health
if !hasHealthchecksEnabled && app.Health != codersdk.WorkspaceAppHealthDisabled {
hasHealthchecksEnabled = true
}
@ -85,21 +85,21 @@ func NewWorkspaceAppHealthReporter(logger slog.Logger, apps []codersdk.Workspace
}()
if err != nil {
mu.Lock()
if failures[app.Name] < int(app.Healthcheck.Threshold) {
if failures[app.DisplayName] < int(app.Healthcheck.Threshold) {
// increment the failure count and keep status the same.
// we will change it when we hit the threshold.
failures[app.Name]++
failures[app.DisplayName]++
} else {
// set to unhealthy if we hit the failure threshold.
// we stop incrementing at the threshold to prevent the failure value from increasing forever.
health[app.Name] = codersdk.WorkspaceAppHealthUnhealthy
health[app.DisplayName] = codersdk.WorkspaceAppHealthUnhealthy
}
mu.Unlock()
} else {
mu.Lock()
// we only need one successful health check to be considered healthy.
health[app.Name] = codersdk.WorkspaceAppHealthHealthy
failures[app.Name] = 0
health[app.DisplayName] = codersdk.WorkspaceAppHealthHealthy
failures[app.DisplayName] = 0
mu.Unlock()
}

View File

@ -27,12 +27,12 @@ func TestAppHealth(t *testing.T) {
defer cancel()
apps := []codersdk.WorkspaceApp{
{
Name: "app1",
DisplayName: "app1",
Healthcheck: codersdk.Healthcheck{},
Health: codersdk.WorkspaceAppHealthDisabled,
},
{
Name: "app2",
DisplayName: "app2",
Healthcheck: codersdk.Healthcheck{
// URL: We don't set the URL for this test because the setup will
// create a httptest server for us and set it for us.
@ -69,7 +69,7 @@ func TestAppHealth(t *testing.T) {
defer cancel()
apps := []codersdk.WorkspaceApp{
{
Name: "app2",
DisplayName: "app2",
Healthcheck: codersdk.Healthcheck{
// URL: We don't set the URL for this test because the setup will
// create a httptest server for us and set it for us.
@ -102,7 +102,7 @@ func TestAppHealth(t *testing.T) {
defer cancel()
apps := []codersdk.WorkspaceApp{
{
Name: "app2",
DisplayName: "app2",
Healthcheck: codersdk.Healthcheck{
// URL: We don't set the URL for this test because the setup will
// create a httptest server for us and set it for us.
@ -137,7 +137,7 @@ func TestAppHealth(t *testing.T) {
defer cancel()
apps := []codersdk.WorkspaceApp{
{
Name: "app2",
DisplayName: "app2",
Healthcheck: codersdk.Healthcheck{
// URL: We don't set the URL for this test because the setup will
// create a httptest server for us and set it for us.
@ -187,7 +187,7 @@ func setupAppReporter(ctx context.Context, t *testing.T, apps []codersdk.Workspa
mu.Lock()
for name, health := range req.Healths {
for i, app := range apps {
if app.Name != name {
if app.DisplayName != name {
continue
}
app.Health = health

View File

@ -51,7 +51,11 @@ func TestScheduleShow(t *testing.T) {
lines := strings.Split(strings.TrimSpace(stdoutBuf.String()), "\n")
if assert.Len(t, lines, 4) {
assert.Contains(t, lines[0], "Starts at 7:30AM Mon-Fri (Europe/Dublin)")
assert.Contains(t, lines[1], "Starts next 7:30AM IST on ")
assert.Contains(t, lines[1], "Starts next 7:30AM")
// it should have either IST or GMT
if !strings.Contains(lines[1], "IST") && !strings.Contains(lines[1], "GMT") {
t.Error("expected either IST or GMT")
}
assert.Contains(t, lines[2], "Stops at 8h after start")
assert.NotContains(t, lines[3], "Stops next -")
}
@ -137,7 +141,11 @@ func TestScheduleStart(t *testing.T) {
lines := strings.Split(strings.TrimSpace(stdoutBuf.String()), "\n")
if assert.Len(t, lines, 4) {
assert.Contains(t, lines[0], "Starts at 9:30AM Mon-Fri (Europe/Dublin)")
assert.Contains(t, lines[1], "Starts next 9:30AM IST on")
assert.Contains(t, lines[1], "Starts next 9:30AM")
// it should have either IST or GMT
if !strings.Contains(lines[1], "IST") && !strings.Contains(lines[1], "GMT") {
t.Error("expected either IST or GMT")
}
}
// Ensure autostart schedule updated

View File

@ -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,

View File

@ -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,

View File

@ -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);

View 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";

View 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;

View 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;

View File

@ -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;

View File

@ -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 {

View File

@ -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)

View File

@ -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
}

View File

@ -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;

View File

@ -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

View File

@ -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);

View File

@ -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

View File

@ -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",

View File

@ -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 != "",

View File

@ -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
}
}

View File

@ -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,

View File

@ -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(),
})

View File

@ -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")

View File

@ -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)

View File

@ -23,9 +23,11 @@ const (
type WorkspaceApp struct {
ID uuid.UUID `json:"id"`
// Name is a unique identifier attached to an agent.
Name string `json:"name"`
Command string `json:"command,omitempty"`
// Slug is a unique identifier within the agent.
Slug string `json:"slug"`
// DisplayName is a friendly name for the app.
DisplayName string `json:"display_name"`
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,omitempty"`

View File

@ -19,7 +19,8 @@ be used as a Coder application. For example:
# Note: Portainer must be already running in the workspace
resource "coder_app" "portainer" {
agent_id = coder_agent.main.id
name = "portainer"
slug = "portainer"
display_name = "Portainer"
icon = "https://simpleicons.org/icons/portainer.svg"
url = "https://localhost:9443/api/status"
@ -75,10 +76,11 @@ You'll also need to specify a `coder_app` resource related to the agent. This is
```hcl
resource "coder_app" "code-server" {
agent_id = coder_agent.main.id
name = "code-server"
url = "http://localhost:13337/?folder=/home/coder"
icon = "/icon/code.svg"
agent_id = coder_agent.main.id
slug = "code-server"
display_name = "code-server"
url = "http://localhost:13337/?folder=/home/coder"
icon = "/icon/code.svg"
healthcheck {
url = "http://localhost:13337/healthz"
@ -179,10 +181,11 @@ EOT
}
resource "coder_app" "intellij" {
agent_id = coder_agent.coder.id
name = "${var.jetbrains-ide}"
icon = "/icon/intellij.svg"
url = "http://localhost:8997/"
agent_id = coder_agent.coder.id
slug = "intellij"
display_name = "${var.jetbrains-ide}"
icon = "/icon/intellij.svg"
url = "http://localhost:8997/"
healthcheck {
url = "http://localhost:8997/"
@ -233,10 +236,11 @@ EOF
}
resource "coder_app" "jupyter" {
agent_id = coder_agent.coder.id
name = "JupyterLab"
url = "http://localhost:8888${local.jupyter_base_path}"
icon = "/icon/jupyter.svg"
agent_id = coder_agent.coder.id
slug = "jupyter"
display_name = "JupyterLab"
url = "http://localhost:8888${local.jupyter_base_path}"
icon = "/icon/jupyter.svg"
healthcheck {
url = "http://localhost:8888${local.jupyter_base_path}"

View File

@ -2,7 +2,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = "0.5.3"
version = "0.6.0"
}
docker = {
source = "kreuzwerker/docker"
@ -38,12 +38,13 @@ resource "coder_agent" "dev" {
}
resource "coder_app" "code-server" {
agent_id = coder_agent.dev.id
name = "code-server"
url = "http://localhost:13337/"
icon = "/icon/code.svg"
subdomain = false
share = "owner"
agent_id = coder_agent.dev.id
slug = "code-server"
display_name = "code-server"
url = "http://localhost:13337/"
icon = "/icon/code.svg"
subdomain = false
share = "owner"
healthcheck {
url = "http://localhost:13337/healthz"

View File

@ -83,17 +83,20 @@ func setupWorkspaceAgent(t *testing.T, client *codersdk.Client, user codersdk.Cr
},
Apps: []*proto.App{
{
Name: testAppNameOwner,
Slug: testAppNameOwner,
DisplayName: testAppNameOwner,
SharingLevel: proto.AppSharingLevel_OWNER,
Url: fmt.Sprintf("http://localhost:%d", appPort),
},
{
Name: testAppNameAuthenticated,
Slug: testAppNameAuthenticated,
DisplayName: testAppNameAuthenticated,
SharingLevel: proto.AppSharingLevel_AUTHENTICATED,
Url: fmt.Sprintf("http://localhost:%d", appPort),
},
{
Name: testAppNamePublic,
Slug: testAppNamePublic,
DisplayName: testAppNamePublic,
SharingLevel: proto.AppSharingLevel_PUBLIC,
Url: fmt.Sprintf("http://localhost:%d", appPort),
},

View File

@ -6,7 +6,7 @@ terraform {
}
coder = {
source = "coder/coder"
version = "0.5.3"
version = "0.6.0"
}
}
}
@ -105,12 +105,13 @@ resource "coder_agent" "coder" {
}
resource "coder_app" "code-server" {
agent_id = coder_agent.coder.id
name = "code-server"
icon = "/icon/code.svg"
url = "http://localhost:13337?folder=/home/coder"
subdomain = false
share = "owner"
agent_id = coder_agent.coder.id
slug = "code-server"
display_name = "code-server"
icon = "/icon/code.svg"
url = "http://localhost:13337?folder=/home/coder"
subdomain = false
share = "owner"
healthcheck {
url = "http://localhost:13337/healthz"

View File

@ -2,7 +2,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = "0.5.3"
version = "0.6.0"
}
}
}
@ -86,12 +86,13 @@ resource "coder_agent" "main" {
}
resource "coder_app" "code-server" {
agent_id = coder_agent.main.id
name = "code-server"
url = "http://localhost:13337/?folder=/home/coder"
icon = "/icon/code.svg"
subdomain = false
share = "owner"
agent_id = coder_agent.main.id
slug = "code-server"
display_name = "code-server"
url = "http://localhost:13337/?folder=/home/coder"
icon = "/icon/code.svg"
subdomain = false
share = "owner"
healthcheck {
url = "http://localhost:13337/healthz"

View File

@ -2,7 +2,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = "0.5.3"
version = "0.6.0"
}
}
}

View File

@ -2,7 +2,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = "0.5.3"
version = "0.6.0"
}
azurerm = {
source = "hashicorp/azurerm"

View File

@ -2,7 +2,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = "0.5.3"
version = "0.6.0"
}
digitalocean = {
source = "digitalocean/digitalocean"

View File

@ -2,7 +2,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = "0.5.3"
version = "0.6.0"
}
docker = {
source = "kreuzwerker/docker"
@ -38,12 +38,13 @@ resource "coder_agent" "main" {
}
resource "coder_app" "code-server" {
agent_id = coder_agent.main.id
name = "code-server"
url = "http://localhost:8080/?folder=/home/coder"
icon = "/icon/code.svg"
subdomain = false
share = "owner"
agent_id = coder_agent.main.id
slug = "code-server"
display_name = "code-server"
url = "http://localhost:8080/?folder=/home/coder"
icon = "/icon/code.svg"
subdomain = false
share = "owner"
healthcheck {
url = "http://localhost:8080/healthz"

View File

@ -3,7 +3,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = "0.5.3"
version = "0.6.0"
}
docker = {
source = "kreuzwerker/docker"
@ -34,12 +34,13 @@ resource "coder_agent" "main" {
}
resource "coder_app" "code-server" {
agent_id = coder_agent.main.id
name = "code-server"
url = "http://localhost:13337/?folder=/home/coder"
icon = "/icon/code.svg"
subdomain = false
share = "owner"
agent_id = coder_agent.main.id
slug = "code-server"
display_name = "code-server"
url = "http://localhost:13337/?folder=/home/coder"
icon = "/icon/code.svg"
subdomain = false
share = "owner"
healthcheck {
url = "http://localhost:13337/healthz"

View File

@ -9,7 +9,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = "0.5.3"
version = "0.6.0"
}
docker = {
source = "kreuzwerker/docker"

View File

@ -2,7 +2,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = "0.5.3"
version = "0.6.0"
}
docker = {
source = "kreuzwerker/docker"
@ -43,12 +43,13 @@ resource "coder_agent" "main" {
}
resource "coder_app" "code-server" {
agent_id = coder_agent.main.id
name = "code-server"
url = "http://localhost:13337/?folder=/home/coder"
icon = "/icon/code.svg"
subdomain = false
share = "owner"
agent_id = coder_agent.main.id
slug = "code-server"
display_name = "code-server"
url = "http://localhost:13337/?folder=/home/coder"
icon = "/icon/code.svg"
subdomain = false
share = "owner"
healthcheck {
url = "http://localhost:13337/healthz"

View File

@ -2,7 +2,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = "0.5.3"
version = "0.6.0"
}
google = {
source = "hashicorp/google"
@ -60,12 +60,13 @@ resource "coder_agent" "main" {
# code-server
resource "coder_app" "code-server" {
agent_id = coder_agent.main.id
name = "code-server"
icon = "/icon/code.svg"
url = "http://localhost:13337?folder=/home/coder"
subdomain = false
share = "owner"
agent_id = coder_agent.main.id
slug = "code-server"
display_name = "code-server"
icon = "/icon/code.svg"
url = "http://localhost:13337?folder=/home/coder"
subdomain = false
share = "owner"
healthcheck {
url = "http://localhost:13337/healthz"

View File

@ -2,7 +2,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = "0.5.3"
version = "0.6.0"
}
google = {
source = "hashicorp/google"
@ -50,12 +50,13 @@ resource "coder_agent" "main" {
# code-server
resource "coder_app" "code-server" {
agent_id = coder_agent.main.id
name = "code-server"
icon = "/icon/code.svg"
url = "http://localhost:13337?folder=/home/coder"
subdomain = false
share = "owner"
agent_id = coder_agent.main.id
slug = "code-server"
display_name = "code-server"
icon = "/icon/code.svg"
url = "http://localhost:13337?folder=/home/coder"
subdomain = false
share = "owner"
healthcheck {
url = "http://localhost:13337/healthz"

View File

@ -2,7 +2,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = "0.5.3"
version = "0.6.0"
}
google = {
source = "hashicorp/google"

View File

@ -2,7 +2,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = "0.5.3"
version = "0.6.0"
}
kubernetes = {
source = "hashicorp/kubernetes"
@ -70,12 +70,13 @@ resource "coder_agent" "main" {
# code-server
resource "coder_app" "code-server" {
agent_id = coder_agent.main.id
name = "code-server"
icon = "/icon/code.svg"
url = "http://localhost:13337?folder=/home/coder"
subdomain = false
share = "owner"
agent_id = coder_agent.main.id
slug = "code-server"
display_name = "code-server"
icon = "/icon/code.svg"
url = "http://localhost:13337?folder=/home/coder"
subdomain = false
share = "owner"
healthcheck {
url = "http://localhost:13337/healthz"

15
provisioner/appslug.go Normal file
View File

@ -0,0 +1,15 @@
package provisioner
import "regexp"
var (
// AppSlugRegex is the regex used to validate the slug of a coder_app
// resource. It must be a valid hostname and cannot contain two consecutive
// hyphens or start/end with a hyphen.
//
// This regex is duplicated in the terraform provider code, so make sure to
// update it there as well.
//
// There are test cases for this regex in appslug_test.go.
AppSlugRegex = regexp.MustCompile(`^[a-z0-9](-?[a-z0-9])*$`)
)

View File

@ -0,0 +1,64 @@
package provisioner_test
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/coder/coder/provisioner"
)
func TestValidAppSlugRegex(t *testing.T) {
t.Parallel()
t.Run("Valid", func(t *testing.T) {
t.Parallel()
validStrings := []string{
"a",
"1",
"a1",
"1a",
"1a1",
"1-1",
"a-a",
"ab-cd",
"ab-cd-ef",
"abc-123",
"a-123",
"abc-1",
"ab-c",
"a-bc",
}
for _, s := range validStrings {
require.True(t, provisioner.AppSlugRegex.MatchString(s), s)
}
})
t.Run("Invalid", func(t *testing.T) {
t.Parallel()
invalidStrings := []string{
"",
"-",
"-abc",
"abc-",
"ab--cd",
"a--bc",
"ab--c",
"_",
"ab_cd",
"_abc",
"abc_",
" ",
"abc ",
" abc",
"ab cd",
}
for _, s := range invalidStrings {
require.False(t, provisioner.AppSlugRegex.MatchString(s), s)
}
})
}

View File

@ -8,6 +8,7 @@ import (
"github.com/mitchellh/mapstructure"
"golang.org/x/xerrors"
"github.com/coder/coder/provisioner"
"github.com/coder/coder/provisionersdk/proto"
)
@ -25,7 +26,12 @@ type agentAttributes struct {
// A mapping of attributes on the "coder_app" resource.
type agentAppAttributes struct {
AgentID string `mapstructure:"agent_id"`
AgentID string `mapstructure:"agent_id"`
// Slug is required in terraform, but to avoid breaking existing users we
// will default to the resource name if it is not specified.
Slug string `mapstructure:"slug"`
DisplayName string `mapstructure:"display_name"`
// Name is deprecated in favor of DisplayName.
Name string `mapstructure:"name"`
Icon string `mapstructure:"icon"`
URL string `mapstructure:"url"`
@ -214,19 +220,40 @@ func ConvertResources(module *tfjson.StateModule, rawGraph string) ([]*proto.Res
}
// Associate Apps with agents.
appSlugs := make(map[string]struct{})
for _, resource := range tfResourceByLabel {
if resource.Type != "coder_app" {
continue
}
var attrs agentAppAttributes
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
// Default to the resource name if none is set!
if attrs.Slug == "" {
attrs.Slug = resource.Name
}
if attrs.DisplayName == "" {
if attrs.Name != "" {
// Name is deprecated but still accepted.
attrs.DisplayName = attrs.Name
} else {
attrs.DisplayName = attrs.Slug
}
}
if !provisioner.AppSlugRegex.MatchString(attrs.Slug) {
return nil, xerrors.Errorf("invalid app slug %q, please update your coder/coder provider to the latest version and specify the slug property on each coder_app", attrs.Slug)
}
if _, exists := appSlugs[attrs.Slug]; exists {
return nil, xerrors.Errorf("duplicate app slug, they must be unique per template: %q", attrs.Slug)
}
appSlugs[attrs.Slug] = struct{}{}
var healthcheck *proto.Healthcheck
if len(attrs.Healthcheck) != 0 {
healthcheck = &proto.Healthcheck{
@ -253,7 +280,8 @@ func ConvertResources(module *tfjson.StateModule, rawGraph string) ([]*proto.Res
continue
}
agent.Apps = append(agent.Apps, &proto.App{
Name: attrs.Name,
Slug: attrs.Slug,
DisplayName: attrs.DisplayName,
Command: attrs.Command,
Url: attrs.URL,
Icon: attrs.Icon,

View File

@ -110,13 +110,15 @@ func TestConvertResources(t *testing.T) {
Architecture: "amd64",
Apps: []*proto.App{
{
Name: "app1",
Slug: "app1",
DisplayName: "app1",
// Subdomain defaults to false if unspecified.
Subdomain: false,
},
{
Name: "app2",
Subdomain: true,
Slug: "app2",
DisplayName: "app2",
Subdomain: true,
Healthcheck: &proto.Healthcheck{
Url: "http://localhost:13337/healthz",
Interval: 5,
@ -124,8 +126,9 @@ func TestConvertResources(t *testing.T) {
},
},
{
Name: "app3",
Subdomain: false,
Slug: "app3",
DisplayName: "app3",
Subdomain: false,
},
},
Auth: &proto.Agent_Token{},
@ -182,12 +185,23 @@ func TestConvertResources(t *testing.T) {
expectedNoMetadata = append(expectedNoMetadata, resourceCopy)
}
resourcesWant, err := json.Marshal(expectedNoMetadata)
// Convert expectedNoMetadata and resources into a
// []map[string]interface{} so they can be compared easily.
data, err := json.Marshal(expectedNoMetadata)
require.NoError(t, err)
resourcesGot, err := json.Marshal(resources)
var expectedNoMetadataMap []map[string]interface{}
err = json.Unmarshal(data, &expectedNoMetadataMap)
require.NoError(t, err)
require.Equal(t, string(resourcesWant), string(resourcesGot))
data, err = json.Marshal(resources)
require.NoError(t, err)
var resourcesMap []map[string]interface{}
err = json.Unmarshal(data, &resourcesMap)
require.NoError(t, err)
require.Equal(t, expectedNoMetadataMap, resourcesMap)
})
t.Run("Provision", func(t *testing.T) {
t.Parallel()
tfStateRaw, err := os.ReadFile(filepath.Join(dir, folderName+".tfstate.json"))
@ -212,17 +226,67 @@ func TestConvertResources(t *testing.T) {
}
}
}
resourcesWant, err := json.Marshal(expected)
// Convert expectedNoMetadata and resources into a
// []map[string]interface{} so they can be compared easily.
data, err := json.Marshal(expected)
require.NoError(t, err)
resourcesGot, err := json.Marshal(resources)
var expectedMap []map[string]interface{}
err = json.Unmarshal(data, &expectedMap)
require.NoError(t, err)
require.Equal(t, string(resourcesWant), string(resourcesGot))
data, err = json.Marshal(resources)
require.NoError(t, err)
var resourcesMap []map[string]interface{}
err = json.Unmarshal(data, &resourcesMap)
require.NoError(t, err)
require.Equal(t, expectedMap, resourcesMap)
})
})
}
}
func TestAppSlugValidation(t *testing.T) {
t.Parallel()
// nolint:dogsled
_, filename, _, _ := runtime.Caller(0)
// Load the multiple-apps state file and edit it.
dir := filepath.Join(filepath.Dir(filename), "testdata", "multiple-apps")
tfPlanRaw, err := os.ReadFile(filepath.Join(dir, "multiple-apps.tfplan.json"))
require.NoError(t, err)
var tfPlan tfjson.Plan
err = json.Unmarshal(tfPlanRaw, &tfPlan)
require.NoError(t, err)
tfPlanGraph, err := os.ReadFile(filepath.Join(dir, "multiple-apps.tfplan.dot"))
require.NoError(t, err)
// Change all slugs to be invalid.
for _, resource := range tfPlan.PlannedValues.RootModule.Resources {
if resource.Type == "coder_app" {
resource.AttributeValues["slug"] = "$$$ invalid slug $$$"
}
}
resources, err := terraform.ConvertResources(tfPlan.PlannedValues.RootModule, string(tfPlanGraph))
require.Nil(t, resources)
require.Error(t, err)
require.ErrorContains(t, err, "invalid app slug")
// Change all slugs to be identical and valid.
for _, resource := range tfPlan.PlannedValues.RootModule.Resources {
if resource.Type == "coder_app" {
resource.AttributeValues["slug"] = "valid"
}
}
resources, err = terraform.ConvertResources(tfPlan.PlannedValues.RootModule, string(tfPlanGraph))
require.Nil(t, resources)
require.Error(t, err)
require.ErrorContains(t, err, "duplicate app slug")
}
func TestInstanceIDAssociation(t *testing.T) {
t.Parallel()
type tc struct {
@ -304,7 +368,7 @@ func sortResources(resources []*proto.Resource) {
for _, resource := range resources {
for _, agent := range resource.Agents {
sort.Slice(agent.Apps, func(i, j int) bool {
return agent.Apps[i].Name < agent.Apps[j].Name
return agent.Apps[i].Slug < agent.Apps[j].Slug
})
}
sort.Slice(resource.Agents, func(i, j int) bool {

View File

@ -2,7 +2,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = "0.5.3"
version = "0.6.0"
}
}
}

View File

@ -66,7 +66,9 @@
"name": "main",
"provider_name": "registry.terraform.io/coder/coder",
"change": {
"actions": ["create"],
"actions": [
"create"
],
"before": null,
"after": {
"arch": "amd64",
@ -95,7 +97,9 @@
"name": "script",
"provider_name": "registry.terraform.io/hashicorp/null",
"change": {
"actions": ["read"],
"actions": [
"read"
],
"before": null,
"after": {
"inputs": {}
@ -125,7 +129,9 @@
"name": "example",
"provider_name": "registry.terraform.io/hashicorp/null",
"change": {
"actions": ["create"],
"actions": [
"create"
],
"before": null,
"after": {
"triggers": null
@ -143,7 +149,7 @@
"coder": {
"name": "coder",
"full_name": "registry.terraform.io/coder/coder",
"version_constraint": "0.5.0"
"version_constraint": "0.6.0"
},
"module.module:null": {
"name": "null",
@ -175,7 +181,10 @@
"source": "./module",
"expressions": {
"script": {
"references": ["coder_agent.main.init_script", "coder_agent.main"]
"references": [
"coder_agent.main.init_script",
"coder_agent.main"
]
}
},
"module": {
@ -187,7 +196,9 @@
"name": "example",
"provider_config_key": "module.module:null",
"schema_version": 0,
"depends_on": ["data.null_data_source.script"]
"depends_on": [
"data.null_data_source.script"
]
},
{
"address": "data.null_data_source.script",
@ -197,7 +208,9 @@
"provider_config_key": "module.module:null",
"expressions": {
"inputs": {
"references": ["var.script"]
"references": [
"var.script"
]
}
},
"schema_version": 0
@ -214,7 +227,9 @@
"relevant_attributes": [
{
"resource": "coder_agent.main",
"attribute": ["init_script"]
"attribute": [
"init_script"
]
}
]
}

View File

@ -16,11 +16,11 @@
"auth": "token",
"dir": null,
"env": null,
"id": "b92bd0ce-d854-47af-a2f6-4941cd5dbd27",
"id": "8a08d6a8-2ae8-4af3-b385-9d7c9230c3d3",
"init_script": "",
"os": "linux",
"startup_script": null,
"token": "3f1b6b3f-7ea9-4944-bef4-8be9b78db8ae"
"token": "e5397170-34e8-4f59-9b3d-85d11203aba1"
},
"sensitive_values": {}
}
@ -44,7 +44,7 @@
"outputs": {
"script": ""
},
"random": "5257014674084238393"
"random": "4606778210381604065"
},
"sensitive_values": {
"inputs": {},
@ -59,7 +59,7 @@
"provider_name": "registry.terraform.io/hashicorp/null",
"schema_version": 0,
"values": {
"id": "6805057619323391144",
"id": "8484494817832091886",
"triggers": null
},
"sensitive_values": {},

View File

@ -2,7 +2,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = "0.5.3"
version = "0.6.0"
}
}
}

View File

@ -121,7 +121,7 @@
"coder": {
"name": "coder",
"full_name": "registry.terraform.io/coder/coder",
"version_constraint": "0.5.0"
"version_constraint": "0.6.0"
},
"null": {
"name": "null",

View File

@ -16,11 +16,11 @@
"auth": "token",
"dir": null,
"env": null,
"id": "d8de89cb-bb6b-4f4f-80f8-e5d39e8c5f62",
"id": "8c46ed09-5988-47fe-8f1b-2afe4ec0b35a",
"init_script": "",
"os": "linux",
"startup_script": null,
"token": "4e877d5c-95c4-4365-b9a1-856348b54f43"
"token": "af26634c-4fa8-4b60-aff4-736d43457b35"
},
"sensitive_values": {}
},
@ -32,7 +32,7 @@
"provider_name": "registry.terraform.io/hashicorp/null",
"schema_version": 0,
"values": {
"id": "2870641260310442024",
"id": "1333327345487383126",
"triggers": null
},
"sensitive_values": {},
@ -46,7 +46,7 @@
"provider_name": "registry.terraform.io/hashicorp/null",
"schema_version": 0,
"values": {
"id": "7093709823890756895",
"id": "1306294717300675697",
"triggers": null
},
"sensitive_values": {},

View File

@ -2,7 +2,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = "0.5.3"
version = "0.6.0"
}
}
}

View File

@ -121,7 +121,7 @@
"coder": {
"name": "coder",
"full_name": "registry.terraform.io/coder/coder",
"version_constraint": "0.5.0"
"version_constraint": "0.6.0"
},
"null": {
"name": "null",

View File

@ -16,11 +16,11 @@
"auth": "token",
"dir": null,
"env": null,
"id": "5c00c97c-7291-47b7-96cf-3ac7d7588a99",
"id": "3621f0c7-090a-4610-8fd0-bdcf835225bd",
"init_script": "",
"os": "linux",
"startup_script": null,
"token": "a1939d12-8b8a-414b-b745-3fac020e51c0"
"token": "4cb0ef71-0161-4a1a-b8f1-b9d81f53d658"
},
"sensitive_values": {}
},
@ -32,7 +32,7 @@
"provider_name": "registry.terraform.io/hashicorp/null",
"schema_version": 0,
"values": {
"id": "8930370582092686733",
"id": "3108014752132131382",
"triggers": null
},
"sensitive_values": {},
@ -46,7 +46,7 @@
"provider_name": "registry.terraform.io/hashicorp/null",
"schema_version": 0,
"values": {
"id": "8209925920170986769",
"id": "8356243415524842498",
"triggers": null
},
"sensitive_values": {},

View File

@ -2,7 +2,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = "0.5.3"
version = "0.6.0"
}
}
}

View File

@ -122,7 +122,7 @@
"coder": {
"name": "coder",
"full_name": "registry.terraform.io/coder/coder",
"version_constraint": "0.5.0"
"version_constraint": "0.6.0"
},
"null": {
"name": "null",

View File

@ -16,11 +16,11 @@
"auth": "google-instance-identity",
"dir": null,
"env": null,
"id": "248ed639-3dbe-479e-909a-37d5d226529f",
"id": "1156666a-c202-4c54-9831-6b62dbf665fe",
"init_script": "",
"os": "linux",
"startup_script": null,
"token": "8bee2595-095f-4965-ade2-deef475023d6"
"token": "80a893a4-fcb1-4a3a-824d-74cf5317d307"
},
"sensitive_values": {}
},
@ -32,8 +32,8 @@
"provider_name": "registry.terraform.io/coder/coder",
"schema_version": 0,
"values": {
"agent_id": "248ed639-3dbe-479e-909a-37d5d226529f",
"id": "edbfac7a-a88d-433a-ab7c-be3816656477",
"agent_id": "1156666a-c202-4c54-9831-6b62dbf665fe",
"id": "ec6451f5-fea2-4d6f-aedc-822b93723abd",
"instance_id": "example"
},
"sensitive_values": {},
@ -47,7 +47,7 @@
"provider_name": "registry.terraform.io/hashicorp/null",
"schema_version": 0,
"values": {
"id": "5674804341417746589",
"id": "5076117657273396114",
"triggers": null
},
"sensitive_values": {},

View File

@ -2,7 +2,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = "0.5.3"
version = "0.6.0"
}
}
}

View File

@ -180,7 +180,7 @@
"coder": {
"name": "coder",
"full_name": "registry.terraform.io/coder/coder",
"version_constraint": "0.5.0"
"version_constraint": "0.6.0"
},
"null": {
"name": "null",

View File

@ -16,11 +16,11 @@
"auth": "token",
"dir": null,
"env": null,
"id": "882ce97a-3c12-410f-8916-e3bc03862162",
"id": "dc6b52bf-7bcb-4657-9c11-2859d8721ba9",
"init_script": "",
"os": "linux",
"startup_script": null,
"token": "b24ba29b-8cb3-42da-91c5-599c7be310f7"
"token": "85317d35-1e92-4565-850e-8ee17bf86992"
},
"sensitive_values": {}
},
@ -36,11 +36,11 @@
"auth": "token",
"dir": null,
"env": null,
"id": "8a26cec7-3189-4eaf-99a1-1dce00b756dc",
"id": "a709bb80-b4df-4d4a-9cc3-4bedd009b44f",
"init_script": "",
"os": "darwin",
"startup_script": null,
"token": "6a155e3b-3279-40cb-9c16-4b827b561bc1"
"token": "a4b37df4-dbdd-494b-9434-92abaa88c23b"
},
"sensitive_values": {}
},
@ -56,11 +56,11 @@
"auth": "token",
"dir": null,
"env": null,
"id": "57486477-64a5-4fea-8223-dbf3c259d710",
"id": "e429fb2c-1d4a-4c7c-9747-f495e5611c9e",
"init_script": "",
"os": "windows",
"startup_script": null,
"token": "0fa9933e-802a-4d6a-b273-43c05993e52a"
"token": "27009ab7-ec2e-476c-9193-177eeea0766c"
},
"sensitive_values": {}
},
@ -72,7 +72,7 @@
"provider_name": "registry.terraform.io/hashicorp/null",
"schema_version": 0,
"values": {
"id": "8587500025119121667",
"id": "4682926564646626748",
"triggers": null
},
"sensitive_values": {},

View File

@ -2,7 +2,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = "0.5.3"
version = "0.6.0"
}
}
}
@ -15,6 +15,7 @@ resource "coder_agent" "dev1" {
# app1 is for testing subdomain default.
resource "coder_app" "app1" {
agent_id = coder_agent.dev1.id
slug = "app1"
# subdomain should default to false.
# subdomain = false
}
@ -22,6 +23,7 @@ resource "coder_app" "app1" {
# app2 tests that subdomaincan be true, and that healthchecks work.
resource "coder_app" "app2" {
agent_id = coder_agent.dev1.id
slug = "app2"
subdomain = true
healthcheck {
url = "http://localhost:13337/healthz"
@ -33,6 +35,7 @@ resource "coder_app" "app2" {
# app3 tests that subdomain can explicitly be false.
resource "coder_app" "app3" {
agent_id = coder_agent.dev1.id
slug = "app3"
subdomain = false
}

View File

@ -30,10 +30,13 @@
"schema_version": 0,
"values": {
"command": null,
"display_name": null,
"healthcheck": [],
"icon": null,
"name": null,
"relative_path": null,
"share": "owner",
"slug": "app1",
"subdomain": null,
"url": null
},
@ -50,6 +53,7 @@
"schema_version": 0,
"values": {
"command": null,
"display_name": null,
"healthcheck": [
{
"interval": 5,
@ -60,6 +64,8 @@
"icon": null,
"name": null,
"relative_path": null,
"share": "owner",
"slug": "app2",
"subdomain": true,
"url": null
},
@ -76,10 +82,13 @@
"schema_version": 0,
"values": {
"command": null,
"display_name": null,
"healthcheck": [],
"icon": null,
"name": null,
"relative_path": null,
"share": "owner",
"slug": "app3",
"subdomain": false,
"url": null
},
@ -142,10 +151,13 @@
"before": null,
"after": {
"command": null,
"display_name": null,
"healthcheck": [],
"icon": null,
"name": null,
"relative_path": null,
"share": "owner",
"slug": "app1",
"subdomain": null,
"url": null
},
@ -171,6 +183,7 @@
"before": null,
"after": {
"command": null,
"display_name": null,
"healthcheck": [
{
"interval": 5,
@ -181,6 +194,8 @@
"icon": null,
"name": null,
"relative_path": null,
"share": "owner",
"slug": "app2",
"subdomain": true,
"url": null
},
@ -206,10 +221,13 @@
"before": null,
"after": {
"command": null,
"display_name": null,
"healthcheck": [],
"icon": null,
"name": null,
"relative_path": null,
"share": "owner",
"slug": "app3",
"subdomain": false,
"url": null
},
@ -249,7 +267,7 @@
"coder": {
"name": "coder",
"full_name": "registry.terraform.io/coder/coder",
"version_constraint": "0.5.0"
"version_constraint": "0.6.0"
},
"null": {
"name": "null",
@ -283,6 +301,9 @@
"expressions": {
"agent_id": {
"references": ["coder_agent.dev1.id", "coder_agent.dev1"]
},
"slug": {
"constant_value": "app1"
}
},
"schema_version": 0
@ -310,6 +331,9 @@
}
}
],
"slug": {
"constant_value": "app2"
},
"subdomain": {
"constant_value": true
}
@ -326,6 +350,9 @@
"agent_id": {
"references": ["coder_agent.dev1.id", "coder_agent.dev1"]
},
"slug": {
"constant_value": "app3"
},
"subdomain": {
"constant_value": false
}

View File

@ -16,11 +16,11 @@
"auth": "token",
"dir": null,
"env": null,
"id": "ecf210c8-aaa7-4a14-9b44-2a5f805f0126",
"id": "4fa379bd-8aa9-48f2-9868-2da104013c3c",
"init_script": "",
"os": "linux",
"startup_script": null,
"token": "7e748146-cea2-45cb-927d-b4a90b0021b3"
"token": "4eb813cb-8f29-454c-91d9-b430d76d7fcd"
},
"sensitive_values": {}
},
@ -32,13 +32,16 @@
"provider_name": "registry.terraform.io/coder/coder",
"schema_version": 0,
"values": {
"agent_id": "ecf210c8-aaa7-4a14-9b44-2a5f805f0126",
"agent_id": "4fa379bd-8aa9-48f2-9868-2da104013c3c",
"command": null,
"display_name": null,
"healthcheck": [],
"icon": null,
"id": "95667002-bd60-4d2c-9313-0666f66c44ff",
"id": "f303f406-b9ea-4253-935e-f80f7be54a97",
"name": null,
"relative_path": null,
"share": "owner",
"slug": "app1",
"subdomain": null,
"url": null
},
@ -55,8 +58,9 @@
"provider_name": "registry.terraform.io/coder/coder",
"schema_version": 0,
"values": {
"agent_id": "ecf210c8-aaa7-4a14-9b44-2a5f805f0126",
"agent_id": "4fa379bd-8aa9-48f2-9868-2da104013c3c",
"command": null,
"display_name": null,
"healthcheck": [
{
"interval": 5,
@ -65,9 +69,11 @@
}
],
"icon": null,
"id": "817c6904-69e1-485f-a057-4ddac83a9c5a",
"id": "7086ae57-501d-4b39-bfaf-d30b83f753d4",
"name": null,
"relative_path": null,
"share": "owner",
"slug": "app2",
"subdomain": true,
"url": null
},
@ -84,13 +90,16 @@
"provider_name": "registry.terraform.io/coder/coder",
"schema_version": 0,
"values": {
"agent_id": "ecf210c8-aaa7-4a14-9b44-2a5f805f0126",
"agent_id": "4fa379bd-8aa9-48f2-9868-2da104013c3c",
"command": null,
"display_name": null,
"healthcheck": [],
"icon": null,
"id": "c4a502b3-cc82-4fdf-952b-4b429e711798",
"id": "e4b1f16b-2b8d-4278-abec-1f876f8a6aba",
"name": null,
"relative_path": null,
"share": "owner",
"slug": "app3",
"subdomain": false,
"url": null
},
@ -107,7 +116,7 @@
"provider_name": "registry.terraform.io/hashicorp/null",
"schema_version": 0,
"values": {
"id": "1281108380136021489",
"id": "7676198272426781226",
"triggers": null
},
"sensitive_values": {},

View File

@ -2,7 +2,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = "0.5.3"
version = "0.6.0"
}
}
}

View File

@ -186,7 +186,7 @@
"coder": {
"name": "coder",
"full_name": "registry.terraform.io/coder/coder",
"version_constraint": "0.5.0"
"version_constraint": "0.6.0"
},
"null": {
"name": "null",

View File

@ -16,11 +16,11 @@
"auth": "token",
"dir": null,
"env": null,
"id": "0bfa269a-e373-4fbc-929a-07b8ed0f3477",
"id": "a7e62a9d-ef94-4abc-8bd5-e0555eae4aaf",
"init_script": "",
"os": "linux",
"startup_script": null,
"token": "4bc54f84-7d97-492a-ad98-40ae7dfbb300"
"token": "812935fe-858a-4ff5-b890-6c8eea6a3764"
},
"sensitive_values": {}
},
@ -34,7 +34,7 @@
"values": {
"hide": true,
"icon": "/icon/server.svg",
"id": "2ee6d253-dec1-4336-95ba-bd5e93cf4c84",
"id": "5e954683-7a6d-47f4-bc82-5831c0ea2120",
"item": [
{
"is_null": false,
@ -61,7 +61,7 @@
"value": "squirrel"
}
],
"resource_id": "3043919679469754967"
"resource_id": "288893601116381968"
},
"sensitive_values": {
"item": [{}, {}, {}, {}]
@ -76,7 +76,7 @@
"provider_name": "registry.terraform.io/hashicorp/null",
"schema_version": 0,
"values": {
"id": "3043919679469754967",
"id": "288893601116381968",
"triggers": null
},
"sensitive_values": {}

View File

@ -899,13 +899,16 @@ 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"`
Url string `protobuf:"bytes,3,opt,name=url,proto3" json:"url,omitempty"`
Icon string `protobuf:"bytes,4,opt,name=icon,proto3" json:"icon,omitempty"`
Subdomain bool `protobuf:"varint,5,opt,name=subdomain,proto3" json:"subdomain,omitempty"`
Healthcheck *Healthcheck `protobuf:"bytes,6,opt,name=healthcheck,proto3" json:"healthcheck,omitempty"`
SharingLevel AppSharingLevel `protobuf:"varint,7,opt,name=sharing_level,json=sharingLevel,proto3,enum=provisioner.AppSharingLevel" json:"sharing_level,omitempty"`
// slug is the unique identifier for the app, usually the name from the
// template. It must be URL-safe and hostname-safe.
Slug string `protobuf:"bytes,1,opt,name=slug,proto3" json:"slug,omitempty"`
DisplayName string `protobuf:"bytes,2,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"`
Command string `protobuf:"bytes,3,opt,name=command,proto3" json:"command,omitempty"`
Url string `protobuf:"bytes,4,opt,name=url,proto3" json:"url,omitempty"`
Icon string `protobuf:"bytes,5,opt,name=icon,proto3" json:"icon,omitempty"`
Subdomain bool `protobuf:"varint,6,opt,name=subdomain,proto3" json:"subdomain,omitempty"`
Healthcheck *Healthcheck `protobuf:"bytes,7,opt,name=healthcheck,proto3" json:"healthcheck,omitempty"`
SharingLevel AppSharingLevel `protobuf:"varint,8,opt,name=sharing_level,json=sharingLevel,proto3,enum=provisioner.AppSharingLevel" json:"sharing_level,omitempty"`
}
func (x *App) Reset() {
@ -940,9 +943,16 @@ func (*App) Descriptor() ([]byte, []int) {
return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{8}
}
func (x *App) GetName() string {
func (x *App) GetSlug() string {
if x != nil {
return x.Name
return x.Slug
}
return ""
}
func (x *App) GetDisplayName() string {
if x != nil {
return x.DisplayName
}
return ""
}
@ -2009,148 +2019,150 @@ var file_provisionersdk_proto_provisioner_proto_rawDesc = []byte{
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, 0xf6,
0x01, 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, 0x1c, 0x0a, 0x09, 0x73, 0x75,
0x62, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x73,
0x75, 0x62, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x3a, 0x0a, 0x0b, 0x68, 0x65, 0x61, 0x6c,
0x74, 0x68, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e,
0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x48, 0x65, 0x61, 0x6c,
0x74, 0x68, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x0b, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x63,
0x68, 0x65, 0x63, 0x6b, 0x12, 0x41, 0x0a, 0x0d, 0x73, 0x68, 0x61, 0x72, 0x69, 0x6e, 0x67, 0x5f,
0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x70, 0x72,
0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x41, 0x70, 0x70, 0x53, 0x68, 0x61,
0x72, 0x69, 0x6e, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x0c, 0x73, 0x68, 0x61, 0x72, 0x69,
0x6e, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x22, 0x59, 0x0a, 0x0b, 0x48, 0x65, 0x61, 0x6c, 0x74,
0x68, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20,
0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x69, 0x6e, 0x74, 0x65,
0x72, 0x76, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x69, 0x6e, 0x74, 0x65,
0x72, 0x76, 0x61, 0x6c, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c,
0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x74, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f,
0x6c, 0x64, 0x22, 0xad, 0x02, 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, 0x12, 0x3a, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18,
0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f,
0x6e, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4d, 0x65, 0x74,
0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12,
0x12, 0x0a, 0x04, 0x68, 0x69, 0x64, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x68,
0x69, 0x64, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x69, 0x63, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28,
0x09, 0x52, 0x04, 0x69, 0x63, 0x6f, 0x6e, 0x1a, 0x69, 0x0a, 0x08, 0x4d, 0x65, 0x74, 0x61, 0x64,
0x61, 0x74, 0x61, 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, 0x12, 0x1c, 0x0a, 0x09, 0x73,
0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09,
0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x69, 0x73, 0x5f,
0x6e, 0x75, 0x6c, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x69, 0x73, 0x4e, 0x75,
0x6c, 0x6c, 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,
0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x06, 0x0a, 0x04, 0x61, 0x75, 0x74, 0x68, 0x22, 0x99,
0x02, 0x0a, 0x03, 0x41, 0x70, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x6c, 0x75, 0x67, 0x18, 0x01,
0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x73, 0x6c, 0x75, 0x67, 0x12, 0x21, 0x0a, 0x0c, 0x64, 0x69,
0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
0x52, 0x0b, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a,
0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07,
0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x04,
0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x69, 0x63, 0x6f,
0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x69, 0x63, 0x6f, 0x6e, 0x12, 0x1c, 0x0a,
0x09, 0x73, 0x75, 0x62, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08,
0x52, 0x09, 0x73, 0x75, 0x62, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x3a, 0x0a, 0x0b, 0x68,
0x65, 0x61, 0x6c, 0x74, 0x68, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b,
0x32, 0x18, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x48,
0x65, 0x61, 0x6c, 0x74, 0x68, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x0b, 0x68, 0x65, 0x61, 0x6c,
0x74, 0x68, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x12, 0x41, 0x0a, 0x0d, 0x73, 0x68, 0x61, 0x72, 0x69,
0x6e, 0x67, 0x5f, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c,
0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x41, 0x70, 0x70,
0x53, 0x68, 0x61, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x0c, 0x73, 0x68,
0x61, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x22, 0x59, 0x0a, 0x0b, 0x48, 0x65,
0x61, 0x6c, 0x74, 0x68, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c,
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x69,
0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x69,
0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x68, 0x72, 0x65, 0x73,
0x68, 0x6f, 0x6c, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x74, 0x68, 0x72, 0x65,
0x73, 0x68, 0x6f, 0x6c, 0x64, 0x22, 0xad, 0x02, 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, 0x12, 0x3a, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61,
0x74, 0x61, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69,
0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e,
0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61,
0x74, 0x61, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x69, 0x64, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08,
0x52, 0x04, 0x68, 0x69, 0x64, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x69, 0x63, 0x6f, 0x6e, 0x18, 0x06,
0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x69, 0x63, 0x6f, 0x6e, 0x1a, 0x69, 0x0a, 0x08, 0x4d, 0x65,
0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 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, 0x12, 0x1c,
0x0a, 0x09, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28,
0x08, 0x52, 0x09, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x12, 0x17, 0x0a, 0x07,
0x69, 0x73, 0x5f, 0x6e, 0x75, 0x6c, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x69,
0x73, 0x4e, 0x75, 0x6c, 0x6c, 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, 0xae, 0x07, 0x0a, 0x09, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69,
0x6f, 0x6e, 0x1a, 0xd1, 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, 0x12, 0x32, 0x0a, 0x15, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f,
0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x07, 0x20, 0x01, 0x28,
0x09, 0x52, 0x13, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65,
0x72, 0x45, 0x6d, 0x61, 0x69, 0x6c, 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, 0x39,
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, 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, 0xae, 0x07, 0x0a, 0x09, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x1a,
0xd1, 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, 0x12,
0x32, 0x0a, 0x15, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e,
0x65, 0x72, 0x5f, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13,
0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x45, 0x6d,
0x61, 0x69, 0x6c, 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,
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, 0x3b, 0x0a, 0x0f, 0x41, 0x70, 0x70, 0x53, 0x68, 0x61,
0x72, 0x69, 0x6e, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x09, 0x0a, 0x05, 0x4f, 0x57, 0x4e,
0x45, 0x52, 0x10, 0x00, 0x12, 0x11, 0x0a, 0x0d, 0x41, 0x55, 0x54, 0x48, 0x45, 0x4e, 0x54, 0x49,
0x43, 0x41, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x50, 0x55, 0x42, 0x4c, 0x49,
0x43, 0x10, 0x02, 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, 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, 0x3b, 0x0a, 0x0f, 0x41, 0x70, 0x70, 0x53, 0x68, 0x61, 0x72, 0x69, 0x6e,
0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x09, 0x0a, 0x05, 0x4f, 0x57, 0x4e, 0x45, 0x52, 0x10,
0x00, 0x12, 0x11, 0x0a, 0x0d, 0x41, 0x55, 0x54, 0x48, 0x45, 0x4e, 0x54, 0x49, 0x43, 0x41, 0x54,
0x45, 0x44, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x10, 0x02,
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,
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 (

View File

@ -88,20 +88,23 @@ message Agent {
}
enum AppSharingLevel {
OWNER = 0;
AUTHENTICATED = 1;
PUBLIC = 2;
OWNER = 0;
AUTHENTICATED = 1;
PUBLIC = 2;
}
// App represents a dev-accessible application on the workspace.
message App {
string name = 1;
string command = 2;
string url = 3;
string icon = 4;
bool subdomain = 5;
Healthcheck healthcheck = 6;
AppSharingLevel sharing_level = 7;
// slug is the unique identifier for the app, usually the name from the
// template. It must be URL-safe and hostname-safe.
string slug = 1;
string display_name = 2;
string command = 3;
string url = 4;
string icon = 5;
bool subdomain = 6;
Healthcheck healthcheck = 7;
AppSharingLevel sharing_level = 8;
}
// Healthcheck represents configuration for checking for app readiness.
@ -125,7 +128,7 @@ message Resource {
}
repeated Metadata metadata = 4;
bool hide = 5;
string icon = 6;
string icon = 6;
}
// Parse consumes source-code from a directory to produce inputs.

View File

@ -823,7 +823,8 @@ export interface WorkspaceAgentResourceMetadata {
// From codersdk/workspaceapps.go
export interface WorkspaceApp {
readonly id: string
readonly name: string
readonly slug: string
readonly display_name: string
readonly command?: string
readonly icon?: string
readonly subdomain: boolean

View File

@ -31,18 +31,27 @@ export const AppLink: FC<AppLinkProps> = ({
const styles = useStyles()
const username = workspace.owner_name
let appSlug = app.slug
let appDisplayName = app.display_name
if (!appSlug) {
appSlug = appDisplayName
}
if (!appDisplayName) {
appDisplayName = appSlug
}
// The backend redirects if the trailing slash isn't included, so we add it
// here to avoid extra roundtrips.
let href = `/@${username}/${workspace.name}.${
agent.name
}/apps/${encodeURIComponent(app.name)}/`
}/apps/${encodeURIComponent(appSlug)}/`
if (app.command) {
href = `/@${username}/${workspace.name}.${
agent.name
}/terminal?command=${encodeURIComponent(app.command)}`
}
if (appsHost && app.subdomain) {
const subdomain = `${app.name}--${agent.name}--${workspace.name}--${username}`
const subdomain = `${appSlug}--${agent.name}--${workspace.name}--${username}`
href = `${window.location.protocol}//${appsHost}/`.replace("*", subdomain)
}
@ -75,7 +84,7 @@ export const AppLink: FC<AppLinkProps> = ({
className={styles.button}
disabled={!canClick}
>
<span className={styles.appName}>{app.name}</span>
<span className={styles.appName}>{appDisplayName}</span>
</Button>
)
@ -92,7 +101,7 @@ export const AppLink: FC<AppLinkProps> = ({
event.preventDefault()
window.open(
href,
Language.appTitle(app.name, generateRandomString(12)),
Language.appTitle(appDisplayName, generateRandomString(12)),
"width=900,height=600",
)
}

View File

@ -20,7 +20,7 @@ export const AppPreviewLink: FC<AppPreviewProps> = ({ app }) => {
spacing={1}
>
<BaseIcon app={app} />
{app.name}
{app.display_name}
<ShareIcon app={app} />
</Stack>
)

View File

@ -4,7 +4,7 @@ import ComputerIcon from "@material-ui/icons/Computer"
export const BaseIcon: FC<{ app: WorkspaceApp }> = ({ app }) => {
return app.icon ? (
<img alt={`${app.name} Icon`} src={app.icon} />
<img alt={`${app.display_name} Icon`} src={app.icon} />
) : (
<ComputerIcon />
)

View File

@ -74,7 +74,7 @@ export const AgentRow: FC<AgentRowProps> = ({
<>
{agent.apps.map((app) => (
<AppLink
key={app.name}
key={app.slug}
appsHost={applicationsHost}
app={app}
agent={agent}

View File

@ -73,7 +73,7 @@ export const AgentRowPreview: FC<AgentRowPreviewProps> = ({ agent }) => {
wrap="wrap"
>
{agent.apps.map((app) => (
<AppPreviewLink key={app.name} app={app} />
<AppPreviewLink key={app.slug} app={app} />
))}
</Stack>
</Stack>

View File

@ -200,7 +200,8 @@ export const MockTemplate: TypesGen.Template = {
export const MockWorkspaceApp: TypesGen.WorkspaceApp = {
id: "test-app",
name: "test-app",
slug: "test-app",
display_name: "Test App",
icon: "",
subdomain: false,
health: "disabled",