diff --git a/.vscode/settings.json b/.vscode/settings.json index 1b09dba36a..809770f4f1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,7 @@ { "cSpell.words": [ "afero", + "agentsdk", "apps", "ASKPASS", "autostop", @@ -183,6 +184,10 @@ "files.exclude": { "**/node_modules": true }, + "search.exclude": { + "scripts/metricsdocgen/metrics": true, + "docs/api/*.md": true + }, // Ensure files always have a newline. "files.insertFinalNewline": true, "go.lintTool": "golangci-lint", diff --git a/agent/agent.go b/agent/agent.go index 3083beb8ac..54d17bb929 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -41,6 +41,7 @@ import ( "github.com/coder/coder/buildinfo" "github.com/coder/coder/coderd/gitauth" "github.com/coder/coder/codersdk" + "github.com/coder/coder/codersdk/agentsdk" "github.com/coder/coder/pty" "github.com/coder/coder/tailnet" "github.com/coder/retry" @@ -68,12 +69,12 @@ type Options struct { } type Client interface { - WorkspaceAgentMetadata(ctx context.Context) (codersdk.WorkspaceAgentMetadata, error) - ListenWorkspaceAgent(ctx context.Context) (net.Conn, error) - AgentReportStats(ctx context.Context, log slog.Logger, stats func() *codersdk.AgentStats) (io.Closer, error) - PostWorkspaceAgentLifecycle(ctx context.Context, state codersdk.PostWorkspaceAgentLifecycleRequest) error - PostWorkspaceAgentAppHealth(ctx context.Context, req codersdk.PostWorkspaceAppHealthsRequest) error - PostWorkspaceAgentVersion(ctx context.Context, version string) error + Metadata(ctx context.Context) (agentsdk.Metadata, error) + Listen(ctx context.Context) (net.Conn, error) + ReportStats(ctx context.Context, log slog.Logger, stats func() *agentsdk.Stats) (io.Closer, error) + PostLifecycle(ctx context.Context, state agentsdk.PostLifecycleRequest) error + PostAppHealth(ctx context.Context, req agentsdk.PostAppHealthsRequest) error + PostVersion(ctx context.Context, version string) error } func New(options Options) io.Closer { @@ -187,7 +188,7 @@ func (a *agent) reportLifecycleLoop(ctx context.Context) { a.logger.Debug(ctx, "post lifecycle state", slog.F("state", state)) - err := a.client.PostWorkspaceAgentLifecycle(ctx, codersdk.PostWorkspaceAgentLifecycleRequest{ + err := a.client.PostLifecycle(ctx, agentsdk.PostLifecycleRequest{ State: state, }) if err == nil { @@ -226,12 +227,12 @@ func (a *agent) run(ctx context.Context) error { } a.sessionToken.Store(&sessionToken) - err = a.client.PostWorkspaceAgentVersion(ctx, buildinfo.Version()) + err = a.client.PostVersion(ctx, buildinfo.Version()) if err != nil { return xerrors.Errorf("update workspace agent version: %w", err) } - metadata, err := a.client.WorkspaceAgentMetadata(ctx) + metadata, err := a.client.Metadata(ctx) if err != nil { return xerrors.Errorf("fetch metadata: %w", err) } @@ -300,7 +301,7 @@ func (a *agent) run(ctx context.Context) error { appReporterCtx, appReporterCtxCancel := context.WithCancel(ctx) defer appReporterCtxCancel() go NewWorkspaceAppHealthReporter( - a.logger, metadata.Apps, a.client.PostWorkspaceAgentAppHealth)(appReporterCtx) + a.logger, metadata.Apps, a.client.PostAppHealth)(appReporterCtx) a.logger.Debug(ctx, "running tailnet with derpmap", slog.F("derpmap", metadata.DERPMap)) @@ -326,7 +327,7 @@ func (a *agent) run(ctx context.Context) error { } // Report statistics from the created network. - cl, err := a.client.AgentReportStats(ctx, a.logger, func() *codersdk.AgentStats { + cl, err := a.client.ReportStats(ctx, a.logger, func() *agentsdk.Stats { stats := network.ExtractTrafficStats() return convertAgentStats(stats) }) @@ -373,7 +374,7 @@ func (a *agent) trackConnGoroutine(fn func()) error { func (a *agent) createTailnet(ctx context.Context, derpMap *tailcfg.DERPMap) (_ *tailnet.Conn, err error) { network, err := tailnet.NewConn(&tailnet.Options{ - Addresses: []netip.Prefix{netip.PrefixFrom(codersdk.TailnetIP, 128)}, + Addresses: []netip.Prefix{netip.PrefixFrom(codersdk.WorkspaceAgentIP, 128)}, DERPMap: derpMap, Logger: a.logger.Named("tailnet"), EnableTrafficStats: true, @@ -387,7 +388,7 @@ func (a *agent) createTailnet(ctx context.Context, derpMap *tailcfg.DERPMap) (_ } }() - sshListener, err := network.Listen("tcp", ":"+strconv.Itoa(codersdk.TailnetSSHPort)) + sshListener, err := network.Listen("tcp", ":"+strconv.Itoa(codersdk.WorkspaceAgentSSHPort)) if err != nil { return nil, xerrors.Errorf("listen on the ssh port: %w", err) } @@ -419,7 +420,7 @@ func (a *agent) createTailnet(ctx context.Context, derpMap *tailcfg.DERPMap) (_ return nil, err } - reconnectingPTYListener, err := network.Listen("tcp", ":"+strconv.Itoa(codersdk.TailnetReconnectingPTYPort)) + reconnectingPTYListener, err := network.Listen("tcp", ":"+strconv.Itoa(codersdk.WorkspaceAgentReconnectingPTYPort)) if err != nil { return nil, xerrors.Errorf("listen for reconnecting pty: %w", err) } @@ -450,7 +451,7 @@ func (a *agent) createTailnet(ctx context.Context, derpMap *tailcfg.DERPMap) (_ if err != nil { continue } - var msg codersdk.ReconnectingPTYInit + var msg codersdk.WorkspaceAgentReconnectingPTYInit err = json.Unmarshal(data, &msg) if err != nil { continue @@ -463,7 +464,7 @@ func (a *agent) createTailnet(ctx context.Context, derpMap *tailcfg.DERPMap) (_ return nil, err } - speedtestListener, err := network.Listen("tcp", ":"+strconv.Itoa(codersdk.TailnetSpeedtestPort)) + speedtestListener, err := network.Listen("tcp", ":"+strconv.Itoa(codersdk.WorkspaceAgentSpeedtestPort)) if err != nil { return nil, xerrors.Errorf("listen for speedtest: %w", err) } @@ -491,7 +492,7 @@ func (a *agent) createTailnet(ctx context.Context, derpMap *tailcfg.DERPMap) (_ return nil, err } - statisticsListener, err := network.Listen("tcp", ":"+strconv.Itoa(codersdk.TailnetStatisticsPort)) + statisticsListener, err := network.Listen("tcp", ":"+strconv.Itoa(codersdk.WorkspaceAgentStatisticsPort)) if err != nil { return nil, xerrors.Errorf("listen for statistics: %w", err) } @@ -531,7 +532,7 @@ func (a *agent) runCoordinator(ctx context.Context, network *tailnet.Conn) error ctx, cancel := context.WithCancel(ctx) defer cancel() - coordinator, err := a.client.ListenWorkspaceAgent(ctx) + coordinator, err := a.client.Listen(ctx) if err != nil { return err } @@ -700,8 +701,8 @@ func (a *agent) init(ctx context.Context) { go a.runLoop(ctx) } -func convertAgentStats(counts map[netlogtype.Connection]netlogtype.Counts) *codersdk.AgentStats { - stats := &codersdk.AgentStats{ +func convertAgentStats(counts map[netlogtype.Connection]netlogtype.Counts) *agentsdk.Stats { + stats := &agentsdk.Stats{ ConnsByProto: map[string]int64{}, NumConns: int64(len(counts)), } @@ -736,7 +737,7 @@ func (a *agent) createCommand(ctx context.Context, rawCommand string, env []stri if rawMetadata == nil { return nil, xerrors.Errorf("no metadata was provided: %w", err) } - metadata, valid := rawMetadata.(codersdk.WorkspaceAgentMetadata) + metadata, valid := rawMetadata.(agentsdk.Metadata) if !valid { return nil, xerrors.Errorf("metadata is the wrong type: %T", metadata) } @@ -845,7 +846,7 @@ func (a *agent) handleSSHSession(session ssh.Session) (retErr error) { session.DisablePTYEmulation() if !isQuietLogin(session.RawCommand()) { - metadata, ok := a.metadata.Load().(codersdk.WorkspaceAgentMetadata) + metadata, ok := a.metadata.Load().(agentsdk.Metadata) if ok { err = showMOTD(session, metadata.MOTDFile) if err != nil { @@ -918,7 +919,7 @@ func (a *agent) handleSSHSession(session ssh.Session) (retErr error) { return cmd.Wait() } -func (a *agent) handleReconnectingPTY(ctx context.Context, logger slog.Logger, msg codersdk.ReconnectingPTYInit, conn net.Conn) (retErr error) { +func (a *agent) handleReconnectingPTY(ctx context.Context, logger slog.Logger, msg codersdk.WorkspaceAgentReconnectingPTYInit, conn net.Conn) (retErr error) { defer conn.Close() connectionID := uuid.NewString() diff --git a/agent/agent_test.go b/agent/agent_test.go index 384c967a7f..3379c2dde2 100644 --- a/agent/agent_test.go +++ b/agent/agent_test.go @@ -42,6 +42,7 @@ import ( "cdr.dev/slog/sloggers/slogtest" "github.com/coder/coder/agent" "github.com/coder/coder/codersdk" + "github.com/coder/coder/codersdk/agentsdk" "github.com/coder/coder/pty/ptytest" "github.com/coder/coder/tailnet" "github.com/coder/coder/tailnet/tailnettest" @@ -57,7 +58,7 @@ func TestAgent_Stats_SSH(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - conn, _, stats, _ := setupAgent(t, codersdk.WorkspaceAgentMetadata{}, 0) + conn, _, stats, _ := setupAgent(t, agentsdk.Metadata{}, 0) sshClient, err := conn.SSHClient(ctx) require.NoError(t, err) @@ -67,7 +68,7 @@ func TestAgent_Stats_SSH(t *testing.T) { defer session.Close() require.NoError(t, session.Run("echo test")) - var s *codersdk.AgentStats + var s *agentsdk.Stats require.Eventuallyf(t, func() bool { var ok bool s, ok = <-stats @@ -83,7 +84,7 @@ func TestAgent_Stats_ReconnectingPTY(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - conn, _, stats, _ := setupAgent(t, codersdk.WorkspaceAgentMetadata{}, 0) + conn, _, stats, _ := setupAgent(t, agentsdk.Metadata{}, 0) ptyConn, err := conn.ReconnectingPTY(ctx, uuid.New(), 128, 128, "/bin/bash") require.NoError(t, err) @@ -96,7 +97,7 @@ func TestAgent_Stats_ReconnectingPTY(t *testing.T) { _, err = ptyConn.Write(data) require.NoError(t, err) - var s *codersdk.AgentStats + var s *agentsdk.Stats require.Eventuallyf(t, func() bool { var ok bool s, ok = <-stats @@ -108,7 +109,7 @@ func TestAgent_Stats_ReconnectingPTY(t *testing.T) { func TestAgent_SessionExec(t *testing.T) { t.Parallel() - session := setupSSHSession(t, codersdk.WorkspaceAgentMetadata{}) + session := setupSSHSession(t, agentsdk.Metadata{}) command := "echo test" if runtime.GOOS == "windows" { @@ -121,7 +122,7 @@ func TestAgent_SessionExec(t *testing.T) { func TestAgent_GitSSH(t *testing.T) { t.Parallel() - session := setupSSHSession(t, codersdk.WorkspaceAgentMetadata{}) + session := setupSSHSession(t, agentsdk.Metadata{}) command := "sh -c 'echo $GIT_SSH_COMMAND'" if runtime.GOOS == "windows" { command = "cmd.exe /c echo %GIT_SSH_COMMAND%" @@ -141,7 +142,7 @@ func TestAgent_SessionTTYShell(t *testing.T) { // it seems like it could be either. t.Skip("ConPTY appears to be inconsistent on Windows.") } - session := setupSSHSession(t, codersdk.WorkspaceAgentMetadata{}) + session := setupSSHSession(t, agentsdk.Metadata{}) command := "sh" if runtime.GOOS == "windows" { command = "cmd.exe" @@ -164,7 +165,7 @@ func TestAgent_SessionTTYShell(t *testing.T) { func TestAgent_SessionTTYExitCode(t *testing.T) { t.Parallel() - session := setupSSHSession(t, codersdk.WorkspaceAgentMetadata{}) + session := setupSSHSession(t, agentsdk.Metadata{}) command := "areallynotrealcommand" err := session.RequestPty("xterm", 128, 128, ssh.TerminalModes{}) require.NoError(t, err) @@ -203,7 +204,7 @@ func TestAgent_Session_TTY_MOTD(t *testing.T) { // Set HOME so we can ensure no ~/.hushlogin is present. t.Setenv("HOME", tmpdir) - session := setupSSHSession(t, codersdk.WorkspaceAgentMetadata{ + session := setupSSHSession(t, agentsdk.Metadata{ MOTDFile: name, }) err = session.RequestPty("xterm", 128, 128, ssh.TerminalModes{}) @@ -249,7 +250,7 @@ func TestAgent_Session_TTY_Hushlogin(t *testing.T) { // Set HOME so we can ensure ~/.hushlogin is present. t.Setenv("HOME", tmpdir) - session := setupSSHSession(t, codersdk.WorkspaceAgentMetadata{ + session := setupSSHSession(t, agentsdk.Metadata{ MOTDFile: name, }) err = session.RequestPty("xterm", 128, 128, ssh.TerminalModes{}) @@ -530,7 +531,7 @@ func TestAgent_SFTP(t *testing.T) { home = "/" + strings.ReplaceAll(home, "\\", "/") } //nolint:dogsled - conn, _, _, _ := setupAgent(t, codersdk.WorkspaceAgentMetadata{}, 0) + conn, _, _, _ := setupAgent(t, agentsdk.Metadata{}, 0) sshClient, err := conn.SSHClient(ctx) require.NoError(t, err) defer sshClient.Close() @@ -562,7 +563,7 @@ func TestAgent_SCP(t *testing.T) { defer cancel() //nolint:dogsled - conn, _, _, _ := setupAgent(t, codersdk.WorkspaceAgentMetadata{}, 0) + conn, _, _, _ := setupAgent(t, agentsdk.Metadata{}, 0) sshClient, err := conn.SSHClient(ctx) require.NoError(t, err) defer sshClient.Close() @@ -581,7 +582,7 @@ func TestAgent_EnvironmentVariables(t *testing.T) { t.Parallel() key := "EXAMPLE" value := "value" - session := setupSSHSession(t, codersdk.WorkspaceAgentMetadata{ + session := setupSSHSession(t, agentsdk.Metadata{ EnvironmentVariables: map[string]string{ key: value, }, @@ -598,7 +599,7 @@ func TestAgent_EnvironmentVariables(t *testing.T) { func TestAgent_EnvironmentVariableExpansion(t *testing.T) { t.Parallel() key := "EXAMPLE" - session := setupSSHSession(t, codersdk.WorkspaceAgentMetadata{ + session := setupSSHSession(t, agentsdk.Metadata{ EnvironmentVariables: map[string]string{ key: "$SOMETHINGNOTSET", }, @@ -625,7 +626,7 @@ func TestAgent_CoderEnvVars(t *testing.T) { t.Run(key, func(t *testing.T) { t.Parallel() - session := setupSSHSession(t, codersdk.WorkspaceAgentMetadata{}) + session := setupSSHSession(t, agentsdk.Metadata{}) command := "sh -c 'echo $" + key + "'" if runtime.GOOS == "windows" { command = "cmd.exe /c echo %" + key + "%" @@ -648,7 +649,7 @@ func TestAgent_SSHConnectionEnvVars(t *testing.T) { t.Run(key, func(t *testing.T) { t.Parallel() - session := setupSSHSession(t, codersdk.WorkspaceAgentMetadata{}) + session := setupSSHSession(t, agentsdk.Metadata{}) command := "sh -c 'echo $" + key + "'" if runtime.GOOS == "windows" { command = "cmd.exe /c echo %" + key + "%" @@ -667,7 +668,7 @@ func TestAgent_StartupScript(t *testing.T) { } content := "output" //nolint:dogsled - _, _, _, fs := setupAgent(t, codersdk.WorkspaceAgentMetadata{ + _, _, _, fs := setupAgent(t, agentsdk.Metadata{ StartupScript: "echo " + content, }, 0) var gotContent string @@ -701,7 +702,7 @@ func TestAgent_Lifecycle(t *testing.T) { t.Run("Timeout", func(t *testing.T) { t.Parallel() - _, client, _, _ := setupAgent(t, codersdk.WorkspaceAgentMetadata{ + _, client, _, _ := setupAgent(t, agentsdk.Metadata{ StartupScript: "sleep 10", StartupScriptTimeout: time.Nanosecond, }, 0) @@ -730,7 +731,7 @@ func TestAgent_Lifecycle(t *testing.T) { t.Run("Error", func(t *testing.T) { t.Parallel() - _, client, _, _ := setupAgent(t, codersdk.WorkspaceAgentMetadata{ + _, client, _, _ := setupAgent(t, agentsdk.Metadata{ StartupScript: "false", StartupScriptTimeout: 30 * time.Second, }, 0) @@ -759,7 +760,7 @@ func TestAgent_Lifecycle(t *testing.T) { t.Run("Ready", func(t *testing.T) { t.Parallel() - _, client, _, _ := setupAgent(t, codersdk.WorkspaceAgentMetadata{ + _, client, _, _ := setupAgent(t, agentsdk.Metadata{ StartupScript: "true", StartupScriptTimeout: 30 * time.Second, }, 0) @@ -799,7 +800,7 @@ func TestAgent_ReconnectingPTY(t *testing.T) { defer cancel() //nolint:dogsled - conn, _, _, _ := setupAgent(t, codersdk.WorkspaceAgentMetadata{}, 0) + conn, _, _, _ := setupAgent(t, agentsdk.Metadata{}, 0) id := uuid.New() netConn, err := conn.ReconnectingPTY(ctx, id, 100, 100, "/bin/bash") require.NoError(t, err) @@ -901,7 +902,7 @@ func TestAgent_Dial(t *testing.T) { }() //nolint:dogsled - conn, _, _, _ := setupAgent(t, codersdk.WorkspaceAgentMetadata{}, 0) + conn, _, _, _ := setupAgent(t, agentsdk.Metadata{}, 0) require.True(t, conn.AwaitReachable(context.Background())) conn1, err := conn.DialContext(context.Background(), l.Addr().Network(), l.Addr().String()) require.NoError(t, err) @@ -923,7 +924,7 @@ func TestAgent_Speedtest(t *testing.T) { defer cancel() derpMap := tailnettest.RunDERPAndSTUN(t) //nolint:dogsled - conn, _, _, _ := setupAgent(t, codersdk.WorkspaceAgentMetadata{ + conn, _, _, _ := setupAgent(t, agentsdk.Metadata{ DERPMap: derpMap, }, 0) defer conn.Close() @@ -940,12 +941,12 @@ func TestAgent_Reconnect(t *testing.T) { defer coordinator.Close() agentID := uuid.New() - statsCh := make(chan *codersdk.AgentStats) + statsCh := make(chan *agentsdk.Stats) derpMap := tailnettest.RunDERPAndSTUN(t) client := &client{ t: t, agentID: agentID, - metadata: codersdk.WorkspaceAgentMetadata{ + metadata: agentsdk.Metadata{ DERPMap: derpMap, }, statsChan: statsCh, @@ -980,11 +981,11 @@ func TestAgent_WriteVSCodeConfigs(t *testing.T) { client := &client{ t: t, agentID: uuid.New(), - metadata: codersdk.WorkspaceAgentMetadata{ + metadata: agentsdk.Metadata{ GitAuthConfigs: 1, DERPMap: &tailcfg.DERPMap{}, }, - statsChan: make(chan *codersdk.AgentStats), + statsChan: make(chan *agentsdk.Stats), coordinator: coordinator, } filesystem := afero.NewMemMapFs() @@ -1009,7 +1010,7 @@ func TestAgent_WriteVSCodeConfigs(t *testing.T) { func setupSSHCommand(t *testing.T, beforeArgs []string, afterArgs []string) *exec.Cmd { //nolint:dogsled - agentConn, _, _, _ := setupAgent(t, codersdk.WorkspaceAgentMetadata{}, 0) + agentConn, _, _, _ := setupAgent(t, agentsdk.Metadata{}, 0) listener, err := net.Listen("tcp", "127.0.0.1:0") require.NoError(t, err) waitGroup := sync.WaitGroup{} @@ -1052,7 +1053,7 @@ func setupSSHCommand(t *testing.T, beforeArgs []string, afterArgs []string) *exe return exec.Command("ssh", args...) } -func setupSSHSession(t *testing.T, options codersdk.WorkspaceAgentMetadata) *ssh.Session { +func setupSSHSession(t *testing.T, options agentsdk.Metadata) *ssh.Session { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() //nolint:dogsled @@ -1076,10 +1077,10 @@ func (c closeFunc) Close() error { return c() } -func setupAgent(t *testing.T, metadata codersdk.WorkspaceAgentMetadata, ptyTimeout time.Duration) ( - *codersdk.AgentConn, +func setupAgent(t *testing.T, metadata agentsdk.Metadata, ptyTimeout time.Duration) ( + *codersdk.WorkspaceAgentConn, *client, - <-chan *codersdk.AgentStats, + <-chan *agentsdk.Stats, afero.Fs, ) { if metadata.DERPMap == nil { @@ -1090,7 +1091,7 @@ func setupAgent(t *testing.T, metadata codersdk.WorkspaceAgentMetadata, ptyTimeo _ = coordinator.Close() }) agentID := uuid.New() - statsCh := make(chan *codersdk.AgentStats, 50) + statsCh := make(chan *agentsdk.Stats, 50) fs := afero.NewMemMapFs() c := &client{ t: t, @@ -1131,7 +1132,7 @@ func setupAgent(t *testing.T, metadata codersdk.WorkspaceAgentMetadata, ptyTimeo return conn.UpdateNodes(node) }) conn.SetNodeCallback(sendNode) - return &codersdk.AgentConn{ + return &codersdk.WorkspaceAgentConn{ Conn: conn, }, c, statsCh, fs } @@ -1170,8 +1171,8 @@ func assertWritePayload(t *testing.T, w io.Writer, payload []byte) { type client struct { t *testing.T agentID uuid.UUID - metadata codersdk.WorkspaceAgentMetadata - statsChan chan *codersdk.AgentStats + metadata agentsdk.Metadata + statsChan chan *agentsdk.Stats coordinator tailnet.Coordinator lastWorkspaceAgent func() @@ -1179,11 +1180,11 @@ type client struct { lifecycleStates []codersdk.WorkspaceAgentLifecycle } -func (c *client) WorkspaceAgentMetadata(_ context.Context) (codersdk.WorkspaceAgentMetadata, error) { +func (c *client) Metadata(_ context.Context) (agentsdk.Metadata, error) { return c.metadata, nil } -func (c *client) ListenWorkspaceAgent(_ context.Context) (net.Conn, error) { +func (c *client) Listen(_ context.Context) (net.Conn, error) { clientConn, serverConn := net.Pipe() closed := make(chan struct{}) c.lastWorkspaceAgent = func() { @@ -1199,7 +1200,7 @@ func (c *client) ListenWorkspaceAgent(_ context.Context) (net.Conn, error) { return clientConn, nil } -func (c *client) AgentReportStats(ctx context.Context, _ slog.Logger, stats func() *codersdk.AgentStats) (io.Closer, error) { +func (c *client) ReportStats(ctx context.Context, _ slog.Logger, stats func() *agentsdk.Stats) (io.Closer, error) { doneCh := make(chan struct{}) ctx, cancel := context.WithCancel(ctx) @@ -1238,18 +1239,18 @@ func (c *client) getLifecycleStates() []codersdk.WorkspaceAgentLifecycle { return c.lifecycleStates } -func (c *client) PostWorkspaceAgentLifecycle(_ context.Context, req codersdk.PostWorkspaceAgentLifecycleRequest) error { +func (c *client) PostLifecycle(_ context.Context, req agentsdk.PostLifecycleRequest) error { c.mu.Lock() defer c.mu.Unlock() c.lifecycleStates = append(c.lifecycleStates, req.State) return nil } -func (*client) PostWorkspaceAgentAppHealth(_ context.Context, _ codersdk.PostWorkspaceAppHealthsRequest) error { +func (*client) PostAppHealth(_ context.Context, _ agentsdk.PostAppHealthsRequest) error { return nil } -func (*client) PostWorkspaceAgentVersion(_ context.Context, _ string) error { +func (*client) PostVersion(_ context.Context, _ string) error { return nil } diff --git a/agent/apphealth.go b/agent/apphealth.go index 88707a137f..3d93b6c85a 100644 --- a/agent/apphealth.go +++ b/agent/apphealth.go @@ -11,6 +11,7 @@ import ( "cdr.dev/slog" "github.com/coder/coder/codersdk" + "github.com/coder/coder/codersdk/agentsdk" "github.com/coder/retry" ) @@ -18,7 +19,7 @@ import ( type WorkspaceAgentApps func(context.Context) ([]codersdk.WorkspaceApp, error) // PostWorkspaceAgentAppHealth updates the workspace app health. -type PostWorkspaceAgentAppHealth func(context.Context, codersdk.PostWorkspaceAppHealthsRequest) error +type PostWorkspaceAgentAppHealth func(context.Context, agentsdk.PostAppHealthsRequest) error // WorkspaceAppHealthReporter is a function that checks and reports the health of the workspace apps until the passed context is canceled. type WorkspaceAppHealthReporter func(ctx context.Context) @@ -132,7 +133,7 @@ func NewWorkspaceAppHealthReporter(logger slog.Logger, apps []codersdk.Workspace mu.Lock() lastHealth = copyHealth(health) mu.Unlock() - err := postWorkspaceAgentAppHealth(ctx, codersdk.PostWorkspaceAppHealthsRequest{ + err := postWorkspaceAgentAppHealth(ctx, agentsdk.PostAppHealthsRequest{ Healths: lastHealth, }) if err != nil { diff --git a/agent/apphealth_test.go b/agent/apphealth_test.go index 14965f18ff..e09b1ca0cc 100644 --- a/agent/apphealth_test.go +++ b/agent/apphealth_test.go @@ -16,6 +16,7 @@ import ( "github.com/coder/coder/agent" "github.com/coder/coder/coderd/httpapi" "github.com/coder/coder/codersdk" + "github.com/coder/coder/codersdk/agentsdk" "github.com/coder/coder/testutil" ) @@ -180,7 +181,7 @@ func setupAppReporter(ctx context.Context, t *testing.T, apps []codersdk.Workspa var newApps []codersdk.WorkspaceApp return append(newApps, apps...), nil } - postWorkspaceAgentAppHealth := func(_ context.Context, req codersdk.PostWorkspaceAppHealthsRequest) error { + postWorkspaceAgentAppHealth := func(_ context.Context, req agentsdk.PostAppHealthsRequest) error { mu.Lock() for id, health := range req.Healths { for i, app := range apps { diff --git a/agent/ports_supported.go b/agent/ports_supported.go index 7c9449d1e2..0b13f5d4b6 100644 --- a/agent/ports_supported.go +++ b/agent/ports_supported.go @@ -11,13 +11,13 @@ import ( "github.com/coder/coder/codersdk" ) -func (lp *listeningPortsHandler) getListeningPorts() ([]codersdk.ListeningPort, error) { +func (lp *listeningPortsHandler) getListeningPorts() ([]codersdk.WorkspaceAgentListeningPort, error) { lp.mut.Lock() defer lp.mut.Unlock() if time.Since(lp.mtime) < time.Second { // copy - ports := make([]codersdk.ListeningPort, len(lp.ports)) + ports := make([]codersdk.WorkspaceAgentListeningPort, len(lp.ports)) copy(ports, lp.ports) return ports, nil } @@ -30,9 +30,9 @@ func (lp *listeningPortsHandler) getListeningPorts() ([]codersdk.ListeningPort, } seen := make(map[uint16]struct{}, len(tabs)) - ports := []codersdk.ListeningPort{} + ports := []codersdk.WorkspaceAgentListeningPort{} for _, tab := range tabs { - if tab.LocalAddr == nil || tab.LocalAddr.Port < codersdk.MinimumListeningPort { + if tab.LocalAddr == nil || tab.LocalAddr.Port < codersdk.WorkspaceAgentMinimumListeningPort { continue } @@ -47,9 +47,9 @@ func (lp *listeningPortsHandler) getListeningPorts() ([]codersdk.ListeningPort, if tab.Process != nil { procName = tab.Process.Name } - ports = append(ports, codersdk.ListeningPort{ + ports = append(ports, codersdk.WorkspaceAgentListeningPort{ ProcessName: procName, - Network: codersdk.ListeningPortNetworkTCP, + Network: "tcp", Port: tab.LocalAddr.Port, }) } @@ -58,7 +58,7 @@ func (lp *listeningPortsHandler) getListeningPorts() ([]codersdk.ListeningPort, lp.mtime = time.Now() // copy - ports = make([]codersdk.ListeningPort, len(lp.ports)) + ports = make([]codersdk.WorkspaceAgentListeningPort, len(lp.ports)) copy(ports, lp.ports) return ports, nil } diff --git a/agent/ports_unsupported.go b/agent/ports_unsupported.go index 5f4c446f5e..0ab26ac299 100644 --- a/agent/ports_unsupported.go +++ b/agent/ports_unsupported.go @@ -4,9 +4,9 @@ package agent import "github.com/coder/coder/codersdk" -func (lp *listeningPortsHandler) getListeningPorts() ([]codersdk.ListeningPort, error) { +func (lp *listeningPortsHandler) getListeningPorts() ([]codersdk.WorkspaceAgentListeningPort, error) { // Can't scan for ports on non-linux or non-windows_amd64 systems at the // moment. The UI will not show any "no ports found" message to the user, so // the user won't suspect a thing. - return []codersdk.ListeningPort{}, nil + return []codersdk.WorkspaceAgentListeningPort{}, nil } diff --git a/agent/statsendpoint.go b/agent/statsendpoint.go index 0ddc01f70d..65fece063e 100644 --- a/agent/statsendpoint.go +++ b/agent/statsendpoint.go @@ -27,7 +27,7 @@ func (*agent) statisticsHandler() http.Handler { type listeningPortsHandler struct { mut sync.Mutex - ports []codersdk.ListeningPort + ports []codersdk.WorkspaceAgentListeningPort mtime time.Time } @@ -43,7 +43,7 @@ func (lp *listeningPortsHandler) handler(rw http.ResponseWriter, r *http.Request return } - httpapi.Write(r.Context(), rw, http.StatusOK, codersdk.ListeningPortsResponse{ + httpapi.Write(r.Context(), rw, http.StatusOK, codersdk.WorkspaceAgentListeningPortsResponse{ Ports: ports, }) } diff --git a/cli/agent.go b/cli/agent.go index 2edc472bd6..fe334c2c2d 100644 --- a/cli/agent.go +++ b/cli/agent.go @@ -22,7 +22,7 @@ import ( "github.com/coder/coder/agent/reaper" "github.com/coder/coder/buildinfo" "github.com/coder/coder/cli/cliflag" - "github.com/coder/coder/codersdk" + "github.com/coder/coder/codersdk/agentsdk" ) func workspaceAgent() *cobra.Command { @@ -82,10 +82,10 @@ func workspaceAgent() *cobra.Command { slog.F("auth", auth), slog.F("version", version), ) - client := codersdk.New(coderURL) - client.Logger = logger + client := agentsdk.New(coderURL) + client.SDK.Logger = logger // Set a reasonable timeout so requests can't hang forever! - client.HTTPClient.Timeout = 10 * time.Second + client.SDK.HTTPClient.Timeout = 10 * time.Second // Enable pprof handler // This prevents the pprof import from being accidentally deleted. @@ -96,7 +96,7 @@ func workspaceAgent() *cobra.Command { // exchangeToken returns a session token. // This is abstracted to allow for the same looping condition // regardless of instance identity auth type. - var exchangeToken func(context.Context) (codersdk.WorkspaceAgentAuthenticateResponse, error) + var exchangeToken func(context.Context) (agentsdk.AuthenticateResponse, error) switch auth { case "token": token, err := cmd.Flags().GetString(varAgentToken) @@ -112,8 +112,8 @@ func workspaceAgent() *cobra.Command { if gcpClientRaw != nil { gcpClient, _ = gcpClientRaw.(*metadata.Client) } - exchangeToken = func(ctx context.Context) (codersdk.WorkspaceAgentAuthenticateResponse, error) { - return client.AuthWorkspaceGoogleInstanceIdentity(ctx, "", gcpClient) + exchangeToken = func(ctx context.Context) (agentsdk.AuthenticateResponse, error) { + return client.AuthGoogleInstanceIdentity(ctx, "", gcpClient) } case "aws-instance-identity": // This is *only* done for testing to mock client authentication. @@ -123,11 +123,11 @@ func workspaceAgent() *cobra.Command { if awsClientRaw != nil { awsClient, _ = awsClientRaw.(*http.Client) if awsClient != nil { - client.HTTPClient = awsClient + client.SDK.HTTPClient = awsClient } } - exchangeToken = func(ctx context.Context) (codersdk.WorkspaceAgentAuthenticateResponse, error) { - return client.AuthWorkspaceAWSInstanceIdentity(ctx) + exchangeToken = func(ctx context.Context) (agentsdk.AuthenticateResponse, error) { + return client.AuthAWSInstanceIdentity(ctx) } case "azure-instance-identity": // This is *only* done for testing to mock client authentication. @@ -137,11 +137,11 @@ func workspaceAgent() *cobra.Command { if azureClientRaw != nil { azureClient, _ = azureClientRaw.(*http.Client) if azureClient != nil { - client.HTTPClient = azureClient + client.SDK.HTTPClient = azureClient } } - exchangeToken = func(ctx context.Context) (codersdk.WorkspaceAgentAuthenticateResponse, error) { - return client.AuthWorkspaceAzureInstanceIdentity(ctx) + exchangeToken = func(ctx context.Context) (agentsdk.AuthenticateResponse, error) { + return client.AuthAzureInstanceIdentity(ctx) } } @@ -159,7 +159,7 @@ func workspaceAgent() *cobra.Command { Logger: logger, ExchangeToken: func(ctx context.Context) (string, error) { if exchangeToken == nil { - return client.SessionToken(), nil + return client.SDK.SessionToken(), nil } resp, err := exchangeToken(ctx) if err != nil { diff --git a/cli/configssh_test.go b/cli/configssh_test.go index d8fcae980b..ceb7e42317 100644 --- a/cli/configssh_test.go +++ b/cli/configssh_test.go @@ -24,7 +24,7 @@ import ( "github.com/coder/coder/agent" "github.com/coder/coder/cli/clitest" "github.com/coder/coder/coderd/coderdtest" - "github.com/coder/coder/codersdk" + "github.com/coder/coder/codersdk/agentsdk" "github.com/coder/coder/provisioner/echo" "github.com/coder/coder/provisionersdk/proto" "github.com/coder/coder/pty/ptytest" @@ -104,7 +104,7 @@ func TestConfigSSH(t *testing.T) { template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) - agentClient := codersdk.New(client.URL) + agentClient := agentsdk.New(client.URL) agentClient.SetSessionToken(authToken) agentCloser := agent.New(agent.Options{ Client: agentClient, diff --git a/cli/gitaskpass.go b/cli/gitaskpass.go index 4d98b0e8da..0296ca5266 100644 --- a/cli/gitaskpass.go +++ b/cli/gitaskpass.go @@ -39,7 +39,7 @@ func gitAskpass() *cobra.Command { return xerrors.Errorf("create agent client: %w", err) } - token, err := client.WorkspaceAgentGitAuth(ctx, host, false) + token, err := client.GitAuth(ctx, host, false) if err != nil { var apiError *codersdk.Error if errors.As(err, &apiError) && apiError.StatusCode() == http.StatusNotFound { @@ -58,7 +58,7 @@ func gitAskpass() *cobra.Command { } for r := retry.New(250*time.Millisecond, 10*time.Second); r.Wait(ctx); { - token, err = client.WorkspaceAgentGitAuth(ctx, host, true) + token, err = client.GitAuth(ctx, host, true) if err != nil { continue } diff --git a/cli/gitaskpass_test.go b/cli/gitaskpass_test.go index 344f95eef5..2e3bdc8850 100644 --- a/cli/gitaskpass_test.go +++ b/cli/gitaskpass_test.go @@ -14,6 +14,7 @@ import ( "github.com/coder/coder/cli/cliui" "github.com/coder/coder/coderd/httpapi" "github.com/coder/coder/codersdk" + "github.com/coder/coder/codersdk/agentsdk" "github.com/coder/coder/pty/ptytest" ) @@ -22,7 +23,7 @@ func TestGitAskpass(t *testing.T) { t.Setenv("GIT_PREFIX", "/") t.Run("UsernameAndPassword", func(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - httpapi.Write(context.Background(), w, http.StatusOK, codersdk.WorkspaceAgentGitAuthResponse{ + httpapi.Write(context.Background(), w, http.StatusOK, agentsdk.GitAuthResponse{ Username: "something", Password: "bananas", }) @@ -61,8 +62,8 @@ func TestGitAskpass(t *testing.T) { }) t.Run("Poll", func(t *testing.T) { - resp := atomic.Pointer[codersdk.WorkspaceAgentGitAuthResponse]{} - resp.Store(&codersdk.WorkspaceAgentGitAuthResponse{ + resp := atomic.Pointer[agentsdk.GitAuthResponse]{} + resp.Store(&agentsdk.GitAuthResponse{ URL: "https://something.org", }) poll := make(chan struct{}, 10) @@ -88,7 +89,7 @@ func TestGitAskpass(t *testing.T) { assert.NoError(t, err) }() <-poll - resp.Store(&codersdk.WorkspaceAgentGitAuthResponse{ + resp.Store(&agentsdk.GitAuthResponse{ Username: "username", Password: "password", }) diff --git a/cli/gitssh.go b/cli/gitssh.go index 09ebc396fd..02a985abee 100644 --- a/cli/gitssh.go +++ b/cli/gitssh.go @@ -42,7 +42,7 @@ func gitssh() *cobra.Command { if err != nil { return xerrors.Errorf("create agent client: %w", err) } - key, err := client.AgentGitSSHKey(ctx) + key, err := client.GitSSHKey(ctx) if err != nil { return xerrors.Errorf("get agent git ssh token: %w", err) } diff --git a/cli/portforward.go b/cli/portforward.go index ea6edb2c9d..b3728212a9 100644 --- a/cli/portforward.go +++ b/cli/portforward.go @@ -156,7 +156,7 @@ func portForward() *cobra.Command { return cmd } -func listenAndPortForward(ctx context.Context, cmd *cobra.Command, conn *codersdk.AgentConn, wg *sync.WaitGroup, spec portForwardSpec) (net.Listener, error) { +func listenAndPortForward(ctx context.Context, cmd *cobra.Command, conn *codersdk.WorkspaceAgentConn, wg *sync.WaitGroup, spec portForwardSpec) (net.Listener, error) { _, _ = fmt.Fprintf(cmd.OutOrStderr(), "Forwarding '%v://%v' locally to '%v://%v' in the workspace\n", spec.listenNetwork, spec.listenAddress, spec.dialNetwork, spec.dialAddress) var ( diff --git a/cli/root.go b/cli/root.go index 867b77e69d..76cc965d18 100644 --- a/cli/root.go +++ b/cli/root.go @@ -5,6 +5,7 @@ import ( "flag" "fmt" "io" + "net" "net/http" "net/url" "os" @@ -31,6 +32,7 @@ import ( "github.com/coder/coder/coderd" "github.com/coder/coder/coderd/gitauth" "github.com/coder/coder/codersdk" + "github.com/coder/coder/codersdk/agentsdk" ) var ( @@ -333,7 +335,7 @@ func createUnauthenticatedClient(cmd *cobra.Command, serverURL *url.URL) (*coder // createAgentClient returns a new client from the command context. // It works just like CreateClient, but uses the agent token and URL instead. -func createAgentClient(cmd *cobra.Command) (*codersdk.Client, error) { +func createAgentClient(cmd *cobra.Command) (*agentsdk.Client, error) { rawURL, err := cmd.Flags().GetString(varAgentURL) if err != nil { return nil, err @@ -346,7 +348,7 @@ func createAgentClient(cmd *cobra.Command) (*codersdk.Client, error) { if err != nil { return nil, err } - client := codersdk.New(serverURL) + client := agentsdk.New(serverURL) client.SetSessionToken(token) return client, nil } @@ -590,7 +592,7 @@ func checkVersions(cmd *cobra.Command, client *codersdk.Client) error { clientVersion := buildinfo.Version() info, err := client.BuildInfo(ctx) // Avoid printing errors that are connection-related. - if codersdk.IsConnectionErr(err) { + if isConnectionError(err) { return nil } @@ -735,3 +737,16 @@ func dumpHandler(ctx context.Context) { } } } + +// IiConnectionErr is a convenience function for checking if the source of an +// error is due to a 'connection refused', 'no such host', etc. +func isConnectionError(err error) bool { + var ( + // E.g. no such host + dnsErr *net.DNSError + // Eg. connection refused + opErr *net.OpError + ) + + return xerrors.As(err, &dnsErr) || xerrors.As(err, &opErr) +} diff --git a/cli/scaletest.go b/cli/scaletest.go index 27a40b3ae1..8dbd270f28 100644 --- a/cli/scaletest.go +++ b/cli/scaletest.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "io" + "net/http" "os" "strconv" "strings" @@ -328,7 +329,14 @@ func scaletestCleanup() *cobra.Command { return err } - client.BypassRatelimits = true + client.HTTPClient = &http.Client{ + Transport: &headerTransport{ + transport: http.DefaultTransport, + headers: map[string]string{ + codersdk.BypassRatelimitHeader: "true", + }, + }, + } cmd.PrintErrln("Fetching scaletest workspaces...") var ( @@ -506,7 +514,14 @@ It is recommended that all rate limits are disabled on the server before running return err } - client.BypassRatelimits = true + client.HTTPClient = &http.Client{ + Transport: &headerTransport{ + transport: http.DefaultTransport, + headers: map[string]string{ + codersdk.BypassRatelimitHeader: "true", + }, + }, + } if count <= 0 { return xerrors.Errorf("--count is required and must be greater than 0") @@ -668,7 +683,7 @@ It is recommended that all rate limits are disabled on the server before running if runCommand != "" { config.ReconnectingPTY = &reconnectingpty.Config{ // AgentID is set by the test automatically. - Init: codersdk.ReconnectingPTYInit{ + Init: codersdk.WorkspaceAgentReconnectingPTYInit{ ID: uuid.Nil, Height: 24, Width: 80, diff --git a/cli/speedtest_test.go b/cli/speedtest_test.go index aa31d1cd37..b91577983c 100644 --- a/cli/speedtest_test.go +++ b/cli/speedtest_test.go @@ -10,7 +10,7 @@ import ( "github.com/coder/coder/agent" "github.com/coder/coder/cli/clitest" "github.com/coder/coder/coderd/coderdtest" - "github.com/coder/coder/codersdk" + "github.com/coder/coder/codersdk/agentsdk" "github.com/coder/coder/pty/ptytest" "github.com/coder/coder/testutil" ) @@ -21,7 +21,7 @@ func TestSpeedtest(t *testing.T) { t.Skip("This test takes a minimum of 5ms per a hardcoded value in Tailscale!") } client, workspace, agentToken := setupWorkspaceForAgent(t, nil) - agentClient := codersdk.New(client.URL) + agentClient := agentsdk.New(client.URL) agentClient.SetSessionToken(agentToken) agentCloser := agent.New(agent.Options{ Client: agentClient, diff --git a/cli/ssh_test.go b/cli/ssh_test.go index bf0aab1417..1e9ddf2e8f 100644 --- a/cli/ssh_test.go +++ b/cli/ssh_test.go @@ -31,6 +31,7 @@ import ( "github.com/coder/coder/cli/cliui" "github.com/coder/coder/coderd/coderdtest" "github.com/coder/coder/codersdk" + "github.com/coder/coder/codersdk/agentsdk" "github.com/coder/coder/provisioner/echo" "github.com/coder/coder/provisionersdk/proto" "github.com/coder/coder/pty" @@ -100,7 +101,7 @@ func TestSSH(t *testing.T) { }) pty.ExpectMatch("Waiting") - agentClient := codersdk.New(client.URL) + agentClient := agentsdk.New(client.URL) agentClient.SetSessionToken(agentToken) agentCloser := agent.New(agent.Options{ Client: agentClient, @@ -149,7 +150,7 @@ func TestSSH(t *testing.T) { _, _ = tGoContext(t, func(ctx context.Context) { // Run this async so the SSH command has to wait for // the build and agent to connect! - agentClient := codersdk.New(client.URL) + agentClient := agentsdk.New(client.URL) agentClient.SetSessionToken(agentToken) agentCloser := agent.New(agent.Options{ Client: agentClient, @@ -216,7 +217,7 @@ func TestSSH(t *testing.T) { client, workspace, agentToken := setupWorkspaceForAgent(t, nil) - agentClient := codersdk.New(client.URL) + agentClient := agentsdk.New(client.URL) agentClient.SetSessionToken(agentToken) agentCloser := agent.New(agent.Options{ Client: agentClient, @@ -449,7 +450,7 @@ Expire-Date: 0 client, workspace, agentToken := setupWorkspaceForAgent(t, nil) - agentClient := codersdk.New(client.URL) + agentClient := agentsdk.New(client.URL) agentClient.SetSessionToken(agentToken) agentCloser := agent.New(agent.Options{ Client: agentClient, diff --git a/cli/tokens.go b/cli/tokens.go index 9fd5362151..847e4b718c 100644 --- a/cli/tokens.go +++ b/cli/tokens.go @@ -73,7 +73,7 @@ func createToken() *cobra.Command { cmd.Println(cliui.Styles.Code.Render(strings.TrimSpace(res.Key))) cmd.Println() cmd.Println(cliui.Styles.Wrap.Render( - fmt.Sprintf("You can use this token by setting the --%s CLI flag, the %s environment variable, or the %q HTTP header.", varToken, envSessionToken, codersdk.SessionCustomHeader), + fmt.Sprintf("You can use this token by setting the --%s CLI flag, the %s environment variable, or the %q HTTP header.", varToken, envSessionToken, codersdk.SessionTokenHeader), )) return nil @@ -103,7 +103,7 @@ func listTokens() *cobra.Command { return xerrors.Errorf("create codersdk client: %w", err) } - keys, err := client.GetTokens(cmd.Context(), codersdk.Me) + keys, err := client.Tokens(cmd.Context(), codersdk.Me) if err != nil { return xerrors.Errorf("create tokens: %w", err) } diff --git a/cli/vscodessh.go b/cli/vscodessh.go index f4f27b832d..adbbbdb5aa 100644 --- a/cli/vscodessh.go +++ b/cli/vscodessh.go @@ -184,7 +184,7 @@ type sshNetworkStats struct { DownloadBytesSec int64 `json:"download_bytes_sec"` } -func collectNetworkStats(ctx context.Context, agentConn *codersdk.AgentConn, lastCollected time.Time) (*sshNetworkStats, error) { +func collectNetworkStats(ctx context.Context, agentConn *codersdk.WorkspaceAgentConn, lastCollected time.Time) (*sshNetworkStats, error) { latency, p2p, err := agentConn.Ping(ctx) if err != nil { return nil, err diff --git a/cli/vscodessh_test.go b/cli/vscodessh_test.go index 6af34913c9..83fd665bb5 100644 --- a/cli/vscodessh_test.go +++ b/cli/vscodessh_test.go @@ -15,6 +15,7 @@ import ( "github.com/coder/coder/cli/clitest" "github.com/coder/coder/coderd/coderdtest" "github.com/coder/coder/codersdk" + "github.com/coder/coder/codersdk/agentsdk" "github.com/coder/coder/testutil" ) @@ -28,7 +29,7 @@ func TestVSCodeSSH(t *testing.T) { user, err := client.User(ctx, codersdk.Me) require.NoError(t, err) - agentClient := codersdk.New(client.URL) + agentClient := agentsdk.New(client.URL) agentClient.SetSessionToken(agentToken) agentCloser := agent.New(agent.Options{ Client: agentClient, diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index d5d7dab0f5..26df5b8fee 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -153,7 +153,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/codersdk.GetAppHostResponse" + "$ref": "#/definitions/codersdk.AppHostResponse" } } } @@ -3781,7 +3781,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/codersdk.AWSInstanceIdentityToken" + "$ref": "#/definitions/agentsdk.AWSInstanceIdentityToken" } } ], @@ -3789,7 +3789,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/codersdk.WorkspaceAgentAuthenticateResponse" + "$ref": "#/definitions/agentsdk.AuthenticateResponse" } } } @@ -3820,7 +3820,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/codersdk.AzureInstanceIdentityToken" + "$ref": "#/definitions/agentsdk.AzureInstanceIdentityToken" } } ], @@ -3828,7 +3828,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/codersdk.WorkspaceAgentAuthenticateResponse" + "$ref": "#/definitions/agentsdk.AuthenticateResponse" } } } @@ -3859,7 +3859,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/codersdk.GoogleInstanceIdentityToken" + "$ref": "#/definitions/agentsdk.GoogleInstanceIdentityToken" } } ], @@ -3867,7 +3867,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/codersdk.WorkspaceAgentAuthenticateResponse" + "$ref": "#/definitions/agentsdk.AuthenticateResponse" } } } @@ -3898,7 +3898,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/codersdk.PostWorkspaceAppHealthsRequest" + "$ref": "#/definitions/agentsdk.PostAppHealthsRequest" } } ], @@ -3964,7 +3964,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/codersdk.WorkspaceAgentGitAuthResponse" + "$ref": "#/definitions/agentsdk.GitAuthResponse" } } } @@ -3989,7 +3989,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/codersdk.AgentGitSSHKey" + "$ref": "#/definitions/agentsdk.GitSSHKey" } } } @@ -4014,7 +4014,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/codersdk.WorkspaceAgentMetadata" + "$ref": "#/definitions/agentsdk.Metadata" } } } @@ -4042,7 +4042,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/codersdk.PostWorkspaceAgentLifecycleRequest" + "$ref": "#/definitions/agentsdk.PostLifecycleRequest" } } ], @@ -4081,7 +4081,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/codersdk.AgentStats" + "$ref": "#/definitions/agentsdk.Stats" } } ], @@ -4089,7 +4089,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/codersdk.AgentStatsResponse" + "$ref": "#/definitions/agentsdk.StatsResponse" } } } @@ -4120,7 +4120,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/codersdk.PostWorkspaceAgentVersionRequest" + "$ref": "#/definitions/agentsdk.PostVersionRequest" } } ], @@ -4262,7 +4262,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/codersdk.ListeningPortsResponse" + "$ref": "#/definitions/codersdk.WorkspaceAgentListeningPortsResponse" } } } @@ -4957,6 +4957,188 @@ const docTemplate = `{ } }, "definitions": { + "agentsdk.AWSInstanceIdentityToken": { + "type": "object", + "required": [ + "document", + "signature" + ], + "properties": { + "document": { + "type": "string" + }, + "signature": { + "type": "string" + } + } + }, + "agentsdk.AuthenticateResponse": { + "type": "object", + "properties": { + "session_token": { + "type": "string" + } + } + }, + "agentsdk.AzureInstanceIdentityToken": { + "type": "object", + "required": [ + "encoding", + "signature" + ], + "properties": { + "encoding": { + "type": "string" + }, + "signature": { + "type": "string" + } + } + }, + "agentsdk.GitAuthResponse": { + "type": "object", + "properties": { + "password": { + "type": "string" + }, + "url": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "agentsdk.GitSSHKey": { + "type": "object", + "properties": { + "private_key": { + "type": "string" + }, + "public_key": { + "type": "string" + } + } + }, + "agentsdk.GoogleInstanceIdentityToken": { + "type": "object", + "required": [ + "json_web_token" + ], + "properties": { + "json_web_token": { + "type": "string" + } + } + }, + "agentsdk.Metadata": { + "type": "object", + "properties": { + "apps": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.WorkspaceApp" + } + }, + "derpmap": { + "$ref": "#/definitions/tailcfg.DERPMap" + }, + "directory": { + "type": "string" + }, + "environment_variables": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "git_auth_configs": { + "description": "GitAuthConfigs stores the number of Git configurations\nthe Coder deployment has. If this number is \u003e0, we\nset up special configuration in the workspace.", + "type": "integer" + }, + "motd_file": { + "type": "string" + }, + "startup_script": { + "type": "string" + }, + "startup_script_timeout": { + "type": "integer" + }, + "vscode_port_proxy_uri": { + "type": "string" + } + } + }, + "agentsdk.PostAppHealthsRequest": { + "type": "object", + "properties": { + "healths": { + "description": "Healths is a map of the workspace app name and the health of the app.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/codersdk.WorkspaceAppHealth" + } + } + } + }, + "agentsdk.PostLifecycleRequest": { + "type": "object", + "properties": { + "state": { + "$ref": "#/definitions/codersdk.WorkspaceAgentLifecycle" + } + } + }, + "agentsdk.PostVersionRequest": { + "type": "object", + "properties": { + "version": { + "type": "string" + } + } + }, + "agentsdk.Stats": { + "type": "object", + "properties": { + "conns_by_proto": { + "description": "ConnsByProto is a count of connections by protocol.", + "type": "object", + "additionalProperties": { + "type": "integer" + } + }, + "num_comms": { + "description": "NumConns is the number of connections received by an agent.", + "type": "integer" + }, + "rx_bytes": { + "description": "RxBytes is the number of received bytes.", + "type": "integer" + }, + "rx_packets": { + "description": "RxPackets is the number of received packets.", + "type": "integer" + }, + "tx_bytes": { + "description": "TxBytes is the number of transmitted bytes.", + "type": "integer" + }, + "tx_packets": { + "description": "TxPackets is the number of transmitted bytes.", + "type": "integer" + } + } + }, + "agentsdk.StatsResponse": { + "type": "object", + "properties": { + "report_interval": { + "description": "ReportInterval is the duration after which the agent should send stats\nagain.", + "type": "integer" + } + } + }, "coderd.SCIMUser": { "type": "object", "properties": { @@ -5107,21 +5289,6 @@ const docTemplate = `{ "APIKeyScopeApplicationConnect" ] }, - "codersdk.AWSInstanceIdentityToken": { - "type": "object", - "required": [ - "document", - "signature" - ], - "properties": { - "document": { - "type": "string" - }, - "signature": { - "type": "string" - } - } - }, "codersdk.AddLicenseRequest": { "type": "object", "required": [ @@ -5133,55 +5300,12 @@ const docTemplate = `{ } } }, - "codersdk.AgentGitSSHKey": { + "codersdk.AppHostResponse": { "type": "object", "properties": { - "private_key": { + "host": { + "description": "Host is the externally accessible URL for the Coder instance.", "type": "string" - }, - "public_key": { - "type": "string" - } - } - }, - "codersdk.AgentStats": { - "type": "object", - "properties": { - "conns_by_proto": { - "description": "ConnsByProto is a count of connections by protocol.", - "type": "object", - "additionalProperties": { - "type": "integer" - } - }, - "num_comms": { - "description": "NumConns is the number of connections received by an agent.", - "type": "integer" - }, - "rx_bytes": { - "description": "RxBytes is the number of received bytes.", - "type": "integer" - }, - "rx_packets": { - "description": "RxPackets is the number of received packets.", - "type": "integer" - }, - "tx_bytes": { - "description": "TxBytes is the number of transmitted bytes.", - "type": "integer" - }, - "tx_packets": { - "description": "TxPackets is the number of transmitted bytes.", - "type": "integer" - } - } - }, - "codersdk.AgentStatsResponse": { - "type": "object", - "properties": { - "report_interval": { - "description": "ReportInterval is the duration after which the agent should send stats\nagain.", - "type": "integer" } } }, @@ -5402,21 +5526,6 @@ const docTemplate = `{ "type": "boolean" } }, - "codersdk.AzureInstanceIdentityToken": { - "type": "object", - "required": [ - "encoding", - "signature" - ], - "properties": { - "encoding": { - "type": "string" - }, - "signature": { - "type": "string" - } - } - }, "codersdk.BuildInfoResponse": { "type": "object", "properties": { @@ -6263,15 +6372,6 @@ const docTemplate = `{ } } }, - "codersdk.GetAppHostResponse": { - "type": "object", - "properties": { - "host": { - "description": "Host is the externally accessible URL for the Coder instance.", - "type": "string" - } - } - }, "codersdk.GetUsersResponse": { "type": "object", "properties": { @@ -6341,17 +6441,6 @@ const docTemplate = `{ } } }, - "codersdk.GoogleInstanceIdentityToken": { - "type": "object", - "required": [ - "json_web_token" - ], - "properties": { - "json_web_token": { - "type": "string" - } - } - }, "codersdk.Group": { "type": "object", "properties": { @@ -6418,47 +6507,6 @@ const docTemplate = `{ } } }, - "codersdk.ListeningPort": { - "type": "object", - "properties": { - "network": { - "description": "only \"tcp\" at the moment", - "allOf": [ - { - "$ref": "#/definitions/codersdk.ListeningPortNetwork" - } - ] - }, - "port": { - "type": "integer" - }, - "process_name": { - "description": "may be empty", - "type": "string" - } - } - }, - "codersdk.ListeningPortNetwork": { - "type": "string", - "enum": [ - "tcp" - ], - "x-enum-varnames": [ - "ListeningPortNetworkTCP" - ] - }, - "codersdk.ListeningPortsResponse": { - "type": "object", - "properties": { - "ports": { - "description": "If there are no ports in the list, nothing should be displayed in the UI.\nThere must not be a \"no ports available\" message or anything similar, as\nthere will always be no ports displayed on platforms where our port\ndetection logic is unsupported.", - "type": "array", - "items": { - "$ref": "#/definitions/codersdk.ListeningPort" - } - } - } - }, "codersdk.LogLevel": { "type": "string", "enum": [ @@ -6836,35 +6884,6 @@ const docTemplate = `{ "ParameterSourceSchemeData" ] }, - "codersdk.PostWorkspaceAgentLifecycleRequest": { - "type": "object", - "properties": { - "state": { - "$ref": "#/definitions/codersdk.WorkspaceAgentLifecycle" - } - } - }, - "codersdk.PostWorkspaceAgentVersionRequest": { - "description": "x-apidocgen:skip", - "type": "object", - "properties": { - "version": { - "type": "string" - } - } - }, - "codersdk.PostWorkspaceAppHealthsRequest": { - "type": "object", - "properties": { - "healths": { - "description": "Healths is a map of the workspace app name and the health of the app.", - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/codersdk.WorkspaceAppHealth" - } - } - } - }, "codersdk.PprofConfig": { "type": "object", "properties": { @@ -7844,14 +7863,6 @@ const docTemplate = `{ } } }, - "codersdk.WorkspaceAgentAuthenticateResponse": { - "type": "object", - "properties": { - "session_token": { - "type": "string" - } - } - }, "codersdk.WorkspaceAgentConnectionInfo": { "type": "object", "properties": { @@ -7860,20 +7871,6 @@ const docTemplate = `{ } } }, - "codersdk.WorkspaceAgentGitAuthResponse": { - "type": "object", - "properties": { - "password": { - "type": "string" - }, - "url": { - "type": "string" - }, - "username": { - "type": "string" - } - } - }, "codersdk.WorkspaceAgentLifecycle": { "type": "string", "enum": [ @@ -7891,42 +7888,31 @@ const docTemplate = `{ "WorkspaceAgentLifecycleReady" ] }, - "codersdk.WorkspaceAgentMetadata": { + "codersdk.WorkspaceAgentListeningPort": { "type": "object", "properties": { - "apps": { + "network": { + "description": "only \"tcp\" at the moment", + "type": "string" + }, + "port": { + "type": "integer" + }, + "process_name": { + "description": "may be empty", + "type": "string" + } + } + }, + "codersdk.WorkspaceAgentListeningPortsResponse": { + "type": "object", + "properties": { + "ports": { + "description": "If there are no ports in the list, nothing should be displayed in the UI.\nThere must not be a \"no ports available\" message or anything similar, as\nthere will always be no ports displayed on platforms where our port\ndetection logic is unsupported.", "type": "array", "items": { - "$ref": "#/definitions/codersdk.WorkspaceApp" + "$ref": "#/definitions/codersdk.WorkspaceAgentListeningPort" } - }, - "derpmap": { - "$ref": "#/definitions/tailcfg.DERPMap" - }, - "directory": { - "type": "string" - }, - "environment_variables": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "git_auth_configs": { - "description": "GitAuthConfigs stores the number of Git configurations\nthe Coder deployment has. If this number is \u003e0, we\nset up special configuration in the workspace.", - "type": "integer" - }, - "motd_file": { - "type": "string" - }, - "startup_script": { - "type": "string" - }, - "startup_script_timeout": { - "type": "integer" - }, - "vscode_port_proxy_uri": { - "type": "string" } } }, diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index ae4e160a33..66a8369ff5 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -125,7 +125,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/codersdk.GetAppHostResponse" + "$ref": "#/definitions/codersdk.AppHostResponse" } } } @@ -3325,7 +3325,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/codersdk.AWSInstanceIdentityToken" + "$ref": "#/definitions/agentsdk.AWSInstanceIdentityToken" } } ], @@ -3333,7 +3333,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/codersdk.WorkspaceAgentAuthenticateResponse" + "$ref": "#/definitions/agentsdk.AuthenticateResponse" } } } @@ -3358,7 +3358,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/codersdk.AzureInstanceIdentityToken" + "$ref": "#/definitions/agentsdk.AzureInstanceIdentityToken" } } ], @@ -3366,7 +3366,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/codersdk.WorkspaceAgentAuthenticateResponse" + "$ref": "#/definitions/agentsdk.AuthenticateResponse" } } } @@ -3391,7 +3391,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/codersdk.GoogleInstanceIdentityToken" + "$ref": "#/definitions/agentsdk.GoogleInstanceIdentityToken" } } ], @@ -3399,7 +3399,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/codersdk.WorkspaceAgentAuthenticateResponse" + "$ref": "#/definitions/agentsdk.AuthenticateResponse" } } } @@ -3424,7 +3424,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/codersdk.PostWorkspaceAppHealthsRequest" + "$ref": "#/definitions/agentsdk.PostAppHealthsRequest" } } ], @@ -3484,7 +3484,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/codersdk.WorkspaceAgentGitAuthResponse" + "$ref": "#/definitions/agentsdk.GitAuthResponse" } } } @@ -3505,7 +3505,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/codersdk.AgentGitSSHKey" + "$ref": "#/definitions/agentsdk.GitSSHKey" } } } @@ -3526,7 +3526,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/codersdk.WorkspaceAgentMetadata" + "$ref": "#/definitions/agentsdk.Metadata" } } } @@ -3550,7 +3550,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/codersdk.PostWorkspaceAgentLifecycleRequest" + "$ref": "#/definitions/agentsdk.PostLifecycleRequest" } } ], @@ -3583,7 +3583,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/codersdk.AgentStats" + "$ref": "#/definitions/agentsdk.Stats" } } ], @@ -3591,7 +3591,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/codersdk.AgentStatsResponse" + "$ref": "#/definitions/agentsdk.StatsResponse" } } } @@ -3616,7 +3616,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/codersdk.PostWorkspaceAgentVersionRequest" + "$ref": "#/definitions/agentsdk.PostVersionRequest" } } ], @@ -3744,7 +3744,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/codersdk.ListeningPortsResponse" + "$ref": "#/definitions/codersdk.WorkspaceAgentListeningPortsResponse" } } } @@ -4368,6 +4368,180 @@ } }, "definitions": { + "agentsdk.AWSInstanceIdentityToken": { + "type": "object", + "required": ["document", "signature"], + "properties": { + "document": { + "type": "string" + }, + "signature": { + "type": "string" + } + } + }, + "agentsdk.AuthenticateResponse": { + "type": "object", + "properties": { + "session_token": { + "type": "string" + } + } + }, + "agentsdk.AzureInstanceIdentityToken": { + "type": "object", + "required": ["encoding", "signature"], + "properties": { + "encoding": { + "type": "string" + }, + "signature": { + "type": "string" + } + } + }, + "agentsdk.GitAuthResponse": { + "type": "object", + "properties": { + "password": { + "type": "string" + }, + "url": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "agentsdk.GitSSHKey": { + "type": "object", + "properties": { + "private_key": { + "type": "string" + }, + "public_key": { + "type": "string" + } + } + }, + "agentsdk.GoogleInstanceIdentityToken": { + "type": "object", + "required": ["json_web_token"], + "properties": { + "json_web_token": { + "type": "string" + } + } + }, + "agentsdk.Metadata": { + "type": "object", + "properties": { + "apps": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.WorkspaceApp" + } + }, + "derpmap": { + "$ref": "#/definitions/tailcfg.DERPMap" + }, + "directory": { + "type": "string" + }, + "environment_variables": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "git_auth_configs": { + "description": "GitAuthConfigs stores the number of Git configurations\nthe Coder deployment has. If this number is \u003e0, we\nset up special configuration in the workspace.", + "type": "integer" + }, + "motd_file": { + "type": "string" + }, + "startup_script": { + "type": "string" + }, + "startup_script_timeout": { + "type": "integer" + }, + "vscode_port_proxy_uri": { + "type": "string" + } + } + }, + "agentsdk.PostAppHealthsRequest": { + "type": "object", + "properties": { + "healths": { + "description": "Healths is a map of the workspace app name and the health of the app.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/codersdk.WorkspaceAppHealth" + } + } + } + }, + "agentsdk.PostLifecycleRequest": { + "type": "object", + "properties": { + "state": { + "$ref": "#/definitions/codersdk.WorkspaceAgentLifecycle" + } + } + }, + "agentsdk.PostVersionRequest": { + "type": "object", + "properties": { + "version": { + "type": "string" + } + } + }, + "agentsdk.Stats": { + "type": "object", + "properties": { + "conns_by_proto": { + "description": "ConnsByProto is a count of connections by protocol.", + "type": "object", + "additionalProperties": { + "type": "integer" + } + }, + "num_comms": { + "description": "NumConns is the number of connections received by an agent.", + "type": "integer" + }, + "rx_bytes": { + "description": "RxBytes is the number of received bytes.", + "type": "integer" + }, + "rx_packets": { + "description": "RxPackets is the number of received packets.", + "type": "integer" + }, + "tx_bytes": { + "description": "TxBytes is the number of transmitted bytes.", + "type": "integer" + }, + "tx_packets": { + "description": "TxPackets is the number of transmitted bytes.", + "type": "integer" + } + } + }, + "agentsdk.StatsResponse": { + "type": "object", + "properties": { + "report_interval": { + "description": "ReportInterval is the duration after which the agent should send stats\nagain.", + "type": "integer" + } + } + }, "coderd.SCIMUser": { "type": "object", "properties": { @@ -4504,18 +4678,6 @@ "enum": ["all", "application_connect"], "x-enum-varnames": ["APIKeyScopeAll", "APIKeyScopeApplicationConnect"] }, - "codersdk.AWSInstanceIdentityToken": { - "type": "object", - "required": ["document", "signature"], - "properties": { - "document": { - "type": "string" - }, - "signature": { - "type": "string" - } - } - }, "codersdk.AddLicenseRequest": { "type": "object", "required": ["license"], @@ -4525,55 +4687,12 @@ } } }, - "codersdk.AgentGitSSHKey": { + "codersdk.AppHostResponse": { "type": "object", "properties": { - "private_key": { + "host": { + "description": "Host is the externally accessible URL for the Coder instance.", "type": "string" - }, - "public_key": { - "type": "string" - } - } - }, - "codersdk.AgentStats": { - "type": "object", - "properties": { - "conns_by_proto": { - "description": "ConnsByProto is a count of connections by protocol.", - "type": "object", - "additionalProperties": { - "type": "integer" - } - }, - "num_comms": { - "description": "NumConns is the number of connections received by an agent.", - "type": "integer" - }, - "rx_bytes": { - "description": "RxBytes is the number of received bytes.", - "type": "integer" - }, - "rx_packets": { - "description": "RxPackets is the number of received packets.", - "type": "integer" - }, - "tx_bytes": { - "description": "TxBytes is the number of transmitted bytes.", - "type": "integer" - }, - "tx_packets": { - "description": "TxPackets is the number of transmitted bytes.", - "type": "integer" - } - } - }, - "codersdk.AgentStatsResponse": { - "type": "object", - "properties": { - "report_interval": { - "description": "ReportInterval is the duration after which the agent should send stats\nagain.", - "type": "integer" } } }, @@ -4783,18 +4902,6 @@ "type": "boolean" } }, - "codersdk.AzureInstanceIdentityToken": { - "type": "object", - "required": ["encoding", "signature"], - "properties": { - "encoding": { - "type": "string" - }, - "signature": { - "type": "string" - } - } - }, "codersdk.BuildInfoResponse": { "type": "object", "properties": { @@ -5588,15 +5695,6 @@ } } }, - "codersdk.GetAppHostResponse": { - "type": "object", - "properties": { - "host": { - "description": "Host is the externally accessible URL for the Coder instance.", - "type": "string" - } - } - }, "codersdk.GetUsersResponse": { "type": "object", "properties": { @@ -5666,15 +5764,6 @@ } } }, - "codersdk.GoogleInstanceIdentityToken": { - "type": "object", - "required": ["json_web_token"], - "properties": { - "json_web_token": { - "type": "string" - } - } - }, "codersdk.Group": { "type": "object", "properties": { @@ -5741,43 +5830,6 @@ } } }, - "codersdk.ListeningPort": { - "type": "object", - "properties": { - "network": { - "description": "only \"tcp\" at the moment", - "allOf": [ - { - "$ref": "#/definitions/codersdk.ListeningPortNetwork" - } - ] - }, - "port": { - "type": "integer" - }, - "process_name": { - "description": "may be empty", - "type": "string" - } - } - }, - "codersdk.ListeningPortNetwork": { - "type": "string", - "enum": ["tcp"], - "x-enum-varnames": ["ListeningPortNetworkTCP"] - }, - "codersdk.ListeningPortsResponse": { - "type": "object", - "properties": { - "ports": { - "description": "If there are no ports in the list, nothing should be displayed in the UI.\nThere must not be a \"no ports available\" message or anything similar, as\nthere will always be no ports displayed on platforms where our port\ndetection logic is unsupported.", - "type": "array", - "items": { - "$ref": "#/definitions/codersdk.ListeningPort" - } - } - } - }, "codersdk.LogLevel": { "type": "string", "enum": ["trace", "debug", "info", "warn", "error"], @@ -6099,35 +6151,6 @@ "ParameterSourceSchemeData" ] }, - "codersdk.PostWorkspaceAgentLifecycleRequest": { - "type": "object", - "properties": { - "state": { - "$ref": "#/definitions/codersdk.WorkspaceAgentLifecycle" - } - } - }, - "codersdk.PostWorkspaceAgentVersionRequest": { - "description": "x-apidocgen:skip", - "type": "object", - "properties": { - "version": { - "type": "string" - } - } - }, - "codersdk.PostWorkspaceAppHealthsRequest": { - "type": "object", - "properties": { - "healths": { - "description": "Healths is a map of the workspace app name and the health of the app.", - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/codersdk.WorkspaceAppHealth" - } - } - } - }, "codersdk.PprofConfig": { "type": "object", "properties": { @@ -7059,14 +7082,6 @@ } } }, - "codersdk.WorkspaceAgentAuthenticateResponse": { - "type": "object", - "properties": { - "session_token": { - "type": "string" - } - } - }, "codersdk.WorkspaceAgentConnectionInfo": { "type": "object", "properties": { @@ -7075,20 +7090,6 @@ } } }, - "codersdk.WorkspaceAgentGitAuthResponse": { - "type": "object", - "properties": { - "password": { - "type": "string" - }, - "url": { - "type": "string" - }, - "username": { - "type": "string" - } - } - }, "codersdk.WorkspaceAgentLifecycle": { "type": "string", "enum": ["created", "starting", "start_timeout", "start_error", "ready"], @@ -7100,42 +7101,31 @@ "WorkspaceAgentLifecycleReady" ] }, - "codersdk.WorkspaceAgentMetadata": { + "codersdk.WorkspaceAgentListeningPort": { "type": "object", "properties": { - "apps": { + "network": { + "description": "only \"tcp\" at the moment", + "type": "string" + }, + "port": { + "type": "integer" + }, + "process_name": { + "description": "may be empty", + "type": "string" + } + } + }, + "codersdk.WorkspaceAgentListeningPortsResponse": { + "type": "object", + "properties": { + "ports": { + "description": "If there are no ports in the list, nothing should be displayed in the UI.\nThere must not be a \"no ports available\" message or anything similar, as\nthere will always be no ports displayed on platforms where our port\ndetection logic is unsupported.", "type": "array", "items": { - "$ref": "#/definitions/codersdk.WorkspaceApp" + "$ref": "#/definitions/codersdk.WorkspaceAgentListeningPort" } - }, - "derpmap": { - "$ref": "#/definitions/tailcfg.DERPMap" - }, - "directory": { - "type": "string" - }, - "environment_variables": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "git_auth_configs": { - "description": "GitAuthConfigs stores the number of Git configurations\nthe Coder deployment has. If this number is \u003e0, we\nset up special configuration in the workspace.", - "type": "integer" - }, - "motd_file": { - "type": "string" - }, - "startup_script": { - "type": "string" - }, - "startup_script_timeout": { - "type": "integer" - }, - "vscode_port_proxy_uri": { - "type": "string" } } }, diff --git a/coderd/apikey.go b/coderd/apikey.go index babde27084..5a8d7882eb 100644 --- a/coderd/apikey.go +++ b/coderd/apikey.go @@ -343,7 +343,7 @@ func (api *API) createAPIKey(ctx context.Context, params createAPIKeyParams) (*h // This format is consumed by the APIKey middleware. sessionToken := fmt.Sprintf("%s-%s", keyID, keySecret) return &http.Cookie{ - Name: codersdk.SessionTokenKey, + Name: codersdk.SessionTokenCookie, Value: sessionToken, Path: "/", HttpOnly: true, diff --git a/coderd/apikey_test.go b/coderd/apikey_test.go index daa632091c..6d4b710984 100644 --- a/coderd/apikey_test.go +++ b/coderd/apikey_test.go @@ -19,7 +19,7 @@ func TestTokenCRUD(t *testing.T) { defer cancel() client := coderdtest.New(t, nil) _ = coderdtest.CreateFirstUser(t, client) - keys, err := client.GetTokens(ctx, codersdk.Me) + keys, err := client.Tokens(ctx, codersdk.Me) require.NoError(t, err) require.Empty(t, keys) @@ -27,7 +27,7 @@ func TestTokenCRUD(t *testing.T) { require.NoError(t, err) require.Greater(t, len(res.Key), 2) - keys, err = client.GetTokens(ctx, codersdk.Me) + keys, err = client.Tokens(ctx, codersdk.Me) require.NoError(t, err) require.EqualValues(t, len(keys), 1) require.Contains(t, res.Key, keys[0].ID) @@ -40,7 +40,7 @@ func TestTokenCRUD(t *testing.T) { err = client.DeleteAPIKey(ctx, codersdk.Me, keys[0].ID) require.NoError(t, err) - keys, err = client.GetTokens(ctx, codersdk.Me) + keys, err = client.Tokens(ctx, codersdk.Me) require.NoError(t, err) require.Empty(t, keys) } @@ -59,7 +59,7 @@ func TestTokenScoped(t *testing.T) { require.NoError(t, err) require.Greater(t, len(res.Key), 2) - keys, err := client.GetTokens(ctx, codersdk.Me) + keys, err := client.Tokens(ctx, codersdk.Me) require.NoError(t, err) require.EqualValues(t, len(keys), 1) require.Contains(t, res.Key, keys[0].ID) @@ -78,7 +78,7 @@ func TestTokenDuration(t *testing.T) { Lifetime: time.Hour * 24 * 7, }) require.NoError(t, err) - keys, err := client.GetTokens(ctx, codersdk.Me) + keys, err := client.Tokens(ctx, codersdk.Me) require.NoError(t, err) require.Greater(t, keys[0].ExpiresAt, time.Now().Add(time.Hour*6*24)) require.Less(t, keys[0].ExpiresAt, time.Now().Add(time.Hour*8*24)) diff --git a/coderd/audit.go b/coderd/audit.go index 947264d45d..23a2375729 100644 --- a/coderd/audit.go +++ b/coderd/audit.go @@ -261,7 +261,7 @@ func (api *API) convertAuditLog(ctx context.Context, dblog database.GetAuditLogs func auditLogDescription(alog database.GetAuditLogsOffsetRow, additionalFields AdditionalFields) string { str := fmt.Sprintf("{user} %s", - codersdk.AuditAction(alog.Action).FriendlyString(), + codersdk.AuditAction(alog.Action).Friendly(), ) // Strings for starting/stopping workspace builds follow the below format: diff --git a/coderd/authorize_test.go b/coderd/authorize_test.go index b495bda4d0..811ade7048 100644 --- a/coderd/authorize_test.go +++ b/coderd/authorize_test.go @@ -133,7 +133,7 @@ func TestCheckPermissions(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) t.Cleanup(cancel) - resp, err := c.Client.CheckAuthorization(ctx, codersdk.AuthorizationRequest{Checks: params}) + resp, err := c.Client.AuthCheck(ctx, codersdk.AuthorizationRequest{Checks: params}) require.NoError(t, err, "check perms") require.Equal(t, c.Check, resp) }) diff --git a/coderd/coderdtest/authorize.go b/coderd/coderdtest/authorize.go index ab6a5a2db9..14019815d2 100644 --- a/coderd/coderdtest/authorize.go +++ b/coderd/coderdtest/authorize.go @@ -342,7 +342,7 @@ func NewAuthTester(ctx context.Context, t *testing.T, client *codersdk.Client, a }) require.NoError(t, err, "create token") - apiKeys, err := client.GetTokens(ctx, admin.UserID.String()) + apiKeys, err := client.Tokens(ctx, admin.UserID.String()) require.NoError(t, err, "get tokens") apiKey := apiKeys[0] diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index 5bc1012366..38ca73ec5c 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -68,6 +68,7 @@ import ( "github.com/coder/coder/coderd/updatecheck" "github.com/coder/coder/coderd/util/ptr" "github.com/coder/coder/codersdk" + "github.com/coder/coder/codersdk/agentsdk" "github.com/coder/coder/cryptorand" "github.com/coder/coder/provisioner/echo" "github.com/coder/coder/provisionerd" @@ -951,7 +952,7 @@ func NewAzureInstanceIdentity(t *testing.T, instanceID string) (x509.VerifyOptio signature := make([]byte, base64.StdEncoding.EncodedLen(len(signatureRaw))) base64.StdEncoding.Encode(signature, signatureRaw) - payload, err := json.Marshal(codersdk.AzureInstanceIdentityToken{ + payload, err := json.Marshal(agentsdk.AzureInstanceIdentityToken{ Signature: string(signature), Encoding: "pkcs7", }) diff --git a/coderd/gitsshkey.go b/coderd/gitsshkey.go index 22f1a5e9e6..3380382f15 100644 --- a/coderd/gitsshkey.go +++ b/coderd/gitsshkey.go @@ -10,6 +10,7 @@ import ( "github.com/coder/coder/coderd/httpmw" "github.com/coder/coder/coderd/rbac" "github.com/coder/coder/codersdk" + "github.com/coder/coder/codersdk/agentsdk" ) // @Summary Regenerate user SSH key @@ -121,7 +122,7 @@ func (api *API) gitSSHKey(rw http.ResponseWriter, r *http.Request) { // @Security CoderSessionToken // @Produce json // @Tags Agents -// @Success 200 {object} codersdk.AgentGitSSHKey +// @Success 200 {object} agentsdk.GitSSHKey // @Router /workspaceagents/me/gitsshkey [get] func (api *API) agentGitSSHKey(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -162,7 +163,7 @@ func (api *API) agentGitSSHKey(rw http.ResponseWriter, r *http.Request) { return } - httpapi.Write(ctx, rw, http.StatusOK, codersdk.AgentGitSSHKey{ + httpapi.Write(ctx, rw, http.StatusOK, agentsdk.GitSSHKey{ PublicKey: gitSSHKey.PublicKey, PrivateKey: gitSSHKey.PrivateKey, }) diff --git a/coderd/gitsshkey_test.go b/coderd/gitsshkey_test.go index 60dc60ac5c..7a0932c5ec 100644 --- a/coderd/gitsshkey_test.go +++ b/coderd/gitsshkey_test.go @@ -12,7 +12,7 @@ import ( "github.com/coder/coder/coderd/coderdtest" "github.com/coder/coder/coderd/database" "github.com/coder/coder/coderd/gitsshkey" - "github.com/coder/coder/codersdk" + "github.com/coder/coder/codersdk/agentsdk" "github.com/coder/coder/provisioner/echo" "github.com/coder/coder/provisionersdk/proto" "github.com/coder/coder/testutil" @@ -133,13 +133,13 @@ func TestAgentGitSSHKey(t *testing.T) { workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, project.ID) coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) - agentClient := codersdk.New(client.URL) + agentClient := agentsdk.New(client.URL) agentClient.SetSessionToken(authToken) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - agentKey, err := agentClient.AgentGitSSHKey(ctx) + agentKey, err := agentClient.GitSSHKey(ctx) require.NoError(t, err) require.NotEmpty(t, agentKey.PrivateKey) } diff --git a/coderd/httpapi/cookie.go b/coderd/httpapi/cookie.go index 7e0893c477..06e5d6ed77 100644 --- a/coderd/httpapi/cookie.go +++ b/coderd/httpapi/cookie.go @@ -20,9 +20,9 @@ func StripCoderCookies(header string) string { continue } name, _, _ := strings.Cut(part, "=") - if name == codersdk.SessionTokenKey || - name == codersdk.OAuth2StateKey || - name == codersdk.OAuth2RedirectKey { + if name == codersdk.SessionTokenCookie || + name == codersdk.OAuth2StateCookie || + name == codersdk.OAuth2RedirectCookie { continue } cookies = append(cookies, part) diff --git a/coderd/httpmw/apikey.go b/coderd/httpmw/apikey.go index ceb974c92c..5899cc6e07 100644 --- a/coderd/httpmw/apikey.go +++ b/coderd/httpmw/apikey.go @@ -144,7 +144,7 @@ func ExtractAPIKey(cfg ExtractAPIKeyConfig) func(http.Handler) http.Handler { if token == "" { optionalWrite(http.StatusUnauthorized, codersdk.Response{ Message: SignedOutErrorMessage, - Detail: fmt.Sprintf("Cookie %q or query parameter must be provided.", codersdk.SessionTokenKey), + Detail: fmt.Sprintf("Cookie %q or query parameter must be provided.", codersdk.SessionTokenCookie), }) return } @@ -364,17 +364,17 @@ func ExtractAPIKey(cfg ExtractAPIKeyConfig) func(http.Handler) http.Handler { // 4. The coder_session_token query parameter // 5. The custom auth header func apiTokenFromRequest(r *http.Request) string { - cookie, err := r.Cookie(codersdk.SessionTokenKey) + cookie, err := r.Cookie(codersdk.SessionTokenCookie) if err == nil && cookie.Value != "" { return cookie.Value } - urlValue := r.URL.Query().Get(codersdk.SessionTokenKey) + urlValue := r.URL.Query().Get(codersdk.SessionTokenCookie) if urlValue != "" { return urlValue } - headerValue := r.Header.Get(codersdk.SessionCustomHeader) + headerValue := r.Header.Get(codersdk.SessionTokenHeader) if headerValue != "" { return headerValue } diff --git a/coderd/httpmw/apikey_test.go b/coderd/httpmw/apikey_test.go index d5b5ac382c..c4266646ed 100644 --- a/coderd/httpmw/apikey_test.go +++ b/coderd/httpmw/apikey_test.go @@ -82,7 +82,7 @@ func TestAPIKey(t *testing.T) { r = httptest.NewRequest("GET", "/", nil) rw = httptest.NewRecorder() ) - r.Header.Set(codersdk.SessionCustomHeader, "test-wow-hello") + r.Header.Set(codersdk.SessionTokenHeader, "test-wow-hello") httpmw.ExtractAPIKey(httpmw.ExtractAPIKeyConfig{ DB: db, @@ -100,7 +100,7 @@ func TestAPIKey(t *testing.T) { r = httptest.NewRequest("GET", "/", nil) rw = httptest.NewRecorder() ) - r.Header.Set(codersdk.SessionCustomHeader, "test-wow") + r.Header.Set(codersdk.SessionTokenHeader, "test-wow") httpmw.ExtractAPIKey(httpmw.ExtractAPIKeyConfig{ DB: db, @@ -118,7 +118,7 @@ func TestAPIKey(t *testing.T) { r = httptest.NewRequest("GET", "/", nil) rw = httptest.NewRecorder() ) - r.Header.Set(codersdk.SessionCustomHeader, "testtestid-wow") + r.Header.Set(codersdk.SessionTokenHeader, "testtestid-wow") httpmw.ExtractAPIKey(httpmw.ExtractAPIKeyConfig{ DB: db, @@ -137,7 +137,7 @@ func TestAPIKey(t *testing.T) { r = httptest.NewRequest("GET", "/", nil) rw = httptest.NewRecorder() ) - r.Header.Set(codersdk.SessionCustomHeader, fmt.Sprintf("%s-%s", id, secret)) + r.Header.Set(codersdk.SessionTokenHeader, fmt.Sprintf("%s-%s", id, secret)) httpmw.ExtractAPIKey(httpmw.ExtractAPIKeyConfig{ DB: db, @@ -157,7 +157,7 @@ func TestAPIKey(t *testing.T) { rw = httptest.NewRecorder() user = createUser(r.Context(), t, db) ) - r.Header.Set(codersdk.SessionCustomHeader, fmt.Sprintf("%s-%s", id, secret)) + r.Header.Set(codersdk.SessionTokenHeader, fmt.Sprintf("%s-%s", id, secret)) // Use a different secret so they don't match! hashed := sha256.Sum256([]byte("differentsecret")) @@ -188,7 +188,7 @@ func TestAPIKey(t *testing.T) { rw = httptest.NewRecorder() user = createUser(r.Context(), t, db) ) - r.Header.Set(codersdk.SessionCustomHeader, fmt.Sprintf("%s-%s", id, secret)) + r.Header.Set(codersdk.SessionTokenHeader, fmt.Sprintf("%s-%s", id, secret)) _, err := db.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{ ID: id, @@ -217,7 +217,7 @@ func TestAPIKey(t *testing.T) { rw = httptest.NewRecorder() user = createUser(r.Context(), t, db) ) - r.Header.Set(codersdk.SessionCustomHeader, fmt.Sprintf("%s-%s", id, secret)) + r.Header.Set(codersdk.SessionTokenHeader, fmt.Sprintf("%s-%s", id, secret)) sentAPIKey, err := db.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{ ID: id, @@ -259,7 +259,7 @@ func TestAPIKey(t *testing.T) { user = createUser(r.Context(), t, db) ) r.AddCookie(&http.Cookie{ - Name: codersdk.SessionTokenKey, + Name: codersdk.SessionTokenCookie, Value: fmt.Sprintf("%s-%s", id, secret), }) @@ -302,7 +302,7 @@ func TestAPIKey(t *testing.T) { user = createUser(r.Context(), t, db) ) q := r.URL.Query() - q.Add(codersdk.SessionTokenKey, fmt.Sprintf("%s-%s", id, secret)) + q.Add(codersdk.SessionTokenCookie, fmt.Sprintf("%s-%s", id, secret)) r.URL.RawQuery = q.Encode() _, err := db.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{ @@ -339,7 +339,7 @@ func TestAPIKey(t *testing.T) { rw = httptest.NewRecorder() user = createUser(r.Context(), t, db) ) - r.Header.Set(codersdk.SessionCustomHeader, fmt.Sprintf("%s-%s", id, secret)) + r.Header.Set(codersdk.SessionTokenHeader, fmt.Sprintf("%s-%s", id, secret)) sentAPIKey, err := db.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{ ID: id, @@ -376,7 +376,7 @@ func TestAPIKey(t *testing.T) { rw = httptest.NewRecorder() user = createUser(r.Context(), t, db) ) - r.Header.Set(codersdk.SessionCustomHeader, fmt.Sprintf("%s-%s", id, secret)) + r.Header.Set(codersdk.SessionTokenHeader, fmt.Sprintf("%s-%s", id, secret)) sentAPIKey, err := db.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{ ID: id, @@ -413,7 +413,7 @@ func TestAPIKey(t *testing.T) { rw = httptest.NewRecorder() user = createUser(r.Context(), t, db) ) - r.Header.Set(codersdk.SessionCustomHeader, fmt.Sprintf("%s-%s", id, secret)) + r.Header.Set(codersdk.SessionTokenHeader, fmt.Sprintf("%s-%s", id, secret)) sentAPIKey, err := db.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{ ID: id, @@ -457,7 +457,7 @@ func TestAPIKey(t *testing.T) { rw = httptest.NewRecorder() user = createUser(r.Context(), t, db) ) - r.Header.Set(codersdk.SessionCustomHeader, fmt.Sprintf("%s-%s", id, secret)) + r.Header.Set(codersdk.SessionTokenHeader, fmt.Sprintf("%s-%s", id, secret)) sentAPIKey, err := db.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{ ID: id, @@ -514,7 +514,7 @@ func TestAPIKey(t *testing.T) { user = createUser(r.Context(), t, db) ) r.RemoteAddr = "1.1.1.1" - r.Header.Set(codersdk.SessionCustomHeader, fmt.Sprintf("%s-%s", id, secret)) + r.Header.Set(codersdk.SessionTokenHeader, fmt.Sprintf("%s-%s", id, secret)) _, err := db.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{ ID: id, @@ -602,7 +602,7 @@ func TestAPIKey(t *testing.T) { rw = httptest.NewRecorder() user = createUser(r.Context(), t, db) ) - r.Header.Set(codersdk.SessionCustomHeader, fmt.Sprintf("%s-%s", id, secret)) + r.Header.Set(codersdk.SessionTokenHeader, fmt.Sprintf("%s-%s", id, secret)) sentAPIKey, err := db.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{ ID: id, diff --git a/coderd/httpmw/authorize_test.go b/coderd/httpmw/authorize_test.go index 46678cb884..d715c7e313 100644 --- a/coderd/httpmw/authorize_test.go +++ b/coderd/httpmw/authorize_test.go @@ -131,7 +131,7 @@ func TestExtractUserRoles(t *testing.T) { }) req := httptest.NewRequest("GET", "/", nil) - req.Header.Set(codersdk.SessionCustomHeader, token) + req.Header.Set(codersdk.SessionTokenHeader, token) rtr.ServeHTTP(rw, req) resp := rw.Result() diff --git a/coderd/httpmw/csrf.go b/coderd/httpmw/csrf.go index b4f0880569..ce25c60094 100644 --- a/coderd/httpmw/csrf.go +++ b/coderd/httpmw/csrf.go @@ -41,19 +41,19 @@ func CSRF(secureCookie bool) func(next http.Handler) http.Handler { // CSRF only affects requests that automatically attach credentials via a cookie. // If no cookie is present, then there is no risk of CSRF. //nolint:govet - sessCookie, err := r.Cookie(codersdk.SessionTokenKey) + sessCookie, err := r.Cookie(codersdk.SessionTokenCookie) if xerrors.Is(err, http.ErrNoCookie) { return true } - if token := r.Header.Get(codersdk.SessionCustomHeader); token == sessCookie.Value { + if token := r.Header.Get(codersdk.SessionTokenHeader); token == sessCookie.Value { // If the cookie and header match, we can assume this is the same as just using the // custom header auth. Custom header auth can bypass CSRF, as CSRF attacks // cannot add custom headers. return true } - if token := r.URL.Query().Get(codersdk.SessionTokenKey); token == sessCookie.Value { + if token := r.URL.Query().Get(codersdk.SessionTokenCookie); token == sessCookie.Value { // If the auth is set in a url param and matches the cookie, it // is the same as just using the url param. return true diff --git a/coderd/httpmw/oauth2.go b/coderd/httpmw/oauth2.go index f335e9175e..820523b6be 100644 --- a/coderd/httpmw/oauth2.go +++ b/coderd/httpmw/oauth2.go @@ -71,7 +71,7 @@ func ExtractOAuth2(config OAuth2Config, client *http.Client) func(http.Handler) } http.SetCookie(rw, &http.Cookie{ - Name: codersdk.OAuth2StateKey, + Name: codersdk.OAuth2StateCookie, Value: state, Path: "/", HttpOnly: true, @@ -80,7 +80,7 @@ func ExtractOAuth2(config OAuth2Config, client *http.Client) func(http.Handler) // Redirect must always be specified, otherwise // an old redirect could apply! http.SetCookie(rw, &http.Cookie{ - Name: codersdk.OAuth2RedirectKey, + Name: codersdk.OAuth2RedirectCookie, Value: r.URL.Query().Get("redirect"), Path: "/", HttpOnly: true, @@ -98,10 +98,10 @@ func ExtractOAuth2(config OAuth2Config, client *http.Client) func(http.Handler) return } - stateCookie, err := r.Cookie(codersdk.OAuth2StateKey) + stateCookie, err := r.Cookie(codersdk.OAuth2StateCookie) if err != nil { httpapi.Write(ctx, rw, http.StatusUnauthorized, codersdk.Response{ - Message: fmt.Sprintf("Cookie %q must be provided.", codersdk.OAuth2StateKey), + Message: fmt.Sprintf("Cookie %q must be provided.", codersdk.OAuth2StateCookie), }) return } @@ -113,7 +113,7 @@ func ExtractOAuth2(config OAuth2Config, client *http.Client) func(http.Handler) } var redirect string - stateRedirect, err := r.Cookie(codersdk.OAuth2RedirectKey) + stateRedirect, err := r.Cookie(codersdk.OAuth2RedirectCookie) if err == nil { redirect = stateRedirect.Value } diff --git a/coderd/httpmw/oauth2_test.go b/coderd/httpmw/oauth2_test.go index b53755d21a..40fcb9186e 100644 --- a/coderd/httpmw/oauth2_test.go +++ b/coderd/httpmw/oauth2_test.go @@ -73,7 +73,7 @@ func TestOAuth2(t *testing.T) { t.Parallel() req := httptest.NewRequest("GET", "/?code=something&state=test", nil) req.AddCookie(&http.Cookie{ - Name: codersdk.OAuth2StateKey, + Name: codersdk.OAuth2StateCookie, Value: "mismatch", }) res := httptest.NewRecorder() @@ -84,7 +84,7 @@ func TestOAuth2(t *testing.T) { t.Parallel() req := httptest.NewRequest("GET", "/?code=test&state=something", nil) req.AddCookie(&http.Cookie{ - Name: codersdk.OAuth2StateKey, + Name: codersdk.OAuth2StateCookie, Value: "something", }) req.AddCookie(&http.Cookie{ diff --git a/coderd/httpmw/organizationparam_test.go b/coderd/httpmw/organizationparam_test.go index 0a58911bda..3704569599 100644 --- a/coderd/httpmw/organizationparam_test.go +++ b/coderd/httpmw/organizationparam_test.go @@ -29,7 +29,7 @@ func TestOrganizationParam(t *testing.T) { r = httptest.NewRequest("GET", "/", nil) hashed = sha256.Sum256([]byte(secret)) ) - r.Header.Set(codersdk.SessionCustomHeader, fmt.Sprintf("%s-%s", id, secret)) + r.Header.Set(codersdk.SessionTokenHeader, fmt.Sprintf("%s-%s", id, secret)) userID := uuid.New() username, err := cryptorand.String(8) diff --git a/coderd/httpmw/ratelimit_test.go b/coderd/httpmw/ratelimit_test.go index 9e2ec370e8..61a6f5d903 100644 --- a/coderd/httpmw/ratelimit_test.go +++ b/coderd/httpmw/ratelimit_test.go @@ -111,7 +111,7 @@ func TestRateLimit(t *testing.T) { // Bypass must fail req := httptest.NewRequest("GET", "/", nil) - req.Header.Set(codersdk.SessionCustomHeader, key) + req.Header.Set(codersdk.SessionTokenHeader, key) req.Header.Set(codersdk.BypassRatelimitHeader, "true") rec := httptest.NewRecorder() // Assert we're not using IP address. @@ -123,7 +123,7 @@ func TestRateLimit(t *testing.T) { require.Eventually(t, func() bool { req := httptest.NewRequest("GET", "/", nil) - req.Header.Set(codersdk.SessionCustomHeader, key) + req.Header.Set(codersdk.SessionTokenHeader, key) rec := httptest.NewRecorder() // Assert we're not using IP address. req.RemoteAddr = randRemoteAddr() @@ -160,7 +160,7 @@ func TestRateLimit(t *testing.T) { require.Never(t, func() bool { req := httptest.NewRequest("GET", "/", nil) - req.Header.Set(codersdk.SessionCustomHeader, key) + req.Header.Set(codersdk.SessionTokenHeader, key) req.Header.Set(codersdk.BypassRatelimitHeader, "true") rec := httptest.NewRecorder() // Assert we're not using IP address. diff --git a/coderd/httpmw/templateparam_test.go b/coderd/httpmw/templateparam_test.go index 1189877ded..e4cbe60260 100644 --- a/coderd/httpmw/templateparam_test.go +++ b/coderd/httpmw/templateparam_test.go @@ -29,7 +29,7 @@ func TestTemplateParam(t *testing.T) { hashed = sha256.Sum256([]byte(secret)) ) r := httptest.NewRequest("GET", "/", nil) - r.Header.Set(codersdk.SessionCustomHeader, fmt.Sprintf("%s-%s", id, secret)) + r.Header.Set(codersdk.SessionTokenHeader, fmt.Sprintf("%s-%s", id, secret)) userID := uuid.New() username, err := cryptorand.String(8) diff --git a/coderd/httpmw/templateversionparam_test.go b/coderd/httpmw/templateversionparam_test.go index 36a916f150..796ae7ec9b 100644 --- a/coderd/httpmw/templateversionparam_test.go +++ b/coderd/httpmw/templateversionparam_test.go @@ -29,7 +29,7 @@ func TestTemplateVersionParam(t *testing.T) { hashed = sha256.Sum256([]byte(secret)) ) r := httptest.NewRequest("GET", "/", nil) - r.Header.Set(codersdk.SessionCustomHeader, fmt.Sprintf("%s-%s", id, secret)) + r.Header.Set(codersdk.SessionTokenHeader, fmt.Sprintf("%s-%s", id, secret)) userID := uuid.New() username, err := cryptorand.String(8) diff --git a/coderd/httpmw/userparam_test.go b/coderd/httpmw/userparam_test.go index 496e3c0f2c..0beca82768 100644 --- a/coderd/httpmw/userparam_test.go +++ b/coderd/httpmw/userparam_test.go @@ -29,7 +29,7 @@ func TestUserParam(t *testing.T) { r = httptest.NewRequest("GET", "/", nil) rw = httptest.NewRecorder() ) - r.Header.Set(codersdk.SessionCustomHeader, fmt.Sprintf("%s-%s", id, secret)) + r.Header.Set(codersdk.SessionTokenHeader, fmt.Sprintf("%s-%s", id, secret)) user, err := db.InsertUser(r.Context(), database.InsertUserParams{ ID: uuid.New(), diff --git a/coderd/httpmw/workspaceagent.go b/coderd/httpmw/workspaceagent.go index d2172430e0..ea76ac8ad0 100644 --- a/coderd/httpmw/workspaceagent.go +++ b/coderd/httpmw/workspaceagent.go @@ -33,7 +33,7 @@ func ExtractWorkspaceAgent(db database.Store) func(http.Handler) http.Handler { tokenValue := apiTokenFromRequest(r) if tokenValue == "" { httpapi.Write(ctx, rw, http.StatusUnauthorized, codersdk.Response{ - Message: fmt.Sprintf("Cookie %q must be provided.", codersdk.SessionTokenKey), + Message: fmt.Sprintf("Cookie %q must be provided.", codersdk.SessionTokenCookie), }) return } diff --git a/coderd/httpmw/workspaceagent_test.go b/coderd/httpmw/workspaceagent_test.go index a33bc51bdc..b205ea6fde 100644 --- a/coderd/httpmw/workspaceagent_test.go +++ b/coderd/httpmw/workspaceagent_test.go @@ -22,7 +22,7 @@ func TestWorkspaceAgent(t *testing.T) { setup := func(db database.Store) (*http.Request, uuid.UUID) { token := uuid.New() r := httptest.NewRequest("GET", "/", nil) - r.Header.Set(codersdk.SessionCustomHeader, token.String()) + r.Header.Set(codersdk.SessionTokenHeader, token.String()) return r, token } diff --git a/coderd/httpmw/workspaceagentparam_test.go b/coderd/httpmw/workspaceagentparam_test.go index d6d83c6ba8..fa31b60a00 100644 --- a/coderd/httpmw/workspaceagentparam_test.go +++ b/coderd/httpmw/workspaceagentparam_test.go @@ -29,7 +29,7 @@ func TestWorkspaceAgentParam(t *testing.T) { hashed = sha256.Sum256([]byte(secret)) ) r := httptest.NewRequest("GET", "/", nil) - r.Header.Set(codersdk.SessionCustomHeader, fmt.Sprintf("%s-%s", id, secret)) + r.Header.Set(codersdk.SessionTokenHeader, fmt.Sprintf("%s-%s", id, secret)) userID := uuid.New() username, err := cryptorand.String(8) diff --git a/coderd/httpmw/workspacebuildparam_test.go b/coderd/httpmw/workspacebuildparam_test.go index ef21e12788..a664d3bb3f 100644 --- a/coderd/httpmw/workspacebuildparam_test.go +++ b/coderd/httpmw/workspacebuildparam_test.go @@ -29,7 +29,7 @@ func TestWorkspaceBuildParam(t *testing.T) { hashed = sha256.Sum256([]byte(secret)) ) r := httptest.NewRequest("GET", "/", nil) - r.Header.Set(codersdk.SessionCustomHeader, fmt.Sprintf("%s-%s", id, secret)) + r.Header.Set(codersdk.SessionTokenHeader, fmt.Sprintf("%s-%s", id, secret)) userID := uuid.New() username, err := cryptorand.String(8) diff --git a/coderd/httpmw/workspaceparam_test.go b/coderd/httpmw/workspaceparam_test.go index e5d617b955..267d994a17 100644 --- a/coderd/httpmw/workspaceparam_test.go +++ b/coderd/httpmw/workspaceparam_test.go @@ -32,7 +32,7 @@ func TestWorkspaceParam(t *testing.T) { hashed = sha256.Sum256([]byte(secret)) ) r := httptest.NewRequest("GET", "/", nil) - r.Header.Set(codersdk.SessionCustomHeader, fmt.Sprintf("%s-%s", id, secret)) + r.Header.Set(codersdk.SessionTokenHeader, fmt.Sprintf("%s-%s", id, secret)) userID := uuid.New() username, err := cryptorand.String(8) @@ -345,7 +345,7 @@ func setupWorkspaceWithAgents(t testing.TB, cfg setupConfig) (database.Store, *h hashed = sha256.Sum256([]byte(secret)) ) r := httptest.NewRequest("GET", "/", nil) - r.Header.Set(codersdk.SessionCustomHeader, fmt.Sprintf("%s-%s", id, secret)) + r.Header.Set(codersdk.SessionTokenHeader, fmt.Sprintf("%s-%s", id, secret)) userID := uuid.New() username, err := cryptorand.String(8) diff --git a/coderd/insights_test.go b/coderd/insights_test.go index 08ac17bad2..8eec448b62 100644 --- a/coderd/insights_test.go +++ b/coderd/insights_test.go @@ -13,6 +13,7 @@ import ( "github.com/coder/coder/agent" "github.com/coder/coder/coderd/coderdtest" "github.com/coder/coder/codersdk" + "github.com/coder/coder/codersdk/agentsdk" "github.com/coder/coder/provisioner/echo" "github.com/coder/coder/provisionersdk/proto" "github.com/coder/coder/testutil" @@ -56,7 +57,7 @@ func TestDeploymentInsights(t *testing.T) { workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) - agentClient := codersdk.New(client.URL) + agentClient := agentsdk.New(client.URL) agentClient.SetSessionToken(authToken) agentCloser := agent.New(agent.Options{ Logger: slogtest.Make(t, nil), diff --git a/coderd/templates_test.go b/coderd/templates_test.go index 0bd46ba180..73a846fe35 100644 --- a/coderd/templates_test.go +++ b/coderd/templates_test.go @@ -18,6 +18,7 @@ import ( "github.com/coder/coder/coderd/rbac" "github.com/coder/coder/coderd/util/ptr" "github.com/coder/coder/codersdk" + "github.com/coder/coder/codersdk/agentsdk" "github.com/coder/coder/provisioner/echo" "github.com/coder/coder/provisionersdk/proto" "github.com/coder/coder/testutil" @@ -544,7 +545,7 @@ func TestTemplateMetrics(t *testing.T) { workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) - agentClient := codersdk.New(client.URL) + agentClient := agentsdk.New(client.URL) agentClient.SetSessionToken(authToken) agentCloser := agent.New(agent.Options{ Logger: slogtest.Make(t, nil), diff --git a/coderd/userauth_test.go b/coderd/userauth_test.go index 0cb16afa33..ea70728963 100644 --- a/coderd/userauth_test.go +++ b/coderd/userauth_test.go @@ -751,7 +751,7 @@ func oauth2Callback(t *testing.T, client *codersdk.Client) *http.Response { req, err := http.NewRequestWithContext(context.Background(), "GET", oauthURL.String(), nil) require.NoError(t, err) req.AddCookie(&http.Cookie{ - Name: codersdk.OAuth2StateKey, + Name: codersdk.OAuth2StateCookie, Value: state, }) res, err := client.HTTPClient.Do(req) @@ -772,7 +772,7 @@ func oidcCallback(t *testing.T, client *codersdk.Client, code string) *http.Resp req, err := http.NewRequestWithContext(context.Background(), "GET", oauthURL.String(), nil) require.NoError(t, err) req.AddCookie(&http.Cookie{ - Name: codersdk.OAuth2StateKey, + Name: codersdk.OAuth2StateCookie, Value: "somestate", }) res, err := client.HTTPClient.Do(req) @@ -790,7 +790,7 @@ func i64ptr(i int64) *int64 { func authCookieValue(cookies []*http.Cookie) string { for _, cookie := range cookies { - if cookie.Name == codersdk.SessionTokenKey { + if cookie.Name == codersdk.SessionTokenCookie { return cookie.Value } } diff --git a/coderd/users.go b/coderd/users.go index bd2c991a03..9ad4d208f3 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -1079,7 +1079,7 @@ func (api *API) postLogout(rw http.ResponseWriter, r *http.Request) { cookie := &http.Cookie{ // MaxAge < 0 means to delete the cookie now. MaxAge: -1, - Name: codersdk.SessionTokenKey, + Name: codersdk.SessionTokenCookie, Path: "/", } http.SetCookie(rw, cookie) diff --git a/coderd/users_test.go b/coderd/users_test.go index f19e935482..7e6932073b 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -230,7 +230,7 @@ func TestPostLogin(t *testing.T) { defer cancel() split := strings.Split(client.SessionToken(), "-") - key, err := client.GetAPIKey(ctx, admin.UserID.String(), split[0]) + key, err := client.APIKey(ctx, admin.UserID.String(), split[0]) require.NoError(t, err, "fetch login key") require.Equal(t, int64(86400), key.LifetimeSeconds, "default should be 86400") @@ -238,7 +238,7 @@ func TestPostLogin(t *testing.T) { token, err := client.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{}) require.NoError(t, err, "make new token api key") split = strings.Split(token.Key, "-") - apiKey, err := client.GetAPIKey(ctx, admin.UserID.String(), split[0]) + apiKey, err := client.APIKey(ctx, admin.UserID.String(), split[0]) require.NoError(t, err, "fetch api key") require.True(t, apiKey.ExpiresAt.After(time.Now().Add(time.Hour*24*29)), "default tokens lasts more than 29 days") @@ -307,7 +307,7 @@ func TestPostLogout(t *testing.T) { defer cancel() keyID := strings.Split(client.SessionToken(), "-")[0] - apiKey, err := client.GetAPIKey(ctx, admin.UserID.String(), keyID) + apiKey, err := client.APIKey(ctx, admin.UserID.String(), keyID) require.NoError(t, err) require.Equal(t, keyID, apiKey.ID, "API key should exist in the database") @@ -323,15 +323,15 @@ func TestPostLogout(t *testing.T) { var found bool for _, cookie := range cookies { - if cookie.Name == codersdk.SessionTokenKey { - require.Equal(t, codersdk.SessionTokenKey, cookie.Name, "Cookie should be the auth cookie") + if cookie.Name == codersdk.SessionTokenCookie { + require.Equal(t, codersdk.SessionTokenCookie, cookie.Name, "Cookie should be the auth cookie") require.Equal(t, -1, cookie.MaxAge, "Cookie should be set to delete") found = true } } require.True(t, found, "auth cookie should be returned") - _, err = client.GetAPIKey(ctx, admin.UserID.String(), keyID) + _, err = client.APIKey(ctx, admin.UserID.String(), keyID) sdkErr := &codersdk.Error{} require.ErrorAs(t, err, &sdkErr) require.Equal(t, http.StatusUnauthorized, sdkErr.StatusCode(), "Expecting 401") @@ -615,7 +615,7 @@ func TestUpdateUserPassword(t *testing.T) { // Trying to get an API key should fail since our client's token // has been deleted. - _, err = client.GetAPIKey(ctx, user.UserID.String(), apikey1.Key) + _, err = client.APIKey(ctx, user.UserID.String(), apikey1.Key) require.Error(t, err) cerr := coderdtest.SDKError(t, err) require.Equal(t, http.StatusUnauthorized, cerr.StatusCode()) @@ -630,12 +630,12 @@ func TestUpdateUserPassword(t *testing.T) { // Trying to get an API key should fail since all keys are deleted // on password change. - _, err = client.GetAPIKey(ctx, user.UserID.String(), apikey1.Key) + _, err = client.APIKey(ctx, user.UserID.String(), apikey1.Key) require.Error(t, err) cerr = coderdtest.SDKError(t, err) require.Equal(t, http.StatusNotFound, cerr.StatusCode()) - _, err = client.GetAPIKey(ctx, user.UserID.String(), apikey2.Key) + _, err = client.APIKey(ctx, user.UserID.String(), apikey2.Key) require.Error(t, err) cerr = coderdtest.SDKError(t, err) require.Equal(t, http.StatusNotFound, cerr.StatusCode()) @@ -833,7 +833,7 @@ func TestInitialRoles(t *testing.T) { client := coderdtest.New(t, nil) first := coderdtest.CreateFirstUser(t, client) - roles, err := client.GetUserRoles(ctx, codersdk.Me) + roles, err := client.UserRoles(ctx, codersdk.Me) require.NoError(t, err) require.ElementsMatch(t, roles.Roles, []string{ rbac.RoleOwner(), diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index 6bd848d072..87456b6d82 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -32,6 +32,7 @@ import ( "github.com/coder/coder/coderd/rbac" "github.com/coder/coder/coderd/tracing" "github.com/coder/coder/codersdk" + "github.com/coder/coder/codersdk/agentsdk" "github.com/coder/coder/tailnet" ) @@ -76,7 +77,7 @@ func (api *API) workspaceAgent(rw http.ResponseWriter, r *http.Request) { // @Security CoderSessionToken // @Produce json // @Tags Agents -// @Success 200 {object} codersdk.WorkspaceAgentMetadata +// @Success 200 {object} agentsdk.Metadata // @Router /workspaceagents/me/metadata [get] func (api *API) workspaceAgentMetadata(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -141,7 +142,7 @@ func (api *API) workspaceAgentMetadata(rw http.ResponseWriter, r *http.Request) vscodeProxyURI += fmt.Sprintf(":%s", api.AccessURL.Port()) } - httpapi.Write(ctx, rw, http.StatusOK, codersdk.WorkspaceAgentMetadata{ + httpapi.Write(ctx, rw, http.StatusOK, agentsdk.Metadata{ Apps: convertApps(dbApps), DERPMap: api.DERPMap, GitAuthConfigs: len(api.GitAuthConfigs), @@ -160,7 +161,7 @@ func (api *API) workspaceAgentMetadata(rw http.ResponseWriter, r *http.Request) // @Accept json // @Produce json // @Tags Agents -// @Param request body codersdk.PostWorkspaceAgentVersionRequest true "Version request" +// @Param request body agentsdk.PostVersionRequest true "Version request" // @Success 200 // @Router /workspaceagents/me/version [post] // @x-apidocgen {"skip": true} @@ -176,7 +177,7 @@ func (api *API) postWorkspaceAgentVersion(rw http.ResponseWriter, r *http.Reques return } - var req codersdk.PostWorkspaceAgentVersionRequest + var req agentsdk.PostVersionRequest if !httpapi.Read(ctx, rw, r, &req) { return } @@ -299,7 +300,7 @@ func (api *API) workspaceAgentPTY(rw http.ResponseWriter, r *http.Request) { // @Produce json // @Tags Agents // @Param workspaceagent path string true "Workspace agent ID" format(uuid) -// @Success 200 {object} codersdk.ListeningPortsResponse +// @Success 200 {object} codersdk.WorkspaceAgentListeningPortsResponse // @Router /workspaceagents/{workspaceagent}/listening-ports [get] func (api *API) workspaceAgentListeningPorts(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -382,15 +383,15 @@ func (api *API) workspaceAgentListeningPorts(rw http.ResponseWriter, r *http.Req // Filter out ports that are globally blocked, in-use by applications, or // common non-HTTP ports such as databases, FTP, SSH, etc. - filteredPorts := make([]codersdk.ListeningPort, 0, len(portsResponse.Ports)) + filteredPorts := make([]codersdk.WorkspaceAgentListeningPort, 0, len(portsResponse.Ports)) for _, port := range portsResponse.Ports { - if port.Port < codersdk.MinimumListeningPort { + if port.Port < codersdk.WorkspaceAgentMinimumListeningPort { continue } if _, ok := appPorts[port.Port]; ok { continue } - if _, ok := codersdk.IgnoredListeningPorts[port.Port]; ok { + if _, ok := codersdk.WorkspaceAgentIgnoredListeningPorts[port.Port]; ok { continue } filteredPorts = append(filteredPorts, port) @@ -400,7 +401,7 @@ func (api *API) workspaceAgentListeningPorts(rw http.ResponseWriter, r *http.Req httpapi.Write(ctx, rw, http.StatusOK, portsResponse) } -func (api *API) dialWorkspaceAgentTailnet(r *http.Request, agentID uuid.UUID) (*codersdk.AgentConn, error) { +func (api *API) dialWorkspaceAgentTailnet(r *http.Request, agentID uuid.UUID) (*codersdk.WorkspaceAgentConn, error) { clientConn, serverConn := net.Pipe() derpMap := api.DERPMap.Clone() @@ -467,7 +468,7 @@ func (api *API) dialWorkspaceAgentTailnet(r *http.Request, agentID uuid.UUID) (* _ = conn.Close() } }() - return &codersdk.AgentConn{ + return &codersdk.WorkspaceAgentConn{ Conn: conn, CloseFunc: func() { _ = clientConn.Close() @@ -861,8 +862,8 @@ func convertWorkspaceAgent(derpMap *tailcfg.DERPMap, coordinator tailnet.Coordin // @Accept json // @Produce json // @Tags Agents -// @Param request body codersdk.AgentStats true "Stats request" -// @Success 200 {object} codersdk.AgentStatsResponse +// @Param request body agentsdk.Stats true "Stats request" +// @Success 200 {object} agentsdk.StatsResponse // @Router /workspaceagents/me/report-stats [post] func (api *API) workspaceAgentReportStats(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -877,13 +878,13 @@ func (api *API) workspaceAgentReportStats(rw http.ResponseWriter, r *http.Reques return } - var req codersdk.AgentStats + var req agentsdk.Stats if !httpapi.Read(ctx, rw, r, &req) { return } if req.RxBytes == 0 && req.TxBytes == 0 { - httpapi.Write(ctx, rw, http.StatusOK, codersdk.AgentStatsResponse{ + httpapi.Write(ctx, rw, http.StatusOK, agentsdk.StatsResponse{ ReportInterval: api.AgentStatsRefreshInterval, }) return @@ -928,7 +929,7 @@ func (api *API) workspaceAgentReportStats(rw http.ResponseWriter, r *http.Reques return } - httpapi.Write(ctx, rw, http.StatusOK, codersdk.AgentStatsResponse{ + httpapi.Write(ctx, rw, http.StatusOK, agentsdk.StatsResponse{ ReportInterval: api.AgentStatsRefreshInterval, }) } @@ -938,7 +939,7 @@ func (api *API) workspaceAgentReportStats(rw http.ResponseWriter, r *http.Reques // @Security CoderSessionToken // @Accept json // @Tags Agents -// @Param request body codersdk.PostWorkspaceAgentLifecycleRequest true "Workspace agent lifecycle request" +// @Param request body agentsdk.PostLifecycleRequest true "Workspace agent lifecycle request" // @Success 204 "Success" // @Router /workspaceagents/me/report-lifecycle [post] // @x-apidocgen {"skip": true} @@ -955,7 +956,7 @@ func (api *API) workspaceAgentReportLifecycle(rw http.ResponseWriter, r *http.Re return } - var req codersdk.PostWorkspaceAgentLifecycleRequest + var req agentsdk.PostLifecycleRequest if !httpapi.Read(ctx, rw, r, &req) { return } @@ -994,13 +995,13 @@ func (api *API) workspaceAgentReportLifecycle(rw http.ResponseWriter, r *http.Re // @Accept json // @Produce json // @Tags Agents -// @Param request body codersdk.PostWorkspaceAppHealthsRequest true "Application health request" +// @Param request body agentsdk.PostAppHealthsRequest true "Application health request" // @Success 200 // @Router /workspaceagents/me/app-health [post] func (api *API) postWorkspaceAppHealth(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() workspaceAgent := httpmw.WorkspaceAgent(r) - var req codersdk.PostWorkspaceAppHealthsRequest + var req agentsdk.PostAppHealthsRequest if !httpapi.Read(ctx, rw, r, &req) { return } @@ -1122,7 +1123,7 @@ func (api *API) postWorkspaceAppHealth(rw http.ResponseWriter, r *http.Request) // @Tags Agents // @Param url query string true "Git URL" format(uri) // @Param listen query bool false "Wait for a new token to be issued" -// @Success 200 {object} codersdk.WorkspaceAgentGitAuthResponse +// @Success 200 {object} agentsdk.GitAuthResponse // @Router /workspaceagents/me/gitauth [get] func (api *API) workspaceAgentsGitAuth(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -1272,7 +1273,7 @@ func (api *API) workspaceAgentsGitAuth(rw http.ResponseWriter, r *http.Request) return } - httpapi.Write(ctx, rw, http.StatusOK, codersdk.WorkspaceAgentGitAuthResponse{ + httpapi.Write(ctx, rw, http.StatusOK, agentsdk.GitAuthResponse{ URL: redirectURL.String(), }) return @@ -1281,7 +1282,7 @@ func (api *API) workspaceAgentsGitAuth(rw http.ResponseWriter, r *http.Request) // If the token is expired and refresh is disabled, we prompt // the user to authenticate again. if gitAuthConfig.NoRefresh && gitAuthLink.OAuthExpiry.Before(database.Now()) { - httpapi.Write(ctx, rw, http.StatusOK, codersdk.WorkspaceAgentGitAuthResponse{ + httpapi.Write(ctx, rw, http.StatusOK, agentsdk.GitAuthResponse{ URL: redirectURL.String(), }) return @@ -1293,7 +1294,7 @@ func (api *API) workspaceAgentsGitAuth(rw http.ResponseWriter, r *http.Request) Expiry: gitAuthLink.OAuthExpiry, }).Token() if err != nil { - httpapi.Write(ctx, rw, http.StatusOK, codersdk.WorkspaceAgentGitAuthResponse{ + httpapi.Write(ctx, rw, http.StatusOK, agentsdk.GitAuthResponse{ URL: redirectURL.String(), }) return @@ -1310,7 +1311,7 @@ func (api *API) workspaceAgentsGitAuth(rw http.ResponseWriter, r *http.Request) } if !valid { // The token is no longer valid! - httpapi.Write(ctx, rw, http.StatusOK, codersdk.WorkspaceAgentGitAuthResponse{ + httpapi.Write(ctx, rw, http.StatusOK, agentsdk.GitAuthResponse{ URL: redirectURL.String(), }) return @@ -1363,23 +1364,23 @@ func validateGitToken(ctx context.Context, validateURL, token string) (bool, err } // Provider types have different username/password formats. -func formatGitAuthAccessToken(typ codersdk.GitProvider, token string) codersdk.WorkspaceAgentGitAuthResponse { - var resp codersdk.WorkspaceAgentGitAuthResponse +func formatGitAuthAccessToken(typ codersdk.GitProvider, token string) agentsdk.GitAuthResponse { + var resp agentsdk.GitAuthResponse switch typ { case codersdk.GitProviderGitLab: // https://stackoverflow.com/questions/25409700/using-gitlab-token-to-clone-without-authentication - resp = codersdk.WorkspaceAgentGitAuthResponse{ + resp = agentsdk.GitAuthResponse{ Username: "oauth2", Password: token, } case codersdk.GitProviderBitBucket: // https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud/#Cloning-a-repository-with-an-access-token - resp = codersdk.WorkspaceAgentGitAuthResponse{ + resp = agentsdk.GitAuthResponse{ Username: "x-token-auth", Password: token, } default: - resp = codersdk.WorkspaceAgentGitAuthResponse{ + resp = agentsdk.GitAuthResponse{ Username: token, } } diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go index b5974b6f5d..3fe824f040 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -27,6 +27,7 @@ import ( "github.com/coder/coder/coderd/database" "github.com/coder/coder/coderd/gitauth" "github.com/coder/coder/codersdk" + "github.com/coder/coder/codersdk/agentsdk" "github.com/coder/coder/provisioner/echo" "github.com/coder/coder/provisionersdk/proto" "github.com/coder/coder/testutil" @@ -210,7 +211,7 @@ func TestWorkspaceAgentListen(t *testing.T) { workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) - agentClient := codersdk.New(client.URL) + agentClient := agentsdk.New(client.URL) agentClient.SetSessionToken(authToken) agentCloser := agent.New(agent.Options{ Client: agentClient, @@ -299,10 +300,10 @@ func TestWorkspaceAgentListen(t *testing.T) { require.NoError(t, err) coderdtest.AwaitWorkspaceBuildJob(t, client, stopBuild.ID) - agentClient := codersdk.New(client.URL) + agentClient := agentsdk.New(client.URL) agentClient.SetSessionToken(authToken) - _, err = agentClient.ListenWorkspaceAgent(ctx) + _, err = agentClient.Listen(ctx) require.Error(t, err) require.ErrorContains(t, err, "build is outdated") }) @@ -339,7 +340,7 @@ func TestWorkspaceAgentTailnet(t *testing.T) { coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) daemonCloser.Close() - agentClient := codersdk.New(client.URL) + agentClient := agentsdk.New(client.URL) agentClient.SetSessionToken(authToken) agentCloser := agent.New(agent.Options{ Client: agentClient, @@ -405,7 +406,7 @@ func TestWorkspaceAgentPTY(t *testing.T) { workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) - agentClient := codersdk.New(client.URL) + agentClient := agentsdk.New(client.URL) agentClient.SetSessionToken(authToken) agentCloser := agent.New(agent.Options{ Client: agentClient, @@ -502,7 +503,7 @@ func TestWorkspaceAgentListeningPorts(t *testing.T) { workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) - agentClient := codersdk.New(client.URL) + agentClient := agentsdk.New(client.URL) agentClient.SetSessionToken(authToken) agentCloser := agent.New(agent.Options{ Client: agentClient, @@ -517,10 +518,10 @@ func TestWorkspaceAgentListeningPorts(t *testing.T) { } willFilterPort := func(port int) bool { - if port < codersdk.MinimumListeningPort || port > 65535 { + if port < codersdk.WorkspaceAgentMinimumListeningPort || port > 65535 { return true } - if _, ok := codersdk.IgnoredListeningPorts[uint16(port)]; ok { + if _, ok := codersdk.WorkspaceAgentIgnoredListeningPorts[uint16(port)]; ok { return true } @@ -560,7 +561,7 @@ func TestWorkspaceAgentListeningPorts(t *testing.T) { port uint16 ) require.Eventually(t, func() bool { - for ignoredPort := range codersdk.IgnoredListeningPorts { + for ignoredPort := range codersdk.WorkspaceAgentIgnoredListeningPorts { if ignoredPort < 1024 || ignoredPort == 5432 { continue } @@ -615,7 +616,7 @@ func TestWorkspaceAgentListeningPorts(t *testing.T) { } ) for _, port := range res.Ports { - if port.Network == codersdk.ListeningPortNetworkTCP { + if port.Network == "tcp" { if val, ok := expected[port.Port]; ok { if val { t.Fatalf("expected to find TCP port %d only once in response", port.Port) @@ -637,7 +638,7 @@ func TestWorkspaceAgentListeningPorts(t *testing.T) { require.NoError(t, err) for _, port := range res.Ports { - if port.Network == codersdk.ListeningPortNetworkTCP && port.Port == lPort { + if port.Network == "tcp" && port.Port == lPort { t.Fatalf("expected to not find TCP port %d in response", lPort) } } @@ -667,7 +668,7 @@ func TestWorkspaceAgentListeningPorts(t *testing.T) { sawCoderdPort := false for _, port := range res.Ports { - if port.Network == codersdk.ListeningPortNetworkTCP { + if port.Network == "tcp" { if port.Port == appLPort { t.Fatalf("expected to not find TCP port (app port) %d in response", appLPort) } @@ -764,50 +765,50 @@ func TestWorkspaceAgentAppHealth(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - agentClient := codersdk.New(client.URL) + agentClient := agentsdk.New(client.URL) agentClient.SetSessionToken(authToken) - metadata, err := agentClient.WorkspaceAgentMetadata(ctx) + metadata, err := agentClient.Metadata(ctx) require.NoError(t, err) require.EqualValues(t, codersdk.WorkspaceAppHealthDisabled, metadata.Apps[0].Health) require.EqualValues(t, codersdk.WorkspaceAppHealthInitializing, metadata.Apps[1].Health) - err = agentClient.PostWorkspaceAgentAppHealth(ctx, codersdk.PostWorkspaceAppHealthsRequest{}) + err = agentClient.PostAppHealth(ctx, agentsdk.PostAppHealthsRequest{}) require.Error(t, err) // empty - err = agentClient.PostWorkspaceAgentAppHealth(ctx, codersdk.PostWorkspaceAppHealthsRequest{}) + err = agentClient.PostAppHealth(ctx, agentsdk.PostAppHealthsRequest{}) require.Error(t, err) // healthcheck disabled - err = agentClient.PostWorkspaceAgentAppHealth(ctx, codersdk.PostWorkspaceAppHealthsRequest{ + err = agentClient.PostAppHealth(ctx, agentsdk.PostAppHealthsRequest{ Healths: map[uuid.UUID]codersdk.WorkspaceAppHealth{ metadata.Apps[0].ID: codersdk.WorkspaceAppHealthInitializing, }, }) require.Error(t, err) // invalid value - err = agentClient.PostWorkspaceAgentAppHealth(ctx, codersdk.PostWorkspaceAppHealthsRequest{ + err = agentClient.PostAppHealth(ctx, agentsdk.PostAppHealthsRequest{ Healths: map[uuid.UUID]codersdk.WorkspaceAppHealth{ metadata.Apps[1].ID: codersdk.WorkspaceAppHealth("bad-value"), }, }) require.Error(t, err) // update to healthy - err = agentClient.PostWorkspaceAgentAppHealth(ctx, codersdk.PostWorkspaceAppHealthsRequest{ + err = agentClient.PostAppHealth(ctx, agentsdk.PostAppHealthsRequest{ Healths: map[uuid.UUID]codersdk.WorkspaceAppHealth{ metadata.Apps[1].ID: codersdk.WorkspaceAppHealthHealthy, }, }) require.NoError(t, err) - metadata, err = agentClient.WorkspaceAgentMetadata(ctx) + metadata, err = agentClient.Metadata(ctx) require.NoError(t, err) require.EqualValues(t, codersdk.WorkspaceAppHealthHealthy, metadata.Apps[1].Health) // update to unhealthy - err = agentClient.PostWorkspaceAgentAppHealth(ctx, codersdk.PostWorkspaceAppHealthsRequest{ + err = agentClient.PostAppHealth(ctx, agentsdk.PostAppHealthsRequest{ Healths: map[uuid.UUID]codersdk.WorkspaceAppHealth{ metadata.Apps[1].ID: codersdk.WorkspaceAppHealthUnhealthy, }, }) require.NoError(t, err) - metadata, err = agentClient.WorkspaceAgentMetadata(ctx) + metadata, err = agentClient.Metadata(ctx) require.NoError(t, err) require.EqualValues(t, codersdk.WorkspaceAppHealthUnhealthy, metadata.Apps[1].Health) } @@ -848,9 +849,9 @@ func TestWorkspaceAgentsGitAuth(t *testing.T) { workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) - agentClient := codersdk.New(client.URL) + agentClient := agentsdk.New(client.URL) agentClient.SetSessionToken(authToken) - _, err := agentClient.WorkspaceAgentGitAuth(context.Background(), "github.com", false) + _, err := agentClient.GitAuth(context.Background(), "github.com", false) var apiError *codersdk.Error require.ErrorAs(t, err, &apiError) require.Equal(t, http.StatusNotFound, apiError.StatusCode()) @@ -893,9 +894,9 @@ func TestWorkspaceAgentsGitAuth(t *testing.T) { workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) - agentClient := codersdk.New(client.URL) + agentClient := agentsdk.New(client.URL) agentClient.SetSessionToken(authToken) - token, err := agentClient.WorkspaceAgentGitAuth(context.Background(), "github.com/asd/asd", false) + token, err := agentClient.GitAuth(context.Background(), "github.com/asd/asd", false) require.NoError(t, err) require.True(t, strings.HasSuffix(token.URL, fmt.Sprintf("/gitauth/%s", "github"))) }) @@ -979,7 +980,7 @@ func TestWorkspaceAgentsGitAuth(t *testing.T) { workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) - agentClient := codersdk.New(client.URL) + agentClient := agentsdk.New(client.URL) agentClient.SetSessionToken(authToken) resp := gitAuthCallback(t, "github", client) @@ -990,7 +991,7 @@ func TestWorkspaceAgentsGitAuth(t *testing.T) { srv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusUnauthorized) }) - res, err := agentClient.WorkspaceAgentGitAuth(ctx, "github.com/asd/asd", false) + res, err := agentClient.GitAuth(ctx, "github.com/asd/asd", false) require.NoError(t, err) require.NotEmpty(t, res.URL) @@ -1000,7 +1001,7 @@ func TestWorkspaceAgentsGitAuth(t *testing.T) { w.WriteHeader(http.StatusForbidden) w.Write([]byte("Something went wrong!")) }) - _, err = agentClient.WorkspaceAgentGitAuth(ctx, "github.com/asd/asd", false) + _, err = agentClient.GitAuth(ctx, "github.com/asd/asd", false) var apiError *codersdk.Error require.ErrorAs(t, err, &apiError) require.Equal(t, http.StatusInternalServerError, apiError.StatusCode()) @@ -1052,10 +1053,10 @@ func TestWorkspaceAgentsGitAuth(t *testing.T) { workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) - agentClient := codersdk.New(client.URL) + agentClient := agentsdk.New(client.URL) agentClient.SetSessionToken(authToken) - token, err := agentClient.WorkspaceAgentGitAuth(context.Background(), "github.com/asd/asd", false) + token, err := agentClient.GitAuth(context.Background(), "github.com/asd/asd", false) require.NoError(t, err) require.NotEmpty(t, token.URL) @@ -1067,7 +1068,7 @@ func TestWorkspaceAgentsGitAuth(t *testing.T) { // Because the token is expired and `NoRefresh` is specified, // a redirect URL should be returned again. - token, err = agentClient.WorkspaceAgentGitAuth(context.Background(), "github.com/asd/asd", false) + token, err = agentClient.GitAuth(context.Background(), "github.com/asd/asd", false) require.NoError(t, err) require.NotEmpty(t, token.URL) }) @@ -1110,17 +1111,17 @@ func TestWorkspaceAgentsGitAuth(t *testing.T) { workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) - agentClient := codersdk.New(client.URL) + agentClient := agentsdk.New(client.URL) agentClient.SetSessionToken(authToken) - token, err := agentClient.WorkspaceAgentGitAuth(context.Background(), "github.com/asd/asd", false) + token, err := agentClient.GitAuth(context.Background(), "github.com/asd/asd", false) require.NoError(t, err) require.NotEmpty(t, token.URL) // Start waiting for the token callback... - tokenChan := make(chan codersdk.WorkspaceAgentGitAuthResponse, 1) + tokenChan := make(chan agentsdk.GitAuthResponse, 1) go func() { - token, err := agentClient.WorkspaceAgentGitAuth(context.Background(), "github.com/asd/asd", true) + token, err := agentClient.GitAuth(context.Background(), "github.com/asd/asd", true) assert.NoError(t, err) tokenChan <- token }() @@ -1132,7 +1133,7 @@ func TestWorkspaceAgentsGitAuth(t *testing.T) { token = <-tokenChan require.Equal(t, "token", token.Username) - token, err = agentClient.WorkspaceAgentGitAuth(context.Background(), "github.com/asd/asd", false) + token, err = agentClient.GitAuth(context.Background(), "github.com/asd/asd", false) require.NoError(t, err) }) } @@ -1173,10 +1174,10 @@ func TestWorkspaceAgentReportStats(t *testing.T) { workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) - agentClient := codersdk.New(client.URL) + agentClient := agentsdk.New(client.URL) agentClient.SetSessionToken(authToken) - _, err := agentClient.PostAgentStats(context.Background(), &codersdk.AgentStats{ + _, err := agentClient.PostStats(context.Background(), &agentsdk.Stats{ ConnsByProto: map[string]int64{"TCP": 1}, NumConns: 1, RxPackets: 1, @@ -1206,11 +1207,11 @@ func gitAuthCallback(t *testing.T, id string, client *codersdk.Client) *http.Res req, err := http.NewRequestWithContext(context.Background(), "GET", oauthURL.String(), nil) require.NoError(t, err) req.AddCookie(&http.Cookie{ - Name: codersdk.OAuth2StateKey, + Name: codersdk.OAuth2StateCookie, Value: state, }) req.AddCookie(&http.Cookie{ - Name: codersdk.SessionTokenKey, + Name: codersdk.SessionTokenCookie, Value: client.SessionToken(), }) res, err := client.HTTPClient.Do(req) @@ -1263,7 +1264,7 @@ func TestWorkspaceAgent_LifecycleState(t *testing.T) { } } - agentClient := codersdk.New(client.URL) + agentClient := agentsdk.New(client.URL) agentClient.SetSessionToken(authToken) tests := []struct { @@ -1284,7 +1285,7 @@ func TestWorkspaceAgent_LifecycleState(t *testing.T) { t.Run(string(tt.state), func(t *testing.T) { ctx, _ := testutil.Context(t) - err := agentClient.PostWorkspaceAgentLifecycle(ctx, codersdk.PostWorkspaceAgentLifecycleRequest{ + err := agentClient.PostLifecycle(ctx, agentsdk.PostLifecycleRequest{ State: tt.state, }) if tt.wantErr { diff --git a/coderd/workspaceapps.go b/coderd/workspaceapps.go index 23ef5170b8..168345bc2d 100644 --- a/coderd/workspaceapps.go +++ b/coderd/workspaceapps.go @@ -77,7 +77,7 @@ const ( // @Security CoderSessionToken // @Produce json // @Tags Applications -// @Success 200 {object} codersdk.GetAppHostResponse +// @Success 200 {object} codersdk.AppHostResponse // @Router /applications/host [get] func (api *API) appHost(rw http.ResponseWriter, r *http.Request) { host := api.AppHostname @@ -85,7 +85,7 @@ func (api *API) appHost(rw http.ResponseWriter, r *http.Request) { host += fmt.Sprintf(":%s", api.AccessURL.Port()) } - httpapi.Write(r.Context(), rw, http.StatusOK, codersdk.GetAppHostResponse{ + httpapi.Write(r.Context(), rw, http.StatusOK, codersdk.AppHostResponse{ Host: host, }) } @@ -862,9 +862,9 @@ func (api *API) proxyWorkspaceApplication(proxyApp proxyApplication, rw http.Res return } - if portInt < codersdk.MinimumListeningPort { + if portInt < codersdk.WorkspaceAgentMinimumListeningPort { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: fmt.Sprintf("Application port %d is not permitted. Coder reserves ports less than %d for internal use.", portInt, codersdk.MinimumListeningPort), + Message: fmt.Sprintf("Application port %d is not permitted. Coder reserves ports less than %d for internal use.", portInt, codersdk.WorkspaceAgentMinimumListeningPort), }) return } diff --git a/coderd/workspaceapps_test.go b/coderd/workspaceapps_test.go index a9782dce32..ad47c19231 100644 --- a/coderd/workspaceapps_test.go +++ b/coderd/workspaceapps_test.go @@ -27,6 +27,7 @@ import ( "github.com/coder/coder/coderd/httpmw" "github.com/coder/coder/coderd/rbac" "github.com/coder/coder/codersdk" + "github.com/coder/coder/codersdk/agentsdk" "github.com/coder/coder/provisioner/echo" "github.com/coder/coder/provisionersdk/proto" "github.com/coder/coder/testutil" @@ -96,12 +97,12 @@ func TestGetAppHost(t *testing.T) { defer cancel() // Should not leak to unauthenticated users. - host, err := client.GetAppHost(ctx) + host, err := client.AppHost(ctx) require.Error(t, err) require.Equal(t, "", host.Host) _ = coderdtest.CreateFirstUser(t, client) - host, err = client.GetAppHost(ctx) + host, err = client.AppHost(ctx) require.NoError(t, err) require.Equal(t, c.expected, host.Host) }) @@ -134,7 +135,7 @@ func setupProxyTest(t *testing.T, opts *setupProxyTestOpts) (*codersdk.Client, c server := http.Server{ ReadHeaderTimeout: time.Minute, Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, err := r.Cookie(codersdk.SessionTokenKey) + _, err := r.Cookie(codersdk.SessionTokenCookie) assert.ErrorIs(t, err, http.ErrNoCookie) w.Header().Set("X-Forwarded-For", r.Header.Get("X-Forwarded-For")) w.WriteHeader(http.StatusOK) @@ -252,10 +253,10 @@ func createWorkspaceWithApps(t *testing.T, client *codersdk.Client, orgID uuid.U user, err := client.User(ctx, codersdk.Me) require.NoError(t, err) - agentClient := codersdk.New(client.URL) + agentClient := agentsdk.New(client.URL) agentClient.SetSessionToken(authToken) if appHost != "" { - metadata, err := agentClient.WorkspaceAgentMetadata(context.Background()) + metadata, err := agentClient.Metadata(context.Background()) require.NoError(t, err) proxyURL := fmt.Sprintf( "http://{{port}}--%s--%s--%s%s", @@ -437,7 +438,7 @@ func TestWorkspaceApplicationAuth(t *testing.T) { // Get the current user and API key. user, err := client.User(ctx, codersdk.Me) require.NoError(t, err) - currentAPIKey, err := client.GetAPIKey(ctx, firstUser.UserID.String(), strings.Split(client.SessionToken(), "-")[0]) + currentAPIKey, err := client.APIKey(ctx, firstUser.UserID.String(), strings.Split(client.SessionToken(), "-")[0]) require.NoError(t, err) // Try to load the application without authentication. @@ -499,7 +500,7 @@ func TestWorkspaceApplicationAuth(t *testing.T) { apiKey := cookies[0].Value // Fetch the API key. - apiKeyInfo, err := client.GetAPIKey(ctx, firstUser.UserID.String(), strings.Split(apiKey, "-")[0]) + apiKeyInfo, err := client.APIKey(ctx, firstUser.UserID.String(), strings.Split(apiKey, "-")[0]) require.NoError(t, err) require.Equal(t, user.ID, apiKeyInfo.UserID) require.Equal(t, codersdk.LoginTypePassword, apiKeyInfo.LoginType) @@ -515,7 +516,7 @@ func TestWorkspaceApplicationAuth(t *testing.T) { canCreateApplicationConnect = "can-create-application_connect" canReadUserMe = "can-read-user-me" ) - authRes, err := appClient.CheckAuthorization(ctx, codersdk.AuthorizationRequest{ + authRes, err := appClient.AuthCheck(ctx, codersdk.AuthorizationRequest{ Checks: map[string]codersdk.AuthorizationCheck{ canCreateApplicationConnect: { Object: codersdk.AuthorizationObject{ @@ -546,7 +547,7 @@ func TestWorkspaceApplicationAuth(t *testing.T) { t.Log("navigating to: ", gotLocation.String()) req, err = http.NewRequestWithContext(ctx, "GET", gotLocation.String(), nil) require.NoError(t, err) - req.Header.Set(codersdk.SessionCustomHeader, apiKey) + req.Header.Set(codersdk.SessionTokenHeader, apiKey) resp, err = doWithRetries(t, client, req) require.NoError(t, err) resp.Body.Close() @@ -730,7 +731,7 @@ func TestWorkspaceAppsProxySubdomain(t *testing.T) { require.NoError(t, err, "get workspaces") require.Len(t, res.Workspaces, 1, "expected 1 workspace") - appHost, err := client.GetAppHost(ctx) + appHost, err := client.AppHost(ctx) require.NoError(t, err, "get app host") subdomain := httpapi.ApplicationURL{ @@ -858,7 +859,7 @@ func TestWorkspaceAppsProxySubdomain(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - port := uint16(codersdk.MinimumListeningPort - 1) + port := uint16(codersdk.WorkspaceAgentMinimumListeningPort - 1) resp, err := requestWithRetries(ctx, t, client, http.MethodGet, proxyURL(t, client, port, "/", proxyTestAppQuery), nil) require.NoError(t, err) defer resp.Body.Close() @@ -1049,7 +1050,7 @@ func TestAppSubdomainLogout(t *testing.T) { _, err := client.User(ctx, codersdk.Me) require.NoError(t, err) - appHost, err := client.GetAppHost(ctx) + appHost, err := client.AppHost(ctx) require.NoError(t, err, "get app host") if c.cookie == "-" { @@ -1087,7 +1088,7 @@ func TestAppSubdomainLogout(t *testing.T) { // The header is prioritized over the devurl cookie if both are // set, so this ensures we can trigger the logout code path with // bad cookies during tests. - req.Header.Set(codersdk.SessionCustomHeader, client.SessionToken()) + req.Header.Set(codersdk.SessionTokenHeader, client.SessionToken()) if c.cookie != "" { req.AddCookie(&http.Cookie{ Name: httpmw.DevURLSessionTokenCookie, @@ -1526,7 +1527,7 @@ func TestWorkspaceAppsNonCanonicalHeaders(t *testing.T) { secWebSocketKey := "test-dean-was-here" req.Header["Sec-WebSocket-Key"] = []string{secWebSocketKey} - req.Header.Set(codersdk.SessionCustomHeader, client.SessionToken()) + req.Header.Set(codersdk.SessionTokenHeader, client.SessionToken()) resp, err := doWithRetries(t, client, req) require.NoError(t, err) defer resp.Body.Close() @@ -1578,7 +1579,7 @@ func TestWorkspaceAppsNonCanonicalHeaders(t *testing.T) { secWebSocketKey := "test-dean-was-here" req.Header["Sec-WebSocket-Key"] = []string{secWebSocketKey} - req.Header.Set(codersdk.SessionCustomHeader, client.SessionToken()) + req.Header.Set(codersdk.SessionTokenHeader, client.SessionToken()) resp, err := doWithRetries(t, client, req) require.NoError(t, err) defer resp.Body.Close() diff --git a/coderd/workspaceresourceauth.go b/coderd/workspaceresourceauth.go index 2ecc48a56a..f8f9b3a32b 100644 --- a/coderd/workspaceresourceauth.go +++ b/coderd/workspaceresourceauth.go @@ -13,6 +13,7 @@ import ( "github.com/coder/coder/coderd/httpapi" "github.com/coder/coder/coderd/provisionerdserver" "github.com/coder/coder/codersdk" + "github.com/coder/coder/codersdk/agentsdk" "github.com/mitchellh/mapstructure" ) @@ -26,12 +27,12 @@ import ( // @Accept json // @Produce json // @Tags Agents -// @Param request body codersdk.AzureInstanceIdentityToken true "Instance identity token" -// @Success 200 {object} codersdk.WorkspaceAgentAuthenticateResponse +// @Param request body agentsdk.AzureInstanceIdentityToken true "Instance identity token" +// @Success 200 {object} agentsdk.AuthenticateResponse // @Router /workspaceagents/azure-instance-identity [post] func (api *API) postWorkspaceAuthAzureInstanceIdentity(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() - var req codersdk.AzureInstanceIdentityToken + var req agentsdk.AzureInstanceIdentityToken if !httpapi.Read(ctx, rw, r, &req) { return } @@ -56,12 +57,12 @@ func (api *API) postWorkspaceAuthAzureInstanceIdentity(rw http.ResponseWriter, r // @Accept json // @Produce json // @Tags Agents -// @Param request body codersdk.AWSInstanceIdentityToken true "Instance identity token" -// @Success 200 {object} codersdk.WorkspaceAgentAuthenticateResponse +// @Param request body agentsdk.AWSInstanceIdentityToken true "Instance identity token" +// @Success 200 {object} agentsdk.AuthenticateResponse // @Router /workspaceagents/aws-instance-identity [post] func (api *API) postWorkspaceAuthAWSInstanceIdentity(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() - var req codersdk.AWSInstanceIdentityToken + var req agentsdk.AWSInstanceIdentityToken if !httpapi.Read(ctx, rw, r, &req) { return } @@ -86,12 +87,12 @@ func (api *API) postWorkspaceAuthAWSInstanceIdentity(rw http.ResponseWriter, r * // @Accept json // @Produce json // @Tags Agents -// @Param request body codersdk.GoogleInstanceIdentityToken true "Instance identity token" -// @Success 200 {object} codersdk.WorkspaceAgentAuthenticateResponse +// @Param request body agentsdk.GoogleInstanceIdentityToken true "Instance identity token" +// @Success 200 {object} agentsdk.AuthenticateResponse // @Router /workspaceagents/google-instance-identity [post] func (api *API) postWorkspaceAuthGoogleInstanceIdentity(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() - var req codersdk.GoogleInstanceIdentityToken + var req agentsdk.GoogleInstanceIdentityToken if !httpapi.Read(ctx, rw, r, &req) { return } @@ -196,7 +197,7 @@ func (api *API) handleAuthInstanceID(rw http.ResponseWriter, r *http.Request, in return } - httpapi.Write(ctx, rw, http.StatusOK, codersdk.WorkspaceAgentAuthenticateResponse{ + httpapi.Write(ctx, rw, http.StatusOK, agentsdk.AuthenticateResponse{ SessionToken: agent.AuthToken.String(), }) } diff --git a/coderd/workspaceresourceauth_test.go b/coderd/workspaceresourceauth_test.go index 5e39aa02a9..d6c088de58 100644 --- a/coderd/workspaceresourceauth_test.go +++ b/coderd/workspaceresourceauth_test.go @@ -9,6 +9,7 @@ import ( "github.com/coder/coder/coderd/coderdtest" "github.com/coder/coder/codersdk" + "github.com/coder/coder/codersdk/agentsdk" "github.com/coder/coder/provisioner/echo" "github.com/coder/coder/provisionersdk/proto" "github.com/coder/coder/testutil" @@ -50,7 +51,10 @@ func TestPostWorkspaceAuthAzureInstanceIdentity(t *testing.T) { defer cancel() client.HTTPClient = metadataClient - _, err := client.AuthWorkspaceAzureInstanceIdentity(ctx) + agentClient := &agentsdk.Client{ + SDK: client, + } + _, err := agentClient.AuthAzureInstanceIdentity(ctx) require.NoError(t, err) } @@ -92,7 +96,10 @@ func TestPostWorkspaceAuthAWSInstanceIdentity(t *testing.T) { defer cancel() client.HTTPClient = metadataClient - _, err := client.AuthWorkspaceAWSInstanceIdentity(ctx) + agentClient := &agentsdk.Client{ + SDK: client, + } + _, err := agentClient.AuthAWSInstanceIdentity(ctx) require.NoError(t, err) }) } @@ -110,7 +117,10 @@ func TestPostWorkspaceAuthGoogleInstanceIdentity(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - _, err := client.AuthWorkspaceGoogleInstanceIdentity(ctx, "", metadata) + agentClient := &agentsdk.Client{ + SDK: client, + } + _, err := agentClient.AuthGoogleInstanceIdentity(ctx, "", metadata) var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode()) @@ -127,7 +137,10 @@ func TestPostWorkspaceAuthGoogleInstanceIdentity(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - _, err := client.AuthWorkspaceGoogleInstanceIdentity(ctx, "", metadata) + agentClient := &agentsdk.Client{ + SDK: client, + } + _, err := agentClient.AuthGoogleInstanceIdentity(ctx, "", metadata) var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) @@ -168,7 +181,10 @@ func TestPostWorkspaceAuthGoogleInstanceIdentity(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - _, err := client.AuthWorkspaceGoogleInstanceIdentity(ctx, "", metadata) + agentClient := &agentsdk.Client{ + SDK: client, + } + _, err := agentClient.AuthGoogleInstanceIdentity(ctx, "", metadata) require.NoError(t, err) }) } diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index 58531d31d5..52784feaa1 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -23,6 +23,7 @@ import ( "github.com/coder/coder/coderd/rbac" "github.com/coder/coder/coderd/util/ptr" "github.com/coder/coder/codersdk" + "github.com/coder/coder/codersdk/agentsdk" "github.com/coder/coder/cryptorand" "github.com/coder/coder/provisioner/echo" "github.com/coder/coder/provisionersdk/proto" @@ -900,7 +901,7 @@ func TestWorkspaceFilterManual(t *testing.T) { workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) - agentClient := codersdk.New(client.URL) + agentClient := agentsdk.New(client.URL) agentClient.SetSessionToken(authToken) agentCloser := agent.New(agent.Options{ Client: agentClient, @@ -1551,7 +1552,7 @@ func TestWorkspaceWatcher(t *testing.T) { wait("agent timeout after create") wait("agent timeout after start") - agentClient := codersdk.New(client.URL) + agentClient := agentsdk.New(client.URL) agentClient.SetSessionToken(authToken) agentCloser := agent.New(agent.Options{ Client: agentClient, diff --git a/coderd/wsconncache/wsconncache.go b/coderd/wsconncache/wsconncache.go index d012e2d1f4..63e9399550 100644 --- a/coderd/wsconncache/wsconncache.go +++ b/coderd/wsconncache/wsconncache.go @@ -32,11 +32,11 @@ func New(dialer Dialer, inactiveTimeout time.Duration) *Cache { } // Dialer creates a new agent connection by ID. -type Dialer func(r *http.Request, id uuid.UUID) (*codersdk.AgentConn, error) +type Dialer func(r *http.Request, id uuid.UUID) (*codersdk.WorkspaceAgentConn, error) // Conn wraps an agent connection with a reusable HTTP transport. type Conn struct { - *codersdk.AgentConn + *codersdk.WorkspaceAgentConn locks atomic.Uint64 timeoutMutex sync.Mutex @@ -49,8 +49,8 @@ func (c *Conn) HTTPTransport() *http.Transport { return c.transport } -// CloseWithError ends the HTTP transport if exists, and closes the agent. -func (c *Conn) CloseWithError(err error) error { +// Close ends the HTTP transport if exists, and closes the agent. +func (c *Conn) Close() error { if c.transport != nil { c.transport.CloseIdleConnections() } @@ -59,7 +59,7 @@ func (c *Conn) CloseWithError(err error) error { if c.timeout != nil { c.timeout.Stop() } - return c.AgentConn.CloseWithError(err) + return c.WorkspaceAgentConn.Close() } type Cache struct { @@ -108,24 +108,20 @@ func (c *Cache) Acquire(r *http.Request, id uuid.UUID) (*Conn, func(), error) { transport := defaultTransport.Clone() transport.DialContext = agentConn.DialContext conn := &Conn{ - AgentConn: agentConn, - timeoutCancel: timeoutCancelFunc, - transport: transport, + WorkspaceAgentConn: agentConn, + timeoutCancel: timeoutCancelFunc, + transport: transport, } go func() { defer c.closeGroup.Done() - var err error select { case <-timeoutCtx.Done(): - err = xerrors.New("cache timeout") case <-c.closed: - err = xerrors.New("cache closed") case <-conn.Closed(): } - c.connMap.Delete(id.String()) c.connGroup.Forget(id.String()) - _ = conn.CloseWithError(err) + _ = conn.Close() }() return conn, nil }) diff --git a/coderd/wsconncache/wsconncache_test.go b/coderd/wsconncache/wsconncache_test.go index 37ff4fc0b8..45abb42033 100644 --- a/coderd/wsconncache/wsconncache_test.go +++ b/coderd/wsconncache/wsconncache_test.go @@ -26,6 +26,7 @@ import ( "github.com/coder/coder/agent" "github.com/coder/coder/coderd/wsconncache" "github.com/coder/coder/codersdk" + "github.com/coder/coder/codersdk/agentsdk" "github.com/coder/coder/tailnet" "github.com/coder/coder/tailnet/tailnettest" ) @@ -38,8 +39,8 @@ func TestCache(t *testing.T) { t.Parallel() t.Run("Same", func(t *testing.T) { t.Parallel() - cache := wsconncache.New(func(r *http.Request, id uuid.UUID) (*codersdk.AgentConn, error) { - return setupAgent(t, codersdk.WorkspaceAgentMetadata{}, 0), nil + cache := wsconncache.New(func(r *http.Request, id uuid.UUID) (*codersdk.WorkspaceAgentConn, error) { + return setupAgent(t, agentsdk.Metadata{}, 0), nil }, 0) defer func() { _ = cache.Close() @@ -53,9 +54,9 @@ func TestCache(t *testing.T) { t.Run("Expire", func(t *testing.T) { t.Parallel() called := atomic.NewInt32(0) - cache := wsconncache.New(func(r *http.Request, id uuid.UUID) (*codersdk.AgentConn, error) { + cache := wsconncache.New(func(r *http.Request, id uuid.UUID) (*codersdk.WorkspaceAgentConn, error) { called.Add(1) - return setupAgent(t, codersdk.WorkspaceAgentMetadata{}, 0), nil + return setupAgent(t, agentsdk.Metadata{}, 0), nil }, time.Microsecond) defer func() { _ = cache.Close() @@ -72,8 +73,8 @@ func TestCache(t *testing.T) { }) t.Run("NoExpireWhenLocked", func(t *testing.T) { t.Parallel() - cache := wsconncache.New(func(r *http.Request, id uuid.UUID) (*codersdk.AgentConn, error) { - return setupAgent(t, codersdk.WorkspaceAgentMetadata{}, 0), nil + cache := wsconncache.New(func(r *http.Request, id uuid.UUID) (*codersdk.WorkspaceAgentConn, error) { + return setupAgent(t, agentsdk.Metadata{}, 0), nil }, time.Microsecond) defer func() { _ = cache.Close() @@ -105,8 +106,8 @@ func TestCache(t *testing.T) { }() go server.Serve(random) - cache := wsconncache.New(func(r *http.Request, id uuid.UUID) (*codersdk.AgentConn, error) { - return setupAgent(t, codersdk.WorkspaceAgentMetadata{}, 0), nil + cache := wsconncache.New(func(r *http.Request, id uuid.UUID) (*codersdk.WorkspaceAgentConn, error) { + return setupAgent(t, agentsdk.Metadata{}, 0), nil }, time.Microsecond) defer func() { _ = cache.Close() @@ -144,7 +145,7 @@ func TestCache(t *testing.T) { }) } -func setupAgent(t *testing.T, metadata codersdk.WorkspaceAgentMetadata, ptyTimeout time.Duration) *codersdk.AgentConn { +func setupAgent(t *testing.T, metadata agentsdk.Metadata, ptyTimeout time.Duration) *codersdk.WorkspaceAgentConn { metadata.DERPMap = tailnettest.RunDERPAndSTUN(t) coordinator := tailnet.NewCoordinator() @@ -182,7 +183,7 @@ func setupAgent(t *testing.T, metadata codersdk.WorkspaceAgentMetadata, ptyTimeo return conn.UpdateNodes(node) }) conn.SetNodeCallback(sendNode) - return &codersdk.AgentConn{ + return &codersdk.WorkspaceAgentConn{ Conn: conn, } } @@ -190,15 +191,15 @@ func setupAgent(t *testing.T, metadata codersdk.WorkspaceAgentMetadata, ptyTimeo type client struct { t *testing.T agentID uuid.UUID - metadata codersdk.WorkspaceAgentMetadata + metadata agentsdk.Metadata coordinator tailnet.Coordinator } -func (c *client) WorkspaceAgentMetadata(_ context.Context) (codersdk.WorkspaceAgentMetadata, error) { +func (c *client) Metadata(_ context.Context) (agentsdk.Metadata, error) { return c.metadata, nil } -func (c *client) ListenWorkspaceAgent(_ context.Context) (net.Conn, error) { +func (c *client) Listen(_ context.Context) (net.Conn, error) { clientConn, serverConn := net.Pipe() closed := make(chan struct{}) c.t.Cleanup(func() { @@ -213,18 +214,18 @@ func (c *client) ListenWorkspaceAgent(_ context.Context) (net.Conn, error) { return clientConn, nil } -func (*client) AgentReportStats(_ context.Context, _ slog.Logger, _ func() *codersdk.AgentStats) (io.Closer, error) { +func (*client) ReportStats(_ context.Context, _ slog.Logger, _ func() *agentsdk.Stats) (io.Closer, error) { return io.NopCloser(strings.NewReader("")), nil } -func (*client) PostWorkspaceAgentLifecycle(_ context.Context, _ codersdk.PostWorkspaceAgentLifecycleRequest) error { +func (*client) PostLifecycle(_ context.Context, _ agentsdk.PostLifecycleRequest) error { return nil } -func (*client) PostWorkspaceAgentAppHealth(_ context.Context, _ codersdk.PostWorkspaceAppHealthsRequest) error { +func (*client) PostAppHealth(_ context.Context, _ agentsdk.PostAppHealthsRequest) error { return nil } -func (*client) PostWorkspaceAgentVersion(_ context.Context, _ string) error { +func (*client) PostVersion(_ context.Context, _ string) error { return nil } diff --git a/codersdk/agentsdk/agentsdk.go b/codersdk/agentsdk/agentsdk.go new file mode 100644 index 0000000000..d09c4a5382 --- /dev/null +++ b/codersdk/agentsdk/agentsdk.go @@ -0,0 +1,520 @@ +package agentsdk + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "net/http/cookiejar" + "net/url" + "strconv" + "time" + + "cloud.google.com/go/compute/metadata" + "golang.org/x/xerrors" + "nhooyr.io/websocket" + "tailscale.com/tailcfg" + + "github.com/coder/retry" + + "cdr.dev/slog" + + "github.com/google/uuid" + + "github.com/coder/coder/codersdk" +) + +// New returns a client that is used to interact with the +// Coder API from a workspace agent. +func New(serverURL *url.URL) *Client { + return &Client{ + SDK: codersdk.New(serverURL), + } +} + +// Client wraps `codersdk.Client` with specific functions +// scoped to a workspace agent. +type Client struct { + SDK *codersdk.Client +} + +func (c *Client) SetSessionToken(token string) { + c.SDK.SetSessionToken(token) +} + +type GitSSHKey struct { + PublicKey string `json:"public_key"` + PrivateKey string `json:"private_key"` +} + +// GitSSHKey will return the user's SSH key pair for the workspace. +func (c *Client) GitSSHKey(ctx context.Context) (GitSSHKey, error) { + res, err := c.SDK.Request(ctx, http.MethodGet, "/api/v2/workspaceagents/me/gitsshkey", nil) + if err != nil { + return GitSSHKey{}, xerrors.Errorf("execute request: %w", err) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return GitSSHKey{}, codersdk.ReadBodyAsError(res) + } + + var gitSSHKey GitSSHKey + return gitSSHKey, json.NewDecoder(res.Body).Decode(&gitSSHKey) +} + +type Metadata struct { + // GitAuthConfigs stores the number of Git configurations + // the Coder deployment has. If this number is >0, we + // set up special configuration in the workspace. + GitAuthConfigs int `json:"git_auth_configs"` + VSCodePortProxyURI string `json:"vscode_port_proxy_uri"` + Apps []codersdk.WorkspaceApp `json:"apps"` + DERPMap *tailcfg.DERPMap `json:"derpmap"` + EnvironmentVariables map[string]string `json:"environment_variables"` + StartupScript string `json:"startup_script"` + StartupScriptTimeout time.Duration `json:"startup_script_timeout"` + Directory string `json:"directory"` + MOTDFile string `json:"motd_file"` +} + +// Metadata fetches metadata for the currently authenticated workspace agent. +func (c *Client) Metadata(ctx context.Context) (Metadata, error) { + res, err := c.SDK.Request(ctx, http.MethodGet, "/api/v2/workspaceagents/me/metadata", nil) + if err != nil { + return Metadata{}, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return Metadata{}, codersdk.ReadBodyAsError(res) + } + var agentMeta Metadata + err = json.NewDecoder(res.Body).Decode(&agentMeta) + if err != nil { + return Metadata{}, err + } + accessingPort := c.SDK.URL.Port() + if accessingPort == "" { + accessingPort = "80" + if c.SDK.URL.Scheme == "https" { + accessingPort = "443" + } + } + accessPort, err := strconv.Atoi(accessingPort) + if err != nil { + return Metadata{}, xerrors.Errorf("convert accessing port %q: %w", accessingPort, err) + } + // Agents can provide an arbitrary access URL that may be different + // that the globally configured one. This breaks the built-in DERP, + // which would continue to reference the global access URL. + // + // This converts all built-in DERPs to use the access URL that the + // metadata request was performed with. + for _, region := range agentMeta.DERPMap.Regions { + if !region.EmbeddedRelay { + continue + } + + for _, node := range region.Nodes { + if node.STUNOnly { + continue + } + node.HostName = c.SDK.URL.Hostname() + node.DERPPort = accessPort + node.ForceHTTP = c.SDK.URL.Scheme == "http" + } + } + return agentMeta, nil +} + +// Listen connects to the workspace agent coordinate WebSocket +// that handles connection negotiation. +func (c *Client) Listen(ctx context.Context) (net.Conn, error) { + coordinateURL, err := c.SDK.URL.Parse("/api/v2/workspaceagents/me/coordinate") + if err != nil { + return nil, xerrors.Errorf("parse url: %w", err) + } + jar, err := cookiejar.New(nil) + if err != nil { + return nil, xerrors.Errorf("create cookie jar: %w", err) + } + jar.SetCookies(coordinateURL, []*http.Cookie{{ + Name: codersdk.SessionTokenCookie, + Value: c.SDK.SessionToken(), + }}) + httpClient := &http.Client{ + Jar: jar, + Transport: c.SDK.HTTPClient.Transport, + } + // nolint:bodyclose + conn, res, err := websocket.Dial(ctx, coordinateURL.String(), &websocket.DialOptions{ + HTTPClient: httpClient, + }) + if err != nil { + if res == nil { + return nil, err + } + return nil, codersdk.ReadBodyAsError(res) + } + + // Ping once every 30 seconds to ensure that the websocket is alive. If we + // don't get a response within 30s we kill the websocket and reconnect. + // See: https://github.com/coder/coder/pull/5824 + go func() { + tick := 30 * time.Second + ticker := time.NewTicker(tick) + defer ticker.Stop() + defer func() { + c.SDK.Logger.Debug(ctx, "coordinate pinger exited") + }() + for { + select { + case <-ctx.Done(): + return + case start := <-ticker.C: + ctx, cancel := context.WithTimeout(ctx, tick) + + err := conn.Ping(ctx) + if err != nil { + c.SDK.Logger.Error(ctx, "workspace agent coordinate ping", slog.Error(err)) + + err := conn.Close(websocket.StatusGoingAway, "Ping failed") + if err != nil { + c.SDK.Logger.Error(ctx, "close workspace agent coordinate websocket", slog.Error(err)) + } + + cancel() + return + } + + c.SDK.Logger.Debug(ctx, "got coordinate pong", slog.F("took", time.Since(start))) + cancel() + } + } + }() + + return websocket.NetConn(ctx, conn, websocket.MessageBinary), nil +} + +type PostAppHealthsRequest struct { + // Healths is a map of the workspace app name and the health of the app. + Healths map[uuid.UUID]codersdk.WorkspaceAppHealth +} + +// PostAppHealth updates the workspace agent app health status. +func (c *Client) PostAppHealth(ctx context.Context, req PostAppHealthsRequest) error { + res, err := c.SDK.Request(ctx, http.MethodPost, "/api/v2/workspaceagents/me/app-health", req) + if err != nil { + return err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return codersdk.ReadBodyAsError(res) + } + + return nil +} + +// AuthenticateResponse is returned when an instance ID +// has been exchanged for a session token. +// @typescript-ignore AuthenticateResponse +type AuthenticateResponse struct { + SessionToken string `json:"session_token"` +} + +type GoogleInstanceIdentityToken struct { + JSONWebToken string `json:"json_web_token" validate:"required"` +} + +// AuthWorkspaceGoogleInstanceIdentity uses the Google Compute Engine Metadata API to +// fetch a signed JWT, and exchange it for a session token for a workspace agent. +// +// The requesting instance must be registered as a resource in the latest history for a workspace. +func (c *Client) AuthGoogleInstanceIdentity(ctx context.Context, serviceAccount string, gcpClient *metadata.Client) (AuthenticateResponse, error) { + if serviceAccount == "" { + // This is the default name specified by Google. + serviceAccount = "default" + } + if gcpClient == nil { + gcpClient = metadata.NewClient(c.SDK.HTTPClient) + } + // "format=full" is required, otherwise the responding payload will be missing "instance_id". + jwt, err := gcpClient.Get(fmt.Sprintf("instance/service-accounts/%s/identity?audience=coder&format=full", serviceAccount)) + if err != nil { + return AuthenticateResponse{}, xerrors.Errorf("get metadata identity: %w", err) + } + res, err := c.SDK.Request(ctx, http.MethodPost, "/api/v2/workspaceagents/google-instance-identity", GoogleInstanceIdentityToken{ + JSONWebToken: jwt, + }) + if err != nil { + return AuthenticateResponse{}, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return AuthenticateResponse{}, codersdk.ReadBodyAsError(res) + } + var resp AuthenticateResponse + return resp, json.NewDecoder(res.Body).Decode(&resp) +} + +type AWSInstanceIdentityToken struct { + Signature string `json:"signature" validate:"required"` + Document string `json:"document" validate:"required"` +} + +// AuthWorkspaceAWSInstanceIdentity uses the Amazon Metadata API to +// fetch a signed payload, and exchange it for a session token for a workspace agent. +// +// The requesting instance must be registered as a resource in the latest history for a workspace. +func (c *Client) AuthAWSInstanceIdentity(ctx context.Context) (AuthenticateResponse, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodPut, "http://169.254.169.254/latest/api/token", nil) + if err != nil { + return AuthenticateResponse{}, nil + } + req.Header.Set("X-aws-ec2-metadata-token-ttl-seconds", "21600") + res, err := c.SDK.HTTPClient.Do(req) + if err != nil { + return AuthenticateResponse{}, err + } + defer res.Body.Close() + token, err := io.ReadAll(res.Body) + if err != nil { + return AuthenticateResponse{}, xerrors.Errorf("read token: %w", err) + } + + req, err = http.NewRequestWithContext(ctx, http.MethodGet, "http://169.254.169.254/latest/dynamic/instance-identity/signature", nil) + if err != nil { + return AuthenticateResponse{}, nil + } + req.Header.Set("X-aws-ec2-metadata-token", string(token)) + res, err = c.SDK.HTTPClient.Do(req) + if err != nil { + return AuthenticateResponse{}, err + } + defer res.Body.Close() + signature, err := io.ReadAll(res.Body) + if err != nil { + return AuthenticateResponse{}, xerrors.Errorf("read token: %w", err) + } + + req, err = http.NewRequestWithContext(ctx, http.MethodGet, "http://169.254.169.254/latest/dynamic/instance-identity/document", nil) + if err != nil { + return AuthenticateResponse{}, nil + } + req.Header.Set("X-aws-ec2-metadata-token", string(token)) + res, err = c.SDK.HTTPClient.Do(req) + if err != nil { + return AuthenticateResponse{}, err + } + defer res.Body.Close() + document, err := io.ReadAll(res.Body) + if err != nil { + return AuthenticateResponse{}, xerrors.Errorf("read token: %w", err) + } + + res, err = c.SDK.Request(ctx, http.MethodPost, "/api/v2/workspaceagents/aws-instance-identity", AWSInstanceIdentityToken{ + Signature: string(signature), + Document: string(document), + }) + if err != nil { + return AuthenticateResponse{}, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return AuthenticateResponse{}, codersdk.ReadBodyAsError(res) + } + var resp AuthenticateResponse + return resp, json.NewDecoder(res.Body).Decode(&resp) +} + +type AzureInstanceIdentityToken struct { + Signature string `json:"signature" validate:"required"` + Encoding string `json:"encoding" validate:"required"` +} + +// AuthWorkspaceAzureInstanceIdentity uses the Azure Instance Metadata Service to +// fetch a signed payload, and exchange it for a session token for a workspace agent. +func (c *Client) AuthAzureInstanceIdentity(ctx context.Context) (AuthenticateResponse, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://169.254.169.254/metadata/attested/document?api-version=2020-09-01", nil) + if err != nil { + return AuthenticateResponse{}, nil + } + req.Header.Set("Metadata", "true") + res, err := c.SDK.HTTPClient.Do(req) + if err != nil { + return AuthenticateResponse{}, err + } + defer res.Body.Close() + + var token AzureInstanceIdentityToken + err = json.NewDecoder(res.Body).Decode(&token) + if err != nil { + return AuthenticateResponse{}, err + } + + res, err = c.SDK.Request(ctx, http.MethodPost, "/api/v2/workspaceagents/azure-instance-identity", token) + if err != nil { + return AuthenticateResponse{}, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return AuthenticateResponse{}, codersdk.ReadBodyAsError(res) + } + var resp AuthenticateResponse + return resp, json.NewDecoder(res.Body).Decode(&resp) +} + +// ReportStats begins a stat streaming connection with the Coder server. +// It is resilient to network failures and intermittent coderd issues. +func (c *Client) ReportStats( + ctx context.Context, + log slog.Logger, + getStats func() *Stats, +) (io.Closer, error) { + ctx, cancel := context.WithCancel(ctx) + + go func() { + // Immediately trigger a stats push to get the correct interval. + timer := time.NewTimer(time.Nanosecond) + defer timer.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-timer.C: + } + + var nextInterval time.Duration + for r := retry.New(100*time.Millisecond, time.Minute); r.Wait(ctx); { + resp, err := c.PostStats(ctx, getStats()) + if err != nil { + if !xerrors.Is(err, context.Canceled) { + log.Error(ctx, "report stats", slog.Error(err)) + } + continue + } + + nextInterval = resp.ReportInterval + break + } + timer.Reset(nextInterval) + } + }() + + return closeFunc(func() error { + cancel() + return nil + }), nil +} + +// Stats records the Agent's network connection statistics for use in +// user-facing metrics and debugging. +type Stats struct { + // ConnsByProto is a count of connections by protocol. + ConnsByProto map[string]int64 `json:"conns_by_proto"` + // NumConns is the number of connections received by an agent. + NumConns int64 `json:"num_comms"` + // RxPackets is the number of received packets. + RxPackets int64 `json:"rx_packets"` + // RxBytes is the number of received bytes. + RxBytes int64 `json:"rx_bytes"` + // TxPackets is the number of transmitted bytes. + TxPackets int64 `json:"tx_packets"` + // TxBytes is the number of transmitted bytes. + TxBytes int64 `json:"tx_bytes"` +} + +type StatsResponse struct { + // ReportInterval is the duration after which the agent should send stats + // again. + ReportInterval time.Duration `json:"report_interval"` +} + +func (c *Client) PostStats(ctx context.Context, stats *Stats) (StatsResponse, error) { + res, err := c.SDK.Request(ctx, http.MethodPost, "/api/v2/workspaceagents/me/report-stats", stats) + if err != nil { + return StatsResponse{}, xerrors.Errorf("send request: %w", err) + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return StatsResponse{}, codersdk.ReadBodyAsError(res) + } + + var interval StatsResponse + err = json.NewDecoder(res.Body).Decode(&interval) + if err != nil { + return StatsResponse{}, xerrors.Errorf("decode stats response: %w", err) + } + + return interval, nil +} + +type PostLifecycleRequest struct { + State codersdk.WorkspaceAgentLifecycle `json:"state"` +} + +func (c *Client) PostLifecycle(ctx context.Context, req PostLifecycleRequest) error { + res, err := c.SDK.Request(ctx, http.MethodPost, "/api/v2/workspaceagents/me/report-lifecycle", req) + if err != nil { + return xerrors.Errorf("agent state post request: %w", err) + } + defer res.Body.Close() + if res.StatusCode != http.StatusNoContent { + return codersdk.ReadBodyAsError(res) + } + + return nil +} + +type PostVersionRequest struct { + Version string `json:"version"` +} + +func (c *Client) PostVersion(ctx context.Context, version string) error { + versionReq := PostVersionRequest{Version: version} + res, err := c.SDK.Request(ctx, http.MethodPost, "/api/v2/workspaceagents/me/version", versionReq) + if err != nil { + return err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return codersdk.ReadBodyAsError(res) + } + return nil +} + +type GitAuthResponse struct { + Username string `json:"username"` + Password string `json:"password"` + URL string `json:"url"` +} + +// GitAuth submits a URL to fetch a GIT_ASKPASS username and password for. +// nolint:revive +func (c *Client) GitAuth(ctx context.Context, gitURL string, listen bool) (GitAuthResponse, error) { + reqURL := "/api/v2/workspaceagents/me/gitauth?url=" + url.QueryEscape(gitURL) + if listen { + reqURL += "&listen" + } + res, err := c.SDK.Request(ctx, http.MethodGet, reqURL, nil) + if err != nil { + return GitAuthResponse{}, xerrors.Errorf("execute request: %w", err) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return GitAuthResponse{}, codersdk.ReadBodyAsError(res) + } + + var authResp GitAuthResponse + return authResp, json.NewDecoder(res.Body).Decode(&authResp) +} + +type closeFunc func() error + +func (c closeFunc) Close() error { + return c() +} diff --git a/codersdk/apikey.go b/codersdk/apikey.go index 739fe7d255..a9aeb3486d 100644 --- a/codersdk/apikey.go +++ b/codersdk/apikey.go @@ -23,6 +23,7 @@ type APIKey struct { LifetimeSeconds int64 `json:"lifetime_seconds" validate:"required"` } +// LoginType is the type of login used to create the API key. type LoginType string const ( @@ -35,7 +36,10 @@ const ( type APIKeyScope string const ( - APIKeyScopeAll APIKeyScope = "all" + // APIKeyScopeAll is a scope that allows the user to do everything. + APIKeyScopeAll APIKeyScope = "all" + // APIKeyScopeApplicationConnect is a scope that allows the user + // to connect to applications in a workspace. APIKeyScopeApplicationConnect APIKeyScope = "application_connect" ) @@ -49,7 +53,9 @@ type GenerateAPIKeyResponse struct { Key string `json:"key"` } -// CreateToken generates an API key that doesn't expire. +// CreateToken generates an API key for the user ID provided with +// custom expiration. These tokens can be used for long-lived access, +// like for use with CI. func (c *Client) CreateToken(ctx context.Context, userID string, req CreateTokenRequest) (GenerateAPIKeyResponse, error) { res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/keys/tokens", userID), req) if err != nil { @@ -57,7 +63,7 @@ func (c *Client) CreateToken(ctx context.Context, userID string, req CreateToken } defer res.Body.Close() if res.StatusCode > http.StatusCreated { - return GenerateAPIKeyResponse{}, readBodyAsError(res) + return GenerateAPIKeyResponse{}, ReadBodyAsError(res) } var apiKey GenerateAPIKeyResponse @@ -73,36 +79,36 @@ func (c *Client) CreateAPIKey(ctx context.Context, user string) (GenerateAPIKeyR } defer res.Body.Close() if res.StatusCode > http.StatusCreated { - return GenerateAPIKeyResponse{}, readBodyAsError(res) + return GenerateAPIKeyResponse{}, ReadBodyAsError(res) } var apiKey GenerateAPIKeyResponse return apiKey, json.NewDecoder(res.Body).Decode(&apiKey) } -// GetTokens list machine API keys. -func (c *Client) GetTokens(ctx context.Context, userID string) ([]APIKey, error) { +// Tokens list machine API keys. +func (c *Client) Tokens(ctx context.Context, userID string) ([]APIKey, error) { res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/keys/tokens", userID), nil) if err != nil { return nil, err } defer res.Body.Close() if res.StatusCode > http.StatusOK { - return nil, readBodyAsError(res) + return nil, ReadBodyAsError(res) } var apiKey = []APIKey{} return apiKey, json.NewDecoder(res.Body).Decode(&apiKey) } -// GetAPIKey returns the api key by id. -func (c *Client) GetAPIKey(ctx context.Context, userID string, id string) (*APIKey, error) { +// APIKey returns the api key by id. +func (c *Client) APIKey(ctx context.Context, userID string, id string) (*APIKey, error) { res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/keys/%s", userID, id), nil) if err != nil { return nil, err } defer res.Body.Close() if res.StatusCode > http.StatusCreated { - return nil, readBodyAsError(res) + return nil, ReadBodyAsError(res) } apiKey := &APIKey{} return apiKey, json.NewDecoder(res.Body).Decode(apiKey) @@ -116,7 +122,7 @@ func (c *Client) DeleteAPIKey(ctx context.Context, userID string, id string) err } defer res.Body.Close() if res.StatusCode > http.StatusNoContent { - return readBodyAsError(res) + return ReadBodyAsError(res) } return nil } diff --git a/codersdk/appearance.go b/codersdk/appearance.go deleted file mode 100644 index 07b69e67dc..0000000000 --- a/codersdk/appearance.go +++ /dev/null @@ -1,43 +0,0 @@ -package codersdk - -import ( - "context" - "encoding/json" - "net/http" -) - -type AppearanceConfig struct { - LogoURL string `json:"logo_url"` - ServiceBanner ServiceBannerConfig `json:"service_banner"` -} - -type ServiceBannerConfig struct { - Enabled bool `json:"enabled"` - Message string `json:"message,omitempty"` - BackgroundColor string `json:"background_color,omitempty"` -} - -func (c *Client) Appearance(ctx context.Context) (AppearanceConfig, error) { - res, err := c.Request(ctx, http.MethodGet, "/api/v2/appearance", nil) - if err != nil { - return AppearanceConfig{}, err - } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return AppearanceConfig{}, readBodyAsError(res) - } - var cfg AppearanceConfig - return cfg, json.NewDecoder(res.Body).Decode(&cfg) -} - -func (c *Client) UpdateAppearance(ctx context.Context, appearance AppearanceConfig) error { - res, err := c.Request(ctx, http.MethodPut, "/api/v2/appearance", appearance) - if err != nil { - return err - } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return readBodyAsError(res) - } - return nil -} diff --git a/codersdk/audit.go b/codersdk/audit.go index e06b47d757..38def7f709 100644 --- a/codersdk/audit.go +++ b/codersdk/audit.go @@ -59,7 +59,7 @@ const ( AuditActionStop AuditAction = "stop" ) -func (a AuditAction) FriendlyString() string { +func (a AuditAction) Friendly() string { switch a { case AuditActionCreate: return "created" @@ -142,7 +142,7 @@ func (c *Client) AuditLogs(ctx context.Context, req AuditLogsRequest) (AuditLogR defer res.Body.Close() if res.StatusCode != http.StatusOK { - return AuditLogResponse{}, readBodyAsError(res) + return AuditLogResponse{}, ReadBodyAsError(res) } var logRes AuditLogResponse @@ -154,6 +154,8 @@ func (c *Client) AuditLogs(ctx context.Context, req AuditLogsRequest) (AuditLogR return logRes, nil } +// CreateTestAuditLog creates a fake audit log. Only owners of the organization +// can perform this action. It's used for testing purposes. func (c *Client) CreateTestAuditLog(ctx context.Context, req CreateTestAuditLogRequest) error { res, err := c.Request(ctx, http.MethodPost, "/api/v2/audit/testgenerate", req) if err != nil { diff --git a/codersdk/authorization.go b/codersdk/authorization.go index b16e924236..2f4365e793 100644 --- a/codersdk/authorization.go +++ b/codersdk/authorization.go @@ -56,14 +56,16 @@ type AuthorizationObject struct { ResourceID string `json:"resource_id,omitempty"` } -func (c *Client) CheckAuthorization(ctx context.Context, req AuthorizationRequest) (AuthorizationResponse, error) { +// AuthCheck allows the authenticated user to check if they have the given permissions +// to a set of resources. +func (c *Client) AuthCheck(ctx context.Context, req AuthorizationRequest) (AuthorizationResponse, error) { res, err := c.Request(ctx, http.MethodPost, "/api/v2/authcheck", req) if err != nil { return nil, err } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return AuthorizationResponse{}, readBodyAsError(res) + return AuthorizationResponse{}, ReadBodyAsError(res) } var resp AuthorizationResponse return resp, json.NewDecoder(res.Body).Decode(&resp) diff --git a/codersdk/branding.go b/codersdk/branding.go deleted file mode 100644 index fc24303511..0000000000 --- a/codersdk/branding.go +++ /dev/null @@ -1,23 +0,0 @@ -package codersdk - -import ( - "context" - "net/http" -) - -type UpdateBrandingRequest struct { - LogoURL string `json:"logo_url"` -} - -// UpdateBranding applies customization settings available to Enterprise customers. -func (c *Client) UpdateBranding(ctx context.Context, req UpdateBrandingRequest) error { - res, err := c.Request(ctx, http.MethodPut, "/api/v2/branding", req) - if err != nil { - return err - } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return readBodyAsError(res) - } - return nil -} diff --git a/codersdk/buildinfo.go b/codersdk/buildinfo.go deleted file mode 100644 index 19c7f21910..0000000000 --- a/codersdk/buildinfo.go +++ /dev/null @@ -1,44 +0,0 @@ -package codersdk - -import ( - "context" - "encoding/json" - "net/http" - "strings" - - "golang.org/x/mod/semver" -) - -// BuildInfoResponse contains build information for this instance of Coder. -type BuildInfoResponse struct { - // ExternalURL references the current Coder version. - // For production builds, this will link directly to a release. For development builds, this will link to a commit. - ExternalURL string `json:"external_url"` - // Version returns the semantic version of the build. - Version string `json:"version"` -} - -// CanonicalVersion trims build information from the version. -// E.g. 'v0.7.4-devel+11573034' -> 'v0.7.4'. -func (b BuildInfoResponse) CanonicalVersion() string { - // We do a little hack here to massage the string into a form - // that works well with semver. - trimmed := strings.ReplaceAll(b.Version, "-devel+", "+devel-") - return semver.Canonical(trimmed) -} - -// BuildInfo returns build information for this instance of Coder. -func (c *Client) BuildInfo(ctx context.Context) (BuildInfoResponse, error) { - res, err := c.Request(ctx, http.MethodGet, "/api/v2/buildinfo", nil) - if err != nil { - return BuildInfoResponse{}, err - } - defer res.Body.Close() - - if res.StatusCode != http.StatusOK { - return BuildInfoResponse{}, readBodyAsError(res) - } - - var buildInfo BuildInfoResponse - return buildInfo, json.NewDecoder(res.Body).Decode(&buildInfo) -} diff --git a/codersdk/client.go b/codersdk/client.go index d977623cda..dac6fdc533 100644 --- a/codersdk/client.go +++ b/codersdk/client.go @@ -8,6 +8,7 @@ import ( "fmt" "io" "mime" + "net" "net/http" "net/url" "strings" @@ -28,17 +29,23 @@ import ( // shouldn't be likely to conflict with any user-application set cookies. // Be sure to strip additional cookies in httpapi.StripCoderCookies! const ( - // SessionTokenKey represents the name of the cookie or query parameter the API key is stored in. - SessionTokenKey = "coder_session_token" - // SessionCustomHeader is the custom header to use for authentication. - SessionCustomHeader = "Coder-Session-Token" - OAuth2StateKey = "oauth_state" - OAuth2RedirectKey = "oauth_redirect" + // SessionTokenCookie represents the name of the cookie or query parameter the API key is stored in. + SessionTokenCookie = "coder_session_token" + // SessionTokenHeader is the custom header to use for authentication. + SessionTokenHeader = "Coder-Session-Token" + // OAuth2StateCookie is the name of the cookie that stores the oauth2 state. + OAuth2StateCookie = "oauth_state" + // OAuth2RedirectCookie is the name of the cookie that stores the oauth2 redirect. + OAuth2RedirectCookie = "oauth_redirect" + // BypassRatelimitHeader is the custom header to use to bypass ratelimits. + // Only owners can bypass rate limits. This is typically used for scale testing. // nolint: gosec BypassRatelimitHeader = "X-Coder-Bypass-Ratelimit" ) +// loggableMimeTypes is a list of MIME types that are safe to log +// the output of. This is useful for debugging or testing. var loggableMimeTypes = map[string]struct{}{ "application/json": {}, "text/plain": {}, @@ -63,65 +70,32 @@ type Client struct { HTTPClient *http.Client URL *url.URL - // Logger can be provided to log requests. Request method, URL and response - // status code will be logged by default. + // Logger is optionally provided to log requests. + // Method, URL, and response code will be logged by default. Logger slog.Logger - // LogBodies determines whether the request and response bodies are logged - // to the provided Logger. This is useful for debugging or testing. + + // LogBodies can be enabled to print request and response bodies to the logger. LogBodies bool - // BypassRatelimits is an optional flag that can be set by the site owner to - // disable ratelimit checks for the client. - BypassRatelimits bool - - // PropagateTracing is an optional flag that can be set to propagate tracing - // spans to the Coder API. This is useful for seeing the entire request - // from end-to-end. - PropagateTracing bool + // Trace can be enabled to propagate tracing spans to the Coder API. + // This is useful for tracking a request end-to-end. + Trace bool } +// SessionToken returns the currently set token for the client. func (c *Client) SessionToken() string { c.mu.RLock() defer c.mu.RUnlock() return c.sessionToken } +// SetSessionToken returns the currently set token for the client. func (c *Client) SetSessionToken(token string) { c.mu.Lock() defer c.mu.Unlock() c.sessionToken = token } -func (c *Client) Clone() *Client { - c.mu.Lock() - defer c.mu.Unlock() - - hc := *c.HTTPClient - u := *c.URL - return &Client{ - HTTPClient: &hc, - sessionToken: c.sessionToken, - URL: &u, - Logger: c.Logger, - LogBodies: c.LogBodies, - BypassRatelimits: c.BypassRatelimits, - PropagateTracing: c.PropagateTracing, - } -} - -type RequestOption func(*http.Request) - -func WithQueryParam(key, value string) RequestOption { - return func(r *http.Request) { - if value == "" { - return - } - q := r.URL.Query() - q.Add(key, value) - r.URL.RawQuery = q.Encode() - } -} - // Request performs a HTTP request with the body provided. The caller is // responsible for closing the response body. func (c *Client) Request(ctx context.Context, method, path string, body interface{}, opts ...RequestOption) (*http.Response, error) { @@ -165,10 +139,7 @@ func (c *Client) Request(ctx context.Context, method, path string, body interfac if err != nil { return nil, xerrors.Errorf("create request: %w", err) } - req.Header.Set(SessionCustomHeader, c.SessionToken()) - if c.BypassRatelimits { - req.Header.Set(BypassRatelimitHeader, "true") - } + req.Header.Set(SessionTokenHeader, c.SessionToken()) if r != nil { req.Header.Set("Content-Type", "application/json") @@ -181,7 +152,7 @@ func (c *Client) Request(ctx context.Context, method, path string, body interfac span.SetAttributes(semconv.HTTPClientAttributesFromHTTPRequest(req)...) // Inject tracing headers if enabled. - if c.PropagateTracing { + if c.Trace { tmp := otel.GetTextMapPropagator() hc := propagation.HeaderCarrier(req.Header) tmp.Inject(ctx, hc) @@ -235,28 +206,28 @@ func (c *Client) Request(ctx context.Context, method, path string, body interfac return resp, err } -// readBodyAsError reads the response as an .Message, and +// ReadBodyAsError reads the response as a codersdk.Response, and // wraps it in a codersdk.Error type for easy marshaling. -func readBodyAsError(res *http.Response) error { +func ReadBodyAsError(res *http.Response) error { if res == nil { return xerrors.Errorf("no body returned") } defer res.Body.Close() contentType := res.Header.Get("Content-Type") - var method, u string + var requestMethod, requestURL string if res.Request != nil { - method = res.Request.Method + requestMethod = res.Request.Method if res.Request.URL != nil { - u = res.Request.URL.String() + requestURL = res.Request.URL.String() } } - var helper string + var helpMessage string if res.StatusCode == http.StatusUnauthorized { // 401 means the user is not logged in // 403 would mean that the user is not authorized - helper = "Try logging in using 'coder login '." + helpMessage = "Try logging in using 'coder login '." } resp, err := io.ReadAll(res.Body) @@ -278,7 +249,7 @@ func readBodyAsError(res *http.Response) error { Message: fmt.Sprintf("unexpected non-JSON response %q", contentType), Detail: string(resp), }, - Helper: helper, + Helper: helpMessage, } } @@ -291,7 +262,7 @@ func readBodyAsError(res *http.Response) error { Response: Response{ Message: "empty response body", }, - Helper: helper, + Helper: helpMessage, } } return xerrors.Errorf("decode body: %w", err) @@ -307,9 +278,9 @@ func readBodyAsError(res *http.Response) error { return &Error{ Response: m, statusCode: res.StatusCode, - method: method, - url: u, - Helper: helper, + method: requestMethod, + url: requestURL, + Helper: helpMessage, } } @@ -370,3 +341,68 @@ func parseMimeType(contentType string) string { return mimeType } + +// Response represents a generic HTTP response. +type Response struct { + // Message is an actionable message that depicts actions the request took. + // These messages should be fully formed sentences with proper punctuation. + // Examples: + // - "A user has been created." + // - "Failed to create a user." + Message string `json:"message"` + // Detail is a debug message that provides further insight into why the + // action failed. This information can be technical and a regular golang + // err.Error() text. + // - "database: too many open connections" + // - "stat: too many open files" + Detail string `json:"detail,omitempty"` + // Validations are form field-specific friendly error messages. They will be + // shown on a form field in the UI. These can also be used to add additional + // context if there is a set of errors in the primary 'Message'. + Validations []ValidationError `json:"validations,omitempty"` +} + +// ValidationError represents a scoped error to a user input. +type ValidationError struct { + Field string `json:"field" validate:"required"` + Detail string `json:"detail" validate:"required"` +} + +func (e ValidationError) Error() string { + return fmt.Sprintf("field: %s detail: %s", e.Field, e.Detail) +} + +var _ error = (*ValidationError)(nil) + +// IsConnectionError is a convenience function for checking if the source of an +// error is due to a 'connection refused', 'no such host', etc. +func IsConnectionError(err error) bool { + var ( + // E.g. no such host + dnsErr *net.DNSError + // Eg. connection refused + opErr *net.OpError + ) + + return xerrors.As(err, &dnsErr) || xerrors.As(err, &opErr) +} + +func AsError(err error) (*Error, bool) { + var e *Error + return e, xerrors.As(err, &e) +} + +// RequestOption is a function that can be used to modify an http.Request. +type RequestOption func(*http.Request) + +// WithQueryParam adds a query parameter to the request. +func WithQueryParam(key, value string) RequestOption { + return func(r *http.Request) { + if value == "" { + return + } + q := r.URL.Query() + q.Add(key, value) + r.URL.RawQuery = q.Encode() + } +} diff --git a/codersdk/client_internal_test.go b/codersdk/client_internal_test.go index f48467718a..3dea65a24a 100644 --- a/codersdk/client_internal_test.go +++ b/codersdk/client_internal_test.go @@ -6,9 +6,11 @@ import ( "encoding/json" "fmt" "io" + "net" "net/http" "net/http/httptest" "net/url" + "os" "strconv" "strings" "testing" @@ -31,6 +33,59 @@ import ( const jsonCT = "application/json" +func TestIsConnectionErr(t *testing.T) { + t.Parallel() + + type tc = struct { + name string + err error + expectedResult bool + } + + cases := []tc{ + { + // E.g. "no such host" + name: "DNSError", + err: &net.DNSError{ + Err: "no such host", + Name: "foofoo", + Server: "1.1.1.1:53", + IsTimeout: false, + IsTemporary: false, + IsNotFound: true, + }, + expectedResult: true, + }, + { + // E.g. "connection refused" + name: "OpErr", + err: &net.OpError{ + Op: "dial", + Net: "tcp", + Source: nil, + Addr: nil, + Err: &os.SyscallError{}, + }, + expectedResult: true, + }, + { + name: "OpaqueError", + err: xerrors.Errorf("I'm opaque!"), + expectedResult: false, + }, + } + + for _, c := range cases { + c := c + + t.Run(c.name, func(t *testing.T) { + t.Parallel() + + require.Equal(t, c.expectedResult, IsConnectionError(c.err)) + }) + } +} + func Test_Client(t *testing.T) { t.Parallel() @@ -43,8 +98,7 @@ func Test_Client(t *testing.T) { s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, method, r.Method) assert.Equal(t, path, r.URL.Path) - assert.Equal(t, token, r.Header.Get(SessionCustomHeader)) - assert.Equal(t, "true", r.Header.Get(BypassRatelimitHeader)) + assert.Equal(t, token, r.Header.Get(SessionTokenHeader)) assert.NotEmpty(t, r.Header.Get("Traceparent")) for k, v := range r.Header { t.Logf("header %q: %q", k, strings.Join(v, ", ")) @@ -59,7 +113,6 @@ func Test_Client(t *testing.T) { require.NoError(t, err) client := New(u) client.SetSessionToken(token) - client.BypassRatelimits = true logBuf := bytes.NewBuffer(nil) client.Logger = slog.Make(sloghuman.Sink(logBuf)).Leveled(slog.LevelDebug) @@ -83,7 +136,7 @@ func Test_Client(t *testing.T) { ), ) otel.SetLogger(logr.Discard()) - client.PropagateTracing = true + client.Trace = true ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() @@ -240,7 +293,7 @@ func Test_readBodyAsError(t *testing.T) { c.res.Request = c.req - err := readBodyAsError(c.res) + err := ReadBodyAsError(c.res) c.assert(t, err) }) } diff --git a/codersdk/deploymentconfig.go b/codersdk/deployment.go similarity index 60% rename from codersdk/deploymentconfig.go rename to codersdk/deployment.go index 6aaa326cb8..3d158fe44c 100644 --- a/codersdk/deploymentconfig.go +++ b/codersdk/deployment.go @@ -4,11 +4,106 @@ import ( "context" "encoding/json" "net/http" + "strings" "time" + "golang.org/x/mod/semver" "golang.org/x/xerrors" ) +// Entitlement represents whether a feature is licensed. +type Entitlement string + +const ( + EntitlementEntitled Entitlement = "entitled" + EntitlementGracePeriod Entitlement = "grace_period" + EntitlementNotEntitled Entitlement = "not_entitled" +) + +// FeatureName represents the internal name of a feature. +// To add a new feature, add it to this set of enums as well as the FeatureNames +// array below. +type FeatureName string + +const ( + FeatureUserLimit FeatureName = "user_limit" + FeatureAuditLog FeatureName = "audit_log" + FeatureBrowserOnly FeatureName = "browser_only" + FeatureSCIM FeatureName = "scim" + FeatureTemplateRBAC FeatureName = "template_rbac" + FeatureHighAvailability FeatureName = "high_availability" + FeatureMultipleGitAuth FeatureName = "multiple_git_auth" + FeatureExternalProvisionerDaemons FeatureName = "external_provisioner_daemons" + FeatureAppearance FeatureName = "appearance" +) + +// FeatureNames must be kept in-sync with the Feature enum above. +var FeatureNames = []FeatureName{ + FeatureUserLimit, + FeatureAuditLog, + FeatureBrowserOnly, + FeatureSCIM, + FeatureTemplateRBAC, + FeatureHighAvailability, + FeatureMultipleGitAuth, + FeatureExternalProvisionerDaemons, + FeatureAppearance, +} + +// Humanize returns the feature name in a human-readable format. +func (n FeatureName) Humanize() string { + switch n { + case FeatureTemplateRBAC: + return "Template RBAC" + case FeatureSCIM: + return "SCIM" + default: + return strings.Title(strings.ReplaceAll(string(n), "_", " ")) + } +} + +// AlwaysEnable returns if the feature is always enabled if entitled. +// Warning: We don't know if we need this functionality. +// This method may disappear at any time. +func (n FeatureName) AlwaysEnable() bool { + return map[FeatureName]bool{ + FeatureMultipleGitAuth: true, + FeatureExternalProvisionerDaemons: true, + FeatureAppearance: true, + }[n] +} + +type Feature struct { + Entitlement Entitlement `json:"entitlement"` + Enabled bool `json:"enabled"` + Limit *int64 `json:"limit,omitempty"` + Actual *int64 `json:"actual,omitempty"` +} + +type Entitlements struct { + Features map[FeatureName]Feature `json:"features"` + Warnings []string `json:"warnings"` + Errors []string `json:"errors"` + HasLicense bool `json:"has_license"` + Trial bool `json:"trial"` + + // DEPRECATED: use Experiments instead. + Experimental bool `json:"experimental"` +} + +func (c *Client) Entitlements(ctx context.Context) (Entitlements, error) { + res, err := c.Request(ctx, http.MethodGet, "/api/v2/entitlements", nil) + if err != nil { + return Entitlements{}, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return Entitlements{}, ReadBodyAsError(res) + } + var ent Entitlements + return ent, json.NewDecoder(res.Body).Decode(&ent) +} + // DeploymentConfig is the central configuration for the coder server. type DeploymentConfig struct { AccessURL *DeploymentConfigField[string] `json:"access_url" typescript:",notnull"` @@ -234,9 +329,173 @@ func (c *Client) DeploymentConfig(ctx context.Context) (DeploymentConfig, error) defer res.Body.Close() if res.StatusCode != http.StatusOK { - return DeploymentConfig{}, readBodyAsError(res) + return DeploymentConfig{}, ReadBodyAsError(res) } var df DeploymentConfig return df, json.NewDecoder(res.Body).Decode(&df) } + +type AppearanceConfig struct { + LogoURL string `json:"logo_url"` + ServiceBanner ServiceBannerConfig `json:"service_banner"` +} + +type ServiceBannerConfig struct { + Enabled bool `json:"enabled"` + Message string `json:"message,omitempty"` + BackgroundColor string `json:"background_color,omitempty"` +} + +// Appearance returns the configuration that modifies the visual +// display of the dashboard. +func (c *Client) Appearance(ctx context.Context) (AppearanceConfig, error) { + res, err := c.Request(ctx, http.MethodGet, "/api/v2/appearance", nil) + if err != nil { + return AppearanceConfig{}, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return AppearanceConfig{}, ReadBodyAsError(res) + } + var cfg AppearanceConfig + return cfg, json.NewDecoder(res.Body).Decode(&cfg) +} + +func (c *Client) UpdateAppearance(ctx context.Context, appearance AppearanceConfig) error { + res, err := c.Request(ctx, http.MethodPut, "/api/v2/appearance", appearance) + if err != nil { + return err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return ReadBodyAsError(res) + } + return nil +} + +// BuildInfoResponse contains build information for this instance of Coder. +type BuildInfoResponse struct { + // ExternalURL references the current Coder version. + // For production builds, this will link directly to a release. For development builds, this will link to a commit. + ExternalURL string `json:"external_url"` + // Version returns the semantic version of the build. + Version string `json:"version"` +} + +// CanonicalVersion trims build information from the version. +// E.g. 'v0.7.4-devel+11573034' -> 'v0.7.4'. +func (b BuildInfoResponse) CanonicalVersion() string { + // We do a little hack here to massage the string into a form + // that works well with semver. + trimmed := strings.ReplaceAll(b.Version, "-devel+", "+devel-") + return semver.Canonical(trimmed) +} + +// BuildInfo returns build information for this instance of Coder. +func (c *Client) BuildInfo(ctx context.Context) (BuildInfoResponse, error) { + res, err := c.Request(ctx, http.MethodGet, "/api/v2/buildinfo", nil) + if err != nil { + return BuildInfoResponse{}, err + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return BuildInfoResponse{}, ReadBodyAsError(res) + } + + var buildInfo BuildInfoResponse + return buildInfo, json.NewDecoder(res.Body).Decode(&buildInfo) +} + +type Experiment string + +const ( + // ExperimentAuthzQuerier is an internal experiment that enables the ExperimentAuthzQuerier + // interface for all RBAC operations. NOT READY FOR PRODUCTION USE. + ExperimentAuthzQuerier Experiment = "authz_querier" + + // Add new experiments here! + // ExperimentExample Experiment = "example" +) + +var ( + // ExperimentsAll should include all experiments that are safe for + // users to opt-in to via --experimental='*'. + // Experiments that are not ready for consumption by all users should + // not be included here and will be essentially hidden. + ExperimentsAll = Experiments{} +) + +// Experiments is a list of experiments that are enabled for the deployment. +// Multiple experiments may be enabled at the same time. +// Experiments are not safe for production use, and are not guaranteed to +// be backwards compatible. They may be removed or renamed at any time. +type Experiments []Experiment + +func (e Experiments) Enabled(ex Experiment) bool { + for _, v := range e { + if v == ex { + return true + } + } + return false +} + +func (c *Client) Experiments(ctx context.Context) (Experiments, error) { + res, err := c.Request(ctx, http.MethodGet, "/api/v2/experiments", nil) + if err != nil { + return nil, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return nil, ReadBodyAsError(res) + } + var exp []Experiment + return exp, json.NewDecoder(res.Body).Decode(&exp) +} + +type DeploymentDAUsResponse struct { + Entries []DAUEntry `json:"entries"` +} + +func (c *Client) DeploymentDAUs(ctx context.Context) (*DeploymentDAUsResponse, error) { + res, err := c.Request(ctx, http.MethodGet, "/api/v2/insights/daus", nil) + if err != nil { + return nil, xerrors.Errorf("execute request: %w", err) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return nil, ReadBodyAsError(res) + } + + var resp DeploymentDAUsResponse + return &resp, json.NewDecoder(res.Body).Decode(&resp) +} + +type AppHostResponse struct { + // Host is the externally accessible URL for the Coder instance. + Host string `json:"host"` +} + +// AppHost returns the site-wide application wildcard hostname without the +// leading "*.", e.g. "apps.coder.com". Apps are accessible at: +// "------.", e.g. +// "my-app--agent--workspace--username.apps.coder.com". +// +// If the app host is not set, the response will contain an empty string. +func (c *Client) AppHost(ctx context.Context) (AppHostResponse, error) { + res, err := c.Request(ctx, http.MethodGet, "/api/v2/applications/host", nil) + if err != nil { + return AppHostResponse{}, err + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return AppHostResponse{}, ReadBodyAsError(res) + } + + var host AppHostResponse + return host, json.NewDecoder(res.Body).Decode(&host) +} diff --git a/codersdk/error.go b/codersdk/error.go deleted file mode 100644 index f215bac67e..0000000000 --- a/codersdk/error.go +++ /dev/null @@ -1,58 +0,0 @@ -package codersdk - -import ( - "fmt" - "net" - - "golang.org/x/xerrors" -) - -// Response represents a generic HTTP response. -type Response struct { - // Message is an actionable message that depicts actions the request took. - // These messages should be fully formed sentences with proper punctuation. - // Examples: - // - "A user has been created." - // - "Failed to create a user." - Message string `json:"message"` - // Detail is a debug message that provides further insight into why the - // action failed. This information can be technical and a regular golang - // err.Error() text. - // - "database: too many open connections" - // - "stat: too many open files" - Detail string `json:"detail,omitempty"` - // Validations are form field-specific friendly error messages. They will be - // shown on a form field in the UI. These can also be used to add additional - // context if there is a set of errors in the primary 'Message'. - Validations []ValidationError `json:"validations,omitempty"` -} - -// ValidationError represents a scoped error to a user input. -type ValidationError struct { - Field string `json:"field" validate:"required"` - Detail string `json:"detail" validate:"required"` -} - -func (e ValidationError) Error() string { - return fmt.Sprintf("field: %s detail: %s", e.Field, e.Detail) -} - -var _ error = (*ValidationError)(nil) - -// IsConnectionErr is a convenience function for checking if the source of an -// error is due to a 'connection refused', 'no such host', etc. -func IsConnectionErr(err error) bool { - var ( - // E.g. no such host - dnsErr *net.DNSError - // Eg. connection refused - opErr *net.OpError - ) - - return xerrors.As(err, &dnsErr) || xerrors.As(err, &opErr) -} - -func AsError(err error) (*Error, bool) { - var e *Error - return e, xerrors.As(err, &e) -} diff --git a/codersdk/error_test.go b/codersdk/error_test.go deleted file mode 100644 index d03024cbf1..0000000000 --- a/codersdk/error_test.go +++ /dev/null @@ -1,65 +0,0 @@ -package codersdk_test - -import ( - "net" - "os" - "testing" - - "github.com/stretchr/testify/require" - "golang.org/x/xerrors" - - "github.com/coder/coder/codersdk" -) - -func TestIsConnectionErr(t *testing.T) { - t.Parallel() - - type tc = struct { - name string - err error - expectedResult bool - } - - cases := []tc{ - { - // E.g. "no such host" - name: "DNSError", - err: &net.DNSError{ - Err: "no such host", - Name: "foofoo", - Server: "1.1.1.1:53", - IsTimeout: false, - IsTemporary: false, - IsNotFound: true, - }, - expectedResult: true, - }, - { - // E.g. "connection refused" - name: "OpErr", - err: &net.OpError{ - Op: "dial", - Net: "tcp", - Source: nil, - Addr: nil, - Err: &os.SyscallError{}, - }, - expectedResult: true, - }, - { - name: "OpaqueError", - err: xerrors.Errorf("I'm opaque!"), - expectedResult: false, - }, - } - - for _, c := range cases { - c := c - - t.Run(c.name, func(t *testing.T) { - t.Parallel() - - require.Equal(t, c.expectedResult, codersdk.IsConnectionErr(c.err)) - }) - } -} diff --git a/codersdk/experiments.go b/codersdk/experiments.go deleted file mode 100644 index 1412f375e3..0000000000 --- a/codersdk/experiments.go +++ /dev/null @@ -1,54 +0,0 @@ -package codersdk - -import ( - "context" - "encoding/json" - "net/http" -) - -type Experiment string - -const ( - // ExperimentAuthzQuerier is an internal experiment that enables the ExperimentAuthzQuerier - // interface for all RBAC operations. NOT READY FOR PRODUCTION USE. - ExperimentAuthzQuerier Experiment = "authz_querier" - - // Add new experiments here! - // ExperimentExample Experiment = "example" -) - -var ( - // ExperimentsAll should include all experiments that are safe for - // users to opt-in to via --experimental='*'. - // Experiments that are not ready for consumption by all users should - // not be included here and will be essentially hidden. - ExperimentsAll = Experiments{} -) - -// Experiments is a list of experiments that are enabled for the deployment. -// Multiple experiments may be enabled at the same time. -// Experiments are not safe for production use, and are not guaranteed to -// be backwards compatible. They may be removed or renamed at any time. -type Experiments []Experiment - -func (e Experiments) Enabled(ex Experiment) bool { - for _, v := range e { - if v == ex { - return true - } - } - return false -} - -func (c *Client) Experiments(ctx context.Context) (Experiments, error) { - res, err := c.Request(ctx, http.MethodGet, "/api/v2/experiments", nil) - if err != nil { - return nil, err - } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return nil, readBodyAsError(res) - } - var exp []Experiment - return exp, json.NewDecoder(res.Body).Decode(&exp) -} diff --git a/codersdk/features.go b/codersdk/features.go deleted file mode 100644 index 9ad04ea79a..0000000000 --- a/codersdk/features.go +++ /dev/null @@ -1,99 +0,0 @@ -package codersdk - -import ( - "context" - "encoding/json" - "net/http" - "strings" -) - -type Entitlement string - -const ( - EntitlementEntitled Entitlement = "entitled" - EntitlementGracePeriod Entitlement = "grace_period" - EntitlementNotEntitled Entitlement = "not_entitled" -) - -// To add a new feature, modify this set of enums as well as the FeatureNames -// array below. -type FeatureName string - -const ( - FeatureUserLimit FeatureName = "user_limit" - FeatureAuditLog FeatureName = "audit_log" - FeatureBrowserOnly FeatureName = "browser_only" - FeatureSCIM FeatureName = "scim" - FeatureTemplateRBAC FeatureName = "template_rbac" - FeatureHighAvailability FeatureName = "high_availability" - FeatureMultipleGitAuth FeatureName = "multiple_git_auth" - FeatureExternalProvisionerDaemons FeatureName = "external_provisioner_daemons" - FeatureAppearance FeatureName = "appearance" -) - -// FeatureNames must be kept in-sync with the Feature enum above. -var FeatureNames = []FeatureName{ - FeatureUserLimit, - FeatureAuditLog, - FeatureBrowserOnly, - FeatureSCIM, - FeatureTemplateRBAC, - FeatureHighAvailability, - FeatureMultipleGitAuth, - FeatureExternalProvisionerDaemons, - FeatureAppearance, -} - -// Humanize returns the feature name in a human-readable format. -func (n FeatureName) Humanize() string { - switch n { - case FeatureTemplateRBAC: - return "Template RBAC" - case FeatureSCIM: - return "SCIM" - default: - return strings.Title(strings.ReplaceAll(string(n), "_", " ")) - } -} - -// AlwaysEnable returns if the feature is always enabled if entitled. -// Warning: We don't know if we need this functionality. -// This method may disappear at any time. -func (n FeatureName) AlwaysEnable() bool { - return map[FeatureName]bool{ - FeatureMultipleGitAuth: true, - FeatureExternalProvisionerDaemons: true, - FeatureAppearance: true, - }[n] -} - -type Feature struct { - Entitlement Entitlement `json:"entitlement"` - Enabled bool `json:"enabled"` - Limit *int64 `json:"limit,omitempty"` - Actual *int64 `json:"actual,omitempty"` -} - -type Entitlements struct { - Features map[FeatureName]Feature `json:"features"` - Warnings []string `json:"warnings"` - Errors []string `json:"errors"` - HasLicense bool `json:"has_license"` - Trial bool `json:"trial"` - - // DEPRECATED: use Experiments instead. - Experimental bool `json:"experimental"` -} - -func (c *Client) Entitlements(ctx context.Context) (Entitlements, error) { - res, err := c.Request(ctx, http.MethodGet, "/api/v2/entitlements", nil) - if err != nil { - return Entitlements{}, err - } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return Entitlements{}, readBodyAsError(res) - } - var ent Entitlements - return ent, json.NewDecoder(res.Body).Decode(&ent) -} diff --git a/codersdk/files.go b/codersdk/files.go index 91e0a25c57..cc66132df9 100644 --- a/codersdk/files.go +++ b/codersdk/files.go @@ -30,7 +30,7 @@ func (c *Client) Upload(ctx context.Context, contentType string, content []byte) } defer res.Body.Close() if res.StatusCode != http.StatusCreated && res.StatusCode != http.StatusOK { - return UploadResponse{}, readBodyAsError(res) + return UploadResponse{}, ReadBodyAsError(res) } var resp UploadResponse return resp, json.NewDecoder(res.Body).Decode(&resp) @@ -44,7 +44,7 @@ func (c *Client) Download(ctx context.Context, id uuid.UUID) ([]byte, string, er } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return nil, "", readBodyAsError(res) + return nil, "", ReadBodyAsError(res) } data, err := io.ReadAll(res.Body) if err != nil { diff --git a/codersdk/gitsshkey.go b/codersdk/gitsshkey.go index 4a19110b5b..7b56e01427 100644 --- a/codersdk/gitsshkey.go +++ b/codersdk/gitsshkey.go @@ -18,11 +18,6 @@ type GitSSHKey struct { PublicKey string `json:"public_key"` } -type AgentGitSSHKey struct { - PublicKey string `json:"public_key"` - PrivateKey string `json:"private_key"` -} - // GitSSHKey returns the user's git SSH public key. func (c *Client) GitSSHKey(ctx context.Context, user string) (GitSSHKey, error) { res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/gitsshkey", user), nil) @@ -32,7 +27,7 @@ func (c *Client) GitSSHKey(ctx context.Context, user string) (GitSSHKey, error) defer res.Body.Close() if res.StatusCode != http.StatusOK { - return GitSSHKey{}, readBodyAsError(res) + return GitSSHKey{}, ReadBodyAsError(res) } var gitsshkey GitSSHKey @@ -48,25 +43,9 @@ func (c *Client) RegenerateGitSSHKey(ctx context.Context, user string) (GitSSHKe defer res.Body.Close() if res.StatusCode != http.StatusOK { - return GitSSHKey{}, readBodyAsError(res) + return GitSSHKey{}, ReadBodyAsError(res) } var gitsshkey GitSSHKey return gitsshkey, json.NewDecoder(res.Body).Decode(&gitsshkey) } - -// AgentGitSSHKey will return the user's SSH key pair for the workspace. -func (c *Client) AgentGitSSHKey(ctx context.Context) (AgentGitSSHKey, error) { - res, err := c.Request(ctx, http.MethodGet, "/api/v2/workspaceagents/me/gitsshkey", nil) - if err != nil { - return AgentGitSSHKey{}, xerrors.Errorf("execute request: %w", err) - } - defer res.Body.Close() - - if res.StatusCode != http.StatusOK { - return AgentGitSSHKey{}, readBodyAsError(res) - } - - var agentgitsshkey AgentGitSSHKey - return agentgitsshkey, json.NewDecoder(res.Body).Decode(&agentgitsshkey) -} diff --git a/codersdk/groups.go b/codersdk/groups.go index b0608f3530..3d9495eb07 100644 --- a/codersdk/groups.go +++ b/codersdk/groups.go @@ -36,7 +36,7 @@ func (c *Client) CreateGroup(ctx context.Context, orgID uuid.UUID, req CreateGro defer res.Body.Close() if res.StatusCode != http.StatusCreated { - return Group{}, readBodyAsError(res) + return Group{}, ReadBodyAsError(res) } var resp Group return resp, json.NewDecoder(res.Body).Decode(&resp) @@ -53,7 +53,7 @@ func (c *Client) GroupsByOrganization(ctx context.Context, orgID uuid.UUID) ([]G defer res.Body.Close() if res.StatusCode != http.StatusOK { - return nil, readBodyAsError(res) + return nil, ReadBodyAsError(res) } var groups []Group @@ -71,7 +71,7 @@ func (c *Client) GroupByOrgAndName(ctx context.Context, orgID uuid.UUID, name st defer res.Body.Close() if res.StatusCode != http.StatusOK { - return Group{}, readBodyAsError(res) + return Group{}, ReadBodyAsError(res) } var resp Group return resp, json.NewDecoder(res.Body).Decode(&resp) @@ -88,7 +88,7 @@ func (c *Client) Group(ctx context.Context, group uuid.UUID) (Group, error) { defer res.Body.Close() if res.StatusCode != http.StatusOK { - return Group{}, readBodyAsError(res) + return Group{}, ReadBodyAsError(res) } var resp Group return resp, json.NewDecoder(res.Body).Decode(&resp) @@ -113,7 +113,7 @@ func (c *Client) PatchGroup(ctx context.Context, group uuid.UUID, req PatchGroup defer res.Body.Close() if res.StatusCode != http.StatusOK { - return Group{}, readBodyAsError(res) + return Group{}, ReadBodyAsError(res) } var resp Group return resp, json.NewDecoder(res.Body).Decode(&resp) @@ -130,7 +130,7 @@ func (c *Client) DeleteGroup(ctx context.Context, group uuid.UUID) error { defer res.Body.Close() if res.StatusCode != http.StatusOK { - return readBodyAsError(res) + return ReadBodyAsError(res) } return nil } diff --git a/codersdk/insights.go b/codersdk/insights.go deleted file mode 100644 index 77e1a2e100..0000000000 --- a/codersdk/insights.go +++ /dev/null @@ -1,28 +0,0 @@ -package codersdk - -import ( - "context" - "encoding/json" - "net/http" - - "golang.org/x/xerrors" -) - -type DeploymentDAUsResponse struct { - Entries []DAUEntry `json:"entries"` -} - -func (c *Client) DeploymentDAUs(ctx context.Context) (*DeploymentDAUsResponse, error) { - res, err := c.Request(ctx, http.MethodGet, "/api/v2/insights/daus", nil) - if err != nil { - return nil, xerrors.Errorf("execute request: %w", err) - } - defer res.Body.Close() - - if res.StatusCode != http.StatusOK { - return nil, readBodyAsError(res) - } - - var resp DeploymentDAUsResponse - return &resp, json.NewDecoder(res.Body).Decode(&resp) -} diff --git a/codersdk/licenses.go b/codersdk/licenses.go index 59319a326e..388055fd86 100644 --- a/codersdk/licenses.go +++ b/codersdk/licenses.go @@ -57,7 +57,7 @@ func (c *Client) AddLicense(ctx context.Context, r AddLicenseRequest) (License, } defer res.Body.Close() if res.StatusCode != http.StatusCreated { - return License{}, readBodyAsError(res) + return License{}, ReadBodyAsError(res) } var l License d := json.NewDecoder(res.Body) @@ -72,7 +72,7 @@ func (c *Client) Licenses(ctx context.Context) ([]License, error) { } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return nil, readBodyAsError(res) + return nil, ReadBodyAsError(res) } var licenses []License d := json.NewDecoder(res.Body) @@ -87,7 +87,7 @@ func (c *Client) DeleteLicense(ctx context.Context, id int32) error { } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return readBodyAsError(res) + return ReadBodyAsError(res) } return nil } diff --git a/codersdk/organizationmember.go b/codersdk/organizationmember.go deleted file mode 100644 index 4ec90fb29c..0000000000 --- a/codersdk/organizationmember.go +++ /dev/null @@ -1,15 +0,0 @@ -package codersdk - -import ( - "time" - - "github.com/google/uuid" -) - -type OrganizationMember struct { - UserID uuid.UUID `db:"user_id" json:"user_id" format:"uuid"` - OrganizationID uuid.UUID `db:"organization_id" json:"organization_id" format:"uuid"` - CreatedAt time.Time `db:"created_at" json:"created_at" format:"date-time"` - UpdatedAt time.Time `db:"updated_at" json:"updated_at" format:"date-time"` - Roles []Role `db:"roles" json:"roles"` -} diff --git a/codersdk/organizations.go b/codersdk/organizations.go index 425f3d14d5..e4a07f6990 100644 --- a/codersdk/organizations.go +++ b/codersdk/organizations.go @@ -32,6 +32,14 @@ type Organization struct { UpdatedAt time.Time `json:"updated_at" validate:"required" format:"date-time"` } +type OrganizationMember struct { + UserID uuid.UUID `db:"user_id" json:"user_id" format:"uuid"` + OrganizationID uuid.UUID `db:"organization_id" json:"organization_id" format:"uuid"` + CreatedAt time.Time `db:"created_at" json:"created_at" format:"date-time"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at" format:"date-time"` + Roles []Role `db:"roles" json:"roles"` +} + // CreateTemplateVersionRequest enables callers to create a new Template Version. type CreateTemplateVersionRequest struct { Name string `json:"name,omitempty" validate:"omitempty,template_name"` @@ -99,7 +107,7 @@ func (c *Client) Organization(ctx context.Context, id uuid.UUID) (Organization, defer res.Body.Close() if res.StatusCode != http.StatusOK { - return Organization{}, readBodyAsError(res) + return Organization{}, ReadBodyAsError(res) } var organization Organization @@ -118,7 +126,7 @@ func (c *Client) ProvisionerDaemons(ctx context.Context) ([]ProvisionerDaemon, e defer res.Body.Close() if res.StatusCode != http.StatusOK { - return nil, readBodyAsError(res) + return nil, ReadBodyAsError(res) } var daemons []ProvisionerDaemon @@ -138,7 +146,7 @@ func (c *Client) CreateTemplateVersion(ctx context.Context, organizationID uuid. defer res.Body.Close() if res.StatusCode != http.StatusCreated { - return TemplateVersion{}, readBodyAsError(res) + return TemplateVersion{}, ReadBodyAsError(res) } var templateVersion TemplateVersion @@ -157,7 +165,7 @@ func (c *Client) TemplateVersionByOrganizationAndName(ctx context.Context, organ defer res.Body.Close() if res.StatusCode != http.StatusOK { - return TemplateVersion{}, readBodyAsError(res) + return TemplateVersion{}, ReadBodyAsError(res) } var templateVersion TemplateVersion @@ -176,7 +184,7 @@ func (c *Client) CreateTemplate(ctx context.Context, organizationID uuid.UUID, r defer res.Body.Close() if res.StatusCode != http.StatusCreated { - return Template{}, readBodyAsError(res) + return Template{}, ReadBodyAsError(res) } var template Template @@ -195,7 +203,7 @@ func (c *Client) TemplatesByOrganization(ctx context.Context, organizationID uui defer res.Body.Close() if res.StatusCode != http.StatusOK { - return nil, readBodyAsError(res) + return nil, ReadBodyAsError(res) } var templates []Template @@ -214,7 +222,7 @@ func (c *Client) TemplateByName(ctx context.Context, organizationID uuid.UUID, n defer res.Body.Close() if res.StatusCode != http.StatusOK { - return Template{}, readBodyAsError(res) + return Template{}, ReadBodyAsError(res) } var template Template @@ -230,7 +238,7 @@ func (c *Client) CreateWorkspace(ctx context.Context, organizationID uuid.UUID, defer res.Body.Close() if res.StatusCode != http.StatusCreated { - return Workspace{}, readBodyAsError(res) + return Workspace{}, ReadBodyAsError(res) } var workspace Workspace diff --git a/codersdk/parameters.go b/codersdk/parameters.go index 23b4408c2e..6dbab6cdb8 100644 --- a/codersdk/parameters.go +++ b/codersdk/parameters.go @@ -110,7 +110,7 @@ func (c *Client) CreateParameter(ctx context.Context, scope ParameterScope, id u defer res.Body.Close() if res.StatusCode != http.StatusCreated { - return Parameter{}, readBodyAsError(res) + return Parameter{}, ReadBodyAsError(res) } var param Parameter @@ -125,7 +125,7 @@ func (c *Client) DeleteParameter(ctx context.Context, scope ParameterScope, id u defer res.Body.Close() if res.StatusCode != http.StatusOK { - return readBodyAsError(res) + return ReadBodyAsError(res) } _, _ = io.Copy(io.Discard, res.Body) @@ -140,7 +140,7 @@ func (c *Client) Parameters(ctx context.Context, scope ParameterScope, id uuid.U defer res.Body.Close() if res.StatusCode != http.StatusOK { - return nil, readBodyAsError(res) + return nil, ReadBodyAsError(res) } var parameters []Parameter diff --git a/codersdk/provisionerdaemons.go b/codersdk/provisionerdaemons.go index c1b85345d0..2a7ed58bb0 100644 --- a/codersdk/provisionerdaemons.go +++ b/codersdk/provisionerdaemons.go @@ -104,7 +104,7 @@ func (c *Client) provisionerJobLogsBefore(ctx context.Context, path string, befo } if res.StatusCode != http.StatusOK { defer res.Body.Close() - return nil, readBodyAsError(res) + return nil, ReadBodyAsError(res) } var logs []ProvisionerJobLog @@ -126,7 +126,7 @@ func (c *Client) provisionerJobLogsAfter(ctx context.Context, path string, after return nil, nil, xerrors.Errorf("create cookie jar: %w", err) } jar.SetCookies(followURL, []*http.Cookie{{ - Name: SessionTokenKey, + Name: SessionTokenCookie, Value: c.SessionToken(), }}) httpClient := &http.Client{ @@ -140,7 +140,7 @@ func (c *Client) provisionerJobLogsAfter(ctx context.Context, path string, after if res == nil { return nil, nil, err } - return nil, nil, readBodyAsError(res) + return nil, nil, ReadBodyAsError(res) } logs := make(chan ProvisionerJobLog) decoder := json.NewDecoder(websocket.NetConn(ctx, conn, websocket.MessageText)) @@ -188,7 +188,7 @@ func (c *Client) ServeProvisionerDaemon(ctx context.Context, organization uuid.U return nil, xerrors.Errorf("create cookie jar: %w", err) } jar.SetCookies(serverURL, []*http.Cookie{{ - Name: SessionTokenKey, + Name: SessionTokenCookie, Value: c.SessionToken(), }}) httpClient := &http.Client{ @@ -203,7 +203,7 @@ func (c *Client) ServeProvisionerDaemon(ctx context.Context, organization uuid.U if res == nil { return nil, err } - return nil, readBodyAsError(res) + return nil, ReadBodyAsError(res) } // Align with the frame size of yamux. conn.SetReadLimit(256 * 1024) diff --git a/codersdk/quota.go b/codersdk/quota.go deleted file mode 100644 index 60ca4569ab..0000000000 --- a/codersdk/quota.go +++ /dev/null @@ -1,26 +0,0 @@ -package codersdk - -import ( - "context" - "encoding/json" - "fmt" - "net/http" -) - -type WorkspaceQuota struct { - CreditsConsumed int `json:"credits_consumed"` - Budget int `json:"budget"` -} - -func (c *Client) WorkspaceQuota(ctx context.Context, userID string) (WorkspaceQuota, error) { - res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspace-quota/%s", userID), nil) - if err != nil { - return WorkspaceQuota{}, err - } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return WorkspaceQuota{}, readBodyAsError(res) - } - var quota WorkspaceQuota - return quota, json.NewDecoder(res.Body).Decode("a) -} diff --git a/codersdk/replicas.go b/codersdk/replicas.go index 756ebcfcb7..7263aba91b 100644 --- a/codersdk/replicas.go +++ b/codersdk/replicas.go @@ -36,7 +36,7 @@ func (c *Client) Replicas(ctx context.Context) ([]Replica, error) { defer res.Body.Close() if res.StatusCode != http.StatusOK { - return nil, readBodyAsError(res) + return nil, ReadBodyAsError(res) } var replicas []Replica diff --git a/codersdk/roles.go b/codersdk/roles.go index 45f27f15bc..5ed9a92539 100644 --- a/codersdk/roles.go +++ b/codersdk/roles.go @@ -27,7 +27,7 @@ func (c *Client) ListSiteRoles(ctx context.Context) ([]AssignableRoles, error) { } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return nil, readBodyAsError(res) + return nil, ReadBodyAsError(res) } var roles []AssignableRoles return roles, json.NewDecoder(res.Body).Decode(&roles) @@ -41,7 +41,7 @@ func (c *Client) ListOrganizationRoles(ctx context.Context, org uuid.UUID) ([]As } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return nil, readBodyAsError(res) + return nil, ReadBodyAsError(res) } var roles []AssignableRoles return roles, json.NewDecoder(res.Body).Decode(&roles) diff --git a/codersdk/sse.go b/codersdk/serversentevents.go similarity index 100% rename from codersdk/sse.go rename to codersdk/serversentevents.go diff --git a/codersdk/templates.go b/codersdk/templates.go index 44471a234e..faa9615a78 100644 --- a/codersdk/templates.go +++ b/codersdk/templates.go @@ -100,7 +100,7 @@ func (c *Client) Template(ctx context.Context, template uuid.UUID) (Template, er } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return Template{}, readBodyAsError(res) + return Template{}, ReadBodyAsError(res) } var resp Template return resp, json.NewDecoder(res.Body).Decode(&resp) @@ -113,7 +113,7 @@ func (c *Client) DeleteTemplate(ctx context.Context, template uuid.UUID) error { } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return readBodyAsError(res) + return ReadBodyAsError(res) } return nil } @@ -128,7 +128,7 @@ func (c *Client) UpdateTemplateMeta(ctx context.Context, templateID uuid.UUID, r return Template{}, xerrors.New("template metadata not modified") } if res.StatusCode != http.StatusOK { - return Template{}, readBodyAsError(res) + return Template{}, ReadBodyAsError(res) } var updated Template return updated, json.NewDecoder(res.Body).Decode(&updated) @@ -141,7 +141,7 @@ func (c *Client) UpdateTemplateACL(ctx context.Context, templateID uuid.UUID, re } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return readBodyAsError(res) + return ReadBodyAsError(res) } return nil } @@ -153,7 +153,7 @@ func (c *Client) TemplateACL(ctx context.Context, templateID uuid.UUID) (Templat } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return TemplateACL{}, readBodyAsError(res) + return TemplateACL{}, ReadBodyAsError(res) } var acl TemplateACL return acl, json.NewDecoder(res.Body).Decode(&acl) @@ -168,7 +168,7 @@ func (c *Client) UpdateActiveTemplateVersion(ctx context.Context, template uuid. } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return readBodyAsError(res) + return ReadBodyAsError(res) } return nil } @@ -188,7 +188,7 @@ func (c *Client) TemplateVersionsByTemplate(ctx context.Context, req TemplateVer } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return nil, readBodyAsError(res) + return nil, ReadBodyAsError(res) } var templateVersion []TemplateVersion return templateVersion, json.NewDecoder(res.Body).Decode(&templateVersion) @@ -203,7 +203,7 @@ func (c *Client) TemplateVersionByName(ctx context.Context, template uuid.UUID, } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return TemplateVersion{}, readBodyAsError(res) + return TemplateVersion{}, ReadBodyAsError(res) } var templateVersion TemplateVersion return templateVersion, json.NewDecoder(res.Body).Decode(&templateVersion) @@ -227,7 +227,7 @@ func (c *Client) TemplateDAUs(ctx context.Context, templateID uuid.UUID) (*Templ defer res.Body.Close() if res.StatusCode != http.StatusOK { - return nil, readBodyAsError(res) + return nil, ReadBodyAsError(res) } var resp TemplateDAUsResponse @@ -258,7 +258,7 @@ func (c *Client) TemplateExamples(ctx context.Context, organizationID uuid.UUID) } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return nil, readBodyAsError(res) + return nil, ReadBodyAsError(res) } var templateExamples []TemplateExample return templateExamples, json.NewDecoder(res.Body).Decode(&templateExamples) diff --git a/codersdk/templateversions.go b/codersdk/templateversions.go index 442e85b4e7..f7b456779c 100644 --- a/codersdk/templateversions.go +++ b/codersdk/templateversions.go @@ -55,7 +55,7 @@ func (c *Client) TemplateVersion(ctx context.Context, id uuid.UUID) (TemplateVer } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return TemplateVersion{}, readBodyAsError(res) + return TemplateVersion{}, ReadBodyAsError(res) } var version TemplateVersion return version, json.NewDecoder(res.Body).Decode(&version) @@ -69,7 +69,7 @@ func (c *Client) CancelTemplateVersion(ctx context.Context, version uuid.UUID) e } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return readBodyAsError(res) + return ReadBodyAsError(res) } return nil } @@ -82,7 +82,7 @@ func (c *Client) TemplateVersionRichParameters(ctx context.Context, version uuid } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return nil, readBodyAsError(res) + return nil, ReadBodyAsError(res) } var params []TemplateVersionParameter return params, json.NewDecoder(res.Body).Decode(¶ms) @@ -96,7 +96,7 @@ func (c *Client) TemplateVersionSchema(ctx context.Context, version uuid.UUID) ( } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return nil, readBodyAsError(res) + return nil, ReadBodyAsError(res) } var params []ParameterSchema return params, json.NewDecoder(res.Body).Decode(¶ms) @@ -110,7 +110,7 @@ func (c *Client) TemplateVersionParameters(ctx context.Context, version uuid.UUI } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return nil, readBodyAsError(res) + return nil, ReadBodyAsError(res) } var params []ComputedParameter return params, json.NewDecoder(res.Body).Decode(¶ms) @@ -124,7 +124,7 @@ func (c *Client) TemplateVersionResources(ctx context.Context, version uuid.UUID } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return nil, readBodyAsError(res) + return nil, ReadBodyAsError(res) } var resources []WorkspaceResource return resources, json.NewDecoder(res.Body).Decode(&resources) @@ -157,7 +157,7 @@ func (c *Client) CreateTemplateVersionDryRun(ctx context.Context, version uuid.U } defer res.Body.Close() if res.StatusCode != http.StatusCreated { - return ProvisionerJob{}, readBodyAsError(res) + return ProvisionerJob{}, ReadBodyAsError(res) } var job ProvisionerJob @@ -173,7 +173,7 @@ func (c *Client) TemplateVersionDryRun(ctx context.Context, version, job uuid.UU } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return ProvisionerJob{}, readBodyAsError(res) + return ProvisionerJob{}, ReadBodyAsError(res) } var j ProvisionerJob @@ -189,7 +189,7 @@ func (c *Client) TemplateVersionDryRunResources(ctx context.Context, version, jo } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return nil, readBodyAsError(res) + return nil, ReadBodyAsError(res) } var resources []WorkspaceResource @@ -216,7 +216,7 @@ func (c *Client) CancelTemplateVersionDryRun(ctx context.Context, version, job u } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return readBodyAsError(res) + return ReadBodyAsError(res) } return nil } @@ -228,7 +228,7 @@ func (c *Client) PreviousTemplateVersion(ctx context.Context, organization uuid. } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return TemplateVersion{}, readBodyAsError(res) + return TemplateVersion{}, ReadBodyAsError(res) } var version TemplateVersion return version, json.NewDecoder(res.Body).Decode(&version) diff --git a/codersdk/updatecheck.go b/codersdk/updatecheck.go index 25f6bfc96e..a3652494d2 100644 --- a/codersdk/updatecheck.go +++ b/codersdk/updatecheck.go @@ -26,7 +26,7 @@ func (c *Client) UpdateCheck(ctx context.Context) (UpdateCheckResponse, error) { defer res.Body.Close() if res.StatusCode != http.StatusOK { - return UpdateCheckResponse{}, readBodyAsError(res) + return UpdateCheckResponse{}, ReadBodyAsError(res) } var buildInfo UpdateCheckResponse diff --git a/codersdk/users.go b/codersdk/users.go index b940b0bc48..23b75bff34 100644 --- a/codersdk/users.go +++ b/codersdk/users.go @@ -123,7 +123,7 @@ func (c *Client) HasFirstUser(ctx context.Context) (bool, error) { return false, nil } if res.StatusCode != http.StatusOK { - return false, readBodyAsError(res) + return false, ReadBodyAsError(res) } return true, nil } @@ -137,7 +137,7 @@ func (c *Client) CreateFirstUser(ctx context.Context, req CreateFirstUserRequest } defer res.Body.Close() if res.StatusCode != http.StatusCreated { - return CreateFirstUserResponse{}, readBodyAsError(res) + return CreateFirstUserResponse{}, ReadBodyAsError(res) } var resp CreateFirstUserResponse return resp, json.NewDecoder(res.Body).Decode(&resp) @@ -151,7 +151,7 @@ func (c *Client) CreateUser(ctx context.Context, req CreateUserRequest) (User, e } defer res.Body.Close() if res.StatusCode != http.StatusCreated { - return User{}, readBodyAsError(res) + return User{}, ReadBodyAsError(res) } var user User return user, json.NewDecoder(res.Body).Decode(&user) @@ -165,7 +165,7 @@ func (c *Client) DeleteUser(ctx context.Context, id uuid.UUID) error { } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return readBodyAsError(res) + return ReadBodyAsError(res) } return nil } @@ -178,7 +178,7 @@ func (c *Client) UpdateUserProfile(ctx context.Context, user string, req UpdateU } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return User{}, readBodyAsError(res) + return User{}, ReadBodyAsError(res) } var resp User return resp, json.NewDecoder(res.Body).Decode(&resp) @@ -202,7 +202,7 @@ func (c *Client) UpdateUserStatus(ctx context.Context, user string, status UserS } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return User{}, readBodyAsError(res) + return User{}, ReadBodyAsError(res) } var resp User @@ -218,7 +218,7 @@ func (c *Client) UpdateUserPassword(ctx context.Context, user string, req Update } defer res.Body.Close() if res.StatusCode != http.StatusNoContent { - return readBodyAsError(res) + return ReadBodyAsError(res) } return nil } @@ -232,7 +232,7 @@ func (c *Client) UpdateUserRoles(ctx context.Context, user string, req UpdateRol } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return User{}, readBodyAsError(res) + return User{}, ReadBodyAsError(res) } var resp User return resp, json.NewDecoder(res.Body).Decode(&resp) @@ -247,21 +247,21 @@ func (c *Client) UpdateOrganizationMemberRoles(ctx context.Context, organization } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return OrganizationMember{}, readBodyAsError(res) + return OrganizationMember{}, ReadBodyAsError(res) } var member OrganizationMember return member, json.NewDecoder(res.Body).Decode(&member) } -// GetUserRoles returns all roles the user has -func (c *Client) GetUserRoles(ctx context.Context, user string) (UserRoles, error) { +// UserRoles returns all roles the user has +func (c *Client) UserRoles(ctx context.Context, user string) (UserRoles, error) { res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/roles", user), nil) if err != nil { return UserRoles{}, err } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return UserRoles{}, readBodyAsError(res) + return UserRoles{}, ReadBodyAsError(res) } var roles UserRoles return roles, json.NewDecoder(res.Body).Decode(&roles) @@ -276,7 +276,7 @@ func (c *Client) LoginWithPassword(ctx context.Context, req LoginWithPasswordReq } defer res.Body.Close() if res.StatusCode != http.StatusCreated { - return LoginWithPasswordResponse{}, readBodyAsError(res) + return LoginWithPasswordResponse{}, ReadBodyAsError(res) } var resp LoginWithPasswordResponse err = json.NewDecoder(res.Body).Decode(&resp) @@ -307,7 +307,7 @@ func (c *Client) User(ctx context.Context, userIdent string) (User, error) { } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return User{}, readBodyAsError(res) + return User{}, ReadBodyAsError(res) } var user User return user, json.NewDecoder(res.Body).Decode(&user) @@ -343,7 +343,7 @@ func (c *Client) Users(ctx context.Context, req UsersRequest) (GetUsersResponse, defer res.Body.Close() if res.StatusCode != http.StatusOK { - return GetUsersResponse{}, readBodyAsError(res) + return GetUsersResponse{}, ReadBodyAsError(res) } var usersRes GetUsersResponse @@ -358,7 +358,7 @@ func (c *Client) OrganizationsByUser(ctx context.Context, user string) ([]Organi } defer res.Body.Close() if res.StatusCode > http.StatusOK { - return nil, readBodyAsError(res) + return nil, ReadBodyAsError(res) } var orgs []Organization return orgs, json.NewDecoder(res.Body).Decode(&orgs) @@ -371,7 +371,7 @@ func (c *Client) OrganizationByName(ctx context.Context, user string, name strin } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return Organization{}, readBodyAsError(res) + return Organization{}, ReadBodyAsError(res) } var org Organization return org, json.NewDecoder(res.Body).Decode(&org) @@ -386,7 +386,7 @@ func (c *Client) CreateOrganization(ctx context.Context, req CreateOrganizationR defer res.Body.Close() if res.StatusCode != http.StatusCreated { - return Organization{}, readBodyAsError(res) + return Organization{}, ReadBodyAsError(res) } var org Organization @@ -402,7 +402,7 @@ func (c *Client) AuthMethods(ctx context.Context) (AuthMethods, error) { defer res.Body.Close() if res.StatusCode != http.StatusOK { - return AuthMethods{}, readBodyAsError(res) + return AuthMethods{}, ReadBodyAsError(res) } var userAuth AuthMethods diff --git a/codersdk/agentconn.go b/codersdk/workspaceagentconn.go similarity index 54% rename from codersdk/agentconn.go rename to codersdk/workspaceagentconn.go index f5439bcdf4..438c4032fa 100644 --- a/codersdk/agentconn.go +++ b/codersdk/workspaceagentconn.go @@ -24,21 +24,21 @@ import ( ) var ( - // TailnetIP is a static IPv6 address with the Tailscale prefix that is used to route + // WorkspaceAgentIP is a static IPv6 address with the Tailscale prefix that is used to route // connections from clients to this node. A dynamic address is not required because a Tailnet // client only dials a single agent at a time. - TailnetIP = netip.MustParseAddr("fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4") + WorkspaceAgentIP = netip.MustParseAddr("fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4") ) const ( - TailnetSSHPort = 1 - TailnetReconnectingPTYPort = 2 - TailnetSpeedtestPort = 3 - // TailnetStatisticsPort serves a HTTP server with endpoints for gathering + WorkspaceAgentSSHPort = 1 + WorkspaceAgentReconnectingPTYPort = 2 + WorkspaceAgentSpeedtestPort = 3 + // WorkspaceAgentStatisticsPort serves a HTTP server with endpoints for gathering // agent statistics. - TailnetStatisticsPort = 4 + WorkspaceAgentStatisticsPort = 4 - // MinimumListeningPort is the minimum port that the listening-ports + // WorkspaceAgentMinimumListeningPort is the minimum port that the listening-ports // endpoint will return to the client, and the minimum port that is accepted // by the proxy applications endpoint. Coder consumes ports 1-4 at the // moment, and we reserve some extra ports for future use. Port 9 and up are @@ -47,15 +47,15 @@ const ( // This is not enforced in the CLI intentionally as we don't really care // *that* much. The user could bypass this in the CLI by using SSH instead // anyways. - MinimumListeningPort = 9 + WorkspaceAgentMinimumListeningPort = 9 ) -// IgnoredListeningPorts contains a list of ports in the global ignore list. -// This list contains common TCP ports that are not HTTP servers, such as -// databases, SSH, FTP, etc. +// WorkspaceAgentIgnoredListeningPorts contains a list of ports to ignore when looking for +// running applications inside a workspace. We want to ignore non-HTTP servers, +// so we pre-populate this list with common ports that are not HTTP servers. // // This is implemented as a map for fast lookup. -var IgnoredListeningPorts = map[uint16]struct{}{ +var WorkspaceAgentIgnoredListeningPorts = map[uint16]struct{}{ 0: {}, // Ports 1-8 are reserved for future use by the Coder agent. 1: {}, @@ -111,15 +111,57 @@ var IgnoredListeningPorts = map[uint16]struct{}{ } func init() { + if !strings.HasSuffix(os.Args[0], ".test") { + return + } // Add a thousand more ports to the ignore list during tests so it's easier // to find an available port. - if strings.HasSuffix(os.Args[0], ".test") { - for i := 63000; i < 64000; i++ { - IgnoredListeningPorts[uint16(i)] = struct{}{} - } + for i := 63000; i < 64000; i++ { + WorkspaceAgentIgnoredListeningPorts[uint16(i)] = struct{}{} } } +// WorkspaceAgentConn represents a connection to a workspace agent. +// @typescript-ignore WorkspaceAgentConn +type WorkspaceAgentConn struct { + *tailnet.Conn + CloseFunc func() +} + +// AwaitReachable waits for the agent to be reachable. +func (c *WorkspaceAgentConn) AwaitReachable(ctx context.Context) bool { + ctx, span := tracing.StartSpan(ctx) + defer span.End() + + return c.Conn.AwaitReachable(ctx, WorkspaceAgentIP) +} + +// Ping pings the agent and returns the round-trip time. +// The bool returns true if the ping was made P2P. +func (c *WorkspaceAgentConn) Ping(ctx context.Context) (time.Duration, bool, error) { + ctx, span := tracing.StartSpan(ctx) + defer span.End() + + return c.Conn.Ping(ctx, WorkspaceAgentIP) +} + +// Close ends the connection to the workspace agent. +func (c *WorkspaceAgentConn) Close() error { + if c.CloseFunc != nil { + c.CloseFunc() + } + return c.Conn.Close() +} + +// WorkspaceAgentReconnectingPTYInit initializes a new reconnecting PTY session. +// @typescript-ignore WorkspaceAgentReconnectingPTYInit +type WorkspaceAgentReconnectingPTYInit struct { + ID uuid.UUID + Height uint16 + Width uint16 + Command string +} + // ReconnectingPTYRequest is sent from the client to the server // to pipe data to a PTY. // @typescript-ignore ReconnectingPTYRequest @@ -129,56 +171,18 @@ type ReconnectingPTYRequest struct { Width uint16 `json:"width"` } -// @typescript-ignore AgentConn -type AgentConn struct { - *tailnet.Conn - CloseFunc func() -} - -func (c *AgentConn) AwaitReachable(ctx context.Context) bool { +// ReconnectingPTY spawns a new reconnecting terminal session. +// `ReconnectingPTYRequest` should be JSON marshaled and written to the returned net.Conn. +// Raw terminal output will be read from the returned net.Conn. +func (c *WorkspaceAgentConn) ReconnectingPTY(ctx context.Context, id uuid.UUID, height, width uint16, command string) (net.Conn, error) { ctx, span := tracing.StartSpan(ctx) defer span.End() - return c.Conn.AwaitReachable(ctx, TailnetIP) -} - -// Ping pings the agent and returns the round-trip time. -// The bool returns true if the ping was made P2P. -func (c *AgentConn) Ping(ctx context.Context) (time.Duration, bool, error) { - ctx, span := tracing.StartSpan(ctx) - defer span.End() - - return c.Conn.Ping(ctx, TailnetIP) -} - -func (c *AgentConn) CloseWithError(_ error) error { - return c.Close() -} - -func (c *AgentConn) Close() error { - if c.CloseFunc != nil { - c.CloseFunc() - } - return c.Conn.Close() -} - -// @typescript-ignore ReconnectingPTYInit -type ReconnectingPTYInit struct { - ID uuid.UUID - Height uint16 - Width uint16 - Command string -} - -func (c *AgentConn) ReconnectingPTY(ctx context.Context, id uuid.UUID, height, width uint16, command string) (net.Conn, error) { - ctx, span := tracing.StartSpan(ctx) - defer span.End() - - conn, err := c.DialContextTCP(ctx, netip.AddrPortFrom(TailnetIP, TailnetReconnectingPTYPort)) + conn, err := c.DialContextTCP(ctx, netip.AddrPortFrom(WorkspaceAgentIP, WorkspaceAgentReconnectingPTYPort)) if err != nil { return nil, err } - data, err := json.Marshal(ReconnectingPTYInit{ + data, err := json.Marshal(WorkspaceAgentReconnectingPTYInit{ ID: id, Height: height, Width: width, @@ -199,15 +203,17 @@ func (c *AgentConn) ReconnectingPTY(ctx context.Context, id uuid.UUID, height, w return conn, nil } -func (c *AgentConn) SSH(ctx context.Context) (net.Conn, error) { +// SSH pipes the SSH protocol over the returned net.Conn. +// This connects to the built-in SSH server in the workspace agent. +func (c *WorkspaceAgentConn) SSH(ctx context.Context) (net.Conn, error) { ctx, span := tracing.StartSpan(ctx) defer span.End() - return c.DialContextTCP(ctx, netip.AddrPortFrom(TailnetIP, TailnetSSHPort)) + return c.DialContextTCP(ctx, netip.AddrPortFrom(WorkspaceAgentIP, WorkspaceAgentSSHPort)) } // SSHClient calls SSH to create a client that uses a weak cipher -// for high throughput. -func (c *AgentConn) SSHClient(ctx context.Context) (*ssh.Client, error) { +// to improve throughput. +func (c *WorkspaceAgentConn) SSHClient(ctx context.Context) (*ssh.Client, error) { ctx, span := tracing.StartSpan(ctx) defer span.End() netConn, err := c.SSH(ctx) @@ -226,10 +232,11 @@ func (c *AgentConn) SSHClient(ctx context.Context) (*ssh.Client, error) { return ssh.NewClient(sshConn, channels, requests), nil } -func (c *AgentConn) Speedtest(ctx context.Context, direction speedtest.Direction, duration time.Duration) ([]speedtest.Result, error) { +// Speedtest runs a speedtest against the workspace agent. +func (c *WorkspaceAgentConn) Speedtest(ctx context.Context, direction speedtest.Direction, duration time.Duration) ([]speedtest.Result, error) { ctx, span := tracing.StartSpan(ctx) defer span.End() - speedConn, err := c.DialContextTCP(ctx, netip.AddrPortFrom(TailnetIP, TailnetSpeedtestPort)) + speedConn, err := c.DialContextTCP(ctx, netip.AddrPortFrom(WorkspaceAgentIP, WorkspaceAgentSpeedtestPort)) if err != nil { return nil, xerrors.Errorf("dial speedtest: %w", err) } @@ -240,7 +247,9 @@ func (c *AgentConn) Speedtest(ctx context.Context, direction speedtest.Direction return results, err } -func (c *AgentConn) DialContext(ctx context.Context, network string, addr string) (net.Conn, error) { +// DialContext dials the address provided in the workspace agent. +// The network must be "tcp" or "udp". +func (c *WorkspaceAgentConn) DialContext(ctx context.Context, network string, addr string) (net.Conn, error) { ctx, span := tracing.StartSpan(ctx) defer span.End() if network == "unix" { @@ -248,14 +257,62 @@ func (c *AgentConn) DialContext(ctx context.Context, network string, addr string } _, rawPort, _ := net.SplitHostPort(addr) port, _ := strconv.ParseUint(rawPort, 10, 16) - ipp := netip.AddrPortFrom(TailnetIP, uint16(port)) + ipp := netip.AddrPortFrom(WorkspaceAgentIP, uint16(port)) if network == "udp" { return c.Conn.DialContextUDP(ctx, ipp) } return c.Conn.DialContextTCP(ctx, ipp) } -func (c *AgentConn) statisticsClient() *http.Client { +type WorkspaceAgentListeningPortsResponse struct { + // If there are no ports in the list, nothing should be displayed in the UI. + // There must not be a "no ports available" message or anything similar, as + // there will always be no ports displayed on platforms where our port + // detection logic is unsupported. + Ports []WorkspaceAgentListeningPort `json:"ports"` +} + +type WorkspaceAgentListeningPort struct { + ProcessName string `json:"process_name"` // may be empty + Network string `json:"network"` // only "tcp" at the moment + Port uint16 `json:"port"` +} + +// ListeningPorts lists the ports that are currently in use by the workspace. +func (c *WorkspaceAgentConn) ListeningPorts(ctx context.Context) (WorkspaceAgentListeningPortsResponse, error) { + ctx, span := tracing.StartSpan(ctx) + defer span.End() + res, err := c.requestStatisticsServer(ctx, http.MethodGet, "/api/v0/listening-ports", nil) + if err != nil { + return WorkspaceAgentListeningPortsResponse{}, xerrors.Errorf("do request: %w", err) + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return WorkspaceAgentListeningPortsResponse{}, ReadBodyAsError(res) + } + + var resp WorkspaceAgentListeningPortsResponse + return resp, json.NewDecoder(res.Body).Decode(&resp) +} + +// requestStatisticsServer makes a request to the workspace agent's statistics server. +func (c *WorkspaceAgentConn) requestStatisticsServer(ctx context.Context, method, path string, body io.Reader) (*http.Response, error) { + ctx, span := tracing.StartSpan(ctx) + defer span.End() + host := net.JoinHostPort(WorkspaceAgentIP.String(), strconv.Itoa(WorkspaceAgentStatisticsPort)) + url := fmt.Sprintf("http://%s%s", host, path) + + req, err := http.NewRequestWithContext(ctx, method, url, body) + if err != nil { + return nil, xerrors.Errorf("new statistics server request to %q: %w", url, err) + } + + return c.statisticsServerClient().Do(req) +} + +// statisticsServerClient returns an HTTP client that can be used to make +// requests to the workspace agent's statistics server. +func (c *WorkspaceAgentConn) statisticsServerClient() *http.Client { return &http.Client{ Transport: &http.Transport{ // Disable keep alives as we're usually only making a single @@ -271,11 +328,11 @@ func (c *AgentConn) statisticsClient() *http.Client { } // Verify that host is TailnetIP and port is // TailnetStatisticsPort. - if host != TailnetIP.String() || port != strconv.Itoa(TailnetStatisticsPort) { + if host != WorkspaceAgentIP.String() || port != strconv.Itoa(WorkspaceAgentStatisticsPort) { return nil, xerrors.Errorf("request %q does not appear to be for statistics server", addr) } - conn, err := c.DialContextTCP(context.Background(), netip.AddrPortFrom(TailnetIP, TailnetStatisticsPort)) + conn, err := c.DialContextTCP(context.Background(), netip.AddrPortFrom(WorkspaceAgentIP, WorkspaceAgentStatisticsPort)) if err != nil { return nil, xerrors.Errorf("dial statistics: %w", err) } @@ -285,53 +342,3 @@ func (c *AgentConn) statisticsClient() *http.Client { }, } } - -func (c *AgentConn) doStatisticsRequest(ctx context.Context, method, path string, body io.Reader) (*http.Response, error) { - ctx, span := tracing.StartSpan(ctx) - defer span.End() - host := net.JoinHostPort(TailnetIP.String(), strconv.Itoa(TailnetStatisticsPort)) - url := fmt.Sprintf("http://%s%s", host, path) - - req, err := http.NewRequestWithContext(ctx, method, url, body) - if err != nil { - return nil, xerrors.Errorf("new statistics server request to %q: %w", url, err) - } - - return c.statisticsClient().Do(req) -} - -type ListeningPortsResponse struct { - // If there are no ports in the list, nothing should be displayed in the UI. - // There must not be a "no ports available" message or anything similar, as - // there will always be no ports displayed on platforms where our port - // detection logic is unsupported. - Ports []ListeningPort `json:"ports"` -} - -type ListeningPortNetwork string - -const ( - ListeningPortNetworkTCP ListeningPortNetwork = "tcp" -) - -type ListeningPort struct { - ProcessName string `json:"process_name"` // may be empty - Network ListeningPortNetwork `json:"network"` // only "tcp" at the moment - Port uint16 `json:"port"` -} - -func (c *AgentConn) ListeningPorts(ctx context.Context) (ListeningPortsResponse, error) { - ctx, span := tracing.StartSpan(ctx) - defer span.End() - res, err := c.doStatisticsRequest(ctx, http.MethodGet, "/api/v0/listening-ports", nil) - if err != nil { - return ListeningPortsResponse{}, xerrors.Errorf("do request: %w", err) - } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return ListeningPortsResponse{}, readBodyAsError(res) - } - - var resp ListeningPortsResponse - return resp, json.NewDecoder(res.Body).Decode(&resp) -} diff --git a/codersdk/workspaceagents.go b/codersdk/workspaceagents.go index 3d9a085b9f..244fb95eee 100644 --- a/codersdk/workspaceagents.go +++ b/codersdk/workspaceagents.go @@ -5,16 +5,13 @@ import ( "encoding/json" "errors" "fmt" - "io" "net" "net/http" "net/http/cookiejar" "net/netip" - "net/url" "strconv" "time" - "cloud.google.com/go/compute/metadata" "github.com/google/uuid" "golang.org/x/xerrors" "nhooyr.io/websocket" @@ -83,55 +80,11 @@ type WorkspaceAgent struct { StartupScriptTimeoutSeconds int32 `db:"startup_script_timeout_seconds" json:"startup_script_timeout_seconds"` } -type WorkspaceAgentResourceMetadata struct { - MemoryTotal uint64 `json:"memory_total"` - DiskTotal uint64 `json:"disk_total"` - CPUCores uint64 `json:"cpu_cores"` - CPUModel string `json:"cpu_model"` - CPUMhz float64 `json:"cpu_mhz"` -} - type DERPRegion struct { Preferred bool `json:"preferred"` LatencyMilliseconds float64 `json:"latency_ms"` } -type WorkspaceAgentInstanceMetadata struct { - JailOrchestrator string `json:"jail_orchestrator"` - OperatingSystem string `json:"operating_system"` - Platform string `json:"platform"` - PlatformFamily string `json:"platform_family"` - KernelVersion string `json:"kernel_version"` - KernelArchitecture string `json:"kernel_architecture"` - Cloud string `json:"cloud"` - Jail string `json:"jail"` - VNC bool `json:"vnc"` -} - -// @typescript-ignore GoogleInstanceIdentityToken -type GoogleInstanceIdentityToken struct { - JSONWebToken string `json:"json_web_token" validate:"required"` -} - -// @typescript-ignore AWSInstanceIdentityToken -type AWSInstanceIdentityToken struct { - Signature string `json:"signature" validate:"required"` - Document string `json:"document" validate:"required"` -} - -// @typescript-ignore ReconnectingPTYRequest -type AzureInstanceIdentityToken struct { - Signature string `json:"signature" validate:"required"` - Encoding string `json:"encoding" validate:"required"` -} - -// WorkspaceAgentAuthenticateResponse is returned when an instance ID -// has been exchanged for a session token. -// @typescript-ignore WorkspaceAgentAuthenticateResponse -type WorkspaceAgentAuthenticateResponse struct { - SessionToken string `json:"session_token"` -} - // WorkspaceAgentConnectionInfo returns required information for establishing // a connection with a workspace. // @typescript-ignore WorkspaceAgentConnectionInfo @@ -139,272 +92,6 @@ type WorkspaceAgentConnectionInfo struct { DERPMap *tailcfg.DERPMap `json:"derp_map"` } -// @typescript-ignore PostWorkspaceAgentVersionRequest -// @Description x-apidocgen:skip -type PostWorkspaceAgentVersionRequest struct { - Version string `json:"version"` -} - -// @typescript-ignore WorkspaceAgentMetadata -type WorkspaceAgentMetadata struct { - // GitAuthConfigs stores the number of Git configurations - // the Coder deployment has. If this number is >0, we - // set up special configuration in the workspace. - GitAuthConfigs int `json:"git_auth_configs"` - VSCodePortProxyURI string `json:"vscode_port_proxy_uri"` - Apps []WorkspaceApp `json:"apps"` - DERPMap *tailcfg.DERPMap `json:"derpmap"` - EnvironmentVariables map[string]string `json:"environment_variables"` - StartupScript string `json:"startup_script"` - StartupScriptTimeout time.Duration `json:"startup_script_timeout"` - Directory string `json:"directory"` - MOTDFile string `json:"motd_file"` -} - -// AuthWorkspaceGoogleInstanceIdentity uses the Google Compute Engine Metadata API to -// fetch a signed JWT, and exchange it for a session token for a workspace agent. -// -// The requesting instance must be registered as a resource in the latest history for a workspace. -func (c *Client) AuthWorkspaceGoogleInstanceIdentity(ctx context.Context, serviceAccount string, gcpClient *metadata.Client) (WorkspaceAgentAuthenticateResponse, error) { - if serviceAccount == "" { - // This is the default name specified by Google. - serviceAccount = "default" - } - if gcpClient == nil { - gcpClient = metadata.NewClient(c.HTTPClient) - } - // "format=full" is required, otherwise the responding payload will be missing "instance_id". - jwt, err := gcpClient.Get(fmt.Sprintf("instance/service-accounts/%s/identity?audience=coder&format=full", serviceAccount)) - if err != nil { - return WorkspaceAgentAuthenticateResponse{}, xerrors.Errorf("get metadata identity: %w", err) - } - res, err := c.Request(ctx, http.MethodPost, "/api/v2/workspaceagents/google-instance-identity", GoogleInstanceIdentityToken{ - JSONWebToken: jwt, - }) - if err != nil { - return WorkspaceAgentAuthenticateResponse{}, err - } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return WorkspaceAgentAuthenticateResponse{}, readBodyAsError(res) - } - var resp WorkspaceAgentAuthenticateResponse - return resp, json.NewDecoder(res.Body).Decode(&resp) -} - -// AuthWorkspaceAWSInstanceIdentity uses the Amazon Metadata API to -// fetch a signed payload, and exchange it for a session token for a workspace agent. -// -// The requesting instance must be registered as a resource in the latest history for a workspace. -func (c *Client) AuthWorkspaceAWSInstanceIdentity(ctx context.Context) (WorkspaceAgentAuthenticateResponse, error) { - req, err := http.NewRequestWithContext(ctx, http.MethodPut, "http://169.254.169.254/latest/api/token", nil) - if err != nil { - return WorkspaceAgentAuthenticateResponse{}, nil - } - req.Header.Set("X-aws-ec2-metadata-token-ttl-seconds", "21600") - res, err := c.HTTPClient.Do(req) - if err != nil { - return WorkspaceAgentAuthenticateResponse{}, err - } - defer res.Body.Close() - token, err := io.ReadAll(res.Body) - if err != nil { - return WorkspaceAgentAuthenticateResponse{}, xerrors.Errorf("read token: %w", err) - } - - req, err = http.NewRequestWithContext(ctx, http.MethodGet, "http://169.254.169.254/latest/dynamic/instance-identity/signature", nil) - if err != nil { - return WorkspaceAgentAuthenticateResponse{}, nil - } - req.Header.Set("X-aws-ec2-metadata-token", string(token)) - res, err = c.HTTPClient.Do(req) - if err != nil { - return WorkspaceAgentAuthenticateResponse{}, err - } - defer res.Body.Close() - signature, err := io.ReadAll(res.Body) - if err != nil { - return WorkspaceAgentAuthenticateResponse{}, xerrors.Errorf("read token: %w", err) - } - - req, err = http.NewRequestWithContext(ctx, http.MethodGet, "http://169.254.169.254/latest/dynamic/instance-identity/document", nil) - if err != nil { - return WorkspaceAgentAuthenticateResponse{}, nil - } - req.Header.Set("X-aws-ec2-metadata-token", string(token)) - res, err = c.HTTPClient.Do(req) - if err != nil { - return WorkspaceAgentAuthenticateResponse{}, err - } - defer res.Body.Close() - document, err := io.ReadAll(res.Body) - if err != nil { - return WorkspaceAgentAuthenticateResponse{}, xerrors.Errorf("read token: %w", err) - } - - res, err = c.Request(ctx, http.MethodPost, "/api/v2/workspaceagents/aws-instance-identity", AWSInstanceIdentityToken{ - Signature: string(signature), - Document: string(document), - }) - if err != nil { - return WorkspaceAgentAuthenticateResponse{}, err - } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return WorkspaceAgentAuthenticateResponse{}, readBodyAsError(res) - } - var resp WorkspaceAgentAuthenticateResponse - return resp, json.NewDecoder(res.Body).Decode(&resp) -} - -// AuthWorkspaceAzureInstanceIdentity uses the Azure Instance Metadata Service to -// fetch a signed payload, and exchange it for a session token for a workspace agent. -func (c *Client) AuthWorkspaceAzureInstanceIdentity(ctx context.Context) (WorkspaceAgentAuthenticateResponse, error) { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://169.254.169.254/metadata/attested/document?api-version=2020-09-01", nil) - if err != nil { - return WorkspaceAgentAuthenticateResponse{}, nil - } - req.Header.Set("Metadata", "true") - res, err := c.HTTPClient.Do(req) - if err != nil { - return WorkspaceAgentAuthenticateResponse{}, err - } - defer res.Body.Close() - - var token AzureInstanceIdentityToken - err = json.NewDecoder(res.Body).Decode(&token) - if err != nil { - return WorkspaceAgentAuthenticateResponse{}, err - } - - res, err = c.Request(ctx, http.MethodPost, "/api/v2/workspaceagents/azure-instance-identity", token) - if err != nil { - return WorkspaceAgentAuthenticateResponse{}, err - } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return WorkspaceAgentAuthenticateResponse{}, readBodyAsError(res) - } - var resp WorkspaceAgentAuthenticateResponse - return resp, json.NewDecoder(res.Body).Decode(&resp) -} - -// WorkspaceAgentMetadata fetches metadata for the currently authenticated workspace agent. -func (c *Client) WorkspaceAgentMetadata(ctx context.Context) (WorkspaceAgentMetadata, error) { - res, err := c.Request(ctx, http.MethodGet, "/api/v2/workspaceagents/me/metadata", nil) - if err != nil { - return WorkspaceAgentMetadata{}, err - } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return WorkspaceAgentMetadata{}, readBodyAsError(res) - } - var agentMetadata WorkspaceAgentMetadata - err = json.NewDecoder(res.Body).Decode(&agentMetadata) - if err != nil { - return WorkspaceAgentMetadata{}, err - } - accessingPort := c.URL.Port() - if accessingPort == "" { - accessingPort = "80" - if c.URL.Scheme == "https" { - accessingPort = "443" - } - } - accessPort, err := strconv.Atoi(accessingPort) - if err != nil { - return WorkspaceAgentMetadata{}, xerrors.Errorf("convert accessing port %q: %w", accessingPort, err) - } - // Agents can provide an arbitrary access URL that may be different - // that the globally configured one. This breaks the built-in DERP, - // which would continue to reference the global access URL. - // - // This converts all built-in DERPs to use the access URL that the - // metadata request was performed with. - for _, region := range agentMetadata.DERPMap.Regions { - if !region.EmbeddedRelay { - continue - } - - for _, node := range region.Nodes { - if node.STUNOnly { - continue - } - node.HostName = c.URL.Hostname() - node.DERPPort = accessPort - node.ForceHTTP = c.URL.Scheme == "http" - } - } - return agentMetadata, nil -} - -func (c *Client) ListenWorkspaceAgent(ctx context.Context) (net.Conn, error) { - coordinateURL, err := c.URL.Parse("/api/v2/workspaceagents/me/coordinate") - if err != nil { - return nil, xerrors.Errorf("parse url: %w", err) - } - jar, err := cookiejar.New(nil) - if err != nil { - return nil, xerrors.Errorf("create cookie jar: %w", err) - } - jar.SetCookies(coordinateURL, []*http.Cookie{{ - Name: SessionTokenKey, - Value: c.SessionToken(), - }}) - httpClient := &http.Client{ - Jar: jar, - Transport: c.HTTPClient.Transport, - } - // nolint:bodyclose - conn, res, err := websocket.Dial(ctx, coordinateURL.String(), &websocket.DialOptions{ - HTTPClient: httpClient, - }) - if err != nil { - if res == nil { - return nil, err - } - return nil, readBodyAsError(res) - } - - // Ping once every 30 seconds to ensure that the websocket is alive. If we - // don't get a response within 30s we kill the websocket and reconnect. - // See: https://github.com/coder/coder/pull/5824 - go func() { - tick := 30 * time.Second - ticker := time.NewTicker(tick) - defer ticker.Stop() - defer func() { - c.Logger.Debug(ctx, "coordinate pinger exited") - }() - for { - select { - case <-ctx.Done(): - return - case start := <-ticker.C: - ctx, cancel := context.WithTimeout(ctx, tick) - - err := conn.Ping(ctx) - if err != nil { - c.Logger.Error(ctx, "workspace agent coordinate ping", slog.Error(err)) - - err := conn.Close(websocket.StatusGoingAway, "Ping failed") - if err != nil { - c.Logger.Error(ctx, "close workspace agent coordinate websocket", slog.Error(err)) - } - - cancel() - return - } - - c.Logger.Debug(ctx, "got coordinate pong", slog.F("took", time.Since(start))) - cancel() - } - } - }() - - return websocket.NetConn(ctx, conn, websocket.MessageBinary), nil -} - // @typescript-ignore DialWorkspaceAgentOptions type DialWorkspaceAgentOptions struct { Logger slog.Logger @@ -413,7 +100,7 @@ type DialWorkspaceAgentOptions struct { EnableTrafficStats bool } -func (c *Client) DialWorkspaceAgent(ctx context.Context, agentID uuid.UUID, options *DialWorkspaceAgentOptions) (*AgentConn, error) { +func (c *Client) DialWorkspaceAgent(ctx context.Context, agentID uuid.UUID, options *DialWorkspaceAgentOptions) (*WorkspaceAgentConn, error) { if options == nil { options = &DialWorkspaceAgentOptions{} } @@ -423,7 +110,7 @@ func (c *Client) DialWorkspaceAgent(ctx context.Context, agentID uuid.UUID, opti } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return nil, readBodyAsError(res) + return nil, ReadBodyAsError(res) } var connInfo WorkspaceAgentConnectionInfo err = json.NewDecoder(res.Body).Decode(&connInfo) @@ -452,7 +139,7 @@ func (c *Client) DialWorkspaceAgent(ctx context.Context, agentID uuid.UUID, opti return nil, xerrors.Errorf("create cookie jar: %w", err) } jar.SetCookies(coordinateURL, []*http.Cookie{{ - Name: SessionTokenKey, + Name: SessionTokenCookie, Value: c.SessionToken(), }}) httpClient := &http.Client{ @@ -475,7 +162,7 @@ func (c *Client) DialWorkspaceAgent(ctx context.Context, agentID uuid.UUID, opti }) if isFirst { if res != nil && res.StatusCode == http.StatusConflict { - first <- readBodyAsError(res) + first <- ReadBodyAsError(res) return } isFirst = false @@ -513,7 +200,7 @@ func (c *Client) DialWorkspaceAgent(ctx context.Context, agentID uuid.UUID, opti return nil, err } - return &AgentConn{ + return &WorkspaceAgentConn{ Conn: conn, CloseFunc: func() { cancelFunc() @@ -530,39 +217,12 @@ func (c *Client) WorkspaceAgent(ctx context.Context, id uuid.UUID) (WorkspaceAge } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return WorkspaceAgent{}, readBodyAsError(res) + return WorkspaceAgent{}, ReadBodyAsError(res) } var workspaceAgent WorkspaceAgent return workspaceAgent, json.NewDecoder(res.Body).Decode(&workspaceAgent) } -// PostWorkspaceAgentAppHealth updates the workspace agent app health status. -func (c *Client) PostWorkspaceAgentAppHealth(ctx context.Context, req PostWorkspaceAppHealthsRequest) error { - res, err := c.Request(ctx, http.MethodPost, "/api/v2/workspaceagents/me/app-health", req) - if err != nil { - return err - } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return readBodyAsError(res) - } - - return nil -} - -func (c *Client) PostWorkspaceAgentVersion(ctx context.Context, version string) error { - versionReq := PostWorkspaceAgentVersionRequest{Version: version} - res, err := c.Request(ctx, http.MethodPost, "/api/v2/workspaceagents/me/version", versionReq) - if err != nil { - return err - } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return readBodyAsError(res) - } - return nil -} - // WorkspaceAgentReconnectingPTY spawns a PTY that reconnects using the token provided. // It communicates using `agent.ReconnectingPTYRequest` marshaled as JSON. // Responses are PTY output that can be rendered. @@ -583,7 +243,7 @@ func (c *Client) WorkspaceAgentReconnectingPTY(ctx context.Context, agentID, rec return nil, xerrors.Errorf("create cookie jar: %w", err) } jar.SetCookies(serverURL, []*http.Cookie{{ - Name: SessionTokenKey, + Name: SessionTokenCookie, Value: c.SessionToken(), }}) httpClient := &http.Client{ @@ -596,114 +256,26 @@ func (c *Client) WorkspaceAgentReconnectingPTY(ctx context.Context, agentID, rec if res == nil { return nil, err } - return nil, readBodyAsError(res) + return nil, ReadBodyAsError(res) } return websocket.NetConn(ctx, conn, websocket.MessageBinary), nil } // WorkspaceAgentListeningPorts returns a list of ports that are currently being // listened on inside the workspace agent's network namespace. -func (c *Client) WorkspaceAgentListeningPorts(ctx context.Context, agentID uuid.UUID) (ListeningPortsResponse, error) { +func (c *Client) WorkspaceAgentListeningPorts(ctx context.Context, agentID uuid.UUID) (WorkspaceAgentListeningPortsResponse, error) { res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaceagents/%s/listening-ports", agentID), nil) if err != nil { - return ListeningPortsResponse{}, err + return WorkspaceAgentListeningPortsResponse{}, err } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return ListeningPortsResponse{}, readBodyAsError(res) + return WorkspaceAgentListeningPortsResponse{}, ReadBodyAsError(res) } - var listeningPorts ListeningPortsResponse + var listeningPorts WorkspaceAgentListeningPortsResponse return listeningPorts, json.NewDecoder(res.Body).Decode(&listeningPorts) } -// Stats records the Agent's network connection statistics for use in -// user-facing metrics and debugging. -// @typescript-ignore AgentStats -type AgentStats struct { - // ConnsByProto is a count of connections by protocol. - ConnsByProto map[string]int64 `json:"conns_by_proto"` - // NumConns is the number of connections received by an agent. - NumConns int64 `json:"num_comms"` - // RxPackets is the number of received packets. - RxPackets int64 `json:"rx_packets"` - // RxBytes is the number of received bytes. - RxBytes int64 `json:"rx_bytes"` - // TxPackets is the number of transmitted bytes. - TxPackets int64 `json:"tx_packets"` - // TxBytes is the number of transmitted bytes. - TxBytes int64 `json:"tx_bytes"` -} - -// @typescript-ignore AgentStatsResponse -type AgentStatsResponse struct { - // ReportInterval is the duration after which the agent should send stats - // again. - ReportInterval time.Duration `json:"report_interval"` -} - -func (c *Client) PostAgentStats(ctx context.Context, stats *AgentStats) (AgentStatsResponse, error) { - res, err := c.Request(ctx, http.MethodPost, "/api/v2/workspaceagents/me/report-stats", stats) - if err != nil { - return AgentStatsResponse{}, xerrors.Errorf("send request: %w", err) - } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return AgentStatsResponse{}, readBodyAsError(res) - } - - var interval AgentStatsResponse - err = json.NewDecoder(res.Body).Decode(&interval) - if err != nil { - return AgentStatsResponse{}, xerrors.Errorf("decode stats response: %w", err) - } - - return interval, nil -} - -// AgentReportStats begins a stat streaming connection with the Coder server. -// It is resilient to network failures and intermittent coderd issues. -func (c *Client) AgentReportStats( - ctx context.Context, - log slog.Logger, - getStats func() *AgentStats, -) (io.Closer, error) { - ctx, cancel := context.WithCancel(ctx) - - go func() { - // Immediately trigger a stats push to get the correct interval. - timer := time.NewTimer(time.Nanosecond) - defer timer.Stop() - - for { - select { - case <-ctx.Done(): - return - case <-timer.C: - } - - var nextInterval time.Duration - for r := retry.New(100*time.Millisecond, time.Minute); r.Wait(ctx); { - resp, err := c.PostAgentStats(ctx, getStats()) - if err != nil { - if !xerrors.Is(err, context.Canceled) { - log.Error(ctx, "report stats", slog.Error(err)) - } - continue - } - - nextInterval = resp.ReportInterval - break - } - timer.Reset(nextInterval) - } - }() - - return closeFunc(func() error { - cancel() - return nil - }), nil -} - // GitProvider is a constant that represents the // type of providers that are supported within Coder. // @typescript-ignore GitProvider @@ -715,49 +287,3 @@ const ( GitProviderGitLab = "gitlab" GitProviderBitBucket = "bitbucket" ) - -type WorkspaceAgentGitAuthResponse struct { - Username string `json:"username"` - Password string `json:"password"` - URL string `json:"url"` -} - -// WorkspaceAgentGitAuth submits a URL to fetch a GIT_ASKPASS username -// and password for. -// nolint:revive -func (c *Client) WorkspaceAgentGitAuth(ctx context.Context, gitURL string, listen bool) (WorkspaceAgentGitAuthResponse, error) { - reqURL := "/api/v2/workspaceagents/me/gitauth?url=" + url.QueryEscape(gitURL) - if listen { - reqURL += "&listen" - } - res, err := c.Request(ctx, http.MethodGet, reqURL, nil) - if err != nil { - return WorkspaceAgentGitAuthResponse{}, xerrors.Errorf("execute request: %w", err) - } - defer res.Body.Close() - - if res.StatusCode != http.StatusOK { - return WorkspaceAgentGitAuthResponse{}, readBodyAsError(res) - } - - var authResp WorkspaceAgentGitAuthResponse - return authResp, json.NewDecoder(res.Body).Decode(&authResp) -} - -// @typescript-ignore PostWorkspaceAgentLifecycleRequest -type PostWorkspaceAgentLifecycleRequest struct { - State WorkspaceAgentLifecycle `json:"state"` -} - -func (c *Client) PostWorkspaceAgentLifecycle(ctx context.Context, req PostWorkspaceAgentLifecycleRequest) error { - res, err := c.Request(ctx, http.MethodPost, "/api/v2/workspaceagents/me/report-lifecycle", req) - if err != nil { - return xerrors.Errorf("agent state post request: %w", err) - } - defer res.Body.Close() - if res.StatusCode != http.StatusNoContent { - return readBodyAsError(res) - } - - return nil -} diff --git a/codersdk/workspaceagents_test.go b/codersdk/workspaceagents_test.go index e94181f12c..869e22f3d5 100644 --- a/codersdk/workspaceagents_test.go +++ b/codersdk/workspaceagents_test.go @@ -15,7 +15,7 @@ import ( "cdr.dev/slog/sloggers/slogtest" "github.com/coder/coder/coderd/httpapi" - "github.com/coder/coder/codersdk" + "github.com/coder/coder/codersdk/agentsdk" "github.com/coder/coder/testutil" ) @@ -24,7 +24,7 @@ func TestWorkspaceAgentMetadata(t *testing.T) { // This test ensures that the DERP map returned properly // mutates built-in DERPs with the client access URL. srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - httpapi.Write(context.Background(), w, http.StatusOK, codersdk.WorkspaceAgentMetadata{ + httpapi.Write(context.Background(), w, http.StatusOK, agentsdk.Metadata{ DERPMap: &tailcfg.DERPMap{ Regions: map[int]*tailcfg.DERPRegion{ 1: { @@ -41,8 +41,8 @@ func TestWorkspaceAgentMetadata(t *testing.T) { })) parsed, err := url.Parse(srv.URL) require.NoError(t, err) - client := codersdk.New(parsed) - metadata, err := client.WorkspaceAgentMetadata(context.Background()) + client := agentsdk.New(parsed) + metadata, err := client.Metadata(context.Background()) require.NoError(t, err) region := metadata.DERPMap.Regions[1] require.True(t, region.EmbeddedRelay) @@ -58,17 +58,17 @@ func TestAgentReportStats(t *testing.T) { var numReports atomic.Int64 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { numReports.Add(1) - httpapi.Write(context.Background(), w, http.StatusOK, codersdk.AgentStatsResponse{ + httpapi.Write(context.Background(), w, http.StatusOK, agentsdk.StatsResponse{ ReportInterval: 5 * time.Millisecond, }) })) parsed, err := url.Parse(srv.URL) require.NoError(t, err) - client := codersdk.New(parsed) + client := agentsdk.New(parsed) ctx := context.Background() - closeStream, err := client.AgentReportStats(ctx, slogtest.Make(t, nil), func() *codersdk.AgentStats { - return &codersdk.AgentStats{} + closeStream, err := client.ReportStats(ctx, slogtest.Make(t, nil), func() *agentsdk.Stats { + return &agentsdk.Stats{} }) require.NoError(t, err) defer closeStream.Close() diff --git a/codersdk/workspaceapps.go b/codersdk/workspaceapps.go index a8bcb31b79..ba2e725588 100644 --- a/codersdk/workspaceapps.go +++ b/codersdk/workspaceapps.go @@ -56,9 +56,3 @@ type Healthcheck struct { // Threshold specifies the number of consecutive failed health checks before returning "unhealthy". Threshold int32 `json:"threshold"` } - -// @typescript-ignore PostWorkspaceAppHealthsRequest -type PostWorkspaceAppHealthsRequest struct { - // Healths is a map of the workspace app name and the health of the app. - Healths map[uuid.UUID]WorkspaceAppHealth -} diff --git a/codersdk/workspacebuilds.go b/codersdk/workspacebuilds.go index ffa875a2cc..ab43379c28 100644 --- a/codersdk/workspacebuilds.go +++ b/codersdk/workspacebuilds.go @@ -110,7 +110,7 @@ func (c *Client) WorkspaceBuild(ctx context.Context, id uuid.UUID) (WorkspaceBui } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return WorkspaceBuild{}, readBodyAsError(res) + return WorkspaceBuild{}, ReadBodyAsError(res) } var workspaceBuild WorkspaceBuild return workspaceBuild, json.NewDecoder(res.Body).Decode(&workspaceBuild) @@ -124,7 +124,7 @@ func (c *Client) CancelWorkspaceBuild(ctx context.Context, id uuid.UUID) error { } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return readBodyAsError(res) + return ReadBodyAsError(res) } return nil } @@ -147,7 +147,7 @@ func (c *Client) WorkspaceBuildState(ctx context.Context, build uuid.UUID) ([]by } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return nil, readBodyAsError(res) + return nil, ReadBodyAsError(res) } return io.ReadAll(res.Body) } @@ -159,7 +159,7 @@ func (c *Client) WorkspaceBuildByUsernameAndWorkspaceNameAndBuildNumber(ctx cont } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return WorkspaceBuild{}, readBodyAsError(res) + return WorkspaceBuild{}, ReadBodyAsError(res) } var workspaceBuild WorkspaceBuild return workspaceBuild, json.NewDecoder(res.Body).Decode(&workspaceBuild) @@ -172,7 +172,7 @@ func (c *Client) WorkspaceBuildParameters(ctx context.Context, build uuid.UUID) } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return nil, readBodyAsError(res) + return nil, ReadBodyAsError(res) } var params []WorkspaceBuildParameter return params, json.NewDecoder(res.Body).Decode(¶ms) diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index 45e73aaa55..c41fb91f97 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -96,7 +96,7 @@ func (c *Client) getWorkspace(ctx context.Context, id uuid.UUID, opts ...Request } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return Workspace{}, readBodyAsError(res) + return Workspace{}, ReadBodyAsError(res) } var workspace Workspace return workspace, json.NewDecoder(res.Body).Decode(&workspace) @@ -119,7 +119,7 @@ func (c *Client) WorkspaceBuilds(ctx context.Context, req WorkspaceBuildsRequest } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return nil, readBodyAsError(res) + return nil, ReadBodyAsError(res) } var workspaceBuild []WorkspaceBuild return workspaceBuild, json.NewDecoder(res.Body).Decode(&workspaceBuild) @@ -133,7 +133,7 @@ func (c *Client) CreateWorkspaceBuild(ctx context.Context, workspace uuid.UUID, } defer res.Body.Close() if res.StatusCode != http.StatusCreated { - return WorkspaceBuild{}, readBodyAsError(res) + return WorkspaceBuild{}, ReadBodyAsError(res) } var workspaceBuild WorkspaceBuild return workspaceBuild, json.NewDecoder(res.Body).Decode(&workspaceBuild) @@ -148,7 +148,7 @@ func (c *Client) WatchWorkspace(ctx context.Context, id uuid.UUID) (<-chan Works return nil, err } if res.StatusCode != http.StatusOK { - return nil, readBodyAsError(res) + return nil, ReadBodyAsError(res) } nextEvent := ServerSentEventReader(ctx, res.Body) @@ -198,7 +198,7 @@ func (c *Client) UpdateWorkspace(ctx context.Context, id uuid.UUID, req UpdateWo } defer res.Body.Close() if res.StatusCode != http.StatusNoContent { - return readBodyAsError(res) + return ReadBodyAsError(res) } return nil } @@ -218,7 +218,7 @@ func (c *Client) UpdateWorkspaceAutostart(ctx context.Context, id uuid.UUID, req } defer res.Body.Close() if res.StatusCode != http.StatusNoContent { - return readBodyAsError(res) + return ReadBodyAsError(res) } return nil } @@ -238,7 +238,7 @@ func (c *Client) UpdateWorkspaceTTL(ctx context.Context, id uuid.UUID, req Updat } defer res.Body.Close() if res.StatusCode != http.StatusNoContent { - return readBodyAsError(res) + return ReadBodyAsError(res) } return nil } @@ -258,7 +258,7 @@ func (c *Client) PutExtendWorkspace(ctx context.Context, id uuid.UUID, req PutEx } defer res.Body.Close() if res.StatusCode != http.StatusOK && res.StatusCode != http.StatusNotModified { - return readBodyAsError(res) + return ReadBodyAsError(res) } return nil } @@ -323,7 +323,7 @@ func (c *Client) Workspaces(ctx context.Context, filter WorkspaceFilter) (Worksp defer res.Body.Close() if res.StatusCode != http.StatusOK { - return WorkspacesResponse{}, readBodyAsError(res) + return WorkspacesResponse{}, ReadBodyAsError(res) } var wres WorkspacesResponse @@ -343,37 +343,29 @@ func (c *Client) WorkspaceByOwnerAndName(ctx context.Context, owner string, name defer res.Body.Close() if res.StatusCode != http.StatusOK { - return Workspace{}, readBodyAsError(res) + return Workspace{}, ReadBodyAsError(res) } var workspace Workspace return workspace, json.NewDecoder(res.Body).Decode(&workspace) } -type GetAppHostResponse struct { - // Host is the externally accessible URL for the Coder instance. - Host string `json:"host"` +type WorkspaceQuota struct { + CreditsConsumed int `json:"credits_consumed"` + Budget int `json:"budget"` } -// GetAppHost returns the site-wide application wildcard hostname without the -// leading "*.", e.g. "apps.coder.com". Apps are accessible at: -// "------.", e.g. -// "my-app--agent--workspace--username.apps.coder.com". -// -// If the app host is not set, the response will contain an empty string. -func (c *Client) GetAppHost(ctx context.Context) (GetAppHostResponse, error) { - res, err := c.Request(ctx, http.MethodGet, "/api/v2/applications/host", nil) +func (c *Client) WorkspaceQuota(ctx context.Context, userID string) (WorkspaceQuota, error) { + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspace-quota/%s", userID), nil) if err != nil { - return GetAppHostResponse{}, err + return WorkspaceQuota{}, err } defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return GetAppHostResponse{}, readBodyAsError(res) + return WorkspaceQuota{}, ReadBodyAsError(res) } - - var host GetAppHostResponse - return host, json.NewDecoder(res.Body).Decode(&host) + var quota WorkspaceQuota + return quota, json.NewDecoder(res.Body).Decode("a) } // WorkspaceNotifyChannel is the PostgreSQL NOTIFY diff --git a/docs/api/agents.md b/docs/api/agents.md index e4f746a188..31f88ad590 100644 --- a/docs/api/agents.md +++ b/docs/api/agents.md @@ -27,7 +27,7 @@ curl -X POST http://coder-server:8080/api/v2/workspaceagents/aws-instance-identi | Name | In | Type | Required | Description | | ------ | ---- | -------------------------------------------------------------------------------- | -------- | ----------------------- | -| `body` | body | [codersdk.AWSInstanceIdentityToken](schemas.md#codersdkawsinstanceidentitytoken) | true | Instance identity token | +| `body` | body | [agentsdk.AWSInstanceIdentityToken](schemas.md#agentsdkawsinstanceidentitytoken) | true | Instance identity token | ### Example responses @@ -41,9 +41,9 @@ curl -X POST http://coder-server:8080/api/v2/workspaceagents/aws-instance-identi ### Responses -| Status | Meaning | Description | Schema | -| ------ | ------------------------------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------- | -| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.WorkspaceAgentAuthenticateResponse](schemas.md#codersdkworkspaceagentauthenticateresponse) | +| Status | Meaning | Description | Schema | +| ------ | ------------------------------------------------------- | ----------- | ------------------------------------------------------------------------ | +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [agentsdk.AuthenticateResponse](schemas.md#agentsdkauthenticateresponse) | To perform this operation, you must be authenticated. [Learn more](authentication.md). @@ -74,7 +74,7 @@ curl -X POST http://coder-server:8080/api/v2/workspaceagents/azure-instance-iden | Name | In | Type | Required | Description | | ------ | ---- | ------------------------------------------------------------------------------------ | -------- | ----------------------- | -| `body` | body | [codersdk.AzureInstanceIdentityToken](schemas.md#codersdkazureinstanceidentitytoken) | true | Instance identity token | +| `body` | body | [agentsdk.AzureInstanceIdentityToken](schemas.md#agentsdkazureinstanceidentitytoken) | true | Instance identity token | ### Example responses @@ -88,9 +88,9 @@ curl -X POST http://coder-server:8080/api/v2/workspaceagents/azure-instance-iden ### Responses -| Status | Meaning | Description | Schema | -| ------ | ------------------------------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------- | -| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.WorkspaceAgentAuthenticateResponse](schemas.md#codersdkworkspaceagentauthenticateresponse) | +| Status | Meaning | Description | Schema | +| ------ | ------------------------------------------------------- | ----------- | ------------------------------------------------------------------------ | +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [agentsdk.AuthenticateResponse](schemas.md#agentsdkauthenticateresponse) | To perform this operation, you must be authenticated. [Learn more](authentication.md). @@ -120,7 +120,7 @@ curl -X POST http://coder-server:8080/api/v2/workspaceagents/google-instance-ide | Name | In | Type | Required | Description | | ------ | ---- | -------------------------------------------------------------------------------------- | -------- | ----------------------- | -| `body` | body | [codersdk.GoogleInstanceIdentityToken](schemas.md#codersdkgoogleinstanceidentitytoken) | true | Instance identity token | +| `body` | body | [agentsdk.GoogleInstanceIdentityToken](schemas.md#agentsdkgoogleinstanceidentitytoken) | true | Instance identity token | ### Example responses @@ -134,9 +134,9 @@ curl -X POST http://coder-server:8080/api/v2/workspaceagents/google-instance-ide ### Responses -| Status | Meaning | Description | Schema | -| ------ | ------------------------------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------- | -| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.WorkspaceAgentAuthenticateResponse](schemas.md#codersdkworkspaceagentauthenticateresponse) | +| Status | Meaning | Description | Schema | +| ------ | ------------------------------------------------------- | ----------- | ------------------------------------------------------------------------ | +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [agentsdk.AuthenticateResponse](schemas.md#agentsdkauthenticateresponse) | To perform this operation, you must be authenticated. [Learn more](authentication.md). @@ -166,9 +166,9 @@ curl -X POST http://coder-server:8080/api/v2/workspaceagents/me/app-health \ ### Parameters -| Name | In | Type | Required | Description | -| ------ | ---- | -------------------------------------------------------------------------------------------- | -------- | -------------------------- | -| `body` | body | [codersdk.PostWorkspaceAppHealthsRequest](schemas.md#codersdkpostworkspaceapphealthsrequest) | true | Application health request | +| Name | In | Type | Required | Description | +| ------ | ---- | -------------------------------------------------------------------------- | -------- | -------------------------- | +| `body` | body | [agentsdk.PostAppHealthsRequest](schemas.md#agentsdkpostapphealthsrequest) | true | Application health request | ### Responses @@ -235,9 +235,9 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/me/gitauth?url=http% ### Responses -| Status | Meaning | Description | Schema | -| ------ | ------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------ | -| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.WorkspaceAgentGitAuthResponse](schemas.md#codersdkworkspaceagentgitauthresponse) | +| Status | Meaning | Description | Schema | +| ------ | ------------------------------------------------------- | ----------- | -------------------------------------------------------------- | +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [agentsdk.GitAuthResponse](schemas.md#agentsdkgitauthresponse) | To perform this operation, you must be authenticated. [Learn more](authentication.md). @@ -267,9 +267,9 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/me/gitsshkey \ ### Responses -| Status | Meaning | Description | Schema | -| ------ | ------------------------------------------------------- | ----------- | ------------------------------------------------------------ | -| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.AgentGitSSHKey](schemas.md#codersdkagentgitsshkey) | +| Status | Meaning | Description | Schema | +| ------ | ------------------------------------------------------- | ----------- | -------------------------------------------------- | +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [agentsdk.GitSSHKey](schemas.md#agentsdkgitsshkey) | To perform this operation, you must be authenticated. [Learn more](authentication.md). @@ -377,9 +377,9 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/me/metadata \ ### Responses -| Status | Meaning | Description | Schema | -| ------ | ------------------------------------------------------- | ----------- | ---------------------------------------------------------------------------- | -| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.WorkspaceAgentMetadata](schemas.md#codersdkworkspaceagentmetadata) | +| Status | Meaning | Description | Schema | +| ------ | ------------------------------------------------------- | ----------- | ------------------------------------------------ | +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [agentsdk.Metadata](schemas.md#agentsdkmetadata) | To perform this operation, you must be authenticated. [Learn more](authentication.md). @@ -415,9 +415,9 @@ curl -X POST http://coder-server:8080/api/v2/workspaceagents/me/report-stats \ ### Parameters -| Name | In | Type | Required | Description | -| ------ | ---- | ---------------------------------------------------- | -------- | ------------- | -| `body` | body | [codersdk.AgentStats](schemas.md#codersdkagentstats) | true | Stats request | +| Name | In | Type | Required | Description | +| ------ | ---- | ------------------------------------------ | -------- | ------------- | +| `body` | body | [agentsdk.Stats](schemas.md#agentsdkstats) | true | Stats request | ### Example responses @@ -431,9 +431,9 @@ curl -X POST http://coder-server:8080/api/v2/workspaceagents/me/report-stats \ ### Responses -| Status | Meaning | Description | Schema | -| ------ | ------------------------------------------------------- | ----------- | -------------------------------------------------------------------- | -| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.AgentStatsResponse](schemas.md#codersdkagentstatsresponse) | +| Status | Meaning | Description | Schema | +| ------ | ------------------------------------------------------- | ----------- | ---------------------------------------------------------- | +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [agentsdk.StatsResponse](schemas.md#agentsdkstatsresponse) | To perform this operation, you must be authenticated. [Learn more](authentication.md). @@ -666,7 +666,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/{workspaceagent}/lis { "ports": [ { - "network": "tcp", + "network": "string", "port": 0, "process_name": "string" } @@ -676,9 +676,9 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/{workspaceagent}/lis ### Responses -| Status | Meaning | Description | Schema | -| ------ | ------------------------------------------------------- | ----------- | ---------------------------------------------------------------------------- | -| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.ListeningPortsResponse](schemas.md#codersdklisteningportsresponse) | +| Status | Meaning | Description | Schema | +| ------ | ------------------------------------------------------- | ----------- | -------------------------------------------------------------------------------------------------------- | +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.WorkspaceAgentListeningPortsResponse](schemas.md#codersdkworkspaceagentlisteningportsresponse) | To perform this operation, you must be authenticated. [Learn more](authentication.md). diff --git a/docs/api/applications.md b/docs/api/applications.md index 11026ae39d..2aa3623122 100644 --- a/docs/api/applications.md +++ b/docs/api/applications.md @@ -51,8 +51,8 @@ curl -X GET http://coder-server:8080/api/v2/applications/host \ ### Responses -| Status | Meaning | Description | Schema | -| ------ | ------------------------------------------------------- | ----------- | -------------------------------------------------------------------- | -| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.GetAppHostResponse](schemas.md#codersdkgetapphostresponse) | +| Status | Meaning | Description | Schema | +| ------ | ------------------------------------------------------- | ----------- | -------------------------------------------------------------- | +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.AppHostResponse](schemas.md#codersdkapphostresponse) | To perform this operation, you must be authenticated. [Learn more](authentication.md). diff --git a/docs/api/schemas.md b/docs/api/schemas.md index 6eacdfaa5c..5166ff9c0b 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -1,5 +1,289 @@ # Schemas +## agentsdk.AWSInstanceIdentityToken + +```json +{ + "document": "string", + "signature": "string" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| ----------- | ------ | -------- | ------------ | ----------- | +| `document` | string | true | | | +| `signature` | string | true | | | + +## agentsdk.AuthenticateResponse + +```json +{ + "session_token": "string" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| --------------- | ------ | -------- | ------------ | ----------- | +| `session_token` | string | false | | | + +## agentsdk.AzureInstanceIdentityToken + +```json +{ + "encoding": "string", + "signature": "string" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| ----------- | ------ | -------- | ------------ | ----------- | +| `encoding` | string | true | | | +| `signature` | string | true | | | + +## agentsdk.GitAuthResponse + +```json +{ + "password": "string", + "url": "string", + "username": "string" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| ---------- | ------ | -------- | ------------ | ----------- | +| `password` | string | false | | | +| `url` | string | false | | | +| `username` | string | false | | | + +## agentsdk.GitSSHKey + +```json +{ + "private_key": "string", + "public_key": "string" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| ------------- | ------ | -------- | ------------ | ----------- | +| `private_key` | string | false | | | +| `public_key` | string | false | | | + +## agentsdk.GoogleInstanceIdentityToken + +```json +{ + "json_web_token": "string" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| ---------------- | ------ | -------- | ------------ | ----------- | +| `json_web_token` | string | true | | | + +## agentsdk.Metadata + +```json +{ + "apps": [ + { + "command": "string", + "display_name": "string", + "external": true, + "health": "disabled", + "healthcheck": { + "interval": 0, + "threshold": 0, + "url": "string" + }, + "icon": "string", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "sharing_level": "owner", + "slug": "string", + "subdomain": true, + "url": "string" + } + ], + "derpmap": { + "omitDefaultRegions": true, + "regions": { + "property1": { + "avoid": true, + "embeddedRelay": true, + "nodes": [ + { + "certName": "string", + "derpport": 0, + "forceHTTP": true, + "hostName": "string", + "insecureForTests": true, + "ipv4": "string", + "ipv6": "string", + "name": "string", + "regionID": 0, + "stunonly": true, + "stunport": 0, + "stuntestIP": "string" + } + ], + "regionCode": "string", + "regionID": 0, + "regionName": "string" + }, + "property2": { + "avoid": true, + "embeddedRelay": true, + "nodes": [ + { + "certName": "string", + "derpport": 0, + "forceHTTP": true, + "hostName": "string", + "insecureForTests": true, + "ipv4": "string", + "ipv6": "string", + "name": "string", + "regionID": 0, + "stunonly": true, + "stunport": 0, + "stuntestIP": "string" + } + ], + "regionCode": "string", + "regionID": 0, + "regionName": "string" + } + } + }, + "directory": "string", + "environment_variables": { + "property1": "string", + "property2": "string" + }, + "git_auth_configs": 0, + "motd_file": "string", + "startup_script": "string", + "startup_script_timeout": 0, + "vscode_port_proxy_uri": "string" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| ------------------------ | ------------------------------------------------------- | -------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `apps` | array of [codersdk.WorkspaceApp](#codersdkworkspaceapp) | false | | | +| `derpmap` | [tailcfg.DERPMap](#tailcfgderpmap) | false | | | +| `directory` | string | false | | | +| `environment_variables` | object | false | | | +| » `[any property]` | string | false | | | +| `git_auth_configs` | integer | false | | Git auth configs stores the number of Git configurations the Coder deployment has. If this number is >0, we set up special configuration in the workspace. | +| `motd_file` | string | false | | | +| `startup_script` | string | false | | | +| `startup_script_timeout` | integer | false | | | +| `vscode_port_proxy_uri` | string | false | | | + +## agentsdk.PostAppHealthsRequest + +```json +{ + "healths": { + "property1": "disabled", + "property2": "disabled" + } +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| ------------------ | ---------------------------------------------------------- | -------- | ------------ | --------------------------------------------------------------------- | +| `healths` | object | false | | Healths is a map of the workspace app name and the health of the app. | +| » `[any property]` | [codersdk.WorkspaceAppHealth](#codersdkworkspaceapphealth) | false | | | + +## agentsdk.PostLifecycleRequest + +```json +{ + "state": "created" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| ------- | -------------------------------------------------------------------- | -------- | ------------ | ----------- | +| `state` | [codersdk.WorkspaceAgentLifecycle](#codersdkworkspaceagentlifecycle) | false | | | + +## agentsdk.PostVersionRequest + +```json +{ + "version": "string" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| --------- | ------ | -------- | ------------ | ----------- | +| `version` | string | false | | | + +## agentsdk.Stats + +```json +{ + "conns_by_proto": { + "property1": 0, + "property2": 0 + }, + "num_comms": 0, + "rx_bytes": 0, + "rx_packets": 0, + "tx_bytes": 0, + "tx_packets": 0 +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| ------------------ | ------- | -------- | ------------ | ------------------------------------------------------------ | +| `conns_by_proto` | object | false | | Conns by proto is a count of connections by protocol. | +| » `[any property]` | integer | false | | | +| `num_comms` | integer | false | | Num comms is the number of connections received by an agent. | +| `rx_bytes` | integer | false | | Rx bytes is the number of received bytes. | +| `rx_packets` | integer | false | | Rx packets is the number of received packets. | +| `tx_bytes` | integer | false | | Tx bytes is the number of transmitted bytes. | +| `tx_packets` | integer | false | | Tx packets is the number of transmitted bytes. | + +## agentsdk.StatsResponse + +```json +{ + "report_interval": 0 +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| ----------------- | ------- | -------- | ------------ | ------------------------------------------------------------------------------ | +| `report_interval` | integer | false | | Report interval is the duration after which the agent should send stats again. | + ## coderd.SCIMUser ```json @@ -117,22 +401,6 @@ | `all` | | `application_connect` | -## codersdk.AWSInstanceIdentityToken - -```json -{ - "document": "string", - "signature": "string" -} -``` - -### Properties - -| Name | Type | Required | Restrictions | Description | -| ----------- | ------ | -------- | ------------ | ----------- | -| `document` | string | true | | | -| `signature` | string | true | | | - ## codersdk.AddLicenseRequest ```json @@ -147,63 +415,19 @@ | --------- | ------ | -------- | ------------ | ----------- | | `license` | string | true | | | -## codersdk.AgentGitSSHKey +## codersdk.AppHostResponse ```json { - "private_key": "string", - "public_key": "string" + "host": "string" } ``` ### Properties -| Name | Type | Required | Restrictions | Description | -| ------------- | ------ | -------- | ------------ | ----------- | -| `private_key` | string | false | | | -| `public_key` | string | false | | | - -## codersdk.AgentStats - -```json -{ - "conns_by_proto": { - "property1": 0, - "property2": 0 - }, - "num_comms": 0, - "rx_bytes": 0, - "rx_packets": 0, - "tx_bytes": 0, - "tx_packets": 0 -} -``` - -### Properties - -| Name | Type | Required | Restrictions | Description | -| ------------------ | ------- | -------- | ------------ | ------------------------------------------------------------ | -| `conns_by_proto` | object | false | | Conns by proto is a count of connections by protocol. | -| » `[any property]` | integer | false | | | -| `num_comms` | integer | false | | Num comms is the number of connections received by an agent. | -| `rx_bytes` | integer | false | | Rx bytes is the number of received bytes. | -| `rx_packets` | integer | false | | Rx packets is the number of received packets. | -| `tx_bytes` | integer | false | | Tx bytes is the number of transmitted bytes. | -| `tx_packets` | integer | false | | Tx packets is the number of transmitted bytes. | - -## codersdk.AgentStatsResponse - -```json -{ - "report_interval": 0 -} -``` - -### Properties - -| Name | Type | Required | Restrictions | Description | -| ----------------- | ------- | -------- | ------------ | ------------------------------------------------------------------------------ | -| `report_interval` | integer | false | | Report interval is the duration after which the agent should send stats again. | +| Name | Type | Required | Restrictions | Description | +| ------ | ------ | -------- | ------------ | ------------------------------------------------------------- | +| `host` | string | false | | Host is the externally accessible URL for the Coder instance. | ## codersdk.AppearanceConfig @@ -560,22 +784,6 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in | ---------------- | ------- | -------- | ------------ | ----------- | | `[any property]` | boolean | false | | | -## codersdk.AzureInstanceIdentityToken - -```json -{ - "encoding": "string", - "signature": "string" -} -``` - -### Properties - -| Name | Type | Required | Restrictions | Description | -| ----------- | ------ | -------- | ------------ | ----------- | -| `encoding` | string | true | | | -| `signature` | string | true | | | - ## codersdk.BuildInfoResponse ```json @@ -2504,20 +2712,6 @@ CreateParameterRequest is a structure used to create a new parameter value for a | ----- | ------ | -------- | ------------ | ----------- | | `key` | string | false | | | -## codersdk.GetAppHostResponse - -```json -{ - "host": "string" -} -``` - -### Properties - -| Name | Type | Required | Restrictions | Description | -| ------ | ------ | -------- | ------------ | ------------------------------------------------------------- | -| `host` | string | false | | Host is the externally accessible URL for the Coder instance. | - ## codersdk.GetUsersResponse ```json @@ -2601,20 +2795,6 @@ CreateParameterRequest is a structure used to create a new parameter value for a | `updated_at` | string | false | | | | `user_id` | string | false | | | -## codersdk.GoogleInstanceIdentityToken - -```json -{ - "json_web_token": "string" -} -``` - -### Properties - -| Name | Type | Required | Restrictions | Description | -| ---------------- | ------ | -------- | ------------ | ----------- | -| `json_web_token` | string | true | | | - ## codersdk.Group ```json @@ -2694,58 +2874,6 @@ CreateParameterRequest is a structure used to create a new parameter value for a | `uploaded_at` | string | false | | | | `uuid` | string | false | | | -## codersdk.ListeningPort - -```json -{ - "network": "tcp", - "port": 0, - "process_name": "string" -} -``` - -### Properties - -| Name | Type | Required | Restrictions | Description | -| -------------- | -------------------------------------------------------------- | -------- | ------------ | ------------------------ | -| `network` | [codersdk.ListeningPortNetwork](#codersdklisteningportnetwork) | false | | only "tcp" at the moment | -| `port` | integer | false | | | -| `process_name` | string | false | | may be empty | - -## codersdk.ListeningPortNetwork - -```json -"tcp" -``` - -### Properties - -#### Enumerated Values - -| Value | -| ----- | -| `tcp` | - -## codersdk.ListeningPortsResponse - -```json -{ - "ports": [ - { - "network": "tcp", - "port": 0, - "process_name": "string" - } - ] -} -``` - -### Properties - -| Name | Type | Required | Restrictions | Description | -| ------- | --------------------------------------------------------- | -------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `ports` | array of [codersdk.ListeningPort](#codersdklisteningport) | false | | If there are no ports in the list, nothing should be displayed in the UI. There must not be a "no ports available" message or anything similar, as there will always be no ports displayed on platforms where our port detection logic is unsupported. | - ## codersdk.LogLevel ```json @@ -3363,38 +3491,6 @@ Parameter represents a set value for the scope. | `none` | | `data` | -## codersdk.PostWorkspaceAgentLifecycleRequest - -```json -{ - "state": "created" -} -``` - -### Properties - -| Name | Type | Required | Restrictions | Description | -| ------- | -------------------------------------------------------------------- | -------- | ------------ | ----------- | -| `state` | [codersdk.WorkspaceAgentLifecycle](#codersdkworkspaceagentlifecycle) | false | | | - -## codersdk.PostWorkspaceAppHealthsRequest - -```json -{ - "healths": { - "property1": "disabled", - "property2": "disabled" - } -} -``` - -### Properties - -| Name | Type | Required | Restrictions | Description | -| ------------------ | ---------------------------------------------------------- | -------- | ------------ | --------------------------------------------------------------------- | -| `healths` | object | false | | Healths is a map of the workspace app name and the health of the app. | -| » `[any property]` | [codersdk.WorkspaceAppHealth](#codersdkworkspaceapphealth) | false | | | - ## codersdk.PprofConfig ```json @@ -4801,20 +4897,6 @@ Parameter represents a set value for the scope. | `updated_at` | string | false | | | | `version` | string | false | | | -## codersdk.WorkspaceAgentAuthenticateResponse - -```json -{ - "session_token": "string" -} -``` - -### Properties - -| Name | Type | Required | Restrictions | Description | -| --------------- | ------ | -------- | ------------ | ----------- | -| `session_token` | string | false | | | - ## codersdk.WorkspaceAgentConnectionInfo ```json @@ -4879,24 +4961,6 @@ Parameter represents a set value for the scope. | ---------- | ---------------------------------- | -------- | ------------ | ----------- | | `derp_map` | [tailcfg.DERPMap](#tailcfgderpmap) | false | | | -## codersdk.WorkspaceAgentGitAuthResponse - -```json -{ - "password": "string", - "url": "string", - "username": "string" -} -``` - -### Properties - -| Name | Type | Required | Restrictions | Description | -| ---------- | ------ | -------- | ------------ | ----------- | -| `password` | string | false | | | -| `url` | string | false | | | -| `username` | string | false | | | - ## codersdk.WorkspaceAgentLifecycle ```json @@ -4915,107 +4979,43 @@ Parameter represents a set value for the scope. | `start_error` | | `ready` | -## codersdk.WorkspaceAgentMetadata +## codersdk.WorkspaceAgentListeningPort ```json { - "apps": [ - { - "command": "string", - "display_name": "string", - "external": true, - "health": "disabled", - "healthcheck": { - "interval": 0, - "threshold": 0, - "url": "string" - }, - "icon": "string", - "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", - "sharing_level": "owner", - "slug": "string", - "subdomain": true, - "url": "string" - } - ], - "derpmap": { - "omitDefaultRegions": true, - "regions": { - "property1": { - "avoid": true, - "embeddedRelay": true, - "nodes": [ - { - "certName": "string", - "derpport": 0, - "forceHTTP": true, - "hostName": "string", - "insecureForTests": true, - "ipv4": "string", - "ipv6": "string", - "name": "string", - "regionID": 0, - "stunonly": true, - "stunport": 0, - "stuntestIP": "string" - } - ], - "regionCode": "string", - "regionID": 0, - "regionName": "string" - }, - "property2": { - "avoid": true, - "embeddedRelay": true, - "nodes": [ - { - "certName": "string", - "derpport": 0, - "forceHTTP": true, - "hostName": "string", - "insecureForTests": true, - "ipv4": "string", - "ipv6": "string", - "name": "string", - "regionID": 0, - "stunonly": true, - "stunport": 0, - "stuntestIP": "string" - } - ], - "regionCode": "string", - "regionID": 0, - "regionName": "string" - } - } - }, - "directory": "string", - "environment_variables": { - "property1": "string", - "property2": "string" - }, - "git_auth_configs": 0, - "motd_file": "string", - "startup_script": "string", - "startup_script_timeout": 0, - "vscode_port_proxy_uri": "string" + "network": "string", + "port": 0, + "process_name": "string" } ``` ### Properties -| Name | Type | Required | Restrictions | Description | -| ------------------------ | ------------------------------------------------------- | -------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `apps` | array of [codersdk.WorkspaceApp](#codersdkworkspaceapp) | false | | | -| `derpmap` | [tailcfg.DERPMap](#tailcfgderpmap) | false | | | -| `directory` | string | false | | | -| `environment_variables` | object | false | | | -| » `[any property]` | string | false | | | -| `git_auth_configs` | integer | false | | Git auth configs stores the number of Git configurations the Coder deployment has. If this number is >0, we set up special configuration in the workspace. | -| `motd_file` | string | false | | | -| `startup_script` | string | false | | | -| `startup_script_timeout` | integer | false | | | -| `vscode_port_proxy_uri` | string | false | | | +| Name | Type | Required | Restrictions | Description | +| -------------- | ------- | -------- | ------------ | ------------------------ | +| `network` | string | false | | only "tcp" at the moment | +| `port` | integer | false | | | +| `process_name` | string | false | | may be empty | + +## codersdk.WorkspaceAgentListeningPortsResponse + +```json +{ + "ports": [ + { + "network": "string", + "port": 0, + "process_name": "string" + } + ] +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| ------- | ------------------------------------------------------------------------------------- | -------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `ports` | array of [codersdk.WorkspaceAgentListeningPort](#codersdkworkspaceagentlisteningport) | false | | If there are no ports in the list, nothing should be displayed in the UI. There must not be a "no ports available" message or anything similar, as there will always be no ports displayed on platforms where our port detection logic is unsupported. | ## codersdk.WorkspaceAgentStatus diff --git a/enterprise/coderd/authorize_test.go b/enterprise/coderd/authorize_test.go index ef181e5b98..c15c4e4683 100644 --- a/enterprise/coderd/authorize_test.go +++ b/enterprise/coderd/authorize_test.go @@ -106,7 +106,7 @@ func TestCheckACLPermissions(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) t.Cleanup(cancel) - resp, err := c.Client.CheckAuthorization(ctx, codersdk.AuthorizationRequest{Checks: params}) + resp, err := c.Client.AuthCheck(ctx, codersdk.AuthorizationRequest{Checks: params}) require.NoError(t, err, "check perms") require.Equal(t, c.Check, resp) }) diff --git a/enterprise/coderd/workspaceagents_test.go b/enterprise/coderd/workspaceagents_test.go index aaef3c28f9..8cfae27995 100644 --- a/enterprise/coderd/workspaceagents_test.go +++ b/enterprise/coderd/workspaceagents_test.go @@ -14,6 +14,7 @@ import ( "github.com/coder/coder/agent" "github.com/coder/coder/coderd/coderdtest" "github.com/coder/coder/codersdk" + "github.com/coder/coder/codersdk/agentsdk" "github.com/coder/coder/enterprise/coderd/coderdenttest" "github.com/coder/coder/enterprise/coderd/license" "github.com/coder/coder/provisioner/echo" @@ -116,8 +117,8 @@ func setupWorkspaceAgent(t *testing.T, client *codersdk.Client, user codersdk.Cr template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) - agentClient := codersdk.New(client.URL) - agentClient.HTTPClient = &http.Client{ + agentClient := agentsdk.New(client.URL) + agentClient.SDK.HTTPClient = &http.Client{ Transport: &http.Transport{ TLSClientConfig: &tls.Config{ //nolint:gosec diff --git a/scaletest/agentconn/run.go b/scaletest/agentconn/run.go index 1a93977efe..2229e6706c 100644 --- a/scaletest/agentconn/run.go +++ b/scaletest/agentconn/run.go @@ -131,7 +131,7 @@ func (r *Runner) Run(ctx context.Context, _ string, logs io.Writer) error { return nil } -func waitForDisco(ctx context.Context, logs io.Writer, conn *codersdk.AgentConn) error { +func waitForDisco(ctx context.Context, logs io.Writer, conn *codersdk.WorkspaceAgentConn) error { const pingAttempts = 10 const pingDelay = 1 * time.Second @@ -162,7 +162,7 @@ func waitForDisco(ctx context.Context, logs io.Writer, conn *codersdk.AgentConn) return nil } -func waitForDirectConnection(ctx context.Context, logs io.Writer, conn *codersdk.AgentConn) error { +func waitForDirectConnection(ctx context.Context, logs io.Writer, conn *codersdk.WorkspaceAgentConn) error { const directConnectionAttempts = 30 const directConnectionDelay = 1 * time.Second @@ -204,7 +204,7 @@ func waitForDirectConnection(ctx context.Context, logs io.Writer, conn *codersdk return nil } -func verifyConnection(ctx context.Context, logs io.Writer, conn *codersdk.AgentConn) error { +func verifyConnection(ctx context.Context, logs io.Writer, conn *codersdk.WorkspaceAgentConn) error { const verifyConnectionAttempts = 30 const verifyConnectionDelay = 1 * time.Second @@ -218,7 +218,7 @@ func verifyConnection(ctx context.Context, logs io.Writer, conn *codersdk.AgentC u := &url.URL{ Scheme: "http", - Host: net.JoinHostPort("localhost", strconv.Itoa(codersdk.TailnetStatisticsPort)), + Host: net.JoinHostPort("localhost", strconv.Itoa(codersdk.WorkspaceAgentStatisticsPort)), Path: "/", } req, err := http.NewRequestWithContext(verifyCtx, http.MethodGet, u.String(), nil) @@ -246,7 +246,7 @@ func verifyConnection(ctx context.Context, logs io.Writer, conn *codersdk.AgentC return nil } -func performInitialConnections(ctx context.Context, logs io.Writer, conn *codersdk.AgentConn, specs []Connection) error { +func performInitialConnections(ctx context.Context, logs io.Writer, conn *codersdk.WorkspaceAgentConn, specs []Connection) error { if len(specs) == 0 { return nil } @@ -284,7 +284,7 @@ func performInitialConnections(ctx context.Context, logs io.Writer, conn *coders return nil } -func holdConnection(ctx context.Context, logs io.Writer, conn *codersdk.AgentConn, holdDur time.Duration, specs []Connection) error { +func holdConnection(ctx context.Context, logs io.Writer, conn *codersdk.WorkspaceAgentConn, holdDur time.Duration, specs []Connection) error { ctx, span := tracing.StartSpan(ctx) defer span.End() @@ -361,7 +361,7 @@ func holdConnection(ctx context.Context, logs io.Writer, conn *codersdk.AgentCon return nil } -func agentHTTPClient(conn *codersdk.AgentConn) *http.Client { +func agentHTTPClient(conn *codersdk.WorkspaceAgentConn) *http.Client { return &http.Client{ Transport: &http.Transport{ DisableKeepAlives: true, @@ -375,7 +375,7 @@ func agentHTTPClient(conn *codersdk.AgentConn) *http.Client { if err != nil { return nil, xerrors.Errorf("parse port %q: %w", port, err) } - return conn.DialContextTCP(ctx, netip.AddrPortFrom(codersdk.TailnetIP, uint16(portUint))) + return conn.DialContextTCP(ctx, netip.AddrPortFrom(codersdk.WorkspaceAgentIP, uint16(portUint))) }, }, } diff --git a/scaletest/agentconn/run_test.go b/scaletest/agentconn/run_test.go index 433a614e6f..c773432f82 100644 --- a/scaletest/agentconn/run_test.go +++ b/scaletest/agentconn/run_test.go @@ -18,6 +18,7 @@ import ( "github.com/coder/coder/coderd/coderdtest" "github.com/coder/coder/coderd/httpapi" "github.com/coder/coder/codersdk" + "github.com/coder/coder/codersdk/agentsdk" "github.com/coder/coder/provisioner/echo" "github.com/coder/coder/provisionersdk/proto" "github.com/coder/coder/scaletest/agentconn" @@ -253,7 +254,7 @@ func setupRunnerTest(t *testing.T) (client *codersdk.Client, agentID uuid.UUID) workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) - agentClient := codersdk.New(client.URL) + agentClient := agentsdk.New(client.URL) agentClient.SetSessionToken(authToken) agentCloser := agent.New(agent.Options{ Client: agentClient, diff --git a/scaletest/createworkspaces/run_test.go b/scaletest/createworkspaces/run_test.go index f52e18420d..d04baed526 100644 --- a/scaletest/createworkspaces/run_test.go +++ b/scaletest/createworkspaces/run_test.go @@ -16,6 +16,7 @@ import ( "github.com/coder/coder/coderd/coderdtest" "github.com/coder/coder/coderd/httpapi" "github.com/coder/coder/codersdk" + "github.com/coder/coder/codersdk/agentsdk" "github.com/coder/coder/provisioner/echo" "github.com/coder/coder/provisionersdk/proto" "github.com/coder/coder/scaletest/agentconn" @@ -103,7 +104,7 @@ func Test_Runner(t *testing.T) { coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) - agentClient := codersdk.New(client.URL) + agentClient := agentsdk.New(client.URL) agentClient.SetSessionToken(authToken) agentCloser := agent.New(agent.Options{ Client: agentClient, @@ -133,7 +134,7 @@ func Test_Runner(t *testing.T) { }, }, ReconnectingPTY: &reconnectingpty.Config{ - Init: codersdk.ReconnectingPTYInit{ + Init: codersdk.WorkspaceAgentReconnectingPTYInit{ Height: 24, Width: 80, Command: "echo hello", diff --git a/scaletest/reconnectingpty/config.go b/scaletest/reconnectingpty/config.go index 56bf7b9ac2..924a3e885e 100644 --- a/scaletest/reconnectingpty/config.go +++ b/scaletest/reconnectingpty/config.go @@ -23,7 +23,7 @@ type Config struct { // If the ID is not set, defaults to a random UUID. If the width or height // is not set, defaults to 80x24. If the command is not set, defaults to // opening a login shell. Command runs in the default shell. - Init codersdk.ReconnectingPTYInit `json:"init"` + Init codersdk.WorkspaceAgentReconnectingPTYInit `json:"init"` // Timeout is the duration to wait for the command to exit. Defaults to // 5 minutes. Timeout httpapi.Duration `json:"timeout"` diff --git a/scaletest/reconnectingpty/config_test.go b/scaletest/reconnectingpty/config_test.go index 1061b81691..0c5200bfd7 100644 --- a/scaletest/reconnectingpty/config_test.go +++ b/scaletest/reconnectingpty/config_test.go @@ -31,7 +31,7 @@ func Test_Config(t *testing.T) { name: "OKFull", config: reconnectingpty.Config{ AgentID: id, - Init: codersdk.ReconnectingPTYInit{ + Init: codersdk.WorkspaceAgentReconnectingPTYInit{ ID: id, Width: 80, Height: 24, diff --git a/scaletest/reconnectingpty/run_test.go b/scaletest/reconnectingpty/run_test.go index 960d5b9d9d..b47cf98b03 100644 --- a/scaletest/reconnectingpty/run_test.go +++ b/scaletest/reconnectingpty/run_test.go @@ -14,6 +14,7 @@ import ( "github.com/coder/coder/coderd/coderdtest" "github.com/coder/coder/coderd/httpapi" "github.com/coder/coder/codersdk" + "github.com/coder/coder/codersdk/agentsdk" "github.com/coder/coder/provisioner/echo" "github.com/coder/coder/provisionersdk/proto" "github.com/coder/coder/scaletest/reconnectingpty" @@ -34,7 +35,7 @@ func Test_Runner(t *testing.T) { runner := reconnectingpty.NewRunner(client, reconnectingpty.Config{ AgentID: agentID, - Init: codersdk.ReconnectingPTYInit{ + Init: codersdk.WorkspaceAgentReconnectingPTYInit{ // Use ; here because it's powershell compatible (vs &&). Command: "echo 'hello world'; sleep 1", }, @@ -63,7 +64,7 @@ func Test_Runner(t *testing.T) { runner := reconnectingpty.NewRunner(client, reconnectingpty.Config{ AgentID: agentID, - Init: codersdk.ReconnectingPTYInit{ + Init: codersdk.WorkspaceAgentReconnectingPTYInit{ Command: "echo 'hello world'", }, LogOutput: false, @@ -91,7 +92,7 @@ func Test_Runner(t *testing.T) { runner := reconnectingpty.NewRunner(client, reconnectingpty.Config{ AgentID: agentID, - Init: codersdk.ReconnectingPTYInit{ + Init: codersdk.WorkspaceAgentReconnectingPTYInit{ Command: "echo 'hello world'", }, Timeout: httpapi.Duration(2 * testutil.WaitSuperLong), @@ -115,7 +116,7 @@ func Test_Runner(t *testing.T) { runner := reconnectingpty.NewRunner(client, reconnectingpty.Config{ AgentID: agentID, - Init: codersdk.ReconnectingPTYInit{ + Init: codersdk.WorkspaceAgentReconnectingPTYInit{ Command: "sleep 120", }, Timeout: httpapi.Duration(2 * time.Second), @@ -144,7 +145,7 @@ func Test_Runner(t *testing.T) { runner := reconnectingpty.NewRunner(client, reconnectingpty.Config{ AgentID: agentID, - Init: codersdk.ReconnectingPTYInit{ + Init: codersdk.WorkspaceAgentReconnectingPTYInit{ Command: "sleep 120", }, Timeout: httpapi.Duration(2 * time.Second), @@ -169,7 +170,7 @@ func Test_Runner(t *testing.T) { runner := reconnectingpty.NewRunner(client, reconnectingpty.Config{ AgentID: agentID, - Init: codersdk.ReconnectingPTYInit{ + Init: codersdk.WorkspaceAgentReconnectingPTYInit{ Command: "echo 'hello world'", }, Timeout: httpapi.Duration(2 * testutil.WaitSuperLong), @@ -199,7 +200,7 @@ func Test_Runner(t *testing.T) { runner := reconnectingpty.NewRunner(client, reconnectingpty.Config{ AgentID: agentID, - Init: codersdk.ReconnectingPTYInit{ + Init: codersdk.WorkspaceAgentReconnectingPTYInit{ Command: "echo 'hello world'; sleep 1", }, ExpectOutput: "hello world", @@ -223,7 +224,7 @@ func Test_Runner(t *testing.T) { runner := reconnectingpty.NewRunner(client, reconnectingpty.Config{ AgentID: agentID, - Init: codersdk.ReconnectingPTYInit{ + Init: codersdk.WorkspaceAgentReconnectingPTYInit{ Command: "echo 'hello world'; sleep 1", }, ExpectOutput: "bello borld", @@ -281,7 +282,7 @@ func setupRunnerTest(t *testing.T) (client *codersdk.Client, agentID uuid.UUID) workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) - agentClient := codersdk.New(client.URL) + agentClient := agentsdk.New(client.URL) agentClient.SetSessionToken(authToken) agentCloser := agent.New(agent.Options{ Client: agentClient, diff --git a/scaletest/workspacebuild/run_test.go b/scaletest/workspacebuild/run_test.go index 127273fd6f..b26539b81e 100644 --- a/scaletest/workspacebuild/run_test.go +++ b/scaletest/workspacebuild/run_test.go @@ -16,6 +16,7 @@ import ( "github.com/coder/coder/agent" "github.com/coder/coder/coderd/coderdtest" "github.com/coder/coder/codersdk" + "github.com/coder/coder/codersdk/agentsdk" "github.com/coder/coder/provisioner/echo" "github.com/coder/coder/provisionersdk/proto" "github.com/coder/coder/scaletest/workspacebuild" @@ -130,7 +131,7 @@ func Test_Runner(t *testing.T) { for i, authToken := range []string{authToken1, authToken2, authToken3} { i := i + 1 - agentClient := codersdk.New(client.URL) + agentClient := agentsdk.New(client.URL) agentClient.SetSessionToken(authToken) agentCloser := agent.New(agent.Options{ Client: agentClient, diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 8333a01b3a..2aee82ed96 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -667,7 +667,7 @@ export const updateTemplateACL = async ( } export const getApplicationsHost = - async (): Promise => { + async (): Promise => { const response = await axios.get(`/api/v2/applications/host`) return response.data } @@ -718,7 +718,7 @@ export const getWorkspaceQuota = async ( export const getAgentListeningPorts = async ( agentID: string, -): Promise => { +): Promise => { const response = await axios.get( `/api/v2/workspaceagents/${agentID}/listening-ports`, ) diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 62e590c65d..dd4fcbf8f7 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -18,12 +18,6 @@ export interface AddLicenseRequest { readonly license: string } -// From codersdk/gitsshkey.go -export interface AgentGitSSHKey { - readonly public_key: string - readonly private_key: string -} - // From codersdk/templates.go export interface AgentStatsReportResponse { readonly num_comms: number @@ -31,7 +25,12 @@ export interface AgentStatsReportResponse { readonly tx_bytes: number } -// From codersdk/appearance.go +// From codersdk/deployment.go +export interface AppHostResponse { + readonly host: string +} + +// From codersdk/deployment.go export interface AppearanceConfig { readonly logo_url: string readonly service_banner: ServiceBannerConfig @@ -118,13 +117,7 @@ export interface AuthorizationRequest { // From codersdk/authorization.go export type AuthorizationResponse = Record -// From codersdk/workspaceagents.go -export interface AzureInstanceIdentityToken { - readonly signature: string - readonly encoding: string -} - -// From codersdk/buildinfo.go +// From codersdk/deployment.go export interface BuildInfoResponse { readonly external_url: string readonly version: string @@ -254,13 +247,13 @@ export interface DAUEntry { readonly amount: number } -// From codersdk/deploymentconfig.go +// From codersdk/deployment.go export interface DERP { readonly server: DERPServerConfig readonly config: DERPConfig } -// From codersdk/deploymentconfig.go +// From codersdk/deployment.go export interface DERPConfig { readonly url: DeploymentConfigField readonly path: DeploymentConfigField @@ -272,7 +265,7 @@ export interface DERPRegion { readonly latency_ms: number } -// From codersdk/deploymentconfig.go +// From codersdk/deployment.go export interface DERPServerConfig { readonly enable: DeploymentConfigField readonly region_id: DeploymentConfigField @@ -282,13 +275,13 @@ export interface DERPServerConfig { readonly relay_url: DeploymentConfigField } -// From codersdk/deploymentconfig.go +// From codersdk/deployment.go export interface DangerousConfig { readonly allow_path_app_sharing: DeploymentConfigField readonly allow_path_app_site_owner_access: DeploymentConfigField } -// From codersdk/deploymentconfig.go +// From codersdk/deployment.go export interface DeploymentConfig { readonly access_url: DeploymentConfigField readonly wildcard_access_url: DeploymentConfigField @@ -329,7 +322,7 @@ export interface DeploymentConfig { readonly experimental: DeploymentConfigField } -// From codersdk/deploymentconfig.go +// From codersdk/deployment.go export interface DeploymentConfigField { readonly name: string readonly usage: string @@ -342,12 +335,12 @@ export interface DeploymentConfigField { readonly value: T } -// From codersdk/insights.go +// From codersdk/deployment.go export interface DeploymentDAUsResponse { readonly entries: DAUEntry[] } -// From codersdk/features.go +// From codersdk/deployment.go export interface Entitlements { readonly features: Record readonly warnings: string[] @@ -357,10 +350,10 @@ export interface Entitlements { readonly experimental: boolean } -// From codersdk/experiments.go +// From codersdk/deployment.go export type Experiments = Experiment[] -// From codersdk/features.go +// From codersdk/deployment.go export interface Feature { readonly entitlement: Entitlement readonly enabled: boolean @@ -373,18 +366,13 @@ export interface GenerateAPIKeyResponse { readonly key: string } -// From codersdk/workspaces.go -export interface GetAppHostResponse { - readonly host: string -} - // From codersdk/users.go export interface GetUsersResponse { readonly users: User[] readonly count: number } -// From codersdk/deploymentconfig.go +// From codersdk/deployment.go export interface GitAuthConfig { readonly id: string readonly type: string @@ -431,19 +419,7 @@ export interface License { readonly claims: Record } -// From codersdk/agentconn.go -export interface ListeningPort { - readonly process_name: string - readonly network: ListeningPortNetwork - readonly port: number -} - -// From codersdk/agentconn.go -export interface ListeningPortsResponse { - readonly ports: ListeningPort[] -} - -// From codersdk/deploymentconfig.go +// From codersdk/deployment.go export interface LoggingConfig { readonly human: DeploymentConfigField readonly json: DeploymentConfigField @@ -461,12 +437,12 @@ export interface LoginWithPasswordResponse { readonly session_token: string } -// From codersdk/deploymentconfig.go +// From codersdk/deployment.go export interface OAuth2Config { readonly github: OAuth2GithubConfig } -// From codersdk/deploymentconfig.go +// From codersdk/deployment.go export interface OAuth2GithubConfig { readonly client_id: DeploymentConfigField readonly client_secret: DeploymentConfigField @@ -477,7 +453,7 @@ export interface OAuth2GithubConfig { readonly enterprise_base_url: DeploymentConfigField } -// From codersdk/deploymentconfig.go +// From codersdk/deployment.go export interface OIDCConfig { readonly allow_signups: DeploymentConfigField readonly client_id: DeploymentConfigField @@ -497,7 +473,7 @@ export interface Organization { readonly updated_at: string } -// From codersdk/organizationmember.go +// From codersdk/organizations.go export interface OrganizationMember { readonly user_id: string readonly organization_id: string @@ -555,19 +531,19 @@ export interface PatchGroupRequest { readonly quota_allowance?: number } -// From codersdk/deploymentconfig.go +// From codersdk/deployment.go export interface PprofConfig { readonly enable: DeploymentConfigField readonly address: DeploymentConfigField } -// From codersdk/deploymentconfig.go +// From codersdk/deployment.go export interface PrometheusConfig { readonly enable: DeploymentConfigField readonly address: DeploymentConfigField } -// From codersdk/deploymentconfig.go +// From codersdk/deployment.go export interface ProvisionerConfig { readonly daemons: DeploymentConfigField readonly daemon_poll_interval: DeploymentConfigField @@ -614,7 +590,7 @@ export interface PutExtendWorkspaceRequest { readonly deadline: string } -// From codersdk/deploymentconfig.go +// From codersdk/deployment.go export interface RateLimitConfig { readonly disable_all: DeploymentConfigField readonly api: DeploymentConfigField @@ -631,7 +607,7 @@ export interface Replica { readonly database_latency: number } -// From codersdk/error.go +// From codersdk/client.go export interface Response { readonly message: string readonly detail?: string @@ -644,26 +620,26 @@ export interface Role { readonly display_name: string } -// From codersdk/sse.go +// From codersdk/serversentevents.go export interface ServerSentEvent { readonly type: ServerSentEventType // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO explain why this is needed readonly data: any } -// From codersdk/appearance.go +// From codersdk/deployment.go export interface ServiceBannerConfig { readonly enabled: boolean readonly message?: string readonly background_color?: string } -// From codersdk/deploymentconfig.go +// From codersdk/deployment.go export interface SwaggerConfig { readonly enable: DeploymentConfigField } -// From codersdk/deploymentconfig.go +// From codersdk/deployment.go export interface TLSConfig { readonly enable: DeploymentConfigField readonly address: DeploymentConfigField @@ -677,7 +653,7 @@ export interface TLSConfig { readonly client_key_file: DeploymentConfigField } -// From codersdk/deploymentconfig.go +// From codersdk/deployment.go export interface TelemetryConfig { readonly enable: DeploymentConfigField readonly trace: DeploymentConfigField @@ -784,7 +760,7 @@ export interface TemplateVersionsByTemplateRequest extends Pagination { readonly template_id: string } -// From codersdk/deploymentconfig.go +// From codersdk/deployment.go export interface TraceConfig { readonly enable: DeploymentConfigField readonly honeycomb_api_key: DeploymentConfigField @@ -802,11 +778,6 @@ export interface UpdateActiveTemplateVersion { readonly id: string } -// From codersdk/branding.go -export interface UpdateBrandingRequest { - readonly logo_url: string -} - // From codersdk/updatecheck.go export interface UpdateCheckResponse { readonly current: boolean @@ -890,7 +861,7 @@ export interface UsersRequest extends Pagination { readonly q?: string } -// From codersdk/error.go +// From codersdk/client.go export interface ValidationError { readonly field: string readonly detail: string @@ -943,33 +914,16 @@ export interface WorkspaceAgent { readonly startup_script_timeout_seconds: number } -// From codersdk/workspaceagents.go -export interface WorkspaceAgentGitAuthResponse { - readonly username: string - readonly password: string - readonly url: string +// From codersdk/workspaceagentconn.go +export interface WorkspaceAgentListeningPort { + readonly process_name: string + readonly network: string + readonly port: number } -// From codersdk/workspaceagents.go -export interface WorkspaceAgentInstanceMetadata { - readonly jail_orchestrator: string - readonly operating_system: string - readonly platform: string - readonly platform_family: string - readonly kernel_version: string - readonly kernel_architecture: string - readonly cloud: string - readonly jail: string - readonly vnc: boolean -} - -// From codersdk/workspaceagents.go -export interface WorkspaceAgentResourceMetadata { - readonly memory_total: number - readonly disk_total: number - readonly cpu_cores: number - readonly cpu_model: string - readonly cpu_mhz: number +// From codersdk/workspaceagentconn.go +export interface WorkspaceAgentListeningPortsResponse { + readonly ports: WorkspaceAgentListeningPort[] } // From codersdk/workspaceapps.go @@ -1032,7 +986,7 @@ export interface WorkspaceOptions { readonly include_deleted?: boolean } -// From codersdk/quota.go +// From codersdk/workspaces.go export interface WorkspaceQuota { readonly credits_consumed: number readonly budget: number @@ -1093,7 +1047,7 @@ export const BuildReasons: BuildReason[] = [ "initiator", ] -// From codersdk/features.go +// From codersdk/deployment.go export type Entitlement = "entitled" | "grace_period" | "not_entitled" export const Entitlements: Entitlement[] = [ "entitled", @@ -1101,11 +1055,11 @@ export const Entitlements: Entitlement[] = [ "not_entitled", ] -// From codersdk/experiments.go +// From codersdk/deployment.go export type Experiment = "authz_querier" export const Experiments: Experiment[] = ["authz_querier"] -// From codersdk/features.go +// From codersdk/deployment.go export type FeatureName = | "appearance" | "audit_log" @@ -1128,10 +1082,6 @@ export const FeatureNames: FeatureName[] = [ "user_limit", ] -// From codersdk/agentconn.go -export type ListeningPortNetwork = "tcp" -export const ListeningPortNetworks: ListeningPortNetwork[] = ["tcp"] - // From codersdk/provisionerdaemons.go export type LogLevel = "debug" | "error" | "info" | "trace" | "warn" export const LogLevels: LogLevel[] = ["debug", "error", "info", "trace", "warn"] @@ -1217,7 +1167,7 @@ export const ResourceTypes: ResourceType[] = [ "workspace_build", ] -// From codersdk/sse.go +// From codersdk/serversentevents.go export type ServerSentEventType = "data" | "error" | "ping" export const ServerSentEventTypes: ServerSentEventType[] = [ "data", @@ -1315,5 +1265,5 @@ export const WorkspaceTransitions: WorkspaceTransition[] = [ "stop", ] -// From codersdk/deploymentconfig.go +// From codersdk/deployment.go export type Flaggable = string | number | boolean | string[] | GitAuthConfig[] diff --git a/site/src/xServices/portForward/portForwardXService.ts b/site/src/xServices/portForward/portForwardXService.ts index 8c2494083c..cd908155db 100644 --- a/site/src/xServices/portForward/portForwardXService.ts +++ b/site/src/xServices/portForward/portForwardXService.ts @@ -1,5 +1,5 @@ import { getAgentListeningPorts } from "api/api" -import { ListeningPortsResponse } from "api/typesGenerated" +import { WorkspaceAgentListeningPortsResponse } from "api/typesGenerated" import { createMachine, assign } from "xstate" export const portForwardMachine = createMachine( @@ -9,11 +9,11 @@ export const portForwardMachine = createMachine( schema: { context: {} as { agentId: string - listeningPorts?: ListeningPortsResponse + listeningPorts?: WorkspaceAgentListeningPortsResponse }, services: {} as { getListeningPorts: { - data: ListeningPortsResponse + data: WorkspaceAgentListeningPortsResponse } }, }, diff --git a/site/src/xServices/workspace/workspaceXService.ts b/site/src/xServices/workspace/workspaceXService.ts index ec6cad2bae..bcb450d83d 100644 --- a/site/src/xServices/workspace/workspaceXService.ts +++ b/site/src/xServices/workspace/workspaceXService.ts @@ -155,7 +155,7 @@ export const workspaceMachine = createMachine( data: TypesGen.AuthorizationResponse } getApplicationsHost: { - data: TypesGen.GetAppHostResponse + data: TypesGen.AppHostResponse } }, },