diff --git a/agent/agent.go b/agent/agent.go index 6b9adfcac83c0..453f810013aa4 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -121,6 +121,7 @@ func New(options Options) io.Closer { logDir: options.LogDir, tempDir: options.TempDir, lifecycleUpdate: make(chan struct{}, 1), + lifecycleReported: make(chan codersdk.WorkspaceAgentLifecycle, 1), connStatsChan: make(chan *agentsdk.Stats, 1), } a.init(ctx) @@ -149,9 +150,10 @@ type agent struct { sessionToken atomic.Pointer[string] sshServer *ssh.Server - lifecycleUpdate chan struct{} - lifecycleMu sync.Mutex // Protects following. - lifecycleState codersdk.WorkspaceAgentLifecycle + lifecycleUpdate chan struct{} + lifecycleReported chan codersdk.WorkspaceAgentLifecycle + lifecycleMu sync.RWMutex // Protects following. + lifecycleState codersdk.WorkspaceAgentLifecycle network *tailnet.Conn connStatsChan chan *agentsdk.Stats @@ -207,9 +209,9 @@ func (a *agent) reportLifecycleLoop(ctx context.Context) { } for r := retry.New(time.Second, 15*time.Second); r.Wait(ctx); { - a.lifecycleMu.Lock() + a.lifecycleMu.RLock() state := a.lifecycleState - a.lifecycleMu.Unlock() + a.lifecycleMu.RUnlock() if state == lastReported { break @@ -222,6 +224,11 @@ func (a *agent) reportLifecycleLoop(ctx context.Context) { }) if err == nil { lastReported = state + select { + case a.lifecycleReported <- state: + case <-a.lifecycleReported: + a.lifecycleReported <- state + } break } if xerrors.Is(err, context.Canceled) || xerrors.Is(err, context.DeadlineExceeded) { @@ -233,13 +240,20 @@ func (a *agent) reportLifecycleLoop(ctx context.Context) { } } +// setLifecycle sets the lifecycle state and notifies the lifecycle loop. +// The state is only updated if it's a valid state transition. func (a *agent) setLifecycle(ctx context.Context, state codersdk.WorkspaceAgentLifecycle) { a.lifecycleMu.Lock() - defer a.lifecycleMu.Unlock() - - a.logger.Debug(ctx, "set lifecycle state", slog.F("state", state), slog.F("previous", a.lifecycleState)) - + lastState := a.lifecycleState + if slices.Index(codersdk.WorkspaceAgentLifecycleOrder, lastState) > slices.Index(codersdk.WorkspaceAgentLifecycleOrder, state) { + a.logger.Warn(ctx, "attempted to set lifecycle state to a previous state", slog.F("last", lastState), slog.F("state", state)) + a.lifecycleMu.Unlock() + return + } a.lifecycleState = state + a.logger.Debug(ctx, "set lifecycle state", slog.F("state", state), slog.F("last", lastState)) + a.lifecycleMu.Unlock() + select { case a.lifecycleUpdate <- struct{}{}: default: @@ -299,9 +313,10 @@ func (a *agent) run(ctx context.Context) error { } } + lifecycleState := codersdk.WorkspaceAgentLifecycleReady scriptDone := make(chan error, 1) scriptStart := time.Now() - err := a.trackConnGoroutine(func() { + err = a.trackConnGoroutine(func() { defer close(scriptDone) scriptDone <- a.runStartupScript(ctx, metadata.StartupScript) }) @@ -329,16 +344,17 @@ func (a *agent) run(ctx context.Context) error { if errors.Is(err, context.Canceled) { return } - execTime := time.Since(scriptStart) - lifecycleStatus := codersdk.WorkspaceAgentLifecycleReady - if err != nil { - a.logger.Warn(ctx, "startup script failed", slog.F("execution_time", execTime), slog.Error(err)) - lifecycleStatus = codersdk.WorkspaceAgentLifecycleStartError - } else { - a.logger.Info(ctx, "startup script completed", slog.F("execution_time", execTime)) + // Only log if there was a startup script. + if metadata.StartupScript != "" { + execTime := time.Since(scriptStart) + if err != nil { + a.logger.Warn(ctx, "startup script failed", slog.F("execution_time", execTime), slog.Error(err)) + lifecycleState = codersdk.WorkspaceAgentLifecycleStartError + } else { + a.logger.Info(ctx, "startup script completed", slog.F("execution_time", execTime)) + } } - - a.setLifecycle(ctx, lifecycleStatus) + a.setLifecycle(ctx, lifecycleState) }() } @@ -606,14 +622,22 @@ func (a *agent) runCoordinator(ctx context.Context, network *tailnet.Conn) error } func (a *agent) runStartupScript(ctx context.Context, script string) error { + return a.runScript(ctx, "startup", script) +} + +func (a *agent) runShutdownScript(ctx context.Context, script string) error { + return a.runScript(ctx, "shutdown", script) +} + +func (a *agent) runScript(ctx context.Context, lifecycle, script string) error { if script == "" { return nil } - a.logger.Info(ctx, "running startup script", slog.F("script", script)) - writer, err := a.filesystem.OpenFile(filepath.Join(a.logDir, "coder-startup-script.log"), os.O_CREATE|os.O_RDWR, 0o600) + a.logger.Info(ctx, "running script", slog.F("lifecycle", lifecycle), slog.F("script", script)) + writer, err := a.filesystem.OpenFile(filepath.Join(a.logDir, fmt.Sprintf("coder-%s-script.log", lifecycle)), os.O_CREATE|os.O_RDWR, 0o600) if err != nil { - return xerrors.Errorf("open startup script log file: %w", err) + return xerrors.Errorf("open %s script log file: %w", lifecycle, err) } defer func() { _ = writer.Close() @@ -774,7 +798,7 @@ func (a *agent) createCommand(ctx context.Context, rawCommand string, env []stri rawMetadata := a.metadata.Load() if rawMetadata == nil { - return nil, xerrors.Errorf("no metadata was provided: %w", err) + return nil, xerrors.Errorf("no metadata was provided") } metadata, valid := rawMetadata.(agentsdk.Metadata) if !valid { @@ -1290,13 +1314,73 @@ func (a *agent) Close() error { if a.isClosed() { return nil } + + ctx := context.Background() + a.setLifecycle(ctx, codersdk.WorkspaceAgentLifecycleShuttingDown) + + lifecycleState := codersdk.WorkspaceAgentLifecycleOff + if metadata, ok := a.metadata.Load().(agentsdk.Metadata); ok && metadata.ShutdownScript != "" { + scriptDone := make(chan error, 1) + scriptStart := time.Now() + go func() { + defer close(scriptDone) + scriptDone <- a.runShutdownScript(ctx, metadata.ShutdownScript) + }() + + var timeout <-chan time.Time + // If timeout is zero, an older version of the coder + // provider was used. Otherwise a timeout is always > 0. + if metadata.ShutdownScriptTimeout > 0 { + t := time.NewTimer(metadata.ShutdownScriptTimeout) + defer t.Stop() + timeout = t.C + } + + var err error + select { + case err = <-scriptDone: + case <-timeout: + a.logger.Warn(ctx, "shutdown script timed out") + a.setLifecycle(ctx, codersdk.WorkspaceAgentLifecycleShutdownTimeout) + err = <-scriptDone // The script can still complete after a timeout. + } + execTime := time.Since(scriptStart) + if err != nil { + a.logger.Warn(ctx, "shutdown script failed", slog.F("execution_time", execTime), slog.Error(err)) + lifecycleState = codersdk.WorkspaceAgentLifecycleShutdownError + } else { + a.logger.Info(ctx, "shutdown script completed", slog.F("execution_time", execTime)) + } + } + + // Set final state and wait for it to be reported because context + // cancellation will stop the report loop. + a.setLifecycle(ctx, lifecycleState) + + // Wait for the lifecycle to be reported, but don't wait forever so + // that we don't break user expectations. + ctx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() +lifecycleWaitLoop: + for { + select { + case <-ctx.Done(): + break lifecycleWaitLoop + case s := <-a.lifecycleReported: + if s == lifecycleState { + break lifecycleWaitLoop + } + } + } + close(a.closed) a.closeCancel() + _ = a.sshServer.Close() if a.network != nil { _ = a.network.Close() } - _ = a.sshServer.Close() a.connCloseWait.Wait() + return nil } diff --git a/agent/agent_test.go b/agent/agent_test.go index 0a72bbeaa10a1..a35f872288dc3 100644 --- a/agent/agent_test.go +++ b/agent/agent_test.go @@ -59,7 +59,8 @@ func TestAgent_Stats_SSH(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - conn, _, stats, _ := setupAgent(t, agentsdk.Metadata{}, 0) + //nolint:dogsled + conn, _, stats, _, _ := setupAgent(t, agentsdk.Metadata{}, 0) sshClient, err := conn.SSHClient(ctx) require.NoError(t, err) @@ -85,7 +86,8 @@ func TestAgent_Stats_ReconnectingPTY(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - conn, _, stats, _ := setupAgent(t, agentsdk.Metadata{}, 0) + //nolint:dogsled + conn, _, stats, _, _ := setupAgent(t, agentsdk.Metadata{}, 0) ptyConn, err := conn.ReconnectingPTY(ctx, uuid.New(), 128, 128, "/bin/bash") require.NoError(t, err) @@ -114,7 +116,8 @@ func TestAgent_Stats_Magic(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - conn, _, stats, _ := setupAgent(t, agentsdk.Metadata{}, 0) + //nolint:dogsled + conn, _, stats, _, _ := setupAgent(t, agentsdk.Metadata{}, 0) sshClient, err := conn.SSHClient(ctx) require.NoError(t, err) defer sshClient.Close() @@ -572,7 +575,7 @@ func TestAgent_SFTP(t *testing.T) { home = "/" + strings.ReplaceAll(home, "\\", "/") } //nolint:dogsled - conn, _, _, _ := setupAgent(t, agentsdk.Metadata{}, 0) + conn, _, _, _, _ := setupAgent(t, agentsdk.Metadata{}, 0) sshClient, err := conn.SSHClient(ctx) require.NoError(t, err) defer sshClient.Close() @@ -604,7 +607,7 @@ func TestAgent_SCP(t *testing.T) { defer cancel() //nolint:dogsled - conn, _, _, _ := setupAgent(t, agentsdk.Metadata{}, 0) + conn, _, _, _, _ := setupAgent(t, agentsdk.Metadata{}, 0) sshClient, err := conn.SSHClient(ctx) require.NoError(t, err) defer sshClient.Close() @@ -709,7 +712,7 @@ func TestAgent_StartupScript(t *testing.T) { } content := "output" //nolint:dogsled - _, _, _, fs := setupAgent(t, agentsdk.Metadata{ + _, _, _, fs, _ := setupAgent(t, agentsdk.Metadata{ StartupScript: "echo " + content, }, 0) var gotContent string @@ -740,10 +743,10 @@ func TestAgent_StartupScript(t *testing.T) { func TestAgent_Lifecycle(t *testing.T) { t.Parallel() - t.Run("Timeout", func(t *testing.T) { + t.Run("StartTimeout", func(t *testing.T) { t.Parallel() - _, client, _, _ := setupAgent(t, agentsdk.Metadata{ + _, client, _, _, _ := setupAgent(t, agentsdk.Metadata{ StartupScript: "sleep 5", StartupScriptTimeout: time.Nanosecond, }, 0) @@ -769,10 +772,10 @@ func TestAgent_Lifecycle(t *testing.T) { } }) - t.Run("Error", func(t *testing.T) { + t.Run("StartError", func(t *testing.T) { t.Parallel() - _, client, _, _ := setupAgent(t, agentsdk.Metadata{ + _, client, _, _, _ := setupAgent(t, agentsdk.Metadata{ StartupScript: "false", StartupScriptTimeout: 30 * time.Second, }, 0) @@ -801,7 +804,7 @@ func TestAgent_Lifecycle(t *testing.T) { t.Run("Ready", func(t *testing.T) { t.Parallel() - _, client, _, _ := setupAgent(t, agentsdk.Metadata{ + _, client, _, _, _ := setupAgent(t, agentsdk.Metadata{ StartupScript: "true", StartupScriptTimeout: 30 * time.Second, }, 0) @@ -826,6 +829,191 @@ func TestAgent_Lifecycle(t *testing.T) { require.Equal(t, want, got) } }) + + t.Run("ShuttingDown", func(t *testing.T) { + t.Parallel() + + _, client, _, _, closer := setupAgent(t, agentsdk.Metadata{ + ShutdownScript: "sleep 5", + StartupScriptTimeout: 30 * time.Second, + }, 0) + + var ready []codersdk.WorkspaceAgentLifecycle + assert.Eventually(t, func() bool { + ready = client.getLifecycleStates() + return len(ready) > 0 && ready[len(ready)-1] == codersdk.WorkspaceAgentLifecycleReady + }, testutil.WaitShort, testutil.IntervalMedium) + + // Start close asynchronously so that we an inspect the state. + done := make(chan struct{}) + go func() { + defer close(done) + err := closer.Close() + assert.NoError(t, err) + }() + t.Cleanup(func() { + <-done + }) + + want := []codersdk.WorkspaceAgentLifecycle{ + codersdk.WorkspaceAgentLifecycleShuttingDown, + } + + var got []codersdk.WorkspaceAgentLifecycle + assert.Eventually(t, func() bool { + got = client.getLifecycleStates()[len(ready):] + return len(got) > 0 && got[len(got)-1] == want[len(want)-1] + }, testutil.WaitShort, testutil.IntervalMedium) + + require.Equal(t, want, got) + }) + + t.Run("ShutdownTimeout", func(t *testing.T) { + t.Parallel() + + _, client, _, _, closer := setupAgent(t, agentsdk.Metadata{ + ShutdownScript: "sleep 5", + ShutdownScriptTimeout: time.Nanosecond, + }, 0) + + var ready []codersdk.WorkspaceAgentLifecycle + assert.Eventually(t, func() bool { + ready = client.getLifecycleStates() + return len(ready) > 0 && ready[len(ready)-1] == codersdk.WorkspaceAgentLifecycleReady + }, testutil.WaitShort, testutil.IntervalMedium) + + // Start close asynchronously so that we an inspect the state. + done := make(chan struct{}) + go func() { + defer close(done) + err := closer.Close() + assert.NoError(t, err) + }() + t.Cleanup(func() { + <-done + }) + + want := []codersdk.WorkspaceAgentLifecycle{ + codersdk.WorkspaceAgentLifecycleShuttingDown, + codersdk.WorkspaceAgentLifecycleShutdownTimeout, + } + + var got []codersdk.WorkspaceAgentLifecycle + assert.Eventually(t, func() bool { + got = client.getLifecycleStates()[len(ready):] + return len(got) > 0 && got[len(got)-1] == want[len(want)-1] + }, testutil.WaitShort, testutil.IntervalMedium) + + switch len(got) { + case 1: + // This can happen if lifecycle state updates are + // too fast, only the latest one is reported. + require.Equal(t, want[1:], got) + default: + // This is the expected case. + require.Equal(t, want, got) + } + }) + + t.Run("ShutdownError", func(t *testing.T) { + t.Parallel() + + _, client, _, _, closer := setupAgent(t, agentsdk.Metadata{ + ShutdownScript: "false", + ShutdownScriptTimeout: 30 * time.Second, + }, 0) + + var ready []codersdk.WorkspaceAgentLifecycle + assert.Eventually(t, func() bool { + ready = client.getLifecycleStates() + return len(ready) > 0 && ready[len(ready)-1] == codersdk.WorkspaceAgentLifecycleReady + }, testutil.WaitShort, testutil.IntervalMedium) + + // Start close asynchronously so that we an inspect the state. + done := make(chan struct{}) + go func() { + defer close(done) + err := closer.Close() + assert.NoError(t, err) + }() + t.Cleanup(func() { + <-done + }) + + want := []codersdk.WorkspaceAgentLifecycle{ + codersdk.WorkspaceAgentLifecycleShuttingDown, + codersdk.WorkspaceAgentLifecycleShutdownError, + } + + var got []codersdk.WorkspaceAgentLifecycle + assert.Eventually(t, func() bool { + got = client.getLifecycleStates()[len(ready):] + return len(got) > 0 && got[len(got)-1] == want[len(want)-1] + }, testutil.WaitShort, testutil.IntervalMedium) + + switch len(got) { + case 1: + // This can happen if lifecycle state updates are + // too fast, only the latest one is reported. + require.Equal(t, want[1:], got) + default: + // This is the expected case. + require.Equal(t, want, got) + } + }) + + t.Run("ShutdownScriptOnce", func(t *testing.T) { + t.Parallel() + + expected := "this-is-shutdown" + client := &client{ + t: t, + agentID: uuid.New(), + metadata: agentsdk.Metadata{ + DERPMap: tailnettest.RunDERPAndSTUN(t), + StartupScript: "echo 1", + ShutdownScript: "echo " + expected, + }, + statsChan: make(chan *agentsdk.Stats), + coordinator: tailnet.NewCoordinator(), + } + + fs := afero.NewMemMapFs() + agent := agent.New(agent.Options{ + Client: client, + Logger: slogtest.Make(t, nil).Leveled(slog.LevelInfo), + Filesystem: fs, + }) + + // agent.Close() loads the shutdown script from the agent metadata. + // The metadata is populated just before execution of the startup script, so it's mandatory to wait + // until the startup starts. + require.Eventually(t, func() bool { + outputPath := filepath.Join(os.TempDir(), "coder-startup-script.log") + content, err := afero.ReadFile(fs, outputPath) + if err != nil { + t.Logf("read file %q: %s", outputPath, err) + return false + } + return len(content) > 0 // something is in the startup log file + }, testutil.WaitShort, testutil.IntervalMedium) + + err := agent.Close() + require.NoError(t, err, "agent should be closed successfully") + + outputPath := filepath.Join(os.TempDir(), "coder-shutdown-script.log") + logFirstRead, err := afero.ReadFile(fs, outputPath) + require.NoError(t, err, "log file should be present") + require.Equal(t, expected, string(bytes.TrimSpace(logFirstRead))) + + // Make sure that script can't be executed twice. + err = agent.Close() + require.NoError(t, err, "don't need to close the agent twice, no effect") + + logSecondRead, err := afero.ReadFile(fs, outputPath) + require.NoError(t, err, "log file should be present") + require.Equal(t, string(bytes.TrimSpace(logFirstRead)), string(bytes.TrimSpace(logSecondRead))) + }) } func TestAgent_Startup(t *testing.T) { @@ -834,7 +1022,7 @@ func TestAgent_Startup(t *testing.T) { t.Run("EmptyDirectory", func(t *testing.T) { t.Parallel() - _, client, _, _ := setupAgent(t, agentsdk.Metadata{ + _, client, _, _, _ := setupAgent(t, agentsdk.Metadata{ StartupScript: "true", StartupScriptTimeout: 30 * time.Second, Directory: "", @@ -848,7 +1036,7 @@ func TestAgent_Startup(t *testing.T) { t.Run("HomeDirectory", func(t *testing.T) { t.Parallel() - _, client, _, _ := setupAgent(t, agentsdk.Metadata{ + _, client, _, _, _ := setupAgent(t, agentsdk.Metadata{ StartupScript: "true", StartupScriptTimeout: 30 * time.Second, Directory: "~", @@ -864,7 +1052,7 @@ func TestAgent_Startup(t *testing.T) { t.Run("HomeEnvironmentVariable", func(t *testing.T) { t.Parallel() - _, client, _, _ := setupAgent(t, agentsdk.Metadata{ + _, client, _, _, _ := setupAgent(t, agentsdk.Metadata{ StartupScript: "true", StartupScriptTimeout: 30 * time.Second, Directory: "$HOME", @@ -891,7 +1079,7 @@ func TestAgent_ReconnectingPTY(t *testing.T) { defer cancel() //nolint:dogsled - conn, _, _, _ := setupAgent(t, agentsdk.Metadata{}, 0) + conn, _, _, _, _ := setupAgent(t, agentsdk.Metadata{}, 0) id := uuid.New() netConn, err := conn.ReconnectingPTY(ctx, id, 100, 100, "/bin/bash") require.NoError(t, err) @@ -993,7 +1181,7 @@ func TestAgent_Dial(t *testing.T) { }() //nolint:dogsled - conn, _, _, _ := setupAgent(t, agentsdk.Metadata{}, 0) + conn, _, _, _, _ := setupAgent(t, agentsdk.Metadata{}, 0) require.True(t, conn.AwaitReachable(context.Background())) conn1, err := conn.DialContext(context.Background(), l.Addr().Network(), l.Addr().String()) require.NoError(t, err) @@ -1015,7 +1203,7 @@ func TestAgent_Speedtest(t *testing.T) { defer cancel() derpMap := tailnettest.RunDERPAndSTUN(t) //nolint:dogsled - conn, _, _, _ := setupAgent(t, agentsdk.Metadata{ + conn, _, _, _, _ := setupAgent(t, agentsdk.Metadata{ DERPMap: derpMap, }, 0) defer conn.Close() @@ -1101,7 +1289,7 @@ func TestAgent_WriteVSCodeConfigs(t *testing.T) { func setupSSHCommand(t *testing.T, beforeArgs []string, afterArgs []string) *exec.Cmd { //nolint:dogsled - agentConn, _, _, _ := setupAgent(t, agentsdk.Metadata{}, 0) + agentConn, _, _, _, _ := setupAgent(t, agentsdk.Metadata{}, 0) listener, err := net.Listen("tcp", "127.0.0.1:0") require.NoError(t, err) waitGroup := sync.WaitGroup{} @@ -1148,7 +1336,7 @@ func setupSSHSession(t *testing.T, options agentsdk.Metadata) *ssh.Session { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() //nolint:dogsled - conn, _, _, _ := setupAgent(t, options, 0) + conn, _, _, _, _ := setupAgent(t, options, 0) sshClient, err := conn.SSHClient(ctx) require.NoError(t, err) t.Cleanup(func() { @@ -1173,6 +1361,7 @@ func setupAgent(t *testing.T, metadata agentsdk.Metadata, ptyTimeout time.Durati *client, <-chan *agentsdk.Stats, afero.Fs, + io.Closer, ) { if metadata.DERPMap == nil { metadata.DERPMap = tailnettest.RunDERPAndSTUN(t) @@ -1233,7 +1422,7 @@ func setupAgent(t *testing.T, metadata agentsdk.Metadata, ptyTimeout time.Durati if !agentConn.AwaitReachable(ctx) { t.Fatal("agent not reachable") } - return agentConn, c, statsCh, fs + return agentConn, c, statsCh, fs, closer } var dialTestPayload = []byte("dean-was-here123") diff --git a/cli/cliui/agent.go b/cli/cliui/agent.go index bfa717c03de18..fda7b84b724d2 100644 --- a/cli/cliui/agent.go +++ b/cli/cliui/agent.go @@ -17,7 +17,10 @@ import ( "github.com/coder/coder/codersdk" ) -var AgentStartError = xerrors.New("agent startup exited with non-zero exit status") +var ( + AgentStartError = xerrors.New("agent startup exited with non-zero exit status") + AgentShuttingDown = xerrors.New("agent is shutting down") +) type AgentOptions struct { WorkspaceName string @@ -146,6 +149,10 @@ func Agent(ctx context.Context, writer io.Writer, opts AgentOptions) error { case codersdk.WorkspaceAgentLifecycleStartError: showMessage() return AgentStartError + case codersdk.WorkspaceAgentLifecycleShuttingDown, codersdk.WorkspaceAgentLifecycleShutdownTimeout, + codersdk.WorkspaceAgentLifecycleShutdownError, codersdk.WorkspaceAgentLifecycleOff: + showMessage() + return AgentShuttingDown default: select { case <-warningShown: @@ -229,6 +236,22 @@ func waitingMessage(agent codersdk.WorkspaceAgent, opts AgentOptions) (m *messag m.Spin = "" m.Prompt = "The workspace ran into a problem while getting ready, the agent startup script exited with non-zero status." default: + switch agent.LifecycleState { + case codersdk.WorkspaceAgentLifecycleShutdownTimeout: + m.Spin = "" + m.Prompt = "The workspace is shutting down, but is taking longer than expected to shut down and the agent shutdown script is still executing." + m.Troubleshoot = true + case codersdk.WorkspaceAgentLifecycleShutdownError: + m.Spin = "" + m.Prompt = "The workspace ran into a problem while shutting down, the agent shutdown script exited with non-zero status." + m.Troubleshoot = true + case codersdk.WorkspaceAgentLifecycleShuttingDown: + m.Spin = "" + m.Prompt = "The workspace is shutting down." + case codersdk.WorkspaceAgentLifecycleOff: + m.Spin = "" + m.Prompt = "The workspace is not running." + } // Not a failure state, no troubleshooting necessary. return m } diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index fc55d2f32982d..0ca4aa61e78d3 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -5198,6 +5198,12 @@ const docTemplate = `{ "motd_file": { "type": "string" }, + "shutdown_script": { + "type": "string" + }, + "shutdown_script_timeout": { + "type": "integer" + }, "startup_script": { "type": "string" }, @@ -8425,6 +8431,12 @@ const docTemplate = `{ "type": "string", "format": "uuid" }, + "shutdown_script": { + "type": "string" + }, + "shutdown_script_timeout_seconds": { + "type": "integer" + }, "startup_script": { "type": "string" }, @@ -8462,14 +8474,22 @@ const docTemplate = `{ "starting", "start_timeout", "start_error", - "ready" + "ready", + "shutting_down", + "shutdown_timeout", + "shutdown_error", + "off" ], "x-enum-varnames": [ "WorkspaceAgentLifecycleCreated", "WorkspaceAgentLifecycleStarting", "WorkspaceAgentLifecycleStartTimeout", "WorkspaceAgentLifecycleStartError", - "WorkspaceAgentLifecycleReady" + "WorkspaceAgentLifecycleReady", + "WorkspaceAgentLifecycleShuttingDown", + "WorkspaceAgentLifecycleShutdownTimeout", + "WorkspaceAgentLifecycleShutdownError", + "WorkspaceAgentLifecycleOff" ] }, "codersdk.WorkspaceAgentListeningPort": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 874c41aafd080..7dd83af9690ff 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -4587,6 +4587,12 @@ "motd_file": { "type": "string" }, + "shutdown_script": { + "type": "string" + }, + "shutdown_script_timeout": { + "type": "integer" + }, "startup_script": { "type": "string" }, @@ -7592,6 +7598,12 @@ "type": "string", "format": "uuid" }, + "shutdown_script": { + "type": "string" + }, + "shutdown_script_timeout_seconds": { + "type": "integer" + }, "startup_script": { "type": "string" }, @@ -7624,13 +7636,27 @@ }, "codersdk.WorkspaceAgentLifecycle": { "type": "string", - "enum": ["created", "starting", "start_timeout", "start_error", "ready"], + "enum": [ + "created", + "starting", + "start_timeout", + "start_error", + "ready", + "shutting_down", + "shutdown_timeout", + "shutdown_error", + "off" + ], "x-enum-varnames": [ "WorkspaceAgentLifecycleCreated", "WorkspaceAgentLifecycleStarting", "WorkspaceAgentLifecycleStartTimeout", "WorkspaceAgentLifecycleStartError", - "WorkspaceAgentLifecycleReady" + "WorkspaceAgentLifecycleReady", + "WorkspaceAgentLifecycleShuttingDown", + "WorkspaceAgentLifecycleShutdownTimeout", + "WorkspaceAgentLifecycleShutdownError", + "WorkspaceAgentLifecycleOff" ] }, "codersdk.WorkspaceAgentListeningPort": { diff --git a/coderd/database/dbfake/databasefake.go b/coderd/database/dbfake/databasefake.go index 567705c1ec42c..e0ac02dbcfeb5 100644 --- a/coderd/database/dbfake/databasefake.go +++ b/coderd/database/dbfake/databasefake.go @@ -2856,6 +2856,7 @@ func (q *fakeQuerier) InsertWorkspaceAgent(_ context.Context, arg database.Inser TroubleshootingURL: arg.TroubleshootingURL, MOTDFile: arg.MOTDFile, LifecycleState: database.WorkspaceAgentLifecycleStateCreated, + ShutdownScript: arg.ShutdownScript, } q.workspaceAgents = append(q.workspaceAgents, agent) diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 94df373a0cca1..264108c0e844c 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -107,7 +107,11 @@ CREATE TYPE workspace_agent_lifecycle_state AS ENUM ( 'starting', 'start_timeout', 'start_error', - 'ready' + 'ready', + 'shutting_down', + 'shutdown_timeout', + 'shutdown_error', + 'off' ); CREATE TYPE workspace_app_health AS ENUM ( @@ -509,7 +513,9 @@ CREATE TABLE workspace_agents ( lifecycle_state workspace_agent_lifecycle_state DEFAULT 'created'::workspace_agent_lifecycle_state NOT NULL, login_before_ready boolean DEFAULT true NOT NULL, startup_script_timeout_seconds integer DEFAULT 0 NOT NULL, - expanded_directory character varying(4096) DEFAULT ''::character varying NOT NULL + expanded_directory character varying(4096) DEFAULT ''::character varying NOT NULL, + shutdown_script character varying(65534), + shutdown_script_timeout_seconds integer DEFAULT 0 NOT NULL ); COMMENT ON COLUMN workspace_agents.version IS 'Version tracks the version of the currently running workspace agent. Workspace agents register their version upon start.'; @@ -528,6 +534,10 @@ COMMENT ON COLUMN workspace_agents.startup_script_timeout_seconds IS 'The number COMMENT ON COLUMN workspace_agents.expanded_directory IS 'The resolved path of a user-specified directory. e.g. ~/coder -> /home/coder/coder'; +COMMENT ON COLUMN workspace_agents.shutdown_script IS 'Script that is executed before the agent is stopped.'; + +COMMENT ON COLUMN workspace_agents.shutdown_script_timeout_seconds IS 'The number of seconds to wait for the shutdown script to complete. If the script does not complete within this time, the agent lifecycle will be marked as shutdown_timeout.'; + CREATE TABLE workspace_apps ( id uuid NOT NULL, created_at timestamp with time zone NOT NULL, diff --git a/coderd/database/migrations/000104_add_shutdown_script_to_coder_agents.down.sql b/coderd/database/migrations/000104_add_shutdown_script_to_coder_agents.down.sql new file mode 100644 index 0000000000000..20ae02ae46386 --- /dev/null +++ b/coderd/database/migrations/000104_add_shutdown_script_to_coder_agents.down.sql @@ -0,0 +1,12 @@ +ALTER TABLE workspace_agents DROP COLUMN shutdown_script; + +ALTER TABLE workspace_agents DROP COLUMN shutdown_script_timeout_seconds; + +-- We can't drop values from enums, so we have to create a new one and convert the data. +UPDATE workspace_agents SET lifecycle_state = 'ready' WHERE lifecycle_state IN ('shutting_down', 'shutdown_timeout', 'shutdown_error', 'off'); +ALTER TYPE workspace_agent_lifecycle_state RENAME TO workspace_agent_lifecycle_state_old; +CREATE TYPE workspace_agent_lifecycle_state AS ENUM ('created', 'starting', 'start_timeout', 'start_error', 'ready'); +ALTER TABLE workspace_agents ALTER COLUMN lifecycle_state DROP DEFAULT; +ALTER TABLE workspace_agents ALTER COLUMN lifecycle_state TYPE workspace_agent_lifecycle_state USING lifecycle_state::text::workspace_agent_lifecycle_state; +ALTER TABLE workspace_agents ALTER COLUMN lifecycle_state SET DEFAULT 'created'; +DROP TYPE workspace_agent_lifecycle_state_old; diff --git a/coderd/database/migrations/000104_add_shutdown_script_to_coder_agents.up.sql b/coderd/database/migrations/000104_add_shutdown_script_to_coder_agents.up.sql new file mode 100644 index 0000000000000..5f279ed10cd55 --- /dev/null +++ b/coderd/database/migrations/000104_add_shutdown_script_to_coder_agents.up.sql @@ -0,0 +1,14 @@ +ALTER TABLE workspace_agents ADD COLUMN shutdown_script varchar(65534); + +COMMENT ON COLUMN workspace_agents.shutdown_script IS 'Script that is executed before the agent is stopped.'; + +-- Disable shutdown script timeouts by default. +ALTER TABLE workspace_agents ADD COLUMN shutdown_script_timeout_seconds int4 NOT NULL DEFAULT 0; + +COMMENT ON COLUMN workspace_agents.shutdown_script_timeout_seconds IS 'The number of seconds to wait for the shutdown script to complete. If the script does not complete within this time, the agent lifecycle will be marked as shutdown_timeout.'; + +-- Add enum fields +ALTER TYPE workspace_agent_lifecycle_state ADD VALUE 'shutting_down'; +ALTER TYPE workspace_agent_lifecycle_state ADD VALUE 'shutdown_timeout'; +ALTER TYPE workspace_agent_lifecycle_state ADD VALUE 'shutdown_error'; +ALTER TYPE workspace_agent_lifecycle_state ADD VALUE 'off'; diff --git a/coderd/database/migrations/testdata/fixtures/000091_lifecycle.up.sql b/coderd/database/migrations/testdata/fixtures/000091_lifecycle.up.sql new file mode 100644 index 0000000000000..462dadaf853ee --- /dev/null +++ b/coderd/database/migrations/testdata/fixtures/000091_lifecycle.up.sql @@ -0,0 +1,2 @@ +-- Set a non-default lifecycle_state. +UPDATE workspace_agents SET lifecycle_state = 'ready' WHERE id = '7a1ce5f8-8d00-431c-ad1b-97a846512804'; diff --git a/coderd/database/migrations/testdata/fixtures/000104_lifecycle.up.sql b/coderd/database/migrations/testdata/fixtures/000104_lifecycle.up.sql new file mode 100644 index 0000000000000..77ad31a423885 --- /dev/null +++ b/coderd/database/migrations/testdata/fixtures/000104_lifecycle.up.sql @@ -0,0 +1,2 @@ +-- Set lifecycle_state to enum value not available in previous migration. +UPDATE workspace_agents SET lifecycle_state = 'off' WHERE id = '7a1ce5f8-8d00-431c-ad1b-97a846512804'; diff --git a/coderd/database/models.go b/coderd/database/models.go index e67dc7127db65..8f9673d56438b 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -1014,11 +1014,15 @@ func AllUserStatusValues() []UserStatus { type WorkspaceAgentLifecycleState string const ( - WorkspaceAgentLifecycleStateCreated WorkspaceAgentLifecycleState = "created" - WorkspaceAgentLifecycleStateStarting WorkspaceAgentLifecycleState = "starting" - WorkspaceAgentLifecycleStateStartTimeout WorkspaceAgentLifecycleState = "start_timeout" - WorkspaceAgentLifecycleStateStartError WorkspaceAgentLifecycleState = "start_error" - WorkspaceAgentLifecycleStateReady WorkspaceAgentLifecycleState = "ready" + WorkspaceAgentLifecycleStateCreated WorkspaceAgentLifecycleState = "created" + WorkspaceAgentLifecycleStateStarting WorkspaceAgentLifecycleState = "starting" + WorkspaceAgentLifecycleStateStartTimeout WorkspaceAgentLifecycleState = "start_timeout" + WorkspaceAgentLifecycleStateStartError WorkspaceAgentLifecycleState = "start_error" + WorkspaceAgentLifecycleStateReady WorkspaceAgentLifecycleState = "ready" + WorkspaceAgentLifecycleStateShuttingDown WorkspaceAgentLifecycleState = "shutting_down" + WorkspaceAgentLifecycleStateShutdownTimeout WorkspaceAgentLifecycleState = "shutdown_timeout" + WorkspaceAgentLifecycleStateShutdownError WorkspaceAgentLifecycleState = "shutdown_error" + WorkspaceAgentLifecycleStateOff WorkspaceAgentLifecycleState = "off" ) func (e *WorkspaceAgentLifecycleState) Scan(src interface{}) error { @@ -1062,7 +1066,11 @@ func (e WorkspaceAgentLifecycleState) Valid() bool { WorkspaceAgentLifecycleStateStarting, WorkspaceAgentLifecycleStateStartTimeout, WorkspaceAgentLifecycleStateStartError, - WorkspaceAgentLifecycleStateReady: + WorkspaceAgentLifecycleStateReady, + WorkspaceAgentLifecycleStateShuttingDown, + WorkspaceAgentLifecycleStateShutdownTimeout, + WorkspaceAgentLifecycleStateShutdownError, + WorkspaceAgentLifecycleStateOff: return true } return false @@ -1075,6 +1083,10 @@ func AllWorkspaceAgentLifecycleStateValues() []WorkspaceAgentLifecycleState { WorkspaceAgentLifecycleStateStartTimeout, WorkspaceAgentLifecycleStateStartError, WorkspaceAgentLifecycleStateReady, + WorkspaceAgentLifecycleStateShuttingDown, + WorkspaceAgentLifecycleStateShutdownTimeout, + WorkspaceAgentLifecycleStateShutdownError, + WorkspaceAgentLifecycleStateOff, } } @@ -1547,6 +1559,10 @@ type WorkspaceAgent struct { StartupScriptTimeoutSeconds int32 `db:"startup_script_timeout_seconds" json:"startup_script_timeout_seconds"` // The resolved path of a user-specified directory. e.g. ~/coder -> /home/coder/coder ExpandedDirectory string `db:"expanded_directory" json:"expanded_directory"` + // Script that is executed before the agent is stopped. + ShutdownScript sql.NullString `db:"shutdown_script" json:"shutdown_script"` + // The number of seconds to wait for the shutdown script to complete. If the script does not complete within this time, the agent lifecycle will be marked as shutdown_timeout. + ShutdownScriptTimeoutSeconds int32 `db:"shutdown_script_timeout_seconds" json:"shutdown_script_timeout_seconds"` } type WorkspaceAgentStat struct { diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index ceb4308bc9b9d..fd7381512bc97 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -4937,7 +4937,7 @@ func (q *sqlQuerier) UpdateUserStatus(ctx context.Context, arg UpdateUserStatusP const getWorkspaceAgentByAuthToken = `-- name: GetWorkspaceAgentByAuthToken :one 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, startup_script, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, login_before_ready, startup_script_timeout_seconds, expanded_directory + 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, startup_script, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, login_before_ready, startup_script_timeout_seconds, expanded_directory, shutdown_script, shutdown_script_timeout_seconds FROM workspace_agents WHERE @@ -4976,13 +4976,15 @@ func (q *sqlQuerier) GetWorkspaceAgentByAuthToken(ctx context.Context, authToken &i.LoginBeforeReady, &i.StartupScriptTimeoutSeconds, &i.ExpandedDirectory, + &i.ShutdownScript, + &i.ShutdownScriptTimeoutSeconds, ) return i, err } const getWorkspaceAgentByID = `-- name: GetWorkspaceAgentByID :one 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, startup_script, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, login_before_ready, startup_script_timeout_seconds, expanded_directory + 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, startup_script, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, login_before_ready, startup_script_timeout_seconds, expanded_directory, shutdown_script, shutdown_script_timeout_seconds FROM workspace_agents WHERE @@ -5019,13 +5021,15 @@ func (q *sqlQuerier) GetWorkspaceAgentByID(ctx context.Context, id uuid.UUID) (W &i.LoginBeforeReady, &i.StartupScriptTimeoutSeconds, &i.ExpandedDirectory, + &i.ShutdownScript, + &i.ShutdownScriptTimeoutSeconds, ) return i, err } const getWorkspaceAgentByInstanceID = `-- name: GetWorkspaceAgentByInstanceID :one 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, startup_script, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, login_before_ready, startup_script_timeout_seconds, expanded_directory + 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, startup_script, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, login_before_ready, startup_script_timeout_seconds, expanded_directory, shutdown_script, shutdown_script_timeout_seconds FROM workspace_agents WHERE @@ -5064,13 +5068,15 @@ func (q *sqlQuerier) GetWorkspaceAgentByInstanceID(ctx context.Context, authInst &i.LoginBeforeReady, &i.StartupScriptTimeoutSeconds, &i.ExpandedDirectory, + &i.ShutdownScript, + &i.ShutdownScriptTimeoutSeconds, ) return i, err } 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, startup_script, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, login_before_ready, startup_script_timeout_seconds, expanded_directory + 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, startup_script, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, login_before_ready, startup_script_timeout_seconds, expanded_directory, shutdown_script, shutdown_script_timeout_seconds FROM workspace_agents WHERE @@ -5113,6 +5119,8 @@ func (q *sqlQuerier) GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids [] &i.LoginBeforeReady, &i.StartupScriptTimeoutSeconds, &i.ExpandedDirectory, + &i.ShutdownScript, + &i.ShutdownScriptTimeoutSeconds, ); err != nil { return nil, err } @@ -5128,7 +5136,7 @@ func (q *sqlQuerier) GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids [] } const getWorkspaceAgentsCreatedAfter = `-- name: GetWorkspaceAgentsCreatedAfter :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, startup_script, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, login_before_ready, startup_script_timeout_seconds, expanded_directory FROM workspace_agents WHERE created_at > $1 +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, startup_script, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, login_before_ready, startup_script_timeout_seconds, expanded_directory, shutdown_script, shutdown_script_timeout_seconds FROM workspace_agents WHERE created_at > $1 ` func (q *sqlQuerier) GetWorkspaceAgentsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceAgent, error) { @@ -5167,6 +5175,8 @@ func (q *sqlQuerier) GetWorkspaceAgentsCreatedAfter(ctx context.Context, created &i.LoginBeforeReady, &i.StartupScriptTimeoutSeconds, &i.ExpandedDirectory, + &i.ShutdownScript, + &i.ShutdownScriptTimeoutSeconds, ); err != nil { return nil, err } @@ -5202,32 +5212,36 @@ INSERT INTO troubleshooting_url, motd_file, login_before_ready, - startup_script_timeout_seconds + startup_script_timeout_seconds, + shutdown_script, + shutdown_script_timeout_seconds ) VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19) RETURNING 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, startup_script, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, login_before_ready, startup_script_timeout_seconds, expanded_directory + ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21) RETURNING 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, startup_script, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, login_before_ready, startup_script_timeout_seconds, expanded_directory, shutdown_script, shutdown_script_timeout_seconds ` type InsertWorkspaceAgentParams struct { - ID uuid.UUID `db:"id" json:"id"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - UpdatedAt time.Time `db:"updated_at" json:"updated_at"` - Name string `db:"name" json:"name"` - ResourceID uuid.UUID `db:"resource_id" json:"resource_id"` - AuthToken uuid.UUID `db:"auth_token" json:"auth_token"` - AuthInstanceID sql.NullString `db:"auth_instance_id" json:"auth_instance_id"` - Architecture string `db:"architecture" json:"architecture"` - EnvironmentVariables pqtype.NullRawMessage `db:"environment_variables" json:"environment_variables"` - OperatingSystem string `db:"operating_system" json:"operating_system"` - StartupScript sql.NullString `db:"startup_script" json:"startup_script"` - Directory string `db:"directory" json:"directory"` - InstanceMetadata pqtype.NullRawMessage `db:"instance_metadata" json:"instance_metadata"` - ResourceMetadata pqtype.NullRawMessage `db:"resource_metadata" json:"resource_metadata"` - ConnectionTimeoutSeconds int32 `db:"connection_timeout_seconds" json:"connection_timeout_seconds"` - TroubleshootingURL string `db:"troubleshooting_url" json:"troubleshooting_url"` - MOTDFile string `db:"motd_file" json:"motd_file"` - LoginBeforeReady bool `db:"login_before_ready" json:"login_before_ready"` - StartupScriptTimeoutSeconds int32 `db:"startup_script_timeout_seconds" json:"startup_script_timeout_seconds"` + ID uuid.UUID `db:"id" json:"id"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + Name string `db:"name" json:"name"` + ResourceID uuid.UUID `db:"resource_id" json:"resource_id"` + AuthToken uuid.UUID `db:"auth_token" json:"auth_token"` + AuthInstanceID sql.NullString `db:"auth_instance_id" json:"auth_instance_id"` + Architecture string `db:"architecture" json:"architecture"` + EnvironmentVariables pqtype.NullRawMessage `db:"environment_variables" json:"environment_variables"` + OperatingSystem string `db:"operating_system" json:"operating_system"` + StartupScript sql.NullString `db:"startup_script" json:"startup_script"` + Directory string `db:"directory" json:"directory"` + InstanceMetadata pqtype.NullRawMessage `db:"instance_metadata" json:"instance_metadata"` + ResourceMetadata pqtype.NullRawMessage `db:"resource_metadata" json:"resource_metadata"` + ConnectionTimeoutSeconds int32 `db:"connection_timeout_seconds" json:"connection_timeout_seconds"` + TroubleshootingURL string `db:"troubleshooting_url" json:"troubleshooting_url"` + MOTDFile string `db:"motd_file" json:"motd_file"` + LoginBeforeReady bool `db:"login_before_ready" json:"login_before_ready"` + StartupScriptTimeoutSeconds int32 `db:"startup_script_timeout_seconds" json:"startup_script_timeout_seconds"` + ShutdownScript sql.NullString `db:"shutdown_script" json:"shutdown_script"` + ShutdownScriptTimeoutSeconds int32 `db:"shutdown_script_timeout_seconds" json:"shutdown_script_timeout_seconds"` } func (q *sqlQuerier) InsertWorkspaceAgent(ctx context.Context, arg InsertWorkspaceAgentParams) (WorkspaceAgent, error) { @@ -5251,6 +5265,8 @@ func (q *sqlQuerier) InsertWorkspaceAgent(ctx context.Context, arg InsertWorkspa arg.MOTDFile, arg.LoginBeforeReady, arg.StartupScriptTimeoutSeconds, + arg.ShutdownScript, + arg.ShutdownScriptTimeoutSeconds, ) var i WorkspaceAgent err := row.Scan( @@ -5280,6 +5296,8 @@ func (q *sqlQuerier) InsertWorkspaceAgent(ctx context.Context, arg InsertWorkspa &i.LoginBeforeReady, &i.StartupScriptTimeoutSeconds, &i.ExpandedDirectory, + &i.ShutdownScript, + &i.ShutdownScriptTimeoutSeconds, ) return i, err } diff --git a/coderd/database/queries/workspaceagents.sql b/coderd/database/queries/workspaceagents.sql index 544e58e432c4c..2bc30faf21095 100644 --- a/coderd/database/queries/workspaceagents.sql +++ b/coderd/database/queries/workspaceagents.sql @@ -58,10 +58,12 @@ INSERT INTO troubleshooting_url, motd_file, login_before_ready, - startup_script_timeout_seconds + startup_script_timeout_seconds, + shutdown_script, + shutdown_script_timeout_seconds ) VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19) RETURNING *; + ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21) RETURNING *; -- name: UpdateWorkspaceAgentConnectionByID :exec UPDATE diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index 9ca1da8bf8372..ce2c8aa220cad 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -1144,6 +1144,11 @@ func InsertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid. MOTDFile: prAgent.GetMotdFile(), LoginBeforeReady: prAgent.GetLoginBeforeReady(), StartupScriptTimeoutSeconds: prAgent.GetStartupScriptTimeoutSeconds(), + ShutdownScript: sql.NullString{ + String: prAgent.ShutdownScript, + Valid: prAgent.ShutdownScript != "", + }, + ShutdownScriptTimeoutSeconds: prAgent.GetShutdownScriptTimeoutSeconds(), }) if err != nil { return xerrors.Errorf("insert agent: %w", err) diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go index bcc0d6b0ad93a..2eea229e7c8f7 100644 --- a/coderd/provisionerdserver/provisionerdserver_test.go +++ b/coderd/provisionerdserver/provisionerdserver_test.go @@ -957,6 +957,7 @@ func TestInsertWorkspaceResource(t *testing.T) { Apps: []*sdkproto.App{{ Slug: "a", }}, + ShutdownScript: "shutdown", }}, }) require.NoError(t, err) @@ -971,6 +972,7 @@ func TestInsertWorkspaceResource(t *testing.T) { require.Equal(t, "amd64", agent.Architecture) require.Equal(t, "linux", agent.OperatingSystem) require.Equal(t, "value", agent.StartupScript.String) + require.Equal(t, "shutdown", agent.ShutdownScript.String) want, err := json.Marshal(map[string]string{ "something": "test", }) diff --git a/coderd/telemetry/telemetry.go b/coderd/telemetry/telemetry.go index eeac2286aef5c..368a10fb9a9c4 100644 --- a/coderd/telemetry/telemetry.go +++ b/coderd/telemetry/telemetry.go @@ -550,6 +550,7 @@ func ConvertWorkspaceAgent(agent database.WorkspaceAgent) WorkspaceAgent { StartupScript: agent.StartupScript.Valid, Directory: agent.Directory != "", ConnectionTimeoutSeconds: agent.ConnectionTimeoutSeconds, + ShutdownScript: agent.ShutdownScript.Valid, } if agent.FirstConnectedAt.Valid { snapAgent.FirstConnectedAt = &agent.FirstConnectedAt.Time @@ -750,6 +751,7 @@ type WorkspaceAgent struct { LastConnectedAt *time.Time `json:"last_connected_at"` DisconnectedAt *time.Time `json:"disconnected_at"` ConnectionTimeoutSeconds int32 `json:"connection_timeout_seconds"` + ShutdownScript bool `json:"shutdown_script"` } type WorkspaceApp struct { diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index 20202849aa47f..0505c6057c1aa 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -144,15 +144,17 @@ func (api *API) workspaceAgentMetadata(rw http.ResponseWriter, r *http.Request) } httpapi.Write(ctx, rw, http.StatusOK, agentsdk.Metadata{ - Apps: convertApps(dbApps), - DERPMap: api.DERPMap, - GitAuthConfigs: len(api.GitAuthConfigs), - EnvironmentVariables: apiAgent.EnvironmentVariables, - StartupScript: apiAgent.StartupScript, - Directory: apiAgent.Directory, - VSCodePortProxyURI: vscodeProxyURI, - MOTDFile: workspaceAgent.MOTDFile, - StartupScriptTimeout: time.Duration(apiAgent.StartupScriptTimeoutSeconds) * time.Second, + Apps: convertApps(dbApps), + DERPMap: api.DERPMap, + GitAuthConfigs: len(api.GitAuthConfigs), + EnvironmentVariables: apiAgent.EnvironmentVariables, + StartupScript: apiAgent.StartupScript, + Directory: apiAgent.Directory, + VSCodePortProxyURI: vscodeProxyURI, + MOTDFile: workspaceAgent.MOTDFile, + StartupScriptTimeout: time.Duration(apiAgent.StartupScriptTimeoutSeconds) * time.Second, + ShutdownScript: apiAgent.ShutdownScript, + ShutdownScriptTimeout: time.Duration(apiAgent.ShutdownScriptTimeoutSeconds) * time.Second, }) } @@ -803,25 +805,27 @@ func convertWorkspaceAgent(derpMap *tailcfg.DERPMap, coordinator tailnet.Coordin troubleshootingURL = dbAgent.TroubleshootingURL } workspaceAgent := codersdk.WorkspaceAgent{ - ID: dbAgent.ID, - CreatedAt: dbAgent.CreatedAt, - UpdatedAt: dbAgent.UpdatedAt, - ResourceID: dbAgent.ResourceID, - InstanceID: dbAgent.AuthInstanceID.String, - Name: dbAgent.Name, - Architecture: dbAgent.Architecture, - OperatingSystem: dbAgent.OperatingSystem, - StartupScript: dbAgent.StartupScript.String, - Version: dbAgent.Version, - EnvironmentVariables: envs, - Directory: dbAgent.Directory, - ExpandedDirectory: dbAgent.ExpandedDirectory, - Apps: apps, - ConnectionTimeoutSeconds: dbAgent.ConnectionTimeoutSeconds, - TroubleshootingURL: troubleshootingURL, - LifecycleState: codersdk.WorkspaceAgentLifecycle(dbAgent.LifecycleState), - LoginBeforeReady: dbAgent.LoginBeforeReady, - StartupScriptTimeoutSeconds: dbAgent.StartupScriptTimeoutSeconds, + ID: dbAgent.ID, + CreatedAt: dbAgent.CreatedAt, + UpdatedAt: dbAgent.UpdatedAt, + ResourceID: dbAgent.ResourceID, + InstanceID: dbAgent.AuthInstanceID.String, + Name: dbAgent.Name, + Architecture: dbAgent.Architecture, + OperatingSystem: dbAgent.OperatingSystem, + StartupScript: dbAgent.StartupScript.String, + Version: dbAgent.Version, + EnvironmentVariables: envs, + Directory: dbAgent.Directory, + ExpandedDirectory: dbAgent.ExpandedDirectory, + Apps: apps, + ConnectionTimeoutSeconds: dbAgent.ConnectionTimeoutSeconds, + TroubleshootingURL: troubleshootingURL, + LifecycleState: codersdk.WorkspaceAgentLifecycle(dbAgent.LifecycleState), + LoginBeforeReady: dbAgent.LoginBeforeReady, + StartupScriptTimeoutSeconds: dbAgent.StartupScriptTimeoutSeconds, + ShutdownScript: dbAgent.ShutdownScript.String, + ShutdownScriptTimeoutSeconds: dbAgent.ShutdownScriptTimeoutSeconds, } node := coordinator.Node(dbAgent.ID) if node != nil { diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go index 46dd945296b65..e4937143b4291 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -1256,6 +1256,10 @@ func TestWorkspaceAgent_LifecycleState(t *testing.T) { {codersdk.WorkspaceAgentLifecycleStartTimeout, false}, {codersdk.WorkspaceAgentLifecycleStartError, false}, {codersdk.WorkspaceAgentLifecycleReady, false}, + {codersdk.WorkspaceAgentLifecycleShuttingDown, false}, + {codersdk.WorkspaceAgentLifecycleShutdownTimeout, false}, + {codersdk.WorkspaceAgentLifecycleShutdownError, false}, + {codersdk.WorkspaceAgentLifecycleOff, false}, {codersdk.WorkspaceAgentLifecycle("nonexistent_state"), true}, {codersdk.WorkspaceAgentLifecycle(""), true}, } diff --git a/codersdk/agentsdk/agentsdk.go b/codersdk/agentsdk/agentsdk.go index 8d43e0013a390..eedeaf4a0226a 100644 --- a/codersdk/agentsdk/agentsdk.go +++ b/codersdk/agentsdk/agentsdk.go @@ -69,15 +69,17 @@ type Metadata struct { // GitAuthConfigs stores the number of Git configurations // the Coder deployment has. If this number is >0, we // set up special configuration in the workspace. - GitAuthConfigs int `json:"git_auth_configs"` - VSCodePortProxyURI string `json:"vscode_port_proxy_uri"` - Apps []codersdk.WorkspaceApp `json:"apps"` - DERPMap *tailcfg.DERPMap `json:"derpmap"` - EnvironmentVariables map[string]string `json:"environment_variables"` - StartupScript string `json:"startup_script"` - StartupScriptTimeout time.Duration `json:"startup_script_timeout"` - Directory string `json:"directory"` - MOTDFile string `json:"motd_file"` + GitAuthConfigs int `json:"git_auth_configs"` + VSCodePortProxyURI string `json:"vscode_port_proxy_uri"` + Apps []codersdk.WorkspaceApp `json:"apps"` + DERPMap *tailcfg.DERPMap `json:"derpmap"` + EnvironmentVariables map[string]string `json:"environment_variables"` + StartupScript string `json:"startup_script"` + StartupScriptTimeout time.Duration `json:"startup_script_timeout"` + Directory string `json:"directory"` + MOTDFile string `json:"motd_file"` + ShutdownScript string `json:"shutdown_script"` + ShutdownScriptTimeout time.Duration `json:"shutdown_script_timeout"` } // Metadata fetches metadata for the currently authenticated workspace agent. diff --git a/codersdk/workspaceagents.go b/codersdk/workspaceagents.go index 6dd1c1d178f17..f59b7ce9231ec 100644 --- a/codersdk/workspaceagents.go +++ b/codersdk/workspaceagents.go @@ -44,13 +44,35 @@ type WorkspaceAgentLifecycle string // WorkspaceAgentLifecycle enums. const ( - WorkspaceAgentLifecycleCreated WorkspaceAgentLifecycle = "created" - WorkspaceAgentLifecycleStarting WorkspaceAgentLifecycle = "starting" - WorkspaceAgentLifecycleStartTimeout WorkspaceAgentLifecycle = "start_timeout" - WorkspaceAgentLifecycleStartError WorkspaceAgentLifecycle = "start_error" - WorkspaceAgentLifecycleReady WorkspaceAgentLifecycle = "ready" + WorkspaceAgentLifecycleCreated WorkspaceAgentLifecycle = "created" + WorkspaceAgentLifecycleStarting WorkspaceAgentLifecycle = "starting" + WorkspaceAgentLifecycleStartTimeout WorkspaceAgentLifecycle = "start_timeout" + WorkspaceAgentLifecycleStartError WorkspaceAgentLifecycle = "start_error" + WorkspaceAgentLifecycleReady WorkspaceAgentLifecycle = "ready" + WorkspaceAgentLifecycleShuttingDown WorkspaceAgentLifecycle = "shutting_down" + WorkspaceAgentLifecycleShutdownTimeout WorkspaceAgentLifecycle = "shutdown_timeout" + WorkspaceAgentLifecycleShutdownError WorkspaceAgentLifecycle = "shutdown_error" + WorkspaceAgentLifecycleOff WorkspaceAgentLifecycle = "off" ) +// WorkspaceAgentLifecycleOrder is the order in which workspace agent +// lifecycle states are expected to be reported during the lifetime of +// the agent process. For instance, the agent can go from starting to +// ready without reporting timeout or error, but it should not go from +// ready to starting. This is merely a hint for the agent process, and +// is not enforced by the server. +var WorkspaceAgentLifecycleOrder = []WorkspaceAgentLifecycle{ + WorkspaceAgentLifecycleCreated, + WorkspaceAgentLifecycleStarting, + WorkspaceAgentLifecycleStartTimeout, + WorkspaceAgentLifecycleStartError, + WorkspaceAgentLifecycleReady, + WorkspaceAgentLifecycleShuttingDown, + WorkspaceAgentLifecycleShutdownTimeout, + WorkspaceAgentLifecycleShutdownError, + WorkspaceAgentLifecycleOff, +} + type WorkspaceAgent struct { ID uuid.UUID `json:"id" format:"uuid"` CreatedAt time.Time `json:"created_at" format:"date-time"` @@ -76,9 +98,11 @@ type WorkspaceAgent struct { ConnectionTimeoutSeconds int32 `json:"connection_timeout_seconds"` TroubleshootingURL string `json:"troubleshooting_url"` // LoginBeforeReady if true, the agent will delay logins until it is ready (e.g. executing startup script has ended). - LoginBeforeReady bool `db:"login_before_ready" json:"login_before_ready"` + LoginBeforeReady bool `json:"login_before_ready"` // StartupScriptTimeoutSeconds is the number of seconds to wait for the startup script to complete. If the script does not complete within this time, the agent lifecycle will be marked as start_timeout. - StartupScriptTimeoutSeconds int32 `db:"startup_script_timeout_seconds" json:"startup_script_timeout_seconds"` + StartupScriptTimeoutSeconds int32 `json:"startup_script_timeout_seconds"` + ShutdownScript string `json:"shutdown_script,omitempty"` + ShutdownScriptTimeoutSeconds int32 `json:"shutdown_script_timeout_seconds"` } type DERPRegion struct { diff --git a/docs/api/agents.md b/docs/api/agents.md index 1da3536e9c08c..6031e8698815b 100644 --- a/docs/api/agents.md +++ b/docs/api/agents.md @@ -369,6 +369,8 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/me/metadata \ }, "git_auth_configs": 0, "motd_file": "string", + "shutdown_script": "string", + "shutdown_script_timeout": 0, "startup_script": "string", "startup_script_timeout": 0, "vscode_port_proxy_uri": "string" @@ -515,6 +517,8 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/{workspaceagent} \ "name": "string", "operating_system": "string", "resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f", + "shutdown_script": "string", + "shutdown_script_timeout_seconds": 0, "startup_script": "string", "startup_script_timeout_seconds": 0, "status": "connecting", diff --git a/docs/api/builds.md b/docs/api/builds.md index 343bac7312b96..2f6ba88602508 100644 --- a/docs/api/builds.md +++ b/docs/api/builds.md @@ -102,6 +102,8 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam "name": "string", "operating_system": "string", "resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f", + "shutdown_script": "string", + "shutdown_script_timeout_seconds": 0, "startup_script": "string", "startup_script_timeout_seconds": 0, "status": "connecting", @@ -248,6 +250,8 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild} \ "name": "string", "operating_system": "string", "resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f", + "shutdown_script": "string", + "shutdown_script_timeout_seconds": 0, "startup_script": "string", "startup_script_timeout_seconds": 0, "status": "connecting", @@ -537,6 +541,8 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild}/res "name": "string", "operating_system": "string", "resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f", + "shutdown_script": "string", + "shutdown_script_timeout_seconds": 0, "startup_script": "string", "startup_script_timeout_seconds": 0, "status": "connecting", @@ -575,89 +581,95 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild}/res Status Code **200** -| Name | Type | Required | Restrictions | Description | -| ----------------------------------- | -------------------------------------------------------------------------------- | -------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `[array item]` | array | false | | | -| `» agents` | array | false | | | -| `»» apps` | array | false | | | -| `»»» command` | string | false | | | -| `»»» display_name` | string | false | | »»display name is a friendly name for the app. | -| `»»» external` | boolean | false | | External specifies whether the URL should be opened externally on the client or not. | -| `»»» health` | [codersdk.WorkspaceAppHealth](schemas.md#codersdkworkspaceapphealth) | false | | | -| `»»» healthcheck` | [codersdk.Healthcheck](schemas.md#codersdkhealthcheck) | false | | Healthcheck specifies the configuration for checking app health. | -| `»»»» interval` | integer | false | | Interval specifies the seconds between each health check. | -| `»»»» threshold` | integer | false | | Threshold specifies the number of consecutive failed health checks before returning "unhealthy". | -| `»»»» url` | string | false | | »»»url specifies the endpoint to check for the app health. | -| `»»» icon` | string | false | | Icon is a relative path or external URL that specifies an icon to be displayed in the dashboard. | -| `»»» id` | string(uuid) | false | | | -| `»»» sharing_level` | [codersdk.WorkspaceAppSharingLevel](schemas.md#codersdkworkspaceappsharinglevel) | false | | | -| `»»» slug` | string | false | | Slug is a unique identifier within the agent. | -| `»»» subdomain` | boolean | false | | Subdomain denotes whether the app should be accessed via a path on the `coder server` or via a hostname-based dev URL. If this is set to true and there is no app wildcard configured on the server, the app will not be accessible in the UI. | -| `»»» url` | string | false | | »»url is the address being proxied to inside the workspace. If external is specified, this will be opened on the client. | -| `»» architecture` | string | false | | | -| `»» connection_timeout_seconds` | integer | false | | | -| `»» created_at` | string(date-time) | false | | | -| `»» directory` | string | false | | | -| `»» disconnected_at` | string(date-time) | false | | | -| `»» environment_variables` | object | false | | | -| `»»» [any property]` | string | false | | | -| `»» expanded_directory` | string | false | | | -| `»» first_connected_at` | string(date-time) | false | | | -| `»» id` | string(uuid) | false | | | -| `»» instance_id` | string | false | | | -| `»» last_connected_at` | string(date-time) | false | | | -| `»» latency` | object | false | | »latency is mapped by region name (e.g. "New York City", "Seattle"). | -| `»»» [any property]` | [codersdk.DERPRegion](schemas.md#codersdkderpregion) | false | | | -| `»»»» latency_ms` | number | false | | | -| `»»»» preferred` | boolean | false | | | -| `»» lifecycle_state` | [codersdk.WorkspaceAgentLifecycle](schemas.md#codersdkworkspaceagentlifecycle) | false | | | -| `»» login_before_ready` | boolean | false | | »login before ready if true, the agent will delay logins until it is ready (e.g. executing startup script has ended). | -| `»» name` | string | false | | | -| `»» operating_system` | string | false | | | -| `»» resource_id` | string(uuid) | false | | | -| `»» startup_script` | string | false | | | -| `»» startup_script_timeout_seconds` | integer | false | | »startup script timeout seconds is the number of seconds to wait for the startup script to complete. If the script does not complete within this time, the agent lifecycle will be marked as start_timeout. | -| `»» status` | [codersdk.WorkspaceAgentStatus](schemas.md#codersdkworkspaceagentstatus) | false | | | -| `»» troubleshooting_url` | string | false | | | -| `»» updated_at` | string(date-time) | false | | | -| `»» version` | string | false | | | -| `» created_at` | string(date-time) | false | | | -| `» daily_cost` | integer | false | | | -| `» hide` | boolean | false | | | -| `» icon` | string | false | | | -| `» id` | string(uuid) | false | | | -| `» job_id` | string(uuid) | false | | | -| `» metadata` | array | false | | | -| `»» key` | string | false | | | -| `»» sensitive` | boolean | false | | | -| `»» value` | string | false | | | -| `» name` | string | false | | | -| `» type` | string | false | | | -| `» workspace_transition` | [codersdk.WorkspaceTransition](schemas.md#codersdkworkspacetransition) | false | | | +| Name | Type | Required | Restrictions | Description | +| ------------------------------------ | -------------------------------------------------------------------------------- | -------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `[array item]` | array | false | | | +| `» agents` | array | false | | | +| `»» apps` | array | false | | | +| `»»» command` | string | false | | | +| `»»» display_name` | string | false | | »»display name is a friendly name for the app. | +| `»»» external` | boolean | false | | External specifies whether the URL should be opened externally on the client or not. | +| `»»» health` | [codersdk.WorkspaceAppHealth](schemas.md#codersdkworkspaceapphealth) | false | | | +| `»»» healthcheck` | [codersdk.Healthcheck](schemas.md#codersdkhealthcheck) | false | | Healthcheck specifies the configuration for checking app health. | +| `»»»» interval` | integer | false | | Interval specifies the seconds between each health check. | +| `»»»» threshold` | integer | false | | Threshold specifies the number of consecutive failed health checks before returning "unhealthy". | +| `»»»» url` | string | false | | »»»url specifies the endpoint to check for the app health. | +| `»»» icon` | string | false | | Icon is a relative path or external URL that specifies an icon to be displayed in the dashboard. | +| `»»» id` | string(uuid) | false | | | +| `»»» sharing_level` | [codersdk.WorkspaceAppSharingLevel](schemas.md#codersdkworkspaceappsharinglevel) | false | | | +| `»»» slug` | string | false | | Slug is a unique identifier within the agent. | +| `»»» subdomain` | boolean | false | | Subdomain denotes whether the app should be accessed via a path on the `coder server` or via a hostname-based dev URL. If this is set to true and there is no app wildcard configured on the server, the app will not be accessible in the UI. | +| `»»» url` | string | false | | »»url is the address being proxied to inside the workspace. If external is specified, this will be opened on the client. | +| `»» architecture` | string | false | | | +| `»» connection_timeout_seconds` | integer | false | | | +| `»» created_at` | string(date-time) | false | | | +| `»» directory` | string | false | | | +| `»» disconnected_at` | string(date-time) | false | | | +| `»» environment_variables` | object | false | | | +| `»»» [any property]` | string | false | | | +| `»» expanded_directory` | string | false | | | +| `»» first_connected_at` | string(date-time) | false | | | +| `»» id` | string(uuid) | false | | | +| `»» instance_id` | string | false | | | +| `»» last_connected_at` | string(date-time) | false | | | +| `»» latency` | object | false | | »latency is mapped by region name (e.g. "New York City", "Seattle"). | +| `»»» [any property]` | [codersdk.DERPRegion](schemas.md#codersdkderpregion) | false | | | +| `»»»» latency_ms` | number | false | | | +| `»»»» preferred` | boolean | false | | | +| `»» lifecycle_state` | [codersdk.WorkspaceAgentLifecycle](schemas.md#codersdkworkspaceagentlifecycle) | false | | | +| `»» login_before_ready` | boolean | false | | »login before ready if true, the agent will delay logins until it is ready (e.g. executing startup script has ended). | +| `»» name` | string | false | | | +| `»» operating_system` | string | false | | | +| `»» resource_id` | string(uuid) | false | | | +| `»» shutdown_script` | string | false | | | +| `»» shutdown_script_timeout_seconds` | integer | false | | | +| `»» startup_script` | string | false | | | +| `»» startup_script_timeout_seconds` | integer | false | | »startup script timeout seconds is the number of seconds to wait for the startup script to complete. If the script does not complete within this time, the agent lifecycle will be marked as start_timeout. | +| `»» status` | [codersdk.WorkspaceAgentStatus](schemas.md#codersdkworkspaceagentstatus) | false | | | +| `»» troubleshooting_url` | string | false | | | +| `»» updated_at` | string(date-time) | false | | | +| `»» version` | string | false | | | +| `» created_at` | string(date-time) | false | | | +| `» daily_cost` | integer | false | | | +| `» hide` | boolean | false | | | +| `» icon` | string | false | | | +| `» id` | string(uuid) | false | | | +| `» job_id` | string(uuid) | false | | | +| `» metadata` | array | false | | | +| `»» key` | string | false | | | +| `»» sensitive` | boolean | false | | | +| `»» value` | string | false | | | +| `» name` | string | false | | | +| `» type` | string | false | | | +| `» workspace_transition` | [codersdk.WorkspaceTransition](schemas.md#codersdkworkspacetransition) | false | | | #### Enumerated Values -| Property | Value | -| ---------------------- | --------------- | -| `health` | `disabled` | -| `health` | `initializing` | -| `health` | `healthy` | -| `health` | `unhealthy` | -| `sharing_level` | `owner` | -| `sharing_level` | `authenticated` | -| `sharing_level` | `public` | -| `lifecycle_state` | `created` | -| `lifecycle_state` | `starting` | -| `lifecycle_state` | `start_timeout` | -| `lifecycle_state` | `start_error` | -| `lifecycle_state` | `ready` | -| `status` | `connecting` | -| `status` | `connected` | -| `status` | `disconnected` | -| `status` | `timeout` | -| `workspace_transition` | `start` | -| `workspace_transition` | `stop` | -| `workspace_transition` | `delete` | +| Property | Value | +| ---------------------- | ------------------ | +| `health` | `disabled` | +| `health` | `initializing` | +| `health` | `healthy` | +| `health` | `unhealthy` | +| `sharing_level` | `owner` | +| `sharing_level` | `authenticated` | +| `sharing_level` | `public` | +| `lifecycle_state` | `created` | +| `lifecycle_state` | `starting` | +| `lifecycle_state` | `start_timeout` | +| `lifecycle_state` | `start_error` | +| `lifecycle_state` | `ready` | +| `lifecycle_state` | `shutting_down` | +| `lifecycle_state` | `shutdown_timeout` | +| `lifecycle_state` | `shutdown_error` | +| `lifecycle_state` | `off` | +| `status` | `connecting` | +| `status` | `connected` | +| `status` | `disconnected` | +| `status` | `timeout` | +| `workspace_transition` | `start` | +| `workspace_transition` | `stop` | +| `workspace_transition` | `delete` | To perform this operation, you must be authenticated. [Learn more](authentication.md). @@ -761,6 +773,8 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild}/sta "name": "string", "operating_system": "string", "resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f", + "shutdown_script": "string", + "shutdown_script_timeout_seconds": 0, "startup_script": "string", "startup_script_timeout_seconds": 0, "status": "connecting", @@ -912,6 +926,8 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace}/builds \ "name": "string", "operating_system": "string", "resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f", + "shutdown_script": "string", + "shutdown_script_timeout_seconds": 0, "startup_script": "string", "startup_script_timeout_seconds": 0, "status": "connecting", @@ -961,141 +977,147 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace}/builds \ Status Code **200** -| Name | Type | Required | Restrictions | Description | -| ------------------------------------ | -------------------------------------------------------------------------------- | -------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `[array item]` | array | false | | | -| `» build_number` | integer | false | | | -| `» created_at` | string(date-time) | false | | | -| `» daily_cost` | integer | false | | | -| `» deadline` | string(date-time) | false | | | -| `» id` | string(uuid) | false | | | -| `» initiator_id` | string(uuid) | false | | | -| `» initiator_name` | string | false | | | -| `» job` | [codersdk.ProvisionerJob](schemas.md#codersdkprovisionerjob) | false | | | -| `»» canceled_at` | string(date-time) | false | | | -| `»» completed_at` | string(date-time) | false | | | -| `»» created_at` | string(date-time) | false | | | -| `»» error` | string | false | | | -| `»» file_id` | string(uuid) | false | | | -| `»» id` | string(uuid) | false | | | -| `»» started_at` | string(date-time) | false | | | -| `»» status` | [codersdk.ProvisionerJobStatus](schemas.md#codersdkprovisionerjobstatus) | false | | | -| `»» tags` | object | false | | | -| `»»» [any property]` | string | false | | | -| `»» worker_id` | string(uuid) | false | | | -| `» reason` | [codersdk.BuildReason](schemas.md#codersdkbuildreason) | false | | | -| `» resources` | array | false | | | -| `»» agents` | array | false | | | -| `»»» apps` | array | false | | | -| `»»»» command` | string | false | | | -| `»»»» display_name` | string | false | | »»»display name is a friendly name for the app. | -| `»»»» external` | boolean | false | | External specifies whether the URL should be opened externally on the client or not. | -| `»»»» health` | [codersdk.WorkspaceAppHealth](schemas.md#codersdkworkspaceapphealth) | false | | | -| `»»»» healthcheck` | [codersdk.Healthcheck](schemas.md#codersdkhealthcheck) | false | | Healthcheck specifies the configuration for checking app health. | -| `»»»»» interval` | integer | false | | Interval specifies the seconds between each health check. | -| `»»»»» threshold` | integer | false | | Threshold specifies the number of consecutive failed health checks before returning "unhealthy". | -| `»»»»» url` | string | false | | »»»»url specifies the endpoint to check for the app health. | -| `»»»» icon` | string | false | | Icon is a relative path or external URL that specifies an icon to be displayed in the dashboard. | -| `»»»» id` | string(uuid) | false | | | -| `»»»» sharing_level` | [codersdk.WorkspaceAppSharingLevel](schemas.md#codersdkworkspaceappsharinglevel) | false | | | -| `»»»» slug` | string | false | | Slug is a unique identifier within the agent. | -| `»»»» subdomain` | boolean | false | | Subdomain denotes whether the app should be accessed via a path on the `coder server` or via a hostname-based dev URL. If this is set to true and there is no app wildcard configured on the server, the app will not be accessible in the UI. | -| `»»»» url` | string | false | | »»»url is the address being proxied to inside the workspace. If external is specified, this will be opened on the client. | -| `»»» architecture` | string | false | | | -| `»»» connection_timeout_seconds` | integer | false | | | -| `»»» created_at` | string(date-time) | false | | | -| `»»» directory` | string | false | | | -| `»»» disconnected_at` | string(date-time) | false | | | -| `»»» environment_variables` | object | false | | | -| `»»»» [any property]` | string | false | | | -| `»»» expanded_directory` | string | false | | | -| `»»» first_connected_at` | string(date-time) | false | | | -| `»»» id` | string(uuid) | false | | | -| `»»» instance_id` | string | false | | | -| `»»» last_connected_at` | string(date-time) | false | | | -| `»»» latency` | object | false | | »»latency is mapped by region name (e.g. "New York City", "Seattle"). | -| `»»»» [any property]` | [codersdk.DERPRegion](schemas.md#codersdkderpregion) | false | | | -| `»»»»» latency_ms` | number | false | | | -| `»»»»» preferred` | boolean | false | | | -| `»»» lifecycle_state` | [codersdk.WorkspaceAgentLifecycle](schemas.md#codersdkworkspaceagentlifecycle) | false | | | -| `»»» login_before_ready` | boolean | false | | »»login before ready if true, the agent will delay logins until it is ready (e.g. executing startup script has ended). | -| `»»» name` | string | false | | | -| `»»» operating_system` | string | false | | | -| `»»» resource_id` | string(uuid) | false | | | -| `»»» startup_script` | string | false | | | -| `»»» startup_script_timeout_seconds` | integer | false | | »»startup script timeout seconds is the number of seconds to wait for the startup script to complete. If the script does not complete within this time, the agent lifecycle will be marked as start_timeout. | -| `»»» status` | [codersdk.WorkspaceAgentStatus](schemas.md#codersdkworkspaceagentstatus) | false | | | -| `»»» troubleshooting_url` | string | false | | | -| `»»» updated_at` | string(date-time) | false | | | -| `»»» version` | string | false | | | -| `»» created_at` | string(date-time) | false | | | -| `»» daily_cost` | integer | false | | | -| `»» hide` | boolean | false | | | -| `»» icon` | string | false | | | -| `»» id` | string(uuid) | false | | | -| `»» job_id` | string(uuid) | false | | | -| `»» metadata` | array | false | | | -| `»»» key` | string | false | | | -| `»»» sensitive` | boolean | false | | | -| `»»» value` | string | false | | | -| `»» name` | string | false | | | -| `»» type` | string | false | | | -| `»» workspace_transition` | [codersdk.WorkspaceTransition](schemas.md#codersdkworkspacetransition) | false | | | -| `» status` | [codersdk.WorkspaceStatus](schemas.md#codersdkworkspacestatus) | false | | | -| `» template_version_id` | string(uuid) | false | | | -| `» template_version_name` | string | false | | | -| `» transition` | [codersdk.WorkspaceTransition](schemas.md#codersdkworkspacetransition) | false | | | -| `» updated_at` | string(date-time) | false | | | -| `» workspace_id` | string(uuid) | false | | | -| `» workspace_name` | string | false | | | -| `» workspace_owner_id` | string(uuid) | false | | | -| `» workspace_owner_name` | string | false | | | +| Name | Type | Required | Restrictions | Description | +| ------------------------------------- | -------------------------------------------------------------------------------- | -------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `[array item]` | array | false | | | +| `» build_number` | integer | false | | | +| `» created_at` | string(date-time) | false | | | +| `» daily_cost` | integer | false | | | +| `» deadline` | string(date-time) | false | | | +| `» id` | string(uuid) | false | | | +| `» initiator_id` | string(uuid) | false | | | +| `» initiator_name` | string | false | | | +| `» job` | [codersdk.ProvisionerJob](schemas.md#codersdkprovisionerjob) | false | | | +| `»» canceled_at` | string(date-time) | false | | | +| `»» completed_at` | string(date-time) | false | | | +| `»» created_at` | string(date-time) | false | | | +| `»» error` | string | false | | | +| `»» file_id` | string(uuid) | false | | | +| `»» id` | string(uuid) | false | | | +| `»» started_at` | string(date-time) | false | | | +| `»» status` | [codersdk.ProvisionerJobStatus](schemas.md#codersdkprovisionerjobstatus) | false | | | +| `»» tags` | object | false | | | +| `»»» [any property]` | string | false | | | +| `»» worker_id` | string(uuid) | false | | | +| `» reason` | [codersdk.BuildReason](schemas.md#codersdkbuildreason) | false | | | +| `» resources` | array | false | | | +| `»» agents` | array | false | | | +| `»»» apps` | array | false | | | +| `»»»» command` | string | false | | | +| `»»»» display_name` | string | false | | »»»display name is a friendly name for the app. | +| `»»»» external` | boolean | false | | External specifies whether the URL should be opened externally on the client or not. | +| `»»»» health` | [codersdk.WorkspaceAppHealth](schemas.md#codersdkworkspaceapphealth) | false | | | +| `»»»» healthcheck` | [codersdk.Healthcheck](schemas.md#codersdkhealthcheck) | false | | Healthcheck specifies the configuration for checking app health. | +| `»»»»» interval` | integer | false | | Interval specifies the seconds between each health check. | +| `»»»»» threshold` | integer | false | | Threshold specifies the number of consecutive failed health checks before returning "unhealthy". | +| `»»»»» url` | string | false | | »»»»url specifies the endpoint to check for the app health. | +| `»»»» icon` | string | false | | Icon is a relative path or external URL that specifies an icon to be displayed in the dashboard. | +| `»»»» id` | string(uuid) | false | | | +| `»»»» sharing_level` | [codersdk.WorkspaceAppSharingLevel](schemas.md#codersdkworkspaceappsharinglevel) | false | | | +| `»»»» slug` | string | false | | Slug is a unique identifier within the agent. | +| `»»»» subdomain` | boolean | false | | Subdomain denotes whether the app should be accessed via a path on the `coder server` or via a hostname-based dev URL. If this is set to true and there is no app wildcard configured on the server, the app will not be accessible in the UI. | +| `»»»» url` | string | false | | »»»url is the address being proxied to inside the workspace. If external is specified, this will be opened on the client. | +| `»»» architecture` | string | false | | | +| `»»» connection_timeout_seconds` | integer | false | | | +| `»»» created_at` | string(date-time) | false | | | +| `»»» directory` | string | false | | | +| `»»» disconnected_at` | string(date-time) | false | | | +| `»»» environment_variables` | object | false | | | +| `»»»» [any property]` | string | false | | | +| `»»» expanded_directory` | string | false | | | +| `»»» first_connected_at` | string(date-time) | false | | | +| `»»» id` | string(uuid) | false | | | +| `»»» instance_id` | string | false | | | +| `»»» last_connected_at` | string(date-time) | false | | | +| `»»» latency` | object | false | | »»latency is mapped by region name (e.g. "New York City", "Seattle"). | +| `»»»» [any property]` | [codersdk.DERPRegion](schemas.md#codersdkderpregion) | false | | | +| `»»»»» latency_ms` | number | false | | | +| `»»»»» preferred` | boolean | false | | | +| `»»» lifecycle_state` | [codersdk.WorkspaceAgentLifecycle](schemas.md#codersdkworkspaceagentlifecycle) | false | | | +| `»»» login_before_ready` | boolean | false | | »»login before ready if true, the agent will delay logins until it is ready (e.g. executing startup script has ended). | +| `»»» name` | string | false | | | +| `»»» operating_system` | string | false | | | +| `»»» resource_id` | string(uuid) | false | | | +| `»»» shutdown_script` | string | false | | | +| `»»» shutdown_script_timeout_seconds` | integer | false | | | +| `»»» startup_script` | string | false | | | +| `»»» startup_script_timeout_seconds` | integer | false | | »»startup script timeout seconds is the number of seconds to wait for the startup script to complete. If the script does not complete within this time, the agent lifecycle will be marked as start_timeout. | +| `»»» status` | [codersdk.WorkspaceAgentStatus](schemas.md#codersdkworkspaceagentstatus) | false | | | +| `»»» troubleshooting_url` | string | false | | | +| `»»» updated_at` | string(date-time) | false | | | +| `»»» version` | string | false | | | +| `»» created_at` | string(date-time) | false | | | +| `»» daily_cost` | integer | false | | | +| `»» hide` | boolean | false | | | +| `»» icon` | string | false | | | +| `»» id` | string(uuid) | false | | | +| `»» job_id` | string(uuid) | false | | | +| `»» metadata` | array | false | | | +| `»»» key` | string | false | | | +| `»»» sensitive` | boolean | false | | | +| `»»» value` | string | false | | | +| `»» name` | string | false | | | +| `»» type` | string | false | | | +| `»» workspace_transition` | [codersdk.WorkspaceTransition](schemas.md#codersdkworkspacetransition) | false | | | +| `» status` | [codersdk.WorkspaceStatus](schemas.md#codersdkworkspacestatus) | false | | | +| `» template_version_id` | string(uuid) | false | | | +| `» template_version_name` | string | false | | | +| `» transition` | [codersdk.WorkspaceTransition](schemas.md#codersdkworkspacetransition) | false | | | +| `» updated_at` | string(date-time) | false | | | +| `» workspace_id` | string(uuid) | false | | | +| `» workspace_name` | string | false | | | +| `» workspace_owner_id` | string(uuid) | false | | | +| `» workspace_owner_name` | string | false | | | #### Enumerated Values -| Property | Value | -| ---------------------- | --------------- | -| `status` | `pending` | -| `status` | `running` | -| `status` | `succeeded` | -| `status` | `canceling` | -| `status` | `canceled` | -| `status` | `failed` | -| `reason` | `initiator` | -| `reason` | `autostart` | -| `reason` | `autostop` | -| `health` | `disabled` | -| `health` | `initializing` | -| `health` | `healthy` | -| `health` | `unhealthy` | -| `sharing_level` | `owner` | -| `sharing_level` | `authenticated` | -| `sharing_level` | `public` | -| `lifecycle_state` | `created` | -| `lifecycle_state` | `starting` | -| `lifecycle_state` | `start_timeout` | -| `lifecycle_state` | `start_error` | -| `lifecycle_state` | `ready` | -| `status` | `connecting` | -| `status` | `connected` | -| `status` | `disconnected` | -| `status` | `timeout` | -| `workspace_transition` | `start` | -| `workspace_transition` | `stop` | -| `workspace_transition` | `delete` | -| `status` | `pending` | -| `status` | `starting` | -| `status` | `running` | -| `status` | `stopping` | -| `status` | `stopped` | -| `status` | `failed` | -| `status` | `canceling` | -| `status` | `canceled` | -| `status` | `deleting` | -| `status` | `deleted` | -| `transition` | `start` | -| `transition` | `stop` | -| `transition` | `delete` | +| Property | Value | +| ---------------------- | ------------------ | +| `status` | `pending` | +| `status` | `running` | +| `status` | `succeeded` | +| `status` | `canceling` | +| `status` | `canceled` | +| `status` | `failed` | +| `reason` | `initiator` | +| `reason` | `autostart` | +| `reason` | `autostop` | +| `health` | `disabled` | +| `health` | `initializing` | +| `health` | `healthy` | +| `health` | `unhealthy` | +| `sharing_level` | `owner` | +| `sharing_level` | `authenticated` | +| `sharing_level` | `public` | +| `lifecycle_state` | `created` | +| `lifecycle_state` | `starting` | +| `lifecycle_state` | `start_timeout` | +| `lifecycle_state` | `start_error` | +| `lifecycle_state` | `ready` | +| `lifecycle_state` | `shutting_down` | +| `lifecycle_state` | `shutdown_timeout` | +| `lifecycle_state` | `shutdown_error` | +| `lifecycle_state` | `off` | +| `status` | `connecting` | +| `status` | `connected` | +| `status` | `disconnected` | +| `status` | `timeout` | +| `workspace_transition` | `start` | +| `workspace_transition` | `stop` | +| `workspace_transition` | `delete` | +| `status` | `pending` | +| `status` | `starting` | +| `status` | `running` | +| `status` | `stopping` | +| `status` | `stopped` | +| `status` | `failed` | +| `status` | `canceling` | +| `status` | `canceled` | +| `status` | `deleting` | +| `status` | `deleted` | +| `transition` | `start` | +| `transition` | `stop` | +| `transition` | `delete` | To perform this operation, you must be authenticated. [Learn more](authentication.md). @@ -1228,6 +1250,8 @@ curl -X POST http://coder-server:8080/api/v2/workspaces/{workspace}/builds \ "name": "string", "operating_system": "string", "resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f", + "shutdown_script": "string", + "shutdown_script_timeout_seconds": 0, "startup_script": "string", "startup_script_timeout_seconds": 0, "status": "connecting", diff --git a/docs/api/schemas.md b/docs/api/schemas.md index afb7cd2fb764e..9a807e4e17a4f 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -175,6 +175,8 @@ }, "git_auth_configs": 0, "motd_file": "string", + "shutdown_script": "string", + "shutdown_script_timeout": 0, "startup_script": "string", "startup_script_timeout": 0, "vscode_port_proxy_uri": "string" @@ -183,18 +185,20 @@ ### Properties -| Name | Type | Required | Restrictions | Description | -| ------------------------ | ------------------------------------------------------- | -------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `apps` | array of [codersdk.WorkspaceApp](#codersdkworkspaceapp) | false | | | -| `derpmap` | [tailcfg.DERPMap](#tailcfgderpmap) | false | | | -| `directory` | string | false | | | -| `environment_variables` | object | false | | | -| » `[any property]` | string | false | | | -| `git_auth_configs` | integer | false | | Git auth configs stores the number of Git configurations the Coder deployment has. If this number is >0, we set up special configuration in the workspace. | -| `motd_file` | string | false | | | -| `startup_script` | string | false | | | -| `startup_script_timeout` | integer | false | | | -| `vscode_port_proxy_uri` | string | false | | | +| Name | Type | Required | Restrictions | Description | +| ------------------------- | ------------------------------------------------------- | -------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `apps` | array of [codersdk.WorkspaceApp](#codersdkworkspaceapp) | false | | | +| `derpmap` | [tailcfg.DERPMap](#tailcfgderpmap) | false | | | +| `directory` | string | false | | | +| `environment_variables` | object | false | | | +| » `[any property]` | string | false | | | +| `git_auth_configs` | integer | false | | Git auth configs stores the number of Git configurations the Coder deployment has. If this number is >0, we set up special configuration in the workspace. | +| `motd_file` | string | false | | | +| `shutdown_script` | string | false | | | +| `shutdown_script_timeout` | integer | false | | | +| `startup_script` | string | false | | | +| `startup_script_timeout` | integer | false | | | +| `vscode_port_proxy_uri` | string | false | | | ## agentsdk.PostAppHealthsRequest @@ -5357,6 +5361,8 @@ Parameter represents a set value for the scope. "name": "string", "operating_system": "string", "resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f", + "shutdown_script": "string", + "shutdown_script_timeout_seconds": 0, "startup_script": "string", "startup_script_timeout_seconds": 0, "status": "connecting", @@ -5480,6 +5486,8 @@ Parameter represents a set value for the scope. "name": "string", "operating_system": "string", "resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f", + "shutdown_script": "string", + "shutdown_script_timeout_seconds": 0, "startup_script": "string", "startup_script_timeout_seconds": 0, "status": "connecting", @@ -5491,34 +5499,36 @@ Parameter represents a set value for the scope. ### Properties -| Name | Type | Required | Restrictions | Description | -| -------------------------------- | -------------------------------------------------------------------- | -------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `apps` | array of [codersdk.WorkspaceApp](#codersdkworkspaceapp) | false | | | -| `architecture` | string | false | | | -| `connection_timeout_seconds` | integer | false | | | -| `created_at` | string | false | | | -| `directory` | string | false | | | -| `disconnected_at` | string | false | | | -| `environment_variables` | object | false | | | -| » `[any property]` | string | false | | | -| `expanded_directory` | string | false | | | -| `first_connected_at` | string | false | | | -| `id` | string | false | | | -| `instance_id` | string | false | | | -| `last_connected_at` | string | false | | | -| `latency` | object | false | | Latency is mapped by region name (e.g. "New York City", "Seattle"). | -| » `[any property]` | [codersdk.DERPRegion](#codersdkderpregion) | false | | | -| `lifecycle_state` | [codersdk.WorkspaceAgentLifecycle](#codersdkworkspaceagentlifecycle) | false | | | -| `login_before_ready` | boolean | false | | Login before ready if true, the agent will delay logins until it is ready (e.g. executing startup script has ended). | -| `name` | string | false | | | -| `operating_system` | string | false | | | -| `resource_id` | string | false | | | -| `startup_script` | string | false | | | -| `startup_script_timeout_seconds` | integer | false | | Startup script timeout seconds is the number of seconds to wait for the startup script to complete. If the script does not complete within this time, the agent lifecycle will be marked as start_timeout. | -| `status` | [codersdk.WorkspaceAgentStatus](#codersdkworkspaceagentstatus) | false | | | -| `troubleshooting_url` | string | false | | | -| `updated_at` | string | false | | | -| `version` | string | false | | | +| Name | Type | Required | Restrictions | Description | +| --------------------------------- | -------------------------------------------------------------------- | -------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `apps` | array of [codersdk.WorkspaceApp](#codersdkworkspaceapp) | false | | | +| `architecture` | string | false | | | +| `connection_timeout_seconds` | integer | false | | | +| `created_at` | string | false | | | +| `directory` | string | false | | | +| `disconnected_at` | string | false | | | +| `environment_variables` | object | false | | | +| » `[any property]` | string | false | | | +| `expanded_directory` | string | false | | | +| `first_connected_at` | string | false | | | +| `id` | string | false | | | +| `instance_id` | string | false | | | +| `last_connected_at` | string | false | | | +| `latency` | object | false | | Latency is mapped by region name (e.g. "New York City", "Seattle"). | +| » `[any property]` | [codersdk.DERPRegion](#codersdkderpregion) | false | | | +| `lifecycle_state` | [codersdk.WorkspaceAgentLifecycle](#codersdkworkspaceagentlifecycle) | false | | | +| `login_before_ready` | boolean | false | | Login before ready if true, the agent will delay logins until it is ready (e.g. executing startup script has ended). | +| `name` | string | false | | | +| `operating_system` | string | false | | | +| `resource_id` | string | false | | | +| `shutdown_script` | string | false | | | +| `shutdown_script_timeout_seconds` | integer | false | | | +| `startup_script` | string | false | | | +| `startup_script_timeout_seconds` | integer | false | | Startup script timeout seconds is the number of seconds to wait for the startup script to complete. If the script does not complete within this time, the agent lifecycle will be marked as start_timeout. | +| `status` | [codersdk.WorkspaceAgentStatus](#codersdkworkspaceagentstatus) | false | | | +| `troubleshooting_url` | string | false | | | +| `updated_at` | string | false | | | +| `version` | string | false | | | ## codersdk.WorkspaceAgentConnectionInfo @@ -5594,13 +5604,17 @@ Parameter represents a set value for the scope. #### Enumerated Values -| Value | -| --------------- | -| `created` | -| `starting` | -| `start_timeout` | -| `start_error` | -| `ready` | +| Value | +| ------------------ | +| `created` | +| `starting` | +| `start_timeout` | +| `start_error` | +| `ready` | +| `shutting_down` | +| `shutdown_timeout` | +| `shutdown_error` | +| `off` | ## codersdk.WorkspaceAgentListeningPort @@ -5815,6 +5829,8 @@ Parameter represents a set value for the scope. "name": "string", "operating_system": "string", "resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f", + "shutdown_script": "string", + "shutdown_script_timeout_seconds": 0, "startup_script": "string", "startup_script_timeout_seconds": 0, "status": "connecting", @@ -5984,6 +6000,8 @@ Parameter represents a set value for the scope. "name": "string", "operating_system": "string", "resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f", + "shutdown_script": "string", + "shutdown_script_timeout_seconds": 0, "startup_script": "string", "startup_script_timeout_seconds": 0, "status": "connecting", @@ -6175,6 +6193,8 @@ Parameter represents a set value for the scope. "name": "string", "operating_system": "string", "resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f", + "shutdown_script": "string", + "shutdown_script_timeout_seconds": 0, "startup_script": "string", "startup_script_timeout_seconds": 0, "status": "connecting", diff --git a/docs/api/templates.md b/docs/api/templates.md index ff45ed49473e7..5160210ae1792 100644 --- a/docs/api/templates.md +++ b/docs/api/templates.md @@ -1601,6 +1601,8 @@ curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion}/d "name": "string", "operating_system": "string", "resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f", + "shutdown_script": "string", + "shutdown_script_timeout_seconds": 0, "startup_script": "string", "startup_script_timeout_seconds": 0, "status": "connecting", @@ -1639,89 +1641,95 @@ curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion}/d Status Code **200** -| Name | Type | Required | Restrictions | Description | -| ----------------------------------- | -------------------------------------------------------------------------------- | -------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `[array item]` | array | false | | | -| `» agents` | array | false | | | -| `»» apps` | array | false | | | -| `»»» command` | string | false | | | -| `»»» display_name` | string | false | | »»display name is a friendly name for the app. | -| `»»» external` | boolean | false | | External specifies whether the URL should be opened externally on the client or not. | -| `»»» health` | [codersdk.WorkspaceAppHealth](schemas.md#codersdkworkspaceapphealth) | false | | | -| `»»» healthcheck` | [codersdk.Healthcheck](schemas.md#codersdkhealthcheck) | false | | Healthcheck specifies the configuration for checking app health. | -| `»»»» interval` | integer | false | | Interval specifies the seconds between each health check. | -| `»»»» threshold` | integer | false | | Threshold specifies the number of consecutive failed health checks before returning "unhealthy". | -| `»»»» url` | string | false | | »»»url specifies the endpoint to check for the app health. | -| `»»» icon` | string | false | | Icon is a relative path or external URL that specifies an icon to be displayed in the dashboard. | -| `»»» id` | string(uuid) | false | | | -| `»»» sharing_level` | [codersdk.WorkspaceAppSharingLevel](schemas.md#codersdkworkspaceappsharinglevel) | false | | | -| `»»» slug` | string | false | | Slug is a unique identifier within the agent. | -| `»»» subdomain` | boolean | false | | Subdomain denotes whether the app should be accessed via a path on the `coder server` or via a hostname-based dev URL. If this is set to true and there is no app wildcard configured on the server, the app will not be accessible in the UI. | -| `»»» url` | string | false | | »»url is the address being proxied to inside the workspace. If external is specified, this will be opened on the client. | -| `»» architecture` | string | false | | | -| `»» connection_timeout_seconds` | integer | false | | | -| `»» created_at` | string(date-time) | false | | | -| `»» directory` | string | false | | | -| `»» disconnected_at` | string(date-time) | false | | | -| `»» environment_variables` | object | false | | | -| `»»» [any property]` | string | false | | | -| `»» expanded_directory` | string | false | | | -| `»» first_connected_at` | string(date-time) | false | | | -| `»» id` | string(uuid) | false | | | -| `»» instance_id` | string | false | | | -| `»» last_connected_at` | string(date-time) | false | | | -| `»» latency` | object | false | | »latency is mapped by region name (e.g. "New York City", "Seattle"). | -| `»»» [any property]` | [codersdk.DERPRegion](schemas.md#codersdkderpregion) | false | | | -| `»»»» latency_ms` | number | false | | | -| `»»»» preferred` | boolean | false | | | -| `»» lifecycle_state` | [codersdk.WorkspaceAgentLifecycle](schemas.md#codersdkworkspaceagentlifecycle) | false | | | -| `»» login_before_ready` | boolean | false | | »login before ready if true, the agent will delay logins until it is ready (e.g. executing startup script has ended). | -| `»» name` | string | false | | | -| `»» operating_system` | string | false | | | -| `»» resource_id` | string(uuid) | false | | | -| `»» startup_script` | string | false | | | -| `»» startup_script_timeout_seconds` | integer | false | | »startup script timeout seconds is the number of seconds to wait for the startup script to complete. If the script does not complete within this time, the agent lifecycle will be marked as start_timeout. | -| `»» status` | [codersdk.WorkspaceAgentStatus](schemas.md#codersdkworkspaceagentstatus) | false | | | -| `»» troubleshooting_url` | string | false | | | -| `»» updated_at` | string(date-time) | false | | | -| `»» version` | string | false | | | -| `» created_at` | string(date-time) | false | | | -| `» daily_cost` | integer | false | | | -| `» hide` | boolean | false | | | -| `» icon` | string | false | | | -| `» id` | string(uuid) | false | | | -| `» job_id` | string(uuid) | false | | | -| `» metadata` | array | false | | | -| `»» key` | string | false | | | -| `»» sensitive` | boolean | false | | | -| `»» value` | string | false | | | -| `» name` | string | false | | | -| `» type` | string | false | | | -| `» workspace_transition` | [codersdk.WorkspaceTransition](schemas.md#codersdkworkspacetransition) | false | | | +| Name | Type | Required | Restrictions | Description | +| ------------------------------------ | -------------------------------------------------------------------------------- | -------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `[array item]` | array | false | | | +| `» agents` | array | false | | | +| `»» apps` | array | false | | | +| `»»» command` | string | false | | | +| `»»» display_name` | string | false | | »»display name is a friendly name for the app. | +| `»»» external` | boolean | false | | External specifies whether the URL should be opened externally on the client or not. | +| `»»» health` | [codersdk.WorkspaceAppHealth](schemas.md#codersdkworkspaceapphealth) | false | | | +| `»»» healthcheck` | [codersdk.Healthcheck](schemas.md#codersdkhealthcheck) | false | | Healthcheck specifies the configuration for checking app health. | +| `»»»» interval` | integer | false | | Interval specifies the seconds between each health check. | +| `»»»» threshold` | integer | false | | Threshold specifies the number of consecutive failed health checks before returning "unhealthy". | +| `»»»» url` | string | false | | »»»url specifies the endpoint to check for the app health. | +| `»»» icon` | string | false | | Icon is a relative path or external URL that specifies an icon to be displayed in the dashboard. | +| `»»» id` | string(uuid) | false | | | +| `»»» sharing_level` | [codersdk.WorkspaceAppSharingLevel](schemas.md#codersdkworkspaceappsharinglevel) | false | | | +| `»»» slug` | string | false | | Slug is a unique identifier within the agent. | +| `»»» subdomain` | boolean | false | | Subdomain denotes whether the app should be accessed via a path on the `coder server` or via a hostname-based dev URL. If this is set to true and there is no app wildcard configured on the server, the app will not be accessible in the UI. | +| `»»» url` | string | false | | »»url is the address being proxied to inside the workspace. If external is specified, this will be opened on the client. | +| `»» architecture` | string | false | | | +| `»» connection_timeout_seconds` | integer | false | | | +| `»» created_at` | string(date-time) | false | | | +| `»» directory` | string | false | | | +| `»» disconnected_at` | string(date-time) | false | | | +| `»» environment_variables` | object | false | | | +| `»»» [any property]` | string | false | | | +| `»» expanded_directory` | string | false | | | +| `»» first_connected_at` | string(date-time) | false | | | +| `»» id` | string(uuid) | false | | | +| `»» instance_id` | string | false | | | +| `»» last_connected_at` | string(date-time) | false | | | +| `»» latency` | object | false | | »latency is mapped by region name (e.g. "New York City", "Seattle"). | +| `»»» [any property]` | [codersdk.DERPRegion](schemas.md#codersdkderpregion) | false | | | +| `»»»» latency_ms` | number | false | | | +| `»»»» preferred` | boolean | false | | | +| `»» lifecycle_state` | [codersdk.WorkspaceAgentLifecycle](schemas.md#codersdkworkspaceagentlifecycle) | false | | | +| `»» login_before_ready` | boolean | false | | »login before ready if true, the agent will delay logins until it is ready (e.g. executing startup script has ended). | +| `»» name` | string | false | | | +| `»» operating_system` | string | false | | | +| `»» resource_id` | string(uuid) | false | | | +| `»» shutdown_script` | string | false | | | +| `»» shutdown_script_timeout_seconds` | integer | false | | | +| `»» startup_script` | string | false | | | +| `»» startup_script_timeout_seconds` | integer | false | | »startup script timeout seconds is the number of seconds to wait for the startup script to complete. If the script does not complete within this time, the agent lifecycle will be marked as start_timeout. | +| `»» status` | [codersdk.WorkspaceAgentStatus](schemas.md#codersdkworkspaceagentstatus) | false | | | +| `»» troubleshooting_url` | string | false | | | +| `»» updated_at` | string(date-time) | false | | | +| `»» version` | string | false | | | +| `» created_at` | string(date-time) | false | | | +| `» daily_cost` | integer | false | | | +| `» hide` | boolean | false | | | +| `» icon` | string | false | | | +| `» id` | string(uuid) | false | | | +| `» job_id` | string(uuid) | false | | | +| `» metadata` | array | false | | | +| `»» key` | string | false | | | +| `»» sensitive` | boolean | false | | | +| `»» value` | string | false | | | +| `» name` | string | false | | | +| `» type` | string | false | | | +| `» workspace_transition` | [codersdk.WorkspaceTransition](schemas.md#codersdkworkspacetransition) | false | | | #### Enumerated Values -| Property | Value | -| ---------------------- | --------------- | -| `health` | `disabled` | -| `health` | `initializing` | -| `health` | `healthy` | -| `health` | `unhealthy` | -| `sharing_level` | `owner` | -| `sharing_level` | `authenticated` | -| `sharing_level` | `public` | -| `lifecycle_state` | `created` | -| `lifecycle_state` | `starting` | -| `lifecycle_state` | `start_timeout` | -| `lifecycle_state` | `start_error` | -| `lifecycle_state` | `ready` | -| `status` | `connecting` | -| `status` | `connected` | -| `status` | `disconnected` | -| `status` | `timeout` | -| `workspace_transition` | `start` | -| `workspace_transition` | `stop` | -| `workspace_transition` | `delete` | +| Property | Value | +| ---------------------- | ------------------ | +| `health` | `disabled` | +| `health` | `initializing` | +| `health` | `healthy` | +| `health` | `unhealthy` | +| `sharing_level` | `owner` | +| `sharing_level` | `authenticated` | +| `sharing_level` | `public` | +| `lifecycle_state` | `created` | +| `lifecycle_state` | `starting` | +| `lifecycle_state` | `start_timeout` | +| `lifecycle_state` | `start_error` | +| `lifecycle_state` | `ready` | +| `lifecycle_state` | `shutting_down` | +| `lifecycle_state` | `shutdown_timeout` | +| `lifecycle_state` | `shutdown_error` | +| `lifecycle_state` | `off` | +| `status` | `connecting` | +| `status` | `connected` | +| `status` | `disconnected` | +| `status` | `timeout` | +| `workspace_transition` | `start` | +| `workspace_transition` | `stop` | +| `workspace_transition` | `delete` | To perform this operation, you must be authenticated. [Learn more](authentication.md). @@ -2018,6 +2026,8 @@ curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion}/r "name": "string", "operating_system": "string", "resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f", + "shutdown_script": "string", + "shutdown_script_timeout_seconds": 0, "startup_script": "string", "startup_script_timeout_seconds": 0, "status": "connecting", @@ -2056,89 +2066,95 @@ curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion}/r Status Code **200** -| Name | Type | Required | Restrictions | Description | -| ----------------------------------- | -------------------------------------------------------------------------------- | -------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `[array item]` | array | false | | | -| `» agents` | array | false | | | -| `»» apps` | array | false | | | -| `»»» command` | string | false | | | -| `»»» display_name` | string | false | | »»display name is a friendly name for the app. | -| `»»» external` | boolean | false | | External specifies whether the URL should be opened externally on the client or not. | -| `»»» health` | [codersdk.WorkspaceAppHealth](schemas.md#codersdkworkspaceapphealth) | false | | | -| `»»» healthcheck` | [codersdk.Healthcheck](schemas.md#codersdkhealthcheck) | false | | Healthcheck specifies the configuration for checking app health. | -| `»»»» interval` | integer | false | | Interval specifies the seconds between each health check. | -| `»»»» threshold` | integer | false | | Threshold specifies the number of consecutive failed health checks before returning "unhealthy". | -| `»»»» url` | string | false | | »»»url specifies the endpoint to check for the app health. | -| `»»» icon` | string | false | | Icon is a relative path or external URL that specifies an icon to be displayed in the dashboard. | -| `»»» id` | string(uuid) | false | | | -| `»»» sharing_level` | [codersdk.WorkspaceAppSharingLevel](schemas.md#codersdkworkspaceappsharinglevel) | false | | | -| `»»» slug` | string | false | | Slug is a unique identifier within the agent. | -| `»»» subdomain` | boolean | false | | Subdomain denotes whether the app should be accessed via a path on the `coder server` or via a hostname-based dev URL. If this is set to true and there is no app wildcard configured on the server, the app will not be accessible in the UI. | -| `»»» url` | string | false | | »»url is the address being proxied to inside the workspace. If external is specified, this will be opened on the client. | -| `»» architecture` | string | false | | | -| `»» connection_timeout_seconds` | integer | false | | | -| `»» created_at` | string(date-time) | false | | | -| `»» directory` | string | false | | | -| `»» disconnected_at` | string(date-time) | false | | | -| `»» environment_variables` | object | false | | | -| `»»» [any property]` | string | false | | | -| `»» expanded_directory` | string | false | | | -| `»» first_connected_at` | string(date-time) | false | | | -| `»» id` | string(uuid) | false | | | -| `»» instance_id` | string | false | | | -| `»» last_connected_at` | string(date-time) | false | | | -| `»» latency` | object | false | | »latency is mapped by region name (e.g. "New York City", "Seattle"). | -| `»»» [any property]` | [codersdk.DERPRegion](schemas.md#codersdkderpregion) | false | | | -| `»»»» latency_ms` | number | false | | | -| `»»»» preferred` | boolean | false | | | -| `»» lifecycle_state` | [codersdk.WorkspaceAgentLifecycle](schemas.md#codersdkworkspaceagentlifecycle) | false | | | -| `»» login_before_ready` | boolean | false | | »login before ready if true, the agent will delay logins until it is ready (e.g. executing startup script has ended). | -| `»» name` | string | false | | | -| `»» operating_system` | string | false | | | -| `»» resource_id` | string(uuid) | false | | | -| `»» startup_script` | string | false | | | -| `»» startup_script_timeout_seconds` | integer | false | | »startup script timeout seconds is the number of seconds to wait for the startup script to complete. If the script does not complete within this time, the agent lifecycle will be marked as start_timeout. | -| `»» status` | [codersdk.WorkspaceAgentStatus](schemas.md#codersdkworkspaceagentstatus) | false | | | -| `»» troubleshooting_url` | string | false | | | -| `»» updated_at` | string(date-time) | false | | | -| `»» version` | string | false | | | -| `» created_at` | string(date-time) | false | | | -| `» daily_cost` | integer | false | | | -| `» hide` | boolean | false | | | -| `» icon` | string | false | | | -| `» id` | string(uuid) | false | | | -| `» job_id` | string(uuid) | false | | | -| `» metadata` | array | false | | | -| `»» key` | string | false | | | -| `»» sensitive` | boolean | false | | | -| `»» value` | string | false | | | -| `» name` | string | false | | | -| `» type` | string | false | | | -| `» workspace_transition` | [codersdk.WorkspaceTransition](schemas.md#codersdkworkspacetransition) | false | | | +| Name | Type | Required | Restrictions | Description | +| ------------------------------------ | -------------------------------------------------------------------------------- | -------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `[array item]` | array | false | | | +| `» agents` | array | false | | | +| `»» apps` | array | false | | | +| `»»» command` | string | false | | | +| `»»» display_name` | string | false | | »»display name is a friendly name for the app. | +| `»»» external` | boolean | false | | External specifies whether the URL should be opened externally on the client or not. | +| `»»» health` | [codersdk.WorkspaceAppHealth](schemas.md#codersdkworkspaceapphealth) | false | | | +| `»»» healthcheck` | [codersdk.Healthcheck](schemas.md#codersdkhealthcheck) | false | | Healthcheck specifies the configuration for checking app health. | +| `»»»» interval` | integer | false | | Interval specifies the seconds between each health check. | +| `»»»» threshold` | integer | false | | Threshold specifies the number of consecutive failed health checks before returning "unhealthy". | +| `»»»» url` | string | false | | »»»url specifies the endpoint to check for the app health. | +| `»»» icon` | string | false | | Icon is a relative path or external URL that specifies an icon to be displayed in the dashboard. | +| `»»» id` | string(uuid) | false | | | +| `»»» sharing_level` | [codersdk.WorkspaceAppSharingLevel](schemas.md#codersdkworkspaceappsharinglevel) | false | | | +| `»»» slug` | string | false | | Slug is a unique identifier within the agent. | +| `»»» subdomain` | boolean | false | | Subdomain denotes whether the app should be accessed via a path on the `coder server` or via a hostname-based dev URL. If this is set to true and there is no app wildcard configured on the server, the app will not be accessible in the UI. | +| `»»» url` | string | false | | »»url is the address being proxied to inside the workspace. If external is specified, this will be opened on the client. | +| `»» architecture` | string | false | | | +| `»» connection_timeout_seconds` | integer | false | | | +| `»» created_at` | string(date-time) | false | | | +| `»» directory` | string | false | | | +| `»» disconnected_at` | string(date-time) | false | | | +| `»» environment_variables` | object | false | | | +| `»»» [any property]` | string | false | | | +| `»» expanded_directory` | string | false | | | +| `»» first_connected_at` | string(date-time) | false | | | +| `»» id` | string(uuid) | false | | | +| `»» instance_id` | string | false | | | +| `»» last_connected_at` | string(date-time) | false | | | +| `»» latency` | object | false | | »latency is mapped by region name (e.g. "New York City", "Seattle"). | +| `»»» [any property]` | [codersdk.DERPRegion](schemas.md#codersdkderpregion) | false | | | +| `»»»» latency_ms` | number | false | | | +| `»»»» preferred` | boolean | false | | | +| `»» lifecycle_state` | [codersdk.WorkspaceAgentLifecycle](schemas.md#codersdkworkspaceagentlifecycle) | false | | | +| `»» login_before_ready` | boolean | false | | »login before ready if true, the agent will delay logins until it is ready (e.g. executing startup script has ended). | +| `»» name` | string | false | | | +| `»» operating_system` | string | false | | | +| `»» resource_id` | string(uuid) | false | | | +| `»» shutdown_script` | string | false | | | +| `»» shutdown_script_timeout_seconds` | integer | false | | | +| `»» startup_script` | string | false | | | +| `»» startup_script_timeout_seconds` | integer | false | | »startup script timeout seconds is the number of seconds to wait for the startup script to complete. If the script does not complete within this time, the agent lifecycle will be marked as start_timeout. | +| `»» status` | [codersdk.WorkspaceAgentStatus](schemas.md#codersdkworkspaceagentstatus) | false | | | +| `»» troubleshooting_url` | string | false | | | +| `»» updated_at` | string(date-time) | false | | | +| `»» version` | string | false | | | +| `» created_at` | string(date-time) | false | | | +| `» daily_cost` | integer | false | | | +| `» hide` | boolean | false | | | +| `» icon` | string | false | | | +| `» id` | string(uuid) | false | | | +| `» job_id` | string(uuid) | false | | | +| `» metadata` | array | false | | | +| `»» key` | string | false | | | +| `»» sensitive` | boolean | false | | | +| `»» value` | string | false | | | +| `» name` | string | false | | | +| `» type` | string | false | | | +| `» workspace_transition` | [codersdk.WorkspaceTransition](schemas.md#codersdkworkspacetransition) | false | | | #### Enumerated Values -| Property | Value | -| ---------------------- | --------------- | -| `health` | `disabled` | -| `health` | `initializing` | -| `health` | `healthy` | -| `health` | `unhealthy` | -| `sharing_level` | `owner` | -| `sharing_level` | `authenticated` | -| `sharing_level` | `public` | -| `lifecycle_state` | `created` | -| `lifecycle_state` | `starting` | -| `lifecycle_state` | `start_timeout` | -| `lifecycle_state` | `start_error` | -| `lifecycle_state` | `ready` | -| `status` | `connecting` | -| `status` | `connected` | -| `status` | `disconnected` | -| `status` | `timeout` | -| `workspace_transition` | `start` | -| `workspace_transition` | `stop` | -| `workspace_transition` | `delete` | +| Property | Value | +| ---------------------- | ------------------ | +| `health` | `disabled` | +| `health` | `initializing` | +| `health` | `healthy` | +| `health` | `unhealthy` | +| `sharing_level` | `owner` | +| `sharing_level` | `authenticated` | +| `sharing_level` | `public` | +| `lifecycle_state` | `created` | +| `lifecycle_state` | `starting` | +| `lifecycle_state` | `start_timeout` | +| `lifecycle_state` | `start_error` | +| `lifecycle_state` | `ready` | +| `lifecycle_state` | `shutting_down` | +| `lifecycle_state` | `shutdown_timeout` | +| `lifecycle_state` | `shutdown_error` | +| `lifecycle_state` | `off` | +| `status` | `connecting` | +| `status` | `connected` | +| `status` | `disconnected` | +| `status` | `timeout` | +| `workspace_transition` | `start` | +| `workspace_transition` | `stop` | +| `workspace_transition` | `delete` | To perform this operation, you must be authenticated. [Learn more](authentication.md). diff --git a/docs/api/workspaces.md b/docs/api/workspaces.md index 67e1f8c0125f7..ebfb9cab84e61 100644 --- a/docs/api/workspaces.md +++ b/docs/api/workspaces.md @@ -134,6 +134,8 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/member "name": "string", "operating_system": "string", "resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f", + "shutdown_script": "string", + "shutdown_script_timeout_seconds": 0, "startup_script": "string", "startup_script_timeout_seconds": 0, "status": "connecting", @@ -299,6 +301,8 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam "name": "string", "operating_system": "string", "resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f", + "shutdown_script": "string", + "shutdown_script_timeout_seconds": 0, "startup_script": "string", "startup_script_timeout_seconds": 0, "status": "connecting", @@ -483,6 +487,8 @@ curl -X GET http://coder-server:8080/api/v2/workspaces \ "name": "string", "operating_system": "string", "resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f", + "shutdown_script": "string", + "shutdown_script_timeout_seconds": 0, "startup_script": "string", "startup_script_timeout_seconds": 0, "status": "connecting", @@ -649,6 +655,8 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace} \ "name": "string", "operating_system": "string", "resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f", + "shutdown_script": "string", + "shutdown_script_timeout_seconds": 0, "startup_script": "string", "startup_script_timeout_seconds": 0, "status": "connecting", diff --git a/docs/templates.md b/docs/templates.md index 591d321c906e0..f84107ae11655 100644 --- a/docs/templates.md +++ b/docs/templates.md @@ -444,8 +444,8 @@ practices: URL](./admin/configure.md#access-url) - Manually connect to the resource and check the agent logs (e.g., `kubectl exec`, `docker exec` or AWS console) - The Coder agent logs are typically stored in `/tmp/coder-agent.log` - - The Coder agent startup script logs are typically stored in - `/tmp/coder-startup-script.log` + - The Coder agent startup script logs are typically stored in `/tmp/coder-startup-script.log` + - The Coder agent shutdown script logs are typically stored in `/tmp/coder-shutdown-script.log` - This can also happen if the websockets are not being forwarded correctly when running Coder behind a reverse proxy. [Read our reverse-proxy docs](https://coder.com/docs/v2/latest/admin/configure#tls--reverse-proxy) ### Agent does not become ready diff --git a/docs/workspaces.md b/docs/workspaces.md index c6224b9a44a51..6225645458182 100644 --- a/docs/workspaces.md +++ b/docs/workspaces.md @@ -84,10 +84,11 @@ coder update --always-prompt Coder stores macOS and Linux logs at the following locations: -| Service | Location | -| ---------------- | ------------------------------- | -| `startup_script` | `/tmp/coder-startup-script.log` | -| Agent | `/tmp/coder-agent.log` | +| Service | Location | +| ----------------- | -------------------------------- | +| `startup_script` | `/tmp/coder-startup-script.log` | +| `shutdown_script` | `/tmp/coder-shutdown-script.log` | +| Agent | `/tmp/coder-agent.log` | --- diff --git a/provisioner/terraform/resources.go b/provisioner/terraform/resources.go index e370d7371541c..27234f8f0ae4f 100644 --- a/provisioner/terraform/resources.go +++ b/provisioner/terraform/resources.go @@ -16,19 +16,21 @@ import ( // A mapping of attributes on the "coder_agent" resource. type agentAttributes struct { - Auth string `mapstructure:"auth"` - OperatingSystem string `mapstructure:"os"` - Architecture string `mapstructure:"arch"` - Directory string `mapstructure:"dir"` - ID string `mapstructure:"id"` - Token string `mapstructure:"token"` - Env map[string]string `mapstructure:"env"` - StartupScript string `mapstructure:"startup_script"` - ConnectionTimeoutSeconds int32 `mapstructure:"connection_timeout"` - TroubleshootingURL string `mapstructure:"troubleshooting_url"` - MOTDFile string `mapstructure:"motd_file"` - LoginBeforeReady bool `mapstructure:"login_before_ready"` - StartupScriptTimeoutSeconds int32 `mapstructure:"startup_script_timeout"` + Auth string `mapstructure:"auth"` + OperatingSystem string `mapstructure:"os"` + Architecture string `mapstructure:"arch"` + Directory string `mapstructure:"dir"` + ID string `mapstructure:"id"` + Token string `mapstructure:"token"` + Env map[string]string `mapstructure:"env"` + StartupScript string `mapstructure:"startup_script"` + ConnectionTimeoutSeconds int32 `mapstructure:"connection_timeout"` + TroubleshootingURL string `mapstructure:"troubleshooting_url"` + MOTDFile string `mapstructure:"motd_file"` + LoginBeforeReady bool `mapstructure:"login_before_ready"` + StartupScriptTimeoutSeconds int32 `mapstructure:"startup_script_timeout"` + ShutdownScript string `mapstructure:"shutdown_script"` + ShutdownScriptTimeoutSeconds int32 `mapstructure:"shutdown_script_timeout"` } // A mapping of attributes on the "coder_app" resource. @@ -139,18 +141,20 @@ func ConvertState(modules []*tfjson.StateModule, rawGraph string) (*State, error } agent := &proto.Agent{ - Name: tfResource.Name, - Id: attrs.ID, - Env: attrs.Env, - StartupScript: attrs.StartupScript, - OperatingSystem: attrs.OperatingSystem, - Architecture: attrs.Architecture, - Directory: attrs.Directory, - ConnectionTimeoutSeconds: attrs.ConnectionTimeoutSeconds, - TroubleshootingUrl: attrs.TroubleshootingURL, - MotdFile: attrs.MOTDFile, - LoginBeforeReady: loginBeforeReady, - StartupScriptTimeoutSeconds: attrs.StartupScriptTimeoutSeconds, + Name: tfResource.Name, + Id: attrs.ID, + Env: attrs.Env, + StartupScript: attrs.StartupScript, + OperatingSystem: attrs.OperatingSystem, + Architecture: attrs.Architecture, + Directory: attrs.Directory, + ConnectionTimeoutSeconds: attrs.ConnectionTimeoutSeconds, + TroubleshootingUrl: attrs.TroubleshootingURL, + MotdFile: attrs.MOTDFile, + LoginBeforeReady: loginBeforeReady, + StartupScriptTimeoutSeconds: attrs.StartupScriptTimeoutSeconds, + ShutdownScript: attrs.ShutdownScript, + ShutdownScriptTimeoutSeconds: attrs.ShutdownScriptTimeoutSeconds, } switch attrs.Auth { case "token": diff --git a/provisioner/terraform/resources_test.go b/provisioner/terraform/resources_test.go index ef95ef5154ff8..72ca9a8960370 100644 --- a/provisioner/terraform/resources_test.go +++ b/provisioner/terraform/resources_test.go @@ -105,31 +105,35 @@ func TestConvertResources(t *testing.T) { Name: "dev", Type: "null_resource", Agents: []*proto.Agent{{ - Name: "dev1", - OperatingSystem: "linux", - Architecture: "amd64", - Auth: &proto.Agent_Token{}, - ConnectionTimeoutSeconds: 120, - LoginBeforeReady: true, - StartupScriptTimeoutSeconds: 300, + Name: "dev1", + OperatingSystem: "linux", + Architecture: "amd64", + Auth: &proto.Agent_Token{}, + ConnectionTimeoutSeconds: 120, + LoginBeforeReady: true, + StartupScriptTimeoutSeconds: 300, + ShutdownScriptTimeoutSeconds: 300, }, { - Name: "dev2", - OperatingSystem: "darwin", - Architecture: "amd64", - Auth: &proto.Agent_Token{}, - ConnectionTimeoutSeconds: 1, - MotdFile: "/etc/motd", - LoginBeforeReady: true, - StartupScriptTimeoutSeconds: 30, + Name: "dev2", + OperatingSystem: "darwin", + Architecture: "amd64", + Auth: &proto.Agent_Token{}, + ConnectionTimeoutSeconds: 1, + MotdFile: "/etc/motd", + LoginBeforeReady: true, + StartupScriptTimeoutSeconds: 30, + ShutdownScript: "echo bye bye", + ShutdownScriptTimeoutSeconds: 30, }, { - Name: "dev3", - OperatingSystem: "windows", - Architecture: "arm64", - Auth: &proto.Agent_Token{}, - ConnectionTimeoutSeconds: 120, - TroubleshootingUrl: "https://coder.com/troubleshoot", - LoginBeforeReady: false, - StartupScriptTimeoutSeconds: 300, + Name: "dev3", + OperatingSystem: "windows", + Architecture: "arm64", + Auth: &proto.Agent_Token{}, + ConnectionTimeoutSeconds: 120, + TroubleshootingUrl: "https://coder.com/troubleshoot", + LoginBeforeReady: false, + StartupScriptTimeoutSeconds: 300, + ShutdownScriptTimeoutSeconds: 300, }}, }}, }, @@ -300,13 +304,14 @@ func TestConvertResources(t *testing.T) { Name: "dev", Type: "null_resource", Agents: []*proto.Agent{{ - Name: "main", - OperatingSystem: "linux", - Architecture: "amd64", - Auth: &proto.Agent_Token{}, - LoginBeforeReady: true, - ConnectionTimeoutSeconds: 120, - StartupScriptTimeoutSeconds: 300, + Name: "main", + OperatingSystem: "linux", + Architecture: "amd64", + Auth: &proto.Agent_Token{}, + LoginBeforeReady: true, + ConnectionTimeoutSeconds: 120, + StartupScriptTimeoutSeconds: 300, + ShutdownScriptTimeoutSeconds: 300, }}, }}, gitAuthProviders: []string{"github", "gitlab"}, diff --git a/provisioner/terraform/testdata/multiple-agents/multiple-agents.tf b/provisioner/terraform/testdata/multiple-agents/multiple-agents.tf index 1f266bbe0f450..78387372f5adb 100644 --- a/provisioner/terraform/testdata/multiple-agents/multiple-agents.tf +++ b/provisioner/terraform/testdata/multiple-agents/multiple-agents.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.6.10" + version = "0.6.12" } } } @@ -13,12 +13,14 @@ resource "coder_agent" "dev1" { } resource "coder_agent" "dev2" { - os = "darwin" - arch = "amd64" - connection_timeout = 1 - motd_file = "/etc/motd" - startup_script_timeout = 30 - login_before_ready = true + os = "darwin" + arch = "amd64" + connection_timeout = 1 + motd_file = "/etc/motd" + startup_script_timeout = 30 + login_before_ready = true + shutdown_script = "echo bye bye" + shutdown_script_timeout = 30 } resource "coder_agent" "dev3" { diff --git a/provisioner/terraform/testdata/multiple-agents/multiple-agents.tfplan.json b/provisioner/terraform/testdata/multiple-agents/multiple-agents.tfplan.json index 2f549b9ab2b57..eb1c2e3f87da7 100644 --- a/provisioner/terraform/testdata/multiple-agents/multiple-agents.tfplan.json +++ b/provisioner/terraform/testdata/multiple-agents/multiple-agents.tfplan.json @@ -21,6 +21,7 @@ "motd_file": null, "os": "linux", "shutdown_script": null, + "shutdown_script_timeout": 300, "startup_script": null, "startup_script_timeout": 300, "troubleshooting_url": null @@ -43,7 +44,8 @@ "login_before_ready": true, "motd_file": "/etc/motd", "os": "darwin", - "shutdown_script": null, + "shutdown_script": "echo bye bye", + "shutdown_script_timeout": 30, "startup_script": null, "startup_script_timeout": 30, "troubleshooting_url": null @@ -67,6 +69,7 @@ "motd_file": null, "os": "windows", "shutdown_script": null, + "shutdown_script_timeout": 300, "startup_script": null, "startup_script_timeout": 300, "troubleshooting_url": "https://coder.com/troubleshoot" @@ -110,6 +113,7 @@ "motd_file": null, "os": "linux", "shutdown_script": null, + "shutdown_script_timeout": 300, "startup_script": null, "startup_script_timeout": 300, "troubleshooting_url": null @@ -145,7 +149,8 @@ "login_before_ready": true, "motd_file": "/etc/motd", "os": "darwin", - "shutdown_script": null, + "shutdown_script": "echo bye bye", + "shutdown_script_timeout": 30, "startup_script": null, "startup_script_timeout": 30, "troubleshooting_url": null @@ -182,6 +187,7 @@ "motd_file": null, "os": "windows", "shutdown_script": null, + "shutdown_script_timeout": 300, "startup_script": null, "startup_script_timeout": 300, "troubleshooting_url": "https://coder.com/troubleshoot" @@ -224,7 +230,7 @@ "coder": { "name": "coder", "full_name": "registry.terraform.io/coder/coder", - "version_constraint": "0.6.10" + "version_constraint": "0.6.12" }, "null": { "name": "null", @@ -271,6 +277,12 @@ "os": { "constant_value": "darwin" }, + "shutdown_script": { + "constant_value": "echo bye bye" + }, + "shutdown_script_timeout": { + "constant_value": 30 + }, "startup_script_timeout": { "constant_value": 30 } diff --git a/provisioner/terraform/testdata/multiple-agents/multiple-agents.tfstate.json b/provisioner/terraform/testdata/multiple-agents/multiple-agents.tfstate.json index 2c9d78e8e5645..f4b5ff036e511 100644 --- a/provisioner/terraform/testdata/multiple-agents/multiple-agents.tfstate.json +++ b/provisioner/terraform/testdata/multiple-agents/multiple-agents.tfstate.json @@ -23,6 +23,7 @@ "motd_file": null, "os": "linux", "shutdown_script": null, + "shutdown_script_timeout": 300, "startup_script": null, "startup_script_timeout": 300, "token": "d296a9cd-6f7c-4c6b-b2f3-7a647512efe8", @@ -48,7 +49,8 @@ "login_before_ready": true, "motd_file": "/etc/motd", "os": "darwin", - "shutdown_script": null, + "shutdown_script": "echo bye bye", + "shutdown_script_timeout": 30, "startup_script": null, "startup_script_timeout": 30, "token": "b1e0fba4-5bba-439f-b3ea-3f6a8ba4d301", @@ -75,6 +77,7 @@ "motd_file": null, "os": "windows", "shutdown_script": null, + "shutdown_script_timeout": 300, "startup_script": null, "startup_script_timeout": 300, "token": "238ff017-12ae-403f-b3f8-4dea4dc87a7d", diff --git a/provisionersdk/proto/provisioner.pb.go b/provisionersdk/proto/provisioner.pb.go index 51f3a475f6022..dc804efdc6157 100644 --- a/provisionersdk/proto/provisioner.pb.go +++ b/provisionersdk/proto/provisioner.pb.go @@ -1236,12 +1236,14 @@ type Agent struct { // // *Agent_Token // *Agent_InstanceId - Auth isAgent_Auth `protobuf_oneof:"auth"` - ConnectionTimeoutSeconds int32 `protobuf:"varint,11,opt,name=connection_timeout_seconds,json=connectionTimeoutSeconds,proto3" json:"connection_timeout_seconds,omitempty"` - TroubleshootingUrl string `protobuf:"bytes,12,opt,name=troubleshooting_url,json=troubleshootingUrl,proto3" json:"troubleshooting_url,omitempty"` - MotdFile string `protobuf:"bytes,13,opt,name=motd_file,json=motdFile,proto3" json:"motd_file,omitempty"` - LoginBeforeReady bool `protobuf:"varint,14,opt,name=login_before_ready,json=loginBeforeReady,proto3" json:"login_before_ready,omitempty"` - StartupScriptTimeoutSeconds int32 `protobuf:"varint,15,opt,name=startup_script_timeout_seconds,json=startupScriptTimeoutSeconds,proto3" json:"startup_script_timeout_seconds,omitempty"` + Auth isAgent_Auth `protobuf_oneof:"auth"` + ConnectionTimeoutSeconds int32 `protobuf:"varint,11,opt,name=connection_timeout_seconds,json=connectionTimeoutSeconds,proto3" json:"connection_timeout_seconds,omitempty"` + TroubleshootingUrl string `protobuf:"bytes,12,opt,name=troubleshooting_url,json=troubleshootingUrl,proto3" json:"troubleshooting_url,omitempty"` + MotdFile string `protobuf:"bytes,13,opt,name=motd_file,json=motdFile,proto3" json:"motd_file,omitempty"` + LoginBeforeReady bool `protobuf:"varint,14,opt,name=login_before_ready,json=loginBeforeReady,proto3" json:"login_before_ready,omitempty"` + StartupScriptTimeoutSeconds int32 `protobuf:"varint,15,opt,name=startup_script_timeout_seconds,json=startupScriptTimeoutSeconds,proto3" json:"startup_script_timeout_seconds,omitempty"` + ShutdownScript string `protobuf:"bytes,16,opt,name=shutdown_script,json=shutdownScript,proto3" json:"shutdown_script,omitempty"` + ShutdownScriptTimeoutSeconds int32 `protobuf:"varint,17,opt,name=shutdown_script_timeout_seconds,json=shutdownScriptTimeoutSeconds,proto3" json:"shutdown_script_timeout_seconds,omitempty"` } func (x *Agent) Reset() { @@ -1388,6 +1390,20 @@ func (x *Agent) GetStartupScriptTimeoutSeconds() int32 { return 0 } +func (x *Agent) GetShutdownScript() string { + if x != nil { + return x.ShutdownScript + } + return "" +} + +func (x *Agent) GetShutdownScriptTimeoutSeconds() int32 { + if x != nil { + return x.ShutdownScriptTimeoutSeconds + } + return 0 +} + type isAgent_Auth interface { isAgent_Auth() } @@ -2774,7 +2790,7 @@ var file_provisionersdk_proto_provisioner_proto_rawDesc = []byte{ 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, - 0x65, 0x6e, 0x22, 0x8e, 0x05, 0x0a, 0x05, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, + 0x65, 0x6e, 0x22, 0xfe, 0x05, 0x0a, 0x05, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2d, 0x0a, 0x03, 0x65, 0x6e, 0x76, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, @@ -2811,6 +2827,13 @@ var file_provisionersdk_proto_provisioner_proto_rawDesc = []byte{ 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x5f, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x05, 0x52, 0x1b, 0x73, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x53, 0x65, 0x63, 0x6f, + 0x6e, 0x64, 0x73, 0x12, 0x27, 0x0a, 0x0f, 0x73, 0x68, 0x75, 0x74, 0x64, 0x6f, 0x77, 0x6e, 0x5f, + 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x18, 0x10, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x73, 0x68, + 0x75, 0x74, 0x64, 0x6f, 0x77, 0x6e, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x12, 0x45, 0x0a, 0x1f, + 0x73, 0x68, 0x75, 0x74, 0x64, 0x6f, 0x77, 0x6e, 0x5f, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x5f, + 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x5f, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x18, + 0x11, 0x20, 0x01, 0x28, 0x05, 0x52, 0x1c, 0x73, 0x68, 0x75, 0x74, 0x64, 0x6f, 0x77, 0x6e, 0x53, + 0x63, 0x72, 0x69, 0x70, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x1a, 0x36, 0x0a, 0x08, 0x45, 0x6e, 0x76, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, diff --git a/provisionersdk/proto/provisioner.proto b/provisionersdk/proto/provisioner.proto index 826b5ef5f11e8..d900aaf2674e9 100644 --- a/provisionersdk/proto/provisioner.proto +++ b/provisionersdk/proto/provisioner.proto @@ -142,6 +142,8 @@ message Agent { string motd_file = 13; bool login_before_ready = 14; int32 startup_script_timeout_seconds = 15; + string shutdown_script = 16; + int32 shutdown_script_timeout_seconds = 17; } enum AppSharingLevel { diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index b2e4115b03bce..d75b733fe103a 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -994,6 +994,8 @@ export interface WorkspaceAgent { readonly troubleshooting_url: string readonly login_before_ready: boolean readonly startup_script_timeout_seconds: number + readonly shutdown_script?: string + readonly shutdown_script_timeout_seconds: number } // From codersdk/workspaceagentconn.go @@ -1295,13 +1297,21 @@ export const ValidationMonotonicOrders: ValidationMonotonicOrder[] = [ // From codersdk/workspaceagents.go export type WorkspaceAgentLifecycle = | "created" + | "off" | "ready" + | "shutdown_error" + | "shutdown_timeout" + | "shutting_down" | "start_error" | "start_timeout" | "starting" export const WorkspaceAgentLifecycles: WorkspaceAgentLifecycle[] = [ "created", + "off", "ready", + "shutdown_error", + "shutdown_timeout", + "shutting_down", "start_error", "start_timeout", "starting", diff --git a/site/src/components/Resources/AgentRow.stories.tsx b/site/src/components/Resources/AgentRow.stories.tsx index 96a88fed9b7d2..d994ec77faa1d 100644 --- a/site/src/components/Resources/AgentRow.stories.tsx +++ b/site/src/components/Resources/AgentRow.stories.tsx @@ -3,7 +3,11 @@ import { MockWorkspace, MockWorkspaceAgent, MockWorkspaceAgentConnecting, + MockWorkspaceAgentOff, MockWorkspaceAgentOutdated, + MockWorkspaceAgentShutdownError, + MockWorkspaceAgentShutdownTimeout, + MockWorkspaceAgentShuttingDown, MockWorkspaceAgentStartError, MockWorkspaceAgentStarting, MockWorkspaceAgentStartTimeout, @@ -114,6 +118,38 @@ StartError.args = { showApps: true, } +export const ShuttingDown = Template.bind({}) +ShuttingDown.args = { + agent: MockWorkspaceAgentShuttingDown, + workspace: MockWorkspace, + applicationsHost: "", + showApps: true, +} + +export const ShutdownTimeout = Template.bind({}) +ShutdownTimeout.args = { + agent: MockWorkspaceAgentShutdownTimeout, + workspace: MockWorkspace, + applicationsHost: "", + showApps: true, +} + +export const ShutdownError = Template.bind({}) +ShutdownError.args = { + agent: MockWorkspaceAgentShutdownError, + workspace: MockWorkspace, + applicationsHost: "", + showApps: true, +} + +export const Off = Template.bind({}) +Off.args = { + agent: MockWorkspaceAgentOff, + workspace: MockWorkspace, + applicationsHost: "", + showApps: true, +} + export const ShowingPortForward = Template.bind({}) ShowingPortForward.args = { agent: MockWorkspaceAgent, diff --git a/site/src/components/Resources/AgentStatus.tsx b/site/src/components/Resources/AgentStatus.tsx index c0c2974462fe9..0a332d6d17878 100644 --- a/site/src/components/Resources/AgentStatus.tsx +++ b/site/src/components/Resources/AgentStatus.tsx @@ -16,9 +16,10 @@ import Link from "@material-ui/core/Link" // If we think in the agent status and lifecycle into a single enum/state I’d // say we would have: connecting, timeout, disconnected, connected:created, // connected:starting, connected:start_timeout, connected:start_error, -// connected:ready +// connected:ready, connected:shutting_down, connected:shutdown_timeout, +// connected:shutdown_error, connected:off. -const ReadyLifeCycle: React.FC = () => { +const ReadyLifecycle: React.FC = () => { const styles = useStyles() const { t } = useTranslation("workspacePage") @@ -132,6 +133,122 @@ const StartErrorLifecycle: React.FC<{ ) } +const ShuttingDownLifecycle: React.FC = () => { + const styles = useStyles() + const { t } = useTranslation("workspacePage") + + return ( + +
+ + ) +} + +const ShutdownTimeoutLifecycle: React.FC<{ + agent: WorkspaceAgent +}> = ({ agent }) => { + const { t } = useTranslation("agent") + const styles = useStyles() + const anchorRef = useRef(null) + const [isOpen, setIsOpen] = useState(false) + const id = isOpen ? "timeout-popover" : undefined + + return ( + <> + setIsOpen(true)} + onMouseLeave={() => setIsOpen(false)} + role="status" + aria-label={t("status.shutdownTimeout")} + className={styles.timeoutWarning} + /> + setIsOpen(true)} + onClose={() => setIsOpen(false)} + > + {t("shutdownTimeoutTooltip.title")} + + {t("shutdownTimeoutTooltip.message")}{" "} + + {t("shutdownTimeoutTooltip.link")} + + . + + + + ) +} + +const ShutdownErrorLifecycle: React.FC<{ + agent: WorkspaceAgent +}> = ({ agent }) => { + const { t } = useTranslation("agent") + const styles = useStyles() + const anchorRef = useRef(null) + const [isOpen, setIsOpen] = useState(false) + const id = isOpen ? "timeout-popover" : undefined + + return ( + <> + setIsOpen(true)} + onMouseLeave={() => setIsOpen(false)} + role="status" + aria-label={t("status.error")} + className={styles.errorWarning} + /> + setIsOpen(true)} + onClose={() => setIsOpen(false)} + > + {t("shutdownErrorTooltip.title")} + + {t("shutdownErrorTooltip.message")}{" "} + + {t("shutdownErrorTooltip.link")} + + . + + + + ) +} + +const OffLifecycle: React.FC = () => { + const styles = useStyles() + const { t } = useTranslation("workspacePage") + + return ( + +
+ + ) +} + const ConnectedStatus: React.FC<{ agent: WorkspaceAgent }> = ({ agent }) => { @@ -143,12 +260,12 @@ const ConnectedStatus: React.FC<{ // release indicating startup script behavior has changed. // https://github.com/coder/coder/issues/5749 if (agent.login_before_ready) { - return + return } return ( - + @@ -156,6 +273,18 @@ const ConnectedStatus: React.FC<{ + + + + + + + + + + + + diff --git a/site/src/i18n/en/agent.json b/site/src/i18n/en/agent.json index e30049da483dd..b4b9c8119e730 100644 --- a/site/src/i18n/en/agent.json +++ b/site/src/i18n/en/agent.json @@ -14,7 +14,9 @@ "status": { "timeout": "Timeout", "startTimeout": "Start Timeout", - "startError": "Error" + "startError": "Start Error", + "shutdownTimeout": "Stop Timeout", + "shutdownError": "Stop Error" }, "timeoutTooltip": { "title": "Agent is taking too long to connect", @@ -27,8 +29,18 @@ "link": "Troubleshoot" }, "startErrorTooltip": { - "title": "Error starting agent", - "message": "Something went wrong during the agent start.", + "title": "Error starting the agent", + "message": "Something went wrong during the agent startup.", + "link": "Troubleshoot" + }, + "shutdownTimeoutTooltip": { + "title": "Agent is taking too long to stop", + "message": "We noticed this agent is taking longer than expected to stop.", + "link": "Troubleshoot" + }, + "shutdownErrorTooltip": { + "title": "Error stopping the agent", + "message": "Something went wrong while trying to stop the agent.", "link": "Troubleshoot" }, "unableToConnect": "Unable to connect" diff --git a/site/src/i18n/en/workspacePage.json b/site/src/i18n/en/workspacePage.json index 2351c9029e7c5..114ff1fcf831e 100644 --- a/site/src/i18n/en/workspacePage.json +++ b/site/src/i18n/en/workspacePage.json @@ -39,7 +39,9 @@ "agentStatus": { "connected": { "ready": "Ready", - "starting": "Starting..." + "starting": "Starting...", + "shuttingDown": "Stopping...", + "off": "Stopped" }, "connecting": "Connecting...", "disconnected": "Disconnected", diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 3baafe214f829..61d8d211b32d2 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -401,6 +401,7 @@ export const MockWorkspaceAgent: TypesGen.WorkspaceAgent = { lifecycle_state: "starting", login_before_ready: false, startup_script_timeout_seconds: 120, + shutdown_script_timeout_seconds: 120, } export const MockWorkspaceAgentDisconnected: TypesGen.WorkspaceAgent = { @@ -478,6 +479,34 @@ export const MockWorkspaceAgentStartError: TypesGen.WorkspaceAgent = { lifecycle_state: "start_error", } +export const MockWorkspaceAgentShuttingDown: TypesGen.WorkspaceAgent = { + ...MockWorkspaceAgent, + id: "test-workspace-agent-shutting-down", + name: "a-shutting-down-workspace-agent", + lifecycle_state: "shutting_down", +} + +export const MockWorkspaceAgentShutdownTimeout: TypesGen.WorkspaceAgent = { + ...MockWorkspaceAgent, + id: "test-workspace-agent-shutdown-timeout", + name: "a-workspace-agent-timed-out-while-running-shutdownup-script", + lifecycle_state: "shutdown_timeout", +} + +export const MockWorkspaceAgentShutdownError: TypesGen.WorkspaceAgent = { + ...MockWorkspaceAgent, + id: "test-workspace-agent-shutdown-error", + name: "a-workspace-agent-errored-while-running-shutdownup-script", + lifecycle_state: "shutdown_error", +} + +export const MockWorkspaceAgentOff: TypesGen.WorkspaceAgent = { + ...MockWorkspaceAgent, + id: "test-workspace-agent-off", + name: "a-workspace-agent-is-shut-down", + lifecycle_state: "off", +} + export const MockWorkspaceResource: TypesGen.WorkspaceResource = { agents: [ MockWorkspaceAgent,