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", pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy