From 688945019ebfbc8da5656e08c90190ca2b385f0a Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Sun, 16 Oct 2022 19:26:17 +0000 Subject: [PATCH 01/14] feat: add slug property to app, use in URLs --- coderd/coderdtest/authorize.go | 3 +- coderd/database/databasefake/databasefake.go | 5 +- coderd/database/dump.sql | 3 +- .../migrations/000061_app_slug.down.sql | 2 + .../migrations/000061_app_slug.up.sql | 9 + coderd/database/models.go | 1 + coderd/database/querier.go | 2 +- coderd/database/queries.sql.go | 28 +- coderd/database/queries/workspaceapps.sql | 13 +- coderd/httpapi/url.go | 22 +- coderd/httpapi/url_test.go | 12 +- coderd/provisionerdaemons.go | 13 + coderd/workspaceagents.go | 1 + coderd/workspaceagents_test.go | 3 + coderd/workspaceapps.go | 47 ++- coderd/workspaceapps_test.go | 6 +- coderd/workspaces_test.go | 2 + codersdk/workspaceapps.go | 4 +- enterprise/coderd/workspaceagents_test.go | 3 + provisioner/appname.go | 14 + provisioner/appname_test.go | 64 ++++ provisioner/terraform/resources.go | 8 + provisioner/terraform/resources_test.go | 3 + provisionersdk/proto/provisioner.pb.go | 311 +++++++++--------- provisionersdk/proto/provisioner.proto | 17 +- site/src/api/typesGenerated.ts | 1 + .../components/AppLink/AppLink.stories.tsx | 15 +- site/src/components/AppLink/AppLink.tsx | 12 +- site/src/components/Resources/Resources.tsx | 1 + site/src/testHelpers/entities.ts | 3 +- 30 files changed, 406 insertions(+), 222 deletions(-) create mode 100644 coderd/database/migrations/000061_app_slug.down.sql create mode 100644 coderd/database/migrations/000061_app_slug.up.sql create mode 100644 provisioner/appname.go create mode 100644 provisioner/appname_test.go diff --git a/coderd/coderdtest/authorize.go b/coderd/coderdtest/authorize.go index a5183f2b6e450..548a4fb17c5b0 100644 --- a/coderd/coderdtest/authorize.go +++ b/coderd/coderdtest/authorize.go @@ -330,6 +330,7 @@ func NewAuthTester(ctx context.Context, t *testing.T, client *codersdk.Client, a Id: "something", Auth: &proto.Agent_Token{}, Apps: []*proto.App{{ + Slug: "testapp", Name: "testapp", Url: "http://localhost:3000", }}, @@ -371,7 +372,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 54314d221da1b..091a74b1fe6fb 100644 --- a/coderd/database/databasefake/databasefake.go +++ b/coderd/database/databasefake/databasefake.go @@ -1705,7 +1705,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() @@ -1713,7 +1713,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 @@ -2362,6 +2362,7 @@ func (q *fakeQuerier) InsertWorkspaceApp(_ context.Context, arg database.InsertW ID: arg.ID, AgentID: arg.AgentID, CreatedAt: arg.CreatedAt, + Slug: arg.Slug, Name: arg.Name, Icon: arg.Icon, Command: arg.Command, diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 9e2c68dbf6ef3..bb63b3bd5a09d 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -378,7 +378,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 ( diff --git a/coderd/database/migrations/000061_app_slug.down.sql b/coderd/database/migrations/000061_app_slug.down.sql new file mode 100644 index 0000000000000..2d409263cc5c7 --- /dev/null +++ b/coderd/database/migrations/000061_app_slug.down.sql @@ -0,0 +1,2 @@ +-- drop "slug" column from "workspace_apps" table +ALTER TABLE "workspace_apps" DROP COLUMN "slug"; diff --git a/coderd/database/migrations/000061_app_slug.up.sql b/coderd/database/migrations/000061_app_slug.up.sql new file mode 100644 index 0000000000000..5962bc2ed1d6e --- /dev/null +++ b/coderd/database/migrations/000061_app_slug.up.sql @@ -0,0 +1,9 @@ +-- 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; diff --git a/coderd/database/models.go b/coderd/database/models.go index e30615244e299..5c7fe8583a541 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -647,6 +647,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 d3680675d51d5..521894626a3f3 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -95,7 +95,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 e34c0fa841d30..05fc0537110ee 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -4369,17 +4369,17 @@ 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, 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, @@ -4395,12 +4395,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, 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) { @@ -4426,6 +4427,7 @@ func (q *sqlQuerier) GetWorkspaceAppsByAgentID(ctx context.Context, agentID uuid &i.Health, &i.Subdomain, &i.SharingLevel, + &i.Slug, ); err != nil { return nil, err } @@ -4441,7 +4443,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, 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) { @@ -4467,6 +4469,7 @@ func (q *sqlQuerier) GetWorkspaceAppsByAgentIDs(ctx context.Context, ids []uuid. &i.Health, &i.Subdomain, &i.SharingLevel, + &i.Slug, ); err != nil { return nil, err } @@ -4482,7 +4485,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, 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) { @@ -4508,6 +4511,7 @@ func (q *sqlQuerier) GetWorkspaceAppsCreatedAfter(ctx context.Context, createdAt &i.Health, &i.Subdomain, &i.SharingLevel, + &i.Slug, ); err != nil { return nil, err } @@ -4528,6 +4532,7 @@ INSERT INTO id, created_at, agent_id, + slug, name, icon, command, @@ -4540,13 +4545,14 @@ 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, 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"` + Slug string `db:"slug" json:"slug"` Name string `db:"name" json:"name"` Icon string `db:"icon" json:"icon"` Command sql.NullString `db:"command" json:"command"` @@ -4564,6 +4570,7 @@ func (q *sqlQuerier) InsertWorkspaceApp(ctx context.Context, arg InsertWorkspace arg.ID, arg.CreatedAt, arg.AgentID, + arg.Slug, arg.Name, arg.Icon, arg.Command, @@ -4590,6 +4597,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/workspaceapps.sql b/coderd/database/queries/workspaceapps.sql index 36494a8e9aeb2..507a664c46a85 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,6 +16,7 @@ INSERT INTO id, created_at, agent_id, + slug, name, icon, command, @@ -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/httpapi/url.go b/coderd/httpapi/url.go index 2de7038c32ecf..4083ccd4761f2 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 @@ -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..4e4b42acd7462 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", @@ -133,7 +133,7 @@ func TestParseSubdomainAppURL(t *testing.T) { Name: "HyphenatedNames", Subdomain: "app-name--agent-name--workspace-name--user-name", Expected: httpapi.ApplicationURL{ - AppName: "app-name", + AppSlug: "app-name", Port: 0, AgentName: "agent-name", WorkspaceName: "workspace-name", diff --git a/coderd/provisionerdaemons.go b/coderd/provisionerdaemons.go index adb5cb2edfc35..468ac56ef9c0a 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" @@ -806,6 +807,17 @@ 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 == "" { + slug = app.Name + } + if slug == "" { + return xerrors.Errorf("app must have a slug or name set") + } + if !provisioner.ValidAppNameRegex.MatchString(slug) { + return xerrors.Errorf("app slug %q does not match regex %q", slug, provisioner.ValidAppNameRegex.String()) + } + health := database.WorkspaceAppHealthDisabled if app.Healthcheck == nil { app.Healthcheck = &sdkproto.Healthcheck{} @@ -826,6 +838,7 @@ func insertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid. ID: uuid.New(), CreatedAt: database.Now(), AgentID: dbAgent.ID, + Slug: slug, Name: app.Name, Icon: app.Icon, Command: sql.NullString{ diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index 295beff0d2b7e..dbc34d9cbc48c 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -595,6 +595,7 @@ func convertApps(dbApps []database.WorkspaceApp) []codersdk.WorkspaceApp { for _, dbApp := range dbApps { apps = append(apps, codersdk.WorkspaceApp{ ID: dbApp.ID, + Slug: dbApp.Slug, Name: dbApp.Name, Command: dbApp.Command.String, Icon: dbApp.Icon, diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go index 6bd569dde9f71..52fcabf53eb0f 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -553,6 +553,7 @@ func TestWorkspaceAgentListeningPorts(t *testing.T) { // should not exist in the response. _, appLPort := generateUnfilteredPort(t) app := &proto.App{ + Slug: "test-app", Name: "test-app", Url: fmt.Sprintf("http://localhost:%d", appLPort), } @@ -621,12 +622,14 @@ func TestWorkspaceAgentAppHealth(t *testing.T) { authToken := uuid.NewString() apps := []*proto.App{ { + Slug: "code-server", Name: "code-server", Command: "some-command", Url: "http://localhost:3000", Icon: "/code.svg", }, { + Slug: "code-server-2", Name: "code-server-2", Command: "some-command", Url: "http://localhost:3000", diff --git a/coderd/workspaceapps.go b/coderd/workspaceapps.go index 5a7c192602fb5..cd8a8f12affc4 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, @@ -580,21 +596,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 bbf80746d7987..db810274b247d 100644 --- a/coderd/workspaceapps_test.go +++ b/coderd/workspaceapps_test.go @@ -149,22 +149,26 @@ func createWorkspaceWithApps(t *testing.T, client *codersdk.Client, orgID uuid.U }, Apps: []*proto.App{ { + Slug: proxyTestAppNameFake, Name: proxyTestAppNameFake, SharingLevel: proto.AppSharingLevel_OWNER, // Hopefully this IP and port doesn't exist. Url: "http://127.1.0.1:65535", }, { + Slug: proxyTestAppNameOwner, Name: proxyTestAppNameOwner, SharingLevel: proto.AppSharingLevel_OWNER, Url: appURL, }, { + Slug: proxyTestAppNameAuthenticated, Name: proxyTestAppNameAuthenticated, SharingLevel: proto.AppSharingLevel_AUTHENTICATED, Url: appURL, }, { + Slug: proxyTestAppNamePublic, Name: proxyTestAppNamePublic, SharingLevel: proto.AppSharingLevel_PUBLIC, Url: appURL, @@ -597,7 +601,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, diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index 1026dcdd6dd76..e68937149b40e 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -1408,12 +1408,14 @@ func TestWorkspaceResource(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) apps := []*proto.App{ { + Slug: "code-server", Name: "code-server", Command: "some-command", Url: "http://localhost:3000", Icon: "/code.svg", }, { + Slug: "code-server-2", Name: "code-server-2", Command: "some-command", Url: "http://localhost:3000", diff --git a/codersdk/workspaceapps.go b/codersdk/workspaceapps.go index 6faf4bd3c3ba2..6f1847a96e69f 100644 --- a/codersdk/workspaceapps.go +++ b/codersdk/workspaceapps.go @@ -23,7 +23,9 @@ const ( type WorkspaceApp struct { ID uuid.UUID `json:"id"` - // Name is a unique identifier attached to an agent. + // Slug is a unique identifier within the agent.. + Slug string `json:"slug"` + // Name is a friendly name for the app. Name string `json:"name"` Command string `json:"command,omitempty"` // Icon is a relative path or external URL that specifies diff --git a/enterprise/coderd/workspaceagents_test.go b/enterprise/coderd/workspaceagents_test.go index 9fe3cfeaa3064..fa5fdbab98abf 100644 --- a/enterprise/coderd/workspaceagents_test.go +++ b/enterprise/coderd/workspaceagents_test.go @@ -83,16 +83,19 @@ func setupWorkspaceAgent(t *testing.T, client *codersdk.Client, user codersdk.Cr }, Apps: []*proto.App{ { + Slug: testAppNameOwner, Name: testAppNameOwner, SharingLevel: proto.AppSharingLevel_OWNER, Url: fmt.Sprintf("http://localhost:%d", appPort), }, { + Slug: testAppNameAuthenticated, Name: testAppNameAuthenticated, SharingLevel: proto.AppSharingLevel_AUTHENTICATED, Url: fmt.Sprintf("http://localhost:%d", appPort), }, { + Slug: testAppNamePublic, Name: testAppNamePublic, SharingLevel: proto.AppSharingLevel_PUBLIC, Url: fmt.Sprintf("http://localhost:%d", appPort), diff --git a/provisioner/appname.go b/provisioner/appname.go new file mode 100644 index 0000000000000..96f3b615bfc59 --- /dev/null +++ b/provisioner/appname.go @@ -0,0 +1,14 @@ +package provisioner + +import "regexp" + +var ( + // ValidAppNameRegex is the regex used to validate the name 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 looks complicated but it's written this way to avoid a + // negative lookahead (which is not supported by Go). There are test cases + // for this regex in appname_test.go. + ValidAppNameRegex = regexp.MustCompile(`^[a-z0-9]([a-z0-9-][a-z0-9]|[a-z0-9]([a-z0-9-]?[a-z0-9])?)*$`) +) diff --git a/provisioner/appname_test.go b/provisioner/appname_test.go new file mode 100644 index 0000000000000..91f2668d19ac2 --- /dev/null +++ b/provisioner/appname_test.go @@ -0,0 +1,64 @@ +package provisioner_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/coder/coder/provisioner" +) + +func TestValidAppNameRegex(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.ValidAppNameRegex.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.ValidAppNameRegex.MatchString(s), s) + } + }) +} diff --git a/provisioner/terraform/resources.go b/provisioner/terraform/resources.go index 604c99c7fbbb6..faa44a77fdf12 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" ) @@ -218,6 +219,12 @@ func ConvertResources(module *tfjson.StateModule, rawGraph string) ([]*proto.Res if resource.Type != "coder_app" { continue } + + slug := resource.Name + if !provisioner.ValidAppNameRegex.MatchString(slug) { + return nil, xerrors.Errorf("invalid app name, must be a valid hostname (%q, cannot contain two consecutive hyphens or start/end with a hyphen): %q", provisioner.ValidAppNameRegex.String(), slug) + } + var attrs agentAppAttributes err = mapstructure.Decode(resource.AttributeValues, &attrs) if err != nil { @@ -253,6 +260,7 @@ func ConvertResources(module *tfjson.StateModule, rawGraph string) ([]*proto.Res continue } agent.Apps = append(agent.Apps, &proto.App{ + Slug: slug, Name: attrs.Name, Command: attrs.Command, Url: attrs.URL, diff --git a/provisioner/terraform/resources_test.go b/provisioner/terraform/resources_test.go index 4034ee395eaed..6b0175218755d 100644 --- a/provisioner/terraform/resources_test.go +++ b/provisioner/terraform/resources_test.go @@ -110,11 +110,13 @@ func TestConvertResources(t *testing.T) { Architecture: "amd64", Apps: []*proto.App{ { + Slug: "app1", Name: "app1", // Subdomain defaults to false if unspecified. Subdomain: false, }, { + Slug: "app2", Name: "app2", Subdomain: true, Healthcheck: &proto.Healthcheck{ @@ -124,6 +126,7 @@ func TestConvertResources(t *testing.T) { }, }, { + Slug: "app3", Name: "app3", Subdomain: false, }, diff --git a/provisionersdk/proto/provisioner.pb.go b/provisionersdk/proto/provisioner.pb.go index 0e70c8f919185..613a607a17e16 100644 --- a/provisionersdk/proto/provisioner.pb.go +++ b/provisionersdk/proto/provisioner.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.26.0 -// protoc v3.21.5 +// protoc v3.6.1 // source: provisionersdk/proto/provisioner.proto package proto @@ -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"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"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,6 +943,13 @@ func (*App) Descriptor() ([]byte, []int) { return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{8} } +func (x *App) GetSlug() string { + if x != nil { + return x.Slug + } + return "" +} + func (x *App) GetName() string { if x != nil { return x.Name @@ -2009,148 +2019,149 @@ 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, - 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, 0x3d, 0x0a, 0x08, 0x63, - 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x02, 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, 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, + 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x06, 0x0a, 0x04, 0x61, 0x75, 0x74, 0x68, 0x22, 0x8a, + 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, 0x12, 0x0a, 0x04, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 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, + 0x3d, 0x0a, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x02, 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, 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, } var ( diff --git a/provisionersdk/proto/provisioner.proto b/provisionersdk/proto/provisioner.proto index bc6ab711a4add..878f2bc6e7c5e 100644 --- a/provisionersdk/proto/provisioner.proto +++ b/provisionersdk/proto/provisioner.proto @@ -95,13 +95,16 @@ enum AppSharingLevel { // 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 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. diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 519b828d30980..800be58e3fd71 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -786,6 +786,7 @@ export interface WorkspaceAgentResourceMetadata { // From codersdk/workspaceapps.go export interface WorkspaceApp { readonly id: string + readonly slug: string readonly name: string readonly command?: string readonly icon?: string diff --git a/site/src/components/AppLink/AppLink.stories.tsx b/site/src/components/AppLink/AppLink.stories.tsx index 845d1e350abc9..454f97b5bc6c3 100644 --- a/site/src/components/AppLink/AppLink.stories.tsx +++ b/site/src/components/AppLink/AppLink.stories.tsx @@ -13,7 +13,8 @@ export const WithIcon = Template.bind({}) WithIcon.args = { username: "developer", workspaceName: MockWorkspace.name, - appName: "code-server", + appSlug: "code-server", + appName: "Code Server", appIcon: "/icon/code.svg", appSharingLevel: "owner", health: "healthy", @@ -23,7 +24,8 @@ export const WithoutIcon = Template.bind({}) WithoutIcon.args = { username: "developer", workspaceName: MockWorkspace.name, - appName: "code-server", + appSlug: "code-server", + appName: "Code Server", appSharingLevel: "owner", health: "healthy", } @@ -32,7 +34,8 @@ export const HealthDisabled = Template.bind({}) HealthDisabled.args = { username: "developer", workspaceName: MockWorkspace.name, - appName: "code-server", + appSlug: "code-server", + appName: "Code Server", appSharingLevel: "owner", health: "disabled", } @@ -41,7 +44,8 @@ export const HealthInitializing = Template.bind({}) HealthInitializing.args = { username: "developer", workspaceName: MockWorkspace.name, - appName: "code-server", + appSlug: "code-server", + appName: "Code Server", health: "initializing", } @@ -49,6 +53,7 @@ export const HealthUnhealthy = Template.bind({}) HealthUnhealthy.args = { username: "developer", workspaceName: MockWorkspace.name, - appName: "code-server", + appSlug: "code-server", + appName: "Code Server", health: "unhealthy", } diff --git a/site/src/components/AppLink/AppLink.tsx b/site/src/components/AppLink/AppLink.tsx index 50477c08362ee..378fad3d110a8 100644 --- a/site/src/components/AppLink/AppLink.tsx +++ b/site/src/components/AppLink/AppLink.tsx @@ -22,6 +22,7 @@ export interface AppLinkProps { username: TypesGen.User["username"] workspaceName: TypesGen.Workspace["name"] agentName: TypesGen.WorkspaceAgent["name"] + appSlug: TypesGen.WorkspaceApp["slug"] appName: TypesGen.WorkspaceApp["name"] appIcon?: TypesGen.WorkspaceApp["icon"] appCommand?: TypesGen.WorkspaceApp["command"] @@ -35,6 +36,7 @@ export const AppLink: FC> = ({ username, workspaceName, agentName, + appSlug, appName, appIcon, appCommand, @@ -43,11 +45,17 @@ export const AppLink: FC> = ({ health, }) => { const styles = useStyles() + if (appSlug === "") { + appSlug = appName + } + if (appName === "") { + appName = appSlug + } // The backend redirects if the trailing slash isn't included, so we add it // here to avoid extra roundtrips. let href = `/@${username}/${workspaceName}.${agentName}/apps/${encodeURIComponent( - appName, + appSlug, )}/` if (appCommand) { href = `/@${username}/${workspaceName}.${agentName}/terminal?command=${encodeURIComponent( @@ -55,7 +63,7 @@ export const AppLink: FC> = ({ )}` } if (appsHost && appSubdomain) { - const subdomain = `${appName}--${agentName}--${workspaceName}--${username}` + const subdomain = `${appSlug}--${agentName}--${workspaceName}--${username}` href = `${window.location.protocol}//${appsHost}/`.replace("*", subdomain) } diff --git a/site/src/components/Resources/Resources.tsx b/site/src/components/Resources/Resources.tsx index 9d2e63ac9e39c..48e7948986aea 100644 --- a/site/src/components/Resources/Resources.tsx +++ b/site/src/components/Resources/Resources.tsx @@ -200,6 +200,7 @@ export const Resources: FC> = ({ key={app.name} appsHost={applicationsHost} appIcon={app.icon} + appSlug={app.slug} appName={app.name} appCommand={app.command} appSubdomain={app.subdomain} diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 3362e968cf607..62d9faa851c24 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -196,7 +196,8 @@ export const MockTemplate: TypesGen.Template = { export const MockWorkspaceApp: TypesGen.WorkspaceApp = { id: "test-app", - name: "test-app", + slug: "test-app", + name: "Test App", icon: "", subdomain: false, health: "disabled", From ec5ecfe389d0d6ab87fafc7e4433a634e44ce40b Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Sun, 16 Oct 2022 19:49:36 +0000 Subject: [PATCH 02/14] fixup! feat: add slug property to app, use in URLs --- provisionersdk/proto/provisioner.pb.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/provisionersdk/proto/provisioner.pb.go b/provisionersdk/proto/provisioner.pb.go index 613a607a17e16..f66d2de3006fd 100644 --- a/provisionersdk/proto/provisioner.pb.go +++ b/provisionersdk/proto/provisioner.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.26.0 -// protoc v3.6.1 +// protoc v3.21.5 // source: provisionersdk/proto/provisioner.proto package proto From e6ddf3c358cd2a1c401b1c2892a332ccb2748d41 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Mon, 17 Oct 2022 19:52:50 +0000 Subject: [PATCH 03/14] fixup! feat: add slug property to app, use in URLs --- coderd/provisionerdaemons.go | 4 +- provisioner/appname.go | 14 - provisioner/appslug.go | 15 + .../{appname_test.go => appslug_test.go} | 6 +- provisioner/terraform/resources.go | 4 +- provisioner/terraform/resources_test.go | 283 ++++++++++-------- .../invalid-app-slug/invalid-app-slug.tf | 25 ++ .../invalid-app-slug.tfplan.dot | 20 ++ .../invalid-app-slug.tfplan.json | 213 +++++++++++++ .../invalid-app-slug.tfstate.dot | 20 ++ .../invalid-app-slug.tfstate.json | 68 +++++ .../multiple-apps/multiple-apps.tfplan.json | 2 +- 12 files changed, 534 insertions(+), 140 deletions(-) delete mode 100644 provisioner/appname.go create mode 100644 provisioner/appslug.go rename provisioner/{appname_test.go => appslug_test.go} (80%) create mode 100644 provisioner/terraform/testdata/invalid-app-slug/invalid-app-slug.tf create mode 100644 provisioner/terraform/testdata/invalid-app-slug/invalid-app-slug.tfplan.dot create mode 100644 provisioner/terraform/testdata/invalid-app-slug/invalid-app-slug.tfplan.json create mode 100644 provisioner/terraform/testdata/invalid-app-slug/invalid-app-slug.tfstate.dot create mode 100644 provisioner/terraform/testdata/invalid-app-slug/invalid-app-slug.tfstate.json diff --git a/coderd/provisionerdaemons.go b/coderd/provisionerdaemons.go index 468ac56ef9c0a..ae4f2c1575e28 100644 --- a/coderd/provisionerdaemons.go +++ b/coderd/provisionerdaemons.go @@ -814,8 +814,8 @@ func insertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid. if slug == "" { return xerrors.Errorf("app must have a slug or name set") } - if !provisioner.ValidAppNameRegex.MatchString(slug) { - return xerrors.Errorf("app slug %q does not match regex %q", slug, provisioner.ValidAppNameRegex.String()) + if !provisioner.ValidAppSlugRegex.MatchString(slug) { + return xerrors.Errorf("app slug %q does not match regex %q", slug, provisioner.ValidAppSlugRegex.String()) } health := database.WorkspaceAppHealthDisabled diff --git a/provisioner/appname.go b/provisioner/appname.go deleted file mode 100644 index 96f3b615bfc59..0000000000000 --- a/provisioner/appname.go +++ /dev/null @@ -1,14 +0,0 @@ -package provisioner - -import "regexp" - -var ( - // ValidAppNameRegex is the regex used to validate the name 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 looks complicated but it's written this way to avoid a - // negative lookahead (which is not supported by Go). There are test cases - // for this regex in appname_test.go. - ValidAppNameRegex = regexp.MustCompile(`^[a-z0-9]([a-z0-9-][a-z0-9]|[a-z0-9]([a-z0-9-]?[a-z0-9])?)*$`) -) diff --git a/provisioner/appslug.go b/provisioner/appslug.go new file mode 100644 index 0000000000000..18c8563e8edba --- /dev/null +++ b/provisioner/appslug.go @@ -0,0 +1,15 @@ +package provisioner + +import "regexp" + +var ( + // ValidAppSlugRegex 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. + ValidAppSlugRegex = regexp.MustCompile(`^[a-z0-9](-?[a-z0-9])*$`) +) diff --git a/provisioner/appname_test.go b/provisioner/appslug_test.go similarity index 80% rename from provisioner/appname_test.go rename to provisioner/appslug_test.go index 91f2668d19ac2..154aa002935ba 100644 --- a/provisioner/appname_test.go +++ b/provisioner/appslug_test.go @@ -8,7 +8,7 @@ import ( "github.com/coder/coder/provisioner" ) -func TestValidAppNameRegex(t *testing.T) { +func TestValidAppSlugRegex(t *testing.T) { t.Parallel() t.Run("Valid", func(t *testing.T) { @@ -32,7 +32,7 @@ func TestValidAppNameRegex(t *testing.T) { } for _, s := range validStrings { - require.True(t, provisioner.ValidAppNameRegex.MatchString(s), s) + require.True(t, provisioner.ValidAppSlugRegex.MatchString(s), s) } }) @@ -58,7 +58,7 @@ func TestValidAppNameRegex(t *testing.T) { } for _, s := range invalidStrings { - require.False(t, provisioner.ValidAppNameRegex.MatchString(s), s) + require.False(t, provisioner.ValidAppSlugRegex.MatchString(s), s) } }) } diff --git a/provisioner/terraform/resources.go b/provisioner/terraform/resources.go index faa44a77fdf12..9b083fecfc5db 100644 --- a/provisioner/terraform/resources.go +++ b/provisioner/terraform/resources.go @@ -221,8 +221,8 @@ func ConvertResources(module *tfjson.StateModule, rawGraph string) ([]*proto.Res } slug := resource.Name - if !provisioner.ValidAppNameRegex.MatchString(slug) { - return nil, xerrors.Errorf("invalid app name, must be a valid hostname (%q, cannot contain two consecutive hyphens or start/end with a hyphen): %q", provisioner.ValidAppNameRegex.String(), slug) + if !provisioner.ValidAppSlugRegex.MatchString(slug) { + return nil, xerrors.Errorf("invalid app slug, must be a valid hostname (%q, cannot contain two consecutive hyphens or start/end with a hyphen): %q", provisioner.ValidAppSlugRegex.String(), slug) } var attrs agentAppAttributes diff --git a/provisioner/terraform/resources_test.go b/provisioner/terraform/resources_test.go index 6b0175218755d..adf40f316fce0 100644 --- a/provisioner/terraform/resources_test.go +++ b/provisioner/terraform/resources_test.go @@ -22,144 +22,178 @@ func TestConvertResources(t *testing.T) { t.Parallel() // nolint:dogsled _, filename, _, _ := runtime.Caller(0) - // nolint:paralleltest - for folderName, expected := range map[string][]*proto.Resource{ + + cases := []struct { + // name must correspond to ./testadata//.* + name string + expected []*proto.Resource + errorContains string + }{ // When a resource depends on another, the shortest route // to a resource should always be chosen for the agent. - "chaining-resources": {{ - Name: "a", - Type: "null_resource", - }, { - Name: "b", - Type: "null_resource", - Agents: []*proto.Agent{{ - Name: "main", - OperatingSystem: "linux", - Architecture: "amd64", - Auth: &proto.Agent_Token{}, + { + name: "chaining-resources", + expected: []*proto.Resource{{ + Name: "a", + Type: "null_resource", + }, { + Name: "b", + Type: "null_resource", + Agents: []*proto.Agent{{ + Name: "main", + OperatingSystem: "linux", + Architecture: "amd64", + Auth: &proto.Agent_Token{}, + }}, }}, - }}, + }, // This can happen when resources hierarchically conflict. // When multiple resources exist at the same level, the first // listed in state will be chosen. - "conflicting-resources": {{ - Name: "first", - Type: "null_resource", - Agents: []*proto.Agent{{ - Name: "main", - OperatingSystem: "linux", - Architecture: "amd64", - Auth: &proto.Agent_Token{}, + { + name: "conflicting-resources", + expected: []*proto.Resource{{ + Name: "first", + Type: "null_resource", + Agents: []*proto.Agent{{ + Name: "main", + OperatingSystem: "linux", + Architecture: "amd64", + Auth: &proto.Agent_Token{}, + }}, + }, { + Name: "second", + Type: "null_resource", }}, - }, { - Name: "second", - Type: "null_resource", - }}, + }, // Ensures the instance ID authentication type surfaces. - "instance-id": {{ - Name: "main", - Type: "null_resource", - Agents: []*proto.Agent{{ - Name: "main", - OperatingSystem: "linux", - Architecture: "amd64", - Auth: &proto.Agent_InstanceId{}, + { + name: "instance-id", + expected: []*proto.Resource{{ + Name: "main", + Type: "null_resource", + Agents: []*proto.Agent{{ + Name: "main", + OperatingSystem: "linux", + Architecture: "amd64", + Auth: &proto.Agent_InstanceId{}, + }}, }}, - }}, - // Ensures that calls to resources through modules work - // as expected. - "calling-module": {{ - Name: "example", - Type: "null_resource", - Agents: []*proto.Agent{{ - Name: "main", - OperatingSystem: "linux", - Architecture: "amd64", - Auth: &proto.Agent_Token{}, + }, + { + name: "calling-module", + expected: []*proto.Resource{{ + Name: "example", + Type: "null_resource", + Agents: []*proto.Agent{{ + Name: "main", + OperatingSystem: "linux", + Architecture: "amd64", + Auth: &proto.Agent_Token{}, + }}, }}, - }}, + }, // Ensures the attachment of multiple agents to a single // resource is successful. - "multiple-agents": {{ - Name: "dev", - Type: "null_resource", - Agents: []*proto.Agent{{ - Name: "dev1", - OperatingSystem: "linux", - Architecture: "amd64", - Auth: &proto.Agent_Token{}, - }, { - Name: "dev2", - OperatingSystem: "darwin", - Architecture: "amd64", - Auth: &proto.Agent_Token{}, - }, { - Name: "dev3", - OperatingSystem: "windows", - Architecture: "arm64", - Auth: &proto.Agent_Token{}, + { + name: "multiple-agents", + expected: []*proto.Resource{{ + Name: "dev", + Type: "null_resource", + Agents: []*proto.Agent{{ + Name: "dev1", + OperatingSystem: "linux", + Architecture: "amd64", + Auth: &proto.Agent_Token{}, + }, { + Name: "dev2", + OperatingSystem: "darwin", + Architecture: "amd64", + Auth: &proto.Agent_Token{}, + }, { + Name: "dev3", + OperatingSystem: "windows", + Architecture: "arm64", + Auth: &proto.Agent_Token{}, + }}, }}, - }}, + }, // Ensures multiple applications can be set for a single agent. - "multiple-apps": {{ - Name: "dev", - Type: "null_resource", - Agents: []*proto.Agent{{ - Name: "dev1", - OperatingSystem: "linux", - Architecture: "amd64", - Apps: []*proto.App{ - { - Slug: "app1", - Name: "app1", - // Subdomain defaults to false if unspecified. - Subdomain: false, - }, - { - Slug: "app2", - Name: "app2", - Subdomain: true, - Healthcheck: &proto.Healthcheck{ - Url: "http://localhost:13337/healthz", - Interval: 5, - Threshold: 6, + { + name: "multiple-apps", + expected: []*proto.Resource{{ + Name: "dev", + Type: "null_resource", + Agents: []*proto.Agent{{ + Name: "dev1", + OperatingSystem: "linux", + Architecture: "amd64", + Apps: []*proto.App{ + { + Slug: "app1", + Name: "app1", + // Subdomain defaults to false if unspecified. + Subdomain: false, + }, + { + Slug: "app2", + Name: "app2", + Subdomain: true, + Healthcheck: &proto.Healthcheck{ + Url: "http://localhost:13337/healthz", + Interval: 5, + Threshold: 6, + }, + }, + { + Slug: "app3", + Name: "app3", + Subdomain: false, }, }, - { - Slug: "app3", - Name: "app3", - Subdomain: false, - }, - }, - Auth: &proto.Agent_Token{}, + Auth: &proto.Agent_Token{}, + }}, }}, - }}, + }, // Tests fetching metadata about workspace resources. - "resource-metadata": {{ - Name: "about", - Type: "null_resource", - Hide: true, - Icon: "/icon/server.svg", - Metadata: []*proto.Resource_Metadata{{ - Key: "hello", - Value: "world", - }, { - Key: "null", - IsNull: true, - }, { - Key: "empty", - }, { - Key: "secret", - Value: "squirrel", - Sensitive: true, + { + name: "resource-metadata", + expected: []*proto.Resource{{ + Name: "about", + Type: "null_resource", + Hide: true, + Icon: "/icon/server.svg", + Metadata: []*proto.Resource_Metadata{{ + Key: "hello", + Value: "world", + }, { + Key: "null", + IsNull: true, + }, { + Key: "empty", + }, { + Key: "secret", + Value: "squirrel", + Sensitive: true, + }}, }}, - }}, - } { - folderName := folderName - expected := expected - t.Run(folderName, func(t *testing.T) { + }, + // Ensure that invalid app slugs fail. + { + name: "invalid-app-slug", + expected: nil, + errorContains: "invalid app slug", + }, + } + + for _, c := range cases { + c := c + t.Run(c.name, func(t *testing.T) { t.Parallel() + + folderName, expected := c.name, c.expected dir := filepath.Join(filepath.Dir(filename), "testdata", folderName) + t.Run("Plan", func(t *testing.T) { t.Parallel() @@ -172,6 +206,11 @@ func TestConvertResources(t *testing.T) { require.NoError(t, err) resources, err := terraform.ConvertResources(tfPlan.PlannedValues.RootModule, string(tfPlanGraph)) + if c.errorContains != "" { + require.Error(t, err) + require.ErrorContains(t, err, c.errorContains) + return + } require.NoError(t, err) sortResources(resources) @@ -191,8 +230,10 @@ func TestConvertResources(t *testing.T) { require.NoError(t, err) require.Equal(t, string(resourcesWant), string(resourcesGot)) }) + t.Run("Provision", func(t *testing.T) { t.Parallel() + tfStateRaw, err := os.ReadFile(filepath.Join(dir, folderName+".tfstate.json")) require.NoError(t, err) var tfState tfjson.State @@ -202,8 +243,14 @@ func TestConvertResources(t *testing.T) { require.NoError(t, err) resources, err := terraform.ConvertResources(tfState.Values.RootModule, string(tfStateGraph)) + if c.errorContains != "" { + require.Error(t, err) + require.ErrorContains(t, err, c.errorContains) + return + } require.NoError(t, err) sortResources(resources) + for _, resource := range resources { for _, agent := range resource.Agents { agent.Id = "" @@ -215,11 +262,11 @@ func TestConvertResources(t *testing.T) { } } } + resourcesWant, err := json.Marshal(expected) require.NoError(t, err) resourcesGot, err := json.Marshal(resources) require.NoError(t, err) - require.Equal(t, string(resourcesWant), string(resourcesGot)) }) }) diff --git a/provisioner/terraform/testdata/invalid-app-slug/invalid-app-slug.tf b/provisioner/terraform/testdata/invalid-app-slug/invalid-app-slug.tf new file mode 100644 index 0000000000000..e17eabdae7058 --- /dev/null +++ b/provisioner/terraform/testdata/invalid-app-slug/invalid-app-slug.tf @@ -0,0 +1,25 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + # future versions of coder/coder have built-in regex testing for valid + # app names, so we can't use a version after this. + version = "0.5.3" + } + } +} + +resource "coder_agent" "dev" { + os = "linux" + arch = "amd64" +} + +resource "null_resource" "dev" { + depends_on = [ + coder_agent.dev + ] +} + +resource "coder_app" "invalid_app_name" { + agent_id = coder_agent.dev.id +} diff --git a/provisioner/terraform/testdata/invalid-app-slug/invalid-app-slug.tfplan.dot b/provisioner/terraform/testdata/invalid-app-slug/invalid-app-slug.tfplan.dot new file mode 100644 index 0000000000000..d69316f58749c --- /dev/null +++ b/provisioner/terraform/testdata/invalid-app-slug/invalid-app-slug.tfplan.dot @@ -0,0 +1,20 @@ +digraph { + compound = "true" + newrank = "true" + subgraph "root" { + "[root] coder_agent.dev (expand)" [label = "coder_agent.dev", shape = "box"] + "[root] coder_app.invalid_app_name (expand)" [label = "coder_app.invalid_app_name", shape = "box"] + "[root] null_resource.dev (expand)" [label = "null_resource.dev", shape = "box"] + "[root] provider[\"registry.terraform.io/coder/coder\"]" [label = "provider[\"registry.terraform.io/coder/coder\"]", shape = "diamond"] + "[root] provider[\"registry.terraform.io/hashicorp/null\"]" [label = "provider[\"registry.terraform.io/hashicorp/null\"]", shape = "diamond"] + "[root] coder_agent.dev (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]" + "[root] coder_app.invalid_app_name (expand)" -> "[root] coder_agent.dev (expand)" + "[root] null_resource.dev (expand)" -> "[root] coder_agent.dev (expand)" + "[root] null_resource.dev (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/null\"]" + "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] coder_app.invalid_app_name (expand)" + "[root] provider[\"registry.terraform.io/hashicorp/null\"] (close)" -> "[root] null_resource.dev (expand)" + "[root] root" -> "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" + "[root] root" -> "[root] provider[\"registry.terraform.io/hashicorp/null\"] (close)" + } +} + diff --git a/provisioner/terraform/testdata/invalid-app-slug/invalid-app-slug.tfplan.json b/provisioner/terraform/testdata/invalid-app-slug/invalid-app-slug.tfplan.json new file mode 100644 index 0000000000000..7711af0e642d0 --- /dev/null +++ b/provisioner/terraform/testdata/invalid-app-slug/invalid-app-slug.tfplan.json @@ -0,0 +1,213 @@ +{ + "format_version": "1.1", + "terraform_version": "1.2.7", + "planned_values": { + "root_module": { + "resources": [ + { + "address": "coder_agent.dev", + "mode": "managed", + "type": "coder_agent", + "name": "dev", + "provider_name": "registry.terraform.io/coder/coder", + "schema_version": 0, + "values": { + "arch": "amd64", + "auth": "token", + "dir": null, + "env": null, + "os": "linux", + "startup_script": null + }, + "sensitive_values": {} + }, + { + "address": "coder_app.invalid_app_name", + "mode": "managed", + "type": "coder_app", + "name": "invalid_app_name", + "provider_name": "registry.terraform.io/coder/coder", + "schema_version": 0, + "values": { + "command": null, + "healthcheck": [], + "icon": null, + "name": null, + "relative_path": null, + "share": "owner", + "subdomain": null, + "url": null + }, + "sensitive_values": { + "healthcheck": [] + } + }, + { + "address": "null_resource.dev", + "mode": "managed", + "type": "null_resource", + "name": "dev", + "provider_name": "registry.terraform.io/hashicorp/null", + "schema_version": 0, + "values": { + "triggers": null + }, + "sensitive_values": {} + } + ] + } + }, + "resource_changes": [ + { + "address": "coder_agent.dev", + "mode": "managed", + "type": "coder_agent", + "name": "dev", + "provider_name": "registry.terraform.io/coder/coder", + "change": { + "actions": [ + "create" + ], + "before": null, + "after": { + "arch": "amd64", + "auth": "token", + "dir": null, + "env": null, + "os": "linux", + "startup_script": null + }, + "after_unknown": { + "id": true, + "init_script": true, + "token": true + }, + "before_sensitive": false, + "after_sensitive": { + "token": true + } + } + }, + { + "address": "coder_app.invalid_app_name", + "mode": "managed", + "type": "coder_app", + "name": "invalid_app_name", + "provider_name": "registry.terraform.io/coder/coder", + "change": { + "actions": [ + "create" + ], + "before": null, + "after": { + "command": null, + "healthcheck": [], + "icon": null, + "name": null, + "relative_path": null, + "share": "owner", + "subdomain": null, + "url": null + }, + "after_unknown": { + "agent_id": true, + "healthcheck": [], + "id": true + }, + "before_sensitive": false, + "after_sensitive": { + "healthcheck": [] + } + } + }, + { + "address": "null_resource.dev", + "mode": "managed", + "type": "null_resource", + "name": "dev", + "provider_name": "registry.terraform.io/hashicorp/null", + "change": { + "actions": [ + "create" + ], + "before": null, + "after": { + "triggers": null + }, + "after_unknown": { + "id": true + }, + "before_sensitive": false, + "after_sensitive": {} + } + } + ], + "configuration": { + "provider_config": { + "coder": { + "name": "coder", + "full_name": "registry.terraform.io/coder/coder", + "version_constraint": "0.5.3" + }, + "null": { + "name": "null", + "full_name": "registry.terraform.io/hashicorp/null" + } + }, + "root_module": { + "resources": [ + { + "address": "coder_agent.dev", + "mode": "managed", + "type": "coder_agent", + "name": "dev", + "provider_config_key": "coder", + "expressions": { + "arch": { + "constant_value": "amd64" + }, + "os": { + "constant_value": "linux" + } + }, + "schema_version": 0 + }, + { + "address": "coder_app.invalid_app_name", + "mode": "managed", + "type": "coder_app", + "name": "invalid_app_name", + "provider_config_key": "coder", + "expressions": { + "agent_id": { + "references": [ + "coder_agent.dev.id", + "coder_agent.dev" + ] + } + }, + "schema_version": 0 + }, + { + "address": "null_resource.dev", + "mode": "managed", + "type": "null_resource", + "name": "dev", + "provider_config_key": "null", + "schema_version": 0, + "depends_on": [ + "coder_agent.dev" + ] + } + ] + } + }, + "relevant_attributes": [ + { + "resource": "coder_agent.dev", + "attribute": [ + "id" + ] + } + ] +} diff --git a/provisioner/terraform/testdata/invalid-app-slug/invalid-app-slug.tfstate.dot b/provisioner/terraform/testdata/invalid-app-slug/invalid-app-slug.tfstate.dot new file mode 100644 index 0000000000000..d69316f58749c --- /dev/null +++ b/provisioner/terraform/testdata/invalid-app-slug/invalid-app-slug.tfstate.dot @@ -0,0 +1,20 @@ +digraph { + compound = "true" + newrank = "true" + subgraph "root" { + "[root] coder_agent.dev (expand)" [label = "coder_agent.dev", shape = "box"] + "[root] coder_app.invalid_app_name (expand)" [label = "coder_app.invalid_app_name", shape = "box"] + "[root] null_resource.dev (expand)" [label = "null_resource.dev", shape = "box"] + "[root] provider[\"registry.terraform.io/coder/coder\"]" [label = "provider[\"registry.terraform.io/coder/coder\"]", shape = "diamond"] + "[root] provider[\"registry.terraform.io/hashicorp/null\"]" [label = "provider[\"registry.terraform.io/hashicorp/null\"]", shape = "diamond"] + "[root] coder_agent.dev (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]" + "[root] coder_app.invalid_app_name (expand)" -> "[root] coder_agent.dev (expand)" + "[root] null_resource.dev (expand)" -> "[root] coder_agent.dev (expand)" + "[root] null_resource.dev (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/null\"]" + "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] coder_app.invalid_app_name (expand)" + "[root] provider[\"registry.terraform.io/hashicorp/null\"] (close)" -> "[root] null_resource.dev (expand)" + "[root] root" -> "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" + "[root] root" -> "[root] provider[\"registry.terraform.io/hashicorp/null\"] (close)" + } +} + diff --git a/provisioner/terraform/testdata/invalid-app-slug/invalid-app-slug.tfstate.json b/provisioner/terraform/testdata/invalid-app-slug/invalid-app-slug.tfstate.json new file mode 100644 index 0000000000000..17f51fef79675 --- /dev/null +++ b/provisioner/terraform/testdata/invalid-app-slug/invalid-app-slug.tfstate.json @@ -0,0 +1,68 @@ +{ + "format_version": "1.0", + "terraform_version": "1.2.8", + "values": { + "root_module": { + "resources": [ + { + "address": "coder_agent.dev", + "mode": "managed", + "type": "coder_agent", + "name": "dev", + "provider_name": "registry.terraform.io/coder/coder", + "schema_version": 0, + "values": { + "arch": "amd64", + "auth": "token", + "dir": null, + "env": null, + "id": "4df45b21-c841-46d3-b3e3-d1efd3a9657e", + "init_script": "", + "os": "linux", + "startup_script": null, + "token": "4bddad38-b622-4963-b353-249171359be8" + }, + "sensitive_values": {} + }, + { + "address": "coder_app.invalid_app_name", + "mode": "managed", + "type": "coder_app", + "name": "invalid_app_name", + "provider_name": "registry.terraform.io/coder/coder", + "schema_version": 0, + "values": { + "agent_id": "4df45b21-c841-46d3-b3e3-d1efd3a9657e", + "command": null, + "healthcheck": [], + "icon": null, + "id": "d2b5d94a-4c47-484e-862f-4eb700290dbb", + "name": null, + "relative_path": null, + "share": "owner", + "subdomain": null, + "url": null + }, + "sensitive_values": { + "healthcheck": [] + }, + "depends_on": ["coder_agent.dev"] + }, + { + "address": "null_resource.dev", + "mode": "managed", + "type": "null_resource", + "name": "dev", + "provider_name": "registry.terraform.io/hashicorp/null", + "schema_version": 0, + "values": { + "id": "6380390816158464544", + "triggers": null + }, + "sensitive_values": {}, + "depends_on": ["coder_agent.dev"] + } + ] + } + } +} diff --git a/provisioner/terraform/testdata/multiple-apps/multiple-apps.tfplan.json b/provisioner/terraform/testdata/multiple-apps/multiple-apps.tfplan.json index 93f60329583f7..be1d57b1eb258 100644 --- a/provisioner/terraform/testdata/multiple-apps/multiple-apps.tfplan.json +++ b/provisioner/terraform/testdata/multiple-apps/multiple-apps.tfplan.json @@ -1,6 +1,6 @@ { "format_version": "1.1", - "terraform_version": "1.2.8", + "terraform_version": "1.2.7", "planned_values": { "root_module": { "resources": [ From 43df27e3e4f3be73eb60f5730ceff2f7acf85f66 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Tue, 18 Oct 2022 20:54:43 +0000 Subject: [PATCH 04/14] feat: app slugs pt.2 --- coderd/coderdtest/authorize.go | 6 +- coderd/provisionerdaemons.go | 9 +- coderd/workspaceagents_test.go | 26 +- coderd/workspaceapps_test.go | 8 +- coderd/workspaces_test.go | 24 +- docs/ides/web-ides.md | 30 +- dogfood/main.tf | 13 +- enterprise/coderd/workspaceagents_test.go | 6 +- examples/templates/aws-ecs-container/main.tf | 13 +- examples/templates/aws-linux/main.tf | 13 +- examples/templates/bare/main.tf | 13 +- examples/templates/docker-code-server/main.tf | 13 +- .../templates/docker-image-builds/main.tf | 13 +- examples/templates/docker/main.tf | 13 +- examples/templates/gcp-linux/main.tf | 13 +- examples/templates/gcp-vm-container/main.tf | 13 +- examples/templates/kubernetes/main.tf | 13 +- provisioner/appslug.go | 4 +- provisioner/appslug_test.go | 4 +- provisioner/terraform/resources.go | 36 ++- provisioner/terraform/resources_test.go | 18 +- .../invalid-app-slug/invalid-app-slug.tf | 3 +- .../testdata/multiple-apps/multiple-apps.tf | 3 + provisionersdk/proto/provisioner.pb.go | 287 +++++++++--------- provisionersdk/proto/provisioner.proto | 2 +- 25 files changed, 313 insertions(+), 283 deletions(-) diff --git a/coderd/coderdtest/authorize.go b/coderd/coderdtest/authorize.go index 548a4fb17c5b0..7fadfa3419c7c 100644 --- a/coderd/coderdtest/authorize.go +++ b/coderd/coderdtest/authorize.go @@ -330,9 +330,9 @@ func NewAuthTester(ctx context.Context, t *testing.T, client *codersdk.Client, a Id: "something", Auth: &proto.Agent_Token{}, Apps: []*proto.App{{ - Slug: "testapp", - Name: "testapp", - Url: "http://localhost:3000", + Slug: "testapp", + DisplayName: "testapp", + Url: "http://localhost:3000", }}, }}, }}, diff --git a/coderd/provisionerdaemons.go b/coderd/provisionerdaemons.go index ae4f2c1575e28..df7e4339ed6ab 100644 --- a/coderd/provisionerdaemons.go +++ b/coderd/provisionerdaemons.go @@ -808,14 +808,11 @@ func insertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid. for _, app := range prAgent.Apps { slug := app.Slug - if slug == "" { - slug = app.Name - } if slug == "" { return xerrors.Errorf("app must have a slug or name set") } - if !provisioner.ValidAppSlugRegex.MatchString(slug) { - return xerrors.Errorf("app slug %q does not match regex %q", slug, provisioner.ValidAppSlugRegex.String()) + if !provisioner.AppSlugRegex.MatchString(slug) { + return xerrors.Errorf("app slug %q does not match regex %q", slug, provisioner.AppSlugRegex.String()) } health := database.WorkspaceAppHealthDisabled @@ -839,7 +836,7 @@ func insertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid. CreatedAt: database.Now(), AgentID: dbAgent.ID, Slug: slug, - Name: app.Name, + Name: app.DisplayName, Icon: app.Icon, Command: sql.NullString{ String: app.Command, diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go index 52fcabf53eb0f..f5644e5308788 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -553,9 +553,9 @@ func TestWorkspaceAgentListeningPorts(t *testing.T) { // should not exist in the response. _, appLPort := generateUnfilteredPort(t) app := &proto.App{ - Slug: "test-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. @@ -622,18 +622,18 @@ func TestWorkspaceAgentAppHealth(t *testing.T) { authToken := uuid.NewString() apps := []*proto.App{ { - Slug: "code-server", - 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", }, { - Slug: "code-server-2", - 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_test.go b/coderd/workspaceapps_test.go index db810274b247d..91df3305738d2 100644 --- a/coderd/workspaceapps_test.go +++ b/coderd/workspaceapps_test.go @@ -150,26 +150,26 @@ func createWorkspaceWithApps(t *testing.T, client *codersdk.Client, orgID uuid.U Apps: []*proto.App{ { Slug: proxyTestAppNameFake, - Name: proxyTestAppNameFake, + DisplayName: proxyTestAppNameFake, SharingLevel: proto.AppSharingLevel_OWNER, // Hopefully this IP and port doesn't exist. Url: "http://127.1.0.1:65535", }, { Slug: proxyTestAppNameOwner, - Name: proxyTestAppNameOwner, + DisplayName: proxyTestAppNameOwner, SharingLevel: proto.AppSharingLevel_OWNER, Url: appURL, }, { Slug: proxyTestAppNameAuthenticated, - Name: proxyTestAppNameAuthenticated, + DisplayName: proxyTestAppNameAuthenticated, SharingLevel: proto.AppSharingLevel_AUTHENTICATED, Url: appURL, }, { Slug: proxyTestAppNamePublic, - Name: proxyTestAppNamePublic, + DisplayName: proxyTestAppNamePublic, SharingLevel: proto.AppSharingLevel_PUBLIC, Url: appURL, }, diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index e68937149b40e..736b223c2f5f7 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -1408,18 +1408,18 @@ func TestWorkspaceResource(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) apps := []*proto.App{ { - Slug: "code-server", - 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", }, { - Slug: "code-server-2", - 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, @@ -1462,7 +1462,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.Name) require.EqualValues(t, codersdk.WorkspaceAppHealthDisabled, got.Health) require.EqualValues(t, "", got.Healthcheck.URL) require.EqualValues(t, 0, got.Healthcheck.Interval) @@ -1471,7 +1471,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.Name) 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/docs/ides/web-ides.md b/docs/ides/web-ides.md index ef7249df5cf1d..9c4907c5834b1 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/" @@ -235,10 +238,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 cc65f6b1c6f42..6143e5666aa32 100644 --- a/dogfood/main.tf +++ b/dogfood/main.tf @@ -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 fa5fdbab98abf..8ca6ea94baabf 100644 --- a/enterprise/coderd/workspaceagents_test.go +++ b/enterprise/coderd/workspaceagents_test.go @@ -84,19 +84,19 @@ func setupWorkspaceAgent(t *testing.T, client *codersdk.Client, user codersdk.Cr Apps: []*proto.App{ { Slug: testAppNameOwner, - Name: testAppNameOwner, + DisplayName: testAppNameOwner, SharingLevel: proto.AppSharingLevel_OWNER, Url: fmt.Sprintf("http://localhost:%d", appPort), }, { Slug: testAppNameAuthenticated, - Name: testAppNameAuthenticated, + DisplayName: testAppNameAuthenticated, SharingLevel: proto.AppSharingLevel_AUTHENTICATED, Url: fmt.Sprintf("http://localhost:%d", appPort), }, { Slug: testAppNamePublic, - Name: 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..b7c41d45873f5 100644 --- a/examples/templates/aws-ecs-container/main.tf +++ b/examples/templates/aws-ecs-container/main.tf @@ -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..420c4babed28b 100644 --- a/examples/templates/aws-linux/main.tf +++ b/examples/templates/aws-linux/main.tf @@ -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/bare/main.tf b/examples/templates/bare/main.tf index b51b3e777c3e9..ee668d4997416 100644 --- a/examples/templates/bare/main.tf +++ b/examples/templates/bare/main.tf @@ -43,12 +43,13 @@ resource "null_resource" "fake-disk" { resource "coder_app" "fake-app" { # Access :8080 in the workspace from the Coder dashboard. - name = "VS Code" - icon = "/icon/code.svg" - agent_id = "fake-compute" - url = "http://localhost:8080" - subdomain = false - share = "owner" + slug = "fake-app" + display_name = "VS Code" + icon = "/icon/code.svg" + agent_id = "fake-compute" + url = "http://localhost:8080" + subdomain = false + share = "owner" healthcheck { url = "http://localhost:8080/healthz" diff --git a/examples/templates/docker-code-server/main.tf b/examples/templates/docker-code-server/main.tf index 2e4f4f5b488bc..c36328fd2bef7 100644 --- a/examples/templates/docker-code-server/main.tf +++ b/examples/templates/docker-code-server/main.tf @@ -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..2e7bbfd4cfd91 100644 --- a/examples/templates/docker-image-builds/main.tf +++ b/examples/templates/docker-image-builds/main.tf @@ -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/main.tf b/examples/templates/docker/main.tf index 677dace7f43f6..4f22622f319ce 100644 --- a/examples/templates/docker/main.tf +++ b/examples/templates/docker/main.tf @@ -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..92ad5bbb240eb 100644 --- a/examples/templates/gcp-linux/main.tf +++ b/examples/templates/gcp-linux/main.tf @@ -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..18814b1036508 100644 --- a/examples/templates/gcp-vm-container/main.tf +++ b/examples/templates/gcp-vm-container/main.tf @@ -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/kubernetes/main.tf b/examples/templates/kubernetes/main.tf index b9d6ebd0baf1f..dd07e0c0aabb9 100644 --- a/examples/templates/kubernetes/main.tf +++ b/examples/templates/kubernetes/main.tf @@ -71,12 +71,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 index 18c8563e8edba..cf3f37942dad0 100644 --- a/provisioner/appslug.go +++ b/provisioner/appslug.go @@ -3,7 +3,7 @@ package provisioner import "regexp" var ( - // ValidAppSlugRegex is the regex used to validate the slug of a coder_app + // 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. // @@ -11,5 +11,5 @@ var ( // update it there as well. // // There are test cases for this regex in appslug_test.go. - ValidAppSlugRegex = regexp.MustCompile(`^[a-z0-9](-?[a-z0-9])*$`) + AppSlugRegex = regexp.MustCompile(`^[a-z0-9](-?[a-z0-9])*$`) ) diff --git a/provisioner/appslug_test.go b/provisioner/appslug_test.go index 154aa002935ba..2fbd3f08ea1cd 100644 --- a/provisioner/appslug_test.go +++ b/provisioner/appslug_test.go @@ -32,7 +32,7 @@ func TestValidAppSlugRegex(t *testing.T) { } for _, s := range validStrings { - require.True(t, provisioner.ValidAppSlugRegex.MatchString(s), s) + require.True(t, provisioner.AppSlugRegex.MatchString(s), s) } }) @@ -58,7 +58,7 @@ func TestValidAppSlugRegex(t *testing.T) { } for _, s := range invalidStrings { - require.False(t, provisioner.ValidAppSlugRegex.MatchString(s), s) + require.False(t, provisioner.AppSlugRegex.MatchString(s), s) } }) } diff --git a/provisioner/terraform/resources.go b/provisioner/terraform/resources.go index 9b083fecfc5db..c8038d5a154c2 100644 --- a/provisioner/terraform/resources.go +++ b/provisioner/terraform/resources.go @@ -26,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"` @@ -220,20 +225,29 @@ func ConvertResources(module *tfjson.StateModule, rawGraph string) ([]*proto.Res continue } - slug := resource.Name - if !provisioner.ValidAppSlugRegex.MatchString(slug) { - return nil, xerrors.Errorf("invalid app slug, must be a valid hostname (%q, cannot contain two consecutive hyphens or start/end with a hyphen): %q", provisioner.ValidAppSlugRegex.String(), slug) - } - 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) } + var healthcheck *proto.Healthcheck if len(attrs.Healthcheck) != 0 { healthcheck = &proto.Healthcheck{ @@ -260,8 +274,8 @@ func ConvertResources(module *tfjson.StateModule, rawGraph string) ([]*proto.Res continue } agent.Apps = append(agent.Apps, &proto.App{ - Slug: slug, - 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 adf40f316fce0..c3d7536993ce5 100644 --- a/provisioner/terraform/resources_test.go +++ b/provisioner/terraform/resources_test.go @@ -130,15 +130,15 @@ func TestConvertResources(t *testing.T) { Architecture: "amd64", Apps: []*proto.App{ { - Slug: "app1", - Name: "app1", + Slug: "app1", + DisplayName: "app1", // Subdomain defaults to false if unspecified. Subdomain: false, }, { - Slug: "app2", - Name: "app2", - Subdomain: true, + Slug: "app2", + DisplayName: "app2", + Subdomain: true, Healthcheck: &proto.Healthcheck{ Url: "http://localhost:13337/healthz", Interval: 5, @@ -146,9 +146,9 @@ func TestConvertResources(t *testing.T) { }, }, { - Slug: "app3", - Name: "app3", - Subdomain: false, + Slug: "app3", + DisplayName: "app3", + Subdomain: false, }, }, Auth: &proto.Agent_Token{}, @@ -354,7 +354,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].DisplayName < agent.Apps[j].DisplayName }) } sort.Slice(resource.Agents, func(i, j int) bool { diff --git a/provisioner/terraform/testdata/invalid-app-slug/invalid-app-slug.tf b/provisioner/terraform/testdata/invalid-app-slug/invalid-app-slug.tf index e17eabdae7058..9febc994854b8 100644 --- a/provisioner/terraform/testdata/invalid-app-slug/invalid-app-slug.tf +++ b/provisioner/terraform/testdata/invalid-app-slug/invalid-app-slug.tf @@ -20,6 +20,7 @@ resource "null_resource" "dev" { ] } -resource "coder_app" "invalid_app_name" { +resource "coder_app" "invalid-app-slug" { agent_id = coder_agent.dev.id + slug = "$$$" } diff --git a/provisioner/terraform/testdata/multiple-apps/multiple-apps.tf b/provisioner/terraform/testdata/multiple-apps/multiple-apps.tf index 446183a9dbb06..f73630c056181 100644 --- a/provisioner/terraform/testdata/multiple-apps/multiple-apps.tf +++ b/provisioner/terraform/testdata/multiple-apps/multiple-apps.tf @@ -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/provisionersdk/proto/provisioner.pb.go b/provisionersdk/proto/provisioner.pb.go index f66d2de3006fd..b26ab11d1171f 100644 --- a/provisionersdk/proto/provisioner.pb.go +++ b/provisionersdk/proto/provisioner.pb.go @@ -902,7 +902,7 @@ type App struct { // 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"` - Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,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"` @@ -950,9 +950,9 @@ func (x *App) GetSlug() string { return "" } -func (x *App) GetName() string { +func (x *App) GetDisplayName() string { if x != nil { - return x.Name + return x.DisplayName } return "" } @@ -2019,149 +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, 0x8a, + 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, 0x12, 0x0a, 0x04, 0x6e, 0x61, - 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 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, + 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, 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, 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, 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, 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, + 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, 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, } var ( diff --git a/provisionersdk/proto/provisioner.proto b/provisionersdk/proto/provisioner.proto index 878f2bc6e7c5e..e3f59ba6bf6c5 100644 --- a/provisionersdk/proto/provisioner.proto +++ b/provisionersdk/proto/provisioner.proto @@ -98,7 +98,7 @@ message App { // 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 name = 2; + string display_name = 2; string command = 3; string url = 4; string icon = 5; From c65857275c235a53ae0833ac947dbcf44b4e890c Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Tue, 18 Oct 2022 22:51:05 +0000 Subject: [PATCH 05/14] feat: app slugs pt.3 --- agent/apphealth.go | 12 +++---- agent/apphealth_test.go | 12 +++---- coderd/database/databasefake/databasefake.go | 2 +- coderd/database/dump.sql | 4 +-- .../migrations/000061_app_slug.up.sql | 4 +++ .../000062_app_display_name.down.sql | 2 ++ .../migrations/000062_app_display_name.up.sql | 2 ++ coderd/database/models.go | 2 +- coderd/database/queries.sql.go | 36 +++++++++---------- coderd/database/queries/workspaceapps.sql | 4 +-- coderd/database/unique_constraint.go | 2 +- coderd/httpapi/url.go | 2 +- coderd/httpapi/url_test.go | 4 +-- coderd/provisionerdaemons.go | 12 +++---- coderd/workspaceagents.go | 4 +-- coderd/workspaceapps_test.go | 2 +- coderd/workspaces_test.go | 4 +-- codersdk/workspaceapps.go | 8 ++--- .../invalid-app-slug/invalid-app-slug.tf | 4 +-- provisionersdk/proto/provisioner.pb.go | 2 +- provisionersdk/proto/provisioner.proto | 16 ++++----- site/src/api/typesGenerated.ts | 2 +- .../components/AppLink/AppLink.stories.tsx | 10 +++--- site/src/components/AppLink/AppLink.tsx | 16 ++++----- site/src/components/Resources/Resources.tsx | 4 +-- site/src/testHelpers/entities.ts | 2 +- 26 files changed, 90 insertions(+), 84 deletions(-) create mode 100644 coderd/database/migrations/000062_app_display_name.down.sql create mode 100644 coderd/database/migrations/000062_app_display_name.up.sql diff --git a/agent/apphealth.go b/agent/apphealth.go index d53c76e57a9b8..9b803ecedcb15 100644 --- a/agent/apphealth.go +++ b/agent/apphealth.go @@ -41,7 +41,7 @@ func NewWorkspaceAppHealthReporter(logger slog.Logger, workspaceAgentApps Worksp 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 } @@ -91,21 +91,21 @@ func NewWorkspaceAppHealthReporter(logger slog.Logger, workspaceAgentApps Worksp }() 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 92d2a895b8be3..621cb7e96cdc1 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/coderd/database/databasefake/databasefake.go b/coderd/database/databasefake/databasefake.go index 091a74b1fe6fb..57fa0118f77ae 100644 --- a/coderd/database/databasefake/databasefake.go +++ b/coderd/database/databasefake/databasefake.go @@ -2363,7 +2363,7 @@ func (q *fakeQuerier) InsertWorkspaceApp(_ context.Context, arg database.InsertW AgentID: arg.AgentID, CreatedAt: arg.CreatedAt, Slug: arg.Slug, - Name: arg.Name, + 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 bb63b3bd5a09d..250a70d6878a8 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -369,7 +369,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), @@ -516,7 +516,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_name_key UNIQUE (agent_id, display_name); ALTER TABLE ONLY workspace_apps ADD CONSTRAINT workspace_apps_pkey PRIMARY KEY (id); diff --git a/coderd/database/migrations/000061_app_slug.up.sql b/coderd/database/migrations/000061_app_slug.up.sql index 5962bc2ed1d6e..6f604dae0a608 100644 --- a/coderd/database/migrations/000061_app_slug.up.sql +++ b/coderd/database/migrations/000061_app_slug.up.sql @@ -1,3 +1,5 @@ +BEGIN; + -- add "slug" column to "workspace_apps" table ALTER TABLE "workspace_apps" ADD COLUMN "slug" text DEFAULT ''; @@ -7,3 +9,5 @@ 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; + +COMMIT; diff --git a/coderd/database/migrations/000062_app_display_name.down.sql b/coderd/database/migrations/000062_app_display_name.down.sql new file mode 100644 index 0000000000000..21be778264303 --- /dev/null +++ b/coderd/database/migrations/000062_app_display_name.down.sql @@ -0,0 +1,2 @@ +-- rename column "display_name" to "name" on "workspace_apps" +ALTER TABLE "workspace_apps" RENAME COLUMN "display_name" TO "name"; diff --git a/coderd/database/migrations/000062_app_display_name.up.sql b/coderd/database/migrations/000062_app_display_name.up.sql new file mode 100644 index 0000000000000..5a422777f2dfa --- /dev/null +++ b/coderd/database/migrations/000062_app_display_name.up.sql @@ -0,0 +1,2 @@ +-- rename column "name" to "display_name" on "workspace_apps" +ALTER TABLE "workspace_apps" RENAME COLUMN "name" TO "display_name"; diff --git a/coderd/database/models.go b/coderd/database/models.go index 5c7fe8583a541..d91f77b90e12f 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -637,7 +637,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"` diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 05fc0537110ee..a8ce47c5ad532 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -854,8 +854,8 @@ func (q *sqlQuerier) UpdateGitSSHKey(ctx context.Context, arg UpdateGitSSHKeyPar } const deleteGroupByID = `-- name: DeleteGroupByID :exec -DELETE FROM - groups +DELETE FROM + groups WHERE id = $1 ` @@ -866,8 +866,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 ` @@ -4370,7 +4370,7 @@ func (q *sqlQuerier) UpdateWorkspaceAgentVersionByID(ctx context.Context, arg Up } const getWorkspaceAppByAgentIDAndSlug = `-- name: GetWorkspaceAppByAgentIDAndSlug :one -SELECT id, created_at, agent_id, 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 +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 GetWorkspaceAppByAgentIDAndSlugParams struct { @@ -4385,7 +4385,7 @@ func (q *sqlQuerier) GetWorkspaceAppByAgentIDAndSlug(ctx context.Context, arg Ge &i.ID, &i.CreatedAt, &i.AgentID, - &i.Name, + &i.DisplayName, &i.Icon, &i.Command, &i.Url, @@ -4401,7 +4401,7 @@ func (q *sqlQuerier) GetWorkspaceAppByAgentIDAndSlug(ctx context.Context, arg Ge } 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, slug FROM workspace_apps WHERE agent_id = $1 ORDER BY slug 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) { @@ -4417,7 +4417,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, @@ -4443,7 +4443,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, slug FROM workspace_apps WHERE agent_id = ANY($1 :: uuid [ ]) ORDER BY slug 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) { @@ -4459,7 +4459,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, @@ -4485,7 +4485,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, slug FROM workspace_apps WHERE created_at > $1 ORDER BY slug 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) { @@ -4501,7 +4501,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, @@ -4532,8 +4532,8 @@ INSERT INTO id, created_at, agent_id, - slug, - name, + slug, + display_name, icon, command, url, @@ -4545,7 +4545,7 @@ INSERT INTO health ) VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) RETURNING id, created_at, agent_id, name, icon, command, url, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, subdomain, sharing_level, slug + ($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 { @@ -4553,7 +4553,7 @@ type InsertWorkspaceAppParams struct { CreatedAt time.Time `db:"created_at" json:"created_at"` AgentID uuid.UUID `db:"agent_id" json:"agent_id"` Slug string `db:"slug" json:"slug"` - 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"` @@ -4571,7 +4571,7 @@ func (q *sqlQuerier) InsertWorkspaceApp(ctx context.Context, arg InsertWorkspace arg.CreatedAt, arg.AgentID, arg.Slug, - arg.Name, + arg.DisplayName, arg.Icon, arg.Command, arg.Url, @@ -4587,7 +4587,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, diff --git a/coderd/database/queries/workspaceapps.sql b/coderd/database/queries/workspaceapps.sql index 507a664c46a85..03f5b62b15111 100644 --- a/coderd/database/queries/workspaceapps.sql +++ b/coderd/database/queries/workspaceapps.sql @@ -16,8 +16,8 @@ INSERT INTO id, created_at, agent_id, - slug, - name, + slug, + display_name, icon, command, url, diff --git a/coderd/database/unique_constraint.go b/coderd/database/unique_constraint.go index b4263c09b4762..f1990113ce2c3 100644 --- a/coderd/database/unique_constraint.go +++ b/coderd/database/unique_constraint.go @@ -15,7 +15,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); + UniqueWorkspaceAppsAgentIDNameKey UniqueConstraint = "workspace_apps_agent_id_name_key" // ALTER TABLE ONLY workspace_apps ADD CONSTRAINT workspace_apps_agent_id_name_key UNIQUE (agent_id, display_name); 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 4083ccd4761f2..ecef3d4f5e960 100644 --- a/coderd/httpapi/url.go +++ b/coderd/httpapi/url.go @@ -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) diff --git a/coderd/httpapi/url_test.go b/coderd/httpapi/url_test.go index 4e4b42acd7462..84cfcac7d39ca 100644 --- a/coderd/httpapi/url_test.go +++ b/coderd/httpapi/url_test.go @@ -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{ - AppSlug: "app-name", + AppSlug: "app-slug", Port: 0, AgentName: "agent-name", WorkspaceName: "workspace-name", diff --git a/coderd/provisionerdaemons.go b/coderd/provisionerdaemons.go index df7e4339ed6ab..96d21e661b206 100644 --- a/coderd/provisionerdaemons.go +++ b/coderd/provisionerdaemons.go @@ -832,12 +832,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, - Slug: slug, - Name: app.DisplayName, - 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 dbc34d9cbc48c..c1a4afaab2a54 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -596,7 +596,7 @@ func convertApps(dbApps []database.WorkspaceApp) []codersdk.WorkspaceApp { apps = append(apps, codersdk.WorkspaceApp{ ID: dbApp.ID, Slug: dbApp.Slug, - Name: dbApp.Name, + DisplayName: dbApp.DisplayName, Command: dbApp.Command.String, Icon: dbApp.Icon, Subdomain: dbApp.Subdomain, @@ -864,7 +864,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/workspaceapps_test.go b/coderd/workspaceapps_test.go index 91df3305738d2..ffa3d9828aa3d 100644 --- a/coderd/workspaceapps_test.go +++ b/coderd/workspaceapps_test.go @@ -832,7 +832,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 736b223c2f5f7..c6f4343153628 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -1462,7 +1462,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.DisplayName, 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) @@ -1471,7 +1471,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.DisplayName, 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 6f1847a96e69f..3cee425ebfe03 100644 --- a/codersdk/workspaceapps.go +++ b/codersdk/workspaceapps.go @@ -23,11 +23,11 @@ const ( type WorkspaceApp struct { ID uuid.UUID `json:"id"` - // Slug is a unique identifier within the agent.. + // Slug is a unique identifier within the agent. Slug string `json:"slug"` - // Name is a friendly name for the app. - Name string `json:"name"` - Command string `json:"command,omitempty"` + // 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/provisioner/terraform/testdata/invalid-app-slug/invalid-app-slug.tf b/provisioner/terraform/testdata/invalid-app-slug/invalid-app-slug.tf index 9febc994854b8..96d53de937a96 100644 --- a/provisioner/terraform/testdata/invalid-app-slug/invalid-app-slug.tf +++ b/provisioner/terraform/testdata/invalid-app-slug/invalid-app-slug.tf @@ -1,9 +1,7 @@ terraform { required_providers { coder = { - source = "coder/coder" - # future versions of coder/coder have built-in regex testing for valid - # app names, so we can't use a version after this. + source = "coder/coder" version = "0.5.3" } } diff --git a/provisionersdk/proto/provisioner.pb.go b/provisionersdk/proto/provisioner.pb.go index b26ab11d1171f..03028a74e1265 100644 --- a/provisionersdk/proto/provisioner.pb.go +++ b/provisionersdk/proto/provisioner.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.26.0 -// protoc v3.21.5 +// protoc v3.6.1 // source: provisionersdk/proto/provisioner.proto package proto diff --git a/provisionersdk/proto/provisioner.proto b/provisionersdk/proto/provisioner.proto index e3f59ba6bf6c5..aa0c14a38a80f 100644 --- a/provisionersdk/proto/provisioner.proto +++ b/provisionersdk/proto/provisioner.proto @@ -88,23 +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 { - // 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; + // 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; + AppSharingLevel sharing_level = 8; } // Healthcheck represents configuration for checking for app readiness. @@ -128,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 800be58e3fd71..606dd78bbf7ff 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -787,7 +787,7 @@ export interface WorkspaceAgentResourceMetadata { export interface WorkspaceApp { readonly id: string readonly slug: string - readonly name: string + readonly display_name: string readonly command?: string readonly icon?: string readonly subdomain: boolean diff --git a/site/src/components/AppLink/AppLink.stories.tsx b/site/src/components/AppLink/AppLink.stories.tsx index 454f97b5bc6c3..b772c669b80ef 100644 --- a/site/src/components/AppLink/AppLink.stories.tsx +++ b/site/src/components/AppLink/AppLink.stories.tsx @@ -14,7 +14,7 @@ WithIcon.args = { username: "developer", workspaceName: MockWorkspace.name, appSlug: "code-server", - appName: "Code Server", + appDisplayName: "code-server", appIcon: "/icon/code.svg", appSharingLevel: "owner", health: "healthy", @@ -25,7 +25,7 @@ WithoutIcon.args = { username: "developer", workspaceName: MockWorkspace.name, appSlug: "code-server", - appName: "Code Server", + appDisplayName: "code-server", appSharingLevel: "owner", health: "healthy", } @@ -35,7 +35,7 @@ HealthDisabled.args = { username: "developer", workspaceName: MockWorkspace.name, appSlug: "code-server", - appName: "Code Server", + appDisplayName: "code-server", appSharingLevel: "owner", health: "disabled", } @@ -45,7 +45,7 @@ HealthInitializing.args = { username: "developer", workspaceName: MockWorkspace.name, appSlug: "code-server", - appName: "Code Server", + appDisplayName: "code-server", health: "initializing", } @@ -54,6 +54,6 @@ HealthUnhealthy.args = { username: "developer", workspaceName: MockWorkspace.name, appSlug: "code-server", - appName: "Code Server", + appDisplayName: "code-server", health: "unhealthy", } diff --git a/site/src/components/AppLink/AppLink.tsx b/site/src/components/AppLink/AppLink.tsx index 378fad3d110a8..f7121377cce89 100644 --- a/site/src/components/AppLink/AppLink.tsx +++ b/site/src/components/AppLink/AppLink.tsx @@ -23,7 +23,7 @@ export interface AppLinkProps { workspaceName: TypesGen.Workspace["name"] agentName: TypesGen.WorkspaceAgent["name"] appSlug: TypesGen.WorkspaceApp["slug"] - appName: TypesGen.WorkspaceApp["name"] + appDisplayName: TypesGen.WorkspaceApp["name"] appIcon?: TypesGen.WorkspaceApp["icon"] appCommand?: TypesGen.WorkspaceApp["command"] appSubdomain: TypesGen.WorkspaceApp["subdomain"] @@ -37,7 +37,7 @@ export const AppLink: FC> = ({ workspaceName, agentName, appSlug, - appName, + appDisplayName, appIcon, appCommand, appSubdomain, @@ -46,10 +46,10 @@ export const AppLink: FC> = ({ }) => { const styles = useStyles() if (appSlug === "") { - appSlug = appName + appSlug = appDisplayName } - if (appName === "") { - appName = appSlug + if (appDisplayName === "") { + appDisplayName = appSlug } // The backend redirects if the trailing slash isn't included, so we add it @@ -69,7 +69,7 @@ export const AppLink: FC> = ({ let canClick = true let icon = appIcon ? ( - {`${appName} + {`${appDisplayName} ) : ( ) @@ -111,7 +111,7 @@ export const AppLink: FC> = ({ className={styles.button} disabled={!canClick} > - {appName} + {appDisplayName} ) @@ -128,7 +128,7 @@ export const AppLink: FC> = ({ event.preventDefault() window.open( href, - Language.appTitle(appName, generateRandomString(12)), + Language.appTitle(appDisplayName, generateRandomString(12)), "width=900,height=600", ) } diff --git a/site/src/components/Resources/Resources.tsx b/site/src/components/Resources/Resources.tsx index 48e7948986aea..88b5f3de49926 100644 --- a/site/src/components/Resources/Resources.tsx +++ b/site/src/components/Resources/Resources.tsx @@ -197,11 +197,11 @@ export const Resources: FC> = ({ /> {agent.apps.map((app) => ( Date: Wed, 19 Oct 2022 23:23:43 +0000 Subject: [PATCH 06/14] fixup! Merge branch 'main' into dean/app-urls-use-slug --- docs/ides/web-ides.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ides/web-ides.md b/docs/ides/web-ides.md index 9c4907c5834b1..2efa6d9a898b7 100644 --- a/docs/ides/web-ides.md +++ b/docs/ides/web-ides.md @@ -240,7 +240,7 @@ EOF resource "coder_app" "jupyter" { agent_id = coder_agent.coder.id slug = "jupyter" - displaY_name = "JupyterLab" + display_name = "JupyterLab" url = "http://localhost:8888${local.jupyter_base_path}" icon = "/icon/jupyter.svg" From 4efc3891805528c93ede2e6202c3edc77180549b Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Fri, 21 Oct 2022 23:26:14 +0000 Subject: [PATCH 07/14] chore: upgrade coder tf provider to 0.6.0 --- dogfood/main.tf | 2 +- examples/templates/aws-ecs-container/main.tf | 2 +- examples/templates/aws-linux/main.tf | 2 +- examples/templates/aws-windows/main.tf | 2 +- examples/templates/azure-linux/main.tf | 2 +- examples/templates/do-linux/main.tf | 2 +- examples/templates/docker-code-server/main.tf | 2 +- examples/templates/docker-image-builds/main.tf | 2 +- examples/templates/docker-with-dotfiles/main.tf | 2 +- examples/templates/docker/main.tf | 2 +- examples/templates/gcp-linux/main.tf | 2 +- examples/templates/gcp-vm-container/main.tf | 2 +- examples/templates/gcp-windows/main.tf | 2 +- examples/templates/kubernetes/main.tf | 2 +- provisioner/terraform/testdata/calling-module/calling-module.tf | 2 +- .../terraform/testdata/chaining-resources/chaining-resources.tf | 2 +- .../testdata/conflicting-resources/conflicting-resources.tf | 2 +- provisioner/terraform/testdata/instance-id/instance-id.tf | 2 +- .../terraform/testdata/invalid-app-slug/invalid-app-slug.tf | 2 +- .../terraform/testdata/multiple-agents/multiple-agents.tf | 2 +- provisioner/terraform/testdata/multiple-apps/multiple-apps.tf | 2 +- .../terraform/testdata/resource-metadata/resource-metadata.tf | 2 +- 22 files changed, 22 insertions(+), 22 deletions(-) diff --git a/dogfood/main.tf b/dogfood/main.tf index 6143e5666aa32..0e8814f558c7c 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" diff --git a/examples/templates/aws-ecs-container/main.tf b/examples/templates/aws-ecs-container/main.tf index b7c41d45873f5..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" } } } diff --git a/examples/templates/aws-linux/main.tf b/examples/templates/aws-linux/main.tf index 420c4babed28b..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" } } } 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 c36328fd2bef7..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" diff --git a/examples/templates/docker-image-builds/main.tf b/examples/templates/docker-image-builds/main.tf index 2e7bbfd4cfd91..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" 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 4f22622f319ce..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" diff --git a/examples/templates/gcp-linux/main.tf b/examples/templates/gcp-linux/main.tf index 92ad5bbb240eb..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" diff --git a/examples/templates/gcp-vm-container/main.tf b/examples/templates/gcp-vm-container/main.tf index 18814b1036508..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" 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 43e61818603e0..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" 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/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/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/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/invalid-app-slug/invalid-app-slug.tf b/provisioner/terraform/testdata/invalid-app-slug/invalid-app-slug.tf index 96d53de937a96..98015875e536a 100644 --- a/provisioner/terraform/testdata/invalid-app-slug/invalid-app-slug.tf +++ b/provisioner/terraform/testdata/invalid-app-slug/invalid-app-slug.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.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-apps/multiple-apps.tf b/provisioner/terraform/testdata/multiple-apps/multiple-apps.tf index f73630c056181..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" } } } 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" } } } From fecef1e42a593b9992b9169816602be4b9d744b9 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Fri, 21 Oct 2022 23:56:59 +0000 Subject: [PATCH 08/14] chore: add terraform provider test for slug validity --- provisioner/terraform/resources.go | 6 + provisioner/terraform/resources_test.go | 354 +++++++++--------- .../calling-module/calling-module.tfplan.json | 31 +- .../calling-module.tfstate.json | 8 +- .../chaining-resources.tfplan.json | 2 +- .../chaining-resources.tfstate.json | 8 +- .../conflicting-resources.tfplan.json | 2 +- .../conflicting-resources.tfstate.json | 8 +- .../instance-id/instance-id.tfplan.json | 2 +- .../instance-id/instance-id.tfstate.json | 10 +- .../invalid-app-slug/invalid-app-slug.tf | 24 -- .../invalid-app-slug.tfplan.dot | 20 - .../invalid-app-slug.tfplan.json | 213 ----------- .../invalid-app-slug.tfstate.dot | 20 - .../invalid-app-slug.tfstate.json | 68 ---- .../multiple-agents.tfplan.json | 2 +- .../multiple-agents.tfstate.json | 14 +- .../multiple-apps/multiple-apps.tfplan.json | 31 +- .../multiple-apps/multiple-apps.tfstate.json | 27 +- .../resource-metadata.tfplan.json | 2 +- .../resource-metadata.tfstate.json | 10 +- 21 files changed, 294 insertions(+), 568 deletions(-) delete mode 100644 provisioner/terraform/testdata/invalid-app-slug/invalid-app-slug.tf delete mode 100644 provisioner/terraform/testdata/invalid-app-slug/invalid-app-slug.tfplan.dot delete mode 100644 provisioner/terraform/testdata/invalid-app-slug/invalid-app-slug.tfplan.json delete mode 100644 provisioner/terraform/testdata/invalid-app-slug/invalid-app-slug.tfstate.dot delete mode 100644 provisioner/terraform/testdata/invalid-app-slug/invalid-app-slug.tfstate.json diff --git a/provisioner/terraform/resources.go b/provisioner/terraform/resources.go index c8038d5a154c2..db6a23d06ede6 100644 --- a/provisioner/terraform/resources.go +++ b/provisioner/terraform/resources.go @@ -220,6 +220,7 @@ 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 @@ -248,6 +249,11 @@ func ConvertResources(module *tfjson.StateModule, rawGraph string) ([]*proto.Res 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{ diff --git a/provisioner/terraform/resources_test.go b/provisioner/terraform/resources_test.go index c3d7536993ce5..483fe8e4b13f5 100644 --- a/provisioner/terraform/resources_test.go +++ b/provisioner/terraform/resources_test.go @@ -22,178 +22,144 @@ func TestConvertResources(t *testing.T) { t.Parallel() // nolint:dogsled _, filename, _, _ := runtime.Caller(0) - - cases := []struct { - // name must correspond to ./testadata//.* - name string - expected []*proto.Resource - errorContains string - }{ + // nolint:paralleltest + for folderName, expected := range map[string][]*proto.Resource{ // When a resource depends on another, the shortest route // to a resource should always be chosen for the agent. - { - name: "chaining-resources", - expected: []*proto.Resource{{ - Name: "a", - Type: "null_resource", - }, { - Name: "b", - Type: "null_resource", - Agents: []*proto.Agent{{ - Name: "main", - OperatingSystem: "linux", - Architecture: "amd64", - Auth: &proto.Agent_Token{}, - }}, + "chaining-resources": {{ + Name: "a", + Type: "null_resource", + }, { + Name: "b", + Type: "null_resource", + Agents: []*proto.Agent{{ + Name: "main", + OperatingSystem: "linux", + Architecture: "amd64", + Auth: &proto.Agent_Token{}, }}, - }, + }}, // This can happen when resources hierarchically conflict. // When multiple resources exist at the same level, the first // listed in state will be chosen. - { - name: "conflicting-resources", - expected: []*proto.Resource{{ - Name: "first", - Type: "null_resource", - Agents: []*proto.Agent{{ - Name: "main", - OperatingSystem: "linux", - Architecture: "amd64", - Auth: &proto.Agent_Token{}, - }}, - }, { - Name: "second", - Type: "null_resource", + "conflicting-resources": {{ + Name: "first", + Type: "null_resource", + Agents: []*proto.Agent{{ + Name: "main", + OperatingSystem: "linux", + Architecture: "amd64", + Auth: &proto.Agent_Token{}, }}, - }, + }, { + Name: "second", + Type: "null_resource", + }}, // Ensures the instance ID authentication type surfaces. - { - name: "instance-id", - expected: []*proto.Resource{{ - Name: "main", - Type: "null_resource", - Agents: []*proto.Agent{{ - Name: "main", - OperatingSystem: "linux", - Architecture: "amd64", - Auth: &proto.Agent_InstanceId{}, - }}, + "instance-id": {{ + Name: "main", + Type: "null_resource", + Agents: []*proto.Agent{{ + Name: "main", + OperatingSystem: "linux", + Architecture: "amd64", + Auth: &proto.Agent_InstanceId{}, }}, - }, - { - name: "calling-module", - expected: []*proto.Resource{{ - Name: "example", - Type: "null_resource", - Agents: []*proto.Agent{{ - Name: "main", - OperatingSystem: "linux", - Architecture: "amd64", - Auth: &proto.Agent_Token{}, - }}, + }}, + // Ensures that calls to resources through modules work + // as expected. + "calling-module": {{ + Name: "example", + Type: "null_resource", + Agents: []*proto.Agent{{ + Name: "main", + OperatingSystem: "linux", + Architecture: "amd64", + Auth: &proto.Agent_Token{}, }}, - }, + }}, // Ensures the attachment of multiple agents to a single // resource is successful. - { - name: "multiple-agents", - expected: []*proto.Resource{{ - Name: "dev", - Type: "null_resource", - Agents: []*proto.Agent{{ - Name: "dev1", - OperatingSystem: "linux", - Architecture: "amd64", - Auth: &proto.Agent_Token{}, - }, { - Name: "dev2", - OperatingSystem: "darwin", - Architecture: "amd64", - Auth: &proto.Agent_Token{}, - }, { - Name: "dev3", - OperatingSystem: "windows", - Architecture: "arm64", - Auth: &proto.Agent_Token{}, - }}, + "multiple-agents": {{ + Name: "dev", + Type: "null_resource", + Agents: []*proto.Agent{{ + Name: "dev1", + OperatingSystem: "linux", + Architecture: "amd64", + Auth: &proto.Agent_Token{}, + }, { + Name: "dev2", + OperatingSystem: "darwin", + Architecture: "amd64", + Auth: &proto.Agent_Token{}, + }, { + Name: "dev3", + OperatingSystem: "windows", + Architecture: "arm64", + Auth: &proto.Agent_Token{}, }}, - }, + }}, // Ensures multiple applications can be set for a single agent. - { - name: "multiple-apps", - expected: []*proto.Resource{{ - Name: "dev", - Type: "null_resource", - Agents: []*proto.Agent{{ - Name: "dev1", - OperatingSystem: "linux", - Architecture: "amd64", - Apps: []*proto.App{ - { - Slug: "app1", - DisplayName: "app1", - // Subdomain defaults to false if unspecified. - Subdomain: false, - }, - { - Slug: "app2", - DisplayName: "app2", - Subdomain: true, - Healthcheck: &proto.Healthcheck{ - Url: "http://localhost:13337/healthz", - Interval: 5, - Threshold: 6, - }, - }, - { - Slug: "app3", - DisplayName: "app3", - Subdomain: false, + "multiple-apps": {{ + Name: "dev", + Type: "null_resource", + Agents: []*proto.Agent{{ + Name: "dev1", + OperatingSystem: "linux", + Architecture: "amd64", + Apps: []*proto.App{ + { + Slug: "app1", + DisplayName: "app1", + // Subdomain defaults to false if unspecified. + Subdomain: false, + }, + { + Slug: "app2", + DisplayName: "app2", + Subdomain: true, + Healthcheck: &proto.Healthcheck{ + Url: "http://localhost:13337/healthz", + Interval: 5, + Threshold: 6, }, }, - Auth: &proto.Agent_Token{}, - }}, + { + Slug: "app3", + DisplayName: "app3", + Subdomain: false, + }, + }, + Auth: &proto.Agent_Token{}, }}, - }, + }}, // Tests fetching metadata about workspace resources. - { - name: "resource-metadata", - expected: []*proto.Resource{{ - Name: "about", - Type: "null_resource", - Hide: true, - Icon: "/icon/server.svg", - Metadata: []*proto.Resource_Metadata{{ - Key: "hello", - Value: "world", - }, { - Key: "null", - IsNull: true, - }, { - Key: "empty", - }, { - Key: "secret", - Value: "squirrel", - Sensitive: true, - }}, + "resource-metadata": {{ + Name: "about", + Type: "null_resource", + Hide: true, + Icon: "/icon/server.svg", + Metadata: []*proto.Resource_Metadata{{ + Key: "hello", + Value: "world", + }, { + Key: "null", + IsNull: true, + }, { + Key: "empty", + }, { + Key: "secret", + Value: "squirrel", + Sensitive: true, }}, - }, - // Ensure that invalid app slugs fail. - { - name: "invalid-app-slug", - expected: nil, - errorContains: "invalid app slug", - }, - } - - for _, c := range cases { - c := c - t.Run(c.name, func(t *testing.T) { + }}, + } { + folderName := folderName + expected := expected + t.Run(folderName, func(t *testing.T) { t.Parallel() - - folderName, expected := c.name, c.expected dir := filepath.Join(filepath.Dir(filename), "testdata", folderName) - t.Run("Plan", func(t *testing.T) { t.Parallel() @@ -206,11 +172,6 @@ func TestConvertResources(t *testing.T) { require.NoError(t, err) resources, err := terraform.ConvertResources(tfPlan.PlannedValues.RootModule, string(tfPlanGraph)) - if c.errorContains != "" { - require.Error(t, err) - require.ErrorContains(t, err, c.errorContains) - return - } require.NoError(t, err) sortResources(resources) @@ -224,16 +185,25 @@ func TestConvertResources(t *testing.T) { expectedNoMetadata = append(expectedNoMetadata, resourceCopy) } - resourcesWant, err := json.Marshal(expectedNoMetadata) + // Convert expectedNoMetadata and resources into a + // []map[string]interface{} so they can be compared easily. + data, err := json.Marshal(expectedNoMetadata) require.NoError(t, err) - resourcesGot, err := json.Marshal(resources) + var expectedNoMetadataMap []map[string]interface{} + err = json.Unmarshal(data, &expectedNoMetadataMap) + require.NoError(t, err) + + data, err = json.Marshal(resources) require.NoError(t, err) - require.Equal(t, string(resourcesWant), string(resourcesGot)) + var resourcesMap []map[string]interface{} + err = json.Unmarshal(data, &resourcesMap) + require.NoError(t, err) + + require.Equal(t, expectedNoMetadataMap, resourcesMap) }) t.Run("Provision", func(t *testing.T) { t.Parallel() - tfStateRaw, err := os.ReadFile(filepath.Join(dir, folderName+".tfstate.json")) require.NoError(t, err) var tfState tfjson.State @@ -243,14 +213,8 @@ func TestConvertResources(t *testing.T) { require.NoError(t, err) resources, err := terraform.ConvertResources(tfState.Values.RootModule, string(tfStateGraph)) - if c.errorContains != "" { - require.Error(t, err) - require.ErrorContains(t, err, c.errorContains) - return - } require.NoError(t, err) sortResources(resources) - for _, resource := range resources { for _, agent := range resource.Agents { agent.Id = "" @@ -262,17 +226,67 @@ func TestConvertResources(t *testing.T) { } } } + // Convert expectedNoMetadata and resources into a + // []map[string]interface{} so they can be compared easily. + data, err := json.Marshal(expected) + require.NoError(t, err) + var expectedMap []map[string]interface{} + err = json.Unmarshal(data, &expectedMap) + require.NoError(t, err) - resourcesWant, err := json.Marshal(expected) + 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, 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 { @@ -354,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].DisplayName < agent.Apps[j].DisplayName + 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.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.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.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.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/invalid-app-slug/invalid-app-slug.tf b/provisioner/terraform/testdata/invalid-app-slug/invalid-app-slug.tf deleted file mode 100644 index 98015875e536a..0000000000000 --- a/provisioner/terraform/testdata/invalid-app-slug/invalid-app-slug.tf +++ /dev/null @@ -1,24 +0,0 @@ -terraform { - required_providers { - coder = { - source = "coder/coder" - version = "0.6.0" - } - } -} - -resource "coder_agent" "dev" { - os = "linux" - arch = "amd64" -} - -resource "null_resource" "dev" { - depends_on = [ - coder_agent.dev - ] -} - -resource "coder_app" "invalid-app-slug" { - agent_id = coder_agent.dev.id - slug = "$$$" -} diff --git a/provisioner/terraform/testdata/invalid-app-slug/invalid-app-slug.tfplan.dot b/provisioner/terraform/testdata/invalid-app-slug/invalid-app-slug.tfplan.dot deleted file mode 100644 index d69316f58749c..0000000000000 --- a/provisioner/terraform/testdata/invalid-app-slug/invalid-app-slug.tfplan.dot +++ /dev/null @@ -1,20 +0,0 @@ -digraph { - compound = "true" - newrank = "true" - subgraph "root" { - "[root] coder_agent.dev (expand)" [label = "coder_agent.dev", shape = "box"] - "[root] coder_app.invalid_app_name (expand)" [label = "coder_app.invalid_app_name", shape = "box"] - "[root] null_resource.dev (expand)" [label = "null_resource.dev", shape = "box"] - "[root] provider[\"registry.terraform.io/coder/coder\"]" [label = "provider[\"registry.terraform.io/coder/coder\"]", shape = "diamond"] - "[root] provider[\"registry.terraform.io/hashicorp/null\"]" [label = "provider[\"registry.terraform.io/hashicorp/null\"]", shape = "diamond"] - "[root] coder_agent.dev (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]" - "[root] coder_app.invalid_app_name (expand)" -> "[root] coder_agent.dev (expand)" - "[root] null_resource.dev (expand)" -> "[root] coder_agent.dev (expand)" - "[root] null_resource.dev (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/null\"]" - "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] coder_app.invalid_app_name (expand)" - "[root] provider[\"registry.terraform.io/hashicorp/null\"] (close)" -> "[root] null_resource.dev (expand)" - "[root] root" -> "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" - "[root] root" -> "[root] provider[\"registry.terraform.io/hashicorp/null\"] (close)" - } -} - diff --git a/provisioner/terraform/testdata/invalid-app-slug/invalid-app-slug.tfplan.json b/provisioner/terraform/testdata/invalid-app-slug/invalid-app-slug.tfplan.json deleted file mode 100644 index 7711af0e642d0..0000000000000 --- a/provisioner/terraform/testdata/invalid-app-slug/invalid-app-slug.tfplan.json +++ /dev/null @@ -1,213 +0,0 @@ -{ - "format_version": "1.1", - "terraform_version": "1.2.7", - "planned_values": { - "root_module": { - "resources": [ - { - "address": "coder_agent.dev", - "mode": "managed", - "type": "coder_agent", - "name": "dev", - "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, - "values": { - "arch": "amd64", - "auth": "token", - "dir": null, - "env": null, - "os": "linux", - "startup_script": null - }, - "sensitive_values": {} - }, - { - "address": "coder_app.invalid_app_name", - "mode": "managed", - "type": "coder_app", - "name": "invalid_app_name", - "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, - "values": { - "command": null, - "healthcheck": [], - "icon": null, - "name": null, - "relative_path": null, - "share": "owner", - "subdomain": null, - "url": null - }, - "sensitive_values": { - "healthcheck": [] - } - }, - { - "address": "null_resource.dev", - "mode": "managed", - "type": "null_resource", - "name": "dev", - "provider_name": "registry.terraform.io/hashicorp/null", - "schema_version": 0, - "values": { - "triggers": null - }, - "sensitive_values": {} - } - ] - } - }, - "resource_changes": [ - { - "address": "coder_agent.dev", - "mode": "managed", - "type": "coder_agent", - "name": "dev", - "provider_name": "registry.terraform.io/coder/coder", - "change": { - "actions": [ - "create" - ], - "before": null, - "after": { - "arch": "amd64", - "auth": "token", - "dir": null, - "env": null, - "os": "linux", - "startup_script": null - }, - "after_unknown": { - "id": true, - "init_script": true, - "token": true - }, - "before_sensitive": false, - "after_sensitive": { - "token": true - } - } - }, - { - "address": "coder_app.invalid_app_name", - "mode": "managed", - "type": "coder_app", - "name": "invalid_app_name", - "provider_name": "registry.terraform.io/coder/coder", - "change": { - "actions": [ - "create" - ], - "before": null, - "after": { - "command": null, - "healthcheck": [], - "icon": null, - "name": null, - "relative_path": null, - "share": "owner", - "subdomain": null, - "url": null - }, - "after_unknown": { - "agent_id": true, - "healthcheck": [], - "id": true - }, - "before_sensitive": false, - "after_sensitive": { - "healthcheck": [] - } - } - }, - { - "address": "null_resource.dev", - "mode": "managed", - "type": "null_resource", - "name": "dev", - "provider_name": "registry.terraform.io/hashicorp/null", - "change": { - "actions": [ - "create" - ], - "before": null, - "after": { - "triggers": null - }, - "after_unknown": { - "id": true - }, - "before_sensitive": false, - "after_sensitive": {} - } - } - ], - "configuration": { - "provider_config": { - "coder": { - "name": "coder", - "full_name": "registry.terraform.io/coder/coder", - "version_constraint": "0.5.3" - }, - "null": { - "name": "null", - "full_name": "registry.terraform.io/hashicorp/null" - } - }, - "root_module": { - "resources": [ - { - "address": "coder_agent.dev", - "mode": "managed", - "type": "coder_agent", - "name": "dev", - "provider_config_key": "coder", - "expressions": { - "arch": { - "constant_value": "amd64" - }, - "os": { - "constant_value": "linux" - } - }, - "schema_version": 0 - }, - { - "address": "coder_app.invalid_app_name", - "mode": "managed", - "type": "coder_app", - "name": "invalid_app_name", - "provider_config_key": "coder", - "expressions": { - "agent_id": { - "references": [ - "coder_agent.dev.id", - "coder_agent.dev" - ] - } - }, - "schema_version": 0 - }, - { - "address": "null_resource.dev", - "mode": "managed", - "type": "null_resource", - "name": "dev", - "provider_config_key": "null", - "schema_version": 0, - "depends_on": [ - "coder_agent.dev" - ] - } - ] - } - }, - "relevant_attributes": [ - { - "resource": "coder_agent.dev", - "attribute": [ - "id" - ] - } - ] -} diff --git a/provisioner/terraform/testdata/invalid-app-slug/invalid-app-slug.tfstate.dot b/provisioner/terraform/testdata/invalid-app-slug/invalid-app-slug.tfstate.dot deleted file mode 100644 index d69316f58749c..0000000000000 --- a/provisioner/terraform/testdata/invalid-app-slug/invalid-app-slug.tfstate.dot +++ /dev/null @@ -1,20 +0,0 @@ -digraph { - compound = "true" - newrank = "true" - subgraph "root" { - "[root] coder_agent.dev (expand)" [label = "coder_agent.dev", shape = "box"] - "[root] coder_app.invalid_app_name (expand)" [label = "coder_app.invalid_app_name", shape = "box"] - "[root] null_resource.dev (expand)" [label = "null_resource.dev", shape = "box"] - "[root] provider[\"registry.terraform.io/coder/coder\"]" [label = "provider[\"registry.terraform.io/coder/coder\"]", shape = "diamond"] - "[root] provider[\"registry.terraform.io/hashicorp/null\"]" [label = "provider[\"registry.terraform.io/hashicorp/null\"]", shape = "diamond"] - "[root] coder_agent.dev (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]" - "[root] coder_app.invalid_app_name (expand)" -> "[root] coder_agent.dev (expand)" - "[root] null_resource.dev (expand)" -> "[root] coder_agent.dev (expand)" - "[root] null_resource.dev (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/null\"]" - "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] coder_app.invalid_app_name (expand)" - "[root] provider[\"registry.terraform.io/hashicorp/null\"] (close)" -> "[root] null_resource.dev (expand)" - "[root] root" -> "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" - "[root] root" -> "[root] provider[\"registry.terraform.io/hashicorp/null\"] (close)" - } -} - diff --git a/provisioner/terraform/testdata/invalid-app-slug/invalid-app-slug.tfstate.json b/provisioner/terraform/testdata/invalid-app-slug/invalid-app-slug.tfstate.json deleted file mode 100644 index 17f51fef79675..0000000000000 --- a/provisioner/terraform/testdata/invalid-app-slug/invalid-app-slug.tfstate.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "format_version": "1.0", - "terraform_version": "1.2.8", - "values": { - "root_module": { - "resources": [ - { - "address": "coder_agent.dev", - "mode": "managed", - "type": "coder_agent", - "name": "dev", - "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, - "values": { - "arch": "amd64", - "auth": "token", - "dir": null, - "env": null, - "id": "4df45b21-c841-46d3-b3e3-d1efd3a9657e", - "init_script": "", - "os": "linux", - "startup_script": null, - "token": "4bddad38-b622-4963-b353-249171359be8" - }, - "sensitive_values": {} - }, - { - "address": "coder_app.invalid_app_name", - "mode": "managed", - "type": "coder_app", - "name": "invalid_app_name", - "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, - "values": { - "agent_id": "4df45b21-c841-46d3-b3e3-d1efd3a9657e", - "command": null, - "healthcheck": [], - "icon": null, - "id": "d2b5d94a-4c47-484e-862f-4eb700290dbb", - "name": null, - "relative_path": null, - "share": "owner", - "subdomain": null, - "url": null - }, - "sensitive_values": { - "healthcheck": [] - }, - "depends_on": ["coder_agent.dev"] - }, - { - "address": "null_resource.dev", - "mode": "managed", - "type": "null_resource", - "name": "dev", - "provider_name": "registry.terraform.io/hashicorp/null", - "schema_version": 0, - "values": { - "id": "6380390816158464544", - "triggers": null - }, - "sensitive_values": {}, - "depends_on": ["coder_agent.dev"] - } - ] - } - } -} 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.tfplan.json b/provisioner/terraform/testdata/multiple-apps/multiple-apps.tfplan.json index be1d57b1eb258..5c1e167f14813 100644 --- a/provisioner/terraform/testdata/multiple-apps/multiple-apps.tfplan.json +++ b/provisioner/terraform/testdata/multiple-apps/multiple-apps.tfplan.json @@ -1,6 +1,6 @@ { "format_version": "1.1", - "terraform_version": "1.2.7", + "terraform_version": "1.2.8", "planned_values": { "root_module": { "resources": [ @@ -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.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": {} From 1a5eabcd8edb66aa317337f5a2c734891cba08cb Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Sat, 22 Oct 2022 00:01:29 +0000 Subject: [PATCH 09/14] chore: add app slug uniqueness check to coderd --- coderd/provisionerdaemons.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/coderd/provisionerdaemons.go b/coderd/provisionerdaemons.go index 96d21e661b206..aa6c5c18cb89e 100644 --- a/coderd/provisionerdaemons.go +++ b/coderd/provisionerdaemons.go @@ -756,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() != "" { @@ -814,6 +815,10 @@ func insertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid. 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 { From 9d41a46442fa4e93ac7dc91edbbdcc3857785175 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Sat, 22 Oct 2022 00:41:00 +0000 Subject: [PATCH 10/14] chore: fix and improve app slug migrations --- coderd/database/dump.sql | 2 +- .../000062_app_display_name.down.sql | 2 -- .../migrations/000062_app_display_name.up.sql | 2 -- ...slug.down.sql => 000064_app_slug.down.sql} | 0 ...app_slug.up.sql => 000064_app_slug.up.sql} | 3 ++ .../000065_app_display_name.down.sql | 34 +++++++++++++++++++ .../migrations/000065_app_display_name.up.sql | 9 +++++ coderd/database/queries.sql.go | 8 ++--- coderd/database/unique_constraint.go | 2 +- 9 files changed, 52 insertions(+), 10 deletions(-) delete mode 100644 coderd/database/migrations/000062_app_display_name.down.sql delete mode 100644 coderd/database/migrations/000062_app_display_name.up.sql rename coderd/database/migrations/{000061_app_slug.down.sql => 000064_app_slug.down.sql} (100%) rename coderd/database/migrations/{000061_app_slug.up.sql => 000064_app_slug.up.sql} (74%) create mode 100644 coderd/database/migrations/000065_app_display_name.down.sql create mode 100644 coderd/database/migrations/000065_app_display_name.up.sql diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index b2637e5a9cf7a..e89e348e2681d 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -533,7 +533,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, display_name); + ADD CONSTRAINT workspace_apps_agent_id_slug_key UNIQUE (agent_id, slug); ALTER TABLE ONLY workspace_apps ADD CONSTRAINT workspace_apps_pkey PRIMARY KEY (id); diff --git a/coderd/database/migrations/000062_app_display_name.down.sql b/coderd/database/migrations/000062_app_display_name.down.sql deleted file mode 100644 index 21be778264303..0000000000000 --- a/coderd/database/migrations/000062_app_display_name.down.sql +++ /dev/null @@ -1,2 +0,0 @@ --- rename column "display_name" to "name" on "workspace_apps" -ALTER TABLE "workspace_apps" RENAME COLUMN "display_name" TO "name"; diff --git a/coderd/database/migrations/000062_app_display_name.up.sql b/coderd/database/migrations/000062_app_display_name.up.sql deleted file mode 100644 index 5a422777f2dfa..0000000000000 --- a/coderd/database/migrations/000062_app_display_name.up.sql +++ /dev/null @@ -1,2 +0,0 @@ --- rename column "name" to "display_name" on "workspace_apps" -ALTER TABLE "workspace_apps" RENAME COLUMN "name" TO "display_name"; diff --git a/coderd/database/migrations/000061_app_slug.down.sql b/coderd/database/migrations/000064_app_slug.down.sql similarity index 100% rename from coderd/database/migrations/000061_app_slug.down.sql rename to coderd/database/migrations/000064_app_slug.down.sql diff --git a/coderd/database/migrations/000061_app_slug.up.sql b/coderd/database/migrations/000064_app_slug.up.sql similarity index 74% rename from coderd/database/migrations/000061_app_slug.up.sql rename to coderd/database/migrations/000064_app_slug.up.sql index 6f604dae0a608..286b709de5862 100644 --- a/coderd/database/migrations/000061_app_slug.up.sql +++ b/coderd/database/migrations/000064_app_slug.up.sql @@ -10,4 +10,7 @@ UPDATE "workspace_apps" SET "slug" = "name"; 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_key" UNIQUE ("agent_id", "slug"); + COMMIT; diff --git a/coderd/database/migrations/000065_app_display_name.down.sql b/coderd/database/migrations/000065_app_display_name.down.sql new file mode 100644 index 0000000000000..1139140465c10 --- /dev/null +++ b/coderd/database/migrations/000065_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/000065_app_display_name.up.sql b/coderd/database/migrations/000065_app_display_name.up.sql new file mode 100644 index 0000000000000..8d210b35a71bc --- /dev/null +++ b/coderd/database/migrations/000065_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/queries.sql.go b/coderd/database/queries.sql.go index eda06f6ca9cee..8b97809daaced 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -866,8 +866,8 @@ func (q *sqlQuerier) UpdateGitSSHKey(ctx context.Context, arg UpdateGitSSHKeyPar } const deleteGroupByID = `-- name: DeleteGroupByID :exec -DELETE FROM - groups +DELETE FROM + groups WHERE id = $1 ` @@ -878,8 +878,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 ` diff --git a/coderd/database/unique_constraint.go b/coderd/database/unique_constraint.go index f1990113ce2c3..da8edf6dd145e 100644 --- a/coderd/database/unique_constraint.go +++ b/coderd/database/unique_constraint.go @@ -15,7 +15,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, display_name); + UniqueWorkspaceAppsAgentIDSlugKey UniqueConstraint = "workspace_apps_agent_id_slug_key" // ALTER TABLE ONLY workspace_apps ADD CONSTRAINT workspace_apps_agent_id_slug_key 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); From b53611f3c92295637ae949329d4dc9daf07feae6 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Tue, 25 Oct 2022 15:04:00 +0000 Subject: [PATCH 11/14] fixup! Merge branch 'main' into dean/app-urls-use-slug --- .../{000064_app_slug.down.sql => 000065_app_slug.down.sql} | 0 .../migrations/{000064_app_slug.up.sql => 000065_app_slug.up.sql} | 0 ...app_display_name.down.sql => 000066_app_display_name.down.sql} | 0 ...065_app_display_name.up.sql => 000066_app_display_name.up.sql} | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename coderd/database/migrations/{000064_app_slug.down.sql => 000065_app_slug.down.sql} (100%) rename coderd/database/migrations/{000064_app_slug.up.sql => 000065_app_slug.up.sql} (100%) rename coderd/database/migrations/{000065_app_display_name.down.sql => 000066_app_display_name.down.sql} (100%) rename coderd/database/migrations/{000065_app_display_name.up.sql => 000066_app_display_name.up.sql} (100%) diff --git a/coderd/database/migrations/000064_app_slug.down.sql b/coderd/database/migrations/000065_app_slug.down.sql similarity index 100% rename from coderd/database/migrations/000064_app_slug.down.sql rename to coderd/database/migrations/000065_app_slug.down.sql diff --git a/coderd/database/migrations/000064_app_slug.up.sql b/coderd/database/migrations/000065_app_slug.up.sql similarity index 100% rename from coderd/database/migrations/000064_app_slug.up.sql rename to coderd/database/migrations/000065_app_slug.up.sql diff --git a/coderd/database/migrations/000065_app_display_name.down.sql b/coderd/database/migrations/000066_app_display_name.down.sql similarity index 100% rename from coderd/database/migrations/000065_app_display_name.down.sql rename to coderd/database/migrations/000066_app_display_name.down.sql diff --git a/coderd/database/migrations/000065_app_display_name.up.sql b/coderd/database/migrations/000066_app_display_name.up.sql similarity index 100% rename from coderd/database/migrations/000065_app_display_name.up.sql rename to coderd/database/migrations/000066_app_display_name.up.sql From 3e07cf1d71ec0c4321d7b740a2a77e75994bd367 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Fri, 28 Oct 2022 16:41:17 +0000 Subject: [PATCH 12/14] fixup! Merge branch 'main' into dean/app-urls-use-slug --- coderd/database/migrations/000065_app_slug.down.sql | 2 -- coderd/database/migrations/000066_app_slug.down.sql | 5 +++++ .../{000065_app_slug.up.sql => 000066_app_slug.up.sql} | 2 +- ...isplay_name.down.sql => 000067_app_display_name.down.sql} | 2 +- ...pp_display_name.up.sql => 000067_app_display_name.up.sql} | 0 site/src/components/Resources/AgentRow.tsx | 2 +- 6 files changed, 8 insertions(+), 5 deletions(-) delete mode 100644 coderd/database/migrations/000065_app_slug.down.sql create mode 100644 coderd/database/migrations/000066_app_slug.down.sql rename coderd/database/migrations/{000065_app_slug.up.sql => 000066_app_slug.up.sql} (92%) rename coderd/database/migrations/{000066_app_display_name.down.sql => 000067_app_display_name.down.sql} (93%) rename coderd/database/migrations/{000066_app_display_name.up.sql => 000067_app_display_name.up.sql} (100%) diff --git a/coderd/database/migrations/000065_app_slug.down.sql b/coderd/database/migrations/000065_app_slug.down.sql deleted file mode 100644 index 2d409263cc5c7..0000000000000 --- a/coderd/database/migrations/000065_app_slug.down.sql +++ /dev/null @@ -1,2 +0,0 @@ --- drop "slug" column from "workspace_apps" table -ALTER TABLE "workspace_apps" DROP COLUMN "slug"; 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/000065_app_slug.up.sql b/coderd/database/migrations/000066_app_slug.up.sql similarity index 92% rename from coderd/database/migrations/000065_app_slug.up.sql rename to coderd/database/migrations/000066_app_slug.up.sql index 286b709de5862..6f67451f2796e 100644 --- a/coderd/database/migrations/000065_app_slug.up.sql +++ b/coderd/database/migrations/000066_app_slug.up.sql @@ -11,6 +11,6 @@ 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_key" UNIQUE ("agent_id", "slug"); +ALTER TABLE "workspace_apps" ADD CONSTRAINT "workspace_apps_agent_id_slug_idx" UNIQUE ("agent_id", "slug"); COMMIT; diff --git a/coderd/database/migrations/000066_app_display_name.down.sql b/coderd/database/migrations/000067_app_display_name.down.sql similarity index 93% rename from coderd/database/migrations/000066_app_display_name.down.sql rename to coderd/database/migrations/000067_app_display_name.down.sql index 1139140465c10..1b6fe06a0e25b 100644 --- a/coderd/database/migrations/000066_app_display_name.down.sql +++ b/coderd/database/migrations/000067_app_display_name.down.sql @@ -18,7 +18,7 @@ WITH row_numbers AS ( UPDATE workspace_apps SET - display_name = workspace_apps.display_name || floor(random() * 10000)::text + display_name = workspace_apps.display_name || floor(random() * 10000)::text FROM row_numbers WHERE diff --git a/coderd/database/migrations/000066_app_display_name.up.sql b/coderd/database/migrations/000067_app_display_name.up.sql similarity index 100% rename from coderd/database/migrations/000066_app_display_name.up.sql rename to coderd/database/migrations/000067_app_display_name.up.sql 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) => ( Date: Fri, 28 Oct 2022 17:23:13 +0000 Subject: [PATCH 13/14] fixup! Merge branch 'main' into dean/app-urls-use-slug --- cli/schedule_test.go | 12 ++++++++++-- coderd/database/dump.sql | 2 +- coderd/database/queries.sql.go | 8 ++++---- coderd/database/unique_constraint.go | 2 +- provisionersdk/proto/provisioner.pb.go | 2 +- site/src/components/AppLink/AppPreviewLink.tsx | 2 +- site/src/components/AppLink/BaseIcon.tsx | 2 +- site/src/components/Resources/AgentRowPreview.tsx | 2 +- 8 files changed, 20 insertions(+), 12 deletions(-) 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/database/dump.sql b/coderd/database/dump.sql index 4b64cb1186928..4953c9885fe65 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -549,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_slug_key UNIQUE (agent_id, slug); + 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/queries.sql.go b/coderd/database/queries.sql.go index f1e87ff44781a..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 ` diff --git a/coderd/database/unique_constraint.go b/coderd/database/unique_constraint.go index 0d3c06cdf1dd2..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); - UniqueWorkspaceAppsAgentIDSlugKey UniqueConstraint = "workspace_apps_agent_id_slug_key" // ALTER TABLE ONLY workspace_apps ADD CONSTRAINT workspace_apps_agent_id_slug_key UNIQUE (agent_id, slug); + 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/provisionersdk/proto/provisioner.pb.go b/provisionersdk/proto/provisioner.pb.go index 03028a74e1265..b26ab11d1171f 100644 --- a/provisionersdk/proto/provisioner.pb.go +++ b/provisionersdk/proto/provisioner.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.26.0 -// protoc v3.6.1 +// protoc v3.21.5 // source: provisionersdk/proto/provisioner.proto package proto 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/AgentRowPreview.tsx b/site/src/components/Resources/AgentRowPreview.tsx index 53e6ce0a139b4..54f5b2e3bb816 100644 --- a/site/src/components/Resources/AgentRowPreview.tsx +++ b/site/src/components/Resources/AgentRowPreview.tsx @@ -73,7 +73,7 @@ export const AgentRowPreview: FC = ({ agent }) => { wrap="wrap" > {agent.apps.map((app) => ( - + ))} From a423ce877b88ed774bc4750041f74ba5458f1ef8 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Fri, 28 Oct 2022 17:29:46 +0000 Subject: [PATCH 14/14] fixup! Merge branch 'main' into dean/app-urls-use-slug --- coderd/database/queries/groups.sql | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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;