From c09c9b90ad90a4d616dba6499e781cbb869fc21b Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Mon, 21 Apr 2025 10:55:43 +0000 Subject: [PATCH 01/42] WIP: agent reinitialization --- agent/agent.go | 13 +- agent/metrics.go | 10 + agent/reaper/reaper_unix.go | 5 + cli/agent.go | 130 ++- coderd/agentapi/api.go | 2 + coderd/agentapi/manifest.go | 6 + coderd/apidoc/docs.go | 45 + coderd/apidoc/swagger.json | 37 + coderd/coderd.go | 4 +- .../provisionerdserver/provisionerdserver.go | 29 + coderd/workspaceagents.go | 99 +++ coderd/workspaces.go | 17 +- coderd/wsbuilder/wsbuilder.go | 21 +- codersdk/agentsdk/agentsdk.go | 70 +- docs/reference/api/agents.md | 32 + docs/reference/api/schemas.md | 30 + go.mod | 3 + provisioner/terraform/executor.go | 11 + provisioner/terraform/provision.go | 16 +- provisionersdk/proto/provisioner.pb.go | 806 ++++++++++-------- provisionersdk/proto/provisioner.proto | 7 +- site/e2e/provisionerGenerated.ts | 23 +- 22 files changed, 990 insertions(+), 426 deletions(-) diff --git a/agent/agent.go b/agent/agent.go index a7434b90d4854..9f4a5d0bd54be 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -36,6 +36,9 @@ import ( "tailscale.com/util/clientmetric" "cdr.dev/slog" + + "github.com/coder/retry" + "github.com/coder/clistat" "github.com/coder/coder/v2/agent/agentcontainers" "github.com/coder/coder/v2/agent/agentexec" @@ -53,7 +56,6 @@ import ( "github.com/coder/coder/v2/tailnet" tailnetproto "github.com/coder/coder/v2/tailnet/proto" "github.com/coder/quartz" - "github.com/coder/retry" ) const ( @@ -365,9 +367,11 @@ func (a *agent) runLoop() { if ctx.Err() != nil { // Context canceled errors may come from websocket pings, so we // don't want to use `errors.Is(err, context.Canceled)` here. + a.logger.Warn(ctx, "runLoop exited with error", slog.Error(ctx.Err())) return } if a.isClosed() { + a.logger.Warn(ctx, "runLoop exited because agent is closed") return } if errors.Is(err, io.EOF) { @@ -1048,7 +1052,12 @@ func (a *agent) run() (retErr error) { return a.statsReporter.reportLoop(ctx, aAPI) }) - return connMan.wait() + err = connMan.wait() + // TODO: this broke some tests at some point. investigate. + if err != nil { + a.logger.Warn(context.Background(), "connection manager errored", slog.Error(err)) + } + return err } // handleManifest returns a function that fetches and processes the manifest diff --git a/agent/metrics.go b/agent/metrics.go index 1755e43a1a365..d0307a647a239 100644 --- a/agent/metrics.go +++ b/agent/metrics.go @@ -20,6 +20,7 @@ type agentMetrics struct { // took to run. This is reported once per agent. startupScriptSeconds *prometheus.GaugeVec currentConnections *prometheus.GaugeVec + manifestsReceived prometheus.Counter } func newAgentMetrics(registerer prometheus.Registerer) *agentMetrics { @@ -54,11 +55,20 @@ func newAgentMetrics(registerer prometheus.Registerer) *agentMetrics { }, []string{"connection_type"}) registerer.MustRegister(currentConnections) + manifestsReceived := prometheus.NewCounter(prometheus.CounterOpts{ + Namespace: "coderd", + Subsystem: "agentstats", + Name: "manifests_received", + Help: "The number of manifests this agent has received from the control plane.", + }) + registerer.MustRegister(manifestsReceived) + return &agentMetrics{ connectionsTotal: connectionsTotal, reconnectingPTYErrors: reconnectingPTYErrors, startupScriptSeconds: startupScriptSeconds, currentConnections: currentConnections, + manifestsReceived: manifestsReceived, } } diff --git a/agent/reaper/reaper_unix.go b/agent/reaper/reaper_unix.go index 35ce9bfaa1c48..5a7c7d2f51efa 100644 --- a/agent/reaper/reaper_unix.go +++ b/agent/reaper/reaper_unix.go @@ -3,6 +3,7 @@ package reaper import ( + "fmt" "os" "os/signal" "syscall" @@ -29,6 +30,10 @@ func catchSignals(pid int, sigs []os.Signal) { s := <-sc sig, ok := s.(syscall.Signal) if ok { + // TODO: + // Tried using a logger here but the I/O streams are already closed at this point... + // Why is os.Stderr still working then? + _, _ = fmt.Fprintf(os.Stderr, "reaper caught %q signal, killing process %v\n", sig.String(), pid) _ = syscall.Kill(pid, sig) } } diff --git a/cli/agent.go b/cli/agent.go index 18c4542a6c3a0..f8252c9b8a699 100644 --- a/cli/agent.go +++ b/cli/agent.go @@ -19,12 +19,16 @@ import ( "golang.org/x/xerrors" "gopkg.in/natefinch/lumberjack.v2" + "github.com/coder/retry" + "github.com/prometheus/client_golang/prometheus" "cdr.dev/slog" "cdr.dev/slog/sloggers/sloghuman" "cdr.dev/slog/sloggers/slogjson" "cdr.dev/slog/sloggers/slogstackdriver" + "github.com/coder/serpent" + "github.com/coder/coder/v2/agent" "github.com/coder/coder/v2/agent/agentcontainers" "github.com/coder/coder/v2/agent/agentexec" @@ -34,7 +38,6 @@ import ( "github.com/coder/coder/v2/cli/clilog" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/agentsdk" - "github.com/coder/serpent" ) func (r *RootCmd) workspaceAgent() *serpent.Command { @@ -63,8 +66,10 @@ func (r *RootCmd) workspaceAgent() *serpent.Command { // This command isn't useful to manually execute. Hidden: true, Handler: func(inv *serpent.Invocation) error { - ctx, cancel := context.WithCancel(inv.Context()) - defer cancel() + ctx, cancel := context.WithCancelCause(inv.Context()) + defer func() { + cancel(xerrors.New("defer")) + }() var ( ignorePorts = map[int]string{} @@ -281,7 +286,6 @@ func (r *RootCmd) workspaceAgent() *serpent.Command { return xerrors.Errorf("add executable to $PATH: %w", err) } - prometheusRegistry := prometheus.NewRegistry() subsystemsRaw := inv.Environ.Get(agent.EnvAgentSubsystem) subsystems := []codersdk.AgentSubsystem{} for _, s := range strings.Split(subsystemsRaw, ",") { @@ -328,46 +332,90 @@ func (r *RootCmd) workspaceAgent() *serpent.Command { containerLister = agentcontainers.NewDocker(execer) } - agnt := agent.New(agent.Options{ - Client: client, - Logger: logger, - LogDir: logDir, - ScriptDataDir: scriptDataDir, - // #nosec G115 - Safe conversion as tailnet listen port is within uint16 range (0-65535) - TailnetListenPort: uint16(tailnetListenPort), - ExchangeToken: func(ctx context.Context) (string, error) { - if exchangeToken == nil { - return client.SDK.SessionToken(), nil + // TODO: timeout ok? + reinitCtx, reinitCancel := context.WithTimeout(context.Background(), time.Hour*24) + defer reinitCancel() + reinitEvents := make(chan agentsdk.ReinitializationResponse) + + go func() { + // Retry to wait for reinit, main context cancels the retrier. + for retrier := retry.New(100*time.Millisecond, 10*time.Second); retrier.Wait(ctx); { + select { + case <-reinitCtx.Done(): + return + default: } - resp, err := exchangeToken(ctx) + + err := client.WaitForReinit(reinitCtx, reinitEvents) if err != nil { - return "", err + logger.Error(ctx, "failed to wait for reinit instructions, will retry", slog.Error(err)) } - client.SetSessionToken(resp.SessionToken) - return resp.SessionToken, nil - }, - EnvironmentVariables: environmentVariables, - IgnorePorts: ignorePorts, - SSHMaxTimeout: sshMaxTimeout, - Subsystems: subsystems, - - PrometheusRegistry: prometheusRegistry, - BlockFileTransfer: blockFileTransfer, - Execer: execer, - ContainerLister: containerLister, - - ExperimentalDevcontainersEnabled: experimentalDevcontainersEnabled, - }) - - promHandler := agent.PrometheusMetricsHandler(prometheusRegistry, logger) - prometheusSrvClose := ServeHandler(ctx, logger, promHandler, prometheusAddress, "prometheus") - defer prometheusSrvClose() - - debugSrvClose := ServeHandler(ctx, logger, agnt.HTTPDebug(), debugAddress, "debug") - defer debugSrvClose() - - <-ctx.Done() - return agnt.Close() + } + }() + + var ( + lastErr error + mustExit bool + ) + for { + prometheusRegistry := prometheus.NewRegistry() + + agnt := agent.New(agent.Options{ + Client: client, + Logger: logger, + LogDir: logDir, + ScriptDataDir: scriptDataDir, + // #nosec G115 - Safe conversion as tailnet listen port is within uint16 range (0-65535) + TailnetListenPort: uint16(tailnetListenPort), + ExchangeToken: func(ctx context.Context) (string, error) { + if exchangeToken == nil { + return client.SDK.SessionToken(), nil + } + resp, err := exchangeToken(ctx) + if err != nil { + return "", err + } + client.SetSessionToken(resp.SessionToken) + return resp.SessionToken, nil + }, + EnvironmentVariables: environmentVariables, + IgnorePorts: ignorePorts, + SSHMaxTimeout: sshMaxTimeout, + Subsystems: subsystems, + + PrometheusRegistry: prometheusRegistry, + BlockFileTransfer: blockFileTransfer, + Execer: execer, + ContainerLister: containerLister, + ExperimentalDevcontainersEnabled: experimentalDevcontainersEnabled, + }) + + promHandler := agent.PrometheusMetricsHandler(prometheusRegistry, logger) + prometheusSrvClose := ServeHandler(ctx, logger, promHandler, prometheusAddress, "prometheus") + + debugSrvClose := ServeHandler(ctx, logger, agnt.HTTPDebug(), debugAddress, "debug") + + select { + case <-ctx.Done(): + logger.Warn(ctx, "agent shutting down", slog.Error(ctx.Err()), slog.F("cause", context.Cause(ctx))) + mustExit = true + case event := <-reinitEvents: + logger.Warn(ctx, "agent received instruction to reinitialize", + slog.F("message", event.Message), slog.F("reason", event.Reason)) + } + + lastErr = agnt.Close() + debugSrvClose() + prometheusSrvClose() + + if mustExit { + reinitCancel() + break + } + + logger.Info(ctx, "reinitializing...") + } + return lastErr }, } diff --git a/coderd/agentapi/api.go b/coderd/agentapi/api.go index 1b2b8d92a10ef..c1bd25b3e6514 100644 --- a/coderd/agentapi/api.go +++ b/coderd/agentapi/api.go @@ -109,6 +109,8 @@ func New(opts Options) *API { Database: opts.Database, DerpMapFn: opts.DerpMapFn, WorkspaceID: opts.WorkspaceID, + Log: opts.Log.Named("manifests"), + Pubsub: opts.Pubsub, } api.AnnouncementBannerAPI = &AnnouncementBannerAPI{ diff --git a/coderd/agentapi/manifest.go b/coderd/agentapi/manifest.go index db8a0af3946a9..f760e24ca6c90 100644 --- a/coderd/agentapi/manifest.go +++ b/coderd/agentapi/manifest.go @@ -8,6 +8,10 @@ import ( "strings" "time" + "cdr.dev/slog" + + "github.com/coder/coder/v2/coderd/database/pubsub" + "github.com/google/uuid" "golang.org/x/sync/errgroup" "golang.org/x/xerrors" @@ -35,6 +39,8 @@ type ManifestAPI struct { AgentFn func(context.Context) (database.WorkspaceAgent, error) Database database.Store DerpMapFn func() *tailcfg.DERPMap + Pubsub pubsub.Pubsub + Log slog.Logger } func (a *ManifestAPI) GetManifest(ctx context.Context, _ *agentproto.GetManifestRequest) (*agentproto.Manifest, error) { diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 62f91a858247d..75b2787bcc08f 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -8252,6 +8252,31 @@ const docTemplate = `{ } } }, + "/workspaceagents/me/reinit": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Agents" + ], + "summary": "Get workspace agent reinitialization", + "operationId": "get-workspace-agent-reinitialization", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/agentsdk.ReinitializationResponse" + } + } + } + } + }, "/workspaceagents/me/rpc": { "get": { "security": [ @@ -10297,6 +10322,26 @@ const docTemplate = `{ } } }, + "agentsdk.ReinitializationReason": { + "type": "string", + "enum": [ + "prebuild_claimed" + ], + "x-enum-varnames": [ + "ReinitializeReasonPrebuildClaimed" + ] + }, + "agentsdk.ReinitializationResponse": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "reason": { + "$ref": "#/definitions/agentsdk.ReinitializationReason" + } + } + }, "coderd.SCIMUser": { "type": "object", "properties": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index e9e0470462b39..e525bf5da1e7a 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -7295,6 +7295,27 @@ } } }, + "/workspaceagents/me/reinit": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": ["application/json"], + "tags": ["Agents"], + "summary": "Get workspace agent reinitialization", + "operationId": "get-workspace-agent-reinitialization", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/agentsdk.ReinitializationResponse" + } + } + } + } + }, "/workspaceagents/me/rpc": { "get": { "security": [ @@ -9134,6 +9155,22 @@ } } }, + "agentsdk.ReinitializationReason": { + "type": "string", + "enum": ["prebuild_claimed"], + "x-enum-varnames": ["ReinitializeReasonPrebuildClaimed"] + }, + "agentsdk.ReinitializationResponse": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "reason": { + "$ref": "#/definitions/agentsdk.ReinitializationReason" + } + } + }, "coderd.SCIMUser": { "type": "object", "properties": { diff --git a/coderd/coderd.go b/coderd/coderd.go index 4a9e3e61d9cf5..a28255abc8d07 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -19,6 +19,8 @@ import ( "sync/atomic" "time" + "github.com/coder/coder/v2/coderd/prebuilds" + "github.com/andybalholm/brotli" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" @@ -45,7 +47,6 @@ import ( "github.com/coder/coder/v2/coderd/entitlements" "github.com/coder/coder/v2/coderd/files" "github.com/coder/coder/v2/coderd/idpsync" - "github.com/coder/coder/v2/coderd/prebuilds" "github.com/coder/coder/v2/coderd/runtimeconfig" "github.com/coder/coder/v2/coderd/webpush" @@ -1277,6 +1278,7 @@ func New(options *Options) *API { r.Get("/external-auth", api.workspaceAgentsExternalAuth) r.Get("/gitsshkey", api.agentGitSSHKey) r.Post("/log-source", api.workspaceAgentPostLogSource) + r.Get("/reinit", api.workspaceAgentReinit) }) r.Route("/{workspaceagent}", func(r chi.Router) { r.Use( diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index 9362d2f3e5a85..c5064e17d8683 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -16,6 +16,8 @@ import ( "sync/atomic" "time" + "github.com/coder/coder/v2/codersdk/agentsdk" + "github.com/google/uuid" "github.com/sqlc-dev/pqtype" semconv "go.opentelemetry.io/otel/semconv/v1.14.0" @@ -617,6 +619,14 @@ func (s *server) acquireProtoJob(ctx context.Context, job database.ProvisionerJo } } + runningAgentAuthTokens := []*sdkproto.RunningAgentAuthToken{} + for agentID, token := range input.RunningAgentAuthTokens { + runningAgentAuthTokens = append(runningAgentAuthTokens, &sdkproto.RunningAgentAuthToken{ + AgentId: agentID.String(), + Token: token, + }) + } + protoJob.Type = &proto.AcquiredJob_WorkspaceBuild_{ WorkspaceBuild: &proto.AcquiredJob_WorkspaceBuild{ WorkspaceBuildId: workspaceBuild.ID.String(), @@ -646,6 +656,7 @@ func (s *server) acquireProtoJob(ctx context.Context, job database.ProvisionerJo WorkspaceOwnerLoginType: string(owner.LoginType), WorkspaceOwnerRbacRoles: ownerRbacRoles, IsPrebuild: input.IsPrebuild, + RunningAgentAuthTokens: runningAgentAuthTokens, }, LogLevel: input.LogLevel, }, @@ -1733,6 +1744,17 @@ func (s *server) CompleteJob(ctx context.Context, completed *proto.CompletedJob) if err != nil { return nil, xerrors.Errorf("update workspace: %w", err) } + + if input.PrebuildClaimedByUser != uuid.Nil { + channel := agentsdk.PrebuildClaimedChannel(workspace.ID) + s.Logger.Info(ctx, "workspace prebuild successfully claimed by user", + slog.F("user", input.PrebuildClaimedByUser.String()), + slog.F("workspace_id", workspace.ID), + slog.F("channel", channel)) + if err := s.Pubsub.Publish(channel, []byte(input.PrebuildClaimedByUser.String())); err != nil { + s.Logger.Error(ctx, "failed to publish message to workspace agent to pull new manifest", slog.Error(err)) + } + } case *proto.CompletedJob_TemplateDryRun_: for _, resource := range jobType.TemplateDryRun.Resources { s.Logger.Info(ctx, "inserting template dry-run job resource", @@ -2476,6 +2498,13 @@ type WorkspaceProvisionJob struct { IsPrebuild bool `json:"is_prebuild,omitempty"` PrebuildClaimedByUser uuid.UUID `json:"prebuild_claimed_by,omitempty"` LogLevel string `json:"log_level,omitempty"` + // RunningAgentAuthTokens is *only* used for prebuilds. We pass it down when we want to rebuild a prebuilt workspace + // but not generate new agent tokens. The provisionerdserver will retrieve these tokens and push them down to + // the provisioner (and ultimately to the `coder_agent` resource in the Terraform provider) where they will be + // reused. Context: the agent token is often used in immutable attributes of workspace resource (e.g. VM/container) + // to initialize the agent, so if that value changes it will necessitate a replacement of that resource, thus + // obviating the whole point of the prebuild. + RunningAgentAuthTokens map[uuid.UUID]string `json:"running_agent_auth_tokens"` } // TemplateVersionDryRunJob is the payload for the "template_version_dry_run" job type. diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index 1388b61030d38..388e2eadd4063 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -1154,6 +1154,105 @@ func (api *API) workspaceAgentPostLogSource(rw http.ResponseWriter, r *http.Requ httpapi.Write(ctx, rw, http.StatusCreated, apiSource) } +// @Summary Get workspace agent reinitialization +// @ID get-workspace-agent-reinitialization +// @Security CoderSessionToken +// @Produce json +// @Tags Agents +// @Success 200 {object} agentsdk.ReinitializationResponse +// @Router /workspaceagents/me/reinit [get] +func (api *API) workspaceAgentReinit(rw http.ResponseWriter, r *http.Request) { + // Allow us to interrupt watch via cancel. + ctx, cancel := context.WithCancel(r.Context()) + defer cancel() + r = r.WithContext(ctx) // Rewire context for SSE cancellation. + + workspaceAgent := httpmw.WorkspaceAgent(r) + log := api.Logger.Named("workspace_agent_reinit_watcher").With( + slog.F("workspace_agent_id", workspaceAgent.ID), + ) + + workspace, err := api.Database.GetWorkspaceByAgentID(ctx, workspaceAgent.ID) + if err != nil { + log.Error(ctx, "failed to retrieve workspace from agent token", slog.Error(err)) + httpapi.InternalServerError(rw, xerrors.New("failed to determine workspace from agent token")) + } + + log.Info(ctx, "agent waiting for reinit instruction") + + prebuildClaims := make(chan uuid.UUID, 1) + cancelSub, err := api.Pubsub.Subscribe(agentsdk.PrebuildClaimedChannel(workspace.ID), func(inner context.Context, id []byte) { + select { + case <-ctx.Done(): + return + case <-inner.Done(): + return + default: + } + + parsed, err := uuid.ParseBytes(id) + if err != nil { + log.Error(ctx, "invalid prebuild claimed channel payload", slog.F("input", string(id))) + return + } + prebuildClaims <- parsed + }) + if err != nil { + log.Error(ctx, "failed to subscribe to prebuild claimed channel", slog.Error(err)) + httpapi.InternalServerError(rw, xerrors.New("failed to subscribe to prebuild claimed channel")) + return + } + defer cancelSub() + + sseSendEvent, sseSenderClosed, err := httpapi.ServerSentEventSender(rw, r) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error setting up server-sent events.", + Detail: err.Error(), + }) + return + } + // Prevent handler from returning until the sender is closed. + defer func() { + cancel() + <-sseSenderClosed + }() + // Synchronize cancellation from SSE -> context, this lets us simplify the + // cancellation logic. + go func() { + select { + case <-ctx.Done(): + case <-sseSenderClosed: + cancel() + } + }() + + // An initial ping signals to the request that the server is now ready + // and the client can begin servicing a channel with data. + _ = sseSendEvent(codersdk.ServerSentEvent{ + Type: codersdk.ServerSentEventTypePing, + }) + + // Expand with future use-cases for agent reinitialization. + for { + select { + case <-ctx.Done(): + return + case user := <-prebuildClaims: + err = sseSendEvent(codersdk.ServerSentEvent{ + Type: codersdk.ServerSentEventTypeData, + Data: agentsdk.ReinitializationResponse{ + Message: fmt.Sprintf("prebuild claimed by user: %s", user), + Reason: agentsdk.ReinitializeReasonPrebuildClaimed, + }, + }) + if err != nil { + log.Warn(ctx, "failed to send SSE response to trigger reinit", slog.Error(err)) + } + } + } +} + // convertProvisionedApps converts applications that are in the middle of provisioning process. // It means that they may not have an agent or workspace assigned (dry-run job). func convertProvisionedApps(dbApps []database.WorkspaceApp) []codersdk.WorkspaceApp { diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 12b3787acf3d8..6f6bb937c0a9c 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -634,9 +634,10 @@ func createWorkspace( } var ( - provisionerJob *database.ProvisionerJob - workspaceBuild *database.WorkspaceBuild - provisionerDaemons []database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow + provisionerJob *database.ProvisionerJob + workspaceBuild *database.WorkspaceBuild + provisionerDaemons []database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow + agentTokensByAgentID map[uuid.UUID]string ) err = api.Database.InTx(func(db database.Store) error { @@ -683,6 +684,14 @@ func createWorkspace( // Prebuild found! workspaceID = claimedWorkspace.ID initiatorID = prebuildsClaimer.Initiator() + agents, err := db.GetWorkspaceAgentsInLatestBuildByWorkspaceID(ctx, claimedWorkspace.ID) + if err != nil { + api.Logger.Error(ctx, "failed to retrieve running agents of claimed prebuilt workspace", + slog.F("workspace_id", claimedWorkspace.ID), slog.Error(err)) + } + for _, agent := range agents { + agentTokensByAgentID[agent.ID] = agent.AuthToken.String() + } } // We have to refetch the workspace for the joined in fields. @@ -698,7 +707,7 @@ func createWorkspace( Initiator(initiatorID). ActiveVersion(). RichParameterValues(req.RichParameterValues). - TemplateVersionPresetID(req.TemplateVersionPresetID) + RunningAgentAuthTokens(agentTokensByAgentID) if req.TemplateVersionID != uuid.Nil { builder = builder.VersionID(req.TemplateVersionID) } diff --git a/coderd/wsbuilder/wsbuilder.go b/coderd/wsbuilder/wsbuilder.go index 942829004309c..1b00076949bb6 100644 --- a/coderd/wsbuilder/wsbuilder.go +++ b/coderd/wsbuilder/wsbuilder.go @@ -76,9 +76,9 @@ type Builder struct { parameterValues *[]string templateVersionPresetParameterValues []database.TemplateVersionPresetParameter - prebuild bool - prebuildClaimedBy uuid.UUID - + prebuild bool + prebuildClaimedBy uuid.UUID + runningAgentAuthTokens map[uuid.UUID]string verifyNoLegacyParametersOnce bool } @@ -191,6 +191,12 @@ func (b Builder) UsingDynamicParameters() Builder { return b } +func (b Builder) RunningAgentAuthTokens(tokens map[uuid.UUID]string) Builder { + // nolint: revive + b.runningAgentAuthTokens = tokens + return b +} + // SetLastWorkspaceBuildInTx prepopulates the Builder's cache with the last workspace build. This allows us // to avoid a repeated database query when the Builder's caller also needs the workspace build, e.g. auto-start & // auto-stop. @@ -322,10 +328,11 @@ func (b *Builder) buildTx(authFunc func(action policy.Action, object rbac.Object workspaceBuildID := uuid.New() input, err := json.Marshal(provisionerdserver.WorkspaceProvisionJob{ - WorkspaceBuildID: workspaceBuildID, - LogLevel: b.logLevel, - IsPrebuild: b.prebuild, - PrebuildClaimedByUser: b.prebuildClaimedBy, + WorkspaceBuildID: workspaceBuildID, + LogLevel: b.logLevel, + IsPrebuild: b.prebuild, + PrebuildClaimedByUser: b.prebuildClaimedBy, + RunningAgentAuthTokens: b.runningAgentAuthTokens, }) if err != nil { return nil, nil, nil, BuildError{ diff --git a/codersdk/agentsdk/agentsdk.go b/codersdk/agentsdk/agentsdk.go index 109d14b84d050..da6a22bed69fc 100644 --- a/codersdk/agentsdk/agentsdk.go +++ b/codersdk/agentsdk/agentsdk.go @@ -19,12 +19,13 @@ import ( "tailscale.com/tailcfg" "cdr.dev/slog" + "github.com/coder/websocket" + "github.com/coder/coder/v2/agent/proto" "github.com/coder/coder/v2/apiversion" "github.com/coder/coder/v2/codersdk" drpcsdk "github.com/coder/coder/v2/codersdk/drpc" tailnetproto "github.com/coder/coder/v2/tailnet/proto" - "github.com/coder/websocket" ) // ExternalLogSourceID is the statically-defined ID of a log-source that @@ -686,3 +687,70 @@ func LogsNotifyChannel(agentID uuid.UUID) string { type LogsNotifyMessage struct { CreatedAfter int64 `json:"created_after"` } + +type ReinitializationReason string + +const ( + ReinitializeReasonPrebuildClaimed ReinitializationReason = "prebuild_claimed" +) + +type ReinitializationResponse struct { + Message string `json:"message"` + Reason ReinitializationReason `json:"reason"` +} + +// TODO: maybe place this somewhere else? +func PrebuildClaimedChannel(id uuid.UUID) string { + return fmt.Sprintf("prebuild_claimed_%s", id) +} + +// WaitForReinit polls a SSE endpoint, and receives an event back under the following conditions: +// - ping: ignored, keepalive +// - prebuild claimed: a prebuilt workspace is claimed, so the agent must reinitialize. +// NOTE: the caller is responsible for closing the events chan. +func (c *Client) WaitForReinit(ctx context.Context, events chan<- ReinitializationResponse) error { + // TODO: allow configuring httpclient + c.SDK.HTTPClient.Timeout = time.Hour * 24 + + res, err := c.SDK.Request(ctx, http.MethodGet, "/api/v2/workspaceagents/me/reinit", nil) + if err != nil { + return xerrors.Errorf("execute request: %w", err) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return codersdk.ReadBodyAsError(res) + } + + nextEvent := codersdk.ServerSentEventReader(ctx, res.Body) + + for { + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + + sse, err := nextEvent() + if err != nil { + return xerrors.Errorf("failed to read server-sent event: %w", err) + } + if sse.Type != codersdk.ServerSentEventTypeData { + continue + } + var reinitResp ReinitializationResponse + b, ok := sse.Data.([]byte) + if !ok { + return xerrors.Errorf("expected data as []byte, got %T", sse.Data) + } + err = json.Unmarshal(b, &reinitResp) + if err != nil { + return xerrors.Errorf("unmarshal reinit response: %w", err) + } + select { + case <-ctx.Done(): + return ctx.Err() + case events <- reinitResp: + } + } +} diff --git a/docs/reference/api/agents.md b/docs/reference/api/agents.md index 853cb67e38bfd..b9b0c8973f64b 100644 --- a/docs/reference/api/agents.md +++ b/docs/reference/api/agents.md @@ -470,6 +470,38 @@ curl -X PATCH http://coder-server:8080/api/v2/workspaceagents/me/logs \ To perform this operation, you must be authenticated. [Learn more](authentication.md). +## Get workspace agent reinitialization + +### Code samples + +```shell +# Example request using curl +curl -X GET http://coder-server:8080/api/v2/workspaceagents/me/reinit \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`GET /workspaceagents/me/reinit` + +### Example responses + +> 200 Response + +```json +{ + "message": "string", + "reason": "prebuild_claimed" +} +``` + +### Responses + +| Status | Meaning | Description | Schema | +|--------|---------------------------------------------------------|-------------|----------------------------------------------------------------------------------| +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [agentsdk.ReinitializationResponse](schemas.md#agentsdkreinitializationresponse) | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + ## Get workspace agent by ID ### Code samples diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index dd8ffd1971cb8..6acb220aef0db 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -182,6 +182,36 @@ | `icon` | string | false | | | | `id` | string | false | | ID is a unique identifier for the log source. It is scoped to a workspace agent, and can be statically defined inside code to prevent duplicate sources from being created for the same agent. | +## agentsdk.ReinitializationReason + +```json +"prebuild_claimed" +``` + +### Properties + +#### Enumerated Values + +| Value | +|--------------------| +| `prebuild_claimed` | + +## agentsdk.ReinitializationResponse + +```json +{ + "message": "string", + "reason": "prebuild_claimed" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +|-----------|--------------------------------------------------------------------|----------|--------------|-------------| +| `message` | string | false | | | +| `reason` | [agentsdk.ReinitializationReason](#agentsdkreinitializationreason) | false | | | + ## coderd.SCIMUser ```json diff --git a/go.mod b/go.mod index 230c911779b2f..ddbe92fb33e0f 100644 --- a/go.mod +++ b/go.mod @@ -529,3 +529,6 @@ require ( google.golang.org/genai v0.7.0 // indirect k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect ) + +// TODO: remove this once code merged upstream +replace github.com/coder/terraform-provider-coder/v2 => ../terraform-provider-coder/ diff --git a/provisioner/terraform/executor.go b/provisioner/terraform/executor.go index 150f51e6dd10d..7d7f72a470e90 100644 --- a/provisioner/terraform/executor.go +++ b/provisioner/terraform/executor.go @@ -261,6 +261,17 @@ func (e *executor) plan(ctx, killCtx context.Context, env, vars []string, logr l e.mut.Lock() defer e.mut.Unlock() + // TODO: defunct? + // var isPrebuild bool + // for _, v := range env { + // if envName(v) == provider.IsPrebuildEnvironmentVariable() && envVar(v) == "true" { + // isPrebuild = true + // break + // } + // } + + // _ = isPrebuild + planfilePath := getPlanFilePath(e.workdir) args := []string{ "plan", diff --git a/provisioner/terraform/provision.go b/provisioner/terraform/provision.go index f8f82bbad7b9a..c31f5a300a3fe 100644 --- a/provisioner/terraform/provision.go +++ b/provisioner/terraform/provision.go @@ -272,8 +272,22 @@ func provisionEnv( ) if metadata.GetIsPrebuild() { env = append(env, provider.IsPrebuildEnvironmentVariable()+"=true") + } else { + // TODO: provide an agentID to these functions so that we provide the right token + // for single agent support, we use the zero value "" as the agentID + // TODO: looks like we only provide agent tokens for reinit if metadata.GetIsPrebuild() is false + // check this for consistency wherever else we use the isPrebuild attribute from the Proto and where we use the env derived from it. + const singleAgentID = "" + tokens := metadata.GetRunningAgentAuthTokens() + var token string + for _, t := range tokens { + if t.AgentId == singleAgentID { + token = t.Token + break + } + } + env = append(env, provider.RunningAgentTokenEnvironmentVariable(singleAgentID)+"="+token) } - for key, value := range provisionersdk.AgentScriptEnv() { env = append(env, key+"="+value) } diff --git a/provisionersdk/proto/provisioner.pb.go b/provisionersdk/proto/provisioner.pb.go index f258f79e36f94..db0e1c7a949b6 100644 --- a/provisionersdk/proto/provisioner.pb.go +++ b/provisionersdk/proto/provisioner.pb.go @@ -2278,39 +2278,94 @@ func (x *Role) GetOrgId() string { return "" } +type RunningAgentAuthToken struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + AgentId string `protobuf:"bytes,1,opt,name=agent_id,json=agentId,proto3" json:"agent_id,omitempty"` + Token string `protobuf:"bytes,2,opt,name=token,proto3" json:"token,omitempty"` +} + +func (x *RunningAgentAuthToken) Reset() { + *x = RunningAgentAuthToken{} + if protoimpl.UnsafeEnabled { + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RunningAgentAuthToken) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RunningAgentAuthToken) ProtoMessage() {} + +func (x *RunningAgentAuthToken) ProtoReflect() protoreflect.Message { + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[26] + 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 RunningAgentAuthToken.ProtoReflect.Descriptor instead. +func (*RunningAgentAuthToken) Descriptor() ([]byte, []int) { + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{26} +} + +func (x *RunningAgentAuthToken) GetAgentId() string { + if x != nil { + return x.AgentId + } + return "" +} + +func (x *RunningAgentAuthToken) GetToken() string { + if x != nil { + return x.Token + } + return "" +} + // Metadata is information about a workspace used in the execution of a build type Metadata struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - CoderUrl string `protobuf:"bytes,1,opt,name=coder_url,json=coderUrl,proto3" json:"coder_url,omitempty"` - WorkspaceTransition WorkspaceTransition `protobuf:"varint,2,opt,name=workspace_transition,json=workspaceTransition,proto3,enum=provisioner.WorkspaceTransition" json:"workspace_transition,omitempty"` - WorkspaceName string `protobuf:"bytes,3,opt,name=workspace_name,json=workspaceName,proto3" json:"workspace_name,omitempty"` - WorkspaceOwner string `protobuf:"bytes,4,opt,name=workspace_owner,json=workspaceOwner,proto3" json:"workspace_owner,omitempty"` - WorkspaceId string `protobuf:"bytes,5,opt,name=workspace_id,json=workspaceId,proto3" json:"workspace_id,omitempty"` - WorkspaceOwnerId string `protobuf:"bytes,6,opt,name=workspace_owner_id,json=workspaceOwnerId,proto3" json:"workspace_owner_id,omitempty"` - WorkspaceOwnerEmail string `protobuf:"bytes,7,opt,name=workspace_owner_email,json=workspaceOwnerEmail,proto3" json:"workspace_owner_email,omitempty"` - TemplateName string `protobuf:"bytes,8,opt,name=template_name,json=templateName,proto3" json:"template_name,omitempty"` - TemplateVersion string `protobuf:"bytes,9,opt,name=template_version,json=templateVersion,proto3" json:"template_version,omitempty"` - WorkspaceOwnerOidcAccessToken string `protobuf:"bytes,10,opt,name=workspace_owner_oidc_access_token,json=workspaceOwnerOidcAccessToken,proto3" json:"workspace_owner_oidc_access_token,omitempty"` - WorkspaceOwnerSessionToken string `protobuf:"bytes,11,opt,name=workspace_owner_session_token,json=workspaceOwnerSessionToken,proto3" json:"workspace_owner_session_token,omitempty"` - TemplateId string `protobuf:"bytes,12,opt,name=template_id,json=templateId,proto3" json:"template_id,omitempty"` - WorkspaceOwnerName string `protobuf:"bytes,13,opt,name=workspace_owner_name,json=workspaceOwnerName,proto3" json:"workspace_owner_name,omitempty"` - WorkspaceOwnerGroups []string `protobuf:"bytes,14,rep,name=workspace_owner_groups,json=workspaceOwnerGroups,proto3" json:"workspace_owner_groups,omitempty"` - WorkspaceOwnerSshPublicKey string `protobuf:"bytes,15,opt,name=workspace_owner_ssh_public_key,json=workspaceOwnerSshPublicKey,proto3" json:"workspace_owner_ssh_public_key,omitempty"` - WorkspaceOwnerSshPrivateKey string `protobuf:"bytes,16,opt,name=workspace_owner_ssh_private_key,json=workspaceOwnerSshPrivateKey,proto3" json:"workspace_owner_ssh_private_key,omitempty"` - WorkspaceBuildId string `protobuf:"bytes,17,opt,name=workspace_build_id,json=workspaceBuildId,proto3" json:"workspace_build_id,omitempty"` - WorkspaceOwnerLoginType string `protobuf:"bytes,18,opt,name=workspace_owner_login_type,json=workspaceOwnerLoginType,proto3" json:"workspace_owner_login_type,omitempty"` - WorkspaceOwnerRbacRoles []*Role `protobuf:"bytes,19,rep,name=workspace_owner_rbac_roles,json=workspaceOwnerRbacRoles,proto3" json:"workspace_owner_rbac_roles,omitempty"` - IsPrebuild bool `protobuf:"varint,20,opt,name=is_prebuild,json=isPrebuild,proto3" json:"is_prebuild,omitempty"` - RunningWorkspaceAgentToken string `protobuf:"bytes,21,opt,name=running_workspace_agent_token,json=runningWorkspaceAgentToken,proto3" json:"running_workspace_agent_token,omitempty"` + CoderUrl string `protobuf:"bytes,1,opt,name=coder_url,json=coderUrl,proto3" json:"coder_url,omitempty"` + WorkspaceTransition WorkspaceTransition `protobuf:"varint,2,opt,name=workspace_transition,json=workspaceTransition,proto3,enum=provisioner.WorkspaceTransition" json:"workspace_transition,omitempty"` + WorkspaceName string `protobuf:"bytes,3,opt,name=workspace_name,json=workspaceName,proto3" json:"workspace_name,omitempty"` + WorkspaceOwner string `protobuf:"bytes,4,opt,name=workspace_owner,json=workspaceOwner,proto3" json:"workspace_owner,omitempty"` + WorkspaceId string `protobuf:"bytes,5,opt,name=workspace_id,json=workspaceId,proto3" json:"workspace_id,omitempty"` + WorkspaceOwnerId string `protobuf:"bytes,6,opt,name=workspace_owner_id,json=workspaceOwnerId,proto3" json:"workspace_owner_id,omitempty"` + WorkspaceOwnerEmail string `protobuf:"bytes,7,opt,name=workspace_owner_email,json=workspaceOwnerEmail,proto3" json:"workspace_owner_email,omitempty"` + TemplateName string `protobuf:"bytes,8,opt,name=template_name,json=templateName,proto3" json:"template_name,omitempty"` + TemplateVersion string `protobuf:"bytes,9,opt,name=template_version,json=templateVersion,proto3" json:"template_version,omitempty"` + WorkspaceOwnerOidcAccessToken string `protobuf:"bytes,10,opt,name=workspace_owner_oidc_access_token,json=workspaceOwnerOidcAccessToken,proto3" json:"workspace_owner_oidc_access_token,omitempty"` + WorkspaceOwnerSessionToken string `protobuf:"bytes,11,opt,name=workspace_owner_session_token,json=workspaceOwnerSessionToken,proto3" json:"workspace_owner_session_token,omitempty"` + TemplateId string `protobuf:"bytes,12,opt,name=template_id,json=templateId,proto3" json:"template_id,omitempty"` + WorkspaceOwnerName string `protobuf:"bytes,13,opt,name=workspace_owner_name,json=workspaceOwnerName,proto3" json:"workspace_owner_name,omitempty"` + WorkspaceOwnerGroups []string `protobuf:"bytes,14,rep,name=workspace_owner_groups,json=workspaceOwnerGroups,proto3" json:"workspace_owner_groups,omitempty"` + WorkspaceOwnerSshPublicKey string `protobuf:"bytes,15,opt,name=workspace_owner_ssh_public_key,json=workspaceOwnerSshPublicKey,proto3" json:"workspace_owner_ssh_public_key,omitempty"` + WorkspaceOwnerSshPrivateKey string `protobuf:"bytes,16,opt,name=workspace_owner_ssh_private_key,json=workspaceOwnerSshPrivateKey,proto3" json:"workspace_owner_ssh_private_key,omitempty"` + WorkspaceBuildId string `protobuf:"bytes,17,opt,name=workspace_build_id,json=workspaceBuildId,proto3" json:"workspace_build_id,omitempty"` + WorkspaceOwnerLoginType string `protobuf:"bytes,18,opt,name=workspace_owner_login_type,json=workspaceOwnerLoginType,proto3" json:"workspace_owner_login_type,omitempty"` + WorkspaceOwnerRbacRoles []*Role `protobuf:"bytes,19,rep,name=workspace_owner_rbac_roles,json=workspaceOwnerRbacRoles,proto3" json:"workspace_owner_rbac_roles,omitempty"` + IsPrebuild bool `protobuf:"varint,20,opt,name=is_prebuild,json=isPrebuild,proto3" json:"is_prebuild,omitempty"` + RunningAgentAuthTokens []*RunningAgentAuthToken `protobuf:"bytes,21,rep,name=running_agent_auth_tokens,json=runningAgentAuthTokens,proto3" json:"running_agent_auth_tokens,omitempty"` } func (x *Metadata) Reset() { *x = Metadata{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[26] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2323,7 +2378,7 @@ func (x *Metadata) String() string { func (*Metadata) ProtoMessage() {} func (x *Metadata) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[26] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[27] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2336,7 +2391,7 @@ func (x *Metadata) ProtoReflect() protoreflect.Message { // Deprecated: Use Metadata.ProtoReflect.Descriptor instead. func (*Metadata) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{26} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{27} } func (x *Metadata) GetCoderUrl() string { @@ -2479,11 +2534,11 @@ func (x *Metadata) GetIsPrebuild() bool { return false } -func (x *Metadata) GetRunningWorkspaceAgentToken() string { +func (x *Metadata) GetRunningAgentAuthTokens() []*RunningAgentAuthToken { if x != nil { - return x.RunningWorkspaceAgentToken + return x.RunningAgentAuthTokens } - return "" + return nil } // Config represents execution configuration shared by all subsequent requests in the Session @@ -2502,7 +2557,7 @@ type Config struct { func (x *Config) Reset() { *x = Config{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[27] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2515,7 +2570,7 @@ func (x *Config) String() string { func (*Config) ProtoMessage() {} func (x *Config) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[27] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[28] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2528,7 +2583,7 @@ func (x *Config) ProtoReflect() protoreflect.Message { // Deprecated: Use Config.ProtoReflect.Descriptor instead. func (*Config) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{27} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{28} } func (x *Config) GetTemplateSourceArchive() []byte { @@ -2562,7 +2617,7 @@ type ParseRequest struct { func (x *ParseRequest) Reset() { *x = ParseRequest{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[28] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2575,7 +2630,7 @@ func (x *ParseRequest) String() string { func (*ParseRequest) ProtoMessage() {} func (x *ParseRequest) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[28] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[29] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2588,7 +2643,7 @@ func (x *ParseRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ParseRequest.ProtoReflect.Descriptor instead. func (*ParseRequest) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{28} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{29} } // ParseComplete indicates a request to parse completed. @@ -2606,7 +2661,7 @@ type ParseComplete struct { func (x *ParseComplete) Reset() { *x = ParseComplete{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[29] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2619,7 +2674,7 @@ func (x *ParseComplete) String() string { func (*ParseComplete) ProtoMessage() {} func (x *ParseComplete) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[29] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[30] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2632,7 +2687,7 @@ func (x *ParseComplete) ProtoReflect() protoreflect.Message { // Deprecated: Use ParseComplete.ProtoReflect.Descriptor instead. func (*ParseComplete) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{29} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{30} } func (x *ParseComplete) GetError() string { @@ -2678,7 +2733,7 @@ type PlanRequest struct { func (x *PlanRequest) Reset() { *x = PlanRequest{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[30] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2691,7 +2746,7 @@ func (x *PlanRequest) String() string { func (*PlanRequest) ProtoMessage() {} func (x *PlanRequest) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[30] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[31] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2704,7 +2759,7 @@ func (x *PlanRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PlanRequest.ProtoReflect.Descriptor instead. func (*PlanRequest) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{30} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{31} } func (x *PlanRequest) GetMetadata() *Metadata { @@ -2754,7 +2809,7 @@ type PlanComplete struct { func (x *PlanComplete) Reset() { *x = PlanComplete{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[31] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2767,7 +2822,7 @@ func (x *PlanComplete) String() string { func (*PlanComplete) ProtoMessage() {} func (x *PlanComplete) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[31] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[32] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2780,7 +2835,7 @@ func (x *PlanComplete) ProtoReflect() protoreflect.Message { // Deprecated: Use PlanComplete.ProtoReflect.Descriptor instead. func (*PlanComplete) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{31} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{32} } func (x *PlanComplete) GetError() string { @@ -2852,7 +2907,7 @@ type ApplyRequest struct { func (x *ApplyRequest) Reset() { *x = ApplyRequest{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[32] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2865,7 +2920,7 @@ func (x *ApplyRequest) String() string { func (*ApplyRequest) ProtoMessage() {} func (x *ApplyRequest) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[32] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[33] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2878,7 +2933,7 @@ func (x *ApplyRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ApplyRequest.ProtoReflect.Descriptor instead. func (*ApplyRequest) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{32} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{33} } func (x *ApplyRequest) GetMetadata() *Metadata { @@ -2905,7 +2960,7 @@ type ApplyComplete struct { func (x *ApplyComplete) Reset() { *x = ApplyComplete{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[33] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2918,7 +2973,7 @@ func (x *ApplyComplete) String() string { func (*ApplyComplete) ProtoMessage() {} func (x *ApplyComplete) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[33] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[34] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2931,7 +2986,7 @@ func (x *ApplyComplete) ProtoReflect() protoreflect.Message { // Deprecated: Use ApplyComplete.ProtoReflect.Descriptor instead. func (*ApplyComplete) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{33} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{34} } func (x *ApplyComplete) GetState() []byte { @@ -2993,7 +3048,7 @@ type Timing struct { func (x *Timing) Reset() { *x = Timing{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[34] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3006,7 +3061,7 @@ func (x *Timing) String() string { func (*Timing) ProtoMessage() {} func (x *Timing) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[34] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[35] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3019,7 +3074,7 @@ func (x *Timing) ProtoReflect() protoreflect.Message { // Deprecated: Use Timing.ProtoReflect.Descriptor instead. func (*Timing) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{34} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{35} } func (x *Timing) GetStart() *timestamppb.Timestamp { @@ -3081,7 +3136,7 @@ type CancelRequest struct { func (x *CancelRequest) Reset() { *x = CancelRequest{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[35] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3094,7 +3149,7 @@ func (x *CancelRequest) String() string { func (*CancelRequest) ProtoMessage() {} func (x *CancelRequest) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[35] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[36] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3107,7 +3162,7 @@ func (x *CancelRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CancelRequest.ProtoReflect.Descriptor instead. func (*CancelRequest) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{35} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{36} } type Request struct { @@ -3128,7 +3183,7 @@ type Request struct { func (x *Request) Reset() { *x = Request{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[36] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3141,7 +3196,7 @@ func (x *Request) String() string { func (*Request) ProtoMessage() {} func (x *Request) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[36] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[37] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3154,7 +3209,7 @@ func (x *Request) ProtoReflect() protoreflect.Message { // Deprecated: Use Request.ProtoReflect.Descriptor instead. func (*Request) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{36} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{37} } func (m *Request) GetType() isRequest_Type { @@ -3250,7 +3305,7 @@ type Response struct { func (x *Response) Reset() { *x = Response{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[37] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3263,7 +3318,7 @@ func (x *Response) String() string { func (*Response) ProtoMessage() {} func (x *Response) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[37] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[38] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3276,7 +3331,7 @@ func (x *Response) ProtoReflect() protoreflect.Message { // Deprecated: Use Response.ProtoReflect.Descriptor instead. func (*Response) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{37} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{38} } func (m *Response) GetType() isResponse_Type { @@ -3358,7 +3413,7 @@ type Agent_Metadata struct { func (x *Agent_Metadata) Reset() { *x = Agent_Metadata{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[38] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3371,7 +3426,7 @@ func (x *Agent_Metadata) String() string { func (*Agent_Metadata) ProtoMessage() {} func (x *Agent_Metadata) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[38] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[39] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3443,7 +3498,7 @@ type Resource_Metadata struct { func (x *Resource_Metadata) Reset() { *x = Resource_Metadata{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[40] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[41] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3456,7 +3511,7 @@ func (x *Resource_Metadata) String() string { func (*Resource_Metadata) ProtoMessage() {} func (x *Resource_Metadata) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[40] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[41] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3804,167 +3859,142 @@ var file_provisionersdk_proto_provisioner_proto_rawDesc = []byte{ 0x6b, 0x65, 0x79, 0x22, 0x31, 0x0a, 0x04, 0x52, 0x6f, 0x6c, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x15, 0x0a, 0x06, 0x6f, 0x72, 0x67, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x05, 0x6f, 0x72, 0x67, 0x49, 0x64, 0x22, 0xe0, 0x08, 0x0a, 0x08, 0x4d, 0x65, 0x74, 0x61, 0x64, - 0x61, 0x74, 0x61, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x5f, 0x75, 0x72, 0x6c, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x55, 0x72, 0x6c, - 0x12, 0x53, 0x0a, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x74, 0x72, - 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x20, - 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x57, 0x6f, 0x72, - 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, - 0x52, 0x13, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, - 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x25, 0x0a, 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, - 0x63, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x77, - 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x27, 0x0a, 0x0f, - 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, - 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x12, 0x21, 0x0a, 0x0c, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, - 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x77, 0x6f, 0x72, - 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, 0x2c, 0x0a, 0x12, 0x77, 0x6f, 0x72, 0x6b, - 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x06, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, - 0x77, 0x6e, 0x65, 0x72, 0x49, 0x64, 0x12, 0x32, 0x0a, 0x15, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, - 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, - 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, - 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x45, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x23, 0x0a, 0x0d, 0x74, 0x65, - 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0c, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, - 0x29, 0x0a, 0x10, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, - 0x69, 0x6f, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x74, 0x65, 0x6d, 0x70, 0x6c, - 0x61, 0x74, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x48, 0x0a, 0x21, 0x77, 0x6f, - 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x6f, 0x69, - 0x64, 0x63, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, - 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x1d, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, - 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x4f, 0x69, 0x64, 0x63, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, - 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x41, 0x0a, 0x1d, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, - 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, - 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x1a, 0x77, 0x6f, 0x72, - 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x53, 0x65, 0x73, 0x73, 0x69, - 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1f, 0x0a, 0x0b, 0x74, 0x65, 0x6d, 0x70, 0x6c, - 0x61, 0x74, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x74, 0x65, - 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x49, 0x64, 0x12, 0x30, 0x0a, 0x14, 0x77, 0x6f, 0x72, 0x6b, - 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x6e, 0x61, 0x6d, 0x65, - 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, - 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x34, 0x0a, 0x16, 0x77, 0x6f, - 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x67, 0x72, - 0x6f, 0x75, 0x70, 0x73, 0x18, 0x0e, 0x20, 0x03, 0x28, 0x09, 0x52, 0x14, 0x77, 0x6f, 0x72, 0x6b, - 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, - 0x12, 0x42, 0x0a, 0x1e, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, - 0x6e, 0x65, 0x72, 0x5f, 0x73, 0x73, 0x68, 0x5f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, - 0x65, 0x79, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x09, 0x52, 0x1a, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, - 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x53, 0x73, 0x68, 0x50, 0x75, 0x62, 0x6c, 0x69, - 0x63, 0x4b, 0x65, 0x79, 0x12, 0x44, 0x0a, 0x1f, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, - 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x73, 0x73, 0x68, 0x5f, 0x70, 0x72, 0x69, 0x76, - 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x10, 0x20, 0x01, 0x28, 0x09, 0x52, 0x1b, 0x77, - 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x53, 0x73, 0x68, - 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x2c, 0x0a, 0x12, 0x77, 0x6f, - 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x5f, 0x69, 0x64, - 0x18, 0x11, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, - 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x64, 0x12, 0x3b, 0x0a, 0x1a, 0x77, 0x6f, 0x72, 0x6b, - 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x6c, 0x6f, 0x67, 0x69, - 0x6e, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x12, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x77, 0x6f, - 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x4c, 0x6f, 0x67, 0x69, - 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x4e, 0x0a, 0x1a, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, - 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x72, 0x62, 0x61, 0x63, 0x5f, 0x72, 0x6f, - 0x6c, 0x65, 0x73, 0x18, 0x13, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x72, 0x6f, 0x76, - 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x6f, 0x6c, 0x65, 0x52, 0x17, 0x77, 0x6f, - 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x52, 0x62, 0x61, 0x63, - 0x52, 0x6f, 0x6c, 0x65, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x69, 0x73, 0x5f, 0x70, 0x72, 0x65, 0x62, - 0x75, 0x69, 0x6c, 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x69, 0x73, 0x50, 0x72, - 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x12, 0x41, 0x0a, 0x1d, 0x72, 0x75, 0x6e, 0x6e, 0x69, 0x6e, - 0x67, 0x5f, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x61, 0x67, 0x65, 0x6e, - 0x74, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x15, 0x20, 0x01, 0x28, 0x09, 0x52, 0x1a, 0x72, - 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, - 0x67, 0x65, 0x6e, 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x8a, 0x01, 0x0a, 0x06, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x12, 0x36, 0x0a, 0x17, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, - 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x61, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x15, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x53, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x41, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x12, 0x14, 0x0a, 0x05, - 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, - 0x74, 0x65, 0x12, 0x32, 0x0a, 0x15, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, - 0x72, 0x5f, 0x6c, 0x6f, 0x67, 0x5f, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x13, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x4c, 0x6f, - 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x22, 0x0e, 0x0a, 0x0c, 0x50, 0x61, 0x72, 0x73, 0x65, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xa3, 0x02, 0x0a, 0x0d, 0x50, 0x61, 0x72, 0x73, 0x65, - 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, - 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x4c, - 0x0a, 0x12, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x76, 0x61, 0x72, 0x69, 0x61, - 0x62, 0x6c, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x70, 0x72, 0x6f, - 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, - 0x65, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x11, 0x74, 0x65, 0x6d, 0x70, 0x6c, - 0x61, 0x74, 0x65, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, 0x12, 0x16, 0x0a, 0x06, - 0x72, 0x65, 0x61, 0x64, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x72, 0x65, - 0x61, 0x64, 0x6d, 0x65, 0x12, 0x54, 0x0a, 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, - 0x65, 0x5f, 0x74, 0x61, 0x67, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x70, - 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, - 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, - 0x63, 0x65, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0d, 0x77, 0x6f, 0x72, - 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x61, 0x67, 0x73, 0x1a, 0x40, 0x0a, 0x12, 0x57, 0x6f, - 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x61, 0x67, 0x73, 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, 0x22, 0xb5, 0x02, 0x0a, - 0x0b, 0x50, 0x6c, 0x61, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x08, - 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, - 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, - 0x53, 0x0a, 0x15, 0x72, 0x69, 0x63, 0x68, 0x5f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, - 0x72, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, - 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x69, 0x63, - 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, - 0x13, 0x72, 0x69, 0x63, 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, - 0x6c, 0x75, 0x65, 0x73, 0x12, 0x43, 0x0a, 0x0f, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, - 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, - 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x56, 0x61, 0x72, 0x69, - 0x61, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0e, 0x76, 0x61, 0x72, 0x69, 0x61, - 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x59, 0x0a, 0x17, 0x65, 0x78, 0x74, - 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, - 0x64, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x70, 0x72, 0x6f, - 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, - 0x6c, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x15, 0x65, - 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, - 0x64, 0x65, 0x72, 0x73, 0x22, 0x99, 0x03, 0x0a, 0x0c, 0x50, 0x6c, 0x61, 0x6e, 0x43, 0x6f, 0x6d, - 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, - 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, 0x02, 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, - 0x12, 0x3a, 0x0a, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x18, 0x03, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, + 0x05, 0x6f, 0x72, 0x67, 0x49, 0x64, 0x22, 0x48, 0x0a, 0x15, 0x52, 0x75, 0x6e, 0x6e, 0x69, 0x6e, + 0x67, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x41, 0x75, 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, + 0x19, 0x0a, 0x08, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x07, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, + 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, + 0x22, 0xfc, 0x08, 0x0a, 0x08, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x1b, 0x0a, + 0x09, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x55, 0x72, 0x6c, 0x12, 0x53, 0x0a, 0x14, 0x77, 0x6f, + 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, + 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x20, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, + 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, + 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x13, 0x77, 0x6f, 0x72, 0x6b, + 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, + 0x25, 0x0a, 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, + 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x12, + 0x21, 0x0a, 0x0c, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, + 0x49, 0x64, 0x12, 0x2c, 0x0a, 0x12, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, + 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, + 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x49, 0x64, + 0x12, 0x32, 0x0a, 0x15, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, + 0x6e, 0x65, 0x72, 0x5f, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x13, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x45, + 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x23, 0x0a, 0x0d, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, + 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x74, 0x65, 0x6d, + 0x70, 0x6c, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x29, 0x0a, 0x10, 0x74, 0x65, 0x6d, + 0x70, 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x09, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0f, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x56, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x48, 0x0a, 0x21, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, + 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x6f, 0x69, 0x64, 0x63, 0x5f, 0x61, 0x63, 0x63, + 0x65, 0x73, 0x73, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x1d, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x4f, + 0x69, 0x64, 0x63, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x41, + 0x0a, 0x1d, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, + 0x72, 0x5f, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, + 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x1a, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, + 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, + 0x6e, 0x12, 0x1f, 0x0a, 0x0b, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x69, 0x64, + 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, + 0x49, 0x64, 0x12, 0x30, 0x0a, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, + 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x12, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, + 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x34, 0x0a, 0x16, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, + 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x0e, + 0x20, 0x03, 0x28, 0x09, 0x52, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, + 0x77, 0x6e, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x42, 0x0a, 0x1e, 0x77, 0x6f, + 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x73, 0x73, + 0x68, 0x5f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x0f, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x1a, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, + 0x65, 0x72, 0x53, 0x73, 0x68, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x44, + 0x0a, 0x1f, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, + 0x72, 0x5f, 0x73, 0x73, 0x68, 0x5f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, + 0x79, 0x18, 0x10, 0x20, 0x01, 0x28, 0x09, 0x52, 0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x53, 0x73, 0x68, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, + 0x65, 0x4b, 0x65, 0x79, 0x12, 0x2c, 0x0a, 0x12, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, + 0x65, 0x5f, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x11, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x10, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, + 0x49, 0x64, 0x12, 0x3b, 0x0a, 0x1a, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, + 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x5f, 0x74, 0x79, 0x70, 0x65, + 0x18, 0x12, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, + 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, + 0x4e, 0x0a, 0x1a, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, + 0x65, 0x72, 0x5f, 0x72, 0x62, 0x61, 0x63, 0x5f, 0x72, 0x6f, 0x6c, 0x65, 0x73, 0x18, 0x13, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, + 0x72, 0x2e, 0x52, 0x6f, 0x6c, 0x65, 0x52, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, + 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x52, 0x62, 0x61, 0x63, 0x52, 0x6f, 0x6c, 0x65, 0x73, 0x12, + 0x1f, 0x0a, 0x0b, 0x69, 0x73, 0x5f, 0x70, 0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x18, 0x14, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x69, 0x73, 0x50, 0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, + 0x12, 0x5d, 0x0a, 0x19, 0x72, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x5f, 0x61, 0x67, 0x65, 0x6e, + 0x74, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x18, 0x15, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, + 0x72, 0x2e, 0x52, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x41, 0x75, + 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x16, 0x72, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, + 0x41, 0x67, 0x65, 0x6e, 0x74, 0x41, 0x75, 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x22, + 0x8a, 0x01, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x36, 0x0a, 0x17, 0x74, 0x65, + 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x61, 0x72, + 0x63, 0x68, 0x69, 0x76, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x15, 0x74, 0x65, 0x6d, + 0x70, 0x6c, 0x61, 0x74, 0x65, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x41, 0x72, 0x63, 0x68, 0x69, + 0x76, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x32, 0x0a, 0x15, 0x70, 0x72, 0x6f, 0x76, + 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x5f, 0x6c, 0x6f, 0x67, 0x5f, 0x6c, 0x65, 0x76, 0x65, + 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, + 0x6f, 0x6e, 0x65, 0x72, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x22, 0x0e, 0x0a, 0x0c, + 0x50, 0x61, 0x72, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xa3, 0x02, 0x0a, + 0x0d, 0x50, 0x61, 0x72, 0x73, 0x65, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x14, + 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, + 0x72, 0x72, 0x6f, 0x72, 0x12, 0x4c, 0x0a, 0x12, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, + 0x5f, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x54, + 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x52, + 0x11, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, + 0x65, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x64, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x06, 0x72, 0x65, 0x61, 0x64, 0x6d, 0x65, 0x12, 0x54, 0x0a, 0x0e, 0x77, 0x6f, + 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x74, 0x61, 0x67, 0x73, 0x18, 0x04, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, + 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x2e, 0x57, + 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, + 0x79, 0x52, 0x0d, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x61, 0x67, 0x73, + 0x1a, 0x40, 0x0a, 0x12, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x61, 0x67, + 0x73, 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, 0x22, 0xb5, 0x02, 0x0a, 0x0b, 0x50, 0x6c, 0x61, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x31, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, + 0x65, 0x72, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x53, 0x0a, 0x15, 0x72, 0x69, 0x63, 0x68, 0x5f, 0x70, 0x61, + 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x02, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x69, 0x63, 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, - 0x52, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x12, 0x61, 0x0a, 0x17, - 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x70, 0x72, - 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, - 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x45, 0x78, 0x74, 0x65, - 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, - 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x15, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, - 0x61, 0x6c, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x12, - 0x2d, 0x0a, 0x07, 0x74, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x54, - 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x52, 0x07, 0x74, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x2d, - 0x0a, 0x07, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4d, 0x6f, - 0x64, 0x75, 0x6c, 0x65, 0x52, 0x07, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x2d, 0x0a, - 0x07, 0x70, 0x72, 0x65, 0x73, 0x65, 0x74, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, - 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x65, - 0x73, 0x65, 0x74, 0x52, 0x07, 0x70, 0x72, 0x65, 0x73, 0x65, 0x74, 0x73, 0x12, 0x12, 0x0a, 0x04, - 0x70, 0x6c, 0x61, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x70, 0x6c, 0x61, 0x6e, - 0x22, 0x41, 0x0a, 0x0c, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x31, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, - 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, - 0x61, 0x74, 0x61, 0x22, 0xbe, 0x02, 0x0a, 0x0d, 0x41, 0x70, 0x70, 0x6c, 0x79, 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, + 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x13, 0x72, 0x69, 0x63, 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, + 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x43, 0x0a, 0x0f, 0x76, 0x61, + 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x03, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, + 0x72, 0x2e, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, + 0x0e, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, + 0x59, 0x0a, 0x17, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x61, 0x75, 0x74, 0x68, + 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x21, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x45, + 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, + 0x64, 0x65, 0x72, 0x52, 0x15, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x75, 0x74, + 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x22, 0x99, 0x03, 0x0a, 0x0c, 0x50, + 0x6c, 0x61, 0x6e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, + 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 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, 0x02, 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, 0x12, 0x3a, 0x0a, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, - 0x74, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x69, 0x63, 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x52, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x12, 0x61, 0x0a, 0x17, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x61, - 0x75, 0x74, 0x68, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x18, 0x05, 0x20, + 0x75, 0x74, 0x68, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x15, @@ -3972,80 +4002,112 @@ var file_provisionersdk_proto_provisioner_proto_rawDesc = []byte{ 0x69, 0x64, 0x65, 0x72, 0x73, 0x12, 0x2d, 0x0a, 0x07, 0x74, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x54, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x52, 0x07, 0x74, 0x69, 0x6d, - 0x69, 0x6e, 0x67, 0x73, 0x22, 0xfa, 0x01, 0x0a, 0x06, 0x54, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x12, - 0x30, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, - 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, - 0x74, 0x12, 0x2c, 0x0a, 0x03, 0x65, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, - 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x03, 0x65, 0x6e, 0x64, 0x12, - 0x16, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, - 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, - 0x74, 0x61, 0x67, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x74, 0x61, 0x67, - 0x65, 0x12, 0x2e, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0e, - 0x32, 0x18, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x54, - 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, - 0x65, 0x22, 0x0f, 0x0a, 0x0d, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x22, 0x8c, 0x02, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2d, - 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, - 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x48, 0x00, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x31, 0x0a, - 0x05, 0x70, 0x61, 0x72, 0x73, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x70, - 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x05, 0x70, 0x61, 0x72, 0x73, 0x65, - 0x12, 0x2e, 0x0a, 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, - 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x6c, 0x61, - 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x04, 0x70, 0x6c, 0x61, 0x6e, - 0x12, 0x31, 0x0a, 0x05, 0x61, 0x70, 0x70, 0x6c, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x41, 0x70, - 0x70, 0x6c, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x05, 0x61, 0x70, - 0x70, 0x6c, 0x79, 0x12, 0x34, 0x0a, 0x06, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, - 0x72, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, - 0x00, 0x52, 0x06, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, - 0x65, 0x22, 0xd1, 0x01, 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, 0x32, 0x0a, 0x05, 0x70, 0x61, 0x72, 0x73, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, - 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x48, - 0x00, 0x52, 0x05, 0x70, 0x61, 0x72, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x04, 0x70, 0x6c, 0x61, 0x6e, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, - 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x6c, 0x61, 0x6e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, - 0x65, 0x48, 0x00, 0x52, 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x12, 0x32, 0x0a, 0x05, 0x61, 0x70, 0x70, - 0x6c, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, - 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x43, 0x6f, 0x6d, 0x70, - 0x6c, 0x65, 0x74, 0x65, 0x48, 0x00, 0x52, 0x05, 0x61, 0x70, 0x70, 0x6c, 0x79, 0x42, 0x06, 0x0a, - 0x04, 0x74, 0x79, 0x70, 0x65, 0x2a, 0x3f, 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, - 0x6c, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x52, 0x41, 0x43, 0x45, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, - 0x44, 0x45, 0x42, 0x55, 0x47, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, - 0x02, 0x12, 0x08, 0x0a, 0x04, 0x57, 0x41, 0x52, 0x4e, 0x10, 0x03, 0x12, 0x09, 0x0a, 0x05, 0x45, - 0x52, 0x52, 0x4f, 0x52, 0x10, 0x04, 0x2a, 0x3b, 0x0a, 0x0f, 0x41, 0x70, 0x70, 0x53, 0x68, 0x61, - 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x09, 0x0a, 0x05, 0x4f, 0x57, 0x4e, - 0x45, 0x52, 0x10, 0x00, 0x12, 0x11, 0x0a, 0x0d, 0x41, 0x55, 0x54, 0x48, 0x45, 0x4e, 0x54, 0x49, - 0x43, 0x41, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x50, 0x55, 0x42, 0x4c, 0x49, - 0x43, 0x10, 0x02, 0x2a, 0x35, 0x0a, 0x09, 0x41, 0x70, 0x70, 0x4f, 0x70, 0x65, 0x6e, 0x49, 0x6e, - 0x12, 0x0e, 0x0a, 0x06, 0x57, 0x49, 0x4e, 0x44, 0x4f, 0x57, 0x10, 0x00, 0x1a, 0x02, 0x08, 0x01, - 0x12, 0x0f, 0x0a, 0x0b, 0x53, 0x4c, 0x49, 0x4d, 0x5f, 0x57, 0x49, 0x4e, 0x44, 0x4f, 0x57, 0x10, - 0x01, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x41, 0x42, 0x10, 0x02, 0x2a, 0x37, 0x0a, 0x13, 0x57, 0x6f, - 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, - 0x6e, 0x12, 0x09, 0x0a, 0x05, 0x53, 0x54, 0x41, 0x52, 0x54, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, - 0x53, 0x54, 0x4f, 0x50, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x53, 0x54, 0x52, 0x4f, - 0x59, 0x10, 0x02, 0x2a, 0x35, 0x0a, 0x0b, 0x54, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, - 0x74, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x54, 0x41, 0x52, 0x54, 0x45, 0x44, 0x10, 0x00, 0x12, - 0x0d, 0x0a, 0x09, 0x43, 0x4f, 0x4d, 0x50, 0x4c, 0x45, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, 0x0a, - 0x0a, 0x06, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x02, 0x32, 0x49, 0x0a, 0x0b, 0x50, 0x72, - 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x12, 0x3a, 0x0a, 0x07, 0x53, 0x65, 0x73, - 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, - 0x65, 0x72, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x70, 0x72, 0x6f, - 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x28, 0x01, 0x30, 0x01, 0x42, 0x30, 0x5a, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, - 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, - 0x76, 0x32, 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, 0x6e, 0x67, 0x73, 0x12, 0x2d, 0x0a, 0x07, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x73, 0x18, + 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, + 0x6e, 0x65, 0x72, 0x2e, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x52, 0x07, 0x6d, 0x6f, 0x64, 0x75, + 0x6c, 0x65, 0x73, 0x12, 0x2d, 0x0a, 0x07, 0x70, 0x72, 0x65, 0x73, 0x65, 0x74, 0x73, 0x18, 0x08, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, + 0x65, 0x72, 0x2e, 0x50, 0x72, 0x65, 0x73, 0x65, 0x74, 0x52, 0x07, 0x70, 0x72, 0x65, 0x73, 0x65, + 0x74, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0c, + 0x52, 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x22, 0x41, 0x0a, 0x0c, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, + 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, + 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0xbe, 0x02, 0x0a, 0x0d, 0x41, 0x70, + 0x70, 0x6c, 0x79, 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, 0x12, 0x3a, 0x0a, 0x0a, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, + 0x69, 0x63, 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x52, 0x0a, 0x70, 0x61, + 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x12, 0x61, 0x0a, 0x17, 0x65, 0x78, 0x74, 0x65, + 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, + 0x65, 0x72, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x70, 0x72, 0x6f, 0x76, + 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, + 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x52, 0x15, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x75, + 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x12, 0x2d, 0x0a, 0x07, 0x74, + 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, + 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x54, 0x69, 0x6d, 0x69, 0x6e, + 0x67, 0x52, 0x07, 0x74, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x73, 0x22, 0xfa, 0x01, 0x0a, 0x06, 0x54, + 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x12, 0x30, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, 0x2c, 0x0a, 0x03, 0x65, 0x6e, 0x64, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x52, 0x03, 0x65, 0x6e, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, + 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x67, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x05, 0x73, 0x74, 0x61, 0x67, 0x65, 0x12, 0x2e, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, + 0x18, 0x07, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, + 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x54, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x65, + 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x22, 0x0f, 0x0a, 0x0d, 0x43, 0x61, 0x6e, 0x63, 0x65, + 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x8c, 0x02, 0x0a, 0x07, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x2d, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, + 0x65, 0x72, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x48, 0x00, 0x52, 0x06, 0x63, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x12, 0x31, 0x0a, 0x05, 0x70, 0x61, 0x72, 0x73, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, + 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, + 0x05, 0x70, 0x61, 0x72, 0x73, 0x65, 0x12, 0x2e, 0x0a, 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, + 0x65, 0x72, 0x2e, 0x50, 0x6c, 0x61, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, + 0x52, 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x12, 0x31, 0x0a, 0x05, 0x61, 0x70, 0x70, 0x6c, 0x79, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, + 0x6e, 0x65, 0x72, 0x2e, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x48, 0x00, 0x52, 0x05, 0x61, 0x70, 0x70, 0x6c, 0x79, 0x12, 0x34, 0x0a, 0x06, 0x63, 0x61, 0x6e, + 0x63, 0x65, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, + 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x06, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x42, + 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0xd1, 0x01, 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, 0x32, 0x0a, 0x05, 0x70, 0x61, + 0x72, 0x73, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, + 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x43, 0x6f, 0x6d, + 0x70, 0x6c, 0x65, 0x74, 0x65, 0x48, 0x00, 0x52, 0x05, 0x70, 0x61, 0x72, 0x73, 0x65, 0x12, 0x2f, + 0x0a, 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x70, + 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x6c, 0x61, 0x6e, 0x43, + 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x48, 0x00, 0x52, 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x12, + 0x32, 0x0a, 0x05, 0x61, 0x70, 0x70, 0x6c, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, + 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x41, 0x70, 0x70, + 0x6c, 0x79, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x48, 0x00, 0x52, 0x05, 0x61, 0x70, + 0x70, 0x6c, 0x79, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x2a, 0x3f, 0x0a, 0x08, 0x4c, + 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x52, 0x41, 0x43, 0x45, + 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x44, 0x45, 0x42, 0x55, 0x47, 0x10, 0x01, 0x12, 0x08, 0x0a, + 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x57, 0x41, 0x52, 0x4e, 0x10, + 0x03, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x04, 0x2a, 0x3b, 0x0a, 0x0f, + 0x41, 0x70, 0x70, 0x53, 0x68, 0x61, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, + 0x09, 0x0a, 0x05, 0x4f, 0x57, 0x4e, 0x45, 0x52, 0x10, 0x00, 0x12, 0x11, 0x0a, 0x0d, 0x41, 0x55, + 0x54, 0x48, 0x45, 0x4e, 0x54, 0x49, 0x43, 0x41, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, 0x0a, 0x0a, + 0x06, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x10, 0x02, 0x2a, 0x35, 0x0a, 0x09, 0x41, 0x70, 0x70, + 0x4f, 0x70, 0x65, 0x6e, 0x49, 0x6e, 0x12, 0x0e, 0x0a, 0x06, 0x57, 0x49, 0x4e, 0x44, 0x4f, 0x57, + 0x10, 0x00, 0x1a, 0x02, 0x08, 0x01, 0x12, 0x0f, 0x0a, 0x0b, 0x53, 0x4c, 0x49, 0x4d, 0x5f, 0x57, + 0x49, 0x4e, 0x44, 0x4f, 0x57, 0x10, 0x01, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x41, 0x42, 0x10, 0x02, + 0x2a, 0x37, 0x0a, 0x13, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, + 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x09, 0x0a, 0x05, 0x53, 0x54, 0x41, 0x52, 0x54, + 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x53, 0x54, 0x4f, 0x50, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, + 0x44, 0x45, 0x53, 0x54, 0x52, 0x4f, 0x59, 0x10, 0x02, 0x2a, 0x35, 0x0a, 0x0b, 0x54, 0x69, 0x6d, + 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x54, 0x41, 0x52, + 0x54, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x43, 0x4f, 0x4d, 0x50, 0x4c, 0x45, 0x54, + 0x45, 0x44, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x02, + 0x32, 0x49, 0x0a, 0x0b, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x12, + 0x3a, 0x0a, 0x07, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x2e, 0x70, 0x72, 0x6f, + 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x30, 0x01, 0x42, 0x30, 0x5a, 0x2e, 0x67, + 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, + 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 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 ( @@ -4061,7 +4123,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, 42) +var file_provisionersdk_proto_provisioner_proto_msgTypes = make([]protoimpl.MessageInfo, 43) var file_provisionersdk_proto_provisioner_proto_goTypes = []interface{}{ (LogLevel)(0), // 0: provisioner.LogLevel (AppSharingLevel)(0), // 1: provisioner.AppSharingLevel @@ -4094,32 +4156,33 @@ var file_provisionersdk_proto_provisioner_proto_goTypes = []interface{}{ (*Resource)(nil), // 28: provisioner.Resource (*Module)(nil), // 29: provisioner.Module (*Role)(nil), // 30: provisioner.Role - (*Metadata)(nil), // 31: provisioner.Metadata - (*Config)(nil), // 32: provisioner.Config - (*ParseRequest)(nil), // 33: provisioner.ParseRequest - (*ParseComplete)(nil), // 34: provisioner.ParseComplete - (*PlanRequest)(nil), // 35: provisioner.PlanRequest - (*PlanComplete)(nil), // 36: provisioner.PlanComplete - (*ApplyRequest)(nil), // 37: provisioner.ApplyRequest - (*ApplyComplete)(nil), // 38: provisioner.ApplyComplete - (*Timing)(nil), // 39: provisioner.Timing - (*CancelRequest)(nil), // 40: provisioner.CancelRequest - (*Request)(nil), // 41: provisioner.Request - (*Response)(nil), // 42: provisioner.Response - (*Agent_Metadata)(nil), // 43: provisioner.Agent.Metadata - nil, // 44: provisioner.Agent.EnvEntry - (*Resource_Metadata)(nil), // 45: provisioner.Resource.Metadata - nil, // 46: provisioner.ParseComplete.WorkspaceTagsEntry - (*timestamppb.Timestamp)(nil), // 47: google.protobuf.Timestamp + (*RunningAgentAuthToken)(nil), // 31: provisioner.RunningAgentAuthToken + (*Metadata)(nil), // 32: provisioner.Metadata + (*Config)(nil), // 33: provisioner.Config + (*ParseRequest)(nil), // 34: provisioner.ParseRequest + (*ParseComplete)(nil), // 35: provisioner.ParseComplete + (*PlanRequest)(nil), // 36: provisioner.PlanRequest + (*PlanComplete)(nil), // 37: provisioner.PlanComplete + (*ApplyRequest)(nil), // 38: provisioner.ApplyRequest + (*ApplyComplete)(nil), // 39: provisioner.ApplyComplete + (*Timing)(nil), // 40: provisioner.Timing + (*CancelRequest)(nil), // 41: provisioner.CancelRequest + (*Request)(nil), // 42: provisioner.Request + (*Response)(nil), // 43: provisioner.Response + (*Agent_Metadata)(nil), // 44: provisioner.Agent.Metadata + nil, // 45: provisioner.Agent.EnvEntry + (*Resource_Metadata)(nil), // 46: provisioner.Resource.Metadata + nil, // 47: provisioner.ParseComplete.WorkspaceTagsEntry + (*timestamppb.Timestamp)(nil), // 48: google.protobuf.Timestamp } var file_provisionersdk_proto_provisioner_proto_depIdxs = []int32{ 7, // 0: provisioner.RichParameter.options:type_name -> provisioner.RichParameterOption 12, // 1: provisioner.Preset.parameters:type_name -> provisioner.PresetParameter 10, // 2: provisioner.Preset.prebuild:type_name -> provisioner.Prebuild 0, // 3: provisioner.Log.level:type_name -> provisioner.LogLevel - 44, // 4: provisioner.Agent.env:type_name -> provisioner.Agent.EnvEntry + 45, // 4: provisioner.Agent.env:type_name -> provisioner.Agent.EnvEntry 26, // 5: provisioner.Agent.apps:type_name -> provisioner.App - 43, // 6: provisioner.Agent.metadata:type_name -> provisioner.Agent.Metadata + 44, // 6: provisioner.Agent.metadata:type_name -> provisioner.Agent.Metadata 22, // 7: provisioner.Agent.display_apps:type_name -> provisioner.DisplayApps 24, // 8: provisioner.Agent.scripts:type_name -> provisioner.Script 23, // 9: provisioner.Agent.extra_envs:type_name -> provisioner.Env @@ -4131,45 +4194,46 @@ var file_provisionersdk_proto_provisioner_proto_depIdxs = []int32{ 1, // 15: provisioner.App.sharing_level:type_name -> provisioner.AppSharingLevel 2, // 16: provisioner.App.open_in:type_name -> provisioner.AppOpenIn 18, // 17: provisioner.Resource.agents:type_name -> provisioner.Agent - 45, // 18: provisioner.Resource.metadata:type_name -> provisioner.Resource.Metadata + 46, // 18: provisioner.Resource.metadata:type_name -> provisioner.Resource.Metadata 3, // 19: provisioner.Metadata.workspace_transition:type_name -> provisioner.WorkspaceTransition 30, // 20: provisioner.Metadata.workspace_owner_rbac_roles:type_name -> provisioner.Role - 6, // 21: provisioner.ParseComplete.template_variables:type_name -> provisioner.TemplateVariable - 46, // 22: provisioner.ParseComplete.workspace_tags:type_name -> provisioner.ParseComplete.WorkspaceTagsEntry - 31, // 23: provisioner.PlanRequest.metadata:type_name -> provisioner.Metadata - 9, // 24: provisioner.PlanRequest.rich_parameter_values:type_name -> provisioner.RichParameterValue - 13, // 25: provisioner.PlanRequest.variable_values:type_name -> provisioner.VariableValue - 17, // 26: provisioner.PlanRequest.external_auth_providers:type_name -> provisioner.ExternalAuthProvider - 28, // 27: provisioner.PlanComplete.resources:type_name -> provisioner.Resource - 8, // 28: provisioner.PlanComplete.parameters:type_name -> provisioner.RichParameter - 16, // 29: provisioner.PlanComplete.external_auth_providers:type_name -> provisioner.ExternalAuthProviderResource - 39, // 30: provisioner.PlanComplete.timings:type_name -> provisioner.Timing - 29, // 31: provisioner.PlanComplete.modules:type_name -> provisioner.Module - 11, // 32: provisioner.PlanComplete.presets:type_name -> provisioner.Preset - 31, // 33: provisioner.ApplyRequest.metadata:type_name -> provisioner.Metadata - 28, // 34: provisioner.ApplyComplete.resources:type_name -> provisioner.Resource - 8, // 35: provisioner.ApplyComplete.parameters:type_name -> provisioner.RichParameter - 16, // 36: provisioner.ApplyComplete.external_auth_providers:type_name -> provisioner.ExternalAuthProviderResource - 39, // 37: provisioner.ApplyComplete.timings:type_name -> provisioner.Timing - 47, // 38: provisioner.Timing.start:type_name -> google.protobuf.Timestamp - 47, // 39: provisioner.Timing.end:type_name -> google.protobuf.Timestamp - 4, // 40: provisioner.Timing.state:type_name -> provisioner.TimingState - 32, // 41: provisioner.Request.config:type_name -> provisioner.Config - 33, // 42: provisioner.Request.parse:type_name -> provisioner.ParseRequest - 35, // 43: provisioner.Request.plan:type_name -> provisioner.PlanRequest - 37, // 44: provisioner.Request.apply:type_name -> provisioner.ApplyRequest - 40, // 45: provisioner.Request.cancel:type_name -> provisioner.CancelRequest - 14, // 46: provisioner.Response.log:type_name -> provisioner.Log - 34, // 47: provisioner.Response.parse:type_name -> provisioner.ParseComplete - 36, // 48: provisioner.Response.plan:type_name -> provisioner.PlanComplete - 38, // 49: provisioner.Response.apply:type_name -> provisioner.ApplyComplete - 41, // 50: provisioner.Provisioner.Session:input_type -> provisioner.Request - 42, // 51: provisioner.Provisioner.Session:output_type -> provisioner.Response - 51, // [51:52] is the sub-list for method output_type - 50, // [50:51] is the sub-list for method input_type - 50, // [50:50] is the sub-list for extension type_name - 50, // [50:50] is the sub-list for extension extendee - 0, // [0:50] is the sub-list for field type_name + 31, // 21: provisioner.Metadata.running_agent_auth_tokens:type_name -> provisioner.RunningAgentAuthToken + 6, // 22: provisioner.ParseComplete.template_variables:type_name -> provisioner.TemplateVariable + 47, // 23: provisioner.ParseComplete.workspace_tags:type_name -> provisioner.ParseComplete.WorkspaceTagsEntry + 32, // 24: provisioner.PlanRequest.metadata:type_name -> provisioner.Metadata + 9, // 25: provisioner.PlanRequest.rich_parameter_values:type_name -> provisioner.RichParameterValue + 13, // 26: provisioner.PlanRequest.variable_values:type_name -> provisioner.VariableValue + 17, // 27: provisioner.PlanRequest.external_auth_providers:type_name -> provisioner.ExternalAuthProvider + 28, // 28: provisioner.PlanComplete.resources:type_name -> provisioner.Resource + 8, // 29: provisioner.PlanComplete.parameters:type_name -> provisioner.RichParameter + 16, // 30: provisioner.PlanComplete.external_auth_providers:type_name -> provisioner.ExternalAuthProviderResource + 40, // 31: provisioner.PlanComplete.timings:type_name -> provisioner.Timing + 29, // 32: provisioner.PlanComplete.modules:type_name -> provisioner.Module + 11, // 33: provisioner.PlanComplete.presets:type_name -> provisioner.Preset + 32, // 34: provisioner.ApplyRequest.metadata:type_name -> provisioner.Metadata + 28, // 35: provisioner.ApplyComplete.resources:type_name -> provisioner.Resource + 8, // 36: provisioner.ApplyComplete.parameters:type_name -> provisioner.RichParameter + 16, // 37: provisioner.ApplyComplete.external_auth_providers:type_name -> provisioner.ExternalAuthProviderResource + 40, // 38: provisioner.ApplyComplete.timings:type_name -> provisioner.Timing + 48, // 39: provisioner.Timing.start:type_name -> google.protobuf.Timestamp + 48, // 40: provisioner.Timing.end:type_name -> google.protobuf.Timestamp + 4, // 41: provisioner.Timing.state:type_name -> provisioner.TimingState + 33, // 42: provisioner.Request.config:type_name -> provisioner.Config + 34, // 43: provisioner.Request.parse:type_name -> provisioner.ParseRequest + 36, // 44: provisioner.Request.plan:type_name -> provisioner.PlanRequest + 38, // 45: provisioner.Request.apply:type_name -> provisioner.ApplyRequest + 41, // 46: provisioner.Request.cancel:type_name -> provisioner.CancelRequest + 14, // 47: provisioner.Response.log:type_name -> provisioner.Log + 35, // 48: provisioner.Response.parse:type_name -> provisioner.ParseComplete + 37, // 49: provisioner.Response.plan:type_name -> provisioner.PlanComplete + 39, // 50: provisioner.Response.apply:type_name -> provisioner.ApplyComplete + 42, // 51: provisioner.Provisioner.Session:input_type -> provisioner.Request + 43, // 52: provisioner.Provisioner.Session:output_type -> provisioner.Response + 52, // [52:53] is the sub-list for method output_type + 51, // [51:52] is the sub-list for method input_type + 51, // [51:51] is the sub-list for extension type_name + 51, // [51:51] is the sub-list for extension extendee + 0, // [0:51] is the sub-list for field type_name } func init() { file_provisionersdk_proto_provisioner_proto_init() } @@ -4491,7 +4555,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Metadata); i { + switch v := v.(*RunningAgentAuthToken); i { case 0: return &v.state case 1: @@ -4503,7 +4567,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[27].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Config); i { + switch v := v.(*Metadata); i { case 0: return &v.state case 1: @@ -4515,7 +4579,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[28].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ParseRequest); i { + switch v := v.(*Config); i { case 0: return &v.state case 1: @@ -4527,7 +4591,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[29].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ParseComplete); i { + switch v := v.(*ParseRequest); i { case 0: return &v.state case 1: @@ -4539,7 +4603,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[30].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PlanRequest); i { + switch v := v.(*ParseComplete); i { case 0: return &v.state case 1: @@ -4551,7 +4615,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[31].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PlanComplete); i { + switch v := v.(*PlanRequest); i { case 0: return &v.state case 1: @@ -4563,7 +4627,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[32].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ApplyRequest); i { + switch v := v.(*PlanComplete); i { case 0: return &v.state case 1: @@ -4575,7 +4639,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[33].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ApplyComplete); i { + switch v := v.(*ApplyRequest); i { case 0: return &v.state case 1: @@ -4587,7 +4651,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[34].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Timing); i { + switch v := v.(*ApplyComplete); i { case 0: return &v.state case 1: @@ -4599,7 +4663,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[35].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CancelRequest); i { + switch v := v.(*Timing); i { case 0: return &v.state case 1: @@ -4611,7 +4675,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[36].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Request); i { + switch v := v.(*CancelRequest); i { case 0: return &v.state case 1: @@ -4623,7 +4687,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[37].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Response); i { + switch v := v.(*Request); i { case 0: return &v.state case 1: @@ -4635,6 +4699,18 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[38].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Response); 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[39].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Agent_Metadata); i { case 0: return &v.state @@ -4646,7 +4722,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { return nil } } - file_provisionersdk_proto_provisioner_proto_msgTypes[40].Exporter = func(v interface{}, i int) interface{} { + file_provisionersdk_proto_provisioner_proto_msgTypes[41].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Resource_Metadata); i { case 0: return &v.state @@ -4664,14 +4740,14 @@ func file_provisionersdk_proto_provisioner_proto_init() { (*Agent_Token)(nil), (*Agent_InstanceId)(nil), } - file_provisionersdk_proto_provisioner_proto_msgTypes[36].OneofWrappers = []interface{}{ + file_provisionersdk_proto_provisioner_proto_msgTypes[37].OneofWrappers = []interface{}{ (*Request_Config)(nil), (*Request_Parse)(nil), (*Request_Plan)(nil), (*Request_Apply)(nil), (*Request_Cancel)(nil), } - file_provisionersdk_proto_provisioner_proto_msgTypes[37].OneofWrappers = []interface{}{ + file_provisionersdk_proto_provisioner_proto_msgTypes[38].OneofWrappers = []interface{}{ (*Response_Log)(nil), (*Response_Parse)(nil), (*Response_Plan)(nil), @@ -4683,7 +4759,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_provisionersdk_proto_provisioner_proto_rawDesc, NumEnums: 5, - NumMessages: 42, + NumMessages: 43, NumExtensions: 0, NumServices: 1, }, diff --git a/provisionersdk/proto/provisioner.proto b/provisionersdk/proto/provisioner.proto index 3e6841fb24450..fb3600b42a642 100644 --- a/provisionersdk/proto/provisioner.proto +++ b/provisionersdk/proto/provisioner.proto @@ -272,6 +272,11 @@ message Role { string org_id = 2; } +message RunningAgentAuthToken { + string agent_id = 1; + string token = 2; +} + // Metadata is information about a workspace used in the execution of a build message Metadata { string coder_url = 1; @@ -294,7 +299,7 @@ message Metadata { string workspace_owner_login_type = 18; repeated Role workspace_owner_rbac_roles = 19; bool is_prebuild = 20; - string running_workspace_agent_token = 21; + repeated RunningAgentAuthToken running_agent_auth_tokens = 21; } // Config represents execution configuration shared by all subsequent requests in the Session diff --git a/site/e2e/provisionerGenerated.ts b/site/e2e/provisionerGenerated.ts index cea6f9cb364af..e33d3fb5b3bb6 100644 --- a/site/e2e/provisionerGenerated.ts +++ b/site/e2e/provisionerGenerated.ts @@ -286,6 +286,11 @@ export interface Role { orgId: string; } +export interface RunningAgentAuthToken { + agentId: string; + token: string; +} + /** Metadata is information about a workspace used in the execution of a build */ export interface Metadata { coderUrl: string; @@ -308,7 +313,7 @@ export interface Metadata { workspaceOwnerLoginType: string; workspaceOwnerRbacRoles: Role[]; isPrebuild: boolean; - runningWorkspaceAgentToken: string; + runningAgentAuthTokens: RunningAgentAuthToken[]; } /** Config represents execution configuration shared by all subsequent requests in the Session */ @@ -968,6 +973,18 @@ export const Role = { }, }; +export const RunningAgentAuthToken = { + encode(message: RunningAgentAuthToken, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.agentId !== "") { + writer.uint32(10).string(message.agentId); + } + if (message.token !== "") { + writer.uint32(18).string(message.token); + } + return writer; + }, +}; + export const Metadata = { encode(message: Metadata, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { if (message.coderUrl !== "") { @@ -1030,8 +1047,8 @@ export const Metadata = { if (message.isPrebuild === true) { writer.uint32(160).bool(message.isPrebuild); } - if (message.runningWorkspaceAgentToken !== "") { - writer.uint32(170).string(message.runningWorkspaceAgentToken); + for (const v of message.runningAgentAuthTokens) { + RunningAgentAuthToken.encode(v!, writer.uint32(170).fork()).ldelim(); } return writer; }, From 476fe71f9acba3ca6d7ca2f14e098d4e38576e3b Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Mon, 21 Apr 2025 12:26:24 +0000 Subject: [PATCH 02/42] fix assignment to nil map --- coderd/workspaces.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 6f6bb937c0a9c..2a2d88ea72d03 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -634,17 +634,17 @@ func createWorkspace( } var ( - provisionerJob *database.ProvisionerJob - workspaceBuild *database.WorkspaceBuild - provisionerDaemons []database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow - agentTokensByAgentID map[uuid.UUID]string + provisionerJob *database.ProvisionerJob + workspaceBuild *database.WorkspaceBuild + provisionerDaemons []database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow ) err = api.Database.InTx(func(db database.Store) error { var ( - workspaceID uuid.UUID - claimedWorkspace *database.Workspace - prebuildsClaimer = *api.PrebuildsClaimer.Load() + prebuildsClaimer = *api.PrebuildsClaimer.Load() + workspaceID uuid.UUID + claimedWorkspace *database.Workspace + agentTokensByAgentID map[uuid.UUID]string ) // If a template preset was chosen, try claim a prebuilt workspace. @@ -689,6 +689,7 @@ func createWorkspace( api.Logger.Error(ctx, "failed to retrieve running agents of claimed prebuilt workspace", slog.F("workspace_id", claimedWorkspace.ID), slog.Error(err)) } + agentTokensByAgentID = make(map[uuid.UUID]string, len(agents)) for _, agent := range agents { agentTokensByAgentID[agent.ID] = agent.AuthToken.String() } From 8c8bca6886aa6731af799012f5ed89539cc2553e Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Wed, 23 Apr 2025 12:15:39 +0000 Subject: [PATCH 03/42] fix: ensure prebuilt workspace agent tokens are reused when a prebuild agent reinitializes --- provisioner/terraform/executor.go | 75 +++++++++++++++++++++++++----- provisioner/terraform/provision.go | 19 +++----- 2 files changed, 71 insertions(+), 23 deletions(-) diff --git a/provisioner/terraform/executor.go b/provisioner/terraform/executor.go index 7d7f72a470e90..e664115f7e3a7 100644 --- a/provisioner/terraform/executor.go +++ b/provisioner/terraform/executor.go @@ -261,17 +261,6 @@ func (e *executor) plan(ctx, killCtx context.Context, env, vars []string, logr l e.mut.Lock() defer e.mut.Unlock() - // TODO: defunct? - // var isPrebuild bool - // for _, v := range env { - // if envName(v) == provider.IsPrebuildEnvironmentVariable() && envVar(v) == "true" { - // isPrebuild = true - // break - // } - // } - - // _ = isPrebuild - planfilePath := getPlanFilePath(e.workdir) args := []string{ "plan", @@ -341,6 +330,68 @@ func onlyDataResources(sm tfjson.StateModule) tfjson.StateModule { return filtered } +func (e *executor) logResourceReplacements(ctx context.Context, plan *tfjson.Plan) { + if plan == nil { + return + } + + if len(plan.ResourceChanges) == 0 { + return + } + var ( + count int + replacements = make(map[string][]string, len(plan.ResourceChanges)) + ) + + for _, ch := range plan.ResourceChanges { + // No change, no problem! + if ch.Change == nil { + continue + } + + // No-op change, no problem! + if ch.Change.Actions.NoOp() { + continue + } + + // No replacements, no problem! + if len(ch.Change.ReplacePaths) == 0 { + continue + } + + // Replacing our resources, no problem! + if strings.Index(ch.Type, "coder_") == 0 { + continue + } + + for _, p := range ch.Change.ReplacePaths { + var path string + switch p := p.(type) { + case []interface{}: + segs := p + list := make([]string, 0, len(segs)) + for _, s := range segs { + list = append(list, fmt.Sprintf("%v", s)) + } + path = strings.Join(list, ".") + default: + path = fmt.Sprintf("%v", p) + } + + replacements[ch.Address] = append(replacements[ch.Address], path) + } + + count++ + } + + if count > 0 { + e.server.logger.Warn(ctx, "plan introduces resource changes", slog.F("count", count)) + for n, p := range replacements { + e.server.logger.Warn(ctx, "resource will be replaced!", slog.F("name", n), slog.F("replacement_paths", strings.Join(p, ","))) + } + } +} + // planResources must only be called while the lock is held. func (e *executor) planResources(ctx, killCtx context.Context, planfilePath string) (*State, json.RawMessage, error) { ctx, span := e.server.startTrace(ctx, tracing.FuncName()) @@ -351,6 +402,8 @@ func (e *executor) planResources(ctx, killCtx context.Context, planfilePath stri return nil, nil, xerrors.Errorf("show terraform plan file: %w", err) } + e.logResourceReplacements(ctx, plan) + rawGraph, err := e.graph(ctx, killCtx) if err != nil { return nil, nil, xerrors.Errorf("graph: %w", err) diff --git a/provisioner/terraform/provision.go b/provisioner/terraform/provision.go index c31f5a300a3fe..f3bc3c18129ce 100644 --- a/provisioner/terraform/provision.go +++ b/provisioner/terraform/provision.go @@ -272,21 +272,16 @@ func provisionEnv( ) if metadata.GetIsPrebuild() { env = append(env, provider.IsPrebuildEnvironmentVariable()+"=true") + } + tokens := metadata.GetRunningAgentAuthTokens() + if len(tokens) == 1 { + env = append(env, provider.RunningAgentTokenEnvironmentVariable("")+"="+tokens[0].Token) } else { - // TODO: provide an agentID to these functions so that we provide the right token - // for single agent support, we use the zero value "" as the agentID - // TODO: looks like we only provide agent tokens for reinit if metadata.GetIsPrebuild() is false - // check this for consistency wherever else we use the isPrebuild attribute from the Proto and where we use the env derived from it. - const singleAgentID = "" - tokens := metadata.GetRunningAgentAuthTokens() - var token string for _, t := range tokens { - if t.AgentId == singleAgentID { - token = t.Token - break - } + // If there are multiple agents, provide all the tokens to terraform so that it can + // choose the correct one for each agent ID. + env = append(env, provider.RunningAgentTokenEnvironmentVariable(t.AgentId)+"="+t.Token) } - env = append(env, provider.RunningAgentTokenEnvironmentVariable(singleAgentID)+"="+token) } for key, value := range provisionersdk.AgentScriptEnv() { env = append(env, key+"="+value) From 7ce4eea2af98c805bd61ade3be14086c2e684f4a Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Thu, 24 Apr 2025 12:14:06 +0000 Subject: [PATCH 04/42] test agent reinitialization --- cli/agent.go | 2 +- cli/agent_test.go | 52 ++++++++++++++++++++++++++++++++ coderd/coderdtest/coderdtest.go | 53 +++++++++++++++++++++++++++++++++ coderd/workspaces.go | 9 ++++-- go.sum | 2 -- 5 files changed, 112 insertions(+), 6 deletions(-) diff --git a/cli/agent.go b/cli/agent.go index f8252c9b8a699..b99ce20fa1644 100644 --- a/cli/agent.go +++ b/cli/agent.go @@ -413,7 +413,7 @@ func (r *RootCmd) workspaceAgent() *serpent.Command { break } - logger.Info(ctx, "reinitializing...") + logger.Info(ctx, "agent reinitializing") } return lastErr }, diff --git a/cli/agent_test.go b/cli/agent_test.go index 0a948c0c84e9a..262df4febdd48 100644 --- a/cli/agent_test.go +++ b/cli/agent_test.go @@ -21,7 +21,9 @@ import ( "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbfake" + "github.com/coder/coder/v2/coderd/database/dbtestutil" "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/codersdk/agentsdk" "github.com/coder/coder/v2/codersdk/workspacesdk" "github.com/coder/coder/v2/provisionersdk/proto" "github.com/coder/coder/v2/testutil" @@ -321,6 +323,56 @@ func TestWorkspaceAgent(t *testing.T) { }) } +func TestAgent_Prebuild(t *testing.T) { + t.Parallel() + + db, pubsub := dbtestutil.NewDB(t) + client := coderdtest.New(t, &coderdtest.Options{ + Database: db, + Pubsub: pubsub, + }) + user := coderdtest.CreateFirstUser(t, client) + r := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{ + OrganizationID: user.OrganizationID, + OwnerID: user.UserID, + }).WithAgent(func(a []*proto.Agent) []*proto.Agent { + a[0].Scripts = []*proto.Script{ + { + DisplayName: "Prebuild Test Script", + Script: "sleep 5", // Make reinitiazation take long enough to assert that it happened + RunOnStart: true, + }, + } + return a + }).Do() + + // Spin up an agent + logDir := t.TempDir() + inv, _ := clitest.New(t, + "agent", + "--auth", "token", + "--agent-token", r.AgentToken, + "--agent-url", client.URL.String(), + "--log-dir", logDir, + ) + clitest.Start(t, inv) + + // Check that the agent is in a happy steady state + waiter := coderdtest.NewWorkspaceAgentWaiter(t, client, r.Workspace.ID) + waiter.WaitFor(coderdtest.AgentReady) + + // Trigger reinitialization + channel := agentsdk.PrebuildClaimedChannel(r.Workspace.ID) + err := pubsub.Publish(channel, []byte(user.UserID.String())) + require.NoError(t, err) + + // Check that the agent reinitializes + waiter.WaitFor(coderdtest.AgentNotReady) + + // Check that reinitialization completed + waiter.WaitFor(coderdtest.AgentReady) +} + func matchAgentWithVersion(rs []codersdk.WorkspaceResource) bool { if len(rs) < 1 { return false diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index dbf1f62abfb28..e9663761dccaf 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -1105,6 +1105,59 @@ func (w WorkspaceAgentWaiter) MatchResources(m func([]codersdk.WorkspaceResource return w } +type WaitForCriterium func(agent codersdk.WorkspaceAgent) bool + +func AgentReady(agent codersdk.WorkspaceAgent) bool { + return agent.LifecycleState == codersdk.WorkspaceAgentLifecycleReady +} + +func AgentNotReady(agent codersdk.WorkspaceAgent) bool { + return !AgentReady(agent) +} + +func (w WorkspaceAgentWaiter) WaitFor(criteria ...WaitForCriterium) { + w.t.Helper() + + agentNamesMap := make(map[string]struct{}, len(w.agentNames)) + for _, name := range w.agentNames { + agentNamesMap[name] = struct{}{} + } + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + w.t.Logf("waiting for workspace agents (workspace %s)", w.workspaceID) + require.Eventually(w.t, func() bool { + var err error + workspace, err := w.client.Workspace(ctx, w.workspaceID) + if err != nil { + return false + } + if workspace.LatestBuild.Job.CompletedAt == nil { + return false + } + if workspace.LatestBuild.Job.CompletedAt.IsZero() { + return false + } + + for _, resource := range workspace.LatestBuild.Resources { + for _, agent := range resource.Agents { + if len(w.agentNames) > 0 { + if _, ok := agentNamesMap[agent.Name]; !ok { + continue + } + } + for _, criterium := range criteria { + if !criterium(agent) { + return false + } + } + } + } + return true + }, testutil.WaitLong, testutil.IntervalMedium) +} + // Wait waits for the agent(s) to connect and fails the test if they do not within testutil.WaitLong func (w WorkspaceAgentWaiter) Wait() []codersdk.WorkspaceResource { w.t.Helper() diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 2a2d88ea72d03..9875286c37f90 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -689,10 +689,13 @@ func createWorkspace( api.Logger.Error(ctx, "failed to retrieve running agents of claimed prebuilt workspace", slog.F("workspace_id", claimedWorkspace.ID), slog.Error(err)) } - agentTokensByAgentID = make(map[uuid.UUID]string, len(agents)) - for _, agent := range agents { - agentTokensByAgentID[agent.ID] = agent.AuthToken.String() + if len(agents) > 1 { + return xerrors.Errorf("multiple agents are not yet supported in prebuilt workspaces") } + // agentTokensByAgentID = make(map[uuid.UUID]string, len(agents)) + // for _, agent := range agents { + // agentTokensByAgentID[agent.ID] = agent.AuthToken.String() + // } } // We have to refetch the workspace for the joined in fields. diff --git a/go.sum b/go.sum index acdc4d34c8286..a613e21eebb02 100644 --- a/go.sum +++ b/go.sum @@ -921,8 +921,6 @@ github.com/coder/tailscale v1.1.1-0.20250422090654-5090e715905e h1:nope/SZfoLB9M github.com/coder/tailscale v1.1.1-0.20250422090654-5090e715905e/go.mod h1:1ggFFdHTRjPRu9Yc1yA7nVHBYB50w9Ce7VIXNqcW6Ko= github.com/coder/terraform-config-inspect v0.0.0-20250107175719-6d06d90c630e h1:JNLPDi2P73laR1oAclY6jWzAbucf70ASAvf5mh2cME0= github.com/coder/terraform-config-inspect v0.0.0-20250107175719-6d06d90c630e/go.mod h1:Gz/z9Hbn+4KSp8A2FBtNszfLSdT2Tn/uAKGuVqqWmDI= -github.com/coder/terraform-provider-coder/v2 v2.4.0-pre1.0.20250417100258-c86bb5c3ddcd h1:FsIG6Fd0YOEK7D0Hl/CJywRA+Y6Gd5RQbSIa2L+/BmE= -github.com/coder/terraform-provider-coder/v2 v2.4.0-pre1.0.20250417100258-c86bb5c3ddcd/go.mod h1:56/KdGYaA+VbwXJbTI8CA57XPfnuTxN8rjxbR34PbZw= github.com/coder/trivy v0.0.0-20250409153844-e6b004bc465a h1:yryP7e+IQUAArlycH4hQrjXQ64eRNbxsV5/wuVXHgME= github.com/coder/trivy v0.0.0-20250409153844-e6b004bc465a/go.mod h1:dDvq9axp3kZsT63gY2Znd1iwzfqDq3kXbQnccIrjRYY= github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE= From 52ac64e30891b7a59634e901ce38a40c3f715839 Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Thu, 24 Apr 2025 12:36:22 +0000 Subject: [PATCH 05/42] remove defunct metric --- agent/metrics.go | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/agent/metrics.go b/agent/metrics.go index d0307a647a239..1755e43a1a365 100644 --- a/agent/metrics.go +++ b/agent/metrics.go @@ -20,7 +20,6 @@ type agentMetrics struct { // took to run. This is reported once per agent. startupScriptSeconds *prometheus.GaugeVec currentConnections *prometheus.GaugeVec - manifestsReceived prometheus.Counter } func newAgentMetrics(registerer prometheus.Registerer) *agentMetrics { @@ -55,20 +54,11 @@ func newAgentMetrics(registerer prometheus.Registerer) *agentMetrics { }, []string{"connection_type"}) registerer.MustRegister(currentConnections) - manifestsReceived := prometheus.NewCounter(prometheus.CounterOpts{ - Namespace: "coderd", - Subsystem: "agentstats", - Name: "manifests_received", - Help: "The number of manifests this agent has received from the control plane.", - }) - registerer.MustRegister(manifestsReceived) - return &agentMetrics{ connectionsTotal: connectionsTotal, reconnectingPTYErrors: reconnectingPTYErrors, startupScriptSeconds: startupScriptSeconds, currentConnections: currentConnections, - manifestsReceived: manifestsReceived, } } From 362db7c50c6d3e393918f398632120d80e691dd4 Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Fri, 25 Apr 2025 07:47:13 +0000 Subject: [PATCH 06/42] Remove todo --- agent/agent.go | 1 - 1 file changed, 1 deletion(-) diff --git a/agent/agent.go b/agent/agent.go index 9f4a5d0bd54be..6b9a4b86bba8f 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -1053,7 +1053,6 @@ func (a *agent) run() (retErr error) { }) err = connMan.wait() - // TODO: this broke some tests at some point. investigate. if err != nil { a.logger.Warn(context.Background(), "connection manager errored", slog.Error(err)) } From dcc73795b4a52d423c5a035aad79ee3879205c4b Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Mon, 28 Apr 2025 09:03:52 +0000 Subject: [PATCH 07/42] test that we trigger workspace agent reinitialization under the right conditions --- coderd/agentapi/api.go | 2 - coderd/agentapi/manifest.go | 6 - .../provisionerdserver/provisionerdserver.go | 8 +- .../provisionerdserver_test.go | 118 ++++++++++++++++++ codersdk/agentsdk/agentsdk.go | 1 - 5 files changed, 122 insertions(+), 13 deletions(-) diff --git a/coderd/agentapi/api.go b/coderd/agentapi/api.go index c1bd25b3e6514..1b2b8d92a10ef 100644 --- a/coderd/agentapi/api.go +++ b/coderd/agentapi/api.go @@ -109,8 +109,6 @@ func New(opts Options) *API { Database: opts.Database, DerpMapFn: opts.DerpMapFn, WorkspaceID: opts.WorkspaceID, - Log: opts.Log.Named("manifests"), - Pubsub: opts.Pubsub, } api.AnnouncementBannerAPI = &AnnouncementBannerAPI{ diff --git a/coderd/agentapi/manifest.go b/coderd/agentapi/manifest.go index f760e24ca6c90..db8a0af3946a9 100644 --- a/coderd/agentapi/manifest.go +++ b/coderd/agentapi/manifest.go @@ -8,10 +8,6 @@ import ( "strings" "time" - "cdr.dev/slog" - - "github.com/coder/coder/v2/coderd/database/pubsub" - "github.com/google/uuid" "golang.org/x/sync/errgroup" "golang.org/x/xerrors" @@ -39,8 +35,6 @@ type ManifestAPI struct { AgentFn func(context.Context) (database.WorkspaceAgent, error) Database database.Store DerpMapFn func() *tailcfg.DERPMap - Pubsub pubsub.Pubsub - Log slog.Logger } func (a *ManifestAPI) GetManifest(ctx context.Context, _ *agentproto.GetManifestRequest) (*agentproto.Manifest, error) { diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index c5064e17d8683..aced551daea67 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -1746,13 +1746,13 @@ func (s *server) CompleteJob(ctx context.Context, completed *proto.CompletedJob) } if input.PrebuildClaimedByUser != uuid.Nil { - channel := agentsdk.PrebuildClaimedChannel(workspace.ID) s.Logger.Info(ctx, "workspace prebuild successfully claimed by user", slog.F("user", input.PrebuildClaimedByUser.String()), - slog.F("workspace_id", workspace.ID), - slog.F("channel", channel)) + slog.F("workspace_id", workspace.ID)) + + channel := agentsdk.PrebuildClaimedChannel(workspace.ID) if err := s.Pubsub.Publish(channel, []byte(input.PrebuildClaimedByUser.String())); err != nil { - s.Logger.Error(ctx, "failed to publish message to workspace agent to pull new manifest", slog.Error(err)) + s.Logger.Error(ctx, "failed to publish message to instruct prebuilt workspace agent reinitialization", slog.Error(err)) } } case *proto.CompletedJob_TemplateDryRun_: diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go index caeef8a9793b7..dca6a2b990023 100644 --- a/coderd/provisionerdserver/provisionerdserver_test.go +++ b/coderd/provisionerdserver/provisionerdserver_test.go @@ -24,6 +24,7 @@ import ( "cdr.dev/slog/sloggers/slogtest" "github.com/coder/coder/v2/coderd/rbac" + "github.com/coder/coder/v2/codersdk/agentsdk" "github.com/coder/quartz" "github.com/coder/serpent" @@ -1745,6 +1746,123 @@ func TestCompleteJob(t *testing.T) { }) } }) + + t.Run("ReinitializePrebuiltAgents", func(t *testing.T) { + t.Parallel() + type testcase struct { + name string + shouldReinitializeAgent bool + } + + for _, tc := range []testcase{ + // Whether or not there are presets and those presets define prebuilds, etc + // are all irrelevant at this level. Those factors are useful earlier in the process. + // Everything relevant to this test is determined by whether or not the workspace build job + // has `PrebuildClaimedByUser` set. As such, there are only two significant test cases: + { + name: "claimed prebuild", + shouldReinitializeAgent: true, + }, + { + name: "not a claimed prebuild", + shouldReinitializeAgent: false, + }, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // Setup starting state: + + userID := uuid.New() + + srv, db, ps, pd := setup(t, false, &overrides{}) + + buildID := uuid.New() + scheduledJobInput := provisionerdserver.WorkspaceProvisionJob{ + WorkspaceBuildID: buildID, + } + if tc.shouldReinitializeAgent { // This is the key lever in the test + scheduledJobInput.PrebuildClaimedByUser = userID + } + input, err := json.Marshal(scheduledJobInput) + require.NoError(t, err) + + ctx := testutil.Context(t, time.Second) // Even testutil.WaitShort feels too long for this. + job, err := db.InsertProvisionerJob(ctx, database.InsertProvisionerJobParams{ + Input: input, + Provisioner: database.ProvisionerTypeEcho, + StorageMethod: database.ProvisionerStorageMethodFile, + Type: database.ProvisionerJobTypeWorkspaceBuild, + }) + require.NoError(t, err) + + tpl := dbgen.Template(t, db, database.Template{ + OrganizationID: pd.OrganizationID, + }) + tv := dbgen.TemplateVersion(t, db, database.TemplateVersion{ + TemplateID: uuid.NullUUID{UUID: tpl.ID, Valid: true}, + JobID: job.ID, + }) + workspace := dbgen.Workspace(t, db, database.WorkspaceTable{ + TemplateID: tpl.ID, + }) + _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ + ID: buildID, + JobID: job.ID, + WorkspaceID: workspace.ID, + TemplateVersionID: tv.ID, + }) + _, err = db.AcquireProvisionerJob(ctx, database.AcquireProvisionerJobParams{ + WorkerID: uuid.NullUUID{ + UUID: pd.ID, + Valid: true, + }, + Types: []database.ProvisionerType{database.ProvisionerTypeEcho}, + }) + require.NoError(t, err) + + // Subscribe to workspace reinitialization: + + // If the job needs to reinitialize agents for the workspace, + // check that the instruction to do so was enqueued + eventName := agentsdk.PrebuildClaimedChannel(workspace.ID) + gotChan := make(chan []byte, 1) + cancel, err := ps.Subscribe(eventName, func(inner context.Context, userIDMessage []byte) { + select { + case <-ctx.Done(): + return + case <-inner.Done(): + return + default: + } + gotChan <- userIDMessage + }) + require.NoError(t, err) + defer cancel() + + // Complete the job, optionally triggering workspace agent reinitialization: + + completedJob := proto.CompletedJob{ + JobId: job.ID.String(), + Type: &proto.CompletedJob_WorkspaceBuild_{ + WorkspaceBuild: &proto.CompletedJob_WorkspaceBuild{}, + }, + } + _, err = srv.CompleteJob(ctx, &completedJob) + require.NoError(t, err) + + select { + case userIDMessage := <-gotChan: + gotUserID, err := uuid.ParseBytes(userIDMessage) + require.NoError(t, err) + require.True(t, tc.shouldReinitializeAgent) + require.Equal(t, userID, gotUserID) + case <-ctx.Done(): + require.False(t, tc.shouldReinitializeAgent) + } + }) + } + }) } func TestInsertWorkspacePresetsAndParameters(t *testing.T) { diff --git a/codersdk/agentsdk/agentsdk.go b/codersdk/agentsdk/agentsdk.go index da6a22bed69fc..763d1d548fd2f 100644 --- a/codersdk/agentsdk/agentsdk.go +++ b/codersdk/agentsdk/agentsdk.go @@ -699,7 +699,6 @@ type ReinitializationResponse struct { Reason ReinitializationReason `json:"reason"` } -// TODO: maybe place this somewhere else? func PrebuildClaimedChannel(id uuid.UUID) string { return fmt.Sprintf("prebuild_claimed_%s", id) } From ff66b3fb1e2043b34ae9659d0caca3920c175a07 Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Mon, 28 Apr 2025 09:07:05 +0000 Subject: [PATCH 08/42] slight improvements to a test --- .../provisionerdserver/provisionerdserver_test.go | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go index dca6a2b990023..2f808fe6a16a2 100644 --- a/coderd/provisionerdserver/provisionerdserver_test.go +++ b/coderd/provisionerdserver/provisionerdserver_test.go @@ -1757,8 +1757,8 @@ func TestCompleteJob(t *testing.T) { for _, tc := range []testcase{ // Whether or not there are presets and those presets define prebuilds, etc // are all irrelevant at this level. Those factors are useful earlier in the process. - // Everything relevant to this test is determined by whether or not the workspace build job - // has `PrebuildClaimedByUser` set. As such, there are only two significant test cases: + // Everything relevant to this test is determined by the value of `PrebuildClaimedByUser` + // on the provisioner job. As such, there are only two significant test cases: { name: "claimed prebuild", shouldReinitializeAgent: true, @@ -1787,7 +1787,7 @@ func TestCompleteJob(t *testing.T) { input, err := json.Marshal(scheduledJobInput) require.NoError(t, err) - ctx := testutil.Context(t, time.Second) // Even testutil.WaitShort feels too long for this. + ctx := testutil.Context(t, testutil.WaitShort) job, err := db.InsertProvisionerJob(ctx, database.InsertProvisionerJobParams{ Input: input, Provisioner: database.ProvisionerTypeEcho, @@ -1828,13 +1828,6 @@ func TestCompleteJob(t *testing.T) { eventName := agentsdk.PrebuildClaimedChannel(workspace.ID) gotChan := make(chan []byte, 1) cancel, err := ps.Subscribe(eventName, func(inner context.Context, userIDMessage []byte) { - select { - case <-ctx.Done(): - return - case <-inner.Done(): - return - default: - } gotChan <- userIDMessage }) require.NoError(t, err) From efff5d9e7383653f079d2f682cf003171b005c89 Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Mon, 28 Apr 2025 11:13:03 +0000 Subject: [PATCH 09/42] review notes to improve legibility --- cli/agent.go | 4 ++-- cli/agent_test.go | 6 ++--- coderd/coderdtest/coderdtest.go | 16 ++++++++++--- .../provisionerdserver/provisionerdserver.go | 2 +- .../provisionerdserver_test.go | 23 ++++++++++--------- coderd/workspaceagents.go | 3 +-- coderd/workspaces.go | 4 ---- codersdk/agentsdk/agentsdk.go | 10 ++++---- provisioner/terraform/provision.go | 1 + 9 files changed, 38 insertions(+), 31 deletions(-) diff --git a/cli/agent.go b/cli/agent.go index b99ce20fa1644..63fe590690a41 100644 --- a/cli/agent.go +++ b/cli/agent.go @@ -68,7 +68,7 @@ func (r *RootCmd) workspaceAgent() *serpent.Command { Handler: func(inv *serpent.Invocation) error { ctx, cancel := context.WithCancelCause(inv.Context()) defer func() { - cancel(xerrors.New("defer")) + cancel(xerrors.New("agent exited")) }() var ( @@ -335,7 +335,7 @@ func (r *RootCmd) workspaceAgent() *serpent.Command { // TODO: timeout ok? reinitCtx, reinitCancel := context.WithTimeout(context.Background(), time.Hour*24) defer reinitCancel() - reinitEvents := make(chan agentsdk.ReinitializationResponse) + reinitEvents := make(chan agentsdk.ReinitializationEvent) go func() { // Retry to wait for reinit, main context cancels the retrier. diff --git a/cli/agent_test.go b/cli/agent_test.go index 262df4febdd48..a0b3a491dd51e 100644 --- a/cli/agent_test.go +++ b/cli/agent_test.go @@ -359,7 +359,7 @@ func TestAgent_Prebuild(t *testing.T) { // Check that the agent is in a happy steady state waiter := coderdtest.NewWorkspaceAgentWaiter(t, client, r.Workspace.ID) - waiter.WaitFor(coderdtest.AgentReady) + waiter.WaitFor(coderdtest.AgentsReady) // Trigger reinitialization channel := agentsdk.PrebuildClaimedChannel(r.Workspace.ID) @@ -367,10 +367,10 @@ func TestAgent_Prebuild(t *testing.T) { require.NoError(t, err) // Check that the agent reinitializes - waiter.WaitFor(coderdtest.AgentNotReady) + waiter.WaitFor(coderdtest.AgentsNotReady) // Check that reinitialization completed - waiter.WaitFor(coderdtest.AgentReady) + waiter.WaitFor(coderdtest.AgentsReady) } func matchAgentWithVersion(rs []codersdk.WorkspaceResource) bool { diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index e9663761dccaf..2d525732237a9 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -1105,14 +1105,24 @@ func (w WorkspaceAgentWaiter) MatchResources(m func([]codersdk.WorkspaceResource return w } +// WaitForCriterium represents a boolean assertion to be made against each agent +// that a given WorkspaceAgentWaited knows about. Each WaitForCriterium should apply +// the check to a single agent, but it should be named for plural, because `func (w WorkspaceAgentWaiter) WaitFor` +// applies the check to all agents that it is aware of. This ensures that the public API of the waiter +// reads correctly. For example: +// +// waiter := coderdtest.NewWorkspaceAgentWaiter(t, client, r.Workspace.ID) +// waiter.WaitFor(coderdtest.AgentsReady) type WaitForCriterium func(agent codersdk.WorkspaceAgent) bool -func AgentReady(agent codersdk.WorkspaceAgent) bool { +// AgentsReady checks that the latest lifecycle state of an agent is "Ready". +func AgentsReady(agent codersdk.WorkspaceAgent) bool { return agent.LifecycleState == codersdk.WorkspaceAgentLifecycleReady } -func AgentNotReady(agent codersdk.WorkspaceAgent) bool { - return !AgentReady(agent) +// AgentsReady checks that the latest lifecycle state of an agent is anything except "Ready". +func AgentsNotReady(agent codersdk.WorkspaceAgent) bool { + return !AgentsReady(agent) } func (w WorkspaceAgentWaiter) WaitFor(criteria ...WaitForCriterium) { diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index aced551daea67..01e24dbcb2279 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -1752,7 +1752,7 @@ func (s *server) CompleteJob(ctx context.Context, completed *proto.CompletedJob) channel := agentsdk.PrebuildClaimedChannel(workspace.ID) if err := s.Pubsub.Publish(channel, []byte(input.PrebuildClaimedByUser.String())); err != nil { - s.Logger.Error(ctx, "failed to publish message to instruct prebuilt workspace agent reinitialization", slog.Error(err)) + s.Logger.Error(ctx, "failed to trigger prebuilt workspace agent reinitialization", slog.Error(err)) } } case *proto.CompletedJob_TemplateDryRun_: diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go index 2f808fe6a16a2..8f95c9b56fa9c 100644 --- a/coderd/provisionerdserver/provisionerdserver_test.go +++ b/coderd/provisionerdserver/provisionerdserver_test.go @@ -1771,20 +1771,21 @@ func TestCompleteJob(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - // Setup starting state: + // GIVEN an enqueued provisioner job and its dependencies: userID := uuid.New() srv, db, ps, pd := setup(t, false, &overrides{}) buildID := uuid.New() - scheduledJobInput := provisionerdserver.WorkspaceProvisionJob{ + jobInput := provisionerdserver.WorkspaceProvisionJob{ WorkspaceBuildID: buildID, } if tc.shouldReinitializeAgent { // This is the key lever in the test - scheduledJobInput.PrebuildClaimedByUser = userID + // GIVEN the enqueued provisioner job is for a workspace being claimed by a user: + jobInput.PrebuildClaimedByUser = userID } - input, err := json.Marshal(scheduledJobInput) + input, err := json.Marshal(jobInput) require.NoError(t, err) ctx := testutil.Context(t, testutil.WaitShort) @@ -1821,19 +1822,17 @@ func TestCompleteJob(t *testing.T) { }) require.NoError(t, err) - // Subscribe to workspace reinitialization: + // GIVEN something is listening to process workspace reinitialization: - // If the job needs to reinitialize agents for the workspace, - // check that the instruction to do so was enqueued eventName := agentsdk.PrebuildClaimedChannel(workspace.ID) - gotChan := make(chan []byte, 1) + reinitChan := make(chan []byte, 1) cancel, err := ps.Subscribe(eventName, func(inner context.Context, userIDMessage []byte) { - gotChan <- userIDMessage + reinitChan <- userIDMessage }) require.NoError(t, err) defer cancel() - // Complete the job, optionally triggering workspace agent reinitialization: + // WHEN the jop is completed completedJob := proto.CompletedJob{ JobId: job.ID.String(), @@ -1845,12 +1844,14 @@ func TestCompleteJob(t *testing.T) { require.NoError(t, err) select { - case userIDMessage := <-gotChan: + case userIDMessage := <-reinitChan: + // THEN workspace agent reinitialization instruction was received: gotUserID, err := uuid.ParseBytes(userIDMessage) require.NoError(t, err) require.True(t, tc.shouldReinitializeAgent) require.Equal(t, userID, gotUserID) case <-ctx.Done(): + // THEN workspace agent reinitialization instruction was not received. require.False(t, tc.shouldReinitializeAgent) } }) diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index 388e2eadd4063..898ce6d2ec763 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -1233,7 +1233,6 @@ func (api *API) workspaceAgentReinit(rw http.ResponseWriter, r *http.Request) { Type: codersdk.ServerSentEventTypePing, }) - // Expand with future use-cases for agent reinitialization. for { select { case <-ctx.Done(): @@ -1241,7 +1240,7 @@ func (api *API) workspaceAgentReinit(rw http.ResponseWriter, r *http.Request) { case user := <-prebuildClaims: err = sseSendEvent(codersdk.ServerSentEvent{ Type: codersdk.ServerSentEventTypeData, - Data: agentsdk.ReinitializationResponse{ + Data: agentsdk.ReinitializationEvent{ Message: fmt.Sprintf("prebuild claimed by user: %s", user), Reason: agentsdk.ReinitializeReasonPrebuildClaimed, }, diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 9875286c37f90..c1e67bd2b5a97 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -692,10 +692,6 @@ func createWorkspace( if len(agents) > 1 { return xerrors.Errorf("multiple agents are not yet supported in prebuilt workspaces") } - // agentTokensByAgentID = make(map[uuid.UUID]string, len(agents)) - // for _, agent := range agents { - // agentTokensByAgentID[agent.ID] = agent.AuthToken.String() - // } } // We have to refetch the workspace for the joined in fields. diff --git a/codersdk/agentsdk/agentsdk.go b/codersdk/agentsdk/agentsdk.go index 763d1d548fd2f..e52d314961ad8 100644 --- a/codersdk/agentsdk/agentsdk.go +++ b/codersdk/agentsdk/agentsdk.go @@ -694,7 +694,7 @@ const ( ReinitializeReasonPrebuildClaimed ReinitializationReason = "prebuild_claimed" ) -type ReinitializationResponse struct { +type ReinitializationEvent struct { Message string `json:"message"` Reason ReinitializationReason `json:"reason"` } @@ -707,7 +707,7 @@ func PrebuildClaimedChannel(id uuid.UUID) string { // - ping: ignored, keepalive // - prebuild claimed: a prebuilt workspace is claimed, so the agent must reinitialize. // NOTE: the caller is responsible for closing the events chan. -func (c *Client) WaitForReinit(ctx context.Context, events chan<- ReinitializationResponse) error { +func (c *Client) WaitForReinit(ctx context.Context, events chan<- ReinitializationEvent) error { // TODO: allow configuring httpclient c.SDK.HTTPClient.Timeout = time.Hour * 24 @@ -737,19 +737,19 @@ func (c *Client) WaitForReinit(ctx context.Context, events chan<- Reinitializati if sse.Type != codersdk.ServerSentEventTypeData { continue } - var reinitResp ReinitializationResponse + var reinitEvent ReinitializationEvent b, ok := sse.Data.([]byte) if !ok { return xerrors.Errorf("expected data as []byte, got %T", sse.Data) } - err = json.Unmarshal(b, &reinitResp) + err = json.Unmarshal(b, &reinitEvent) if err != nil { return xerrors.Errorf("unmarshal reinit response: %w", err) } select { case <-ctx.Done(): return ctx.Err() - case events <- reinitResp: + case events <- reinitEvent: } } } diff --git a/provisioner/terraform/provision.go b/provisioner/terraform/provision.go index f3bc3c18129ce..518b42e20ad48 100644 --- a/provisioner/terraform/provision.go +++ b/provisioner/terraform/provision.go @@ -277,6 +277,7 @@ func provisionEnv( if len(tokens) == 1 { env = append(env, provider.RunningAgentTokenEnvironmentVariable("")+"="+tokens[0].Token) } else { + // Not currently supported, but added for forward-compatibility for _, t := range tokens { // If there are multiple agents, provide all the tokens to terraform so that it can // choose the correct one for each agent ID. From cebd5db97cba54976d84b8232c97fe2fe884668c Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Tue, 29 Apr 2025 11:27:46 +0000 Subject: [PATCH 10/42] add an integration test for prebuilt workspace agent reinitialization --- cli/agent_test.go | 14 +- coderd/database/dbfake/dbfake.go | 28 + coderd/database/dbgen/dbgen.go | 1 + coderd/database/queries.sql.go | 21 +- coderd/database/queries/presets.sql | 5 +- docs/manifest.json | 3362 +++++++++++---------- enterprise/coderd/workspaceagents_test.go | 76 + 7 files changed, 1841 insertions(+), 1666 deletions(-) diff --git a/cli/agent_test.go b/cli/agent_test.go index a0b3a491dd51e..c126df477de22 100644 --- a/cli/agent_test.go +++ b/cli/agent_test.go @@ -22,6 +22,7 @@ import ( "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbfake" "github.com/coder/coder/v2/coderd/database/dbtestutil" + "github.com/coder/coder/v2/coderd/prebuilds" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/agentsdk" "github.com/coder/coder/v2/codersdk/workspacesdk" @@ -332,14 +333,21 @@ func TestAgent_Prebuild(t *testing.T) { Pubsub: pubsub, }) user := coderdtest.CreateFirstUser(t, client) - r := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{ + presetID := uuid.New() + tv := dbfake.TemplateVersion(t, db).Seed(database.TemplateVersion{ OrganizationID: user.OrganizationID, - OwnerID: user.UserID, + CreatedBy: user.UserID, + }).Preset(database.TemplateVersionPreset{ + ID: presetID, + }).Do() + r := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{ + OwnerID: prebuilds.SystemUserID, + TemplateID: tv.Template.ID, }).WithAgent(func(a []*proto.Agent) []*proto.Agent { a[0].Scripts = []*proto.Script{ { DisplayName: "Prebuild Test Script", - Script: "sleep 5", // Make reinitiazation take long enough to assert that it happened + Script: "sleep 5", // Make reinitialization take long enough to assert that it happened RunOnStart: true, }, } diff --git a/coderd/database/dbfake/dbfake.go b/coderd/database/dbfake/dbfake.go index abadd78f07b36..fb2ea4bfd56b1 100644 --- a/coderd/database/dbfake/dbfake.go +++ b/coderd/database/dbfake/dbfake.go @@ -294,6 +294,8 @@ type TemplateVersionBuilder struct { ps pubsub.Pubsub resources []*sdkproto.Resource params []database.TemplateVersionParameter + presets []database.TemplateVersionPreset + presetParams []database.TemplateVersionPresetParameter promote bool autoCreateTemplate bool } @@ -339,6 +341,13 @@ func (t TemplateVersionBuilder) Params(ps ...database.TemplateVersionParameter) return t } +func (t TemplateVersionBuilder) Preset(preset database.TemplateVersionPreset, params ...database.TemplateVersionPresetParameter) TemplateVersionBuilder { + // nolint: revive // returns modified struct + t.presets = append(t.presets, preset) + t.presetParams = append(t.presetParams, params...) + return t +} + func (t TemplateVersionBuilder) SkipCreateTemplate() TemplateVersionBuilder { // nolint: revive // returns modified struct t.autoCreateTemplate = false @@ -378,6 +387,25 @@ func (t TemplateVersionBuilder) Do() TemplateVersionResponse { require.NoError(t.t, err) } + for _, preset := range t.presets { + dbgen.Preset(t.t, t.db, database.InsertPresetParams{ + ID: preset.ID, + TemplateVersionID: version.ID, + Name: preset.Name, + CreatedAt: version.CreatedAt, + DesiredInstances: preset.DesiredInstances, + InvalidateAfterSecs: preset.InvalidateAfterSecs, + }) + } + + for _, presetParam := range t.presetParams { + dbgen.PresetParameter(t.t, t.db, database.InsertPresetParametersParams{ + TemplateVersionPresetID: presetParam.TemplateVersionPresetID, + Names: []string{presetParam.Name}, + Values: []string{presetParam.Value}, + }) + } + payload, err := json.Marshal(provisionerdserver.TemplateVersionImportJob{ TemplateVersionID: t.seed.ID, }) diff --git a/coderd/database/dbgen/dbgen.go b/coderd/database/dbgen/dbgen.go index 854c7c2974fe6..193c107d51da9 100644 --- a/coderd/database/dbgen/dbgen.go +++ b/coderd/database/dbgen/dbgen.go @@ -1198,6 +1198,7 @@ func TelemetryItem(t testing.TB, db database.Store, seed database.TelemetryItem) func Preset(t testing.TB, db database.Store, seed database.InsertPresetParams) database.TemplateVersionPreset { preset, err := db.InsertPreset(genCtx, database.InsertPresetParams{ + ID: takeFirst(seed.ID, uuid.New()), TemplateVersionID: takeFirst(seed.TemplateVersionID, uuid.New()), Name: takeFirst(seed.Name, testutil.GetRandomName(t)), CreatedAt: takeFirst(seed.CreatedAt, dbtime.Now()), diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 60416b1a35730..b5ef53b15b3ff 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -6443,6 +6443,7 @@ func (q *sqlQuerier) GetPresetsByTemplateVersionID(ctx context.Context, template const insertPreset = `-- name: InsertPreset :one INSERT INTO template_version_presets ( + id, template_version_id, name, created_at, @@ -6454,11 +6455,13 @@ VALUES ( $2, $3, $4, - $5 + $5, + $6 ) RETURNING id, template_version_id, name, created_at, desired_instances, invalidate_after_secs ` type InsertPresetParams struct { + ID uuid.UUID `db:"id" json:"id"` TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"` Name string `db:"name" json:"name"` CreatedAt time.Time `db:"created_at" json:"created_at"` @@ -6468,6 +6471,7 @@ type InsertPresetParams struct { func (q *sqlQuerier) InsertPreset(ctx context.Context, arg InsertPresetParams) (TemplateVersionPreset, error) { row := q.db.QueryRowContext(ctx, insertPreset, + arg.ID, arg.TemplateVersionID, arg.Name, arg.CreatedAt, @@ -6488,22 +6492,29 @@ func (q *sqlQuerier) InsertPreset(ctx context.Context, arg InsertPresetParams) ( const insertPresetParameters = `-- name: InsertPresetParameters :many INSERT INTO - template_version_preset_parameters (template_version_preset_id, name, value) + template_version_preset_parameters (id, template_version_preset_id, name, value) SELECT $1, - unnest($2 :: TEXT[]), - unnest($3 :: TEXT[]) + $2, + unnest($3 :: TEXT[]), + unnest($4 :: TEXT[]) RETURNING id, template_version_preset_id, name, value ` type InsertPresetParametersParams struct { + ID uuid.UUID `db:"id" json:"id"` TemplateVersionPresetID uuid.UUID `db:"template_version_preset_id" json:"template_version_preset_id"` Names []string `db:"names" json:"names"` Values []string `db:"values" json:"values"` } func (q *sqlQuerier) InsertPresetParameters(ctx context.Context, arg InsertPresetParametersParams) ([]TemplateVersionPresetParameter, error) { - rows, err := q.db.QueryContext(ctx, insertPresetParameters, arg.TemplateVersionPresetID, pq.Array(arg.Names), pq.Array(arg.Values)) + rows, err := q.db.QueryContext(ctx, insertPresetParameters, + arg.ID, + arg.TemplateVersionPresetID, + pq.Array(arg.Names), + pq.Array(arg.Values), + ) if err != nil { return nil, err } diff --git a/coderd/database/queries/presets.sql b/coderd/database/queries/presets.sql index 15bcea0c28fb5..5f0aa28db8fe1 100644 --- a/coderd/database/queries/presets.sql +++ b/coderd/database/queries/presets.sql @@ -1,5 +1,6 @@ -- name: InsertPreset :one INSERT INTO template_version_presets ( + id, template_version_id, name, created_at, @@ -7,6 +8,7 @@ INSERT INTO template_version_presets ( invalidate_after_secs ) VALUES ( + @id, @template_version_id, @name, @created_at, @@ -16,8 +18,9 @@ VALUES ( -- name: InsertPresetParameters :many INSERT INTO - template_version_preset_parameters (template_version_preset_id, name, value) + template_version_preset_parameters (id, template_version_preset_id, name, value) SELECT + @id, @template_version_preset_id, unnest(@names :: TEXT[]), unnest(@values :: TEXT[]) diff --git a/docs/manifest.json b/docs/manifest.json index ea1d19561593f..ab51694422891 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -1,1658 +1,1706 @@ { - "versions": ["main"], - "routes": [ - { - "title": "About", - "description": "Coder docs", - "path": "./README.md", - "icon_path": "./images/icons/home.svg", - "children": [ - { - "title": "Quickstart", - "description": "Learn how to install and run Coder quickly", - "path": "./tutorials/quickstart.md" - }, - { - "title": "Screenshots", - "description": "View screenshots of the Coder platform", - "path": "./start/screenshots.md" - } - ] - }, - { - "title": "Install", - "description": "Installing Coder", - "path": "./install/index.md", - "icon_path": "./images/icons/download.svg", - "children": [ - { - "title": "Coder CLI", - "description": "Install the standalone binary", - "path": "./install/cli.md", - "icon_path": "./images/icons/terminal.svg" - }, - { - "title": "Docker", - "description": "Install Coder using Docker", - "path": "./install/docker.md", - "icon_path": "./images/icons/docker.svg" - }, - { - "title": "Kubernetes", - "description": "Install Coder on Kubernetes", - "path": "./install/kubernetes.md", - "icon_path": "./images/icons/kubernetes.svg" - }, - { - "title": "Rancher", - "description": "Deploy Coder on Rancher", - "path": "./install/rancher.md", - "icon_path": "./images/icons/rancher.svg" - }, - { - "title": "OpenShift", - "description": "Install Coder on OpenShift", - "path": "./install/openshift.md", - "icon_path": "./images/icons/openshift.svg" - }, - { - "title": "Cloud Providers", - "description": "Install Coder on cloud providers", - "path": "./install/cloud/index.md", - "icon_path": "./images/icons/cloud.svg", - "children": [ - { - "title": "AWS EC2", - "description": "Install Coder on AWS EC2", - "path": "./install/cloud/ec2.md" - }, - { - "title": "GCP Compute Engine", - "description": "Install Coder on GCP Compute Engine", - "path": "./install/cloud/compute-engine.md" - }, - { - "title": "Azure VM", - "description": "Install Coder on an Azure VM", - "path": "./install/cloud/azure-vm.md" - } - ] - }, - { - "title": "Offline Deployments", - "description": "Run Coder in offline / air-gapped environments", - "path": "./install/offline.md", - "icon_path": "./images/icons/lan.svg" - }, - { - "title": "Unofficial Install Methods", - "description": "Other installation methods", - "path": "./install/other/index.md", - "icon_path": "./images/icons/generic.svg" - }, - { - "title": "Upgrading", - "description": "Learn how to upgrade Coder", - "path": "./install/upgrade.md", - "icon_path": "./images/icons/upgrade.svg" - }, - { - "title": "Uninstall", - "description": "Learn how to uninstall Coder", - "path": "./install/uninstall.md", - "icon_path": "./images/icons/trash.svg" - }, - { - "title": "Releases", - "description": "Learn about the Coder release channels and schedule", - "path": "./install/releases/index.md", - "icon_path": "./images/icons/star.svg", - "children": [ - { - "title": "Feature stages", - "description": "Information about pre-GA stages.", - "path": "./install/releases/feature-stages.md" - } - ] - } - ] - }, - { - "title": "User Guides", - "description": "Guides for end-users of Coder", - "path": "./user-guides/index.md", - "icon_path": "./images/icons/users.svg", - "children": [ - { - "title": "Access Workspaces", - "description": "Connect to your Coder workspaces", - "path": "./user-guides/workspace-access/index.md", - "icon_path": "./images/icons/access.svg", - "children": [ - { - "title": "Visual Studio Code", - "description": "Use VSCode with Coder in the desktop or browser", - "path": "./user-guides/workspace-access/vscode.md" - }, - { - "title": "JetBrains IDEs", - "description": "Use JetBrains IDEs with Gateway", - "path": "./user-guides/workspace-access/jetbrains/index.md", - "children": [ - { - "title": "JetBrains Gateway in an air-gapped environment", - "description": "Use JetBrains Gateway in an air-gapped offline environment", - "path": "./user-guides/workspace-access/jetbrains/jetbrains-airgapped.md" - } - ] - }, - { - "title": "Remote Desktop", - "description": "Use RDP in Coder", - "path": "./user-guides/workspace-access/remote-desktops.md" - }, - { - "title": "Emacs TRAMP", - "description": "Use Emacs TRAMP in Coder", - "path": "./user-guides/workspace-access/emacs-tramp.md" - }, - { - "title": "Port Forwarding", - "description": "Access ports on your workspace", - "path": "./user-guides/workspace-access/port-forwarding.md" - }, - { - "title": "Filebrowser", - "description": "Access your workspace files", - "path": "./user-guides/workspace-access/filebrowser.md" - }, - { - "title": "Web IDEs and Coder Apps", - "description": "Access your workspace with IDEs in the browser", - "path": "./user-guides/workspace-access/web-ides.md" - }, - { - "title": "Zed", - "description": "Access your workspace with Zed", - "path": "./user-guides/workspace-access/zed.md" - }, - { - "title": "Cursor", - "description": "Access your workspace with Cursor", - "path": "./user-guides/workspace-access/cursor.md" - }, - { - "title": "Windsurf", - "description": "Access your workspace with Windsurf", - "path": "./user-guides/workspace-access/windsurf.md" - } - ] - }, - { - "title": "Coder Desktop", - "description": "Use Coder Desktop to access your workspace like it's a local machine", - "path": "./user-guides/desktop/index.md", - "icon_path": "./images/icons/computer-code.svg", - "state": ["early access"] - }, - { - "title": "Workspace Management", - "description": "Manage workspaces", - "path": "./user-guides/workspace-management.md", - "icon_path": "./images/icons/generic.svg" - }, - { - "title": "Workspace Scheduling", - "description": "Cost control with workspace schedules", - "path": "./user-guides/workspace-scheduling.md", - "icon_path": "./images/icons/stopwatch.svg" - }, - { - "title": "Workspace Lifecycle", - "description": "A guide to the workspace lifecycle, from creation and status through stopping and deletion.", - "path": "./user-guides/workspace-lifecycle.md", - "icon_path": "./images/icons/circle-dot.svg" - }, - { - "title": "Dotfiles", - "description": "Personalize your environment with dotfiles", - "path": "./user-guides/workspace-dotfiles.md", - "icon_path": "./images/icons/art-pad.svg" - } - ] - }, - { - "title": "Administration", - "description": "Guides for template and deployment administrators", - "path": "./admin/index.md", - "icon_path": "./images/icons/wrench.svg", - "children": [ - { - "title": "Setup", - "description": "Configure user access to your control plane.", - "path": "./admin/setup/index.md", - "icon_path": "./images/icons/toggle_on.svg", - "children": [ - { - "title": "Appearance", - "description": "Learn how to configure the appearance of Coder", - "path": "./admin/setup/appearance.md", - "state": ["premium"] - }, - { - "title": "Telemetry", - "description": "Learn what usage telemetry Coder collects", - "path": "./admin/setup/telemetry.md" - } - ] - }, - { - "title": "Infrastructure", - "description": "How to integrate Coder with your organization's compute", - "path": "./admin/infrastructure/index.md", - "icon_path": "./images/icons/container.svg", - "children": [ - { - "title": "Architecture", - "description": "Learn about Coder's architecture", - "path": "./admin/infrastructure/architecture.md" - }, - { - "title": "Validated Architectures", - "description": "Architectures for large Coder deployments", - "path": "./admin/infrastructure/validated-architectures/index.md", - "children": [ - { - "title": "Up to 1,000 Users", - "path": "./admin/infrastructure/validated-architectures/1k-users.md" - }, - { - "title": "Up to 2,000 Users", - "path": "./admin/infrastructure/validated-architectures/2k-users.md" - }, - { - "title": "Up to 3,000 Users", - "path": "./admin/infrastructure/validated-architectures/3k-users.md" - } - ] - }, - { - "title": "Scale Testing", - "description": "Ensure your deployment can handle your organization's needs", - "path": "./admin/infrastructure/scale-testing.md" - }, - { - "title": "Scaling Utilities", - "description": "Tools to help you scale your deployment", - "path": "./admin/infrastructure/scale-utility.md" - }, - { - "title": "Scaling best practices", - "description": "How to prepare a Coder deployment for scale", - "path": "./tutorials/best-practices/scale-coder.md" - } - ] - }, - { - "title": "Users", - "description": "Learn how to manage and audit users", - "path": "./admin/users/index.md", - "icon_path": "./images/icons/users.svg", - "children": [ - { - "title": "OIDC Authentication", - "path": "./admin/users/oidc-auth.md" - }, - { - "title": "GitHub Authentication", - "path": "./admin/users/github-auth.md" - }, - { - "title": "Password Authentication", - "path": "./admin/users/password-auth.md" - }, - { - "title": "Headless Authentication", - "path": "./admin/users/headless-auth.md" - }, - { - "title": "Groups \u0026 Roles", - "path": "./admin/users/groups-roles.md", - "state": ["premium"] - }, - { - "title": "IdP Sync", - "path": "./admin/users/idp-sync.md", - "state": ["premium"] - }, - { - "title": "Organizations", - "path": "./admin/users/organizations.md", - "state": ["premium"] - }, - { - "title": "Quotas", - "path": "./admin/users/quotas.md", - "state": ["premium"] - }, - { - "title": "Sessions \u0026 API Tokens", - "path": "./admin/users/sessions-tokens.md" - } - ] - }, - { - "title": "Templates", - "description": "Learn how to author and maintain Coder templates", - "path": "./admin/templates/index.md", - "icon_path": "./images/icons/picture.svg", - "children": [ - { - "title": "Creating Templates", - "description": "Learn how to create templates with Terraform", - "path": "./admin/templates/creating-templates.md" - }, - { - "title": "Managing Templates", - "description": "Learn how to manage templates and best practices", - "path": "./admin/templates/managing-templates/index.md", - "children": [ - { - "title": "Image Management", - "description": "Learn about template image management", - "path": "./admin/templates/managing-templates/image-management.md" - }, - { - "title": "Change Management", - "description": "Learn about template change management and versioning", - "path": "./admin/templates/managing-templates/change-management.md" - }, - { - "title": "Dev containers", - "description": "Learn about using development containers in templates", - "path": "./admin/templates/managing-templates/devcontainers/index.md", - "children": [ - { - "title": "Add a dev container template", - "description": "How to add a dev container template to Coder", - "path": "./admin/templates/managing-templates/devcontainers/add-devcontainer.md" - }, - { - "title": "Dev container security and caching", - "description": "Configure dev container authentication and caching", - "path": "./admin/templates/managing-templates/devcontainers/devcontainer-security-caching.md" - }, - { - "title": "Dev container releases and known issues", - "description": "Dev container releases and known issues", - "path": "./admin/templates/managing-templates/devcontainers/devcontainer-releases-known-issues.md" - } - ] - }, - { - "title": "Template Dependencies", - "description": "Learn how to manage template dependencies", - "path": "./admin/templates/managing-templates/dependencies.md" - }, - { - "title": "Workspace Scheduling", - "description": "Learn how to control how workspaces are started and stopped", - "path": "./admin/templates/managing-templates/schedule.md" - } - ] - }, - { - "title": "Extending Templates", - "description": "Learn best practices in extending templates", - "path": "./admin/templates/extending-templates/index.md", - "children": [ - { - "title": "Agent Metadata", - "description": "Retrieve real-time stats from the workspace agent", - "path": "./admin/templates/extending-templates/agent-metadata.md" - }, - { - "title": "Build Parameters", - "description": "Use parameters to customize workspaces at build", - "path": "./admin/templates/extending-templates/parameters.md" - }, - { - "title": "Icons", - "description": "Customize your template with built-in icons", - "path": "./admin/templates/extending-templates/icons.md" - }, - { - "title": "Resource Metadata", - "description": "Display resource state in the workspace dashboard", - "path": "./admin/templates/extending-templates/resource-metadata.md" - }, - { - "title": "Resource Monitoring", - "description": "Monitor resources in the workspace dashboard", - "path": "./admin/templates/extending-templates/resource-monitoring.md" - }, - { - "title": "Resource Ordering", - "description": "Design the UI of workspaces", - "path": "./admin/templates/extending-templates/resource-ordering.md" - }, - { - "title": "Resource Persistence", - "description": "Control resource persistence", - "path": "./admin/templates/extending-templates/resource-persistence.md" - }, - { - "title": "Terraform Variables", - "description": "Use variables to manage template state", - "path": "./admin/templates/extending-templates/variables.md" - }, - { - "title": "Terraform Modules", - "description": "Reuse terraform code across templates", - "path": "./admin/templates/extending-templates/modules.md" - }, - { - "title": "Web IDEs and Coder Apps", - "description": "Add and configure Web IDEs in your templates as coder apps", - "path": "./admin/templates/extending-templates/web-ides.md" - }, - { - "title": "Pre-install JetBrains Gateway", - "description": "Pre-install JetBrains Gateway in a template for faster IDE startup", - "path": "./admin/templates/extending-templates/jetbrains-gateway.md" - }, - { - "title": "Docker in Workspaces", - "description": "Use Docker in your workspaces", - "path": "./admin/templates/extending-templates/docker-in-workspaces.md" - }, - { - "title": "Workspace Tags", - "description": "Control provisioning using Workspace Tags and Parameters", - "path": "./admin/templates/extending-templates/workspace-tags.md" - }, - { - "title": "Provider Authentication", - "description": "Authenticate with provider APIs to provision workspaces", - "path": "./admin/templates/extending-templates/provider-authentication.md" - }, - { - "title": "Process Logging", - "description": "Log workspace processes", - "path": "./admin/templates/extending-templates/process-logging.md", - "state": ["premium"] - } - ] - }, - { - "title": "Open in Coder", - "description": "Open workspaces in Coder", - "path": "./admin/templates/open-in-coder.md" - }, - { - "title": "Permissions \u0026 Policies", - "description": "Learn how to create templates with Terraform", - "path": "./admin/templates/template-permissions.md", - "state": ["premium"] - }, - { - "title": "Troubleshooting Templates", - "description": "Learn how to troubleshoot template issues", - "path": "./admin/templates/troubleshooting.md" - } - ] - }, - { - "title": "External Provisioners", - "description": "Learn how to run external provisioners with Coder", - "path": "./admin/provisioners/index.md", - "icon_path": "./images/icons/key.svg", - "state": ["premium"], - "children": [ - { - "title": "Manage Provisioner Jobs", - "description": "Learn how to run external provisioners with Coder", - "path": "./admin/provisioners/manage-provisioner-jobs.md", - "state": ["premium"] - } - ] - }, - { - "title": "External Auth", - "description": "Learn how to configure external authentication", - "path": "./admin/external-auth.md", - "icon_path": "./images/icons/plug.svg" - }, - { - "title": "Integrations", - "description": "Use integrations to extend Coder", - "path": "./admin/integrations/index.md", - "icon_path": "./images/icons/puzzle.svg", - "children": [ - { - "title": "Prometheus", - "description": "Collect deployment metrics with Prometheus", - "path": "./admin/integrations/prometheus.md" - }, - { - "title": "Kubernetes Logging", - "description": "Stream K8s event logs on workspace startup", - "path": "./admin/integrations/kubernetes-logs.md" - }, - { - "title": "Additional Kubernetes Clusters", - "description": "Deploy workspaces on additional Kubernetes clusters", - "path": "./admin/integrations/multiple-kube-clusters.md" - }, - { - "title": "JFrog Artifactory", - "description": "Integrate Coder with JFrog Artifactory", - "path": "./admin/integrations/jfrog-artifactory.md" - }, - { - "title": "JFrog Xray", - "description": "Integrate Coder with JFrog Xray", - "path": "./admin/integrations/jfrog-xray.md" - }, - { - "title": "Island Secure Browser", - "description": "Integrate Coder with Island's Secure Browser", - "path": "./admin/integrations/island.md" - }, - { - "title": "DX PlatformX", - "description": "Integrate Coder with DX PlatformX", - "path": "./admin/integrations/platformx.md" - }, - { - "title": "Hashicorp Vault", - "description": "Integrate Coder with Hashicorp Vault", - "path": "./admin/integrations/vault.md" - } - ] - }, - { - "title": "Networking", - "description": "Understand Coder's networking layer", - "path": "./admin/networking/index.md", - "icon_path": "./images/icons/networking.svg", - "children": [ - { - "title": "Port Forwarding", - "description": "Learn how to forward ports in Coder", - "path": "./admin/networking/port-forwarding.md" - }, - { - "title": "STUN and NAT", - "description": "Learn how to forward ports in Coder", - "path": "./admin/networking/stun.md" - }, - { - "title": "Workspace Proxies", - "description": "Run geo distributed workspace proxies", - "path": "./admin/networking/workspace-proxies.md", - "state": ["premium"] - }, - { - "title": "High Availability", - "description": "Learn how to configure Coder for High Availability", - "path": "./admin/networking/high-availability.md", - "state": ["premium"] - }, - { - "title": "Troubleshooting", - "description": "Troubleshoot networking issues in Coder", - "path": "./admin/networking/troubleshooting.md" - } - ] - }, - { - "title": "Monitoring", - "description": "Configure security policy and audit your deployment", - "path": "./admin/monitoring/index.md", - "icon_path": "./images/icons/speed.svg", - "children": [ - { - "title": "Logs", - "description": "Learn about Coder's logs", - "path": "./admin/monitoring/logs.md" - }, - { - "title": "Metrics", - "description": "Learn about Coder's logs", - "path": "./admin/monitoring/metrics.md" - }, - { - "title": "Health Check", - "description": "Learn about Coder's automated health checks", - "path": "./admin/monitoring/health-check.md" - }, - { - "title": "Notifications", - "description": "Configure notifications for your deployment", - "path": "./admin/monitoring/notifications/index.md", - "children": [ - { - "title": "Slack Notifications", - "description": "Learn how to setup Slack notifications", - "path": "./admin/monitoring/notifications/slack.md" - }, - { - "title": "Microsoft Teams Notifications", - "description": "Learn how to setup Microsoft Teams notifications", - "path": "./admin/monitoring/notifications/teams.md" - } - ] - } - ] - }, - { - "title": "Security", - "description": "Configure security policy and audit your deployment", - "path": "./admin/security/index.md", - "icon_path": "./images/icons/lock.svg", - "children": [ - { - "title": "Audit Logs", - "description": "Audit actions taken inside Coder", - "path": "./admin/security/audit-logs.md", - "state": ["premium"] - }, - { - "title": "Secrets", - "description": "Use sensitive variables in your workspaces", - "path": "./admin/security/secrets.md" - }, - { - "title": "Database Encryption", - "description": "Encrypt the database to prevent unauthorized access", - "path": "./admin/security/database-encryption.md", - "state": ["premium"] - } - ] - }, - { - "title": "Licensing", - "description": "Configure licensing for your deployment", - "path": "./admin/licensing/index.md", - "icon_path": "./images/icons/licensing.svg" - } - ] - }, - { - "title": "Run AI Coding Agents in Coder", - "description": "Learn how to run and integrate AI coding agents like GPT-Code, OpenDevin, or SWE-Agent in Coder workspaces to boost developer productivity.", - "path": "./ai-coder/index.md", - "icon_path": "./images/icons/wand.svg", - "state": ["early access"], - "children": [ - { - "title": "Learn about coding agents", - "description": "Learn about the different AI agents and their tradeoffs", - "path": "./ai-coder/agents.md" - }, - { - "title": "Create a Coder template for agents", - "description": "Create a purpose-built template for your AI agents", - "path": "./ai-coder/create-template.md", - "state": ["early access"] - }, - { - "title": "Integrate with your issue tracker", - "description": "Assign tickets to AI agents and interact via code reviews", - "path": "./ai-coder/issue-tracker.md", - "state": ["early access"] - }, - { - "title": "Model Context Protocols (MCP) and adding AI tools", - "description": "Improve results by adding tools to your AI agents", - "path": "./ai-coder/best-practices.md", - "state": ["early access"] - }, - { - "title": "Supervise agents via Coder UI", - "description": "Interact with agents via the Coder UI", - "path": "./ai-coder/coder-dashboard.md", - "state": ["early access"] - }, - { - "title": "Supervise agents via the IDE", - "description": "Interact with agents via VS Code or Cursor", - "path": "./ai-coder/ide-integration.md", - "state": ["early access"] - }, - { - "title": "Programmatically manage agents", - "description": "Manage agents via MCP, the Coder CLI, and/or REST API", - "path": "./ai-coder/headless.md", - "state": ["early access"] - }, - { - "title": "Securing agents in Coder", - "description": "Learn how to secure agents with boundaries", - "path": "./ai-coder/securing.md", - "state": ["early access"] - }, - { - "title": "Custom agents", - "description": "Learn how to use custom agents with Coder", - "path": "./ai-coder/custom-agents.md", - "state": ["early access"] - } - ] - }, - { - "title": "Contributing", - "description": "Learn how to contribute to Coder", - "path": "./CONTRIBUTING.md", - "icon_path": "./images/icons/contributing.svg", - "children": [ - { - "title": "Code of Conduct", - "description": "See the code of conduct for contributing to Coder", - "path": "./contributing/CODE_OF_CONDUCT.md", - "icon_path": "./images/icons/circle-dot.svg" - }, - { - "title": "Documentation", - "description": "Our style guide for use when authoring documentation", - "path": "./contributing/documentation.md", - "icon_path": "./images/icons/document.svg" - }, - { - "title": "Frontend", - "description": "Our guide for frontend development", - "path": "./contributing/frontend.md", - "icon_path": "./images/icons/frontend.svg" - }, - { - "title": "Security", - "description": "Our guide for security", - "path": "./contributing/SECURITY.md", - "icon_path": "./images/icons/lock.svg" - } - ] - }, - { - "title": "Tutorials", - "description": "Coder knowledgebase for administrating your deployment", - "path": "./tutorials/index.md", - "icon_path": "./images/icons/generic.svg", - "children": [ - { - "title": "Quickstart", - "description": "Learn how to install and run Coder quickly", - "path": "./tutorials/quickstart.md" - }, - { - "title": "Write a Template from Scratch", - "description": "Learn how to author Coder templates", - "path": "./tutorials/template-from-scratch.md" - }, - { - "title": "Using an External Database", - "description": "Use Coder with an external database", - "path": "./tutorials/external-database.md" - }, - { - "title": "Image Management", - "description": "Learn about image management with Coder", - "path": "./admin/templates/managing-templates/image-management.md" - }, - { - "title": "Generate a Support Bundle", - "description": "Generate and upload a Support Bundle to Coder Support", - "path": "./tutorials/support-bundle.md" - }, - { - "title": "Configuring Okta", - "description": "Custom claims/scopes with Okta for group/role sync", - "path": "./tutorials/configuring-okta.md" - }, - { - "title": "Google to AWS Federation", - "description": "Federating a Google Cloud service account to AWS", - "path": "./tutorials/gcp-to-aws.md" - }, - { - "title": "JFrog Artifactory Integration", - "description": "Integrate Coder with JFrog Artifactory", - "path": "./admin/integrations/jfrog-artifactory.md" - }, - { - "title": "Istio Integration", - "description": "Integrate Coder with Istio", - "path": "./admin/integrations/istio.md" - }, - { - "title": "Island Secure Browser Integration", - "description": "Integrate Coder with Island's Secure Browser", - "path": "./admin/integrations/island.md" - }, - { - "title": "Template ImagePullSecrets", - "description": "Creating ImagePullSecrets for private registries", - "path": "./tutorials/image-pull-secret.md" - }, - { - "title": "Postgres SSL", - "description": "Configure Coder to connect to Postgres over SSL", - "path": "./tutorials/postgres-ssl.md" - }, - { - "title": "Azure Federation", - "description": "Federating Coder to Azure", - "path": "./tutorials/azure-federation.md" - }, - { - "title": "Scanning Workspaces with JFrog Xray", - "description": "Integrate Coder with JFrog Xray", - "path": "./admin/integrations/jfrog-xray.md" - }, - { - "title": "Cloning Git Repositories", - "description": "Learn how to clone Git repositories in Coder", - "path": "./tutorials/cloning-git-repositories.md" - }, - { - "title": "Test Templates Through CI/CD", - "description": "Learn how to test and publish Coder templates in a CI/CD pipeline", - "path": "./tutorials/testing-templates.md" - }, - { - "title": "Use Apache as a Reverse Proxy", - "description": "Learn how to use Apache as a reverse proxy", - "path": "./tutorials/reverse-proxy-apache.md" - }, - { - "title": "Use Caddy as a Reverse Proxy", - "description": "Learn how to use Caddy as a reverse proxy", - "path": "./tutorials/reverse-proxy-caddy.md" - }, - { - "title": "Use NGINX as a Reverse Proxy", - "description": "Learn how to use NGINX as a reverse proxy", - "path": "./tutorials/reverse-proxy-nginx.md" - }, - { - "title": "FAQs", - "description": "Miscellaneous FAQs from our community", - "path": "./tutorials/faqs.md" - }, - { - "title": "Best practices", - "description": "Guides to help you make the most of your Coder experience", - "path": "./tutorials/best-practices/index.md", - "children": [ - { - "title": "Organizations - best practices", - "description": "How to make the best use of Coder Organizations", - "path": "./tutorials/best-practices/organizations.md" - }, - { - "title": "Scale Coder", - "description": "How to prepare a Coder deployment for scale", - "path": "./tutorials/best-practices/scale-coder.md" - }, - { - "title": "Security - best practices", - "description": "Make your Coder deployment more secure", - "path": "./tutorials/best-practices/security-best-practices.md" - }, - { - "title": "Speed up your workspaces", - "description": "Speed up your Coder templates and workspaces", - "path": "./tutorials/best-practices/speed-up-templates.md" - } - ] - } - ] - }, - { - "title": "Reference", - "description": "Reference", - "path": "./reference/index.md", - "icon_path": "./images/icons/notes.svg", - "children": [ - { - "title": "REST API", - "description": "Learn how to use Coderd API", - "path": "./reference/api/index.md", - "icon_path": "./images/icons/api.svg", - "children": [ - { - "title": "General", - "path": "./reference/api/general.md" - }, - { - "title": "Agents", - "path": "./reference/api/agents.md" - }, - { - "title": "Applications", - "path": "./reference/api/applications.md" - }, - { - "title": "Audit", - "path": "./reference/api/audit.md" - }, - { - "title": "Authentication", - "path": "./reference/api/authentication.md" - }, - { - "title": "Authorization", - "path": "./reference/api/authorization.md" - }, - { - "title": "Builds", - "path": "./reference/api/builds.md" - }, - { - "title": "Debug", - "path": "./reference/api/debug.md" - }, - { - "title": "Enterprise", - "path": "./reference/api/enterprise.md" - }, - { - "title": "Files", - "path": "./reference/api/files.md" - }, - { - "title": "Git", - "path": "./reference/api/git.md" - }, - { - "title": "Insights", - "path": "./reference/api/insights.md" - }, - { - "title": "Members", - "path": "./reference/api/members.md" - }, - { - "title": "Organizations", - "path": "./reference/api/organizations.md" - }, - { - "title": "PortSharing", - "path": "./reference/api/portsharing.md" - }, - { - "title": "Schemas", - "path": "./reference/api/schemas.md" - }, - { - "title": "Templates", - "path": "./reference/api/templates.md" - }, - { - "title": "Users", - "path": "./reference/api/users.md" - }, - { - "title": "WorkspaceProxies", - "path": "./reference/api/workspaceproxies.md" - }, - { - "title": "Workspaces", - "path": "./reference/api/workspaces.md" - } - ] - }, - { - "title": "Command Line", - "description": "Learn how to use Coder CLI", - "path": "./reference/cli/index.md", - "icon_path": "./images/icons/terminal.svg", - "children": [ - { - "title": "autoupdate", - "description": "Toggle auto-update policy for a workspace", - "path": "reference/cli/autoupdate.md" - }, - { - "title": "coder", - "path": "reference/cli/index.md" - }, - { - "title": "completion", - "description": "Install or update shell completion scripts for the detected or chosen shell.", - "path": "reference/cli/completion.md" - }, - { - "title": "config-ssh", - "description": "Add an SSH Host entry for your workspaces \"ssh coder.workspace\"", - "path": "reference/cli/config-ssh.md" - }, - { - "title": "create", - "description": "Create a workspace", - "path": "reference/cli/create.md" - }, - { - "title": "delete", - "description": "Delete a workspace", - "path": "reference/cli/delete.md" - }, - { - "title": "dotfiles", - "description": "Personalize your workspace by applying a canonical dotfiles repository", - "path": "reference/cli/dotfiles.md" - }, - { - "title": "external-auth", - "description": "Manage external authentication", - "path": "reference/cli/external-auth.md" - }, - { - "title": "external-auth access-token", - "description": "Print auth for an external provider", - "path": "reference/cli/external-auth_access-token.md" - }, - { - "title": "favorite", - "description": "Add a workspace to your favorites", - "path": "reference/cli/favorite.md" - }, - { - "title": "features", - "description": "List Enterprise features", - "path": "reference/cli/features.md" - }, - { - "title": "features list", - "path": "reference/cli/features_list.md" - }, - { - "title": "groups", - "description": "Manage groups", - "path": "reference/cli/groups.md" - }, - { - "title": "groups create", - "description": "Create a user group", - "path": "reference/cli/groups_create.md" - }, - { - "title": "groups delete", - "description": "Delete a user group", - "path": "reference/cli/groups_delete.md" - }, - { - "title": "groups edit", - "description": "Edit a user group", - "path": "reference/cli/groups_edit.md" - }, - { - "title": "groups list", - "description": "List user groups", - "path": "reference/cli/groups_list.md" - }, - { - "title": "licenses", - "description": "Add, delete, and list licenses", - "path": "reference/cli/licenses.md" - }, - { - "title": "licenses add", - "description": "Add license to Coder deployment", - "path": "reference/cli/licenses_add.md" - }, - { - "title": "licenses delete", - "description": "Delete license by ID", - "path": "reference/cli/licenses_delete.md" - }, - { - "title": "licenses list", - "description": "List licenses (including expired)", - "path": "reference/cli/licenses_list.md" - }, - { - "title": "list", - "description": "List workspaces", - "path": "reference/cli/list.md" - }, - { - "title": "login", - "description": "Authenticate with Coder deployment", - "path": "reference/cli/login.md" - }, - { - "title": "logout", - "description": "Unauthenticate your local session", - "path": "reference/cli/logout.md" - }, - { - "title": "netcheck", - "description": "Print network debug information for DERP and STUN", - "path": "reference/cli/netcheck.md" - }, - { - "title": "notifications", - "description": "Manage Coder notifications", - "path": "reference/cli/notifications.md" - }, - { - "title": "notifications pause", - "description": "Pause notifications", - "path": "reference/cli/notifications_pause.md" - }, - { - "title": "notifications resume", - "description": "Resume notifications", - "path": "reference/cli/notifications_resume.md" - }, - { - "title": "notifications test", - "description": "Send a test notification", - "path": "reference/cli/notifications_test.md" - }, - { - "title": "open", - "description": "Open a workspace", - "path": "reference/cli/open.md" - }, - { - "title": "open app", - "description": "Open a workspace application.", - "path": "reference/cli/open_app.md" - }, - { - "title": "open vscode", - "description": "Open a workspace in VS Code Desktop", - "path": "reference/cli/open_vscode.md" - }, - { - "title": "organizations", - "description": "Organization related commands", - "path": "reference/cli/organizations.md" - }, - { - "title": "organizations create", - "description": "Create a new organization.", - "path": "reference/cli/organizations_create.md" - }, - { - "title": "organizations members", - "description": "Manage organization members", - "path": "reference/cli/organizations_members.md" - }, - { - "title": "organizations members add", - "description": "Add a new member to the current organization", - "path": "reference/cli/organizations_members_add.md" - }, - { - "title": "organizations members edit-roles", - "description": "Edit organization member's roles", - "path": "reference/cli/organizations_members_edit-roles.md" - }, - { - "title": "organizations members list", - "description": "List all organization members", - "path": "reference/cli/organizations_members_list.md" - }, - { - "title": "organizations members remove", - "description": "Remove a new member to the current organization", - "path": "reference/cli/organizations_members_remove.md" - }, - { - "title": "organizations roles", - "description": "Manage organization roles.", - "path": "reference/cli/organizations_roles.md" - }, - { - "title": "organizations roles create", - "description": "Create a new organization custom role", - "path": "reference/cli/organizations_roles_create.md" - }, - { - "title": "organizations roles show", - "description": "Show role(s)", - "path": "reference/cli/organizations_roles_show.md" - }, - { - "title": "organizations roles update", - "description": "Update an organization custom role", - "path": "reference/cli/organizations_roles_update.md" - }, - { - "title": "organizations settings", - "description": "Manage organization settings.", - "path": "reference/cli/organizations_settings.md" - }, - { - "title": "organizations settings set", - "description": "Update specified organization setting.", - "path": "reference/cli/organizations_settings_set.md" - }, - { - "title": "organizations settings set group-sync", - "description": "Group sync settings to sync groups from an IdP.", - "path": "reference/cli/organizations_settings_set_group-sync.md" - }, - { - "title": "organizations settings set organization-sync", - "description": "Organization sync settings to sync organization memberships from an IdP.", - "path": "reference/cli/organizations_settings_set_organization-sync.md" - }, - { - "title": "organizations settings set role-sync", - "description": "Role sync settings to sync organization roles from an IdP.", - "path": "reference/cli/organizations_settings_set_role-sync.md" - }, - { - "title": "organizations settings show", - "description": "Outputs specified organization setting.", - "path": "reference/cli/organizations_settings_show.md" - }, - { - "title": "organizations settings show group-sync", - "description": "Group sync settings to sync groups from an IdP.", - "path": "reference/cli/organizations_settings_show_group-sync.md" - }, - { - "title": "organizations settings show organization-sync", - "description": "Organization sync settings to sync organization memberships from an IdP.", - "path": "reference/cli/organizations_settings_show_organization-sync.md" - }, - { - "title": "organizations settings show role-sync", - "description": "Role sync settings to sync organization roles from an IdP.", - "path": "reference/cli/organizations_settings_show_role-sync.md" - }, - { - "title": "organizations show", - "description": "Show the organization. Using \"selected\" will show the selected organization from the \"--org\" flag. Using \"me\" will show all organizations you are a member of.", - "path": "reference/cli/organizations_show.md" - }, - { - "title": "ping", - "description": "Ping a workspace", - "path": "reference/cli/ping.md" - }, - { - "title": "port-forward", - "description": "Forward ports from a workspace to the local machine. For reverse port forwarding, use \"coder ssh -R\".", - "path": "reference/cli/port-forward.md" - }, - { - "title": "provisioner", - "description": "View and manage provisioner daemons and jobs", - "path": "reference/cli/provisioner.md" - }, - { - "title": "provisioner jobs", - "description": "View and manage provisioner jobs", - "path": "reference/cli/provisioner_jobs.md" - }, - { - "title": "provisioner jobs cancel", - "description": "Cancel a provisioner job", - "path": "reference/cli/provisioner_jobs_cancel.md" - }, - { - "title": "provisioner jobs list", - "description": "List provisioner jobs", - "path": "reference/cli/provisioner_jobs_list.md" - }, - { - "title": "provisioner keys", - "description": "Manage provisioner keys", - "path": "reference/cli/provisioner_keys.md" - }, - { - "title": "provisioner keys create", - "description": "Create a new provisioner key", - "path": "reference/cli/provisioner_keys_create.md" - }, - { - "title": "provisioner keys delete", - "description": "Delete a provisioner key", - "path": "reference/cli/provisioner_keys_delete.md" - }, - { - "title": "provisioner keys list", - "description": "List provisioner keys in an organization", - "path": "reference/cli/provisioner_keys_list.md" - }, - { - "title": "provisioner list", - "description": "List provisioner daemons in an organization", - "path": "reference/cli/provisioner_list.md" - }, - { - "title": "provisioner start", - "description": "Run a provisioner daemon", - "path": "reference/cli/provisioner_start.md" - }, - { - "title": "publickey", - "description": "Output your Coder public key used for Git operations", - "path": "reference/cli/publickey.md" - }, - { - "title": "rename", - "description": "Rename a workspace", - "path": "reference/cli/rename.md" - }, - { - "title": "reset-password", - "description": "Directly connect to the database to reset a user's password", - "path": "reference/cli/reset-password.md" - }, - { - "title": "restart", - "description": "Restart a workspace", - "path": "reference/cli/restart.md" - }, - { - "title": "schedule", - "description": "Schedule automated start and stop times for workspaces", - "path": "reference/cli/schedule.md" - }, - { - "title": "schedule extend", - "description": "Extend the stop time of a currently running workspace instance.", - "path": "reference/cli/schedule_extend.md" - }, - { - "title": "schedule show", - "description": "Show workspace schedules", - "path": "reference/cli/schedule_show.md" - }, - { - "title": "schedule start", - "description": "Edit workspace start schedule", - "path": "reference/cli/schedule_start.md" - }, - { - "title": "schedule stop", - "description": "Edit workspace stop schedule", - "path": "reference/cli/schedule_stop.md" - }, - { - "title": "server", - "description": "Start a Coder server", - "path": "reference/cli/server.md" - }, - { - "title": "server create-admin-user", - "description": "Create a new admin user with the given username, email and password and adds it to every organization.", - "path": "reference/cli/server_create-admin-user.md" - }, - { - "title": "server dbcrypt", - "description": "Manage database encryption.", - "path": "reference/cli/server_dbcrypt.md" - }, - { - "title": "server dbcrypt decrypt", - "description": "Decrypt a previously encrypted database.", - "path": "reference/cli/server_dbcrypt_decrypt.md" - }, - { - "title": "server dbcrypt delete", - "description": "Delete all encrypted data from the database. THIS IS A DESTRUCTIVE OPERATION.", - "path": "reference/cli/server_dbcrypt_delete.md" - }, - { - "title": "server dbcrypt rotate", - "description": "Rotate database encryption keys.", - "path": "reference/cli/server_dbcrypt_rotate.md" - }, - { - "title": "server postgres-builtin-serve", - "description": "Run the built-in PostgreSQL deployment.", - "path": "reference/cli/server_postgres-builtin-serve.md" - }, - { - "title": "server postgres-builtin-url", - "description": "Output the connection URL for the built-in PostgreSQL deployment.", - "path": "reference/cli/server_postgres-builtin-url.md" - }, - { - "title": "show", - "description": "Display details of a workspace's resources and agents", - "path": "reference/cli/show.md" - }, - { - "title": "speedtest", - "description": "Run upload and download tests from your machine to a workspace", - "path": "reference/cli/speedtest.md" - }, - { - "title": "ssh", - "description": "Start a shell into a workspace", - "path": "reference/cli/ssh.md" - }, - { - "title": "start", - "description": "Start a workspace", - "path": "reference/cli/start.md" - }, - { - "title": "stat", - "description": "Show resource usage for the current workspace.", - "path": "reference/cli/stat.md" - }, - { - "title": "stat cpu", - "description": "Show CPU usage, in cores.", - "path": "reference/cli/stat_cpu.md" - }, - { - "title": "stat disk", - "description": "Show disk usage, in gigabytes.", - "path": "reference/cli/stat_disk.md" - }, - { - "title": "stat mem", - "description": "Show memory usage, in gigabytes.", - "path": "reference/cli/stat_mem.md" - }, - { - "title": "state", - "description": "Manually manage Terraform state to fix broken workspaces", - "path": "reference/cli/state.md" - }, - { - "title": "state pull", - "description": "Pull a Terraform state file from a workspace.", - "path": "reference/cli/state_pull.md" - }, - { - "title": "state push", - "description": "Push a Terraform state file to a workspace.", - "path": "reference/cli/state_push.md" - }, - { - "title": "stop", - "description": "Stop a workspace", - "path": "reference/cli/stop.md" - }, - { - "title": "support", - "description": "Commands for troubleshooting issues with a Coder deployment.", - "path": "reference/cli/support.md" - }, - { - "title": "support bundle", - "description": "Generate a support bundle to troubleshoot issues connecting to a workspace.", - "path": "reference/cli/support_bundle.md" - }, - { - "title": "templates", - "description": "Manage templates", - "path": "reference/cli/templates.md" - }, - { - "title": "templates archive", - "description": "Archive unused or failed template versions from a given template(s)", - "path": "reference/cli/templates_archive.md" - }, - { - "title": "templates create", - "description": "DEPRECATED: Create a template from the current directory or as specified by flag", - "path": "reference/cli/templates_create.md" - }, - { - "title": "templates delete", - "description": "Delete templates", - "path": "reference/cli/templates_delete.md" - }, - { - "title": "templates edit", - "description": "Edit the metadata of a template by name.", - "path": "reference/cli/templates_edit.md" - }, - { - "title": "templates init", - "description": "Get started with a templated template.", - "path": "reference/cli/templates_init.md" - }, - { - "title": "templates list", - "description": "List all the templates available for the organization", - "path": "reference/cli/templates_list.md" - }, - { - "title": "templates pull", - "description": "Download the active, latest, or specified version of a template to a path.", - "path": "reference/cli/templates_pull.md" - }, - { - "title": "templates push", - "description": "Create or update a template from the current directory or as specified by flag", - "path": "reference/cli/templates_push.md" - }, - { - "title": "templates versions", - "description": "Manage different versions of the specified template", - "path": "reference/cli/templates_versions.md" - }, - { - "title": "templates versions archive", - "description": "Archive a template version(s).", - "path": "reference/cli/templates_versions_archive.md" - }, - { - "title": "templates versions list", - "description": "List all the versions of the specified template", - "path": "reference/cli/templates_versions_list.md" - }, - { - "title": "templates versions promote", - "description": "Promote a template version to active.", - "path": "reference/cli/templates_versions_promote.md" - }, - { - "title": "templates versions unarchive", - "description": "Unarchive a template version(s).", - "path": "reference/cli/templates_versions_unarchive.md" - }, - { - "title": "tokens", - "description": "Manage personal access tokens", - "path": "reference/cli/tokens.md" - }, - { - "title": "tokens create", - "description": "Create a token", - "path": "reference/cli/tokens_create.md" - }, - { - "title": "tokens list", - "description": "List tokens", - "path": "reference/cli/tokens_list.md" - }, - { - "title": "tokens remove", - "description": "Delete a token", - "path": "reference/cli/tokens_remove.md" - }, - { - "title": "unfavorite", - "description": "Remove a workspace from your favorites", - "path": "reference/cli/unfavorite.md" - }, - { - "title": "update", - "description": "Will update and start a given workspace if it is out of date", - "path": "reference/cli/update.md" - }, - { - "title": "users", - "description": "Manage users", - "path": "reference/cli/users.md" - }, - { - "title": "users activate", - "description": "Update a user's status to 'active'. Active users can fully interact with the platform", - "path": "reference/cli/users_activate.md" - }, - { - "title": "users create", - "path": "reference/cli/users_create.md" - }, - { - "title": "users delete", - "description": "Delete a user by username or user_id.", - "path": "reference/cli/users_delete.md" - }, - { - "title": "users edit-roles", - "description": "Edit a user's roles by username or id", - "path": "reference/cli/users_edit-roles.md" - }, - { - "title": "users list", - "path": "reference/cli/users_list.md" - }, - { - "title": "users show", - "description": "Show a single user. Use 'me' to indicate the currently authenticated user.", - "path": "reference/cli/users_show.md" - }, - { - "title": "users suspend", - "description": "Update a user's status to 'suspended'. A suspended user cannot log into the platform", - "path": "reference/cli/users_suspend.md" - }, - { - "title": "version", - "description": "Show coder version", - "path": "reference/cli/version.md" - }, - { - "title": "whoami", - "description": "Fetch authenticated user info for Coder deployment", - "path": "reference/cli/whoami.md" - } - ] - }, - { - "title": "Agent API", - "description": "Learn how to use Coder Agent API", - "path": "./reference/agent-api/index.md", - "icon_path": "./images/icons/api.svg", - "children": [ - { - "title": "Debug", - "path": "./reference/agent-api/debug.md" - }, - { - "title": "Schemas", - "path": "./reference/agent-api/schemas.md" - } - ] - } - ] - } - ] -} + "versions": [ + "main" + ], + "routes": [ + { + "title": "About", + "description": "Coder docs", + "path": "./README.md", + "icon_path": "./images/icons/home.svg", + "children": [ + { + "title": "Quickstart", + "description": "Learn how to install and run Coder quickly", + "path": "./tutorials/quickstart.md" + }, + { + "title": "Screenshots", + "description": "View screenshots of the Coder platform", + "path": "./start/screenshots.md" + } + ] + }, + { + "title": "Install", + "description": "Installing Coder", + "path": "./install/index.md", + "icon_path": "./images/icons/download.svg", + "children": [ + { + "title": "Coder CLI", + "description": "Install the standalone binary", + "path": "./install/cli.md", + "icon_path": "./images/icons/terminal.svg" + }, + { + "title": "Docker", + "description": "Install Coder using Docker", + "path": "./install/docker.md", + "icon_path": "./images/icons/docker.svg" + }, + { + "title": "Kubernetes", + "description": "Install Coder on Kubernetes", + "path": "./install/kubernetes.md", + "icon_path": "./images/icons/kubernetes.svg" + }, + { + "title": "Rancher", + "description": "Deploy Coder on Rancher", + "path": "./install/rancher.md", + "icon_path": "./images/icons/rancher.svg" + }, + { + "title": "OpenShift", + "description": "Install Coder on OpenShift", + "path": "./install/openshift.md", + "icon_path": "./images/icons/openshift.svg" + }, + { + "title": "Cloud Providers", + "description": "Install Coder on cloud providers", + "path": "./install/cloud/index.md", + "icon_path": "./images/icons/cloud.svg", + "children": [ + { + "title": "AWS EC2", + "description": "Install Coder on AWS EC2", + "path": "./install/cloud/ec2.md" + }, + { + "title": "GCP Compute Engine", + "description": "Install Coder on GCP Compute Engine", + "path": "./install/cloud/compute-engine.md" + }, + { + "title": "Azure VM", + "description": "Install Coder on an Azure VM", + "path": "./install/cloud/azure-vm.md" + } + ] + }, + { + "title": "Offline Deployments", + "description": "Run Coder in offline / air-gapped environments", + "path": "./install/offline.md", + "icon_path": "./images/icons/lan.svg" + }, + { + "title": "Unofficial Install Methods", + "description": "Other installation methods", + "path": "./install/other/index.md", + "icon_path": "./images/icons/generic.svg" + }, + { + "title": "Upgrading", + "description": "Learn how to upgrade Coder", + "path": "./install/upgrade.md", + "icon_path": "./images/icons/upgrade.svg" + }, + { + "title": "Uninstall", + "description": "Learn how to uninstall Coder", + "path": "./install/uninstall.md", + "icon_path": "./images/icons/trash.svg" + }, + { + "title": "Releases", + "description": "Learn about the Coder release channels and schedule", + "path": "./install/releases/index.md", + "icon_path": "./images/icons/star.svg", + "children": [ + { + "title": "Feature stages", + "description": "Information about pre-GA stages.", + "path": "./install/releases/feature-stages.md" + } + ] + } + ] + }, + { + "title": "User Guides", + "description": "Guides for end-users of Coder", + "path": "./user-guides/index.md", + "icon_path": "./images/icons/users.svg", + "children": [ + { + "title": "Access Workspaces", + "description": "Connect to your Coder workspaces", + "path": "./user-guides/workspace-access/index.md", + "icon_path": "./images/icons/access.svg", + "children": [ + { + "title": "Visual Studio Code", + "description": "Use VSCode with Coder in the desktop or browser", + "path": "./user-guides/workspace-access/vscode.md" + }, + { + "title": "JetBrains IDEs", + "description": "Use JetBrains IDEs with Gateway", + "path": "./user-guides/workspace-access/jetbrains/index.md", + "children": [ + { + "title": "JetBrains Gateway in an air-gapped environment", + "description": "Use JetBrains Gateway in an air-gapped offline environment", + "path": "./user-guides/workspace-access/jetbrains/jetbrains-airgapped.md" + } + ] + }, + { + "title": "Remote Desktop", + "description": "Use RDP in Coder", + "path": "./user-guides/workspace-access/remote-desktops.md" + }, + { + "title": "Emacs TRAMP", + "description": "Use Emacs TRAMP in Coder", + "path": "./user-guides/workspace-access/emacs-tramp.md" + }, + { + "title": "Port Forwarding", + "description": "Access ports on your workspace", + "path": "./user-guides/workspace-access/port-forwarding.md" + }, + { + "title": "Filebrowser", + "description": "Access your workspace files", + "path": "./user-guides/workspace-access/filebrowser.md" + }, + { + "title": "Web IDEs and Coder Apps", + "description": "Access your workspace with IDEs in the browser", + "path": "./user-guides/workspace-access/web-ides.md" + }, + { + "title": "Zed", + "description": "Access your workspace with Zed", + "path": "./user-guides/workspace-access/zed.md" + }, + { + "title": "Cursor", + "description": "Access your workspace with Cursor", + "path": "./user-guides/workspace-access/cursor.md" + }, + { + "title": "Windsurf", + "description": "Access your workspace with Windsurf", + "path": "./user-guides/workspace-access/windsurf.md" + } + ] + }, + { + "title": "Coder Desktop", + "description": "Use Coder Desktop to access your workspace like it's a local machine", + "path": "./user-guides/desktop/index.md", + "icon_path": "./images/icons/computer-code.svg", + "state": [ + "early access" + ] + }, + { + "title": "Workspace Management", + "description": "Manage workspaces", + "path": "./user-guides/workspace-management.md", + "icon_path": "./images/icons/generic.svg" + }, + { + "title": "Workspace Scheduling", + "description": "Cost control with workspace schedules", + "path": "./user-guides/workspace-scheduling.md", + "icon_path": "./images/icons/stopwatch.svg" + }, + { + "title": "Workspace Lifecycle", + "description": "A guide to the workspace lifecycle, from creation and status through stopping and deletion.", + "path": "./user-guides/workspace-lifecycle.md", + "icon_path": "./images/icons/circle-dot.svg" + }, + { + "title": "Dotfiles", + "description": "Personalize your environment with dotfiles", + "path": "./user-guides/workspace-dotfiles.md", + "icon_path": "./images/icons/art-pad.svg" + } + ] + }, + { + "title": "Administration", + "description": "Guides for template and deployment administrators", + "path": "./admin/index.md", + "icon_path": "./images/icons/wrench.svg", + "children": [ + { + "title": "Setup", + "description": "Configure user access to your control plane.", + "path": "./admin/setup/index.md", + "icon_path": "./images/icons/toggle_on.svg", + "children": [ + { + "title": "Appearance", + "description": "Learn how to configure the appearance of Coder", + "path": "./admin/setup/appearance.md", + "state": [ + "premium" + ] + }, + { + "title": "Telemetry", + "description": "Learn what usage telemetry Coder collects", + "path": "./admin/setup/telemetry.md" + } + ] + }, + { + "title": "Infrastructure", + "description": "How to integrate Coder with your organization's compute", + "path": "./admin/infrastructure/index.md", + "icon_path": "./images/icons/container.svg", + "children": [ + { + "title": "Architecture", + "description": "Learn about Coder's architecture", + "path": "./admin/infrastructure/architecture.md" + }, + { + "title": "Validated Architectures", + "description": "Architectures for large Coder deployments", + "path": "./admin/infrastructure/validated-architectures/index.md", + "children": [ + { + "title": "Up to 1,000 Users", + "path": "./admin/infrastructure/validated-architectures/1k-users.md" + }, + { + "title": "Up to 2,000 Users", + "path": "./admin/infrastructure/validated-architectures/2k-users.md" + }, + { + "title": "Up to 3,000 Users", + "path": "./admin/infrastructure/validated-architectures/3k-users.md" + } + ] + }, + { + "title": "Scale Testing", + "description": "Ensure your deployment can handle your organization's needs", + "path": "./admin/infrastructure/scale-testing.md" + }, + { + "title": "Scaling Utilities", + "description": "Tools to help you scale your deployment", + "path": "./admin/infrastructure/scale-utility.md" + }, + { + "title": "Scaling best practices", + "description": "How to prepare a Coder deployment for scale", + "path": "./tutorials/best-practices/scale-coder.md" + } + ] + }, + { + "title": "Users", + "description": "Learn how to manage and audit users", + "path": "./admin/users/index.md", + "icon_path": "./images/icons/users.svg", + "children": [ + { + "title": "OIDC Authentication", + "path": "./admin/users/oidc-auth.md" + }, + { + "title": "GitHub Authentication", + "path": "./admin/users/github-auth.md" + }, + { + "title": "Password Authentication", + "path": "./admin/users/password-auth.md" + }, + { + "title": "Headless Authentication", + "path": "./admin/users/headless-auth.md" + }, + { + "title": "Groups \u0026 Roles", + "path": "./admin/users/groups-roles.md", + "state": [ + "premium" + ] + }, + { + "title": "IdP Sync", + "path": "./admin/users/idp-sync.md", + "state": [ + "premium" + ] + }, + { + "title": "Organizations", + "path": "./admin/users/organizations.md", + "state": [ + "premium" + ] + }, + { + "title": "Quotas", + "path": "./admin/users/quotas.md", + "state": [ + "premium" + ] + }, + { + "title": "Sessions \u0026 API Tokens", + "path": "./admin/users/sessions-tokens.md" + } + ] + }, + { + "title": "Templates", + "description": "Learn how to author and maintain Coder templates", + "path": "./admin/templates/index.md", + "icon_path": "./images/icons/picture.svg", + "children": [ + { + "title": "Creating Templates", + "description": "Learn how to create templates with Terraform", + "path": "./admin/templates/creating-templates.md" + }, + { + "title": "Managing Templates", + "description": "Learn how to manage templates and best practices", + "path": "./admin/templates/managing-templates/index.md", + "children": [ + { + "title": "Image Management", + "description": "Learn about template image management", + "path": "./admin/templates/managing-templates/image-management.md" + }, + { + "title": "Change Management", + "description": "Learn about template change management and versioning", + "path": "./admin/templates/managing-templates/change-management.md" + }, + { + "title": "Dev containers", + "description": "Learn about using development containers in templates", + "path": "./admin/templates/managing-templates/devcontainers/index.md", + "children": [ + { + "title": "Add a dev container template", + "description": "How to add a dev container template to Coder", + "path": "./admin/templates/managing-templates/devcontainers/add-devcontainer.md" + }, + { + "title": "Dev container security and caching", + "description": "Configure dev container authentication and caching", + "path": "./admin/templates/managing-templates/devcontainers/devcontainer-security-caching.md" + }, + { + "title": "Dev container releases and known issues", + "description": "Dev container releases and known issues", + "path": "./admin/templates/managing-templates/devcontainers/devcontainer-releases-known-issues.md" + } + ] + }, + { + "title": "Template Dependencies", + "description": "Learn how to manage template dependencies", + "path": "./admin/templates/managing-templates/dependencies.md" + }, + { + "title": "Workspace Scheduling", + "description": "Learn how to control how workspaces are started and stopped", + "path": "./admin/templates/managing-templates/schedule.md" + } + ] + }, + { + "title": "Extending Templates", + "description": "Learn best practices in extending templates", + "path": "./admin/templates/extending-templates/index.md", + "children": [ + { + "title": "Agent Metadata", + "description": "Retrieve real-time stats from the workspace agent", + "path": "./admin/templates/extending-templates/agent-metadata.md" + }, + { + "title": "Build Parameters", + "description": "Use parameters to customize workspaces at build", + "path": "./admin/templates/extending-templates/parameters.md" + }, + { + "title": "Icons", + "description": "Customize your template with built-in icons", + "path": "./admin/templates/extending-templates/icons.md" + }, + { + "title": "Resource Metadata", + "description": "Display resource state in the workspace dashboard", + "path": "./admin/templates/extending-templates/resource-metadata.md" + }, + { + "title": "Resource Monitoring", + "description": "Monitor resources in the workspace dashboard", + "path": "./admin/templates/extending-templates/resource-monitoring.md" + }, + { + "title": "Resource Ordering", + "description": "Design the UI of workspaces", + "path": "./admin/templates/extending-templates/resource-ordering.md" + }, + { + "title": "Resource Persistence", + "description": "Control resource persistence", + "path": "./admin/templates/extending-templates/resource-persistence.md" + }, + { + "title": "Terraform Variables", + "description": "Use variables to manage template state", + "path": "./admin/templates/extending-templates/variables.md" + }, + { + "title": "Terraform Modules", + "description": "Reuse terraform code across templates", + "path": "./admin/templates/extending-templates/modules.md" + }, + { + "title": "Web IDEs and Coder Apps", + "description": "Add and configure Web IDEs in your templates as coder apps", + "path": "./admin/templates/extending-templates/web-ides.md" + }, + { + "title": "Pre-install JetBrains Gateway", + "description": "Pre-install JetBrains Gateway in a template for faster IDE startup", + "path": "./admin/templates/extending-templates/jetbrains-gateway.md" + }, + { + "title": "Docker in Workspaces", + "description": "Use Docker in your workspaces", + "path": "./admin/templates/extending-templates/docker-in-workspaces.md" + }, + { + "title": "Workspace Tags", + "description": "Control provisioning using Workspace Tags and Parameters", + "path": "./admin/templates/extending-templates/workspace-tags.md" + }, + { + "title": "Provider Authentication", + "description": "Authenticate with provider APIs to provision workspaces", + "path": "./admin/templates/extending-templates/provider-authentication.md" + }, + { + "title": "Process Logging", + "description": "Log workspace processes", + "path": "./admin/templates/extending-templates/process-logging.md", + "state": [ + "premium" + ] + } + ] + }, + { + "title": "Open in Coder", + "description": "Open workspaces in Coder", + "path": "./admin/templates/open-in-coder.md" + }, + { + "title": "Permissions \u0026 Policies", + "description": "Learn how to create templates with Terraform", + "path": "./admin/templates/template-permissions.md", + "state": [ + "premium" + ] + }, + { + "title": "Troubleshooting Templates", + "description": "Learn how to troubleshoot template issues", + "path": "./admin/templates/troubleshooting.md" + } + ] + }, + { + "title": "External Provisioners", + "description": "Learn how to run external provisioners with Coder", + "path": "./admin/provisioners/index.md", + "icon_path": "./images/icons/key.svg", + "state": [ + "premium" + ], + "children": [ + { + "title": "Manage Provisioner Jobs", + "description": "Learn how to run external provisioners with Coder", + "path": "./admin/provisioners/manage-provisioner-jobs.md", + "state": [ + "premium" + ] + } + ] + }, + { + "title": "External Auth", + "description": "Learn how to configure external authentication", + "path": "./admin/external-auth.md", + "icon_path": "./images/icons/plug.svg" + }, + { + "title": "Integrations", + "description": "Use integrations to extend Coder", + "path": "./admin/integrations/index.md", + "icon_path": "./images/icons/puzzle.svg", + "children": [ + { + "title": "Prometheus", + "description": "Collect deployment metrics with Prometheus", + "path": "./admin/integrations/prometheus.md" + }, + { + "title": "Kubernetes Logging", + "description": "Stream K8s event logs on workspace startup", + "path": "./admin/integrations/kubernetes-logs.md" + }, + { + "title": "Additional Kubernetes Clusters", + "description": "Deploy workspaces on additional Kubernetes clusters", + "path": "./admin/integrations/multiple-kube-clusters.md" + }, + { + "title": "JFrog Artifactory", + "description": "Integrate Coder with JFrog Artifactory", + "path": "./admin/integrations/jfrog-artifactory.md" + }, + { + "title": "JFrog Xray", + "description": "Integrate Coder with JFrog Xray", + "path": "./admin/integrations/jfrog-xray.md" + }, + { + "title": "Island Secure Browser", + "description": "Integrate Coder with Island's Secure Browser", + "path": "./admin/integrations/island.md" + }, + { + "title": "DX PlatformX", + "description": "Integrate Coder with DX PlatformX", + "path": "./admin/integrations/platformx.md" + }, + { + "title": "Hashicorp Vault", + "description": "Integrate Coder with Hashicorp Vault", + "path": "./admin/integrations/vault.md" + } + ] + }, + { + "title": "Networking", + "description": "Understand Coder's networking layer", + "path": "./admin/networking/index.md", + "icon_path": "./images/icons/networking.svg", + "children": [ + { + "title": "Port Forwarding", + "description": "Learn how to forward ports in Coder", + "path": "./admin/networking/port-forwarding.md" + }, + { + "title": "STUN and NAT", + "description": "Learn how to forward ports in Coder", + "path": "./admin/networking/stun.md" + }, + { + "title": "Workspace Proxies", + "description": "Run geo distributed workspace proxies", + "path": "./admin/networking/workspace-proxies.md", + "state": [ + "premium" + ] + }, + { + "title": "High Availability", + "description": "Learn how to configure Coder for High Availability", + "path": "./admin/networking/high-availability.md", + "state": [ + "premium" + ] + }, + { + "title": "Troubleshooting", + "description": "Troubleshoot networking issues in Coder", + "path": "./admin/networking/troubleshooting.md" + } + ] + }, + { + "title": "Monitoring", + "description": "Configure security policy and audit your deployment", + "path": "./admin/monitoring/index.md", + "icon_path": "./images/icons/speed.svg", + "children": [ + { + "title": "Logs", + "description": "Learn about Coder's logs", + "path": "./admin/monitoring/logs.md" + }, + { + "title": "Metrics", + "description": "Learn about Coder's logs", + "path": "./admin/monitoring/metrics.md" + }, + { + "title": "Health Check", + "description": "Learn about Coder's automated health checks", + "path": "./admin/monitoring/health-check.md" + }, + { + "title": "Notifications", + "description": "Configure notifications for your deployment", + "path": "./admin/monitoring/notifications/index.md", + "children": [ + { + "title": "Slack Notifications", + "description": "Learn how to setup Slack notifications", + "path": "./admin/monitoring/notifications/slack.md" + }, + { + "title": "Microsoft Teams Notifications", + "description": "Learn how to setup Microsoft Teams notifications", + "path": "./admin/monitoring/notifications/teams.md" + } + ] + } + ] + }, + { + "title": "Security", + "description": "Configure security policy and audit your deployment", + "path": "./admin/security/index.md", + "icon_path": "./images/icons/lock.svg", + "children": [ + { + "title": "Audit Logs", + "description": "Audit actions taken inside Coder", + "path": "./admin/security/audit-logs.md", + "state": [ + "premium" + ] + }, + { + "title": "Secrets", + "description": "Use sensitive variables in your workspaces", + "path": "./admin/security/secrets.md" + }, + { + "title": "Database Encryption", + "description": "Encrypt the database to prevent unauthorized access", + "path": "./admin/security/database-encryption.md", + "state": [ + "premium" + ] + } + ] + }, + { + "title": "Licensing", + "description": "Configure licensing for your deployment", + "path": "./admin/licensing/index.md", + "icon_path": "./images/icons/licensing.svg" + } + ] + }, + { + "title": "Run AI Coding Agents in Coder", + "description": "Learn how to run and integrate AI coding agents like GPT-Code, OpenDevin, or SWE-Agent in Coder workspaces to boost developer productivity.", + "path": "./ai-coder/index.md", + "icon_path": "./images/icons/wand.svg", + "state": [ + "early access" + ], + "children": [ + { + "title": "Learn about coding agents", + "description": "Learn about the different AI agents and their tradeoffs", + "path": "./ai-coder/agents.md" + }, + { + "title": "Create a Coder template for agents", + "description": "Create a purpose-built template for your AI agents", + "path": "./ai-coder/create-template.md", + "state": [ + "early access" + ] + }, + { + "title": "Integrate with your issue tracker", + "description": "Assign tickets to AI agents and interact via code reviews", + "path": "./ai-coder/issue-tracker.md", + "state": [ + "early access" + ] + }, + { + "title": "Model Context Protocols (MCP) and adding AI tools", + "description": "Improve results by adding tools to your AI agents", + "path": "./ai-coder/best-practices.md", + "state": [ + "early access" + ] + }, + { + "title": "Supervise agents via Coder UI", + "description": "Interact with agents via the Coder UI", + "path": "./ai-coder/coder-dashboard.md", + "state": [ + "early access" + ] + }, + { + "title": "Supervise agents via the IDE", + "description": "Interact with agents via VS Code or Cursor", + "path": "./ai-coder/ide-integration.md", + "state": [ + "early access" + ] + }, + { + "title": "Programmatically manage agents", + "description": "Manage agents via MCP, the Coder CLI, and/or REST API", + "path": "./ai-coder/headless.md", + "state": [ + "early access" + ] + }, + { + "title": "Securing agents in Coder", + "description": "Learn how to secure agents with boundaries", + "path": "./ai-coder/securing.md", + "state": [ + "early access" + ] + }, + { + "title": "Custom agents", + "description": "Learn how to use custom agents with Coder", + "path": "./ai-coder/custom-agents.md", + "state": [ + "early access" + ] + } + ] + }, + { + "title": "Contributing", + "description": "Learn how to contribute to Coder", + "path": "./CONTRIBUTING.md", + "icon_path": "./images/icons/contributing.svg", + "children": [ + { + "title": "Code of Conduct", + "description": "See the code of conduct for contributing to Coder", + "path": "./contributing/CODE_OF_CONDUCT.md", + "icon_path": "./images/icons/circle-dot.svg" + }, + { + "title": "Documentation", + "description": "Our style guide for use when authoring documentation", + "path": "./contributing/documentation.md", + "icon_path": "./images/icons/document.svg" + }, + { + "title": "Frontend", + "description": "Our guide for frontend development", + "path": "./contributing/frontend.md", + "icon_path": "./images/icons/frontend.svg" + }, + { + "title": "Security", + "description": "Our guide for security", + "path": "./contributing/SECURITY.md", + "icon_path": "./images/icons/lock.svg" + } + ] + }, + { + "title": "Tutorials", + "description": "Coder knowledgebase for administrating your deployment", + "path": "./tutorials/index.md", + "icon_path": "./images/icons/generic.svg", + "children": [ + { + "title": "Quickstart", + "description": "Learn how to install and run Coder quickly", + "path": "./tutorials/quickstart.md" + }, + { + "title": "Write a Template from Scratch", + "description": "Learn how to author Coder templates", + "path": "./tutorials/template-from-scratch.md" + }, + { + "title": "Using an External Database", + "description": "Use Coder with an external database", + "path": "./tutorials/external-database.md" + }, + { + "title": "Image Management", + "description": "Learn about image management with Coder", + "path": "./admin/templates/managing-templates/image-management.md" + }, + { + "title": "Generate a Support Bundle", + "description": "Generate and upload a Support Bundle to Coder Support", + "path": "./tutorials/support-bundle.md" + }, + { + "title": "Configuring Okta", + "description": "Custom claims/scopes with Okta for group/role sync", + "path": "./tutorials/configuring-okta.md" + }, + { + "title": "Google to AWS Federation", + "description": "Federating a Google Cloud service account to AWS", + "path": "./tutorials/gcp-to-aws.md" + }, + { + "title": "JFrog Artifactory Integration", + "description": "Integrate Coder with JFrog Artifactory", + "path": "./admin/integrations/jfrog-artifactory.md" + }, + { + "title": "Istio Integration", + "description": "Integrate Coder with Istio", + "path": "./admin/integrations/istio.md" + }, + { + "title": "Island Secure Browser Integration", + "description": "Integrate Coder with Island's Secure Browser", + "path": "./admin/integrations/island.md" + }, + { + "title": "Template ImagePullSecrets", + "description": "Creating ImagePullSecrets for private registries", + "path": "./tutorials/image-pull-secret.md" + }, + { + "title": "Postgres SSL", + "description": "Configure Coder to connect to Postgres over SSL", + "path": "./tutorials/postgres-ssl.md" + }, + { + "title": "Azure Federation", + "description": "Federating Coder to Azure", + "path": "./tutorials/azure-federation.md" + }, + { + "title": "Scanning Workspaces with JFrog Xray", + "description": "Integrate Coder with JFrog Xray", + "path": "./admin/integrations/jfrog-xray.md" + }, + { + "title": "Cloning Git Repositories", + "description": "Learn how to clone Git repositories in Coder", + "path": "./tutorials/cloning-git-repositories.md" + }, + { + "title": "Test Templates Through CI/CD", + "description": "Learn how to test and publish Coder templates in a CI/CD pipeline", + "path": "./tutorials/testing-templates.md" + }, + { + "title": "Use Apache as a Reverse Proxy", + "description": "Learn how to use Apache as a reverse proxy", + "path": "./tutorials/reverse-proxy-apache.md" + }, + { + "title": "Use Caddy as a Reverse Proxy", + "description": "Learn how to use Caddy as a reverse proxy", + "path": "./tutorials/reverse-proxy-caddy.md" + }, + { + "title": "Use NGINX as a Reverse Proxy", + "description": "Learn how to use NGINX as a reverse proxy", + "path": "./tutorials/reverse-proxy-nginx.md" + }, + { + "title": "FAQs", + "description": "Miscellaneous FAQs from our community", + "path": "./tutorials/faqs.md" + }, + { + "title": "Best practices", + "description": "Guides to help you make the most of your Coder experience", + "path": "./tutorials/best-practices/index.md", + "children": [ + { + "title": "Organizations - best practices", + "description": "How to make the best use of Coder Organizations", + "path": "./tutorials/best-practices/organizations.md" + }, + { + "title": "Scale Coder", + "description": "How to prepare a Coder deployment for scale", + "path": "./tutorials/best-practices/scale-coder.md" + }, + { + "title": "Security - best practices", + "description": "Make your Coder deployment more secure", + "path": "./tutorials/best-practices/security-best-practices.md" + }, + { + "title": "Speed up your workspaces", + "description": "Speed up your Coder templates and workspaces", + "path": "./tutorials/best-practices/speed-up-templates.md" + } + ] + } + ] + }, + { + "title": "Reference", + "description": "Reference", + "path": "./reference/index.md", + "icon_path": "./images/icons/notes.svg", + "children": [ + { + "title": "REST API", + "description": "Learn how to use Coderd API", + "path": "./reference/api/index.md", + "icon_path": "./images/icons/api.svg", + "children": [ + { + "title": "General", + "path": "./reference/api/general.md" + }, + { + "title": "Agents", + "path": "./reference/api/agents.md" + }, + { + "title": "Applications", + "path": "./reference/api/applications.md" + }, + { + "title": "Audit", + "path": "./reference/api/audit.md" + }, + { + "title": "Authentication", + "path": "./reference/api/authentication.md" + }, + { + "title": "Authorization", + "path": "./reference/api/authorization.md" + }, + { + "title": "Builds", + "path": "./reference/api/builds.md" + }, + { + "title": "Debug", + "path": "./reference/api/debug.md" + }, + { + "title": "Enterprise", + "path": "./reference/api/enterprise.md" + }, + { + "title": "Files", + "path": "./reference/api/files.md" + }, + { + "title": "Git", + "path": "./reference/api/git.md" + }, + { + "title": "Insights", + "path": "./reference/api/insights.md" + }, + { + "title": "Members", + "path": "./reference/api/members.md" + }, + { + "title": "Organizations", + "path": "./reference/api/organizations.md" + }, + { + "title": "PortSharing", + "path": "./reference/api/portsharing.md" + }, + { + "title": "Schemas", + "path": "./reference/api/schemas.md" + }, + { + "title": "Templates", + "path": "./reference/api/templates.md" + }, + { + "title": "Users", + "path": "./reference/api/users.md" + }, + { + "title": "WorkspaceProxies", + "path": "./reference/api/workspaceproxies.md" + }, + { + "title": "Workspaces", + "path": "./reference/api/workspaces.md" + } + ] + }, + { + "title": "Command Line", + "description": "Learn how to use Coder CLI", + "path": "./reference/cli/index.md", + "icon_path": "./images/icons/terminal.svg", + "children": [ + { + "title": "autoupdate", + "description": "Toggle auto-update policy for a workspace", + "path": "reference/cli/autoupdate.md" + }, + { + "title": "coder", + "path": "reference/cli/index.md" + }, + { + "title": "completion", + "description": "Install or update shell completion scripts for the detected or chosen shell.", + "path": "reference/cli/completion.md" + }, + { + "title": "config-ssh", + "description": "Add an SSH Host entry for your workspaces \"ssh coder.workspace\"", + "path": "reference/cli/config-ssh.md" + }, + { + "title": "create", + "description": "Create a workspace", + "path": "reference/cli/create.md" + }, + { + "title": "delete", + "description": "Delete a workspace", + "path": "reference/cli/delete.md" + }, + { + "title": "dotfiles", + "description": "Personalize your workspace by applying a canonical dotfiles repository", + "path": "reference/cli/dotfiles.md" + }, + { + "title": "external-auth", + "description": "Manage external authentication", + "path": "reference/cli/external-auth.md" + }, + { + "title": "external-auth access-token", + "description": "Print auth for an external provider", + "path": "reference/cli/external-auth_access-token.md" + }, + { + "title": "favorite", + "description": "Add a workspace to your favorites", + "path": "reference/cli/favorite.md" + }, + { + "title": "features", + "description": "List Enterprise features", + "path": "reference/cli/features.md" + }, + { + "title": "features list", + "path": "reference/cli/features_list.md" + }, + { + "title": "groups", + "description": "Manage groups", + "path": "reference/cli/groups.md" + }, + { + "title": "groups create", + "description": "Create a user group", + "path": "reference/cli/groups_create.md" + }, + { + "title": "groups delete", + "description": "Delete a user group", + "path": "reference/cli/groups_delete.md" + }, + { + "title": "groups edit", + "description": "Edit a user group", + "path": "reference/cli/groups_edit.md" + }, + { + "title": "groups list", + "description": "List user groups", + "path": "reference/cli/groups_list.md" + }, + { + "title": "licenses", + "description": "Add, delete, and list licenses", + "path": "reference/cli/licenses.md" + }, + { + "title": "licenses add", + "description": "Add license to Coder deployment", + "path": "reference/cli/licenses_add.md" + }, + { + "title": "licenses delete", + "description": "Delete license by ID", + "path": "reference/cli/licenses_delete.md" + }, + { + "title": "licenses list", + "description": "List licenses (including expired)", + "path": "reference/cli/licenses_list.md" + }, + { + "title": "list", + "description": "List workspaces", + "path": "reference/cli/list.md" + }, + { + "title": "login", + "description": "Authenticate with Coder deployment", + "path": "reference/cli/login.md" + }, + { + "title": "logout", + "description": "Unauthenticate your local session", + "path": "reference/cli/logout.md" + }, + { + "title": "netcheck", + "description": "Print network debug information for DERP and STUN", + "path": "reference/cli/netcheck.md" + }, + { + "title": "notifications", + "description": "Manage Coder notifications", + "path": "reference/cli/notifications.md" + }, + { + "title": "notifications pause", + "description": "Pause notifications", + "path": "reference/cli/notifications_pause.md" + }, + { + "title": "notifications resume", + "description": "Resume notifications", + "path": "reference/cli/notifications_resume.md" + }, + { + "title": "notifications test", + "description": "Send a test notification", + "path": "reference/cli/notifications_test.md" + }, + { + "title": "open", + "description": "Open a workspace", + "path": "reference/cli/open.md" + }, + { + "title": "open app", + "description": "Open a workspace application.", + "path": "reference/cli/open_app.md" + }, + { + "title": "open vscode", + "description": "Open a workspace in VS Code Desktop", + "path": "reference/cli/open_vscode.md" + }, + { + "title": "organizations", + "description": "Organization related commands", + "path": "reference/cli/organizations.md" + }, + { + "title": "organizations create", + "description": "Create a new organization.", + "path": "reference/cli/organizations_create.md" + }, + { + "title": "organizations members", + "description": "Manage organization members", + "path": "reference/cli/organizations_members.md" + }, + { + "title": "organizations members add", + "description": "Add a new member to the current organization", + "path": "reference/cli/organizations_members_add.md" + }, + { + "title": "organizations members edit-roles", + "description": "Edit organization member's roles", + "path": "reference/cli/organizations_members_edit-roles.md" + }, + { + "title": "organizations members list", + "description": "List all organization members", + "path": "reference/cli/organizations_members_list.md" + }, + { + "title": "organizations members remove", + "description": "Remove a new member to the current organization", + "path": "reference/cli/organizations_members_remove.md" + }, + { + "title": "organizations roles", + "description": "Manage organization roles.", + "path": "reference/cli/organizations_roles.md" + }, + { + "title": "organizations roles create", + "description": "Create a new organization custom role", + "path": "reference/cli/organizations_roles_create.md" + }, + { + "title": "organizations roles show", + "description": "Show role(s)", + "path": "reference/cli/organizations_roles_show.md" + }, + { + "title": "organizations roles update", + "description": "Update an organization custom role", + "path": "reference/cli/organizations_roles_update.md" + }, + { + "title": "organizations settings", + "description": "Manage organization settings.", + "path": "reference/cli/organizations_settings.md" + }, + { + "title": "organizations settings set", + "description": "Update specified organization setting.", + "path": "reference/cli/organizations_settings_set.md" + }, + { + "title": "organizations settings set group-sync", + "description": "Group sync settings to sync groups from an IdP.", + "path": "reference/cli/organizations_settings_set_group-sync.md" + }, + { + "title": "organizations settings set organization-sync", + "description": "Organization sync settings to sync organization memberships from an IdP.", + "path": "reference/cli/organizations_settings_set_organization-sync.md" + }, + { + "title": "organizations settings set role-sync", + "description": "Role sync settings to sync organization roles from an IdP.", + "path": "reference/cli/organizations_settings_set_role-sync.md" + }, + { + "title": "organizations settings show", + "description": "Outputs specified organization setting.", + "path": "reference/cli/organizations_settings_show.md" + }, + { + "title": "organizations settings show group-sync", + "description": "Group sync settings to sync groups from an IdP.", + "path": "reference/cli/organizations_settings_show_group-sync.md" + }, + { + "title": "organizations settings show organization-sync", + "description": "Organization sync settings to sync organization memberships from an IdP.", + "path": "reference/cli/organizations_settings_show_organization-sync.md" + }, + { + "title": "organizations settings show role-sync", + "description": "Role sync settings to sync organization roles from an IdP.", + "path": "reference/cli/organizations_settings_show_role-sync.md" + }, + { + "title": "organizations show", + "description": "Show the organization. Using \"selected\" will show the selected organization from the \"--org\" flag. Using \"me\" will show all organizations you are a member of.", + "path": "reference/cli/organizations_show.md" + }, + { + "title": "ping", + "description": "Ping a workspace", + "path": "reference/cli/ping.md" + }, + { + "title": "port-forward", + "description": "Forward ports from a workspace to the local machine. For reverse port forwarding, use \"coder ssh -R\".", + "path": "reference/cli/port-forward.md" + }, + { + "title": "provisioner", + "description": "View and manage provisioner daemons and jobs", + "path": "reference/cli/provisioner.md" + }, + { + "title": "provisioner jobs", + "description": "View and manage provisioner jobs", + "path": "reference/cli/provisioner_jobs.md" + }, + { + "title": "provisioner jobs cancel", + "description": "Cancel a provisioner job", + "path": "reference/cli/provisioner_jobs_cancel.md" + }, + { + "title": "provisioner jobs list", + "description": "List provisioner jobs", + "path": "reference/cli/provisioner_jobs_list.md" + }, + { + "title": "provisioner keys", + "description": "Manage provisioner keys", + "path": "reference/cli/provisioner_keys.md" + }, + { + "title": "provisioner keys create", + "description": "Create a new provisioner key", + "path": "reference/cli/provisioner_keys_create.md" + }, + { + "title": "provisioner keys delete", + "description": "Delete a provisioner key", + "path": "reference/cli/provisioner_keys_delete.md" + }, + { + "title": "provisioner keys list", + "description": "List provisioner keys in an organization", + "path": "reference/cli/provisioner_keys_list.md" + }, + { + "title": "provisioner list", + "description": "List provisioner daemons in an organization", + "path": "reference/cli/provisioner_list.md" + }, + { + "title": "provisioner start", + "description": "Run a provisioner daemon", + "path": "reference/cli/provisioner_start.md" + }, + { + "title": "publickey", + "description": "Output your Coder public key used for Git operations", + "path": "reference/cli/publickey.md" + }, + { + "title": "rename", + "description": "Rename a workspace", + "path": "reference/cli/rename.md" + }, + { + "title": "reset-password", + "description": "Directly connect to the database to reset a user's password", + "path": "reference/cli/reset-password.md" + }, + { + "title": "restart", + "description": "Restart a workspace", + "path": "reference/cli/restart.md" + }, + { + "title": "schedule", + "description": "Schedule automated start and stop times for workspaces", + "path": "reference/cli/schedule.md" + }, + { + "title": "schedule extend", + "description": "Extend the stop time of a currently running workspace instance.", + "path": "reference/cli/schedule_extend.md" + }, + { + "title": "schedule show", + "description": "Show workspace schedules", + "path": "reference/cli/schedule_show.md" + }, + { + "title": "schedule start", + "description": "Edit workspace start schedule", + "path": "reference/cli/schedule_start.md" + }, + { + "title": "schedule stop", + "description": "Edit workspace stop schedule", + "path": "reference/cli/schedule_stop.md" + }, + { + "title": "server", + "description": "Start a Coder server", + "path": "reference/cli/server.md" + }, + { + "title": "server create-admin-user", + "description": "Create a new admin user with the given username, email and password and adds it to every organization.", + "path": "reference/cli/server_create-admin-user.md" + }, + { + "title": "server dbcrypt", + "description": "Manage database encryption.", + "path": "reference/cli/server_dbcrypt.md" + }, + { + "title": "server dbcrypt decrypt", + "description": "Decrypt a previously encrypted database.", + "path": "reference/cli/server_dbcrypt_decrypt.md" + }, + { + "title": "server dbcrypt delete", + "description": "Delete all encrypted data from the database. THIS IS A DESTRUCTIVE OPERATION.", + "path": "reference/cli/server_dbcrypt_delete.md" + }, + { + "title": "server dbcrypt rotate", + "description": "Rotate database encryption keys.", + "path": "reference/cli/server_dbcrypt_rotate.md" + }, + { + "title": "server postgres-builtin-serve", + "description": "Run the built-in PostgreSQL deployment.", + "path": "reference/cli/server_postgres-builtin-serve.md" + }, + { + "title": "server postgres-builtin-url", + "description": "Output the connection URL for the built-in PostgreSQL deployment.", + "path": "reference/cli/server_postgres-builtin-url.md" + }, + { + "title": "show", + "description": "Display details of a workspace's resources and agents", + "path": "reference/cli/show.md" + }, + { + "title": "speedtest", + "description": "Run upload and download tests from your machine to a workspace", + "path": "reference/cli/speedtest.md" + }, + { + "title": "ssh", + "description": "Start a shell into a workspace", + "path": "reference/cli/ssh.md" + }, + { + "title": "start", + "description": "Start a workspace", + "path": "reference/cli/start.md" + }, + { + "title": "stat", + "description": "Show resource usage for the current workspace.", + "path": "reference/cli/stat.md" + }, + { + "title": "stat cpu", + "description": "Show CPU usage, in cores.", + "path": "reference/cli/stat_cpu.md" + }, + { + "title": "stat disk", + "description": "Show disk usage, in gigabytes.", + "path": "reference/cli/stat_disk.md" + }, + { + "title": "stat mem", + "description": "Show memory usage, in gigabytes.", + "path": "reference/cli/stat_mem.md" + }, + { + "title": "state", + "description": "Manually manage Terraform state to fix broken workspaces", + "path": "reference/cli/state.md" + }, + { + "title": "state pull", + "description": "Pull a Terraform state file from a workspace.", + "path": "reference/cli/state_pull.md" + }, + { + "title": "state push", + "description": "Push a Terraform state file to a workspace.", + "path": "reference/cli/state_push.md" + }, + { + "title": "stop", + "description": "Stop a workspace", + "path": "reference/cli/stop.md" + }, + { + "title": "support", + "description": "Commands for troubleshooting issues with a Coder deployment.", + "path": "reference/cli/support.md" + }, + { + "title": "support bundle", + "description": "Generate a support bundle to troubleshoot issues connecting to a workspace.", + "path": "reference/cli/support_bundle.md" + }, + { + "title": "templates", + "description": "Manage templates", + "path": "reference/cli/templates.md" + }, + { + "title": "templates archive", + "description": "Archive unused or failed template versions from a given template(s)", + "path": "reference/cli/templates_archive.md" + }, + { + "title": "templates create", + "description": "DEPRECATED: Create a template from the current directory or as specified by flag", + "path": "reference/cli/templates_create.md" + }, + { + "title": "templates delete", + "description": "Delete templates", + "path": "reference/cli/templates_delete.md" + }, + { + "title": "templates edit", + "description": "Edit the metadata of a template by name.", + "path": "reference/cli/templates_edit.md" + }, + { + "title": "templates init", + "description": "Get started with a templated template.", + "path": "reference/cli/templates_init.md" + }, + { + "title": "templates list", + "description": "List all the templates available for the organization", + "path": "reference/cli/templates_list.md" + }, + { + "title": "templates pull", + "description": "Download the active, latest, or specified version of a template to a path.", + "path": "reference/cli/templates_pull.md" + }, + { + "title": "templates push", + "description": "Create or update a template from the current directory or as specified by flag", + "path": "reference/cli/templates_push.md" + }, + { + "title": "templates versions", + "description": "Manage different versions of the specified template", + "path": "reference/cli/templates_versions.md" + }, + { + "title": "templates versions archive", + "description": "Archive a template version(s).", + "path": "reference/cli/templates_versions_archive.md" + }, + { + "title": "templates versions list", + "description": "List all the versions of the specified template", + "path": "reference/cli/templates_versions_list.md" + }, + { + "title": "templates versions promote", + "description": "Promote a template version to active.", + "path": "reference/cli/templates_versions_promote.md" + }, + { + "title": "templates versions unarchive", + "description": "Unarchive a template version(s).", + "path": "reference/cli/templates_versions_unarchive.md" + }, + { + "title": "tokens", + "description": "Manage personal access tokens", + "path": "reference/cli/tokens.md" + }, + { + "title": "tokens create", + "description": "Create a token", + "path": "reference/cli/tokens_create.md" + }, + { + "title": "tokens list", + "description": "List tokens", + "path": "reference/cli/tokens_list.md" + }, + { + "title": "tokens remove", + "description": "Delete a token", + "path": "reference/cli/tokens_remove.md" + }, + { + "title": "unfavorite", + "description": "Remove a workspace from your favorites", + "path": "reference/cli/unfavorite.md" + }, + { + "title": "update", + "description": "Will update and start a given workspace if it is out of date", + "path": "reference/cli/update.md" + }, + { + "title": "users", + "description": "Manage users", + "path": "reference/cli/users.md" + }, + { + "title": "users activate", + "description": "Update a user's status to 'active'. Active users can fully interact with the platform", + "path": "reference/cli/users_activate.md" + }, + { + "title": "users create", + "path": "reference/cli/users_create.md" + }, + { + "title": "users delete", + "description": "Delete a user by username or user_id.", + "path": "reference/cli/users_delete.md" + }, + { + "title": "users edit-roles", + "description": "Edit a user's roles by username or id", + "path": "reference/cli/users_edit-roles.md" + }, + { + "title": "users list", + "path": "reference/cli/users_list.md" + }, + { + "title": "users show", + "description": "Show a single user. Use 'me' to indicate the currently authenticated user.", + "path": "reference/cli/users_show.md" + }, + { + "title": "users suspend", + "description": "Update a user's status to 'suspended'. A suspended user cannot log into the platform", + "path": "reference/cli/users_suspend.md" + }, + { + "title": "version", + "description": "Show coder version", + "path": "reference/cli/version.md" + }, + { + "title": "whoami", + "description": "Fetch authenticated user info for Coder deployment", + "path": "reference/cli/whoami.md" + } + ] + }, + { + "title": "Agent API", + "description": "Learn how to use Coder Agent API", + "path": "./reference/agent-api/index.md", + "icon_path": "./images/icons/api.svg", + "children": [ + { + "title": "Debug", + "path": "./reference/agent-api/debug.md" + }, + { + "title": "Schemas", + "path": "./reference/agent-api/schemas.md" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/enterprise/coderd/workspaceagents_test.go b/enterprise/coderd/workspaceagents_test.go index 4ac374a3c8c8e..31fd186009ac2 100644 --- a/enterprise/coderd/workspaceagents_test.go +++ b/enterprise/coderd/workspaceagents_test.go @@ -11,7 +11,11 @@ import ( "github.com/stretchr/testify/require" "github.com/coder/coder/v2/agent" + "github.com/coder/coder/v2/cli/clitest" "github.com/coder/coder/v2/coderd/coderdtest" + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/dbfake" + "github.com/coder/coder/v2/coderd/prebuilds" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/agentsdk" "github.com/coder/coder/v2/codersdk/workspacesdk" @@ -73,6 +77,78 @@ func TestBlockNonBrowser(t *testing.T) { }) } +func TestReinitializeAgent(t *testing.T) { + t.Parallel() + + // GIVEN a live enterprise API with the prebuilds feature enabled + client, db, user := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{ + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + // TODO: enable the prebuilds feature and experiment + }, + }, + }) + + // GIVEN a template, template version, preset and a prebuilt workspace that uses them all + presetID := uuid.New() + tv := dbfake.TemplateVersion(t, db).Seed(database.TemplateVersion{ + OrganizationID: user.OrganizationID, + CreatedBy: user.UserID, + }).Preset(database.TemplateVersionPreset{ + ID: presetID, + }).Do() + + r := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{ + OwnerID: prebuilds.SystemUserID, + TemplateID: tv.Template.ID, + }).Seed(database.WorkspaceBuild{ + TemplateVersionID: tv.TemplateVersion.ID, + TemplateVersionPresetID: uuid.NullUUID{ + UUID: presetID, + Valid: true, + }, + }).WithAgent(func(a []*proto.Agent) []*proto.Agent { + a[0].Scripts = []*proto.Script{ + { + DisplayName: "Prebuild Test Script", + Script: "sleep 5", // Make reinitialization take long enough to assert that it happened + RunOnStart: true, + }, + } + return a + }).Do() + + // GIVEN a running agent + logDir := t.TempDir() + inv, _ := clitest.New(t, + "agent", + "--auth", "token", + "--agent-token", r.AgentToken, + "--agent-url", client.URL.String(), + "--log-dir", logDir, + ) + clitest.Start(t, inv) + + // GIVEN the agent is in a happy steady state + waiter := coderdtest.NewWorkspaceAgentWaiter(t, client, r.Workspace.ID) + waiter.WaitFor(coderdtest.AgentsReady) + + // WHEN a workspace is created that can benefit from prebuilds + ctx := testutil.Context(t, testutil.WaitShort) + _, err := client.CreateUserWorkspace(ctx, user.UserID.String(), codersdk.CreateWorkspaceRequest{ + TemplateVersionID: tv.TemplateVersion.ID, + TemplateVersionPresetID: presetID, + Name: "claimed-workspace", + }) + require.NoError(t, err) + + // THEN the now claimed workspace agent reinitializes + waiter.WaitFor(coderdtest.AgentsNotReady) + + // THEN reinitialization completes + waiter.WaitFor(coderdtest.AgentsReady) +} + type setupResp struct { workspace codersdk.Workspace sdkAgent codersdk.WorkspaceAgent From 9feebef22ed41a7c7f12cf19da8663cbdabc8293 Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Tue, 29 Apr 2025 12:26:23 +0000 Subject: [PATCH 11/42] enable the premium license in a prebuilds integration test --- coderd/workspaceagents.go | 2 +- enterprise/coderd/coderd.go | 2 +- enterprise/coderd/workspaceagents_test.go | 7 ++++++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index 898ce6d2ec763..c154e7a904490 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -1159,7 +1159,7 @@ func (api *API) workspaceAgentPostLogSource(rw http.ResponseWriter, r *http.Requ // @Security CoderSessionToken // @Produce json // @Tags Agents -// @Success 200 {object} agentsdk.ReinitializationResponse +// @Success 200 {object} agentsdk.ReinitializationEvent // @Router /workspaceagents/me/reinit [get] func (api *API) workspaceAgentReinit(rw http.ResponseWriter, r *http.Request) { // Allow us to interrupt watch via cancel. diff --git a/enterprise/coderd/coderd.go b/enterprise/coderd/coderd.go index ca3531b60db78..8b473e8168ffa 100644 --- a/enterprise/coderd/coderd.go +++ b/enterprise/coderd/coderd.go @@ -1166,5 +1166,5 @@ func (api *API) setupPrebuilds(featureEnabled bool) (agplprebuilds.Reconciliatio reconciler := prebuilds.NewStoreReconciler(api.Database, api.Pubsub, api.DeploymentValues.Prebuilds, api.Logger.Named("prebuilds"), quartz.NewReal(), api.PrometheusRegistry) - return reconciler, prebuilds.EnterpriseClaimer{} + return reconciler, prebuilds.NewEnterpriseClaimer(api.Database) } diff --git a/enterprise/coderd/workspaceagents_test.go b/enterprise/coderd/workspaceagents_test.go index 31fd186009ac2..400e8d144fcd2 100644 --- a/enterprise/coderd/workspaceagents_test.go +++ b/enterprise/coderd/workspaceagents_test.go @@ -82,9 +82,14 @@ func TestReinitializeAgent(t *testing.T) { // GIVEN a live enterprise API with the prebuilds feature enabled client, db, user := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + DeploymentValues: coderdtest.DeploymentValues(t, func(dv *codersdk.DeploymentValues) { + dv.Experiments.Append(string(codersdk.ExperimentWorkspacePrebuilds)) + }), + }, LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ - // TODO: enable the prebuilds feature and experiment + codersdk.FeatureWorkspacePrebuilds: 1, }, }, }) From b117b5c34d9eb9ee26fca644c24173fc94da7036 Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Wed, 30 Apr 2025 08:17:50 +0000 Subject: [PATCH 12/42] encapsulate WaitForReinitLoop for easier testing --- cli/agent.go | 25 +- coderd/apidoc/docs.go | 22 +- coderd/apidoc/swagger.json | 14 +- codersdk/agentsdk/agentsdk.go | 54 +- docs/manifest.json | 3362 ++++++++++++++++----------------- docs/reference/api/agents.md | 6 +- docs/reference/api/schemas.md | 30 +- 7 files changed, 1739 insertions(+), 1774 deletions(-) diff --git a/cli/agent.go b/cli/agent.go index 63fe590690a41..d17b1d31858bb 100644 --- a/cli/agent.go +++ b/cli/agent.go @@ -19,8 +19,6 @@ import ( "golang.org/x/xerrors" "gopkg.in/natefinch/lumberjack.v2" - "github.com/coder/retry" - "github.com/prometheus/client_golang/prometheus" "cdr.dev/slog" @@ -332,27 +330,7 @@ func (r *RootCmd) workspaceAgent() *serpent.Command { containerLister = agentcontainers.NewDocker(execer) } - // TODO: timeout ok? - reinitCtx, reinitCancel := context.WithTimeout(context.Background(), time.Hour*24) - defer reinitCancel() - reinitEvents := make(chan agentsdk.ReinitializationEvent) - - go func() { - // Retry to wait for reinit, main context cancels the retrier. - for retrier := retry.New(100*time.Millisecond, 10*time.Second); retrier.Wait(ctx); { - select { - case <-reinitCtx.Done(): - return - default: - } - - err := client.WaitForReinit(reinitCtx, reinitEvents) - if err != nil { - logger.Error(ctx, "failed to wait for reinit instructions, will retry", slog.Error(err)) - } - } - }() - + reinitEvents := agentsdk.WaitForReinitLoop(ctx, logger, client) var ( lastErr error mustExit bool @@ -409,7 +387,6 @@ func (r *RootCmd) workspaceAgent() *serpent.Command { prometheusSrvClose() if mustExit { - reinitCancel() break } diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 071708c6e61be..d99733eaa7226 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -8271,7 +8271,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/agentsdk.ReinitializationResponse" + "$ref": "#/definitions/agentsdk.ReinitializationEvent" } } } @@ -10322,16 +10322,7 @@ const docTemplate = `{ } } }, - "agentsdk.ReinitializationReason": { - "type": "string", - "enum": [ - "prebuild_claimed" - ], - "x-enum-varnames": [ - "ReinitializeReasonPrebuildClaimed" - ] - }, - "agentsdk.ReinitializationResponse": { + "agentsdk.ReinitializationEvent": { "type": "object", "properties": { "message": { @@ -10342,6 +10333,15 @@ const docTemplate = `{ } } }, + "agentsdk.ReinitializationReason": { + "type": "string", + "enum": [ + "prebuild_claimed" + ], + "x-enum-varnames": [ + "ReinitializeReasonPrebuildClaimed" + ] + }, "coderd.SCIMUser": { "type": "object", "properties": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index debc5096bb25c..eeecc485cccab 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -7310,7 +7310,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/agentsdk.ReinitializationResponse" + "$ref": "#/definitions/agentsdk.ReinitializationEvent" } } } @@ -9155,12 +9155,7 @@ } } }, - "agentsdk.ReinitializationReason": { - "type": "string", - "enum": ["prebuild_claimed"], - "x-enum-varnames": ["ReinitializeReasonPrebuildClaimed"] - }, - "agentsdk.ReinitializationResponse": { + "agentsdk.ReinitializationEvent": { "type": "object", "properties": { "message": { @@ -9171,6 +9166,11 @@ } } }, + "agentsdk.ReinitializationReason": { + "type": "string", + "enum": ["prebuild_claimed"], + "x-enum-varnames": ["ReinitializeReasonPrebuildClaimed"] + }, "coderd.SCIMUser": { "type": "object", "properties": { diff --git a/codersdk/agentsdk/agentsdk.go b/codersdk/agentsdk/agentsdk.go index e52d314961ad8..b40a73138240c 100644 --- a/codersdk/agentsdk/agentsdk.go +++ b/codersdk/agentsdk/agentsdk.go @@ -19,6 +19,7 @@ import ( "tailscale.com/tailcfg" "cdr.dev/slog" + "github.com/coder/retry" "github.com/coder/websocket" "github.com/coder/coder/v2/agent/proto" @@ -707,32 +708,43 @@ func PrebuildClaimedChannel(id uuid.UUID) string { // - ping: ignored, keepalive // - prebuild claimed: a prebuilt workspace is claimed, so the agent must reinitialize. // NOTE: the caller is responsible for closing the events chan. -func (c *Client) WaitForReinit(ctx context.Context, events chan<- ReinitializationEvent) error { +func (c *Client) WaitForReinit(ctx context.Context) (*ReinitializationEvent, error) { // TODO: allow configuring httpclient c.SDK.HTTPClient.Timeout = time.Hour * 24 + // TODO (sasswart): tried the following to fix the above, it won't work. The shorter timeout wins. + // I also considered cloning c.SDK.HTTPClient and setting the timeout on the cloned client. + // That won't work because we can't pass the cloned HTTPClient into s.SDK.Request. + // Looks like we're going to need a separate client to be able to have a longer timeout. + // + // timeoutCtx, cancelTimeoutCtx := context.WithTimeout(ctx, 24*time.Hour) + // defer cancelTimeoutCtx() + res, err := c.SDK.Request(ctx, http.MethodGet, "/api/v2/workspaceagents/me/reinit", nil) if err != nil { - return xerrors.Errorf("execute request: %w", err) + return nil, xerrors.Errorf("execute request: %w", err) } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return codersdk.ReadBodyAsError(res) + return nil, codersdk.ReadBodyAsError(res) } nextEvent := codersdk.ServerSentEventReader(ctx, res.Body) for { + // TODO (Sasswart): I don't like that we do this select at the start and at the end. + // nextEvent should return an error if the context is canceled, but that feels like a larger refactor. + // if it did, we'd only have the select at the end of the loop. select { case <-ctx.Done(): - return ctx.Err() + return nil, ctx.Err() default: } sse, err := nextEvent() if err != nil { - return xerrors.Errorf("failed to read server-sent event: %w", err) + return nil, xerrors.Errorf("failed to read server-sent event: %w", err) } if sse.Type != codersdk.ServerSentEventTypeData { continue @@ -740,16 +752,40 @@ func (c *Client) WaitForReinit(ctx context.Context, events chan<- Reinitializati var reinitEvent ReinitializationEvent b, ok := sse.Data.([]byte) if !ok { - return xerrors.Errorf("expected data as []byte, got %T", sse.Data) + return nil, xerrors.Errorf("expected data as []byte, got %T", sse.Data) } err = json.Unmarshal(b, &reinitEvent) if err != nil { - return xerrors.Errorf("unmarshal reinit response: %w", err) + return nil, xerrors.Errorf("unmarshal reinit response: %w", err) } select { case <-ctx.Done(): - return ctx.Err() - case events <- reinitEvent: + return nil, ctx.Err() + default: + return &reinitEvent, nil } } } + +func WaitForReinitLoop(ctx context.Context, logger slog.Logger, client *Client) <-chan ReinitializationEvent { + reinitEvents := make(chan ReinitializationEvent) + + go func() { + for retrier := retry.New(100*time.Millisecond, 10*time.Second); retrier.Wait(ctx); { + logger.Debug(ctx, "waiting for agent reinitialization instructions") + reinitEvent, err := client.WaitForReinit(ctx) + if err != nil { + logger.Error(ctx, "failed to wait for agent reinitialization instructions", slog.Error(err)) + } + reinitEvents <- *reinitEvent + select { + case <-ctx.Done(): + close(reinitEvents) + return + case reinitEvents <- *reinitEvent: + } + } + }() + + return reinitEvents +} diff --git a/docs/manifest.json b/docs/manifest.json index ab51694422891..ea1d19561593f 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -1,1706 +1,1658 @@ { - "versions": [ - "main" - ], - "routes": [ - { - "title": "About", - "description": "Coder docs", - "path": "./README.md", - "icon_path": "./images/icons/home.svg", - "children": [ - { - "title": "Quickstart", - "description": "Learn how to install and run Coder quickly", - "path": "./tutorials/quickstart.md" - }, - { - "title": "Screenshots", - "description": "View screenshots of the Coder platform", - "path": "./start/screenshots.md" - } - ] - }, - { - "title": "Install", - "description": "Installing Coder", - "path": "./install/index.md", - "icon_path": "./images/icons/download.svg", - "children": [ - { - "title": "Coder CLI", - "description": "Install the standalone binary", - "path": "./install/cli.md", - "icon_path": "./images/icons/terminal.svg" - }, - { - "title": "Docker", - "description": "Install Coder using Docker", - "path": "./install/docker.md", - "icon_path": "./images/icons/docker.svg" - }, - { - "title": "Kubernetes", - "description": "Install Coder on Kubernetes", - "path": "./install/kubernetes.md", - "icon_path": "./images/icons/kubernetes.svg" - }, - { - "title": "Rancher", - "description": "Deploy Coder on Rancher", - "path": "./install/rancher.md", - "icon_path": "./images/icons/rancher.svg" - }, - { - "title": "OpenShift", - "description": "Install Coder on OpenShift", - "path": "./install/openshift.md", - "icon_path": "./images/icons/openshift.svg" - }, - { - "title": "Cloud Providers", - "description": "Install Coder on cloud providers", - "path": "./install/cloud/index.md", - "icon_path": "./images/icons/cloud.svg", - "children": [ - { - "title": "AWS EC2", - "description": "Install Coder on AWS EC2", - "path": "./install/cloud/ec2.md" - }, - { - "title": "GCP Compute Engine", - "description": "Install Coder on GCP Compute Engine", - "path": "./install/cloud/compute-engine.md" - }, - { - "title": "Azure VM", - "description": "Install Coder on an Azure VM", - "path": "./install/cloud/azure-vm.md" - } - ] - }, - { - "title": "Offline Deployments", - "description": "Run Coder in offline / air-gapped environments", - "path": "./install/offline.md", - "icon_path": "./images/icons/lan.svg" - }, - { - "title": "Unofficial Install Methods", - "description": "Other installation methods", - "path": "./install/other/index.md", - "icon_path": "./images/icons/generic.svg" - }, - { - "title": "Upgrading", - "description": "Learn how to upgrade Coder", - "path": "./install/upgrade.md", - "icon_path": "./images/icons/upgrade.svg" - }, - { - "title": "Uninstall", - "description": "Learn how to uninstall Coder", - "path": "./install/uninstall.md", - "icon_path": "./images/icons/trash.svg" - }, - { - "title": "Releases", - "description": "Learn about the Coder release channels and schedule", - "path": "./install/releases/index.md", - "icon_path": "./images/icons/star.svg", - "children": [ - { - "title": "Feature stages", - "description": "Information about pre-GA stages.", - "path": "./install/releases/feature-stages.md" - } - ] - } - ] - }, - { - "title": "User Guides", - "description": "Guides for end-users of Coder", - "path": "./user-guides/index.md", - "icon_path": "./images/icons/users.svg", - "children": [ - { - "title": "Access Workspaces", - "description": "Connect to your Coder workspaces", - "path": "./user-guides/workspace-access/index.md", - "icon_path": "./images/icons/access.svg", - "children": [ - { - "title": "Visual Studio Code", - "description": "Use VSCode with Coder in the desktop or browser", - "path": "./user-guides/workspace-access/vscode.md" - }, - { - "title": "JetBrains IDEs", - "description": "Use JetBrains IDEs with Gateway", - "path": "./user-guides/workspace-access/jetbrains/index.md", - "children": [ - { - "title": "JetBrains Gateway in an air-gapped environment", - "description": "Use JetBrains Gateway in an air-gapped offline environment", - "path": "./user-guides/workspace-access/jetbrains/jetbrains-airgapped.md" - } - ] - }, - { - "title": "Remote Desktop", - "description": "Use RDP in Coder", - "path": "./user-guides/workspace-access/remote-desktops.md" - }, - { - "title": "Emacs TRAMP", - "description": "Use Emacs TRAMP in Coder", - "path": "./user-guides/workspace-access/emacs-tramp.md" - }, - { - "title": "Port Forwarding", - "description": "Access ports on your workspace", - "path": "./user-guides/workspace-access/port-forwarding.md" - }, - { - "title": "Filebrowser", - "description": "Access your workspace files", - "path": "./user-guides/workspace-access/filebrowser.md" - }, - { - "title": "Web IDEs and Coder Apps", - "description": "Access your workspace with IDEs in the browser", - "path": "./user-guides/workspace-access/web-ides.md" - }, - { - "title": "Zed", - "description": "Access your workspace with Zed", - "path": "./user-guides/workspace-access/zed.md" - }, - { - "title": "Cursor", - "description": "Access your workspace with Cursor", - "path": "./user-guides/workspace-access/cursor.md" - }, - { - "title": "Windsurf", - "description": "Access your workspace with Windsurf", - "path": "./user-guides/workspace-access/windsurf.md" - } - ] - }, - { - "title": "Coder Desktop", - "description": "Use Coder Desktop to access your workspace like it's a local machine", - "path": "./user-guides/desktop/index.md", - "icon_path": "./images/icons/computer-code.svg", - "state": [ - "early access" - ] - }, - { - "title": "Workspace Management", - "description": "Manage workspaces", - "path": "./user-guides/workspace-management.md", - "icon_path": "./images/icons/generic.svg" - }, - { - "title": "Workspace Scheduling", - "description": "Cost control with workspace schedules", - "path": "./user-guides/workspace-scheduling.md", - "icon_path": "./images/icons/stopwatch.svg" - }, - { - "title": "Workspace Lifecycle", - "description": "A guide to the workspace lifecycle, from creation and status through stopping and deletion.", - "path": "./user-guides/workspace-lifecycle.md", - "icon_path": "./images/icons/circle-dot.svg" - }, - { - "title": "Dotfiles", - "description": "Personalize your environment with dotfiles", - "path": "./user-guides/workspace-dotfiles.md", - "icon_path": "./images/icons/art-pad.svg" - } - ] - }, - { - "title": "Administration", - "description": "Guides for template and deployment administrators", - "path": "./admin/index.md", - "icon_path": "./images/icons/wrench.svg", - "children": [ - { - "title": "Setup", - "description": "Configure user access to your control plane.", - "path": "./admin/setup/index.md", - "icon_path": "./images/icons/toggle_on.svg", - "children": [ - { - "title": "Appearance", - "description": "Learn how to configure the appearance of Coder", - "path": "./admin/setup/appearance.md", - "state": [ - "premium" - ] - }, - { - "title": "Telemetry", - "description": "Learn what usage telemetry Coder collects", - "path": "./admin/setup/telemetry.md" - } - ] - }, - { - "title": "Infrastructure", - "description": "How to integrate Coder with your organization's compute", - "path": "./admin/infrastructure/index.md", - "icon_path": "./images/icons/container.svg", - "children": [ - { - "title": "Architecture", - "description": "Learn about Coder's architecture", - "path": "./admin/infrastructure/architecture.md" - }, - { - "title": "Validated Architectures", - "description": "Architectures for large Coder deployments", - "path": "./admin/infrastructure/validated-architectures/index.md", - "children": [ - { - "title": "Up to 1,000 Users", - "path": "./admin/infrastructure/validated-architectures/1k-users.md" - }, - { - "title": "Up to 2,000 Users", - "path": "./admin/infrastructure/validated-architectures/2k-users.md" - }, - { - "title": "Up to 3,000 Users", - "path": "./admin/infrastructure/validated-architectures/3k-users.md" - } - ] - }, - { - "title": "Scale Testing", - "description": "Ensure your deployment can handle your organization's needs", - "path": "./admin/infrastructure/scale-testing.md" - }, - { - "title": "Scaling Utilities", - "description": "Tools to help you scale your deployment", - "path": "./admin/infrastructure/scale-utility.md" - }, - { - "title": "Scaling best practices", - "description": "How to prepare a Coder deployment for scale", - "path": "./tutorials/best-practices/scale-coder.md" - } - ] - }, - { - "title": "Users", - "description": "Learn how to manage and audit users", - "path": "./admin/users/index.md", - "icon_path": "./images/icons/users.svg", - "children": [ - { - "title": "OIDC Authentication", - "path": "./admin/users/oidc-auth.md" - }, - { - "title": "GitHub Authentication", - "path": "./admin/users/github-auth.md" - }, - { - "title": "Password Authentication", - "path": "./admin/users/password-auth.md" - }, - { - "title": "Headless Authentication", - "path": "./admin/users/headless-auth.md" - }, - { - "title": "Groups \u0026 Roles", - "path": "./admin/users/groups-roles.md", - "state": [ - "premium" - ] - }, - { - "title": "IdP Sync", - "path": "./admin/users/idp-sync.md", - "state": [ - "premium" - ] - }, - { - "title": "Organizations", - "path": "./admin/users/organizations.md", - "state": [ - "premium" - ] - }, - { - "title": "Quotas", - "path": "./admin/users/quotas.md", - "state": [ - "premium" - ] - }, - { - "title": "Sessions \u0026 API Tokens", - "path": "./admin/users/sessions-tokens.md" - } - ] - }, - { - "title": "Templates", - "description": "Learn how to author and maintain Coder templates", - "path": "./admin/templates/index.md", - "icon_path": "./images/icons/picture.svg", - "children": [ - { - "title": "Creating Templates", - "description": "Learn how to create templates with Terraform", - "path": "./admin/templates/creating-templates.md" - }, - { - "title": "Managing Templates", - "description": "Learn how to manage templates and best practices", - "path": "./admin/templates/managing-templates/index.md", - "children": [ - { - "title": "Image Management", - "description": "Learn about template image management", - "path": "./admin/templates/managing-templates/image-management.md" - }, - { - "title": "Change Management", - "description": "Learn about template change management and versioning", - "path": "./admin/templates/managing-templates/change-management.md" - }, - { - "title": "Dev containers", - "description": "Learn about using development containers in templates", - "path": "./admin/templates/managing-templates/devcontainers/index.md", - "children": [ - { - "title": "Add a dev container template", - "description": "How to add a dev container template to Coder", - "path": "./admin/templates/managing-templates/devcontainers/add-devcontainer.md" - }, - { - "title": "Dev container security and caching", - "description": "Configure dev container authentication and caching", - "path": "./admin/templates/managing-templates/devcontainers/devcontainer-security-caching.md" - }, - { - "title": "Dev container releases and known issues", - "description": "Dev container releases and known issues", - "path": "./admin/templates/managing-templates/devcontainers/devcontainer-releases-known-issues.md" - } - ] - }, - { - "title": "Template Dependencies", - "description": "Learn how to manage template dependencies", - "path": "./admin/templates/managing-templates/dependencies.md" - }, - { - "title": "Workspace Scheduling", - "description": "Learn how to control how workspaces are started and stopped", - "path": "./admin/templates/managing-templates/schedule.md" - } - ] - }, - { - "title": "Extending Templates", - "description": "Learn best practices in extending templates", - "path": "./admin/templates/extending-templates/index.md", - "children": [ - { - "title": "Agent Metadata", - "description": "Retrieve real-time stats from the workspace agent", - "path": "./admin/templates/extending-templates/agent-metadata.md" - }, - { - "title": "Build Parameters", - "description": "Use parameters to customize workspaces at build", - "path": "./admin/templates/extending-templates/parameters.md" - }, - { - "title": "Icons", - "description": "Customize your template with built-in icons", - "path": "./admin/templates/extending-templates/icons.md" - }, - { - "title": "Resource Metadata", - "description": "Display resource state in the workspace dashboard", - "path": "./admin/templates/extending-templates/resource-metadata.md" - }, - { - "title": "Resource Monitoring", - "description": "Monitor resources in the workspace dashboard", - "path": "./admin/templates/extending-templates/resource-monitoring.md" - }, - { - "title": "Resource Ordering", - "description": "Design the UI of workspaces", - "path": "./admin/templates/extending-templates/resource-ordering.md" - }, - { - "title": "Resource Persistence", - "description": "Control resource persistence", - "path": "./admin/templates/extending-templates/resource-persistence.md" - }, - { - "title": "Terraform Variables", - "description": "Use variables to manage template state", - "path": "./admin/templates/extending-templates/variables.md" - }, - { - "title": "Terraform Modules", - "description": "Reuse terraform code across templates", - "path": "./admin/templates/extending-templates/modules.md" - }, - { - "title": "Web IDEs and Coder Apps", - "description": "Add and configure Web IDEs in your templates as coder apps", - "path": "./admin/templates/extending-templates/web-ides.md" - }, - { - "title": "Pre-install JetBrains Gateway", - "description": "Pre-install JetBrains Gateway in a template for faster IDE startup", - "path": "./admin/templates/extending-templates/jetbrains-gateway.md" - }, - { - "title": "Docker in Workspaces", - "description": "Use Docker in your workspaces", - "path": "./admin/templates/extending-templates/docker-in-workspaces.md" - }, - { - "title": "Workspace Tags", - "description": "Control provisioning using Workspace Tags and Parameters", - "path": "./admin/templates/extending-templates/workspace-tags.md" - }, - { - "title": "Provider Authentication", - "description": "Authenticate with provider APIs to provision workspaces", - "path": "./admin/templates/extending-templates/provider-authentication.md" - }, - { - "title": "Process Logging", - "description": "Log workspace processes", - "path": "./admin/templates/extending-templates/process-logging.md", - "state": [ - "premium" - ] - } - ] - }, - { - "title": "Open in Coder", - "description": "Open workspaces in Coder", - "path": "./admin/templates/open-in-coder.md" - }, - { - "title": "Permissions \u0026 Policies", - "description": "Learn how to create templates with Terraform", - "path": "./admin/templates/template-permissions.md", - "state": [ - "premium" - ] - }, - { - "title": "Troubleshooting Templates", - "description": "Learn how to troubleshoot template issues", - "path": "./admin/templates/troubleshooting.md" - } - ] - }, - { - "title": "External Provisioners", - "description": "Learn how to run external provisioners with Coder", - "path": "./admin/provisioners/index.md", - "icon_path": "./images/icons/key.svg", - "state": [ - "premium" - ], - "children": [ - { - "title": "Manage Provisioner Jobs", - "description": "Learn how to run external provisioners with Coder", - "path": "./admin/provisioners/manage-provisioner-jobs.md", - "state": [ - "premium" - ] - } - ] - }, - { - "title": "External Auth", - "description": "Learn how to configure external authentication", - "path": "./admin/external-auth.md", - "icon_path": "./images/icons/plug.svg" - }, - { - "title": "Integrations", - "description": "Use integrations to extend Coder", - "path": "./admin/integrations/index.md", - "icon_path": "./images/icons/puzzle.svg", - "children": [ - { - "title": "Prometheus", - "description": "Collect deployment metrics with Prometheus", - "path": "./admin/integrations/prometheus.md" - }, - { - "title": "Kubernetes Logging", - "description": "Stream K8s event logs on workspace startup", - "path": "./admin/integrations/kubernetes-logs.md" - }, - { - "title": "Additional Kubernetes Clusters", - "description": "Deploy workspaces on additional Kubernetes clusters", - "path": "./admin/integrations/multiple-kube-clusters.md" - }, - { - "title": "JFrog Artifactory", - "description": "Integrate Coder with JFrog Artifactory", - "path": "./admin/integrations/jfrog-artifactory.md" - }, - { - "title": "JFrog Xray", - "description": "Integrate Coder with JFrog Xray", - "path": "./admin/integrations/jfrog-xray.md" - }, - { - "title": "Island Secure Browser", - "description": "Integrate Coder with Island's Secure Browser", - "path": "./admin/integrations/island.md" - }, - { - "title": "DX PlatformX", - "description": "Integrate Coder with DX PlatformX", - "path": "./admin/integrations/platformx.md" - }, - { - "title": "Hashicorp Vault", - "description": "Integrate Coder with Hashicorp Vault", - "path": "./admin/integrations/vault.md" - } - ] - }, - { - "title": "Networking", - "description": "Understand Coder's networking layer", - "path": "./admin/networking/index.md", - "icon_path": "./images/icons/networking.svg", - "children": [ - { - "title": "Port Forwarding", - "description": "Learn how to forward ports in Coder", - "path": "./admin/networking/port-forwarding.md" - }, - { - "title": "STUN and NAT", - "description": "Learn how to forward ports in Coder", - "path": "./admin/networking/stun.md" - }, - { - "title": "Workspace Proxies", - "description": "Run geo distributed workspace proxies", - "path": "./admin/networking/workspace-proxies.md", - "state": [ - "premium" - ] - }, - { - "title": "High Availability", - "description": "Learn how to configure Coder for High Availability", - "path": "./admin/networking/high-availability.md", - "state": [ - "premium" - ] - }, - { - "title": "Troubleshooting", - "description": "Troubleshoot networking issues in Coder", - "path": "./admin/networking/troubleshooting.md" - } - ] - }, - { - "title": "Monitoring", - "description": "Configure security policy and audit your deployment", - "path": "./admin/monitoring/index.md", - "icon_path": "./images/icons/speed.svg", - "children": [ - { - "title": "Logs", - "description": "Learn about Coder's logs", - "path": "./admin/monitoring/logs.md" - }, - { - "title": "Metrics", - "description": "Learn about Coder's logs", - "path": "./admin/monitoring/metrics.md" - }, - { - "title": "Health Check", - "description": "Learn about Coder's automated health checks", - "path": "./admin/monitoring/health-check.md" - }, - { - "title": "Notifications", - "description": "Configure notifications for your deployment", - "path": "./admin/monitoring/notifications/index.md", - "children": [ - { - "title": "Slack Notifications", - "description": "Learn how to setup Slack notifications", - "path": "./admin/monitoring/notifications/slack.md" - }, - { - "title": "Microsoft Teams Notifications", - "description": "Learn how to setup Microsoft Teams notifications", - "path": "./admin/monitoring/notifications/teams.md" - } - ] - } - ] - }, - { - "title": "Security", - "description": "Configure security policy and audit your deployment", - "path": "./admin/security/index.md", - "icon_path": "./images/icons/lock.svg", - "children": [ - { - "title": "Audit Logs", - "description": "Audit actions taken inside Coder", - "path": "./admin/security/audit-logs.md", - "state": [ - "premium" - ] - }, - { - "title": "Secrets", - "description": "Use sensitive variables in your workspaces", - "path": "./admin/security/secrets.md" - }, - { - "title": "Database Encryption", - "description": "Encrypt the database to prevent unauthorized access", - "path": "./admin/security/database-encryption.md", - "state": [ - "premium" - ] - } - ] - }, - { - "title": "Licensing", - "description": "Configure licensing for your deployment", - "path": "./admin/licensing/index.md", - "icon_path": "./images/icons/licensing.svg" - } - ] - }, - { - "title": "Run AI Coding Agents in Coder", - "description": "Learn how to run and integrate AI coding agents like GPT-Code, OpenDevin, or SWE-Agent in Coder workspaces to boost developer productivity.", - "path": "./ai-coder/index.md", - "icon_path": "./images/icons/wand.svg", - "state": [ - "early access" - ], - "children": [ - { - "title": "Learn about coding agents", - "description": "Learn about the different AI agents and their tradeoffs", - "path": "./ai-coder/agents.md" - }, - { - "title": "Create a Coder template for agents", - "description": "Create a purpose-built template for your AI agents", - "path": "./ai-coder/create-template.md", - "state": [ - "early access" - ] - }, - { - "title": "Integrate with your issue tracker", - "description": "Assign tickets to AI agents and interact via code reviews", - "path": "./ai-coder/issue-tracker.md", - "state": [ - "early access" - ] - }, - { - "title": "Model Context Protocols (MCP) and adding AI tools", - "description": "Improve results by adding tools to your AI agents", - "path": "./ai-coder/best-practices.md", - "state": [ - "early access" - ] - }, - { - "title": "Supervise agents via Coder UI", - "description": "Interact with agents via the Coder UI", - "path": "./ai-coder/coder-dashboard.md", - "state": [ - "early access" - ] - }, - { - "title": "Supervise agents via the IDE", - "description": "Interact with agents via VS Code or Cursor", - "path": "./ai-coder/ide-integration.md", - "state": [ - "early access" - ] - }, - { - "title": "Programmatically manage agents", - "description": "Manage agents via MCP, the Coder CLI, and/or REST API", - "path": "./ai-coder/headless.md", - "state": [ - "early access" - ] - }, - { - "title": "Securing agents in Coder", - "description": "Learn how to secure agents with boundaries", - "path": "./ai-coder/securing.md", - "state": [ - "early access" - ] - }, - { - "title": "Custom agents", - "description": "Learn how to use custom agents with Coder", - "path": "./ai-coder/custom-agents.md", - "state": [ - "early access" - ] - } - ] - }, - { - "title": "Contributing", - "description": "Learn how to contribute to Coder", - "path": "./CONTRIBUTING.md", - "icon_path": "./images/icons/contributing.svg", - "children": [ - { - "title": "Code of Conduct", - "description": "See the code of conduct for contributing to Coder", - "path": "./contributing/CODE_OF_CONDUCT.md", - "icon_path": "./images/icons/circle-dot.svg" - }, - { - "title": "Documentation", - "description": "Our style guide for use when authoring documentation", - "path": "./contributing/documentation.md", - "icon_path": "./images/icons/document.svg" - }, - { - "title": "Frontend", - "description": "Our guide for frontend development", - "path": "./contributing/frontend.md", - "icon_path": "./images/icons/frontend.svg" - }, - { - "title": "Security", - "description": "Our guide for security", - "path": "./contributing/SECURITY.md", - "icon_path": "./images/icons/lock.svg" - } - ] - }, - { - "title": "Tutorials", - "description": "Coder knowledgebase for administrating your deployment", - "path": "./tutorials/index.md", - "icon_path": "./images/icons/generic.svg", - "children": [ - { - "title": "Quickstart", - "description": "Learn how to install and run Coder quickly", - "path": "./tutorials/quickstart.md" - }, - { - "title": "Write a Template from Scratch", - "description": "Learn how to author Coder templates", - "path": "./tutorials/template-from-scratch.md" - }, - { - "title": "Using an External Database", - "description": "Use Coder with an external database", - "path": "./tutorials/external-database.md" - }, - { - "title": "Image Management", - "description": "Learn about image management with Coder", - "path": "./admin/templates/managing-templates/image-management.md" - }, - { - "title": "Generate a Support Bundle", - "description": "Generate and upload a Support Bundle to Coder Support", - "path": "./tutorials/support-bundle.md" - }, - { - "title": "Configuring Okta", - "description": "Custom claims/scopes with Okta for group/role sync", - "path": "./tutorials/configuring-okta.md" - }, - { - "title": "Google to AWS Federation", - "description": "Federating a Google Cloud service account to AWS", - "path": "./tutorials/gcp-to-aws.md" - }, - { - "title": "JFrog Artifactory Integration", - "description": "Integrate Coder with JFrog Artifactory", - "path": "./admin/integrations/jfrog-artifactory.md" - }, - { - "title": "Istio Integration", - "description": "Integrate Coder with Istio", - "path": "./admin/integrations/istio.md" - }, - { - "title": "Island Secure Browser Integration", - "description": "Integrate Coder with Island's Secure Browser", - "path": "./admin/integrations/island.md" - }, - { - "title": "Template ImagePullSecrets", - "description": "Creating ImagePullSecrets for private registries", - "path": "./tutorials/image-pull-secret.md" - }, - { - "title": "Postgres SSL", - "description": "Configure Coder to connect to Postgres over SSL", - "path": "./tutorials/postgres-ssl.md" - }, - { - "title": "Azure Federation", - "description": "Federating Coder to Azure", - "path": "./tutorials/azure-federation.md" - }, - { - "title": "Scanning Workspaces with JFrog Xray", - "description": "Integrate Coder with JFrog Xray", - "path": "./admin/integrations/jfrog-xray.md" - }, - { - "title": "Cloning Git Repositories", - "description": "Learn how to clone Git repositories in Coder", - "path": "./tutorials/cloning-git-repositories.md" - }, - { - "title": "Test Templates Through CI/CD", - "description": "Learn how to test and publish Coder templates in a CI/CD pipeline", - "path": "./tutorials/testing-templates.md" - }, - { - "title": "Use Apache as a Reverse Proxy", - "description": "Learn how to use Apache as a reverse proxy", - "path": "./tutorials/reverse-proxy-apache.md" - }, - { - "title": "Use Caddy as a Reverse Proxy", - "description": "Learn how to use Caddy as a reverse proxy", - "path": "./tutorials/reverse-proxy-caddy.md" - }, - { - "title": "Use NGINX as a Reverse Proxy", - "description": "Learn how to use NGINX as a reverse proxy", - "path": "./tutorials/reverse-proxy-nginx.md" - }, - { - "title": "FAQs", - "description": "Miscellaneous FAQs from our community", - "path": "./tutorials/faqs.md" - }, - { - "title": "Best practices", - "description": "Guides to help you make the most of your Coder experience", - "path": "./tutorials/best-practices/index.md", - "children": [ - { - "title": "Organizations - best practices", - "description": "How to make the best use of Coder Organizations", - "path": "./tutorials/best-practices/organizations.md" - }, - { - "title": "Scale Coder", - "description": "How to prepare a Coder deployment for scale", - "path": "./tutorials/best-practices/scale-coder.md" - }, - { - "title": "Security - best practices", - "description": "Make your Coder deployment more secure", - "path": "./tutorials/best-practices/security-best-practices.md" - }, - { - "title": "Speed up your workspaces", - "description": "Speed up your Coder templates and workspaces", - "path": "./tutorials/best-practices/speed-up-templates.md" - } - ] - } - ] - }, - { - "title": "Reference", - "description": "Reference", - "path": "./reference/index.md", - "icon_path": "./images/icons/notes.svg", - "children": [ - { - "title": "REST API", - "description": "Learn how to use Coderd API", - "path": "./reference/api/index.md", - "icon_path": "./images/icons/api.svg", - "children": [ - { - "title": "General", - "path": "./reference/api/general.md" - }, - { - "title": "Agents", - "path": "./reference/api/agents.md" - }, - { - "title": "Applications", - "path": "./reference/api/applications.md" - }, - { - "title": "Audit", - "path": "./reference/api/audit.md" - }, - { - "title": "Authentication", - "path": "./reference/api/authentication.md" - }, - { - "title": "Authorization", - "path": "./reference/api/authorization.md" - }, - { - "title": "Builds", - "path": "./reference/api/builds.md" - }, - { - "title": "Debug", - "path": "./reference/api/debug.md" - }, - { - "title": "Enterprise", - "path": "./reference/api/enterprise.md" - }, - { - "title": "Files", - "path": "./reference/api/files.md" - }, - { - "title": "Git", - "path": "./reference/api/git.md" - }, - { - "title": "Insights", - "path": "./reference/api/insights.md" - }, - { - "title": "Members", - "path": "./reference/api/members.md" - }, - { - "title": "Organizations", - "path": "./reference/api/organizations.md" - }, - { - "title": "PortSharing", - "path": "./reference/api/portsharing.md" - }, - { - "title": "Schemas", - "path": "./reference/api/schemas.md" - }, - { - "title": "Templates", - "path": "./reference/api/templates.md" - }, - { - "title": "Users", - "path": "./reference/api/users.md" - }, - { - "title": "WorkspaceProxies", - "path": "./reference/api/workspaceproxies.md" - }, - { - "title": "Workspaces", - "path": "./reference/api/workspaces.md" - } - ] - }, - { - "title": "Command Line", - "description": "Learn how to use Coder CLI", - "path": "./reference/cli/index.md", - "icon_path": "./images/icons/terminal.svg", - "children": [ - { - "title": "autoupdate", - "description": "Toggle auto-update policy for a workspace", - "path": "reference/cli/autoupdate.md" - }, - { - "title": "coder", - "path": "reference/cli/index.md" - }, - { - "title": "completion", - "description": "Install or update shell completion scripts for the detected or chosen shell.", - "path": "reference/cli/completion.md" - }, - { - "title": "config-ssh", - "description": "Add an SSH Host entry for your workspaces \"ssh coder.workspace\"", - "path": "reference/cli/config-ssh.md" - }, - { - "title": "create", - "description": "Create a workspace", - "path": "reference/cli/create.md" - }, - { - "title": "delete", - "description": "Delete a workspace", - "path": "reference/cli/delete.md" - }, - { - "title": "dotfiles", - "description": "Personalize your workspace by applying a canonical dotfiles repository", - "path": "reference/cli/dotfiles.md" - }, - { - "title": "external-auth", - "description": "Manage external authentication", - "path": "reference/cli/external-auth.md" - }, - { - "title": "external-auth access-token", - "description": "Print auth for an external provider", - "path": "reference/cli/external-auth_access-token.md" - }, - { - "title": "favorite", - "description": "Add a workspace to your favorites", - "path": "reference/cli/favorite.md" - }, - { - "title": "features", - "description": "List Enterprise features", - "path": "reference/cli/features.md" - }, - { - "title": "features list", - "path": "reference/cli/features_list.md" - }, - { - "title": "groups", - "description": "Manage groups", - "path": "reference/cli/groups.md" - }, - { - "title": "groups create", - "description": "Create a user group", - "path": "reference/cli/groups_create.md" - }, - { - "title": "groups delete", - "description": "Delete a user group", - "path": "reference/cli/groups_delete.md" - }, - { - "title": "groups edit", - "description": "Edit a user group", - "path": "reference/cli/groups_edit.md" - }, - { - "title": "groups list", - "description": "List user groups", - "path": "reference/cli/groups_list.md" - }, - { - "title": "licenses", - "description": "Add, delete, and list licenses", - "path": "reference/cli/licenses.md" - }, - { - "title": "licenses add", - "description": "Add license to Coder deployment", - "path": "reference/cli/licenses_add.md" - }, - { - "title": "licenses delete", - "description": "Delete license by ID", - "path": "reference/cli/licenses_delete.md" - }, - { - "title": "licenses list", - "description": "List licenses (including expired)", - "path": "reference/cli/licenses_list.md" - }, - { - "title": "list", - "description": "List workspaces", - "path": "reference/cli/list.md" - }, - { - "title": "login", - "description": "Authenticate with Coder deployment", - "path": "reference/cli/login.md" - }, - { - "title": "logout", - "description": "Unauthenticate your local session", - "path": "reference/cli/logout.md" - }, - { - "title": "netcheck", - "description": "Print network debug information for DERP and STUN", - "path": "reference/cli/netcheck.md" - }, - { - "title": "notifications", - "description": "Manage Coder notifications", - "path": "reference/cli/notifications.md" - }, - { - "title": "notifications pause", - "description": "Pause notifications", - "path": "reference/cli/notifications_pause.md" - }, - { - "title": "notifications resume", - "description": "Resume notifications", - "path": "reference/cli/notifications_resume.md" - }, - { - "title": "notifications test", - "description": "Send a test notification", - "path": "reference/cli/notifications_test.md" - }, - { - "title": "open", - "description": "Open a workspace", - "path": "reference/cli/open.md" - }, - { - "title": "open app", - "description": "Open a workspace application.", - "path": "reference/cli/open_app.md" - }, - { - "title": "open vscode", - "description": "Open a workspace in VS Code Desktop", - "path": "reference/cli/open_vscode.md" - }, - { - "title": "organizations", - "description": "Organization related commands", - "path": "reference/cli/organizations.md" - }, - { - "title": "organizations create", - "description": "Create a new organization.", - "path": "reference/cli/organizations_create.md" - }, - { - "title": "organizations members", - "description": "Manage organization members", - "path": "reference/cli/organizations_members.md" - }, - { - "title": "organizations members add", - "description": "Add a new member to the current organization", - "path": "reference/cli/organizations_members_add.md" - }, - { - "title": "organizations members edit-roles", - "description": "Edit organization member's roles", - "path": "reference/cli/organizations_members_edit-roles.md" - }, - { - "title": "organizations members list", - "description": "List all organization members", - "path": "reference/cli/organizations_members_list.md" - }, - { - "title": "organizations members remove", - "description": "Remove a new member to the current organization", - "path": "reference/cli/organizations_members_remove.md" - }, - { - "title": "organizations roles", - "description": "Manage organization roles.", - "path": "reference/cli/organizations_roles.md" - }, - { - "title": "organizations roles create", - "description": "Create a new organization custom role", - "path": "reference/cli/organizations_roles_create.md" - }, - { - "title": "organizations roles show", - "description": "Show role(s)", - "path": "reference/cli/organizations_roles_show.md" - }, - { - "title": "organizations roles update", - "description": "Update an organization custom role", - "path": "reference/cli/organizations_roles_update.md" - }, - { - "title": "organizations settings", - "description": "Manage organization settings.", - "path": "reference/cli/organizations_settings.md" - }, - { - "title": "organizations settings set", - "description": "Update specified organization setting.", - "path": "reference/cli/organizations_settings_set.md" - }, - { - "title": "organizations settings set group-sync", - "description": "Group sync settings to sync groups from an IdP.", - "path": "reference/cli/organizations_settings_set_group-sync.md" - }, - { - "title": "organizations settings set organization-sync", - "description": "Organization sync settings to sync organization memberships from an IdP.", - "path": "reference/cli/organizations_settings_set_organization-sync.md" - }, - { - "title": "organizations settings set role-sync", - "description": "Role sync settings to sync organization roles from an IdP.", - "path": "reference/cli/organizations_settings_set_role-sync.md" - }, - { - "title": "organizations settings show", - "description": "Outputs specified organization setting.", - "path": "reference/cli/organizations_settings_show.md" - }, - { - "title": "organizations settings show group-sync", - "description": "Group sync settings to sync groups from an IdP.", - "path": "reference/cli/organizations_settings_show_group-sync.md" - }, - { - "title": "organizations settings show organization-sync", - "description": "Organization sync settings to sync organization memberships from an IdP.", - "path": "reference/cli/organizations_settings_show_organization-sync.md" - }, - { - "title": "organizations settings show role-sync", - "description": "Role sync settings to sync organization roles from an IdP.", - "path": "reference/cli/organizations_settings_show_role-sync.md" - }, - { - "title": "organizations show", - "description": "Show the organization. Using \"selected\" will show the selected organization from the \"--org\" flag. Using \"me\" will show all organizations you are a member of.", - "path": "reference/cli/organizations_show.md" - }, - { - "title": "ping", - "description": "Ping a workspace", - "path": "reference/cli/ping.md" - }, - { - "title": "port-forward", - "description": "Forward ports from a workspace to the local machine. For reverse port forwarding, use \"coder ssh -R\".", - "path": "reference/cli/port-forward.md" - }, - { - "title": "provisioner", - "description": "View and manage provisioner daemons and jobs", - "path": "reference/cli/provisioner.md" - }, - { - "title": "provisioner jobs", - "description": "View and manage provisioner jobs", - "path": "reference/cli/provisioner_jobs.md" - }, - { - "title": "provisioner jobs cancel", - "description": "Cancel a provisioner job", - "path": "reference/cli/provisioner_jobs_cancel.md" - }, - { - "title": "provisioner jobs list", - "description": "List provisioner jobs", - "path": "reference/cli/provisioner_jobs_list.md" - }, - { - "title": "provisioner keys", - "description": "Manage provisioner keys", - "path": "reference/cli/provisioner_keys.md" - }, - { - "title": "provisioner keys create", - "description": "Create a new provisioner key", - "path": "reference/cli/provisioner_keys_create.md" - }, - { - "title": "provisioner keys delete", - "description": "Delete a provisioner key", - "path": "reference/cli/provisioner_keys_delete.md" - }, - { - "title": "provisioner keys list", - "description": "List provisioner keys in an organization", - "path": "reference/cli/provisioner_keys_list.md" - }, - { - "title": "provisioner list", - "description": "List provisioner daemons in an organization", - "path": "reference/cli/provisioner_list.md" - }, - { - "title": "provisioner start", - "description": "Run a provisioner daemon", - "path": "reference/cli/provisioner_start.md" - }, - { - "title": "publickey", - "description": "Output your Coder public key used for Git operations", - "path": "reference/cli/publickey.md" - }, - { - "title": "rename", - "description": "Rename a workspace", - "path": "reference/cli/rename.md" - }, - { - "title": "reset-password", - "description": "Directly connect to the database to reset a user's password", - "path": "reference/cli/reset-password.md" - }, - { - "title": "restart", - "description": "Restart a workspace", - "path": "reference/cli/restart.md" - }, - { - "title": "schedule", - "description": "Schedule automated start and stop times for workspaces", - "path": "reference/cli/schedule.md" - }, - { - "title": "schedule extend", - "description": "Extend the stop time of a currently running workspace instance.", - "path": "reference/cli/schedule_extend.md" - }, - { - "title": "schedule show", - "description": "Show workspace schedules", - "path": "reference/cli/schedule_show.md" - }, - { - "title": "schedule start", - "description": "Edit workspace start schedule", - "path": "reference/cli/schedule_start.md" - }, - { - "title": "schedule stop", - "description": "Edit workspace stop schedule", - "path": "reference/cli/schedule_stop.md" - }, - { - "title": "server", - "description": "Start a Coder server", - "path": "reference/cli/server.md" - }, - { - "title": "server create-admin-user", - "description": "Create a new admin user with the given username, email and password and adds it to every organization.", - "path": "reference/cli/server_create-admin-user.md" - }, - { - "title": "server dbcrypt", - "description": "Manage database encryption.", - "path": "reference/cli/server_dbcrypt.md" - }, - { - "title": "server dbcrypt decrypt", - "description": "Decrypt a previously encrypted database.", - "path": "reference/cli/server_dbcrypt_decrypt.md" - }, - { - "title": "server dbcrypt delete", - "description": "Delete all encrypted data from the database. THIS IS A DESTRUCTIVE OPERATION.", - "path": "reference/cli/server_dbcrypt_delete.md" - }, - { - "title": "server dbcrypt rotate", - "description": "Rotate database encryption keys.", - "path": "reference/cli/server_dbcrypt_rotate.md" - }, - { - "title": "server postgres-builtin-serve", - "description": "Run the built-in PostgreSQL deployment.", - "path": "reference/cli/server_postgres-builtin-serve.md" - }, - { - "title": "server postgres-builtin-url", - "description": "Output the connection URL for the built-in PostgreSQL deployment.", - "path": "reference/cli/server_postgres-builtin-url.md" - }, - { - "title": "show", - "description": "Display details of a workspace's resources and agents", - "path": "reference/cli/show.md" - }, - { - "title": "speedtest", - "description": "Run upload and download tests from your machine to a workspace", - "path": "reference/cli/speedtest.md" - }, - { - "title": "ssh", - "description": "Start a shell into a workspace", - "path": "reference/cli/ssh.md" - }, - { - "title": "start", - "description": "Start a workspace", - "path": "reference/cli/start.md" - }, - { - "title": "stat", - "description": "Show resource usage for the current workspace.", - "path": "reference/cli/stat.md" - }, - { - "title": "stat cpu", - "description": "Show CPU usage, in cores.", - "path": "reference/cli/stat_cpu.md" - }, - { - "title": "stat disk", - "description": "Show disk usage, in gigabytes.", - "path": "reference/cli/stat_disk.md" - }, - { - "title": "stat mem", - "description": "Show memory usage, in gigabytes.", - "path": "reference/cli/stat_mem.md" - }, - { - "title": "state", - "description": "Manually manage Terraform state to fix broken workspaces", - "path": "reference/cli/state.md" - }, - { - "title": "state pull", - "description": "Pull a Terraform state file from a workspace.", - "path": "reference/cli/state_pull.md" - }, - { - "title": "state push", - "description": "Push a Terraform state file to a workspace.", - "path": "reference/cli/state_push.md" - }, - { - "title": "stop", - "description": "Stop a workspace", - "path": "reference/cli/stop.md" - }, - { - "title": "support", - "description": "Commands for troubleshooting issues with a Coder deployment.", - "path": "reference/cli/support.md" - }, - { - "title": "support bundle", - "description": "Generate a support bundle to troubleshoot issues connecting to a workspace.", - "path": "reference/cli/support_bundle.md" - }, - { - "title": "templates", - "description": "Manage templates", - "path": "reference/cli/templates.md" - }, - { - "title": "templates archive", - "description": "Archive unused or failed template versions from a given template(s)", - "path": "reference/cli/templates_archive.md" - }, - { - "title": "templates create", - "description": "DEPRECATED: Create a template from the current directory or as specified by flag", - "path": "reference/cli/templates_create.md" - }, - { - "title": "templates delete", - "description": "Delete templates", - "path": "reference/cli/templates_delete.md" - }, - { - "title": "templates edit", - "description": "Edit the metadata of a template by name.", - "path": "reference/cli/templates_edit.md" - }, - { - "title": "templates init", - "description": "Get started with a templated template.", - "path": "reference/cli/templates_init.md" - }, - { - "title": "templates list", - "description": "List all the templates available for the organization", - "path": "reference/cli/templates_list.md" - }, - { - "title": "templates pull", - "description": "Download the active, latest, or specified version of a template to a path.", - "path": "reference/cli/templates_pull.md" - }, - { - "title": "templates push", - "description": "Create or update a template from the current directory or as specified by flag", - "path": "reference/cli/templates_push.md" - }, - { - "title": "templates versions", - "description": "Manage different versions of the specified template", - "path": "reference/cli/templates_versions.md" - }, - { - "title": "templates versions archive", - "description": "Archive a template version(s).", - "path": "reference/cli/templates_versions_archive.md" - }, - { - "title": "templates versions list", - "description": "List all the versions of the specified template", - "path": "reference/cli/templates_versions_list.md" - }, - { - "title": "templates versions promote", - "description": "Promote a template version to active.", - "path": "reference/cli/templates_versions_promote.md" - }, - { - "title": "templates versions unarchive", - "description": "Unarchive a template version(s).", - "path": "reference/cli/templates_versions_unarchive.md" - }, - { - "title": "tokens", - "description": "Manage personal access tokens", - "path": "reference/cli/tokens.md" - }, - { - "title": "tokens create", - "description": "Create a token", - "path": "reference/cli/tokens_create.md" - }, - { - "title": "tokens list", - "description": "List tokens", - "path": "reference/cli/tokens_list.md" - }, - { - "title": "tokens remove", - "description": "Delete a token", - "path": "reference/cli/tokens_remove.md" - }, - { - "title": "unfavorite", - "description": "Remove a workspace from your favorites", - "path": "reference/cli/unfavorite.md" - }, - { - "title": "update", - "description": "Will update and start a given workspace if it is out of date", - "path": "reference/cli/update.md" - }, - { - "title": "users", - "description": "Manage users", - "path": "reference/cli/users.md" - }, - { - "title": "users activate", - "description": "Update a user's status to 'active'. Active users can fully interact with the platform", - "path": "reference/cli/users_activate.md" - }, - { - "title": "users create", - "path": "reference/cli/users_create.md" - }, - { - "title": "users delete", - "description": "Delete a user by username or user_id.", - "path": "reference/cli/users_delete.md" - }, - { - "title": "users edit-roles", - "description": "Edit a user's roles by username or id", - "path": "reference/cli/users_edit-roles.md" - }, - { - "title": "users list", - "path": "reference/cli/users_list.md" - }, - { - "title": "users show", - "description": "Show a single user. Use 'me' to indicate the currently authenticated user.", - "path": "reference/cli/users_show.md" - }, - { - "title": "users suspend", - "description": "Update a user's status to 'suspended'. A suspended user cannot log into the platform", - "path": "reference/cli/users_suspend.md" - }, - { - "title": "version", - "description": "Show coder version", - "path": "reference/cli/version.md" - }, - { - "title": "whoami", - "description": "Fetch authenticated user info for Coder deployment", - "path": "reference/cli/whoami.md" - } - ] - }, - { - "title": "Agent API", - "description": "Learn how to use Coder Agent API", - "path": "./reference/agent-api/index.md", - "icon_path": "./images/icons/api.svg", - "children": [ - { - "title": "Debug", - "path": "./reference/agent-api/debug.md" - }, - { - "title": "Schemas", - "path": "./reference/agent-api/schemas.md" - } - ] - } - ] - } - ] -} \ No newline at end of file + "versions": ["main"], + "routes": [ + { + "title": "About", + "description": "Coder docs", + "path": "./README.md", + "icon_path": "./images/icons/home.svg", + "children": [ + { + "title": "Quickstart", + "description": "Learn how to install and run Coder quickly", + "path": "./tutorials/quickstart.md" + }, + { + "title": "Screenshots", + "description": "View screenshots of the Coder platform", + "path": "./start/screenshots.md" + } + ] + }, + { + "title": "Install", + "description": "Installing Coder", + "path": "./install/index.md", + "icon_path": "./images/icons/download.svg", + "children": [ + { + "title": "Coder CLI", + "description": "Install the standalone binary", + "path": "./install/cli.md", + "icon_path": "./images/icons/terminal.svg" + }, + { + "title": "Docker", + "description": "Install Coder using Docker", + "path": "./install/docker.md", + "icon_path": "./images/icons/docker.svg" + }, + { + "title": "Kubernetes", + "description": "Install Coder on Kubernetes", + "path": "./install/kubernetes.md", + "icon_path": "./images/icons/kubernetes.svg" + }, + { + "title": "Rancher", + "description": "Deploy Coder on Rancher", + "path": "./install/rancher.md", + "icon_path": "./images/icons/rancher.svg" + }, + { + "title": "OpenShift", + "description": "Install Coder on OpenShift", + "path": "./install/openshift.md", + "icon_path": "./images/icons/openshift.svg" + }, + { + "title": "Cloud Providers", + "description": "Install Coder on cloud providers", + "path": "./install/cloud/index.md", + "icon_path": "./images/icons/cloud.svg", + "children": [ + { + "title": "AWS EC2", + "description": "Install Coder on AWS EC2", + "path": "./install/cloud/ec2.md" + }, + { + "title": "GCP Compute Engine", + "description": "Install Coder on GCP Compute Engine", + "path": "./install/cloud/compute-engine.md" + }, + { + "title": "Azure VM", + "description": "Install Coder on an Azure VM", + "path": "./install/cloud/azure-vm.md" + } + ] + }, + { + "title": "Offline Deployments", + "description": "Run Coder in offline / air-gapped environments", + "path": "./install/offline.md", + "icon_path": "./images/icons/lan.svg" + }, + { + "title": "Unofficial Install Methods", + "description": "Other installation methods", + "path": "./install/other/index.md", + "icon_path": "./images/icons/generic.svg" + }, + { + "title": "Upgrading", + "description": "Learn how to upgrade Coder", + "path": "./install/upgrade.md", + "icon_path": "./images/icons/upgrade.svg" + }, + { + "title": "Uninstall", + "description": "Learn how to uninstall Coder", + "path": "./install/uninstall.md", + "icon_path": "./images/icons/trash.svg" + }, + { + "title": "Releases", + "description": "Learn about the Coder release channels and schedule", + "path": "./install/releases/index.md", + "icon_path": "./images/icons/star.svg", + "children": [ + { + "title": "Feature stages", + "description": "Information about pre-GA stages.", + "path": "./install/releases/feature-stages.md" + } + ] + } + ] + }, + { + "title": "User Guides", + "description": "Guides for end-users of Coder", + "path": "./user-guides/index.md", + "icon_path": "./images/icons/users.svg", + "children": [ + { + "title": "Access Workspaces", + "description": "Connect to your Coder workspaces", + "path": "./user-guides/workspace-access/index.md", + "icon_path": "./images/icons/access.svg", + "children": [ + { + "title": "Visual Studio Code", + "description": "Use VSCode with Coder in the desktop or browser", + "path": "./user-guides/workspace-access/vscode.md" + }, + { + "title": "JetBrains IDEs", + "description": "Use JetBrains IDEs with Gateway", + "path": "./user-guides/workspace-access/jetbrains/index.md", + "children": [ + { + "title": "JetBrains Gateway in an air-gapped environment", + "description": "Use JetBrains Gateway in an air-gapped offline environment", + "path": "./user-guides/workspace-access/jetbrains/jetbrains-airgapped.md" + } + ] + }, + { + "title": "Remote Desktop", + "description": "Use RDP in Coder", + "path": "./user-guides/workspace-access/remote-desktops.md" + }, + { + "title": "Emacs TRAMP", + "description": "Use Emacs TRAMP in Coder", + "path": "./user-guides/workspace-access/emacs-tramp.md" + }, + { + "title": "Port Forwarding", + "description": "Access ports on your workspace", + "path": "./user-guides/workspace-access/port-forwarding.md" + }, + { + "title": "Filebrowser", + "description": "Access your workspace files", + "path": "./user-guides/workspace-access/filebrowser.md" + }, + { + "title": "Web IDEs and Coder Apps", + "description": "Access your workspace with IDEs in the browser", + "path": "./user-guides/workspace-access/web-ides.md" + }, + { + "title": "Zed", + "description": "Access your workspace with Zed", + "path": "./user-guides/workspace-access/zed.md" + }, + { + "title": "Cursor", + "description": "Access your workspace with Cursor", + "path": "./user-guides/workspace-access/cursor.md" + }, + { + "title": "Windsurf", + "description": "Access your workspace with Windsurf", + "path": "./user-guides/workspace-access/windsurf.md" + } + ] + }, + { + "title": "Coder Desktop", + "description": "Use Coder Desktop to access your workspace like it's a local machine", + "path": "./user-guides/desktop/index.md", + "icon_path": "./images/icons/computer-code.svg", + "state": ["early access"] + }, + { + "title": "Workspace Management", + "description": "Manage workspaces", + "path": "./user-guides/workspace-management.md", + "icon_path": "./images/icons/generic.svg" + }, + { + "title": "Workspace Scheduling", + "description": "Cost control with workspace schedules", + "path": "./user-guides/workspace-scheduling.md", + "icon_path": "./images/icons/stopwatch.svg" + }, + { + "title": "Workspace Lifecycle", + "description": "A guide to the workspace lifecycle, from creation and status through stopping and deletion.", + "path": "./user-guides/workspace-lifecycle.md", + "icon_path": "./images/icons/circle-dot.svg" + }, + { + "title": "Dotfiles", + "description": "Personalize your environment with dotfiles", + "path": "./user-guides/workspace-dotfiles.md", + "icon_path": "./images/icons/art-pad.svg" + } + ] + }, + { + "title": "Administration", + "description": "Guides for template and deployment administrators", + "path": "./admin/index.md", + "icon_path": "./images/icons/wrench.svg", + "children": [ + { + "title": "Setup", + "description": "Configure user access to your control plane.", + "path": "./admin/setup/index.md", + "icon_path": "./images/icons/toggle_on.svg", + "children": [ + { + "title": "Appearance", + "description": "Learn how to configure the appearance of Coder", + "path": "./admin/setup/appearance.md", + "state": ["premium"] + }, + { + "title": "Telemetry", + "description": "Learn what usage telemetry Coder collects", + "path": "./admin/setup/telemetry.md" + } + ] + }, + { + "title": "Infrastructure", + "description": "How to integrate Coder with your organization's compute", + "path": "./admin/infrastructure/index.md", + "icon_path": "./images/icons/container.svg", + "children": [ + { + "title": "Architecture", + "description": "Learn about Coder's architecture", + "path": "./admin/infrastructure/architecture.md" + }, + { + "title": "Validated Architectures", + "description": "Architectures for large Coder deployments", + "path": "./admin/infrastructure/validated-architectures/index.md", + "children": [ + { + "title": "Up to 1,000 Users", + "path": "./admin/infrastructure/validated-architectures/1k-users.md" + }, + { + "title": "Up to 2,000 Users", + "path": "./admin/infrastructure/validated-architectures/2k-users.md" + }, + { + "title": "Up to 3,000 Users", + "path": "./admin/infrastructure/validated-architectures/3k-users.md" + } + ] + }, + { + "title": "Scale Testing", + "description": "Ensure your deployment can handle your organization's needs", + "path": "./admin/infrastructure/scale-testing.md" + }, + { + "title": "Scaling Utilities", + "description": "Tools to help you scale your deployment", + "path": "./admin/infrastructure/scale-utility.md" + }, + { + "title": "Scaling best practices", + "description": "How to prepare a Coder deployment for scale", + "path": "./tutorials/best-practices/scale-coder.md" + } + ] + }, + { + "title": "Users", + "description": "Learn how to manage and audit users", + "path": "./admin/users/index.md", + "icon_path": "./images/icons/users.svg", + "children": [ + { + "title": "OIDC Authentication", + "path": "./admin/users/oidc-auth.md" + }, + { + "title": "GitHub Authentication", + "path": "./admin/users/github-auth.md" + }, + { + "title": "Password Authentication", + "path": "./admin/users/password-auth.md" + }, + { + "title": "Headless Authentication", + "path": "./admin/users/headless-auth.md" + }, + { + "title": "Groups \u0026 Roles", + "path": "./admin/users/groups-roles.md", + "state": ["premium"] + }, + { + "title": "IdP Sync", + "path": "./admin/users/idp-sync.md", + "state": ["premium"] + }, + { + "title": "Organizations", + "path": "./admin/users/organizations.md", + "state": ["premium"] + }, + { + "title": "Quotas", + "path": "./admin/users/quotas.md", + "state": ["premium"] + }, + { + "title": "Sessions \u0026 API Tokens", + "path": "./admin/users/sessions-tokens.md" + } + ] + }, + { + "title": "Templates", + "description": "Learn how to author and maintain Coder templates", + "path": "./admin/templates/index.md", + "icon_path": "./images/icons/picture.svg", + "children": [ + { + "title": "Creating Templates", + "description": "Learn how to create templates with Terraform", + "path": "./admin/templates/creating-templates.md" + }, + { + "title": "Managing Templates", + "description": "Learn how to manage templates and best practices", + "path": "./admin/templates/managing-templates/index.md", + "children": [ + { + "title": "Image Management", + "description": "Learn about template image management", + "path": "./admin/templates/managing-templates/image-management.md" + }, + { + "title": "Change Management", + "description": "Learn about template change management and versioning", + "path": "./admin/templates/managing-templates/change-management.md" + }, + { + "title": "Dev containers", + "description": "Learn about using development containers in templates", + "path": "./admin/templates/managing-templates/devcontainers/index.md", + "children": [ + { + "title": "Add a dev container template", + "description": "How to add a dev container template to Coder", + "path": "./admin/templates/managing-templates/devcontainers/add-devcontainer.md" + }, + { + "title": "Dev container security and caching", + "description": "Configure dev container authentication and caching", + "path": "./admin/templates/managing-templates/devcontainers/devcontainer-security-caching.md" + }, + { + "title": "Dev container releases and known issues", + "description": "Dev container releases and known issues", + "path": "./admin/templates/managing-templates/devcontainers/devcontainer-releases-known-issues.md" + } + ] + }, + { + "title": "Template Dependencies", + "description": "Learn how to manage template dependencies", + "path": "./admin/templates/managing-templates/dependencies.md" + }, + { + "title": "Workspace Scheduling", + "description": "Learn how to control how workspaces are started and stopped", + "path": "./admin/templates/managing-templates/schedule.md" + } + ] + }, + { + "title": "Extending Templates", + "description": "Learn best practices in extending templates", + "path": "./admin/templates/extending-templates/index.md", + "children": [ + { + "title": "Agent Metadata", + "description": "Retrieve real-time stats from the workspace agent", + "path": "./admin/templates/extending-templates/agent-metadata.md" + }, + { + "title": "Build Parameters", + "description": "Use parameters to customize workspaces at build", + "path": "./admin/templates/extending-templates/parameters.md" + }, + { + "title": "Icons", + "description": "Customize your template with built-in icons", + "path": "./admin/templates/extending-templates/icons.md" + }, + { + "title": "Resource Metadata", + "description": "Display resource state in the workspace dashboard", + "path": "./admin/templates/extending-templates/resource-metadata.md" + }, + { + "title": "Resource Monitoring", + "description": "Monitor resources in the workspace dashboard", + "path": "./admin/templates/extending-templates/resource-monitoring.md" + }, + { + "title": "Resource Ordering", + "description": "Design the UI of workspaces", + "path": "./admin/templates/extending-templates/resource-ordering.md" + }, + { + "title": "Resource Persistence", + "description": "Control resource persistence", + "path": "./admin/templates/extending-templates/resource-persistence.md" + }, + { + "title": "Terraform Variables", + "description": "Use variables to manage template state", + "path": "./admin/templates/extending-templates/variables.md" + }, + { + "title": "Terraform Modules", + "description": "Reuse terraform code across templates", + "path": "./admin/templates/extending-templates/modules.md" + }, + { + "title": "Web IDEs and Coder Apps", + "description": "Add and configure Web IDEs in your templates as coder apps", + "path": "./admin/templates/extending-templates/web-ides.md" + }, + { + "title": "Pre-install JetBrains Gateway", + "description": "Pre-install JetBrains Gateway in a template for faster IDE startup", + "path": "./admin/templates/extending-templates/jetbrains-gateway.md" + }, + { + "title": "Docker in Workspaces", + "description": "Use Docker in your workspaces", + "path": "./admin/templates/extending-templates/docker-in-workspaces.md" + }, + { + "title": "Workspace Tags", + "description": "Control provisioning using Workspace Tags and Parameters", + "path": "./admin/templates/extending-templates/workspace-tags.md" + }, + { + "title": "Provider Authentication", + "description": "Authenticate with provider APIs to provision workspaces", + "path": "./admin/templates/extending-templates/provider-authentication.md" + }, + { + "title": "Process Logging", + "description": "Log workspace processes", + "path": "./admin/templates/extending-templates/process-logging.md", + "state": ["premium"] + } + ] + }, + { + "title": "Open in Coder", + "description": "Open workspaces in Coder", + "path": "./admin/templates/open-in-coder.md" + }, + { + "title": "Permissions \u0026 Policies", + "description": "Learn how to create templates with Terraform", + "path": "./admin/templates/template-permissions.md", + "state": ["premium"] + }, + { + "title": "Troubleshooting Templates", + "description": "Learn how to troubleshoot template issues", + "path": "./admin/templates/troubleshooting.md" + } + ] + }, + { + "title": "External Provisioners", + "description": "Learn how to run external provisioners with Coder", + "path": "./admin/provisioners/index.md", + "icon_path": "./images/icons/key.svg", + "state": ["premium"], + "children": [ + { + "title": "Manage Provisioner Jobs", + "description": "Learn how to run external provisioners with Coder", + "path": "./admin/provisioners/manage-provisioner-jobs.md", + "state": ["premium"] + } + ] + }, + { + "title": "External Auth", + "description": "Learn how to configure external authentication", + "path": "./admin/external-auth.md", + "icon_path": "./images/icons/plug.svg" + }, + { + "title": "Integrations", + "description": "Use integrations to extend Coder", + "path": "./admin/integrations/index.md", + "icon_path": "./images/icons/puzzle.svg", + "children": [ + { + "title": "Prometheus", + "description": "Collect deployment metrics with Prometheus", + "path": "./admin/integrations/prometheus.md" + }, + { + "title": "Kubernetes Logging", + "description": "Stream K8s event logs on workspace startup", + "path": "./admin/integrations/kubernetes-logs.md" + }, + { + "title": "Additional Kubernetes Clusters", + "description": "Deploy workspaces on additional Kubernetes clusters", + "path": "./admin/integrations/multiple-kube-clusters.md" + }, + { + "title": "JFrog Artifactory", + "description": "Integrate Coder with JFrog Artifactory", + "path": "./admin/integrations/jfrog-artifactory.md" + }, + { + "title": "JFrog Xray", + "description": "Integrate Coder with JFrog Xray", + "path": "./admin/integrations/jfrog-xray.md" + }, + { + "title": "Island Secure Browser", + "description": "Integrate Coder with Island's Secure Browser", + "path": "./admin/integrations/island.md" + }, + { + "title": "DX PlatformX", + "description": "Integrate Coder with DX PlatformX", + "path": "./admin/integrations/platformx.md" + }, + { + "title": "Hashicorp Vault", + "description": "Integrate Coder with Hashicorp Vault", + "path": "./admin/integrations/vault.md" + } + ] + }, + { + "title": "Networking", + "description": "Understand Coder's networking layer", + "path": "./admin/networking/index.md", + "icon_path": "./images/icons/networking.svg", + "children": [ + { + "title": "Port Forwarding", + "description": "Learn how to forward ports in Coder", + "path": "./admin/networking/port-forwarding.md" + }, + { + "title": "STUN and NAT", + "description": "Learn how to forward ports in Coder", + "path": "./admin/networking/stun.md" + }, + { + "title": "Workspace Proxies", + "description": "Run geo distributed workspace proxies", + "path": "./admin/networking/workspace-proxies.md", + "state": ["premium"] + }, + { + "title": "High Availability", + "description": "Learn how to configure Coder for High Availability", + "path": "./admin/networking/high-availability.md", + "state": ["premium"] + }, + { + "title": "Troubleshooting", + "description": "Troubleshoot networking issues in Coder", + "path": "./admin/networking/troubleshooting.md" + } + ] + }, + { + "title": "Monitoring", + "description": "Configure security policy and audit your deployment", + "path": "./admin/monitoring/index.md", + "icon_path": "./images/icons/speed.svg", + "children": [ + { + "title": "Logs", + "description": "Learn about Coder's logs", + "path": "./admin/monitoring/logs.md" + }, + { + "title": "Metrics", + "description": "Learn about Coder's logs", + "path": "./admin/monitoring/metrics.md" + }, + { + "title": "Health Check", + "description": "Learn about Coder's automated health checks", + "path": "./admin/monitoring/health-check.md" + }, + { + "title": "Notifications", + "description": "Configure notifications for your deployment", + "path": "./admin/monitoring/notifications/index.md", + "children": [ + { + "title": "Slack Notifications", + "description": "Learn how to setup Slack notifications", + "path": "./admin/monitoring/notifications/slack.md" + }, + { + "title": "Microsoft Teams Notifications", + "description": "Learn how to setup Microsoft Teams notifications", + "path": "./admin/monitoring/notifications/teams.md" + } + ] + } + ] + }, + { + "title": "Security", + "description": "Configure security policy and audit your deployment", + "path": "./admin/security/index.md", + "icon_path": "./images/icons/lock.svg", + "children": [ + { + "title": "Audit Logs", + "description": "Audit actions taken inside Coder", + "path": "./admin/security/audit-logs.md", + "state": ["premium"] + }, + { + "title": "Secrets", + "description": "Use sensitive variables in your workspaces", + "path": "./admin/security/secrets.md" + }, + { + "title": "Database Encryption", + "description": "Encrypt the database to prevent unauthorized access", + "path": "./admin/security/database-encryption.md", + "state": ["premium"] + } + ] + }, + { + "title": "Licensing", + "description": "Configure licensing for your deployment", + "path": "./admin/licensing/index.md", + "icon_path": "./images/icons/licensing.svg" + } + ] + }, + { + "title": "Run AI Coding Agents in Coder", + "description": "Learn how to run and integrate AI coding agents like GPT-Code, OpenDevin, or SWE-Agent in Coder workspaces to boost developer productivity.", + "path": "./ai-coder/index.md", + "icon_path": "./images/icons/wand.svg", + "state": ["early access"], + "children": [ + { + "title": "Learn about coding agents", + "description": "Learn about the different AI agents and their tradeoffs", + "path": "./ai-coder/agents.md" + }, + { + "title": "Create a Coder template for agents", + "description": "Create a purpose-built template for your AI agents", + "path": "./ai-coder/create-template.md", + "state": ["early access"] + }, + { + "title": "Integrate with your issue tracker", + "description": "Assign tickets to AI agents and interact via code reviews", + "path": "./ai-coder/issue-tracker.md", + "state": ["early access"] + }, + { + "title": "Model Context Protocols (MCP) and adding AI tools", + "description": "Improve results by adding tools to your AI agents", + "path": "./ai-coder/best-practices.md", + "state": ["early access"] + }, + { + "title": "Supervise agents via Coder UI", + "description": "Interact with agents via the Coder UI", + "path": "./ai-coder/coder-dashboard.md", + "state": ["early access"] + }, + { + "title": "Supervise agents via the IDE", + "description": "Interact with agents via VS Code or Cursor", + "path": "./ai-coder/ide-integration.md", + "state": ["early access"] + }, + { + "title": "Programmatically manage agents", + "description": "Manage agents via MCP, the Coder CLI, and/or REST API", + "path": "./ai-coder/headless.md", + "state": ["early access"] + }, + { + "title": "Securing agents in Coder", + "description": "Learn how to secure agents with boundaries", + "path": "./ai-coder/securing.md", + "state": ["early access"] + }, + { + "title": "Custom agents", + "description": "Learn how to use custom agents with Coder", + "path": "./ai-coder/custom-agents.md", + "state": ["early access"] + } + ] + }, + { + "title": "Contributing", + "description": "Learn how to contribute to Coder", + "path": "./CONTRIBUTING.md", + "icon_path": "./images/icons/contributing.svg", + "children": [ + { + "title": "Code of Conduct", + "description": "See the code of conduct for contributing to Coder", + "path": "./contributing/CODE_OF_CONDUCT.md", + "icon_path": "./images/icons/circle-dot.svg" + }, + { + "title": "Documentation", + "description": "Our style guide for use when authoring documentation", + "path": "./contributing/documentation.md", + "icon_path": "./images/icons/document.svg" + }, + { + "title": "Frontend", + "description": "Our guide for frontend development", + "path": "./contributing/frontend.md", + "icon_path": "./images/icons/frontend.svg" + }, + { + "title": "Security", + "description": "Our guide for security", + "path": "./contributing/SECURITY.md", + "icon_path": "./images/icons/lock.svg" + } + ] + }, + { + "title": "Tutorials", + "description": "Coder knowledgebase for administrating your deployment", + "path": "./tutorials/index.md", + "icon_path": "./images/icons/generic.svg", + "children": [ + { + "title": "Quickstart", + "description": "Learn how to install and run Coder quickly", + "path": "./tutorials/quickstart.md" + }, + { + "title": "Write a Template from Scratch", + "description": "Learn how to author Coder templates", + "path": "./tutorials/template-from-scratch.md" + }, + { + "title": "Using an External Database", + "description": "Use Coder with an external database", + "path": "./tutorials/external-database.md" + }, + { + "title": "Image Management", + "description": "Learn about image management with Coder", + "path": "./admin/templates/managing-templates/image-management.md" + }, + { + "title": "Generate a Support Bundle", + "description": "Generate and upload a Support Bundle to Coder Support", + "path": "./tutorials/support-bundle.md" + }, + { + "title": "Configuring Okta", + "description": "Custom claims/scopes with Okta for group/role sync", + "path": "./tutorials/configuring-okta.md" + }, + { + "title": "Google to AWS Federation", + "description": "Federating a Google Cloud service account to AWS", + "path": "./tutorials/gcp-to-aws.md" + }, + { + "title": "JFrog Artifactory Integration", + "description": "Integrate Coder with JFrog Artifactory", + "path": "./admin/integrations/jfrog-artifactory.md" + }, + { + "title": "Istio Integration", + "description": "Integrate Coder with Istio", + "path": "./admin/integrations/istio.md" + }, + { + "title": "Island Secure Browser Integration", + "description": "Integrate Coder with Island's Secure Browser", + "path": "./admin/integrations/island.md" + }, + { + "title": "Template ImagePullSecrets", + "description": "Creating ImagePullSecrets for private registries", + "path": "./tutorials/image-pull-secret.md" + }, + { + "title": "Postgres SSL", + "description": "Configure Coder to connect to Postgres over SSL", + "path": "./tutorials/postgres-ssl.md" + }, + { + "title": "Azure Federation", + "description": "Federating Coder to Azure", + "path": "./tutorials/azure-federation.md" + }, + { + "title": "Scanning Workspaces with JFrog Xray", + "description": "Integrate Coder with JFrog Xray", + "path": "./admin/integrations/jfrog-xray.md" + }, + { + "title": "Cloning Git Repositories", + "description": "Learn how to clone Git repositories in Coder", + "path": "./tutorials/cloning-git-repositories.md" + }, + { + "title": "Test Templates Through CI/CD", + "description": "Learn how to test and publish Coder templates in a CI/CD pipeline", + "path": "./tutorials/testing-templates.md" + }, + { + "title": "Use Apache as a Reverse Proxy", + "description": "Learn how to use Apache as a reverse proxy", + "path": "./tutorials/reverse-proxy-apache.md" + }, + { + "title": "Use Caddy as a Reverse Proxy", + "description": "Learn how to use Caddy as a reverse proxy", + "path": "./tutorials/reverse-proxy-caddy.md" + }, + { + "title": "Use NGINX as a Reverse Proxy", + "description": "Learn how to use NGINX as a reverse proxy", + "path": "./tutorials/reverse-proxy-nginx.md" + }, + { + "title": "FAQs", + "description": "Miscellaneous FAQs from our community", + "path": "./tutorials/faqs.md" + }, + { + "title": "Best practices", + "description": "Guides to help you make the most of your Coder experience", + "path": "./tutorials/best-practices/index.md", + "children": [ + { + "title": "Organizations - best practices", + "description": "How to make the best use of Coder Organizations", + "path": "./tutorials/best-practices/organizations.md" + }, + { + "title": "Scale Coder", + "description": "How to prepare a Coder deployment for scale", + "path": "./tutorials/best-practices/scale-coder.md" + }, + { + "title": "Security - best practices", + "description": "Make your Coder deployment more secure", + "path": "./tutorials/best-practices/security-best-practices.md" + }, + { + "title": "Speed up your workspaces", + "description": "Speed up your Coder templates and workspaces", + "path": "./tutorials/best-practices/speed-up-templates.md" + } + ] + } + ] + }, + { + "title": "Reference", + "description": "Reference", + "path": "./reference/index.md", + "icon_path": "./images/icons/notes.svg", + "children": [ + { + "title": "REST API", + "description": "Learn how to use Coderd API", + "path": "./reference/api/index.md", + "icon_path": "./images/icons/api.svg", + "children": [ + { + "title": "General", + "path": "./reference/api/general.md" + }, + { + "title": "Agents", + "path": "./reference/api/agents.md" + }, + { + "title": "Applications", + "path": "./reference/api/applications.md" + }, + { + "title": "Audit", + "path": "./reference/api/audit.md" + }, + { + "title": "Authentication", + "path": "./reference/api/authentication.md" + }, + { + "title": "Authorization", + "path": "./reference/api/authorization.md" + }, + { + "title": "Builds", + "path": "./reference/api/builds.md" + }, + { + "title": "Debug", + "path": "./reference/api/debug.md" + }, + { + "title": "Enterprise", + "path": "./reference/api/enterprise.md" + }, + { + "title": "Files", + "path": "./reference/api/files.md" + }, + { + "title": "Git", + "path": "./reference/api/git.md" + }, + { + "title": "Insights", + "path": "./reference/api/insights.md" + }, + { + "title": "Members", + "path": "./reference/api/members.md" + }, + { + "title": "Organizations", + "path": "./reference/api/organizations.md" + }, + { + "title": "PortSharing", + "path": "./reference/api/portsharing.md" + }, + { + "title": "Schemas", + "path": "./reference/api/schemas.md" + }, + { + "title": "Templates", + "path": "./reference/api/templates.md" + }, + { + "title": "Users", + "path": "./reference/api/users.md" + }, + { + "title": "WorkspaceProxies", + "path": "./reference/api/workspaceproxies.md" + }, + { + "title": "Workspaces", + "path": "./reference/api/workspaces.md" + } + ] + }, + { + "title": "Command Line", + "description": "Learn how to use Coder CLI", + "path": "./reference/cli/index.md", + "icon_path": "./images/icons/terminal.svg", + "children": [ + { + "title": "autoupdate", + "description": "Toggle auto-update policy for a workspace", + "path": "reference/cli/autoupdate.md" + }, + { + "title": "coder", + "path": "reference/cli/index.md" + }, + { + "title": "completion", + "description": "Install or update shell completion scripts for the detected or chosen shell.", + "path": "reference/cli/completion.md" + }, + { + "title": "config-ssh", + "description": "Add an SSH Host entry for your workspaces \"ssh coder.workspace\"", + "path": "reference/cli/config-ssh.md" + }, + { + "title": "create", + "description": "Create a workspace", + "path": "reference/cli/create.md" + }, + { + "title": "delete", + "description": "Delete a workspace", + "path": "reference/cli/delete.md" + }, + { + "title": "dotfiles", + "description": "Personalize your workspace by applying a canonical dotfiles repository", + "path": "reference/cli/dotfiles.md" + }, + { + "title": "external-auth", + "description": "Manage external authentication", + "path": "reference/cli/external-auth.md" + }, + { + "title": "external-auth access-token", + "description": "Print auth for an external provider", + "path": "reference/cli/external-auth_access-token.md" + }, + { + "title": "favorite", + "description": "Add a workspace to your favorites", + "path": "reference/cli/favorite.md" + }, + { + "title": "features", + "description": "List Enterprise features", + "path": "reference/cli/features.md" + }, + { + "title": "features list", + "path": "reference/cli/features_list.md" + }, + { + "title": "groups", + "description": "Manage groups", + "path": "reference/cli/groups.md" + }, + { + "title": "groups create", + "description": "Create a user group", + "path": "reference/cli/groups_create.md" + }, + { + "title": "groups delete", + "description": "Delete a user group", + "path": "reference/cli/groups_delete.md" + }, + { + "title": "groups edit", + "description": "Edit a user group", + "path": "reference/cli/groups_edit.md" + }, + { + "title": "groups list", + "description": "List user groups", + "path": "reference/cli/groups_list.md" + }, + { + "title": "licenses", + "description": "Add, delete, and list licenses", + "path": "reference/cli/licenses.md" + }, + { + "title": "licenses add", + "description": "Add license to Coder deployment", + "path": "reference/cli/licenses_add.md" + }, + { + "title": "licenses delete", + "description": "Delete license by ID", + "path": "reference/cli/licenses_delete.md" + }, + { + "title": "licenses list", + "description": "List licenses (including expired)", + "path": "reference/cli/licenses_list.md" + }, + { + "title": "list", + "description": "List workspaces", + "path": "reference/cli/list.md" + }, + { + "title": "login", + "description": "Authenticate with Coder deployment", + "path": "reference/cli/login.md" + }, + { + "title": "logout", + "description": "Unauthenticate your local session", + "path": "reference/cli/logout.md" + }, + { + "title": "netcheck", + "description": "Print network debug information for DERP and STUN", + "path": "reference/cli/netcheck.md" + }, + { + "title": "notifications", + "description": "Manage Coder notifications", + "path": "reference/cli/notifications.md" + }, + { + "title": "notifications pause", + "description": "Pause notifications", + "path": "reference/cli/notifications_pause.md" + }, + { + "title": "notifications resume", + "description": "Resume notifications", + "path": "reference/cli/notifications_resume.md" + }, + { + "title": "notifications test", + "description": "Send a test notification", + "path": "reference/cli/notifications_test.md" + }, + { + "title": "open", + "description": "Open a workspace", + "path": "reference/cli/open.md" + }, + { + "title": "open app", + "description": "Open a workspace application.", + "path": "reference/cli/open_app.md" + }, + { + "title": "open vscode", + "description": "Open a workspace in VS Code Desktop", + "path": "reference/cli/open_vscode.md" + }, + { + "title": "organizations", + "description": "Organization related commands", + "path": "reference/cli/organizations.md" + }, + { + "title": "organizations create", + "description": "Create a new organization.", + "path": "reference/cli/organizations_create.md" + }, + { + "title": "organizations members", + "description": "Manage organization members", + "path": "reference/cli/organizations_members.md" + }, + { + "title": "organizations members add", + "description": "Add a new member to the current organization", + "path": "reference/cli/organizations_members_add.md" + }, + { + "title": "organizations members edit-roles", + "description": "Edit organization member's roles", + "path": "reference/cli/organizations_members_edit-roles.md" + }, + { + "title": "organizations members list", + "description": "List all organization members", + "path": "reference/cli/organizations_members_list.md" + }, + { + "title": "organizations members remove", + "description": "Remove a new member to the current organization", + "path": "reference/cli/organizations_members_remove.md" + }, + { + "title": "organizations roles", + "description": "Manage organization roles.", + "path": "reference/cli/organizations_roles.md" + }, + { + "title": "organizations roles create", + "description": "Create a new organization custom role", + "path": "reference/cli/organizations_roles_create.md" + }, + { + "title": "organizations roles show", + "description": "Show role(s)", + "path": "reference/cli/organizations_roles_show.md" + }, + { + "title": "organizations roles update", + "description": "Update an organization custom role", + "path": "reference/cli/organizations_roles_update.md" + }, + { + "title": "organizations settings", + "description": "Manage organization settings.", + "path": "reference/cli/organizations_settings.md" + }, + { + "title": "organizations settings set", + "description": "Update specified organization setting.", + "path": "reference/cli/organizations_settings_set.md" + }, + { + "title": "organizations settings set group-sync", + "description": "Group sync settings to sync groups from an IdP.", + "path": "reference/cli/organizations_settings_set_group-sync.md" + }, + { + "title": "organizations settings set organization-sync", + "description": "Organization sync settings to sync organization memberships from an IdP.", + "path": "reference/cli/organizations_settings_set_organization-sync.md" + }, + { + "title": "organizations settings set role-sync", + "description": "Role sync settings to sync organization roles from an IdP.", + "path": "reference/cli/organizations_settings_set_role-sync.md" + }, + { + "title": "organizations settings show", + "description": "Outputs specified organization setting.", + "path": "reference/cli/organizations_settings_show.md" + }, + { + "title": "organizations settings show group-sync", + "description": "Group sync settings to sync groups from an IdP.", + "path": "reference/cli/organizations_settings_show_group-sync.md" + }, + { + "title": "organizations settings show organization-sync", + "description": "Organization sync settings to sync organization memberships from an IdP.", + "path": "reference/cli/organizations_settings_show_organization-sync.md" + }, + { + "title": "organizations settings show role-sync", + "description": "Role sync settings to sync organization roles from an IdP.", + "path": "reference/cli/organizations_settings_show_role-sync.md" + }, + { + "title": "organizations show", + "description": "Show the organization. Using \"selected\" will show the selected organization from the \"--org\" flag. Using \"me\" will show all organizations you are a member of.", + "path": "reference/cli/organizations_show.md" + }, + { + "title": "ping", + "description": "Ping a workspace", + "path": "reference/cli/ping.md" + }, + { + "title": "port-forward", + "description": "Forward ports from a workspace to the local machine. For reverse port forwarding, use \"coder ssh -R\".", + "path": "reference/cli/port-forward.md" + }, + { + "title": "provisioner", + "description": "View and manage provisioner daemons and jobs", + "path": "reference/cli/provisioner.md" + }, + { + "title": "provisioner jobs", + "description": "View and manage provisioner jobs", + "path": "reference/cli/provisioner_jobs.md" + }, + { + "title": "provisioner jobs cancel", + "description": "Cancel a provisioner job", + "path": "reference/cli/provisioner_jobs_cancel.md" + }, + { + "title": "provisioner jobs list", + "description": "List provisioner jobs", + "path": "reference/cli/provisioner_jobs_list.md" + }, + { + "title": "provisioner keys", + "description": "Manage provisioner keys", + "path": "reference/cli/provisioner_keys.md" + }, + { + "title": "provisioner keys create", + "description": "Create a new provisioner key", + "path": "reference/cli/provisioner_keys_create.md" + }, + { + "title": "provisioner keys delete", + "description": "Delete a provisioner key", + "path": "reference/cli/provisioner_keys_delete.md" + }, + { + "title": "provisioner keys list", + "description": "List provisioner keys in an organization", + "path": "reference/cli/provisioner_keys_list.md" + }, + { + "title": "provisioner list", + "description": "List provisioner daemons in an organization", + "path": "reference/cli/provisioner_list.md" + }, + { + "title": "provisioner start", + "description": "Run a provisioner daemon", + "path": "reference/cli/provisioner_start.md" + }, + { + "title": "publickey", + "description": "Output your Coder public key used for Git operations", + "path": "reference/cli/publickey.md" + }, + { + "title": "rename", + "description": "Rename a workspace", + "path": "reference/cli/rename.md" + }, + { + "title": "reset-password", + "description": "Directly connect to the database to reset a user's password", + "path": "reference/cli/reset-password.md" + }, + { + "title": "restart", + "description": "Restart a workspace", + "path": "reference/cli/restart.md" + }, + { + "title": "schedule", + "description": "Schedule automated start and stop times for workspaces", + "path": "reference/cli/schedule.md" + }, + { + "title": "schedule extend", + "description": "Extend the stop time of a currently running workspace instance.", + "path": "reference/cli/schedule_extend.md" + }, + { + "title": "schedule show", + "description": "Show workspace schedules", + "path": "reference/cli/schedule_show.md" + }, + { + "title": "schedule start", + "description": "Edit workspace start schedule", + "path": "reference/cli/schedule_start.md" + }, + { + "title": "schedule stop", + "description": "Edit workspace stop schedule", + "path": "reference/cli/schedule_stop.md" + }, + { + "title": "server", + "description": "Start a Coder server", + "path": "reference/cli/server.md" + }, + { + "title": "server create-admin-user", + "description": "Create a new admin user with the given username, email and password and adds it to every organization.", + "path": "reference/cli/server_create-admin-user.md" + }, + { + "title": "server dbcrypt", + "description": "Manage database encryption.", + "path": "reference/cli/server_dbcrypt.md" + }, + { + "title": "server dbcrypt decrypt", + "description": "Decrypt a previously encrypted database.", + "path": "reference/cli/server_dbcrypt_decrypt.md" + }, + { + "title": "server dbcrypt delete", + "description": "Delete all encrypted data from the database. THIS IS A DESTRUCTIVE OPERATION.", + "path": "reference/cli/server_dbcrypt_delete.md" + }, + { + "title": "server dbcrypt rotate", + "description": "Rotate database encryption keys.", + "path": "reference/cli/server_dbcrypt_rotate.md" + }, + { + "title": "server postgres-builtin-serve", + "description": "Run the built-in PostgreSQL deployment.", + "path": "reference/cli/server_postgres-builtin-serve.md" + }, + { + "title": "server postgres-builtin-url", + "description": "Output the connection URL for the built-in PostgreSQL deployment.", + "path": "reference/cli/server_postgres-builtin-url.md" + }, + { + "title": "show", + "description": "Display details of a workspace's resources and agents", + "path": "reference/cli/show.md" + }, + { + "title": "speedtest", + "description": "Run upload and download tests from your machine to a workspace", + "path": "reference/cli/speedtest.md" + }, + { + "title": "ssh", + "description": "Start a shell into a workspace", + "path": "reference/cli/ssh.md" + }, + { + "title": "start", + "description": "Start a workspace", + "path": "reference/cli/start.md" + }, + { + "title": "stat", + "description": "Show resource usage for the current workspace.", + "path": "reference/cli/stat.md" + }, + { + "title": "stat cpu", + "description": "Show CPU usage, in cores.", + "path": "reference/cli/stat_cpu.md" + }, + { + "title": "stat disk", + "description": "Show disk usage, in gigabytes.", + "path": "reference/cli/stat_disk.md" + }, + { + "title": "stat mem", + "description": "Show memory usage, in gigabytes.", + "path": "reference/cli/stat_mem.md" + }, + { + "title": "state", + "description": "Manually manage Terraform state to fix broken workspaces", + "path": "reference/cli/state.md" + }, + { + "title": "state pull", + "description": "Pull a Terraform state file from a workspace.", + "path": "reference/cli/state_pull.md" + }, + { + "title": "state push", + "description": "Push a Terraform state file to a workspace.", + "path": "reference/cli/state_push.md" + }, + { + "title": "stop", + "description": "Stop a workspace", + "path": "reference/cli/stop.md" + }, + { + "title": "support", + "description": "Commands for troubleshooting issues with a Coder deployment.", + "path": "reference/cli/support.md" + }, + { + "title": "support bundle", + "description": "Generate a support bundle to troubleshoot issues connecting to a workspace.", + "path": "reference/cli/support_bundle.md" + }, + { + "title": "templates", + "description": "Manage templates", + "path": "reference/cli/templates.md" + }, + { + "title": "templates archive", + "description": "Archive unused or failed template versions from a given template(s)", + "path": "reference/cli/templates_archive.md" + }, + { + "title": "templates create", + "description": "DEPRECATED: Create a template from the current directory or as specified by flag", + "path": "reference/cli/templates_create.md" + }, + { + "title": "templates delete", + "description": "Delete templates", + "path": "reference/cli/templates_delete.md" + }, + { + "title": "templates edit", + "description": "Edit the metadata of a template by name.", + "path": "reference/cli/templates_edit.md" + }, + { + "title": "templates init", + "description": "Get started with a templated template.", + "path": "reference/cli/templates_init.md" + }, + { + "title": "templates list", + "description": "List all the templates available for the organization", + "path": "reference/cli/templates_list.md" + }, + { + "title": "templates pull", + "description": "Download the active, latest, or specified version of a template to a path.", + "path": "reference/cli/templates_pull.md" + }, + { + "title": "templates push", + "description": "Create or update a template from the current directory or as specified by flag", + "path": "reference/cli/templates_push.md" + }, + { + "title": "templates versions", + "description": "Manage different versions of the specified template", + "path": "reference/cli/templates_versions.md" + }, + { + "title": "templates versions archive", + "description": "Archive a template version(s).", + "path": "reference/cli/templates_versions_archive.md" + }, + { + "title": "templates versions list", + "description": "List all the versions of the specified template", + "path": "reference/cli/templates_versions_list.md" + }, + { + "title": "templates versions promote", + "description": "Promote a template version to active.", + "path": "reference/cli/templates_versions_promote.md" + }, + { + "title": "templates versions unarchive", + "description": "Unarchive a template version(s).", + "path": "reference/cli/templates_versions_unarchive.md" + }, + { + "title": "tokens", + "description": "Manage personal access tokens", + "path": "reference/cli/tokens.md" + }, + { + "title": "tokens create", + "description": "Create a token", + "path": "reference/cli/tokens_create.md" + }, + { + "title": "tokens list", + "description": "List tokens", + "path": "reference/cli/tokens_list.md" + }, + { + "title": "tokens remove", + "description": "Delete a token", + "path": "reference/cli/tokens_remove.md" + }, + { + "title": "unfavorite", + "description": "Remove a workspace from your favorites", + "path": "reference/cli/unfavorite.md" + }, + { + "title": "update", + "description": "Will update and start a given workspace if it is out of date", + "path": "reference/cli/update.md" + }, + { + "title": "users", + "description": "Manage users", + "path": "reference/cli/users.md" + }, + { + "title": "users activate", + "description": "Update a user's status to 'active'. Active users can fully interact with the platform", + "path": "reference/cli/users_activate.md" + }, + { + "title": "users create", + "path": "reference/cli/users_create.md" + }, + { + "title": "users delete", + "description": "Delete a user by username or user_id.", + "path": "reference/cli/users_delete.md" + }, + { + "title": "users edit-roles", + "description": "Edit a user's roles by username or id", + "path": "reference/cli/users_edit-roles.md" + }, + { + "title": "users list", + "path": "reference/cli/users_list.md" + }, + { + "title": "users show", + "description": "Show a single user. Use 'me' to indicate the currently authenticated user.", + "path": "reference/cli/users_show.md" + }, + { + "title": "users suspend", + "description": "Update a user's status to 'suspended'. A suspended user cannot log into the platform", + "path": "reference/cli/users_suspend.md" + }, + { + "title": "version", + "description": "Show coder version", + "path": "reference/cli/version.md" + }, + { + "title": "whoami", + "description": "Fetch authenticated user info for Coder deployment", + "path": "reference/cli/whoami.md" + } + ] + }, + { + "title": "Agent API", + "description": "Learn how to use Coder Agent API", + "path": "./reference/agent-api/index.md", + "icon_path": "./images/icons/api.svg", + "children": [ + { + "title": "Debug", + "path": "./reference/agent-api/debug.md" + }, + { + "title": "Schemas", + "path": "./reference/agent-api/schemas.md" + } + ] + } + ] + } + ] +} diff --git a/docs/reference/api/agents.md b/docs/reference/api/agents.md index b9b0c8973f64b..0fa4c6a284f09 100644 --- a/docs/reference/api/agents.md +++ b/docs/reference/api/agents.md @@ -496,9 +496,9 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/me/reinit \ ### Responses -| Status | Meaning | Description | Schema | -|--------|---------------------------------------------------------|-------------|----------------------------------------------------------------------------------| -| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [agentsdk.ReinitializationResponse](schemas.md#agentsdkreinitializationresponse) | +| Status | Meaning | Description | Schema | +|--------|---------------------------------------------------------|-------------|----------------------------------------------------------------------------| +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [agentsdk.ReinitializationEvent](schemas.md#agentsdkreinitializationevent) | To perform this operation, you must be authenticated. [Learn more](authentication.md). diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 2224a707a3264..627815b94f811 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -182,21 +182,7 @@ | `icon` | string | false | | | | `id` | string | false | | ID is a unique identifier for the log source. It is scoped to a workspace agent, and can be statically defined inside code to prevent duplicate sources from being created for the same agent. | -## agentsdk.ReinitializationReason - -```json -"prebuild_claimed" -``` - -### Properties - -#### Enumerated Values - -| Value | -|--------------------| -| `prebuild_claimed` | - -## agentsdk.ReinitializationResponse +## agentsdk.ReinitializationEvent ```json { @@ -212,6 +198,20 @@ | `message` | string | false | | | | `reason` | [agentsdk.ReinitializationReason](#agentsdkreinitializationreason) | false | | | +## agentsdk.ReinitializationReason + +```json +"prebuild_claimed" +``` + +### Properties + +#### Enumerated Values + +| Value | +|--------------------| +| `prebuild_claimed` | + ## coderd.SCIMUser ```json From a22b4143e1e224accd982fdba2a7f758a43b37c4 Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Wed, 30 Apr 2025 11:48:03 +0000 Subject: [PATCH 13/42] introduce unit testable abstraction layers --- coderd/prebuilds/claim.go | 102 ++++++++++++++++++ .../provisionerdserver/provisionerdserver.go | 8 +- coderd/workspaceagents.go | 69 +----------- codersdk/agentsdk/agentsdk.go | 2 +- 4 files changed, 110 insertions(+), 71 deletions(-) create mode 100644 coderd/prebuilds/claim.go diff --git a/coderd/prebuilds/claim.go b/coderd/prebuilds/claim.go new file mode 100644 index 0000000000000..f5963e0d77b12 --- /dev/null +++ b/coderd/prebuilds/claim.go @@ -0,0 +1,102 @@ +package prebuilds + +import ( + "context" + "fmt" + "net/http" + + "github.com/google/uuid" + "golang.org/x/xerrors" + + "cdr.dev/slog" + "github.com/coder/coder/v2/coderd/database/pubsub" + "github.com/coder/coder/v2/coderd/httpapi" + "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/codersdk/agentsdk" +) + +func PublishWorkspaceClaim(ctx context.Context, ps pubsub.Pubsub, workspaceID, userID uuid.UUID) error { + channel := agentsdk.PrebuildClaimedChannel(workspaceID) + if err := ps.Publish(channel, []byte(userID.String())); err != nil { + return xerrors.Errorf("failed to trigger prebuilt workspace agent reinitialization: %w", err) + } + return nil +} + +func ListenForWorkspaceClaims(ctx context.Context, logger slog.Logger, ps pubsub.Pubsub, workspaceID uuid.UUID) (func(), <-chan agentsdk.ReinitializationEvent, error) { + reinitEvents := make(chan agentsdk.ReinitializationEvent, 1) + cancelSub, err := ps.Subscribe(agentsdk.PrebuildClaimedChannel(workspaceID), func(inner context.Context, id []byte) { + select { + case <-ctx.Done(): + return + case <-inner.Done(): + return + default: + } + + claimantID, err := uuid.ParseBytes(id) + if err != nil { + logger.Error(ctx, "invalid prebuild claimed channel payload", slog.F("input", string(id))) + return + } + // TODO: turn this into a <- uuid.UUID + reinitEvents <- agentsdk.ReinitializationEvent{ + Message: fmt.Sprintf("prebuild claimed by user: %s", claimantID), + Reason: agentsdk.ReinitializeReasonPrebuildClaimed, + } + }) + if err != nil { + return func() {}, nil, xerrors.Errorf("failed to subscribe to prebuild claimed channel: %w", err) + } + defer cancelSub() + return func() { cancelSub() }, reinitEvents, nil +} + +func StreamAgentReinitEvents(ctx context.Context, logger slog.Logger, rw http.ResponseWriter, r *http.Request, reinitEvents <-chan agentsdk.ReinitializationEvent) { + sseSendEvent, sseSenderClosed, err := httpapi.ServerSentEventSender(rw, r) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error setting up server-sent events.", + Detail: err.Error(), + }) + return + } + // Prevent handler from returning until the sender is closed. + defer func() { + <-sseSenderClosed + }() + + // An initial ping signals to the requester that the server is now ready + // and the client can begin servicing a channel with data. + _ = sseSendEvent(codersdk.ServerSentEvent{ + Type: codersdk.ServerSentEventTypePing, + }) + + for { + select { + case <-ctx.Done(): + return + case reinitEvent := <-reinitEvents: + err = sseSendEvent(codersdk.ServerSentEvent{ + Type: codersdk.ServerSentEventTypeData, + Data: reinitEvent, + }) + if err != nil { + logger.Warn(ctx, "failed to send SSE response to trigger reinit", slog.Error(err)) + } + } + } +} + +type MockClaimCoordinator interface{} + +type ClaimListener interface{} +type PostgresClaimListener struct{} + +type AgentReinitializer interface{} +type SSEAgentReinitializer struct{} + +type ClaimCoordinator interface { + ClaimListener + AgentReinitializer +} diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index 01e24dbcb2279..2b7d2175b7baa 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -16,8 +16,6 @@ import ( "sync/atomic" "time" - "github.com/coder/coder/v2/codersdk/agentsdk" - "github.com/google/uuid" "github.com/sqlc-dev/pqtype" semconv "go.opentelemetry.io/otel/semconv/v1.14.0" @@ -39,6 +37,7 @@ import ( "github.com/coder/coder/v2/coderd/database/pubsub" "github.com/coder/coder/v2/coderd/externalauth" "github.com/coder/coder/v2/coderd/notifications" + "github.com/coder/coder/v2/coderd/prebuilds" "github.com/coder/coder/v2/coderd/promoauth" "github.com/coder/coder/v2/coderd/schedule" "github.com/coder/coder/v2/coderd/telemetry" @@ -1750,9 +1749,8 @@ func (s *server) CompleteJob(ctx context.Context, completed *proto.CompletedJob) slog.F("user", input.PrebuildClaimedByUser.String()), slog.F("workspace_id", workspace.ID)) - channel := agentsdk.PrebuildClaimedChannel(workspace.ID) - if err := s.Pubsub.Publish(channel, []byte(input.PrebuildClaimedByUser.String())); err != nil { - s.Logger.Error(ctx, "failed to trigger prebuilt workspace agent reinitialization", slog.Error(err)) + if err := prebuilds.PublishWorkspaceClaim(ctx, s.Pubsub, workspace.ID, input.PrebuildClaimedByUser); err != nil { + s.Logger.Error(ctx, "failed to publish workspace claim event", slog.Error(err)) } } case *proto.CompletedJob_TemplateDryRun_: diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index c154e7a904490..20795ad01e3d6 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -35,6 +35,7 @@ import ( "github.com/coder/coder/v2/coderd/httpmw" "github.com/coder/coder/v2/coderd/httpmw/loggermw" "github.com/coder/coder/v2/coderd/jwtutils" + "github.com/coder/coder/v2/coderd/prebuilds" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/rbac/policy" "github.com/coder/coder/v2/coderd/telemetry" @@ -1180,76 +1181,14 @@ func (api *API) workspaceAgentReinit(rw http.ResponseWriter, r *http.Request) { log.Info(ctx, "agent waiting for reinit instruction") - prebuildClaims := make(chan uuid.UUID, 1) - cancelSub, err := api.Pubsub.Subscribe(agentsdk.PrebuildClaimedChannel(workspace.ID), func(inner context.Context, id []byte) { - select { - case <-ctx.Done(): - return - case <-inner.Done(): - return - default: - } - - parsed, err := uuid.ParseBytes(id) - if err != nil { - log.Error(ctx, "invalid prebuild claimed channel payload", slog.F("input", string(id))) - return - } - prebuildClaims <- parsed - }) + cancel, reinitEvents, err := prebuilds.ListenForWorkspaceClaims(ctx, log, api.Pubsub, workspace.ID) if err != nil { log.Error(ctx, "failed to subscribe to prebuild claimed channel", slog.Error(err)) httpapi.InternalServerError(rw, xerrors.New("failed to subscribe to prebuild claimed channel")) return } - defer cancelSub() - - sseSendEvent, sseSenderClosed, err := httpapi.ServerSentEventSender(rw, r) - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error setting up server-sent events.", - Detail: err.Error(), - }) - return - } - // Prevent handler from returning until the sender is closed. - defer func() { - cancel() - <-sseSenderClosed - }() - // Synchronize cancellation from SSE -> context, this lets us simplify the - // cancellation logic. - go func() { - select { - case <-ctx.Done(): - case <-sseSenderClosed: - cancel() - } - }() - - // An initial ping signals to the request that the server is now ready - // and the client can begin servicing a channel with data. - _ = sseSendEvent(codersdk.ServerSentEvent{ - Type: codersdk.ServerSentEventTypePing, - }) - - for { - select { - case <-ctx.Done(): - return - case user := <-prebuildClaims: - err = sseSendEvent(codersdk.ServerSentEvent{ - Type: codersdk.ServerSentEventTypeData, - Data: agentsdk.ReinitializationEvent{ - Message: fmt.Sprintf("prebuild claimed by user: %s", user), - Reason: agentsdk.ReinitializeReasonPrebuildClaimed, - }, - }) - if err != nil { - log.Warn(ctx, "failed to send SSE response to trigger reinit", slog.Error(err)) - } - } - } + defer cancel() + prebuilds.StreamAgentReinitEvents(ctx, log, rw, r, reinitEvents) } // convertProvisionedApps converts applications that are in the middle of provisioning process. diff --git a/codersdk/agentsdk/agentsdk.go b/codersdk/agentsdk/agentsdk.go index b40a73138240c..36be85171f151 100644 --- a/codersdk/agentsdk/agentsdk.go +++ b/codersdk/agentsdk/agentsdk.go @@ -776,8 +776,8 @@ func WaitForReinitLoop(ctx context.Context, logger slog.Logger, client *Client) reinitEvent, err := client.WaitForReinit(ctx) if err != nil { logger.Error(ctx, "failed to wait for agent reinitialization instructions", slog.Error(err)) + continue } - reinitEvents <- *reinitEvent select { case <-ctx.Done(): close(reinitEvents) From 9bbd2c75842ea47ed72637b6f2ab492be1568cf8 Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Thu, 1 May 2025 08:37:30 +0000 Subject: [PATCH 14/42] test workspace claim pubsub --- cli/agent.go | 3 +- coderd/prebuilds/claim.go | 80 ++++++++++---- coderd/prebuilds/claim_test.go | 191 +++++++++++++++++++++++++++++++++ codersdk/agentsdk/agentsdk.go | 9 +- 4 files changed, 256 insertions(+), 27 deletions(-) create mode 100644 coderd/prebuilds/claim_test.go diff --git a/cli/agent.go b/cli/agent.go index d17b1d31858bb..3ae68bcd939c7 100644 --- a/cli/agent.go +++ b/cli/agent.go @@ -331,6 +331,7 @@ func (r *RootCmd) workspaceAgent() *serpent.Command { } reinitEvents := agentsdk.WaitForReinitLoop(ctx, logger, client) + var ( lastErr error mustExit bool @@ -379,7 +380,7 @@ func (r *RootCmd) workspaceAgent() *serpent.Command { mustExit = true case event := <-reinitEvents: logger.Warn(ctx, "agent received instruction to reinitialize", - slog.F("message", event.Message), slog.F("reason", event.Reason)) + slog.F("user_id", event.UserID), slog.F("workspace_id", event.WorkspaceID), slog.F("reason", event.Reason)) } lastErr = agnt.Close() diff --git a/coderd/prebuilds/claim.go b/coderd/prebuilds/claim.go index f5963e0d77b12..f3f08229cfee9 100644 --- a/coderd/prebuilds/claim.go +++ b/coderd/prebuilds/claim.go @@ -2,8 +2,8 @@ package prebuilds import ( "context" - "fmt" "net/http" + "sync" "github.com/google/uuid" "golang.org/x/xerrors" @@ -15,41 +15,81 @@ import ( "github.com/coder/coder/v2/codersdk/agentsdk" ) -func PublishWorkspaceClaim(ctx context.Context, ps pubsub.Pubsub, workspaceID, userID uuid.UUID) error { - channel := agentsdk.PrebuildClaimedChannel(workspaceID) - if err := ps.Publish(channel, []byte(userID.String())); err != nil { +type WorkspaceClaimPublisher interface { + PublishWorkspaceClaim(agentsdk.ReinitializationEvent) +} + +func NewPubsubWorkspaceClaimPublisher(ps pubsub.Pubsub) *PubsubWorkspaceClaimPublisher { + return &PubsubWorkspaceClaimPublisher{ps: ps} +} + +type PubsubWorkspaceClaimPublisher struct { + ps pubsub.Pubsub +} + +func (p PubsubWorkspaceClaimPublisher) PublishWorkspaceClaim(claim agentsdk.ReinitializationEvent) error { + channel := agentsdk.PrebuildClaimedChannel(claim.WorkspaceID) + if err := p.ps.Publish(channel, []byte(claim.UserID.String())); err != nil { return xerrors.Errorf("failed to trigger prebuilt workspace agent reinitialization: %w", err) } return nil } -func ListenForWorkspaceClaims(ctx context.Context, logger slog.Logger, ps pubsub.Pubsub, workspaceID uuid.UUID) (func(), <-chan agentsdk.ReinitializationEvent, error) { - reinitEvents := make(chan agentsdk.ReinitializationEvent, 1) - cancelSub, err := ps.Subscribe(agentsdk.PrebuildClaimedChannel(workspaceID), func(inner context.Context, id []byte) { +type WorkspaceClaimListener interface { + ListenForWorkspaceClaims(ctx context.Context, workspaceID uuid.UUID) (func(), <-chan agentsdk.ReinitializationEvent, error) +} + +func NewPubsubWorkspaceClaimListener(ps pubsub.Pubsub, logger slog.Logger) *PubsubWorkspaceClaimListener { + return &PubsubWorkspaceClaimListener{ps: ps, logger: logger} +} + +type PubsubWorkspaceClaimListener struct { + logger slog.Logger + ps pubsub.Pubsub +} + +func (p PubsubWorkspaceClaimListener) ListenForWorkspaceClaims(ctx context.Context, workspaceID uuid.UUID) (func(), <-chan agentsdk.ReinitializationEvent, error) { + workspaceClaims := make(chan agentsdk.ReinitializationEvent, 1) + cancelSub, err := p.ps.Subscribe(agentsdk.PrebuildClaimedChannel(workspaceID), func(inner context.Context, id []byte) { + claimantID, err := uuid.ParseBytes(id) + if err != nil { + p.logger.Error(ctx, "invalid prebuild claimed channel payload", slog.F("input", string(id))) + return + } + claim := agentsdk.ReinitializationEvent{ + UserID: claimantID, + WorkspaceID: workspaceID, + Reason: agentsdk.ReinitializeReasonPrebuildClaimed, + } select { case <-ctx.Done(): return case <-inner.Done(): return + case workspaceClaims <- claim: default: } - - claimantID, err := uuid.ParseBytes(id) - if err != nil { - logger.Error(ctx, "invalid prebuild claimed channel payload", slog.F("input", string(id))) - return - } - // TODO: turn this into a <- uuid.UUID - reinitEvents <- agentsdk.ReinitializationEvent{ - Message: fmt.Sprintf("prebuild claimed by user: %s", claimantID), - Reason: agentsdk.ReinitializeReasonPrebuildClaimed, - } }) + if err != nil { + close(workspaceClaims) return func() {}, nil, xerrors.Errorf("failed to subscribe to prebuild claimed channel: %w", err) } - defer cancelSub() - return func() { cancelSub() }, reinitEvents, nil + + var once sync.Once + cancel := func() { + once.Do(func() { + cancelSub() + close(workspaceClaims) + }) + } + + go func() { + <-ctx.Done() + cancel() + }() + + return cancel, workspaceClaims, nil } func StreamAgentReinitEvents(ctx context.Context, logger slog.Logger, rw http.ResponseWriter, r *http.Request, reinitEvents <-chan agentsdk.ReinitializationEvent) { diff --git a/coderd/prebuilds/claim_test.go b/coderd/prebuilds/claim_test.go new file mode 100644 index 0000000000000..9d07c63e7919a --- /dev/null +++ b/coderd/prebuilds/claim_test.go @@ -0,0 +1,191 @@ +package prebuilds_test + +import ( + "context" + "testing" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/xerrors" + + "cdr.dev/slog/sloggers/slogtest" + "github.com/coder/coder/v2/coderd/database/pubsub" + "github.com/coder/coder/v2/coderd/prebuilds" + "github.com/coder/coder/v2/codersdk/agentsdk" + "github.com/coder/coder/v2/testutil" +) + +func TestPubsubWorkspaceClaimPublisher(t *testing.T) { + t.Parallel() + t.Run("publish claim", func(t *testing.T) { + t.Parallel() + + ps := pubsub.NewInMemory() + publisher := prebuilds.NewPubsubWorkspaceClaimPublisher(ps) + + workspaceID := uuid.New() + userID := uuid.New() + + userIDCh := make(chan uuid.UUID, 1) + channel := agentsdk.PrebuildClaimedChannel(workspaceID) + cancel, err := ps.Subscribe(channel, func(ctx context.Context, message []byte) { + userIDCh <- uuid.MustParse(string(message)) + }) + require.NoError(t, err) + defer cancel() + + claim := agentsdk.ReinitializationEvent{ + UserID: userID, + WorkspaceID: workspaceID, + Reason: agentsdk.ReinitializeReasonPrebuildClaimed, + } + err = publisher.PublishWorkspaceClaim(claim) + require.NoError(t, err) + + // Verify the message was published + select { + case gotUserID := <-userIDCh: + require.Equal(t, userID, gotUserID) + case <-time.After(testutil.WaitShort): + t.Fatal("timeout waiting for claim") + } + }) + + t.Run("fail to publish claim", func(t *testing.T) { + t.Parallel() + + ps := &brokenPubsub{} + + publisher := prebuilds.NewPubsubWorkspaceClaimPublisher(ps) + claim := agentsdk.ReinitializationEvent{ + UserID: uuid.New(), + WorkspaceID: uuid.New(), + Reason: agentsdk.ReinitializeReasonPrebuildClaimed, + } + + err := publisher.PublishWorkspaceClaim(claim) + require.Error(t, err) + }) +} + +func TestPubsubWorkspaceClaimListener(t *testing.T) { + t.Parallel() + t.Run("stops listening if context canceled", func(t *testing.T) { + t.Parallel() + + ps := pubsub.NewInMemory() + listener := prebuilds.NewPubsubWorkspaceClaimListener(ps, slogtest.Make(t, nil)) + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + cancelFunc, claims, err := listener.ListenForWorkspaceClaims(ctx, uuid.New()) + require.NoError(t, err) + defer cancelFunc() + + // Channel should be closed immediately due to context cancellation + select { + case _, ok := <-claims: + assert.False(t, ok) + case <-time.After(testutil.WaitShort): + t.Fatal("timeout waiting for closed channel") + } + }) + + t.Run("stops listening if cancel func is called", func(t *testing.T) { + t.Parallel() + + ps := pubsub.NewInMemory() + listener := prebuilds.NewPubsubWorkspaceClaimListener(ps, slogtest.Make(t, nil)) + + cancelFunc, claims, err := listener.ListenForWorkspaceClaims(context.Background(), uuid.New()) + require.NoError(t, err) + + cancelFunc() + select { + case _, ok := <-claims: + assert.False(t, ok) + case <-time.After(testutil.WaitShort): + t.Fatal("timeout waiting for closed channel") + } + }) + + t.Run("finds claim events for its workspace", func(t *testing.T) { + t.Parallel() + + ps := pubsub.NewInMemory() + listener := prebuilds.NewPubsubWorkspaceClaimListener(ps, slogtest.Make(t, nil)) + + workspaceID := uuid.New() + userID := uuid.New() + cancelFunc, claims, err := listener.ListenForWorkspaceClaims(context.Background(), workspaceID) + require.NoError(t, err) + defer cancelFunc() + + // Publish a claim + channel := agentsdk.PrebuildClaimedChannel(workspaceID) + err = ps.Publish(channel, []byte(userID.String())) + require.NoError(t, err) + + // Verify we receive the claim + select { + case claim := <-claims: + assert.Equal(t, userID, claim.UserID) + assert.Equal(t, workspaceID, claim.WorkspaceID) + assert.Equal(t, agentsdk.ReinitializeReasonPrebuildClaimed, claim.Reason) + case <-time.After(time.Second): + t.Fatal("timeout waiting for claim") + } + }) + + t.Run("ignores claim events for other workspaces", func(t *testing.T) { + t.Parallel() + + ps := pubsub.NewInMemory() + listener := prebuilds.NewPubsubWorkspaceClaimListener(ps, slogtest.Make(t, nil)) + + workspaceID := uuid.New() + otherWorkspaceID := uuid.New() + cancelFunc, claims, err := listener.ListenForWorkspaceClaims(context.Background(), workspaceID) + require.NoError(t, err) + defer cancelFunc() + + // Publish a claim for a different workspace + channel := agentsdk.PrebuildClaimedChannel(otherWorkspaceID) + err = ps.Publish(channel, []byte(uuid.New().String())) + require.NoError(t, err) + + // Verify we don't receive the claim + select { + case <-claims: + t.Fatal("received claim for wrong workspace") + case <-time.After(100 * time.Millisecond): + // Expected - no claim received + } + }) + + t.Run("communicates the error if it can't subscribe", func(t *testing.T) { + t.Parallel() + + ps := &brokenPubsub{} + listener := prebuilds.NewPubsubWorkspaceClaimListener(ps, slogtest.Make(t, nil)) + + _, _, err := listener.ListenForWorkspaceClaims(context.Background(), uuid.New()) + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to subscribe to prebuild claimed channel") + }) +} + +type brokenPubsub struct { + pubsub.Pubsub +} + +func (brokenPubsub) Subscribe(_ string, _ pubsub.Listener) (func(), error) { + return nil, xerrors.New("broken") +} + +func (brokenPubsub) Publish(_ string, _ []byte) error { + return xerrors.New("broken") +} diff --git a/codersdk/agentsdk/agentsdk.go b/codersdk/agentsdk/agentsdk.go index 36be85171f151..107dd5f47fa92 100644 --- a/codersdk/agentsdk/agentsdk.go +++ b/codersdk/agentsdk/agentsdk.go @@ -696,8 +696,9 @@ const ( ) type ReinitializationEvent struct { - Message string `json:"message"` - Reason ReinitializationReason `json:"reason"` + WorkspaceID uuid.UUID + UserID uuid.UUID + Reason ReinitializationReason `json:"reason"` } func PrebuildClaimedChannel(id uuid.UUID) string { @@ -707,7 +708,6 @@ func PrebuildClaimedChannel(id uuid.UUID) string { // WaitForReinit polls a SSE endpoint, and receives an event back under the following conditions: // - ping: ignored, keepalive // - prebuild claimed: a prebuilt workspace is claimed, so the agent must reinitialize. -// NOTE: the caller is responsible for closing the events chan. func (c *Client) WaitForReinit(ctx context.Context) (*ReinitializationEvent, error) { // TODO: allow configuring httpclient c.SDK.HTTPClient.Timeout = time.Hour * 24 @@ -733,9 +733,6 @@ func (c *Client) WaitForReinit(ctx context.Context) (*ReinitializationEvent, err nextEvent := codersdk.ServerSentEventReader(ctx, res.Body) for { - // TODO (Sasswart): I don't like that we do this select at the start and at the end. - // nextEvent should return an error if the context is canceled, but that feels like a larger refactor. - // if it did, we'd only have the select at the end of the loop. select { case <-ctx.Done(): return nil, ctx.Err() From 580420197f6a736e784dcd3c2f14360c9a716fc6 Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Thu, 1 May 2025 18:49:57 +0000 Subject: [PATCH 15/42] add tests for agent reinitialization --- coderd/prebuilds/claim.go | 66 +-------- coderd/prebuilds/claim_test.go | 2 +- .../provisionerdserver/provisionerdserver.go | 8 +- coderd/workspaceagents.go | 14 +- codersdk/agentsdk/agentsdk.go | 104 +++++++++++---- codersdk/agentsdk/agentsdk_test.go | 125 ++++++++++++++++++ 6 files changed, 231 insertions(+), 88 deletions(-) create mode 100644 codersdk/agentsdk/agentsdk_test.go diff --git a/coderd/prebuilds/claim.go b/coderd/prebuilds/claim.go index f3f08229cfee9..5ff40cbf9bebe 100644 --- a/coderd/prebuilds/claim.go +++ b/coderd/prebuilds/claim.go @@ -2,7 +2,6 @@ package prebuilds import ( "context" - "net/http" "sync" "github.com/google/uuid" @@ -10,15 +9,9 @@ import ( "cdr.dev/slog" "github.com/coder/coder/v2/coderd/database/pubsub" - "github.com/coder/coder/v2/coderd/httpapi" - "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/agentsdk" ) -type WorkspaceClaimPublisher interface { - PublishWorkspaceClaim(agentsdk.ReinitializationEvent) -} - func NewPubsubWorkspaceClaimPublisher(ps pubsub.Pubsub) *PubsubWorkspaceClaimPublisher { return &PubsubWorkspaceClaimPublisher{ps: ps} } @@ -35,10 +28,6 @@ func (p PubsubWorkspaceClaimPublisher) PublishWorkspaceClaim(claim agentsdk.Rein return nil } -type WorkspaceClaimListener interface { - ListenForWorkspaceClaims(ctx context.Context, workspaceID uuid.UUID) (func(), <-chan agentsdk.ReinitializationEvent, error) -} - func NewPubsubWorkspaceClaimListener(ps pubsub.Pubsub, logger slog.Logger) *PubsubWorkspaceClaimListener { return &PubsubWorkspaceClaimListener{ps: ps, logger: logger} } @@ -49,6 +38,12 @@ type PubsubWorkspaceClaimListener struct { } func (p PubsubWorkspaceClaimListener) ListenForWorkspaceClaims(ctx context.Context, workspaceID uuid.UUID) (func(), <-chan agentsdk.ReinitializationEvent, error) { + select { + case <-ctx.Done(): + return func() {}, nil, ctx.Err() + default: + } + workspaceClaims := make(chan agentsdk.ReinitializationEvent, 1) cancelSub, err := p.ps.Subscribe(agentsdk.PrebuildClaimedChannel(workspaceID), func(inner context.Context, id []byte) { claimantID, err := uuid.ParseBytes(id) @@ -91,52 +86,3 @@ func (p PubsubWorkspaceClaimListener) ListenForWorkspaceClaims(ctx context.Conte return cancel, workspaceClaims, nil } - -func StreamAgentReinitEvents(ctx context.Context, logger slog.Logger, rw http.ResponseWriter, r *http.Request, reinitEvents <-chan agentsdk.ReinitializationEvent) { - sseSendEvent, sseSenderClosed, err := httpapi.ServerSentEventSender(rw, r) - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error setting up server-sent events.", - Detail: err.Error(), - }) - return - } - // Prevent handler from returning until the sender is closed. - defer func() { - <-sseSenderClosed - }() - - // An initial ping signals to the requester that the server is now ready - // and the client can begin servicing a channel with data. - _ = sseSendEvent(codersdk.ServerSentEvent{ - Type: codersdk.ServerSentEventTypePing, - }) - - for { - select { - case <-ctx.Done(): - return - case reinitEvent := <-reinitEvents: - err = sseSendEvent(codersdk.ServerSentEvent{ - Type: codersdk.ServerSentEventTypeData, - Data: reinitEvent, - }) - if err != nil { - logger.Warn(ctx, "failed to send SSE response to trigger reinit", slog.Error(err)) - } - } - } -} - -type MockClaimCoordinator interface{} - -type ClaimListener interface{} -type PostgresClaimListener struct{} - -type AgentReinitializer interface{} -type SSEAgentReinitializer struct{} - -type ClaimCoordinator interface { - ClaimListener - AgentReinitializer -} diff --git a/coderd/prebuilds/claim_test.go b/coderd/prebuilds/claim_test.go index 9d07c63e7919a..225337379409e 100644 --- a/coderd/prebuilds/claim_test.go +++ b/coderd/prebuilds/claim_test.go @@ -79,12 +79,12 @@ func TestPubsubWorkspaceClaimListener(t *testing.T) { listener := prebuilds.NewPubsubWorkspaceClaimListener(ps, slogtest.Make(t, nil)) ctx, cancel := context.WithCancel(context.Background()) - cancel() cancelFunc, claims, err := listener.ListenForWorkspaceClaims(ctx, uuid.New()) require.NoError(t, err) defer cancelFunc() + cancel() // Channel should be closed immediately due to context cancellation select { case _, ok := <-claims: diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index 2b7d2175b7baa..ae4ab689b4f25 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -44,6 +44,7 @@ import ( "github.com/coder/coder/v2/coderd/tracing" "github.com/coder/coder/v2/coderd/wspubsub" "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/codersdk/agentsdk" "github.com/coder/coder/v2/codersdk/drpc" "github.com/coder/coder/v2/provisioner" "github.com/coder/coder/v2/provisionerd/proto" @@ -1749,7 +1750,12 @@ func (s *server) CompleteJob(ctx context.Context, completed *proto.CompletedJob) slog.F("user", input.PrebuildClaimedByUser.String()), slog.F("workspace_id", workspace.ID)) - if err := prebuilds.PublishWorkspaceClaim(ctx, s.Pubsub, workspace.ID, input.PrebuildClaimedByUser); err != nil { + err = prebuilds.NewPubsubWorkspaceClaimPublisher(s.Pubsub).PublishWorkspaceClaim(agentsdk.ReinitializationEvent{ + UserID: input.PrebuildClaimedByUser, + WorkspaceID: workspace.ID, + Reason: agentsdk.ReinitializeReasonPrebuildClaimed, + }) + if err != nil { s.Logger.Error(ctx, "failed to publish workspace claim event", slog.Error(err)) } } diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index 20795ad01e3d6..2f4eec0d1f941 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -1181,14 +1181,24 @@ func (api *API) workspaceAgentReinit(rw http.ResponseWriter, r *http.Request) { log.Info(ctx, "agent waiting for reinit instruction") - cancel, reinitEvents, err := prebuilds.ListenForWorkspaceClaims(ctx, log, api.Pubsub, workspace.ID) + cancel, reinitEvents, err := prebuilds.NewPubsubWorkspaceClaimListener(api.Pubsub, log).ListenForWorkspaceClaims(ctx, workspace.ID) if err != nil { log.Error(ctx, "failed to subscribe to prebuild claimed channel", slog.Error(err)) httpapi.InternalServerError(rw, xerrors.New("failed to subscribe to prebuild claimed channel")) return } defer cancel() - prebuilds.StreamAgentReinitEvents(ctx, log, rw, r, reinitEvents) + + transmitter := agentsdk.NewSSEAgentReinitTransmitter(log, rw, r) + + err = transmitter.Transmit(ctx, reinitEvents) + if err != nil { + log.Error(ctx, "failed to stream agent reinit events", slog.Error(err)) + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error streaming agent reinitialization events.", + Detail: err.Error(), + }) + } } // convertProvisionedApps converts applications that are in the middle of provisioning process. diff --git a/codersdk/agentsdk/agentsdk.go b/codersdk/agentsdk/agentsdk.go index 107dd5f47fa92..37d0a463b3bc7 100644 --- a/codersdk/agentsdk/agentsdk.go +++ b/codersdk/agentsdk/agentsdk.go @@ -24,6 +24,7 @@ import ( "github.com/coder/coder/v2/agent/proto" "github.com/coder/coder/v2/apiversion" + "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/codersdk" drpcsdk "github.com/coder/coder/v2/codersdk/drpc" tailnetproto "github.com/coder/coder/v2/tailnet/proto" @@ -730,8 +731,86 @@ func (c *Client) WaitForReinit(ctx context.Context) (*ReinitializationEvent, err return nil, codersdk.ReadBodyAsError(res) } - nextEvent := codersdk.ServerSentEventReader(ctx, res.Body) + reinitEvent, err := NewSSEAgentReinitReceiver(res.Body).Receive(ctx) + if err != nil { + return nil, xerrors.Errorf("listening for reinitialization events: %w", err) + } + return reinitEvent, nil +} + +func WaitForReinitLoop(ctx context.Context, logger slog.Logger, client *Client) <-chan ReinitializationEvent { + reinitEvents := make(chan ReinitializationEvent) + + go func() { + for retrier := retry.New(100*time.Millisecond, 10*time.Second); retrier.Wait(ctx); { + logger.Debug(ctx, "waiting for agent reinitialization instructions") + reinitEvent, err := client.WaitForReinit(ctx) + if err != nil { + logger.Error(ctx, "failed to wait for agent reinitialization instructions", slog.Error(err)) + continue + } + select { + case <-ctx.Done(): + close(reinitEvents) + return + case reinitEvents <- *reinitEvent: + } + } + }() + + return reinitEvents +} + +func NewSSEAgentReinitTransmitter(logger slog.Logger, rw http.ResponseWriter, r *http.Request) *SSEAgentReinitTransmitter { + return &SSEAgentReinitTransmitter{logger: logger, rw: rw, r: r} +} + +type SSEAgentReinitTransmitter struct { + rw http.ResponseWriter + r *http.Request + logger slog.Logger +} + +func (s *SSEAgentReinitTransmitter) Transmit(ctx context.Context, reinitEvents <-chan ReinitializationEvent) error { + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + + sseSendEvent, sseSenderClosed, err := httpapi.ServerSentEventSender(s.rw, s.r) + if err != nil { + return xerrors.Errorf("failed to create sse transmitter: %w", err) + } + for { + select { + case <-ctx.Done(): + return ctx.Err() + case <-sseSenderClosed: + return xerrors.New("sse connection closed") + case reinitEvent := <-reinitEvents: + err := sseSendEvent(codersdk.ServerSentEvent{ + Type: codersdk.ServerSentEventTypeData, + Data: reinitEvent, + }) + if err != nil { + s.logger.Warn(ctx, "failed to send SSE response to trigger reinit", slog.Error(err)) + } + } + } +} + +func NewSSEAgentReinitReceiver(r io.ReadCloser) *SSEAgentReinitReceiver { + return &SSEAgentReinitReceiver{r: r} +} + +type SSEAgentReinitReceiver struct { + r io.ReadCloser +} + +func (s *SSEAgentReinitReceiver) Receive(ctx context.Context) (*ReinitializationEvent, error) { + nextEvent := codersdk.ServerSentEventReader(ctx, s.r) for { select { case <-ctx.Done(): @@ -763,26 +842,3 @@ func (c *Client) WaitForReinit(ctx context.Context) (*ReinitializationEvent, err } } } - -func WaitForReinitLoop(ctx context.Context, logger slog.Logger, client *Client) <-chan ReinitializationEvent { - reinitEvents := make(chan ReinitializationEvent) - - go func() { - for retrier := retry.New(100*time.Millisecond, 10*time.Second); retrier.Wait(ctx); { - logger.Debug(ctx, "waiting for agent reinitialization instructions") - reinitEvent, err := client.WaitForReinit(ctx) - if err != nil { - logger.Error(ctx, "failed to wait for agent reinitialization instructions", slog.Error(err)) - continue - } - select { - case <-ctx.Done(): - close(reinitEvents) - return - case reinitEvents <- *reinitEvent: - } - } - }() - - return reinitEvents -} diff --git a/codersdk/agentsdk/agentsdk_test.go b/codersdk/agentsdk/agentsdk_test.go new file mode 100644 index 0000000000000..ed044ae1534ee --- /dev/null +++ b/codersdk/agentsdk/agentsdk_test.go @@ -0,0 +1,125 @@ +package agentsdk_test + +import ( + "context" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + + "cdr.dev/slog/sloggers/slogtest" + "github.com/coder/coder/v2/codersdk/agentsdk" + "github.com/coder/coder/v2/testutil" +) + +func TestStreamAgentReinitEvents(t *testing.T) { + t.Parallel() + + t.Run("transmitted events are received", func(t *testing.T) { + t.Parallel() + + eventToSend := agentsdk.ReinitializationEvent{ + UserID: uuid.New(), + WorkspaceID: uuid.New(), + Reason: agentsdk.ReinitializeReasonPrebuildClaimed, + } + + events := make(chan agentsdk.ReinitializationEvent, 1) + events <- eventToSend + + transmitCtx := testutil.Context(t, testutil.WaitShort) + transmitErrCh := make(chan error, 1) + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + transmitter := agentsdk.NewSSEAgentReinitTransmitter(slogtest.Make(t, nil), w, r) + transmitErrCh <- transmitter.Transmit(transmitCtx, events) + })) + defer srv.Close() + + requestCtx := testutil.Context(t, testutil.WaitShort) + req, err := http.NewRequestWithContext(requestCtx, "GET", srv.URL, nil) + require.NoError(t, err) + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + receiveCtx := testutil.Context(t, testutil.WaitShort) + receiver := agentsdk.NewSSEAgentReinitReceiver(resp.Body) + sentEvent, receiveErr := receiver.Receive(receiveCtx) + require.Nil(t, receiveErr) + require.Equal(t, eventToSend, *sentEvent) + }) + + t.Run("doesn't transmit events if the transmitter context is canceled", func(t *testing.T) { + t.Parallel() + + eventToSend := agentsdk.ReinitializationEvent{ + UserID: uuid.New(), + WorkspaceID: uuid.New(), + Reason: agentsdk.ReinitializeReasonPrebuildClaimed, + } + + events := make(chan agentsdk.ReinitializationEvent, 1) + events <- eventToSend + + transmitCtx, cancelTransmit := context.WithCancel(testutil.Context(t, testutil.WaitShort)) + cancelTransmit() + transmitErrCh := make(chan error, 1) + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + transmitter := agentsdk.NewSSEAgentReinitTransmitter(slogtest.Make(t, nil), w, r) + transmitErrCh <- transmitter.Transmit(transmitCtx, events) + })) + + defer srv.Close() + + requestCtx := testutil.Context(t, testutil.WaitShort) + req, err := http.NewRequestWithContext(requestCtx, "GET", srv.URL, nil) + require.NoError(t, err) + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + receiveCtx := testutil.Context(t, testutil.WaitShort) + receiver := agentsdk.NewSSEAgentReinitReceiver(resp.Body) + sentEvent, receiveErr := receiver.Receive(receiveCtx) + require.Nil(t, sentEvent) + require.ErrorIs(t, receiveErr, io.EOF) + }) + + t.Run("does not receive events if the receiver context is canceled", func(t *testing.T) { + t.Parallel() + + eventToSend := agentsdk.ReinitializationEvent{ + UserID: uuid.New(), + WorkspaceID: uuid.New(), + Reason: agentsdk.ReinitializeReasonPrebuildClaimed, + } + + events := make(chan agentsdk.ReinitializationEvent, 1) + events <- eventToSend + + transmitCtx := testutil.Context(t, testutil.WaitShort) + transmitErrCh := make(chan error, 1) + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + transmitter := agentsdk.NewSSEAgentReinitTransmitter(slogtest.Make(t, nil), w, r) + transmitErrCh <- transmitter.Transmit(transmitCtx, events) + })) + defer srv.Close() + + requestCtx := testutil.Context(t, testutil.WaitShort) + req, err := http.NewRequestWithContext(requestCtx, "GET", srv.URL, nil) + require.NoError(t, err) + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + receiveCtx, cancelReceive := context.WithCancel(context.Background()) + cancelReceive() + receiver := agentsdk.NewSSEAgentReinitReceiver(resp.Body) + sentEvent, receiveErr := receiver.Receive(receiveCtx) + require.Nil(t, sentEvent) + require.ErrorIs(t, receiveErr, context.Canceled) + }) +} From 7e8dcee987719d372b13f3e89cec517fec5d6200 Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Thu, 1 May 2025 19:05:12 +0000 Subject: [PATCH 16/42] review notes --- coderd/prebuilds/claim_test.go | 25 +++++++------------ .../provisionerdserver_test.go | 2 +- 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/coderd/prebuilds/claim_test.go b/coderd/prebuilds/claim_test.go index 225337379409e..0e24931a50d45 100644 --- a/coderd/prebuilds/claim_test.go +++ b/coderd/prebuilds/claim_test.go @@ -6,7 +6,6 @@ import ( "time" "github.com/google/uuid" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/xerrors" @@ -44,13 +43,8 @@ func TestPubsubWorkspaceClaimPublisher(t *testing.T) { err = publisher.PublishWorkspaceClaim(claim) require.NoError(t, err) - // Verify the message was published - select { - case gotUserID := <-userIDCh: - require.Equal(t, userID, gotUserID) - case <-time.After(testutil.WaitShort): - t.Fatal("timeout waiting for claim") - } + gotUserID := testutil.TryReceive(testutil.Context(t, testutil.WaitShort), t, userIDCh) + require.Equal(t, userID, gotUserID) }) t.Run("fail to publish claim", func(t *testing.T) { @@ -66,7 +60,7 @@ func TestPubsubWorkspaceClaimPublisher(t *testing.T) { } err := publisher.PublishWorkspaceClaim(claim) - require.Error(t, err) + require.ErrorContains(t, err, "failed to trigger prebuilt workspace reinitialization") }) } @@ -88,7 +82,7 @@ func TestPubsubWorkspaceClaimListener(t *testing.T) { // Channel should be closed immediately due to context cancellation select { case _, ok := <-claims: - assert.False(t, ok) + require.False(t, ok) case <-time.After(testutil.WaitShort): t.Fatal("timeout waiting for closed channel") } @@ -106,7 +100,7 @@ func TestPubsubWorkspaceClaimListener(t *testing.T) { cancelFunc() select { case _, ok := <-claims: - assert.False(t, ok) + require.False(t, ok) case <-time.After(testutil.WaitShort): t.Fatal("timeout waiting for closed channel") } @@ -132,9 +126,9 @@ func TestPubsubWorkspaceClaimListener(t *testing.T) { // Verify we receive the claim select { case claim := <-claims: - assert.Equal(t, userID, claim.UserID) - assert.Equal(t, workspaceID, claim.WorkspaceID) - assert.Equal(t, agentsdk.ReinitializeReasonPrebuildClaimed, claim.Reason) + require.Equal(t, userID, claim.UserID) + require.Equal(t, workspaceID, claim.WorkspaceID) + require.Equal(t, agentsdk.ReinitializeReasonPrebuildClaimed, claim.Reason) case <-time.After(time.Second): t.Fatal("timeout waiting for claim") } @@ -173,8 +167,7 @@ func TestPubsubWorkspaceClaimListener(t *testing.T) { listener := prebuilds.NewPubsubWorkspaceClaimListener(ps, slogtest.Make(t, nil)) _, _, err := listener.ListenForWorkspaceClaims(context.Background(), uuid.New()) - require.Error(t, err) - assert.Contains(t, err.Error(), "failed to subscribe to prebuild claimed channel") + require.ErrorContains(t, err, "failed to subscribe to prebuild claimed channel") }) } diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go index 8f95c9b56fa9c..c32feda425916 100644 --- a/coderd/provisionerdserver/provisionerdserver_test.go +++ b/coderd/provisionerdserver/provisionerdserver_test.go @@ -1832,7 +1832,7 @@ func TestCompleteJob(t *testing.T) { require.NoError(t, err) defer cancel() - // WHEN the jop is completed + // WHEN the job is completed completedJob := proto.CompletedJob{ JobId: job.ID.String(), From a9b156715bc499ac76a9c4eb26746bcfbb6dfac9 Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Thu, 1 May 2025 19:24:48 +0000 Subject: [PATCH 17/42] make fmt lint --- cli/agent.go | 2 +- coderd/database/dbfake/dbfake.go | 1 + coderd/database/dbgen/dbgen.go | 1 + coderd/prebuilds/claim.go | 1 - coderd/provisionerdserver/provisionerdserver.go | 2 ++ provisioner/terraform/executor.go | 2 +- 6 files changed, 6 insertions(+), 3 deletions(-) diff --git a/cli/agent.go b/cli/agent.go index 5727c34b0d658..e7a51444e70fa 100644 --- a/cli/agent.go +++ b/cli/agent.go @@ -371,7 +371,7 @@ func (r *RootCmd) workspaceAgent() *serpent.Command { select { case <-ctx.Done(): - logger.Warn(ctx, "agent shutting down", slog.Error(ctx.Err()), slog.F("cause", context.Cause(ctx))) + logger.Warn(ctx, "agent shutting down", slog.Error(ctx.Err()), slog.Error(context.Cause(ctx))) mustExit = true case event := <-reinitEvents: logger.Warn(ctx, "agent received instruction to reinitialize", diff --git a/coderd/database/dbfake/dbfake.go b/coderd/database/dbfake/dbfake.go index fb2ea4bfd56b1..60370555e835e 100644 --- a/coderd/database/dbfake/dbfake.go +++ b/coderd/database/dbfake/dbfake.go @@ -400,6 +400,7 @@ func (t TemplateVersionBuilder) Do() TemplateVersionResponse { for _, presetParam := range t.presetParams { dbgen.PresetParameter(t.t, t.db, database.InsertPresetParametersParams{ + ID: uuid.New(), TemplateVersionPresetID: presetParam.TemplateVersionPresetID, Names: []string{presetParam.Name}, Values: []string{presetParam.Value}, diff --git a/coderd/database/dbgen/dbgen.go b/coderd/database/dbgen/dbgen.go index 193c107d51da9..5e40af4bef3d3 100644 --- a/coderd/database/dbgen/dbgen.go +++ b/coderd/database/dbgen/dbgen.go @@ -1211,6 +1211,7 @@ func Preset(t testing.TB, db database.Store, seed database.InsertPresetParams) d func PresetParameter(t testing.TB, db database.Store, seed database.InsertPresetParametersParams) []database.TemplateVersionPresetParameter { parameters, err := db.InsertPresetParameters(genCtx, database.InsertPresetParametersParams{ + ID: takeFirst(seed.ID, uuid.New()), TemplateVersionPresetID: takeFirst(seed.TemplateVersionPresetID, uuid.New()), Names: takeFirstSlice(seed.Names, []string{testutil.GetRandomName(t)}), Values: takeFirstSlice(seed.Values, []string{testutil.GetRandomName(t)}), diff --git a/coderd/prebuilds/claim.go b/coderd/prebuilds/claim.go index 5ff40cbf9bebe..babf6ab48cb0f 100644 --- a/coderd/prebuilds/claim.go +++ b/coderd/prebuilds/claim.go @@ -65,7 +65,6 @@ func (p PubsubWorkspaceClaimListener) ListenForWorkspaceClaims(ctx context.Conte default: } }) - if err != nil { close(workspaceClaims) return func() {}, nil, xerrors.Errorf("failed to subscribe to prebuild claimed channel: %w", err) diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index ae4ab689b4f25..a8dcb8c16bd36 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -1902,6 +1902,7 @@ func InsertWorkspacePresetAndParameters(ctx context.Context, db database.Store, } } dbPreset, err := tx.InsertPreset(ctx, database.InsertPresetParams{ + ID: uuid.New(), TemplateVersionID: templateVersionID, Name: protoPreset.Name, CreatedAt: t, @@ -1922,6 +1923,7 @@ func InsertWorkspacePresetAndParameters(ctx context.Context, db database.Store, presetParameterValues = append(presetParameterValues, parameter.Value) } _, err = tx.InsertPresetParameters(ctx, database.InsertPresetParametersParams{ + ID: uuid.New(), TemplateVersionPresetID: dbPreset.ID, Names: presetParameterNames, Values: presetParameterValues, diff --git a/provisioner/terraform/executor.go b/provisioner/terraform/executor.go index 6a61e91e46a87..79f3aa0ebcaa8 100644 --- a/provisioner/terraform/executor.go +++ b/provisioner/terraform/executor.go @@ -391,7 +391,7 @@ func (e *executor) logResourceReplacements(ctx context.Context, plan *tfjson.Pla if count > 0 { e.server.logger.Warn(ctx, "plan introduces resource changes", slog.F("count", count)) for n, p := range replacements { - e.server.logger.Warn(ctx, "resource will be replaced!", slog.F("name", n), slog.F("replacement_paths", strings.Join(p, ","))) + e.server.logger.Warn(ctx, "resource will be replaced", slog.F("name", n), slog.F("replacement_paths", strings.Join(p, ","))) } } } From 21ee97092516bbe272a6b214bd86d51da8ddd00f Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Thu, 1 May 2025 19:33:51 +0000 Subject: [PATCH 18/42] remove go mod replace --- codersdk/agentsdk/agentsdk.go | 8 +++++++- go.mod | 3 --- go.sum | 2 ++ 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/codersdk/agentsdk/agentsdk.go b/codersdk/agentsdk/agentsdk.go index 37d0a463b3bc7..73284de2581f0 100644 --- a/codersdk/agentsdk/agentsdk.go +++ b/codersdk/agentsdk/agentsdk.go @@ -795,7 +795,13 @@ func (s *SSEAgentReinitTransmitter) Transmit(ctx context.Context, reinitEvents < Data: reinitEvent, }) if err != nil { - s.logger.Warn(ctx, "failed to send SSE response to trigger reinit", slog.Error(err)) + s.logger.Warn( + ctx, + "failed to send SSE to trigger agent reinitialization", + slog.Error(err), + slog.F("user_id", reinitEvent.UserID), + slog.F("workspace_id", reinitEvent.WorkspaceID), + ) } } } diff --git a/go.mod b/go.mod index 62bb22f9c81e3..8ff0ba1fa2376 100644 --- a/go.mod +++ b/go.mod @@ -530,6 +530,3 @@ require ( google.golang.org/genai v0.7.0 // indirect k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect ) - -// TODO: remove this once code merged upstream -replace github.com/coder/terraform-provider-coder/v2 => ../terraform-provider-coder/ diff --git a/go.sum b/go.sum index c358dd900b070..fc05152d34122 100644 --- a/go.sum +++ b/go.sum @@ -921,6 +921,8 @@ github.com/coder/tailscale v1.1.1-0.20250422090654-5090e715905e h1:nope/SZfoLB9M github.com/coder/tailscale v1.1.1-0.20250422090654-5090e715905e/go.mod h1:1ggFFdHTRjPRu9Yc1yA7nVHBYB50w9Ce7VIXNqcW6Ko= github.com/coder/terraform-config-inspect v0.0.0-20250107175719-6d06d90c630e h1:JNLPDi2P73laR1oAclY6jWzAbucf70ASAvf5mh2cME0= github.com/coder/terraform-config-inspect v0.0.0-20250107175719-6d06d90c630e/go.mod h1:Gz/z9Hbn+4KSp8A2FBtNszfLSdT2Tn/uAKGuVqqWmDI= +github.com/coder/terraform-provider-coder/v2 v2.4.0-pre1.0.20250417100258-c86bb5c3ddcd h1:FsIG6Fd0YOEK7D0Hl/CJywRA+Y6Gd5RQbSIa2L+/BmE= +github.com/coder/terraform-provider-coder/v2 v2.4.0-pre1.0.20250417100258-c86bb5c3ddcd/go.mod h1:56/KdGYaA+VbwXJbTI8CA57XPfnuTxN8rjxbR34PbZw= github.com/coder/trivy v0.0.0-20250409153844-e6b004bc465a h1:yryP7e+IQUAArlycH4hQrjXQ64eRNbxsV5/wuVXHgME= github.com/coder/trivy v0.0.0-20250409153844-e6b004bc465a/go.mod h1:dDvq9axp3kZsT63gY2Znd1iwzfqDq3kXbQnccIrjRYY= github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE= From e54d7e7796b42505a4ccf9d2be6576c41aaa23e5 Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Thu, 1 May 2025 19:35:42 +0000 Subject: [PATCH 19/42] remove defunct logging --- agent/reaper/reaper_unix.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/agent/reaper/reaper_unix.go b/agent/reaper/reaper_unix.go index 5a7c7d2f51efa..35ce9bfaa1c48 100644 --- a/agent/reaper/reaper_unix.go +++ b/agent/reaper/reaper_unix.go @@ -3,7 +3,6 @@ package reaper import ( - "fmt" "os" "os/signal" "syscall" @@ -30,10 +29,6 @@ func catchSignals(pid int, sigs []os.Signal) { s := <-sc sig, ok := s.(syscall.Signal) if ok { - // TODO: - // Tried using a logger here but the I/O streams are already closed at this point... - // Why is os.Stderr still working then? - _, _ = fmt.Fprintf(os.Stderr, "reaper caught %q signal, killing process %v\n", sig.String(), pid) _ = syscall.Kill(pid, sig) } } From 27998586920ac91d70f82b231ce1a5e3739e595d Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Fri, 2 May 2025 07:11:55 +0000 Subject: [PATCH 20/42] update dependency on terraform-provider-coder --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 8ff0ba1fa2376..c7e3dba436c35 100644 --- a/go.mod +++ b/go.mod @@ -323,7 +323,7 @@ require ( github.com/google/btree v1.1.2 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/nftables v0.2.0 // indirect - github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 // indirect + github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect diff --git a/go.sum b/go.sum index fc05152d34122..8ced0f25cc35d 100644 --- a/go.sum +++ b/go.sum @@ -1282,8 +1282,8 @@ github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 h1:y3N7Bm7Y9/CtpiVkw/ZWj6lSlDF3F74SfKwfTCer72Q= -github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= From 1d93003b331cc9c02a2814c505166d11b4abc317 Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Fri, 2 May 2025 07:14:20 +0000 Subject: [PATCH 21/42] update dependency on terraform-provider-coder --- go.mod | 5 ++++- go.sum | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index c7e3dba436c35..a06a62db100f4 100644 --- a/go.mod +++ b/go.mod @@ -101,7 +101,7 @@ require ( github.com/coder/quartz v0.1.2 github.com/coder/retry v1.5.1 github.com/coder/serpent v0.10.0 - github.com/coder/terraform-provider-coder/v2 v2.4.0-pre1.0.20250417100258-c86bb5c3ddcd + github.com/coder/terraform-provider-coder/v2 v2.4.0-pre1.0.20250502065705-5648efbf6db0 github.com/coder/websocket v1.8.13 github.com/coder/wgtunnel v0.1.13-0.20240522110300-ade90dfb2da0 github.com/coreos/go-oidc/v3 v3.14.1 @@ -513,7 +513,10 @@ require ( github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/hashicorp/go-getter v1.7.8 // indirect + github.com/hashicorp/go-plugin v1.6.2 // indirect github.com/hashicorp/go-safetemp v1.0.0 // indirect + github.com/hashicorp/terraform-registry-address v0.2.4 // indirect + github.com/hashicorp/terraform-svchost v0.1.1 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/moby/sys/user v0.3.0 // indirect github.com/openai/openai-go v0.1.0-beta.6 // indirect diff --git a/go.sum b/go.sum index 8ced0f25cc35d..bc5aa76766205 100644 --- a/go.sum +++ b/go.sum @@ -923,6 +923,8 @@ github.com/coder/terraform-config-inspect v0.0.0-20250107175719-6d06d90c630e h1: github.com/coder/terraform-config-inspect v0.0.0-20250107175719-6d06d90c630e/go.mod h1:Gz/z9Hbn+4KSp8A2FBtNszfLSdT2Tn/uAKGuVqqWmDI= github.com/coder/terraform-provider-coder/v2 v2.4.0-pre1.0.20250417100258-c86bb5c3ddcd h1:FsIG6Fd0YOEK7D0Hl/CJywRA+Y6Gd5RQbSIa2L+/BmE= github.com/coder/terraform-provider-coder/v2 v2.4.0-pre1.0.20250417100258-c86bb5c3ddcd/go.mod h1:56/KdGYaA+VbwXJbTI8CA57XPfnuTxN8rjxbR34PbZw= +github.com/coder/terraform-provider-coder/v2 v2.4.0-pre1.0.20250502065705-5648efbf6db0 h1:SyV2O5cEp6pSrgUjjKFtQaz+J0JV/nPt0ORS5gzmaQY= +github.com/coder/terraform-provider-coder/v2 v2.4.0-pre1.0.20250502065705-5648efbf6db0/go.mod h1:2kaBpn5k9ZWtgKq5k4JbkVZG9DzEqR4mJSmpdshcO+s= github.com/coder/trivy v0.0.0-20250409153844-e6b004bc465a h1:yryP7e+IQUAArlycH4hQrjXQ64eRNbxsV5/wuVXHgME= github.com/coder/trivy v0.0.0-20250409153844-e6b004bc465a/go.mod h1:dDvq9axp3kZsT63gY2Znd1iwzfqDq3kXbQnccIrjRYY= github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE= From 763fc12bfb5b93da2b4d99e659ca7f9d46eecd3f Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Fri, 2 May 2025 07:19:45 +0000 Subject: [PATCH 22/42] go mod tidy --- go.mod | 3 --- go.sum | 2 -- 2 files changed, 5 deletions(-) diff --git a/go.mod b/go.mod index a06a62db100f4..063b128f26308 100644 --- a/go.mod +++ b/go.mod @@ -513,10 +513,7 @@ require ( github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/hashicorp/go-getter v1.7.8 // indirect - github.com/hashicorp/go-plugin v1.6.2 // indirect github.com/hashicorp/go-safetemp v1.0.0 // indirect - github.com/hashicorp/terraform-registry-address v0.2.4 // indirect - github.com/hashicorp/terraform-svchost v0.1.1 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/moby/sys/user v0.3.0 // indirect github.com/openai/openai-go v0.1.0-beta.6 // indirect diff --git a/go.sum b/go.sum index bc5aa76766205..dc023cc7950be 100644 --- a/go.sum +++ b/go.sum @@ -921,8 +921,6 @@ github.com/coder/tailscale v1.1.1-0.20250422090654-5090e715905e h1:nope/SZfoLB9M github.com/coder/tailscale v1.1.1-0.20250422090654-5090e715905e/go.mod h1:1ggFFdHTRjPRu9Yc1yA7nVHBYB50w9Ce7VIXNqcW6Ko= github.com/coder/terraform-config-inspect v0.0.0-20250107175719-6d06d90c630e h1:JNLPDi2P73laR1oAclY6jWzAbucf70ASAvf5mh2cME0= github.com/coder/terraform-config-inspect v0.0.0-20250107175719-6d06d90c630e/go.mod h1:Gz/z9Hbn+4KSp8A2FBtNszfLSdT2Tn/uAKGuVqqWmDI= -github.com/coder/terraform-provider-coder/v2 v2.4.0-pre1.0.20250417100258-c86bb5c3ddcd h1:FsIG6Fd0YOEK7D0Hl/CJywRA+Y6Gd5RQbSIa2L+/BmE= -github.com/coder/terraform-provider-coder/v2 v2.4.0-pre1.0.20250417100258-c86bb5c3ddcd/go.mod h1:56/KdGYaA+VbwXJbTI8CA57XPfnuTxN8rjxbR34PbZw= github.com/coder/terraform-provider-coder/v2 v2.4.0-pre1.0.20250502065705-5648efbf6db0 h1:SyV2O5cEp6pSrgUjjKFtQaz+J0JV/nPt0ORS5gzmaQY= github.com/coder/terraform-provider-coder/v2 v2.4.0-pre1.0.20250502065705-5648efbf6db0/go.mod h1:2kaBpn5k9ZWtgKq5k4JbkVZG9DzEqR4mJSmpdshcO+s= github.com/coder/trivy v0.0.0-20250409153844-e6b004bc465a h1:yryP7e+IQUAArlycH4hQrjXQ64eRNbxsV5/wuVXHgME= From 0f879c7ec61c92d0a65e6c41c0eb563f8a119988 Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Fri, 2 May 2025 07:32:50 +0000 Subject: [PATCH 23/42] make -B gen --- coderd/apidoc/docs.go | 9 ++++++--- coderd/apidoc/swagger.json | 9 ++++++--- coderd/prebuilds/claim_test.go | 2 +- docs/reference/api/agents.md | 5 +++-- docs/reference/api/schemas.md | 14 ++++++++------ 5 files changed, 24 insertions(+), 15 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index d99733eaa7226..c75908a5c3de8 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -10325,11 +10325,14 @@ const docTemplate = `{ "agentsdk.ReinitializationEvent": { "type": "object", "properties": { - "message": { - "type": "string" - }, "reason": { "$ref": "#/definitions/agentsdk.ReinitializationReason" + }, + "userID": { + "type": "string" + }, + "workspaceID": { + "type": "string" } } }, diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index eeecc485cccab..a639529fea1e9 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -9158,11 +9158,14 @@ "agentsdk.ReinitializationEvent": { "type": "object", "properties": { - "message": { - "type": "string" - }, "reason": { "$ref": "#/definitions/agentsdk.ReinitializationReason" + }, + "userID": { + "type": "string" + }, + "workspaceID": { + "type": "string" } } }, diff --git a/coderd/prebuilds/claim_test.go b/coderd/prebuilds/claim_test.go index 0e24931a50d45..32135c52fce74 100644 --- a/coderd/prebuilds/claim_test.go +++ b/coderd/prebuilds/claim_test.go @@ -60,7 +60,7 @@ func TestPubsubWorkspaceClaimPublisher(t *testing.T) { } err := publisher.PublishWorkspaceClaim(claim) - require.ErrorContains(t, err, "failed to trigger prebuilt workspace reinitialization") + require.ErrorContains(t, err, "failed to trigger prebuilt workspace agent reinitialization") }) } diff --git a/docs/reference/api/agents.md b/docs/reference/api/agents.md index 0fa4c6a284f09..934efbb2a7fca 100644 --- a/docs/reference/api/agents.md +++ b/docs/reference/api/agents.md @@ -489,8 +489,9 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/me/reinit \ ```json { - "message": "string", - "reason": "prebuild_claimed" + "reason": "prebuild_claimed", + "userID": "string", + "workspaceID": "string" } ``` diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 627815b94f811..7276bee19b89d 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -186,17 +186,19 @@ ```json { - "message": "string", - "reason": "prebuild_claimed" + "reason": "prebuild_claimed", + "userID": "string", + "workspaceID": "string" } ``` ### Properties -| Name | Type | Required | Restrictions | Description | -|-----------|--------------------------------------------------------------------|----------|--------------|-------------| -| `message` | string | false | | | -| `reason` | [agentsdk.ReinitializationReason](#agentsdkreinitializationreason) | false | | | +| Name | Type | Required | Restrictions | Description | +|---------------|--------------------------------------------------------------------|----------|--------------|-------------| +| `reason` | [agentsdk.ReinitializationReason](#agentsdkreinitializationreason) | false | | | +| `userID` | string | false | | | +| `workspaceID` | string | false | | | ## agentsdk.ReinitializationReason From 61784c9df0ca503916afd2a9c63c89bcc58ca237 Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Fri, 2 May 2025 12:41:10 +0000 Subject: [PATCH 24/42] dont require ids to InsertPresetParameters --- coderd/database/dbfake/dbfake.go | 1 - coderd/database/dbgen/dbgen.go | 1 - coderd/database/queries.sql.go | 15 ++++----------- coderd/database/queries/presets.sql | 3 +-- 4 files changed, 5 insertions(+), 15 deletions(-) diff --git a/coderd/database/dbfake/dbfake.go b/coderd/database/dbfake/dbfake.go index 60370555e835e..fb2ea4bfd56b1 100644 --- a/coderd/database/dbfake/dbfake.go +++ b/coderd/database/dbfake/dbfake.go @@ -400,7 +400,6 @@ func (t TemplateVersionBuilder) Do() TemplateVersionResponse { for _, presetParam := range t.presetParams { dbgen.PresetParameter(t.t, t.db, database.InsertPresetParametersParams{ - ID: uuid.New(), TemplateVersionPresetID: presetParam.TemplateVersionPresetID, Names: []string{presetParam.Name}, Values: []string{presetParam.Value}, diff --git a/coderd/database/dbgen/dbgen.go b/coderd/database/dbgen/dbgen.go index 5e40af4bef3d3..193c107d51da9 100644 --- a/coderd/database/dbgen/dbgen.go +++ b/coderd/database/dbgen/dbgen.go @@ -1211,7 +1211,6 @@ func Preset(t testing.TB, db database.Store, seed database.InsertPresetParams) d func PresetParameter(t testing.TB, db database.Store, seed database.InsertPresetParametersParams) []database.TemplateVersionPresetParameter { parameters, err := db.InsertPresetParameters(genCtx, database.InsertPresetParametersParams{ - ID: takeFirst(seed.ID, uuid.New()), TemplateVersionPresetID: takeFirst(seed.TemplateVersionPresetID, uuid.New()), Names: takeFirstSlice(seed.Names, []string{testutil.GetRandomName(t)}), Values: takeFirstSlice(seed.Values, []string{testutil.GetRandomName(t)}), diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 4bdb37430d581..e41db563e1c83 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -6526,29 +6526,22 @@ func (q *sqlQuerier) InsertPreset(ctx context.Context, arg InsertPresetParams) ( const insertPresetParameters = `-- name: InsertPresetParameters :many INSERT INTO - template_version_preset_parameters (id, template_version_preset_id, name, value) + template_version_preset_parameters (template_version_preset_id, name, value) SELECT $1, - $2, - unnest($3 :: TEXT[]), - unnest($4 :: TEXT[]) + unnest($2 :: TEXT[]), + unnest($3 :: TEXT[]) RETURNING id, template_version_preset_id, name, value ` type InsertPresetParametersParams struct { - ID uuid.UUID `db:"id" json:"id"` TemplateVersionPresetID uuid.UUID `db:"template_version_preset_id" json:"template_version_preset_id"` Names []string `db:"names" json:"names"` Values []string `db:"values" json:"values"` } func (q *sqlQuerier) InsertPresetParameters(ctx context.Context, arg InsertPresetParametersParams) ([]TemplateVersionPresetParameter, error) { - rows, err := q.db.QueryContext(ctx, insertPresetParameters, - arg.ID, - arg.TemplateVersionPresetID, - pq.Array(arg.Names), - pq.Array(arg.Values), - ) + rows, err := q.db.QueryContext(ctx, insertPresetParameters, arg.TemplateVersionPresetID, pq.Array(arg.Names), pq.Array(arg.Values)) if err != nil { return nil, err } diff --git a/coderd/database/queries/presets.sql b/coderd/database/queries/presets.sql index 5f0aa28db8fe1..6d5646a285b4a 100644 --- a/coderd/database/queries/presets.sql +++ b/coderd/database/queries/presets.sql @@ -18,9 +18,8 @@ VALUES ( -- name: InsertPresetParameters :many INSERT INTO - template_version_preset_parameters (id, template_version_preset_id, name, value) + template_version_preset_parameters (template_version_preset_id, name, value) SELECT - @id, @template_version_preset_id, unnest(@names :: TEXT[]), unnest(@values :: TEXT[]) From 604eb278c793ae80b7010e1ddc2b9ef7a6ea13d2 Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Fri, 2 May 2025 12:42:01 +0000 Subject: [PATCH 25/42] dont require ids to InsertPresetParameters --- coderd/provisionerdserver/provisionerdserver.go | 1 - 1 file changed, 1 deletion(-) diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index a8dcb8c16bd36..2f5e74fb25c98 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -1923,7 +1923,6 @@ func InsertWorkspacePresetAndParameters(ctx context.Context, db database.Store, presetParameterValues = append(presetParameterValues, parameter.Value) } _, err = tx.InsertPresetParameters(ctx, database.InsertPresetParametersParams{ - ID: uuid.New(), TemplateVersionPresetID: dbPreset.ID, Names: presetParameterNames, Values: presetParameterValues, From bf4d2cf903af8f16d2db5ef7e991410ad6a652b0 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Fri, 2 May 2025 15:30:18 +0200 Subject: [PATCH 26/42] fix: set the running agent token Signed-off-by: Danny Kopping --- coderd/workspaces.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 446401121c4a2..2153a6d63cb39 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -712,6 +712,10 @@ func createWorkspace( if len(agents) > 1 { return xerrors.Errorf("multiple agents are not yet supported in prebuilt workspaces") } + + agentTokensByAgentID = map[uuid.UUID]string{ + agents[0].ID: agents[0].AuthToken.String(), + } } // We have to refetch the workspace for the joined in fields. From 38b4f0d1ae482aa26b2499e6f872ee161da0bae3 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Mon, 5 May 2025 14:40:18 +0200 Subject: [PATCH 27/42] fix: use http client without timeout like we do in connectRPCVersion Signed-off-by: Danny Kopping --- codersdk/agentsdk/agentsdk.go | 36 +++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/codersdk/agentsdk/agentsdk.go b/codersdk/agentsdk/agentsdk.go index 73284de2581f0..d7eae52cec96d 100644 --- a/codersdk/agentsdk/agentsdk.go +++ b/codersdk/agentsdk/agentsdk.go @@ -710,18 +710,30 @@ func PrebuildClaimedChannel(id uuid.UUID) string { // - ping: ignored, keepalive // - prebuild claimed: a prebuilt workspace is claimed, so the agent must reinitialize. func (c *Client) WaitForReinit(ctx context.Context) (*ReinitializationEvent, error) { - // TODO: allow configuring httpclient - c.SDK.HTTPClient.Timeout = time.Hour * 24 - - // TODO (sasswart): tried the following to fix the above, it won't work. The shorter timeout wins. - // I also considered cloning c.SDK.HTTPClient and setting the timeout on the cloned client. - // That won't work because we can't pass the cloned HTTPClient into s.SDK.Request. - // Looks like we're going to need a separate client to be able to have a longer timeout. - // - // timeoutCtx, cancelTimeoutCtx := context.WithTimeout(ctx, 24*time.Hour) - // defer cancelTimeoutCtx() - - res, err := c.SDK.Request(ctx, http.MethodGet, "/api/v2/workspaceagents/me/reinit", nil) + rpcURL, err := c.SDK.URL.Parse("/api/v2/workspaceagents/me/reinit") + if err != nil { + return nil, xerrors.Errorf("parse url: %w", err) + } + + jar, err := cookiejar.New(nil) + if err != nil { + return nil, xerrors.Errorf("create cookie jar: %w", err) + } + jar.SetCookies(rpcURL, []*http.Cookie{{ + Name: codersdk.SessionTokenCookie, + Value: c.SDK.SessionToken(), + }}) + httpClient := &http.Client{ + Jar: jar, + Transport: c.SDK.HTTPClient.Transport, + } + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, rpcURL.String(), nil) + if err != nil { + return nil, xerrors.Errorf("build request: %w", err) + } + + res, err := httpClient.Do(req) if err != nil { return nil, xerrors.Errorf("execute request: %w", err) } From 20df5388676a3c353b2517f6a8f37c10744aa3db Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Tue, 6 May 2025 19:21:51 +0000 Subject: [PATCH 28/42] review notes --- agent/agent.go | 6 +- cli/agent.go | 4 +- cli/agent_test.go | 60 ------------ coderd/coderdtest/coderdtest.go | 10 +- coderd/prebuilds/claim.go | 19 ++-- coderd/prebuilds/claim_test.go | 55 ++--------- .../provisionerdserver/provisionerdserver.go | 30 +++--- coderd/workspaceagents.go | 12 ++- coderd/workspaceagents_test.go | 91 +++++++++++++++++++ coderd/workspaces.go | 22 +---- coderd/wsbuilder/wsbuilder.go | 16 +--- codersdk/agentsdk/agentsdk.go | 37 +++++--- enterprise/coderd/workspaceagents_test.go | 29 +++++- enterprise/coderd/workspaces_test.go | 78 ++++++++++++++++ 14 files changed, 287 insertions(+), 182 deletions(-) diff --git a/agent/agent.go b/agent/agent.go index f7c702e7fdd52..492a915f4b4ab 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -36,9 +36,6 @@ import ( "tailscale.com/util/clientmetric" "cdr.dev/slog" - - "github.com/coder/retry" - "github.com/coder/clistat" "github.com/coder/coder/v2/agent/agentcontainers" "github.com/coder/coder/v2/agent/agentexec" @@ -56,6 +53,7 @@ import ( "github.com/coder/coder/v2/tailnet" tailnetproto "github.com/coder/coder/v2/tailnet/proto" "github.com/coder/quartz" + "github.com/coder/retry" ) const ( @@ -1052,7 +1050,7 @@ func (a *agent) run() (retErr error) { err = connMan.wait() if err != nil { - a.logger.Warn(context.Background(), "connection manager errored", slog.Error(err)) + a.logger.Info(context.Background(), "connection manager errored", slog.Error(err)) } return err } diff --git a/cli/agent.go b/cli/agent.go index e7a51444e70fa..caf3e07d7d98a 100644 --- a/cli/agent.go +++ b/cli/agent.go @@ -371,10 +371,10 @@ func (r *RootCmd) workspaceAgent() *serpent.Command { select { case <-ctx.Done(): - logger.Warn(ctx, "agent shutting down", slog.Error(ctx.Err()), slog.Error(context.Cause(ctx))) + logger.Info(ctx, "agent shutting down", slog.Error(context.Cause(ctx))) mustExit = true case event := <-reinitEvents: - logger.Warn(ctx, "agent received instruction to reinitialize", + logger.Info(ctx, "agent received instruction to reinitialize", slog.F("user_id", event.UserID), slog.F("workspace_id", event.WorkspaceID), slog.F("reason", event.Reason)) } diff --git a/cli/agent_test.go b/cli/agent_test.go index c126df477de22..0a948c0c84e9a 100644 --- a/cli/agent_test.go +++ b/cli/agent_test.go @@ -21,10 +21,7 @@ import ( "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbfake" - "github.com/coder/coder/v2/coderd/database/dbtestutil" - "github.com/coder/coder/v2/coderd/prebuilds" "github.com/coder/coder/v2/codersdk" - "github.com/coder/coder/v2/codersdk/agentsdk" "github.com/coder/coder/v2/codersdk/workspacesdk" "github.com/coder/coder/v2/provisionersdk/proto" "github.com/coder/coder/v2/testutil" @@ -324,63 +321,6 @@ func TestWorkspaceAgent(t *testing.T) { }) } -func TestAgent_Prebuild(t *testing.T) { - t.Parallel() - - db, pubsub := dbtestutil.NewDB(t) - client := coderdtest.New(t, &coderdtest.Options{ - Database: db, - Pubsub: pubsub, - }) - user := coderdtest.CreateFirstUser(t, client) - presetID := uuid.New() - tv := dbfake.TemplateVersion(t, db).Seed(database.TemplateVersion{ - OrganizationID: user.OrganizationID, - CreatedBy: user.UserID, - }).Preset(database.TemplateVersionPreset{ - ID: presetID, - }).Do() - r := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{ - OwnerID: prebuilds.SystemUserID, - TemplateID: tv.Template.ID, - }).WithAgent(func(a []*proto.Agent) []*proto.Agent { - a[0].Scripts = []*proto.Script{ - { - DisplayName: "Prebuild Test Script", - Script: "sleep 5", // Make reinitialization take long enough to assert that it happened - RunOnStart: true, - }, - } - return a - }).Do() - - // Spin up an agent - logDir := t.TempDir() - inv, _ := clitest.New(t, - "agent", - "--auth", "token", - "--agent-token", r.AgentToken, - "--agent-url", client.URL.String(), - "--log-dir", logDir, - ) - clitest.Start(t, inv) - - // Check that the agent is in a happy steady state - waiter := coderdtest.NewWorkspaceAgentWaiter(t, client, r.Workspace.ID) - waiter.WaitFor(coderdtest.AgentsReady) - - // Trigger reinitialization - channel := agentsdk.PrebuildClaimedChannel(r.Workspace.ID) - err := pubsub.Publish(channel, []byte(user.UserID.String())) - require.NoError(t, err) - - // Check that the agent reinitializes - waiter.WaitFor(coderdtest.AgentsNotReady) - - // Check that reinitialization completed - waiter.WaitFor(coderdtest.AgentsReady) -} - func matchAgentWithVersion(rs []codersdk.WorkspaceResource) bool { if len(rs) < 1 { return false diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index 2d525732237a9..93e17048e054e 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -1105,27 +1105,27 @@ func (w WorkspaceAgentWaiter) MatchResources(m func([]codersdk.WorkspaceResource return w } -// WaitForCriterium represents a boolean assertion to be made against each agent -// that a given WorkspaceAgentWaited knows about. Each WaitForCriterium should apply +// WaitForAgentFn represents a boolean assertion to be made against each agent +// that a given WorkspaceAgentWaited knows about. Each WaitForAgentFn should apply // the check to a single agent, but it should be named for plural, because `func (w WorkspaceAgentWaiter) WaitFor` // applies the check to all agents that it is aware of. This ensures that the public API of the waiter // reads correctly. For example: // // waiter := coderdtest.NewWorkspaceAgentWaiter(t, client, r.Workspace.ID) // waiter.WaitFor(coderdtest.AgentsReady) -type WaitForCriterium func(agent codersdk.WorkspaceAgent) bool +type WaitForAgentFn func(agent codersdk.WorkspaceAgent) bool // AgentsReady checks that the latest lifecycle state of an agent is "Ready". func AgentsReady(agent codersdk.WorkspaceAgent) bool { return agent.LifecycleState == codersdk.WorkspaceAgentLifecycleReady } -// AgentsReady checks that the latest lifecycle state of an agent is anything except "Ready". +// AgentsNotReady checks that the latest lifecycle state of an agent is anything except "Ready". func AgentsNotReady(agent codersdk.WorkspaceAgent) bool { return !AgentsReady(agent) } -func (w WorkspaceAgentWaiter) WaitFor(criteria ...WaitForCriterium) { +func (w WorkspaceAgentWaiter) WaitFor(criteria ...WaitForAgentFn) { w.t.Helper() agentNamesMap := make(map[string]struct{}, len(w.agentNames)) diff --git a/coderd/prebuilds/claim.go b/coderd/prebuilds/claim.go index babf6ab48cb0f..64088aa07d0ad 100644 --- a/coderd/prebuilds/claim.go +++ b/coderd/prebuilds/claim.go @@ -37,14 +37,17 @@ type PubsubWorkspaceClaimListener struct { ps pubsub.Pubsub } -func (p PubsubWorkspaceClaimListener) ListenForWorkspaceClaims(ctx context.Context, workspaceID uuid.UUID) (func(), <-chan agentsdk.ReinitializationEvent, error) { +// ListenForWorkspaceClaims subscribes to a pubsub channel and sends any received events on the chan that it returns. +// pubsub.Pubsub does not communicate when its last callback has been called after it has been closed. As such the chan +// returned by this method is never closed. Call the returned cancel() function to close the subscription when it is no longer needed. +// cancel() will be called if ctx expires or is canceled. +func (p PubsubWorkspaceClaimListener) ListenForWorkspaceClaims(ctx context.Context, workspaceID uuid.UUID, reinitEvents chan<- agentsdk.ReinitializationEvent) (func(), error) { select { case <-ctx.Done(): - return func() {}, nil, ctx.Err() + return func() {}, ctx.Err() default: } - workspaceClaims := make(chan agentsdk.ReinitializationEvent, 1) cancelSub, err := p.ps.Subscribe(agentsdk.PrebuildClaimedChannel(workspaceID), func(inner context.Context, id []byte) { claimantID, err := uuid.ParseBytes(id) if err != nil { @@ -56,25 +59,25 @@ func (p PubsubWorkspaceClaimListener) ListenForWorkspaceClaims(ctx context.Conte WorkspaceID: workspaceID, Reason: agentsdk.ReinitializeReasonPrebuildClaimed, } + select { case <-ctx.Done(): return case <-inner.Done(): return - case workspaceClaims <- claim: + case reinitEvents <- claim: default: + return } }) if err != nil { - close(workspaceClaims) - return func() {}, nil, xerrors.Errorf("failed to subscribe to prebuild claimed channel: %w", err) + return func() {}, xerrors.Errorf("failed to subscribe to prebuild claimed channel: %w", err) } var once sync.Once cancel := func() { once.Do(func() { cancelSub() - close(workspaceClaims) }) } @@ -83,5 +86,5 @@ func (p PubsubWorkspaceClaimListener) ListenForWorkspaceClaims(ctx context.Conte cancel() }() - return cancel, workspaceClaims, nil + return cancel, nil } diff --git a/coderd/prebuilds/claim_test.go b/coderd/prebuilds/claim_test.go index 32135c52fce74..b404cdcfde193 100644 --- a/coderd/prebuilds/claim_test.go +++ b/coderd/prebuilds/claim_test.go @@ -66,55 +66,17 @@ func TestPubsubWorkspaceClaimPublisher(t *testing.T) { func TestPubsubWorkspaceClaimListener(t *testing.T) { t.Parallel() - t.Run("stops listening if context canceled", func(t *testing.T) { - t.Parallel() - - ps := pubsub.NewInMemory() - listener := prebuilds.NewPubsubWorkspaceClaimListener(ps, slogtest.Make(t, nil)) - - ctx, cancel := context.WithCancel(context.Background()) - - cancelFunc, claims, err := listener.ListenForWorkspaceClaims(ctx, uuid.New()) - require.NoError(t, err) - defer cancelFunc() - - cancel() - // Channel should be closed immediately due to context cancellation - select { - case _, ok := <-claims: - require.False(t, ok) - case <-time.After(testutil.WaitShort): - t.Fatal("timeout waiting for closed channel") - } - }) - - t.Run("stops listening if cancel func is called", func(t *testing.T) { - t.Parallel() - - ps := pubsub.NewInMemory() - listener := prebuilds.NewPubsubWorkspaceClaimListener(ps, slogtest.Make(t, nil)) - - cancelFunc, claims, err := listener.ListenForWorkspaceClaims(context.Background(), uuid.New()) - require.NoError(t, err) - - cancelFunc() - select { - case _, ok := <-claims: - require.False(t, ok) - case <-time.After(testutil.WaitShort): - t.Fatal("timeout waiting for closed channel") - } - }) - t.Run("finds claim events for its workspace", func(t *testing.T) { t.Parallel() ps := pubsub.NewInMemory() listener := prebuilds.NewPubsubWorkspaceClaimListener(ps, slogtest.Make(t, nil)) + claims := make(chan agentsdk.ReinitializationEvent, 1) // Buffer to avoid messing with goroutines in the rest of the test + workspaceID := uuid.New() userID := uuid.New() - cancelFunc, claims, err := listener.ListenForWorkspaceClaims(context.Background(), workspaceID) + cancelFunc, err := listener.ListenForWorkspaceClaims(context.Background(), workspaceID, claims) require.NoError(t, err) defer cancelFunc() @@ -125,11 +87,12 @@ func TestPubsubWorkspaceClaimListener(t *testing.T) { // Verify we receive the claim select { - case claim := <-claims: + case claim, ok := <-claims: + require.True(t, ok, "received on a closed channel") require.Equal(t, userID, claim.UserID) require.Equal(t, workspaceID, claim.WorkspaceID) require.Equal(t, agentsdk.ReinitializeReasonPrebuildClaimed, claim.Reason) - case <-time.After(time.Second): + case <-time.After(testutil.WaitSuperLong): // TODO: revert to waitshort t.Fatal("timeout waiting for claim") } }) @@ -140,9 +103,10 @@ func TestPubsubWorkspaceClaimListener(t *testing.T) { ps := pubsub.NewInMemory() listener := prebuilds.NewPubsubWorkspaceClaimListener(ps, slogtest.Make(t, nil)) + claims := make(chan agentsdk.ReinitializationEvent) workspaceID := uuid.New() otherWorkspaceID := uuid.New() - cancelFunc, claims, err := listener.ListenForWorkspaceClaims(context.Background(), workspaceID) + cancelFunc, err := listener.ListenForWorkspaceClaims(context.Background(), workspaceID, claims) require.NoError(t, err) defer cancelFunc() @@ -163,10 +127,11 @@ func TestPubsubWorkspaceClaimListener(t *testing.T) { t.Run("communicates the error if it can't subscribe", func(t *testing.T) { t.Parallel() + claims := make(chan agentsdk.ReinitializationEvent) ps := &brokenPubsub{} listener := prebuilds.NewPubsubWorkspaceClaimListener(ps, slogtest.Make(t, nil)) - _, _, err := listener.ListenForWorkspaceClaims(context.Background(), uuid.New()) + _, err := listener.ListenForWorkspaceClaims(context.Background(), uuid.New(), claims) require.ErrorContains(t, err, "failed to subscribe to prebuild claimed channel") }) } diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index 2f5e74fb25c98..90ba2156e5c22 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -620,11 +620,24 @@ func (s *server) acquireProtoJob(ctx context.Context, job database.ProvisionerJo } runningAgentAuthTokens := []*sdkproto.RunningAgentAuthToken{} - for agentID, token := range input.RunningAgentAuthTokens { - runningAgentAuthTokens = append(runningAgentAuthTokens, &sdkproto.RunningAgentAuthToken{ - AgentId: agentID.String(), - Token: token, - }) + if input.PrebuildClaimedByUser != uuid.Nil { + // runningAgentAuthTokens are *only* used for prebuilds. We fetch them when we want to rebuild a prebuilt workspace + // but not generate new agent tokens. The provisionerdserver will push them down to + // the provisioner (and ultimately to the `coder_agent` resource in the Terraform provider) where they will be + // reused. Context: the agent token is often used in immutable attributes of workspace resource (e.g. VM/container) + // to initialize the agent, so if that value changes it will necessitate a replacement of that resource, thus + // obviating the whole point of the prebuild. + agents, err := s.Database.GetWorkspaceAgentsInLatestBuildByWorkspaceID(ctx, workspace.ID) + if err != nil { + s.Logger.Error(ctx, "failed to retrieve running agents of claimed prebuilt workspace", + slog.F("workspace_id", workspace.ID), slog.Error(err)) + } + for _, agent := range agents { + runningAgentAuthTokens = append(runningAgentAuthTokens, &sdkproto.RunningAgentAuthToken{ + AgentId: agent.ID.String(), + Token: agent.AuthToken.String(), + }) + } } protoJob.Type = &proto.AcquiredJob_WorkspaceBuild_{ @@ -2503,13 +2516,6 @@ type WorkspaceProvisionJob struct { IsPrebuild bool `json:"is_prebuild,omitempty"` PrebuildClaimedByUser uuid.UUID `json:"prebuild_claimed_by,omitempty"` LogLevel string `json:"log_level,omitempty"` - // RunningAgentAuthTokens is *only* used for prebuilds. We pass it down when we want to rebuild a prebuilt workspace - // but not generate new agent tokens. The provisionerdserver will retrieve these tokens and push them down to - // the provisioner (and ultimately to the `coder_agent` resource in the Terraform provider) where they will be - // reused. Context: the agent token is often used in immutable attributes of workspace resource (e.g. VM/container) - // to initialize the agent, so if that value changes it will necessitate a replacement of that resource, thus - // obviating the whole point of the prebuild. - RunningAgentAuthTokens map[uuid.UUID]string `json:"running_agent_auth_tokens"` } // TemplateVersionDryRunJob is the payload for the "template_version_dry_run" job type. diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index 1de9abf03ddfd..5f6f3a55d18d2 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -1210,9 +1210,10 @@ func (api *API) workspaceAgentReinit(rw http.ResponseWriter, r *http.Request) { log.Info(ctx, "agent waiting for reinit instruction") - cancel, reinitEvents, err := prebuilds.NewPubsubWorkspaceClaimListener(api.Pubsub, log).ListenForWorkspaceClaims(ctx, workspace.ID) + reinitEvents := make(chan agentsdk.ReinitializationEvent) + cancel, err = prebuilds.NewPubsubWorkspaceClaimListener(api.Pubsub, log).ListenForWorkspaceClaims(ctx, workspace.ID, reinitEvents) if err != nil { - log.Error(ctx, "failed to subscribe to prebuild claimed channel", slog.Error(err)) + log.Error(ctx, "subscribe to prebuild claimed channel", slog.Error(err)) httpapi.InternalServerError(rw, xerrors.New("failed to subscribe to prebuild claimed channel")) return } @@ -1221,7 +1222,12 @@ func (api *API) workspaceAgentReinit(rw http.ResponseWriter, r *http.Request) { transmitter := agentsdk.NewSSEAgentReinitTransmitter(log, rw, r) err = transmitter.Transmit(ctx, reinitEvents) - if err != nil { + switch { + case errors.Is(err, agentsdk.ErrTransmissionSourceClosed): + log.Info(ctx, "agent reinitialization subscription closed", slog.F("workspace_agent_id", workspaceAgent.ID)) + case errors.Is(err, agentsdk.ErrTransmissionTargetClosed): + log.Info(ctx, "agent connection closed", slog.F("workspace_agent_id", workspaceAgent.ID)) + case err != nil: log.Error(ctx, "failed to stream agent reinit events", slog.Error(err)) httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ Message: "Internal error streaming agent reinitialization events.", diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go index 6b757a52ec06d..1e2e0b893d49d 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -2,6 +2,7 @@ package coderd_test import ( "context" + "database/sql" "encoding/json" "fmt" "maps" @@ -44,10 +45,12 @@ import ( "github.com/coder/coder/v2/coderd/database/dbfake" "github.com/coder/coder/v2/coderd/database/dbgen" "github.com/coder/coder/v2/coderd/database/dbmem" + "github.com/coder/coder/v2/coderd/database/dbtestutil" "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/database/pubsub" "github.com/coder/coder/v2/coderd/externalauth" "github.com/coder/coder/v2/coderd/jwtutils" + "github.com/coder/coder/v2/coderd/prebuilds" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/telemetry" "github.com/coder/coder/v2/coderd/util/ptr" @@ -2641,3 +2644,91 @@ func TestAgentConnectionInfo(t *testing.T) { require.True(t, info.DisableDirectConnections) require.True(t, info.DERPForceWebSockets) } + +func TestReinit(t *testing.T) { + t.Parallel() + + t.Run("unclaimed workspaces are not reinitialized", func(t *testing.T) { + t.Parallel() + + if !dbtestutil.WillUsePostgres() { + t.Skip() + } + + client, db := coderdtest.NewWithDatabase(t, nil) + user := coderdtest.CreateFirstUser(t, client) + + r := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{ + OrganizationID: user.OrganizationID, + OwnerID: user.UserID, + }).WithAgent().Do() + ctx := testutil.Context(t, testutil.WaitShort) + err := db.UpdateProvisionerJobWithCompleteByID(ctx, database.UpdateProvisionerJobWithCompleteByIDParams{ + ID: r.Build.JobID, + UpdatedAt: time.Now(), + CompletedAt: sql.NullTime{ + Valid: true, + Time: time.Now(), + }, + }) + require.NoError(t, err) + + agentCtx := testutil.Context(t, testutil.WaitShort) + agentClient := agentsdk.New(client.URL) + agentClient.SetSessionToken(r.AgentToken) + reinitEvent, err := agentClient.WaitForReinit(agentCtx) + require.ErrorIs(t, err, context.DeadlineExceeded) + require.Nil(t, reinitEvent) + }) + t.Run("claimed workspaces are reinitialized", func(t *testing.T) { + t.Parallel() + + if !dbtestutil.WillUsePostgres() { + t.Skip() + } + + db, ps := dbtestutil.NewDB(t) + client := coderdtest.New(t, &coderdtest.Options{ + Database: db, + Pubsub: ps, + }) + user := coderdtest.CreateFirstUser(t, client) + + r := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{ + OrganizationID: user.OrganizationID, + OwnerID: user.UserID, + }).WithAgent().Do() + ctx := testutil.Context(t, testutil.WaitShort) + err := db.UpdateProvisionerJobWithCompleteByID(ctx, database.UpdateProvisionerJobWithCompleteByIDParams{ + ID: r.Build.JobID, + UpdatedAt: time.Now(), + CompletedAt: sql.NullTime{ + Valid: true, + Time: time.Now(), + }, + }) + require.NoError(t, err) + + agentCtx := testutil.Context(t, testutil.WaitShort) + agentClient := agentsdk.New(client.URL) + agentClient.SetSessionToken(r.AgentToken) + + agentReinitializedCh := make(chan *agentsdk.ReinitializationEvent) + go func() { + reinitEvent, err := agentClient.WaitForReinit(agentCtx) + assert.NoError(t, err) + agentReinitializedCh <- reinitEvent + }() + + time.Sleep(time.Second) + + err = prebuilds.NewPubsubWorkspaceClaimPublisher(ps).PublishWorkspaceClaim(agentsdk.ReinitializationEvent{ + WorkspaceID: r.Workspace.ID, + UserID: user.UserID, + }) + require.NoError(t, err) + reinitEvent := testutil.TryReceive(ctx, t, agentReinitializedCh) + require.NotNil(t, reinitEvent) + require.Equal(t, r.Workspace.ID, reinitEvent.WorkspaceID) + }) +} diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 2153a6d63cb39..40b52b4762271 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -641,10 +641,9 @@ func createWorkspace( err = api.Database.InTx(func(db database.Store) error { var ( - prebuildsClaimer = *api.PrebuildsClaimer.Load() - workspaceID uuid.UUID - claimedWorkspace *database.Workspace - agentTokensByAgentID map[uuid.UUID]string + prebuildsClaimer = *api.PrebuildsClaimer.Load() + workspaceID uuid.UUID + claimedWorkspace *database.Workspace ) // If a template preset was chosen, try claim a prebuilt workspace. @@ -704,18 +703,6 @@ func createWorkspace( // Prebuild found! workspaceID = claimedWorkspace.ID initiatorID = prebuildsClaimer.Initiator() - agents, err := db.GetWorkspaceAgentsInLatestBuildByWorkspaceID(ctx, claimedWorkspace.ID) - if err != nil { - api.Logger.Error(ctx, "failed to retrieve running agents of claimed prebuilt workspace", - slog.F("workspace_id", claimedWorkspace.ID), slog.Error(err)) - } - if len(agents) > 1 { - return xerrors.Errorf("multiple agents are not yet supported in prebuilt workspaces") - } - - agentTokensByAgentID = map[uuid.UUID]string{ - agents[0].ID: agents[0].AuthToken.String(), - } } // We have to refetch the workspace for the joined in fields. @@ -730,8 +717,7 @@ func createWorkspace( Reason(database.BuildReasonInitiator). Initiator(initiatorID). ActiveVersion(). - RichParameterValues(req.RichParameterValues). - RunningAgentAuthTokens(agentTokensByAgentID) + RichParameterValues(req.RichParameterValues) if req.TemplateVersionID != uuid.Nil { builder = builder.VersionID(req.TemplateVersionID) } diff --git a/coderd/wsbuilder/wsbuilder.go b/coderd/wsbuilder/wsbuilder.go index 1b00076949bb6..e1468bea43111 100644 --- a/coderd/wsbuilder/wsbuilder.go +++ b/coderd/wsbuilder/wsbuilder.go @@ -78,7 +78,6 @@ type Builder struct { prebuild bool prebuildClaimedBy uuid.UUID - runningAgentAuthTokens map[uuid.UUID]string verifyNoLegacyParametersOnce bool } @@ -191,12 +190,6 @@ func (b Builder) UsingDynamicParameters() Builder { return b } -func (b Builder) RunningAgentAuthTokens(tokens map[uuid.UUID]string) Builder { - // nolint: revive - b.runningAgentAuthTokens = tokens - return b -} - // SetLastWorkspaceBuildInTx prepopulates the Builder's cache with the last workspace build. This allows us // to avoid a repeated database query when the Builder's caller also needs the workspace build, e.g. auto-start & // auto-stop. @@ -328,11 +321,10 @@ func (b *Builder) buildTx(authFunc func(action policy.Action, object rbac.Object workspaceBuildID := uuid.New() input, err := json.Marshal(provisionerdserver.WorkspaceProvisionJob{ - WorkspaceBuildID: workspaceBuildID, - LogLevel: b.logLevel, - IsPrebuild: b.prebuild, - PrebuildClaimedByUser: b.prebuildClaimedBy, - RunningAgentAuthTokens: b.runningAgentAuthTokens, + WorkspaceBuildID: workspaceBuildID, + LogLevel: b.logLevel, + IsPrebuild: b.prebuild, + PrebuildClaimedByUser: b.prebuildClaimedBy, }) if err != nil { return nil, nil, nil, BuildError{ diff --git a/codersdk/agentsdk/agentsdk.go b/codersdk/agentsdk/agentsdk.go index d7eae52cec96d..faf5ce37dd1f9 100644 --- a/codersdk/agentsdk/agentsdk.go +++ b/codersdk/agentsdk/agentsdk.go @@ -761,6 +761,7 @@ func WaitForReinitLoop(ctx context.Context, logger slog.Logger, client *Client) logger.Error(ctx, "failed to wait for agent reinitialization instructions", slog.Error(err)) continue } + retrier.Reset() select { case <-ctx.Done(): close(reinitEvents) @@ -783,6 +784,16 @@ type SSEAgentReinitTransmitter struct { logger slog.Logger } +var ( + ErrTransmissionSourceClosed = xerrors.New("transmission source closed") + ErrTransmissionTargetClosed = xerrors.New("transmission target closed") +) + +// Transmit will read from the given chan and send events for as long as: +// * the chan remains open +// * the context has not been canceled +// * not timed out +// * the connection to the receiver remains open func (s *SSEAgentReinitTransmitter) Transmit(ctx context.Context, reinitEvents <-chan ReinitializationEvent) error { select { case <-ctx.Done(): @@ -800,8 +811,11 @@ func (s *SSEAgentReinitTransmitter) Transmit(ctx context.Context, reinitEvents < case <-ctx.Done(): return ctx.Err() case <-sseSenderClosed: - return xerrors.New("sse connection closed") - case reinitEvent := <-reinitEvents: + return ErrTransmissionTargetClosed + case reinitEvent, ok := <-reinitEvents: + if !ok { + return ErrTransmissionSourceClosed + } err := sseSendEvent(codersdk.ServerSentEvent{ Type: codersdk.ServerSentEventTypeData, Data: reinitEvent, @@ -837,12 +851,18 @@ func (s *SSEAgentReinitReceiver) Receive(ctx context.Context) (*Reinitialization } sse, err := nextEvent() - if err != nil { + switch { + case err != nil: return nil, xerrors.Errorf("failed to read server-sent event: %w", err) - } - if sse.Type != codersdk.ServerSentEventTypeData { + case sse.Type == codersdk.ServerSentEventTypeError: + return nil, xerrors.Errorf("unexpected server sent event type error") + case sse.Type == codersdk.ServerSentEventTypePing: continue + case sse.Type != codersdk.ServerSentEventTypeData: + return nil, xerrors.Errorf("unexpected server sent event type: %s", sse.Type) } + + // At this point we know that the sent event is of type codersdk.ServerSentEventTypeData var reinitEvent ReinitializationEvent b, ok := sse.Data.([]byte) if !ok { @@ -852,11 +872,6 @@ func (s *SSEAgentReinitReceiver) Receive(ctx context.Context) (*Reinitialization if err != nil { return nil, xerrors.Errorf("unmarshal reinit response: %w", err) } - select { - case <-ctx.Done(): - return nil, ctx.Err() - default: - return &reinitEvent, nil - } + return &reinitEvent, nil } } diff --git a/enterprise/coderd/workspaceagents_test.go b/enterprise/coderd/workspaceagents_test.go index 400e8d144fcd2..6bf90889f9469 100644 --- a/enterprise/coderd/workspaceagents_test.go +++ b/enterprise/coderd/workspaceagents_test.go @@ -3,9 +3,14 @@ package coderd_test import ( "context" "crypto/tls" + "database/sql" "fmt" "net/http" + "os" "testing" + "time" + + "github.com/coder/coder/v2/coderd/database/dbtestutil" "github.com/google/uuid" "github.com/stretchr/testify/require" @@ -80,6 +85,12 @@ func TestBlockNonBrowser(t *testing.T) { func TestReinitializeAgent(t *testing.T) { t.Parallel() + tempAgentLog := testutil.CreateTemp(t, "", "testReinitializeAgent") + + if !dbtestutil.WillUsePostgres() { + t.Skip("dbmem cannot currently claim a workspace") + } + // GIVEN a live enterprise API with the prebuilds feature enabled client, db, user := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{ Options: &coderdtest.Options{ @@ -116,7 +127,7 @@ func TestReinitializeAgent(t *testing.T) { a[0].Scripts = []*proto.Script{ { DisplayName: "Prebuild Test Script", - Script: "sleep 5", // Make reinitialization take long enough to assert that it happened + Script: fmt.Sprintf("sleep 5; printenv | grep 'CODER_AGENT_TOKEN' >> %s; echo '---\n' >> %s", tempAgentLog.Name(), tempAgentLog.Name()), // Make reinitialization take long enough to assert that it happened RunOnStart: true, }, } @@ -140,18 +151,32 @@ func TestReinitializeAgent(t *testing.T) { // WHEN a workspace is created that can benefit from prebuilds ctx := testutil.Context(t, testutil.WaitShort) - _, err := client.CreateUserWorkspace(ctx, user.UserID.String(), codersdk.CreateWorkspaceRequest{ + workspace, err := client.CreateUserWorkspace(ctx, user.UserID.String(), codersdk.CreateWorkspaceRequest{ TemplateVersionID: tv.TemplateVersion.ID, TemplateVersionPresetID: presetID, Name: "claimed-workspace", }) require.NoError(t, err) + db.UpdateProvisionerJobWithCompleteByID(ctx, database.UpdateProvisionerJobWithCompleteByIDParams{ + ID: workspace.LatestBuild.ID, + UpdatedAt: time.Now(), + CompletedAt: sql.NullTime{ + Valid: true, + Time: time.Now(), + }, + }) + // THEN the now claimed workspace agent reinitializes waiter.WaitFor(coderdtest.AgentsNotReady) // THEN reinitialization completes waiter.WaitFor(coderdtest.AgentsReady) + + // THEN the agent script ran again and reused the same agent token + contents, err := os.ReadFile(tempAgentLog.Name()) + _ = contents + require.NoError(t, err) } type setupResp struct { diff --git a/enterprise/coderd/workspaces_test.go b/enterprise/coderd/workspaces_test.go index 72859c5460fa7..7b810c8fc06b8 100644 --- a/enterprise/coderd/workspaces_test.go +++ b/enterprise/coderd/workspaces_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "database/sql" + "encoding/json" "fmt" "net/http" "os" @@ -13,6 +14,7 @@ import ( "testing" "time" + "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -30,6 +32,8 @@ import ( "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/httpmw" "github.com/coder/coder/v2/coderd/notifications" + "github.com/coder/coder/v2/coderd/prebuilds" + "github.com/coder/coder/v2/coderd/provisionerdserver" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/rbac/policy" agplschedule "github.com/coder/coder/v2/coderd/schedule" @@ -43,6 +47,7 @@ import ( "github.com/coder/coder/v2/enterprise/coderd/schedule" "github.com/coder/coder/v2/provisioner/echo" "github.com/coder/coder/v2/provisionersdk" + "github.com/coder/coder/v2/provisionersdk/proto" "github.com/coder/coder/v2/testutil" "github.com/coder/quartz" ) @@ -453,6 +458,79 @@ func TestCreateUserWorkspace(t *testing.T) { _, err = client1.CreateUserWorkspace(ctx, user1.ID.String(), req) require.Error(t, err) }) + + t.Run("ClaimPrebuild", func(t *testing.T) { + t.Parallel() + + if !dbtestutil.WillUsePostgres() { + t.Skip("dbmem cannot currently claim a workspace") + } + + client, db, user := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + DeploymentValues: coderdtest.DeploymentValues(t, func(dv *codersdk.DeploymentValues) { + err := dv.Experiments.Append(string(codersdk.ExperimentWorkspacePrebuilds)) + require.NoError(t, err) + }), + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureWorkspacePrebuilds: 1, + }, + }, + }) + + // GIVEN a template, template version, preset and a prebuilt workspace that uses them all + presetID := uuid.New() + tv := dbfake.TemplateVersion(t, db).Seed(database.TemplateVersion{ + OrganizationID: user.OrganizationID, + CreatedBy: user.UserID, + }).Preset(database.TemplateVersionPreset{ + ID: presetID, + }).Do() + + r := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{ + OwnerID: prebuilds.SystemUserID, + TemplateID: tv.Template.ID, + }).Seed(database.WorkspaceBuild{ + TemplateVersionID: tv.TemplateVersion.ID, + TemplateVersionPresetID: uuid.NullUUID{ + UUID: presetID, + Valid: true, + }, + }).WithAgent(func(a []*proto.Agent) []*proto.Agent { + return a + }).Do() + + // nolint:gocritic // this is a test + ctx := dbauthz.AsSystemRestricted(testutil.Context(t, testutil.WaitLong)) + agent, err := db.GetWorkspaceAgentAndLatestBuildByAuthToken(ctx, uuid.MustParse(r.AgentToken)) + require.NoError(t, err) + + err = db.UpdateWorkspaceAgentLifecycleStateByID(ctx, database.UpdateWorkspaceAgentLifecycleStateByIDParams{ + ID: agent.WorkspaceAgent.ID, + LifecycleState: database.WorkspaceAgentLifecycleStateReady, + }) + require.NoError(t, err) + + // WHEN a workspace is created that matches the available prebuilt workspace + _, err = client.CreateUserWorkspace(ctx, user.UserID.String(), codersdk.CreateWorkspaceRequest{ + TemplateVersionID: tv.TemplateVersion.ID, + TemplateVersionPresetID: presetID, + Name: "claimed-workspace", + }) + require.NoError(t, err) + + // THEN a new build is scheduled with the claimant and agent tokens specified + build, err := db.GetLatestWorkspaceBuildByWorkspaceID(ctx, r.Workspace.ID) + require.NoError(t, err) + require.NotEqual(t, build.ID, r.Build.ID) + job, err := db.GetProvisionerJobByID(ctx, build.JobID) + require.NoError(t, err) + var metadata provisionerdserver.WorkspaceProvisionJob + require.NoError(t, json.Unmarshal(job.Input, &metadata)) + require.Equal(t, metadata.PrebuildClaimedByUser, user.UserID) + }) } func TestWorkspaceAutobuild(t *testing.T) { From 83972db1e916e965d5303e46384931e3a0f3e443 Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Wed, 7 May 2025 07:34:59 +0000 Subject: [PATCH 29/42] bump provisionerd proto version --- cli/testdata/coder_provisioner_list_--output_json.golden | 2 +- coderd/prebuilds/claim.go | 2 -- provisionerd/proto/version.go | 5 ++++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/cli/testdata/coder_provisioner_list_--output_json.golden b/cli/testdata/coder_provisioner_list_--output_json.golden index f619dce028cde..3daeb89febcb4 100644 --- a/cli/testdata/coder_provisioner_list_--output_json.golden +++ b/cli/testdata/coder_provisioner_list_--output_json.golden @@ -7,7 +7,7 @@ "last_seen_at": "====[timestamp]=====", "name": "test", "version": "v0.0.0-devel", - "api_version": "1.4", + "api_version": "1.5", "provisioners": [ "echo" ], diff --git a/coderd/prebuilds/claim.go b/coderd/prebuilds/claim.go index 64088aa07d0ad..e819018120d4c 100644 --- a/coderd/prebuilds/claim.go +++ b/coderd/prebuilds/claim.go @@ -66,8 +66,6 @@ func (p PubsubWorkspaceClaimListener) ListenForWorkspaceClaims(ctx context.Conte case <-inner.Done(): return case reinitEvents <- claim: - default: - return } }) if err != nil { diff --git a/provisionerd/proto/version.go b/provisionerd/proto/version.go index d502a1f544fe3..701f72f0596ec 100644 --- a/provisionerd/proto/version.go +++ b/provisionerd/proto/version.go @@ -12,9 +12,12 @@ import "github.com/coder/coder/v2/apiversion" // // API v1.4: // - Add new field named `devcontainers` in the Agent. +// +// API v1.5: +// - Add new field named `running_agent_auth_tokens` to provisioner job metadata const ( CurrentMajor = 1 - CurrentMinor = 4 + CurrentMinor = 5 ) // CurrentVersion is the current provisionerd API version. From 146b15857c5f5637168897dddb94940d94fa6a66 Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Mon, 12 May 2025 06:59:40 +0000 Subject: [PATCH 30/42] fix: fetch the previous agent when we need its token for prebuilt workspaces --- coderd/database/dbauthz/dbauthz.go | 9 ++ coderd/database/dbmem/dbmem.go | 24 ++++ coderd/database/dbmetrics/querymetrics.go | 7 + coderd/database/dbmock/dbmock.go | 15 ++ coderd/database/querier.go | 1 + coderd/database/queries.sql.go | 74 ++++++++++ coderd/database/queries/workspaceagents.sql | 13 ++ coderd/prebuilds/claim_test.go | 19 ++- .../provisionerdserver/provisionerdserver.go | 6 +- .../provisionerdserver_test.go | 28 ++-- coderd/workspaceagents.go | 2 + coderd/workspaceagents_test.go | 74 ++++------ codersdk/agentsdk/agentsdk.go | 8 +- enterprise/coderd/workspaceagents_test.go | 135 +++++++++++++----- 14 files changed, 296 insertions(+), 119 deletions(-) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 2ed230dd7a8f3..1823b4b79d71f 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -3012,6 +3012,15 @@ func (q *querier) GetWorkspaceAgentUsageStatsAndLabels(ctx context.Context, crea return q.db.GetWorkspaceAgentUsageStatsAndLabels(ctx, createdAt) } +func (q *querier) GetWorkspaceAgentsByBuildID(ctx context.Context, arg database.GetWorkspaceAgentsByBuildIDParams) ([]database.WorkspaceAgent, error) { + _, err := q.GetWorkspaceByID(ctx, arg.WorkspaceID) + if err != nil { + return nil, err + } + + return q.db.GetWorkspaceAgentsByBuildID(ctx, arg) +} + // GetWorkspaceAgentsByResourceIDs // The workspace/job is already fetched. func (q *querier) GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]database.WorkspaceAgent, error) { diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 6bae4455a89ef..418a19e1ee6a7 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -7641,6 +7641,30 @@ func (q *FakeQuerier) GetWorkspaceAgentUsageStatsAndLabels(_ context.Context, cr return stats, nil } +func (q *FakeQuerier) GetWorkspaceAgentsByBuildID(ctx context.Context, arg database.GetWorkspaceAgentsByBuildIDParams) ([]database.WorkspaceAgent, error) { + err := validateDatabaseType(arg) + if err != nil { + return nil, err + } + + build, err := q.GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx, database.GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams(arg)) + if err != nil { + return nil, err + } + + resources, err := q.getWorkspaceResourcesByJobIDNoLock(ctx, build.JobID) + if err != nil { + return nil, err + } + + var resourceIDs []uuid.UUID + for _, resource := range resources { + resourceIDs = append(resourceIDs, resource.ID) + } + + return q.GetWorkspaceAgentsByResourceIDs(ctx, resourceIDs) +} + func (q *FakeQuerier) GetWorkspaceAgentsByResourceIDs(ctx context.Context, resourceIDs []uuid.UUID) ([]database.WorkspaceAgent, error) { q.mutex.RLock() defer q.mutex.RUnlock() diff --git a/coderd/database/dbmetrics/querymetrics.go b/coderd/database/dbmetrics/querymetrics.go index 128e741da1d76..e3f4606a15783 100644 --- a/coderd/database/dbmetrics/querymetrics.go +++ b/coderd/database/dbmetrics/querymetrics.go @@ -1747,6 +1747,13 @@ func (m queryMetricsStore) GetWorkspaceAgentUsageStatsAndLabels(ctx context.Cont return r0, r1 } +func (m queryMetricsStore) GetWorkspaceAgentsByBuildID(ctx context.Context, arg database.GetWorkspaceAgentsByBuildIDParams) ([]database.WorkspaceAgent, error) { + start := time.Now() + r0, r1 := m.s.GetWorkspaceAgentsByBuildID(ctx, arg) + m.queryLatencies.WithLabelValues("GetWorkspaceAgentsByBuildID").Observe(time.Since(start).Seconds()) + return r0, r1 +} + func (m queryMetricsStore) GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]database.WorkspaceAgent, error) { start := time.Now() agents, err := m.s.GetWorkspaceAgentsByResourceIDs(ctx, ids) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index 17b263dfb2e07..d2420e97f6197 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -3663,6 +3663,21 @@ func (mr *MockStoreMockRecorder) GetWorkspaceAgentUsageStatsAndLabels(ctx, creat return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAgentUsageStatsAndLabels", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAgentUsageStatsAndLabels), ctx, createdAt) } +// GetWorkspaceAgentsByBuildID mocks base method. +func (m *MockStore) GetWorkspaceAgentsByBuildID(ctx context.Context, arg database.GetWorkspaceAgentsByBuildIDParams) ([]database.WorkspaceAgent, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetWorkspaceAgentsByBuildID", ctx, arg) + ret0, _ := ret[0].([]database.WorkspaceAgent) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetWorkspaceAgentsByBuildID indicates an expected call of GetWorkspaceAgentsByBuildID. +func (mr *MockStoreMockRecorder) GetWorkspaceAgentsByBuildID(ctx, arg any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAgentsByBuildID", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAgentsByBuildID), ctx, arg) +} + // GetWorkspaceAgentsByResourceIDs mocks base method. func (m *MockStore) GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]database.WorkspaceAgent, error) { m.ctrl.T.Helper() diff --git a/coderd/database/querier.go b/coderd/database/querier.go index d0f74ee609724..c133d435d70d7 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -399,6 +399,7 @@ type sqlcQuerier interface { // `minute_buckets` could return 0 rows if there are no usage stats since `created_at`. GetWorkspaceAgentUsageStats(ctx context.Context, createdAt time.Time) ([]GetWorkspaceAgentUsageStatsRow, error) GetWorkspaceAgentUsageStatsAndLabels(ctx context.Context, createdAt time.Time) ([]GetWorkspaceAgentUsageStatsAndLabelsRow, error) + GetWorkspaceAgentsByBuildID(ctx context.Context, arg GetWorkspaceAgentsByBuildIDParams) ([]WorkspaceAgent, error) GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgent, error) GetWorkspaceAgentsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceAgent, error) GetWorkspaceAgentsInLatestBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) ([]WorkspaceAgent, error) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 769df7a38126a..94d1970a20594 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -14339,6 +14339,80 @@ func (q *sqlQuerier) GetWorkspaceAgentScriptTimingsByBuildID(ctx context.Context return items, nil } +const getWorkspaceAgentsByBuildID = `-- name: GetWorkspaceAgentsByBuildID :many +SELECT + workspace_agents.id, workspace_agents.created_at, workspace_agents.updated_at, workspace_agents.name, workspace_agents.first_connected_at, workspace_agents.last_connected_at, workspace_agents.disconnected_at, workspace_agents.resource_id, workspace_agents.auth_token, workspace_agents.auth_instance_id, workspace_agents.architecture, workspace_agents.environment_variables, workspace_agents.operating_system, workspace_agents.instance_metadata, workspace_agents.resource_metadata, workspace_agents.directory, workspace_agents.version, workspace_agents.last_connected_replica_id, workspace_agents.connection_timeout_seconds, workspace_agents.troubleshooting_url, workspace_agents.motd_file, workspace_agents.lifecycle_state, workspace_agents.expanded_directory, workspace_agents.logs_length, workspace_agents.logs_overflowed, workspace_agents.started_at, workspace_agents.ready_at, workspace_agents.subsystems, workspace_agents.display_apps, workspace_agents.api_version, workspace_agents.display_order +FROM + workspace_agents +JOIN + workspace_resources ON workspace_agents.resource_id = workspace_resources.id +JOIN + workspace_builds ON workspace_resources.job_id = workspace_builds.job_id +WHERE + workspace_builds.workspace_id = $1 :: uuid AND + workspace_builds.build_number = $2 :: int +` + +type GetWorkspaceAgentsByBuildIDParams struct { + WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"` + BuildNumber int32 `db:"build_number" json:"build_number"` +} + +func (q *sqlQuerier) GetWorkspaceAgentsByBuildID(ctx context.Context, arg GetWorkspaceAgentsByBuildIDParams) ([]WorkspaceAgent, error) { + rows, err := q.db.QueryContext(ctx, getWorkspaceAgentsByBuildID, arg.WorkspaceID, arg.BuildNumber) + if err != nil { + return nil, err + } + defer rows.Close() + var items []WorkspaceAgent + for rows.Next() { + var i WorkspaceAgent + if err := rows.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.Name, + &i.FirstConnectedAt, + &i.LastConnectedAt, + &i.DisconnectedAt, + &i.ResourceID, + &i.AuthToken, + &i.AuthInstanceID, + &i.Architecture, + &i.EnvironmentVariables, + &i.OperatingSystem, + &i.InstanceMetadata, + &i.ResourceMetadata, + &i.Directory, + &i.Version, + &i.LastConnectedReplicaID, + &i.ConnectionTimeoutSeconds, + &i.TroubleshootingURL, + &i.MOTDFile, + &i.LifecycleState, + &i.ExpandedDirectory, + &i.LogsLength, + &i.LogsOverflowed, + &i.StartedAt, + &i.ReadyAt, + pq.Array(&i.Subsystems), + pq.Array(&i.DisplayApps), + &i.APIVersion, + &i.DisplayOrder, + ); 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 getWorkspaceAgentsByResourceIDs = `-- name: GetWorkspaceAgentsByResourceIDs :many SELECT id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps, api_version, display_order diff --git a/coderd/database/queries/workspaceagents.sql b/coderd/database/queries/workspaceagents.sql index 52d8b5275fc97..a852bcf872cea 100644 --- a/coderd/database/queries/workspaceagents.sql +++ b/coderd/database/queries/workspaceagents.sql @@ -252,6 +252,19 @@ WHERE wb.workspace_id = @workspace_id :: uuid ); +-- name: GetWorkspaceAgentsByBuildID :many +SELECT + workspace_agents.* +FROM + workspace_agents +JOIN + workspace_resources ON workspace_agents.resource_id = workspace_resources.id +JOIN + workspace_builds ON workspace_resources.job_id = workspace_builds.job_id +WHERE + workspace_builds.workspace_id = @workspace_id :: uuid AND + workspace_builds.build_number = @build_number :: int; + -- name: GetWorkspaceAgentAndLatestBuildByAuthToken :one SELECT sqlc.embed(workspaces), diff --git a/coderd/prebuilds/claim_test.go b/coderd/prebuilds/claim_test.go index b404cdcfde193..b447cfa1176b9 100644 --- a/coderd/prebuilds/claim_test.go +++ b/coderd/prebuilds/claim_test.go @@ -18,20 +18,19 @@ import ( func TestPubsubWorkspaceClaimPublisher(t *testing.T) { t.Parallel() - t.Run("publish claim", func(t *testing.T) { + t.Run("published claim is received by a listener for the same workspace", func(t *testing.T) { t.Parallel() + ctx := testutil.Context(t, testutil.WaitShort) + logger := testutil.Logger(t) ps := pubsub.NewInMemory() - publisher := prebuilds.NewPubsubWorkspaceClaimPublisher(ps) - workspaceID := uuid.New() userID := uuid.New() + reinitEvents := make(chan agentsdk.ReinitializationEvent, 1) + publisher := prebuilds.NewPubsubWorkspaceClaimPublisher(ps) + listener := prebuilds.NewPubsubWorkspaceClaimListener(ps, logger) - userIDCh := make(chan uuid.UUID, 1) - channel := agentsdk.PrebuildClaimedChannel(workspaceID) - cancel, err := ps.Subscribe(channel, func(ctx context.Context, message []byte) { - userIDCh <- uuid.MustParse(string(message)) - }) + cancel, err := listener.ListenForWorkspaceClaims(ctx, workspaceID, reinitEvents) require.NoError(t, err) defer cancel() @@ -43,8 +42,8 @@ func TestPubsubWorkspaceClaimPublisher(t *testing.T) { err = publisher.PublishWorkspaceClaim(claim) require.NoError(t, err) - gotUserID := testutil.TryReceive(testutil.Context(t, testutil.WaitShort), t, userIDCh) - require.Equal(t, userID, gotUserID) + gotUserID := testutil.RequireReceive(testutil.Context(t, testutil.WaitShort), t, reinitEvents) + require.Equal(t, userID, gotUserID.UserID) }) t.Run("fail to publish claim", func(t *testing.T) { diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index 90ba2156e5c22..2658b005bdcd4 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -627,7 +627,11 @@ func (s *server) acquireProtoJob(ctx context.Context, job database.ProvisionerJo // reused. Context: the agent token is often used in immutable attributes of workspace resource (e.g. VM/container) // to initialize the agent, so if that value changes it will necessitate a replacement of that resource, thus // obviating the whole point of the prebuild. - agents, err := s.Database.GetWorkspaceAgentsInLatestBuildByWorkspaceID(ctx, workspace.ID) + agents, err := s.Database.GetWorkspaceAgentsByBuildID(ctx, database.GetWorkspaceAgentsByBuildIDParams{ + WorkspaceID: workspace.ID, + BuildNumber: 1, + }) + if err != nil { s.Logger.Error(ctx, "failed to retrieve running agents of claimed prebuilt workspace", slog.F("workspace_id", workspace.ID), slog.Error(err)) diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go index c32feda425916..d772d1a45a731 100644 --- a/coderd/provisionerdserver/provisionerdserver_test.go +++ b/coderd/provisionerdserver/provisionerdserver_test.go @@ -23,6 +23,7 @@ import ( "storj.io/drpc" "cdr.dev/slog/sloggers/slogtest" + "github.com/coder/coder/v2/coderd/prebuilds" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/codersdk/agentsdk" "github.com/coder/quartz" @@ -1823,17 +1824,12 @@ func TestCompleteJob(t *testing.T) { require.NoError(t, err) // GIVEN something is listening to process workspace reinitialization: - - eventName := agentsdk.PrebuildClaimedChannel(workspace.ID) - reinitChan := make(chan []byte, 1) - cancel, err := ps.Subscribe(eventName, func(inner context.Context, userIDMessage []byte) { - reinitChan <- userIDMessage - }) + reinitChan := make(chan agentsdk.ReinitializationEvent, 1) // Buffered to simplify test structure + cancel, err := prebuilds.NewPubsubWorkspaceClaimListener(ps, testutil.Logger(t)).ListenForWorkspaceClaims(ctx, workspace.ID, reinitChan) require.NoError(t, err) defer cancel() // WHEN the job is completed - completedJob := proto.CompletedJob{ JobId: job.ID.String(), Type: &proto.CompletedJob_WorkspaceBuild_{ @@ -1844,13 +1840,11 @@ func TestCompleteJob(t *testing.T) { require.NoError(t, err) select { - case userIDMessage := <-reinitChan: + case reinitEvent := <-reinitChan: // THEN workspace agent reinitialization instruction was received: - gotUserID, err := uuid.ParseBytes(userIDMessage) - require.NoError(t, err) require.True(t, tc.shouldReinitializeAgent) - require.Equal(t, userID, gotUserID) - case <-ctx.Done(): + require.Equal(t, userID, reinitEvent.UserID) + default: // THEN workspace agent reinitialization instruction was not received. require.False(t, tc.shouldReinitializeAgent) } @@ -2953,3 +2947,13 @@ func (s *fakeStream) cancel() { s.canceled = true s.c.Broadcast() } + +type pubsubReinitSpy struct { + pubsub.Pubsub + subscriptions chan string +} + +func (p pubsubReinitSpy) Subscribe(event string, listener pubsub.Listener) (cancel func(), err error) { + p.subscriptions <- event + return p.Pubsub.Subscribe(event, listener) +} diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index 5f6f3a55d18d2..5af9fc009b5aa 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -1227,6 +1227,8 @@ func (api *API) workspaceAgentReinit(rw http.ResponseWriter, r *http.Request) { log.Info(ctx, "agent reinitialization subscription closed", slog.F("workspace_agent_id", workspaceAgent.ID)) case errors.Is(err, agentsdk.ErrTransmissionTargetClosed): log.Info(ctx, "agent connection closed", slog.F("workspace_agent_id", workspaceAgent.ID)) + case errors.Is(err, context.Canceled): + log.Info(ctx, "agent reinitialization", slog.Error(err)) case err != nil: log.Error(ctx, "failed to stream agent reinit events", slog.Error(err)) httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go index 1e2e0b893d49d..89615953b9407 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -2,7 +2,6 @@ package coderd_test import ( "context" - "database/sql" "encoding/json" "fmt" "maps" @@ -2648,49 +2647,17 @@ func TestAgentConnectionInfo(t *testing.T) { func TestReinit(t *testing.T) { t.Parallel() - t.Run("unclaimed workspaces are not reinitialized", func(t *testing.T) { - t.Parallel() - - if !dbtestutil.WillUsePostgres() { - t.Skip() - } - - client, db := coderdtest.NewWithDatabase(t, nil) - user := coderdtest.CreateFirstUser(t, client) - - r := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{ - OrganizationID: user.OrganizationID, - OwnerID: user.UserID, - }).WithAgent().Do() - ctx := testutil.Context(t, testutil.WaitShort) - err := db.UpdateProvisionerJobWithCompleteByID(ctx, database.UpdateProvisionerJobWithCompleteByIDParams{ - ID: r.Build.JobID, - UpdatedAt: time.Now(), - CompletedAt: sql.NullTime{ - Valid: true, - Time: time.Now(), - }, - }) - require.NoError(t, err) - - agentCtx := testutil.Context(t, testutil.WaitShort) - agentClient := agentsdk.New(client.URL) - agentClient.SetSessionToken(r.AgentToken) - reinitEvent, err := agentClient.WaitForReinit(agentCtx) - require.ErrorIs(t, err, context.DeadlineExceeded) - require.Nil(t, reinitEvent) - }) t.Run("claimed workspaces are reinitialized", func(t *testing.T) { t.Parallel() - if !dbtestutil.WillUsePostgres() { - t.Skip() - } - db, ps := dbtestutil.NewDB(t) + pubsubSpy := pubsubReinitSpy{ + Pubsub: ps, + subscriptions: make(chan string), + } client := coderdtest.New(t, &coderdtest.Options{ Database: db, - Pubsub: ps, + Pubsub: pubsubSpy, }) user := coderdtest.CreateFirstUser(t, client) @@ -2698,16 +2665,6 @@ func TestReinit(t *testing.T) { OrganizationID: user.OrganizationID, OwnerID: user.UserID, }).WithAgent().Do() - ctx := testutil.Context(t, testutil.WaitShort) - err := db.UpdateProvisionerJobWithCompleteByID(ctx, database.UpdateProvisionerJobWithCompleteByIDParams{ - ID: r.Build.JobID, - UpdatedAt: time.Now(), - CompletedAt: sql.NullTime{ - Valid: true, - Time: time.Now(), - }, - }) - require.NoError(t, err) agentCtx := testutil.Context(t, testutil.WaitShort) agentClient := agentsdk.New(client.URL) @@ -2720,15 +2677,32 @@ func TestReinit(t *testing.T) { agentReinitializedCh <- reinitEvent }() - time.Sleep(time.Second) + // We need to subscribe before we publish, lest we miss the event + for subscription := range pubsubSpy.subscriptions { + if subscription == agentsdk.PrebuildClaimedChannel(r.Workspace.ID) { + break + } + } - err = prebuilds.NewPubsubWorkspaceClaimPublisher(ps).PublishWorkspaceClaim(agentsdk.ReinitializationEvent{ + err := prebuilds.NewPubsubWorkspaceClaimPublisher(ps).PublishWorkspaceClaim(agentsdk.ReinitializationEvent{ WorkspaceID: r.Workspace.ID, UserID: user.UserID, }) require.NoError(t, err) + + ctx := testutil.Context(t, testutil.WaitShort) reinitEvent := testutil.TryReceive(ctx, t, agentReinitializedCh) require.NotNil(t, reinitEvent) require.Equal(t, r.Workspace.ID, reinitEvent.WorkspaceID) }) } + +type pubsubReinitSpy struct { + pubsub.Pubsub + subscriptions chan string +} + +func (p pubsubReinitSpy) Subscribe(event string, listener pubsub.Listener) (cancel func(), err error) { + p.subscriptions <- event + return p.Pubsub.Subscribe(event, listener) +} diff --git a/codersdk/agentsdk/agentsdk.go b/codersdk/agentsdk/agentsdk.go index faf5ce37dd1f9..0449d3fdbcc25 100644 --- a/codersdk/agentsdk/agentsdk.go +++ b/codersdk/agentsdk/agentsdk.go @@ -821,13 +821,7 @@ func (s *SSEAgentReinitTransmitter) Transmit(ctx context.Context, reinitEvents < Data: reinitEvent, }) if err != nil { - s.logger.Warn( - ctx, - "failed to send SSE to trigger agent reinitialization", - slog.Error(err), - slog.F("user_id", reinitEvent.UserID), - slog.F("workspace_id", reinitEvent.WorkspaceID), - ) + return err } } } diff --git a/enterprise/coderd/workspaceagents_test.go b/enterprise/coderd/workspaceagents_test.go index 6bf90889f9469..fa2eba981a37c 100644 --- a/enterprise/coderd/workspaceagents_test.go +++ b/enterprise/coderd/workspaceagents_test.go @@ -3,7 +3,6 @@ package coderd_test import ( "context" "crypto/tls" - "database/sql" "fmt" "net/http" "os" @@ -11,6 +10,7 @@ import ( "time" "github.com/coder/coder/v2/coderd/database/dbtestutil" + "github.com/coder/serpent" "github.com/google/uuid" "github.com/stretchr/testify/require" @@ -18,8 +18,6 @@ import ( "github.com/coder/coder/v2/agent" "github.com/coder/coder/v2/cli/clitest" "github.com/coder/coder/v2/coderd/coderdtest" - "github.com/coder/coder/v2/coderd/database" - "github.com/coder/coder/v2/coderd/database/dbfake" "github.com/coder/coder/v2/coderd/prebuilds" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/agentsdk" @@ -91,12 +89,17 @@ func TestReinitializeAgent(t *testing.T) { t.Skip("dbmem cannot currently claim a workspace") } + db, ps := dbtestutil.NewDB(t) // GIVEN a live enterprise API with the prebuilds feature enabled - client, db, user := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{ + client, user := coderdenttest.New(t, &coderdenttest.Options{ Options: &coderdtest.Options{ + Database: db, + Pubsub: ps, DeploymentValues: coderdtest.DeploymentValues(t, func(dv *codersdk.DeploymentValues) { + dv.Prebuilds.ReconciliationInterval = serpent.Duration(time.Second) dv.Experiments.Append(string(codersdk.ExperimentWorkspacePrebuilds)) }), + IncludeProvisionerDaemon: true, }, LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ @@ -106,65 +109,119 @@ func TestReinitializeAgent(t *testing.T) { }) // GIVEN a template, template version, preset and a prebuilt workspace that uses them all - presetID := uuid.New() - tv := dbfake.TemplateVersion(t, db).Seed(database.TemplateVersion{ - OrganizationID: user.OrganizationID, - CreatedBy: user.UserID, - }).Preset(database.TemplateVersionPreset{ - ID: presetID, - }).Do() - - r := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{ - OwnerID: prebuilds.SystemUserID, - TemplateID: tv.Template.ID, - }).Seed(database.WorkspaceBuild{ - TemplateVersionID: tv.TemplateVersion.ID, - TemplateVersionPresetID: uuid.NullUUID{ - UUID: presetID, - Valid: true, + agentToken := uuid.UUID{3} + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ + Parse: echo.ParseComplete, + ProvisionPlan: []*proto.Response{ + { + Type: &proto.Response_Plan{ + Plan: &proto.PlanComplete{ + Presets: []*proto.Preset{ + { + Name: "test-preset", + Prebuild: &proto.Prebuild{ + Instances: 1, + }, + }, + }, + Resources: []*proto.Resource{ + { + Agents: []*proto.Agent{ + { + Name: "smith", + OperatingSystem: "linux", + Architecture: "i386", + }, + }, + }, + }, + }, + }, + }, }, - }).WithAgent(func(a []*proto.Agent) []*proto.Agent { - a[0].Scripts = []*proto.Script{ + ProvisionApply: []*proto.Response{ { - DisplayName: "Prebuild Test Script", - Script: fmt.Sprintf("sleep 5; printenv | grep 'CODER_AGENT_TOKEN' >> %s; echo '---\n' >> %s", tempAgentLog.Name(), tempAgentLog.Name()), // Make reinitialization take long enough to assert that it happened - RunOnStart: true, + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ + Resources: []*proto.Resource{ + { + Type: "compute", + Name: "main", + Agents: []*proto.Agent{ + { + Name: "smith", + OperatingSystem: "linux", + Architecture: "i386", + Scripts: []*proto.Script{ + { + RunOnStart: true, + Script: fmt.Sprintf("sleep 5; printenv | grep 'CODER_AGENT_TOKEN' >> %s; echo '---\n' >> %s", tempAgentLog.Name(), tempAgentLog.Name()), // Make reinitialization take long enough to assert that it happened + }, + }, + Auth: &proto.Agent_Token{ + Token: agentToken.String(), + }, + }, + }, + }, + }, + }, + }, }, + }, + }) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + + coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + + // Wait for prebuilds to create a prebuilt workspace + ctx := context.Background() + // ctx := testutil.Context(t, testutil.WaitLong) + var ( + prebuildID uuid.UUID + ) + require.Eventually(t, func() bool { + agentAndBuild, err := db.GetWorkspaceAgentAndLatestBuildByAuthToken(ctx, agentToken) + if err != nil { + return false } - return a - }).Do() + prebuildID = agentAndBuild.WorkspaceBuild.ID + return true + }, testutil.WaitLong, testutil.IntervalFast) + + prebuild := coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, prebuildID) + + preset, err := db.GetPresetByWorkspaceBuildID(ctx, prebuildID) + require.NoError(t, err) // GIVEN a running agent logDir := t.TempDir() inv, _ := clitest.New(t, "agent", "--auth", "token", - "--agent-token", r.AgentToken, + "--agent-token", agentToken.String(), "--agent-url", client.URL.String(), "--log-dir", logDir, ) clitest.Start(t, inv) // GIVEN the agent is in a happy steady state - waiter := coderdtest.NewWorkspaceAgentWaiter(t, client, r.Workspace.ID) + waiter := coderdtest.NewWorkspaceAgentWaiter(t, client, prebuild.WorkspaceID) waiter.WaitFor(coderdtest.AgentsReady) // WHEN a workspace is created that can benefit from prebuilds - ctx := testutil.Context(t, testutil.WaitShort) + // ctx := testutil.Context(t, testutil.WaitShort) workspace, err := client.CreateUserWorkspace(ctx, user.UserID.String(), codersdk.CreateWorkspaceRequest{ - TemplateVersionID: tv.TemplateVersion.ID, - TemplateVersionPresetID: presetID, + TemplateVersionID: version.ID, + TemplateVersionPresetID: preset.ID, Name: "claimed-workspace", }) require.NoError(t, err) - db.UpdateProvisionerJobWithCompleteByID(ctx, database.UpdateProvisionerJobWithCompleteByIDParams{ - ID: workspace.LatestBuild.ID, - UpdatedAt: time.Now(), - CompletedAt: sql.NullTime{ - Valid: true, - Time: time.Now(), - }, + coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID) + prebuilds.NewPubsubWorkspaceClaimPublisher(ps).PublishWorkspaceClaim(agentsdk.ReinitializationEvent{ + WorkspaceID: workspace.ID, + UserID: user.UserID, }) // THEN the now claimed workspace agent reinitializes From 730d803742ce6cf62d6f65e05403e92bb66d420c Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Mon, 12 May 2025 11:03:05 +0000 Subject: [PATCH 31/42] make -B lint --- coderd/provisionerdserver/provisionerdserver.go | 1 - coderd/provisionerdserver/provisionerdserver_test.go | 10 ---------- enterprise/coderd/workspaceagents_test.go | 4 ++-- 3 files changed, 2 insertions(+), 13 deletions(-) diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index 2658b005bdcd4..a1b18afe5cc9c 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -631,7 +631,6 @@ func (s *server) acquireProtoJob(ctx context.Context, job database.ProvisionerJo WorkspaceID: workspace.ID, BuildNumber: 1, }) - if err != nil { s.Logger.Error(ctx, "failed to retrieve running agents of claimed prebuilt workspace", slog.F("workspace_id", workspace.ID), slog.Error(err)) diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go index d772d1a45a731..6ad09aa3bda08 100644 --- a/coderd/provisionerdserver/provisionerdserver_test.go +++ b/coderd/provisionerdserver/provisionerdserver_test.go @@ -2947,13 +2947,3 @@ func (s *fakeStream) cancel() { s.canceled = true s.c.Broadcast() } - -type pubsubReinitSpy struct { - pubsub.Pubsub - subscriptions chan string -} - -func (p pubsubReinitSpy) Subscribe(event string, listener pubsub.Listener) (cancel func(), err error) { - p.subscriptions <- event - return p.Pubsub.Subscribe(event, listener) -} diff --git a/enterprise/coderd/workspaceagents_test.go b/enterprise/coderd/workspaceagents_test.go index fa2eba981a37c..944c44d2c195a 100644 --- a/enterprise/coderd/workspaceagents_test.go +++ b/enterprise/coderd/workspaceagents_test.go @@ -210,8 +210,8 @@ func TestReinitializeAgent(t *testing.T) { waiter.WaitFor(coderdtest.AgentsReady) // WHEN a workspace is created that can benefit from prebuilds - // ctx := testutil.Context(t, testutil.WaitShort) - workspace, err := client.CreateUserWorkspace(ctx, user.UserID.String(), codersdk.CreateWorkspaceRequest{ + anotherClient, anotherUser := coderdtest.CreateAnotherUser(t, client, user.OrganizationID) + workspace, err := anotherClient.CreateUserWorkspace(ctx, anotherUser.ID.String(), codersdk.CreateWorkspaceRequest{ TemplateVersionID: version.ID, TemplateVersionPresetID: preset.ID, Name: "claimed-workspace", From 150adc07c7db25ff6873e448eacaab0e7818ede0 Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Mon, 12 May 2025 11:24:19 +0000 Subject: [PATCH 32/42] Test GetWorkspaceAgentsByBuildID --- coderd/database/dbauthz/dbauthz_test.go | 29 +++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 6dc9a32f03943..a7eaaefb6ab82 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -2009,6 +2009,35 @@ func (s *MethodTestSuite) TestWorkspace() { agt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID}) check.Args(agt.ID).Asserts(w, policy.ActionRead).Returns(agt) })) + s.Run("GetWorkspaceAgentsByBuildID", s.Subtest(func(db database.Store, check *expects) { + u := dbgen.User(s.T(), db, database.User{}) + o := dbgen.Organization(s.T(), db, database.Organization{}) + tpl := dbgen.Template(s.T(), db, database.Template{ + OrganizationID: o.ID, + CreatedBy: u.ID, + }) + tv := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{ + TemplateID: uuid.NullUUID{UUID: tpl.ID, Valid: true}, + OrganizationID: o.ID, + CreatedBy: u.ID, + }) + w := dbgen.Workspace(s.T(), db, database.WorkspaceTable{ + TemplateID: tpl.ID, + OrganizationID: o.ID, + OwnerID: u.ID, + }) + j := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{ + Type: database.ProvisionerJobTypeWorkspaceBuild, + }) + b := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{ + JobID: j.ID, + WorkspaceID: w.ID, + TemplateVersionID: tv.ID, + }) + res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: b.JobID}) + agt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID}) + check.Args(b.ID).Asserts(w, policy.ActionRead).Returns(agt) + })) s.Run("GetWorkspaceAgentLifecycleStateByID", s.Subtest(func(db database.Store, check *expects) { u := dbgen.User(s.T(), db, database.User{}) o := dbgen.Organization(s.T(), db, database.Organization{}) From b4ecf109f0610038d3a44712b3dcd0b59c14a70a Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Mon, 12 May 2025 11:51:05 +0000 Subject: [PATCH 33/42] Rename GetWorkspaceAgentsByWorkspaceAndBuildNumber --- coderd/database/dbauthz/dbauthz.go | 4 +-- coderd/database/dbauthz/dbauthz_test.go | 7 +++-- coderd/database/dbmem/dbmem.go | 2 +- coderd/database/dbmetrics/querymetrics.go | 6 ++--- coderd/database/dbmock/dbmock.go | 12 ++++----- coderd/database/querier.go | 2 +- coderd/database/queries.sql.go | 26 +++++++++---------- coderd/database/queries/workspaceagents.sql | 2 +- .../provisionerdserver/provisionerdserver.go | 2 +- 9 files changed, 33 insertions(+), 30 deletions(-) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 1823b4b79d71f..bddd80b1836fb 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -3012,13 +3012,13 @@ func (q *querier) GetWorkspaceAgentUsageStatsAndLabels(ctx context.Context, crea return q.db.GetWorkspaceAgentUsageStatsAndLabels(ctx, createdAt) } -func (q *querier) GetWorkspaceAgentsByBuildID(ctx context.Context, arg database.GetWorkspaceAgentsByBuildIDParams) ([]database.WorkspaceAgent, error) { +func (q *querier) GetWorkspaceAgentsByWorkspaceAndBuildNumber(ctx context.Context, arg database.GetWorkspaceAgentsByWorkspaceAndBuildNumberParams) ([]database.WorkspaceAgent, error) { _, err := q.GetWorkspaceByID(ctx, arg.WorkspaceID) if err != nil { return nil, err } - return q.db.GetWorkspaceAgentsByBuildID(ctx, arg) + return q.db.GetWorkspaceAgentsByWorkspaceAndBuildNumber(ctx, arg) } // GetWorkspaceAgentsByResourceIDs diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index a7eaaefb6ab82..4efa439395a6b 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -2009,7 +2009,7 @@ func (s *MethodTestSuite) TestWorkspace() { agt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID}) check.Args(agt.ID).Asserts(w, policy.ActionRead).Returns(agt) })) - s.Run("GetWorkspaceAgentsByBuildID", s.Subtest(func(db database.Store, check *expects) { + s.Run("GetWorkspaceAgentsByWorkspaceAndBuildNumber", s.Subtest(func(db database.Store, check *expects) { u := dbgen.User(s.T(), db, database.User{}) o := dbgen.Organization(s.T(), db, database.Organization{}) tpl := dbgen.Template(s.T(), db, database.Template{ @@ -2036,7 +2036,10 @@ func (s *MethodTestSuite) TestWorkspace() { }) res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: b.JobID}) agt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID}) - check.Args(b.ID).Asserts(w, policy.ActionRead).Returns(agt) + check.Args(database.GetWorkspaceAgentsByWorkspaceAndBuildNumberParams{ + WorkspaceID: w.ID, + BuildNumber: 1, + }).Asserts(w, policy.ActionRead).Returns(agt) })) s.Run("GetWorkspaceAgentLifecycleStateByID", s.Subtest(func(db database.Store, check *expects) { u := dbgen.User(s.T(), db, database.User{}) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 418a19e1ee6a7..f0605060163c1 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -7641,7 +7641,7 @@ func (q *FakeQuerier) GetWorkspaceAgentUsageStatsAndLabels(_ context.Context, cr return stats, nil } -func (q *FakeQuerier) GetWorkspaceAgentsByBuildID(ctx context.Context, arg database.GetWorkspaceAgentsByBuildIDParams) ([]database.WorkspaceAgent, error) { +func (q *FakeQuerier) GetWorkspaceAgentsByWorkspaceAndBuildNumber(ctx context.Context, arg database.GetWorkspaceAgentsByWorkspaceAndBuildNumberParams) ([]database.WorkspaceAgent, error) { err := validateDatabaseType(arg) if err != nil { return nil, err diff --git a/coderd/database/dbmetrics/querymetrics.go b/coderd/database/dbmetrics/querymetrics.go index e3f4606a15783..17e6c5756c3b0 100644 --- a/coderd/database/dbmetrics/querymetrics.go +++ b/coderd/database/dbmetrics/querymetrics.go @@ -1747,10 +1747,10 @@ func (m queryMetricsStore) GetWorkspaceAgentUsageStatsAndLabels(ctx context.Cont return r0, r1 } -func (m queryMetricsStore) GetWorkspaceAgentsByBuildID(ctx context.Context, arg database.GetWorkspaceAgentsByBuildIDParams) ([]database.WorkspaceAgent, error) { +func (m queryMetricsStore) GetWorkspaceAgentsByWorkspaceAndBuildNumber(ctx context.Context, arg database.GetWorkspaceAgentsByWorkspaceAndBuildNumberParams) ([]database.WorkspaceAgent, error) { start := time.Now() - r0, r1 := m.s.GetWorkspaceAgentsByBuildID(ctx, arg) - m.queryLatencies.WithLabelValues("GetWorkspaceAgentsByBuildID").Observe(time.Since(start).Seconds()) + r0, r1 := m.s.GetWorkspaceAgentsByWorkspaceAndBuildNumber(ctx, arg) + m.queryLatencies.WithLabelValues("GetWorkspaceAgentsByWorkspaceAndBuildNumber").Observe(time.Since(start).Seconds()) return r0, r1 } diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index d2420e97f6197..c03b7784629a7 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -3663,19 +3663,19 @@ func (mr *MockStoreMockRecorder) GetWorkspaceAgentUsageStatsAndLabels(ctx, creat return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAgentUsageStatsAndLabels", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAgentUsageStatsAndLabels), ctx, createdAt) } -// GetWorkspaceAgentsByBuildID mocks base method. -func (m *MockStore) GetWorkspaceAgentsByBuildID(ctx context.Context, arg database.GetWorkspaceAgentsByBuildIDParams) ([]database.WorkspaceAgent, error) { +// GetWorkspaceAgentsByWorkspaceAndBuildNumber mocks base method. +func (m *MockStore) GetWorkspaceAgentsByWorkspaceAndBuildNumber(ctx context.Context, arg database.GetWorkspaceAgentsByWorkspaceAndBuildNumberParams) ([]database.WorkspaceAgent, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetWorkspaceAgentsByBuildID", ctx, arg) + ret := m.ctrl.Call(m, "GetWorkspaceAgentsByWorkspaceAndBuildNumber", ctx, arg) ret0, _ := ret[0].([]database.WorkspaceAgent) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetWorkspaceAgentsByBuildID indicates an expected call of GetWorkspaceAgentsByBuildID. -func (mr *MockStoreMockRecorder) GetWorkspaceAgentsByBuildID(ctx, arg any) *gomock.Call { +// GetWorkspaceAgentsByWorkspaceAndBuildNumber indicates an expected call of GetWorkspaceAgentsByWorkspaceAndBuildNumber. +func (mr *MockStoreMockRecorder) GetWorkspaceAgentsByWorkspaceAndBuildNumber(ctx, arg any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAgentsByBuildID", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAgentsByBuildID), ctx, arg) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAgentsByWorkspaceAndBuildNumber", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAgentsByWorkspaceAndBuildNumber), ctx, arg) } // GetWorkspaceAgentsByResourceIDs mocks base method. diff --git a/coderd/database/querier.go b/coderd/database/querier.go index c133d435d70d7..6f9951fae0065 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -399,7 +399,7 @@ type sqlcQuerier interface { // `minute_buckets` could return 0 rows if there are no usage stats since `created_at`. GetWorkspaceAgentUsageStats(ctx context.Context, createdAt time.Time) ([]GetWorkspaceAgentUsageStatsRow, error) GetWorkspaceAgentUsageStatsAndLabels(ctx context.Context, createdAt time.Time) ([]GetWorkspaceAgentUsageStatsAndLabelsRow, error) - GetWorkspaceAgentsByBuildID(ctx context.Context, arg GetWorkspaceAgentsByBuildIDParams) ([]WorkspaceAgent, error) + GetWorkspaceAgentsByWorkspaceAndBuildNumber(ctx context.Context, arg GetWorkspaceAgentsByWorkspaceAndBuildNumberParams) ([]WorkspaceAgent, error) GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgent, error) GetWorkspaceAgentsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceAgent, error) GetWorkspaceAgentsInLatestBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) ([]WorkspaceAgent, error) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 94d1970a20594..47e04a99ada7e 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -8174,7 +8174,7 @@ FROM provisioner_keys WHERE organization_id = $1 -AND +AND lower(name) = lower($2) ` @@ -8290,10 +8290,10 @@ WHERE AND -- exclude reserved built-in key id != '00000000-0000-0000-0000-000000000001'::uuid -AND +AND -- exclude reserved user-auth key id != '00000000-0000-0000-0000-000000000002'::uuid -AND +AND -- exclude reserved psk key id != '00000000-0000-0000-0000-000000000003'::uuid ` @@ -10055,7 +10055,7 @@ func (q *sqlQuerier) GetTailnetTunnelPeerIDs(ctx context.Context, srcID uuid.UUI } const updateTailnetPeerStatusByCoordinator = `-- name: UpdateTailnetPeerStatusByCoordinator :exec -UPDATE +UPDATE tailnet_peers SET status = $2 @@ -11899,14 +11899,14 @@ DO $$ DECLARE table_record record; BEGIN - FOR table_record IN - SELECT table_schema, table_name - FROM information_schema.tables + FOR table_record IN + SELECT table_schema, table_name + FROM information_schema.tables WHERE table_schema NOT IN ('pg_catalog', 'information_schema') AND table_type = 'BASE TABLE' LOOP - EXECUTE format('ALTER TABLE %I.%I DISABLE TRIGGER ALL', - table_record.table_schema, + EXECUTE format('ALTER TABLE %I.%I DISABLE TRIGGER ALL', + table_record.table_schema, table_record.table_name); END LOOP; END; @@ -14339,7 +14339,7 @@ func (q *sqlQuerier) GetWorkspaceAgentScriptTimingsByBuildID(ctx context.Context return items, nil } -const getWorkspaceAgentsByBuildID = `-- name: GetWorkspaceAgentsByBuildID :many +const getWorkspaceAgentsByBuildID = `-- name: GetWorkspaceAgentsByWorkspaceAndBuildNumber :many SELECT workspace_agents.id, workspace_agents.created_at, workspace_agents.updated_at, workspace_agents.name, workspace_agents.first_connected_at, workspace_agents.last_connected_at, workspace_agents.disconnected_at, workspace_agents.resource_id, workspace_agents.auth_token, workspace_agents.auth_instance_id, workspace_agents.architecture, workspace_agents.environment_variables, workspace_agents.operating_system, workspace_agents.instance_metadata, workspace_agents.resource_metadata, workspace_agents.directory, workspace_agents.version, workspace_agents.last_connected_replica_id, workspace_agents.connection_timeout_seconds, workspace_agents.troubleshooting_url, workspace_agents.motd_file, workspace_agents.lifecycle_state, workspace_agents.expanded_directory, workspace_agents.logs_length, workspace_agents.logs_overflowed, workspace_agents.started_at, workspace_agents.ready_at, workspace_agents.subsystems, workspace_agents.display_apps, workspace_agents.api_version, workspace_agents.display_order FROM @@ -14353,12 +14353,12 @@ WHERE workspace_builds.build_number = $2 :: int ` -type GetWorkspaceAgentsByBuildIDParams struct { +type GetWorkspaceAgentsByWorkspaceAndBuildNumberParams struct { WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"` BuildNumber int32 `db:"build_number" json:"build_number"` } -func (q *sqlQuerier) GetWorkspaceAgentsByBuildID(ctx context.Context, arg GetWorkspaceAgentsByBuildIDParams) ([]WorkspaceAgent, error) { +func (q *sqlQuerier) GetWorkspaceAgentsByWorkspaceAndBuildNumber(ctx context.Context, arg GetWorkspaceAgentsByWorkspaceAndBuildNumberParams) ([]WorkspaceAgent, error) { rows, err := q.db.QueryContext(ctx, getWorkspaceAgentsByBuildID, arg.WorkspaceID, arg.BuildNumber) if err != nil { return nil, err @@ -15343,7 +15343,7 @@ WITH agent_stats AS ( coalesce((PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY connection_median_latency_ms)), -1)::FLOAT AS workspace_connection_latency_95 FROM workspace_agent_stats -- The greater than 0 is to support legacy agents that don't report connection_median_latency_ms. - WHERE workspace_agent_stats.created_at > $1 AND connection_median_latency_ms > 0 + WHERE workspace_agent_stats.created_at > $1 AND connection_median_latency_ms > 0 GROUP BY user_id, agent_id, workspace_id, template_id ), latest_agent_stats AS ( SELECT diff --git a/coderd/database/queries/workspaceagents.sql b/coderd/database/queries/workspaceagents.sql index a852bcf872cea..0d4a5343f78ba 100644 --- a/coderd/database/queries/workspaceagents.sql +++ b/coderd/database/queries/workspaceagents.sql @@ -252,7 +252,7 @@ WHERE wb.workspace_id = @workspace_id :: uuid ); --- name: GetWorkspaceAgentsByBuildID :many +-- name: GetWorkspaceAgentsByWorkspaceAndBuildNumber :many SELECT workspace_agents.* FROM diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index a1b18afe5cc9c..63a11b6468af6 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -627,7 +627,7 @@ func (s *server) acquireProtoJob(ctx context.Context, job database.ProvisionerJo // reused. Context: the agent token is often used in immutable attributes of workspace resource (e.g. VM/container) // to initialize the agent, so if that value changes it will necessitate a replacement of that resource, thus // obviating the whole point of the prebuild. - agents, err := s.Database.GetWorkspaceAgentsByBuildID(ctx, database.GetWorkspaceAgentsByBuildIDParams{ + agents, err := s.Database.GetWorkspaceAgentsByWorkspaceAndBuildNumber(ctx, database.GetWorkspaceAgentsByWorkspaceAndBuildNumberParams{ WorkspaceID: workspace.ID, BuildNumber: 1, }) From 3fa3edffa814ae82400c7eceb9d292e34a1fd007 Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Mon, 12 May 2025 12:03:46 +0000 Subject: [PATCH 34/42] make gen --- coderd/database/dbauthz/dbauthz.go | 18 +++---- coderd/database/dbauthz/dbauthz_test.go | 2 +- coderd/database/dbmem/dbmem.go | 14 +++--- coderd/database/dbmetrics/querymetrics.go | 14 +++--- coderd/database/dbmock/dbmock.go | 24 ++++----- coderd/database/querier.go | 2 +- coderd/database/queries.sql.go | 60 +++++++++++------------ 7 files changed, 67 insertions(+), 67 deletions(-) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index bddd80b1836fb..2f85e894633f5 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -3012,15 +3012,6 @@ func (q *querier) GetWorkspaceAgentUsageStatsAndLabels(ctx context.Context, crea return q.db.GetWorkspaceAgentUsageStatsAndLabels(ctx, createdAt) } -func (q *querier) GetWorkspaceAgentsByWorkspaceAndBuildNumber(ctx context.Context, arg database.GetWorkspaceAgentsByWorkspaceAndBuildNumberParams) ([]database.WorkspaceAgent, error) { - _, err := q.GetWorkspaceByID(ctx, arg.WorkspaceID) - if err != nil { - return nil, err - } - - return q.db.GetWorkspaceAgentsByWorkspaceAndBuildNumber(ctx, arg) -} - // GetWorkspaceAgentsByResourceIDs // The workspace/job is already fetched. func (q *querier) GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]database.WorkspaceAgent, error) { @@ -3030,6 +3021,15 @@ func (q *querier) GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids []uui return q.db.GetWorkspaceAgentsByResourceIDs(ctx, ids) } +func (q *querier) GetWorkspaceAgentsByWorkspaceAndBuildNumber(ctx context.Context, arg database.GetWorkspaceAgentsByWorkspaceAndBuildNumberParams) ([]database.WorkspaceAgent, error) { + _, err := q.GetWorkspaceByID(ctx, arg.WorkspaceID) + if err != nil { + return nil, err + } + + return q.db.GetWorkspaceAgentsByWorkspaceAndBuildNumber(ctx, arg) +} + func (q *querier) GetWorkspaceAgentsCreatedAfter(ctx context.Context, createdAt time.Time) ([]database.WorkspaceAgent, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return nil, err diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 4efa439395a6b..9936208ae04c1 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -2039,7 +2039,7 @@ func (s *MethodTestSuite) TestWorkspace() { check.Args(database.GetWorkspaceAgentsByWorkspaceAndBuildNumberParams{ WorkspaceID: w.ID, BuildNumber: 1, - }).Asserts(w, policy.ActionRead).Returns(agt) + }).Asserts(w, policy.ActionRead).Returns([]database.WorkspaceAgent{agt}) })) s.Run("GetWorkspaceAgentLifecycleStateByID", s.Subtest(func(db database.Store, check *expects) { u := dbgen.User(s.T(), db, database.User{}) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index f0605060163c1..9762275fd993e 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -7641,6 +7641,13 @@ func (q *FakeQuerier) GetWorkspaceAgentUsageStatsAndLabels(_ context.Context, cr return stats, nil } +func (q *FakeQuerier) GetWorkspaceAgentsByResourceIDs(ctx context.Context, resourceIDs []uuid.UUID) ([]database.WorkspaceAgent, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + return q.getWorkspaceAgentsByResourceIDsNoLock(ctx, resourceIDs) +} + func (q *FakeQuerier) GetWorkspaceAgentsByWorkspaceAndBuildNumber(ctx context.Context, arg database.GetWorkspaceAgentsByWorkspaceAndBuildNumberParams) ([]database.WorkspaceAgent, error) { err := validateDatabaseType(arg) if err != nil { @@ -7665,13 +7672,6 @@ func (q *FakeQuerier) GetWorkspaceAgentsByWorkspaceAndBuildNumber(ctx context.Co return q.GetWorkspaceAgentsByResourceIDs(ctx, resourceIDs) } -func (q *FakeQuerier) GetWorkspaceAgentsByResourceIDs(ctx context.Context, resourceIDs []uuid.UUID) ([]database.WorkspaceAgent, error) { - q.mutex.RLock() - defer q.mutex.RUnlock() - - return q.getWorkspaceAgentsByResourceIDsNoLock(ctx, resourceIDs) -} - func (q *FakeQuerier) GetWorkspaceAgentsCreatedAfter(_ context.Context, after time.Time) ([]database.WorkspaceAgent, error) { q.mutex.RLock() defer q.mutex.RUnlock() diff --git a/coderd/database/dbmetrics/querymetrics.go b/coderd/database/dbmetrics/querymetrics.go index 17e6c5756c3b0..a5a22aad1a0bf 100644 --- a/coderd/database/dbmetrics/querymetrics.go +++ b/coderd/database/dbmetrics/querymetrics.go @@ -1747,13 +1747,6 @@ func (m queryMetricsStore) GetWorkspaceAgentUsageStatsAndLabels(ctx context.Cont return r0, r1 } -func (m queryMetricsStore) GetWorkspaceAgentsByWorkspaceAndBuildNumber(ctx context.Context, arg database.GetWorkspaceAgentsByWorkspaceAndBuildNumberParams) ([]database.WorkspaceAgent, error) { - start := time.Now() - r0, r1 := m.s.GetWorkspaceAgentsByWorkspaceAndBuildNumber(ctx, arg) - m.queryLatencies.WithLabelValues("GetWorkspaceAgentsByWorkspaceAndBuildNumber").Observe(time.Since(start).Seconds()) - return r0, r1 -} - func (m queryMetricsStore) GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]database.WorkspaceAgent, error) { start := time.Now() agents, err := m.s.GetWorkspaceAgentsByResourceIDs(ctx, ids) @@ -1761,6 +1754,13 @@ func (m queryMetricsStore) GetWorkspaceAgentsByResourceIDs(ctx context.Context, return agents, err } +func (m queryMetricsStore) GetWorkspaceAgentsByWorkspaceAndBuildNumber(ctx context.Context, arg database.GetWorkspaceAgentsByWorkspaceAndBuildNumberParams) ([]database.WorkspaceAgent, error) { + start := time.Now() + r0, r1 := m.s.GetWorkspaceAgentsByWorkspaceAndBuildNumber(ctx, arg) + m.queryLatencies.WithLabelValues("GetWorkspaceAgentsByWorkspaceAndBuildNumber").Observe(time.Since(start).Seconds()) + return r0, r1 +} + func (m queryMetricsStore) GetWorkspaceAgentsCreatedAfter(ctx context.Context, createdAt time.Time) ([]database.WorkspaceAgent, error) { start := time.Now() agents, err := m.s.GetWorkspaceAgentsCreatedAfter(ctx, createdAt) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index c03b7784629a7..0d66dcec11848 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -3663,34 +3663,34 @@ func (mr *MockStoreMockRecorder) GetWorkspaceAgentUsageStatsAndLabels(ctx, creat return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAgentUsageStatsAndLabels", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAgentUsageStatsAndLabels), ctx, createdAt) } -// GetWorkspaceAgentsByWorkspaceAndBuildNumber mocks base method. -func (m *MockStore) GetWorkspaceAgentsByWorkspaceAndBuildNumber(ctx context.Context, arg database.GetWorkspaceAgentsByWorkspaceAndBuildNumberParams) ([]database.WorkspaceAgent, error) { +// GetWorkspaceAgentsByResourceIDs mocks base method. +func (m *MockStore) GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]database.WorkspaceAgent, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetWorkspaceAgentsByWorkspaceAndBuildNumber", ctx, arg) + ret := m.ctrl.Call(m, "GetWorkspaceAgentsByResourceIDs", ctx, ids) ret0, _ := ret[0].([]database.WorkspaceAgent) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetWorkspaceAgentsByWorkspaceAndBuildNumber indicates an expected call of GetWorkspaceAgentsByWorkspaceAndBuildNumber. -func (mr *MockStoreMockRecorder) GetWorkspaceAgentsByWorkspaceAndBuildNumber(ctx, arg any) *gomock.Call { +// GetWorkspaceAgentsByResourceIDs indicates an expected call of GetWorkspaceAgentsByResourceIDs. +func (mr *MockStoreMockRecorder) GetWorkspaceAgentsByResourceIDs(ctx, ids any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAgentsByWorkspaceAndBuildNumber", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAgentsByWorkspaceAndBuildNumber), ctx, arg) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAgentsByResourceIDs", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAgentsByResourceIDs), ctx, ids) } -// GetWorkspaceAgentsByResourceIDs mocks base method. -func (m *MockStore) GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]database.WorkspaceAgent, error) { +// GetWorkspaceAgentsByWorkspaceAndBuildNumber mocks base method. +func (m *MockStore) GetWorkspaceAgentsByWorkspaceAndBuildNumber(ctx context.Context, arg database.GetWorkspaceAgentsByWorkspaceAndBuildNumberParams) ([]database.WorkspaceAgent, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetWorkspaceAgentsByResourceIDs", ctx, ids) + ret := m.ctrl.Call(m, "GetWorkspaceAgentsByWorkspaceAndBuildNumber", ctx, arg) ret0, _ := ret[0].([]database.WorkspaceAgent) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetWorkspaceAgentsByResourceIDs indicates an expected call of GetWorkspaceAgentsByResourceIDs. -func (mr *MockStoreMockRecorder) GetWorkspaceAgentsByResourceIDs(ctx, ids any) *gomock.Call { +// GetWorkspaceAgentsByWorkspaceAndBuildNumber indicates an expected call of GetWorkspaceAgentsByWorkspaceAndBuildNumber. +func (mr *MockStoreMockRecorder) GetWorkspaceAgentsByWorkspaceAndBuildNumber(ctx, arg any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAgentsByResourceIDs", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAgentsByResourceIDs), ctx, ids) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAgentsByWorkspaceAndBuildNumber", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAgentsByWorkspaceAndBuildNumber), ctx, arg) } // GetWorkspaceAgentsCreatedAfter mocks base method. diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 6f9951fae0065..81b8d58758ada 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -399,8 +399,8 @@ type sqlcQuerier interface { // `minute_buckets` could return 0 rows if there are no usage stats since `created_at`. GetWorkspaceAgentUsageStats(ctx context.Context, createdAt time.Time) ([]GetWorkspaceAgentUsageStatsRow, error) GetWorkspaceAgentUsageStatsAndLabels(ctx context.Context, createdAt time.Time) ([]GetWorkspaceAgentUsageStatsAndLabelsRow, error) - GetWorkspaceAgentsByWorkspaceAndBuildNumber(ctx context.Context, arg GetWorkspaceAgentsByWorkspaceAndBuildNumberParams) ([]WorkspaceAgent, error) GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgent, error) + GetWorkspaceAgentsByWorkspaceAndBuildNumber(ctx context.Context, arg GetWorkspaceAgentsByWorkspaceAndBuildNumberParams) ([]WorkspaceAgent, error) GetWorkspaceAgentsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceAgent, error) GetWorkspaceAgentsInLatestBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) ([]WorkspaceAgent, error) GetWorkspaceAppByAgentIDAndSlug(ctx context.Context, arg GetWorkspaceAppByAgentIDAndSlugParams) (WorkspaceApp, error) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 47e04a99ada7e..6bda9d704000c 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -8174,7 +8174,7 @@ FROM provisioner_keys WHERE organization_id = $1 -AND +AND lower(name) = lower($2) ` @@ -8290,10 +8290,10 @@ WHERE AND -- exclude reserved built-in key id != '00000000-0000-0000-0000-000000000001'::uuid -AND +AND -- exclude reserved user-auth key id != '00000000-0000-0000-0000-000000000002'::uuid -AND +AND -- exclude reserved psk key id != '00000000-0000-0000-0000-000000000003'::uuid ` @@ -10055,7 +10055,7 @@ func (q *sqlQuerier) GetTailnetTunnelPeerIDs(ctx context.Context, srcID uuid.UUI } const updateTailnetPeerStatusByCoordinator = `-- name: UpdateTailnetPeerStatusByCoordinator :exec -UPDATE +UPDATE tailnet_peers SET status = $2 @@ -11899,14 +11899,14 @@ DO $$ DECLARE table_record record; BEGIN - FOR table_record IN - SELECT table_schema, table_name - FROM information_schema.tables + FOR table_record IN + SELECT table_schema, table_name + FROM information_schema.tables WHERE table_schema NOT IN ('pg_catalog', 'information_schema') AND table_type = 'BASE TABLE' LOOP - EXECUTE format('ALTER TABLE %I.%I DISABLE TRIGGER ALL', - table_record.table_schema, + EXECUTE format('ALTER TABLE %I.%I DISABLE TRIGGER ALL', + table_record.table_schema, table_record.table_name); END LOOP; END; @@ -14339,27 +14339,17 @@ func (q *sqlQuerier) GetWorkspaceAgentScriptTimingsByBuildID(ctx context.Context return items, nil } -const getWorkspaceAgentsByBuildID = `-- name: GetWorkspaceAgentsByWorkspaceAndBuildNumber :many +const getWorkspaceAgentsByResourceIDs = `-- name: GetWorkspaceAgentsByResourceIDs :many SELECT - workspace_agents.id, workspace_agents.created_at, workspace_agents.updated_at, workspace_agents.name, workspace_agents.first_connected_at, workspace_agents.last_connected_at, workspace_agents.disconnected_at, workspace_agents.resource_id, workspace_agents.auth_token, workspace_agents.auth_instance_id, workspace_agents.architecture, workspace_agents.environment_variables, workspace_agents.operating_system, workspace_agents.instance_metadata, workspace_agents.resource_metadata, workspace_agents.directory, workspace_agents.version, workspace_agents.last_connected_replica_id, workspace_agents.connection_timeout_seconds, workspace_agents.troubleshooting_url, workspace_agents.motd_file, workspace_agents.lifecycle_state, workspace_agents.expanded_directory, workspace_agents.logs_length, workspace_agents.logs_overflowed, workspace_agents.started_at, workspace_agents.ready_at, workspace_agents.subsystems, workspace_agents.display_apps, workspace_agents.api_version, workspace_agents.display_order + id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps, api_version, display_order FROM workspace_agents -JOIN - workspace_resources ON workspace_agents.resource_id = workspace_resources.id -JOIN - workspace_builds ON workspace_resources.job_id = workspace_builds.job_id WHERE - workspace_builds.workspace_id = $1 :: uuid AND - workspace_builds.build_number = $2 :: int + resource_id = ANY($1 :: uuid [ ]) ` -type GetWorkspaceAgentsByWorkspaceAndBuildNumberParams struct { - WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"` - BuildNumber int32 `db:"build_number" json:"build_number"` -} - -func (q *sqlQuerier) GetWorkspaceAgentsByWorkspaceAndBuildNumber(ctx context.Context, arg GetWorkspaceAgentsByWorkspaceAndBuildNumberParams) ([]WorkspaceAgent, error) { - rows, err := q.db.QueryContext(ctx, getWorkspaceAgentsByBuildID, arg.WorkspaceID, arg.BuildNumber) +func (q *sqlQuerier) GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgent, error) { + rows, err := q.db.QueryContext(ctx, getWorkspaceAgentsByResourceIDs, pq.Array(ids)) if err != nil { return nil, err } @@ -14413,17 +14403,27 @@ func (q *sqlQuerier) GetWorkspaceAgentsByWorkspaceAndBuildNumber(ctx context.Con return items, nil } -const getWorkspaceAgentsByResourceIDs = `-- name: GetWorkspaceAgentsByResourceIDs :many +const getWorkspaceAgentsByWorkspaceAndBuildNumber = `-- name: GetWorkspaceAgentsByWorkspaceAndBuildNumber :many SELECT - id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps, api_version, display_order + workspace_agents.id, workspace_agents.created_at, workspace_agents.updated_at, workspace_agents.name, workspace_agents.first_connected_at, workspace_agents.last_connected_at, workspace_agents.disconnected_at, workspace_agents.resource_id, workspace_agents.auth_token, workspace_agents.auth_instance_id, workspace_agents.architecture, workspace_agents.environment_variables, workspace_agents.operating_system, workspace_agents.instance_metadata, workspace_agents.resource_metadata, workspace_agents.directory, workspace_agents.version, workspace_agents.last_connected_replica_id, workspace_agents.connection_timeout_seconds, workspace_agents.troubleshooting_url, workspace_agents.motd_file, workspace_agents.lifecycle_state, workspace_agents.expanded_directory, workspace_agents.logs_length, workspace_agents.logs_overflowed, workspace_agents.started_at, workspace_agents.ready_at, workspace_agents.subsystems, workspace_agents.display_apps, workspace_agents.api_version, workspace_agents.display_order FROM workspace_agents +JOIN + workspace_resources ON workspace_agents.resource_id = workspace_resources.id +JOIN + workspace_builds ON workspace_resources.job_id = workspace_builds.job_id WHERE - resource_id = ANY($1 :: uuid [ ]) + workspace_builds.workspace_id = $1 :: uuid AND + workspace_builds.build_number = $2 :: int ` -func (q *sqlQuerier) GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgent, error) { - rows, err := q.db.QueryContext(ctx, getWorkspaceAgentsByResourceIDs, pq.Array(ids)) +type GetWorkspaceAgentsByWorkspaceAndBuildNumberParams struct { + WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"` + BuildNumber int32 `db:"build_number" json:"build_number"` +} + +func (q *sqlQuerier) GetWorkspaceAgentsByWorkspaceAndBuildNumber(ctx context.Context, arg GetWorkspaceAgentsByWorkspaceAndBuildNumberParams) ([]WorkspaceAgent, error) { + rows, err := q.db.QueryContext(ctx, getWorkspaceAgentsByWorkspaceAndBuildNumber, arg.WorkspaceID, arg.BuildNumber) if err != nil { return nil, err } @@ -15343,7 +15343,7 @@ WITH agent_stats AS ( coalesce((PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY connection_median_latency_ms)), -1)::FLOAT AS workspace_connection_latency_95 FROM workspace_agent_stats -- The greater than 0 is to support legacy agents that don't report connection_median_latency_ms. - WHERE workspace_agent_stats.created_at > $1 AND connection_median_latency_ms > 0 + WHERE workspace_agent_stats.created_at > $1 AND connection_median_latency_ms > 0 GROUP BY user_id, agent_id, workspace_id, template_id ), latest_agent_stats AS ( SELECT From 7e45919b9ac791ab82fae2f238ae597f4f1a2e1c Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Mon, 12 May 2025 13:53:04 +0000 Subject: [PATCH 35/42] fix a race condition --- codersdk/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codersdk/client.go b/codersdk/client.go index 8ab5a289b2cf5..4492066785d6f 100644 --- a/codersdk/client.go +++ b/codersdk/client.go @@ -631,7 +631,7 @@ func (h *HeaderTransport) RoundTrip(req *http.Request) (*http.Response, error) { } } if h.Transport == nil { - h.Transport = http.DefaultTransport + return http.DefaultTransport.RoundTrip(req) } return h.Transport.RoundTrip(req) } From b65eea7e947f5552316d488128bdedfe279ad038 Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Tue, 13 May 2025 07:24:56 +0000 Subject: [PATCH 36/42] fix provisionerdserver test for prebuild claims --- coderd/provisionerdserver/provisionerdserver.go | 2 +- coderd/provisionerdserver/provisionerdserver_test.go | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index 82a5dbfa71283..f8a33d3a55188 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -1840,7 +1840,7 @@ func (s *server) CompleteJob(ctx context.Context, completed *proto.CompletedJob) return nil, xerrors.Errorf("update workspace: %w", err) } - if input.PrebuiltWorkspaceBuildStage != sdkproto.PrebuiltWorkspaceBuildStage_CLAIM { + if input.PrebuiltWorkspaceBuildStage == sdkproto.PrebuiltWorkspaceBuildStage_CLAIM { s.Logger.Info(ctx, "workspace prebuild successfully claimed by user", slog.F("workspace_id", workspace.ID)) diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go index b7455c8635fc4..37719a17620ea 100644 --- a/coderd/provisionerdserver/provisionerdserver_test.go +++ b/coderd/provisionerdserver/provisionerdserver_test.go @@ -1779,8 +1779,6 @@ func TestCompleteJob(t *testing.T) { // GIVEN an enqueued provisioner job and its dependencies: - userID := uuid.New() - srv, db, ps, pd := setup(t, false, &overrides{}) buildID := uuid.New() @@ -1848,7 +1846,7 @@ func TestCompleteJob(t *testing.T) { case reinitEvent := <-reinitChan: // THEN workspace agent reinitialization instruction was received: require.True(t, tc.shouldReinitializeAgent) - require.Equal(t, userID, reinitEvent.UserID) + require.Equal(t, workspace.ID, reinitEvent.WorkspaceID) default: // THEN workspace agent reinitialization instruction was not received. require.False(t, tc.shouldReinitializeAgent) From e1339f34b90fa29d81a07bbefabd914f1d326eb9 Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Tue, 13 May 2025 08:42:16 +0000 Subject: [PATCH 37/42] fix race conditions --- codersdk/agentsdk/agentsdk.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/codersdk/agentsdk/agentsdk.go b/codersdk/agentsdk/agentsdk.go index b9dc57a7e15d9..418d903dc0eb2 100644 --- a/codersdk/agentsdk/agentsdk.go +++ b/codersdk/agentsdk/agentsdk.go @@ -806,6 +806,11 @@ func (s *SSEAgentReinitTransmitter) Transmit(ctx context.Context, reinitEvents < return xerrors.Errorf("failed to create sse transmitter: %w", err) } + // Prevent handler from returning until the sender is closed. + defer func() { + <-sseSenderClosed + }() + for { select { case <-ctx.Done(): From 5363dcc078c6dd2c6e83d58c42279a8fde9d9997 Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Tue, 13 May 2025 13:24:57 +0000 Subject: [PATCH 38/42] Make TestReinitializeAgent more robust Minor code quality enhancements --- cli/agent.go | 2 +- coderd/prebuilds/claim.go | 12 +-- coderd/prebuilds/claim_test.go | 27 +++--- .../provisionerdserver_test.go | 50 +++++++---- coderd/workspaceagents_test.go | 88 +++++++++---------- codersdk/agentsdk/agentsdk.go | 4 +- codersdk/agentsdk/agentsdk_test.go | 3 - enterprise/coderd/workspaceagents_test.go | 21 ++--- enterprise/coderd/workspaces_test.go | 2 +- 9 files changed, 103 insertions(+), 106 deletions(-) diff --git a/cli/agent.go b/cli/agent.go index f744d524fee08..50d9db18beee1 100644 --- a/cli/agent.go +++ b/cli/agent.go @@ -377,7 +377,7 @@ func (r *RootCmd) workspaceAgent() *serpent.Command { mustExit = true case event := <-reinitEvents: logger.Info(ctx, "agent received instruction to reinitialize", - slog.F("user_id", event.UserID), slog.F("workspace_id", event.WorkspaceID), slog.F("reason", event.Reason)) + slog.F("workspace_id", event.WorkspaceID), slog.F("reason", event.Reason)) } lastErr = agnt.Close() diff --git a/coderd/prebuilds/claim.go b/coderd/prebuilds/claim.go index e819018120d4c..b5155b8f2a568 100644 --- a/coderd/prebuilds/claim.go +++ b/coderd/prebuilds/claim.go @@ -22,7 +22,7 @@ type PubsubWorkspaceClaimPublisher struct { func (p PubsubWorkspaceClaimPublisher) PublishWorkspaceClaim(claim agentsdk.ReinitializationEvent) error { channel := agentsdk.PrebuildClaimedChannel(claim.WorkspaceID) - if err := p.ps.Publish(channel, []byte(claim.UserID.String())); err != nil { + if err := p.ps.Publish(channel, []byte(claim.Reason)); err != nil { return xerrors.Errorf("failed to trigger prebuilt workspace agent reinitialization: %w", err) } return nil @@ -48,16 +48,10 @@ func (p PubsubWorkspaceClaimListener) ListenForWorkspaceClaims(ctx context.Conte default: } - cancelSub, err := p.ps.Subscribe(agentsdk.PrebuildClaimedChannel(workspaceID), func(inner context.Context, id []byte) { - claimantID, err := uuid.ParseBytes(id) - if err != nil { - p.logger.Error(ctx, "invalid prebuild claimed channel payload", slog.F("input", string(id))) - return - } + cancelSub, err := p.ps.Subscribe(agentsdk.PrebuildClaimedChannel(workspaceID), func(inner context.Context, reason []byte) { claim := agentsdk.ReinitializationEvent{ - UserID: claimantID, WorkspaceID: workspaceID, - Reason: agentsdk.ReinitializeReasonPrebuildClaimed, + Reason: agentsdk.ReinitializationReason(reason), } select { diff --git a/coderd/prebuilds/claim_test.go b/coderd/prebuilds/claim_test.go index b447cfa1176b9..670bb64eec756 100644 --- a/coderd/prebuilds/claim_test.go +++ b/coderd/prebuilds/claim_test.go @@ -25,7 +25,6 @@ func TestPubsubWorkspaceClaimPublisher(t *testing.T) { logger := testutil.Logger(t) ps := pubsub.NewInMemory() workspaceID := uuid.New() - userID := uuid.New() reinitEvents := make(chan agentsdk.ReinitializationEvent, 1) publisher := prebuilds.NewPubsubWorkspaceClaimPublisher(ps) listener := prebuilds.NewPubsubWorkspaceClaimListener(ps, logger) @@ -35,15 +34,15 @@ func TestPubsubWorkspaceClaimPublisher(t *testing.T) { defer cancel() claim := agentsdk.ReinitializationEvent{ - UserID: userID, WorkspaceID: workspaceID, Reason: agentsdk.ReinitializeReasonPrebuildClaimed, } err = publisher.PublishWorkspaceClaim(claim) require.NoError(t, err) - gotUserID := testutil.RequireReceive(testutil.Context(t, testutil.WaitShort), t, reinitEvents) - require.Equal(t, userID, gotUserID.UserID) + gotEvent := testutil.RequireReceive(ctx, t, reinitEvents) + require.Equal(t, workspaceID, gotEvent.WorkspaceID) + require.Equal(t, claim.Reason, gotEvent.Reason) }) t.Run("fail to publish claim", func(t *testing.T) { @@ -53,7 +52,6 @@ func TestPubsubWorkspaceClaimPublisher(t *testing.T) { publisher := prebuilds.NewPubsubWorkspaceClaimPublisher(ps) claim := agentsdk.ReinitializationEvent{ - UserID: uuid.New(), WorkspaceID: uuid.New(), Reason: agentsdk.ReinitializeReasonPrebuildClaimed, } @@ -74,26 +72,21 @@ func TestPubsubWorkspaceClaimListener(t *testing.T) { claims := make(chan agentsdk.ReinitializationEvent, 1) // Buffer to avoid messing with goroutines in the rest of the test workspaceID := uuid.New() - userID := uuid.New() cancelFunc, err := listener.ListenForWorkspaceClaims(context.Background(), workspaceID, claims) require.NoError(t, err) defer cancelFunc() // Publish a claim channel := agentsdk.PrebuildClaimedChannel(workspaceID) - err = ps.Publish(channel, []byte(userID.String())) + reason := agentsdk.ReinitializeReasonPrebuildClaimed + err = ps.Publish(channel, []byte(reason)) require.NoError(t, err) // Verify we receive the claim - select { - case claim, ok := <-claims: - require.True(t, ok, "received on a closed channel") - require.Equal(t, userID, claim.UserID) - require.Equal(t, workspaceID, claim.WorkspaceID) - require.Equal(t, agentsdk.ReinitializeReasonPrebuildClaimed, claim.Reason) - case <-time.After(testutil.WaitSuperLong): // TODO: revert to waitshort - t.Fatal("timeout waiting for claim") - } + ctx := testutil.Context(t, testutil.WaitShort) + claim := testutil.RequireReceive(ctx, t, claims) + require.Equal(t, workspaceID, claim.WorkspaceID) + require.Equal(t, reason, claim.Reason) }) t.Run("ignores claim events for other workspaces", func(t *testing.T) { @@ -111,7 +104,7 @@ func TestPubsubWorkspaceClaimListener(t *testing.T) { // Publish a claim for a different workspace channel := agentsdk.PrebuildClaimedChannel(otherWorkspaceID) - err = ps.Publish(channel, []byte(uuid.New().String())) + err = ps.Publish(channel, []byte(agentsdk.ReinitializeReasonPrebuildClaimed)) require.NoError(t, err) // Verify we don't receive the claim diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go index 37719a17620ea..f6047d83afc3e 100644 --- a/coderd/provisionerdserver/provisionerdserver_test.go +++ b/coderd/provisionerdserver/provisionerdserver_test.go @@ -169,8 +169,12 @@ func TestAcquireJob(t *testing.T) { _, err = tc.acquire(ctx, srv) require.ErrorContains(t, err, "sql: no rows in result set") }) - for _, prebuiltWorkspace := range []bool{false, true} { - prebuiltWorkspace := prebuiltWorkspace + for _, prebuiltWorkspaceBuildStage := range []sdkproto.PrebuiltWorkspaceBuildStage{ + sdkproto.PrebuiltWorkspaceBuildStage_NONE, + sdkproto.PrebuiltWorkspaceBuildStage_CREATE, + sdkproto.PrebuiltWorkspaceBuildStage_CLAIM, + } { + prebuiltWorkspaceBuildStage := prebuiltWorkspaceBuildStage t.Run(tc.name+"_WorkspaceBuildJob", func(t *testing.T) { t.Parallel() // Set the max session token lifetime so we can assert we @@ -294,31 +298,43 @@ func TestAcquireJob(t *testing.T) { OwnerID: user.ID, OrganizationID: pd.OrganizationID, }) - build := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ + build := database.WorkspaceBuild{ WorkspaceID: workspace.ID, BuildNumber: 1, JobID: uuid.New(), TemplateVersionID: version.ID, Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator, - }) - var buildState sdkproto.PrebuiltWorkspaceBuildStage - if prebuiltWorkspace { - buildState = sdkproto.PrebuiltWorkspaceBuildStage_CREATE } - _ = dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{ - ID: build.ID, + input := provisionerdserver.WorkspaceProvisionJob{ + WorkspaceBuildID: build.ID, + } + dbJob := database.ProvisionerJob{ + ID: build.JobID, OrganizationID: pd.OrganizationID, InitiatorID: user.ID, Provisioner: database.ProvisionerTypeEcho, StorageMethod: database.ProvisionerStorageMethodFile, FileID: file.ID, Type: database.ProvisionerJobTypeWorkspaceBuild, - Input: must(json.Marshal(provisionerdserver.WorkspaceProvisionJob{ - WorkspaceBuildID: build.ID, - PrebuiltWorkspaceBuildStage: buildState, - })), - }) + Input: must(json.Marshal(input)), + } + if prebuiltWorkspaceBuildStage == sdkproto.PrebuiltWorkspaceBuildStage_CLAIM { + dbgen.WorkspaceBuild(t, db, build) + prebuildJob := dbgen.ProvisionerJob(t, db, ps, dbJob) + resource := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{ + JobID: prebuildJob.ID, + }) + dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{ + ResourceID: resource.ID, + AuthToken: uuid.New(), + }) + build.BuildNumber = 2 + input.PrebuiltWorkspaceBuildStage = sdkproto.PrebuiltWorkspaceBuildStage_CLAIM + dbJob.Input = must(json.Marshal(input)) + } + build = dbgen.WorkspaceBuild(t, db, build) + dbJob = dbgen.ProvisionerJob(t, db, ps, dbJob) startPublished := make(chan struct{}) var closed bool @@ -352,7 +368,7 @@ func TestAcquireJob(t *testing.T) { <-startPublished - got, err := json.Marshal(job.Type) + got, err := json.Marshal(dbJob.Type) require.NoError(t, err) // Validate that a session token is generated during the job. @@ -385,9 +401,7 @@ func TestAcquireJob(t *testing.T) { WorkspaceBuildId: build.ID.String(), WorkspaceOwnerLoginType: string(user.LoginType), WorkspaceOwnerRbacRoles: []*sdkproto.Role{{Name: rbac.RoleOrgMember(), OrgId: pd.OrganizationID.String()}, {Name: "member", OrgId: ""}, {Name: rbac.RoleOrgAuditor(), OrgId: pd.OrganizationID.String()}}, - } - if prebuiltWorkspace { - wantedMetadata.PrebuiltWorkspaceBuildStage = sdkproto.PrebuiltWorkspaceBuildStage_CREATE + PrebuiltWorkspaceBuildStage: prebuiltWorkspaceBuildStage, } slices.SortFunc(wantedMetadata.WorkspaceOwnerRbacRoles, func(a, b *sdkproto.Role) int { diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go index 89615953b9407..9e549b00aa78c 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -2647,62 +2647,60 @@ func TestAgentConnectionInfo(t *testing.T) { func TestReinit(t *testing.T) { t.Parallel() - t.Run("claimed workspaces are reinitialized", func(t *testing.T) { - t.Parallel() - - db, ps := dbtestutil.NewDB(t) - pubsubSpy := pubsubReinitSpy{ - Pubsub: ps, - subscriptions: make(chan string), - } - client := coderdtest.New(t, &coderdtest.Options{ - Database: db, - Pubsub: pubsubSpy, - }) - user := coderdtest.CreateFirstUser(t, client) - - r := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{ - OrganizationID: user.OrganizationID, - OwnerID: user.UserID, - }).WithAgent().Do() + db, ps := dbtestutil.NewDB(t) + pubsubSpy := pubsubReinitSpy{ + Pubsub: ps, + subscribed: make(chan string), + } + client := coderdtest.New(t, &coderdtest.Options{ + Database: db, + Pubsub: &pubsubSpy, + }) + user := coderdtest.CreateFirstUser(t, client) - agentCtx := testutil.Context(t, testutil.WaitShort) - agentClient := agentsdk.New(client.URL) - agentClient.SetSessionToken(r.AgentToken) + r := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{ + OrganizationID: user.OrganizationID, + OwnerID: user.UserID, + }).WithAgent().Do() + pubsubSpy.expectedEvent = agentsdk.PrebuildClaimedChannel(r.Workspace.ID) - agentReinitializedCh := make(chan *agentsdk.ReinitializationEvent) - go func() { - reinitEvent, err := agentClient.WaitForReinit(agentCtx) - assert.NoError(t, err) - agentReinitializedCh <- reinitEvent - }() + agentCtx := testutil.Context(t, testutil.WaitShort) + agentClient := agentsdk.New(client.URL) + agentClient.SetSessionToken(r.AgentToken) - // We need to subscribe before we publish, lest we miss the event - for subscription := range pubsubSpy.subscriptions { - if subscription == agentsdk.PrebuildClaimedChannel(r.Workspace.ID) { - break - } - } + agentReinitializedCh := make(chan *agentsdk.ReinitializationEvent) + go func() { + reinitEvent, err := agentClient.WaitForReinit(agentCtx) + assert.NoError(t, err) + agentReinitializedCh <- reinitEvent + }() - err := prebuilds.NewPubsubWorkspaceClaimPublisher(ps).PublishWorkspaceClaim(agentsdk.ReinitializationEvent{ - WorkspaceID: r.Workspace.ID, - UserID: user.UserID, - }) - require.NoError(t, err) + // We need to subscribe before we publish, lest we miss the event + ctx := testutil.Context(t, testutil.WaitShort) + testutil.TryReceive(ctx, t, pubsubSpy.subscribed) // Wait for the appropriate subscription - ctx := testutil.Context(t, testutil.WaitShort) - reinitEvent := testutil.TryReceive(ctx, t, agentReinitializedCh) - require.NotNil(t, reinitEvent) - require.Equal(t, r.Workspace.ID, reinitEvent.WorkspaceID) + // Now that we're subscribed, publish the event + err := prebuilds.NewPubsubWorkspaceClaimPublisher(ps).PublishWorkspaceClaim(agentsdk.ReinitializationEvent{ + WorkspaceID: r.Workspace.ID, + Reason: agentsdk.ReinitializeReasonPrebuildClaimed, }) + require.NoError(t, err) + + ctx = testutil.Context(t, testutil.WaitShort) + reinitEvent := testutil.TryReceive(ctx, t, agentReinitializedCh) + require.NotNil(t, reinitEvent) + require.Equal(t, r.Workspace.ID, reinitEvent.WorkspaceID) } type pubsubReinitSpy struct { pubsub.Pubsub - subscriptions chan string + subscribed chan string + expectedEvent string } -func (p pubsubReinitSpy) Subscribe(event string, listener pubsub.Listener) (cancel func(), err error) { - p.subscriptions <- event +func (p *pubsubReinitSpy) Subscribe(event string, listener pubsub.Listener) (cancel func(), err error) { + if p.expectedEvent != "" && event == p.expectedEvent { + close(p.subscribed) + } return p.Pubsub.Subscribe(event, listener) } diff --git a/codersdk/agentsdk/agentsdk.go b/codersdk/agentsdk/agentsdk.go index 418d903dc0eb2..ba3ff5681b742 100644 --- a/codersdk/agentsdk/agentsdk.go +++ b/codersdk/agentsdk/agentsdk.go @@ -698,7 +698,6 @@ const ( type ReinitializationEvent struct { WorkspaceID uuid.UUID - UserID uuid.UUID Reason ReinitializationReason `json:"reason"` } @@ -806,8 +805,9 @@ func (s *SSEAgentReinitTransmitter) Transmit(ctx context.Context, reinitEvents < return xerrors.Errorf("failed to create sse transmitter: %w", err) } - // Prevent handler from returning until the sender is closed. defer func() { + // Block returning until the ServerSentEventSender is closed + // to avoid a race condition where we might write or flush to rw after the handler returns. <-sseSenderClosed }() diff --git a/codersdk/agentsdk/agentsdk_test.go b/codersdk/agentsdk/agentsdk_test.go index ed044ae1534ee..8ad2d69be0b98 100644 --- a/codersdk/agentsdk/agentsdk_test.go +++ b/codersdk/agentsdk/agentsdk_test.go @@ -22,7 +22,6 @@ func TestStreamAgentReinitEvents(t *testing.T) { t.Parallel() eventToSend := agentsdk.ReinitializationEvent{ - UserID: uuid.New(), WorkspaceID: uuid.New(), Reason: agentsdk.ReinitializeReasonPrebuildClaimed, } @@ -56,7 +55,6 @@ func TestStreamAgentReinitEvents(t *testing.T) { t.Parallel() eventToSend := agentsdk.ReinitializationEvent{ - UserID: uuid.New(), WorkspaceID: uuid.New(), Reason: agentsdk.ReinitializeReasonPrebuildClaimed, } @@ -92,7 +90,6 @@ func TestStreamAgentReinitEvents(t *testing.T) { t.Parallel() eventToSend := agentsdk.ReinitializationEvent{ - UserID: uuid.New(), WorkspaceID: uuid.New(), Reason: agentsdk.ReinitializeReasonPrebuildClaimed, } diff --git a/enterprise/coderd/workspaceagents_test.go b/enterprise/coderd/workspaceagents_test.go index 944c44d2c195a..7599acb0159e8 100644 --- a/enterprise/coderd/workspaceagents_test.go +++ b/enterprise/coderd/workspaceagents_test.go @@ -6,6 +6,7 @@ import ( "fmt" "net/http" "os" + "regexp" "testing" "time" @@ -18,7 +19,6 @@ import ( "github.com/coder/coder/v2/agent" "github.com/coder/coder/v2/cli/clitest" "github.com/coder/coder/v2/coderd/coderdtest" - "github.com/coder/coder/v2/coderd/prebuilds" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/agentsdk" "github.com/coder/coder/v2/codersdk/workspacesdk" @@ -155,7 +155,7 @@ func TestReinitializeAgent(t *testing.T) { Scripts: []*proto.Script{ { RunOnStart: true, - Script: fmt.Sprintf("sleep 5; printenv | grep 'CODER_AGENT_TOKEN' >> %s; echo '---\n' >> %s", tempAgentLog.Name(), tempAgentLog.Name()), // Make reinitialization take long enough to assert that it happened + Script: fmt.Sprintf("printenv >> %s; echo '---\n' >> %s", tempAgentLog.Name(), tempAgentLog.Name()), // Make reinitialization take long enough to assert that it happened }, }, Auth: &proto.Agent_Token{ @@ -219,20 +219,21 @@ func TestReinitializeAgent(t *testing.T) { require.NoError(t, err) coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID) - prebuilds.NewPubsubWorkspaceClaimPublisher(ps).PublishWorkspaceClaim(agentsdk.ReinitializationEvent{ - WorkspaceID: workspace.ID, - UserID: user.UserID, - }) - - // THEN the now claimed workspace agent reinitializes - waiter.WaitFor(coderdtest.AgentsNotReady) // THEN reinitialization completes waiter.WaitFor(coderdtest.AgentsReady) // THEN the agent script ran again and reused the same agent token contents, err := os.ReadFile(tempAgentLog.Name()) - _ = contents + // UUID regex pattern (matches UUID v4-like strings) + uuidRegex := regexp.MustCompile(`\bCODER_AGENT_TOKEN=(.+)\b`) + + matches := uuidRegex.FindAll(contents, -1) + // When an agent reinitializes, we expect it to run startup scripts again. + // As such, we expect to have written the agent environment to the temp file twice. + // Once on initial startup and then once on reinitialization. + require.Len(t, matches, 2, "expected exactly 1 occurrence of the agent token per startup script execution") + require.Equal(t, matches[0], matches[1]) require.NoError(t, err) } diff --git a/enterprise/coderd/workspaces_test.go b/enterprise/coderd/workspaces_test.go index 48bab1b55dfd4..7005c93ca36f5 100644 --- a/enterprise/coderd/workspaces_test.go +++ b/enterprise/coderd/workspaces_test.go @@ -527,7 +527,7 @@ func TestCreateUserWorkspace(t *testing.T) { }) require.NoError(t, err) - // THEN a new build is scheduled with the claimant and agent tokens specified + // THEN a new build is scheduled with the build stage specified build, err := db.GetLatestWorkspaceBuildByWorkspaceID(ctx, r.Workspace.ID) require.NoError(t, err) require.NotEqual(t, build.ID, r.Build.ID) From 7ad9b6d12cadec0cc753e4df7deb24a81432d6a4 Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Wed, 14 May 2025 08:59:00 +0000 Subject: [PATCH 39/42] fix tests --- .../provisionerdserver_test.go | 75 +++++++++++++++---- coderd/workspaceagents_test.go | 4 + enterprise/coderd/workspaceagents_test.go | 25 ++++--- 3 files changed, 80 insertions(+), 24 deletions(-) diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go index f6047d83afc3e..f402968b97d60 100644 --- a/coderd/provisionerdserver/provisionerdserver_test.go +++ b/coderd/provisionerdserver/provisionerdserver_test.go @@ -218,7 +218,7 @@ func TestAcquireJob(t *testing.T) { Roles: []string{rbac.RoleOrgAuditor()}, }) - // Add extra erronous roles + // Add extra erroneous roles secondOrg := dbgen.Organization(t, db, database.Organization{}) dbgen.OrganizationMember(t, db, database.OrganizationMember{ UserID: user.ID, @@ -293,11 +293,12 @@ func TestAcquireJob(t *testing.T) { Required: true, Sensitive: false, }) - workspace := dbgen.Workspace(t, db, database.WorkspaceTable{ + workspace := database.WorkspaceTable{ TemplateID: template.ID, OwnerID: user.ID, OrganizationID: pd.OrganizationID, - }) + } + workspace = dbgen.Workspace(t, db, workspace) build := database.WorkspaceBuild{ WorkspaceID: workspace.ID, BuildNumber: 1, @@ -306,6 +307,7 @@ func TestAcquireJob(t *testing.T) { Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator, } + build = dbgen.WorkspaceBuild(t, db, build) input := provisionerdserver.WorkspaceProvisionJob{ WorkspaceBuildID: build.ID, } @@ -319,22 +321,46 @@ func TestAcquireJob(t *testing.T) { Type: database.ProvisionerJobTypeWorkspaceBuild, Input: must(json.Marshal(input)), } + dbJob = dbgen.ProvisionerJob(t, db, ps, dbJob) + + var agent database.WorkspaceAgent if prebuiltWorkspaceBuildStage == sdkproto.PrebuiltWorkspaceBuildStage_CLAIM { - dbgen.WorkspaceBuild(t, db, build) - prebuildJob := dbgen.ProvisionerJob(t, db, ps, dbJob) resource := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{ - JobID: prebuildJob.ID, + JobID: dbJob.ID, }) - dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{ + agent = dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{ ResourceID: resource.ID, AuthToken: uuid.New(), }) - build.BuildNumber = 2 - input.PrebuiltWorkspaceBuildStage = sdkproto.PrebuiltWorkspaceBuildStage_CLAIM - dbJob.Input = must(json.Marshal(input)) + // At this point we have an unclaimed workspace and build, now we need to setup the claim + // build + build = database.WorkspaceBuild{ + WorkspaceID: workspace.ID, + BuildNumber: 2, + JobID: uuid.New(), + TemplateVersionID: version.ID, + Transition: database.WorkspaceTransitionStart, + Reason: database.BuildReasonInitiator, + InitiatorID: user.ID, + } + build = dbgen.WorkspaceBuild(t, db, build) + + input = provisionerdserver.WorkspaceProvisionJob{ + WorkspaceBuildID: build.ID, + PrebuiltWorkspaceBuildStage: prebuiltWorkspaceBuildStage, + } + dbJob = database.ProvisionerJob{ + ID: build.JobID, + OrganizationID: pd.OrganizationID, + InitiatorID: user.ID, + Provisioner: database.ProvisionerTypeEcho, + StorageMethod: database.ProvisionerStorageMethodFile, + FileID: file.ID, + Type: database.ProvisionerJobTypeWorkspaceBuild, + Input: must(json.Marshal(input)), + } + dbJob = dbgen.ProvisionerJob(t, db, ps, dbJob) } - build = dbgen.WorkspaceBuild(t, db, build) - dbJob = dbgen.ProvisionerJob(t, db, ps, dbJob) startPublished := make(chan struct{}) var closed bool @@ -368,7 +394,20 @@ func TestAcquireJob(t *testing.T) { <-startPublished - got, err := json.Marshal(dbJob.Type) + if prebuiltWorkspaceBuildStage == sdkproto.PrebuiltWorkspaceBuildStage_CLAIM { + for { + // In the case of a prebuild claim, there is a second build, which is the + // one that we're interested in. + job, err = tc.acquire(ctx, srv) + require.NoError(t, err) + if _, ok := job.Type.(*proto.AcquiredJob_WorkspaceBuild_); ok { + break + } + } + <-startPublished + } + + got, err := json.Marshal(job.Type) require.NoError(t, err) // Validate that a session token is generated during the job. @@ -401,7 +440,15 @@ func TestAcquireJob(t *testing.T) { WorkspaceBuildId: build.ID.String(), WorkspaceOwnerLoginType: string(user.LoginType), WorkspaceOwnerRbacRoles: []*sdkproto.Role{{Name: rbac.RoleOrgMember(), OrgId: pd.OrganizationID.String()}, {Name: "member", OrgId: ""}, {Name: rbac.RoleOrgAuditor(), OrgId: pd.OrganizationID.String()}}, - PrebuiltWorkspaceBuildStage: prebuiltWorkspaceBuildStage, + } + if prebuiltWorkspaceBuildStage == sdkproto.PrebuiltWorkspaceBuildStage_CLAIM { + // For claimed prebuilds, we expect the prebuild state to be set to CLAIM + // and we expect tokens from the first build to be set for reuse + wantedMetadata.PrebuiltWorkspaceBuildStage = prebuiltWorkspaceBuildStage + wantedMetadata.RunningAgentAuthTokens = append(wantedMetadata.RunningAgentAuthTokens, &sdkproto.RunningAgentAuthToken{ + AgentId: agent.ID.String(), + Token: agent.AuthToken.String(), + }) } slices.SortFunc(wantedMetadata.WorkspaceOwnerRbacRoles, func(a, b *sdkproto.Role) int { diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go index 9e549b00aa78c..432e6b109ec59 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -11,6 +11,7 @@ import ( "runtime" "strconv" "strings" + "sync" "sync/atomic" "testing" "time" @@ -2694,13 +2695,16 @@ func TestReinit(t *testing.T) { type pubsubReinitSpy struct { pubsub.Pubsub + sync.Mutex subscribed chan string expectedEvent string } func (p *pubsubReinitSpy) Subscribe(event string, listener pubsub.Listener) (cancel func(), err error) { + p.Lock() if p.expectedEvent != "" && event == p.expectedEvent { close(p.subscribed) } + p.Unlock() return p.Pubsub.Subscribe(event, listener) } diff --git a/enterprise/coderd/workspaceagents_test.go b/enterprise/coderd/workspaceagents_test.go index 7599acb0159e8..44aba69b9ffaa 100644 --- a/enterprise/coderd/workspaceagents_test.go +++ b/enterprise/coderd/workspaceagents_test.go @@ -223,18 +223,23 @@ func TestReinitializeAgent(t *testing.T) { // THEN reinitialization completes waiter.WaitFor(coderdtest.AgentsReady) - // THEN the agent script ran again and reused the same agent token - contents, err := os.ReadFile(tempAgentLog.Name()) - // UUID regex pattern (matches UUID v4-like strings) - uuidRegex := regexp.MustCompile(`\bCODER_AGENT_TOKEN=(.+)\b`) + var matches [][]byte + require.Eventually(t, func() bool { + // THEN the agent script ran again and reused the same agent token + contents, err := os.ReadFile(tempAgentLog.Name()) + if err != nil { + return false + } + // UUID regex pattern (matches UUID v4-like strings) + uuidRegex := regexp.MustCompile(`\bCODER_AGENT_TOKEN=(.+)\b`) - matches := uuidRegex.FindAll(contents, -1) - // When an agent reinitializes, we expect it to run startup scripts again. - // As such, we expect to have written the agent environment to the temp file twice. - // Once on initial startup and then once on reinitialization. - require.Len(t, matches, 2, "expected exactly 1 occurrence of the agent token per startup script execution") + matches = uuidRegex.FindAll(contents, -1) + // When an agent reinitializes, we expect it to run startup scripts again. + // As such, we expect to have written the agent environment to the temp file twice. + // Once on initial startup and then once on reinitialization. + return len(matches) == 2 + }, testutil.WaitLong, testutil.IntervalMedium) require.Equal(t, matches[0], matches[1]) - require.NoError(t, err) } type setupResp struct { From 394571dc3290a7fe601666675c8614dfe3505663 Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Wed, 14 May 2025 09:07:53 +0000 Subject: [PATCH 40/42] make -B gen --- coderd/apidoc/docs.go | 3 --- coderd/apidoc/swagger.json | 3 --- docs/reference/api/agents.md | 1 - docs/reference/api/schemas.md | 2 -- 4 files changed, 9 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index c184549f9067a..d674383584813 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -10520,9 +10520,6 @@ const docTemplate = `{ "reason": { "$ref": "#/definitions/agentsdk.ReinitializationReason" }, - "userID": { - "type": "string" - }, "workspaceID": { "type": "string" } diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 9fd054f211c54..25e3cad09bdd2 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -9327,9 +9327,6 @@ "reason": { "$ref": "#/definitions/agentsdk.ReinitializationReason" }, - "userID": { - "type": "string" - }, "workspaceID": { "type": "string" } diff --git a/docs/reference/api/agents.md b/docs/reference/api/agents.md index 29928ab0eaad8..eced88f4f72cc 100644 --- a/docs/reference/api/agents.md +++ b/docs/reference/api/agents.md @@ -490,7 +490,6 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/me/reinit \ ```json { "reason": "prebuild_claimed", - "userID": "string", "workspaceID": "string" } ``` diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 6b30ddf7e0c79..eeb0014bd521e 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -187,7 +187,6 @@ ```json { "reason": "prebuild_claimed", - "userID": "string", "workspaceID": "string" } ``` @@ -197,7 +196,6 @@ | Name | Type | Required | Restrictions | Description | |---------------|--------------------------------------------------------------------|----------|--------------|-------------| | `reason` | [agentsdk.ReinitializationReason](#agentsdkreinitializationreason) | false | | | -| `userID` | string | false | | | | `workspaceID` | string | false | | | ## agentsdk.ReinitializationReason From 890747b82e3bcfc166ff8851d95b9e99c05fde19 Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Wed, 14 May 2025 09:27:06 +0000 Subject: [PATCH 41/42] remove a potential race in reinitialization testing in TestCompleteJob --- .../provisionerdserver_test.go | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go index f402968b97d60..876cc5fc2d755 100644 --- a/coderd/provisionerdserver/provisionerdserver_test.go +++ b/coderd/provisionerdserver/provisionerdserver_test.go @@ -1903,14 +1903,16 @@ func TestCompleteJob(t *testing.T) { _, err = srv.CompleteJob(ctx, &completedJob) require.NoError(t, err) - select { - case reinitEvent := <-reinitChan: - // THEN workspace agent reinitialization instruction was received: - require.True(t, tc.shouldReinitializeAgent) - require.Equal(t, workspace.ID, reinitEvent.WorkspaceID) - default: - // THEN workspace agent reinitialization instruction was not received. - require.False(t, tc.shouldReinitializeAgent) + if tc.shouldReinitializeAgent { + event := testutil.RequireReceive(ctx, t, reinitChan) + require.Equal(t, workspace.ID, event.WorkspaceID) + } else { + select { + case <-reinitChan: + t.Fatal("unexpected reinitialization event published") + default: + // OK + } } }) } From b3870dbce8f75b33a7fe05f07bcf3991c1a43d76 Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Wed, 14 May 2025 11:48:47 +0000 Subject: [PATCH 42/42] fix a potential race in TestReinit --- coderd/workspaceagents_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go index 432e6b109ec59..10403f1ac00ae 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -2663,7 +2663,10 @@ func TestReinit(t *testing.T) { OrganizationID: user.OrganizationID, OwnerID: user.UserID, }).WithAgent().Do() + + pubsubSpy.Mutex.Lock() pubsubSpy.expectedEvent = agentsdk.PrebuildClaimedChannel(r.Workspace.ID) + pubsubSpy.Mutex.Unlock() agentCtx := testutil.Context(t, testutil.WaitShort) agentClient := agentsdk.New(client.URL)