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; pFad - Phonifier reborn

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

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


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy