feat: add support for `coder_script` (#9584)

* Add basic migrations

* Improve schema

* Refactor agent scripts into it's own package

* Support legacy start and stop script format

* Pipe the scripts!

* Finish the piping

* Fix context usage

* It works!

* Fix sql query

* Fix SQL query

* Rename `LogSourceID` -> `SourceID`

* Fix the FE

* fmt

* Rename migrations

* Fix log tests

* Fix lint err

* Fix gen

* Fix story type

* Rename source to script

* Fix schema jank

* Uncomment test

* Rename proto to TimeoutSeconds

* Fix comments

* Fix comments

* Fix legacy endpoint without specified log_source

* Fix non-blocking by default in agent

* Fix resources tests

* Fix dbfake

* Fix resources

* Fix linting I think

* Add fixtures

* fmt

* Fix startup script behavior

* Fix comments

* Fix context

* Fix cancel

* Fix SQL tests

* Fix e2e tests

* Interrupt on Windows

* Fix agent leaking script process

* Fix migrations

* Fix stories

* Fix duplicate logs appearing

* Gen

* Fix log location

* Fix tests

* Fix tests

* Fix log output

* Show display name in output

* Fix print

* Return timeout on start context

* Gen

* Fix fixture

* Fix the agent status

* Fix startup timeout msg

* Fix command using shared context

* Fix timeout draining

* Change signal type

* Add deterministic colors to startup script logs

---------

Co-authored-by: Muhammad Atif Ali <atif@coder.com>
This commit is contained in:
Kyle Carberry 2023-09-25 16:47:17 -05:00 committed by GitHub
parent dac1375880
commit 1262eef2c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
61 changed files with 3820 additions and 2117 deletions

View File

@ -12,7 +12,6 @@ import (
"net/http"
"net/netip"
"os"
"os/exec"
"os/user"
"path/filepath"
"runtime"
@ -37,6 +36,7 @@ import (
"cdr.dev/slog"
"github.com/coder/coder/v2/agent/agentproc"
"github.com/coder/coder/v2/agent/agentscripts"
"github.com/coder/coder/v2/agent/agentssh"
"github.com/coder/coder/v2/agent/reconnectingpty"
"github.com/coder/coder/v2/buildinfo"
@ -196,6 +196,7 @@ type agent struct {
manifest atomic.Pointer[agentsdk.Manifest] // manifest is atomic because values can change after reconnection.
reportMetadataInterval time.Duration
scriptRunner *agentscripts.Runner
serviceBanner atomic.Pointer[codersdk.ServiceBannerConfig] // serviceBanner is atomic because it is periodically updated.
serviceBannerRefreshInterval time.Duration
sessionToken atomic.Pointer[string]
@ -238,7 +239,13 @@ func (a *agent) init(ctx context.Context) {
sshSrv.Manifest = &a.manifest
sshSrv.ServiceBanner = &a.serviceBanner
a.sshServer = sshSrv
a.scriptRunner = agentscripts.New(agentscripts.Options{
LogDir: a.logDir,
Logger: a.logger,
SSHServer: sshSrv,
Filesystem: a.filesystem,
PatchLogs: a.client.PatchLogs,
})
go a.runLoop(ctx)
}
@ -657,41 +664,29 @@ func (a *agent) run(ctx context.Context) error {
}
}
lifecycleState := codersdk.WorkspaceAgentLifecycleReady
scriptDone := make(chan error, 1)
err = a.scriptRunner.Init(manifest.Scripts)
if err != nil {
return xerrors.Errorf("init script runner: %w", err)
}
err = a.trackConnGoroutine(func() {
defer close(scriptDone)
scriptDone <- a.runStartupScript(ctx, manifest.StartupScript)
err := a.scriptRunner.Execute(ctx, func(script codersdk.WorkspaceAgentScript) bool {
return script.RunOnStart
})
if err != nil {
a.logger.Warn(ctx, "startup script failed", slog.Error(err))
if errors.Is(err, agentscripts.ErrTimeout) {
a.setLifecycle(ctx, codersdk.WorkspaceAgentLifecycleStartTimeout)
} else {
a.setLifecycle(ctx, codersdk.WorkspaceAgentLifecycleStartError)
}
} else {
a.setLifecycle(ctx, codersdk.WorkspaceAgentLifecycleReady)
}
a.scriptRunner.StartCron()
})
if err != nil {
return xerrors.Errorf("track startup script: %w", err)
return xerrors.Errorf("track conn goroutine: %w", err)
}
go func() {
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 manifest.StartupScriptTimeout > 0 {
t := time.NewTimer(manifest.StartupScriptTimeout)
defer t.Stop()
timeout = t.C
}
var err error
select {
case err = <-scriptDone:
case <-timeout:
a.logger.Warn(ctx, "script timed out", slog.F("lifecycle", "startup"), slog.F("timeout", manifest.StartupScriptTimeout))
a.setLifecycle(ctx, codersdk.WorkspaceAgentLifecycleStartTimeout)
err = <-scriptDone // The script can still complete after a timeout.
}
if err != nil {
if errors.Is(err, context.Canceled) {
return
}
lifecycleState = codersdk.WorkspaceAgentLifecycleStartError
}
a.setLifecycle(ctx, lifecycleState)
}()
}
// This automatically closes when the context ends!
@ -1006,93 +1001,6 @@ func (a *agent) runDERPMapSubscriber(ctx context.Context, network *tailnet.Conn)
}
}
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) (err error) {
if script == "" {
return nil
}
logger := a.logger.With(slog.F("lifecycle", lifecycle))
logger.Info(ctx, fmt.Sprintf("running %s script", lifecycle), slog.F("script", script))
fileWriter, 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 %s script log file: %w", lifecycle, err)
}
defer func() {
err := fileWriter.Close()
if err != nil {
logger.Warn(ctx, fmt.Sprintf("close %s script log file", lifecycle), slog.Error(err))
}
}()
cmdPty, err := a.sshServer.CreateCommand(ctx, script, nil)
if err != nil {
return xerrors.Errorf("%s script: create command: %w", lifecycle, err)
}
cmd := cmdPty.AsExec()
var stdout, stderr io.Writer = fileWriter, fileWriter
if lifecycle == "startup" {
send, flushAndClose := agentsdk.LogsSender(a.client.PatchLogs, logger)
// If ctx is canceled here (or in a writer below), we may be
// discarding logs, but that's okay because we're shutting down
// anyway. We could consider creating a new context here if we
// want better control over flush during shutdown.
defer func() {
if err := flushAndClose(ctx); err != nil {
logger.Warn(ctx, "flush startup logs failed", slog.Error(err))
}
}()
infoW := agentsdk.StartupLogsWriter(ctx, send, codersdk.WorkspaceAgentLogSourceStartupScript, codersdk.LogLevelInfo)
defer infoW.Close()
errW := agentsdk.StartupLogsWriter(ctx, send, codersdk.WorkspaceAgentLogSourceStartupScript, codersdk.LogLevelError)
defer errW.Close()
stdout = io.MultiWriter(fileWriter, infoW)
stderr = io.MultiWriter(fileWriter, errW)
}
cmd.Stdout = stdout
cmd.Stderr = stderr
start := time.Now()
defer func() {
end := time.Now()
execTime := end.Sub(start)
exitCode := 0
if err != nil {
exitCode = 255 // Unknown status.
var exitError *exec.ExitError
if xerrors.As(err, &exitError) {
exitCode = exitError.ExitCode()
}
logger.Warn(ctx, fmt.Sprintf("%s script failed", lifecycle), slog.F("execution_time", execTime), slog.F("exit_code", exitCode), slog.Error(err))
} else {
logger.Info(ctx, fmt.Sprintf("%s script completed", lifecycle), slog.F("execution_time", execTime), slog.F("exit_code", exitCode))
}
}()
err = cmd.Run()
if err != nil {
// cmd.Run does not return a context canceled error, it returns "signal: killed".
if ctx.Err() != nil {
return ctx.Err()
}
return xerrors.Errorf("%s script: run: %w", lifecycle, err)
}
return nil
}
func (a *agent) handleReconnectingPTY(ctx context.Context, logger slog.Logger, msg codersdk.WorkspaceAgentReconnectingPTYInit, conn net.Conn) (retErr error) {
defer conn.Close()
a.metrics.connectionsTotal.Add(1)
@ -1475,39 +1383,23 @@ func (a *agent) Close() error {
}
lifecycleState := codersdk.WorkspaceAgentLifecycleOff
if manifest := a.manifest.Load(); manifest != nil && manifest.ShutdownScript != "" {
scriptDone := make(chan error, 1)
go func() {
defer close(scriptDone)
scriptDone <- a.runShutdownScript(ctx, manifest.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 manifest.ShutdownScriptTimeout > 0 {
t := time.NewTimer(manifest.ShutdownScriptTimeout)
defer t.Stop()
timeout = t.C
}
var err error
select {
case err = <-scriptDone:
case <-timeout:
a.logger.Warn(ctx, "script timed out", slog.F("lifecycle", "shutdown"), slog.F("timeout", manifest.ShutdownScriptTimeout))
a.setLifecycle(ctx, codersdk.WorkspaceAgentLifecycleShutdownTimeout)
err = <-scriptDone // The script can still complete after a timeout.
}
if err != nil {
err = a.scriptRunner.Execute(ctx, func(script codersdk.WorkspaceAgentScript) bool {
return script.RunOnStop
})
if err != nil {
if errors.Is(err, agentscripts.ErrTimeout) {
lifecycleState = codersdk.WorkspaceAgentLifecycleShutdownTimeout
} else {
lifecycleState = codersdk.WorkspaceAgentLifecycleShutdownError
}
}
// Set final state and wait for it to be reported because context
// cancellation will stop the report loop.
a.setLifecycle(ctx, lifecycleState)
err = a.scriptRunner.Close()
if err != nil {
a.logger.Error(ctx, "script runner close", slog.Error(err))
}
// 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)

View File

@ -50,7 +50,6 @@ import (
"github.com/coder/coder/v2/agent/agentproc/agentproctest"
"github.com/coder/coder/v2/agent/agentssh"
"github.com/coder/coder/v2/agent/agenttest"
"github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/agentsdk"
"github.com/coder/coder/v2/pty"
@ -1060,84 +1059,6 @@ func TestAgent_SSHConnectionEnvVars(t *testing.T) {
}
}
func TestAgent_StartupScript(t *testing.T) {
t.Parallel()
output := "something"
command := "sh -c 'echo " + output + "'"
if runtime.GOOS == "windows" {
command = "cmd.exe /c echo " + output
}
t.Run("Success", func(t *testing.T) {
t.Parallel()
logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug)
client := agenttest.NewClient(t,
logger,
uuid.New(),
agentsdk.Manifest{
StartupScript: command,
DERPMap: &tailcfg.DERPMap{},
},
make(chan *agentsdk.Stats),
tailnet.NewCoordinator(logger),
)
closer := agent.New(agent.Options{
Client: client,
Filesystem: afero.NewMemMapFs(),
Logger: logger.Named("agent"),
ReconnectingPTYTimeout: 0,
})
t.Cleanup(func() {
_ = closer.Close()
})
assert.Eventually(t, func() bool {
got := client.GetLifecycleStates()
return len(got) > 0 && got[len(got)-1] == codersdk.WorkspaceAgentLifecycleReady
}, testutil.WaitShort, testutil.IntervalMedium)
require.Len(t, client.GetStartupLogs(), 1)
require.Equal(t, output, client.GetStartupLogs()[0].Output)
})
// This ensures that even when coderd sends back that the startup
// script has written too many lines it will still succeed!
t.Run("OverflowsAndSkips", func(t *testing.T) {
t.Parallel()
logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug)
client := agenttest.NewClient(t,
logger,
uuid.New(),
agentsdk.Manifest{
StartupScript: command,
DERPMap: &tailcfg.DERPMap{},
},
make(chan *agentsdk.Stats, 50),
tailnet.NewCoordinator(logger),
)
client.PatchWorkspaceLogs = func() error {
resp := httptest.NewRecorder()
httpapi.Write(context.Background(), resp, http.StatusRequestEntityTooLarge, codersdk.Response{
Message: "Too many lines!",
})
res := resp.Result()
defer res.Body.Close()
return codersdk.ReadBodyAsError(res)
}
closer := agent.New(agent.Options{
Client: client,
Filesystem: afero.NewMemMapFs(),
Logger: logger.Named("agent"),
ReconnectingPTYTimeout: 0,
})
t.Cleanup(func() {
_ = closer.Close()
})
assert.Eventually(t, func() bool {
got := client.GetLifecycleStates()
return len(got) > 0 && got[len(got)-1] == codersdk.WorkspaceAgentLifecycleReady
}, testutil.WaitShort, testutil.IntervalMedium)
require.Len(t, client.GetStartupLogs(), 0)
})
}
func TestAgent_Metadata(t *testing.T) {
t.Parallel()
@ -1292,8 +1213,11 @@ func TestAgent_Lifecycle(t *testing.T) {
t.Parallel()
_, client, _, _, _ := setupAgent(t, agentsdk.Manifest{
StartupScript: "sleep 3",
StartupScriptTimeout: time.Nanosecond,
Scripts: []codersdk.WorkspaceAgentScript{{
Script: "sleep 3",
Timeout: time.Millisecond,
RunOnStart: true,
}},
}, 0)
want := []codersdk.WorkspaceAgentLifecycle{
@ -1314,8 +1238,11 @@ func TestAgent_Lifecycle(t *testing.T) {
t.Parallel()
_, client, _, _, _ := setupAgent(t, agentsdk.Manifest{
StartupScript: "false",
StartupScriptTimeout: 30 * time.Second,
Scripts: []codersdk.WorkspaceAgentScript{{
Script: "false",
Timeout: 30 * time.Second,
RunOnStart: true,
}},
}, 0)
want := []codersdk.WorkspaceAgentLifecycle{
@ -1336,8 +1263,11 @@ func TestAgent_Lifecycle(t *testing.T) {
t.Parallel()
_, client, _, _, _ := setupAgent(t, agentsdk.Manifest{
StartupScript: "true",
StartupScriptTimeout: 30 * time.Second,
Scripts: []codersdk.WorkspaceAgentScript{{
Script: "true",
Timeout: 30 * time.Second,
RunOnStart: true,
}},
}, 0)
want := []codersdk.WorkspaceAgentLifecycle{
@ -1358,8 +1288,11 @@ func TestAgent_Lifecycle(t *testing.T) {
t.Parallel()
_, client, _, _, closer := setupAgent(t, agentsdk.Manifest{
ShutdownScript: "sleep 3",
StartupScriptTimeout: 30 * time.Second,
Scripts: []codersdk.WorkspaceAgentScript{{
Script: "sleep 3",
Timeout: 30 * time.Second,
RunOnStop: true,
}},
}, 0)
assert.Eventually(t, func() bool {
@ -1396,8 +1329,11 @@ func TestAgent_Lifecycle(t *testing.T) {
t.Parallel()
_, client, _, _, closer := setupAgent(t, agentsdk.Manifest{
ShutdownScript: "sleep 3",
ShutdownScriptTimeout: time.Nanosecond,
Scripts: []codersdk.WorkspaceAgentScript{{
Script: "sleep 3",
Timeout: time.Millisecond,
RunOnStop: true,
}},
}, 0)
assert.Eventually(t, func() bool {
@ -1435,8 +1371,11 @@ func TestAgent_Lifecycle(t *testing.T) {
t.Parallel()
_, client, _, _, closer := setupAgent(t, agentsdk.Manifest{
ShutdownScript: "false",
ShutdownScriptTimeout: 30 * time.Second,
Scripts: []codersdk.WorkspaceAgentScript{{
Script: "false",
Timeout: 30 * time.Second,
RunOnStop: true,
}},
}, 0)
assert.Eventually(t, func() bool {
@ -1480,9 +1419,16 @@ func TestAgent_Lifecycle(t *testing.T) {
logger,
uuid.New(),
agentsdk.Manifest{
DERPMap: derpMap,
StartupScript: "echo 1",
ShutdownScript: "echo " + expected,
DERPMap: derpMap,
Scripts: []codersdk.WorkspaceAgentScript{{
LogPath: "coder-startup-script.log",
Script: "echo 1",
RunOnStart: true,
}, {
LogPath: "coder-shutdown-script.log",
Script: "echo " + expected,
RunOnStop: true,
}},
},
make(chan *agentsdk.Stats, 50),
tailnet.NewCoordinator(logger),
@ -1533,9 +1479,7 @@ func TestAgent_Startup(t *testing.T) {
t.Parallel()
_, client, _, _, _ := setupAgent(t, agentsdk.Manifest{
StartupScript: "true",
StartupScriptTimeout: 30 * time.Second,
Directory: "",
Directory: "",
}, 0)
assert.Eventually(t, func() bool {
return client.GetStartup().Version != ""
@ -1547,9 +1491,7 @@ func TestAgent_Startup(t *testing.T) {
t.Parallel()
_, client, _, _, _ := setupAgent(t, agentsdk.Manifest{
StartupScript: "true",
StartupScriptTimeout: 30 * time.Second,
Directory: "~",
Directory: "~",
}, 0)
assert.Eventually(t, func() bool {
return client.GetStartup().Version != ""
@ -1563,9 +1505,7 @@ func TestAgent_Startup(t *testing.T) {
t.Parallel()
_, client, _, _, _ := setupAgent(t, agentsdk.Manifest{
StartupScript: "true",
StartupScriptTimeout: 30 * time.Second,
Directory: "coder/coder",
Directory: "coder/coder",
}, 0)
assert.Eventually(t, func() bool {
return client.GetStartup().Version != ""
@ -1579,9 +1519,7 @@ func TestAgent_Startup(t *testing.T) {
t.Parallel()
_, client, _, _, _ := setupAgent(t, agentsdk.Manifest{
StartupScript: "true",
StartupScriptTimeout: 30 * time.Second,
Directory: "$HOME",
Directory: "$HOME",
}, 0)
assert.Eventually(t, func() bool {
return client.GetStartup().Version != ""

View File

@ -0,0 +1,269 @@
package agentscripts
import (
"context"
"errors"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"sync"
"sync/atomic"
"time"
"github.com/robfig/cron/v3"
"github.com/spf13/afero"
"golang.org/x/sync/errgroup"
"golang.org/x/xerrors"
"cdr.dev/slog"
"github.com/coder/coder/v2/agent/agentssh"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/agentsdk"
)
var (
// ErrTimeout is returned when a script times out.
ErrTimeout = xerrors.New("script timed out")
parser = cron.NewParser(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.DowOptional)
)
// Options are a set of options for the runner.
type Options struct {
LogDir string
Logger slog.Logger
SSHServer *agentssh.Server
Filesystem afero.Fs
PatchLogs func(ctx context.Context, req agentsdk.PatchLogs) error
}
// New creates a runner for the provided scripts.
func New(opts Options) *Runner {
cronCtx, cronCtxCancel := context.WithCancel(context.Background())
return &Runner{
Options: opts,
cronCtx: cronCtx,
cronCtxCancel: cronCtxCancel,
cron: cron.New(cron.WithParser(parser)),
closed: make(chan struct{}),
}
}
type Runner struct {
Options
cronCtx context.Context
cronCtxCancel context.CancelFunc
cmdCloseWait sync.WaitGroup
closed chan struct{}
closeMutex sync.Mutex
cron *cron.Cron
initialized atomic.Bool
scripts []codersdk.WorkspaceAgentScript
}
// Init initializes the runner with the provided scripts.
// It also schedules any scripts that have a schedule.
// This function must be called before Execute.
func (r *Runner) Init(scripts []codersdk.WorkspaceAgentScript) error {
if r.initialized.Load() {
return xerrors.New("init: already initialized")
}
r.initialized.Store(true)
r.scripts = scripts
r.Logger.Info(r.cronCtx, "initializing agent scripts", slog.F("script_count", len(scripts)), slog.F("log_dir", r.LogDir))
for _, script := range scripts {
if script.Cron == "" {
continue
}
script := script
_, err := r.cron.AddFunc(script.Cron, func() {
err := r.run(r.cronCtx, script)
if err != nil {
r.Logger.Warn(context.Background(), "run agent script on schedule", slog.Error(err))
}
})
if err != nil {
return xerrors.Errorf("add schedule: %w", err)
}
}
return nil
}
// StartCron starts the cron scheduler.
// This is done async to allow for the caller to execute scripts prior.
func (r *Runner) StartCron() {
r.cron.Start()
}
// Execute runs a set of scripts according to a filter.
func (r *Runner) Execute(ctx context.Context, filter func(script codersdk.WorkspaceAgentScript) bool) error {
if filter == nil {
// Execute em' all!
filter = func(script codersdk.WorkspaceAgentScript) bool {
return true
}
}
var eg errgroup.Group
for _, script := range r.scripts {
if !filter(script) {
continue
}
script := script
eg.Go(func() error {
err := r.run(ctx, script)
if err != nil {
return xerrors.Errorf("run agent script %q: %w", script.LogSourceID, err)
}
return nil
})
}
return eg.Wait()
}
// run executes the provided script with the timeout.
// If the timeout is exceeded, the process is sent an interrupt signal.
// If the process does not exit after a few seconds, it is forcefully killed.
// This function immediately returns after a timeout, and does not wait for the process to exit.
func (r *Runner) run(ctx context.Context, script codersdk.WorkspaceAgentScript) error {
logPath := script.LogPath
if logPath == "" {
logPath = fmt.Sprintf("coder-script-%s.log", script.LogSourceID)
}
if !filepath.IsAbs(logPath) {
logPath = filepath.Join(r.LogDir, logPath)
}
logger := r.Logger.With(slog.F("log_path", logPath))
logger.Info(ctx, "running agent script", slog.F("script", script.Script))
fileWriter, err := r.Filesystem.OpenFile(logPath, os.O_CREATE|os.O_RDWR, 0o600)
if err != nil {
return xerrors.Errorf("open %s script log file: %w", logPath, err)
}
defer func() {
err := fileWriter.Close()
if err != nil {
logger.Warn(ctx, fmt.Sprintf("close %s script log file", logPath), slog.Error(err))
}
}()
var cmd *exec.Cmd
cmdCtx := ctx
if script.Timeout > 0 {
var ctxCancel context.CancelFunc
cmdCtx, ctxCancel = context.WithTimeout(ctx, script.Timeout)
defer ctxCancel()
}
cmdPty, err := r.SSHServer.CreateCommand(cmdCtx, script.Script, nil)
if err != nil {
return xerrors.Errorf("%s script: create command: %w", logPath, err)
}
cmd = cmdPty.AsExec()
cmd.SysProcAttr = cmdSysProcAttr()
cmd.WaitDelay = 10 * time.Second
cmd.Cancel = cmdCancel(cmd)
send, flushAndClose := agentsdk.LogsSender(script.LogSourceID, r.PatchLogs, logger)
// If ctx is canceled here (or in a writer below), we may be
// discarding logs, but that's okay because we're shutting down
// anyway. We could consider creating a new context here if we
// want better control over flush during shutdown.
defer func() {
if err := flushAndClose(ctx); err != nil {
logger.Warn(ctx, "flush startup logs failed", slog.Error(err))
}
}()
infoW := agentsdk.LogsWriter(ctx, send, script.LogSourceID, codersdk.LogLevelInfo)
defer infoW.Close()
errW := agentsdk.LogsWriter(ctx, send, script.LogSourceID, codersdk.LogLevelError)
defer errW.Close()
cmd.Stdout = io.MultiWriter(fileWriter, infoW)
cmd.Stderr = io.MultiWriter(fileWriter, errW)
start := time.Now()
defer func() {
end := time.Now()
execTime := end.Sub(start)
exitCode := 0
if err != nil {
exitCode = 255 // Unknown status.
var exitError *exec.ExitError
if xerrors.As(err, &exitError) {
exitCode = exitError.ExitCode()
}
logger.Warn(ctx, fmt.Sprintf("%s script failed", logPath), slog.F("execution_time", execTime), slog.F("exit_code", exitCode), slog.Error(err))
} else {
logger.Info(ctx, fmt.Sprintf("%s script completed", logPath), slog.F("execution_time", execTime), slog.F("exit_code", exitCode))
}
}()
err = cmd.Start()
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
return ErrTimeout
}
return xerrors.Errorf("%s script: start command: %w", logPath, err)
}
cmdDone := make(chan error, 1)
err = r.trackCommandGoroutine(func() {
cmdDone <- cmd.Wait()
})
if err != nil {
return xerrors.Errorf("%s script: track command goroutine: %w", logPath, err)
}
select {
case <-cmdCtx.Done():
// Wait for the command to drain!
select {
case <-cmdDone:
case <-time.After(10 * time.Second):
}
err = cmdCtx.Err()
case err = <-cmdDone:
}
if errors.Is(err, context.DeadlineExceeded) {
err = ErrTimeout
}
return err
}
func (r *Runner) Close() error {
r.closeMutex.Lock()
defer r.closeMutex.Unlock()
if r.isClosed() {
return nil
}
close(r.closed)
r.cronCtxCancel()
r.cron.Stop()
r.cmdCloseWait.Wait()
return nil
}
func (r *Runner) trackCommandGoroutine(fn func()) error {
r.closeMutex.Lock()
defer r.closeMutex.Unlock()
if r.isClosed() {
return xerrors.New("track command goroutine: closed")
}
r.cmdCloseWait.Add(1)
go func() {
defer r.cmdCloseWait.Done()
fn()
}()
return nil
}
func (r *Runner) isClosed() bool {
select {
case <-r.closed:
return true
default:
return false
}
}

View File

@ -0,0 +1,20 @@
//go:build !windows
package agentscripts
import (
"os/exec"
"syscall"
)
func cmdSysProcAttr() *syscall.SysProcAttr {
return &syscall.SysProcAttr{
Setsid: true,
}
}
func cmdCancel(cmd *exec.Cmd) func() error {
return func() error {
return syscall.Kill(-cmd.Process.Pid, syscall.SIGHUP)
}
}

View File

@ -0,0 +1,80 @@
package agentscripts_test
import (
"context"
"testing"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/spf13/afero"
"github.com/stretchr/testify/require"
"go.uber.org/atomic"
"go.uber.org/goleak"
"cdr.dev/slog/sloggers/slogtest"
"github.com/coder/coder/v2/agent/agentscripts"
"github.com/coder/coder/v2/agent/agentssh"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/agentsdk"
)
func TestMain(m *testing.M) {
goleak.VerifyTestMain(m)
}
func TestExecuteBasic(t *testing.T) {
t.Parallel()
logs := make(chan agentsdk.PatchLogs, 1)
runner := setup(t, func(ctx context.Context, req agentsdk.PatchLogs) error {
logs <- req
return nil
})
defer runner.Close()
err := runner.Init([]codersdk.WorkspaceAgentScript{{
Script: "echo hello",
}})
require.NoError(t, err)
require.NoError(t, runner.Execute(context.Background(), func(script codersdk.WorkspaceAgentScript) bool {
return true
}))
log := <-logs
require.Equal(t, "hello", log.Logs[0].Output)
}
func TestTimeout(t *testing.T) {
t.Parallel()
runner := setup(t, nil)
defer runner.Close()
err := runner.Init([]codersdk.WorkspaceAgentScript{{
Script: "sleep infinity",
Timeout: time.Millisecond,
}})
require.NoError(t, err)
require.ErrorIs(t, runner.Execute(context.Background(), nil), agentscripts.ErrTimeout)
}
func setup(t *testing.T, patchLogs func(ctx context.Context, req agentsdk.PatchLogs) error) *agentscripts.Runner {
t.Helper()
if patchLogs == nil {
// noop
patchLogs = func(ctx context.Context, req agentsdk.PatchLogs) error {
return nil
}
}
fs := afero.NewMemMapFs()
logger := slogtest.Make(t, nil)
s, err := agentssh.NewServer(context.Background(), logger, prometheus.NewRegistry(), fs, 0, "")
require.NoError(t, err)
s.AgentToken = func() string { return "" }
s.Manifest = atomic.NewPointer(&agentsdk.Manifest{})
t.Cleanup(func() {
_ = s.Close()
})
return agentscripts.New(agentscripts.Options{
LogDir: t.TempDir(),
Logger: logger,
SSHServer: s,
Filesystem: fs,
PatchLogs: patchLogs,
})
}

View File

@ -0,0 +1,17 @@
package agentscripts
import (
"os"
"os/exec"
"syscall"
)
func cmdSysProcAttr() *syscall.SysProcAttr {
return &syscall.SysProcAttr{}
}
func cmdCancel(cmd *exec.Cmd) func() error {
return func() error {
return cmd.Process.Signal(os.Interrupt)
}
}

View File

@ -80,6 +80,10 @@ func Agent(ctx context.Context, writer io.Writer, agentID uuid.UUID, opts AgentO
if err != nil {
return xerrors.Errorf("fetch: %w", err)
}
logSources := map[uuid.UUID]codersdk.WorkspaceAgentLogSource{}
for _, source := range agent.LogSources {
logSources[source.ID] = source
}
sw := &stageWriter{w: writer}
@ -123,7 +127,7 @@ func Agent(ctx context.Context, writer io.Writer, agentID uuid.UUID, opts AgentO
return nil
}
stage := "Running workspace agent startup script"
stage := "Running workspace agent startup scripts"
follow := opts.Wait
if !follow {
stage += " (non-blocking)"
@ -173,7 +177,12 @@ func Agent(ctx context.Context, writer io.Writer, agentID uuid.UUID, opts AgentO
return nil
}
for _, log := range logs {
sw.Log(log.CreatedAt, log.Level, log.Output)
source, hasSource := logSources[log.SourceID]
output := log.Output
if hasSource && source.DisplayName != "" {
output = source.DisplayName + ": " + output
}
sw.Log(log.CreatedAt, log.Level, output)
lastLog = log
}
}
@ -192,16 +201,19 @@ func Agent(ctx context.Context, writer io.Writer, agentID uuid.UUID, opts AgentO
switch agent.LifecycleState {
case codersdk.WorkspaceAgentLifecycleReady:
sw.Complete(stage, agent.ReadyAt.Sub(*agent.StartedAt))
case codersdk.WorkspaceAgentLifecycleStartTimeout:
sw.Fail(stage, 0)
sw.Log(time.Time{}, codersdk.LogLevelWarn, "Warning: A startup script timed out and your workspace may be incomplete.")
case codersdk.WorkspaceAgentLifecycleStartError:
sw.Fail(stage, agent.ReadyAt.Sub(*agent.StartedAt))
// Use zero time (omitted) to separate these from the startup logs.
sw.Log(time.Time{}, codersdk.LogLevelWarn, "Warning: The startup script exited with an error and your workspace may be incomplete.")
sw.Log(time.Time{}, codersdk.LogLevelWarn, "Warning: A startup script exited with an error and your workspace may be incomplete.")
sw.Log(time.Time{}, codersdk.LogLevelWarn, troubleshootingMessage(agent, "https://coder.com/docs/v2/latest/templates#startup-script-exited-with-an-error"))
default:
switch {
case agent.LifecycleState.Starting():
// Use zero time (omitted) to separate these from the startup logs.
sw.Log(time.Time{}, codersdk.LogLevelWarn, "Notice: The startup script is still running and your workspace may be incomplete.")
sw.Log(time.Time{}, codersdk.LogLevelWarn, "Notice: The startup scripts are still running and your workspace may be incomplete.")
sw.Log(time.Time{}, codersdk.LogLevelWarn, troubleshootingMessage(agent, "https://coder.com/docs/v2/latest/templates#your-workspace-may-be-incomplete"))
// Note: We don't complete or fail the stage here, it's
// intentionally left open to indicate this stage didn't

View File

@ -52,11 +52,36 @@ func TestAgent(t *testing.T) {
want: []string{
"⧗ Waiting for the workspace agent to connect",
"✔ Waiting for the workspace agent to connect",
"⧗ Running workspace agent startup script (non-blocking)",
"Notice: The startup script is still running and your workspace may be incomplete.",
"⧗ Running workspace agent startup scripts (non-blocking)",
"Notice: The startup scripts are still running and your workspace may be incomplete.",
"For more information and troubleshooting, see",
},
},
{
name: "Start timeout",
opts: cliui.AgentOptions{
FetchInterval: time.Millisecond,
},
iter: []func(context.Context, *codersdk.WorkspaceAgent, chan []codersdk.WorkspaceAgentLog) error{
func(_ context.Context, agent *codersdk.WorkspaceAgent, _ chan []codersdk.WorkspaceAgentLog) error {
agent.Status = codersdk.WorkspaceAgentConnecting
return nil
},
func(_ context.Context, agent *codersdk.WorkspaceAgent, logs chan []codersdk.WorkspaceAgentLog) error {
agent.Status = codersdk.WorkspaceAgentConnected
agent.LifecycleState = codersdk.WorkspaceAgentLifecycleStartTimeout
agent.FirstConnectedAt = ptr.Ref(time.Now())
return nil
},
},
want: []string{
"⧗ Waiting for the workspace agent to connect",
"✔ Waiting for the workspace agent to connect",
"⧗ Running workspace agent startup scripts (non-blocking)",
"✘ Running workspace agent startup scripts (non-blocking)",
"Warning: A startup script timed out and your workspace may be incomplete.",
},
},
{
name: "Initial connection timeout",
opts: cliui.AgentOptions{
@ -86,8 +111,8 @@ func TestAgent(t *testing.T) {
"The workspace agent is having trouble connecting, wait for it to connect or restart your workspace.",
"For more information and troubleshooting, see",
"✔ Waiting for the workspace agent to connect",
"⧗ Running workspace agent startup script (non-blocking)",
"✔ Running workspace agent startup script (non-blocking)",
"⧗ Running workspace agent startup scripts (non-blocking)",
"✔ Running workspace agent startup scripts (non-blocking)",
},
},
{
@ -120,7 +145,7 @@ func TestAgent(t *testing.T) {
},
},
{
name: "Startup script logs",
name: "Startup Logs",
opts: cliui.AgentOptions{
FetchInterval: time.Millisecond,
Wait: true,
@ -131,10 +156,15 @@ func TestAgent(t *testing.T) {
agent.FirstConnectedAt = ptr.Ref(time.Now())
agent.LifecycleState = codersdk.WorkspaceAgentLifecycleStarting
agent.StartedAt = ptr.Ref(time.Now())
agent.LogSources = []codersdk.WorkspaceAgentLogSource{{
ID: uuid.Nil,
DisplayName: "testing",
}}
logs <- []codersdk.WorkspaceAgentLog{
{
CreatedAt: time.Now(),
Output: "Hello world",
SourceID: uuid.Nil,
},
}
return nil
@ -152,10 +182,10 @@ func TestAgent(t *testing.T) {
},
},
want: []string{
"⧗ Running workspace agent startup script",
"Hello world",
"⧗ Running workspace agent startup scripts",
"testing: Hello world",
"Bye now",
"✔ Running workspace agent startup script",
"✔ Running workspace agent startup scripts",
},
},
{
@ -181,10 +211,10 @@ func TestAgent(t *testing.T) {
},
},
want: []string{
"⧗ Running workspace agent startup script",
"⧗ Running workspace agent startup scripts",
"Hello world",
"✘ Running workspace agent startup script",
"Warning: The startup script exited with an error and your workspace may be incomplete.",
"✘ Running workspace agent startup scripts",
"Warning: A startup script exited with an error and your workspace may be incomplete.",
"For more information and troubleshooting, see",
},
},
@ -229,9 +259,9 @@ func TestAgent(t *testing.T) {
},
},
want: []string{
"⧗ Running workspace agent startup script",
"⧗ Running workspace agent startup scripts",
"Hello world",
"✔ Running workspace agent startup script",
"✔ Running workspace agent startup scripts",
},
wantErr: true,
},
@ -288,11 +318,10 @@ func TestAgent(t *testing.T) {
var buf bytes.Buffer
agent := codersdk.WorkspaceAgent{
ID: uuid.New(),
Status: codersdk.WorkspaceAgentConnecting,
StartupScriptBehavior: codersdk.WorkspaceAgentStartupScriptBehaviorNonBlocking,
CreatedAt: time.Now(),
LifecycleState: codersdk.WorkspaceAgentLifecycleCreated,
ID: uuid.New(),
Status: codersdk.WorkspaceAgentConnecting,
CreatedAt: time.Now(),
LifecycleState: codersdk.WorkspaceAgentLifecycleCreated,
}
logs := make(chan []codersdk.WorkspaceAgentLog, 1)
@ -340,6 +369,9 @@ func TestAgent(t *testing.T) {
line := s.Text()
t.Log(line)
if len(tc.want) == 0 {
for i := 0; i < 5; i++ {
t.Log(line)
}
require.Fail(t, "unexpected line", line)
}
require.Contains(t, line, tc.want[0])

View File

@ -143,13 +143,11 @@ func (r *RootCmd) ssh() *clibase.Cmd {
case "no":
wait = false
case "auto":
switch workspaceAgent.StartupScriptBehavior {
case codersdk.WorkspaceAgentStartupScriptBehaviorBlocking:
wait = true
case codersdk.WorkspaceAgentStartupScriptBehaviorNonBlocking:
wait = false
default:
return xerrors.Errorf("unknown startup script behavior %q", workspaceAgent.StartupScriptBehavior)
for _, script := range workspaceAgent.Scripts {
if script.StartBlocksLogin {
wait = true
break
}
}
default:
return xerrors.Errorf("unknown wait value %q", waitEnum)

126
coderd/apidoc/docs.go generated
View File

@ -6398,9 +6398,6 @@ const docTemplate = `{
},
"output": {
"type": "string"
},
"source": {
"$ref": "#/definitions/codersdk.WorkspaceAgentLogSource"
}
}
},
@ -6447,17 +6444,11 @@ const docTemplate = `{
"motd_file": {
"type": "string"
},
"shutdown_script": {
"type": "string"
},
"shutdown_script_timeout": {
"type": "integer"
},
"startup_script": {
"type": "string"
},
"startup_script_timeout": {
"type": "integer"
"scripts": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.WorkspaceAgentScript"
}
},
"vscode_port_proxy_uri": {
"type": "string"
@ -6467,6 +6458,9 @@ const docTemplate = `{
"agentsdk.PatchLogs": {
"type": "object",
"properties": {
"log_source_id": {
"type": "string"
},
"logs": {
"type": "array",
"items": {
@ -10694,9 +10688,11 @@ const docTemplate = `{
"lifecycle_state": {
"$ref": "#/definitions/codersdk.WorkspaceAgentLifecycle"
},
"login_before_ready": {
"description": "Deprecated: Use StartupScriptBehavior instead.",
"type": "boolean"
"log_sources": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.WorkspaceAgentLogSource"
}
},
"logs_length": {
"type": "integer"
@ -10718,25 +10714,23 @@ const docTemplate = `{
"type": "string",
"format": "uuid"
},
"shutdown_script": {
"type": "string"
},
"shutdown_script_timeout_seconds": {
"type": "integer"
"scripts": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.WorkspaceAgentScript"
}
},
"started_at": {
"type": "string",
"format": "date-time"
},
"startup_script": {
"type": "string"
},
"startup_script_behavior": {
"$ref": "#/definitions/codersdk.WorkspaceAgentStartupScriptBehavior"
},
"startup_script_timeout_seconds": {
"description": "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.",
"type": "integer"
"description": "StartupScriptBehavior is a legacy field that is deprecated in favor\nof the ` + "`" + `coder_script` + "`" + ` resource. It's only referenced by old clients.\nDeprecated: Remove in the future!",
"allOf": [
{
"$ref": "#/definitions/codersdk.WorkspaceAgentStartupScriptBehavior"
}
]
},
"status": {
"$ref": "#/definitions/codersdk.WorkspaceAgentStatus"
@ -10856,27 +10850,35 @@ const docTemplate = `{
},
"output": {
"type": "string"
},
"source_id": {
"type": "string",
"format": "uuid"
}
}
},
"codersdk.WorkspaceAgentLogSource": {
"type": "string",
"enum": [
"startup_script",
"shutdown_script",
"kubernetes",
"envbox",
"envbuilder",
"external"
],
"x-enum-varnames": [
"WorkspaceAgentLogSourceStartupScript",
"WorkspaceAgentLogSourceShutdownScript",
"WorkspaceAgentLogSourceKubernetes",
"WorkspaceAgentLogSourceEnvbox",
"WorkspaceAgentLogSourceEnvbuilder",
"WorkspaceAgentLogSourceExternal"
]
"type": "object",
"properties": {
"created_at": {
"type": "string",
"format": "date-time"
},
"display_name": {
"type": "string"
},
"icon": {
"type": "string"
},
"id": {
"type": "string",
"format": "uuid"
},
"workspace_agent_id": {
"type": "string",
"format": "uuid"
}
}
},
"codersdk.WorkspaceAgentMetadataDescription": {
"type": "object",
@ -10898,6 +10900,36 @@ const docTemplate = `{
}
}
},
"codersdk.WorkspaceAgentScript": {
"type": "object",
"properties": {
"cron": {
"type": "string"
},
"log_path": {
"type": "string"
},
"log_source_id": {
"type": "string",
"format": "uuid"
},
"run_on_start": {
"type": "boolean"
},
"run_on_stop": {
"type": "boolean"
},
"script": {
"type": "string"
},
"start_blocks_login": {
"type": "boolean"
},
"timeout": {
"type": "integer"
}
}
},
"codersdk.WorkspaceAgentStartupScriptBehavior": {
"type": "string",
"enum": [

View File

@ -5638,9 +5638,6 @@
},
"output": {
"type": "string"
},
"source": {
"$ref": "#/definitions/codersdk.WorkspaceAgentLogSource"
}
}
},
@ -5687,17 +5684,11 @@
"motd_file": {
"type": "string"
},
"shutdown_script": {
"type": "string"
},
"shutdown_script_timeout": {
"type": "integer"
},
"startup_script": {
"type": "string"
},
"startup_script_timeout": {
"type": "integer"
"scripts": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.WorkspaceAgentScript"
}
},
"vscode_port_proxy_uri": {
"type": "string"
@ -5707,6 +5698,9 @@
"agentsdk.PatchLogs": {
"type": "object",
"properties": {
"log_source_id": {
"type": "string"
},
"logs": {
"type": "array",
"items": {
@ -9698,9 +9692,11 @@
"lifecycle_state": {
"$ref": "#/definitions/codersdk.WorkspaceAgentLifecycle"
},
"login_before_ready": {
"description": "Deprecated: Use StartupScriptBehavior instead.",
"type": "boolean"
"log_sources": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.WorkspaceAgentLogSource"
}
},
"logs_length": {
"type": "integer"
@ -9722,25 +9718,23 @@
"type": "string",
"format": "uuid"
},
"shutdown_script": {
"type": "string"
},
"shutdown_script_timeout_seconds": {
"type": "integer"
"scripts": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.WorkspaceAgentScript"
}
},
"started_at": {
"type": "string",
"format": "date-time"
},
"startup_script": {
"type": "string"
},
"startup_script_behavior": {
"$ref": "#/definitions/codersdk.WorkspaceAgentStartupScriptBehavior"
},
"startup_script_timeout_seconds": {
"description": "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.",
"type": "integer"
"description": "StartupScriptBehavior is a legacy field that is deprecated in favor\nof the `coder_script` resource. It's only referenced by old clients.\nDeprecated: Remove in the future!",
"allOf": [
{
"$ref": "#/definitions/codersdk.WorkspaceAgentStartupScriptBehavior"
}
]
},
"status": {
"$ref": "#/definitions/codersdk.WorkspaceAgentStatus"
@ -9860,27 +9854,35 @@
},
"output": {
"type": "string"
},
"source_id": {
"type": "string",
"format": "uuid"
}
}
},
"codersdk.WorkspaceAgentLogSource": {
"type": "string",
"enum": [
"startup_script",
"shutdown_script",
"kubernetes",
"envbox",
"envbuilder",
"external"
],
"x-enum-varnames": [
"WorkspaceAgentLogSourceStartupScript",
"WorkspaceAgentLogSourceShutdownScript",
"WorkspaceAgentLogSourceKubernetes",
"WorkspaceAgentLogSourceEnvbox",
"WorkspaceAgentLogSourceEnvbuilder",
"WorkspaceAgentLogSourceExternal"
]
"type": "object",
"properties": {
"created_at": {
"type": "string",
"format": "date-time"
},
"display_name": {
"type": "string"
},
"icon": {
"type": "string"
},
"id": {
"type": "string",
"format": "uuid"
},
"workspace_agent_id": {
"type": "string",
"format": "uuid"
}
}
},
"codersdk.WorkspaceAgentMetadataDescription": {
"type": "object",
@ -9902,6 +9904,36 @@
}
}
},
"codersdk.WorkspaceAgentScript": {
"type": "object",
"properties": {
"cron": {
"type": "string"
},
"log_path": {
"type": "string"
},
"log_source_id": {
"type": "string",
"format": "uuid"
},
"run_on_start": {
"type": "boolean"
},
"run_on_stop": {
"type": "boolean"
},
"script": {
"type": "string"
},
"start_blocks_login": {
"type": "boolean"
},
"timeout": {
"type": "integer"
}
}
},
"codersdk.WorkspaceAgentStartupScriptBehavior": {
"type": "string",
"enum": ["blocking", "non-blocking"],

View File

@ -1593,6 +1593,13 @@ func (q *querier) GetWorkspaceAgentLifecycleStateByID(ctx context.Context, id uu
return q.db.GetWorkspaceAgentLifecycleStateByID(ctx, id)
}
func (q *querier) GetWorkspaceAgentLogSourcesByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]database.WorkspaceAgentLogSource, error) {
if err := q.authorizeContext(ctx, rbac.ActionRead, rbac.ResourceSystem); err != nil {
return nil, err
}
return q.db.GetWorkspaceAgentLogSourcesByAgentIDs(ctx, ids)
}
func (q *querier) GetWorkspaceAgentLogsAfter(ctx context.Context, arg database.GetWorkspaceAgentLogsAfterParams) ([]database.WorkspaceAgentLog, error) {
_, err := q.GetWorkspaceAgentByID(ctx, arg.AgentID)
if err != nil {
@ -1615,6 +1622,13 @@ func (q *querier) GetWorkspaceAgentMetadata(ctx context.Context, workspaceAgentI
return q.db.GetWorkspaceAgentMetadata(ctx, workspaceAgentID)
}
func (q *querier) GetWorkspaceAgentScriptsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]database.WorkspaceAgentScript, error) {
if err := q.authorizeContext(ctx, rbac.ActionRead, rbac.ResourceSystem); err != nil {
return nil, err
}
return q.db.GetWorkspaceAgentScriptsByAgentIDs(ctx, ids)
}
func (q *querier) GetWorkspaceAgentStats(ctx context.Context, createdAfter time.Time) ([]database.GetWorkspaceAgentStatsRow, error) {
return q.db.GetWorkspaceAgentStats(ctx, createdAfter)
}
@ -2080,8 +2094,6 @@ func (q *querier) InsertWorkspace(ctx context.Context, arg database.InsertWorksp
return insert(q.log, q.auth, obj, q.db.InsertWorkspace)(ctx, arg)
}
// Provisionerd server functions
func (q *querier) InsertWorkspaceAgent(ctx context.Context, arg database.InsertWorkspaceAgentParams) (database.WorkspaceAgent, error) {
if err := q.authorizeContext(ctx, rbac.ActionCreate, rbac.ResourceSystem); err != nil {
return database.WorkspaceAgent{}, err
@ -2089,6 +2101,10 @@ func (q *querier) InsertWorkspaceAgent(ctx context.Context, arg database.InsertW
return q.db.InsertWorkspaceAgent(ctx, arg)
}
func (q *querier) InsertWorkspaceAgentLogSources(ctx context.Context, arg database.InsertWorkspaceAgentLogSourcesParams) ([]database.WorkspaceAgentLogSource, error) {
return q.db.InsertWorkspaceAgentLogSources(ctx, arg)
}
func (q *querier) InsertWorkspaceAgentLogs(ctx context.Context, arg database.InsertWorkspaceAgentLogsParams) ([]database.WorkspaceAgentLog, error) {
return q.db.InsertWorkspaceAgentLogs(ctx, arg)
}
@ -2103,6 +2119,13 @@ func (q *querier) InsertWorkspaceAgentMetadata(ctx context.Context, arg database
return q.db.InsertWorkspaceAgentMetadata(ctx, arg)
}
func (q *querier) InsertWorkspaceAgentScripts(ctx context.Context, arg database.InsertWorkspaceAgentScriptsParams) ([]database.WorkspaceAgentScript, error) {
if err := q.authorizeContext(ctx, rbac.ActionCreate, rbac.ResourceSystem); err != nil {
return []database.WorkspaceAgentScript{}, err
}
return q.db.InsertWorkspaceAgentScripts(ctx, arg)
}
func (q *querier) InsertWorkspaceAgentStat(ctx context.Context, arg database.InsertWorkspaceAgentStatParams) (database.WorkspaceAgentStat, error) {
// TODO: This is a workspace agent operation. Should users be able to query this?
// Not really sure what this is for.

View File

@ -1494,8 +1494,7 @@ func (s *MethodTestSuite) TestSystemFunctions() {
}))
s.Run("InsertWorkspaceAgent", s.Subtest(func(db database.Store, check *expects) {
check.Args(database.InsertWorkspaceAgentParams{
ID: uuid.New(),
StartupScriptBehavior: database.StartupScriptBehaviorNonBlocking,
ID: uuid.New(),
}).Asserts(rbac.ResourceSystem, rbac.ActionCreate)
}))
s.Run("InsertWorkspaceApp", s.Subtest(func(db database.Store, check *expects) {

View File

@ -142,6 +142,8 @@ type data struct {
workspaceAgents []database.WorkspaceAgent
workspaceAgentMetadata []database.WorkspaceAgentMetadatum
workspaceAgentLogs []database.WorkspaceAgentLog
workspaceAgentLogSources []database.WorkspaceAgentLogSource
workspaceAgentScripts []database.WorkspaceAgentScript
workspaceApps []database.WorkspaceApp
workspaceAppStatsLastInsertID int64
workspaceAppStats []database.WorkspaceAppStat
@ -3213,6 +3215,22 @@ func (q *FakeQuerier) GetWorkspaceAgentLifecycleStateByID(ctx context.Context, i
}, nil
}
func (q *FakeQuerier) GetWorkspaceAgentLogSourcesByAgentIDs(_ context.Context, ids []uuid.UUID) ([]database.WorkspaceAgentLogSource, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
logSources := make([]database.WorkspaceAgentLogSource, 0)
for _, logSource := range q.workspaceAgentLogSources {
for _, id := range ids {
if logSource.WorkspaceAgentID == id {
logSources = append(logSources, logSource)
break
}
}
}
return logSources, nil
}
func (q *FakeQuerier) GetWorkspaceAgentLogsAfter(_ context.Context, arg database.GetWorkspaceAgentLogsAfterParams) ([]database.WorkspaceAgentLog, error) {
if err := validateDatabaseType(arg); err != nil {
return nil, err
@ -3247,6 +3265,22 @@ func (q *FakeQuerier) GetWorkspaceAgentMetadata(_ context.Context, workspaceAgen
return metadata, nil
}
func (q *FakeQuerier) GetWorkspaceAgentScriptsByAgentIDs(_ context.Context, ids []uuid.UUID) ([]database.WorkspaceAgentScript, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
scripts := make([]database.WorkspaceAgentScript, 0)
for _, script := range q.workspaceAgentScripts {
for _, id := range ids {
if script.WorkspaceAgentID == id {
scripts = append(scripts, script)
break
}
}
}
return scripts, nil
}
func (q *FakeQuerier) GetWorkspaceAgentStats(_ context.Context, createdAfter time.Time) ([]database.GetWorkspaceAgentStatsRow, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
@ -3468,9 +3502,6 @@ func (q *FakeQuerier) GetWorkspaceAppsByAgentID(_ context.Context, id uuid.UUID)
apps = append(apps, app)
}
}
if len(apps) == 0 {
return nil, sql.ErrNoRows
}
return apps, nil
}
@ -4588,15 +4619,12 @@ func (q *FakeQuerier) InsertWorkspaceAgent(_ context.Context, arg database.Inser
Architecture: arg.Architecture,
OperatingSystem: arg.OperatingSystem,
Directory: arg.Directory,
StartupScriptBehavior: arg.StartupScriptBehavior,
StartupScript: arg.StartupScript,
InstanceMetadata: arg.InstanceMetadata,
ResourceMetadata: arg.ResourceMetadata,
ConnectionTimeoutSeconds: arg.ConnectionTimeoutSeconds,
TroubleshootingURL: arg.TroubleshootingURL,
MOTDFile: arg.MOTDFile,
LifecycleState: database.WorkspaceAgentLifecycleStateCreated,
ShutdownScript: arg.ShutdownScript,
DisplayApps: arg.DisplayApps,
}
@ -4604,6 +4632,30 @@ func (q *FakeQuerier) InsertWorkspaceAgent(_ context.Context, arg database.Inser
return agent, nil
}
func (q *FakeQuerier) InsertWorkspaceAgentLogSources(_ context.Context, arg database.InsertWorkspaceAgentLogSourcesParams) ([]database.WorkspaceAgentLogSource, error) {
err := validateDatabaseType(arg)
if err != nil {
return nil, err
}
q.mutex.Lock()
defer q.mutex.Unlock()
logSources := make([]database.WorkspaceAgentLogSource, 0)
for index, source := range arg.ID {
logSource := database.WorkspaceAgentLogSource{
ID: source,
WorkspaceAgentID: arg.WorkspaceAgentID,
CreatedAt: arg.CreatedAt,
DisplayName: arg.DisplayName[index],
Icon: arg.Icon[index],
}
logSources = append(logSources, logSource)
}
q.workspaceAgentLogSources = append(q.workspaceAgentLogSources, logSources...)
return logSources, nil
}
func (q *FakeQuerier) InsertWorkspaceAgentLogs(_ context.Context, arg database.InsertWorkspaceAgentLogsParams) ([]database.WorkspaceAgentLog, error) {
if err := validateDatabaseType(arg); err != nil {
return nil, err
@ -4621,12 +4673,12 @@ func (q *FakeQuerier) InsertWorkspaceAgentLogs(_ context.Context, arg database.I
for index, output := range arg.Output {
id++
logs = append(logs, database.WorkspaceAgentLog{
ID: id,
AgentID: arg.AgentID,
CreatedAt: arg.CreatedAt[index],
Level: arg.Level[index],
Source: arg.Source[index],
Output: output,
ID: id,
AgentID: arg.AgentID,
CreatedAt: arg.CreatedAt,
Level: arg.Level[index],
LogSourceID: arg.LogSourceID,
Output: output,
})
outputLength += int32(len(output))
}
@ -4667,6 +4719,35 @@ func (q *FakeQuerier) InsertWorkspaceAgentMetadata(_ context.Context, arg databa
return nil
}
func (q *FakeQuerier) InsertWorkspaceAgentScripts(_ context.Context, arg database.InsertWorkspaceAgentScriptsParams) ([]database.WorkspaceAgentScript, error) {
err := validateDatabaseType(arg)
if err != nil {
return nil, err
}
q.mutex.Lock()
defer q.mutex.Unlock()
scripts := make([]database.WorkspaceAgentScript, 0)
for index, source := range arg.LogSourceID {
script := database.WorkspaceAgentScript{
LogSourceID: source,
WorkspaceAgentID: arg.WorkspaceAgentID,
LogPath: arg.LogPath[index],
Script: arg.Script[index],
Cron: arg.Cron[index],
StartBlocksLogin: arg.StartBlocksLogin[index],
RunOnStart: arg.RunOnStart[index],
RunOnStop: arg.RunOnStop[index],
TimeoutSeconds: arg.TimeoutSeconds[index],
CreatedAt: arg.CreatedAt,
}
scripts = append(scripts, script)
}
q.workspaceAgentScripts = append(q.workspaceAgentScripts, scripts...)
return scripts, nil
}
func (q *FakeQuerier) InsertWorkspaceAgentStat(_ context.Context, p database.InsertWorkspaceAgentStatParams) (database.WorkspaceAgentStat, error) {
if err := validateDatabaseType(p); err != nil {
return database.WorkspaceAgentStat{}, err

View File

@ -142,11 +142,7 @@ func WorkspaceAgent(t testing.TB, db database.Store, orig database.WorkspaceAgen
Valid: takeFirst(orig.EnvironmentVariables.Valid, false),
},
OperatingSystem: takeFirst(orig.OperatingSystem, "linux"),
StartupScript: sql.NullString{
String: takeFirst(orig.StartupScript.String, ""),
Valid: takeFirst(orig.StartupScript.Valid, false),
},
Directory: takeFirst(orig.Directory, ""),
Directory: takeFirst(orig.Directory, ""),
InstanceMetadata: pqtype.NullRawMessage{
RawMessage: takeFirstSlice(orig.ResourceMetadata.RawMessage, []byte("{}")),
Valid: takeFirst(orig.ResourceMetadata.Valid, false),
@ -155,11 +151,9 @@ func WorkspaceAgent(t testing.TB, db database.Store, orig database.WorkspaceAgen
RawMessage: takeFirstSlice(orig.ResourceMetadata.RawMessage, []byte("{}")),
Valid: takeFirst(orig.ResourceMetadata.Valid, false),
},
ConnectionTimeoutSeconds: takeFirst(orig.ConnectionTimeoutSeconds, 3600),
TroubleshootingURL: takeFirst(orig.TroubleshootingURL, "https://example.com"),
MOTDFile: takeFirst(orig.TroubleshootingURL, ""),
StartupScriptBehavior: takeFirst(orig.StartupScriptBehavior, "non-blocking"),
StartupScriptTimeoutSeconds: takeFirst(orig.StartupScriptTimeoutSeconds, 3600),
ConnectionTimeoutSeconds: takeFirst(orig.ConnectionTimeoutSeconds, 3600),
TroubleshootingURL: takeFirst(orig.TroubleshootingURL, "https://example.com"),
MOTDFile: takeFirst(orig.TroubleshootingURL, ""),
})
require.NoError(t, err, "insert workspace agent")
return workspace
@ -182,6 +176,18 @@ func Workspace(t testing.TB, db database.Store, orig database.Workspace) databas
return workspace
}
func WorkspaceAgentLogSource(t testing.TB, db database.Store, orig database.WorkspaceAgentLogSource) database.WorkspaceAgentLogSource {
sources, err := db.InsertWorkspaceAgentLogSources(genCtx, database.InsertWorkspaceAgentLogSourcesParams{
WorkspaceAgentID: takeFirst(orig.WorkspaceAgentID, uuid.New()),
ID: []uuid.UUID{takeFirst(orig.ID, uuid.New())},
CreatedAt: takeFirst(orig.CreatedAt, dbtime.Now()),
DisplayName: []string{takeFirst(orig.DisplayName, namesgenerator.GetRandomName(1))},
Icon: []string{takeFirst(orig.Icon, namesgenerator.GetRandomName(1))},
})
require.NoError(t, err, "insert workspace agent log source")
return sources[0]
}
func WorkspaceBuild(t testing.TB, db database.Store, orig database.WorkspaceBuild) database.WorkspaceBuild {
buildID := takeFirst(orig.ID, uuid.New())
var build database.WorkspaceBuild

View File

@ -865,6 +865,13 @@ func (m metricsStore) GetWorkspaceAgentLifecycleStateByID(ctx context.Context, i
return r0, r1
}
func (m metricsStore) GetWorkspaceAgentLogSourcesByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]database.WorkspaceAgentLogSource, error) {
start := time.Now()
r0, r1 := m.s.GetWorkspaceAgentLogSourcesByAgentIDs(ctx, ids)
m.queryLatencies.WithLabelValues("GetWorkspaceAgentLogSourcesByAgentIDs").Observe(time.Since(start).Seconds())
return r0, r1
}
func (m metricsStore) GetWorkspaceAgentLogsAfter(ctx context.Context, arg database.GetWorkspaceAgentLogsAfterParams) ([]database.WorkspaceAgentLog, error) {
start := time.Now()
r0, r1 := m.s.GetWorkspaceAgentLogsAfter(ctx, arg)
@ -879,6 +886,13 @@ func (m metricsStore) GetWorkspaceAgentMetadata(ctx context.Context, workspaceAg
return metadata, err
}
func (m metricsStore) GetWorkspaceAgentScriptsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]database.WorkspaceAgentScript, error) {
start := time.Now()
r0, r1 := m.s.GetWorkspaceAgentScriptsByAgentIDs(ctx, ids)
m.queryLatencies.WithLabelValues("GetWorkspaceAgentScriptsByAgentIDs").Observe(time.Since(start).Seconds())
return r0, r1
}
func (m metricsStore) GetWorkspaceAgentStats(ctx context.Context, createdAt time.Time) ([]database.GetWorkspaceAgentStatsRow, error) {
start := time.Now()
stats, err := m.s.GetWorkspaceAgentStats(ctx, createdAt)
@ -1292,6 +1306,13 @@ func (m metricsStore) InsertWorkspaceAgent(ctx context.Context, arg database.Ins
return agent, err
}
func (m metricsStore) InsertWorkspaceAgentLogSources(ctx context.Context, arg database.InsertWorkspaceAgentLogSourcesParams) ([]database.WorkspaceAgentLogSource, error) {
start := time.Now()
r0, r1 := m.s.InsertWorkspaceAgentLogSources(ctx, arg)
m.queryLatencies.WithLabelValues("InsertWorkspaceAgentLogSources").Observe(time.Since(start).Seconds())
return r0, r1
}
func (m metricsStore) InsertWorkspaceAgentLogs(ctx context.Context, arg database.InsertWorkspaceAgentLogsParams) ([]database.WorkspaceAgentLog, error) {
start := time.Now()
r0, r1 := m.s.InsertWorkspaceAgentLogs(ctx, arg)
@ -1306,6 +1327,13 @@ func (m metricsStore) InsertWorkspaceAgentMetadata(ctx context.Context, arg data
return err
}
func (m metricsStore) InsertWorkspaceAgentScripts(ctx context.Context, arg database.InsertWorkspaceAgentScriptsParams) ([]database.WorkspaceAgentScript, error) {
start := time.Now()
r0, r1 := m.s.InsertWorkspaceAgentScripts(ctx, arg)
m.queryLatencies.WithLabelValues("InsertWorkspaceAgentScripts").Observe(time.Since(start).Seconds())
return r0, r1
}
func (m metricsStore) InsertWorkspaceAgentStat(ctx context.Context, arg database.InsertWorkspaceAgentStatParams) (database.WorkspaceAgentStat, error) {
start := time.Now()
stat, err := m.s.InsertWorkspaceAgentStat(ctx, arg)

View File

@ -1793,6 +1793,21 @@ func (mr *MockStoreMockRecorder) GetWorkspaceAgentLifecycleStateByID(arg0, arg1
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAgentLifecycleStateByID", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAgentLifecycleStateByID), arg0, arg1)
}
// GetWorkspaceAgentLogSourcesByAgentIDs mocks base method.
func (m *MockStore) GetWorkspaceAgentLogSourcesByAgentIDs(arg0 context.Context, arg1 []uuid.UUID) ([]database.WorkspaceAgentLogSource, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetWorkspaceAgentLogSourcesByAgentIDs", arg0, arg1)
ret0, _ := ret[0].([]database.WorkspaceAgentLogSource)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetWorkspaceAgentLogSourcesByAgentIDs indicates an expected call of GetWorkspaceAgentLogSourcesByAgentIDs.
func (mr *MockStoreMockRecorder) GetWorkspaceAgentLogSourcesByAgentIDs(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAgentLogSourcesByAgentIDs", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAgentLogSourcesByAgentIDs), arg0, arg1)
}
// GetWorkspaceAgentLogsAfter mocks base method.
func (m *MockStore) GetWorkspaceAgentLogsAfter(arg0 context.Context, arg1 database.GetWorkspaceAgentLogsAfterParams) ([]database.WorkspaceAgentLog, error) {
m.ctrl.T.Helper()
@ -1823,6 +1838,21 @@ func (mr *MockStoreMockRecorder) GetWorkspaceAgentMetadata(arg0, arg1 interface{
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAgentMetadata", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAgentMetadata), arg0, arg1)
}
// GetWorkspaceAgentScriptsByAgentIDs mocks base method.
func (m *MockStore) GetWorkspaceAgentScriptsByAgentIDs(arg0 context.Context, arg1 []uuid.UUID) ([]database.WorkspaceAgentScript, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetWorkspaceAgentScriptsByAgentIDs", arg0, arg1)
ret0, _ := ret[0].([]database.WorkspaceAgentScript)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetWorkspaceAgentScriptsByAgentIDs indicates an expected call of GetWorkspaceAgentScriptsByAgentIDs.
func (mr *MockStoreMockRecorder) GetWorkspaceAgentScriptsByAgentIDs(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAgentScriptsByAgentIDs", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAgentScriptsByAgentIDs), arg0, arg1)
}
// GetWorkspaceAgentStats mocks base method.
func (m *MockStore) GetWorkspaceAgentStats(arg0 context.Context, arg1 time.Time) ([]database.GetWorkspaceAgentStatsRow, error) {
m.ctrl.T.Helper()
@ -2715,6 +2745,21 @@ func (mr *MockStoreMockRecorder) InsertWorkspaceAgent(arg0, arg1 interface{}) *g
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertWorkspaceAgent", reflect.TypeOf((*MockStore)(nil).InsertWorkspaceAgent), arg0, arg1)
}
// InsertWorkspaceAgentLogSources mocks base method.
func (m *MockStore) InsertWorkspaceAgentLogSources(arg0 context.Context, arg1 database.InsertWorkspaceAgentLogSourcesParams) ([]database.WorkspaceAgentLogSource, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "InsertWorkspaceAgentLogSources", arg0, arg1)
ret0, _ := ret[0].([]database.WorkspaceAgentLogSource)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// InsertWorkspaceAgentLogSources indicates an expected call of InsertWorkspaceAgentLogSources.
func (mr *MockStoreMockRecorder) InsertWorkspaceAgentLogSources(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertWorkspaceAgentLogSources", reflect.TypeOf((*MockStore)(nil).InsertWorkspaceAgentLogSources), arg0, arg1)
}
// InsertWorkspaceAgentLogs mocks base method.
func (m *MockStore) InsertWorkspaceAgentLogs(arg0 context.Context, arg1 database.InsertWorkspaceAgentLogsParams) ([]database.WorkspaceAgentLog, error) {
m.ctrl.T.Helper()
@ -2744,6 +2789,21 @@ func (mr *MockStoreMockRecorder) InsertWorkspaceAgentMetadata(arg0, arg1 interfa
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertWorkspaceAgentMetadata", reflect.TypeOf((*MockStore)(nil).InsertWorkspaceAgentMetadata), arg0, arg1)
}
// InsertWorkspaceAgentScripts mocks base method.
func (m *MockStore) InsertWorkspaceAgentScripts(arg0 context.Context, arg1 database.InsertWorkspaceAgentScriptsParams) ([]database.WorkspaceAgentScript, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "InsertWorkspaceAgentScripts", arg0, arg1)
ret0, _ := ret[0].([]database.WorkspaceAgentScript)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// InsertWorkspaceAgentScripts indicates an expected call of InsertWorkspaceAgentScripts.
func (mr *MockStoreMockRecorder) InsertWorkspaceAgentScripts(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertWorkspaceAgentScripts", reflect.TypeOf((*MockStore)(nil).InsertWorkspaceAgentScripts), arg0, arg1)
}
// InsertWorkspaceAgentStat mocks base method.
func (m *MockStore) InsertWorkspaceAgentStat(arg0 context.Context, arg1 database.InsertWorkspaceAgentStatParams) (database.WorkspaceAgentStat, error) {
m.ctrl.T.Helper()

View File

@ -144,15 +144,6 @@ CREATE TYPE workspace_agent_lifecycle_state AS ENUM (
'off'
);
CREATE TYPE workspace_agent_log_source AS ENUM (
'startup_script',
'shutdown_script',
'kubernetes_logs',
'envbox',
'envbuilder',
'external'
);
CREATE TYPE workspace_agent_subsystem AS ENUM (
'envbuilder',
'envbox',
@ -789,13 +780,21 @@ COMMENT ON COLUMN user_links.oauth_access_token_key_id IS 'The ID of the key use
COMMENT ON COLUMN user_links.oauth_refresh_token_key_id IS 'The ID of the key used to encrypt the OAuth refresh token. If this is NULL, the refresh token is not encrypted';
CREATE TABLE workspace_agent_logs (
CREATE TABLE workspace_agent_log_sources (
workspace_agent_id uuid NOT NULL,
id uuid NOT NULL,
created_at timestamp with time zone NOT NULL,
display_name character varying(127) NOT NULL,
icon text NOT NULL
);
CREATE UNLOGGED TABLE workspace_agent_logs (
agent_id uuid NOT NULL,
created_at timestamp with time zone NOT NULL,
output character varying(1024) NOT NULL,
id bigint NOT NULL,
level log_level DEFAULT 'info'::log_level NOT NULL,
source workspace_agent_log_source DEFAULT 'startup_script'::workspace_agent_log_source NOT NULL
log_source_id uuid DEFAULT '00000000-0000-0000-0000-000000000000'::uuid NOT NULL
);
CREATE UNLOGGED TABLE workspace_agent_metadata (
@ -810,6 +809,19 @@ CREATE UNLOGGED TABLE workspace_agent_metadata (
collected_at timestamp with time zone DEFAULT '0001-01-01 00:00:00+00'::timestamp with time zone NOT NULL
);
CREATE TABLE workspace_agent_scripts (
workspace_agent_id uuid NOT NULL,
log_source_id uuid NOT NULL,
log_path text NOT NULL,
created_at timestamp with time zone NOT NULL,
script text NOT NULL,
cron text NOT NULL,
start_blocks_login boolean NOT NULL,
run_on_start boolean NOT NULL,
run_on_stop boolean NOT NULL,
timeout_seconds integer NOT NULL
);
CREATE SEQUENCE workspace_agent_startup_logs_id_seq
START WITH 1
INCREMENT BY 1
@ -853,7 +865,6 @@ CREATE TABLE workspace_agents (
architecture character varying(64) NOT NULL,
environment_variables jsonb,
operating_system character varying(64) NOT NULL,
startup_script character varying(65534),
instance_metadata jsonb,
resource_metadata jsonb,
directory character varying(4096) DEFAULT ''::character varying NOT NULL,
@ -863,13 +874,9 @@ CREATE TABLE workspace_agents (
troubleshooting_url text DEFAULT ''::text NOT NULL,
motd_file text DEFAULT ''::text NOT NULL,
lifecycle_state workspace_agent_lifecycle_state DEFAULT 'created'::workspace_agent_lifecycle_state NOT NULL,
startup_script_timeout_seconds integer DEFAULT 0 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,
logs_length integer DEFAULT 0 NOT NULL,
logs_overflowed boolean DEFAULT false NOT NULL,
startup_script_behavior startup_script_behavior DEFAULT 'non-blocking'::startup_script_behavior NOT NULL,
started_at timestamp with time zone,
ready_at timestamp with time zone,
subsystems workspace_agent_subsystem[] DEFAULT '{}'::workspace_agent_subsystem[],
@ -888,20 +895,12 @@ COMMENT ON COLUMN workspace_agents.motd_file IS 'Path to file inside workspace c
COMMENT ON COLUMN workspace_agents.lifecycle_state IS 'The current lifecycle state reported by the workspace agent.';
COMMENT ON COLUMN workspace_agents.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.';
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.';
COMMENT ON COLUMN workspace_agents.logs_length IS 'Total length of startup logs';
COMMENT ON COLUMN workspace_agents.logs_overflowed IS 'Whether the startup logs overflowed in length';
COMMENT ON COLUMN workspace_agents.startup_script_behavior IS 'When startup script behavior is non-blocking, the workspace will be ready and accessible upon agent connection, when it is blocking, workspace will wait for the startup script to complete before becoming ready and accessible.';
COMMENT ON COLUMN workspace_agents.started_at IS 'The time the agent entered the starting lifecycle state';
COMMENT ON COLUMN workspace_agents.ready_at IS 'The time the agent entered the ready or start_error lifecycle state';
@ -1224,6 +1223,9 @@ ALTER TABLE ONLY user_links
ALTER TABLE ONLY users
ADD CONSTRAINT users_pkey PRIMARY KEY (id);
ALTER TABLE ONLY workspace_agent_log_sources
ADD CONSTRAINT workspace_agent_log_sources_pkey PRIMARY KEY (workspace_agent_id, id);
ALTER TABLE ONLY workspace_agent_metadata
ADD CONSTRAINT workspace_agent_metadata_pkey PRIMARY KEY (workspace_agent_id, key);
@ -1418,9 +1420,15 @@ ALTER TABLE ONLY user_links
ALTER TABLE ONLY user_links
ADD CONSTRAINT user_links_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
ALTER TABLE ONLY workspace_agent_log_sources
ADD CONSTRAINT workspace_agent_log_sources_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE;
ALTER TABLE ONLY workspace_agent_metadata
ADD CONSTRAINT workspace_agent_metadata_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE;
ALTER TABLE ONLY workspace_agent_scripts
ADD CONSTRAINT workspace_agent_scripts_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE;
ALTER TABLE ONLY workspace_agent_logs
ADD CONSTRAINT workspace_agent_startup_logs_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE;

View File

@ -31,7 +31,9 @@ const (
ForeignKeyUserLinksOauthAccessTokenKeyID ForeignKeyConstraint = "user_links_oauth_access_token_key_id_fkey" // ALTER TABLE ONLY user_links ADD CONSTRAINT user_links_oauth_access_token_key_id_fkey FOREIGN KEY (oauth_access_token_key_id) REFERENCES dbcrypt_keys(active_key_digest);
ForeignKeyUserLinksOauthRefreshTokenKeyID ForeignKeyConstraint = "user_links_oauth_refresh_token_key_id_fkey" // ALTER TABLE ONLY user_links ADD CONSTRAINT user_links_oauth_refresh_token_key_id_fkey FOREIGN KEY (oauth_refresh_token_key_id) REFERENCES dbcrypt_keys(active_key_digest);
ForeignKeyUserLinksUserID ForeignKeyConstraint = "user_links_user_id_fkey" // ALTER TABLE ONLY user_links ADD CONSTRAINT user_links_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
ForeignKeyWorkspaceAgentLogSourcesWorkspaceAgentID ForeignKeyConstraint = "workspace_agent_log_sources_workspace_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_log_sources ADD CONSTRAINT workspace_agent_log_sources_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE;
ForeignKeyWorkspaceAgentMetadataWorkspaceAgentID ForeignKeyConstraint = "workspace_agent_metadata_workspace_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_metadata ADD CONSTRAINT workspace_agent_metadata_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE;
ForeignKeyWorkspaceAgentScriptsWorkspaceAgentID ForeignKeyConstraint = "workspace_agent_scripts_workspace_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_scripts ADD CONSTRAINT workspace_agent_scripts_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE;
ForeignKeyWorkspaceAgentStartupLogsAgentID ForeignKeyConstraint = "workspace_agent_startup_logs_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_logs ADD CONSTRAINT workspace_agent_startup_logs_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE;
ForeignKeyWorkspaceAgentsResourceID ForeignKeyConstraint = "workspace_agents_resource_id_fkey" // ALTER TABLE ONLY workspace_agents ADD CONSTRAINT workspace_agents_resource_id_fkey FOREIGN KEY (resource_id) REFERENCES workspace_resources(id) ON DELETE CASCADE;
ForeignKeyWorkspaceAppStatsAgentID ForeignKeyConstraint = "workspace_app_stats_agent_id_fkey" // ALTER TABLE ONLY workspace_app_stats ADD CONSTRAINT workspace_app_stats_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id);

View File

@ -0,0 +1,23 @@
BEGIN;
ALTER TABLE workspace_agent_logs SET LOGGED;
-- Revert the workspace_agents table to its former state
ALTER TABLE workspace_agents ADD COLUMN startup_script text;
ALTER TABLE workspace_agents ADD COLUMN startup_script_behavior text;
ALTER TABLE workspace_agents ADD COLUMN shutdown_script_timeout_seconds integer;
ALTER TABLE workspace_agents ADD COLUMN shutdown_script text;
ALTER TABLE workspace_agents ADD COLUMN startup_script_timeout_seconds integer;
-- Reinstate the dropped type
CREATE TYPE workspace_agent_log_source AS ENUM ('startup_script', 'shutdown_script', 'kubernetes_logs', 'envbox', 'envbuilder', 'external');
-- Add old source column back with enum type and drop log_source_id
ALTER TABLE workspace_agent_logs ADD COLUMN source workspace_agent_log_source;
ALTER TABLE workspace_agent_logs DROP COLUMN log_source_id;
-- Drop the newly created tables
DROP TABLE workspace_agent_scripts;
DROP TABLE workspace_agent_log_sources;
COMMIT;

View File

@ -0,0 +1,36 @@
BEGIN;
CREATE TABLE workspace_agent_log_sources (
workspace_agent_id uuid NOT NULL REFERENCES workspace_agents(id) ON DELETE CASCADE,
id uuid NOT NULL,
created_at timestamptz NOT NULL,
display_name varchar(127) NOT NULL,
icon text NOT NULL,
PRIMARY KEY (workspace_agent_id, id)
);
CREATE TABLE workspace_agent_scripts (
workspace_agent_id uuid NOT NULL REFERENCES workspace_agents(id) ON DELETE CASCADE,
log_source_id uuid NOT NULL,
log_path text NOT NULL,
created_at timestamptz NOT NULL,
script text NOT NULL,
cron text NOT NULL,
start_blocks_login boolean NOT NULL,
run_on_start boolean NOT NULL,
run_on_stop boolean NOT NULL,
timeout_seconds integer NOT NULL
);
ALTER TABLE workspace_agent_logs ADD COLUMN log_source_id uuid NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000'::uuid;
ALTER TABLE workspace_agent_logs DROP COLUMN source;
DROP TYPE workspace_agent_log_source;
ALTER TABLE workspace_agents DROP COLUMN startup_script_timeout_seconds;
ALTER TABLE workspace_agents DROP COLUMN shutdown_script;
ALTER TABLE workspace_agents DROP COLUMN shutdown_script_timeout_seconds;
ALTER TABLE workspace_agents DROP COLUMN startup_script_behavior;
ALTER TABLE workspace_agents DROP COLUMN startup_script;
-- Set the table to unlogged to speed up the inserts
ALTER TABLE workspace_agent_logs SET UNLOGGED;
COMMIT;

View File

@ -0,0 +1,39 @@
-- INSERT INTO workspace_agents VALUES ('45e89705-e09d-4850-bcec-f9a937f5d78d', '2022-11-02 13:03:45.046432+02', '2022-11-02 13:03:45.046432+02', 'main', NULL, NULL, NULL, '0ff953c0-92a6-4fe6-a415-eb0139a36ad1', 'ffc107ef-7ded-4d80-b1a9-0c1d0bf7ccbf', NULL, 'amd64', '{"GIT_AUTHOR_NAME": "default", "GIT_AUTHOR_EMAIL": "", "GIT_COMMITTER_NAME": "default", "GIT_COMMITTER_EMAIL": ""}', 'linux', 'code-server --auth none', NULL, NULL, '', '') ON CONFLICT DO NOTHING;
INSERT INTO workspace_agent_log_sources (
workspace_agent_id,
id,
created_at,
display_name,
icon
) VALUES (
'45e89705-e09d-4850-bcec-f9a937f5d78d',
'0ff953c0-92a6-4fe6-a415-eb0139a36ad1',
'2022-11-02 13:03:45.046432+02',
'main',
'something.png'
) ON CONFLICT DO NOTHING;
INSERT INTO workspace_agent_scripts (
workspace_agent_id,
created_at,
log_source_id,
log_path,
script,
cron,
start_blocks_login,
run_on_start,
run_on_stop,
timeout_seconds
) VALUES (
'45e89705-e09d-4850-bcec-f9a937f5d78d',
'2022-11-02 13:03:45.046432+02',
'0ff953c0-92a6-4fe6-a415-eb0139a36ad1',
'/tmp',
'echo "hello world"',
'@daily',
TRUE,
TRUE,
TRUE,
60
) ON CONFLICT DO NOTHING;

View File

@ -1298,76 +1298,6 @@ func AllWorkspaceAgentLifecycleStateValues() []WorkspaceAgentLifecycleState {
}
}
type WorkspaceAgentLogSource string
const (
WorkspaceAgentLogSourceStartupScript WorkspaceAgentLogSource = "startup_script"
WorkspaceAgentLogSourceShutdownScript WorkspaceAgentLogSource = "shutdown_script"
WorkspaceAgentLogSourceKubernetesLogs WorkspaceAgentLogSource = "kubernetes_logs"
WorkspaceAgentLogSourceEnvbox WorkspaceAgentLogSource = "envbox"
WorkspaceAgentLogSourceEnvbuilder WorkspaceAgentLogSource = "envbuilder"
WorkspaceAgentLogSourceExternal WorkspaceAgentLogSource = "external"
)
func (e *WorkspaceAgentLogSource) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = WorkspaceAgentLogSource(s)
case string:
*e = WorkspaceAgentLogSource(s)
default:
return fmt.Errorf("unsupported scan type for WorkspaceAgentLogSource: %T", src)
}
return nil
}
type NullWorkspaceAgentLogSource struct {
WorkspaceAgentLogSource WorkspaceAgentLogSource `json:"workspace_agent_log_source"`
Valid bool `json:"valid"` // Valid is true if WorkspaceAgentLogSource is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullWorkspaceAgentLogSource) Scan(value interface{}) error {
if value == nil {
ns.WorkspaceAgentLogSource, ns.Valid = "", false
return nil
}
ns.Valid = true
return ns.WorkspaceAgentLogSource.Scan(value)
}
// Value implements the driver Valuer interface.
func (ns NullWorkspaceAgentLogSource) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return string(ns.WorkspaceAgentLogSource), nil
}
func (e WorkspaceAgentLogSource) Valid() bool {
switch e {
case WorkspaceAgentLogSourceStartupScript,
WorkspaceAgentLogSourceShutdownScript,
WorkspaceAgentLogSourceKubernetesLogs,
WorkspaceAgentLogSourceEnvbox,
WorkspaceAgentLogSourceEnvbuilder,
WorkspaceAgentLogSourceExternal:
return true
}
return false
}
func AllWorkspaceAgentLogSourceValues() []WorkspaceAgentLogSource {
return []WorkspaceAgentLogSource{
WorkspaceAgentLogSourceStartupScript,
WorkspaceAgentLogSourceShutdownScript,
WorkspaceAgentLogSourceKubernetesLogs,
WorkspaceAgentLogSourceEnvbox,
WorkspaceAgentLogSourceEnvbuilder,
WorkspaceAgentLogSourceExternal,
}
}
type WorkspaceAgentSubsystem string
const (
@ -2018,7 +1948,6 @@ type WorkspaceAgent struct {
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"`
InstanceMetadata pqtype.NullRawMessage `db:"instance_metadata" json:"instance_metadata"`
ResourceMetadata pqtype.NullRawMessage `db:"resource_metadata" json:"resource_metadata"`
Directory string `db:"directory" json:"directory"`
@ -2033,20 +1962,12 @@ type WorkspaceAgent struct {
MOTDFile string `db:"motd_file" json:"motd_file"`
// The current lifecycle state reported by the workspace agent.
LifecycleState WorkspaceAgentLifecycleState `db:"lifecycle_state" json:"lifecycle_state"`
// 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"`
// 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"`
// Total length of startup logs
LogsLength int32 `db:"logs_length" json:"logs_length"`
// Whether the startup logs overflowed in length
LogsOverflowed bool `db:"logs_overflowed" json:"logs_overflowed"`
// When startup script behavior is non-blocking, the workspace will be ready and accessible upon agent connection, when it is blocking, workspace will wait for the startup script to complete before becoming ready and accessible.
StartupScriptBehavior StartupScriptBehavior `db:"startup_script_behavior" json:"startup_script_behavior"`
// The time the agent entered the starting lifecycle state
StartedAt sql.NullTime `db:"started_at" json:"started_at"`
// The time the agent entered the ready or start_error lifecycle state
@ -2056,12 +1977,20 @@ type WorkspaceAgent struct {
}
type WorkspaceAgentLog struct {
AgentID uuid.UUID `db:"agent_id" json:"agent_id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
Output string `db:"output" json:"output"`
ID int64 `db:"id" json:"id"`
Level LogLevel `db:"level" json:"level"`
Source WorkspaceAgentLogSource `db:"source" json:"source"`
AgentID uuid.UUID `db:"agent_id" json:"agent_id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
Output string `db:"output" json:"output"`
ID int64 `db:"id" json:"id"`
Level LogLevel `db:"level" json:"level"`
LogSourceID uuid.UUID `db:"log_source_id" json:"log_source_id"`
}
type WorkspaceAgentLogSource struct {
WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"`
ID uuid.UUID `db:"id" json:"id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
DisplayName string `db:"display_name" json:"display_name"`
Icon string `db:"icon" json:"icon"`
}
type WorkspaceAgentMetadatum struct {
@ -2076,6 +2005,19 @@ type WorkspaceAgentMetadatum struct {
CollectedAt time.Time `db:"collected_at" json:"collected_at"`
}
type WorkspaceAgentScript struct {
WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"`
LogSourceID uuid.UUID `db:"log_source_id" json:"log_source_id"`
LogPath string `db:"log_path" json:"log_path"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
Script string `db:"script" json:"script"`
Cron string `db:"cron" json:"cron"`
StartBlocksLogin bool `db:"start_blocks_login" json:"start_blocks_login"`
RunOnStart bool `db:"run_on_start" json:"run_on_start"`
RunOnStop bool `db:"run_on_stop" json:"run_on_stop"`
TimeoutSeconds int32 `db:"timeout_seconds" json:"timeout_seconds"`
}
type WorkspaceAgentStat struct {
ID uuid.UUID `db:"id" json:"id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`

View File

@ -174,8 +174,10 @@ type sqlcQuerier interface {
GetWorkspaceAgentByID(ctx context.Context, id uuid.UUID) (WorkspaceAgent, error)
GetWorkspaceAgentByInstanceID(ctx context.Context, authInstanceID string) (WorkspaceAgent, error)
GetWorkspaceAgentLifecycleStateByID(ctx context.Context, id uuid.UUID) (GetWorkspaceAgentLifecycleStateByIDRow, error)
GetWorkspaceAgentLogSourcesByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgentLogSource, error)
GetWorkspaceAgentLogsAfter(ctx context.Context, arg GetWorkspaceAgentLogsAfterParams) ([]WorkspaceAgentLog, error)
GetWorkspaceAgentMetadata(ctx context.Context, workspaceAgentID uuid.UUID) ([]WorkspaceAgentMetadatum, error)
GetWorkspaceAgentScriptsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgentScript, error)
GetWorkspaceAgentStats(ctx context.Context, createdAt time.Time) ([]GetWorkspaceAgentStatsRow, error)
GetWorkspaceAgentStatsAndLabels(ctx context.Context, createdAt time.Time) ([]GetWorkspaceAgentStatsAndLabelsRow, error)
GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgent, error)
@ -250,8 +252,10 @@ type sqlcQuerier interface {
InsertUserLink(ctx context.Context, arg InsertUserLinkParams) (UserLink, error)
InsertWorkspace(ctx context.Context, arg InsertWorkspaceParams) (Workspace, error)
InsertWorkspaceAgent(ctx context.Context, arg InsertWorkspaceAgentParams) (WorkspaceAgent, error)
InsertWorkspaceAgentLogSources(ctx context.Context, arg InsertWorkspaceAgentLogSourcesParams) ([]WorkspaceAgentLogSource, error)
InsertWorkspaceAgentLogs(ctx context.Context, arg InsertWorkspaceAgentLogsParams) ([]WorkspaceAgentLog, error)
InsertWorkspaceAgentMetadata(ctx context.Context, arg InsertWorkspaceAgentMetadataParams) error
InsertWorkspaceAgentScripts(ctx context.Context, arg InsertWorkspaceAgentScriptsParams) ([]WorkspaceAgentScript, error)
InsertWorkspaceAgentStat(ctx context.Context, arg InsertWorkspaceAgentStatParams) (WorkspaceAgentStat, error)
InsertWorkspaceAgentStats(ctx context.Context, arg InsertWorkspaceAgentStatsParams) error
InsertWorkspaceApp(ctx context.Context, arg InsertWorkspaceAppParams) (WorkspaceApp, error)

View File

@ -112,12 +112,15 @@ func TestInsertWorkspaceAgentLogs(t *testing.T) {
agent := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{
ResourceID: resource.ID,
})
source := dbgen.WorkspaceAgentLogSource(t, db, database.WorkspaceAgentLogSource{
WorkspaceAgentID: agent.ID,
})
logs, err := db.InsertWorkspaceAgentLogs(ctx, database.InsertWorkspaceAgentLogsParams{
AgentID: agent.ID,
CreatedAt: []time.Time{dbtime.Now()},
Output: []string{"first"},
Level: []database.LogLevel{database.LogLevelInfo},
Source: []database.WorkspaceAgentLogSource{database.WorkspaceAgentLogSourceExternal},
AgentID: agent.ID,
CreatedAt: dbtime.Now(),
Output: []string{"first"},
Level: []database.LogLevel{database.LogLevelInfo},
LogSourceID: source.ID,
// 1 MB is the max
OutputLength: 1 << 20,
})
@ -126,10 +129,10 @@ func TestInsertWorkspaceAgentLogs(t *testing.T) {
_, err = db.InsertWorkspaceAgentLogs(ctx, database.InsertWorkspaceAgentLogsParams{
AgentID: agent.ID,
CreatedAt: []time.Time{dbtime.Now()},
CreatedAt: dbtime.Now(),
Output: []string{"second"},
Level: []database.LogLevel{database.LogLevelInfo},
Source: []database.WorkspaceAgentLogSource{database.WorkspaceAgentLogSourceExternal},
LogSourceID: source.ID,
OutputLength: 1,
})
require.True(t, database.IsWorkspaceAgentLogsLimitError(err))

View File

@ -6687,7 +6687,7 @@ func (q *sqlQuerier) DeleteOldWorkspaceAgentLogs(ctx context.Context) error {
const getWorkspaceAgentAndOwnerByAuthToken = `-- name: GetWorkspaceAgentAndOwnerByAuthToken :one
SELECT
workspace_agents.id, workspace_agents.created_at, workspace_agents.updated_at, workspace_agents.name, workspace_agents.first_connected_at, workspace_agents.last_connected_at, workspace_agents.disconnected_at, workspace_agents.resource_id, workspace_agents.auth_token, workspace_agents.auth_instance_id, workspace_agents.architecture, workspace_agents.environment_variables, workspace_agents.operating_system, workspace_agents.startup_script, workspace_agents.instance_metadata, workspace_agents.resource_metadata, workspace_agents.directory, workspace_agents.version, workspace_agents.last_connected_replica_id, workspace_agents.connection_timeout_seconds, workspace_agents.troubleshooting_url, workspace_agents.motd_file, workspace_agents.lifecycle_state, workspace_agents.startup_script_timeout_seconds, workspace_agents.expanded_directory, workspace_agents.shutdown_script, workspace_agents.shutdown_script_timeout_seconds, workspace_agents.logs_length, workspace_agents.logs_overflowed, workspace_agents.startup_script_behavior, workspace_agents.started_at, workspace_agents.ready_at, workspace_agents.subsystems, workspace_agents.display_apps,
workspace_agents.id, workspace_agents.created_at, workspace_agents.updated_at, workspace_agents.name, workspace_agents.first_connected_at, workspace_agents.last_connected_at, workspace_agents.disconnected_at, workspace_agents.resource_id, workspace_agents.auth_token, workspace_agents.auth_instance_id, workspace_agents.architecture, workspace_agents.environment_variables, workspace_agents.operating_system, workspace_agents.instance_metadata, workspace_agents.resource_metadata, workspace_agents.directory, workspace_agents.version, workspace_agents.last_connected_replica_id, workspace_agents.connection_timeout_seconds, workspace_agents.troubleshooting_url, workspace_agents.motd_file, workspace_agents.lifecycle_state, workspace_agents.expanded_directory, workspace_agents.logs_length, workspace_agents.logs_overflowed, workspace_agents.started_at, workspace_agents.ready_at, workspace_agents.subsystems, workspace_agents.display_apps,
workspaces.id AS workspace_id,
users.id AS owner_id,
users.username AS owner_name,
@ -6766,7 +6766,6 @@ func (q *sqlQuerier) GetWorkspaceAgentAndOwnerByAuthToken(ctx context.Context, a
&i.WorkspaceAgent.Architecture,
&i.WorkspaceAgent.EnvironmentVariables,
&i.WorkspaceAgent.OperatingSystem,
&i.WorkspaceAgent.StartupScript,
&i.WorkspaceAgent.InstanceMetadata,
&i.WorkspaceAgent.ResourceMetadata,
&i.WorkspaceAgent.Directory,
@ -6776,13 +6775,9 @@ func (q *sqlQuerier) GetWorkspaceAgentAndOwnerByAuthToken(ctx context.Context, a
&i.WorkspaceAgent.TroubleshootingURL,
&i.WorkspaceAgent.MOTDFile,
&i.WorkspaceAgent.LifecycleState,
&i.WorkspaceAgent.StartupScriptTimeoutSeconds,
&i.WorkspaceAgent.ExpandedDirectory,
&i.WorkspaceAgent.ShutdownScript,
&i.WorkspaceAgent.ShutdownScriptTimeoutSeconds,
&i.WorkspaceAgent.LogsLength,
&i.WorkspaceAgent.LogsOverflowed,
&i.WorkspaceAgent.StartupScriptBehavior,
&i.WorkspaceAgent.StartedAt,
&i.WorkspaceAgent.ReadyAt,
pq.Array(&i.WorkspaceAgent.Subsystems),
@ -6799,7 +6794,7 @@ func (q *sqlQuerier) GetWorkspaceAgentAndOwnerByAuthToken(ctx context.Context, a
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, startup_script_timeout_seconds, expanded_directory, shutdown_script, shutdown_script_timeout_seconds, logs_length, logs_overflowed, startup_script_behavior, started_at, ready_at, subsystems, display_apps
id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps
FROM
workspace_agents
WHERE
@ -6823,7 +6818,6 @@ func (q *sqlQuerier) GetWorkspaceAgentByID(ctx context.Context, id uuid.UUID) (W
&i.Architecture,
&i.EnvironmentVariables,
&i.OperatingSystem,
&i.StartupScript,
&i.InstanceMetadata,
&i.ResourceMetadata,
&i.Directory,
@ -6833,13 +6827,9 @@ func (q *sqlQuerier) GetWorkspaceAgentByID(ctx context.Context, id uuid.UUID) (W
&i.TroubleshootingURL,
&i.MOTDFile,
&i.LifecycleState,
&i.StartupScriptTimeoutSeconds,
&i.ExpandedDirectory,
&i.ShutdownScript,
&i.ShutdownScriptTimeoutSeconds,
&i.LogsLength,
&i.LogsOverflowed,
&i.StartupScriptBehavior,
&i.StartedAt,
&i.ReadyAt,
pq.Array(&i.Subsystems),
@ -6850,7 +6840,7 @@ func (q *sqlQuerier) GetWorkspaceAgentByID(ctx context.Context, id uuid.UUID) (W
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, startup_script_timeout_seconds, expanded_directory, shutdown_script, shutdown_script_timeout_seconds, logs_length, logs_overflowed, startup_script_behavior, started_at, ready_at, subsystems, display_apps
id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps
FROM
workspace_agents
WHERE
@ -6876,7 +6866,6 @@ func (q *sqlQuerier) GetWorkspaceAgentByInstanceID(ctx context.Context, authInst
&i.Architecture,
&i.EnvironmentVariables,
&i.OperatingSystem,
&i.StartupScript,
&i.InstanceMetadata,
&i.ResourceMetadata,
&i.Directory,
@ -6886,13 +6875,9 @@ func (q *sqlQuerier) GetWorkspaceAgentByInstanceID(ctx context.Context, authInst
&i.TroubleshootingURL,
&i.MOTDFile,
&i.LifecycleState,
&i.StartupScriptTimeoutSeconds,
&i.ExpandedDirectory,
&i.ShutdownScript,
&i.ShutdownScriptTimeoutSeconds,
&i.LogsLength,
&i.LogsOverflowed,
&i.StartupScriptBehavior,
&i.StartedAt,
&i.ReadyAt,
pq.Array(&i.Subsystems),
@ -6925,9 +6910,42 @@ func (q *sqlQuerier) GetWorkspaceAgentLifecycleStateByID(ctx context.Context, id
return i, err
}
const getWorkspaceAgentLogSourcesByAgentIDs = `-- name: GetWorkspaceAgentLogSourcesByAgentIDs :many
SELECT workspace_agent_id, id, created_at, display_name, icon FROM workspace_agent_log_sources WHERE workspace_agent_id = ANY($1 :: uuid [ ])
`
func (q *sqlQuerier) GetWorkspaceAgentLogSourcesByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgentLogSource, error) {
rows, err := q.db.QueryContext(ctx, getWorkspaceAgentLogSourcesByAgentIDs, pq.Array(ids))
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceAgentLogSource
for rows.Next() {
var i WorkspaceAgentLogSource
if err := rows.Scan(
&i.WorkspaceAgentID,
&i.ID,
&i.CreatedAt,
&i.DisplayName,
&i.Icon,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getWorkspaceAgentLogsAfter = `-- name: GetWorkspaceAgentLogsAfter :many
SELECT
agent_id, created_at, output, id, level, source
agent_id, created_at, output, id, level, log_source_id
FROM
workspace_agent_logs
WHERE
@ -6957,7 +6975,7 @@ func (q *sqlQuerier) GetWorkspaceAgentLogsAfter(ctx context.Context, arg GetWork
&i.Output,
&i.ID,
&i.Level,
&i.Source,
&i.LogSourceID,
); err != nil {
return nil, err
}
@ -7016,7 +7034,7 @@ func (q *sqlQuerier) GetWorkspaceAgentMetadata(ctx context.Context, workspaceAge
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, startup_script_timeout_seconds, expanded_directory, shutdown_script, shutdown_script_timeout_seconds, logs_length, logs_overflowed, startup_script_behavior, started_at, ready_at, subsystems, display_apps
id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps
FROM
workspace_agents
WHERE
@ -7046,7 +7064,6 @@ func (q *sqlQuerier) GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids []
&i.Architecture,
&i.EnvironmentVariables,
&i.OperatingSystem,
&i.StartupScript,
&i.InstanceMetadata,
&i.ResourceMetadata,
&i.Directory,
@ -7056,13 +7073,9 @@ func (q *sqlQuerier) GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids []
&i.TroubleshootingURL,
&i.MOTDFile,
&i.LifecycleState,
&i.StartupScriptTimeoutSeconds,
&i.ExpandedDirectory,
&i.ShutdownScript,
&i.ShutdownScriptTimeoutSeconds,
&i.LogsLength,
&i.LogsOverflowed,
&i.StartupScriptBehavior,
&i.StartedAt,
&i.ReadyAt,
pq.Array(&i.Subsystems),
@ -7082,7 +7095,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, startup_script_timeout_seconds, expanded_directory, shutdown_script, shutdown_script_timeout_seconds, logs_length, logs_overflowed, startup_script_behavior, started_at, ready_at, subsystems, display_apps 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, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps FROM workspace_agents WHERE created_at > $1
`
func (q *sqlQuerier) GetWorkspaceAgentsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceAgent, error) {
@ -7108,7 +7121,6 @@ func (q *sqlQuerier) GetWorkspaceAgentsCreatedAfter(ctx context.Context, created
&i.Architecture,
&i.EnvironmentVariables,
&i.OperatingSystem,
&i.StartupScript,
&i.InstanceMetadata,
&i.ResourceMetadata,
&i.Directory,
@ -7118,13 +7130,9 @@ func (q *sqlQuerier) GetWorkspaceAgentsCreatedAfter(ctx context.Context, created
&i.TroubleshootingURL,
&i.MOTDFile,
&i.LifecycleState,
&i.StartupScriptTimeoutSeconds,
&i.ExpandedDirectory,
&i.ShutdownScript,
&i.ShutdownScriptTimeoutSeconds,
&i.LogsLength,
&i.LogsOverflowed,
&i.StartupScriptBehavior,
&i.StartedAt,
&i.ReadyAt,
pq.Array(&i.Subsystems),
@ -7145,7 +7153,7 @@ func (q *sqlQuerier) GetWorkspaceAgentsCreatedAfter(ctx context.Context, created
const getWorkspaceAgentsInLatestBuildByWorkspaceID = `-- name: GetWorkspaceAgentsInLatestBuildByWorkspaceID :many
SELECT
workspace_agents.id, workspace_agents.created_at, workspace_agents.updated_at, workspace_agents.name, workspace_agents.first_connected_at, workspace_agents.last_connected_at, workspace_agents.disconnected_at, workspace_agents.resource_id, workspace_agents.auth_token, workspace_agents.auth_instance_id, workspace_agents.architecture, workspace_agents.environment_variables, workspace_agents.operating_system, workspace_agents.startup_script, workspace_agents.instance_metadata, workspace_agents.resource_metadata, workspace_agents.directory, workspace_agents.version, workspace_agents.last_connected_replica_id, workspace_agents.connection_timeout_seconds, workspace_agents.troubleshooting_url, workspace_agents.motd_file, workspace_agents.lifecycle_state, workspace_agents.startup_script_timeout_seconds, workspace_agents.expanded_directory, workspace_agents.shutdown_script, workspace_agents.shutdown_script_timeout_seconds, workspace_agents.logs_length, workspace_agents.logs_overflowed, workspace_agents.startup_script_behavior, workspace_agents.started_at, workspace_agents.ready_at, workspace_agents.subsystems, workspace_agents.display_apps
workspace_agents.id, workspace_agents.created_at, workspace_agents.updated_at, workspace_agents.name, workspace_agents.first_connected_at, workspace_agents.last_connected_at, workspace_agents.disconnected_at, workspace_agents.resource_id, workspace_agents.auth_token, workspace_agents.auth_instance_id, workspace_agents.architecture, workspace_agents.environment_variables, workspace_agents.operating_system, workspace_agents.instance_metadata, workspace_agents.resource_metadata, workspace_agents.directory, workspace_agents.version, workspace_agents.last_connected_replica_id, workspace_agents.connection_timeout_seconds, workspace_agents.troubleshooting_url, workspace_agents.motd_file, workspace_agents.lifecycle_state, workspace_agents.expanded_directory, workspace_agents.logs_length, workspace_agents.logs_overflowed, workspace_agents.started_at, workspace_agents.ready_at, workspace_agents.subsystems, workspace_agents.display_apps
FROM
workspace_agents
JOIN
@ -7187,7 +7195,6 @@ func (q *sqlQuerier) GetWorkspaceAgentsInLatestBuildByWorkspaceID(ctx context.Co
&i.Architecture,
&i.EnvironmentVariables,
&i.OperatingSystem,
&i.StartupScript,
&i.InstanceMetadata,
&i.ResourceMetadata,
&i.Directory,
@ -7197,13 +7204,9 @@ func (q *sqlQuerier) GetWorkspaceAgentsInLatestBuildByWorkspaceID(ctx context.Co
&i.TroubleshootingURL,
&i.MOTDFile,
&i.LifecycleState,
&i.StartupScriptTimeoutSeconds,
&i.ExpandedDirectory,
&i.ShutdownScript,
&i.ShutdownScriptTimeoutSeconds,
&i.LogsLength,
&i.LogsOverflowed,
&i.StartupScriptBehavior,
&i.StartedAt,
&i.ReadyAt,
pq.Array(&i.Subsystems),
@ -7235,46 +7238,36 @@ INSERT INTO
architecture,
environment_variables,
operating_system,
startup_script,
directory,
instance_metadata,
resource_metadata,
connection_timeout_seconds,
troubleshooting_url,
motd_file,
startup_script_behavior,
startup_script_timeout_seconds,
shutdown_script,
shutdown_script_timeout_seconds,
display_apps
)
VALUES
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22) 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, startup_script_timeout_seconds, expanded_directory, shutdown_script, shutdown_script_timeout_seconds, logs_length, logs_overflowed, startup_script_behavior, started_at, ready_at, subsystems, display_apps
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17) 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, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps
`
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"`
StartupScriptBehavior StartupScriptBehavior `db:"startup_script_behavior" json:"startup_script_behavior"`
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"`
DisplayApps []DisplayApp `db:"display_apps" json:"display_apps"`
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"`
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"`
DisplayApps []DisplayApp `db:"display_apps" json:"display_apps"`
}
func (q *sqlQuerier) InsertWorkspaceAgent(ctx context.Context, arg InsertWorkspaceAgentParams) (WorkspaceAgent, error) {
@ -7289,17 +7282,12 @@ func (q *sqlQuerier) InsertWorkspaceAgent(ctx context.Context, arg InsertWorkspa
arg.Architecture,
arg.EnvironmentVariables,
arg.OperatingSystem,
arg.StartupScript,
arg.Directory,
arg.InstanceMetadata,
arg.ResourceMetadata,
arg.ConnectionTimeoutSeconds,
arg.TroubleshootingURL,
arg.MOTDFile,
arg.StartupScriptBehavior,
arg.StartupScriptTimeoutSeconds,
arg.ShutdownScript,
arg.ShutdownScriptTimeoutSeconds,
pq.Array(arg.DisplayApps),
)
var i WorkspaceAgent
@ -7317,7 +7305,6 @@ func (q *sqlQuerier) InsertWorkspaceAgent(ctx context.Context, arg InsertWorkspa
&i.Architecture,
&i.EnvironmentVariables,
&i.OperatingSystem,
&i.StartupScript,
&i.InstanceMetadata,
&i.ResourceMetadata,
&i.Directory,
@ -7327,13 +7314,9 @@ func (q *sqlQuerier) InsertWorkspaceAgent(ctx context.Context, arg InsertWorkspa
&i.TroubleshootingURL,
&i.MOTDFile,
&i.LifecycleState,
&i.StartupScriptTimeoutSeconds,
&i.ExpandedDirectory,
&i.ShutdownScript,
&i.ShutdownScriptTimeoutSeconds,
&i.LogsLength,
&i.LogsOverflowed,
&i.StartupScriptBehavior,
&i.StartedAt,
&i.ReadyAt,
pq.Array(&i.Subsystems),
@ -7342,38 +7325,93 @@ func (q *sqlQuerier) InsertWorkspaceAgent(ctx context.Context, arg InsertWorkspa
return i, err
}
const insertWorkspaceAgentLogSources = `-- name: InsertWorkspaceAgentLogSources :many
INSERT INTO
workspace_agent_log_sources (workspace_agent_id, created_at, id, display_name, icon)
SELECT
$1 :: uuid AS workspace_agent_id,
$2 :: timestamptz AS created_at,
unnest($3 :: uuid [ ]) AS id,
unnest($4 :: VARCHAR(127) [ ]) AS display_name,
unnest($5 :: text [ ]) AS icon
RETURNING workspace_agent_log_sources.workspace_agent_id, workspace_agent_log_sources.id, workspace_agent_log_sources.created_at, workspace_agent_log_sources.display_name, workspace_agent_log_sources.icon
`
type InsertWorkspaceAgentLogSourcesParams struct {
WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
ID []uuid.UUID `db:"id" json:"id"`
DisplayName []string `db:"display_name" json:"display_name"`
Icon []string `db:"icon" json:"icon"`
}
func (q *sqlQuerier) InsertWorkspaceAgentLogSources(ctx context.Context, arg InsertWorkspaceAgentLogSourcesParams) ([]WorkspaceAgentLogSource, error) {
rows, err := q.db.QueryContext(ctx, insertWorkspaceAgentLogSources,
arg.WorkspaceAgentID,
arg.CreatedAt,
pq.Array(arg.ID),
pq.Array(arg.DisplayName),
pq.Array(arg.Icon),
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceAgentLogSource
for rows.Next() {
var i WorkspaceAgentLogSource
if err := rows.Scan(
&i.WorkspaceAgentID,
&i.ID,
&i.CreatedAt,
&i.DisplayName,
&i.Icon,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertWorkspaceAgentLogs = `-- name: InsertWorkspaceAgentLogs :many
WITH new_length AS (
UPDATE workspace_agents SET
logs_length = logs_length + $6 WHERE workspace_agents.id = $1
)
INSERT INTO
workspace_agent_logs (agent_id, created_at, output, level, source)
workspace_agent_logs (agent_id, created_at, output, level, log_source_id)
SELECT
$1 :: uuid AS agent_id,
unnest($2 :: timestamptz [ ]) AS created_at,
$2 :: timestamptz AS created_at,
unnest($3 :: VARCHAR(1024) [ ]) AS output,
unnest($4 :: log_level [ ]) AS level,
unnest($5 :: workspace_agent_log_source [ ]) AS source
RETURNING workspace_agent_logs.agent_id, workspace_agent_logs.created_at, workspace_agent_logs.output, workspace_agent_logs.id, workspace_agent_logs.level, workspace_agent_logs.source
$5 :: uuid AS log_source_id
RETURNING workspace_agent_logs.agent_id, workspace_agent_logs.created_at, workspace_agent_logs.output, workspace_agent_logs.id, workspace_agent_logs.level, workspace_agent_logs.log_source_id
`
type InsertWorkspaceAgentLogsParams struct {
AgentID uuid.UUID `db:"agent_id" json:"agent_id"`
CreatedAt []time.Time `db:"created_at" json:"created_at"`
Output []string `db:"output" json:"output"`
Level []LogLevel `db:"level" json:"level"`
Source []WorkspaceAgentLogSource `db:"source" json:"source"`
OutputLength int32 `db:"output_length" json:"output_length"`
AgentID uuid.UUID `db:"agent_id" json:"agent_id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
Output []string `db:"output" json:"output"`
Level []LogLevel `db:"level" json:"level"`
LogSourceID uuid.UUID `db:"log_source_id" json:"log_source_id"`
OutputLength int32 `db:"output_length" json:"output_length"`
}
func (q *sqlQuerier) InsertWorkspaceAgentLogs(ctx context.Context, arg InsertWorkspaceAgentLogsParams) ([]WorkspaceAgentLog, error) {
rows, err := q.db.QueryContext(ctx, insertWorkspaceAgentLogs,
arg.AgentID,
pq.Array(arg.CreatedAt),
arg.CreatedAt,
pq.Array(arg.Output),
pq.Array(arg.Level),
pq.Array(arg.Source),
arg.LogSourceID,
arg.OutputLength,
)
if err != nil {
@ -7389,7 +7427,7 @@ func (q *sqlQuerier) InsertWorkspaceAgentLogs(ctx context.Context, arg InsertWor
&i.Output,
&i.ID,
&i.Level,
&i.Source,
&i.LogSourceID,
); err != nil {
return nil, err
}
@ -10322,3 +10360,116 @@ func (q *sqlQuerier) UpdateWorkspacesDormantDeletingAtByTemplateID(ctx context.C
_, err := q.db.ExecContext(ctx, updateWorkspacesDormantDeletingAtByTemplateID, arg.TimeTilDormantAutodeleteMs, arg.DormantAt, arg.TemplateID)
return err
}
const getWorkspaceAgentScriptsByAgentIDs = `-- name: GetWorkspaceAgentScriptsByAgentIDs :many
SELECT workspace_agent_id, log_source_id, log_path, created_at, script, cron, start_blocks_login, run_on_start, run_on_stop, timeout_seconds FROM workspace_agent_scripts WHERE workspace_agent_id = ANY($1 :: uuid [ ])
`
func (q *sqlQuerier) GetWorkspaceAgentScriptsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgentScript, error) {
rows, err := q.db.QueryContext(ctx, getWorkspaceAgentScriptsByAgentIDs, pq.Array(ids))
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceAgentScript
for rows.Next() {
var i WorkspaceAgentScript
if err := rows.Scan(
&i.WorkspaceAgentID,
&i.LogSourceID,
&i.LogPath,
&i.CreatedAt,
&i.Script,
&i.Cron,
&i.StartBlocksLogin,
&i.RunOnStart,
&i.RunOnStop,
&i.TimeoutSeconds,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertWorkspaceAgentScripts = `-- name: InsertWorkspaceAgentScripts :many
INSERT INTO
workspace_agent_scripts (workspace_agent_id, created_at, log_source_id, log_path, script, cron, start_blocks_login, run_on_start, run_on_stop, timeout_seconds)
SELECT
$1 :: uuid AS workspace_agent_id,
$2 :: timestamptz AS created_at,
unnest($3 :: uuid [ ]) AS log_source_id,
unnest($4 :: text [ ]) AS log_path,
unnest($5 :: text [ ]) AS script,
unnest($6 :: text [ ]) AS cron,
unnest($7 :: boolean [ ]) AS start_blocks_login,
unnest($8 :: boolean [ ]) AS run_on_start,
unnest($9 :: boolean [ ]) AS run_on_stop,
unnest($10 :: integer [ ]) AS timeout_seconds
RETURNING workspace_agent_scripts.workspace_agent_id, workspace_agent_scripts.log_source_id, workspace_agent_scripts.log_path, workspace_agent_scripts.created_at, workspace_agent_scripts.script, workspace_agent_scripts.cron, workspace_agent_scripts.start_blocks_login, workspace_agent_scripts.run_on_start, workspace_agent_scripts.run_on_stop, workspace_agent_scripts.timeout_seconds
`
type InsertWorkspaceAgentScriptsParams struct {
WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
LogSourceID []uuid.UUID `db:"log_source_id" json:"log_source_id"`
LogPath []string `db:"log_path" json:"log_path"`
Script []string `db:"script" json:"script"`
Cron []string `db:"cron" json:"cron"`
StartBlocksLogin []bool `db:"start_blocks_login" json:"start_blocks_login"`
RunOnStart []bool `db:"run_on_start" json:"run_on_start"`
RunOnStop []bool `db:"run_on_stop" json:"run_on_stop"`
TimeoutSeconds []int32 `db:"timeout_seconds" json:"timeout_seconds"`
}
func (q *sqlQuerier) InsertWorkspaceAgentScripts(ctx context.Context, arg InsertWorkspaceAgentScriptsParams) ([]WorkspaceAgentScript, error) {
rows, err := q.db.QueryContext(ctx, insertWorkspaceAgentScripts,
arg.WorkspaceAgentID,
arg.CreatedAt,
pq.Array(arg.LogSourceID),
pq.Array(arg.LogPath),
pq.Array(arg.Script),
pq.Array(arg.Cron),
pq.Array(arg.StartBlocksLogin),
pq.Array(arg.RunOnStart),
pq.Array(arg.RunOnStop),
pq.Array(arg.TimeoutSeconds),
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceAgentScript
for rows.Next() {
var i WorkspaceAgentScript
if err := rows.Scan(
&i.WorkspaceAgentID,
&i.LogSourceID,
&i.LogPath,
&i.CreatedAt,
&i.Script,
&i.Cron,
&i.StartBlocksLogin,
&i.RunOnStart,
&i.RunOnStop,
&i.TimeoutSeconds,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}

View File

@ -40,21 +40,16 @@ INSERT INTO
architecture,
environment_variables,
operating_system,
startup_script,
directory,
instance_metadata,
resource_metadata,
connection_timeout_seconds,
troubleshooting_url,
motd_file,
startup_script_behavior,
startup_script_timeout_seconds,
shutdown_script,
shutdown_script_timeout_seconds,
display_apps
)
VALUES
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22) RETURNING *;
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17) RETURNING *;
-- name: UpdateWorkspaceAgentConnectionByID :exec
UPDATE
@ -156,15 +151,29 @@ WITH new_length AS (
logs_length = logs_length + @output_length WHERE workspace_agents.id = @agent_id
)
INSERT INTO
workspace_agent_logs (agent_id, created_at, output, level, source)
workspace_agent_logs (agent_id, created_at, output, level, log_source_id)
SELECT
@agent_id :: uuid AS agent_id,
unnest(@created_at :: timestamptz [ ]) AS created_at,
@created_at :: timestamptz AS created_at,
unnest(@output :: VARCHAR(1024) [ ]) AS output,
unnest(@level :: log_level [ ]) AS level,
unnest(@source :: workspace_agent_log_source [ ]) AS source
@log_source_id :: uuid AS log_source_id
RETURNING workspace_agent_logs.*;
-- name: InsertWorkspaceAgentLogSources :many
INSERT INTO
workspace_agent_log_sources (workspace_agent_id, created_at, id, display_name, icon)
SELECT
@workspace_agent_id :: uuid AS workspace_agent_id,
@created_at :: timestamptz AS created_at,
unnest(@id :: uuid [ ]) AS id,
unnest(@display_name :: VARCHAR(127) [ ]) AS display_name,
unnest(@icon :: text [ ]) AS icon
RETURNING workspace_agent_log_sources.*;
-- name: GetWorkspaceAgentLogSourcesByAgentIDs :many
SELECT * FROM workspace_agent_log_sources WHERE workspace_agent_id = ANY(@ids :: uuid [ ]);
-- If an agent hasn't connected in the last 7 days, we purge it's logs.
-- Logs can take up a lot of space, so it's important we clean up frequently.
-- name: DeleteOldWorkspaceAgentLogs :exec

View File

@ -0,0 +1,18 @@
-- name: InsertWorkspaceAgentScripts :many
INSERT INTO
workspace_agent_scripts (workspace_agent_id, created_at, log_source_id, log_path, script, cron, start_blocks_login, run_on_start, run_on_stop, timeout_seconds)
SELECT
@workspace_agent_id :: uuid AS workspace_agent_id,
@created_at :: timestamptz AS created_at,
unnest(@log_source_id :: uuid [ ]) AS log_source_id,
unnest(@log_path :: text [ ]) AS log_path,
unnest(@script :: text [ ]) AS script,
unnest(@cron :: text [ ]) AS cron,
unnest(@start_blocks_login :: boolean [ ]) AS start_blocks_login,
unnest(@run_on_start :: boolean [ ]) AS run_on_start,
unnest(@run_on_stop :: boolean [ ]) AS run_on_stop,
unnest(@timeout_seconds :: integer [ ]) AS timeout_seconds
RETURNING workspace_agent_scripts.*;
-- name: GetWorkspaceAgentScriptsByAgentIDs :many
SELECT * FROM workspace_agent_scripts WHERE workspace_agent_id = ANY(@ids :: uuid [ ]);

View File

@ -6,27 +6,61 @@ type UniqueConstraint string
// UniqueConstraint enums.
const (
UniqueAgentStatsPkey UniqueConstraint = "agent_stats_pkey" // ALTER TABLE ONLY workspace_agent_stats ADD CONSTRAINT agent_stats_pkey PRIMARY KEY (id);
UniqueAPIKeysPkey UniqueConstraint = "api_keys_pkey" // ALTER TABLE ONLY api_keys ADD CONSTRAINT api_keys_pkey PRIMARY KEY (id);
UniqueAuditLogsPkey UniqueConstraint = "audit_logs_pkey" // ALTER TABLE ONLY audit_logs ADD CONSTRAINT audit_logs_pkey PRIMARY KEY (id);
UniqueDbcryptKeysActiveKeyDigestKey UniqueConstraint = "dbcrypt_keys_active_key_digest_key" // ALTER TABLE ONLY dbcrypt_keys ADD CONSTRAINT dbcrypt_keys_active_key_digest_key UNIQUE (active_key_digest);
UniqueDbcryptKeysPkey UniqueConstraint = "dbcrypt_keys_pkey" // ALTER TABLE ONLY dbcrypt_keys ADD CONSTRAINT dbcrypt_keys_pkey PRIMARY KEY (number);
UniqueDbcryptKeysRevokedKeyDigestKey UniqueConstraint = "dbcrypt_keys_revoked_key_digest_key" // ALTER TABLE ONLY dbcrypt_keys ADD CONSTRAINT dbcrypt_keys_revoked_key_digest_key UNIQUE (revoked_key_digest);
UniqueFilesHashCreatedByKey UniqueConstraint = "files_hash_created_by_key" // ALTER TABLE ONLY files ADD CONSTRAINT files_hash_created_by_key UNIQUE (hash, created_by);
UniqueFilesPkey UniqueConstraint = "files_pkey" // ALTER TABLE ONLY files ADD CONSTRAINT files_pkey PRIMARY KEY (id);
UniqueGitAuthLinksProviderIDUserIDKey UniqueConstraint = "git_auth_links_provider_id_user_id_key" // ALTER TABLE ONLY git_auth_links ADD CONSTRAINT git_auth_links_provider_id_user_id_key UNIQUE (provider_id, user_id);
UniqueGitSSHKeysPkey UniqueConstraint = "gitsshkeys_pkey" // ALTER TABLE ONLY gitsshkeys ADD CONSTRAINT gitsshkeys_pkey PRIMARY KEY (user_id);
UniqueGroupMembersUserIDGroupIDKey UniqueConstraint = "group_members_user_id_group_id_key" // ALTER TABLE ONLY group_members ADD CONSTRAINT group_members_user_id_group_id_key UNIQUE (user_id, group_id);
UniqueGroupsNameOrganizationIDKey UniqueConstraint = "groups_name_organization_id_key" // ALTER TABLE ONLY groups ADD CONSTRAINT groups_name_organization_id_key UNIQUE (name, organization_id);
UniqueGroupsPkey UniqueConstraint = "groups_pkey" // ALTER TABLE ONLY groups ADD CONSTRAINT groups_pkey PRIMARY KEY (id);
UniqueLicensesJWTKey UniqueConstraint = "licenses_jwt_key" // ALTER TABLE ONLY licenses ADD CONSTRAINT licenses_jwt_key UNIQUE (jwt);
UniqueLicensesPkey UniqueConstraint = "licenses_pkey" // ALTER TABLE ONLY licenses ADD CONSTRAINT licenses_pkey PRIMARY KEY (id);
UniqueOrganizationMembersPkey UniqueConstraint = "organization_members_pkey" // ALTER TABLE ONLY organization_members ADD CONSTRAINT organization_members_pkey PRIMARY KEY (organization_id, user_id);
UniqueOrganizationsPkey UniqueConstraint = "organizations_pkey" // ALTER TABLE ONLY organizations ADD CONSTRAINT organizations_pkey PRIMARY KEY (id);
UniqueParameterSchemasJobIDNameKey UniqueConstraint = "parameter_schemas_job_id_name_key" // ALTER TABLE ONLY parameter_schemas ADD CONSTRAINT parameter_schemas_job_id_name_key UNIQUE (job_id, name);
UniqueParameterSchemasPkey UniqueConstraint = "parameter_schemas_pkey" // ALTER TABLE ONLY parameter_schemas ADD CONSTRAINT parameter_schemas_pkey PRIMARY KEY (id);
UniqueParameterValuesPkey UniqueConstraint = "parameter_values_pkey" // ALTER TABLE ONLY parameter_values ADD CONSTRAINT parameter_values_pkey PRIMARY KEY (id);
UniqueParameterValuesScopeIDNameKey UniqueConstraint = "parameter_values_scope_id_name_key" // ALTER TABLE ONLY parameter_values ADD CONSTRAINT parameter_values_scope_id_name_key UNIQUE (scope_id, name);
UniqueProvisionerDaemonsNameKey UniqueConstraint = "provisioner_daemons_name_key" // ALTER TABLE ONLY provisioner_daemons ADD CONSTRAINT provisioner_daemons_name_key UNIQUE (name);
UniqueProvisionerDaemonsPkey UniqueConstraint = "provisioner_daemons_pkey" // ALTER TABLE ONLY provisioner_daemons ADD CONSTRAINT provisioner_daemons_pkey PRIMARY KEY (id);
UniqueProvisionerJobLogsPkey UniqueConstraint = "provisioner_job_logs_pkey" // ALTER TABLE ONLY provisioner_job_logs ADD CONSTRAINT provisioner_job_logs_pkey PRIMARY KEY (id);
UniqueProvisionerJobsPkey UniqueConstraint = "provisioner_jobs_pkey" // ALTER TABLE ONLY provisioner_jobs ADD CONSTRAINT provisioner_jobs_pkey PRIMARY KEY (id);
UniqueSiteConfigsKeyKey UniqueConstraint = "site_configs_key_key" // ALTER TABLE ONLY site_configs ADD CONSTRAINT site_configs_key_key UNIQUE (key);
UniqueTailnetAgentsPkey UniqueConstraint = "tailnet_agents_pkey" // ALTER TABLE ONLY tailnet_agents ADD CONSTRAINT tailnet_agents_pkey PRIMARY KEY (id, coordinator_id);
UniqueTailnetClientSubscriptionsPkey UniqueConstraint = "tailnet_client_subscriptions_pkey" // ALTER TABLE ONLY tailnet_client_subscriptions ADD CONSTRAINT tailnet_client_subscriptions_pkey PRIMARY KEY (client_id, coordinator_id, agent_id);
UniqueTailnetClientsPkey UniqueConstraint = "tailnet_clients_pkey" // ALTER TABLE ONLY tailnet_clients ADD CONSTRAINT tailnet_clients_pkey PRIMARY KEY (id, coordinator_id);
UniqueTailnetCoordinatorsPkey UniqueConstraint = "tailnet_coordinators_pkey" // ALTER TABLE ONLY tailnet_coordinators ADD CONSTRAINT tailnet_coordinators_pkey PRIMARY KEY (id);
UniqueTemplateVersionParametersTemplateVersionIDNameKey UniqueConstraint = "template_version_parameters_template_version_id_name_key" // ALTER TABLE ONLY template_version_parameters ADD CONSTRAINT template_version_parameters_template_version_id_name_key UNIQUE (template_version_id, name);
UniqueTemplateVersionVariablesTemplateVersionIDNameKey UniqueConstraint = "template_version_variables_template_version_id_name_key" // ALTER TABLE ONLY template_version_variables ADD CONSTRAINT template_version_variables_template_version_id_name_key UNIQUE (template_version_id, name);
UniqueTemplateVersionsPkey UniqueConstraint = "template_versions_pkey" // ALTER TABLE ONLY template_versions ADD CONSTRAINT template_versions_pkey PRIMARY KEY (id);
UniqueTemplateVersionsTemplateIDNameKey UniqueConstraint = "template_versions_template_id_name_key" // ALTER TABLE ONLY template_versions ADD CONSTRAINT template_versions_template_id_name_key UNIQUE (template_id, name);
UniqueTemplatesPkey UniqueConstraint = "templates_pkey" // ALTER TABLE ONLY templates ADD CONSTRAINT templates_pkey PRIMARY KEY (id);
UniqueUserLinksPkey UniqueConstraint = "user_links_pkey" // ALTER TABLE ONLY user_links ADD CONSTRAINT user_links_pkey PRIMARY KEY (user_id, login_type);
UniqueUsersPkey UniqueConstraint = "users_pkey" // ALTER TABLE ONLY users ADD CONSTRAINT users_pkey PRIMARY KEY (id);
UniqueWorkspaceAgentLogSourcesPkey UniqueConstraint = "workspace_agent_log_sources_pkey" // ALTER TABLE ONLY workspace_agent_log_sources ADD CONSTRAINT workspace_agent_log_sources_pkey PRIMARY KEY (workspace_agent_id, id);
UniqueWorkspaceAgentMetadataPkey UniqueConstraint = "workspace_agent_metadata_pkey" // ALTER TABLE ONLY workspace_agent_metadata ADD CONSTRAINT workspace_agent_metadata_pkey PRIMARY KEY (workspace_agent_id, key);
UniqueWorkspaceAgentStartupLogsPkey UniqueConstraint = "workspace_agent_startup_logs_pkey" // ALTER TABLE ONLY workspace_agent_logs ADD CONSTRAINT workspace_agent_startup_logs_pkey PRIMARY KEY (id);
UniqueWorkspaceAgentsPkey UniqueConstraint = "workspace_agents_pkey" // ALTER TABLE ONLY workspace_agents ADD CONSTRAINT workspace_agents_pkey PRIMARY KEY (id);
UniqueWorkspaceAppStatsPkey UniqueConstraint = "workspace_app_stats_pkey" // ALTER TABLE ONLY workspace_app_stats ADD CONSTRAINT workspace_app_stats_pkey PRIMARY KEY (id);
UniqueWorkspaceAppStatsUserIDAgentIDSessionIDKey UniqueConstraint = "workspace_app_stats_user_id_agent_id_session_id_key" // ALTER TABLE ONLY workspace_app_stats ADD CONSTRAINT workspace_app_stats_user_id_agent_id_session_id_key UNIQUE (user_id, agent_id, session_id);
UniqueWorkspaceAppsAgentIDSlugIndex UniqueConstraint = "workspace_apps_agent_id_slug_idx" // ALTER TABLE ONLY workspace_apps ADD CONSTRAINT workspace_apps_agent_id_slug_idx UNIQUE (agent_id, slug);
UniqueWorkspaceAppsPkey UniqueConstraint = "workspace_apps_pkey" // ALTER TABLE ONLY workspace_apps ADD CONSTRAINT workspace_apps_pkey PRIMARY KEY (id);
UniqueWorkspaceBuildParametersWorkspaceBuildIDNameKey UniqueConstraint = "workspace_build_parameters_workspace_build_id_name_key" // ALTER TABLE ONLY workspace_build_parameters ADD CONSTRAINT workspace_build_parameters_workspace_build_id_name_key UNIQUE (workspace_build_id, name);
UniqueWorkspaceBuildsJobIDKey UniqueConstraint = "workspace_builds_job_id_key" // ALTER TABLE ONLY workspace_builds ADD CONSTRAINT workspace_builds_job_id_key UNIQUE (job_id);
UniqueWorkspaceBuildsPkey UniqueConstraint = "workspace_builds_pkey" // ALTER TABLE ONLY workspace_builds ADD CONSTRAINT workspace_builds_pkey PRIMARY KEY (id);
UniqueWorkspaceBuildsWorkspaceIDBuildNumberKey UniqueConstraint = "workspace_builds_workspace_id_build_number_key" // ALTER TABLE ONLY workspace_builds ADD CONSTRAINT workspace_builds_workspace_id_build_number_key UNIQUE (workspace_id, build_number);
UniqueWorkspaceProxiesPkey UniqueConstraint = "workspace_proxies_pkey" // ALTER TABLE ONLY workspace_proxies ADD CONSTRAINT workspace_proxies_pkey PRIMARY KEY (id);
UniqueWorkspaceProxiesRegionIDUnique UniqueConstraint = "workspace_proxies_region_id_unique" // ALTER TABLE ONLY workspace_proxies ADD CONSTRAINT workspace_proxies_region_id_unique UNIQUE (region_id);
UniqueWorkspaceResourceMetadataName UniqueConstraint = "workspace_resource_metadata_name" // ALTER TABLE ONLY workspace_resource_metadata ADD CONSTRAINT workspace_resource_metadata_name UNIQUE (workspace_resource_id, key);
UniqueWorkspaceResourceMetadataPkey UniqueConstraint = "workspace_resource_metadata_pkey" // ALTER TABLE ONLY workspace_resource_metadata ADD CONSTRAINT workspace_resource_metadata_pkey PRIMARY KEY (id);
UniqueWorkspaceResourcesPkey UniqueConstraint = "workspace_resources_pkey" // ALTER TABLE ONLY workspace_resources ADD CONSTRAINT workspace_resources_pkey PRIMARY KEY (id);
UniqueWorkspacesPkey UniqueConstraint = "workspaces_pkey" // ALTER TABLE ONLY workspaces ADD CONSTRAINT workspaces_pkey PRIMARY KEY (id);
UniqueIndexAPIKeyName UniqueConstraint = "idx_api_key_name" // CREATE UNIQUE INDEX idx_api_key_name ON api_keys USING btree (user_id, token_name) WHERE (login_type = 'token'::login_type);
UniqueIndexOrganizationName UniqueConstraint = "idx_organization_name" // CREATE UNIQUE INDEX idx_organization_name ON organizations USING btree (name);
UniqueIndexOrganizationNameLower UniqueConstraint = "idx_organization_name_lower" // CREATE UNIQUE INDEX idx_organization_name_lower ON organizations USING btree (lower(name));

View File

@ -1367,39 +1367,23 @@ func InsertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid.
}
}
// Set the default in case it was not provided (e.g. echo provider).
if prAgent.GetStartupScriptBehavior() == "" {
prAgent.StartupScriptBehavior = string(codersdk.WorkspaceAgentStartupScriptBehaviorNonBlocking)
}
agentID := uuid.New()
dbAgent, err := db.InsertWorkspaceAgent(ctx, database.InsertWorkspaceAgentParams{
ID: agentID,
CreatedAt: dbtime.Now(),
UpdatedAt: dbtime.Now(),
ResourceID: resource.ID,
Name: prAgent.Name,
AuthToken: authToken,
AuthInstanceID: instanceID,
Architecture: prAgent.Architecture,
EnvironmentVariables: env,
Directory: prAgent.Directory,
OperatingSystem: prAgent.OperatingSystem,
StartupScript: sql.NullString{
String: prAgent.StartupScript,
Valid: prAgent.StartupScript != "",
},
ConnectionTimeoutSeconds: prAgent.GetConnectionTimeoutSeconds(),
TroubleshootingURL: prAgent.GetTroubleshootingUrl(),
MOTDFile: prAgent.GetMotdFile(),
StartupScriptBehavior: database.StartupScriptBehavior(prAgent.GetStartupScriptBehavior()),
StartupScriptTimeoutSeconds: prAgent.GetStartupScriptTimeoutSeconds(),
ShutdownScript: sql.NullString{
String: prAgent.ShutdownScript,
Valid: prAgent.ShutdownScript != "",
},
ShutdownScriptTimeoutSeconds: prAgent.GetShutdownScriptTimeoutSeconds(),
DisplayApps: convertDisplayApps(prAgent.GetDisplayApps()),
ID: agentID,
CreatedAt: dbtime.Now(),
UpdatedAt: dbtime.Now(),
ResourceID: resource.ID,
Name: prAgent.Name,
AuthToken: authToken,
AuthInstanceID: instanceID,
Architecture: prAgent.Architecture,
EnvironmentVariables: env,
Directory: prAgent.Directory,
OperatingSystem: prAgent.OperatingSystem,
ConnectionTimeoutSeconds: prAgent.GetConnectionTimeoutSeconds(),
TroubleshootingURL: prAgent.GetTroubleshootingUrl(),
MOTDFile: prAgent.GetMotdFile(),
DisplayApps: convertDisplayApps(prAgent.GetDisplayApps()),
})
if err != nil {
return xerrors.Errorf("insert agent: %w", err)
@ -1421,6 +1405,57 @@ func InsertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid.
}
}
logSourceIDs := make([]uuid.UUID, 0, len(prAgent.Scripts))
logSourceDisplayNames := make([]string, 0, len(prAgent.Scripts))
logSourceIcons := make([]string, 0, len(prAgent.Scripts))
scriptLogPaths := make([]string, 0, len(prAgent.Scripts))
scriptSources := make([]string, 0, len(prAgent.Scripts))
scriptCron := make([]string, 0, len(prAgent.Scripts))
scriptTimeout := make([]int32, 0, len(prAgent.Scripts))
scriptStartBlocksLogin := make([]bool, 0, len(prAgent.Scripts))
scriptRunOnStart := make([]bool, 0, len(prAgent.Scripts))
scriptRunOnStop := make([]bool, 0, len(prAgent.Scripts))
for _, script := range prAgent.Scripts {
logSourceIDs = append(logSourceIDs, uuid.New())
logSourceDisplayNames = append(logSourceDisplayNames, script.DisplayName)
logSourceIcons = append(logSourceIcons, script.Icon)
scriptLogPaths = append(scriptLogPaths, script.LogPath)
scriptSources = append(scriptSources, script.Script)
scriptCron = append(scriptCron, script.Cron)
scriptTimeout = append(scriptTimeout, script.TimeoutSeconds)
scriptStartBlocksLogin = append(scriptStartBlocksLogin, script.StartBlocksLogin)
scriptRunOnStart = append(scriptRunOnStart, script.RunOnStart)
scriptRunOnStop = append(scriptRunOnStop, script.RunOnStop)
}
_, err = db.InsertWorkspaceAgentLogSources(ctx, database.InsertWorkspaceAgentLogSourcesParams{
WorkspaceAgentID: agentID,
ID: logSourceIDs,
CreatedAt: dbtime.Now(),
DisplayName: logSourceDisplayNames,
Icon: logSourceIcons,
})
if err != nil {
return xerrors.Errorf("insert agent log sources: %w", err)
}
_, err = db.InsertWorkspaceAgentScripts(ctx, database.InsertWorkspaceAgentScriptsParams{
WorkspaceAgentID: agentID,
LogSourceID: logSourceIDs,
LogPath: scriptLogPaths,
CreatedAt: dbtime.Now(),
Script: scriptSources,
Cron: scriptCron,
TimeoutSeconds: scriptTimeout,
StartBlocksLogin: scriptStartBlocksLogin,
RunOnStart: scriptRunOnStart,
RunOnStop: scriptRunOnStop,
})
if err != nil {
return xerrors.Errorf("insert agent scripts: %w", err)
}
for _, app := range prAgent.Apps {
slug := app.Slug
if slug == "" {

View File

@ -1576,7 +1576,6 @@ func TestInsertWorkspaceResource(t *testing.T) {
Env: map[string]string{
"something": "test",
},
StartupScript: "value",
OperatingSystem: "linux",
Architecture: "amd64",
Auth: &sdkproto.Agent_Token{
@ -1585,7 +1584,10 @@ func TestInsertWorkspaceResource(t *testing.T) {
Apps: []*sdkproto.App{{
Slug: "a",
}},
ShutdownScript: "shutdown",
Scripts: []*sdkproto.Script{{
DisplayName: "Startup",
Icon: "/test.png",
}},
DisplayApps: &sdkproto.DisplayApps{
Vscode: true,
PortForwardingHelper: true,
@ -1604,8 +1606,6 @@ func TestInsertWorkspaceResource(t *testing.T) {
agent := agents[0]
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",
})

View File

@ -124,6 +124,32 @@ func (api *API) provisionerJobResources(rw http.ResponseWriter, r *http.Request,
return
}
// nolint:gocritic // GetWorkspaceAgentScriptsByAgentIDs is a system function.
scripts, err := api.Database.GetWorkspaceAgentScriptsByAgentIDs(dbauthz.AsSystemRestricted(ctx), resourceAgentIDs)
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching workspace agent scripts.",
Detail: err.Error(),
})
return
}
// nolint:gocritic // GetWorkspaceAgentLogSourcesByAgentIDs is a system function.
logSources, err := api.Database.GetWorkspaceAgentLogSourcesByAgentIDs(dbauthz.AsSystemRestricted(ctx), resourceAgentIDs)
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching workspace agent log sources.",
Detail: err.Error(),
})
return
}
// nolint:gocritic // GetWorkspaceResourceMetadataByResourceIDs is a system function.
resourceMetadata, err := api.Database.GetWorkspaceResourceMetadataByResourceIDs(dbauthz.AsSystemRestricted(ctx), resourceIDs)
if err != nil {
@ -147,9 +173,21 @@ func (api *API) provisionerJobResources(rw http.ResponseWriter, r *http.Request,
dbApps = append(dbApps, app)
}
}
dbScripts := make([]database.WorkspaceAgentScript, 0)
for _, script := range scripts {
if script.WorkspaceAgentID == agent.ID {
dbScripts = append(dbScripts, script)
}
}
dbLogSources := make([]database.WorkspaceAgentLogSource, 0)
for _, logSource := range logSources {
if logSource.WorkspaceAgentID == agent.ID {
dbLogSources = append(dbLogSources, logSource)
}
}
apiAgent, err := convertWorkspaceAgent(
api.DERPMap(), *api.TailnetCoordinator.Load(), agent, convertProvisionedApps(dbApps), api.AgentInactiveDisconnectTimeout,
api.DERPMap(), *api.TailnetCoordinator.Load(), agent, convertProvisionedApps(dbApps), convertScripts(dbScripts), convertLogSources(dbLogSources), api.AgentInactiveDisconnectTimeout,
api.DeploymentValues.AgentFallbackTroubleshootingURL.String(),
)
if err != nil {

View File

@ -559,10 +559,8 @@ func ConvertWorkspaceAgent(agent database.WorkspaceAgent) WorkspaceAgent {
Architecture: agent.Architecture,
OperatingSystem: agent.OperatingSystem,
EnvironmentVariables: agent.EnvironmentVariables.Valid,
StartupScript: agent.StartupScript.Valid,
Directory: agent.Directory != "",
ConnectionTimeoutSeconds: agent.ConnectionTimeoutSeconds,
ShutdownScript: agent.ShutdownScript.Valid,
Subsystems: subsystems,
}
if agent.FirstConnectedAt.Valid {
@ -792,13 +790,11 @@ type WorkspaceAgent struct {
Architecture string `json:"architecture"`
OperatingSystem string `json:"operating_system"`
EnvironmentVariables bool `json:"environment_variables"`
StartupScript bool `json:"startup_script"`
Directory bool `json:"directory"`
FirstConnectedAt *time.Time `json:"first_connected_at"`
LastConnectedAt *time.Time `json:"last_connected_at"`
DisconnectedAt *time.Time `json:"disconnected_at"`
ConnectionTimeoutSeconds int32 `json:"connection_timeout_seconds"`
ShutdownScript bool `json:"shutdown_script"`
Subsystems []string `json:"subsystems"`
}

View File

@ -56,10 +56,29 @@ func (api *API) workspaceAgent(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
workspaceAgent := httpmw.WorkspaceAgentParam(r)
dbApps, err := api.Database.GetWorkspaceAppsByAgentID(ctx, workspaceAgent.ID)
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
var (
dbApps []database.WorkspaceApp
scripts []database.WorkspaceAgentScript
logSources []database.WorkspaceAgentLogSource
)
var eg errgroup.Group
eg.Go(func() (err error) {
dbApps, err = api.Database.GetWorkspaceAppsByAgentID(ctx, workspaceAgent.ID)
return err
})
eg.Go(func() (err error) {
scripts, err = api.Database.GetWorkspaceAgentScriptsByAgentIDs(ctx, []uuid.UUID{workspaceAgent.ID})
return err
})
eg.Go(func() (err error) {
logSources, err = api.Database.GetWorkspaceAgentLogSourcesByAgentIDs(ctx, []uuid.UUID{workspaceAgent.ID})
return err
})
err := eg.Wait()
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching workspace agent applications.",
Message: "Internal error fetching workspace agent.",
Detail: err.Error(),
})
return
@ -99,7 +118,7 @@ func (api *API) workspaceAgent(rw http.ResponseWriter, r *http.Request) {
}
apiAgent, err := convertWorkspaceAgent(
api.DERPMap(), *api.TailnetCoordinator.Load(), workspaceAgent, convertApps(dbApps, workspaceAgent, owner, workspace), api.AgentInactiveDisconnectTimeout,
api.DERPMap(), *api.TailnetCoordinator.Load(), workspaceAgent, convertApps(dbApps, workspaceAgent, owner, workspace), convertScripts(scripts), convertLogSources(logSources), api.AgentInactiveDisconnectTimeout,
api.DeploymentValues.AgentFallbackTroubleshootingURL.String(),
)
if err != nil {
@ -124,7 +143,7 @@ func (api *API) workspaceAgentManifest(rw http.ResponseWriter, r *http.Request)
ctx := r.Context()
workspaceAgent := httpmw.WorkspaceAgent(r)
apiAgent, err := convertWorkspaceAgent(
api.DERPMap(), *api.TailnetCoordinator.Load(), workspaceAgent, nil, api.AgentInactiveDisconnectTimeout,
api.DERPMap(), *api.TailnetCoordinator.Load(), workspaceAgent, nil, nil, nil, api.AgentInactiveDisconnectTimeout,
api.DeploymentValues.AgentFallbackTroubleshootingURL.String(),
)
if err != nil {
@ -134,52 +153,57 @@ func (api *API) workspaceAgentManifest(rw http.ResponseWriter, r *http.Request)
})
return
}
dbApps, err := api.Database.GetWorkspaceAppsByAgentID(ctx, workspaceAgent.ID)
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching workspace agent applications.",
Detail: err.Error(),
})
return
}
metadata, err := api.Database.GetWorkspaceAgentMetadata(ctx, workspaceAgent.ID)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching workspace agent metadata.",
Detail: err.Error(),
})
return
}
var (
dbApps []database.WorkspaceApp
scripts []database.WorkspaceAgentScript
metadata []database.WorkspaceAgentMetadatum
resource database.WorkspaceResource
build database.WorkspaceBuild
workspace database.Workspace
owner database.User
)
resource, err := api.Database.GetWorkspaceResourceByID(ctx, workspaceAgent.ResourceID)
var eg errgroup.Group
eg.Go(func() (err error) {
dbApps, err = api.Database.GetWorkspaceAppsByAgentID(ctx, workspaceAgent.ID)
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
return err
}
return nil
})
eg.Go(func() (err error) {
// nolint:gocritic // This is necessary to fetch agent scripts!
scripts, err = api.Database.GetWorkspaceAgentScriptsByAgentIDs(dbauthz.AsSystemRestricted(ctx), []uuid.UUID{workspaceAgent.ID})
return err
})
eg.Go(func() (err error) {
metadata, err = api.Database.GetWorkspaceAgentMetadata(ctx, workspaceAgent.ID)
return err
})
eg.Go(func() (err error) {
resource, err = api.Database.GetWorkspaceResourceByID(ctx, workspaceAgent.ResourceID)
if err != nil {
return xerrors.Errorf("getting resource by id: %w", err)
}
build, err = api.Database.GetWorkspaceBuildByJobID(ctx, resource.JobID)
if err != nil {
return xerrors.Errorf("getting workspace build by job id: %w", err)
}
workspace, err = api.Database.GetWorkspaceByID(ctx, build.WorkspaceID)
if err != nil {
return xerrors.Errorf("getting workspace by id: %w", err)
}
owner, err = api.Database.GetUserByID(ctx, workspace.OwnerID)
if err != nil {
return xerrors.Errorf("getting workspace owner by id: %w", err)
}
return err
})
err = eg.Wait()
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching workspace resource.",
Detail: err.Error(),
})
return
}
build, err := api.Database.GetWorkspaceBuildByJobID(ctx, resource.JobID)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching workspace build.",
Detail: err.Error(),
})
return
}
workspace, err := api.Database.GetWorkspaceByID(ctx, build.WorkspaceID)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching workspace.",
Detail: err.Error(),
})
return
}
owner, err := api.Database.GetUserByID(ctx, workspace.OwnerID)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching workspace owner.",
Message: "Internal error fetching workspace agent manifest.",
Detail: err.Error(),
})
return
@ -200,17 +224,14 @@ func (api *API) workspaceAgentManifest(rw http.ResponseWriter, r *http.Request)
httpapi.Write(ctx, rw, http.StatusOK, agentsdk.Manifest{
AgentID: apiAgent.ID,
Apps: convertApps(dbApps, workspaceAgent, owner, workspace),
Scripts: convertScripts(scripts),
DERPMap: api.DERPMap(),
DERPForceWebSockets: api.DeploymentValues.DERP.Config.ForceWebSockets.Value(),
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,
DisableDirectConnections: api.DeploymentValues.DERP.Config.BlockDirect.Value(),
Metadata: convertWorkspaceAgentMetadataDesc(metadata),
})
@ -230,7 +251,7 @@ func (api *API) postWorkspaceAgentStartup(rw http.ResponseWriter, r *http.Reques
ctx := r.Context()
workspaceAgent := httpmw.WorkspaceAgent(r)
apiAgent, err := convertWorkspaceAgent(
api.DERPMap(), *api.TailnetCoordinator.Load(), workspaceAgent, nil, api.AgentInactiveDisconnectTimeout,
api.DERPMap(), *api.TailnetCoordinator.Load(), workspaceAgent, nil, nil, nil, api.AgentInactiveDisconnectTimeout,
api.DeploymentValues.AgentFallbackTroubleshootingURL.String(),
)
if err != nil {
@ -321,13 +342,37 @@ func (api *API) patchWorkspaceAgentLogs(rw http.ResponseWriter, r *http.Request)
})
return
}
createdAt := make([]time.Time, 0)
// This is to support the legacy API where the log source ID was
// not provided in the request body. We default to the external
// log source in this case.
if req.LogSourceID == uuid.Nil {
// Use the external log source
externalSources, err := api.Database.InsertWorkspaceAgentLogSources(ctx, database.InsertWorkspaceAgentLogSourcesParams{
WorkspaceAgentID: workspaceAgent.ID,
CreatedAt: dbtime.Now(),
ID: []uuid.UUID{agentsdk.ExternalLogSourceID},
DisplayName: []string{"External"},
Icon: []string{"/emojis/1f310.png"},
})
if database.IsUniqueViolation(err, database.UniqueWorkspaceAgentLogSourcesPkey) {
err = nil
req.LogSourceID = agentsdk.ExternalLogSourceID
}
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Failed to create external log source.",
Detail: err.Error(),
})
return
}
if len(externalSources) == 1 {
req.LogSourceID = externalSources[0].ID
}
}
output := make([]string, 0)
level := make([]database.LogLevel, 0)
source := make([]database.WorkspaceAgentLogSource, 0)
outputLength := 0
for _, logEntry := range req.Logs {
createdAt = append(createdAt, logEntry.CreatedAt)
output = append(output, logEntry.Output)
outputLength += len(logEntry.Output)
if logEntry.Level == "" {
@ -343,28 +388,14 @@ func (api *API) patchWorkspaceAgentLogs(rw http.ResponseWriter, r *http.Request)
return
}
level = append(level, parsedLevel)
if logEntry.Source == "" {
// Default to "startup_script" to support older agents that didn't have the source field.
logEntry.Source = codersdk.WorkspaceAgentLogSourceStartupScript
}
parsedSource := database.WorkspaceAgentLogSource(logEntry.Source)
if !parsedSource.Valid() {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Invalid log source provided.",
Detail: fmt.Sprintf("invalid log source: %q", logEntry.Source),
})
return
}
source = append(source, parsedSource)
}
logs, err := api.Database.InsertWorkspaceAgentLogs(ctx, database.InsertWorkspaceAgentLogsParams{
AgentID: workspaceAgent.ID,
CreatedAt: createdAt,
CreatedAt: dbtime.Now(),
Output: output,
Level: level,
Source: source,
LogSourceID: req.LogSourceID,
OutputLength: int32(outputLength),
})
if err != nil {
@ -734,7 +765,7 @@ func (api *API) workspaceAgentListeningPorts(rw http.ResponseWriter, r *http.Req
workspaceAgent := httpmw.WorkspaceAgentParam(r)
apiAgent, err := convertWorkspaceAgent(
api.DERPMap(), *api.TailnetCoordinator.Load(), workspaceAgent, nil, api.AgentInactiveDisconnectTimeout,
api.DERPMap(), *api.TailnetCoordinator.Load(), workspaceAgent, nil, nil, nil, api.AgentInactiveDisconnectTimeout,
api.DeploymentValues.AgentFallbackTroubleshootingURL.String(),
)
if err != nil {
@ -1403,6 +1434,37 @@ func convertApps(dbApps []database.WorkspaceApp, agent database.WorkspaceAgent,
return apps
}
func convertLogSources(dbLogSources []database.WorkspaceAgentLogSource) []codersdk.WorkspaceAgentLogSource {
logSources := make([]codersdk.WorkspaceAgentLogSource, 0)
for _, dbLogSource := range dbLogSources {
logSources = append(logSources, codersdk.WorkspaceAgentLogSource{
ID: dbLogSource.ID,
DisplayName: dbLogSource.DisplayName,
WorkspaceAgentID: dbLogSource.WorkspaceAgentID,
CreatedAt: dbLogSource.CreatedAt,
Icon: dbLogSource.Icon,
})
}
return logSources
}
func convertScripts(dbScripts []database.WorkspaceAgentScript) []codersdk.WorkspaceAgentScript {
scripts := make([]codersdk.WorkspaceAgentScript, 0)
for _, dbScript := range dbScripts {
scripts = append(scripts, codersdk.WorkspaceAgentScript{
LogPath: dbScript.LogPath,
LogSourceID: dbScript.LogSourceID,
Script: dbScript.Script,
Cron: dbScript.Cron,
RunOnStart: dbScript.RunOnStart,
RunOnStop: dbScript.RunOnStop,
StartBlocksLogin: dbScript.StartBlocksLogin,
Timeout: time.Duration(dbScript.TimeoutSeconds) * time.Second,
})
}
return scripts
}
func convertWorkspaceAgentMetadataDesc(mds []database.WorkspaceAgentMetadatum) []codersdk.WorkspaceAgentMetadataDescription {
metadata := make([]codersdk.WorkspaceAgentMetadataDescription, 0)
for _, datum := range mds {
@ -1417,7 +1479,10 @@ func convertWorkspaceAgentMetadataDesc(mds []database.WorkspaceAgentMetadatum) [
return metadata
}
func convertWorkspaceAgent(derpMap *tailcfg.DERPMap, coordinator tailnet.Coordinator, dbAgent database.WorkspaceAgent, apps []codersdk.WorkspaceApp, agentInactiveDisconnectTimeout time.Duration, agentFallbackTroubleshootingURL string) (codersdk.WorkspaceAgent, error) {
func convertWorkspaceAgent(derpMap *tailcfg.DERPMap, coordinator tailnet.Coordinator,
dbAgent database.WorkspaceAgent, apps []codersdk.WorkspaceApp, scripts []codersdk.WorkspaceAgentScript, logSources []codersdk.WorkspaceAgentLogSource,
agentInactiveDisconnectTimeout time.Duration, agentFallbackTroubleshootingURL string,
) (codersdk.WorkspaceAgent, error) {
var envs map[string]string
if dbAgent.EnvironmentVariables.Valid {
err := json.Unmarshal(dbAgent.EnvironmentVariables.RawMessage, &envs)
@ -1434,33 +1499,41 @@ func convertWorkspaceAgent(derpMap *tailcfg.DERPMap, coordinator tailnet.Coordin
subsystems[i] = codersdk.AgentSubsystem(subsystem)
}
legacyStartupScriptBehavior := codersdk.WorkspaceAgentStartupScriptBehaviorNonBlocking
for _, script := range scripts {
if !script.RunOnStart {
continue
}
if !script.StartBlocksLogin {
continue
}
legacyStartupScriptBehavior = codersdk.WorkspaceAgentStartupScriptBehaviorBlocking
}
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,
StartupScriptBehavior: codersdk.WorkspaceAgentStartupScriptBehavior(dbAgent.StartupScriptBehavior),
StartupScriptTimeoutSeconds: dbAgent.StartupScriptTimeoutSeconds,
LogsLength: dbAgent.LogsLength,
LogsOverflowed: dbAgent.LogsOverflowed,
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.StartupScriptBehavior != database.StartupScriptBehaviorBlocking,
ShutdownScript: dbAgent.ShutdownScript.String,
ShutdownScriptTimeoutSeconds: dbAgent.ShutdownScriptTimeoutSeconds,
Subsystems: subsystems,
DisplayApps: convertDisplayApps(dbAgent.DisplayApps),
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,
Scripts: scripts,
StartupScriptBehavior: legacyStartupScriptBehavior,
LogsLength: dbAgent.LogsLength,
LogsOverflowed: dbAgent.LogsOverflowed,
LogSources: logSources,
Version: dbAgent.Version,
EnvironmentVariables: envs,
Directory: dbAgent.Directory,
ExpandedDirectory: dbAgent.ExpandedDirectory,
Apps: apps,
ConnectionTimeoutSeconds: dbAgent.ConnectionTimeoutSeconds,
TroubleshootingURL: troubleshootingURL,
LifecycleState: codersdk.WorkspaceAgentLifecycle(dbAgent.LifecycleState),
Subsystems: subsystems,
DisplayApps: convertDisplayApps(dbAgent.DisplayApps),
}
node := coordinator.Node(dbAgent.ID)
if node != nil {
@ -2327,6 +2400,7 @@ func convertWorkspaceAgentLog(logEntry database.WorkspaceAgentLog) codersdk.Work
CreatedAt: logEntry.CreatedAt,
Output: logEntry.Output,
Level: codersdk.LogLevel(logEntry.Level),
SourceID: logEntry.LogSourceID,
}
}

View File

@ -268,7 +268,7 @@ func TestWorkspaceAgent(t *testing.T) {
})
}
func TestWorkspaceAgentStartupLogs(t *testing.T) {
func TestWorkspaceAgentLogs(t *testing.T) {
t.Parallel()
t.Run("Success", func(t *testing.T) {
t.Parallel()

View File

@ -12,6 +12,7 @@ import (
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"golang.org/x/exp/slices"
"golang.org/x/sync/errgroup"
"golang.org/x/xerrors"
"cdr.dev/slog"
@ -77,6 +78,8 @@ func (api *API) workspaceBuild(rw http.ResponseWriter, r *http.Request) {
data.metadata,
data.agents,
data.apps,
data.scripts,
data.logSources,
data.templateVersions[0],
)
if err != nil {
@ -191,6 +194,8 @@ func (api *API) workspaceBuilds(rw http.ResponseWriter, r *http.Request) {
data.metadata,
data.agents,
data.apps,
data.scripts,
data.logSources,
data.templateVersions,
)
if err != nil {
@ -279,6 +284,8 @@ func (api *API) workspaceBuildByBuildNumber(rw http.ResponseWriter, r *http.Requ
data.metadata,
data.agents,
data.apps,
data.scripts,
data.logSources,
data.templateVersions[0],
)
if err != nil {
@ -404,6 +411,8 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
[]database.WorkspaceResourceMetadatum{},
[]database.WorkspaceAgent{},
[]database.WorkspaceApp{},
[]database.WorkspaceAgentScript{},
[]database.WorkspaceAgentLogSource{},
database.TemplateVersion{},
)
if err != nil {
@ -637,6 +646,8 @@ type workspaceBuildsData struct {
metadata []database.WorkspaceResourceMetadatum
agents []database.WorkspaceAgent
apps []database.WorkspaceApp
scripts []database.WorkspaceAgentScript
logSources []database.WorkspaceAgentLogSource
}
func (api *API) workspaceBuildsData(ctx context.Context, workspaces []database.Workspace, workspaceBuilds []database.WorkspaceBuild) (workspaceBuildsData, error) {
@ -715,10 +726,31 @@ func (api *API) workspaceBuildsData(ctx context.Context, workspaces []database.W
agentIDs = append(agentIDs, agent.ID)
}
// nolint:gocritic // Getting workspace apps by agent IDs is a system function.
apps, err := api.Database.GetWorkspaceAppsByAgentIDs(dbauthz.AsSystemRestricted(ctx), agentIDs)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return workspaceBuildsData{}, xerrors.Errorf("fetching workspace apps: %w", err)
var (
apps []database.WorkspaceApp
scripts []database.WorkspaceAgentScript
logSources []database.WorkspaceAgentLogSource
)
var eg errgroup.Group
eg.Go(func() (err error) {
// nolint:gocritic // Getting workspace apps by agent IDs is a system function.
apps, err = api.Database.GetWorkspaceAppsByAgentIDs(dbauthz.AsSystemRestricted(ctx), agentIDs)
return err
})
eg.Go(func() (err error) {
// nolint:gocritic // Getting workspace scripts by agent IDs is a system function.
scripts, err = api.Database.GetWorkspaceAgentScriptsByAgentIDs(dbauthz.AsSystemRestricted(ctx), agentIDs)
return err
})
eg.Go(func() error {
// nolint:gocritic // Getting workspace agent log sources by agent IDs is a system function.
logSources, err = api.Database.GetWorkspaceAgentLogSourcesByAgentIDs(dbauthz.AsSystemRestricted(ctx), agentIDs)
return err
})
err = eg.Wait()
if err != nil {
return workspaceBuildsData{}, err
}
return workspaceBuildsData{
@ -729,6 +761,8 @@ func (api *API) workspaceBuildsData(ctx context.Context, workspaces []database.W
metadata: metadata,
agents: agents,
apps: apps,
scripts: scripts,
logSources: logSources,
}, nil
}
@ -741,6 +775,8 @@ func (api *API) convertWorkspaceBuilds(
resourceMetadata []database.WorkspaceResourceMetadatum,
resourceAgents []database.WorkspaceAgent,
agentApps []database.WorkspaceApp,
agentScripts []database.WorkspaceAgentScript,
agentLogSources []database.WorkspaceAgentLogSource,
templateVersions []database.TemplateVersion,
) ([]codersdk.WorkspaceBuild, error) {
workspaceByID := map[uuid.UUID]database.Workspace{}
@ -781,6 +817,8 @@ func (api *API) convertWorkspaceBuilds(
resourceMetadata,
resourceAgents,
agentApps,
agentScripts,
agentLogSources,
templateVersion,
)
if err != nil {
@ -802,6 +840,8 @@ func (api *API) convertWorkspaceBuild(
resourceMetadata []database.WorkspaceResourceMetadatum,
resourceAgents []database.WorkspaceAgent,
agentApps []database.WorkspaceApp,
agentScripts []database.WorkspaceAgentScript,
agentLogSources []database.WorkspaceAgentLogSource,
templateVersion database.TemplateVersion,
) (codersdk.WorkspaceBuild, error) {
userByID := map[uuid.UUID]database.User{}
@ -824,6 +864,14 @@ func (api *API) convertWorkspaceBuild(
for _, app := range agentApps {
appsByAgentID[app.AgentID] = append(appsByAgentID[app.AgentID], app)
}
scriptsByAgentID := map[uuid.UUID][]database.WorkspaceAgentScript{}
for _, script := range agentScripts {
scriptsByAgentID[script.WorkspaceAgentID] = append(scriptsByAgentID[script.WorkspaceAgentID], script)
}
logSourcesByAgentID := map[uuid.UUID][]database.WorkspaceAgentLogSource{}
for _, logSource := range agentLogSources {
logSourcesByAgentID[logSource.WorkspaceAgentID] = append(logSourcesByAgentID[logSource.WorkspaceAgentID], logSource)
}
owner, exists := userByID[workspace.OwnerID]
if !exists {
@ -837,8 +885,10 @@ func (api *API) convertWorkspaceBuild(
apiAgents := make([]codersdk.WorkspaceAgent, 0)
for _, agent := range agents {
apps := appsByAgentID[agent.ID]
scripts := scriptsByAgentID[agent.ID]
logSources := logSourcesByAgentID[agent.ID]
apiAgent, err := convertWorkspaceAgent(
api.DERPMap(), *api.TailnetCoordinator.Load(), agent, convertApps(apps, agent, owner, workspace), api.AgentInactiveDisconnectTimeout,
api.DERPMap(), *api.TailnetCoordinator.Load(), agent, convertApps(apps, agent, owner, workspace), convertScripts(scripts), convertLogSources(logSources), api.AgentInactiveDisconnectTimeout,
api.DeploymentValues.AgentFallbackTroubleshootingURL.String(),
)
if err != nil {

View File

@ -542,6 +542,8 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
[]database.WorkspaceResourceMetadatum{},
[]database.WorkspaceAgent{},
[]database.WorkspaceApp{},
[]database.WorkspaceAgentScript{},
[]database.WorkspaceAgentLogSource{},
database.TemplateVersion{},
)
if err != nil {
@ -1125,6 +1127,8 @@ func (api *API) workspaceData(ctx context.Context, workspaces []database.Workspa
data.metadata,
data.agents,
data.apps,
data.scripts,
data.logSources,
data.templateVersions,
)
if err != nil {

View File

@ -23,6 +23,13 @@ import (
"github.com/coder/retry"
)
// ExternalLogSourceID is the statically-defined ID of a log-source that
// appears as "External" in the dashboard.
//
// This is to support legacy API-consumers that do not create their own
// log-source. This should be removed in the future.
var ExternalLogSourceID = uuid.MustParse("3b579bf4-1ed8-4b99-87a8-e9a1e3410410")
// New returns a client that is used to interact with the
// Coder API from a workspace agent.
func New(serverURL *url.URL) *Client {
@ -91,14 +98,21 @@ type Manifest struct {
DERPMap *tailcfg.DERPMap `json:"derpmap"`
DERPForceWebSockets bool `json:"derp_force_websockets"`
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"`
DisableDirectConnections bool `json:"disable_direct_connections"`
Metadata []codersdk.WorkspaceAgentMetadataDescription `json:"metadata"`
Scripts []codersdk.WorkspaceAgentScript `json:"scripts"`
}
type LogSource struct {
ID uuid.UUID `json:"id"`
DisplayName string `json:"display_name"`
Icon string `json:"icon"`
}
type Script struct {
Script string `json:"script"`
}
// Manifest fetches manifest for the currently authenticated workspace agent.
@ -631,14 +645,14 @@ func (c *Client) PostStartup(ctx context.Context, req PostStartupRequest) error
}
type Log struct {
CreatedAt time.Time `json:"created_at"`
Output string `json:"output"`
Level codersdk.LogLevel `json:"level"`
Source codersdk.WorkspaceAgentLogSource `json:"source"`
CreatedAt time.Time `json:"created_at"`
Output string `json:"output"`
Level codersdk.LogLevel `json:"level"`
}
type PatchLogs struct {
Logs []Log `json:"logs"`
LogSourceID uuid.UUID `json:"log_source_id"`
Logs []Log `json:"logs"`
}
// PatchLogs writes log messages to the agent startup script.
@ -655,6 +669,29 @@ func (c *Client) PatchLogs(ctx context.Context, req PatchLogs) error {
return nil
}
type PostLogSource struct {
// ID is a unique identifier for the log source.
// It is scoped to a workspace agent, and can be statically
// defined inside code to prevent duplicate sources from being
// created for the same agent.
ID uuid.UUID `json:"id"`
DisplayName string `json:"display_name"`
Icon string `json:"icon"`
}
func (c *Client) PostLogSource(ctx context.Context, req PostLogSource) (codersdk.WorkspaceAgentLogSource, error) {
res, err := c.SDK.Request(ctx, http.MethodPost, "/api/v2/workspaceagents/me/log-source", req)
if err != nil {
return codersdk.WorkspaceAgentLogSource{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusCreated {
return codersdk.WorkspaceAgentLogSource{}, codersdk.ReadBodyAsError(res)
}
var logSource codersdk.WorkspaceAgentLogSource
return logSource, json.NewDecoder(res.Body).Decode(&logSource)
}
// GetServiceBanner relays the service banner config.
func (c *Client) GetServiceBanner(ctx context.Context) (codersdk.ServiceBannerConfig, error) {
res, err := c.SDK.Request(ctx, http.MethodGet, "/api/v2/appearance", nil)

View File

@ -10,6 +10,8 @@ import (
"golang.org/x/xerrors"
"github.com/google/uuid"
"cdr.dev/slog"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/retry"
@ -20,7 +22,7 @@ type startupLogsWriter struct {
ctx context.Context
send func(ctx context.Context, log ...Log) error
level codersdk.LogLevel
source codersdk.WorkspaceAgentLogSource
source uuid.UUID
}
func (w *startupLogsWriter) Write(p []byte) (int, error) {
@ -44,7 +46,6 @@ func (w *startupLogsWriter) Write(p []byte) (int, error) {
CreatedAt: time.Now().UTC(), // UTC, like dbtime.Now().
Level: w.level,
Output: string(partial) + string(p[:nl-cr]),
Source: w.source,
})
if err != nil {
return n - len(p), err
@ -67,24 +68,20 @@ func (w *startupLogsWriter) Close() error {
CreatedAt: time.Now().UTC(), // UTC, like dbtime.Now().
Level: w.level,
Output: w.buf.String(),
Source: w.source,
})
}
return nil
}
// StartupLogsWriter returns an io.WriteCloser that sends logs via the
// LogsWriter returns an io.WriteCloser that sends logs via the
// provided sender. The sender is expected to be non-blocking. Calling
// Close flushes any remaining partially written log lines but is
// otherwise no-op. If the context passed to StartupLogsWriter is
// otherwise no-op. If the context passed to LogsWriter is
// canceled, any remaining logs will be discarded.
//
// Neither Write nor Close is safe for concurrent use and must be used
// by a single goroutine.
func StartupLogsWriter(ctx context.Context, sender func(ctx context.Context, log ...Log) error, source codersdk.WorkspaceAgentLogSource, level codersdk.LogLevel) io.WriteCloser {
if source == "" {
source = codersdk.WorkspaceAgentLogSourceExternal
}
func LogsWriter(ctx context.Context, sender func(ctx context.Context, log ...Log) error, source uuid.UUID, level codersdk.LogLevel) io.WriteCloser {
return &startupLogsWriter{
ctx: ctx,
send: sender,
@ -98,7 +95,7 @@ func StartupLogsWriter(ctx context.Context, sender func(ctx context.Context, log
// has been called. Calling sendLog concurrently is not supported. If
// the context passed to flushAndClose is canceled, any remaining logs
// will be discarded.
func LogsSender(patchLogs func(ctx context.Context, req PatchLogs) error, logger slog.Logger) (sendLog func(ctx context.Context, log ...Log) error, flushAndClose func(context.Context) error) {
func LogsSender(sourceID uuid.UUID, patchLogs func(ctx context.Context, req PatchLogs) error, logger slog.Logger) (sendLog func(ctx context.Context, log ...Log) error, flushAndClose func(context.Context) error) {
// The main context is used to close the sender goroutine and cancel
// any outbound requests to the API. The shutdown context is used to
// signal the sender goroutine to flush logs and then exit.
@ -158,7 +155,8 @@ func LogsSender(patchLogs func(ctx context.Context, req PatchLogs) error, logger
// shutdown.
for r := retry.New(time.Second, 5*time.Second); r.Wait(ctx); {
err := patchLogs(ctx, PatchLogs{
Logs: backlog,
Logs: backlog,
LogSourceID: sourceID,
})
if err == nil {
break

View File

@ -7,6 +7,7 @@ import (
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/exp/slices"
@ -39,12 +40,10 @@ func TestStartupLogsWriter_Write(t *testing.T) {
ctx: context.Background(),
level: codersdk.LogLevelInfo,
writes: []string{"hello world\n"},
source: codersdk.WorkspaceAgentLogSourceShutdownScript,
want: []agentsdk.Log{
{
Level: codersdk.LogLevelInfo,
Output: "hello world",
Source: codersdk.WorkspaceAgentLogSourceShutdownScript,
},
},
},
@ -57,12 +56,10 @@ func TestStartupLogsWriter_Write(t *testing.T) {
{
Level: codersdk.LogLevelInfo,
Output: "hello world",
Source: codersdk.WorkspaceAgentLogSourceExternal,
},
{
Level: codersdk.LogLevelInfo,
Output: "goodbye world",
Source: codersdk.WorkspaceAgentLogSourceExternal,
},
},
},
@ -75,32 +72,26 @@ func TestStartupLogsWriter_Write(t *testing.T) {
{
Level: codersdk.LogLevelInfo,
Output: "",
Source: codersdk.WorkspaceAgentLogSourceExternal,
},
{
Level: codersdk.LogLevelInfo,
Output: "",
Source: codersdk.WorkspaceAgentLogSourceExternal,
},
{
Level: codersdk.LogLevelInfo,
Output: "hello world",
Source: codersdk.WorkspaceAgentLogSourceExternal,
},
{
Level: codersdk.LogLevelInfo,
Output: "",
Source: codersdk.WorkspaceAgentLogSourceExternal,
},
{
Level: codersdk.LogLevelInfo,
Output: "",
Source: codersdk.WorkspaceAgentLogSourceExternal,
},
{
Level: codersdk.LogLevelInfo,
Output: "goodbye world",
Source: codersdk.WorkspaceAgentLogSourceExternal,
},
},
},
@ -113,7 +104,6 @@ func TestStartupLogsWriter_Write(t *testing.T) {
{
Level: codersdk.LogLevelInfo,
Output: "hello world",
Source: codersdk.WorkspaceAgentLogSourceExternal,
},
},
},
@ -127,12 +117,10 @@ func TestStartupLogsWriter_Write(t *testing.T) {
{
Level: codersdk.LogLevelInfo,
Output: "hello world",
Source: codersdk.WorkspaceAgentLogSourceExternal,
},
{
Level: codersdk.LogLevelInfo,
Output: "goodbye world",
Source: codersdk.WorkspaceAgentLogSourceExternal,
},
},
},
@ -145,12 +133,10 @@ func TestStartupLogsWriter_Write(t *testing.T) {
{
Level: codersdk.LogLevelInfo,
Output: "hello world",
Source: codersdk.WorkspaceAgentLogSourceExternal,
},
{
Level: codersdk.LogLevelInfo,
Output: "goodbye world",
Source: codersdk.WorkspaceAgentLogSourceExternal,
},
},
},
@ -163,17 +149,14 @@ func TestStartupLogsWriter_Write(t *testing.T) {
{
Level: codersdk.LogLevelInfo,
Output: "hello world",
Source: codersdk.WorkspaceAgentLogSourceExternal,
},
{
Level: codersdk.LogLevelInfo,
Output: "\r",
Source: codersdk.WorkspaceAgentLogSourceExternal,
},
{
Level: codersdk.LogLevelInfo,
Output: "goodbye world",
Source: codersdk.WorkspaceAgentLogSourceExternal,
},
},
},
@ -201,7 +184,7 @@ func TestStartupLogsWriter_Write(t *testing.T) {
got = append(got, log...)
return nil
}
w := agentsdk.StartupLogsWriter(tt.ctx, send, tt.source, tt.level)
w := agentsdk.LogsWriter(tt.ctx, send, uuid.New(), tt.level)
for _, s := range tt.writes {
_, err := w.Write([]byte(s))
if err != nil {
@ -291,7 +274,7 @@ func TestStartupLogsSender(t *testing.T) {
return nil
}
sendLog, flushAndClose := agentsdk.LogsSender(patchLogs, slogtest.Make(t, nil).Leveled(slog.LevelDebug))
sendLog, flushAndClose := agentsdk.LogsSender(uuid.New(), patchLogs, slogtest.Make(t, nil).Leveled(slog.LevelDebug))
defer func() {
err := flushAndClose(ctx)
require.NoError(t, err)
@ -330,7 +313,7 @@ func TestStartupLogsSender(t *testing.T) {
return nil
}
sendLog, flushAndClose := agentsdk.LogsSender(patchLogs, slogtest.Make(t, nil).Leveled(slog.LevelDebug))
sendLog, flushAndClose := agentsdk.LogsSender(uuid.New(), patchLogs, slogtest.Make(t, nil).Leveled(slog.LevelDebug))
defer func() {
_ = flushAndClose(ctx)
}()
@ -361,7 +344,7 @@ func TestStartupLogsSender(t *testing.T) {
return nil
}
sendLog, flushAndClose := agentsdk.LogsSender(patchLogs, slogtest.Make(t, nil).Leveled(slog.LevelDebug))
sendLog, flushAndClose := agentsdk.LogsSender(uuid.New(), patchLogs, slogtest.Make(t, nil).Leveled(slog.LevelDebug))
defer func() {
_ = flushAndClose(ctx)
}()

View File

@ -58,7 +58,7 @@ const (
// Starting returns true if the agent is in the process of starting.
func (l WorkspaceAgentLifecycle) Starting() bool {
switch l {
case WorkspaceAgentLifecycleCreated, WorkspaceAgentLifecycleStarting, WorkspaceAgentLifecycleStartTimeout:
case WorkspaceAgentLifecycleCreated, WorkspaceAgentLifecycleStarting:
return true
default:
return false
@ -101,6 +101,7 @@ var WorkspaceAgentLifecycleOrder = []WorkspaceAgentLifecycle{
// ready (can be overridden).
//
// Presently, non-blocking is the default, but this may change in the future.
// Deprecated: `coder_script` allows configuration on a per-script basis.
type WorkspaceAgentStartupScriptBehavior string
const (
@ -144,42 +145,61 @@ const (
)
type WorkspaceAgent struct {
ID uuid.UUID `json:"id" format:"uuid"`
CreatedAt time.Time `json:"created_at" format:"date-time"`
UpdatedAt time.Time `json:"updated_at" format:"date-time"`
FirstConnectedAt *time.Time `json:"first_connected_at,omitempty" format:"date-time"`
LastConnectedAt *time.Time `json:"last_connected_at,omitempty" format:"date-time"`
DisconnectedAt *time.Time `json:"disconnected_at,omitempty" format:"date-time"`
StartedAt *time.Time `json:"started_at,omitempty" format:"date-time"`
ReadyAt *time.Time `json:"ready_at,omitempty" format:"date-time"`
Status WorkspaceAgentStatus `json:"status"`
LifecycleState WorkspaceAgentLifecycle `json:"lifecycle_state"`
Name string `json:"name"`
ResourceID uuid.UUID `json:"resource_id" format:"uuid"`
InstanceID string `json:"instance_id,omitempty"`
Architecture string `json:"architecture"`
EnvironmentVariables map[string]string `json:"environment_variables"`
OperatingSystem string `json:"operating_system"`
StartupScript string `json:"startup_script,omitempty"`
StartupScriptBehavior WorkspaceAgentStartupScriptBehavior `json:"startup_script_behavior"`
StartupScriptTimeoutSeconds int32 `json:"startup_script_timeout_seconds"` // 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.
LogsLength int32 `json:"logs_length"`
LogsOverflowed bool `json:"logs_overflowed"`
Directory string `json:"directory,omitempty"`
ExpandedDirectory string `json:"expanded_directory,omitempty"`
Version string `json:"version"`
Apps []WorkspaceApp `json:"apps"`
ID uuid.UUID `json:"id" format:"uuid"`
CreatedAt time.Time `json:"created_at" format:"date-time"`
UpdatedAt time.Time `json:"updated_at" format:"date-time"`
FirstConnectedAt *time.Time `json:"first_connected_at,omitempty" format:"date-time"`
LastConnectedAt *time.Time `json:"last_connected_at,omitempty" format:"date-time"`
DisconnectedAt *time.Time `json:"disconnected_at,omitempty" format:"date-time"`
StartedAt *time.Time `json:"started_at,omitempty" format:"date-time"`
ReadyAt *time.Time `json:"ready_at,omitempty" format:"date-time"`
Status WorkspaceAgentStatus `json:"status"`
LifecycleState WorkspaceAgentLifecycle `json:"lifecycle_state"`
Name string `json:"name"`
ResourceID uuid.UUID `json:"resource_id" format:"uuid"`
InstanceID string `json:"instance_id,omitempty"`
Architecture string `json:"architecture"`
EnvironmentVariables map[string]string `json:"environment_variables"`
OperatingSystem string `json:"operating_system"`
LogsLength int32 `json:"logs_length"`
LogsOverflowed bool `json:"logs_overflowed"`
Directory string `json:"directory,omitempty"`
ExpandedDirectory string `json:"expanded_directory,omitempty"`
Version string `json:"version"`
Apps []WorkspaceApp `json:"apps"`
// DERPLatency is mapped by region name (e.g. "New York City", "Seattle").
DERPLatency map[string]DERPRegion `json:"latency,omitempty"`
ConnectionTimeoutSeconds int32 `json:"connection_timeout_seconds"`
TroubleshootingURL string `json:"troubleshooting_url"`
// Deprecated: Use StartupScriptBehavior instead.
LoginBeforeReady bool `json:"login_before_ready"`
ShutdownScript string `json:"shutdown_script,omitempty"`
ShutdownScriptTimeoutSeconds int32 `json:"shutdown_script_timeout_seconds"`
Subsystems []AgentSubsystem `json:"subsystems"`
Health WorkspaceAgentHealth `json:"health"` // Health reports the health of the agent.
DisplayApps []DisplayApp `json:"display_apps"`
DERPLatency map[string]DERPRegion `json:"latency,omitempty"`
ConnectionTimeoutSeconds int32 `json:"connection_timeout_seconds"`
TroubleshootingURL string `json:"troubleshooting_url"`
Subsystems []AgentSubsystem `json:"subsystems"`
Health WorkspaceAgentHealth `json:"health"` // Health reports the health of the agent.
DisplayApps []DisplayApp `json:"display_apps"`
LogSources []WorkspaceAgentLogSource `json:"log_sources"`
Scripts []WorkspaceAgentScript `json:"scripts"`
// StartupScriptBehavior is a legacy field that is deprecated in favor
// of the `coder_script` resource. It's only referenced by old clients.
// Deprecated: Remove in the future!
StartupScriptBehavior WorkspaceAgentStartupScriptBehavior `json:"startup_script_behavior"`
}
type WorkspaceAgentLogSource struct {
WorkspaceAgentID uuid.UUID `json:"workspace_agent_id" format:"uuid"`
ID uuid.UUID `json:"id" format:"uuid"`
CreatedAt time.Time `json:"created_at" format:"date-time"`
DisplayName string `json:"display_name"`
Icon string `json:"icon"`
}
type WorkspaceAgentScript struct {
LogSourceID uuid.UUID `json:"log_source_id" format:"uuid"`
LogPath string `json:"log_path"`
Script string `json:"script"`
Cron string `json:"cron"`
RunOnStart bool `json:"run_on_start"`
RunOnStop bool `json:"run_on_stop"`
StartBlocksLogin bool `json:"start_blocks_login"`
Timeout time.Duration `json:"timeout"`
}
type WorkspaceAgentHealth struct {
@ -530,13 +550,6 @@ func (c *Client) WorkspaceAgent(ctx context.Context, id uuid.UUID) (WorkspaceAge
if err != nil {
return WorkspaceAgent{}, err
}
// Backwards compatibility for cases where the API is older then the client.
if workspaceAgent.StartupScriptBehavior == "" {
workspaceAgent.StartupScriptBehavior = WorkspaceAgentStartupScriptBehaviorNonBlocking
if !workspaceAgent.LoginBeforeReady {
workspaceAgent.StartupScriptBehavior = WorkspaceAgentStartupScriptBehaviorBlocking
}
}
return workspaceAgent, nil
}
@ -762,6 +775,7 @@ type WorkspaceAgentLog struct {
CreatedAt time.Time `json:"created_at" format:"date-time"`
Output string `json:"output"`
Level LogLevel `json:"level"`
SourceID uuid.UUID `json:"source_id" format:"uuid"`
}
type AgentSubsystem string
@ -780,14 +794,3 @@ func (s AgentSubsystem) Valid() bool {
return false
}
}
type WorkspaceAgentLogSource string
const (
WorkspaceAgentLogSourceStartupScript WorkspaceAgentLogSource = "startup_script"
WorkspaceAgentLogSourceShutdownScript WorkspaceAgentLogSource = "shutdown_script"
WorkspaceAgentLogSourceKubernetes WorkspaceAgentLogSource = "kubernetes"
WorkspaceAgentLogSourceEnvbox WorkspaceAgentLogSource = "envbox"
WorkspaceAgentLogSourceEnvbuilder WorkspaceAgentLogSource = "envbuilder"
WorkspaceAgentLogSourceExternal WorkspaceAgentLogSource = "external"
)

58
docs/api/agents.md generated
View File

@ -311,12 +311,12 @@ curl -X PATCH http://coder-server:8080/api/v2/workspaceagents/me/logs \
```json
{
"log_source_id": "string",
"logs": [
{
"created_at": "string",
"level": "trace",
"output": "string",
"source": "startup_script"
"output": "string"
}
]
}
@ -470,10 +470,18 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/me/manifest \
}
],
"motd_file": "string",
"shutdown_script": "string",
"shutdown_script_timeout": 0,
"startup_script": "string",
"startup_script_timeout": 0,
"scripts": [
{
"cron": "string",
"log_path": "string",
"log_source_id": "4197ab25-95cf-4b91-9c78-f7f2af5d353a",
"run_on_start": true,
"run_on_stop": true,
"script": "string",
"start_blocks_login": true,
"timeout": 0
}
],
"vscode_port_proxy_uri": "string"
}
```
@ -576,12 +584,12 @@ curl -X PATCH http://coder-server:8080/api/v2/workspaceagents/me/startup-logs \
```json
{
"log_source_id": "string",
"logs": [
{
"created_at": "string",
"level": "trace",
"output": "string",
"source": "startup_script"
"output": "string"
}
]
}
@ -693,19 +701,35 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/{workspaceagent} \
}
},
"lifecycle_state": "created",
"login_before_ready": true,
"log_sources": [
{
"created_at": "2019-08-24T14:15:22Z",
"display_name": "string",
"icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"workspace_agent_id": "7ad2e618-fea7-4c1a-b70a-f501566a72f1"
}
],
"logs_length": 0,
"logs_overflowed": true,
"name": "string",
"operating_system": "string",
"ready_at": "2019-08-24T14:15:22Z",
"resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f",
"shutdown_script": "string",
"shutdown_script_timeout_seconds": 0,
"scripts": [
{
"cron": "string",
"log_path": "string",
"log_source_id": "4197ab25-95cf-4b91-9c78-f7f2af5d353a",
"run_on_start": true,
"run_on_stop": true,
"script": "string",
"start_blocks_login": true,
"timeout": 0
}
],
"started_at": "2019-08-24T14:15:22Z",
"startup_script": "string",
"startup_script_behavior": "blocking",
"startup_script_timeout_seconds": 0,
"status": "connecting",
"subsystems": ["envbox"],
"troubleshooting_url": "string",
@ -921,7 +945,8 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/{workspaceagent}/log
"created_at": "2019-08-24T14:15:22Z",
"id": 0,
"level": "trace",
"output": "string"
"output": "string",
"source_id": "ae50a35c-df42-4eff-ba26-f8bc28d2af81"
}
]
```
@ -943,6 +968,7 @@ Status Code **200**
| `» id` | integer | false | | |
| `» level` | [codersdk.LogLevel](schemas.md#codersdkloglevel) | false | | |
| `» output` | string | false | | |
| `» source_id` | string(uuid) | false | | |
#### Enumerated Values
@ -1015,7 +1041,8 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/{workspaceagent}/sta
"created_at": "2019-08-24T14:15:22Z",
"id": 0,
"level": "trace",
"output": "string"
"output": "string",
"source_id": "ae50a35c-df42-4eff-ba26-f8bc28d2af81"
}
]
```
@ -1037,6 +1064,7 @@ Status Code **200**
| `» id` | integer | false | | |
| `» level` | [codersdk.LogLevel](schemas.md#codersdkloglevel) | false | | |
| `» output` | string | false | | |
| `» source_id` | string(uuid) | false | | |
#### Enumerated Values

532
docs/api/builds.md generated
View File

@ -108,19 +108,35 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam
}
},
"lifecycle_state": "created",
"login_before_ready": true,
"log_sources": [
{
"created_at": "2019-08-24T14:15:22Z",
"display_name": "string",
"icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"workspace_agent_id": "7ad2e618-fea7-4c1a-b70a-f501566a72f1"
}
],
"logs_length": 0,
"logs_overflowed": true,
"name": "string",
"operating_system": "string",
"ready_at": "2019-08-24T14:15:22Z",
"resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f",
"shutdown_script": "string",
"shutdown_script_timeout_seconds": 0,
"scripts": [
{
"cron": "string",
"log_path": "string",
"log_source_id": "4197ab25-95cf-4b91-9c78-f7f2af5d353a",
"run_on_start": true,
"run_on_stop": true,
"script": "string",
"start_blocks_login": true,
"timeout": 0
}
],
"started_at": "2019-08-24T14:15:22Z",
"startup_script": "string",
"startup_script_behavior": "blocking",
"startup_script_timeout_seconds": 0,
"status": "connecting",
"subsystems": ["envbox"],
"troubleshooting_url": "string",
@ -272,19 +288,35 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild} \
}
},
"lifecycle_state": "created",
"login_before_ready": true,
"log_sources": [
{
"created_at": "2019-08-24T14:15:22Z",
"display_name": "string",
"icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"workspace_agent_id": "7ad2e618-fea7-4c1a-b70a-f501566a72f1"
}
],
"logs_length": 0,
"logs_overflowed": true,
"name": "string",
"operating_system": "string",
"ready_at": "2019-08-24T14:15:22Z",
"resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f",
"shutdown_script": "string",
"shutdown_script_timeout_seconds": 0,
"scripts": [
{
"cron": "string",
"log_path": "string",
"log_source_id": "4197ab25-95cf-4b91-9c78-f7f2af5d353a",
"run_on_start": true,
"run_on_stop": true,
"script": "string",
"start_blocks_login": true,
"timeout": 0
}
],
"started_at": "2019-08-24T14:15:22Z",
"startup_script": "string",
"startup_script_behavior": "blocking",
"startup_script_timeout_seconds": 0,
"status": "connecting",
"subsystems": ["envbox"],
"troubleshooting_url": "string",
@ -575,19 +607,35 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild}/res
}
},
"lifecycle_state": "created",
"login_before_ready": true,
"log_sources": [
{
"created_at": "2019-08-24T14:15:22Z",
"display_name": "string",
"icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"workspace_agent_id": "7ad2e618-fea7-4c1a-b70a-f501566a72f1"
}
],
"logs_length": 0,
"logs_overflowed": true,
"name": "string",
"operating_system": "string",
"ready_at": "2019-08-24T14:15:22Z",
"resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f",
"shutdown_script": "string",
"shutdown_script_timeout_seconds": 0,
"scripts": [
{
"cron": "string",
"log_path": "string",
"log_source_id": "4197ab25-95cf-4b91-9c78-f7f2af5d353a",
"run_on_start": true,
"run_on_stop": true,
"script": "string",
"start_blocks_login": true,
"timeout": 0
}
],
"started_at": "2019-08-24T14:15:22Z",
"startup_script": "string",
"startup_script_behavior": "blocking",
"startup_script_timeout_seconds": 0,
"status": "connecting",
"subsystems": ["envbox"],
"troubleshooting_url": "string",
@ -625,78 +673,88 @@ 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. |
| `»»» subdomain_name` | string | false | | Subdomain name is the application domain exposed on the `coder server`. |
| `»»» 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 | | |
| `»» display_apps` | array | false | | |
| `»» environment_variables` | object | false | | |
| `»»» [any property]` | string | false | | |
| `»» expanded_directory` | string | false | | |
| `»» first_connected_at` | string(date-time) | false | | |
| `»» health` | [codersdk.WorkspaceAgentHealth](schemas.md#codersdkworkspaceagenthealth) | false | | Health reports the health of the agent. |
| `»»» healthy` | boolean | false | | Healthy is true if the agent is healthy. |
| `»»» reason` | string | false | | Reason is a human-readable explanation of the agent's health. It is empty if Healthy is true. |
| `»» 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 | | Deprecated: Use StartupScriptBehavior instead. |
| `»» logs_length` | integer | false | | |
| `»» logs_overflowed` | boolean | false | | |
| `»» name` | string | false | | |
| `»» operating_system` | string | false | | |
| `»» ready_at` | string(date-time) | false | | |
| `»» resource_id` | string(uuid) | false | | |
| `»» shutdown_script` | string | false | | |
| `»» shutdown_script_timeout_seconds` | integer | false | | |
| `»» started_at` | string(date-time) | false | | |
| `»» startup_script` | string | false | | |
| `»» startup_script_behavior` | [codersdk.WorkspaceAgentStartupScriptBehavior](schemas.md#codersdkworkspaceagentstartupscriptbehavior) | 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 | | |
| `»» subsystems` | array | 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. |
| `»»» subdomain_name` | string | false | | Subdomain name is the application domain exposed on the `coder server`. |
| `»»» 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 | | |
| `»» display_apps` | array | false | | |
| `»» environment_variables` | object | false | | |
| `»»» [any property]` | string | false | | |
| `»» expanded_directory` | string | false | | |
| `»» first_connected_at` | string(date-time) | false | | |
| `»» health` | [codersdk.WorkspaceAgentHealth](schemas.md#codersdkworkspaceagenthealth) | false | | Health reports the health of the agent. |
| `»»» healthy` | boolean | false | | Healthy is true if the agent is healthy. |
| `»»» reason` | string | false | | Reason is a human-readable explanation of the agent's health. It is empty if Healthy is true. |
| `»» 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 | | |
| `»» log_sources` | array | false | | |
| `»»» created_at` | string(date-time) | false | | |
| `»»» display_name` | string | false | | |
| `»»» icon` | string | false | | |
| `»»» id` | string(uuid) | false | | |
| `»»» workspace_agent_id` | string(uuid) | false | | |
| `»» logs_length` | integer | false | | |
| `»» logs_overflowed` | boolean | false | | |
| `»» name` | string | false | | |
| `»» operating_system` | string | false | | |
| `»» ready_at` | string(date-time) | false | | |
| `»» resource_id` | string(uuid) | false | | |
| `»» scripts` | array | false | | |
| `»»» cron` | string | false | | |
| `»»» log_path` | string | false | | |
| `»»» log_source_id` | string(uuid) | false | | |
| `»»» run_on_start` | boolean | false | | |
| `»»» run_on_stop` | boolean | false | | |
| `»»» script` | string | false | | |
| `»»» start_blocks_login` | boolean | false | | |
| `»»» timeout` | integer | false | | |
| `»» started_at` | string(date-time) | false | | |
| `»» startup_script_behavior` | [codersdk.WorkspaceAgentStartupScriptBehavior](schemas.md#codersdkworkspaceagentstartupscriptbehavior) | false | | Startup script behavior is a legacy field that is deprecated in favor of the `coder_script` resource. It's only referenced by old clients. Deprecated: Remove in the future! |
| `»» status` | [codersdk.WorkspaceAgentStatus](schemas.md#codersdkworkspaceagentstatus) | false | | |
| `»» subsystems` | array | 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
@ -836,19 +894,35 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild}/sta
}
},
"lifecycle_state": "created",
"login_before_ready": true,
"log_sources": [
{
"created_at": "2019-08-24T14:15:22Z",
"display_name": "string",
"icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"workspace_agent_id": "7ad2e618-fea7-4c1a-b70a-f501566a72f1"
}
],
"logs_length": 0,
"logs_overflowed": true,
"name": "string",
"operating_system": "string",
"ready_at": "2019-08-24T14:15:22Z",
"resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f",
"shutdown_script": "string",
"shutdown_script_timeout_seconds": 0,
"scripts": [
{
"cron": "string",
"log_path": "string",
"log_source_id": "4197ab25-95cf-4b91-9c78-f7f2af5d353a",
"run_on_start": true,
"run_on_stop": true,
"script": "string",
"start_blocks_login": true,
"timeout": 0
}
],
"started_at": "2019-08-24T14:15:22Z",
"startup_script": "string",
"startup_script_behavior": "blocking",
"startup_script_timeout_seconds": 0,
"status": "connecting",
"subsystems": ["envbox"],
"troubleshooting_url": "string",
@ -1005,19 +1079,35 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace}/builds \
}
},
"lifecycle_state": "created",
"login_before_ready": true,
"log_sources": [
{
"created_at": "2019-08-24T14:15:22Z",
"display_name": "string",
"icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"workspace_agent_id": "7ad2e618-fea7-4c1a-b70a-f501566a72f1"
}
],
"logs_length": 0,
"logs_overflowed": true,
"name": "string",
"operating_system": "string",
"ready_at": "2019-08-24T14:15:22Z",
"resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f",
"shutdown_script": "string",
"shutdown_script_timeout_seconds": 0,
"scripts": [
{
"cron": "string",
"log_path": "string",
"log_source_id": "4197ab25-95cf-4b91-9c78-f7f2af5d353a",
"run_on_start": true,
"run_on_stop": true,
"script": "string",
"start_blocks_login": true,
"timeout": 0
}
],
"started_at": "2019-08-24T14:15:22Z",
"startup_script": "string",
"startup_script_behavior": "blocking",
"startup_script_timeout_seconds": 0,
"status": "connecting",
"subsystems": ["envbox"],
"troubleshooting_url": "string",
@ -1066,112 +1156,122 @@ 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 | | |
| `»» error_code` | [codersdk.JobErrorCode](schemas.md#codersdkjoberrorcode) | false | | |
| `»» file_id` | string(uuid) | false | | |
| `»» id` | string(uuid) | false | | |
| `»» queue_position` | integer | false | | |
| `»» queue_size` | integer | 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 | | |
| `» max_deadline` | string(date-time) | 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. |
| `»»»» subdomain_name` | string | false | | Subdomain name is the application domain exposed on the `coder server`. |
| `»»»» 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 | | |
| `»»» display_apps` | array | false | | |
| `»»» environment_variables` | object | false | | |
| `»»»» [any property]` | string | false | | |
| `»»» expanded_directory` | string | false | | |
| `»»» first_connected_at` | string(date-time) | false | | |
| `»»» health` | [codersdk.WorkspaceAgentHealth](schemas.md#codersdkworkspaceagenthealth) | false | | Health reports the health of the agent. |
| `»»»» healthy` | boolean | false | | Healthy is true if the agent is healthy. |
| `»»»» reason` | string | false | | Reason is a human-readable explanation of the agent's health. It is empty if Healthy is true. |
| `»»» 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 | | Deprecated: Use StartupScriptBehavior instead. |
| `»»» logs_length` | integer | false | | |
| `»»» logs_overflowed` | boolean | false | | |
| `»»» name` | string | false | | |
| `»»» operating_system` | string | false | | |
| `»»» ready_at` | string(date-time) | false | | |
| `»»» resource_id` | string(uuid) | false | | |
| `»»» shutdown_script` | string | false | | |
| `»»» shutdown_script_timeout_seconds` | integer | false | | |
| `»»» started_at` | string(date-time) | false | | |
| `»»» startup_script` | string | false | | |
| `»»» startup_script_behavior` | [codersdk.WorkspaceAgentStartupScriptBehavior](schemas.md#codersdkworkspaceagentstartupscriptbehavior) | 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 | | |
| `»»» subsystems` | array | 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 | | |
| `»» error_code` | [codersdk.JobErrorCode](schemas.md#codersdkjoberrorcode) | false | | |
| `»» file_id` | string(uuid) | false | | |
| `»» id` | string(uuid) | false | | |
| `»» queue_position` | integer | false | | |
| `»» queue_size` | integer | 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 | | |
| `» max_deadline` | string(date-time) | 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. |
| `»»»» subdomain_name` | string | false | | Subdomain name is the application domain exposed on the `coder server`. |
| `»»»» 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 | | |
| `»»» display_apps` | array | false | | |
| `»»» environment_variables` | object | false | | |
| `»»»» [any property]` | string | false | | |
| `»»» expanded_directory` | string | false | | |
| `»»» first_connected_at` | string(date-time) | false | | |
| `»»» health` | [codersdk.WorkspaceAgentHealth](schemas.md#codersdkworkspaceagenthealth) | false | | Health reports the health of the agent. |
| `»»»» healthy` | boolean | false | | Healthy is true if the agent is healthy. |
| `»»»» reason` | string | false | | Reason is a human-readable explanation of the agent's health. It is empty if Healthy is true. |
| `»»» 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 | | |
| `»»» log_sources` | array | false | | |
| `»»»» created_at` | string(date-time) | false | | |
| `»»»» display_name` | string | false | | |
| `»»»» icon` | string | false | | |
| `»»»» id` | string(uuid) | false | | |
| `»»»» workspace_agent_id` | string(uuid) | false | | |
| `»»» logs_length` | integer | false | | |
| `»»» logs_overflowed` | boolean | false | | |
| `»»» name` | string | false | | |
| `»»» operating_system` | string | false | | |
| `»»» ready_at` | string(date-time) | false | | |
| `»»» resource_id` | string(uuid) | false | | |
| `»»» scripts` | array | false | | |
| `»»»» cron` | string | false | | |
| `»»»» log_path` | string | false | | |
| `»»»» log_source_id` | string(uuid) | false | | |
| `»»»» run_on_start` | boolean | false | | |
| `»»»» run_on_stop` | boolean | false | | |
| `»»»» script` | string | false | | |
| `»»»» start_blocks_login` | boolean | false | | |
| `»»»» timeout` | integer | false | | |
| `»»» started_at` | string(date-time) | false | | |
| `»»» startup_script_behavior` | [codersdk.WorkspaceAgentStartupScriptBehavior](schemas.md#codersdkworkspaceagentstartupscriptbehavior) | false | | Startup script behavior is a legacy field that is deprecated in favor of the `coder_script` resource. It's only referenced by old clients. Deprecated: Remove in the future! |
| `»»» status` | [codersdk.WorkspaceAgentStatus](schemas.md#codersdkworkspaceagentstatus) | false | | |
| `»»» subsystems` | array | 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
@ -1355,19 +1455,35 @@ curl -X POST http://coder-server:8080/api/v2/workspaces/{workspace}/builds \
}
},
"lifecycle_state": "created",
"login_before_ready": true,
"log_sources": [
{
"created_at": "2019-08-24T14:15:22Z",
"display_name": "string",
"icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"workspace_agent_id": "7ad2e618-fea7-4c1a-b70a-f501566a72f1"
}
],
"logs_length": 0,
"logs_overflowed": true,
"name": "string",
"operating_system": "string",
"ready_at": "2019-08-24T14:15:22Z",
"resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f",
"shutdown_script": "string",
"shutdown_script_timeout_seconds": 0,
"scripts": [
{
"cron": "string",
"log_path": "string",
"log_source_id": "4197ab25-95cf-4b91-9c78-f7f2af5d353a",
"run_on_start": true,
"run_on_stop": true,
"script": "string",
"start_blocks_login": true,
"timeout": 0
}
],
"started_at": "2019-08-24T14:15:22Z",
"startup_script": "string",
"startup_script_behavior": "blocking",
"startup_script_timeout_seconds": 0,
"status": "connecting",
"subsystems": ["envbox"],
"troubleshooting_url": "string",

306
docs/api/schemas.md generated
View File

@ -163,19 +163,17 @@
{
"created_at": "string",
"level": "trace",
"output": "string",
"source": "startup_script"
"output": "string"
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
| ------------ | -------------------------------------------------------------------- | -------- | ------------ | ----------- |
| `created_at` | string | false | | |
| `level` | [codersdk.LogLevel](#codersdkloglevel) | false | | |
| `output` | string | false | | |
| `source` | [codersdk.WorkspaceAgentLogSource](#codersdkworkspaceagentlogsource) | false | | |
| Name | Type | Required | Restrictions | Description |
| ------------ | -------------------------------------- | -------- | ------------ | ----------- |
| `created_at` | string | false | | |
| `level` | [codersdk.LogLevel](#codersdkloglevel) | false | | |
| `output` | string | false | | |
## agentsdk.Manifest
@ -279,10 +277,18 @@
}
],
"motd_file": "string",
"shutdown_script": "string",
"shutdown_script_timeout": 0,
"startup_script": "string",
"startup_script_timeout": 0,
"scripts": [
{
"cron": "string",
"log_path": "string",
"log_source_id": "4197ab25-95cf-4b91-9c78-f7f2af5d353a",
"run_on_start": true,
"run_on_stop": true,
"script": "string",
"start_blocks_login": true,
"timeout": 0
}
],
"vscode_port_proxy_uri": "string"
}
```
@ -302,22 +308,19 @@
| `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. |
| `metadata` | array of [codersdk.WorkspaceAgentMetadataDescription](#codersdkworkspaceagentmetadatadescription) | false | | |
| `motd_file` | string | false | | |
| `shutdown_script` | string | false | | |
| `shutdown_script_timeout` | integer | false | | |
| `startup_script` | string | false | | |
| `startup_script_timeout` | integer | false | | |
| `scripts` | array of [codersdk.WorkspaceAgentScript](#codersdkworkspaceagentscript) | false | | |
| `vscode_port_proxy_uri` | string | false | | |
## agentsdk.PatchLogs
```json
{
"log_source_id": "string",
"logs": [
{
"created_at": "string",
"level": "trace",
"output": "string",
"source": "startup_script"
"output": "string"
}
]
}
@ -325,9 +328,10 @@
### Properties
| Name | Type | Required | Restrictions | Description |
| ------ | ------------------------------------- | -------- | ------------ | ----------- |
| `logs` | array of [agentsdk.Log](#agentsdklog) | false | | |
| Name | Type | Required | Restrictions | Description |
| --------------- | ------------------------------------- | -------- | ------------ | ----------- |
| `log_source_id` | string | false | | |
| `logs` | array of [agentsdk.Log](#agentsdklog) | false | | |
## agentsdk.PostAppHealthsRequest
@ -5474,19 +5478,35 @@ If the schedule is empty, the user will be updated to use the default schedule.|
}
},
"lifecycle_state": "created",
"login_before_ready": true,
"log_sources": [
{
"created_at": "2019-08-24T14:15:22Z",
"display_name": "string",
"icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"workspace_agent_id": "7ad2e618-fea7-4c1a-b70a-f501566a72f1"
}
],
"logs_length": 0,
"logs_overflowed": true,
"name": "string",
"operating_system": "string",
"ready_at": "2019-08-24T14:15:22Z",
"resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f",
"shutdown_script": "string",
"shutdown_script_timeout_seconds": 0,
"scripts": [
{
"cron": "string",
"log_path": "string",
"log_source_id": "4197ab25-95cf-4b91-9c78-f7f2af5d353a",
"run_on_start": true,
"run_on_stop": true,
"script": "string",
"start_blocks_login": true,
"timeout": 0
}
],
"started_at": "2019-08-24T14:15:22Z",
"startup_script": "string",
"startup_script_behavior": "blocking",
"startup_script_timeout_seconds": 0,
"status": "connecting",
"subsystems": ["envbox"],
"troubleshooting_url": "string",
@ -5618,19 +5638,35 @@ If the schedule is empty, the user will be updated to use the default schedule.|
}
},
"lifecycle_state": "created",
"login_before_ready": true,
"log_sources": [
{
"created_at": "2019-08-24T14:15:22Z",
"display_name": "string",
"icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"workspace_agent_id": "7ad2e618-fea7-4c1a-b70a-f501566a72f1"
}
],
"logs_length": 0,
"logs_overflowed": true,
"name": "string",
"operating_system": "string",
"ready_at": "2019-08-24T14:15:22Z",
"resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f",
"shutdown_script": "string",
"shutdown_script_timeout_seconds": 0,
"scripts": [
{
"cron": "string",
"log_path": "string",
"log_source_id": "4197ab25-95cf-4b91-9c78-f7f2af5d353a",
"run_on_start": true,
"run_on_stop": true,
"script": "string",
"start_blocks_login": true,
"timeout": 0
}
],
"started_at": "2019-08-24T14:15:22Z",
"startup_script": "string",
"startup_script_behavior": "blocking",
"startup_script_timeout_seconds": 0,
"status": "connecting",
"subsystems": ["envbox"],
"troubleshooting_url": "string",
@ -5641,44 +5677,41 @@ If the schedule is empty, the user will be updated to use the default schedule.|
### 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 | | |
| `display_apps` | array of [codersdk.DisplayApp](#codersdkdisplayapp) | false | | |
| `environment_variables` | object | false | | |
| » `[any property]` | string | false | | |
| `expanded_directory` | string | false | | |
| `first_connected_at` | string | false | | |
| `health` | [codersdk.WorkspaceAgentHealth](#codersdkworkspaceagenthealth) | false | | Health reports the health of the agent. |
| `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 | | Deprecated: Use StartupScriptBehavior instead. |
| `logs_length` | integer | false | | |
| `logs_overflowed` | boolean | false | | |
| `name` | string | false | | |
| `operating_system` | string | false | | |
| `ready_at` | string | false | | |
| `resource_id` | string | false | | |
| `shutdown_script` | string | false | | |
| `shutdown_script_timeout_seconds` | integer | false | | |
| `started_at` | string | false | | |
| `startup_script` | string | false | | |
| `startup_script_behavior` | [codersdk.WorkspaceAgentStartupScriptBehavior](#codersdkworkspaceagentstartupscriptbehavior) | 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 | | |
| `subsystems` | array of [codersdk.AgentSubsystem](#codersdkagentsubsystem) | 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 | | |
| `display_apps` | array of [codersdk.DisplayApp](#codersdkdisplayapp) | false | | |
| `environment_variables` | object | false | | |
| » `[any property]` | string | false | | |
| `expanded_directory` | string | false | | |
| `first_connected_at` | string | false | | |
| `health` | [codersdk.WorkspaceAgentHealth](#codersdkworkspaceagenthealth) | false | | Health reports the health of the agent. |
| `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 | | |
| `log_sources` | array of [codersdk.WorkspaceAgentLogSource](#codersdkworkspaceagentlogsource) | false | | |
| `logs_length` | integer | false | | |
| `logs_overflowed` | boolean | false | | |
| `name` | string | false | | |
| `operating_system` | string | false | | |
| `ready_at` | string | false | | |
| `resource_id` | string | false | | |
| `scripts` | array of [codersdk.WorkspaceAgentScript](#codersdkworkspaceagentscript) | false | | |
| `started_at` | string | false | | |
| `startup_script_behavior` | [codersdk.WorkspaceAgentStartupScriptBehavior](#codersdkworkspaceagentstartupscriptbehavior) | false | | Startup script behavior is a legacy field that is deprecated in favor of the `coder_script` resource. It's only referenced by old clients. Deprecated: Remove in the future! |
| `status` | [codersdk.WorkspaceAgentStatus](#codersdkworkspaceagentstatus) | false | | |
| `subsystems` | array of [codersdk.AgentSubsystem](#codersdkagentsubsystem) | false | | |
| `troubleshooting_url` | string | false | | |
| `updated_at` | string | false | | |
| `version` | string | false | | |
## codersdk.WorkspaceAgentConnectionInfo
@ -5839,7 +5872,8 @@ If the schedule is empty, the user will be updated to use the default schedule.|
"created_at": "2019-08-24T14:15:22Z",
"id": 0,
"level": "trace",
"output": "string"
"output": "string",
"source_id": "ae50a35c-df42-4eff-ba26-f8bc28d2af81"
}
```
@ -5851,25 +5885,29 @@ If the schedule is empty, the user will be updated to use the default schedule.|
| `id` | integer | false | | |
| `level` | [codersdk.LogLevel](#codersdkloglevel) | false | | |
| `output` | string | false | | |
| `source_id` | string | false | | |
## codersdk.WorkspaceAgentLogSource
```json
"startup_script"
{
"created_at": "2019-08-24T14:15:22Z",
"display_name": "string",
"icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"workspace_agent_id": "7ad2e618-fea7-4c1a-b70a-f501566a72f1"
}
```
### Properties
#### Enumerated Values
| Value |
| ----------------- |
| `startup_script` |
| `shutdown_script` |
| `kubernetes` |
| `envbox` |
| `envbuilder` |
| `external` |
| Name | Type | Required | Restrictions | Description |
| -------------------- | ------ | -------- | ------------ | ----------- |
| `created_at` | string | false | | |
| `display_name` | string | false | | |
| `icon` | string | false | | |
| `id` | string | false | | |
| `workspace_agent_id` | string | false | | |
## codersdk.WorkspaceAgentMetadataDescription
@ -5893,6 +5931,34 @@ If the schedule is empty, the user will be updated to use the default schedule.|
| `script` | string | false | | |
| `timeout` | integer | false | | |
## codersdk.WorkspaceAgentScript
```json
{
"cron": "string",
"log_path": "string",
"log_source_id": "4197ab25-95cf-4b91-9c78-f7f2af5d353a",
"run_on_start": true,
"run_on_stop": true,
"script": "string",
"start_blocks_login": true,
"timeout": 0
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
| -------------------- | ------- | -------- | ------------ | ----------- |
| `cron` | string | false | | |
| `log_path` | string | false | | |
| `log_source_id` | string | false | | |
| `run_on_start` | boolean | false | | |
| `run_on_stop` | boolean | false | | |
| `script` | string | false | | |
| `start_blocks_login` | boolean | false | | |
| `timeout` | integer | false | | |
## codersdk.WorkspaceAgentStartupScriptBehavior
```json
@ -6091,19 +6157,35 @@ If the schedule is empty, the user will be updated to use the default schedule.|
}
},
"lifecycle_state": "created",
"login_before_ready": true,
"log_sources": [
{
"created_at": "2019-08-24T14:15:22Z",
"display_name": "string",
"icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"workspace_agent_id": "7ad2e618-fea7-4c1a-b70a-f501566a72f1"
}
],
"logs_length": 0,
"logs_overflowed": true,
"name": "string",
"operating_system": "string",
"ready_at": "2019-08-24T14:15:22Z",
"resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f",
"shutdown_script": "string",
"shutdown_script_timeout_seconds": 0,
"scripts": [
{
"cron": "string",
"log_path": "string",
"log_source_id": "4197ab25-95cf-4b91-9c78-f7f2af5d353a",
"run_on_start": true,
"run_on_stop": true,
"script": "string",
"start_blocks_login": true,
"timeout": 0
}
],
"started_at": "2019-08-24T14:15:22Z",
"startup_script": "string",
"startup_script_behavior": "blocking",
"startup_script_timeout_seconds": 0,
"status": "connecting",
"subsystems": ["envbox"],
"troubleshooting_url": "string",
@ -6404,19 +6486,35 @@ If the schedule is empty, the user will be updated to use the default schedule.|
}
},
"lifecycle_state": "created",
"login_before_ready": true,
"log_sources": [
{
"created_at": "2019-08-24T14:15:22Z",
"display_name": "string",
"icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"workspace_agent_id": "7ad2e618-fea7-4c1a-b70a-f501566a72f1"
}
],
"logs_length": 0,
"logs_overflowed": true,
"name": "string",
"operating_system": "string",
"ready_at": "2019-08-24T14:15:22Z",
"resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f",
"shutdown_script": "string",
"shutdown_script_timeout_seconds": 0,
"scripts": [
{
"cron": "string",
"log_path": "string",
"log_source_id": "4197ab25-95cf-4b91-9c78-f7f2af5d353a",
"run_on_start": true,
"run_on_stop": true,
"script": "string",
"start_blocks_login": true,
"timeout": 0
}
],
"started_at": "2019-08-24T14:15:22Z",
"startup_script": "string",
"startup_script_behavior": "blocking",
"startup_script_timeout_seconds": 0,
"status": "connecting",
"subsystems": ["envbox"],
"troubleshooting_url": "string",
@ -6619,19 +6717,35 @@ If the schedule is empty, the user will be updated to use the default schedule.|
}
},
"lifecycle_state": "created",
"login_before_ready": true,
"log_sources": [
{
"created_at": "2019-08-24T14:15:22Z",
"display_name": "string",
"icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"workspace_agent_id": "7ad2e618-fea7-4c1a-b70a-f501566a72f1"
}
],
"logs_length": 0,
"logs_overflowed": true,
"name": "string",
"operating_system": "string",
"ready_at": "2019-08-24T14:15:22Z",
"resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f",
"shutdown_script": "string",
"shutdown_script_timeout_seconds": 0,
"scripts": [
{
"cron": "string",
"log_path": "string",
"log_source_id": "4197ab25-95cf-4b91-9c78-f7f2af5d353a",
"run_on_start": true,
"run_on_stop": true,
"script": "string",
"start_blocks_login": true,
"timeout": 0
}
],
"started_at": "2019-08-24T14:15:22Z",
"startup_script": "string",
"startup_script_behavior": "blocking",
"startup_script_timeout_seconds": 0,
"status": "connecting",
"subsystems": ["envbox"],
"troubleshooting_url": "string",

360
docs/api/templates.md generated
View File

@ -1619,19 +1619,35 @@ curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion}/d
}
},
"lifecycle_state": "created",
"login_before_ready": true,
"log_sources": [
{
"created_at": "2019-08-24T14:15:22Z",
"display_name": "string",
"icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"workspace_agent_id": "7ad2e618-fea7-4c1a-b70a-f501566a72f1"
}
],
"logs_length": 0,
"logs_overflowed": true,
"name": "string",
"operating_system": "string",
"ready_at": "2019-08-24T14:15:22Z",
"resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f",
"shutdown_script": "string",
"shutdown_script_timeout_seconds": 0,
"scripts": [
{
"cron": "string",
"log_path": "string",
"log_source_id": "4197ab25-95cf-4b91-9c78-f7f2af5d353a",
"run_on_start": true,
"run_on_stop": true,
"script": "string",
"start_blocks_login": true,
"timeout": 0
}
],
"started_at": "2019-08-24T14:15:22Z",
"startup_script": "string",
"startup_script_behavior": "blocking",
"startup_script_timeout_seconds": 0,
"status": "connecting",
"subsystems": ["envbox"],
"troubleshooting_url": "string",
@ -1669,78 +1685,88 @@ 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. |
| `»»» subdomain_name` | string | false | | Subdomain name is the application domain exposed on the `coder server`. |
| `»»» 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 | | |
| `»» display_apps` | array | false | | |
| `»» environment_variables` | object | false | | |
| `»»» [any property]` | string | false | | |
| `»» expanded_directory` | string | false | | |
| `»» first_connected_at` | string(date-time) | false | | |
| `»» health` | [codersdk.WorkspaceAgentHealth](schemas.md#codersdkworkspaceagenthealth) | false | | Health reports the health of the agent. |
| `»»» healthy` | boolean | false | | Healthy is true if the agent is healthy. |
| `»»» reason` | string | false | | Reason is a human-readable explanation of the agent's health. It is empty if Healthy is true. |
| `»» 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 | | Deprecated: Use StartupScriptBehavior instead. |
| `»» logs_length` | integer | false | | |
| `»» logs_overflowed` | boolean | false | | |
| `»» name` | string | false | | |
| `»» operating_system` | string | false | | |
| `»» ready_at` | string(date-time) | false | | |
| `»» resource_id` | string(uuid) | false | | |
| `»» shutdown_script` | string | false | | |
| `»» shutdown_script_timeout_seconds` | integer | false | | |
| `»» started_at` | string(date-time) | false | | |
| `»» startup_script` | string | false | | |
| `»» startup_script_behavior` | [codersdk.WorkspaceAgentStartupScriptBehavior](schemas.md#codersdkworkspaceagentstartupscriptbehavior) | 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 | | |
| `»» subsystems` | array | 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. |
| `»»» subdomain_name` | string | false | | Subdomain name is the application domain exposed on the `coder server`. |
| `»»» 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 | | |
| `»» display_apps` | array | false | | |
| `»» environment_variables` | object | false | | |
| `»»» [any property]` | string | false | | |
| `»» expanded_directory` | string | false | | |
| `»» first_connected_at` | string(date-time) | false | | |
| `»» health` | [codersdk.WorkspaceAgentHealth](schemas.md#codersdkworkspaceagenthealth) | false | | Health reports the health of the agent. |
| `»»» healthy` | boolean | false | | Healthy is true if the agent is healthy. |
| `»»» reason` | string | false | | Reason is a human-readable explanation of the agent's health. It is empty if Healthy is true. |
| `»» 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 | | |
| `»» log_sources` | array | false | | |
| `»»» created_at` | string(date-time) | false | | |
| `»»» display_name` | string | false | | |
| `»»» icon` | string | false | | |
| `»»» id` | string(uuid) | false | | |
| `»»» workspace_agent_id` | string(uuid) | false | | |
| `»» logs_length` | integer | false | | |
| `»» logs_overflowed` | boolean | false | | |
| `»» name` | string | false | | |
| `»» operating_system` | string | false | | |
| `»» ready_at` | string(date-time) | false | | |
| `»» resource_id` | string(uuid) | false | | |
| `»» scripts` | array | false | | |
| `»»» cron` | string | false | | |
| `»»» log_path` | string | false | | |
| `»»» log_source_id` | string(uuid) | false | | |
| `»»» run_on_start` | boolean | false | | |
| `»»» run_on_stop` | boolean | false | | |
| `»»» script` | string | false | | |
| `»»» start_blocks_login` | boolean | false | | |
| `»»» timeout` | integer | false | | |
| `»» started_at` | string(date-time) | false | | |
| `»» startup_script_behavior` | [codersdk.WorkspaceAgentStartupScriptBehavior](schemas.md#codersdkworkspaceagentstartupscriptbehavior) | false | | Startup script behavior is a legacy field that is deprecated in favor of the `coder_script` resource. It's only referenced by old clients. Deprecated: Remove in the future! |
| `»» status` | [codersdk.WorkspaceAgentStatus](schemas.md#codersdkworkspaceagentstatus) | false | | |
| `»» subsystems` | array | 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
@ -2014,19 +2040,35 @@ curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion}/r
}
},
"lifecycle_state": "created",
"login_before_ready": true,
"log_sources": [
{
"created_at": "2019-08-24T14:15:22Z",
"display_name": "string",
"icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"workspace_agent_id": "7ad2e618-fea7-4c1a-b70a-f501566a72f1"
}
],
"logs_length": 0,
"logs_overflowed": true,
"name": "string",
"operating_system": "string",
"ready_at": "2019-08-24T14:15:22Z",
"resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f",
"shutdown_script": "string",
"shutdown_script_timeout_seconds": 0,
"scripts": [
{
"cron": "string",
"log_path": "string",
"log_source_id": "4197ab25-95cf-4b91-9c78-f7f2af5d353a",
"run_on_start": true,
"run_on_stop": true,
"script": "string",
"start_blocks_login": true,
"timeout": 0
}
],
"started_at": "2019-08-24T14:15:22Z",
"startup_script": "string",
"startup_script_behavior": "blocking",
"startup_script_timeout_seconds": 0,
"status": "connecting",
"subsystems": ["envbox"],
"troubleshooting_url": "string",
@ -2064,78 +2106,88 @@ 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. |
| `»»» subdomain_name` | string | false | | Subdomain name is the application domain exposed on the `coder server`. |
| `»»» 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 | | |
| `»» display_apps` | array | false | | |
| `»» environment_variables` | object | false | | |
| `»»» [any property]` | string | false | | |
| `»» expanded_directory` | string | false | | |
| `»» first_connected_at` | string(date-time) | false | | |
| `»» health` | [codersdk.WorkspaceAgentHealth](schemas.md#codersdkworkspaceagenthealth) | false | | Health reports the health of the agent. |
| `»»» healthy` | boolean | false | | Healthy is true if the agent is healthy. |
| `»»» reason` | string | false | | Reason is a human-readable explanation of the agent's health. It is empty if Healthy is true. |
| `»» 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 | | Deprecated: Use StartupScriptBehavior instead. |
| `»» logs_length` | integer | false | | |
| `»» logs_overflowed` | boolean | false | | |
| `»» name` | string | false | | |
| `»» operating_system` | string | false | | |
| `»» ready_at` | string(date-time) | false | | |
| `»» resource_id` | string(uuid) | false | | |
| `»» shutdown_script` | string | false | | |
| `»» shutdown_script_timeout_seconds` | integer | false | | |
| `»» started_at` | string(date-time) | false | | |
| `»» startup_script` | string | false | | |
| `»» startup_script_behavior` | [codersdk.WorkspaceAgentStartupScriptBehavior](schemas.md#codersdkworkspaceagentstartupscriptbehavior) | 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 | | |
| `»» subsystems` | array | 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. |
| `»»» subdomain_name` | string | false | | Subdomain name is the application domain exposed on the `coder server`. |
| `»»» 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 | | |
| `»» display_apps` | array | false | | |
| `»» environment_variables` | object | false | | |
| `»»» [any property]` | string | false | | |
| `»» expanded_directory` | string | false | | |
| `»» first_connected_at` | string(date-time) | false | | |
| `»» health` | [codersdk.WorkspaceAgentHealth](schemas.md#codersdkworkspaceagenthealth) | false | | Health reports the health of the agent. |
| `»»» healthy` | boolean | false | | Healthy is true if the agent is healthy. |
| `»»» reason` | string | false | | Reason is a human-readable explanation of the agent's health. It is empty if Healthy is true. |
| `»» 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 | | |
| `»» log_sources` | array | false | | |
| `»»» created_at` | string(date-time) | false | | |
| `»»» display_name` | string | false | | |
| `»»» icon` | string | false | | |
| `»»» id` | string(uuid) | false | | |
| `»»» workspace_agent_id` | string(uuid) | false | | |
| `»» logs_length` | integer | false | | |
| `»» logs_overflowed` | boolean | false | | |
| `»» name` | string | false | | |
| `»» operating_system` | string | false | | |
| `»» ready_at` | string(date-time) | false | | |
| `»» resource_id` | string(uuid) | false | | |
| `»» scripts` | array | false | | |
| `»»» cron` | string | false | | |
| `»»» log_path` | string | false | | |
| `»»» log_source_id` | string(uuid) | false | | |
| `»»» run_on_start` | boolean | false | | |
| `»»» run_on_stop` | boolean | false | | |
| `»»» script` | string | false | | |
| `»»» start_blocks_login` | boolean | false | | |
| `»»» timeout` | integer | false | | |
| `»» started_at` | string(date-time) | false | | |
| `»» startup_script_behavior` | [codersdk.WorkspaceAgentStartupScriptBehavior](schemas.md#codersdkworkspaceagentstartupscriptbehavior) | false | | Startup script behavior is a legacy field that is deprecated in favor of the `coder_script` resource. It's only referenced by old clients. Deprecated: Remove in the future! |
| `»» status` | [codersdk.WorkspaceAgentStatus](schemas.md#codersdkworkspaceagentstatus) | false | | |
| `»» subsystems` | array | 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

130
docs/api/workspaces.md generated
View File

@ -138,19 +138,35 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/member
}
},
"lifecycle_state": "created",
"login_before_ready": true,
"log_sources": [
{
"created_at": "2019-08-24T14:15:22Z",
"display_name": "string",
"icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"workspace_agent_id": "7ad2e618-fea7-4c1a-b70a-f501566a72f1"
}
],
"logs_length": 0,
"logs_overflowed": true,
"name": "string",
"operating_system": "string",
"ready_at": "2019-08-24T14:15:22Z",
"resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f",
"shutdown_script": "string",
"shutdown_script_timeout_seconds": 0,
"scripts": [
{
"cron": "string",
"log_path": "string",
"log_source_id": "4197ab25-95cf-4b91-9c78-f7f2af5d353a",
"run_on_start": true,
"run_on_stop": true,
"script": "string",
"start_blocks_login": true,
"timeout": 0
}
],
"started_at": "2019-08-24T14:15:22Z",
"startup_script": "string",
"startup_script_behavior": "blocking",
"startup_script_timeout_seconds": 0,
"status": "connecting",
"subsystems": ["envbox"],
"troubleshooting_url": "string",
@ -329,19 +345,35 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam
}
},
"lifecycle_state": "created",
"login_before_ready": true,
"log_sources": [
{
"created_at": "2019-08-24T14:15:22Z",
"display_name": "string",
"icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"workspace_agent_id": "7ad2e618-fea7-4c1a-b70a-f501566a72f1"
}
],
"logs_length": 0,
"logs_overflowed": true,
"name": "string",
"operating_system": "string",
"ready_at": "2019-08-24T14:15:22Z",
"resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f",
"shutdown_script": "string",
"shutdown_script_timeout_seconds": 0,
"scripts": [
{
"cron": "string",
"log_path": "string",
"log_source_id": "4197ab25-95cf-4b91-9c78-f7f2af5d353a",
"run_on_start": true,
"run_on_stop": true,
"script": "string",
"start_blocks_login": true,
"timeout": 0
}
],
"started_at": "2019-08-24T14:15:22Z",
"startup_script": "string",
"startup_script_behavior": "blocking",
"startup_script_timeout_seconds": 0,
"status": "connecting",
"subsystems": ["envbox"],
"troubleshooting_url": "string",
@ -519,19 +551,35 @@ curl -X GET http://coder-server:8080/api/v2/workspaces \
}
},
"lifecycle_state": "created",
"login_before_ready": true,
"log_sources": [
{
"created_at": "2019-08-24T14:15:22Z",
"display_name": "string",
"icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"workspace_agent_id": "7ad2e618-fea7-4c1a-b70a-f501566a72f1"
}
],
"logs_length": 0,
"logs_overflowed": true,
"name": "string",
"operating_system": "string",
"ready_at": "2019-08-24T14:15:22Z",
"resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f",
"shutdown_script": "string",
"shutdown_script_timeout_seconds": 0,
"scripts": [
{
"cron": "string",
"log_path": "string",
"log_source_id": "4197ab25-95cf-4b91-9c78-f7f2af5d353a",
"run_on_start": true,
"run_on_stop": true,
"script": "string",
"start_blocks_login": true,
"timeout": 0
}
],
"started_at": "2019-08-24T14:15:22Z",
"startup_script": "string",
"startup_script_behavior": "blocking",
"startup_script_timeout_seconds": 0,
"status": "connecting",
"subsystems": ["envbox"],
"troubleshooting_url": "string",
@ -711,19 +759,35 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace} \
}
},
"lifecycle_state": "created",
"login_before_ready": true,
"log_sources": [
{
"created_at": "2019-08-24T14:15:22Z",
"display_name": "string",
"icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"workspace_agent_id": "7ad2e618-fea7-4c1a-b70a-f501566a72f1"
}
],
"logs_length": 0,
"logs_overflowed": true,
"name": "string",
"operating_system": "string",
"ready_at": "2019-08-24T14:15:22Z",
"resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f",
"shutdown_script": "string",
"shutdown_script_timeout_seconds": 0,
"scripts": [
{
"cron": "string",
"log_path": "string",
"log_source_id": "4197ab25-95cf-4b91-9c78-f7f2af5d353a",
"run_on_start": true,
"run_on_stop": true,
"script": "string",
"start_blocks_login": true,
"timeout": 0
}
],
"started_at": "2019-08-24T14:15:22Z",
"startup_script": "string",
"startup_script_behavior": "blocking",
"startup_script_timeout_seconds": 0,
"status": "connecting",
"subsystems": ["envbox"],
"troubleshooting_url": "string",
@ -982,19 +1046,35 @@ curl -X PUT http://coder-server:8080/api/v2/workspaces/{workspace}/dormant \
}
},
"lifecycle_state": "created",
"login_before_ready": true,
"log_sources": [
{
"created_at": "2019-08-24T14:15:22Z",
"display_name": "string",
"icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"workspace_agent_id": "7ad2e618-fea7-4c1a-b70a-f501566a72f1"
}
],
"logs_length": 0,
"logs_overflowed": true,
"name": "string",
"operating_system": "string",
"ready_at": "2019-08-24T14:15:22Z",
"resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f",
"shutdown_script": "string",
"shutdown_script_timeout_seconds": 0,
"scripts": [
{
"cron": "string",
"log_path": "string",
"log_source_id": "4197ab25-95cf-4b91-9c78-f7f2af5d353a",
"run_on_start": true,
"run_on_stop": true,
"script": "string",
"start_blocks_login": true,
"timeout": 0
}
],
"started_at": "2019-08-24T14:15:22Z",
"startup_script": "string",
"startup_script_behavior": "blocking",
"startup_script_timeout_seconds": 0,
"status": "connecting",
"subsystems": ["envbox"],
"troubleshooting_url": "string",

View File

@ -29,25 +29,26 @@ type agentMetadata struct {
// 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"`
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"`
// Deprecated, but remains here for backwards compatibility.
LoginBeforeReady bool `mapstructure:"login_before_ready"`
StartupScriptBehavior string `mapstructure:"startup_script_behavior"`
StartupScriptTimeoutSeconds int32 `mapstructure:"startup_script_timeout"`
ShutdownScript string `mapstructure:"shutdown_script"`
ShutdownScriptTimeoutSeconds int32 `mapstructure:"shutdown_script_timeout"`
Metadata []agentMetadata `mapstructure:"metadata"`
DisplayApps []agentDisplayAppsAttributes `mapstructure:"display_apps"`
StartupScript string `mapstructure:"startup_script"`
StartupScriptBehavior string `mapstructure:"startup_script_behavior"`
StartupScriptTimeoutSeconds int32 `mapstructure:"startup_script_timeout"`
LoginBeforeReady bool `mapstructure:"login_before_ready"`
ShutdownScript string `mapstructure:"shutdown_script"`
ShutdownScriptTimeoutSeconds int32 `mapstructure:"shutdown_script_timeout"`
ConnectionTimeoutSeconds int32 `mapstructure:"connection_timeout"`
TroubleshootingURL string `mapstructure:"troubleshooting_url"`
MOTDFile string `mapstructure:"motd_file"`
Metadata []agentMetadata `mapstructure:"metadata"`
DisplayApps []agentDisplayAppsAttributes `mapstructure:"display_apps"`
}
type agentDisplayAppsAttributes struct {
@ -76,6 +77,19 @@ type agentAppAttributes struct {
Healthcheck []appHealthcheckAttributes `mapstructure:"healthcheck"`
}
type agentScriptAttributes struct {
AgentID string `mapstructure:"agent_id"`
DisplayName string `mapstructure:"display_name"`
Icon string `mapstructure:"icon"`
Script string `mapstructure:"script"`
Cron string `mapstructure:"cron"`
LogPath string `mapstructure:"log_path"`
StartBlocksLogin bool `mapstructure:"start_blocks_login"`
RunOnStart bool `mapstructure:"run_on_start"`
RunOnStop bool `mapstructure:"run_on_stop"`
TimeoutSeconds int32 `mapstructure:"timeout"`
}
// A mapping of attributes on the "healthcheck" resource.
type appHealthcheckAttributes struct {
URL string `mapstructure:"url"`
@ -107,6 +121,7 @@ type State struct {
// ConvertState consumes Terraform state and a GraphViz representation
// produced by `terraform graph` to produce resources consumable by Coder.
// nolint:gocognit // This function makes more sense being large for now, until refactored.
func ConvertState(modules []*tfjson.StateModule, rawGraph string) (*State, error) {
parsedGraph, err := gographviz.ParseString(rawGraph)
if err != nil {
@ -206,22 +221,39 @@ 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,
StartupScriptBehavior: startupScriptBehavior,
StartupScriptTimeoutSeconds: attrs.StartupScriptTimeoutSeconds,
ShutdownScript: attrs.ShutdownScript,
ShutdownScriptTimeoutSeconds: attrs.ShutdownScriptTimeoutSeconds,
Metadata: metadata,
DisplayApps: displayApps,
Name: tfResource.Name,
Id: attrs.ID,
Env: attrs.Env,
OperatingSystem: attrs.OperatingSystem,
Architecture: attrs.Architecture,
Directory: attrs.Directory,
ConnectionTimeoutSeconds: attrs.ConnectionTimeoutSeconds,
TroubleshootingUrl: attrs.TroubleshootingURL,
MotdFile: attrs.MOTDFile,
Metadata: metadata,
DisplayApps: displayApps,
}
// Support the legacy script attributes in the agent!
if attrs.StartupScript != "" {
agent.Scripts = append(agent.Scripts, &proto.Script{
// This is ▶️
Icon: "/emojis/25b6.png",
LogPath: "coder-startup-script.log",
DisplayName: "Startup Script",
Script: attrs.StartupScript,
StartBlocksLogin: startupScriptBehavior == string(codersdk.WorkspaceAgentStartupScriptBehaviorBlocking),
RunOnStart: true,
})
}
if attrs.ShutdownScript != "" {
agent.Scripts = append(agent.Scripts, &proto.Script{
// This is ◀️
Icon: "/emojis/25c0.png",
LogPath: "coder-shutdown-script.log",
DisplayName: "Shutdown Script",
Script: attrs.ShutdownScript,
RunOnStop: true,
})
}
switch attrs.Auth {
case "token":
@ -403,6 +435,39 @@ func ConvertState(modules []*tfjson.StateModule, rawGraph string) (*State, error
}
}
// Associate scripts with agents.
for _, resources := range tfResourcesByLabel {
for _, resource := range resources {
if resource.Type != "coder_script" {
continue
}
var attrs agentScriptAttributes
err = mapstructure.Decode(resource.AttributeValues, &attrs)
if err != nil {
return nil, xerrors.Errorf("decode app attributes: %w", err)
}
for _, agents := range resourceAgents {
for _, agent := range agents {
// Find agents with the matching ID and associate them!
if agent.Id != attrs.AgentID {
continue
}
agent.Scripts = append(agent.Scripts, &proto.Script{
DisplayName: attrs.DisplayName,
Icon: attrs.Icon,
Script: attrs.Script,
Cron: attrs.Cron,
LogPath: attrs.LogPath,
StartBlocksLogin: attrs.StartBlocksLogin,
RunOnStart: attrs.RunOnStart,
RunOnStop: attrs.RunOnStop,
TimeoutSeconds: attrs.TimeoutSeconds,
})
}
}
}
}
// Associate metadata blocks with resources.
resourceMetadata := map[string][]*proto.Resource_Metadata{}
resourceHidden := map[string]bool{}
@ -482,7 +547,7 @@ func ConvertState(modules []*tfjson.StateModule, rawGraph string) (*State, error
if resource.Mode == tfjson.DataResourceMode {
continue
}
if resource.Type == "coder_agent" || resource.Type == "coder_agent_instance" || resource.Type == "coder_app" || resource.Type == "coder_metadata" {
if resource.Type == "coder_script" || resource.Type == "coder_agent" || resource.Type == "coder_agent_instance" || resource.Type == "coder_app" || resource.Type == "coder_metadata" {
continue
}
label := convertAddressToLabel(resource.Address)

View File

@ -54,7 +54,6 @@ func TestConvertResources(t *testing.T) {
OperatingSystem: "linux",
Architecture: "amd64",
Auth: &proto.Agent_Token{},
StartupScriptBehavior: "non-blocking",
ConnectionTimeoutSeconds: 120,
DisplayApps: &displayApps,
}},
@ -72,7 +71,6 @@ func TestConvertResources(t *testing.T) {
OperatingSystem: "linux",
Architecture: "amd64",
Auth: &proto.Agent_Token{},
StartupScriptBehavior: "non-blocking",
ConnectionTimeoutSeconds: 120,
DisplayApps: &displayApps,
}},
@ -91,7 +89,6 @@ func TestConvertResources(t *testing.T) {
OperatingSystem: "linux",
Architecture: "amd64",
Auth: &proto.Agent_InstanceId{},
StartupScriptBehavior: "non-blocking",
ConnectionTimeoutSeconds: 120,
DisplayApps: &displayApps,
}},
@ -108,7 +105,6 @@ func TestConvertResources(t *testing.T) {
OperatingSystem: "linux",
Architecture: "amd64",
Auth: &proto.Agent_Token{},
StartupScriptBehavior: "non-blocking",
ConnectionTimeoutSeconds: 120,
DisplayApps: &displayApps,
}},
@ -121,48 +117,42 @@ 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,
StartupScriptBehavior: "non-blocking",
StartupScriptTimeoutSeconds: 300,
ShutdownScriptTimeoutSeconds: 300,
DisplayApps: &displayApps,
Name: "dev1",
OperatingSystem: "linux",
Architecture: "amd64",
Auth: &proto.Agent_Token{},
ConnectionTimeoutSeconds: 120,
DisplayApps: &displayApps,
}, {
Name: "dev2",
OperatingSystem: "darwin",
Architecture: "amd64",
Auth: &proto.Agent_Token{},
ConnectionTimeoutSeconds: 1,
MotdFile: "/etc/motd",
StartupScriptBehavior: "non-blocking",
StartupScriptTimeoutSeconds: 30,
ShutdownScript: "echo bye bye",
ShutdownScriptTimeoutSeconds: 30,
DisplayApps: &displayApps,
Name: "dev2",
OperatingSystem: "darwin",
Architecture: "amd64",
Auth: &proto.Agent_Token{},
ConnectionTimeoutSeconds: 1,
MotdFile: "/etc/motd",
DisplayApps: &displayApps,
Scripts: []*proto.Script{{
Icon: "/emojis/25c0.png",
DisplayName: "Shutdown Script",
RunOnStop: true,
LogPath: "coder-shutdown-script.log",
Script: "echo bye bye",
}},
}, {
Name: "dev3",
OperatingSystem: "windows",
Architecture: "arm64",
Auth: &proto.Agent_Token{},
ConnectionTimeoutSeconds: 120,
TroubleshootingUrl: "https://coder.com/troubleshoot",
StartupScriptBehavior: "blocking",
StartupScriptTimeoutSeconds: 300,
ShutdownScriptTimeoutSeconds: 300,
DisplayApps: &displayApps,
Name: "dev3",
OperatingSystem: "windows",
Architecture: "arm64",
Auth: &proto.Agent_Token{},
ConnectionTimeoutSeconds: 120,
TroubleshootingUrl: "https://coder.com/troubleshoot",
DisplayApps: &displayApps,
}, {
Name: "dev4",
OperatingSystem: "linux",
Architecture: "amd64",
Auth: &proto.Agent_Token{},
ConnectionTimeoutSeconds: 120,
StartupScriptBehavior: "blocking",
StartupScriptTimeoutSeconds: 300,
ShutdownScriptTimeoutSeconds: 300,
DisplayApps: &displayApps,
Name: "dev4",
OperatingSystem: "linux",
Architecture: "amd64",
Auth: &proto.Agent_Token{},
ConnectionTimeoutSeconds: 120,
DisplayApps: &displayApps,
}},
}},
},
@ -199,7 +189,6 @@ func TestConvertResources(t *testing.T) {
},
},
Auth: &proto.Agent_Token{},
StartupScriptBehavior: "non-blocking",
ConnectionTimeoutSeconds: 120,
DisplayApps: &displayApps,
}},
@ -224,7 +213,6 @@ func TestConvertResources(t *testing.T) {
},
},
Auth: &proto.Agent_Token{},
StartupScriptBehavior: "non-blocking",
ConnectionTimeoutSeconds: 120,
DisplayApps: &displayApps,
}},
@ -263,11 +251,8 @@ func TestConvertResources(t *testing.T) {
Interval: 5,
Timeout: 1,
}},
ShutdownScriptTimeoutSeconds: 300,
StartupScriptTimeoutSeconds: 300,
StartupScriptBehavior: "non-blocking",
ConnectionTimeoutSeconds: 120,
DisplayApps: &displayApps,
ConnectionTimeoutSeconds: 120,
DisplayApps: &displayApps,
}},
}},
},
@ -307,7 +292,6 @@ func TestConvertResources(t *testing.T) {
Name: "main",
OperatingSystem: "linux",
Architecture: "amd64",
StartupScript: " #!/bin/bash\n # home folder can be empty, so copying default bash settings\n if [ ! -f ~/.profile ]; then\n cp /etc/skel/.profile $HOME\n fi\n if [ ! -f ~/.bashrc ]; then\n cp /etc/skel/.bashrc $HOME\n fi\n # install and start code-server\n curl -fsSL https://code-server.dev/install.sh | sh | tee code-server-install.log\n code-server --auth none --port 13337 | tee code-server-install.log &\n",
Apps: []*proto.App{
{
Icon: "/icon/code.svg",
@ -317,9 +301,15 @@ func TestConvertResources(t *testing.T) {
},
},
Auth: &proto.Agent_Token{},
StartupScriptBehavior: "non-blocking",
ConnectionTimeoutSeconds: 120,
DisplayApps: &displayApps,
Scripts: []*proto.Script{{
DisplayName: "Startup Script",
RunOnStart: true,
LogPath: "coder-startup-script.log",
Icon: "/emojis/25b6.png",
Script: " #!/bin/bash\n # home folder can be empty, so copying default bash settings\n if [ ! -f ~/.profile ]; then\n cp /etc/skel/.profile $HOME\n fi\n if [ ! -f ~/.bashrc ]; then\n cp /etc/skel/.bashrc $HOME\n fi\n # install and start code-server\n curl -fsSL https://code-server.dev/install.sh | sh | tee code-server-install.log\n code-server --auth none --port 13337 | tee code-server-install.log &\n",
}},
}},
},
},
@ -329,15 +319,12 @@ func TestConvertResources(t *testing.T) {
Name: "dev",
Type: "null_resource",
Agents: []*proto.Agent{{
Name: "dev",
OperatingSystem: "windows",
ShutdownScriptTimeoutSeconds: 300,
StartupScriptTimeoutSeconds: 300,
Architecture: "arm64",
Auth: &proto.Agent_Token{},
StartupScriptBehavior: "non-blocking",
ConnectionTimeoutSeconds: 120,
DisplayApps: &displayApps,
Name: "dev",
OperatingSystem: "windows",
Architecture: "arm64",
Auth: &proto.Agent_Token{},
ConnectionTimeoutSeconds: 120,
DisplayApps: &displayApps,
}},
}},
parameters: []*proto.RichParameter{{
@ -411,15 +398,12 @@ func TestConvertResources(t *testing.T) {
Name: "dev",
Type: "null_resource",
Agents: []*proto.Agent{{
Name: "dev",
OperatingSystem: "windows",
ShutdownScriptTimeoutSeconds: 300,
StartupScriptTimeoutSeconds: 300,
Architecture: "arm64",
Auth: &proto.Agent_Token{},
StartupScriptBehavior: "non-blocking",
ConnectionTimeoutSeconds: 120,
DisplayApps: &displayApps,
Name: "dev",
OperatingSystem: "windows",
Architecture: "arm64",
Auth: &proto.Agent_Token{},
ConnectionTimeoutSeconds: 120,
DisplayApps: &displayApps,
}},
}},
parameters: []*proto.RichParameter{{
@ -440,15 +424,12 @@ func TestConvertResources(t *testing.T) {
Name: "dev",
Type: "null_resource",
Agents: []*proto.Agent{{
Name: "dev",
OperatingSystem: "windows",
ShutdownScriptTimeoutSeconds: 300,
StartupScriptTimeoutSeconds: 300,
Architecture: "arm64",
Auth: &proto.Agent_Token{},
StartupScriptBehavior: "non-blocking",
ConnectionTimeoutSeconds: 120,
DisplayApps: &displayApps,
Name: "dev",
OperatingSystem: "windows",
Architecture: "arm64",
Auth: &proto.Agent_Token{},
ConnectionTimeoutSeconds: 120,
DisplayApps: &displayApps,
}},
}},
parameters: []*proto.RichParameter{{
@ -496,15 +477,12 @@ func TestConvertResources(t *testing.T) {
Name: "dev",
Type: "null_resource",
Agents: []*proto.Agent{{
Name: "main",
OperatingSystem: "linux",
Architecture: "amd64",
Auth: &proto.Agent_Token{},
StartupScriptBehavior: "non-blocking",
ConnectionTimeoutSeconds: 120,
StartupScriptTimeoutSeconds: 300,
ShutdownScriptTimeoutSeconds: 300,
DisplayApps: &displayApps,
Name: "main",
OperatingSystem: "linux",
Architecture: "amd64",
Auth: &proto.Agent_Token{},
ConnectionTimeoutSeconds: 120,
DisplayApps: &displayApps,
}},
}},
gitAuthProviders: []string{"github", "gitlab"},
@ -514,14 +492,11 @@ func TestConvertResources(t *testing.T) {
Name: "dev",
Type: "null_resource",
Agents: []*proto.Agent{{
Name: "main",
OperatingSystem: "linux",
Architecture: "amd64",
Auth: &proto.Agent_Token{},
StartupScriptBehavior: "non-blocking",
ConnectionTimeoutSeconds: 120,
StartupScriptTimeoutSeconds: 300,
ShutdownScriptTimeoutSeconds: 300,
Name: "main",
OperatingSystem: "linux",
Architecture: "amd64",
Auth: &proto.Agent_Token{},
ConnectionTimeoutSeconds: 120,
DisplayApps: &proto.DisplayApps{
VscodeInsiders: true,
WebTerminal: true,
@ -534,15 +509,12 @@ func TestConvertResources(t *testing.T) {
Name: "dev",
Type: "null_resource",
Agents: []*proto.Agent{{
Name: "main",
OperatingSystem: "linux",
Architecture: "amd64",
Auth: &proto.Agent_Token{},
StartupScriptBehavior: "non-blocking",
ConnectionTimeoutSeconds: 120,
StartupScriptTimeoutSeconds: 300,
ShutdownScriptTimeoutSeconds: 300,
DisplayApps: &proto.DisplayApps{},
Name: "main",
OperatingSystem: "linux",
Architecture: "amd64",
Auth: &proto.Agent_Token{},
ConnectionTimeoutSeconds: 120,
DisplayApps: &proto.DisplayApps{},
}},
}},
},

File diff suppressed because it is too large Load Diff

View File

@ -101,7 +101,7 @@ message Agent {
string id = 1;
string name = 2;
map<string, string> env = 3;
string startup_script = 4;
// Field 4 was startup_script, now removed.
string operating_system = 5;
string architecture = 6;
string directory = 7;
@ -114,12 +114,11 @@ message Agent {
string troubleshooting_url = 12;
string motd_file = 13;
// Field 14 was bool login_before_ready = 14, now removed.
int32 startup_script_timeout_seconds = 15;
string shutdown_script = 16;
int32 shutdown_script_timeout_seconds = 17;
// Field 15, 16, 17 were related to scripts, which are now removed.
repeated Metadata metadata = 18;
string startup_script_behavior = 19;
// Field 19 was startup_script_behavior, now removed.
DisplayApps display_apps = 20;
repeated Script scripts = 21;
}
enum AppSharingLevel {
@ -136,6 +135,19 @@ message DisplayApps {
bool port_forwarding_helper = 5;
}
// Script represents a script to be run on the workspace.
message Script {
string display_name = 1;
string icon = 2;
string script = 3;
string cron = 4;
bool start_blocks_login = 5;
bool run_on_start = 6;
bool run_on_stop = 7;
int32 timeout_seconds = 8;
string log_path = 9;
}
// App represents a dev-accessible application on the workspace.
message App {
// slug is the unique identifier for the app, usually the name from the

View File

@ -116,7 +116,7 @@ func generateUniqueConstraints() error {
case line == "":
case strings.HasSuffix(line, ";"):
query += line
if strings.Contains(query, "UNIQUE") {
if strings.Contains(query, "UNIQUE") || strings.Contains(query, "PRIMARY KEY") {
uniqueConstraints = append(uniqueConstraints, query)
}
query = ""

View File

@ -473,6 +473,7 @@ const createTemplateVersionTar = async (
env: {},
id: randomUUID(),
metadata: [],
scripts: [],
motdFile: "",
name: "dev",
operatingSystem: "linux",

View File

@ -104,7 +104,7 @@ export interface Agent {
id: string;
name: string;
env: { [key: string]: string };
startupScript: string;
/** Field 4 was startup_script, now removed. */
operatingSystem: string;
architecture: string;
directory: string;
@ -114,13 +114,14 @@ export interface Agent {
connectionTimeoutSeconds: number;
troubleshootingUrl: string;
motdFile: string;
/** Field 14 was bool login_before_ready = 14, now removed. */
startupScriptTimeoutSeconds: number;
shutdownScript: string;
shutdownScriptTimeoutSeconds: number;
/**
* Field 14 was bool login_before_ready = 14, now removed.
* Field 15, 16, 17 were related to scripts, which are now removed.
*/
metadata: Agent_Metadata[];
startupScriptBehavior: string;
/** Field 19 was startup_script_behavior, now removed. */
displayApps: DisplayApps | undefined;
scripts: Script[];
}
export interface Agent_Metadata {
@ -144,6 +145,19 @@ export interface DisplayApps {
portForwardingHelper: boolean;
}
/** Script represents a script to be run on the workspace. */
export interface Script {
displayName: string;
icon: string;
script: string;
cron: string;
startBlocksLogin: boolean;
runOnStart: boolean;
runOnStop: boolean;
timeoutSeconds: number;
logPath: string;
}
/** App represents a dev-accessible application on the workspace. */
export interface App {
/**
@ -470,9 +484,6 @@ export const Agent = {
writer.uint32(26).fork(),
).ldelim();
});
if (message.startupScript !== "") {
writer.uint32(34).string(message.startupScript);
}
if (message.operatingSystem !== "") {
writer.uint32(42).string(message.operatingSystem);
}
@ -500,27 +511,18 @@ export const Agent = {
if (message.motdFile !== "") {
writer.uint32(106).string(message.motdFile);
}
if (message.startupScriptTimeoutSeconds !== 0) {
writer.uint32(120).int32(message.startupScriptTimeoutSeconds);
}
if (message.shutdownScript !== "") {
writer.uint32(130).string(message.shutdownScript);
}
if (message.shutdownScriptTimeoutSeconds !== 0) {
writer.uint32(136).int32(message.shutdownScriptTimeoutSeconds);
}
for (const v of message.metadata) {
Agent_Metadata.encode(v!, writer.uint32(146).fork()).ldelim();
}
if (message.startupScriptBehavior !== "") {
writer.uint32(154).string(message.startupScriptBehavior);
}
if (message.displayApps !== undefined) {
DisplayApps.encode(
message.displayApps,
writer.uint32(162).fork(),
).ldelim();
}
for (const v of message.scripts) {
Script.encode(v!, writer.uint32(170).fork()).ldelim();
}
return writer;
},
};
@ -588,6 +590,42 @@ export const DisplayApps = {
},
};
export const Script = {
encode(
message: Script,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
if (message.displayName !== "") {
writer.uint32(10).string(message.displayName);
}
if (message.icon !== "") {
writer.uint32(18).string(message.icon);
}
if (message.script !== "") {
writer.uint32(26).string(message.script);
}
if (message.cron !== "") {
writer.uint32(34).string(message.cron);
}
if (message.startBlocksLogin === true) {
writer.uint32(40).bool(message.startBlocksLogin);
}
if (message.runOnStart === true) {
writer.uint32(48).bool(message.runOnStart);
}
if (message.runOnStop === true) {
writer.uint32(56).bool(message.runOnStop);
}
if (message.timeoutSeconds !== 0) {
writer.uint32(64).int32(message.timeoutSeconds);
}
if (message.logPath !== "") {
writer.uint32(74).string(message.logPath);
}
return writer;
},
};
export const App = {
encode(message: App, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.slug !== "") {

View File

@ -1307,9 +1307,6 @@ export interface WorkspaceAgent {
readonly architecture: string;
readonly environment_variables: Record<string, string>;
readonly operating_system: string;
readonly startup_script?: string;
readonly startup_script_behavior: WorkspaceAgentStartupScriptBehavior;
readonly startup_script_timeout_seconds: number;
readonly logs_length: number;
readonly logs_overflowed: boolean;
readonly directory?: string;
@ -1319,12 +1316,12 @@ export interface WorkspaceAgent {
readonly latency?: Record<string, DERPRegion>;
readonly connection_timeout_seconds: number;
readonly troubleshooting_url: string;
readonly login_before_ready: boolean;
readonly shutdown_script?: string;
readonly shutdown_script_timeout_seconds: number;
readonly subsystems: AgentSubsystem[];
readonly health: WorkspaceAgentHealth;
readonly display_apps: DisplayApp[];
readonly log_sources: WorkspaceAgentLogSource[];
readonly scripts: WorkspaceAgentScript[];
readonly startup_script_behavior: WorkspaceAgentStartupScriptBehavior;
}
// From codersdk/workspaceagents.go
@ -1351,6 +1348,16 @@ export interface WorkspaceAgentLog {
readonly created_at: string;
readonly output: string;
readonly level: LogLevel;
readonly source_id: string;
}
// From codersdk/workspaceagents.go
export interface WorkspaceAgentLogSource {
readonly workspace_agent_id: string;
readonly id: string;
readonly created_at: string;
readonly display_name: string;
readonly icon: string;
}
// From codersdk/workspaceagents.go
@ -1376,6 +1383,18 @@ export interface WorkspaceAgentMetadataResult {
readonly error: string;
}
// From codersdk/workspaceagents.go
export interface WorkspaceAgentScript {
readonly log_source_id: string;
readonly log_path: string;
readonly script: string;
readonly cron: string;
readonly run_on_start: boolean;
readonly run_on_stop: boolean;
readonly start_blocks_login: boolean;
readonly timeout: number;
}
// From codersdk/workspaceapps.go
export interface WorkspaceApp {
readonly id: string;
@ -1865,23 +1884,6 @@ export const WorkspaceAgentLifecycles: WorkspaceAgentLifecycle[] = [
"starting",
];
// From codersdk/workspaceagents.go
export type WorkspaceAgentLogSource =
| "envbox"
| "envbuilder"
| "external"
| "kubernetes"
| "shutdown_script"
| "startup_script";
export const WorkspaceAgentLogSources: WorkspaceAgentLogSource[] = [
"envbox",
"envbuilder",
"external",
"kubernetes",
"shutdown_script",
"startup_script",
];
// From codersdk/workspaceagents.go
export type WorkspaceAgentStartupScriptBehavior = "blocking" | "non-blocking";
export const WorkspaceAgentStartupScriptBehaviors: WorkspaceAgentStartupScriptBehavior[] =

View File

@ -16,6 +16,7 @@ import {
MockWorkspaceAgentTimeout,
MockWorkspaceApp,
MockProxyLatencies,
MockWorkspaceAgentLogSource,
} from "testHelpers/entities";
import { AgentRow, LineWithID } from "./AgentRow";
import { ProxyContext, getPreferredProxy } from "contexts/ProxyContext";
@ -95,6 +96,7 @@ const storybookLogs: LineWithID[] = [
level: "info",
output: line,
time: "",
source_id: MockWorkspaceAgentLogSource.id,
}));
const meta: Meta<typeof AgentRow> = {
@ -105,8 +107,6 @@ const meta: Meta<typeof AgentRow> = {
agent: {
...MockWorkspaceAgent,
logs_length: storybookLogs.length,
startup_script:
'set -eux -o pipefail\n\n# install and start code-server\ncurl -fsSL https://code-server.dev/install.sh | sh -s -- --method=standalone --prefix=/tmp/code-server --version 4.8.3\n/tmp/code-server/bin/code-server --auth none --port 13337 >/tmp/code-server.log 2>&1 &\n\n\nif [ ! -d ~/coder ]; then\n mkdir -p ~/coder\n\n git clone https://github.com/coder/coder ~/coder\nfi\n\nsudo service docker start\nDOTFILES_URI=" "\nrm -f ~/.personalize.log\nif [ -n "${DOTFILES_URI// }" ]; then\n coder dotfiles "$DOTFILES_URI" -y 2>&1 | tee -a ~/.personalize.log\nfi\nif [ -x ~/personalize ]; then\n ~/personalize 2>&1 | tee -a ~/.personalize.log\nelif [ -f ~/personalize ]; then\n echo "~/personalize is not executable, skipping..." | tee -a ~/.personalize.log\nfi\n',
},
workspace: MockWorkspace,
showApps: true,

View File

@ -1,19 +1,26 @@
import Popover from "@mui/material/Popover";
import { makeStyles, useTheme } from "@mui/styles";
import Collapse from "@mui/material/Collapse";
import Skeleton from "@mui/material/Skeleton";
import Tooltip from "@mui/material/Tooltip";
import { makeStyles } from "@mui/styles";
import * as API from "api/api";
import CodeOutlined from "@mui/icons-material/CodeOutlined";
import {
Workspace,
WorkspaceAgent,
WorkspaceAgentLogSource,
WorkspaceAgentMetadata,
} from "api/typesGenerated";
import {
CloseDropdown,
OpenDropdown,
} from "components/DropdownArrows/DropdownArrows";
import { displayError } from "components/GlobalSnackbar/utils";
import { VSCodeDesktopButton } from "components/Resources/VSCodeDesktopButton/VSCodeDesktopButton";
import {
Line,
LogLine,
logLineHeight,
} from "components/WorkspaceBuildLogs/Logs";
import { PortForwardButton } from "./PortForwardButton";
import { VSCodeDesktopButton } from "components/Resources/VSCodeDesktopButton/VSCodeDesktopButton";
import { useProxy } from "contexts/ProxyContext";
import {
FC,
useCallback,
@ -23,28 +30,19 @@ import {
useRef,
useState,
} from "react";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { darcula } from "react-syntax-highlighter/dist/cjs/styles/prism";
import AutoSizer from "react-virtualized-auto-sizer";
import { FixedSizeList as List, ListOnScrollProps } from "react-window";
import { colors } from "theme/colors";
import { combineClasses } from "utils/combineClasses";
import {
Workspace,
WorkspaceAgent,
WorkspaceAgentMetadata,
} from "api/typesGenerated";
import { AppLink } from "./AppLink/AppLink";
import { SSHButton } from "./SSHButton/SSHButton";
import { Stack } from "../Stack/Stack";
import { TerminalLink } from "./TerminalLink/TerminalLink";
import { AgentLatency } from "./AgentLatency";
import { AgentMetadata } from "./AgentMetadata";
import { AgentVersion } from "./AgentVersion";
import { AgentStatus } from "./AgentStatus";
import Collapse from "@mui/material/Collapse";
import { useProxy } from "contexts/ProxyContext";
import { displayError } from "components/GlobalSnackbar/utils";
import { AgentVersion } from "./AgentVersion";
import { AppLink } from "./AppLink/AppLink";
import { PortForwardButton } from "./PortForwardButton";
import { SSHButton } from "./SSHButton/SSHButton";
import { TerminalLink } from "./TerminalLink/TerminalLink";
// Logs are stored as the Line interface to make rendering
// much more efficient. Instead of mapping objects each time, we're
@ -81,14 +79,18 @@ export const AgentRow: FC<AgentRowProps> = ({
storybookLogs,
}) => {
const styles = useStyles();
const theme = useTheme();
const startupScriptAnchorRef = useRef<HTMLButtonElement>(null);
const [startupScriptOpen, setStartupScriptOpen] = useState(false);
const hasAppsToDisplay = !hideVSCodeDesktopButton || agent.apps.length > 0;
const shouldDisplayApps =
showApps &&
((agent.status === "connected" && hasAppsToDisplay) ||
agent.status === "connecting");
const logSourceByID = useMemo(() => {
const sources: { [id: string]: WorkspaceAgentLogSource } = {};
for (const source of agent.log_sources) {
sources[source.id] = source;
}
return sources;
}, [agent.log_sources]);
const hasStartupFeatures = Boolean(agent.logs_length);
const { proxy } = useProxy();
const [showLogs, setShowLogs] = useState(
@ -111,6 +113,7 @@ export const AgentRow: FC<AgentRowProps> = ({
level: "error",
output: "Startup logs exceeded the max size of 1MB!",
time: new Date().toISOString(),
source_id: "",
});
}
return logs;
@ -292,13 +295,123 @@ export const AgentRow: FC<AgentRowProps> = ({
className={styles.startupLogs}
onScroll={handleLogScroll}
>
{({ index, style }) => (
<LogLine
line={startupLogs[index]}
number={index + 1}
style={style}
/>
)}
{({ index, style }) => {
const log = startupLogs[index];
const sourceIcon: string | undefined =
logSourceByID[log.source_id].icon;
let assignedIcon = false;
let icon: JSX.Element;
// If no icon is specified, we show a deterministic
// colored circle to identify unique scripts.
if (sourceIcon) {
icon = (
<img
src={sourceIcon}
alt=""
width={16}
height={16}
style={{
marginRight: 8,
}}
/>
);
} else {
icon = (
<div
style={{
width: 16,
height: 16,
marginRight: 8,
background: determineScriptDisplayColor(
logSourceByID[log.source_id].display_name,
),
borderRadius: "100%",
}}
/>
);
assignedIcon = true;
}
let nextChangesSource = false;
if (index < startupLogs.length - 1) {
nextChangesSource =
logSourceByID[startupLogs[index + 1].source_id].id !==
log.source_id;
}
// We don't want every line to repeat the icon, because
// that is ugly and repetitive. This removes the icon
// for subsequent lines of the same source and shows a
// line instead, visually indicating they are from the
// same source.
if (
index > 0 &&
logSourceByID[startupLogs[index - 1].source_id].id ===
log.source_id
) {
icon = (
<div
style={{
minWidth: 16,
width: 16,
height: 16,
marginRight: 8,
display: "flex",
justifyContent: "center",
position: "relative",
}}
>
<div
style={{
height: nextChangesSource ? "50%" : "100%",
width: 4,
background: "hsl(222, 31%, 25%)",
borderRadius: 2,
}}
/>
{nextChangesSource && (
<div
style={{
height: 4,
width: "50%",
top: "calc(50% - 2px)",
left: "calc(50% - 1px)",
background: "hsl(222, 31%, 25%)",
borderRadius: 2,
position: "absolute",
}}
/>
)}
</div>
);
}
return (
<LogLine
line={startupLogs[index]}
number={index + 1}
maxNumber={startupLogs.length}
style={style}
sourceIcon={
<Tooltip
title={
<>
{logSourceByID[log.source_id].display_name}
{assignedIcon && (
<i>
<br />
No icon specified!
</i>
)}
</>
}
>
{icon}
</Tooltip>
}
/>
);
}}
</List>
)}
</AutoSizer>
@ -316,7 +429,7 @@ export const AgentRow: FC<AgentRowProps> = ({
}}
>
<CloseDropdown />
Hide startup logs
Hide logs
</button>
) : (
<button
@ -329,50 +442,9 @@ export const AgentRow: FC<AgentRowProps> = ({
}}
>
<OpenDropdown />
Show startup logs
Show logs
</button>
)}
<button
className={combineClasses([
styles.logsPanelButton,
styles.scriptButton,
])}
ref={startupScriptAnchorRef}
onClick={() => {
setStartupScriptOpen(!startupScriptOpen);
}}
>
<CodeOutlined />
Startup script
</button>
<Popover
classes={{
paper: styles.startupScriptPopover,
}}
open={startupScriptOpen}
onClose={() => setStartupScriptOpen(false)}
anchorEl={startupScriptAnchorRef.current}
>
<div>
<SyntaxHighlighter
style={darcula}
language="shell"
showLineNumbers
// Use inline styles does not work correctly
// https://github.com/react-syntax-highlighter/react-syntax-highlighter/issues/329
codeTagProps={{ style: {} }}
customStyle={{
background: theme.palette.background.default,
maxWidth: 600,
margin: 0,
}}
>
{agent.startup_script || ""}
</SyntaxHighlighter>
</div>
</Popover>
</div>
</div>
)}
@ -390,6 +462,7 @@ const useAgentLogs = (
useEffect(() => {
if (!enabled) {
socket.current?.close();
setLogs([]);
return;
}
@ -403,6 +476,7 @@ const useAgentLogs = (
level: log.level || "info",
output: log.output,
time: log.created_at,
source_id: log.source_id,
}));
if (!previousLogs) {
@ -539,10 +613,6 @@ const useStyles = makeStyles((theme) => ({
},
},
startupScriptPopover: {
backgroundColor: theme.palette.background.default,
},
agentNameAndStatus: {
display: "flex",
alignItems: "center",
@ -631,14 +701,30 @@ const useStyles = makeStyles((theme) => ({
color: theme.palette.warning.light,
},
scriptButton: {
"& svg": {
width: theme.spacing(2),
height: theme.spacing(2),
},
},
agentOS: {
textTransform: "capitalize",
},
}));
// These colors were picked at random. Feel free
// to add more, adjust, or change! Users will not
// depend on these colors.
const scriptDisplayColors = [
"#85A3B2",
"#A37EB2",
"#C29FDE",
"#90B3D7",
"#829AC7",
"#728B8E",
"#506080",
"#5654B0",
"#6B56D6",
"#7847CC",
];
const determineScriptDisplayColor = (displayName: string): string => {
const hash = displayName.split("").reduce((hash, char) => {
return (hash << 5) + hash + char.charCodeAt(0); // bit-shift and add for our simple hash
}, 0);
return scriptDisplayColors[Math.abs(hash) % scriptDisplayColors.length];
};

View File

@ -244,39 +244,39 @@ const OffLifecycle: React.FC = () => {
const ConnectedStatus: React.FC<{
agent: WorkspaceAgent;
}> = ({ agent }) => {
switch (agent.startup_script_behavior) {
case "non-blocking":
return <ReadyLifecycle />;
case "blocking":
return (
<ChooseOne>
<Cond condition={agent.lifecycle_state === "ready"}>
<ReadyLifecycle />
</Cond>
<Cond condition={agent.lifecycle_state === "start_timeout"}>
<StartTimeoutLifecycle agent={agent} />
</Cond>
<Cond condition={agent.lifecycle_state === "start_error"}>
<StartErrorLifecycle agent={agent} />
</Cond>
<Cond condition={agent.lifecycle_state === "shutting_down"}>
<ShuttingDownLifecycle />
</Cond>
<Cond condition={agent.lifecycle_state === "shutdown_timeout"}>
<ShutdownTimeoutLifecycle agent={agent} />
</Cond>
<Cond condition={agent.lifecycle_state === "shutdown_error"}>
<ShutdownErrorLifecycle agent={agent} />
</Cond>
<Cond condition={agent.lifecycle_state === "off"}>
<OffLifecycle />
</Cond>
<Cond>
<StartingLifecycle />
</Cond>
</ChooseOne>
);
// This is to support legacy agents that do not support
// reporting the lifecycle_state field.
if (agent.scripts.length === 0) {
return <ReadyLifecycle />;
}
return (
<ChooseOne>
<Cond condition={agent.lifecycle_state === "ready"}>
<ReadyLifecycle />
</Cond>
<Cond condition={agent.lifecycle_state === "start_timeout"}>
<StartTimeoutLifecycle agent={agent} />
</Cond>
<Cond condition={agent.lifecycle_state === "start_error"}>
<StartErrorLifecycle agent={agent} />
</Cond>
<Cond condition={agent.lifecycle_state === "shutting_down"}>
<ShuttingDownLifecycle />
</Cond>
<Cond condition={agent.lifecycle_state === "shutdown_timeout"}>
<ShutdownTimeoutLifecycle agent={agent} />
</Cond>
<Cond condition={agent.lifecycle_state === "shutdown_error"}>
<ShutdownErrorLifecycle agent={agent} />
</Cond>
<Cond condition={agent.lifecycle_state === "off"}>
<OffLifecycle />
</Cond>
<Cond>
<StartingLifecycle />
</Cond>
</ChooseOne>
);
};
const DisconnectedStatus: React.FC = () => {

View File

@ -10,6 +10,7 @@ export interface Line {
time: string;
output: string;
level: LogLevel;
source_id: string;
}
export interface LogsProps {
@ -55,7 +56,9 @@ export const LogLine: FC<{
hideTimestamp?: boolean;
number?: number;
style?: React.CSSProperties;
}> = ({ line, hideTimestamp, number, style }) => {
sourceIcon?: JSX.Element;
maxNumber?: number;
}> = ({ line, hideTimestamp, number, maxNumber, sourceIcon, style }) => {
const styles = useStyles();
const output = useMemo(() => {
return convert.toHtml(line.output.split(/\r/g).pop() as string);
@ -71,6 +74,7 @@ export const LogLine: FC<{
])}
style={style}
>
{sourceIcon}
{!hideTimestamp && (
<>
<span
@ -78,6 +82,9 @@ export const LogLine: FC<{
styles.time,
isUsingLineNumber && styles.number,
])}
style={{
minWidth: `${maxNumber ? maxNumber.toString().length - 1 : 0}em`,
}}
>
{number ? number : dayjs(line.time).format(`HH:mm:ss.SSS`)}
</span>
@ -112,6 +119,7 @@ const useStyles = makeStyles((theme) => ({
line: {
wordBreak: "break-all",
display: "flex",
alignItems: "center",
fontSize: 14,
color: theme.palette.text.primary,
fontFamily: MONOSPACE_FONT_FAMILY,
@ -143,7 +151,6 @@ const useStyles = makeStyles((theme) => ({
},
time: {
userSelect: "none",
width: theme.spacing(12.5),
whiteSpace: "pre",
display: "inline-block",
color: theme.palette.text.secondary,

View File

@ -72,6 +72,7 @@ export const WorkspaceBuildLogs: FC<WorkspaceBuildLogsProps> = ({
time: log.created_at,
output: log.output,
level: log.log_level,
source_id: log.log_source,
}));
const duration = getStageDurationInSeconds(logs);
const shouldDisplayDuration = duration !== undefined;

View File

@ -53,7 +53,7 @@ const mapAlertTypeToText: MapAlertTypeToComponent = {
severity: "info",
children: (
<>
Startup script is still running. You can continue using this terminal,
Startup scripts are still running. You can continue using this terminal,
but{" "}
<Link
title="your workspace may be incomplete."
@ -71,10 +71,10 @@ const mapAlertTypeToText: MapAlertTypeToComponent = {
severity: "success",
children: (
<>
Startup script has completed successfully. The workspace is ready but
Startup scripts have completed successfully. The workspace is ready but
this{" "}
<Link
title="session was started before the startup script finished"
title="session was started before the startup scripts finished"
href={docs("/templates#your-workspace-may-be-incomplete")}
target="_blank"
rel="noreferrer"

View File

@ -539,6 +539,25 @@ export const MockWorkspaceApp: TypesGen.WorkspaceApp = {
},
};
export const MockWorkspaceAgentLogSource: TypesGen.WorkspaceAgentLogSource = {
created_at: "2023-05-04T11:30:41.402072Z",
id: "dc790496-eaec-4f88-a53f-8ce1f61a1fff",
display_name: "Startup Script",
icon: "",
workspace_agent_id: "",
};
export const MockWorkspaceAgentScript: TypesGen.WorkspaceAgentScript = {
log_source_id: MockWorkspaceAgentLogSource.id,
cron: "",
log_path: "",
run_on_start: true,
run_on_stop: false,
script: "echo 'hello world'",
start_blocks_login: false,
timeout: 0,
};
export const MockWorkspaceAgent: TypesGen.WorkspaceAgent = {
apps: [MockWorkspaceApp],
architecture: "amd64",
@ -560,12 +579,11 @@ export const MockWorkspaceAgent: TypesGen.WorkspaceAgent = {
connection_timeout_seconds: 120,
troubleshooting_url: "https://coder.com/troubleshoot",
lifecycle_state: "starting",
login_before_ready: false, // Deprecated.
startup_script_behavior: "blocking",
logs_length: 0,
logs_overflowed: false,
startup_script_timeout_seconds: 120,
shutdown_script_timeout_seconds: 120,
log_sources: [MockWorkspaceAgentLogSource],
scripts: [MockWorkspaceAgentScript],
startup_script_behavior: "non-blocking",
subsystems: ["envbox", "exectrace"],
health: {
healthy: true,
@ -2202,6 +2220,7 @@ export const MockWorkspaceAgentLogs: TypesGen.WorkspaceAgentLog[] = [
created_at: "2023-05-04T11:30:41.402072Z",
output: "+ curl -fsSL https://code-server.dev/install.sh",
level: "info",
source_id: MockWorkspaceAgentLogSource.id,
},
{
id: 166664,
@ -2209,18 +2228,21 @@ export const MockWorkspaceAgentLogs: TypesGen.WorkspaceAgentLog[] = [
output:
"+ sh -s -- --method=standalone --prefix=/tmp/code-server --version 4.8.3",
level: "info",
source_id: MockWorkspaceAgentLogSource.id,
},
{
id: 166665,
created_at: "2023-05-04T11:30:42.590731Z",
output: "Ubuntu 22.04.2 LTS",
level: "info",
source_id: MockWorkspaceAgentLogSource.id,
},
{
id: 166666,
created_at: "2023-05-04T11:30:42.593686Z",
output: "Installing v4.8.3 of the amd64 release from GitHub.",
level: "info",
source_id: MockWorkspaceAgentLogSource.id,
},
];