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

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

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


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy