diff --git a/agent/apphealth.go b/agent/apphealth.go index 3b5925aea65d6..39de7ce5673c5 100644 --- a/agent/apphealth.go +++ b/agent/apphealth.go @@ -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() } diff --git a/agent/apphealth_test.go b/agent/apphealth_test.go index 64ef0e86fa400..6904a870d2812 100644 --- a/agent/apphealth_test.go +++ b/agent/apphealth_test.go @@ -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 diff --git a/cli/schedule_test.go b/cli/schedule_test.go index 6db064bb4f055..8fc7c9b50b6c8 100644 --- a/cli/schedule_test.go +++ b/cli/schedule_test.go @@ -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 diff --git a/coderd/coderdtest/authorize.go b/coderd/coderdtest/authorize.go index f209e7e3b4999..516b96809a049 100644 --- a/coderd/coderdtest/authorize.go +++ b/coderd/coderdtest/authorize.go @@ -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, diff --git a/coderd/database/databasefake/databasefake.go b/coderd/database/databasefake/databasefake.go index d772e6d81a85e..c6900c316ec1d 100644 --- a/coderd/database/databasefake/databasefake.go +++ b/coderd/database/databasefake/databasefake.go @@ -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, diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 5d521ae725fb8..4953c9885fe65 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -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); diff --git a/coderd/database/migrations/000066_app_slug.down.sql b/coderd/database/migrations/000066_app_slug.down.sql new file mode 100644 index 0000000000000..6e5bfb276bd14 --- /dev/null +++ b/coderd/database/migrations/000066_app_slug.down.sql @@ -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"; diff --git a/coderd/database/migrations/000066_app_slug.up.sql b/coderd/database/migrations/000066_app_slug.up.sql new file mode 100644 index 0000000000000..6f67451f2796e --- /dev/null +++ b/coderd/database/migrations/000066_app_slug.up.sql @@ -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; diff --git a/coderd/database/migrations/000067_app_display_name.down.sql b/coderd/database/migrations/000067_app_display_name.down.sql new file mode 100644 index 0000000000000..1b6fe06a0e25b --- /dev/null +++ b/coderd/database/migrations/000067_app_display_name.down.sql @@ -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; diff --git a/coderd/database/migrations/000067_app_display_name.up.sql b/coderd/database/migrations/000067_app_display_name.up.sql new file mode 100644 index 0000000000000..8d210b35a71bc --- /dev/null +++ b/coderd/database/migrations/000067_app_display_name.up.sql @@ -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; diff --git a/coderd/database/models.go b/coderd/database/models.go index f59e09a1aeba8..ec0d04e06beac 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -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 { diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 69eb998f59d57..25ab45c21df96 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -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) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index b99d651016fb3..58f89cad9fd70 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -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 } diff --git a/coderd/database/queries/groups.sql b/coderd/database/queries/groups.sql index 45c1b8d03c405..618ce785526aa 100644 --- a/coderd/database/queries/groups.sql +++ b/coderd/database/queries/groups.sql @@ -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; diff --git a/coderd/database/queries/workspaceapps.sql b/coderd/database/queries/workspaceapps.sql index 36494a8e9aeb2..03f5b62b15111 100644 --- a/coderd/database/queries/workspaceapps.sql +++ b/coderd/database/queries/workspaceapps.sql @@ -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 diff --git a/coderd/database/unique_constraint.go b/coderd/database/unique_constraint.go index dbaddd46838c8..83c7821207025 100644 --- a/coderd/database/unique_constraint.go +++ b/coderd/database/unique_constraint.go @@ -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); diff --git a/coderd/httpapi/url.go b/coderd/httpapi/url.go index 2de7038c32ecf..ecef3d4f5e960 100644 --- a/coderd/httpapi/url.go +++ b/coderd/httpapi/url.go @@ -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%[1]s)--(?P%[1]s)--(?P%[1]s)--(?P%[1]s)$`, + // {PORT/APP_SLUG}--{AGENT_NAME}--{WORKSPACE_NAME}--{USERNAME} + `^(?P%[1]s)--(?P%[1]s)--(?P%[1]s)--(?P%[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 diff --git a/coderd/httpapi/url_test.go b/coderd/httpapi/url_test.go index 2843c5efdd15f..84cfcac7d39ca 100644 --- a/coderd/httpapi/url_test.go +++ b/coderd/httpapi/url_test.go @@ -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", diff --git a/coderd/provisionerdaemons.go b/coderd/provisionerdaemons.go index adb5cb2edfc35..aa6c5c18cb89e 100644 --- a/coderd/provisionerdaemons.go +++ b/coderd/provisionerdaemons.go @@ -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 != "", diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index edae77f37777e..4eb7e95272b68 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -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 } } diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go index b5b6ab1f0c7cd..84ab9933234a5 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -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, diff --git a/coderd/workspaceapps.go b/coderd/workspaceapps.go index 1da951c63d46c..95734387abeb5 100644 --- a/coderd/workspaceapps.go +++ b/coderd/workspaceapps.go @@ -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(), }) diff --git a/coderd/workspaceapps_test.go b/coderd/workspaceapps_test.go index fde0f54e07d05..c37f8f5c5f574 100644 --- a/coderd/workspaceapps_test.go +++ b/coderd/workspaceapps_test.go @@ -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") diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index fce2ffdf9c221..06d98ce87352d 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -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) diff --git a/codersdk/workspaceapps.go b/codersdk/workspaceapps.go index 6faf4bd3c3ba2..3cee425ebfe03 100644 --- a/codersdk/workspaceapps.go +++ b/codersdk/workspaceapps.go @@ -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"` diff --git a/docs/ides/web-ides.md b/docs/ides/web-ides.md index 210aed4777031..e70d756f3f6d2 100644 --- a/docs/ides/web-ides.md +++ b/docs/ides/web-ides.md @@ -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}" diff --git a/dogfood/main.tf b/dogfood/main.tf index 7e473a5a6491c..f39ccc491e8e0 100644 --- a/dogfood/main.tf +++ b/dogfood/main.tf @@ -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" diff --git a/enterprise/coderd/workspaceagents_test.go b/enterprise/coderd/workspaceagents_test.go index e6979bcc525a2..5f84b6f60a10f 100644 --- a/enterprise/coderd/workspaceagents_test.go +++ b/enterprise/coderd/workspaceagents_test.go @@ -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), }, diff --git a/examples/templates/aws-ecs-container/main.tf b/examples/templates/aws-ecs-container/main.tf index 394bbed6dcec6..4eaa8edbc3730 100644 --- a/examples/templates/aws-ecs-container/main.tf +++ b/examples/templates/aws-ecs-container/main.tf @@ -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" diff --git a/examples/templates/aws-linux/main.tf b/examples/templates/aws-linux/main.tf index 89b69be2472c1..7a328b939b468 100644 --- a/examples/templates/aws-linux/main.tf +++ b/examples/templates/aws-linux/main.tf @@ -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" diff --git a/examples/templates/aws-windows/main.tf b/examples/templates/aws-windows/main.tf index a01ee9a7ebad4..fb5217fc90856 100644 --- a/examples/templates/aws-windows/main.tf +++ b/examples/templates/aws-windows/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.5.3" + version = "0.6.0" } } } diff --git a/examples/templates/azure-linux/main.tf b/examples/templates/azure-linux/main.tf index aa6698e6bcfc0..e8294beb2e5a2 100644 --- a/examples/templates/azure-linux/main.tf +++ b/examples/templates/azure-linux/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.5.3" + version = "0.6.0" } azurerm = { source = "hashicorp/azurerm" diff --git a/examples/templates/do-linux/main.tf b/examples/templates/do-linux/main.tf index 9f54de8957981..fbb5a6d227e4d 100644 --- a/examples/templates/do-linux/main.tf +++ b/examples/templates/do-linux/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.5.3" + version = "0.6.0" } digitalocean = { source = "digitalocean/digitalocean" diff --git a/examples/templates/docker-code-server/main.tf b/examples/templates/docker-code-server/main.tf index 2e4f4f5b488bc..d9e713dc26faf 100644 --- a/examples/templates/docker-code-server/main.tf +++ b/examples/templates/docker-code-server/main.tf @@ -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" diff --git a/examples/templates/docker-image-builds/main.tf b/examples/templates/docker-image-builds/main.tf index f5290efdfe440..dbce5de1aaedc 100644 --- a/examples/templates/docker-image-builds/main.tf +++ b/examples/templates/docker-image-builds/main.tf @@ -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" diff --git a/examples/templates/docker-with-dotfiles/main.tf b/examples/templates/docker-with-dotfiles/main.tf index 750dbed2e0e46..ae9475ededee2 100644 --- a/examples/templates/docker-with-dotfiles/main.tf +++ b/examples/templates/docker-with-dotfiles/main.tf @@ -9,7 +9,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.5.3" + version = "0.6.0" } docker = { source = "kreuzwerker/docker" diff --git a/examples/templates/docker/main.tf b/examples/templates/docker/main.tf index 677dace7f43f6..2ca356c60cb73 100644 --- a/examples/templates/docker/main.tf +++ b/examples/templates/docker/main.tf @@ -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" diff --git a/examples/templates/gcp-linux/main.tf b/examples/templates/gcp-linux/main.tf index 8e184b17c3186..59a51c2aebf04 100644 --- a/examples/templates/gcp-linux/main.tf +++ b/examples/templates/gcp-linux/main.tf @@ -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" diff --git a/examples/templates/gcp-vm-container/main.tf b/examples/templates/gcp-vm-container/main.tf index 753a2535fe0a9..c5b505f1eac62 100644 --- a/examples/templates/gcp-vm-container/main.tf +++ b/examples/templates/gcp-vm-container/main.tf @@ -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" diff --git a/examples/templates/gcp-windows/main.tf b/examples/templates/gcp-windows/main.tf index 5f9a65ac1aef5..dac920654a873 100644 --- a/examples/templates/gcp-windows/main.tf +++ b/examples/templates/gcp-windows/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.5.3" + version = "0.6.0" } google = { source = "hashicorp/google" diff --git a/examples/templates/kubernetes/main.tf b/examples/templates/kubernetes/main.tf index 4cf2874922cc2..9322e0c7c02f7 100644 --- a/examples/templates/kubernetes/main.tf +++ b/examples/templates/kubernetes/main.tf @@ -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" diff --git a/provisioner/appslug.go b/provisioner/appslug.go new file mode 100644 index 0000000000000..cf3f37942dad0 --- /dev/null +++ b/provisioner/appslug.go @@ -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])*$`) +) diff --git a/provisioner/appslug_test.go b/provisioner/appslug_test.go new file mode 100644 index 0000000000000..2fbd3f08ea1cd --- /dev/null +++ b/provisioner/appslug_test.go @@ -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) + } + }) +} diff --git a/provisioner/terraform/resources.go b/provisioner/terraform/resources.go index 604c99c7fbbb6..db6a23d06ede6 100644 --- a/provisioner/terraform/resources.go +++ b/provisioner/terraform/resources.go @@ -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, diff --git a/provisioner/terraform/resources_test.go b/provisioner/terraform/resources_test.go index 4034ee395eaed..483fe8e4b13f5 100644 --- a/provisioner/terraform/resources_test.go +++ b/provisioner/terraform/resources_test.go @@ -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) + var expectedNoMetadataMap []map[string]interface{} + err = json.Unmarshal(data, &expectedNoMetadataMap) + require.NoError(t, err) + + data, err = json.Marshal(resources) require.NoError(t, err) - resourcesGot, err := json.Marshal(resources) + var resourcesMap []map[string]interface{} + err = json.Unmarshal(data, &resourcesMap) require.NoError(t, err) - require.Equal(t, string(resourcesWant), string(resourcesGot)) + + 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 { diff --git a/provisioner/terraform/testdata/calling-module/calling-module.tf b/provisioner/terraform/testdata/calling-module/calling-module.tf index 6bde4e1fd0596..2d8d38bf2a5c4 100644 --- a/provisioner/terraform/testdata/calling-module/calling-module.tf +++ b/provisioner/terraform/testdata/calling-module/calling-module.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.5.3" + version = "0.6.0" } } } diff --git a/provisioner/terraform/testdata/calling-module/calling-module.tfplan.json b/provisioner/terraform/testdata/calling-module/calling-module.tfplan.json index 3d491cb410264..1a79f2488c167 100644 --- a/provisioner/terraform/testdata/calling-module/calling-module.tfplan.json +++ b/provisioner/terraform/testdata/calling-module/calling-module.tfplan.json @@ -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" + ] } ] } diff --git a/provisioner/terraform/testdata/calling-module/calling-module.tfstate.json b/provisioner/terraform/testdata/calling-module/calling-module.tfstate.json index b9a30bec50faf..adf9154876b17 100644 --- a/provisioner/terraform/testdata/calling-module/calling-module.tfstate.json +++ b/provisioner/terraform/testdata/calling-module/calling-module.tfstate.json @@ -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": {}, diff --git a/provisioner/terraform/testdata/chaining-resources/chaining-resources.tf b/provisioner/terraform/testdata/chaining-resources/chaining-resources.tf index ce8eea33b1795..8c4dfc2cb76ee 100644 --- a/provisioner/terraform/testdata/chaining-resources/chaining-resources.tf +++ b/provisioner/terraform/testdata/chaining-resources/chaining-resources.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.5.3" + version = "0.6.0" } } } diff --git a/provisioner/terraform/testdata/chaining-resources/chaining-resources.tfplan.json b/provisioner/terraform/testdata/chaining-resources/chaining-resources.tfplan.json index 44698e6885524..efc8bad808aa0 100644 --- a/provisioner/terraform/testdata/chaining-resources/chaining-resources.tfplan.json +++ b/provisioner/terraform/testdata/chaining-resources/chaining-resources.tfplan.json @@ -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", diff --git a/provisioner/terraform/testdata/chaining-resources/chaining-resources.tfstate.json b/provisioner/terraform/testdata/chaining-resources/chaining-resources.tfstate.json index 91ac75eb21f25..2db8be2d6c4fc 100644 --- a/provisioner/terraform/testdata/chaining-resources/chaining-resources.tfstate.json +++ b/provisioner/terraform/testdata/chaining-resources/chaining-resources.tfstate.json @@ -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": {}, diff --git a/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tf b/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tf index 2ec5614cd13e4..5b8d6df78bdf4 100644 --- a/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tf +++ b/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.5.3" + version = "0.6.0" } } } diff --git a/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tfplan.json b/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tfplan.json index 0ec3af57e4da7..9aaacf0134014 100644 --- a/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tfplan.json +++ b/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tfplan.json @@ -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", diff --git a/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tfstate.json b/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tfstate.json index 4e41f6cf6a797..6e8f435ec9718 100644 --- a/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tfstate.json +++ b/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tfstate.json @@ -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": {}, diff --git a/provisioner/terraform/testdata/instance-id/instance-id.tf b/provisioner/terraform/testdata/instance-id/instance-id.tf index 767ed45a63390..19630450f6a9e 100644 --- a/provisioner/terraform/testdata/instance-id/instance-id.tf +++ b/provisioner/terraform/testdata/instance-id/instance-id.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.5.3" + version = "0.6.0" } } } diff --git a/provisioner/terraform/testdata/instance-id/instance-id.tfplan.json b/provisioner/terraform/testdata/instance-id/instance-id.tfplan.json index 595ad74f7d078..3bf0652c934ef 100644 --- a/provisioner/terraform/testdata/instance-id/instance-id.tfplan.json +++ b/provisioner/terraform/testdata/instance-id/instance-id.tfplan.json @@ -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", diff --git a/provisioner/terraform/testdata/instance-id/instance-id.tfstate.json b/provisioner/terraform/testdata/instance-id/instance-id.tfstate.json index 13b36ffa1f936..091a485993c86 100644 --- a/provisioner/terraform/testdata/instance-id/instance-id.tfstate.json +++ b/provisioner/terraform/testdata/instance-id/instance-id.tfstate.json @@ -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": {}, diff --git a/provisioner/terraform/testdata/multiple-agents/multiple-agents.tf b/provisioner/terraform/testdata/multiple-agents/multiple-agents.tf index cae9aac261019..19813c586faea 100644 --- a/provisioner/terraform/testdata/multiple-agents/multiple-agents.tf +++ b/provisioner/terraform/testdata/multiple-agents/multiple-agents.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.5.3" + version = "0.6.0" } } } diff --git a/provisioner/terraform/testdata/multiple-agents/multiple-agents.tfplan.json b/provisioner/terraform/testdata/multiple-agents/multiple-agents.tfplan.json index df8e4d92adfba..46d1ea7714ff6 100644 --- a/provisioner/terraform/testdata/multiple-agents/multiple-agents.tfplan.json +++ b/provisioner/terraform/testdata/multiple-agents/multiple-agents.tfplan.json @@ -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", diff --git a/provisioner/terraform/testdata/multiple-agents/multiple-agents.tfstate.json b/provisioner/terraform/testdata/multiple-agents/multiple-agents.tfstate.json index 1bf2f1e189802..1143c69fe2c0d 100644 --- a/provisioner/terraform/testdata/multiple-agents/multiple-agents.tfstate.json +++ b/provisioner/terraform/testdata/multiple-agents/multiple-agents.tfstate.json @@ -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": {}, diff --git a/provisioner/terraform/testdata/multiple-apps/multiple-apps.tf b/provisioner/terraform/testdata/multiple-apps/multiple-apps.tf index 446183a9dbb06..3a6637ea6e922 100644 --- a/provisioner/terraform/testdata/multiple-apps/multiple-apps.tf +++ b/provisioner/terraform/testdata/multiple-apps/multiple-apps.tf @@ -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 } diff --git a/provisioner/terraform/testdata/multiple-apps/multiple-apps.tfplan.json b/provisioner/terraform/testdata/multiple-apps/multiple-apps.tfplan.json index 93f60329583f7..5c1e167f14813 100644 --- a/provisioner/terraform/testdata/multiple-apps/multiple-apps.tfplan.json +++ b/provisioner/terraform/testdata/multiple-apps/multiple-apps.tfplan.json @@ -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 } diff --git a/provisioner/terraform/testdata/multiple-apps/multiple-apps.tfstate.json b/provisioner/terraform/testdata/multiple-apps/multiple-apps.tfstate.json index 14b90e4cc2395..d419e904b366b 100644 --- a/provisioner/terraform/testdata/multiple-apps/multiple-apps.tfstate.json +++ b/provisioner/terraform/testdata/multiple-apps/multiple-apps.tfstate.json @@ -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": {}, diff --git a/provisioner/terraform/testdata/resource-metadata/resource-metadata.tf b/provisioner/terraform/testdata/resource-metadata/resource-metadata.tf index ab94dcfbf7550..7dc8b361f7f4c 100644 --- a/provisioner/terraform/testdata/resource-metadata/resource-metadata.tf +++ b/provisioner/terraform/testdata/resource-metadata/resource-metadata.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.5.3" + version = "0.6.0" } } } diff --git a/provisioner/terraform/testdata/resource-metadata/resource-metadata.tfplan.json b/provisioner/terraform/testdata/resource-metadata/resource-metadata.tfplan.json index 6f45e70fd6e69..006613db073dd 100644 --- a/provisioner/terraform/testdata/resource-metadata/resource-metadata.tfplan.json +++ b/provisioner/terraform/testdata/resource-metadata/resource-metadata.tfplan.json @@ -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", diff --git a/provisioner/terraform/testdata/resource-metadata/resource-metadata.tfstate.json b/provisioner/terraform/testdata/resource-metadata/resource-metadata.tfstate.json index b759e7590dec6..65a381788aef5 100644 --- a/provisioner/terraform/testdata/resource-metadata/resource-metadata.tfstate.json +++ b/provisioner/terraform/testdata/resource-metadata/resource-metadata.tfstate.json @@ -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": {} diff --git a/provisionersdk/proto/provisioner.pb.go b/provisionersdk/proto/provisioner.pb.go index 0e70c8f919185..b26ab11d1171f 100644 --- a/provisionersdk/proto/provisioner.pb.go +++ b/provisionersdk/proto/provisioner.pb.go @@ -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, 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, 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, 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 ( diff --git a/provisionersdk/proto/provisioner.proto b/provisionersdk/proto/provisioner.proto index bc6ab711a4add..aa0c14a38a80f 100644 --- a/provisionersdk/proto/provisioner.proto +++ b/provisionersdk/proto/provisioner.proto @@ -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. diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 905e1b6de5a30..cab90e5f8ccac 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -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 diff --git a/site/src/components/AppLink/AppLink.tsx b/site/src/components/AppLink/AppLink.tsx index de8c4d485d939..9a0dcfa3dbd89 100644 --- a/site/src/components/AppLink/AppLink.tsx +++ b/site/src/components/AppLink/AppLink.tsx @@ -31,18 +31,27 @@ export const AppLink: FC = ({ 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 = ({ className={styles.button} disabled={!canClick} > - {app.name} + {appDisplayName} ) @@ -92,7 +101,7 @@ export const AppLink: FC = ({ event.preventDefault() window.open( href, - Language.appTitle(app.name, generateRandomString(12)), + Language.appTitle(appDisplayName, generateRandomString(12)), "width=900,height=600", ) } diff --git a/site/src/components/AppLink/AppPreviewLink.tsx b/site/src/components/AppLink/AppPreviewLink.tsx index 4d434f8963a9e..4af53c2383c2b 100644 --- a/site/src/components/AppLink/AppPreviewLink.tsx +++ b/site/src/components/AppLink/AppPreviewLink.tsx @@ -20,7 +20,7 @@ export const AppPreviewLink: FC = ({ app }) => { spacing={1} > - {app.name} + {app.display_name} ) diff --git a/site/src/components/AppLink/BaseIcon.tsx b/site/src/components/AppLink/BaseIcon.tsx index 9343817e9c536..0df2880d29257 100644 --- a/site/src/components/AppLink/BaseIcon.tsx +++ b/site/src/components/AppLink/BaseIcon.tsx @@ -4,7 +4,7 @@ import ComputerIcon from "@material-ui/icons/Computer" export const BaseIcon: FC<{ app: WorkspaceApp }> = ({ app }) => { return app.icon ? ( - {`${app.name} + {`${app.display_name} ) : ( ) diff --git a/site/src/components/Resources/AgentRow.tsx b/site/src/components/Resources/AgentRow.tsx index 40acef8e737e0..57fc5b27a90c6 100644 --- a/site/src/components/Resources/AgentRow.tsx +++ b/site/src/components/Resources/AgentRow.tsx @@ -74,7 +74,7 @@ export const AgentRow: FC = ({ <> {agent.apps.map((app) => ( = ({ agent }) => { wrap="wrap" > {agent.apps.map((app) => ( - + ))} diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index d4670f66baead..93d380e255e37 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -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",