diff --git a/.vscode/settings.json b/.vscode/settings.json index 1ba61d11c056a..396b551029288 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,8 @@ { "cSpell.words": [ + "apps", + "awsidentity", + "buildinfo", "buildname", "circbuf", "cliflag", @@ -16,6 +19,7 @@ "Dsts", "fatih", "Formik", + "gitsshkey", "goarch", "gographviz", "goleak", @@ -30,6 +34,7 @@ "incpatch", "isatty", "Jobf", + "Keygen", "kirsle", "ldflags", "manifoldco", @@ -54,6 +59,7 @@ "retrier", "rpty", "sdkproto", + "sdktrace", "Signup", "sourcemapped", "stretchr", @@ -66,13 +72,18 @@ "tfjson", "tfstate", "trimprefix", + "turnconn", "typegen", "unconvert", "Untar", "VMID", "weblinks", "webrtc", + "workspaceagent", + "workspaceapp", + "workspaceapps", "workspacebuilds", + "wsconncache", "xerrors", "xstate", "yamux" diff --git a/agent/conn.go b/agent/conn.go index d44d6d0c0b0d8..0be45bc05c33e 100644 --- a/agent/conn.go +++ b/agent/conn.go @@ -102,7 +102,7 @@ func (c *Conn) DialContext(ctx context.Context, network string, addr string) (ne var res dialResponse err = dec.Decode(&res) if err != nil { - return nil, xerrors.Errorf("failed to decode initial packet: %w", err) + return nil, xerrors.Errorf("decode agent dial response: %w", err) } if res.Error != "" { _ = channel.Close() diff --git a/coderd/coderd.go b/coderd/coderd.go index 74e41b828477c..0fe8a81e52c33 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -27,6 +27,7 @@ import ( "github.com/coder/coder/coderd/rbac" "github.com/coder/coder/coderd/tracing" "github.com/coder/coder/coderd/turnconn" + "github.com/coder/coder/coderd/wsconncache" "github.com/coder/coder/codersdk" "github.com/coder/coder/site" ) @@ -44,6 +45,7 @@ type Options struct { // app. Specific routes may have their own limiters. APIRateLimit int AWSCertificates awsidentity.Certificates + Authorizer rbac.Authorizer AzureCertificates x509.VerifyOptions GoogleTokenValidator *idtoken.Validator GithubOAuth2Config *GithubOAuth2Config @@ -51,7 +53,6 @@ type Options struct { SecureAuthCookie bool SSHKeygenAlgorithm gitsshkey.Algorithm TURNServer *turnconn.Server - Authorizer rbac.Authorizer TracerProvider *sdktrace.TracerProvider } @@ -75,9 +76,11 @@ func New(options *Options) *API { r := chi.NewRouter() api := &API{ - Options: options, - Handler: r, + Options: options, + Handler: r, + siteHandler: site.Handler(site.FS()), } + api.workspaceAgentCache = wsconncache.New(api.dialWorkspaceAgent, 0) apiKeyMiddleware := httpmw.ExtractAPIKey(options.Database, &httpmw.OAuth2Configs{ Github: options.GithubOAuth2Config, @@ -93,6 +96,20 @@ func New(options *Options) *API { tracing.HTTPMW(api.TracerProvider, "coderd.http"), ) + apps := func(r chi.Router) { + r.Use( + httpmw.RateLimitPerMinute(options.APIRateLimit), + apiKeyMiddleware, + httpmw.ExtractUserParam(api.Database), + ) + r.Get("/*", api.workspaceAppsProxyPath) + } + // %40 is the encoded character of the @ symbol. VS Code Web does + // not handle character encoding properly, so it's safe to assume + // other applications might not as well. + r.Route("/%40{user}/{workspacename}/apps/{workspaceapp}", apps) + r.Route("/@{user}/{workspacename}/apps/{workspaceapp}", apps) + r.Route("/api/v2", func(r chi.Router) { r.NotFound(func(rw http.ResponseWriter, r *http.Request) { httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ @@ -327,24 +344,27 @@ func New(options *Options) *API { r.Get("/state", api.workspaceBuildState) }) }) - r.NotFound(site.Handler(site.FS()).ServeHTTP) - + r.NotFound(api.siteHandler.ServeHTTP) return api } type API struct { *Options - Handler chi.Router - websocketWaitMutex sync.Mutex - websocketWaitGroup sync.WaitGroup + Handler chi.Router + siteHandler http.Handler + websocketWaitMutex sync.Mutex + websocketWaitGroup sync.WaitGroup + workspaceAgentCache *wsconncache.Cache } // Close waits for all WebSocket connections to drain before returning. -func (api *API) Close() { +func (api *API) Close() error { api.websocketWaitMutex.Lock() api.websocketWaitGroup.Wait() api.websocketWaitMutex.Unlock() + + return api.workspaceAgentCache.Close() } func debugLogRequest(log slog.Logger) func(http.Handler) http.Handler { diff --git a/coderd/coderd_test.go b/coderd/coderd_test.go index 86a0b79034590..dbf3a5cede234 100644 --- a/coderd/coderd_test.go +++ b/coderd/coderd_test.go @@ -74,6 +74,10 @@ func TestAuthorizeAllEndpoints(t *testing.T) { Agents: []*proto.Agent{{ Id: "something", Auth: &proto.Agent_Token{}, + Apps: []*proto.App{{ + Name: "app", + Url: "http://localhost:3000", + }}, }}, }}, }, @@ -128,6 +132,15 @@ func TestAuthorizeAllEndpoints(t *testing.T) { "GET:/api/v2/users/authmethods": {NoAuthorize: true}, "POST:/api/v2/csp/reports": {NoAuthorize: true}, + "GET:/%40{user}/{workspacename}/apps/{application}/*": { + AssertAction: rbac.ActionRead, + AssertObject: workspaceRBACObj, + }, + "GET:/@{user}/{workspacename}/apps/{application}/*": { + AssertAction: rbac.ActionRead, + AssertObject: workspaceRBACObj, + }, + // Has it's own auth "GET:/api/v2/users/oauth2/github/callback": {NoAuthorize: true}, @@ -368,6 +381,7 @@ func TestAuthorizeAllEndpoints(t *testing.T) { route = strings.ReplaceAll(route, "{template}", template.ID.String()) route = strings.ReplaceAll(route, "{hash}", file.Hash) route = strings.ReplaceAll(route, "{workspaceresource}", workspaceResources[0].ID.String()) + route = strings.ReplaceAll(route, "{workspaceapp}", workspaceResources[0].Agents[0].Apps[0].Name) route = strings.ReplaceAll(route, "{templateversion}", version.ID.String()) route = strings.ReplaceAll(route, "{templateversiondryrun}", templateVersionDryRun.ID.String()) route = strings.ReplaceAll(route, "{templatename}", template.Name) diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index c103dde98c13b..e9423d50a3826 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -173,7 +173,7 @@ func NewWithAPI(t *testing.T, options *Options) (*codersdk.Client, *coderd.API) cancelFunc() _ = turnServer.Close() srv.Close() - coderAPI.Close() + _ = coderAPI.Close() }) return codersdk.New(serverURL), coderAPI diff --git a/coderd/database/databasefake/databasefake.go b/coderd/database/databasefake/databasefake.go index 2a97dd0d1419e..da3a739c32629 100644 --- a/coderd/database/databasefake/databasefake.go +++ b/coderd/database/databasefake/databasefake.go @@ -35,6 +35,7 @@ func New() database.Store { templateVersions: make([]database.TemplateVersion, 0), templates: make([]database.Template, 0), workspaceBuilds: make([]database.WorkspaceBuild, 0), + workspaceApps: make([]database.WorkspaceApp, 0), workspaces: make([]database.Workspace, 0), } } @@ -63,6 +64,7 @@ type fakeQuerier struct { templateVersions []database.TemplateVersion templates []database.Template workspaceBuilds []database.WorkspaceBuild + workspaceApps []database.WorkspaceApp workspaces []database.Workspace } @@ -388,6 +390,38 @@ func (q *fakeQuerier) GetWorkspaceByOwnerIDAndName(_ context.Context, arg databa return database.Workspace{}, sql.ErrNoRows } +func (q *fakeQuerier) GetWorkspaceAppsByAgentID(_ context.Context, id uuid.UUID) ([]database.WorkspaceApp, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + apps := make([]database.WorkspaceApp, 0) + for _, app := range q.workspaceApps { + if app.AgentID == id { + apps = append(apps, app) + } + } + if len(apps) == 0 { + return nil, sql.ErrNoRows + } + return apps, nil +} + +func (q *fakeQuerier) GetWorkspaceAppsByAgentIDs(_ context.Context, ids []uuid.UUID) ([]database.WorkspaceApp, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + apps := make([]database.WorkspaceApp, 0) + for _, app := range q.workspaceApps { + for _, id := range ids { + if app.AgentID.String() == id.String() { + apps = append(apps, app) + break + } + } + } + return apps, nil +} + func (q *fakeQuerier) GetWorkspacesAutostart(_ context.Context) ([]database.Workspace, error) { q.mutex.RLock() defer q.mutex.RUnlock() @@ -1031,6 +1065,22 @@ func (q *fakeQuerier) GetWorkspaceAgentsByResourceIDs(_ context.Context, resourc return workspaceAgents, nil } +func (q *fakeQuerier) GetWorkspaceAppByAgentIDAndName(_ context.Context, arg database.GetWorkspaceAppByAgentIDAndNameParams) (database.WorkspaceApp, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + for _, app := range q.workspaceApps { + if app.AgentID != arg.AgentID { + continue + } + if app.Name != arg.Name { + continue + } + return app, nil + } + return database.WorkspaceApp{}, sql.ErrNoRows +} + func (q *fakeQuerier) GetProvisionerDaemonByID(_ context.Context, id uuid.UUID) (database.ProvisionerDaemon, error) { q.mutex.RLock() defer q.mutex.RUnlock() @@ -1521,6 +1571,25 @@ func (q *fakeQuerier) InsertWorkspaceBuild(_ context.Context, arg database.Inser return workspaceBuild, nil } +func (q *fakeQuerier) InsertWorkspaceApp(_ context.Context, arg database.InsertWorkspaceAppParams) (database.WorkspaceApp, error) { + q.mutex.Lock() + defer q.mutex.Unlock() + + // nolint:gosimple + workspaceApp := database.WorkspaceApp{ + ID: arg.ID, + AgentID: arg.AgentID, + CreatedAt: arg.CreatedAt, + Name: arg.Name, + Icon: arg.Icon, + Command: arg.Command, + Url: arg.Url, + RelativePath: arg.RelativePath, + } + q.workspaceApps = append(q.workspaceApps, workspaceApp) + return workspaceApp, nil +} + func (q *fakeQuerier) UpdateAPIKeyByID(_ context.Context, arg database.UpdateAPIKeyByIDParams) error { q.mutex.Lock() defer q.mutex.Unlock() diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 7767328f82458..b19f7747a4252 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -280,6 +280,17 @@ CREATE TABLE workspace_agents ( directory character varying(4096) DEFAULT ''::character varying NOT NULL ); +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, + icon character varying(256) NOT NULL, + command character varying(65534), + url character varying(65534), + relative_path boolean DEFAULT false NOT NULL +); + CREATE TABLE workspace_builds ( id uuid NOT NULL, created_at timestamp with time zone NOT NULL, @@ -382,6 +393,12 @@ ALTER TABLE ONLY users 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); + +ALTER TABLE ONLY workspace_apps + ADD CONSTRAINT workspace_apps_pkey PRIMARY KEY (id); + ALTER TABLE ONLY workspace_builds ADD CONSTRAINT workspace_builds_job_id_key UNIQUE (job_id); @@ -463,6 +480,9 @@ ALTER TABLE ONLY templates ALTER TABLE ONLY workspace_agents ADD CONSTRAINT workspace_agents_resource_id_fkey FOREIGN KEY (resource_id) REFERENCES workspace_resources(id) ON DELETE CASCADE; +ALTER TABLE ONLY workspace_apps + ADD CONSTRAINT workspace_apps_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; + ALTER TABLE ONLY workspace_builds ADD CONSTRAINT workspace_builds_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_jobs(id) ON DELETE CASCADE; diff --git a/coderd/database/migrations/000020_workspace_apps.down.sql b/coderd/database/migrations/000020_workspace_apps.down.sql new file mode 100644 index 0000000000000..c1c0dabd478da --- /dev/null +++ b/coderd/database/migrations/000020_workspace_apps.down.sql @@ -0,0 +1 @@ +DROP TABLE workspace_apps; diff --git a/coderd/database/migrations/000020_workspace_apps.up.sql b/coderd/database/migrations/000020_workspace_apps.up.sql new file mode 100644 index 0000000000000..7a991c179ee13 --- /dev/null +++ b/coderd/database/migrations/000020_workspace_apps.up.sql @@ -0,0 +1,12 @@ +CREATE TABLE workspace_apps ( + id uuid NOT NULL, + created_at timestamp with time zone NOT NULL, + agent_id uuid NOT NULL REFERENCES workspace_agents (id) ON DELETE CASCADE, + name varchar(64) NOT NULL, + icon varchar(256) NOT NULL, + command varchar(65534), + url varchar(65534), + relative_path boolean NOT NULL DEFAULT false, + PRIMARY KEY (id), + UNIQUE(agent_id, name) +); diff --git a/coderd/database/models.go b/coderd/database/models.go index 14efef4b48191..bbe997c8a76b0 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -493,6 +493,17 @@ type WorkspaceAgent struct { Directory string `db:"directory" json:"directory"` } +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"` + Icon string `db:"icon" json:"icon"` + Command sql.NullString `db:"command" json:"command"` + Url sql.NullString `db:"url" json:"url"` + RelativePath bool `db:"relative_path" json:"relative_path"` +} + type WorkspaceBuild struct { ID uuid.UUID `db:"id" json:"id"` CreatedAt time.Time `db:"created_at" json:"created_at"` diff --git a/coderd/database/querier.go b/coderd/database/querier.go index fc3274e94abc8..bf66df784fc3c 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -64,6 +64,9 @@ type querier interface { GetWorkspaceAgentByID(ctx context.Context, id uuid.UUID) (WorkspaceAgent, error) GetWorkspaceAgentByInstanceID(ctx context.Context, authInstanceID string) (WorkspaceAgent, error) GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgent, error) + GetWorkspaceAppByAgentIDAndName(ctx context.Context, arg GetWorkspaceAppByAgentIDAndNameParams) (WorkspaceApp, error) + GetWorkspaceAppsByAgentID(ctx context.Context, agentID uuid.UUID) ([]WorkspaceApp, error) + GetWorkspaceAppsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceApp, error) GetWorkspaceBuildByID(ctx context.Context, id uuid.UUID) (WorkspaceBuild, error) GetWorkspaceBuildByJobID(ctx context.Context, jobID uuid.UUID) (WorkspaceBuild, error) GetWorkspaceBuildByWorkspaceID(ctx context.Context, arg GetWorkspaceBuildByWorkspaceIDParams) ([]WorkspaceBuild, error) @@ -93,6 +96,7 @@ type querier interface { InsertUser(ctx context.Context, arg InsertUserParams) (User, error) InsertWorkspace(ctx context.Context, arg InsertWorkspaceParams) (Workspace, error) InsertWorkspaceAgent(ctx context.Context, arg InsertWorkspaceAgentParams) (WorkspaceAgent, error) + InsertWorkspaceApp(ctx context.Context, arg InsertWorkspaceAppParams) (WorkspaceApp, error) InsertWorkspaceBuild(ctx context.Context, arg InsertWorkspaceBuildParams) (WorkspaceBuild, error) InsertWorkspaceResource(ctx context.Context, arg InsertWorkspaceResourceParams) (WorkspaceResource, error) UpdateAPIKeyByID(ctx context.Context, arg UpdateAPIKeyByIDParams) error diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 5aaf9d8f9b389..54cb5bc6a1d53 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -2785,6 +2785,155 @@ func (q *sqlQuerier) UpdateWorkspaceAgentConnectionByID(ctx context.Context, arg return err } +const getWorkspaceAppByAgentIDAndName = `-- name: GetWorkspaceAppByAgentIDAndName :one +SELECT id, created_at, agent_id, name, icon, command, url, relative_path FROM workspace_apps WHERE agent_id = $1 AND name = $2 +` + +type GetWorkspaceAppByAgentIDAndNameParams struct { + AgentID uuid.UUID `db:"agent_id" json:"agent_id"` + Name string `db:"name" json:"name"` +} + +func (q *sqlQuerier) GetWorkspaceAppByAgentIDAndName(ctx context.Context, arg GetWorkspaceAppByAgentIDAndNameParams) (WorkspaceApp, error) { + row := q.db.QueryRowContext(ctx, getWorkspaceAppByAgentIDAndName, arg.AgentID, arg.Name) + var i WorkspaceApp + err := row.Scan( + &i.ID, + &i.CreatedAt, + &i.AgentID, + &i.Name, + &i.Icon, + &i.Command, + &i.Url, + &i.RelativePath, + ) + return i, err +} + +const getWorkspaceAppsByAgentID = `-- name: GetWorkspaceAppsByAgentID :many +SELECT id, created_at, agent_id, name, icon, command, url, relative_path FROM workspace_apps WHERE agent_id = $1 +` + +func (q *sqlQuerier) GetWorkspaceAppsByAgentID(ctx context.Context, agentID uuid.UUID) ([]WorkspaceApp, error) { + rows, err := q.db.QueryContext(ctx, getWorkspaceAppsByAgentID, agentID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []WorkspaceApp + for rows.Next() { + var i WorkspaceApp + if err := rows.Scan( + &i.ID, + &i.CreatedAt, + &i.AgentID, + &i.Name, + &i.Icon, + &i.Command, + &i.Url, + &i.RelativePath, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getWorkspaceAppsByAgentIDs = `-- name: GetWorkspaceAppsByAgentIDs :many +SELECT id, created_at, agent_id, name, icon, command, url, relative_path FROM workspace_apps WHERE agent_id = ANY($1 :: uuid [ ]) +` + +func (q *sqlQuerier) GetWorkspaceAppsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceApp, error) { + rows, err := q.db.QueryContext(ctx, getWorkspaceAppsByAgentIDs, pq.Array(ids)) + if err != nil { + return nil, err + } + defer rows.Close() + var items []WorkspaceApp + for rows.Next() { + var i WorkspaceApp + if err := rows.Scan( + &i.ID, + &i.CreatedAt, + &i.AgentID, + &i.Name, + &i.Icon, + &i.Command, + &i.Url, + &i.RelativePath, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const insertWorkspaceApp = `-- name: InsertWorkspaceApp :one +INSERT INTO + workspace_apps ( + id, + created_at, + agent_id, + name, + icon, + command, + url, + relative_path + ) +VALUES + ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id, created_at, agent_id, name, icon, command, url, relative_path +` + +type InsertWorkspaceAppParams struct { + ID uuid.UUID `db:"id" json:"id"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + AgentID uuid.UUID `db:"agent_id" json:"agent_id"` + Name string `db:"name" json:"name"` + Icon string `db:"icon" json:"icon"` + Command sql.NullString `db:"command" json:"command"` + Url sql.NullString `db:"url" json:"url"` + RelativePath bool `db:"relative_path" json:"relative_path"` +} + +func (q *sqlQuerier) InsertWorkspaceApp(ctx context.Context, arg InsertWorkspaceAppParams) (WorkspaceApp, error) { + row := q.db.QueryRowContext(ctx, insertWorkspaceApp, + arg.ID, + arg.CreatedAt, + arg.AgentID, + arg.Name, + arg.Icon, + arg.Command, + arg.Url, + arg.RelativePath, + ) + var i WorkspaceApp + err := row.Scan( + &i.ID, + &i.CreatedAt, + &i.AgentID, + &i.Name, + &i.Icon, + &i.Command, + &i.Url, + &i.RelativePath, + ) + return i, err +} + const getLatestWorkspaceBuildByWorkspaceID = `-- name: GetLatestWorkspaceBuildByWorkspaceID :one SELECT id, created_at, updated_at, workspace_id, template_version_id, name, build_number, transition, initiator_id, provisioner_state, job_id, deadline diff --git a/coderd/database/queries/workspaceapps.sql b/coderd/database/queries/workspaceapps.sql new file mode 100644 index 0000000000000..61898d1caf56c --- /dev/null +++ b/coderd/database/queries/workspaceapps.sql @@ -0,0 +1,23 @@ +-- name: GetWorkspaceAppsByAgentID :many +SELECT * FROM workspace_apps WHERE agent_id = $1; + +-- name: GetWorkspaceAppsByAgentIDs :many +SELECT * FROM workspace_apps WHERE agent_id = ANY(@ids :: uuid [ ]); + +-- name: GetWorkspaceAppByAgentIDAndName :one +SELECT * FROM workspace_apps WHERE agent_id = $1 AND name = $2; + +-- name: InsertWorkspaceApp :one +INSERT INTO + workspace_apps ( + id, + created_at, + agent_id, + name, + icon, + command, + url, + relative_path + ) +VALUES + ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *; diff --git a/coderd/httpmw/apikey.go b/coderd/httpmw/apikey.go index 583daff59fd23..168bd378d144d 100644 --- a/coderd/httpmw/apikey.go +++ b/coderd/httpmw/apikey.go @@ -17,7 +17,7 @@ import ( "github.com/coder/coder/coderd/httpapi" ) -// SessionTokenKey represents the name of the cookie or query paramater the API key is stored in. +// SessionTokenKey represents the name of the cookie or query parameter the API key is stored in. const SessionTokenKey = "session_token" type apiKeyContextKey struct{} diff --git a/coderd/provisionerdaemons.go b/coderd/provisionerdaemons.go index 2647636c36278..abdf3ddbf01e2 100644 --- a/coderd/provisionerdaemons.go +++ b/coderd/provisionerdaemons.go @@ -724,7 +724,7 @@ func insertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid. } } - _, err := db.InsertWorkspaceAgent(ctx, database.InsertWorkspaceAgentParams{ + dbAgent, err := db.InsertWorkspaceAgent(ctx, database.InsertWorkspaceAgentParams{ ID: uuid.New(), CreatedAt: database.Now(), UpdatedAt: database.Now(), @@ -744,6 +744,28 @@ func insertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid. if err != nil { return xerrors.Errorf("insert agent: %w", err) } + + for _, app := range agent.Apps { + _, err := db.InsertWorkspaceApp(ctx, database.InsertWorkspaceAppParams{ + ID: uuid.New(), + CreatedAt: database.Now(), + AgentID: dbAgent.ID, + Name: app.Name, + Icon: app.Icon, + Command: sql.NullString{ + String: app.Command, + Valid: app.Command != "", + }, + Url: sql.NullString{ + String: app.Url, + Valid: app.Url != "", + }, + RelativePath: app.RelativePath, + }) + if err != nil { + return xerrors.Errorf("insert app: %w", err) + } + } } return nil } diff --git a/coderd/provisionerjobs.go b/coderd/provisionerjobs.go index 52031eaff4bc1..caf8dba1b8c65 100644 --- a/coderd/provisionerjobs.go +++ b/coderd/provisionerjobs.go @@ -220,6 +220,20 @@ func (api *API) provisionerJobResources(rw http.ResponseWriter, r *http.Request, }) return } + resourceAgentIDs := make([]uuid.UUID, 0) + for _, agent := range resourceAgents { + resourceAgentIDs = append(resourceAgentIDs, agent.ID) + } + apps, err := api.Database.GetWorkspaceAppsByAgentIDs(r.Context(), resourceAgentIDs) + if errors.Is(err, sql.ErrNoRows) { + err = nil + } + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get workspace apps: %s", err), + }) + return + } apiResources := make([]codersdk.WorkspaceResource, 0) for _, resource := range resources { @@ -228,7 +242,14 @@ func (api *API) provisionerJobResources(rw http.ResponseWriter, r *http.Request, if agent.ResourceID != resource.ID { continue } - apiAgent, err := convertWorkspaceAgent(agent, api.AgentConnectionUpdateFrequency) + dbApps := make([]database.WorkspaceApp, 0) + for _, app := range apps { + if app.AgentID == agent.ID { + dbApps = append(dbApps, app) + } + } + + apiAgent, err := convertWorkspaceAgent(agent, convertApps(dbApps), api.AgentConnectionUpdateFrequency) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ Message: "Internal error reading job agent", diff --git a/coderd/rbac/README.md b/coderd/rbac/README.md index 69ba5e9f925aa..f2e1283206822 100644 --- a/coderd/rbac/README.md +++ b/coderd/rbac/README.md @@ -51,7 +51,7 @@ This can be represented by the following truth table, where Y represents *positi ## Example Permissions -- `+site.*.*.read`: allowed to perform the `read` action against all objects of type `devurl` in a given Coder deployment. +- `+site.*.*.read`: allowed to perform the `read` action against all objects of type `app` in a given Coder deployment. - `-user.workspace.*.create`: user is not allowed to create workspaces. ## Roles diff --git a/coderd/rbac/object.go b/coderd/rbac/object.go index 95820ff25dad1..3ae9499fd856d 100644 --- a/coderd/rbac/object.go +++ b/coderd/rbac/object.go @@ -113,7 +113,7 @@ type Object struct { // OrgID specifies which org the object is a part of. OrgID string `json:"org_owner"` - // Type is "workspace", "project", "devurl", etc + // Type is "workspace", "project", "app", etc Type string `json:"type"` // TODO: SharedUsers? } diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index 6e51c7d6260b7..37d06cd663b85 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -31,7 +31,14 @@ import ( func (api *API) workspaceAgent(rw http.ResponseWriter, r *http.Request) { workspaceAgent := httpmw.WorkspaceAgentParam(r) - apiAgent, err := convertWorkspaceAgent(workspaceAgent, api.AgentConnectionUpdateFrequency) + dbApps, err := api.Database.GetWorkspaceAppsByAgentID(r.Context(), workspaceAgent.ID) + if err != nil && !xerrors.Is(err, sql.ErrNoRows) { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get workspace agent apps: %s", err), + }) + return + } + apiAgent, err := convertWorkspaceAgent(workspaceAgent, convertApps(dbApps), api.AgentConnectionUpdateFrequency) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ Message: "Internal error reading workspace agent", @@ -50,7 +57,7 @@ func (api *API) workspaceAgentDial(rw http.ResponseWriter, r *http.Request) { defer api.websocketWaitGroup.Done() workspaceAgent := httpmw.WorkspaceAgentParam(r) - apiAgent, err := convertWorkspaceAgent(workspaceAgent, api.AgentConnectionUpdateFrequency) + apiAgent, err := convertWorkspaceAgent(workspaceAgent, nil, api.AgentConnectionUpdateFrequency) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ Message: "Internal error reading workspace agent", @@ -97,7 +104,7 @@ func (api *API) workspaceAgentDial(rw http.ResponseWriter, r *http.Request) { func (api *API) workspaceAgentMetadata(rw http.ResponseWriter, r *http.Request) { workspaceAgent := httpmw.WorkspaceAgent(r) - apiAgent, err := convertWorkspaceAgent(workspaceAgent, api.AgentConnectionUpdateFrequency) + apiAgent, err := convertWorkspaceAgent(workspaceAgent, nil, api.AgentConnectionUpdateFrequency) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ Message: "Internal error reading workspace agent", @@ -358,7 +365,7 @@ func (api *API) workspaceAgentPTY(rw http.ResponseWriter, r *http.Request) { defer api.websocketWaitGroup.Done() workspaceAgent := httpmw.WorkspaceAgentParam(r) - apiAgent, err := convertWorkspaceAgent(workspaceAgent, api.AgentConnectionUpdateFrequency) + apiAgent, err := convertWorkspaceAgent(workspaceAgent, nil, api.AgentConnectionUpdateFrequency) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ Message: "Internal error reading workspace agent", @@ -403,16 +410,16 @@ func (api *API) workspaceAgentPTY(rw http.ResponseWriter, r *http.Request) { return } - ctx, wsNetConn := websocketNetConn(r.Context(), conn, websocket.MessageBinary) + _, wsNetConn := websocketNetConn(r.Context(), conn, websocket.MessageBinary) defer wsNetConn.Close() // Also closes conn. - agentConn, err := api.dialWorkspaceAgent(ctx, r, workspaceAgent.ID) + agentConn, release, err := api.workspaceAgentCache.Acquire(r, workspaceAgent.ID) if err != nil { _ = conn.Close(websocket.StatusInternalError, httpapi.WebsocketCloseSprintf("dial workspace agent: %s", err)) return } - defer agentConn.Close() - ptNetConn, err := agentConn.ReconnectingPTY(reconnect.String(), uint16(height), uint16(width), "") + defer release() + ptNetConn, err := agentConn.ReconnectingPTY(reconnect.String(), uint16(height), uint16(width), r.URL.Query().Get("command")) if err != nil { _ = conn.Close(websocket.StatusInternalError, httpapi.WebsocketCloseSprintf("dial: %s", err)) return @@ -428,8 +435,9 @@ func (api *API) workspaceAgentPTY(rw http.ResponseWriter, r *http.Request) { // dialWorkspaceAgent connects to a workspace agent by ID. Only rely on // r.Context() for cancellation if it's use is safe or r.Hijack() has // not been performed. -func (api *API) dialWorkspaceAgent(ctx context.Context, r *http.Request, agentID uuid.UUID) (*agent.Conn, error) { +func (api *API) dialWorkspaceAgent(r *http.Request, agentID uuid.UUID) (*agent.Conn, error) { client, server := provisionersdk.TransportPipe() + ctx, cancelFunc := context.WithCancel(context.Background()) go func() { _ = peerbroker.ProxyListen(ctx, server, peerbroker.ProxyOptions{ ChannelID: agentID.String(), @@ -443,9 +451,12 @@ func (api *API) dialWorkspaceAgent(ctx context.Context, r *http.Request, agentID peerClient := proto.NewDRPCPeerBrokerClient(provisionersdk.Conn(client)) stream, err := peerClient.NegotiateConnection(ctx) if err != nil { + cancelFunc() return nil, xerrors.Errorf("negotiate: %w", err) } - options := &peer.ConnOptions{} + options := &peer.ConnOptions{ + Logger: api.Logger.Named("agent-dialer"), + } options.SettingEngine.SetSrflxAcceptanceMinWait(0) options.SettingEngine.SetRelayAcceptanceMinWait(0) // Use the ProxyDialer for the TURN server. @@ -476,15 +487,33 @@ func (api *API) dialWorkspaceAgent(ctx context.Context, r *http.Request, agentID })) peerConn, err := peerbroker.Dial(stream, append(api.ICEServers, turnconn.Proxy), options) if err != nil { + cancelFunc() return nil, xerrors.Errorf("dial: %w", err) } + go func() { + <-peerConn.Closed() + cancelFunc() + }() return &agent.Conn{ Negotiator: peerClient, Conn: peerConn, }, nil } -func convertWorkspaceAgent(dbAgent database.WorkspaceAgent, agentUpdateFrequency time.Duration) (codersdk.WorkspaceAgent, error) { +func convertApps(dbApps []database.WorkspaceApp) []codersdk.WorkspaceApp { + apps := make([]codersdk.WorkspaceApp, 0) + for _, dbApp := range dbApps { + apps = append(apps, codersdk.WorkspaceApp{ + ID: dbApp.ID, + Name: dbApp.Name, + Command: dbApp.Command.String, + Icon: dbApp.Icon, + }) + } + return apps +} + +func convertWorkspaceAgent(dbAgent database.WorkspaceAgent, apps []codersdk.WorkspaceApp, agentUpdateFrequency time.Duration) (codersdk.WorkspaceAgent, error) { var envs map[string]string if dbAgent.EnvironmentVariables.Valid { err := json.Unmarshal(dbAgent.EnvironmentVariables.RawMessage, &envs) @@ -504,6 +533,7 @@ func convertWorkspaceAgent(dbAgent database.WorkspaceAgent, agentUpdateFrequency StartupScript: dbAgent.StartupScript.String, EnvironmentVariables: envs, Directory: dbAgent.Directory, + Apps: apps, } if dbAgent.FirstConnectedAt.Valid { workspaceAgent.FirstConnectedAt = &dbAgent.FirstConnectedAt.Time diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go index 360abb3431156..c658791f93941 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -297,7 +297,7 @@ func TestWorkspaceAgentPTY(t *testing.T) { }) resources := coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID) - conn, err := client.WorkspaceAgentReconnectingPTY(context.Background(), resources[0].Agents[0].ID, uuid.New(), 80, 80) + conn, err := client.WorkspaceAgentReconnectingPTY(context.Background(), resources[0].Agents[0].ID, uuid.New(), 80, 80, "/bin/bash") require.NoError(t, err) defer conn.Close() diff --git a/coderd/workspaceapps.go b/coderd/workspaceapps.go new file mode 100644 index 0000000000000..299400116c8dd --- /dev/null +++ b/coderd/workspaceapps.go @@ -0,0 +1,166 @@ +package coderd + +import ( + "database/sql" + "errors" + "fmt" + "net/http" + "net/http/httputil" + "net/url" + "strings" + + "github.com/go-chi/chi/v5" + "github.com/google/uuid" + + "github.com/coder/coder/coderd/database" + "github.com/coder/coder/coderd/httpapi" + "github.com/coder/coder/coderd/httpmw" + "github.com/coder/coder/coderd/rbac" + "github.com/coder/coder/site" +) + +// workspaceAppsProxyPath proxies requests to a workspace application +// through a relative URL path. +func (api *API) workspaceAppsProxyPath(rw http.ResponseWriter, r *http.Request) { + user := httpmw.UserParam(r) + // This can be in the form of: ".[workspace-agent]" or "" + workspaceWithAgent := chi.URLParam(r, "workspacename") + workspaceParts := strings.Split(workspaceWithAgent, ".") + + workspace, err := api.Database.GetWorkspaceByOwnerIDAndName(r.Context(), database.GetWorkspaceByOwnerIDAndNameParams{ + OwnerID: user.ID, + Name: workspaceParts[0], + }) + if errors.Is(err, sql.ErrNoRows) { + httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ + Message: "workspace not found", + }) + return + } + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get workspace: %s", err), + }) + return + } + if !api.Authorize(rw, r, rbac.ActionRead, workspace) { + return + } + + build, err := api.Database.GetLatestWorkspaceBuildByWorkspaceID(r.Context(), workspace.ID) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get workspace build: %s", err), + }) + return + } + + resources, err := api.Database.GetWorkspaceResourcesByJobID(r.Context(), build.JobID) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get workspace resources: %s", err), + }) + return + } + resourceIDs := make([]uuid.UUID, 0) + for _, resource := range resources { + resourceIDs = append(resourceIDs, resource.ID) + } + agents, err := api.Database.GetWorkspaceAgentsByResourceIDs(r.Context(), resourceIDs) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get workspace agents: %s", err), + }) + return + } + if len(agents) == 0 { + httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ + Message: "no agents exist", + }) + } + + agent := agents[0] + if len(workspaceParts) > 1 { + for _, otherAgent := range agents { + if otherAgent.Name == workspaceParts[1] { + agent = otherAgent + break + } + } + } + + app, err := api.Database.GetWorkspaceAppByAgentIDAndName(r.Context(), database.GetWorkspaceAppByAgentIDAndNameParams{ + AgentID: agent.ID, + Name: chi.URLParam(r, "workspaceapp"), + }) + if errors.Is(err, sql.ErrNoRows) { + httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ + Message: "application not found", + }) + return + } + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get workspace app: %s", err), + }) + return + } + if !app.Url.Valid { + httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ + Message: fmt.Sprintf("application does not have a url: %s", err), + }) + return + } + + appURL, err := url.Parse(app.Url.String) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("parse app url: %s", err), + }) + return + } + + proxy := httputil.NewSingleHostReverseProxy(appURL) + proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) { + // This is a browser-facing route so JSON responses are not viable here. + // To pass friendly errors to the frontend, special meta tags are overridden + // in the index.html with the content passed here. + r = r.WithContext(site.WithAPIResponse(r.Context(), site.APIResponse{ + StatusCode: http.StatusBadGateway, + Message: err.Error(), + })) + api.siteHandler.ServeHTTP(w, r) + } + path := chi.URLParam(r, "*") + if !strings.HasSuffix(r.URL.Path, "/") && path == "" { + // Web applications typically request paths relative to the + // root URL. This allows for routing behind a proxy or subpath. + // See https://github.com/coder/code-server/issues/241 for examples. + r.URL.Path += "/" + http.Redirect(rw, r, r.URL.String(), http.StatusTemporaryRedirect) + return + } + if r.URL.RawQuery == "" && appURL.RawQuery != "" { + // If the application defines a default set of query parameters, + // we should always respect them. The reverse proxy will merge + // query parameters for server-side requests, but sometimes + // client-side applications require the query parameters to render + // properly. With code-server, this is the "folder" param. + r.URL.RawQuery = appURL.RawQuery + http.Redirect(rw, r, r.URL.String(), http.StatusTemporaryRedirect) + return + } + r.URL.Path = path + + conn, release, err := api.workspaceAgentCache.Acquire(r, agent.ID) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("dial workspace agent: %s", err), + }) + return + } + defer release() + + proxy.Transport = conn.HTTPTransport() + proxy.ServeHTTP(rw, r) +} diff --git a/coderd/workspaceapps_test.go b/coderd/workspaceapps_test.go new file mode 100644 index 0000000000000..399b1874dc6aa --- /dev/null +++ b/coderd/workspaceapps_test.go @@ -0,0 +1,125 @@ +package coderd_test + +import ( + "context" + "fmt" + "io" + "net" + "net/http" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + + "cdr.dev/slog/sloggers/slogtest" + "github.com/coder/coder/agent" + "github.com/coder/coder/coderd/coderdtest" + "github.com/coder/coder/codersdk" + "github.com/coder/coder/provisioner/echo" + "github.com/coder/coder/provisionersdk/proto" +) + +func TestWorkspaceAppsProxyPath(t *testing.T) { + t.Parallel() + // #nosec + ln, err := net.Listen("tcp", ":0") + require.NoError(t, err) + server := http.Server{ + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }), + } + t.Cleanup(func() { + _ = server.Close() + _ = ln.Close() + }) + go server.Serve(ln) + tcpAddr, _ := ln.Addr().(*net.TCPAddr) + + client, coderAPI := coderdtest.NewWithAPI(t, nil) + user := coderdtest.CreateFirstUser(t, client) + coderdtest.NewProvisionerDaemon(t, coderAPI) + authToken := uuid.NewString() + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ + Parse: echo.ParseComplete, + ProvisionDryRun: echo.ProvisionComplete, + Provision: []*proto.Provision_Response{{ + Type: &proto.Provision_Response_Complete{ + Complete: &proto.Provision_Complete{ + Resources: []*proto.Resource{{ + Name: "example", + Type: "aws_instance", + Agents: []*proto.Agent{{ + Id: uuid.NewString(), + Auth: &proto.Agent_Token{ + Token: authToken, + }, + Apps: []*proto.App{{ + Name: "example", + Url: fmt.Sprintf("http://127.0.0.1:%d?query=true", tcpAddr.Port), + }, { + Name: "fake", + Url: "http://127.0.0.2", + }}, + }}, + }}, + }, + }, + }}, + }) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + coderdtest.AwaitTemplateVersionJob(t, client, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) + coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) + + agentClient := codersdk.New(client.URL) + agentClient.SessionToken = authToken + agentCloser := agent.New(agentClient.ListenWorkspaceAgent, &agent.Options{ + Logger: slogtest.Make(t, nil), + }) + t.Cleanup(func() { + _ = agentCloser.Close() + }) + coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID) + client.HTTPClient.CheckRedirect = func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + } + + t.Run("RedirectsWithSlash", func(t *testing.T) { + t.Parallel() + resp, err := client.Request(context.Background(), http.MethodGet, "/@me/"+workspace.Name+"/apps/example", nil) + require.NoError(t, err) + defer resp.Body.Close() + require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode) + }) + + t.Run("RedirectsWithQuery", func(t *testing.T) { + t.Parallel() + resp, err := client.Request(context.Background(), http.MethodGet, "/@me/"+workspace.Name+"/apps/example/", nil) + require.NoError(t, err) + defer resp.Body.Close() + require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode) + loc, err := resp.Location() + require.NoError(t, err) + require.Equal(t, "query=true", loc.RawQuery) + }) + + t.Run("Proxies", func(t *testing.T) { + t.Parallel() + resp, err := client.Request(context.Background(), http.MethodGet, "/@me/"+workspace.Name+"/apps/example/?query=true", nil) + require.NoError(t, err) + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Equal(t, "", string(body)) + require.Equal(t, http.StatusOK, resp.StatusCode) + }) + + t.Run("ProxyError", func(t *testing.T) { + t.Parallel() + resp, err := client.Request(context.Background(), http.MethodGet, "/@me/"+workspace.Name+"/apps/fake/", nil) + require.NoError(t, err) + defer resp.Body.Close() + require.Equal(t, http.StatusOK, resp.StatusCode) + }) +} diff --git a/coderd/workspaceresources.go b/coderd/workspaceresources.go index c367d89d596c7..5b954cdd08964 100644 --- a/coderd/workspaceresources.go +++ b/coderd/workspaceresources.go @@ -3,10 +3,12 @@ package coderd import ( "database/sql" "errors" + "fmt" "net/http" "github.com/google/uuid" + "github.com/coder/coder/coderd/database" "github.com/coder/coder/coderd/httpapi" "github.com/coder/coder/coderd/httpmw" "github.com/coder/coder/coderd/rbac" @@ -46,9 +48,27 @@ func (api *API) workspaceResource(rw http.ResponseWriter, r *http.Request) { }) return } + agentIDs := make([]uuid.UUID, 0) + for _, agent := range agents { + agentIDs = append(agentIDs, agent.ID) + } + apps, err := api.Database.GetWorkspaceAppsByAgentIDs(r.Context(), agentIDs) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get workspace apps: %s", err), + }) + return + } apiAgents := make([]codersdk.WorkspaceAgent, 0) for _, agent := range agents { - convertedAgent, err := convertWorkspaceAgent(agent, api.AgentConnectionUpdateFrequency) + dbApps := make([]database.WorkspaceApp, 0) + for _, app := range apps { + if app.AgentID == agent.ID { + dbApps = append(dbApps, app) + } + } + + convertedAgent, err := convertWorkspaceAgent(agent, convertApps(dbApps), api.AgentConnectionUpdateFrequency) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ Message: "Internal error reading workspace agent", diff --git a/coderd/workspaceresources_test.go b/coderd/workspaceresources_test.go index 83d10187562b5..a1a941f355386 100644 --- a/coderd/workspaceresources_test.go +++ b/coderd/workspaceresources_test.go @@ -43,4 +43,50 @@ func TestWorkspaceResource(t *testing.T) { _, err = client.WorkspaceResource(context.Background(), resources[0].ID) require.NoError(t, err) }) + + t.Run("Apps", func(t *testing.T) { + t.Parallel() + client, coderd := coderdtest.NewWithAPI(t, nil) + user := coderdtest.CreateFirstUser(t, client) + coderdtest.NewProvisionerDaemon(t, coderd) + app := &proto.App{ + Name: "code-server", + Command: "some-command", + Url: "http://localhost:3000", + Icon: "/code.svg", + } + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ + Parse: echo.ParseComplete, + Provision: []*proto.Provision_Response{{ + Type: &proto.Provision_Response_Complete{ + Complete: &proto.Provision_Complete{ + Resources: []*proto.Resource{{ + Name: "some", + Type: "example", + Agents: []*proto.Agent{{ + Id: "something", + Auth: &proto.Agent_Token{}, + Apps: []*proto.App{app}, + }}, + }}, + }, + }, + }}, + }) + coderdtest.AwaitTemplateVersionJob(t, client, version.ID) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) + coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) + resources, err := client.WorkspaceResourcesByBuild(context.Background(), workspace.LatestBuild.ID) + require.NoError(t, err) + resource, err := client.WorkspaceResource(context.Background(), resources[0].ID) + require.NoError(t, err) + require.Len(t, resource.Agents, 1) + agent := resource.Agents[0] + require.Len(t, agent.Apps, 1) + got := agent.Apps[0] + require.Equal(t, app.Command, got.Command) + require.Equal(t, app.Icon, got.Icon) + require.Equal(t, app.Name, got.Name) + }) } diff --git a/coderd/wsconncache/wsconncache.go b/coderd/wsconncache/wsconncache.go new file mode 100644 index 0000000000000..7d3b741a63b7e --- /dev/null +++ b/coderd/wsconncache/wsconncache.go @@ -0,0 +1,162 @@ +// Package wsconncache caches workspace agent connections by UUID. +package wsconncache + +import ( + "context" + "net/http" + "sync" + "time" + + "github.com/google/uuid" + "go.uber.org/atomic" + "golang.org/x/sync/singleflight" + "golang.org/x/xerrors" + + "github.com/coder/coder/agent" +) + +// New creates a new workspace connection cache that closes +// connections after the inactive timeout provided. +// +// Agent connections are cached due to WebRTC negotiation +// taking a few hundred milliseconds. +func New(dialer Dialer, inactiveTimeout time.Duration) *Cache { + if inactiveTimeout == 0 { + inactiveTimeout = 5 * time.Minute + } + return &Cache{ + closed: make(chan struct{}), + dialer: dialer, + inactiveTimeout: inactiveTimeout, + } +} + +// Dialer creates a new agent connection by ID. +type Dialer func(r *http.Request, id uuid.UUID) (*agent.Conn, error) + +// Conn wraps an agent connection with a reusable HTTP transport. +type Conn struct { + *agent.Conn + + locks atomic.Uint64 + timeoutMutex sync.Mutex + timeout *time.Timer + timeoutCancel context.CancelFunc + transport *http.Transport +} + +func (c *Conn) HTTPTransport() *http.Transport { + return c.transport +} + +// CloseWithError ends the HTTP transport if exists, and closes the agent. +func (c *Conn) CloseWithError(err error) error { + if c.transport != nil { + c.transport.CloseIdleConnections() + } + c.timeoutMutex.Lock() + defer c.timeoutMutex.Unlock() + if c.timeout != nil { + c.timeout.Stop() + } + return c.Conn.CloseWithError(err) +} + +type Cache struct { + closed chan struct{} + closeMutex sync.Mutex + closeGroup sync.WaitGroup + connGroup singleflight.Group + connMap sync.Map + dialer Dialer + inactiveTimeout time.Duration +} + +// Acquire gets or establishes a connection with the dialer using the ID provided. +// If a connection is in-progress, that connection or error will be returned. +// +// The returned function is used to release a lock on the connection. Once zero +// locks exist on a connection, the inactive timeout will begin to tick down. +// After the time expires, the connection will be cleared from the cache. +func (c *Cache) Acquire(r *http.Request, id uuid.UUID) (*Conn, func(), error) { + rawConn, found := c.connMap.Load(id.String()) + // If the connection isn't found, establish a new one! + if !found { + var err error + // A singleflight group is used to allow for concurrent requests to the + // same identifier to resolve. + rawConn, err, _ = c.connGroup.Do(id.String(), func() (interface{}, error) { + agentConn, err := c.dialer(r, id) + if err != nil { + return nil, xerrors.Errorf("dial: %w", err) + } + timeoutCtx, timeoutCancelFunc := context.WithCancel(context.Background()) + defaultTransport, valid := http.DefaultTransport.(*http.Transport) + if !valid { + panic("dev error: default transport is the wrong type") + } + transport := defaultTransport.Clone() + transport.DialContext = agentConn.DialContext + conn := &Conn{ + Conn: agentConn, + timeoutCancel: timeoutCancelFunc, + transport: transport, + } + c.closeMutex.Lock() + c.closeGroup.Add(1) + c.closeMutex.Unlock() + go func() { + defer c.closeGroup.Done() + var err error + select { + case <-timeoutCtx.Done(): + err = xerrors.New("cache timeout") + case <-c.closed: + err = xerrors.New("cache closed") + case <-conn.Closed(): + } + + c.connMap.Delete(id.String()) + c.connGroup.Forget(id.String()) + _ = conn.CloseWithError(err) + }() + return conn, nil + }) + if err != nil { + return nil, nil, err + } + c.connMap.Store(id.String(), rawConn) + } + + conn, _ := rawConn.(*Conn) + conn.timeoutMutex.Lock() + defer conn.timeoutMutex.Unlock() + if conn.timeout != nil { + conn.timeout.Stop() + } + conn.locks.Inc() + return conn, func() { + conn.timeoutMutex.Lock() + defer conn.timeoutMutex.Unlock() + if conn.timeout != nil { + conn.timeout.Stop() + } + conn.locks.Dec() + if conn.locks.Load() == 0 { + conn.timeout = time.AfterFunc(c.inactiveTimeout, conn.timeoutCancel) + } + }, nil +} + +func (c *Cache) Close() error { + c.closeMutex.Lock() + defer c.closeMutex.Unlock() + select { + case <-c.closed: + return nil + default: + } + close(c.closed) + c.closeGroup.Wait() + return nil +} diff --git a/coderd/wsconncache/wsconncache_test.go b/coderd/wsconncache/wsconncache_test.go new file mode 100644 index 0000000000000..34ce39e20b86d --- /dev/null +++ b/coderd/wsconncache/wsconncache_test.go @@ -0,0 +1,175 @@ +package wsconncache_test + +import ( + "context" + "fmt" + "net" + "net/http" + "net/http/httptest" + "net/http/httputil" + "net/url" + "sync" + "testing" + "time" + + "github.com/google/uuid" + "github.com/pion/webrtc/v3" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/atomic" + "go.uber.org/goleak" + + "cdr.dev/slog" + "cdr.dev/slog/sloggers/slogtest" + "github.com/coder/coder/agent" + "github.com/coder/coder/coderd/wsconncache" + "github.com/coder/coder/peer" + "github.com/coder/coder/peerbroker" + "github.com/coder/coder/peerbroker/proto" + "github.com/coder/coder/provisionersdk" +) + +func TestMain(m *testing.M) { + goleak.VerifyTestMain(m) +} + +func TestCache(t *testing.T) { + t.Parallel() + t.Run("Same", func(t *testing.T) { + t.Parallel() + cache := wsconncache.New(func(r *http.Request, id uuid.UUID) (*agent.Conn, error) { + return setupAgent(t, agent.Metadata{}, 0), nil + }, 0) + t.Cleanup(func() { + _ = cache.Close() + }) + conn1, _, err := cache.Acquire(httptest.NewRequest(http.MethodGet, "/", nil), uuid.Nil) + require.NoError(t, err) + conn2, _, err := cache.Acquire(httptest.NewRequest(http.MethodGet, "/", nil), uuid.Nil) + require.NoError(t, err) + require.True(t, conn1 == conn2) + }) + t.Run("Expire", func(t *testing.T) { + t.Parallel() + called := atomic.NewInt32(0) + cache := wsconncache.New(func(r *http.Request, id uuid.UUID) (*agent.Conn, error) { + called.Add(1) + return setupAgent(t, agent.Metadata{}, 0), nil + }, time.Microsecond) + t.Cleanup(func() { + _ = cache.Close() + }) + conn, release, err := cache.Acquire(httptest.NewRequest(http.MethodGet, "/", nil), uuid.Nil) + require.NoError(t, err) + release() + <-conn.Closed() + conn, release, err = cache.Acquire(httptest.NewRequest(http.MethodGet, "/", nil), uuid.Nil) + require.NoError(t, err) + release() + <-conn.Closed() + require.Equal(t, int32(2), called.Load()) + }) + t.Run("NoExpireWhenLocked", func(t *testing.T) { + t.Parallel() + cache := wsconncache.New(func(r *http.Request, id uuid.UUID) (*agent.Conn, error) { + return setupAgent(t, agent.Metadata{}, 0), nil + }, time.Microsecond) + t.Cleanup(func() { + _ = cache.Close() + }) + conn, release, err := cache.Acquire(httptest.NewRequest(http.MethodGet, "/", nil), uuid.Nil) + require.NoError(t, err) + time.Sleep(time.Millisecond) + release() + <-conn.Closed() + }) + t.Run("HTTPTransport", func(t *testing.T) { + t.Parallel() + random, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + t.Cleanup(func() { + _ = random.Close() + }) + tcpAddr, valid := random.Addr().(*net.TCPAddr) + require.True(t, valid) + + server := &http.Server{ + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }), + } + t.Cleanup(func() { + _ = server.Close() + }) + go server.Serve(random) + + cache := wsconncache.New(func(r *http.Request, id uuid.UUID) (*agent.Conn, error) { + return setupAgent(t, agent.Metadata{}, 0), nil + }, time.Microsecond) + t.Cleanup(func() { + _ = cache.Close() + }) + + var wg sync.WaitGroup + // Perform many requests in parallel to simulate + // simultaneous HTTP requests. + for i := 0; i < 50; i++ { + wg.Add(1) + go func() { + defer wg.Done() + proxy := httputil.NewSingleHostReverseProxy(&url.URL{ + Scheme: "http", + Host: fmt.Sprintf("127.0.0.1:%d", tcpAddr.Port), + Path: "/", + }) + req := httptest.NewRequest(http.MethodGet, "/", nil) + conn, release, err := cache.Acquire(req, uuid.Nil) + if !assert.NoError(t, err) { + return + } + defer release() + proxy.Transport = conn.HTTPTransport() + res := httptest.NewRecorder() + proxy.ServeHTTP(res, req) + res.Result().Body.Close() + require.Equal(t, http.StatusOK, res.Result().StatusCode) + }() + } + wg.Wait() + }) +} + +func setupAgent(t *testing.T, metadata agent.Metadata, ptyTimeout time.Duration) *agent.Conn { + client, server := provisionersdk.TransportPipe() + closer := agent.New(func(ctx context.Context, logger slog.Logger) (agent.Metadata, *peerbroker.Listener, error) { + listener, err := peerbroker.Listen(server, func(ctx context.Context) ([]webrtc.ICEServer, *peer.ConnOptions, error) { + return nil, &peer.ConnOptions{ + Logger: slogtest.Make(t, nil).Named("server").Leveled(slog.LevelDebug), + }, nil + }) + return metadata, listener, err + }, &agent.Options{ + Logger: slogtest.Make(t, nil).Named("agent").Leveled(slog.LevelDebug), + ReconnectingPTYTimeout: ptyTimeout, + }) + t.Cleanup(func() { + _ = client.Close() + _ = server.Close() + _ = closer.Close() + }) + api := proto.NewDRPCPeerBrokerClient(provisionersdk.Conn(client)) + stream, err := api.NegotiateConnection(context.Background()) + assert.NoError(t, err) + conn, err := peerbroker.Dial(stream, []webrtc.ICEServer{}, &peer.ConnOptions{ + Logger: slogtest.Make(t, nil).Named("client").Leveled(slog.LevelDebug), + }) + require.NoError(t, err) + t.Cleanup(func() { + _ = conn.Close() + }) + + return &agent.Conn{ + Negotiator: api, + Conn: conn, + } +} diff --git a/codersdk/workspaceagents.go b/codersdk/workspaceagents.go index c634b1de7ea2a..f31ed07dd010a 100644 --- a/codersdk/workspaceagents.go +++ b/codersdk/workspaceagents.go @@ -341,8 +341,8 @@ func (c *Client) WorkspaceAgent(ctx context.Context, id uuid.UUID) (WorkspaceAge // WorkspaceAgentReconnectingPTY spawns a PTY that reconnects using the token provided. // It communicates using `agent.ReconnectingPTYRequest` marshaled as JSON. // Responses are PTY output that can be rendered. -func (c *Client) WorkspaceAgentReconnectingPTY(ctx context.Context, agentID, reconnect uuid.UUID, height, width int) (net.Conn, error) { - serverURL, err := c.URL.Parse(fmt.Sprintf("/api/v2/workspaceagents/%s/pty?reconnect=%s&height=%d&width=%d", agentID, reconnect, height, width)) +func (c *Client) WorkspaceAgentReconnectingPTY(ctx context.Context, agentID, reconnect uuid.UUID, height, width int, command string) (net.Conn, error) { + serverURL, err := c.URL.Parse(fmt.Sprintf("/api/v2/workspaceagents/%s/pty?reconnect=%s&height=%d&width=%d&command=%s", agentID, reconnect, height, width, command)) if err != nil { return nil, xerrors.Errorf("parse url: %w", err) } diff --git a/codersdk/workspaceapps.go b/codersdk/workspaceapps.go new file mode 100644 index 0000000000000..d993a4dcf49ba --- /dev/null +++ b/codersdk/workspaceapps.go @@ -0,0 +1,15 @@ +package codersdk + +import ( + "github.com/google/uuid" +) + +type WorkspaceApp struct { + ID uuid.UUID `json:"id"` + // Name is a unique identifier attached to an agent. + Name string `json:"name"` + Command string `json:"command,omitempty"` + // 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/codersdk/workspaceresources.go b/codersdk/workspaceresources.go index 97ae03186cf39..3259b342c7a08 100644 --- a/codersdk/workspaceresources.go +++ b/codersdk/workspaceresources.go @@ -44,6 +44,7 @@ type WorkspaceAgent struct { OperatingSystem string `json:"operating_system"` StartupScript string `json:"startup_script,omitempty"` Directory string `json:"directory,omitempty"` + Apps []WorkspaceApp `json:"apps"` } type WorkspaceAgentResourceMetadata struct { diff --git a/examples/templates/docker-code-server/README.md b/examples/templates/docker-code-server/README.md new file mode 100644 index 0000000000000..995773a1b66ab --- /dev/null +++ b/examples/templates/docker-code-server/README.md @@ -0,0 +1,11 @@ +--- +name: Develop code-server in Docker +description: Run code-server in a Docker development environment +tags: [local, docker] +--- + +# code-server in Docker + +## Getting started + +Run `coder templates init` and select this template. Follow the instructions that appear. diff --git a/examples/templates/docker-code-server/main.tf b/examples/templates/docker-code-server/main.tf new file mode 100644 index 0000000000000..d46ab3b3eaa04 --- /dev/null +++ b/examples/templates/docker-code-server/main.tf @@ -0,0 +1,45 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + version = "0.4.2" + } + docker = { + source = "kreuzwerker/docker" + version = "~> 2.16.0" + } + } +} + +provider "coder" { +} + +data "coder_workspace" "me" { +} + +resource "coder_agent" "dev" { + arch = "amd64" + os = "linux" + startup_script = "code-server --auth none" +} + +resource "coder_app" "code-server" { + agent_id = coder_agent.dev.id + url = "http://localhost:8080/?folder=/home/coder" +} + +resource "docker_container" "workspace" { + count = data.coder_workspace.me.start_count + image = "codercom/code-server:latest" + # Uses lower() to avoid Docker restriction on container names. + name = "coder-${data.coder_workspace.me.owner}-${lower(data.coder_workspace.me.name)}" + hostname = lower(data.coder_workspace.me.name) + dns = ["1.1.1.1"] + # Use the docker gateway if the access URL is 127.0.0.1 + entrypoint = ["sh", "-c", replace(coder_agent.dev.init_script, "127.0.0.1", "host.docker.internal")] + env = ["CODER_AGENT_TOKEN=${coder_agent.dev.token}"] + host { + host = "host.docker.internal" + ip = "host-gateway" + } +} diff --git a/peer/channel.go b/peer/channel.go index 4af4fbcb465f2..c0415e50baa1a 100644 --- a/peer/channel.go +++ b/peer/channel.go @@ -6,7 +6,6 @@ import ( "io" "net" "sync" - "time" "github.com/pion/datachannel" "github.com/pion/webrtc/v3" @@ -136,12 +135,12 @@ func (c *Channel) init() { // is triggerred with a buffer size less than the chunks written. // // This makes sense when considering UDP connections, because - // bufferring of data that has no transmit guarantees is likely + // buffering of data that has no transmit guarantees is likely // to cause unexpected behavior. // // When ordered, this adds a bufio.Reader. This ensures additional // data on TCP-like connections can be read in parts, while still - // being bufferred. + // being buffered. if c.opts.Unordered { c.reader = c.rwc } else { @@ -193,11 +192,9 @@ func (c *Channel) Read(bytes []byte) (int, error) { if c.isClosed() { return 0, c.closeError } - if !c.isOpened() { - err := c.waitOpened() - if err != nil { - return 0, err - } + err := c.waitOpened() + if err != nil { + return 0, err } bytesRead, err := c.reader.Read(bytes) @@ -211,7 +208,6 @@ func (c *Channel) Read(bytes []byte) (int, error) { if xerrors.Is(err, io.EOF) { err = c.closeWithError(ErrClosed) } - return bytesRead, err } return bytesRead, err } @@ -235,24 +231,14 @@ func (c *Channel) Write(bytes []byte) (n int, err error) { if c.isClosed() { return 0, c.closeWithError(nil) } - if !c.isOpened() { - err := c.waitOpened() - if err != nil { - return 0, err - } + err = c.waitOpened() + if err != nil { + return 0, err } - if c.dc.BufferedAmount()+uint64(len(bytes)) >= maxBufferedAmount { <-c.sendMore } - // There's an obvious race-condition here. This is an edge-case, as - // most-frequently data won't be pooled so synchronously, but is - // definitely possible. - // - // See: https://github.com/pion/sctp/issues/181 - time.Sleep(time.Microsecond) - return c.rwc.Write(bytes) } @@ -319,15 +305,6 @@ func (c *Channel) isClosed() bool { } } -func (c *Channel) isOpened() bool { - select { - case <-c.opened: - return true - default: - return false - } -} - func (c *Channel) waitOpened() error { select { case <-c.opened: diff --git a/peer/conn.go b/peer/conn.go index ed2f8dc746a8d..8eae101ccdbbe 100644 --- a/peer/conn.go +++ b/peer/conn.go @@ -114,6 +114,7 @@ type Conn struct { closeMutex sync.Mutex closeError error + dcCreateMutex sync.Mutex dcOpenChannel chan *webrtc.DataChannel dcDisconnectChannel chan struct{} dcDisconnectListeners atomic.Uint32 @@ -483,6 +484,11 @@ func (c *Conn) CreateChannel(ctx context.Context, label string, opts *ChannelOpt } func (c *Conn) dialChannel(ctx context.Context, label string, opts *ChannelOptions) (*Channel, error) { + // pion/webrtc is slower when opening multiple channels + // in parallel than it is sequentially. + c.dcCreateMutex.Lock() + defer c.dcCreateMutex.Unlock() + c.logger().Debug(ctx, "creating data channel", slog.F("label", label), slog.F("opts", opts)) var id *uint16 if opts.ID != 0 { diff --git a/provisioner/terraform/provision.go b/provisioner/terraform/provision.go index acffae785b759..61e426467dc37 100644 --- a/provisioner/terraform/provision.go +++ b/provisioner/terraform/provision.go @@ -397,7 +397,7 @@ func parseTerraformPlan(ctx context.Context, terraform *tfexec.Terraform, planfi if resource.Mode == tfjson.DataResourceMode { continue } - if resource.Type == "coder_agent" || resource.Type == "coder_agent_instance" { + if resource.Type == "coder_agent" || resource.Type == "coder_agent_instance" || resource.Type == "coder_app" { continue } resources = append(resources, &proto.Resource{ @@ -520,11 +520,47 @@ func parseTerraformApply(ctx context.Context, terraform *tfexec.Terraform, state } } + type appAttributes struct { + AgentID string `mapstructure:"agent_id"` + Name string `mapstructure:"name"` + Icon string `mapstructure:"icon"` + URL string `mapstructure:"url"` + Command string `mapstructure:"command"` + RelativePath bool `mapstructure:"relative_path"` + } + // Associate Apps with agents. + for _, resource := range state.Values.RootModule.Resources { + if resource.Type != "coder_app" { + continue + } + var attrs appAttributes + 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 + } + for _, agent := range agents { + if agent.Id != attrs.AgentID { + continue + } + agent.Apps = append(agent.Apps, &proto.App{ + Name: attrs.Name, + Command: attrs.Command, + Url: attrs.URL, + Icon: attrs.Icon, + RelativePath: attrs.RelativePath, + }) + } + } + for _, resource := range tfResources { if resource.Mode == tfjson.DataResourceMode { continue } - if resource.Type == "coder_agent" || resource.Type == "coder_agent_instance" { + if resource.Type == "coder_agent" || resource.Type == "coder_agent_instance" || resource.Type == "coder_app" { continue } resourceAgents := findAgents(resourceDependencies, agents, convertAddressToLabel(resource.Address)) diff --git a/provisioner/terraform/provision_test.go b/provisioner/terraform/provision_test.go index 05d781dc8d8ff..294a70d3deb8c 100644 --- a/provisioner/terraform/provision_test.go +++ b/provisioner/terraform/provision_test.go @@ -30,7 +30,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.3.4" + version = "0.4.2" } } } @@ -438,6 +438,52 @@ provider "coder" { }, }, }, + }, { + Name: "agent-with-app", + Files: map[string]string{ + "main.tf": provider + ` + resource "coder_agent" "A" { + os = "darwin" + arch = "amd64" + } + resource "null_resource" "A" { + depends_on = [ + coder_agent.A + ] + } + resource "coder_app" "A" { + agent_id = coder_agent.A.id + command = "vim" + } + `, + }, + Request: &proto.Provision_Request{ + Type: &proto.Provision_Request_Start{ + Start: &proto.Provision_Start{ + Metadata: &proto.Provision_Metadata{}, + }, + }, + }, + Response: &proto.Provision_Response{ + Type: &proto.Provision_Response_Complete{ + Complete: &proto.Provision_Complete{ + Resources: []*proto.Resource{{ + Name: "A", + Type: "null_resource", + Agents: []*proto.Agent{{ + Name: "A", + OperatingSystem: "darwin", + Architecture: "amd64", + Auth: &proto.Agent_Token{}, + Apps: []*proto.App{{ + Name: "A", + Command: "vim", + }}, + }}, + }}, + }, + }, + }, }} { testCase := testCase t.Run(testCase.Name, func(t *testing.T) { diff --git a/provisionersdk/proto/provisioner.pb.go b/provisionersdk/proto/provisioner.pb.go index 01d68d25a8619..eee976e770aa9 100644 --- a/provisionersdk/proto/provisioner.pb.go +++ b/provisionersdk/proto/provisioner.pb.go @@ -711,6 +711,7 @@ type Agent struct { OperatingSystem string `protobuf:"bytes,5,opt,name=operating_system,json=operatingSystem,proto3" json:"operating_system,omitempty"` Architecture string `protobuf:"bytes,6,opt,name=architecture,proto3" json:"architecture,omitempty"` Directory string `protobuf:"bytes,7,opt,name=directory,proto3" json:"directory,omitempty"` + Apps []*App `protobuf:"bytes,8,rep,name=apps,proto3" json:"apps,omitempty"` // Types that are assignable to Auth: // *Agent_Token // *Agent_InstanceId @@ -798,6 +799,13 @@ func (x *Agent) GetDirectory() string { return "" } +func (x *Agent) GetApps() []*App { + if x != nil { + return x.Apps + } + return nil +} + func (m *Agent) GetAuth() isAgent_Auth { if m != nil { return m.Auth @@ -824,17 +832,97 @@ type isAgent_Auth interface { } type Agent_Token struct { - Token string `protobuf:"bytes,8,opt,name=token,proto3,oneof"` + Token string `protobuf:"bytes,9,opt,name=token,proto3,oneof"` } type Agent_InstanceId struct { - InstanceId string `protobuf:"bytes,9,opt,name=instance_id,json=instanceId,proto3,oneof"` + InstanceId string `protobuf:"bytes,10,opt,name=instance_id,json=instanceId,proto3,oneof"` } func (*Agent_Token) isAgent_Auth() {} func (*Agent_InstanceId) isAgent_Auth() {} +// App represents a dev-accessible application on the workspace. +type App struct { + state protoimpl.MessageState + 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"` + RelativePath bool `protobuf:"varint,5,opt,name=relative_path,json=relativePath,proto3" json:"relative_path,omitempty"` +} + +func (x *App) Reset() { + *x = App{} + if protoimpl.UnsafeEnabled { + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *App) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*App) ProtoMessage() {} + +func (x *App) ProtoReflect() protoreflect.Message { + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[8] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use App.ProtoReflect.Descriptor instead. +func (*App) Descriptor() ([]byte, []int) { + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{8} +} + +func (x *App) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *App) GetCommand() string { + if x != nil { + return x.Command + } + return "" +} + +func (x *App) GetUrl() string { + if x != nil { + return x.Url + } + return "" +} + +func (x *App) GetIcon() string { + if x != nil { + return x.Icon + } + return "" +} + +func (x *App) GetRelativePath() bool { + if x != nil { + return x.RelativePath + } + return false +} + // Resource represents created infrastructure. type Resource struct { state protoimpl.MessageState @@ -849,7 +937,7 @@ type Resource struct { func (x *Resource) Reset() { *x = Resource{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[8] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -862,7 +950,7 @@ func (x *Resource) String() string { func (*Resource) ProtoMessage() {} func (x *Resource) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[8] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[9] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -875,7 +963,7 @@ func (x *Resource) ProtoReflect() protoreflect.Message { // Deprecated: Use Resource.ProtoReflect.Descriptor instead. func (*Resource) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{8} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{9} } func (x *Resource) GetName() string { @@ -909,7 +997,7 @@ type Parse struct { func (x *Parse) Reset() { *x = Parse{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[9] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -922,7 +1010,7 @@ func (x *Parse) String() string { func (*Parse) ProtoMessage() {} func (x *Parse) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[9] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[10] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -935,7 +1023,7 @@ func (x *Parse) ProtoReflect() protoreflect.Message { // Deprecated: Use Parse.ProtoReflect.Descriptor instead. func (*Parse) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{9} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{10} } // Provision consumes source-code from a directory to produce resources. @@ -948,7 +1036,7 @@ type Provision struct { func (x *Provision) Reset() { *x = Provision{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[10] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -961,7 +1049,7 @@ func (x *Provision) String() string { func (*Provision) ProtoMessage() {} func (x *Provision) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[10] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[11] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -974,7 +1062,7 @@ func (x *Provision) ProtoReflect() protoreflect.Message { // Deprecated: Use Provision.ProtoReflect.Descriptor instead. func (*Provision) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{10} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{11} } type Parse_Request struct { @@ -988,7 +1076,7 @@ type Parse_Request struct { func (x *Parse_Request) Reset() { *x = Parse_Request{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[12] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1001,7 +1089,7 @@ func (x *Parse_Request) String() string { func (*Parse_Request) ProtoMessage() {} func (x *Parse_Request) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[12] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[13] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1014,7 +1102,7 @@ func (x *Parse_Request) ProtoReflect() protoreflect.Message { // Deprecated: Use Parse_Request.ProtoReflect.Descriptor instead. func (*Parse_Request) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{9, 0} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{10, 0} } func (x *Parse_Request) GetDirectory() string { @@ -1035,7 +1123,7 @@ type Parse_Complete struct { func (x *Parse_Complete) Reset() { *x = Parse_Complete{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[13] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1048,7 +1136,7 @@ func (x *Parse_Complete) String() string { func (*Parse_Complete) ProtoMessage() {} func (x *Parse_Complete) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[13] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[14] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1061,7 +1149,7 @@ func (x *Parse_Complete) ProtoReflect() protoreflect.Message { // Deprecated: Use Parse_Complete.ProtoReflect.Descriptor instead. func (*Parse_Complete) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{9, 1} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{10, 1} } func (x *Parse_Complete) GetParameterSchemas() []*ParameterSchema { @@ -1085,7 +1173,7 @@ type Parse_Response struct { func (x *Parse_Response) Reset() { *x = Parse_Response{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[14] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1098,7 +1186,7 @@ func (x *Parse_Response) String() string { func (*Parse_Response) ProtoMessage() {} func (x *Parse_Response) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[14] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[15] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1111,7 +1199,7 @@ func (x *Parse_Response) ProtoReflect() protoreflect.Message { // Deprecated: Use Parse_Response.ProtoReflect.Descriptor instead. func (*Parse_Response) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{9, 2} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{10, 2} } func (m *Parse_Response) GetType() isParse_Response_Type { @@ -1167,7 +1255,7 @@ type Provision_Metadata struct { func (x *Provision_Metadata) Reset() { *x = Provision_Metadata{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[15] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1180,7 +1268,7 @@ func (x *Provision_Metadata) String() string { func (*Provision_Metadata) ProtoMessage() {} func (x *Provision_Metadata) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[15] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[16] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1193,7 +1281,7 @@ func (x *Provision_Metadata) ProtoReflect() protoreflect.Message { // Deprecated: Use Provision_Metadata.ProtoReflect.Descriptor instead. func (*Provision_Metadata) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{10, 0} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{11, 0} } func (x *Provision_Metadata) GetCoderUrl() string { @@ -1253,7 +1341,7 @@ type Provision_Start struct { func (x *Provision_Start) Reset() { *x = Provision_Start{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[16] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1266,7 +1354,7 @@ func (x *Provision_Start) String() string { func (*Provision_Start) ProtoMessage() {} func (x *Provision_Start) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[16] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[17] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1279,7 +1367,7 @@ func (x *Provision_Start) ProtoReflect() protoreflect.Message { // Deprecated: Use Provision_Start.ProtoReflect.Descriptor instead. func (*Provision_Start) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{10, 1} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{11, 1} } func (x *Provision_Start) GetDirectory() string { @@ -1326,7 +1414,7 @@ type Provision_Cancel struct { func (x *Provision_Cancel) Reset() { *x = Provision_Cancel{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[17] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1339,7 +1427,7 @@ func (x *Provision_Cancel) String() string { func (*Provision_Cancel) ProtoMessage() {} func (x *Provision_Cancel) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[17] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[18] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1352,7 +1440,7 @@ func (x *Provision_Cancel) ProtoReflect() protoreflect.Message { // Deprecated: Use Provision_Cancel.ProtoReflect.Descriptor instead. func (*Provision_Cancel) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{10, 2} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{11, 2} } type Provision_Request struct { @@ -1369,7 +1457,7 @@ type Provision_Request struct { func (x *Provision_Request) Reset() { *x = Provision_Request{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[18] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1382,7 +1470,7 @@ func (x *Provision_Request) String() string { func (*Provision_Request) ProtoMessage() {} func (x *Provision_Request) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[18] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[19] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1395,7 +1483,7 @@ func (x *Provision_Request) ProtoReflect() protoreflect.Message { // Deprecated: Use Provision_Request.ProtoReflect.Descriptor instead. func (*Provision_Request) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{10, 3} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{11, 3} } func (m *Provision_Request) GetType() isProvision_Request_Type { @@ -1448,7 +1536,7 @@ type Provision_Complete struct { func (x *Provision_Complete) Reset() { *x = Provision_Complete{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[19] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1461,7 +1549,7 @@ func (x *Provision_Complete) String() string { func (*Provision_Complete) ProtoMessage() {} func (x *Provision_Complete) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[19] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[20] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1474,7 +1562,7 @@ func (x *Provision_Complete) ProtoReflect() protoreflect.Message { // Deprecated: Use Provision_Complete.ProtoReflect.Descriptor instead. func (*Provision_Complete) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{10, 4} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{11, 4} } func (x *Provision_Complete) GetState() []byte { @@ -1512,7 +1600,7 @@ type Provision_Response struct { func (x *Provision_Response) Reset() { *x = Provision_Response{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[20] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1525,7 +1613,7 @@ func (x *Provision_Response) String() string { func (*Provision_Response) ProtoMessage() {} func (x *Provision_Response) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[20] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[21] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1538,7 +1626,7 @@ func (x *Provision_Response) ProtoReflect() protoreflect.Message { // Deprecated: Use Provision_Response.ProtoReflect.Descriptor instead. func (*Provision_Response) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{10, 5} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{11, 5} } func (m *Provision_Response) GetType() isProvision_Response_Type { @@ -1660,7 +1748,7 @@ var file_provisionersdk_proto_provisioner_proto_rawDesc = []byte{ 0x70, 0x75, 0x74, 0x22, 0x37, 0x0a, 0x14, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x41, 0x75, 0x74, 0x68, 0x12, 0x1f, 0x0a, 0x0b, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0a, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x64, 0x22, 0xe9, 0x02, 0x0a, + 0x52, 0x0a, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x64, 0x22, 0x8f, 0x03, 0x0a, 0x05, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2d, 0x0a, 0x03, 0x65, 0x6e, @@ -1675,114 +1763,125 @@ var file_provisionersdk_proto_provisioner_proto_rawDesc = []byte{ 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x61, 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x07, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x16, 0x0a, - 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x05, - 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x21, 0x0a, 0x0b, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, - 0x65, 0x5f, 0x69, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0a, 0x69, 0x6e, - 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x64, 0x1a, 0x36, 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, 0x5e, 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, 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, 0xfa, 0x06, 0x0a, 0x09, 0x50, 0x72, 0x6f, 0x76, - 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x1a, 0x9d, 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, 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, + 0x28, 0x09, 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x24, 0x0a, + 0x04, 0x61, 0x70, 0x70, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x72, + 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x41, 0x70, 0x70, 0x52, 0x04, 0x61, + 0x70, 0x70, 0x73, 0x12, 0x16, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x09, 0x20, 0x01, + 0x28, 0x09, 0x48, 0x00, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x21, 0x0a, 0x0b, 0x69, + 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, + 0x48, 0x00, 0x52, 0x0a, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x64, 0x1a, 0x36, + 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, 0x7e, + 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, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x6c, + 0x61, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x0c, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x76, 0x65, 0x50, 0x61, 0x74, 0x68, 0x22, 0x5e, + 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, 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, 0xfa, 0x06, + 0x0a, 0x09, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x1a, 0x9d, 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, 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, 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, 0x37, 0x0a, 0x13, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, - 0x63, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x09, 0x0a, 0x05, - 0x53, 0x54, 0x41, 0x52, 0x54, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x53, 0x54, 0x4f, 0x50, 0x10, - 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x53, 0x54, 0x52, 0x4f, 0x59, 0x10, 0x02, 0x32, 0xa3, - 0x01, 0x0a, 0x0b, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x12, 0x42, - 0x0a, 0x05, 0x50, 0x61, 0x72, 0x73, 0x65, 0x12, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, - 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x2e, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, - 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x30, 0x01, 0x12, 0x50, 0x0a, 0x09, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x12, - 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, - 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, - 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x28, 0x01, 0x30, 0x01, 0x42, 0x2d, 0x5a, 0x2b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, - 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x70, - 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x73, 0x64, 0x6b, 0x2f, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 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, 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 ( @@ -1798,7 +1897,7 @@ func file_provisionersdk_proto_provisioner_proto_rawDescGZIP() []byte { } var file_provisionersdk_proto_provisioner_proto_enumTypes = make([]protoimpl.EnumInfo, 5) -var file_provisionersdk_proto_provisioner_proto_msgTypes = make([]protoimpl.MessageInfo, 21) +var file_provisionersdk_proto_provisioner_proto_msgTypes = make([]protoimpl.MessageInfo, 22) var file_provisionersdk_proto_provisioner_proto_goTypes = []interface{}{ (LogLevel)(0), // 0: provisioner.LogLevel (WorkspaceTransition)(0), // 1: provisioner.WorkspaceTransition @@ -1813,19 +1912,20 @@ var file_provisionersdk_proto_provisioner_proto_goTypes = []interface{}{ (*Log)(nil), // 10: provisioner.Log (*InstanceIdentityAuth)(nil), // 11: provisioner.InstanceIdentityAuth (*Agent)(nil), // 12: provisioner.Agent - (*Resource)(nil), // 13: provisioner.Resource - (*Parse)(nil), // 14: provisioner.Parse - (*Provision)(nil), // 15: provisioner.Provision - nil, // 16: provisioner.Agent.EnvEntry - (*Parse_Request)(nil), // 17: provisioner.Parse.Request - (*Parse_Complete)(nil), // 18: provisioner.Parse.Complete - (*Parse_Response)(nil), // 19: provisioner.Parse.Response - (*Provision_Metadata)(nil), // 20: provisioner.Provision.Metadata - (*Provision_Start)(nil), // 21: provisioner.Provision.Start - (*Provision_Cancel)(nil), // 22: provisioner.Provision.Cancel - (*Provision_Request)(nil), // 23: provisioner.Provision.Request - (*Provision_Complete)(nil), // 24: provisioner.Provision.Complete - (*Provision_Response)(nil), // 25: provisioner.Provision.Response + (*App)(nil), // 13: provisioner.App + (*Resource)(nil), // 14: provisioner.Resource + (*Parse)(nil), // 15: provisioner.Parse + (*Provision)(nil), // 16: provisioner.Provision + nil, // 17: provisioner.Agent.EnvEntry + (*Parse_Request)(nil), // 18: provisioner.Parse.Request + (*Parse_Complete)(nil), // 19: provisioner.Parse.Complete + (*Parse_Response)(nil), // 20: provisioner.Parse.Response + (*Provision_Metadata)(nil), // 21: provisioner.Provision.Metadata + (*Provision_Start)(nil), // 22: provisioner.Provision.Start + (*Provision_Cancel)(nil), // 23: provisioner.Provision.Cancel + (*Provision_Request)(nil), // 24: provisioner.Provision.Request + (*Provision_Complete)(nil), // 25: provisioner.Provision.Complete + (*Provision_Response)(nil), // 26: provisioner.Provision.Response } var file_provisionersdk_proto_provisioner_proto_depIdxs = []int32{ 2, // 0: provisioner.ParameterSource.scheme:type_name -> provisioner.ParameterSource.Scheme @@ -1835,28 +1935,29 @@ var file_provisionersdk_proto_provisioner_proto_depIdxs = []int32{ 7, // 4: provisioner.ParameterSchema.default_destination:type_name -> provisioner.ParameterDestination 4, // 5: provisioner.ParameterSchema.validation_type_system:type_name -> provisioner.ParameterSchema.TypeSystem 0, // 6: provisioner.Log.level:type_name -> provisioner.LogLevel - 16, // 7: provisioner.Agent.env:type_name -> provisioner.Agent.EnvEntry - 12, // 8: provisioner.Resource.agents:type_name -> provisioner.Agent - 9, // 9: provisioner.Parse.Complete.parameter_schemas:type_name -> provisioner.ParameterSchema - 10, // 10: provisioner.Parse.Response.log:type_name -> provisioner.Log - 18, // 11: provisioner.Parse.Response.complete:type_name -> provisioner.Parse.Complete - 1, // 12: provisioner.Provision.Metadata.workspace_transition:type_name -> provisioner.WorkspaceTransition - 8, // 13: provisioner.Provision.Start.parameter_values:type_name -> provisioner.ParameterValue - 20, // 14: provisioner.Provision.Start.metadata:type_name -> provisioner.Provision.Metadata - 21, // 15: provisioner.Provision.Request.start:type_name -> provisioner.Provision.Start - 22, // 16: provisioner.Provision.Request.cancel:type_name -> provisioner.Provision.Cancel - 13, // 17: provisioner.Provision.Complete.resources:type_name -> provisioner.Resource - 10, // 18: provisioner.Provision.Response.log:type_name -> provisioner.Log - 24, // 19: provisioner.Provision.Response.complete:type_name -> provisioner.Provision.Complete - 17, // 20: provisioner.Provisioner.Parse:input_type -> provisioner.Parse.Request - 23, // 21: provisioner.Provisioner.Provision:input_type -> provisioner.Provision.Request - 19, // 22: provisioner.Provisioner.Parse:output_type -> provisioner.Parse.Response - 25, // 23: provisioner.Provisioner.Provision:output_type -> provisioner.Provision.Response - 22, // [22:24] is the sub-list for method output_type - 20, // [20:22] is the sub-list for method input_type - 20, // [20:20] is the sub-list for extension type_name - 20, // [20:20] is the sub-list for extension extendee - 0, // [0:20] is the sub-list for field type_name + 17, // 7: provisioner.Agent.env:type_name -> provisioner.Agent.EnvEntry + 13, // 8: provisioner.Agent.apps:type_name -> provisioner.App + 12, // 9: provisioner.Resource.agents:type_name -> provisioner.Agent + 9, // 10: provisioner.Parse.Complete.parameter_schemas:type_name -> provisioner.ParameterSchema + 10, // 11: provisioner.Parse.Response.log:type_name -> provisioner.Log + 19, // 12: provisioner.Parse.Response.complete:type_name -> provisioner.Parse.Complete + 1, // 13: provisioner.Provision.Metadata.workspace_transition:type_name -> provisioner.WorkspaceTransition + 8, // 14: provisioner.Provision.Start.parameter_values:type_name -> provisioner.ParameterValue + 21, // 15: provisioner.Provision.Start.metadata:type_name -> provisioner.Provision.Metadata + 22, // 16: provisioner.Provision.Request.start:type_name -> provisioner.Provision.Start + 23, // 17: provisioner.Provision.Request.cancel:type_name -> provisioner.Provision.Cancel + 14, // 18: provisioner.Provision.Complete.resources:type_name -> provisioner.Resource + 10, // 19: provisioner.Provision.Response.log:type_name -> provisioner.Log + 25, // 20: provisioner.Provision.Response.complete:type_name -> provisioner.Provision.Complete + 18, // 21: provisioner.Provisioner.Parse:input_type -> provisioner.Parse.Request + 24, // 22: provisioner.Provisioner.Provision:input_type -> provisioner.Provision.Request + 20, // 23: provisioner.Provisioner.Parse:output_type -> provisioner.Parse.Response + 26, // 24: provisioner.Provisioner.Provision:output_type -> provisioner.Provision.Response + 23, // [23:25] is the sub-list for method output_type + 21, // [21:23] is the sub-list for method input_type + 21, // [21:21] is the sub-list for extension type_name + 21, // [21:21] is the sub-list for extension extendee + 0, // [0:21] is the sub-list for field type_name } func init() { file_provisionersdk_proto_provisioner_proto_init() } @@ -1962,7 +2063,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Resource); i { + switch v := v.(*App); i { case 0: return &v.state case 1: @@ -1974,7 +2075,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Parse); i { + switch v := v.(*Resource); i { case 0: return &v.state case 1: @@ -1986,6 +2087,18 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Parse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_provisionersdk_proto_provisioner_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Provision); i { case 0: return &v.state @@ -1997,7 +2110,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { return nil } } - file_provisionersdk_proto_provisioner_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { + file_provisionersdk_proto_provisioner_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Parse_Request); i { case 0: return &v.state @@ -2009,7 +2122,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { return nil } } - file_provisionersdk_proto_provisioner_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { + file_provisionersdk_proto_provisioner_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Parse_Complete); i { case 0: return &v.state @@ -2021,7 +2134,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { return nil } } - file_provisionersdk_proto_provisioner_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { + file_provisionersdk_proto_provisioner_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Parse_Response); i { case 0: return &v.state @@ -2033,7 +2146,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { return nil } } - file_provisionersdk_proto_provisioner_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { + file_provisionersdk_proto_provisioner_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Provision_Metadata); i { case 0: return &v.state @@ -2045,7 +2158,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { return nil } } - file_provisionersdk_proto_provisioner_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { + file_provisionersdk_proto_provisioner_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Provision_Start); i { case 0: return &v.state @@ -2057,7 +2170,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { return nil } } - file_provisionersdk_proto_provisioner_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { + file_provisionersdk_proto_provisioner_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Provision_Cancel); i { case 0: return &v.state @@ -2069,7 +2182,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { return nil } } - file_provisionersdk_proto_provisioner_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { + file_provisionersdk_proto_provisioner_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Provision_Request); i { case 0: return &v.state @@ -2081,7 +2194,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { return nil } } - file_provisionersdk_proto_provisioner_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { + file_provisionersdk_proto_provisioner_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Provision_Complete); i { case 0: return &v.state @@ -2093,7 +2206,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { return nil } } - file_provisionersdk_proto_provisioner_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { + file_provisionersdk_proto_provisioner_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Provision_Response); i { case 0: return &v.state @@ -2110,15 +2223,15 @@ func file_provisionersdk_proto_provisioner_proto_init() { (*Agent_Token)(nil), (*Agent_InstanceId)(nil), } - file_provisionersdk_proto_provisioner_proto_msgTypes[14].OneofWrappers = []interface{}{ + file_provisionersdk_proto_provisioner_proto_msgTypes[15].OneofWrappers = []interface{}{ (*Parse_Response_Log)(nil), (*Parse_Response_Complete)(nil), } - file_provisionersdk_proto_provisioner_proto_msgTypes[18].OneofWrappers = []interface{}{ + file_provisionersdk_proto_provisioner_proto_msgTypes[19].OneofWrappers = []interface{}{ (*Provision_Request_Start)(nil), (*Provision_Request_Cancel)(nil), } - file_provisionersdk_proto_provisioner_proto_msgTypes[20].OneofWrappers = []interface{}{ + file_provisionersdk_proto_provisioner_proto_msgTypes[21].OneofWrappers = []interface{}{ (*Provision_Response_Log)(nil), (*Provision_Response_Complete)(nil), } @@ -2128,7 +2241,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_provisionersdk_proto_provisioner_proto_rawDesc, NumEnums: 5, - NumMessages: 21, + NumMessages: 22, NumExtensions: 0, NumServices: 1, }, diff --git a/provisionersdk/proto/provisioner.proto b/provisionersdk/proto/provisioner.proto index 66bfee737a00a..86e61ca8d9884 100644 --- a/provisionersdk/proto/provisioner.proto +++ b/provisionersdk/proto/provisioner.proto @@ -80,12 +80,22 @@ message Agent { string operating_system = 5; string architecture = 6; string directory = 7; + repeated App apps = 8; oneof auth { - string token = 8; - string instance_id = 9; + string token = 9; + string instance_id = 10; } } +// App represents a dev-accessible application on the workspace. +message App { + string name = 1; + string command = 2; + string url = 3; + string icon = 4; + bool relative_path = 5; +} + // Resource represents created infrastructure. message Resource { string name = 1; diff --git a/site/htmlTemplates/index.html b/site/htmlTemplates/index.html index cc6a0e5e151a5..78099f88007dd 100644 --- a/site/htmlTemplates/index.html +++ b/site/htmlTemplates/index.html @@ -17,6 +17,7 @@ + diff --git a/site/site.go b/site/site.go index 25ac15db91339..af302f997b814 100644 --- a/site/site.go +++ b/site/site.go @@ -2,6 +2,8 @@ package site import ( "bytes" + "context" + "fmt" "io" "io/fs" @@ -17,6 +19,15 @@ import ( "golang.org/x/xerrors" ) +type apiResponseContextKey struct{} + +// WithAPIResponse returns a context with the APIResponse value attached. +// This is used to inject API response data to the index.html for additional +// metadata in error pages. +func WithAPIResponse(ctx context.Context, apiResponse APIResponse) context.Context { + return context.WithValue(ctx, apiResponseContextKey{}, apiResponse) +} + // Handler returns an HTTP handler for serving the static site. func Handler(fileSystem fs.FS) http.Handler { // html files are handled by a text/template. Non-html files @@ -66,8 +77,14 @@ func (h *handler) exists(filePath string) bool { } type htmlState struct { - CSP cspState - CSRF csrfState + APIResponse APIResponse + CSP cspState + CSRF csrfState +} + +type APIResponse struct { + StatusCode int + Message string } type cspState struct { @@ -115,6 +132,15 @@ func (h *handler) ServeHTTP(resp http.ResponseWriter, req *http.Request) { CSRF: csrfState{Token: nosurf.Token(req)}, } + apiResponseRaw := req.Context().Value(apiResponseContextKey{}) + if apiResponseRaw != nil { + apiResponse, ok := apiResponseRaw.(APIResponse) + if !ok { + panic("dev error: api response in context isn't the correct type") + } + state.APIResponse = apiResponse + } + // First check if it's a file we have in our templates if h.serveHTML(resp, req, reqFile, state) { return diff --git a/site/site_test.go b/site/site_test.go index 0008d404c66c3..86c3e22fa3484 100644 --- a/site/site_test.go +++ b/site/site_test.go @@ -2,6 +2,7 @@ package site_test import ( "context" + "encoding/json" "fmt" "io" "net/http" @@ -169,3 +170,40 @@ func TestShouldCacheFile(t *testing.T) { require.Equal(t, testCase.expected, got, fmt.Sprintf("Expected ShouldCacheFile(%s) to be %t", testCase.reqFile, testCase.expected)) } } + +func TestServeAPIResponse(t *testing.T) { + t.Parallel() + + // Create a test server + rootFS := fstest.MapFS{ + "index.html": &fstest.MapFile{ + Data: []byte(`{"code":{{ .APIResponse.StatusCode }},"message":"{{ .APIResponse.Message }}"}`), + }, + } + + apiResponse := site.APIResponse{ + StatusCode: http.StatusBadGateway, + Message: "This could be an error message!", + } + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + r = r.WithContext(site.WithAPIResponse(r.Context(), apiResponse)) + site.Handler(rootFS).ServeHTTP(w, r) + })) + defer srv.Close() + + req, err := http.NewRequestWithContext(context.Background(), "GET", srv.URL, nil) + require.NoError(t, err) + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + var body struct { + Code int `json:"code"` + Message string `json:"message"` + } + data, err := io.ReadAll(resp.Body) + require.NoError(t, err) + t.Logf("resp: %q", data) + err = json.Unmarshal(data, &body) + require.NoError(t, err) + require.Equal(t, apiResponse.StatusCode, body.Code) + require.Equal(t, apiResponse.Message, body.Message) +} diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index e5620abc18fc3..1f227c08aa88e 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -391,6 +391,7 @@ export interface WorkspaceAgent { readonly operating_system: string readonly startup_script?: string readonly directory?: string + readonly apps: WorkspaceApp[] } // From codersdk/workspaceagents.go:47:6 @@ -398,7 +399,7 @@ export interface WorkspaceAgentAuthenticateResponse { readonly session_token: string } -// From codersdk/workspaceresources.go:57:6 +// From codersdk/workspaceresources.go:58:6 export interface WorkspaceAgentInstanceMetadata { readonly jail_orchestrator: string readonly operating_system: string @@ -411,7 +412,7 @@ export interface WorkspaceAgentInstanceMetadata { readonly vnc: boolean } -// From codersdk/workspaceresources.go:49:6 +// From codersdk/workspaceresources.go:50:6 export interface WorkspaceAgentResourceMetadata { readonly memory_total: number readonly disk_total: number @@ -420,6 +421,14 @@ export interface WorkspaceAgentResourceMetadata { readonly cpu_mhz: number } +// From codersdk/workspaceapps.go:7:6 +export interface WorkspaceApp { + readonly id: string + readonly name: string + readonly command?: string + readonly icon?: string +} + // From codersdk/workspacebuilds.go:24:6 export interface WorkspaceBuild { readonly id: string diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index a73a1b14a9077..2c06cb214020d 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -210,7 +210,14 @@ export const MockDeletedWorkspace: TypesGen.Workspace = { ...MockWorkspace, late export const MockOutdatedWorkspace: TypesGen.Workspace = { ...MockFailedWorkspace, outdated: true } +export const MockWorkspaceApp: TypesGen.WorkspaceApp = { + id: "test-app", + name: "test-app", + icon: "", +} + export const MockWorkspaceAgent: TypesGen.WorkspaceAgent = { + apps: [MockWorkspaceApp], architecture: "amd64", created_at: "", environment_variables: {},