mirror of
https://github.com/coder/coder.git
synced 2025-07-12 00:14:10 +00:00
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:
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}()
|
||||
|
Reference in New Issue
Block a user