From 9747757ac00fb29d0d3d1b26e4132d80951f757e Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Sun, 29 Jan 2023 17:02:12 +0000 Subject: [PATCH 01/18] chore: rename `AgentConn` to `WorkspaceAgentConn` The codersdk was becoming bloated with consts for the workspace agent that made no sense to a reader. `Tailnet*` is an example of these consts. --- agent/agent.go | 14 +- agent/agent_test.go | 4 +- agent/ports_supported.go | 14 +- agent/statsendpoint.go | 4 +- cli/portforward.go | 2 +- cli/root.go | 16 +- cli/scaletest.go | 2 +- cli/vscodessh.go | 2 +- coderd/workspaceagents.go | 10 +- coderd/workspaceagents_test.go | 12 +- coderd/workspaceapps.go | 4 +- coderd/workspaceapps_test.go | 2 +- coderd/wsconncache/wsconncache.go | 22 +- coderd/wsconncache/wsconncache_test.go | 12 +- codersdk/apikey.go | 6 +- .../{agentconn.go => workspaceagentconn.go} | 213 +++++++++--------- codersdk/workspaceagents.go | 12 +- scaletest/agentconn/run.go | 16 +- scaletest/createworkspaces/run_test.go | 2 +- scaletest/reconnectingpty/config.go | 2 +- scaletest/reconnectingpty/config_test.go | 2 +- scaletest/reconnectingpty/run_test.go | 16 +- 22 files changed, 205 insertions(+), 184 deletions(-) rename codersdk/{agentconn.go => workspaceagentconn.go} (54%) diff --git a/agent/agent.go b/agent/agent.go index 3083beb8ac00e..3144281f7cd22 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -373,7 +373,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 +387,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 +419,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 +450,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 +463,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 +491,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) } @@ -918,7 +918,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 384c967a7f438..0412aef7d5d7b 100644 --- a/agent/agent_test.go +++ b/agent/agent_test.go @@ -1077,7 +1077,7 @@ func (c closeFunc) Close() error { } func setupAgent(t *testing.T, metadata codersdk.WorkspaceAgentMetadata, ptyTimeout time.Duration) ( - *codersdk.AgentConn, + *codersdk.WorkspaceAgentConn, *client, <-chan *codersdk.AgentStats, afero.Fs, @@ -1131,7 +1131,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 } diff --git a/agent/ports_supported.go b/agent/ports_supported.go index 7c9449d1e2400..0b13f5d4b6d49 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/statsendpoint.go b/agent/statsendpoint.go index 0ddc01f70ddb5..65fece063e7ad 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/portforward.go b/cli/portforward.go index ea6edb2c9d89e..b3728212a904a 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 867b77e69d446..e2199ac7e4146 100644 --- a/cli/root.go +++ b/cli/root.go @@ -5,6 +5,7 @@ import ( "flag" "fmt" "io" + "net" "net/http" "net/url" "os" @@ -590,7 +591,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 +736,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 27a40b3ae1773..339bf45a130bb 100644 --- a/cli/scaletest.go +++ b/cli/scaletest.go @@ -668,7 +668,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/vscodessh.go b/cli/vscodessh.go index f4f27b832d084..adbbbdb5aab02 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/coderd/workspaceagents.go b/coderd/workspaceagents.go index 6bd848d072f3a..b4088f42facff 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -382,15 +382,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 +400,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 +467,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() diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go index b5974b6f5d35f..d4883d4c2dc97 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -517,10 +517,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 +560,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 +615,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 +637,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 +667,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) } diff --git a/coderd/workspaceapps.go b/coderd/workspaceapps.go index 23ef5170b8f3a..80ff95681dfe4 100644 --- a/coderd/workspaceapps.go +++ b/coderd/workspaceapps.go @@ -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 a9782dce323e5..8f9e5fb496315 100644 --- a/coderd/workspaceapps_test.go +++ b/coderd/workspaceapps_test.go @@ -858,7 +858,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() diff --git a/coderd/wsconncache/wsconncache.go b/coderd/wsconncache/wsconncache.go index d012e2d1f46cd..63e939955045c 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 37ff4fc0b8900..2c80bff88ba97 100644 --- a/coderd/wsconncache/wsconncache_test.go +++ b/coderd/wsconncache/wsconncache_test.go @@ -38,7 +38,7 @@ 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) { + cache := wsconncache.New(func(r *http.Request, id uuid.UUID) (*codersdk.WorkspaceAgentConn, error) { return setupAgent(t, codersdk.WorkspaceAgentMetadata{}, 0), nil }, 0) defer func() { @@ -53,7 +53,7 @@ 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 }, time.Microsecond) @@ -72,7 +72,7 @@ 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) { + cache := wsconncache.New(func(r *http.Request, id uuid.UUID) (*codersdk.WorkspaceAgentConn, error) { return setupAgent(t, codersdk.WorkspaceAgentMetadata{}, 0), nil }, time.Microsecond) defer func() { @@ -105,7 +105,7 @@ func TestCache(t *testing.T) { }() go server.Serve(random) - 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) { return setupAgent(t, codersdk.WorkspaceAgentMetadata{}, 0), nil }, time.Microsecond) defer func() { @@ -144,7 +144,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 codersdk.WorkspaceAgentMetadata, ptyTimeout time.Duration) *codersdk.WorkspaceAgentConn { metadata.DERPMap = tailnettest.RunDERPAndSTUN(t) coordinator := tailnet.NewCoordinator() @@ -182,7 +182,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, } } diff --git a/codersdk/apikey.go b/codersdk/apikey.go index 739fe7d255a3c..2d9f6b6d2108a 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" ) diff --git a/codersdk/agentconn.go b/codersdk/workspaceagentconn.go similarity index 54% rename from codersdk/agentconn.go rename to codersdk/workspaceagentconn.go index f5439bcdf4cfa..4eb6d277a1b3d 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,74 +111,78 @@ 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{}{} } } -// ReconnectingPTYRequest is sent from the client to the server -// to pipe data to a PTY. -// @typescript-ignore ReconnectingPTYRequest -type ReconnectingPTYRequest struct { - Data string `json:"data"` - Height uint16 `json:"height"` - Width uint16 `json:"width"` -} - -// @typescript-ignore AgentConn -type AgentConn struct { +// WorkspaceAgentConn represents a connection to a workspace agent. +// @typescript-ignore WorkspaceAgentConn +type WorkspaceAgentConn struct { *tailnet.Conn CloseFunc func() } -func (c *AgentConn) AwaitReachable(ctx context.Context) bool { +// 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, TailnetIP) + 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 *AgentConn) Ping(ctx context.Context) (time.Duration, bool, error) { +func (c *WorkspaceAgentConn) 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() + return c.Conn.Ping(ctx, WorkspaceAgentIP) } -func (c *AgentConn) Close() error { +// Close ends the connection to the workspace agent. +func (c *WorkspaceAgentConn) Close() error { if c.CloseFunc != nil { c.CloseFunc() } return c.Conn.Close() } -// @typescript-ignore ReconnectingPTYInit -type ReconnectingPTYInit struct { +// WorkspaceAgentReconnectingPTYInit initializes a new reconnecting PTY session. +// @typescript-ignore WorkspaceAgentReconnectingPTYInit +type WorkspaceAgentReconnectingPTYInit 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) { +// ReconnectingPTYRequest is sent from the client to the server +// to pipe data to a PTY. +// @typescript-ignore ReconnectingPTYRequest +type ReconnectingPTYRequest struct { + Data string `json:"data"` + Height uint16 `json:"height"` + Width uint16 `json:"width"` +} + +// 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() - 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 3d9a085b9f99a..386d702f264ac 100644 --- a/codersdk/workspaceagents.go +++ b/codersdk/workspaceagents.go @@ -413,7 +413,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{} } @@ -513,7 +513,7 @@ func (c *Client) DialWorkspaceAgent(ctx context.Context, agentID uuid.UUID, opti return nil, err } - return &AgentConn{ + return &WorkspaceAgentConn{ Conn: conn, CloseFunc: func() { cancelFunc() @@ -603,16 +603,16 @@ func (c *Client) WorkspaceAgentReconnectingPTY(ctx context.Context, agentID, rec // 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) } diff --git a/scaletest/agentconn/run.go b/scaletest/agentconn/run.go index 1a93977efe204..2229e6706c0d9 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/createworkspaces/run_test.go b/scaletest/createworkspaces/run_test.go index f52e18420dc0e..27ffd105340bd 100644 --- a/scaletest/createworkspaces/run_test.go +++ b/scaletest/createworkspaces/run_test.go @@ -133,7 +133,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 56bf7b9ac2ac6..924a3e885e635 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 1061b81691573..0c5200bfd7fe6 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 643e37abe0005..86a0747e04ba9 100644 --- a/scaletest/reconnectingpty/run_test.go +++ b/scaletest/reconnectingpty/run_test.go @@ -30,7 +30,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", }, @@ -59,7 +59,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, @@ -87,7 +87,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), @@ -111,7 +111,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), @@ -140,7 +140,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), @@ -165,7 +165,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), @@ -195,7 +195,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", @@ -219,7 +219,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", From fe02a9f6e091cd649d61afd69efccb090a0a6af6 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Sun, 29 Jan 2023 17:03:39 +0000 Subject: [PATCH 02/18] chore: remove `Get` prefix from *Client functions --- cli/tokens.go | 2 +- coderd/apikey_test.go | 10 +++++----- coderd/coderdtest/authorize.go | 2 +- coderd/users_test.go | 16 ++++++++-------- coderd/workspaceapps.go | 2 +- coderd/workspaceapps_test.go | 12 ++++++------ codersdk/apikey.go | 8 ++++---- codersdk/users.go | 4 ++-- codersdk/workspaces.go | 12 ++++++------ 9 files changed, 34 insertions(+), 34 deletions(-) diff --git a/cli/tokens.go b/cli/tokens.go index 9fd5362151655..4fce31360bf86 100644 --- a/cli/tokens.go +++ b/cli/tokens.go @@ -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/coderd/apikey_test.go b/coderd/apikey_test.go index daa632091c98b..6d4b710984b4e 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/coderdtest/authorize.go b/coderd/coderdtest/authorize.go index ab6a5a2db9f27..14019815d2a43 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/users_test.go b/coderd/users_test.go index f19e93548220e..1336754899a70 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") @@ -331,7 +331,7 @@ func TestPostLogout(t *testing.T) { } 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/workspaceapps.go b/coderd/workspaceapps.go index 80ff95681dfe4..b26a28f56f191 100644 --- a/coderd/workspaceapps.go +++ b/coderd/workspaceapps.go @@ -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, }) } diff --git a/coderd/workspaceapps_test.go b/coderd/workspaceapps_test.go index 8f9e5fb496315..796b432e011e9 100644 --- a/coderd/workspaceapps_test.go +++ b/coderd/workspaceapps_test.go @@ -96,12 +96,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) }) @@ -437,7 +437,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 +499,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) @@ -730,7 +730,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{ @@ -1049,7 +1049,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 == "-" { diff --git a/codersdk/apikey.go b/codersdk/apikey.go index 2d9f6b6d2108a..4f7f140720b95 100644 --- a/codersdk/apikey.go +++ b/codersdk/apikey.go @@ -84,8 +84,8 @@ func (c *Client) CreateAPIKey(ctx context.Context, user string) (GenerateAPIKeyR 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 @@ -98,8 +98,8 @@ func (c *Client) GetTokens(ctx context.Context, userID string) ([]APIKey, error) 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 diff --git a/codersdk/users.go b/codersdk/users.go index b940b0bc48d9b..d7d639bbdfcb4 100644 --- a/codersdk/users.go +++ b/codersdk/users.go @@ -253,8 +253,8 @@ func (c *Client) UpdateOrganizationMemberRoles(ctx context.Context, organization 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 diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index 45e73aaa5525e..902421e3c683f 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -350,29 +350,29 @@ func (c *Client) WorkspaceByOwnerAndName(ctx context.Context, owner string, name return workspace, json.NewDecoder(res.Body).Decode(&workspace) } -type GetAppHostResponse struct { +type AppHostResponse struct { // Host is the externally accessible URL for the Coder instance. Host string `json:"host"` } -// GetAppHost returns the site-wide application wildcard hostname without the +// 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) GetAppHost(ctx context.Context) (GetAppHostResponse, error) { +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 GetAppHostResponse{}, err + return AppHostResponse{}, err } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return GetAppHostResponse{}, readBodyAsError(res) + return AppHostResponse{}, readBodyAsError(res) } - var host GetAppHostResponse + var host AppHostResponse return host, json.NewDecoder(res.Body).Decode(&host) } From b9a0e2eeacf7117b6975a110619cfd97c75adb8f Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Sun, 29 Jan 2023 17:19:07 +0000 Subject: [PATCH 03/18] chore: remove `BypassRatelimits` option in `codersdk.Client` It feels wrong to have this as a direct option because it's so infrequently needed by API callers. It's better to directly modify headers in the two places that we actually use it. --- cli/scaletest.go | 19 +++- cli/tokens.go | 2 +- coderd/apikey.go | 2 +- coderd/audit.go | 2 +- coderd/authorize_test.go | 2 +- coderd/httpapi/cookie.go | 6 +- coderd/httpmw/apikey.go | 8 +- coderd/httpmw/apikey_test.go | 30 +++--- coderd/httpmw/authorize_test.go | 2 +- coderd/httpmw/csrf.go | 6 +- coderd/httpmw/oauth2.go | 10 +- coderd/httpmw/oauth2_test.go | 4 +- coderd/httpmw/organizationparam_test.go | 2 +- coderd/httpmw/ratelimit_test.go | 6 +- coderd/httpmw/templateparam_test.go | 2 +- coderd/httpmw/templateversionparam_test.go | 2 +- coderd/httpmw/userparam_test.go | 2 +- coderd/httpmw/workspaceagent.go | 2 +- coderd/httpmw/workspaceagent_test.go | 2 +- coderd/httpmw/workspaceagentparam_test.go | 2 +- coderd/httpmw/workspacebuildparam_test.go | 2 +- coderd/httpmw/workspaceparam_test.go | 4 +- coderd/userauth_test.go | 6 +- coderd/users.go | 2 +- coderd/users_test.go | 4 +- coderd/workspaceagents_test.go | 4 +- coderd/workspaceapps_test.go | 12 +-- codersdk/apikey.go | 4 +- codersdk/appearance.go | 2 + codersdk/audit.go | 4 +- codersdk/authorization.go | 4 +- codersdk/branding.go | 23 ----- codersdk/client.go | 113 +++++++++------------ codersdk/client_internal_test.go | 6 +- codersdk/provisionerdaemons.go | 4 +- codersdk/workspaceagents.go | 6 +- enterprise/coderd/authorize_test.go | 2 +- 37 files changed, 149 insertions(+), 166 deletions(-) delete mode 100644 codersdk/branding.go diff --git a/cli/scaletest.go b/cli/scaletest.go index 339bf45a130bb..8dbd270f28165 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") diff --git a/cli/tokens.go b/cli/tokens.go index 4fce31360bf86..847e4b718c89e 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 diff --git a/coderd/apikey.go b/coderd/apikey.go index babde27084d2a..5a8d7882eb7ad 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/audit.go b/coderd/audit.go index 947264d45dc8e..23a2375729b1e 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 b495bda4d0529..811ade7048623 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/httpapi/cookie.go b/coderd/httpapi/cookie.go index 7e0893c477bb1..06e5d6ed77779 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 ceb974c92c5ce..5899cc6e074ea 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 d5b5ac382c474..c4266646ed442 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 46678cb88403f..d715c7e313d10 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 b4f0880569187..ce25c600940b5 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 f335e9175eb77..820523b6befcb 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 b53755d21a2c0..40fcb9186e056 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 0a58911bda827..370456959952c 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 9e2ec370e8664..61a6f5d903566 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 1189877ded549..e4cbe60260977 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 36a916f1507ac..796ae7ec9bd9e 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 496e3c0f2c1e1..0beca8276850e 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 d2172430e004b..ea76ac8ad08f6 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 a33bc51bdce9e..b205ea6fdea52 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 d6d83c6ba8daf..fa31b60a00232 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 ef21e12788992..a664d3bb3fb02 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 e5d617b955a67..267d994a17bbc 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/userauth_test.go b/coderd/userauth_test.go index 0cb16afa3342c..ea707289639ec 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 bd2c991a03fdb..9ad4d208f3f5f 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 1336754899a70..7e6932073b5bd 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -323,8 +323,8 @@ 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 } diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go index d4883d4c2dc97..fc9eb0838bd22 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -1206,11 +1206,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) diff --git a/coderd/workspaceapps_test.go b/coderd/workspaceapps_test.go index 796b432e011e9..5c64fb765e063 100644 --- a/coderd/workspaceapps_test.go +++ b/coderd/workspaceapps_test.go @@ -134,7 +134,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) @@ -515,7 +515,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 +546,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() @@ -1087,7 +1087,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 +1526,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 +1578,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/codersdk/apikey.go b/codersdk/apikey.go index 4f7f140720b95..e9fa1c5abcf95 100644 --- a/codersdk/apikey.go +++ b/codersdk/apikey.go @@ -53,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 { diff --git a/codersdk/appearance.go b/codersdk/appearance.go index 07b69e67dc3f1..b6a8828e4da08 100644 --- a/codersdk/appearance.go +++ b/codersdk/appearance.go @@ -17,6 +17,8 @@ type ServiceBannerConfig struct { 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 { diff --git a/codersdk/audit.go b/codersdk/audit.go index e06b47d757e5a..c882b511da6b0 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" @@ -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 b16e924236501..80dc37523a7bc 100644 --- a/codersdk/authorization.go +++ b/codersdk/authorization.go @@ -56,7 +56,9 @@ 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 diff --git a/codersdk/branding.go b/codersdk/branding.go deleted file mode 100644 index fc24303511bf5..0000000000000 --- 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/client.go b/codersdk/client.go index d977623cda3ef..5a0c8a1deb493 100644 --- a/codersdk/client.go +++ b/codersdk/client.go @@ -28,17 +28,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 +69,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 bool - // BypassRatelimits is an optional flag that can be set by the site owner to - // disable ratelimit checks for the client. - BypassRatelimits bool + // LogBodies can be enabled to print request and response bodies to the logger. + LogBodies 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 +138,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 +151,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) @@ -244,19 +214,19 @@ func readBodyAsError(res *http.Response) error { 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 +248,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 +261,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 +277,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 +340,18 @@ func parseMimeType(contentType string) string { return mimeType } + +// 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 f48467718a761..9bac2c04c8afa 100644 --- a/codersdk/client_internal_test.go +++ b/codersdk/client_internal_test.go @@ -43,8 +43,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 +58,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 +81,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() diff --git a/codersdk/provisionerdaemons.go b/codersdk/provisionerdaemons.go index c1b85345d0ba2..c27704b111aeb 100644 --- a/codersdk/provisionerdaemons.go +++ b/codersdk/provisionerdaemons.go @@ -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{ @@ -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{ diff --git a/codersdk/workspaceagents.go b/codersdk/workspaceagents.go index 386d702f264ac..af3a3e3c13320 100644 --- a/codersdk/workspaceagents.go +++ b/codersdk/workspaceagents.go @@ -348,7 +348,7 @@ func (c *Client) ListenWorkspaceAgent(ctx context.Context) (net.Conn, error) { return nil, xerrors.Errorf("create cookie jar: %w", err) } jar.SetCookies(coordinateURL, []*http.Cookie{{ - Name: SessionTokenKey, + Name: SessionTokenCookie, Value: c.SessionToken(), }}) httpClient := &http.Client{ @@ -452,7 +452,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{ @@ -583,7 +583,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{ diff --git a/enterprise/coderd/authorize_test.go b/enterprise/coderd/authorize_test.go index ef181e5b985aa..c15c4e46835ab 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) }) From cb0656c48081b1fc7858aeab8eece6fa34a45edd Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Sun, 29 Jan 2023 17:20:06 +0000 Subject: [PATCH 04/18] Merge `appearance.go` and `buildinfo.go` into `deployment.go` --- codersdk/appearance.go | 45 ----------- codersdk/buildinfo.go | 44 ----------- .../{deploymentconfig.go => deployment.go} | 74 +++++++++++++++++++ 3 files changed, 74 insertions(+), 89 deletions(-) delete mode 100644 codersdk/appearance.go delete mode 100644 codersdk/buildinfo.go rename codersdk/{deploymentconfig.go => deployment.go} (84%) diff --git a/codersdk/appearance.go b/codersdk/appearance.go deleted file mode 100644 index b6a8828e4da08..0000000000000 --- a/codersdk/appearance.go +++ /dev/null @@ -1,45 +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"` -} - -// 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 -} diff --git a/codersdk/buildinfo.go b/codersdk/buildinfo.go deleted file mode 100644 index 19c7f21910ab3..0000000000000 --- 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/deploymentconfig.go b/codersdk/deployment.go similarity index 84% rename from codersdk/deploymentconfig.go rename to codersdk/deployment.go index 6aaa326cb86ea..f383a964b5d0e 100644 --- a/codersdk/deploymentconfig.go +++ b/codersdk/deployment.go @@ -4,8 +4,10 @@ import ( "context" "encoding/json" "net/http" + "strings" "time" + "golang.org/x/mod/semver" "golang.org/x/xerrors" ) @@ -240,3 +242,75 @@ func (c *Client) DeploymentConfig(ctx context.Context) (DeploymentConfig, error) 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) +} From ab01da73cacd8892302bf9040db96717ee92d83f Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Sun, 29 Jan 2023 17:21:02 +0000 Subject: [PATCH 05/18] Merge `experiments.go` and `features.go` into `deployment.go` --- codersdk/deployment.go | 138 ++++++++++++++++++++++++++++++++++++++++ codersdk/experiments.go | 54 ---------------- codersdk/features.go | 99 ---------------------------- 3 files changed, 138 insertions(+), 153 deletions(-) delete mode 100644 codersdk/experiments.go delete mode 100644 codersdk/features.go diff --git a/codersdk/deployment.go b/codersdk/deployment.go index f383a964b5d0e..a7ca7e068359a 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -11,6 +11,97 @@ import ( "golang.org/x/xerrors" ) +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) +} + // DeploymentConfig is the central configuration for the coder server. type DeploymentConfig struct { AccessURL *DeploymentConfigField[string] `json:"access_url" typescript:",notnull"` @@ -314,3 +405,50 @@ func (c *Client) BuildInfo(ctx context.Context) (BuildInfoResponse, error) { 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) +} diff --git a/codersdk/experiments.go b/codersdk/experiments.go deleted file mode 100644 index 1412f375e3998..0000000000000 --- 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 9ad04ea79afee..0000000000000 --- 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) -} From f69e3f08861310715774f184c0019e8ada77dcad Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Sun, 29 Jan 2023 17:24:02 +0000 Subject: [PATCH 06/18] Fix `make gen` referencing old type names --- coderd/apidoc/docs.go | 91 ++++++++++++---------------- coderd/apidoc/swagger.json | 87 ++++++++++++--------------- coderd/workspaceagents.go | 2 +- coderd/workspaceapps.go | 4 +- codersdk/deployment.go | 4 +- codersdk/workspaces.go | 10 ++-- docs/api/agents.md | 8 +-- docs/api/applications.md | 6 +- docs/api/schemas.md | 118 ++++++++++++++++--------------------- 9 files changed, 148 insertions(+), 182 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index d5d7dab0f5fdf..4c3f8f37e2c4b 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.WorkspaceAppHostResponse" } } } @@ -4262,7 +4262,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/codersdk.ListeningPortsResponse" + "$ref": "#/definitions/codersdk.WorkspaceAgentListeningPortsResponse" } } } @@ -6263,15 +6263,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": { @@ -6418,47 +6409,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": [ @@ -7891,6 +7841,34 @@ const docTemplate = `{ "WorkspaceAgentLifecycleReady" ] }, + "codersdk.WorkspaceAgentListeningPort": { + "type": "object", + "properties": { + "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.WorkspaceAgentListeningPort" + } + } + } + }, "codersdk.WorkspaceAgentMetadata": { "type": "object", "properties": { @@ -8019,6 +7997,15 @@ const docTemplate = `{ "WorkspaceAppHealthUnhealthy" ] }, + "codersdk.WorkspaceAppHostResponse": { + "type": "object", + "properties": { + "host": { + "description": "Host is the externally accessible URL for the Coder instance.", + "type": "string" + } + } + }, "codersdk.WorkspaceAppSharingLevel": { "type": "string", "enum": [ diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index ae4e160a335de..fab8c4ec3d1df 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.WorkspaceAppHostResponse" } } } @@ -3744,7 +3744,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/codersdk.ListeningPortsResponse" + "$ref": "#/definitions/codersdk.WorkspaceAgentListeningPortsResponse" } } } @@ -5588,15 +5588,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": { @@ -5741,43 +5732,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"], @@ -7100,6 +7054,34 @@ "WorkspaceAgentLifecycleReady" ] }, + "codersdk.WorkspaceAgentListeningPort": { + "type": "object", + "properties": { + "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.WorkspaceAgentListeningPort" + } + } + } + }, "codersdk.WorkspaceAgentMetadata": { "type": "object", "properties": { @@ -7214,6 +7196,15 @@ "WorkspaceAppHealthUnhealthy" ] }, + "codersdk.WorkspaceAppHostResponse": { + "type": "object", + "properties": { + "host": { + "description": "Host is the externally accessible URL for the Coder instance.", + "type": "string" + } + } + }, "codersdk.WorkspaceAppSharingLevel": { "type": "string", "enum": ["owner", "authenticated", "public"], diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index b4088f42facff..5e95b15f34402 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -299,7 +299,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() diff --git a/coderd/workspaceapps.go b/coderd/workspaceapps.go index b26a28f56f191..67dae91458702 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.WorkspaceAppHostResponse // @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.AppHostResponse{ + httpapi.Write(r.Context(), rw, http.StatusOK, codersdk.WorkspaceAppHostResponse{ Host: host, }) } diff --git a/codersdk/deployment.go b/codersdk/deployment.go index a7ca7e068359a..45524e92965a2 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -11,6 +11,7 @@ import ( "golang.org/x/xerrors" ) +// Entitlement represents whether a feature is licensed. type Entitlement string const ( @@ -19,7 +20,8 @@ const ( EntitlementNotEntitled Entitlement = "not_entitled" ) -// To add a new feature, modify this set of enums as well as the FeatureNames +// 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 diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index 902421e3c683f..152d645040de9 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -350,7 +350,7 @@ func (c *Client) WorkspaceByOwnerAndName(ctx context.Context, owner string, name return workspace, json.NewDecoder(res.Body).Decode(&workspace) } -type AppHostResponse struct { +type WorkspaceAppHostResponse struct { // Host is the externally accessible URL for the Coder instance. Host string `json:"host"` } @@ -361,18 +361,18 @@ type AppHostResponse struct { // "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) { +func (c *Client) AppHost(ctx context.Context) (WorkspaceAppHostResponse, error) { res, err := c.Request(ctx, http.MethodGet, "/api/v2/applications/host", nil) if err != nil { - return AppHostResponse{}, err + return WorkspaceAppHostResponse{}, err } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return AppHostResponse{}, readBodyAsError(res) + return WorkspaceAppHostResponse{}, readBodyAsError(res) } - var host AppHostResponse + var host WorkspaceAppHostResponse return host, json.NewDecoder(res.Body).Decode(&host) } diff --git a/docs/api/agents.md b/docs/api/agents.md index e4f746a188bd6..76f0b883c4138 100644 --- a/docs/api/agents.md +++ b/docs/api/agents.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 11026ae39d61d..3ddb7aafa9247 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.WorkspaceAppHostResponse](schemas.md#codersdkworkspaceapphostresponse) | 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 6eacdfaa5c633..64fe84cc4f75a 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -2504,20 +2504,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 @@ -2694,58 +2680,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 @@ -4915,6 +4849,44 @@ Parameter represents a set value for the scope. | `start_error` | | `ready` | +## codersdk.WorkspaceAgentListeningPort + +```json +{ + "network": "string", + "port": 0, + "process_name": "string" +} +``` + +### Properties + +| 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.WorkspaceAgentMetadata ```json @@ -5097,6 +5069,20 @@ Parameter represents a set value for the scope. | `healthy` | | `unhealthy` | +## codersdk.WorkspaceAppHostResponse + +```json +{ + "host": "string" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| ------ | ------ | -------- | ------------ | ------------------------------------------------------------- | +| `host` | string | false | | Host is the externally accessible URL for the Coder instance. | + ## codersdk.WorkspaceAppSharingLevel ```json From 267fedafe23d5b10fb198f8a8cf3b33fdc59b9f8 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Sun, 29 Jan 2023 17:25:25 +0000 Subject: [PATCH 07/18] Merge `error.go` into `client.go` `codersdk.Response` lived in `error.go`, which is wrong. --- codersdk/client.go | 51 +++++++++++++++++++++++++ codersdk/client_internal_test.go | 55 +++++++++++++++++++++++++++ codersdk/error.go | 58 ---------------------------- codersdk/error_test.go | 65 -------------------------------- 4 files changed, 106 insertions(+), 123 deletions(-) delete mode 100644 codersdk/error.go delete mode 100644 codersdk/error_test.go diff --git a/codersdk/client.go b/codersdk/client.go index 5a0c8a1deb493..9d830b79327cd 100644 --- a/codersdk/client.go +++ b/codersdk/client.go @@ -8,6 +8,7 @@ import ( "fmt" "io" "mime" + "net" "net/http" "net/url" "strings" @@ -341,6 +342,56 @@ 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) diff --git a/codersdk/client_internal_test.go b/codersdk/client_internal_test.go index 9bac2c04c8afa..f45c0592b6ac9 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() diff --git a/codersdk/error.go b/codersdk/error.go deleted file mode 100644 index f215bac67e90f..0000000000000 --- 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 d03024cbf1782..0000000000000 --- 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)) - }) - } -} From 44616605ff9535f8465a52fc175422fe77a2b734 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Sun, 29 Jan 2023 18:03:35 +0000 Subject: [PATCH 08/18] chore: refactor workspace agent functions into agentsdk It was odd conflating the codersdk that clients should use with functions that only the agent should use. This separates them into two SDKs that are closely coupled, but separate. --- .vscode/settings.json | 7 +- agent/agent.go | 33 +- agent/agent_test.go | 81 +-- agent/apphealth.go | 5 +- agent/apphealth_test.go | 3 +- cli/agent.go | 28 +- cli/configssh_test.go | 4 +- cli/gitaskpass.go | 4 +- cli/gitaskpass_test.go | 9 +- cli/gitssh.go | 2 +- cli/root.go | 5 +- cli/speedtest_test.go | 4 +- cli/ssh_test.go | 9 +- cli/vscodessh_test.go | 3 +- coderd/apidoc/docs.go | 393 +++++++------- coderd/apidoc/swagger.json | 377 +++++++------ coderd/coderdtest/coderdtest.go | 3 +- coderd/gitsshkey.go | 5 +- coderd/gitsshkey_test.go | 6 +- coderd/insights_test.go | 3 +- coderd/templates_test.go | 3 +- coderd/workspaceagents.go | 47 +- coderd/workspaceagents_test.go | 71 +-- coderd/workspaceapps_test.go | 5 +- coderd/workspaceresourceauth.go | 21 +- coderd/workspaceresourceauth_test.go | 26 +- coderd/workspaces_test.go | 5 +- coderd/wsconncache/wsconncache_test.go | 25 +- codersdk/agentsdk/agentsdk.go | 520 ++++++++++++++++++ codersdk/apikey.go | 10 +- codersdk/audit.go | 2 +- codersdk/authorization.go | 2 +- codersdk/client.go | 4 +- codersdk/client_internal_test.go | 2 +- codersdk/deployment.go | 12 +- codersdk/files.go | 4 +- codersdk/gitsshkey.go | 25 +- codersdk/groups.go | 12 +- codersdk/insights.go | 2 +- codersdk/licenses.go | 6 +- codersdk/organizations.go | 16 +- codersdk/parameters.go | 6 +- codersdk/provisionerdaemons.go | 6 +- codersdk/quota.go | 2 +- codersdk/replicas.go | 2 +- codersdk/roles.go | 4 +- codersdk/templates.go | 20 +- codersdk/templateversions.go | 22 +- codersdk/updatecheck.go | 2 +- codersdk/users.go | 34 +- codersdk/workspaceagentconn.go | 2 +- codersdk/workspaceagents.go | 484 +---------------- codersdk/workspaceagents_test.go | 16 +- codersdk/workspaceapps.go | 6 - codersdk/workspacebuilds.go | 10 +- codersdk/workspaces.go | 22 +- docs/api/agents.md | 60 +- docs/api/schemas.md | 632 +++++++++++----------- enterprise/coderd/workspaceagents_test.go | 5 +- scaletest/agentconn/run_test.go | 3 +- scaletest/createworkspaces/run_test.go | 3 +- scaletest/reconnectingpty/run_test.go | 3 +- scaletest/workspacebuild/run_test.go | 3 +- site/src/api/typesGenerated.ts | 138 ++--- 64 files changed, 1657 insertions(+), 1632 deletions(-) create mode 100644 codersdk/agentsdk/agentsdk.go diff --git a/.vscode/settings.json b/.vscode/settings.json index 1b09dba36ad09..23379db253904 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,7 @@ { "cSpell.words": [ "afero", + "agentsdk", "apps", "ASKPASS", "autostop", @@ -181,7 +182,11 @@ }, "eslint.workingDirectories": ["./site"], "files.exclude": { - "**/node_modules": true + "**/node_modules": true, + }, + "search.exclude": { + "scripts/metricsdocgen/metrics": true, + "docs/api/*.md": true, }, // Ensure files always have a newline. "files.insertFinalNewline": true, diff --git a/agent/agent.go b/agent/agent.go index 3144281f7cd22..54d17bb929e1a 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) }) @@ -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 { diff --git a/agent/agent_test.go b/agent/agent_test.go index 0412aef7d5d7b..3379c2dde250c 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) ( +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, @@ -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 88707a137f634..3d93b6c85ac26 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 14965f18ff45b..e09b1ca0cc9cc 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/cli/agent.go b/cli/agent.go index 2edc472bd6dc3..fe334c2c2de3f 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 d8fcae980b08c..ceb7e42317751 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 4d98b0e8da535..0296ca5266ced 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 344f95eef52a6..2e3bdc88505e7 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 09ebc396fdbde..02a985abee22f 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/root.go b/cli/root.go index e2199ac7e4146..76cc965d18735 100644 --- a/cli/root.go +++ b/cli/root.go @@ -32,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 ( @@ -334,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 @@ -347,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 } diff --git a/cli/speedtest_test.go b/cli/speedtest_test.go index aa31d1cd37f7b..b91577983c8c8 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 bf0aab1417fb4..1e9ddf2e8f221 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/vscodessh_test.go b/cli/vscodessh_test.go index 6af34913c97c9..83fd665bb5506 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 4c3f8f37e2c4b..12ad1c884b312 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -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" } } ], @@ -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,58 +5300,6 @@ const docTemplate = `{ } } }, - "codersdk.AgentGitSSHKey": { - "type": "object", - "properties": { - "private_key": { - "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" - } - } - }, "codersdk.AppearanceConfig": { "type": "object", "properties": { @@ -5402,21 +5517,6 @@ const docTemplate = `{ "type": "boolean" } }, - "codersdk.AzureInstanceIdentityToken": { - "type": "object", - "required": [ - "encoding", - "signature" - ], - "properties": { - "encoding": { - "type": "string" - }, - "signature": { - "type": "string" - } - } - }, "codersdk.BuildInfoResponse": { "type": "object", "properties": { @@ -6332,17 +6432,6 @@ const docTemplate = `{ } } }, - "codersdk.GoogleInstanceIdentityToken": { - "type": "object", - "required": [ - "json_web_token" - ], - "properties": { - "json_web_token": { - "type": "string" - } - } - }, "codersdk.Group": { "type": "object", "properties": { @@ -6786,35 +6875,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": { @@ -7794,14 +7854,6 @@ const docTemplate = `{ } } }, - "codersdk.WorkspaceAgentAuthenticateResponse": { - "type": "object", - "properties": { - "session_token": { - "type": "string" - } - } - }, "codersdk.WorkspaceAgentConnectionInfo": { "type": "object", "properties": { @@ -7810,20 +7862,6 @@ const docTemplate = `{ } } }, - "codersdk.WorkspaceAgentGitAuthResponse": { - "type": "object", - "properties": { - "password": { - "type": "string" - }, - "url": { - "type": "string" - }, - "username": { - "type": "string" - } - } - }, "codersdk.WorkspaceAgentLifecycle": { "type": "string", "enum": [ @@ -7869,45 +7907,6 @@ const docTemplate = `{ } } }, - "codersdk.WorkspaceAgentMetadata": { - "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" - } - } - }, "codersdk.WorkspaceAgentStatus": { "type": "string", "enum": [ diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index fab8c4ec3d1df..49bf1a088d308 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -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" } } ], @@ -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,58 +4687,6 @@ } } }, - "codersdk.AgentGitSSHKey": { - "type": "object", - "properties": { - "private_key": { - "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" - } - } - }, "codersdk.AppearanceConfig": { "type": "object", "properties": { @@ -4783,18 +4893,6 @@ "type": "boolean" } }, - "codersdk.AzureInstanceIdentityToken": { - "type": "object", - "required": ["encoding", "signature"], - "properties": { - "encoding": { - "type": "string" - }, - "signature": { - "type": "string" - } - } - }, "codersdk.BuildInfoResponse": { "type": "object", "properties": { @@ -5657,15 +5755,6 @@ } } }, - "codersdk.GoogleInstanceIdentityToken": { - "type": "object", - "required": ["json_web_token"], - "properties": { - "json_web_token": { - "type": "string" - } - } - }, "codersdk.Group": { "type": "object", "properties": { @@ -6053,35 +6142,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": { @@ -7013,14 +7073,6 @@ } } }, - "codersdk.WorkspaceAgentAuthenticateResponse": { - "type": "object", - "properties": { - "session_token": { - "type": "string" - } - } - }, "codersdk.WorkspaceAgentConnectionInfo": { "type": "object", "properties": { @@ -7029,20 +7081,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"], @@ -7082,45 +7120,6 @@ } } }, - "codersdk.WorkspaceAgentMetadata": { - "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" - } - } - }, "codersdk.WorkspaceAgentStatus": { "type": "string", "enum": ["connecting", "connected", "disconnected", "timeout"], diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index 5bc1012366333..38ca73ec5cfad 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 22f1a5e9e6c26..3380382f15afd 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 60dc60ac5cb7e..7a0932c5ec62b 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/insights_test.go b/coderd/insights_test.go index 08ac17bad246e..8eec448b62a43 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 0bd46ba180d64..73a846fe35991 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/workspaceagents.go b/coderd/workspaceagents.go index 5e95b15f34402..87456b6d82ad5 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 } @@ -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 fc9eb0838bd22..3fe824f0403f9 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, @@ -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, @@ -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_test.go b/coderd/workspaceapps_test.go index 5c64fb765e063..ad47c19231766 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" @@ -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", diff --git a/coderd/workspaceresourceauth.go b/coderd/workspaceresourceauth.go index 2ecc48a56a4c2..f8f9b3a32b795 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 5e39aa02a977d..d6c088de58507 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 58531d31d5e77..52784feaa16c4 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_test.go b/coderd/wsconncache/wsconncache_test.go index 2c80bff88ba97..45abb42033f8f 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" ) @@ -39,7 +40,7 @@ func TestCache(t *testing.T) { t.Run("Same", func(t *testing.T) { t.Parallel() cache := wsconncache.New(func(r *http.Request, id uuid.UUID) (*codersdk.WorkspaceAgentConn, error) { - return setupAgent(t, codersdk.WorkspaceAgentMetadata{}, 0), nil + return setupAgent(t, agentsdk.Metadata{}, 0), nil }, 0) defer func() { _ = cache.Close() @@ -55,7 +56,7 @@ func TestCache(t *testing.T) { called := atomic.NewInt32(0) 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() @@ -73,7 +74,7 @@ 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.WorkspaceAgentConn, error) { - return setupAgent(t, codersdk.WorkspaceAgentMetadata{}, 0), nil + return setupAgent(t, agentsdk.Metadata{}, 0), nil }, time.Microsecond) defer func() { _ = cache.Close() @@ -106,7 +107,7 @@ func TestCache(t *testing.T) { go server.Serve(random) cache := wsconncache.New(func(r *http.Request, id uuid.UUID) (*codersdk.WorkspaceAgentConn, error) { - return setupAgent(t, codersdk.WorkspaceAgentMetadata{}, 0), nil + 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.WorkspaceAgentConn { +func setupAgent(t *testing.T, metadata agentsdk.Metadata, ptyTimeout time.Duration) *codersdk.WorkspaceAgentConn { metadata.DERPMap = tailnettest.RunDERPAndSTUN(t) coordinator := tailnet.NewCoordinator() @@ -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 0000000000000..d09c4a5382534 --- /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 e9fa1c5abcf95..a9aeb3486dbe4 100644 --- a/codersdk/apikey.go +++ b/codersdk/apikey.go @@ -63,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 @@ -79,7 +79,7 @@ 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 @@ -94,7 +94,7 @@ func (c *Client) Tokens(ctx context.Context, userID string) ([]APIKey, error) { } 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) @@ -108,7 +108,7 @@ func (c *Client) APIKey(ctx context.Context, userID string, id string) (*APIKey, } 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) @@ -122,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/audit.go b/codersdk/audit.go index c882b511da6b0..38def7f709de3 100644 --- a/codersdk/audit.go +++ b/codersdk/audit.go @@ -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 diff --git a/codersdk/authorization.go b/codersdk/authorization.go index 80dc37523a7bc..2f4365e79396b 100644 --- a/codersdk/authorization.go +++ b/codersdk/authorization.go @@ -65,7 +65,7 @@ func (c *Client) AuthCheck(ctx context.Context, req AuthorizationRequest) (Autho } 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/client.go b/codersdk/client.go index 9d830b79327cd..dac6fdc533f1c 100644 --- a/codersdk/client.go +++ b/codersdk/client.go @@ -206,9 +206,9 @@ 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") } diff --git a/codersdk/client_internal_test.go b/codersdk/client_internal_test.go index f45c0592b6ac9..3dea65a24a070 100644 --- a/codersdk/client_internal_test.go +++ b/codersdk/client_internal_test.go @@ -293,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/deployment.go b/codersdk/deployment.go index 45524e92965a2..fda8ac5079f44 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -98,7 +98,7 @@ func (c *Client) Entitlements(ctx context.Context) (Entitlements, error) { } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return Entitlements{}, readBodyAsError(res) + return Entitlements{}, ReadBodyAsError(res) } var ent Entitlements return ent, json.NewDecoder(res.Body).Decode(&ent) @@ -329,7 +329,7 @@ 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 @@ -356,7 +356,7 @@ func (c *Client) Appearance(ctx context.Context) (AppearanceConfig, error) { } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return AppearanceConfig{}, readBodyAsError(res) + return AppearanceConfig{}, ReadBodyAsError(res) } var cfg AppearanceConfig return cfg, json.NewDecoder(res.Body).Decode(&cfg) @@ -369,7 +369,7 @@ func (c *Client) UpdateAppearance(ctx context.Context, appearance AppearanceConf } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return readBodyAsError(res) + return ReadBodyAsError(res) } return nil } @@ -401,7 +401,7 @@ func (c *Client) BuildInfo(ctx context.Context) (BuildInfoResponse, error) { defer res.Body.Close() if res.StatusCode != http.StatusOK { - return BuildInfoResponse{}, readBodyAsError(res) + return BuildInfoResponse{}, ReadBodyAsError(res) } var buildInfo BuildInfoResponse @@ -449,7 +449,7 @@ func (c *Client) Experiments(ctx context.Context) (Experiments, error) { } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return nil, readBodyAsError(res) + return nil, ReadBodyAsError(res) } var exp []Experiment return exp, json.NewDecoder(res.Body).Decode(&exp) diff --git a/codersdk/files.go b/codersdk/files.go index 91e0a25c571dc..cc66132df95e7 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 4a19110b5bb90..7b56e01427f85 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 b0608f3530a3d..3d9495eb074c2 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 index 77e1a2e100454..0e145bd6a5713 100644 --- a/codersdk/insights.go +++ b/codersdk/insights.go @@ -20,7 +20,7 @@ func (c *Client) DeploymentDAUs(ctx context.Context) (*DeploymentDAUsResponse, e defer res.Body.Close() if res.StatusCode != http.StatusOK { - return nil, readBodyAsError(res) + return nil, ReadBodyAsError(res) } var resp DeploymentDAUsResponse diff --git a/codersdk/licenses.go b/codersdk/licenses.go index 59319a326e48e..388055fd86bad 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/organizations.go b/codersdk/organizations.go index 425f3d14d5da5..f2de559e2f2d0 100644 --- a/codersdk/organizations.go +++ b/codersdk/organizations.go @@ -99,7 +99,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 +118,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 +138,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 +157,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 +176,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 +195,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 +214,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 +230,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 23b4408c2e9bf..6dbab6cdb8a60 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 c27704b111aeb..2a7ed58bb0ba0 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 @@ -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)) @@ -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 index 60ca4569aba0f..f816613c2fa98 100644 --- a/codersdk/quota.go +++ b/codersdk/quota.go @@ -19,7 +19,7 @@ func (c *Client) WorkspaceQuota(ctx context.Context, userID string) (WorkspaceQu } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return WorkspaceQuota{}, readBodyAsError(res) + 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 756ebcfcb756d..7263aba91b211 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 45f27f15bcb8a..5ed9a92539654 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/templates.go b/codersdk/templates.go index 44471a234e86f..faa9615a78a52 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 442e85b4e7d25..f7b456779c9e8 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 25f6bfc96e840..a3652494d2aa6 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 d7d639bbdfcb4..23b75bff340fd 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,7 +247,7 @@ 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) @@ -261,7 +261,7 @@ func (c *Client) UserRoles(ctx context.Context, user string) (UserRoles, error) } 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/workspaceagentconn.go b/codersdk/workspaceagentconn.go index 4eb6d277a1b3d..438c4032fa627 100644 --- a/codersdk/workspaceagentconn.go +++ b/codersdk/workspaceagentconn.go @@ -288,7 +288,7 @@ func (c *WorkspaceAgentConn) ListeningPorts(ctx context.Context) (WorkspaceAgent } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return WorkspaceAgentListeningPortsResponse{}, readBodyAsError(res) + return WorkspaceAgentListeningPortsResponse{}, ReadBodyAsError(res) } var resp WorkspaceAgentListeningPortsResponse diff --git a/codersdk/workspaceagents.go b/codersdk/workspaceagents.go index af3a3e3c13320..244fb95eee1ab 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: SessionTokenCookie, - 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 @@ -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) @@ -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 @@ -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. @@ -596,7 +256,7 @@ 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 } @@ -610,100 +270,12 @@ func (c *Client) WorkspaceAgentListeningPorts(ctx context.Context, agentID uuid. } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return WorkspaceAgentListeningPortsResponse{}, readBodyAsError(res) + return WorkspaceAgentListeningPortsResponse{}, ReadBodyAsError(res) } 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 e94181f12c730..869e22f3d54cf 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 a8bcb31b792d7..ba2e7255888cc 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 ffa875a2cc409..ab43379c28da8 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 152d645040de9..e093527001623 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,7 +343,7 @@ 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 @@ -369,7 +369,7 @@ func (c *Client) AppHost(ctx context.Context) (WorkspaceAppHostResponse, error) defer res.Body.Close() if res.StatusCode != http.StatusOK { - return WorkspaceAppHostResponse{}, readBodyAsError(res) + return WorkspaceAppHostResponse{}, ReadBodyAsError(res) } var host WorkspaceAppHostResponse diff --git a/docs/api/agents.md b/docs/api/agents.md index 76f0b883c4138..31f88ad590511 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). diff --git a/docs/api/schemas.md b/docs/api/schemas.md index 64fe84cc4f75a..e72e8da6859a7 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 @@ -70,140 +354,66 @@ "id": "string", "last_used": "2019-08-24T14:15:22Z", "lifetime_seconds": 0, - "login_type": "password", - "scope": "all", - "updated_at": "2019-08-24T14:15:22Z", - "user_id": "a169451c-8525-4352-b8ca-070dd449a1a5" -} -``` - -### Properties - -| Name | Type | Required | Restrictions | Description | -| ------------------ | -------------------------------------------- | -------- | ------------ | ----------- | -| `created_at` | string | true | | | -| `expires_at` | string | true | | | -| `id` | string | true | | | -| `last_used` | string | true | | | -| `lifetime_seconds` | integer | true | | | -| `login_type` | [codersdk.LoginType](#codersdklogintype) | true | | | -| `scope` | [codersdk.APIKeyScope](#codersdkapikeyscope) | true | | | -| `updated_at` | string | true | | | -| `user_id` | string | true | | | - -#### Enumerated Values - -| Property | Value | -| ------------ | --------------------- | -| `login_type` | `password` | -| `login_type` | `github` | -| `login_type` | `oidc` | -| `login_type` | `token` | -| `scope` | `all` | -| `scope` | `application_connect` | - -## codersdk.APIKeyScope - -```json -"all" -``` - -### Properties - -#### Enumerated Values - -| Value | -| --------------------- | -| `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 -{ - "license": "string" -} -``` - -### Properties - -| Name | Type | Required | Restrictions | Description | -| --------- | ------ | -------- | ------------ | ----------- | -| `license` | string | true | | | - -## codersdk.AgentGitSSHKey - -```json -{ - "private_key": "string", - "public_key": "string" + "login_type": "password", + "scope": "all", + "updated_at": "2019-08-24T14:15:22Z", + "user_id": "a169451c-8525-4352-b8ca-070dd449a1a5" } ``` ### Properties -| Name | Type | Required | Restrictions | Description | -| ------------- | ------ | -------- | ------------ | ----------- | -| `private_key` | string | false | | | -| `public_key` | string | false | | | +| Name | Type | Required | Restrictions | Description | +| ------------------ | -------------------------------------------- | -------- | ------------ | ----------- | +| `created_at` | string | true | | | +| `expires_at` | string | true | | | +| `id` | string | true | | | +| `last_used` | string | true | | | +| `lifetime_seconds` | integer | true | | | +| `login_type` | [codersdk.LoginType](#codersdklogintype) | true | | | +| `scope` | [codersdk.APIKeyScope](#codersdkapikeyscope) | true | | | +| `updated_at` | string | true | | | +| `user_id` | string | true | | | + +#### Enumerated Values + +| Property | Value | +| ------------ | --------------------- | +| `login_type` | `password` | +| `login_type` | `github` | +| `login_type` | `oidc` | +| `login_type` | `token` | +| `scope` | `all` | +| `scope` | `application_connect` | -## codersdk.AgentStats +## codersdk.APIKeyScope ```json -{ - "conns_by_proto": { - "property1": 0, - "property2": 0 - }, - "num_comms": 0, - "rx_bytes": 0, - "rx_packets": 0, - "tx_bytes": 0, - "tx_packets": 0 -} +"all" ``` ### 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. | +#### Enumerated Values + +| Value | +| --------------------- | +| `all` | +| `application_connect` | -## codersdk.AgentStatsResponse +## codersdk.AddLicenseRequest ```json { - "report_interval": 0 + "license": "string" } ``` ### 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 | +| --------- | ------ | -------- | ------------ | ----------- | +| `license` | string | true | | | ## codersdk.AppearanceConfig @@ -560,22 +770,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 @@ -2587,20 +2781,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 @@ -3297,38 +3477,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 @@ -4735,20 +4883,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 @@ -4813,24 +4947,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 @@ -4887,108 +5003,6 @@ Parameter represents a set value for the scope. | ------- | ------------------------------------------------------------------------------------- | -------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `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.WorkspaceAgentMetadata - -```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 | | | - ## codersdk.WorkspaceAgentStatus ```json diff --git a/enterprise/coderd/workspaceagents_test.go b/enterprise/coderd/workspaceagents_test.go index aaef3c28f999a..8cfae279954bb 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_test.go b/scaletest/agentconn/run_test.go index 433a614e6f5d6..c773432f82796 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 27ffd105340bd..d04baed5264ed 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, diff --git a/scaletest/reconnectingpty/run_test.go b/scaletest/reconnectingpty/run_test.go index 86a0747e04ba9..f6f70bbf574bf 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" @@ -277,7 +278,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 127273fd6fcc2..b26539b81eea2 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/typesGenerated.ts b/site/src/api/typesGenerated.ts index 62e590c65dc93..e8acd0b07c794 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,7 @@ export interface AgentStatsReportResponse { readonly tx_bytes: number } -// From codersdk/appearance.go +// From codersdk/deployment.go export interface AppearanceConfig { readonly logo_url: string readonly service_banner: ServiceBannerConfig @@ -118,13 +112,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 +242,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 +260,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 +270,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 +317,7 @@ export interface DeploymentConfig { readonly experimental: DeploymentConfigField } -// From codersdk/deploymentconfig.go +// From codersdk/deployment.go export interface DeploymentConfigField { readonly name: string readonly usage: string @@ -347,7 +335,7 @@ 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 +345,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 +361,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 +414,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 +432,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 +448,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 @@ -555,19 +526,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 +585,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 +602,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 @@ -651,19 +622,19 @@ export interface ServerSentEvent { 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 +648,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 +755,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 +773,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 +856,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 +909,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/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/workspaceagentconn.go +export interface WorkspaceAgentListeningPort { + readonly process_name: string + readonly network: string + readonly port: number } -// 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 @@ -987,6 +936,11 @@ export interface WorkspaceApp { readonly health: WorkspaceAppHealth } +// From codersdk/workspaces.go +export interface WorkspaceAppHostResponse { + readonly host: string +} + // From codersdk/workspacebuilds.go export interface WorkspaceBuild { readonly id: string @@ -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"] @@ -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[] From 3db805290210408bae3bfed40f0d42a32fad2054 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Sun, 29 Jan 2023 18:10:59 +0000 Subject: [PATCH 09/18] Merge `insights.go` into `deployment.go` --- codersdk/deployment.go | 19 +++++++++++++++++++ codersdk/insights.go | 28 ---------------------------- 2 files changed, 19 insertions(+), 28 deletions(-) delete mode 100644 codersdk/insights.go diff --git a/codersdk/deployment.go b/codersdk/deployment.go index fda8ac5079f44..56a9168b0b899 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -454,3 +454,22 @@ func (c *Client) Experiments(ctx context.Context) (Experiments, error) { 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) +} diff --git a/codersdk/insights.go b/codersdk/insights.go deleted file mode 100644 index 0e145bd6a5713..0000000000000 --- 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) -} From dc16848a1424d36315ed0b5e7e54f96cfe3b0178 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Sun, 29 Jan 2023 18:11:11 +0000 Subject: [PATCH 10/18] Merge `organizationmember.go` into `organizations.go` --- codersdk/organizationmember.go | 15 --------------- codersdk/organizations.go | 8 ++++++++ 2 files changed, 8 insertions(+), 15 deletions(-) delete mode 100644 codersdk/organizationmember.go diff --git a/codersdk/organizationmember.go b/codersdk/organizationmember.go deleted file mode 100644 index 4ec90fb29cbde..0000000000000 --- 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 f2de559e2f2d0..e4a07f6990619 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"` From 55c5977e4ecedec498f80055aa0887ddec0ecb73 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Sun, 29 Jan 2023 18:12:41 +0000 Subject: [PATCH 11/18] Merge `quota.go` into `workspaces.go` --- coderd/workspaceapps.go | 2 +- codersdk/deployment.go | 26 ++++++++++++++++++++++++++ codersdk/quota.go | 26 -------------------------- codersdk/workspaces.go | 26 +++++++++----------------- 4 files changed, 36 insertions(+), 44 deletions(-) delete mode 100644 codersdk/quota.go diff --git a/coderd/workspaceapps.go b/coderd/workspaceapps.go index 67dae91458702..b0c31577df2d5 100644 --- a/coderd/workspaceapps.go +++ b/coderd/workspaceapps.go @@ -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.WorkspaceAppHostResponse{ + httpapi.Write(r.Context(), rw, http.StatusOK, codersdk.AppHostResponse{ Host: host, }) } diff --git a/codersdk/deployment.go b/codersdk/deployment.go index 56a9168b0b899..3d158fe44c047 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -473,3 +473,29 @@ func (c *Client) DeploymentDAUs(ctx context.Context) (*DeploymentDAUsResponse, e 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/quota.go b/codersdk/quota.go deleted file mode 100644 index f816613c2fa98..0000000000000 --- 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/workspaces.go b/codersdk/workspaces.go index e093527001623..c41fb91f9752c 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -350,30 +350,22 @@ func (c *Client) WorkspaceByOwnerAndName(ctx context.Context, owner string, name return workspace, json.NewDecoder(res.Body).Decode(&workspace) } -type WorkspaceAppHostResponse 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"` } -// 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) (WorkspaceAppHostResponse, 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 WorkspaceAppHostResponse{}, err + return WorkspaceQuota{}, err } defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return WorkspaceAppHostResponse{}, ReadBodyAsError(res) + return WorkspaceQuota{}, ReadBodyAsError(res) } - - var host WorkspaceAppHostResponse - return host, json.NewDecoder(res.Body).Decode(&host) + var quota WorkspaceQuota + return quota, json.NewDecoder(res.Body).Decode("a) } // WorkspaceNotifyChannel is the PostgreSQL NOTIFY From f1944392074dd43931af957b1bfe7316d1b8ff37 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Sun, 29 Jan 2023 18:14:07 +0000 Subject: [PATCH 12/18] Rename `sse.go` to `serversentevents.go` --- codersdk/{sse.go => serversentevents.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename codersdk/{sse.go => serversentevents.go} (100%) diff --git a/codersdk/sse.go b/codersdk/serversentevents.go similarity index 100% rename from codersdk/sse.go rename to codersdk/serversentevents.go From af3f67a6874a00505250bbac7d3b9b952da2e5e2 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Sun, 29 Jan 2023 18:15:35 +0000 Subject: [PATCH 13/18] Rename `codersdk.WorkspaceAppHostResponse` to `codersdk.AppHostResponse` --- coderd/apidoc/docs.go | 20 ++++++++++---------- coderd/apidoc/swagger.json | 20 ++++++++++---------- coderd/workspaceapps.go | 2 +- docs/api/applications.md | 6 +++--- docs/api/schemas.md | 28 ++++++++++++++-------------- site/src/api/typesGenerated.ts | 20 ++++++++++---------- 6 files changed, 48 insertions(+), 48 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 12ad1c884b312..26df5b8feea97 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -153,7 +153,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/codersdk.WorkspaceAppHostResponse" + "$ref": "#/definitions/codersdk.AppHostResponse" } } } @@ -5300,6 +5300,15 @@ const docTemplate = `{ } } }, + "codersdk.AppHostResponse": { + "type": "object", + "properties": { + "host": { + "description": "Host is the externally accessible URL for the Coder instance.", + "type": "string" + } + } + }, "codersdk.AppearanceConfig": { "type": "object", "properties": { @@ -7996,15 +8005,6 @@ const docTemplate = `{ "WorkspaceAppHealthUnhealthy" ] }, - "codersdk.WorkspaceAppHostResponse": { - "type": "object", - "properties": { - "host": { - "description": "Host is the externally accessible URL for the Coder instance.", - "type": "string" - } - } - }, "codersdk.WorkspaceAppSharingLevel": { "type": "string", "enum": [ diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 49bf1a088d308..66a8369ff502d 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -125,7 +125,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/codersdk.WorkspaceAppHostResponse" + "$ref": "#/definitions/codersdk.AppHostResponse" } } } @@ -4687,6 +4687,15 @@ } } }, + "codersdk.AppHostResponse": { + "type": "object", + "properties": { + "host": { + "description": "Host is the externally accessible URL for the Coder instance.", + "type": "string" + } + } + }, "codersdk.AppearanceConfig": { "type": "object", "properties": { @@ -7195,15 +7204,6 @@ "WorkspaceAppHealthUnhealthy" ] }, - "codersdk.WorkspaceAppHostResponse": { - "type": "object", - "properties": { - "host": { - "description": "Host is the externally accessible URL for the Coder instance.", - "type": "string" - } - } - }, "codersdk.WorkspaceAppSharingLevel": { "type": "string", "enum": ["owner", "authenticated", "public"], diff --git a/coderd/workspaceapps.go b/coderd/workspaceapps.go index b0c31577df2d5..168345bc2d27e 100644 --- a/coderd/workspaceapps.go +++ b/coderd/workspaceapps.go @@ -77,7 +77,7 @@ const ( // @Security CoderSessionToken // @Produce json // @Tags Applications -// @Success 200 {object} codersdk.WorkspaceAppHostResponse +// @Success 200 {object} codersdk.AppHostResponse // @Router /applications/host [get] func (api *API) appHost(rw http.ResponseWriter, r *http.Request) { host := api.AppHostname diff --git a/docs/api/applications.md b/docs/api/applications.md index 3ddb7aafa9247..2aa3623122780 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.WorkspaceAppHostResponse](schemas.md#codersdkworkspaceapphostresponse) | +| 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 e72e8da6859a7..5166ff9c0b555 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -415,6 +415,20 @@ | --------- | ------ | -------- | ------------ | ----------- | | `license` | string | true | | | +## codersdk.AppHostResponse + +```json +{ + "host": "string" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| ------ | ------ | -------- | ------------ | ------------------------------------------------------------- | +| `host` | string | false | | Host is the externally accessible URL for the Coder instance. | + ## codersdk.AppearanceConfig ```json @@ -5083,20 +5097,6 @@ Parameter represents a set value for the scope. | `healthy` | | `unhealthy` | -## codersdk.WorkspaceAppHostResponse - -```json -{ - "host": "string" -} -``` - -### Properties - -| Name | Type | Required | Restrictions | Description | -| ------ | ------ | -------- | ------------ | ------------------------------------------------------------- | -| `host` | string | false | | Host is the externally accessible URL for the Coder instance. | - ## codersdk.WorkspaceAppSharingLevel ```json diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index e8acd0b07c794..dd4fcbf8f7c99 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -25,6 +25,11 @@ export interface AgentStatsReportResponse { readonly tx_bytes: number } +// From codersdk/deployment.go +export interface AppHostResponse { + readonly host: string +} + // From codersdk/deployment.go export interface AppearanceConfig { readonly logo_url: string @@ -330,7 +335,7 @@ export interface DeploymentConfigField { readonly value: T } -// From codersdk/insights.go +// From codersdk/deployment.go export interface DeploymentDAUsResponse { readonly entries: DAUEntry[] } @@ -468,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 @@ -615,7 +620,7 @@ 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 @@ -936,11 +941,6 @@ export interface WorkspaceApp { readonly health: WorkspaceAppHealth } -// From codersdk/workspaces.go -export interface WorkspaceAppHostResponse { - readonly host: string -} - // From codersdk/workspacebuilds.go export interface WorkspaceBuild { readonly id: string @@ -986,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 @@ -1167,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", From fe390b50161849a96a30c370bd199f928fa4d22f Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Sun, 29 Jan 2023 18:16:04 +0000 Subject: [PATCH 14/18] Format `.vscode/settings.json` --- .vscode/settings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 23379db253904..809770f4f1a13 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -182,11 +182,11 @@ }, "eslint.workingDirectories": ["./site"], "files.exclude": { - "**/node_modules": true, + "**/node_modules": true }, "search.exclude": { "scripts/metricsdocgen/metrics": true, - "docs/api/*.md": true, + "docs/api/*.md": true }, // Ensure files always have a newline. "files.insertFinalNewline": true, From faaf3019d2cb511bc486a8ac80d6a97e94102603 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Sun, 29 Jan 2023 18:21:20 +0000 Subject: [PATCH 15/18] Fix outdated naming in `api.ts` --- site/src/api/api.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 8333a01b3acbf..2aee82ed96c85 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`, ) From e06c021bf440bac937f24d13543bf3ed2c1ad3bf Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Sun, 29 Jan 2023 18:46:15 +0000 Subject: [PATCH 16/18] Fix app host response --- site/src/xServices/workspace/workspaceXService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/xServices/workspace/workspaceXService.ts b/site/src/xServices/workspace/workspaceXService.ts index ec6cad2bae0b7..bcb450d83d6b1 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 } }, }, From c17cdd05a190c83218d8a218dd801866ce8cea26 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Sun, 29 Jan 2023 21:34:49 +0000 Subject: [PATCH 17/18] Fix unsupported type --- agent/ports_unsupported.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/agent/ports_unsupported.go b/agent/ports_unsupported.go index 5f4c446f5e790..0ab26ac299736 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 } From b0d24020fc143918fcad9ddd1cf46ef7ca041251 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Sun, 29 Jan 2023 21:36:54 +0000 Subject: [PATCH 18/18] Fix imported type --- site/src/xServices/portForward/portForwardXService.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/site/src/xServices/portForward/portForwardXService.ts b/site/src/xServices/portForward/portForwardXService.ts index 8c2494083cbbe..cd908155dbcc1 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 } }, },