mirror of https://github.com/coder/coder.git
chore: move agent functions from `codersdk` into `agentsdk` (#5903)
* 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. * chore: remove `Get` prefix from *Client functions * 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. * Merge `appearance.go` and `buildinfo.go` into `deployment.go` * Merge `experiments.go` and `features.go` into `deployment.go` * Fix `make gen` referencing old type names * Merge `error.go` into `client.go` `codersdk.Response` lived in `error.go`, which is wrong. * 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. * Merge `insights.go` into `deployment.go` * Merge `organizationmember.go` into `organizations.go` * Merge `quota.go` into `workspaces.go` * Rename `sse.go` to `serversentevents.go` * Rename `codersdk.WorkspaceAppHostResponse` to `codersdk.AppHostResponse` * Format `.vscode/settings.json` * Fix outdated naming in `api.ts` * Fix app host response * Fix unsupported type * Fix imported type
This commit is contained in:
parent
e49f41652f
commit
7ad87505c8
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
"cSpell.words": [
|
"cSpell.words": [
|
||||||
"afero",
|
"afero",
|
||||||
|
"agentsdk",
|
||||||
"apps",
|
"apps",
|
||||||
"ASKPASS",
|
"ASKPASS",
|
||||||
"autostop",
|
"autostop",
|
||||||
|
@ -183,6 +184,10 @@
|
||||||
"files.exclude": {
|
"files.exclude": {
|
||||||
"**/node_modules": true
|
"**/node_modules": true
|
||||||
},
|
},
|
||||||
|
"search.exclude": {
|
||||||
|
"scripts/metricsdocgen/metrics": true,
|
||||||
|
"docs/api/*.md": true
|
||||||
|
},
|
||||||
// Ensure files always have a newline.
|
// Ensure files always have a newline.
|
||||||
"files.insertFinalNewline": true,
|
"files.insertFinalNewline": true,
|
||||||
"go.lintTool": "golangci-lint",
|
"go.lintTool": "golangci-lint",
|
||||||
|
|
|
@ -41,6 +41,7 @@ import (
|
||||||
"github.com/coder/coder/buildinfo"
|
"github.com/coder/coder/buildinfo"
|
||||||
"github.com/coder/coder/coderd/gitauth"
|
"github.com/coder/coder/coderd/gitauth"
|
||||||
"github.com/coder/coder/codersdk"
|
"github.com/coder/coder/codersdk"
|
||||||
|
"github.com/coder/coder/codersdk/agentsdk"
|
||||||
"github.com/coder/coder/pty"
|
"github.com/coder/coder/pty"
|
||||||
"github.com/coder/coder/tailnet"
|
"github.com/coder/coder/tailnet"
|
||||||
"github.com/coder/retry"
|
"github.com/coder/retry"
|
||||||
|
@ -68,12 +69,12 @@ type Options struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Client interface {
|
type Client interface {
|
||||||
WorkspaceAgentMetadata(ctx context.Context) (codersdk.WorkspaceAgentMetadata, error)
|
Metadata(ctx context.Context) (agentsdk.Metadata, error)
|
||||||
ListenWorkspaceAgent(ctx context.Context) (net.Conn, error)
|
Listen(ctx context.Context) (net.Conn, error)
|
||||||
AgentReportStats(ctx context.Context, log slog.Logger, stats func() *codersdk.AgentStats) (io.Closer, error)
|
ReportStats(ctx context.Context, log slog.Logger, stats func() *agentsdk.Stats) (io.Closer, error)
|
||||||
PostWorkspaceAgentLifecycle(ctx context.Context, state codersdk.PostWorkspaceAgentLifecycleRequest) error
|
PostLifecycle(ctx context.Context, state agentsdk.PostLifecycleRequest) error
|
||||||
PostWorkspaceAgentAppHealth(ctx context.Context, req codersdk.PostWorkspaceAppHealthsRequest) error
|
PostAppHealth(ctx context.Context, req agentsdk.PostAppHealthsRequest) error
|
||||||
PostWorkspaceAgentVersion(ctx context.Context, version string) error
|
PostVersion(ctx context.Context, version string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(options Options) io.Closer {
|
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))
|
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,
|
State: state,
|
||||||
})
|
})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
@ -226,12 +227,12 @@ func (a *agent) run(ctx context.Context) error {
|
||||||
}
|
}
|
||||||
a.sessionToken.Store(&sessionToken)
|
a.sessionToken.Store(&sessionToken)
|
||||||
|
|
||||||
err = a.client.PostWorkspaceAgentVersion(ctx, buildinfo.Version())
|
err = a.client.PostVersion(ctx, buildinfo.Version())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.Errorf("update workspace agent version: %w", err)
|
return xerrors.Errorf("update workspace agent version: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
metadata, err := a.client.WorkspaceAgentMetadata(ctx)
|
metadata, err := a.client.Metadata(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.Errorf("fetch metadata: %w", err)
|
return xerrors.Errorf("fetch metadata: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -300,7 +301,7 @@ func (a *agent) run(ctx context.Context) error {
|
||||||
appReporterCtx, appReporterCtxCancel := context.WithCancel(ctx)
|
appReporterCtx, appReporterCtxCancel := context.WithCancel(ctx)
|
||||||
defer appReporterCtxCancel()
|
defer appReporterCtxCancel()
|
||||||
go NewWorkspaceAppHealthReporter(
|
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))
|
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.
|
// 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()
|
stats := network.ExtractTrafficStats()
|
||||||
return convertAgentStats(stats)
|
return convertAgentStats(stats)
|
||||||
})
|
})
|
||||||
|
@ -373,7 +374,7 @@ func (a *agent) trackConnGoroutine(fn func()) error {
|
||||||
|
|
||||||
func (a *agent) createTailnet(ctx context.Context, derpMap *tailcfg.DERPMap) (_ *tailnet.Conn, err error) {
|
func (a *agent) createTailnet(ctx context.Context, derpMap *tailcfg.DERPMap) (_ *tailnet.Conn, err error) {
|
||||||
network, err := tailnet.NewConn(&tailnet.Options{
|
network, err := tailnet.NewConn(&tailnet.Options{
|
||||||
Addresses: []netip.Prefix{netip.PrefixFrom(codersdk.TailnetIP, 128)},
|
Addresses: []netip.Prefix{netip.PrefixFrom(codersdk.WorkspaceAgentIP, 128)},
|
||||||
DERPMap: derpMap,
|
DERPMap: derpMap,
|
||||||
Logger: a.logger.Named("tailnet"),
|
Logger: a.logger.Named("tailnet"),
|
||||||
EnableTrafficStats: true,
|
EnableTrafficStats: true,
|
||||||
|
@ -387,7 +388,7 @@ func (a *agent) createTailnet(ctx context.Context, derpMap *tailcfg.DERPMap) (_
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
sshListener, err := network.Listen("tcp", ":"+strconv.Itoa(codersdk.TailnetSSHPort))
|
sshListener, err := network.Listen("tcp", ":"+strconv.Itoa(codersdk.WorkspaceAgentSSHPort))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, xerrors.Errorf("listen on the ssh port: %w", err)
|
return nil, xerrors.Errorf("listen on the ssh port: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -419,7 +420,7 @@ func (a *agent) createTailnet(ctx context.Context, derpMap *tailcfg.DERPMap) (_
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
reconnectingPTYListener, err := network.Listen("tcp", ":"+strconv.Itoa(codersdk.TailnetReconnectingPTYPort))
|
reconnectingPTYListener, err := network.Listen("tcp", ":"+strconv.Itoa(codersdk.WorkspaceAgentReconnectingPTYPort))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, xerrors.Errorf("listen for reconnecting pty: %w", err)
|
return nil, xerrors.Errorf("listen for reconnecting pty: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -450,7 +451,7 @@ func (a *agent) createTailnet(ctx context.Context, derpMap *tailcfg.DERPMap) (_
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
var msg codersdk.ReconnectingPTYInit
|
var msg codersdk.WorkspaceAgentReconnectingPTYInit
|
||||||
err = json.Unmarshal(data, &msg)
|
err = json.Unmarshal(data, &msg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
|
@ -463,7 +464,7 @@ func (a *agent) createTailnet(ctx context.Context, derpMap *tailcfg.DERPMap) (_
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
speedtestListener, err := network.Listen("tcp", ":"+strconv.Itoa(codersdk.TailnetSpeedtestPort))
|
speedtestListener, err := network.Listen("tcp", ":"+strconv.Itoa(codersdk.WorkspaceAgentSpeedtestPort))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, xerrors.Errorf("listen for speedtest: %w", err)
|
return nil, xerrors.Errorf("listen for speedtest: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -491,7 +492,7 @@ func (a *agent) createTailnet(ctx context.Context, derpMap *tailcfg.DERPMap) (_
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
statisticsListener, err := network.Listen("tcp", ":"+strconv.Itoa(codersdk.TailnetStatisticsPort))
|
statisticsListener, err := network.Listen("tcp", ":"+strconv.Itoa(codersdk.WorkspaceAgentStatisticsPort))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, xerrors.Errorf("listen for statistics: %w", err)
|
return nil, xerrors.Errorf("listen for statistics: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -531,7 +532,7 @@ func (a *agent) runCoordinator(ctx context.Context, network *tailnet.Conn) error
|
||||||
ctx, cancel := context.WithCancel(ctx)
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
coordinator, err := a.client.ListenWorkspaceAgent(ctx)
|
coordinator, err := a.client.Listen(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -700,8 +701,8 @@ func (a *agent) init(ctx context.Context) {
|
||||||
go a.runLoop(ctx)
|
go a.runLoop(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertAgentStats(counts map[netlogtype.Connection]netlogtype.Counts) *codersdk.AgentStats {
|
func convertAgentStats(counts map[netlogtype.Connection]netlogtype.Counts) *agentsdk.Stats {
|
||||||
stats := &codersdk.AgentStats{
|
stats := &agentsdk.Stats{
|
||||||
ConnsByProto: map[string]int64{},
|
ConnsByProto: map[string]int64{},
|
||||||
NumConns: int64(len(counts)),
|
NumConns: int64(len(counts)),
|
||||||
}
|
}
|
||||||
|
@ -736,7 +737,7 @@ func (a *agent) createCommand(ctx context.Context, rawCommand string, env []stri
|
||||||
if rawMetadata == nil {
|
if rawMetadata == nil {
|
||||||
return nil, xerrors.Errorf("no metadata was provided: %w", err)
|
return nil, xerrors.Errorf("no metadata was provided: %w", err)
|
||||||
}
|
}
|
||||||
metadata, valid := rawMetadata.(codersdk.WorkspaceAgentMetadata)
|
metadata, valid := rawMetadata.(agentsdk.Metadata)
|
||||||
if !valid {
|
if !valid {
|
||||||
return nil, xerrors.Errorf("metadata is the wrong type: %T", metadata)
|
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()
|
session.DisablePTYEmulation()
|
||||||
|
|
||||||
if !isQuietLogin(session.RawCommand()) {
|
if !isQuietLogin(session.RawCommand()) {
|
||||||
metadata, ok := a.metadata.Load().(codersdk.WorkspaceAgentMetadata)
|
metadata, ok := a.metadata.Load().(agentsdk.Metadata)
|
||||||
if ok {
|
if ok {
|
||||||
err = showMOTD(session, metadata.MOTDFile)
|
err = showMOTD(session, metadata.MOTDFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -918,7 +919,7 @@ func (a *agent) handleSSHSession(session ssh.Session) (retErr error) {
|
||||||
return cmd.Wait()
|
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()
|
defer conn.Close()
|
||||||
|
|
||||||
connectionID := uuid.NewString()
|
connectionID := uuid.NewString()
|
||||||
|
|
|
@ -42,6 +42,7 @@ import (
|
||||||
"cdr.dev/slog/sloggers/slogtest"
|
"cdr.dev/slog/sloggers/slogtest"
|
||||||
"github.com/coder/coder/agent"
|
"github.com/coder/coder/agent"
|
||||||
"github.com/coder/coder/codersdk"
|
"github.com/coder/coder/codersdk"
|
||||||
|
"github.com/coder/coder/codersdk/agentsdk"
|
||||||
"github.com/coder/coder/pty/ptytest"
|
"github.com/coder/coder/pty/ptytest"
|
||||||
"github.com/coder/coder/tailnet"
|
"github.com/coder/coder/tailnet"
|
||||||
"github.com/coder/coder/tailnet/tailnettest"
|
"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)
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
conn, _, stats, _ := setupAgent(t, codersdk.WorkspaceAgentMetadata{}, 0)
|
conn, _, stats, _ := setupAgent(t, agentsdk.Metadata{}, 0)
|
||||||
|
|
||||||
sshClient, err := conn.SSHClient(ctx)
|
sshClient, err := conn.SSHClient(ctx)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -67,7 +68,7 @@ func TestAgent_Stats_SSH(t *testing.T) {
|
||||||
defer session.Close()
|
defer session.Close()
|
||||||
require.NoError(t, session.Run("echo test"))
|
require.NoError(t, session.Run("echo test"))
|
||||||
|
|
||||||
var s *codersdk.AgentStats
|
var s *agentsdk.Stats
|
||||||
require.Eventuallyf(t, func() bool {
|
require.Eventuallyf(t, func() bool {
|
||||||
var ok bool
|
var ok bool
|
||||||
s, ok = <-stats
|
s, ok = <-stats
|
||||||
|
@ -83,7 +84,7 @@ func TestAgent_Stats_ReconnectingPTY(t *testing.T) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||||
defer cancel()
|
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")
|
ptyConn, err := conn.ReconnectingPTY(ctx, uuid.New(), 128, 128, "/bin/bash")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -96,7 +97,7 @@ func TestAgent_Stats_ReconnectingPTY(t *testing.T) {
|
||||||
_, err = ptyConn.Write(data)
|
_, err = ptyConn.Write(data)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var s *codersdk.AgentStats
|
var s *agentsdk.Stats
|
||||||
require.Eventuallyf(t, func() bool {
|
require.Eventuallyf(t, func() bool {
|
||||||
var ok bool
|
var ok bool
|
||||||
s, ok = <-stats
|
s, ok = <-stats
|
||||||
|
@ -108,7 +109,7 @@ func TestAgent_Stats_ReconnectingPTY(t *testing.T) {
|
||||||
|
|
||||||
func TestAgent_SessionExec(t *testing.T) {
|
func TestAgent_SessionExec(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
session := setupSSHSession(t, codersdk.WorkspaceAgentMetadata{})
|
session := setupSSHSession(t, agentsdk.Metadata{})
|
||||||
|
|
||||||
command := "echo test"
|
command := "echo test"
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
|
@ -121,7 +122,7 @@ func TestAgent_SessionExec(t *testing.T) {
|
||||||
|
|
||||||
func TestAgent_GitSSH(t *testing.T) {
|
func TestAgent_GitSSH(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
session := setupSSHSession(t, codersdk.WorkspaceAgentMetadata{})
|
session := setupSSHSession(t, agentsdk.Metadata{})
|
||||||
command := "sh -c 'echo $GIT_SSH_COMMAND'"
|
command := "sh -c 'echo $GIT_SSH_COMMAND'"
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
command = "cmd.exe /c echo %GIT_SSH_COMMAND%"
|
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.
|
// it seems like it could be either.
|
||||||
t.Skip("ConPTY appears to be inconsistent on Windows.")
|
t.Skip("ConPTY appears to be inconsistent on Windows.")
|
||||||
}
|
}
|
||||||
session := setupSSHSession(t, codersdk.WorkspaceAgentMetadata{})
|
session := setupSSHSession(t, agentsdk.Metadata{})
|
||||||
command := "sh"
|
command := "sh"
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
command = "cmd.exe"
|
command = "cmd.exe"
|
||||||
|
@ -164,7 +165,7 @@ func TestAgent_SessionTTYShell(t *testing.T) {
|
||||||
|
|
||||||
func TestAgent_SessionTTYExitCode(t *testing.T) {
|
func TestAgent_SessionTTYExitCode(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
session := setupSSHSession(t, codersdk.WorkspaceAgentMetadata{})
|
session := setupSSHSession(t, agentsdk.Metadata{})
|
||||||
command := "areallynotrealcommand"
|
command := "areallynotrealcommand"
|
||||||
err := session.RequestPty("xterm", 128, 128, ssh.TerminalModes{})
|
err := session.RequestPty("xterm", 128, 128, ssh.TerminalModes{})
|
||||||
require.NoError(t, err)
|
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.
|
// Set HOME so we can ensure no ~/.hushlogin is present.
|
||||||
t.Setenv("HOME", tmpdir)
|
t.Setenv("HOME", tmpdir)
|
||||||
|
|
||||||
session := setupSSHSession(t, codersdk.WorkspaceAgentMetadata{
|
session := setupSSHSession(t, agentsdk.Metadata{
|
||||||
MOTDFile: name,
|
MOTDFile: name,
|
||||||
})
|
})
|
||||||
err = session.RequestPty("xterm", 128, 128, ssh.TerminalModes{})
|
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.
|
// Set HOME so we can ensure ~/.hushlogin is present.
|
||||||
t.Setenv("HOME", tmpdir)
|
t.Setenv("HOME", tmpdir)
|
||||||
|
|
||||||
session := setupSSHSession(t, codersdk.WorkspaceAgentMetadata{
|
session := setupSSHSession(t, agentsdk.Metadata{
|
||||||
MOTDFile: name,
|
MOTDFile: name,
|
||||||
})
|
})
|
||||||
err = session.RequestPty("xterm", 128, 128, ssh.TerminalModes{})
|
err = session.RequestPty("xterm", 128, 128, ssh.TerminalModes{})
|
||||||
|
@ -530,7 +531,7 @@ func TestAgent_SFTP(t *testing.T) {
|
||||||
home = "/" + strings.ReplaceAll(home, "\\", "/")
|
home = "/" + strings.ReplaceAll(home, "\\", "/")
|
||||||
}
|
}
|
||||||
//nolint:dogsled
|
//nolint:dogsled
|
||||||
conn, _, _, _ := setupAgent(t, codersdk.WorkspaceAgentMetadata{}, 0)
|
conn, _, _, _ := setupAgent(t, agentsdk.Metadata{}, 0)
|
||||||
sshClient, err := conn.SSHClient(ctx)
|
sshClient, err := conn.SSHClient(ctx)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer sshClient.Close()
|
defer sshClient.Close()
|
||||||
|
@ -562,7 +563,7 @@ func TestAgent_SCP(t *testing.T) {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
//nolint:dogsled
|
//nolint:dogsled
|
||||||
conn, _, _, _ := setupAgent(t, codersdk.WorkspaceAgentMetadata{}, 0)
|
conn, _, _, _ := setupAgent(t, agentsdk.Metadata{}, 0)
|
||||||
sshClient, err := conn.SSHClient(ctx)
|
sshClient, err := conn.SSHClient(ctx)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer sshClient.Close()
|
defer sshClient.Close()
|
||||||
|
@ -581,7 +582,7 @@ func TestAgent_EnvironmentVariables(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
key := "EXAMPLE"
|
key := "EXAMPLE"
|
||||||
value := "value"
|
value := "value"
|
||||||
session := setupSSHSession(t, codersdk.WorkspaceAgentMetadata{
|
session := setupSSHSession(t, agentsdk.Metadata{
|
||||||
EnvironmentVariables: map[string]string{
|
EnvironmentVariables: map[string]string{
|
||||||
key: value,
|
key: value,
|
||||||
},
|
},
|
||||||
|
@ -598,7 +599,7 @@ func TestAgent_EnvironmentVariables(t *testing.T) {
|
||||||
func TestAgent_EnvironmentVariableExpansion(t *testing.T) {
|
func TestAgent_EnvironmentVariableExpansion(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
key := "EXAMPLE"
|
key := "EXAMPLE"
|
||||||
session := setupSSHSession(t, codersdk.WorkspaceAgentMetadata{
|
session := setupSSHSession(t, agentsdk.Metadata{
|
||||||
EnvironmentVariables: map[string]string{
|
EnvironmentVariables: map[string]string{
|
||||||
key: "$SOMETHINGNOTSET",
|
key: "$SOMETHINGNOTSET",
|
||||||
},
|
},
|
||||||
|
@ -625,7 +626,7 @@ func TestAgent_CoderEnvVars(t *testing.T) {
|
||||||
t.Run(key, func(t *testing.T) {
|
t.Run(key, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
session := setupSSHSession(t, codersdk.WorkspaceAgentMetadata{})
|
session := setupSSHSession(t, agentsdk.Metadata{})
|
||||||
command := "sh -c 'echo $" + key + "'"
|
command := "sh -c 'echo $" + key + "'"
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
command = "cmd.exe /c echo %" + key + "%"
|
command = "cmd.exe /c echo %" + key + "%"
|
||||||
|
@ -648,7 +649,7 @@ func TestAgent_SSHConnectionEnvVars(t *testing.T) {
|
||||||
t.Run(key, func(t *testing.T) {
|
t.Run(key, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
session := setupSSHSession(t, codersdk.WorkspaceAgentMetadata{})
|
session := setupSSHSession(t, agentsdk.Metadata{})
|
||||||
command := "sh -c 'echo $" + key + "'"
|
command := "sh -c 'echo $" + key + "'"
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
command = "cmd.exe /c echo %" + key + "%"
|
command = "cmd.exe /c echo %" + key + "%"
|
||||||
|
@ -667,7 +668,7 @@ func TestAgent_StartupScript(t *testing.T) {
|
||||||
}
|
}
|
||||||
content := "output"
|
content := "output"
|
||||||
//nolint:dogsled
|
//nolint:dogsled
|
||||||
_, _, _, fs := setupAgent(t, codersdk.WorkspaceAgentMetadata{
|
_, _, _, fs := setupAgent(t, agentsdk.Metadata{
|
||||||
StartupScript: "echo " + content,
|
StartupScript: "echo " + content,
|
||||||
}, 0)
|
}, 0)
|
||||||
var gotContent string
|
var gotContent string
|
||||||
|
@ -701,7 +702,7 @@ func TestAgent_Lifecycle(t *testing.T) {
|
||||||
t.Run("Timeout", func(t *testing.T) {
|
t.Run("Timeout", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
_, client, _, _ := setupAgent(t, codersdk.WorkspaceAgentMetadata{
|
_, client, _, _ := setupAgent(t, agentsdk.Metadata{
|
||||||
StartupScript: "sleep 10",
|
StartupScript: "sleep 10",
|
||||||
StartupScriptTimeout: time.Nanosecond,
|
StartupScriptTimeout: time.Nanosecond,
|
||||||
}, 0)
|
}, 0)
|
||||||
|
@ -730,7 +731,7 @@ func TestAgent_Lifecycle(t *testing.T) {
|
||||||
t.Run("Error", func(t *testing.T) {
|
t.Run("Error", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
_, client, _, _ := setupAgent(t, codersdk.WorkspaceAgentMetadata{
|
_, client, _, _ := setupAgent(t, agentsdk.Metadata{
|
||||||
StartupScript: "false",
|
StartupScript: "false",
|
||||||
StartupScriptTimeout: 30 * time.Second,
|
StartupScriptTimeout: 30 * time.Second,
|
||||||
}, 0)
|
}, 0)
|
||||||
|
@ -759,7 +760,7 @@ func TestAgent_Lifecycle(t *testing.T) {
|
||||||
t.Run("Ready", func(t *testing.T) {
|
t.Run("Ready", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
_, client, _, _ := setupAgent(t, codersdk.WorkspaceAgentMetadata{
|
_, client, _, _ := setupAgent(t, agentsdk.Metadata{
|
||||||
StartupScript: "true",
|
StartupScript: "true",
|
||||||
StartupScriptTimeout: 30 * time.Second,
|
StartupScriptTimeout: 30 * time.Second,
|
||||||
}, 0)
|
}, 0)
|
||||||
|
@ -799,7 +800,7 @@ func TestAgent_ReconnectingPTY(t *testing.T) {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
//nolint:dogsled
|
//nolint:dogsled
|
||||||
conn, _, _, _ := setupAgent(t, codersdk.WorkspaceAgentMetadata{}, 0)
|
conn, _, _, _ := setupAgent(t, agentsdk.Metadata{}, 0)
|
||||||
id := uuid.New()
|
id := uuid.New()
|
||||||
netConn, err := conn.ReconnectingPTY(ctx, id, 100, 100, "/bin/bash")
|
netConn, err := conn.ReconnectingPTY(ctx, id, 100, 100, "/bin/bash")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -901,7 +902,7 @@ func TestAgent_Dial(t *testing.T) {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
//nolint:dogsled
|
//nolint:dogsled
|
||||||
conn, _, _, _ := setupAgent(t, codersdk.WorkspaceAgentMetadata{}, 0)
|
conn, _, _, _ := setupAgent(t, agentsdk.Metadata{}, 0)
|
||||||
require.True(t, conn.AwaitReachable(context.Background()))
|
require.True(t, conn.AwaitReachable(context.Background()))
|
||||||
conn1, err := conn.DialContext(context.Background(), l.Addr().Network(), l.Addr().String())
|
conn1, err := conn.DialContext(context.Background(), l.Addr().Network(), l.Addr().String())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -923,7 +924,7 @@ func TestAgent_Speedtest(t *testing.T) {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
derpMap := tailnettest.RunDERPAndSTUN(t)
|
derpMap := tailnettest.RunDERPAndSTUN(t)
|
||||||
//nolint:dogsled
|
//nolint:dogsled
|
||||||
conn, _, _, _ := setupAgent(t, codersdk.WorkspaceAgentMetadata{
|
conn, _, _, _ := setupAgent(t, agentsdk.Metadata{
|
||||||
DERPMap: derpMap,
|
DERPMap: derpMap,
|
||||||
}, 0)
|
}, 0)
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
@ -940,12 +941,12 @@ func TestAgent_Reconnect(t *testing.T) {
|
||||||
defer coordinator.Close()
|
defer coordinator.Close()
|
||||||
|
|
||||||
agentID := uuid.New()
|
agentID := uuid.New()
|
||||||
statsCh := make(chan *codersdk.AgentStats)
|
statsCh := make(chan *agentsdk.Stats)
|
||||||
derpMap := tailnettest.RunDERPAndSTUN(t)
|
derpMap := tailnettest.RunDERPAndSTUN(t)
|
||||||
client := &client{
|
client := &client{
|
||||||
t: t,
|
t: t,
|
||||||
agentID: agentID,
|
agentID: agentID,
|
||||||
metadata: codersdk.WorkspaceAgentMetadata{
|
metadata: agentsdk.Metadata{
|
||||||
DERPMap: derpMap,
|
DERPMap: derpMap,
|
||||||
},
|
},
|
||||||
statsChan: statsCh,
|
statsChan: statsCh,
|
||||||
|
@ -980,11 +981,11 @@ func TestAgent_WriteVSCodeConfigs(t *testing.T) {
|
||||||
client := &client{
|
client := &client{
|
||||||
t: t,
|
t: t,
|
||||||
agentID: uuid.New(),
|
agentID: uuid.New(),
|
||||||
metadata: codersdk.WorkspaceAgentMetadata{
|
metadata: agentsdk.Metadata{
|
||||||
GitAuthConfigs: 1,
|
GitAuthConfigs: 1,
|
||||||
DERPMap: &tailcfg.DERPMap{},
|
DERPMap: &tailcfg.DERPMap{},
|
||||||
},
|
},
|
||||||
statsChan: make(chan *codersdk.AgentStats),
|
statsChan: make(chan *agentsdk.Stats),
|
||||||
coordinator: coordinator,
|
coordinator: coordinator,
|
||||||
}
|
}
|
||||||
filesystem := afero.NewMemMapFs()
|
filesystem := afero.NewMemMapFs()
|
||||||
|
@ -1009,7 +1010,7 @@ func TestAgent_WriteVSCodeConfigs(t *testing.T) {
|
||||||
|
|
||||||
func setupSSHCommand(t *testing.T, beforeArgs []string, afterArgs []string) *exec.Cmd {
|
func setupSSHCommand(t *testing.T, beforeArgs []string, afterArgs []string) *exec.Cmd {
|
||||||
//nolint:dogsled
|
//nolint:dogsled
|
||||||
agentConn, _, _, _ := setupAgent(t, codersdk.WorkspaceAgentMetadata{}, 0)
|
agentConn, _, _, _ := setupAgent(t, agentsdk.Metadata{}, 0)
|
||||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
waitGroup := sync.WaitGroup{}
|
waitGroup := sync.WaitGroup{}
|
||||||
|
@ -1052,7 +1053,7 @@ func setupSSHCommand(t *testing.T, beforeArgs []string, afterArgs []string) *exe
|
||||||
return exec.Command("ssh", args...)
|
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)
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
//nolint:dogsled
|
//nolint:dogsled
|
||||||
|
@ -1076,10 +1077,10 @@ func (c closeFunc) Close() error {
|
||||||
return c()
|
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.AgentConn,
|
*codersdk.WorkspaceAgentConn,
|
||||||
*client,
|
*client,
|
||||||
<-chan *codersdk.AgentStats,
|
<-chan *agentsdk.Stats,
|
||||||
afero.Fs,
|
afero.Fs,
|
||||||
) {
|
) {
|
||||||
if metadata.DERPMap == nil {
|
if metadata.DERPMap == nil {
|
||||||
|
@ -1090,7 +1091,7 @@ func setupAgent(t *testing.T, metadata codersdk.WorkspaceAgentMetadata, ptyTimeo
|
||||||
_ = coordinator.Close()
|
_ = coordinator.Close()
|
||||||
})
|
})
|
||||||
agentID := uuid.New()
|
agentID := uuid.New()
|
||||||
statsCh := make(chan *codersdk.AgentStats, 50)
|
statsCh := make(chan *agentsdk.Stats, 50)
|
||||||
fs := afero.NewMemMapFs()
|
fs := afero.NewMemMapFs()
|
||||||
c := &client{
|
c := &client{
|
||||||
t: t,
|
t: t,
|
||||||
|
@ -1131,7 +1132,7 @@ func setupAgent(t *testing.T, metadata codersdk.WorkspaceAgentMetadata, ptyTimeo
|
||||||
return conn.UpdateNodes(node)
|
return conn.UpdateNodes(node)
|
||||||
})
|
})
|
||||||
conn.SetNodeCallback(sendNode)
|
conn.SetNodeCallback(sendNode)
|
||||||
return &codersdk.AgentConn{
|
return &codersdk.WorkspaceAgentConn{
|
||||||
Conn: conn,
|
Conn: conn,
|
||||||
}, c, statsCh, fs
|
}, c, statsCh, fs
|
||||||
}
|
}
|
||||||
|
@ -1170,8 +1171,8 @@ func assertWritePayload(t *testing.T, w io.Writer, payload []byte) {
|
||||||
type client struct {
|
type client struct {
|
||||||
t *testing.T
|
t *testing.T
|
||||||
agentID uuid.UUID
|
agentID uuid.UUID
|
||||||
metadata codersdk.WorkspaceAgentMetadata
|
metadata agentsdk.Metadata
|
||||||
statsChan chan *codersdk.AgentStats
|
statsChan chan *agentsdk.Stats
|
||||||
coordinator tailnet.Coordinator
|
coordinator tailnet.Coordinator
|
||||||
lastWorkspaceAgent func()
|
lastWorkspaceAgent func()
|
||||||
|
|
||||||
|
@ -1179,11 +1180,11 @@ type client struct {
|
||||||
lifecycleStates []codersdk.WorkspaceAgentLifecycle
|
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
|
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()
|
clientConn, serverConn := net.Pipe()
|
||||||
closed := make(chan struct{})
|
closed := make(chan struct{})
|
||||||
c.lastWorkspaceAgent = func() {
|
c.lastWorkspaceAgent = func() {
|
||||||
|
@ -1199,7 +1200,7 @@ func (c *client) ListenWorkspaceAgent(_ context.Context) (net.Conn, error) {
|
||||||
return clientConn, nil
|
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{})
|
doneCh := make(chan struct{})
|
||||||
ctx, cancel := context.WithCancel(ctx)
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
|
||||||
|
@ -1238,18 +1239,18 @@ func (c *client) getLifecycleStates() []codersdk.WorkspaceAgentLifecycle {
|
||||||
return c.lifecycleStates
|
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()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
c.lifecycleStates = append(c.lifecycleStates, req.State)
|
c.lifecycleStates = append(c.lifecycleStates, req.State)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*client) PostWorkspaceAgentAppHealth(_ context.Context, _ codersdk.PostWorkspaceAppHealthsRequest) error {
|
func (*client) PostAppHealth(_ context.Context, _ agentsdk.PostAppHealthsRequest) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*client) PostWorkspaceAgentVersion(_ context.Context, _ string) error {
|
func (*client) PostVersion(_ context.Context, _ string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
|
|
||||||
"cdr.dev/slog"
|
"cdr.dev/slog"
|
||||||
"github.com/coder/coder/codersdk"
|
"github.com/coder/coder/codersdk"
|
||||||
|
"github.com/coder/coder/codersdk/agentsdk"
|
||||||
"github.com/coder/retry"
|
"github.com/coder/retry"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -18,7 +19,7 @@ import (
|
||||||
type WorkspaceAgentApps func(context.Context) ([]codersdk.WorkspaceApp, error)
|
type WorkspaceAgentApps func(context.Context) ([]codersdk.WorkspaceApp, error)
|
||||||
|
|
||||||
// PostWorkspaceAgentAppHealth updates the workspace app health.
|
// 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.
|
// 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)
|
type WorkspaceAppHealthReporter func(ctx context.Context)
|
||||||
|
@ -132,7 +133,7 @@ func NewWorkspaceAppHealthReporter(logger slog.Logger, apps []codersdk.Workspace
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
lastHealth = copyHealth(health)
|
lastHealth = copyHealth(health)
|
||||||
mu.Unlock()
|
mu.Unlock()
|
||||||
err := postWorkspaceAgentAppHealth(ctx, codersdk.PostWorkspaceAppHealthsRequest{
|
err := postWorkspaceAgentAppHealth(ctx, agentsdk.PostAppHealthsRequest{
|
||||||
Healths: lastHealth,
|
Healths: lastHealth,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -16,6 +16,7 @@ import (
|
||||||
"github.com/coder/coder/agent"
|
"github.com/coder/coder/agent"
|
||||||
"github.com/coder/coder/coderd/httpapi"
|
"github.com/coder/coder/coderd/httpapi"
|
||||||
"github.com/coder/coder/codersdk"
|
"github.com/coder/coder/codersdk"
|
||||||
|
"github.com/coder/coder/codersdk/agentsdk"
|
||||||
"github.com/coder/coder/testutil"
|
"github.com/coder/coder/testutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -180,7 +181,7 @@ func setupAppReporter(ctx context.Context, t *testing.T, apps []codersdk.Workspa
|
||||||
var newApps []codersdk.WorkspaceApp
|
var newApps []codersdk.WorkspaceApp
|
||||||
return append(newApps, apps...), nil
|
return append(newApps, apps...), nil
|
||||||
}
|
}
|
||||||
postWorkspaceAgentAppHealth := func(_ context.Context, req codersdk.PostWorkspaceAppHealthsRequest) error {
|
postWorkspaceAgentAppHealth := func(_ context.Context, req agentsdk.PostAppHealthsRequest) error {
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
for id, health := range req.Healths {
|
for id, health := range req.Healths {
|
||||||
for i, app := range apps {
|
for i, app := range apps {
|
||||||
|
|
|
@ -11,13 +11,13 @@ import (
|
||||||
"github.com/coder/coder/codersdk"
|
"github.com/coder/coder/codersdk"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (lp *listeningPortsHandler) getListeningPorts() ([]codersdk.ListeningPort, error) {
|
func (lp *listeningPortsHandler) getListeningPorts() ([]codersdk.WorkspaceAgentListeningPort, error) {
|
||||||
lp.mut.Lock()
|
lp.mut.Lock()
|
||||||
defer lp.mut.Unlock()
|
defer lp.mut.Unlock()
|
||||||
|
|
||||||
if time.Since(lp.mtime) < time.Second {
|
if time.Since(lp.mtime) < time.Second {
|
||||||
// copy
|
// copy
|
||||||
ports := make([]codersdk.ListeningPort, len(lp.ports))
|
ports := make([]codersdk.WorkspaceAgentListeningPort, len(lp.ports))
|
||||||
copy(ports, lp.ports)
|
copy(ports, lp.ports)
|
||||||
return ports, nil
|
return ports, nil
|
||||||
}
|
}
|
||||||
|
@ -30,9 +30,9 @@ func (lp *listeningPortsHandler) getListeningPorts() ([]codersdk.ListeningPort,
|
||||||
}
|
}
|
||||||
|
|
||||||
seen := make(map[uint16]struct{}, len(tabs))
|
seen := make(map[uint16]struct{}, len(tabs))
|
||||||
ports := []codersdk.ListeningPort{}
|
ports := []codersdk.WorkspaceAgentListeningPort{}
|
||||||
for _, tab := range tabs {
|
for _, tab := range tabs {
|
||||||
if tab.LocalAddr == nil || tab.LocalAddr.Port < codersdk.MinimumListeningPort {
|
if tab.LocalAddr == nil || tab.LocalAddr.Port < codersdk.WorkspaceAgentMinimumListeningPort {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,9 +47,9 @@ func (lp *listeningPortsHandler) getListeningPorts() ([]codersdk.ListeningPort,
|
||||||
if tab.Process != nil {
|
if tab.Process != nil {
|
||||||
procName = tab.Process.Name
|
procName = tab.Process.Name
|
||||||
}
|
}
|
||||||
ports = append(ports, codersdk.ListeningPort{
|
ports = append(ports, codersdk.WorkspaceAgentListeningPort{
|
||||||
ProcessName: procName,
|
ProcessName: procName,
|
||||||
Network: codersdk.ListeningPortNetworkTCP,
|
Network: "tcp",
|
||||||
Port: tab.LocalAddr.Port,
|
Port: tab.LocalAddr.Port,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -58,7 +58,7 @@ func (lp *listeningPortsHandler) getListeningPorts() ([]codersdk.ListeningPort,
|
||||||
lp.mtime = time.Now()
|
lp.mtime = time.Now()
|
||||||
|
|
||||||
// copy
|
// copy
|
||||||
ports = make([]codersdk.ListeningPort, len(lp.ports))
|
ports = make([]codersdk.WorkspaceAgentListeningPort, len(lp.ports))
|
||||||
copy(ports, lp.ports)
|
copy(ports, lp.ports)
|
||||||
return ports, nil
|
return ports, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,9 +4,9 @@ package agent
|
||||||
|
|
||||||
import "github.com/coder/coder/codersdk"
|
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
|
// 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
|
// moment. The UI will not show any "no ports found" message to the user, so
|
||||||
// the user won't suspect a thing.
|
// the user won't suspect a thing.
|
||||||
return []codersdk.ListeningPort{}, nil
|
return []codersdk.WorkspaceAgentListeningPort{}, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@ func (*agent) statisticsHandler() http.Handler {
|
||||||
|
|
||||||
type listeningPortsHandler struct {
|
type listeningPortsHandler struct {
|
||||||
mut sync.Mutex
|
mut sync.Mutex
|
||||||
ports []codersdk.ListeningPort
|
ports []codersdk.WorkspaceAgentListeningPort
|
||||||
mtime time.Time
|
mtime time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ func (lp *listeningPortsHandler) handler(rw http.ResponseWriter, r *http.Request
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
httpapi.Write(r.Context(), rw, http.StatusOK, codersdk.ListeningPortsResponse{
|
httpapi.Write(r.Context(), rw, http.StatusOK, codersdk.WorkspaceAgentListeningPortsResponse{
|
||||||
Ports: ports,
|
Ports: ports,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
28
cli/agent.go
28
cli/agent.go
|
@ -22,7 +22,7 @@ import (
|
||||||
"github.com/coder/coder/agent/reaper"
|
"github.com/coder/coder/agent/reaper"
|
||||||
"github.com/coder/coder/buildinfo"
|
"github.com/coder/coder/buildinfo"
|
||||||
"github.com/coder/coder/cli/cliflag"
|
"github.com/coder/coder/cli/cliflag"
|
||||||
"github.com/coder/coder/codersdk"
|
"github.com/coder/coder/codersdk/agentsdk"
|
||||||
)
|
)
|
||||||
|
|
||||||
func workspaceAgent() *cobra.Command {
|
func workspaceAgent() *cobra.Command {
|
||||||
|
@ -82,10 +82,10 @@ func workspaceAgent() *cobra.Command {
|
||||||
slog.F("auth", auth),
|
slog.F("auth", auth),
|
||||||
slog.F("version", version),
|
slog.F("version", version),
|
||||||
)
|
)
|
||||||
client := codersdk.New(coderURL)
|
client := agentsdk.New(coderURL)
|
||||||
client.Logger = logger
|
client.SDK.Logger = logger
|
||||||
// Set a reasonable timeout so requests can't hang forever!
|
// 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
|
// Enable pprof handler
|
||||||
// This prevents the pprof import from being accidentally deleted.
|
// This prevents the pprof import from being accidentally deleted.
|
||||||
|
@ -96,7 +96,7 @@ func workspaceAgent() *cobra.Command {
|
||||||
// exchangeToken returns a session token.
|
// exchangeToken returns a session token.
|
||||||
// This is abstracted to allow for the same looping condition
|
// This is abstracted to allow for the same looping condition
|
||||||
// regardless of instance identity auth type.
|
// regardless of instance identity auth type.
|
||||||
var exchangeToken func(context.Context) (codersdk.WorkspaceAgentAuthenticateResponse, error)
|
var exchangeToken func(context.Context) (agentsdk.AuthenticateResponse, error)
|
||||||
switch auth {
|
switch auth {
|
||||||
case "token":
|
case "token":
|
||||||
token, err := cmd.Flags().GetString(varAgentToken)
|
token, err := cmd.Flags().GetString(varAgentToken)
|
||||||
|
@ -112,8 +112,8 @@ func workspaceAgent() *cobra.Command {
|
||||||
if gcpClientRaw != nil {
|
if gcpClientRaw != nil {
|
||||||
gcpClient, _ = gcpClientRaw.(*metadata.Client)
|
gcpClient, _ = gcpClientRaw.(*metadata.Client)
|
||||||
}
|
}
|
||||||
exchangeToken = func(ctx context.Context) (codersdk.WorkspaceAgentAuthenticateResponse, error) {
|
exchangeToken = func(ctx context.Context) (agentsdk.AuthenticateResponse, error) {
|
||||||
return client.AuthWorkspaceGoogleInstanceIdentity(ctx, "", gcpClient)
|
return client.AuthGoogleInstanceIdentity(ctx, "", gcpClient)
|
||||||
}
|
}
|
||||||
case "aws-instance-identity":
|
case "aws-instance-identity":
|
||||||
// This is *only* done for testing to mock client authentication.
|
// This is *only* done for testing to mock client authentication.
|
||||||
|
@ -123,11 +123,11 @@ func workspaceAgent() *cobra.Command {
|
||||||
if awsClientRaw != nil {
|
if awsClientRaw != nil {
|
||||||
awsClient, _ = awsClientRaw.(*http.Client)
|
awsClient, _ = awsClientRaw.(*http.Client)
|
||||||
if awsClient != nil {
|
if awsClient != nil {
|
||||||
client.HTTPClient = awsClient
|
client.SDK.HTTPClient = awsClient
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
exchangeToken = func(ctx context.Context) (codersdk.WorkspaceAgentAuthenticateResponse, error) {
|
exchangeToken = func(ctx context.Context) (agentsdk.AuthenticateResponse, error) {
|
||||||
return client.AuthWorkspaceAWSInstanceIdentity(ctx)
|
return client.AuthAWSInstanceIdentity(ctx)
|
||||||
}
|
}
|
||||||
case "azure-instance-identity":
|
case "azure-instance-identity":
|
||||||
// This is *only* done for testing to mock client authentication.
|
// This is *only* done for testing to mock client authentication.
|
||||||
|
@ -137,11 +137,11 @@ func workspaceAgent() *cobra.Command {
|
||||||
if azureClientRaw != nil {
|
if azureClientRaw != nil {
|
||||||
azureClient, _ = azureClientRaw.(*http.Client)
|
azureClient, _ = azureClientRaw.(*http.Client)
|
||||||
if azureClient != nil {
|
if azureClient != nil {
|
||||||
client.HTTPClient = azureClient
|
client.SDK.HTTPClient = azureClient
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
exchangeToken = func(ctx context.Context) (codersdk.WorkspaceAgentAuthenticateResponse, error) {
|
exchangeToken = func(ctx context.Context) (agentsdk.AuthenticateResponse, error) {
|
||||||
return client.AuthWorkspaceAzureInstanceIdentity(ctx)
|
return client.AuthAzureInstanceIdentity(ctx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -159,7 +159,7 @@ func workspaceAgent() *cobra.Command {
|
||||||
Logger: logger,
|
Logger: logger,
|
||||||
ExchangeToken: func(ctx context.Context) (string, error) {
|
ExchangeToken: func(ctx context.Context) (string, error) {
|
||||||
if exchangeToken == nil {
|
if exchangeToken == nil {
|
||||||
return client.SessionToken(), nil
|
return client.SDK.SessionToken(), nil
|
||||||
}
|
}
|
||||||
resp, err := exchangeToken(ctx)
|
resp, err := exchangeToken(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -24,7 +24,7 @@ import (
|
||||||
"github.com/coder/coder/agent"
|
"github.com/coder/coder/agent"
|
||||||
"github.com/coder/coder/cli/clitest"
|
"github.com/coder/coder/cli/clitest"
|
||||||
"github.com/coder/coder/coderd/coderdtest"
|
"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/provisioner/echo"
|
||||||
"github.com/coder/coder/provisionersdk/proto"
|
"github.com/coder/coder/provisionersdk/proto"
|
||||||
"github.com/coder/coder/pty/ptytest"
|
"github.com/coder/coder/pty/ptytest"
|
||||||
|
@ -104,7 +104,7 @@ func TestConfigSSH(t *testing.T) {
|
||||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||||
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
||||||
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
||||||
agentClient := codersdk.New(client.URL)
|
agentClient := agentsdk.New(client.URL)
|
||||||
agentClient.SetSessionToken(authToken)
|
agentClient.SetSessionToken(authToken)
|
||||||
agentCloser := agent.New(agent.Options{
|
agentCloser := agent.New(agent.Options{
|
||||||
Client: agentClient,
|
Client: agentClient,
|
||||||
|
|
|
@ -39,7 +39,7 @@ func gitAskpass() *cobra.Command {
|
||||||
return xerrors.Errorf("create agent client: %w", err)
|
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 {
|
if err != nil {
|
||||||
var apiError *codersdk.Error
|
var apiError *codersdk.Error
|
||||||
if errors.As(err, &apiError) && apiError.StatusCode() == http.StatusNotFound {
|
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); {
|
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 {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ import (
|
||||||
"github.com/coder/coder/cli/cliui"
|
"github.com/coder/coder/cli/cliui"
|
||||||
"github.com/coder/coder/coderd/httpapi"
|
"github.com/coder/coder/coderd/httpapi"
|
||||||
"github.com/coder/coder/codersdk"
|
"github.com/coder/coder/codersdk"
|
||||||
|
"github.com/coder/coder/codersdk/agentsdk"
|
||||||
"github.com/coder/coder/pty/ptytest"
|
"github.com/coder/coder/pty/ptytest"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -22,7 +23,7 @@ func TestGitAskpass(t *testing.T) {
|
||||||
t.Setenv("GIT_PREFIX", "/")
|
t.Setenv("GIT_PREFIX", "/")
|
||||||
t.Run("UsernameAndPassword", func(t *testing.T) {
|
t.Run("UsernameAndPassword", func(t *testing.T) {
|
||||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
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",
|
Username: "something",
|
||||||
Password: "bananas",
|
Password: "bananas",
|
||||||
})
|
})
|
||||||
|
@ -61,8 +62,8 @@ func TestGitAskpass(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Poll", func(t *testing.T) {
|
t.Run("Poll", func(t *testing.T) {
|
||||||
resp := atomic.Pointer[codersdk.WorkspaceAgentGitAuthResponse]{}
|
resp := atomic.Pointer[agentsdk.GitAuthResponse]{}
|
||||||
resp.Store(&codersdk.WorkspaceAgentGitAuthResponse{
|
resp.Store(&agentsdk.GitAuthResponse{
|
||||||
URL: "https://something.org",
|
URL: "https://something.org",
|
||||||
})
|
})
|
||||||
poll := make(chan struct{}, 10)
|
poll := make(chan struct{}, 10)
|
||||||
|
@ -88,7 +89,7 @@ func TestGitAskpass(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}()
|
}()
|
||||||
<-poll
|
<-poll
|
||||||
resp.Store(&codersdk.WorkspaceAgentGitAuthResponse{
|
resp.Store(&agentsdk.GitAuthResponse{
|
||||||
Username: "username",
|
Username: "username",
|
||||||
Password: "password",
|
Password: "password",
|
||||||
})
|
})
|
||||||
|
|
|
@ -42,7 +42,7 @@ func gitssh() *cobra.Command {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.Errorf("create agent client: %w", err)
|
return xerrors.Errorf("create agent client: %w", err)
|
||||||
}
|
}
|
||||||
key, err := client.AgentGitSSHKey(ctx)
|
key, err := client.GitSSHKey(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.Errorf("get agent git ssh token: %w", err)
|
return xerrors.Errorf("get agent git ssh token: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -156,7 +156,7 @@ func portForward() *cobra.Command {
|
||||||
return cmd
|
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)
|
_, _ = fmt.Fprintf(cmd.OutOrStderr(), "Forwarding '%v://%v' locally to '%v://%v' in the workspace\n", spec.listenNetwork, spec.listenAddress, spec.dialNetwork, spec.dialAddress)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
21
cli/root.go
21
cli/root.go
|
@ -5,6 +5,7 @@ import (
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
@ -31,6 +32,7 @@ import (
|
||||||
"github.com/coder/coder/coderd"
|
"github.com/coder/coder/coderd"
|
||||||
"github.com/coder/coder/coderd/gitauth"
|
"github.com/coder/coder/coderd/gitauth"
|
||||||
"github.com/coder/coder/codersdk"
|
"github.com/coder/coder/codersdk"
|
||||||
|
"github.com/coder/coder/codersdk/agentsdk"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -333,7 +335,7 @@ func createUnauthenticatedClient(cmd *cobra.Command, serverURL *url.URL) (*coder
|
||||||
|
|
||||||
// createAgentClient returns a new client from the command context.
|
// createAgentClient returns a new client from the command context.
|
||||||
// It works just like CreateClient, but uses the agent token and URL instead.
|
// 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)
|
rawURL, err := cmd.Flags().GetString(varAgentURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -346,7 +348,7 @@ func createAgentClient(cmd *cobra.Command) (*codersdk.Client, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
client := codersdk.New(serverURL)
|
client := agentsdk.New(serverURL)
|
||||||
client.SetSessionToken(token)
|
client.SetSessionToken(token)
|
||||||
return client, nil
|
return client, nil
|
||||||
}
|
}
|
||||||
|
@ -590,7 +592,7 @@ func checkVersions(cmd *cobra.Command, client *codersdk.Client) error {
|
||||||
clientVersion := buildinfo.Version()
|
clientVersion := buildinfo.Version()
|
||||||
info, err := client.BuildInfo(ctx)
|
info, err := client.BuildInfo(ctx)
|
||||||
// Avoid printing errors that are connection-related.
|
// Avoid printing errors that are connection-related.
|
||||||
if codersdk.IsConnectionErr(err) {
|
if isConnectionError(err) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -735,3 +737,16 @@ func dumpHandler(ctx context.Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IiConnectionErr is a convenience function for checking if the source of an
|
||||||
|
// error is due to a 'connection refused', 'no such host', etc.
|
||||||
|
func isConnectionError(err error) bool {
|
||||||
|
var (
|
||||||
|
// E.g. no such host
|
||||||
|
dnsErr *net.DNSError
|
||||||
|
// Eg. connection refused
|
||||||
|
opErr *net.OpError
|
||||||
|
)
|
||||||
|
|
||||||
|
return xerrors.As(err, &dnsErr) || xerrors.As(err, &opErr)
|
||||||
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -328,7 +329,14 @@ func scaletestCleanup() *cobra.Command {
|
||||||
return err
|
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...")
|
cmd.PrintErrln("Fetching scaletest workspaces...")
|
||||||
var (
|
var (
|
||||||
|
@ -506,7 +514,14 @@ It is recommended that all rate limits are disabled on the server before running
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
client.BypassRatelimits = true
|
client.HTTPClient = &http.Client{
|
||||||
|
Transport: &headerTransport{
|
||||||
|
transport: http.DefaultTransport,
|
||||||
|
headers: map[string]string{
|
||||||
|
codersdk.BypassRatelimitHeader: "true",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
if count <= 0 {
|
if count <= 0 {
|
||||||
return xerrors.Errorf("--count is required and must be greater than 0")
|
return xerrors.Errorf("--count is required and must be greater than 0")
|
||||||
|
@ -668,7 +683,7 @@ It is recommended that all rate limits are disabled on the server before running
|
||||||
if runCommand != "" {
|
if runCommand != "" {
|
||||||
config.ReconnectingPTY = &reconnectingpty.Config{
|
config.ReconnectingPTY = &reconnectingpty.Config{
|
||||||
// AgentID is set by the test automatically.
|
// AgentID is set by the test automatically.
|
||||||
Init: codersdk.ReconnectingPTYInit{
|
Init: codersdk.WorkspaceAgentReconnectingPTYInit{
|
||||||
ID: uuid.Nil,
|
ID: uuid.Nil,
|
||||||
Height: 24,
|
Height: 24,
|
||||||
Width: 80,
|
Width: 80,
|
||||||
|
|
|
@ -10,7 +10,7 @@ import (
|
||||||
"github.com/coder/coder/agent"
|
"github.com/coder/coder/agent"
|
||||||
"github.com/coder/coder/cli/clitest"
|
"github.com/coder/coder/cli/clitest"
|
||||||
"github.com/coder/coder/coderd/coderdtest"
|
"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/pty/ptytest"
|
||||||
"github.com/coder/coder/testutil"
|
"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!")
|
t.Skip("This test takes a minimum of 5ms per a hardcoded value in Tailscale!")
|
||||||
}
|
}
|
||||||
client, workspace, agentToken := setupWorkspaceForAgent(t, nil)
|
client, workspace, agentToken := setupWorkspaceForAgent(t, nil)
|
||||||
agentClient := codersdk.New(client.URL)
|
agentClient := agentsdk.New(client.URL)
|
||||||
agentClient.SetSessionToken(agentToken)
|
agentClient.SetSessionToken(agentToken)
|
||||||
agentCloser := agent.New(agent.Options{
|
agentCloser := agent.New(agent.Options{
|
||||||
Client: agentClient,
|
Client: agentClient,
|
||||||
|
|
|
@ -31,6 +31,7 @@ import (
|
||||||
"github.com/coder/coder/cli/cliui"
|
"github.com/coder/coder/cli/cliui"
|
||||||
"github.com/coder/coder/coderd/coderdtest"
|
"github.com/coder/coder/coderd/coderdtest"
|
||||||
"github.com/coder/coder/codersdk"
|
"github.com/coder/coder/codersdk"
|
||||||
|
"github.com/coder/coder/codersdk/agentsdk"
|
||||||
"github.com/coder/coder/provisioner/echo"
|
"github.com/coder/coder/provisioner/echo"
|
||||||
"github.com/coder/coder/provisionersdk/proto"
|
"github.com/coder/coder/provisionersdk/proto"
|
||||||
"github.com/coder/coder/pty"
|
"github.com/coder/coder/pty"
|
||||||
|
@ -100,7 +101,7 @@ func TestSSH(t *testing.T) {
|
||||||
})
|
})
|
||||||
pty.ExpectMatch("Waiting")
|
pty.ExpectMatch("Waiting")
|
||||||
|
|
||||||
agentClient := codersdk.New(client.URL)
|
agentClient := agentsdk.New(client.URL)
|
||||||
agentClient.SetSessionToken(agentToken)
|
agentClient.SetSessionToken(agentToken)
|
||||||
agentCloser := agent.New(agent.Options{
|
agentCloser := agent.New(agent.Options{
|
||||||
Client: agentClient,
|
Client: agentClient,
|
||||||
|
@ -149,7 +150,7 @@ func TestSSH(t *testing.T) {
|
||||||
_, _ = tGoContext(t, func(ctx context.Context) {
|
_, _ = tGoContext(t, func(ctx context.Context) {
|
||||||
// Run this async so the SSH command has to wait for
|
// Run this async so the SSH command has to wait for
|
||||||
// the build and agent to connect!
|
// the build and agent to connect!
|
||||||
agentClient := codersdk.New(client.URL)
|
agentClient := agentsdk.New(client.URL)
|
||||||
agentClient.SetSessionToken(agentToken)
|
agentClient.SetSessionToken(agentToken)
|
||||||
agentCloser := agent.New(agent.Options{
|
agentCloser := agent.New(agent.Options{
|
||||||
Client: agentClient,
|
Client: agentClient,
|
||||||
|
@ -216,7 +217,7 @@ func TestSSH(t *testing.T) {
|
||||||
|
|
||||||
client, workspace, agentToken := setupWorkspaceForAgent(t, nil)
|
client, workspace, agentToken := setupWorkspaceForAgent(t, nil)
|
||||||
|
|
||||||
agentClient := codersdk.New(client.URL)
|
agentClient := agentsdk.New(client.URL)
|
||||||
agentClient.SetSessionToken(agentToken)
|
agentClient.SetSessionToken(agentToken)
|
||||||
agentCloser := agent.New(agent.Options{
|
agentCloser := agent.New(agent.Options{
|
||||||
Client: agentClient,
|
Client: agentClient,
|
||||||
|
@ -449,7 +450,7 @@ Expire-Date: 0
|
||||||
|
|
||||||
client, workspace, agentToken := setupWorkspaceForAgent(t, nil)
|
client, workspace, agentToken := setupWorkspaceForAgent(t, nil)
|
||||||
|
|
||||||
agentClient := codersdk.New(client.URL)
|
agentClient := agentsdk.New(client.URL)
|
||||||
agentClient.SetSessionToken(agentToken)
|
agentClient.SetSessionToken(agentToken)
|
||||||
agentCloser := agent.New(agent.Options{
|
agentCloser := agent.New(agent.Options{
|
||||||
Client: agentClient,
|
Client: agentClient,
|
||||||
|
|
|
@ -73,7 +73,7 @@ func createToken() *cobra.Command {
|
||||||
cmd.Println(cliui.Styles.Code.Render(strings.TrimSpace(res.Key)))
|
cmd.Println(cliui.Styles.Code.Render(strings.TrimSpace(res.Key)))
|
||||||
cmd.Println()
|
cmd.Println()
|
||||||
cmd.Println(cliui.Styles.Wrap.Render(
|
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
|
return nil
|
||||||
|
@ -103,7 +103,7 @@ func listTokens() *cobra.Command {
|
||||||
return xerrors.Errorf("create codersdk client: %w", err)
|
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 {
|
if err != nil {
|
||||||
return xerrors.Errorf("create tokens: %w", err)
|
return xerrors.Errorf("create tokens: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -184,7 +184,7 @@ type sshNetworkStats struct {
|
||||||
DownloadBytesSec int64 `json:"download_bytes_sec"`
|
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)
|
latency, p2p, err := agentConn.Ping(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -15,6 +15,7 @@ import (
|
||||||
"github.com/coder/coder/cli/clitest"
|
"github.com/coder/coder/cli/clitest"
|
||||||
"github.com/coder/coder/coderd/coderdtest"
|
"github.com/coder/coder/coderd/coderdtest"
|
||||||
"github.com/coder/coder/codersdk"
|
"github.com/coder/coder/codersdk"
|
||||||
|
"github.com/coder/coder/codersdk/agentsdk"
|
||||||
"github.com/coder/coder/testutil"
|
"github.com/coder/coder/testutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -28,7 +29,7 @@ func TestVSCodeSSH(t *testing.T) {
|
||||||
user, err := client.User(ctx, codersdk.Me)
|
user, err := client.User(ctx, codersdk.Me)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
agentClient := codersdk.New(client.URL)
|
agentClient := agentsdk.New(client.URL)
|
||||||
agentClient.SetSessionToken(agentToken)
|
agentClient.SetSessionToken(agentToken)
|
||||||
agentCloser := agent.New(agent.Options{
|
agentCloser := agent.New(agent.Options{
|
||||||
Client: agentClient,
|
Client: agentClient,
|
||||||
|
|
|
@ -153,7 +153,7 @@ const docTemplate = `{
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/codersdk.GetAppHostResponse"
|
"$ref": "#/definitions/codersdk.AppHostResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3781,7 +3781,7 @@ const docTemplate = `{
|
||||||
"in": "body",
|
"in": "body",
|
||||||
"required": true,
|
"required": true,
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/codersdk.AWSInstanceIdentityToken"
|
"$ref": "#/definitions/agentsdk.AWSInstanceIdentityToken"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -3789,7 +3789,7 @@ const docTemplate = `{
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/codersdk.WorkspaceAgentAuthenticateResponse"
|
"$ref": "#/definitions/agentsdk.AuthenticateResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3820,7 +3820,7 @@ const docTemplate = `{
|
||||||
"in": "body",
|
"in": "body",
|
||||||
"required": true,
|
"required": true,
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/codersdk.AzureInstanceIdentityToken"
|
"$ref": "#/definitions/agentsdk.AzureInstanceIdentityToken"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -3828,7 +3828,7 @@ const docTemplate = `{
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/codersdk.WorkspaceAgentAuthenticateResponse"
|
"$ref": "#/definitions/agentsdk.AuthenticateResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3859,7 +3859,7 @@ const docTemplate = `{
|
||||||
"in": "body",
|
"in": "body",
|
||||||
"required": true,
|
"required": true,
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/codersdk.GoogleInstanceIdentityToken"
|
"$ref": "#/definitions/agentsdk.GoogleInstanceIdentityToken"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -3867,7 +3867,7 @@ const docTemplate = `{
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/codersdk.WorkspaceAgentAuthenticateResponse"
|
"$ref": "#/definitions/agentsdk.AuthenticateResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3898,7 +3898,7 @@ const docTemplate = `{
|
||||||
"in": "body",
|
"in": "body",
|
||||||
"required": true,
|
"required": true,
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/codersdk.PostWorkspaceAppHealthsRequest"
|
"$ref": "#/definitions/agentsdk.PostAppHealthsRequest"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -3964,7 +3964,7 @@ const docTemplate = `{
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/codersdk.WorkspaceAgentGitAuthResponse"
|
"$ref": "#/definitions/agentsdk.GitAuthResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3989,7 +3989,7 @@ const docTemplate = `{
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/codersdk.AgentGitSSHKey"
|
"$ref": "#/definitions/agentsdk.GitSSHKey"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4014,7 +4014,7 @@ const docTemplate = `{
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/codersdk.WorkspaceAgentMetadata"
|
"$ref": "#/definitions/agentsdk.Metadata"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4042,7 +4042,7 @@ const docTemplate = `{
|
||||||
"in": "body",
|
"in": "body",
|
||||||
"required": true,
|
"required": true,
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/codersdk.PostWorkspaceAgentLifecycleRequest"
|
"$ref": "#/definitions/agentsdk.PostLifecycleRequest"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -4081,7 +4081,7 @@ const docTemplate = `{
|
||||||
"in": "body",
|
"in": "body",
|
||||||
"required": true,
|
"required": true,
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/codersdk.AgentStats"
|
"$ref": "#/definitions/agentsdk.Stats"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -4089,7 +4089,7 @@ const docTemplate = `{
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/codersdk.AgentStatsResponse"
|
"$ref": "#/definitions/agentsdk.StatsResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4120,7 +4120,7 @@ const docTemplate = `{
|
||||||
"in": "body",
|
"in": "body",
|
||||||
"required": true,
|
"required": true,
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/codersdk.PostWorkspaceAgentVersionRequest"
|
"$ref": "#/definitions/agentsdk.PostVersionRequest"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -4262,7 +4262,7 @@ const docTemplate = `{
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/codersdk.ListeningPortsResponse"
|
"$ref": "#/definitions/codersdk.WorkspaceAgentListeningPortsResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4957,6 +4957,188 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"definitions": {
|
"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": {
|
"coderd.SCIMUser": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
@ -5107,21 +5289,6 @@ const docTemplate = `{
|
||||||
"APIKeyScopeApplicationConnect"
|
"APIKeyScopeApplicationConnect"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"codersdk.AWSInstanceIdentityToken": {
|
|
||||||
"type": "object",
|
|
||||||
"required": [
|
|
||||||
"document",
|
|
||||||
"signature"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"document": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"signature": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"codersdk.AddLicenseRequest": {
|
"codersdk.AddLicenseRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
@ -5133,55 +5300,12 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"codersdk.AgentGitSSHKey": {
|
"codersdk.AppHostResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"private_key": {
|
"host": {
|
||||||
|
"description": "Host is the externally accessible URL for the Coder instance.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
|
||||||
"public_key": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"codersdk.AgentStats": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"conns_by_proto": {
|
|
||||||
"description": "ConnsByProto is a count of connections by protocol.",
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": {
|
|
||||||
"type": "integer"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"num_comms": {
|
|
||||||
"description": "NumConns is the number of connections received by an agent.",
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"rx_bytes": {
|
|
||||||
"description": "RxBytes is the number of received bytes.",
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"rx_packets": {
|
|
||||||
"description": "RxPackets is the number of received packets.",
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"tx_bytes": {
|
|
||||||
"description": "TxBytes is the number of transmitted bytes.",
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"tx_packets": {
|
|
||||||
"description": "TxPackets is the number of transmitted bytes.",
|
|
||||||
"type": "integer"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"codersdk.AgentStatsResponse": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"report_interval": {
|
|
||||||
"description": "ReportInterval is the duration after which the agent should send stats\nagain.",
|
|
||||||
"type": "integer"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -5402,21 +5526,6 @@ const docTemplate = `{
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"codersdk.AzureInstanceIdentityToken": {
|
|
||||||
"type": "object",
|
|
||||||
"required": [
|
|
||||||
"encoding",
|
|
||||||
"signature"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"encoding": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"signature": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"codersdk.BuildInfoResponse": {
|
"codersdk.BuildInfoResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
@ -6263,15 +6372,6 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"codersdk.GetAppHostResponse": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"host": {
|
|
||||||
"description": "Host is the externally accessible URL for the Coder instance.",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"codersdk.GetUsersResponse": {
|
"codersdk.GetUsersResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
@ -6341,17 +6441,6 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"codersdk.GoogleInstanceIdentityToken": {
|
|
||||||
"type": "object",
|
|
||||||
"required": [
|
|
||||||
"json_web_token"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"json_web_token": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"codersdk.Group": {
|
"codersdk.Group": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
@ -6418,47 +6507,6 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"codersdk.ListeningPort": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"network": {
|
|
||||||
"description": "only \"tcp\" at the moment",
|
|
||||||
"allOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/definitions/codersdk.ListeningPortNetwork"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"port": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"process_name": {
|
|
||||||
"description": "may be empty",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"codersdk.ListeningPortNetwork": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": [
|
|
||||||
"tcp"
|
|
||||||
],
|
|
||||||
"x-enum-varnames": [
|
|
||||||
"ListeningPortNetworkTCP"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"codersdk.ListeningPortsResponse": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"ports": {
|
|
||||||
"description": "If there are no ports in the list, nothing should be displayed in the UI.\nThere must not be a \"no ports available\" message or anything similar, as\nthere will always be no ports displayed on platforms where our port\ndetection logic is unsupported.",
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/codersdk.ListeningPort"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"codersdk.LogLevel": {
|
"codersdk.LogLevel": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
|
@ -6836,35 +6884,6 @@ const docTemplate = `{
|
||||||
"ParameterSourceSchemeData"
|
"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": {
|
"codersdk.PprofConfig": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
@ -7844,14 +7863,6 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"codersdk.WorkspaceAgentAuthenticateResponse": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"session_token": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"codersdk.WorkspaceAgentConnectionInfo": {
|
"codersdk.WorkspaceAgentConnectionInfo": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
@ -7860,20 +7871,6 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"codersdk.WorkspaceAgentGitAuthResponse": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"password": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"username": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"codersdk.WorkspaceAgentLifecycle": {
|
"codersdk.WorkspaceAgentLifecycle": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
|
@ -7891,42 +7888,31 @@ const docTemplate = `{
|
||||||
"WorkspaceAgentLifecycleReady"
|
"WorkspaceAgentLifecycleReady"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"codersdk.WorkspaceAgentMetadata": {
|
"codersdk.WorkspaceAgentListeningPort": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"apps": {
|
"network": {
|
||||||
|
"description": "only \"tcp\" at the moment",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"port": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"process_name": {
|
||||||
|
"description": "may be empty",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"codersdk.WorkspaceAgentListeningPortsResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"ports": {
|
||||||
|
"description": "If there are no ports in the list, nothing should be displayed in the UI.\nThere must not be a \"no ports available\" message or anything similar, as\nthere will always be no ports displayed on platforms where our port\ndetection logic is unsupported.",
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/definitions/codersdk.WorkspaceApp"
|
"$ref": "#/definitions/codersdk.WorkspaceAgentListeningPort"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"derpmap": {
|
|
||||||
"$ref": "#/definitions/tailcfg.DERPMap"
|
|
||||||
},
|
|
||||||
"directory": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"environment_variables": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"git_auth_configs": {
|
|
||||||
"description": "GitAuthConfigs stores the number of Git configurations\nthe Coder deployment has. If this number is \u003e0, we\nset up special configuration in the workspace.",
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"motd_file": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"startup_script": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"startup_script_timeout": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"vscode_port_proxy_uri": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -125,7 +125,7 @@
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/codersdk.GetAppHostResponse"
|
"$ref": "#/definitions/codersdk.AppHostResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3325,7 +3325,7 @@
|
||||||
"in": "body",
|
"in": "body",
|
||||||
"required": true,
|
"required": true,
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/codersdk.AWSInstanceIdentityToken"
|
"$ref": "#/definitions/agentsdk.AWSInstanceIdentityToken"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -3333,7 +3333,7 @@
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/codersdk.WorkspaceAgentAuthenticateResponse"
|
"$ref": "#/definitions/agentsdk.AuthenticateResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3358,7 +3358,7 @@
|
||||||
"in": "body",
|
"in": "body",
|
||||||
"required": true,
|
"required": true,
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/codersdk.AzureInstanceIdentityToken"
|
"$ref": "#/definitions/agentsdk.AzureInstanceIdentityToken"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -3366,7 +3366,7 @@
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/codersdk.WorkspaceAgentAuthenticateResponse"
|
"$ref": "#/definitions/agentsdk.AuthenticateResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3391,7 +3391,7 @@
|
||||||
"in": "body",
|
"in": "body",
|
||||||
"required": true,
|
"required": true,
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/codersdk.GoogleInstanceIdentityToken"
|
"$ref": "#/definitions/agentsdk.GoogleInstanceIdentityToken"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -3399,7 +3399,7 @@
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/codersdk.WorkspaceAgentAuthenticateResponse"
|
"$ref": "#/definitions/agentsdk.AuthenticateResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3424,7 +3424,7 @@
|
||||||
"in": "body",
|
"in": "body",
|
||||||
"required": true,
|
"required": true,
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/codersdk.PostWorkspaceAppHealthsRequest"
|
"$ref": "#/definitions/agentsdk.PostAppHealthsRequest"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -3484,7 +3484,7 @@
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/codersdk.WorkspaceAgentGitAuthResponse"
|
"$ref": "#/definitions/agentsdk.GitAuthResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3505,7 +3505,7 @@
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/codersdk.AgentGitSSHKey"
|
"$ref": "#/definitions/agentsdk.GitSSHKey"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3526,7 +3526,7 @@
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/codersdk.WorkspaceAgentMetadata"
|
"$ref": "#/definitions/agentsdk.Metadata"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3550,7 +3550,7 @@
|
||||||
"in": "body",
|
"in": "body",
|
||||||
"required": true,
|
"required": true,
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/codersdk.PostWorkspaceAgentLifecycleRequest"
|
"$ref": "#/definitions/agentsdk.PostLifecycleRequest"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -3583,7 +3583,7 @@
|
||||||
"in": "body",
|
"in": "body",
|
||||||
"required": true,
|
"required": true,
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/codersdk.AgentStats"
|
"$ref": "#/definitions/agentsdk.Stats"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -3591,7 +3591,7 @@
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/codersdk.AgentStatsResponse"
|
"$ref": "#/definitions/agentsdk.StatsResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3616,7 +3616,7 @@
|
||||||
"in": "body",
|
"in": "body",
|
||||||
"required": true,
|
"required": true,
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/codersdk.PostWorkspaceAgentVersionRequest"
|
"$ref": "#/definitions/agentsdk.PostVersionRequest"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -3744,7 +3744,7 @@
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/codersdk.ListeningPortsResponse"
|
"$ref": "#/definitions/codersdk.WorkspaceAgentListeningPortsResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4368,6 +4368,180 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"definitions": {
|
"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": {
|
"coderd.SCIMUser": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
@ -4504,18 +4678,6 @@
|
||||||
"enum": ["all", "application_connect"],
|
"enum": ["all", "application_connect"],
|
||||||
"x-enum-varnames": ["APIKeyScopeAll", "APIKeyScopeApplicationConnect"]
|
"x-enum-varnames": ["APIKeyScopeAll", "APIKeyScopeApplicationConnect"]
|
||||||
},
|
},
|
||||||
"codersdk.AWSInstanceIdentityToken": {
|
|
||||||
"type": "object",
|
|
||||||
"required": ["document", "signature"],
|
|
||||||
"properties": {
|
|
||||||
"document": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"signature": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"codersdk.AddLicenseRequest": {
|
"codersdk.AddLicenseRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["license"],
|
"required": ["license"],
|
||||||
|
@ -4525,55 +4687,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"codersdk.AgentGitSSHKey": {
|
"codersdk.AppHostResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"private_key": {
|
"host": {
|
||||||
|
"description": "Host is the externally accessible URL for the Coder instance.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
|
||||||
"public_key": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"codersdk.AgentStats": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"conns_by_proto": {
|
|
||||||
"description": "ConnsByProto is a count of connections by protocol.",
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": {
|
|
||||||
"type": "integer"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"num_comms": {
|
|
||||||
"description": "NumConns is the number of connections received by an agent.",
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"rx_bytes": {
|
|
||||||
"description": "RxBytes is the number of received bytes.",
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"rx_packets": {
|
|
||||||
"description": "RxPackets is the number of received packets.",
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"tx_bytes": {
|
|
||||||
"description": "TxBytes is the number of transmitted bytes.",
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"tx_packets": {
|
|
||||||
"description": "TxPackets is the number of transmitted bytes.",
|
|
||||||
"type": "integer"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"codersdk.AgentStatsResponse": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"report_interval": {
|
|
||||||
"description": "ReportInterval is the duration after which the agent should send stats\nagain.",
|
|
||||||
"type": "integer"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -4783,18 +4902,6 @@
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"codersdk.AzureInstanceIdentityToken": {
|
|
||||||
"type": "object",
|
|
||||||
"required": ["encoding", "signature"],
|
|
||||||
"properties": {
|
|
||||||
"encoding": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"signature": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"codersdk.BuildInfoResponse": {
|
"codersdk.BuildInfoResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
@ -5588,15 +5695,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"codersdk.GetAppHostResponse": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"host": {
|
|
||||||
"description": "Host is the externally accessible URL for the Coder instance.",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"codersdk.GetUsersResponse": {
|
"codersdk.GetUsersResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
@ -5666,15 +5764,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"codersdk.GoogleInstanceIdentityToken": {
|
|
||||||
"type": "object",
|
|
||||||
"required": ["json_web_token"],
|
|
||||||
"properties": {
|
|
||||||
"json_web_token": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"codersdk.Group": {
|
"codersdk.Group": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
@ -5741,43 +5830,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"codersdk.ListeningPort": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"network": {
|
|
||||||
"description": "only \"tcp\" at the moment",
|
|
||||||
"allOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/definitions/codersdk.ListeningPortNetwork"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"port": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"process_name": {
|
|
||||||
"description": "may be empty",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"codersdk.ListeningPortNetwork": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["tcp"],
|
|
||||||
"x-enum-varnames": ["ListeningPortNetworkTCP"]
|
|
||||||
},
|
|
||||||
"codersdk.ListeningPortsResponse": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"ports": {
|
|
||||||
"description": "If there are no ports in the list, nothing should be displayed in the UI.\nThere must not be a \"no ports available\" message or anything similar, as\nthere will always be no ports displayed on platforms where our port\ndetection logic is unsupported.",
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/codersdk.ListeningPort"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"codersdk.LogLevel": {
|
"codersdk.LogLevel": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["trace", "debug", "info", "warn", "error"],
|
"enum": ["trace", "debug", "info", "warn", "error"],
|
||||||
|
@ -6099,35 +6151,6 @@
|
||||||
"ParameterSourceSchemeData"
|
"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": {
|
"codersdk.PprofConfig": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
@ -7059,14 +7082,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"codersdk.WorkspaceAgentAuthenticateResponse": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"session_token": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"codersdk.WorkspaceAgentConnectionInfo": {
|
"codersdk.WorkspaceAgentConnectionInfo": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
@ -7075,20 +7090,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"codersdk.WorkspaceAgentGitAuthResponse": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"password": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"username": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"codersdk.WorkspaceAgentLifecycle": {
|
"codersdk.WorkspaceAgentLifecycle": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["created", "starting", "start_timeout", "start_error", "ready"],
|
"enum": ["created", "starting", "start_timeout", "start_error", "ready"],
|
||||||
|
@ -7100,42 +7101,31 @@
|
||||||
"WorkspaceAgentLifecycleReady"
|
"WorkspaceAgentLifecycleReady"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"codersdk.WorkspaceAgentMetadata": {
|
"codersdk.WorkspaceAgentListeningPort": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"apps": {
|
"network": {
|
||||||
|
"description": "only \"tcp\" at the moment",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"port": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"process_name": {
|
||||||
|
"description": "may be empty",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"codersdk.WorkspaceAgentListeningPortsResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"ports": {
|
||||||
|
"description": "If there are no ports in the list, nothing should be displayed in the UI.\nThere must not be a \"no ports available\" message or anything similar, as\nthere will always be no ports displayed on platforms where our port\ndetection logic is unsupported.",
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/definitions/codersdk.WorkspaceApp"
|
"$ref": "#/definitions/codersdk.WorkspaceAgentListeningPort"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"derpmap": {
|
|
||||||
"$ref": "#/definitions/tailcfg.DERPMap"
|
|
||||||
},
|
|
||||||
"directory": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"environment_variables": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"git_auth_configs": {
|
|
||||||
"description": "GitAuthConfigs stores the number of Git configurations\nthe Coder deployment has. If this number is \u003e0, we\nset up special configuration in the workspace.",
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"motd_file": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"startup_script": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"startup_script_timeout": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"vscode_port_proxy_uri": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -343,7 +343,7 @@ func (api *API) createAPIKey(ctx context.Context, params createAPIKeyParams) (*h
|
||||||
// This format is consumed by the APIKey middleware.
|
// This format is consumed by the APIKey middleware.
|
||||||
sessionToken := fmt.Sprintf("%s-%s", keyID, keySecret)
|
sessionToken := fmt.Sprintf("%s-%s", keyID, keySecret)
|
||||||
return &http.Cookie{
|
return &http.Cookie{
|
||||||
Name: codersdk.SessionTokenKey,
|
Name: codersdk.SessionTokenCookie,
|
||||||
Value: sessionToken,
|
Value: sessionToken,
|
||||||
Path: "/",
|
Path: "/",
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
|
|
|
@ -19,7 +19,7 @@ func TestTokenCRUD(t *testing.T) {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
client := coderdtest.New(t, nil)
|
client := coderdtest.New(t, nil)
|
||||||
_ = coderdtest.CreateFirstUser(t, client)
|
_ = coderdtest.CreateFirstUser(t, client)
|
||||||
keys, err := client.GetTokens(ctx, codersdk.Me)
|
keys, err := client.Tokens(ctx, codersdk.Me)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Empty(t, keys)
|
require.Empty(t, keys)
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ func TestTokenCRUD(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Greater(t, len(res.Key), 2)
|
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.NoError(t, err)
|
||||||
require.EqualValues(t, len(keys), 1)
|
require.EqualValues(t, len(keys), 1)
|
||||||
require.Contains(t, res.Key, keys[0].ID)
|
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)
|
err = client.DeleteAPIKey(ctx, codersdk.Me, keys[0].ID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
keys, err = client.GetTokens(ctx, codersdk.Me)
|
keys, err = client.Tokens(ctx, codersdk.Me)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Empty(t, keys)
|
require.Empty(t, keys)
|
||||||
}
|
}
|
||||||
|
@ -59,7 +59,7 @@ func TestTokenScoped(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Greater(t, len(res.Key), 2)
|
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.NoError(t, err)
|
||||||
require.EqualValues(t, len(keys), 1)
|
require.EqualValues(t, len(keys), 1)
|
||||||
require.Contains(t, res.Key, keys[0].ID)
|
require.Contains(t, res.Key, keys[0].ID)
|
||||||
|
@ -78,7 +78,7 @@ func TestTokenDuration(t *testing.T) {
|
||||||
Lifetime: time.Hour * 24 * 7,
|
Lifetime: time.Hour * 24 * 7,
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
keys, err := client.GetTokens(ctx, codersdk.Me)
|
keys, err := client.Tokens(ctx, codersdk.Me)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Greater(t, keys[0].ExpiresAt, time.Now().Add(time.Hour*6*24))
|
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))
|
require.Less(t, keys[0].ExpiresAt, time.Now().Add(time.Hour*8*24))
|
||||||
|
|
|
@ -261,7 +261,7 @@ func (api *API) convertAuditLog(ctx context.Context, dblog database.GetAuditLogs
|
||||||
|
|
||||||
func auditLogDescription(alog database.GetAuditLogsOffsetRow, additionalFields AdditionalFields) string {
|
func auditLogDescription(alog database.GetAuditLogsOffsetRow, additionalFields AdditionalFields) string {
|
||||||
str := fmt.Sprintf("{user} %s",
|
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:
|
// Strings for starting/stopping workspace builds follow the below format:
|
||||||
|
|
|
@ -133,7 +133,7 @@ func TestCheckPermissions(t *testing.T) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||||
t.Cleanup(cancel)
|
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.NoError(t, err, "check perms")
|
||||||
require.Equal(t, c.Check, resp)
|
require.Equal(t, c.Check, resp)
|
||||||
})
|
})
|
||||||
|
|
|
@ -342,7 +342,7 @@ func NewAuthTester(ctx context.Context, t *testing.T, client *codersdk.Client, a
|
||||||
})
|
})
|
||||||
require.NoError(t, err, "create token")
|
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")
|
require.NoError(t, err, "get tokens")
|
||||||
apiKey := apiKeys[0]
|
apiKey := apiKeys[0]
|
||||||
|
|
||||||
|
|
|
@ -68,6 +68,7 @@ import (
|
||||||
"github.com/coder/coder/coderd/updatecheck"
|
"github.com/coder/coder/coderd/updatecheck"
|
||||||
"github.com/coder/coder/coderd/util/ptr"
|
"github.com/coder/coder/coderd/util/ptr"
|
||||||
"github.com/coder/coder/codersdk"
|
"github.com/coder/coder/codersdk"
|
||||||
|
"github.com/coder/coder/codersdk/agentsdk"
|
||||||
"github.com/coder/coder/cryptorand"
|
"github.com/coder/coder/cryptorand"
|
||||||
"github.com/coder/coder/provisioner/echo"
|
"github.com/coder/coder/provisioner/echo"
|
||||||
"github.com/coder/coder/provisionerd"
|
"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)))
|
signature := make([]byte, base64.StdEncoding.EncodedLen(len(signatureRaw)))
|
||||||
base64.StdEncoding.Encode(signature, signatureRaw)
|
base64.StdEncoding.Encode(signature, signatureRaw)
|
||||||
|
|
||||||
payload, err := json.Marshal(codersdk.AzureInstanceIdentityToken{
|
payload, err := json.Marshal(agentsdk.AzureInstanceIdentityToken{
|
||||||
Signature: string(signature),
|
Signature: string(signature),
|
||||||
Encoding: "pkcs7",
|
Encoding: "pkcs7",
|
||||||
})
|
})
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"github.com/coder/coder/coderd/httpmw"
|
"github.com/coder/coder/coderd/httpmw"
|
||||||
"github.com/coder/coder/coderd/rbac"
|
"github.com/coder/coder/coderd/rbac"
|
||||||
"github.com/coder/coder/codersdk"
|
"github.com/coder/coder/codersdk"
|
||||||
|
"github.com/coder/coder/codersdk/agentsdk"
|
||||||
)
|
)
|
||||||
|
|
||||||
// @Summary Regenerate user SSH key
|
// @Summary Regenerate user SSH key
|
||||||
|
@ -121,7 +122,7 @@ func (api *API) gitSSHKey(rw http.ResponseWriter, r *http.Request) {
|
||||||
// @Security CoderSessionToken
|
// @Security CoderSessionToken
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Tags Agents
|
// @Tags Agents
|
||||||
// @Success 200 {object} codersdk.AgentGitSSHKey
|
// @Success 200 {object} agentsdk.GitSSHKey
|
||||||
// @Router /workspaceagents/me/gitsshkey [get]
|
// @Router /workspaceagents/me/gitsshkey [get]
|
||||||
func (api *API) agentGitSSHKey(rw http.ResponseWriter, r *http.Request) {
|
func (api *API) agentGitSSHKey(rw http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
@ -162,7 +163,7 @@ func (api *API) agentGitSSHKey(rw http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
httpapi.Write(ctx, rw, http.StatusOK, codersdk.AgentGitSSHKey{
|
httpapi.Write(ctx, rw, http.StatusOK, agentsdk.GitSSHKey{
|
||||||
PublicKey: gitSSHKey.PublicKey,
|
PublicKey: gitSSHKey.PublicKey,
|
||||||
PrivateKey: gitSSHKey.PrivateKey,
|
PrivateKey: gitSSHKey.PrivateKey,
|
||||||
})
|
})
|
||||||
|
|
|
@ -12,7 +12,7 @@ import (
|
||||||
"github.com/coder/coder/coderd/coderdtest"
|
"github.com/coder/coder/coderd/coderdtest"
|
||||||
"github.com/coder/coder/coderd/database"
|
"github.com/coder/coder/coderd/database"
|
||||||
"github.com/coder/coder/coderd/gitsshkey"
|
"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/provisioner/echo"
|
||||||
"github.com/coder/coder/provisionersdk/proto"
|
"github.com/coder/coder/provisionersdk/proto"
|
||||||
"github.com/coder/coder/testutil"
|
"github.com/coder/coder/testutil"
|
||||||
|
@ -133,13 +133,13 @@ func TestAgentGitSSHKey(t *testing.T) {
|
||||||
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, project.ID)
|
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, project.ID)
|
||||||
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
||||||
|
|
||||||
agentClient := codersdk.New(client.URL)
|
agentClient := agentsdk.New(client.URL)
|
||||||
agentClient.SetSessionToken(authToken)
|
agentClient.SetSessionToken(authToken)
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
agentKey, err := agentClient.AgentGitSSHKey(ctx)
|
agentKey, err := agentClient.GitSSHKey(ctx)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotEmpty(t, agentKey.PrivateKey)
|
require.NotEmpty(t, agentKey.PrivateKey)
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,9 +20,9 @@ func StripCoderCookies(header string) string {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
name, _, _ := strings.Cut(part, "=")
|
name, _, _ := strings.Cut(part, "=")
|
||||||
if name == codersdk.SessionTokenKey ||
|
if name == codersdk.SessionTokenCookie ||
|
||||||
name == codersdk.OAuth2StateKey ||
|
name == codersdk.OAuth2StateCookie ||
|
||||||
name == codersdk.OAuth2RedirectKey {
|
name == codersdk.OAuth2RedirectCookie {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
cookies = append(cookies, part)
|
cookies = append(cookies, part)
|
||||||
|
|
|
@ -144,7 +144,7 @@ func ExtractAPIKey(cfg ExtractAPIKeyConfig) func(http.Handler) http.Handler {
|
||||||
if token == "" {
|
if token == "" {
|
||||||
optionalWrite(http.StatusUnauthorized, codersdk.Response{
|
optionalWrite(http.StatusUnauthorized, codersdk.Response{
|
||||||
Message: SignedOutErrorMessage,
|
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
|
return
|
||||||
}
|
}
|
||||||
|
@ -364,17 +364,17 @@ func ExtractAPIKey(cfg ExtractAPIKeyConfig) func(http.Handler) http.Handler {
|
||||||
// 4. The coder_session_token query parameter
|
// 4. The coder_session_token query parameter
|
||||||
// 5. The custom auth header
|
// 5. The custom auth header
|
||||||
func apiTokenFromRequest(r *http.Request) string {
|
func apiTokenFromRequest(r *http.Request) string {
|
||||||
cookie, err := r.Cookie(codersdk.SessionTokenKey)
|
cookie, err := r.Cookie(codersdk.SessionTokenCookie)
|
||||||
if err == nil && cookie.Value != "" {
|
if err == nil && cookie.Value != "" {
|
||||||
return cookie.Value
|
return cookie.Value
|
||||||
}
|
}
|
||||||
|
|
||||||
urlValue := r.URL.Query().Get(codersdk.SessionTokenKey)
|
urlValue := r.URL.Query().Get(codersdk.SessionTokenCookie)
|
||||||
if urlValue != "" {
|
if urlValue != "" {
|
||||||
return urlValue
|
return urlValue
|
||||||
}
|
}
|
||||||
|
|
||||||
headerValue := r.Header.Get(codersdk.SessionCustomHeader)
|
headerValue := r.Header.Get(codersdk.SessionTokenHeader)
|
||||||
if headerValue != "" {
|
if headerValue != "" {
|
||||||
return headerValue
|
return headerValue
|
||||||
}
|
}
|
||||||
|
|
|
@ -82,7 +82,7 @@ func TestAPIKey(t *testing.T) {
|
||||||
r = httptest.NewRequest("GET", "/", nil)
|
r = httptest.NewRequest("GET", "/", nil)
|
||||||
rw = httptest.NewRecorder()
|
rw = httptest.NewRecorder()
|
||||||
)
|
)
|
||||||
r.Header.Set(codersdk.SessionCustomHeader, "test-wow-hello")
|
r.Header.Set(codersdk.SessionTokenHeader, "test-wow-hello")
|
||||||
|
|
||||||
httpmw.ExtractAPIKey(httpmw.ExtractAPIKeyConfig{
|
httpmw.ExtractAPIKey(httpmw.ExtractAPIKeyConfig{
|
||||||
DB: db,
|
DB: db,
|
||||||
|
@ -100,7 +100,7 @@ func TestAPIKey(t *testing.T) {
|
||||||
r = httptest.NewRequest("GET", "/", nil)
|
r = httptest.NewRequest("GET", "/", nil)
|
||||||
rw = httptest.NewRecorder()
|
rw = httptest.NewRecorder()
|
||||||
)
|
)
|
||||||
r.Header.Set(codersdk.SessionCustomHeader, "test-wow")
|
r.Header.Set(codersdk.SessionTokenHeader, "test-wow")
|
||||||
|
|
||||||
httpmw.ExtractAPIKey(httpmw.ExtractAPIKeyConfig{
|
httpmw.ExtractAPIKey(httpmw.ExtractAPIKeyConfig{
|
||||||
DB: db,
|
DB: db,
|
||||||
|
@ -118,7 +118,7 @@ func TestAPIKey(t *testing.T) {
|
||||||
r = httptest.NewRequest("GET", "/", nil)
|
r = httptest.NewRequest("GET", "/", nil)
|
||||||
rw = httptest.NewRecorder()
|
rw = httptest.NewRecorder()
|
||||||
)
|
)
|
||||||
r.Header.Set(codersdk.SessionCustomHeader, "testtestid-wow")
|
r.Header.Set(codersdk.SessionTokenHeader, "testtestid-wow")
|
||||||
|
|
||||||
httpmw.ExtractAPIKey(httpmw.ExtractAPIKeyConfig{
|
httpmw.ExtractAPIKey(httpmw.ExtractAPIKeyConfig{
|
||||||
DB: db,
|
DB: db,
|
||||||
|
@ -137,7 +137,7 @@ func TestAPIKey(t *testing.T) {
|
||||||
r = httptest.NewRequest("GET", "/", nil)
|
r = httptest.NewRequest("GET", "/", nil)
|
||||||
rw = httptest.NewRecorder()
|
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{
|
httpmw.ExtractAPIKey(httpmw.ExtractAPIKeyConfig{
|
||||||
DB: db,
|
DB: db,
|
||||||
|
@ -157,7 +157,7 @@ func TestAPIKey(t *testing.T) {
|
||||||
rw = httptest.NewRecorder()
|
rw = httptest.NewRecorder()
|
||||||
user = createUser(r.Context(), t, db)
|
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!
|
// Use a different secret so they don't match!
|
||||||
hashed := sha256.Sum256([]byte("differentsecret"))
|
hashed := sha256.Sum256([]byte("differentsecret"))
|
||||||
|
@ -188,7 +188,7 @@ func TestAPIKey(t *testing.T) {
|
||||||
rw = httptest.NewRecorder()
|
rw = httptest.NewRecorder()
|
||||||
user = createUser(r.Context(), t, db)
|
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{
|
_, err := db.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{
|
||||||
ID: id,
|
ID: id,
|
||||||
|
@ -217,7 +217,7 @@ func TestAPIKey(t *testing.T) {
|
||||||
rw = httptest.NewRecorder()
|
rw = httptest.NewRecorder()
|
||||||
user = createUser(r.Context(), t, db)
|
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{
|
sentAPIKey, err := db.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{
|
||||||
ID: id,
|
ID: id,
|
||||||
|
@ -259,7 +259,7 @@ func TestAPIKey(t *testing.T) {
|
||||||
user = createUser(r.Context(), t, db)
|
user = createUser(r.Context(), t, db)
|
||||||
)
|
)
|
||||||
r.AddCookie(&http.Cookie{
|
r.AddCookie(&http.Cookie{
|
||||||
Name: codersdk.SessionTokenKey,
|
Name: codersdk.SessionTokenCookie,
|
||||||
Value: fmt.Sprintf("%s-%s", id, secret),
|
Value: fmt.Sprintf("%s-%s", id, secret),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -302,7 +302,7 @@ func TestAPIKey(t *testing.T) {
|
||||||
user = createUser(r.Context(), t, db)
|
user = createUser(r.Context(), t, db)
|
||||||
)
|
)
|
||||||
q := r.URL.Query()
|
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()
|
r.URL.RawQuery = q.Encode()
|
||||||
|
|
||||||
_, err := db.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{
|
_, err := db.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{
|
||||||
|
@ -339,7 +339,7 @@ func TestAPIKey(t *testing.T) {
|
||||||
rw = httptest.NewRecorder()
|
rw = httptest.NewRecorder()
|
||||||
user = createUser(r.Context(), t, db)
|
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{
|
sentAPIKey, err := db.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{
|
||||||
ID: id,
|
ID: id,
|
||||||
|
@ -376,7 +376,7 @@ func TestAPIKey(t *testing.T) {
|
||||||
rw = httptest.NewRecorder()
|
rw = httptest.NewRecorder()
|
||||||
user = createUser(r.Context(), t, db)
|
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{
|
sentAPIKey, err := db.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{
|
||||||
ID: id,
|
ID: id,
|
||||||
|
@ -413,7 +413,7 @@ func TestAPIKey(t *testing.T) {
|
||||||
rw = httptest.NewRecorder()
|
rw = httptest.NewRecorder()
|
||||||
user = createUser(r.Context(), t, db)
|
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{
|
sentAPIKey, err := db.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{
|
||||||
ID: id,
|
ID: id,
|
||||||
|
@ -457,7 +457,7 @@ func TestAPIKey(t *testing.T) {
|
||||||
rw = httptest.NewRecorder()
|
rw = httptest.NewRecorder()
|
||||||
user = createUser(r.Context(), t, db)
|
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{
|
sentAPIKey, err := db.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{
|
||||||
ID: id,
|
ID: id,
|
||||||
|
@ -514,7 +514,7 @@ func TestAPIKey(t *testing.T) {
|
||||||
user = createUser(r.Context(), t, db)
|
user = createUser(r.Context(), t, db)
|
||||||
)
|
)
|
||||||
r.RemoteAddr = "1.1.1.1"
|
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{
|
_, err := db.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{
|
||||||
ID: id,
|
ID: id,
|
||||||
|
@ -602,7 +602,7 @@ func TestAPIKey(t *testing.T) {
|
||||||
rw = httptest.NewRecorder()
|
rw = httptest.NewRecorder()
|
||||||
user = createUser(r.Context(), t, db)
|
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{
|
sentAPIKey, err := db.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{
|
||||||
ID: id,
|
ID: id,
|
||||||
|
|
|
@ -131,7 +131,7 @@ func TestExtractUserRoles(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "/", nil)
|
req := httptest.NewRequest("GET", "/", nil)
|
||||||
req.Header.Set(codersdk.SessionCustomHeader, token)
|
req.Header.Set(codersdk.SessionTokenHeader, token)
|
||||||
|
|
||||||
rtr.ServeHTTP(rw, req)
|
rtr.ServeHTTP(rw, req)
|
||||||
resp := rw.Result()
|
resp := rw.Result()
|
||||||
|
|
|
@ -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.
|
// CSRF only affects requests that automatically attach credentials via a cookie.
|
||||||
// If no cookie is present, then there is no risk of CSRF.
|
// If no cookie is present, then there is no risk of CSRF.
|
||||||
//nolint:govet
|
//nolint:govet
|
||||||
sessCookie, err := r.Cookie(codersdk.SessionTokenKey)
|
sessCookie, err := r.Cookie(codersdk.SessionTokenCookie)
|
||||||
if xerrors.Is(err, http.ErrNoCookie) {
|
if xerrors.Is(err, http.ErrNoCookie) {
|
||||||
return true
|
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
|
// 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
|
// custom header auth. Custom header auth can bypass CSRF, as CSRF attacks
|
||||||
// cannot add custom headers.
|
// cannot add custom headers.
|
||||||
return true
|
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
|
// If the auth is set in a url param and matches the cookie, it
|
||||||
// is the same as just using the url param.
|
// is the same as just using the url param.
|
||||||
return true
|
return true
|
||||||
|
|
|
@ -71,7 +71,7 @@ func ExtractOAuth2(config OAuth2Config, client *http.Client) func(http.Handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
http.SetCookie(rw, &http.Cookie{
|
http.SetCookie(rw, &http.Cookie{
|
||||||
Name: codersdk.OAuth2StateKey,
|
Name: codersdk.OAuth2StateCookie,
|
||||||
Value: state,
|
Value: state,
|
||||||
Path: "/",
|
Path: "/",
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
|
@ -80,7 +80,7 @@ func ExtractOAuth2(config OAuth2Config, client *http.Client) func(http.Handler)
|
||||||
// Redirect must always be specified, otherwise
|
// Redirect must always be specified, otherwise
|
||||||
// an old redirect could apply!
|
// an old redirect could apply!
|
||||||
http.SetCookie(rw, &http.Cookie{
|
http.SetCookie(rw, &http.Cookie{
|
||||||
Name: codersdk.OAuth2RedirectKey,
|
Name: codersdk.OAuth2RedirectCookie,
|
||||||
Value: r.URL.Query().Get("redirect"),
|
Value: r.URL.Query().Get("redirect"),
|
||||||
Path: "/",
|
Path: "/",
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
|
@ -98,10 +98,10 @@ func ExtractOAuth2(config OAuth2Config, client *http.Client) func(http.Handler)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
stateCookie, err := r.Cookie(codersdk.OAuth2StateKey)
|
stateCookie, err := r.Cookie(codersdk.OAuth2StateCookie)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpapi.Write(ctx, rw, http.StatusUnauthorized, codersdk.Response{
|
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
|
return
|
||||||
}
|
}
|
||||||
|
@ -113,7 +113,7 @@ func ExtractOAuth2(config OAuth2Config, client *http.Client) func(http.Handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
var redirect string
|
var redirect string
|
||||||
stateRedirect, err := r.Cookie(codersdk.OAuth2RedirectKey)
|
stateRedirect, err := r.Cookie(codersdk.OAuth2RedirectCookie)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
redirect = stateRedirect.Value
|
redirect = stateRedirect.Value
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,7 +73,7 @@ func TestOAuth2(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
req := httptest.NewRequest("GET", "/?code=something&state=test", nil)
|
req := httptest.NewRequest("GET", "/?code=something&state=test", nil)
|
||||||
req.AddCookie(&http.Cookie{
|
req.AddCookie(&http.Cookie{
|
||||||
Name: codersdk.OAuth2StateKey,
|
Name: codersdk.OAuth2StateCookie,
|
||||||
Value: "mismatch",
|
Value: "mismatch",
|
||||||
})
|
})
|
||||||
res := httptest.NewRecorder()
|
res := httptest.NewRecorder()
|
||||||
|
@ -84,7 +84,7 @@ func TestOAuth2(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
req := httptest.NewRequest("GET", "/?code=test&state=something", nil)
|
req := httptest.NewRequest("GET", "/?code=test&state=something", nil)
|
||||||
req.AddCookie(&http.Cookie{
|
req.AddCookie(&http.Cookie{
|
||||||
Name: codersdk.OAuth2StateKey,
|
Name: codersdk.OAuth2StateCookie,
|
||||||
Value: "something",
|
Value: "something",
|
||||||
})
|
})
|
||||||
req.AddCookie(&http.Cookie{
|
req.AddCookie(&http.Cookie{
|
||||||
|
|
|
@ -29,7 +29,7 @@ func TestOrganizationParam(t *testing.T) {
|
||||||
r = httptest.NewRequest("GET", "/", nil)
|
r = httptest.NewRequest("GET", "/", nil)
|
||||||
hashed = sha256.Sum256([]byte(secret))
|
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()
|
userID := uuid.New()
|
||||||
username, err := cryptorand.String(8)
|
username, err := cryptorand.String(8)
|
||||||
|
|
|
@ -111,7 +111,7 @@ func TestRateLimit(t *testing.T) {
|
||||||
|
|
||||||
// Bypass must fail
|
// Bypass must fail
|
||||||
req := httptest.NewRequest("GET", "/", nil)
|
req := httptest.NewRequest("GET", "/", nil)
|
||||||
req.Header.Set(codersdk.SessionCustomHeader, key)
|
req.Header.Set(codersdk.SessionTokenHeader, key)
|
||||||
req.Header.Set(codersdk.BypassRatelimitHeader, "true")
|
req.Header.Set(codersdk.BypassRatelimitHeader, "true")
|
||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
// Assert we're not using IP address.
|
// Assert we're not using IP address.
|
||||||
|
@ -123,7 +123,7 @@ func TestRateLimit(t *testing.T) {
|
||||||
|
|
||||||
require.Eventually(t, func() bool {
|
require.Eventually(t, func() bool {
|
||||||
req := httptest.NewRequest("GET", "/", nil)
|
req := httptest.NewRequest("GET", "/", nil)
|
||||||
req.Header.Set(codersdk.SessionCustomHeader, key)
|
req.Header.Set(codersdk.SessionTokenHeader, key)
|
||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
// Assert we're not using IP address.
|
// Assert we're not using IP address.
|
||||||
req.RemoteAddr = randRemoteAddr()
|
req.RemoteAddr = randRemoteAddr()
|
||||||
|
@ -160,7 +160,7 @@ func TestRateLimit(t *testing.T) {
|
||||||
|
|
||||||
require.Never(t, func() bool {
|
require.Never(t, func() bool {
|
||||||
req := httptest.NewRequest("GET", "/", nil)
|
req := httptest.NewRequest("GET", "/", nil)
|
||||||
req.Header.Set(codersdk.SessionCustomHeader, key)
|
req.Header.Set(codersdk.SessionTokenHeader, key)
|
||||||
req.Header.Set(codersdk.BypassRatelimitHeader, "true")
|
req.Header.Set(codersdk.BypassRatelimitHeader, "true")
|
||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
// Assert we're not using IP address.
|
// Assert we're not using IP address.
|
||||||
|
|
|
@ -29,7 +29,7 @@ func TestTemplateParam(t *testing.T) {
|
||||||
hashed = sha256.Sum256([]byte(secret))
|
hashed = sha256.Sum256([]byte(secret))
|
||||||
)
|
)
|
||||||
r := httptest.NewRequest("GET", "/", nil)
|
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()
|
userID := uuid.New()
|
||||||
username, err := cryptorand.String(8)
|
username, err := cryptorand.String(8)
|
||||||
|
|
|
@ -29,7 +29,7 @@ func TestTemplateVersionParam(t *testing.T) {
|
||||||
hashed = sha256.Sum256([]byte(secret))
|
hashed = sha256.Sum256([]byte(secret))
|
||||||
)
|
)
|
||||||
r := httptest.NewRequest("GET", "/", nil)
|
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()
|
userID := uuid.New()
|
||||||
username, err := cryptorand.String(8)
|
username, err := cryptorand.String(8)
|
||||||
|
|
|
@ -29,7 +29,7 @@ func TestUserParam(t *testing.T) {
|
||||||
r = httptest.NewRequest("GET", "/", nil)
|
r = httptest.NewRequest("GET", "/", nil)
|
||||||
rw = httptest.NewRecorder()
|
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{
|
user, err := db.InsertUser(r.Context(), database.InsertUserParams{
|
||||||
ID: uuid.New(),
|
ID: uuid.New(),
|
||||||
|
|
|
@ -33,7 +33,7 @@ func ExtractWorkspaceAgent(db database.Store) func(http.Handler) http.Handler {
|
||||||
tokenValue := apiTokenFromRequest(r)
|
tokenValue := apiTokenFromRequest(r)
|
||||||
if tokenValue == "" {
|
if tokenValue == "" {
|
||||||
httpapi.Write(ctx, rw, http.StatusUnauthorized, codersdk.Response{
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ func TestWorkspaceAgent(t *testing.T) {
|
||||||
setup := func(db database.Store) (*http.Request, uuid.UUID) {
|
setup := func(db database.Store) (*http.Request, uuid.UUID) {
|
||||||
token := uuid.New()
|
token := uuid.New()
|
||||||
r := httptest.NewRequest("GET", "/", nil)
|
r := httptest.NewRequest("GET", "/", nil)
|
||||||
r.Header.Set(codersdk.SessionCustomHeader, token.String())
|
r.Header.Set(codersdk.SessionTokenHeader, token.String())
|
||||||
return r, token
|
return r, token
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,7 +29,7 @@ func TestWorkspaceAgentParam(t *testing.T) {
|
||||||
hashed = sha256.Sum256([]byte(secret))
|
hashed = sha256.Sum256([]byte(secret))
|
||||||
)
|
)
|
||||||
r := httptest.NewRequest("GET", "/", nil)
|
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()
|
userID := uuid.New()
|
||||||
username, err := cryptorand.String(8)
|
username, err := cryptorand.String(8)
|
||||||
|
|
|
@ -29,7 +29,7 @@ func TestWorkspaceBuildParam(t *testing.T) {
|
||||||
hashed = sha256.Sum256([]byte(secret))
|
hashed = sha256.Sum256([]byte(secret))
|
||||||
)
|
)
|
||||||
r := httptest.NewRequest("GET", "/", nil)
|
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()
|
userID := uuid.New()
|
||||||
username, err := cryptorand.String(8)
|
username, err := cryptorand.String(8)
|
||||||
|
|
|
@ -32,7 +32,7 @@ func TestWorkspaceParam(t *testing.T) {
|
||||||
hashed = sha256.Sum256([]byte(secret))
|
hashed = sha256.Sum256([]byte(secret))
|
||||||
)
|
)
|
||||||
r := httptest.NewRequest("GET", "/", nil)
|
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()
|
userID := uuid.New()
|
||||||
username, err := cryptorand.String(8)
|
username, err := cryptorand.String(8)
|
||||||
|
@ -345,7 +345,7 @@ func setupWorkspaceWithAgents(t testing.TB, cfg setupConfig) (database.Store, *h
|
||||||
hashed = sha256.Sum256([]byte(secret))
|
hashed = sha256.Sum256([]byte(secret))
|
||||||
)
|
)
|
||||||
r := httptest.NewRequest("GET", "/", nil)
|
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()
|
userID := uuid.New()
|
||||||
username, err := cryptorand.String(8)
|
username, err := cryptorand.String(8)
|
||||||
|
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"github.com/coder/coder/agent"
|
"github.com/coder/coder/agent"
|
||||||
"github.com/coder/coder/coderd/coderdtest"
|
"github.com/coder/coder/coderd/coderdtest"
|
||||||
"github.com/coder/coder/codersdk"
|
"github.com/coder/coder/codersdk"
|
||||||
|
"github.com/coder/coder/codersdk/agentsdk"
|
||||||
"github.com/coder/coder/provisioner/echo"
|
"github.com/coder/coder/provisioner/echo"
|
||||||
"github.com/coder/coder/provisionersdk/proto"
|
"github.com/coder/coder/provisionersdk/proto"
|
||||||
"github.com/coder/coder/testutil"
|
"github.com/coder/coder/testutil"
|
||||||
|
@ -56,7 +57,7 @@ func TestDeploymentInsights(t *testing.T) {
|
||||||
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
||||||
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
||||||
|
|
||||||
agentClient := codersdk.New(client.URL)
|
agentClient := agentsdk.New(client.URL)
|
||||||
agentClient.SetSessionToken(authToken)
|
agentClient.SetSessionToken(authToken)
|
||||||
agentCloser := agent.New(agent.Options{
|
agentCloser := agent.New(agent.Options{
|
||||||
Logger: slogtest.Make(t, nil),
|
Logger: slogtest.Make(t, nil),
|
||||||
|
|
|
@ -18,6 +18,7 @@ import (
|
||||||
"github.com/coder/coder/coderd/rbac"
|
"github.com/coder/coder/coderd/rbac"
|
||||||
"github.com/coder/coder/coderd/util/ptr"
|
"github.com/coder/coder/coderd/util/ptr"
|
||||||
"github.com/coder/coder/codersdk"
|
"github.com/coder/coder/codersdk"
|
||||||
|
"github.com/coder/coder/codersdk/agentsdk"
|
||||||
"github.com/coder/coder/provisioner/echo"
|
"github.com/coder/coder/provisioner/echo"
|
||||||
"github.com/coder/coder/provisionersdk/proto"
|
"github.com/coder/coder/provisionersdk/proto"
|
||||||
"github.com/coder/coder/testutil"
|
"github.com/coder/coder/testutil"
|
||||||
|
@ -544,7 +545,7 @@ func TestTemplateMetrics(t *testing.T) {
|
||||||
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
||||||
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
||||||
|
|
||||||
agentClient := codersdk.New(client.URL)
|
agentClient := agentsdk.New(client.URL)
|
||||||
agentClient.SetSessionToken(authToken)
|
agentClient.SetSessionToken(authToken)
|
||||||
agentCloser := agent.New(agent.Options{
|
agentCloser := agent.New(agent.Options{
|
||||||
Logger: slogtest.Make(t, nil),
|
Logger: slogtest.Make(t, nil),
|
||||||
|
|
|
@ -751,7 +751,7 @@ func oauth2Callback(t *testing.T, client *codersdk.Client) *http.Response {
|
||||||
req, err := http.NewRequestWithContext(context.Background(), "GET", oauthURL.String(), nil)
|
req, err := http.NewRequestWithContext(context.Background(), "GET", oauthURL.String(), nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
req.AddCookie(&http.Cookie{
|
req.AddCookie(&http.Cookie{
|
||||||
Name: codersdk.OAuth2StateKey,
|
Name: codersdk.OAuth2StateCookie,
|
||||||
Value: state,
|
Value: state,
|
||||||
})
|
})
|
||||||
res, err := client.HTTPClient.Do(req)
|
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)
|
req, err := http.NewRequestWithContext(context.Background(), "GET", oauthURL.String(), nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
req.AddCookie(&http.Cookie{
|
req.AddCookie(&http.Cookie{
|
||||||
Name: codersdk.OAuth2StateKey,
|
Name: codersdk.OAuth2StateCookie,
|
||||||
Value: "somestate",
|
Value: "somestate",
|
||||||
})
|
})
|
||||||
res, err := client.HTTPClient.Do(req)
|
res, err := client.HTTPClient.Do(req)
|
||||||
|
@ -790,7 +790,7 @@ func i64ptr(i int64) *int64 {
|
||||||
|
|
||||||
func authCookieValue(cookies []*http.Cookie) string {
|
func authCookieValue(cookies []*http.Cookie) string {
|
||||||
for _, cookie := range cookies {
|
for _, cookie := range cookies {
|
||||||
if cookie.Name == codersdk.SessionTokenKey {
|
if cookie.Name == codersdk.SessionTokenCookie {
|
||||||
return cookie.Value
|
return cookie.Value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1079,7 +1079,7 @@ func (api *API) postLogout(rw http.ResponseWriter, r *http.Request) {
|
||||||
cookie := &http.Cookie{
|
cookie := &http.Cookie{
|
||||||
// MaxAge < 0 means to delete the cookie now.
|
// MaxAge < 0 means to delete the cookie now.
|
||||||
MaxAge: -1,
|
MaxAge: -1,
|
||||||
Name: codersdk.SessionTokenKey,
|
Name: codersdk.SessionTokenCookie,
|
||||||
Path: "/",
|
Path: "/",
|
||||||
}
|
}
|
||||||
http.SetCookie(rw, cookie)
|
http.SetCookie(rw, cookie)
|
||||||
|
|
|
@ -230,7 +230,7 @@ func TestPostLogin(t *testing.T) {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
split := strings.Split(client.SessionToken(), "-")
|
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.NoError(t, err, "fetch login key")
|
||||||
require.Equal(t, int64(86400), key.LifetimeSeconds, "default should be 86400")
|
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{})
|
token, err := client.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{})
|
||||||
require.NoError(t, err, "make new token api key")
|
require.NoError(t, err, "make new token api key")
|
||||||
split = strings.Split(token.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.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")
|
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()
|
defer cancel()
|
||||||
|
|
||||||
keyID := strings.Split(client.SessionToken(), "-")[0]
|
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.NoError(t, err)
|
||||||
require.Equal(t, keyID, apiKey.ID, "API key should exist in the database")
|
require.Equal(t, keyID, apiKey.ID, "API key should exist in the database")
|
||||||
|
|
||||||
|
@ -323,15 +323,15 @@ func TestPostLogout(t *testing.T) {
|
||||||
|
|
||||||
var found bool
|
var found bool
|
||||||
for _, cookie := range cookies {
|
for _, cookie := range cookies {
|
||||||
if cookie.Name == codersdk.SessionTokenKey {
|
if cookie.Name == codersdk.SessionTokenCookie {
|
||||||
require.Equal(t, codersdk.SessionTokenKey, cookie.Name, "Cookie should be the auth cookie")
|
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")
|
require.Equal(t, -1, cookie.MaxAge, "Cookie should be set to delete")
|
||||||
found = true
|
found = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
require.True(t, found, "auth cookie should be returned")
|
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{}
|
sdkErr := &codersdk.Error{}
|
||||||
require.ErrorAs(t, err, &sdkErr)
|
require.ErrorAs(t, err, &sdkErr)
|
||||||
require.Equal(t, http.StatusUnauthorized, sdkErr.StatusCode(), "Expecting 401")
|
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
|
// Trying to get an API key should fail since our client's token
|
||||||
// has been deleted.
|
// 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)
|
require.Error(t, err)
|
||||||
cerr := coderdtest.SDKError(t, err)
|
cerr := coderdtest.SDKError(t, err)
|
||||||
require.Equal(t, http.StatusUnauthorized, cerr.StatusCode())
|
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
|
// Trying to get an API key should fail since all keys are deleted
|
||||||
// on password change.
|
// 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)
|
require.Error(t, err)
|
||||||
cerr = coderdtest.SDKError(t, err)
|
cerr = coderdtest.SDKError(t, err)
|
||||||
require.Equal(t, http.StatusNotFound, cerr.StatusCode())
|
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)
|
require.Error(t, err)
|
||||||
cerr = coderdtest.SDKError(t, err)
|
cerr = coderdtest.SDKError(t, err)
|
||||||
require.Equal(t, http.StatusNotFound, cerr.StatusCode())
|
require.Equal(t, http.StatusNotFound, cerr.StatusCode())
|
||||||
|
@ -833,7 +833,7 @@ func TestInitialRoles(t *testing.T) {
|
||||||
client := coderdtest.New(t, nil)
|
client := coderdtest.New(t, nil)
|
||||||
first := coderdtest.CreateFirstUser(t, client)
|
first := coderdtest.CreateFirstUser(t, client)
|
||||||
|
|
||||||
roles, err := client.GetUserRoles(ctx, codersdk.Me)
|
roles, err := client.UserRoles(ctx, codersdk.Me)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.ElementsMatch(t, roles.Roles, []string{
|
require.ElementsMatch(t, roles.Roles, []string{
|
||||||
rbac.RoleOwner(),
|
rbac.RoleOwner(),
|
||||||
|
|
|
@ -32,6 +32,7 @@ import (
|
||||||
"github.com/coder/coder/coderd/rbac"
|
"github.com/coder/coder/coderd/rbac"
|
||||||
"github.com/coder/coder/coderd/tracing"
|
"github.com/coder/coder/coderd/tracing"
|
||||||
"github.com/coder/coder/codersdk"
|
"github.com/coder/coder/codersdk"
|
||||||
|
"github.com/coder/coder/codersdk/agentsdk"
|
||||||
"github.com/coder/coder/tailnet"
|
"github.com/coder/coder/tailnet"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -76,7 +77,7 @@ func (api *API) workspaceAgent(rw http.ResponseWriter, r *http.Request) {
|
||||||
// @Security CoderSessionToken
|
// @Security CoderSessionToken
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Tags Agents
|
// @Tags Agents
|
||||||
// @Success 200 {object} codersdk.WorkspaceAgentMetadata
|
// @Success 200 {object} agentsdk.Metadata
|
||||||
// @Router /workspaceagents/me/metadata [get]
|
// @Router /workspaceagents/me/metadata [get]
|
||||||
func (api *API) workspaceAgentMetadata(rw http.ResponseWriter, r *http.Request) {
|
func (api *API) workspaceAgentMetadata(rw http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
@ -141,7 +142,7 @@ func (api *API) workspaceAgentMetadata(rw http.ResponseWriter, r *http.Request)
|
||||||
vscodeProxyURI += fmt.Sprintf(":%s", api.AccessURL.Port())
|
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),
|
Apps: convertApps(dbApps),
|
||||||
DERPMap: api.DERPMap,
|
DERPMap: api.DERPMap,
|
||||||
GitAuthConfigs: len(api.GitAuthConfigs),
|
GitAuthConfigs: len(api.GitAuthConfigs),
|
||||||
|
@ -160,7 +161,7 @@ func (api *API) workspaceAgentMetadata(rw http.ResponseWriter, r *http.Request)
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Tags Agents
|
// @Tags Agents
|
||||||
// @Param request body codersdk.PostWorkspaceAgentVersionRequest true "Version request"
|
// @Param request body agentsdk.PostVersionRequest true "Version request"
|
||||||
// @Success 200
|
// @Success 200
|
||||||
// @Router /workspaceagents/me/version [post]
|
// @Router /workspaceagents/me/version [post]
|
||||||
// @x-apidocgen {"skip": true}
|
// @x-apidocgen {"skip": true}
|
||||||
|
@ -176,7 +177,7 @@ func (api *API) postWorkspaceAgentVersion(rw http.ResponseWriter, r *http.Reques
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var req codersdk.PostWorkspaceAgentVersionRequest
|
var req agentsdk.PostVersionRequest
|
||||||
if !httpapi.Read(ctx, rw, r, &req) {
|
if !httpapi.Read(ctx, rw, r, &req) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -299,7 +300,7 @@ func (api *API) workspaceAgentPTY(rw http.ResponseWriter, r *http.Request) {
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Tags Agents
|
// @Tags Agents
|
||||||
// @Param workspaceagent path string true "Workspace agent ID" format(uuid)
|
// @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]
|
// @Router /workspaceagents/{workspaceagent}/listening-ports [get]
|
||||||
func (api *API) workspaceAgentListeningPorts(rw http.ResponseWriter, r *http.Request) {
|
func (api *API) workspaceAgentListeningPorts(rw http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
@ -382,15 +383,15 @@ func (api *API) workspaceAgentListeningPorts(rw http.ResponseWriter, r *http.Req
|
||||||
|
|
||||||
// Filter out ports that are globally blocked, in-use by applications, or
|
// Filter out ports that are globally blocked, in-use by applications, or
|
||||||
// common non-HTTP ports such as databases, FTP, SSH, etc.
|
// 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 {
|
for _, port := range portsResponse.Ports {
|
||||||
if port.Port < codersdk.MinimumListeningPort {
|
if port.Port < codersdk.WorkspaceAgentMinimumListeningPort {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if _, ok := appPorts[port.Port]; ok {
|
if _, ok := appPorts[port.Port]; ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if _, ok := codersdk.IgnoredListeningPorts[port.Port]; ok {
|
if _, ok := codersdk.WorkspaceAgentIgnoredListeningPorts[port.Port]; ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
filteredPorts = append(filteredPorts, port)
|
filteredPorts = append(filteredPorts, port)
|
||||||
|
@ -400,7 +401,7 @@ func (api *API) workspaceAgentListeningPorts(rw http.ResponseWriter, r *http.Req
|
||||||
httpapi.Write(ctx, rw, http.StatusOK, portsResponse)
|
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()
|
clientConn, serverConn := net.Pipe()
|
||||||
|
|
||||||
derpMap := api.DERPMap.Clone()
|
derpMap := api.DERPMap.Clone()
|
||||||
|
@ -467,7 +468,7 @@ func (api *API) dialWorkspaceAgentTailnet(r *http.Request, agentID uuid.UUID) (*
|
||||||
_ = conn.Close()
|
_ = conn.Close()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
return &codersdk.AgentConn{
|
return &codersdk.WorkspaceAgentConn{
|
||||||
Conn: conn,
|
Conn: conn,
|
||||||
CloseFunc: func() {
|
CloseFunc: func() {
|
||||||
_ = clientConn.Close()
|
_ = clientConn.Close()
|
||||||
|
@ -861,8 +862,8 @@ func convertWorkspaceAgent(derpMap *tailcfg.DERPMap, coordinator tailnet.Coordin
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Tags Agents
|
// @Tags Agents
|
||||||
// @Param request body codersdk.AgentStats true "Stats request"
|
// @Param request body agentsdk.Stats true "Stats request"
|
||||||
// @Success 200 {object} codersdk.AgentStatsResponse
|
// @Success 200 {object} agentsdk.StatsResponse
|
||||||
// @Router /workspaceagents/me/report-stats [post]
|
// @Router /workspaceagents/me/report-stats [post]
|
||||||
func (api *API) workspaceAgentReportStats(rw http.ResponseWriter, r *http.Request) {
|
func (api *API) workspaceAgentReportStats(rw http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
@ -877,13 +878,13 @@ func (api *API) workspaceAgentReportStats(rw http.ResponseWriter, r *http.Reques
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var req codersdk.AgentStats
|
var req agentsdk.Stats
|
||||||
if !httpapi.Read(ctx, rw, r, &req) {
|
if !httpapi.Read(ctx, rw, r, &req) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.RxBytes == 0 && req.TxBytes == 0 {
|
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,
|
ReportInterval: api.AgentStatsRefreshInterval,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
@ -928,7 +929,7 @@ func (api *API) workspaceAgentReportStats(rw http.ResponseWriter, r *http.Reques
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
httpapi.Write(ctx, rw, http.StatusOK, codersdk.AgentStatsResponse{
|
httpapi.Write(ctx, rw, http.StatusOK, agentsdk.StatsResponse{
|
||||||
ReportInterval: api.AgentStatsRefreshInterval,
|
ReportInterval: api.AgentStatsRefreshInterval,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -938,7 +939,7 @@ func (api *API) workspaceAgentReportStats(rw http.ResponseWriter, r *http.Reques
|
||||||
// @Security CoderSessionToken
|
// @Security CoderSessionToken
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Tags Agents
|
// @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"
|
// @Success 204 "Success"
|
||||||
// @Router /workspaceagents/me/report-lifecycle [post]
|
// @Router /workspaceagents/me/report-lifecycle [post]
|
||||||
// @x-apidocgen {"skip": true}
|
// @x-apidocgen {"skip": true}
|
||||||
|
@ -955,7 +956,7 @@ func (api *API) workspaceAgentReportLifecycle(rw http.ResponseWriter, r *http.Re
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var req codersdk.PostWorkspaceAgentLifecycleRequest
|
var req agentsdk.PostLifecycleRequest
|
||||||
if !httpapi.Read(ctx, rw, r, &req) {
|
if !httpapi.Read(ctx, rw, r, &req) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -994,13 +995,13 @@ func (api *API) workspaceAgentReportLifecycle(rw http.ResponseWriter, r *http.Re
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Tags Agents
|
// @Tags Agents
|
||||||
// @Param request body codersdk.PostWorkspaceAppHealthsRequest true "Application health request"
|
// @Param request body agentsdk.PostAppHealthsRequest true "Application health request"
|
||||||
// @Success 200
|
// @Success 200
|
||||||
// @Router /workspaceagents/me/app-health [post]
|
// @Router /workspaceagents/me/app-health [post]
|
||||||
func (api *API) postWorkspaceAppHealth(rw http.ResponseWriter, r *http.Request) {
|
func (api *API) postWorkspaceAppHealth(rw http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
workspaceAgent := httpmw.WorkspaceAgent(r)
|
workspaceAgent := httpmw.WorkspaceAgent(r)
|
||||||
var req codersdk.PostWorkspaceAppHealthsRequest
|
var req agentsdk.PostAppHealthsRequest
|
||||||
if !httpapi.Read(ctx, rw, r, &req) {
|
if !httpapi.Read(ctx, rw, r, &req) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -1122,7 +1123,7 @@ func (api *API) postWorkspaceAppHealth(rw http.ResponseWriter, r *http.Request)
|
||||||
// @Tags Agents
|
// @Tags Agents
|
||||||
// @Param url query string true "Git URL" format(uri)
|
// @Param url query string true "Git URL" format(uri)
|
||||||
// @Param listen query bool false "Wait for a new token to be issued"
|
// @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]
|
// @Router /workspaceagents/me/gitauth [get]
|
||||||
func (api *API) workspaceAgentsGitAuth(rw http.ResponseWriter, r *http.Request) {
|
func (api *API) workspaceAgentsGitAuth(rw http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
@ -1272,7 +1273,7 @@ func (api *API) workspaceAgentsGitAuth(rw http.ResponseWriter, r *http.Request)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
httpapi.Write(ctx, rw, http.StatusOK, codersdk.WorkspaceAgentGitAuthResponse{
|
httpapi.Write(ctx, rw, http.StatusOK, agentsdk.GitAuthResponse{
|
||||||
URL: redirectURL.String(),
|
URL: redirectURL.String(),
|
||||||
})
|
})
|
||||||
return
|
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
|
// If the token is expired and refresh is disabled, we prompt
|
||||||
// the user to authenticate again.
|
// the user to authenticate again.
|
||||||
if gitAuthConfig.NoRefresh && gitAuthLink.OAuthExpiry.Before(database.Now()) {
|
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(),
|
URL: redirectURL.String(),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
@ -1293,7 +1294,7 @@ func (api *API) workspaceAgentsGitAuth(rw http.ResponseWriter, r *http.Request)
|
||||||
Expiry: gitAuthLink.OAuthExpiry,
|
Expiry: gitAuthLink.OAuthExpiry,
|
||||||
}).Token()
|
}).Token()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpapi.Write(ctx, rw, http.StatusOK, codersdk.WorkspaceAgentGitAuthResponse{
|
httpapi.Write(ctx, rw, http.StatusOK, agentsdk.GitAuthResponse{
|
||||||
URL: redirectURL.String(),
|
URL: redirectURL.String(),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
@ -1310,7 +1311,7 @@ func (api *API) workspaceAgentsGitAuth(rw http.ResponseWriter, r *http.Request)
|
||||||
}
|
}
|
||||||
if !valid {
|
if !valid {
|
||||||
// The token is no longer 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(),
|
URL: redirectURL.String(),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
@ -1363,23 +1364,23 @@ func validateGitToken(ctx context.Context, validateURL, token string) (bool, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provider types have different username/password formats.
|
// Provider types have different username/password formats.
|
||||||
func formatGitAuthAccessToken(typ codersdk.GitProvider, token string) codersdk.WorkspaceAgentGitAuthResponse {
|
func formatGitAuthAccessToken(typ codersdk.GitProvider, token string) agentsdk.GitAuthResponse {
|
||||||
var resp codersdk.WorkspaceAgentGitAuthResponse
|
var resp agentsdk.GitAuthResponse
|
||||||
switch typ {
|
switch typ {
|
||||||
case codersdk.GitProviderGitLab:
|
case codersdk.GitProviderGitLab:
|
||||||
// https://stackoverflow.com/questions/25409700/using-gitlab-token-to-clone-without-authentication
|
// https://stackoverflow.com/questions/25409700/using-gitlab-token-to-clone-without-authentication
|
||||||
resp = codersdk.WorkspaceAgentGitAuthResponse{
|
resp = agentsdk.GitAuthResponse{
|
||||||
Username: "oauth2",
|
Username: "oauth2",
|
||||||
Password: token,
|
Password: token,
|
||||||
}
|
}
|
||||||
case codersdk.GitProviderBitBucket:
|
case codersdk.GitProviderBitBucket:
|
||||||
// https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud/#Cloning-a-repository-with-an-access-token
|
// 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",
|
Username: "x-token-auth",
|
||||||
Password: token,
|
Password: token,
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
resp = codersdk.WorkspaceAgentGitAuthResponse{
|
resp = agentsdk.GitAuthResponse{
|
||||||
Username: token,
|
Username: token,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ import (
|
||||||
"github.com/coder/coder/coderd/database"
|
"github.com/coder/coder/coderd/database"
|
||||||
"github.com/coder/coder/coderd/gitauth"
|
"github.com/coder/coder/coderd/gitauth"
|
||||||
"github.com/coder/coder/codersdk"
|
"github.com/coder/coder/codersdk"
|
||||||
|
"github.com/coder/coder/codersdk/agentsdk"
|
||||||
"github.com/coder/coder/provisioner/echo"
|
"github.com/coder/coder/provisioner/echo"
|
||||||
"github.com/coder/coder/provisionersdk/proto"
|
"github.com/coder/coder/provisionersdk/proto"
|
||||||
"github.com/coder/coder/testutil"
|
"github.com/coder/coder/testutil"
|
||||||
|
@ -210,7 +211,7 @@ func TestWorkspaceAgentListen(t *testing.T) {
|
||||||
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
||||||
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
||||||
|
|
||||||
agentClient := codersdk.New(client.URL)
|
agentClient := agentsdk.New(client.URL)
|
||||||
agentClient.SetSessionToken(authToken)
|
agentClient.SetSessionToken(authToken)
|
||||||
agentCloser := agent.New(agent.Options{
|
agentCloser := agent.New(agent.Options{
|
||||||
Client: agentClient,
|
Client: agentClient,
|
||||||
|
@ -299,10 +300,10 @@ func TestWorkspaceAgentListen(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
coderdtest.AwaitWorkspaceBuildJob(t, client, stopBuild.ID)
|
coderdtest.AwaitWorkspaceBuildJob(t, client, stopBuild.ID)
|
||||||
|
|
||||||
agentClient := codersdk.New(client.URL)
|
agentClient := agentsdk.New(client.URL)
|
||||||
agentClient.SetSessionToken(authToken)
|
agentClient.SetSessionToken(authToken)
|
||||||
|
|
||||||
_, err = agentClient.ListenWorkspaceAgent(ctx)
|
_, err = agentClient.Listen(ctx)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
require.ErrorContains(t, err, "build is outdated")
|
require.ErrorContains(t, err, "build is outdated")
|
||||||
})
|
})
|
||||||
|
@ -339,7 +340,7 @@ func TestWorkspaceAgentTailnet(t *testing.T) {
|
||||||
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
||||||
daemonCloser.Close()
|
daemonCloser.Close()
|
||||||
|
|
||||||
agentClient := codersdk.New(client.URL)
|
agentClient := agentsdk.New(client.URL)
|
||||||
agentClient.SetSessionToken(authToken)
|
agentClient.SetSessionToken(authToken)
|
||||||
agentCloser := agent.New(agent.Options{
|
agentCloser := agent.New(agent.Options{
|
||||||
Client: agentClient,
|
Client: agentClient,
|
||||||
|
@ -405,7 +406,7 @@ func TestWorkspaceAgentPTY(t *testing.T) {
|
||||||
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
||||||
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
||||||
|
|
||||||
agentClient := codersdk.New(client.URL)
|
agentClient := agentsdk.New(client.URL)
|
||||||
agentClient.SetSessionToken(authToken)
|
agentClient.SetSessionToken(authToken)
|
||||||
agentCloser := agent.New(agent.Options{
|
agentCloser := agent.New(agent.Options{
|
||||||
Client: agentClient,
|
Client: agentClient,
|
||||||
|
@ -502,7 +503,7 @@ func TestWorkspaceAgentListeningPorts(t *testing.T) {
|
||||||
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
||||||
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
||||||
|
|
||||||
agentClient := codersdk.New(client.URL)
|
agentClient := agentsdk.New(client.URL)
|
||||||
agentClient.SetSessionToken(authToken)
|
agentClient.SetSessionToken(authToken)
|
||||||
agentCloser := agent.New(agent.Options{
|
agentCloser := agent.New(agent.Options{
|
||||||
Client: agentClient,
|
Client: agentClient,
|
||||||
|
@ -517,10 +518,10 @@ func TestWorkspaceAgentListeningPorts(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
willFilterPort := func(port int) bool {
|
willFilterPort := func(port int) bool {
|
||||||
if port < codersdk.MinimumListeningPort || port > 65535 {
|
if port < codersdk.WorkspaceAgentMinimumListeningPort || port > 65535 {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if _, ok := codersdk.IgnoredListeningPorts[uint16(port)]; ok {
|
if _, ok := codersdk.WorkspaceAgentIgnoredListeningPorts[uint16(port)]; ok {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -560,7 +561,7 @@ func TestWorkspaceAgentListeningPorts(t *testing.T) {
|
||||||
port uint16
|
port uint16
|
||||||
)
|
)
|
||||||
require.Eventually(t, func() bool {
|
require.Eventually(t, func() bool {
|
||||||
for ignoredPort := range codersdk.IgnoredListeningPorts {
|
for ignoredPort := range codersdk.WorkspaceAgentIgnoredListeningPorts {
|
||||||
if ignoredPort < 1024 || ignoredPort == 5432 {
|
if ignoredPort < 1024 || ignoredPort == 5432 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -615,7 +616,7 @@ func TestWorkspaceAgentListeningPorts(t *testing.T) {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
for _, port := range res.Ports {
|
for _, port := range res.Ports {
|
||||||
if port.Network == codersdk.ListeningPortNetworkTCP {
|
if port.Network == "tcp" {
|
||||||
if val, ok := expected[port.Port]; ok {
|
if val, ok := expected[port.Port]; ok {
|
||||||
if val {
|
if val {
|
||||||
t.Fatalf("expected to find TCP port %d only once in response", port.Port)
|
t.Fatalf("expected to find TCP port %d only once in response", port.Port)
|
||||||
|
@ -637,7 +638,7 @@ func TestWorkspaceAgentListeningPorts(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
for _, port := range res.Ports {
|
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)
|
t.Fatalf("expected to not find TCP port %d in response", lPort)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -667,7 +668,7 @@ func TestWorkspaceAgentListeningPorts(t *testing.T) {
|
||||||
|
|
||||||
sawCoderdPort := false
|
sawCoderdPort := false
|
||||||
for _, port := range res.Ports {
|
for _, port := range res.Ports {
|
||||||
if port.Network == codersdk.ListeningPortNetworkTCP {
|
if port.Network == "tcp" {
|
||||||
if port.Port == appLPort {
|
if port.Port == appLPort {
|
||||||
t.Fatalf("expected to not find TCP port (app port) %d in response", appLPort)
|
t.Fatalf("expected to not find TCP port (app port) %d in response", appLPort)
|
||||||
}
|
}
|
||||||
|
@ -764,50 +765,50 @@ func TestWorkspaceAgentAppHealth(t *testing.T) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
agentClient := codersdk.New(client.URL)
|
agentClient := agentsdk.New(client.URL)
|
||||||
agentClient.SetSessionToken(authToken)
|
agentClient.SetSessionToken(authToken)
|
||||||
|
|
||||||
metadata, err := agentClient.WorkspaceAgentMetadata(ctx)
|
metadata, err := agentClient.Metadata(ctx)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.EqualValues(t, codersdk.WorkspaceAppHealthDisabled, metadata.Apps[0].Health)
|
require.EqualValues(t, codersdk.WorkspaceAppHealthDisabled, metadata.Apps[0].Health)
|
||||||
require.EqualValues(t, codersdk.WorkspaceAppHealthInitializing, metadata.Apps[1].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)
|
require.Error(t, err)
|
||||||
// empty
|
// empty
|
||||||
err = agentClient.PostWorkspaceAgentAppHealth(ctx, codersdk.PostWorkspaceAppHealthsRequest{})
|
err = agentClient.PostAppHealth(ctx, agentsdk.PostAppHealthsRequest{})
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
// healthcheck disabled
|
// healthcheck disabled
|
||||||
err = agentClient.PostWorkspaceAgentAppHealth(ctx, codersdk.PostWorkspaceAppHealthsRequest{
|
err = agentClient.PostAppHealth(ctx, agentsdk.PostAppHealthsRequest{
|
||||||
Healths: map[uuid.UUID]codersdk.WorkspaceAppHealth{
|
Healths: map[uuid.UUID]codersdk.WorkspaceAppHealth{
|
||||||
metadata.Apps[0].ID: codersdk.WorkspaceAppHealthInitializing,
|
metadata.Apps[0].ID: codersdk.WorkspaceAppHealthInitializing,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
// invalid value
|
// invalid value
|
||||||
err = agentClient.PostWorkspaceAgentAppHealth(ctx, codersdk.PostWorkspaceAppHealthsRequest{
|
err = agentClient.PostAppHealth(ctx, agentsdk.PostAppHealthsRequest{
|
||||||
Healths: map[uuid.UUID]codersdk.WorkspaceAppHealth{
|
Healths: map[uuid.UUID]codersdk.WorkspaceAppHealth{
|
||||||
metadata.Apps[1].ID: codersdk.WorkspaceAppHealth("bad-value"),
|
metadata.Apps[1].ID: codersdk.WorkspaceAppHealth("bad-value"),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
// update to healthy
|
// update to healthy
|
||||||
err = agentClient.PostWorkspaceAgentAppHealth(ctx, codersdk.PostWorkspaceAppHealthsRequest{
|
err = agentClient.PostAppHealth(ctx, agentsdk.PostAppHealthsRequest{
|
||||||
Healths: map[uuid.UUID]codersdk.WorkspaceAppHealth{
|
Healths: map[uuid.UUID]codersdk.WorkspaceAppHealth{
|
||||||
metadata.Apps[1].ID: codersdk.WorkspaceAppHealthHealthy,
|
metadata.Apps[1].ID: codersdk.WorkspaceAppHealthHealthy,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
metadata, err = agentClient.WorkspaceAgentMetadata(ctx)
|
metadata, err = agentClient.Metadata(ctx)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.EqualValues(t, codersdk.WorkspaceAppHealthHealthy, metadata.Apps[1].Health)
|
require.EqualValues(t, codersdk.WorkspaceAppHealthHealthy, metadata.Apps[1].Health)
|
||||||
// update to unhealthy
|
// update to unhealthy
|
||||||
err = agentClient.PostWorkspaceAgentAppHealth(ctx, codersdk.PostWorkspaceAppHealthsRequest{
|
err = agentClient.PostAppHealth(ctx, agentsdk.PostAppHealthsRequest{
|
||||||
Healths: map[uuid.UUID]codersdk.WorkspaceAppHealth{
|
Healths: map[uuid.UUID]codersdk.WorkspaceAppHealth{
|
||||||
metadata.Apps[1].ID: codersdk.WorkspaceAppHealthUnhealthy,
|
metadata.Apps[1].ID: codersdk.WorkspaceAppHealthUnhealthy,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
metadata, err = agentClient.WorkspaceAgentMetadata(ctx)
|
metadata, err = agentClient.Metadata(ctx)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.EqualValues(t, codersdk.WorkspaceAppHealthUnhealthy, metadata.Apps[1].Health)
|
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)
|
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
||||||
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
||||||
|
|
||||||
agentClient := codersdk.New(client.URL)
|
agentClient := agentsdk.New(client.URL)
|
||||||
agentClient.SetSessionToken(authToken)
|
agentClient.SetSessionToken(authToken)
|
||||||
_, err := agentClient.WorkspaceAgentGitAuth(context.Background(), "github.com", false)
|
_, err := agentClient.GitAuth(context.Background(), "github.com", false)
|
||||||
var apiError *codersdk.Error
|
var apiError *codersdk.Error
|
||||||
require.ErrorAs(t, err, &apiError)
|
require.ErrorAs(t, err, &apiError)
|
||||||
require.Equal(t, http.StatusNotFound, apiError.StatusCode())
|
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)
|
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
||||||
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
||||||
|
|
||||||
agentClient := codersdk.New(client.URL)
|
agentClient := agentsdk.New(client.URL)
|
||||||
agentClient.SetSessionToken(authToken)
|
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.NoError(t, err)
|
||||||
require.True(t, strings.HasSuffix(token.URL, fmt.Sprintf("/gitauth/%s", "github")))
|
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)
|
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
||||||
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
||||||
|
|
||||||
agentClient := codersdk.New(client.URL)
|
agentClient := agentsdk.New(client.URL)
|
||||||
agentClient.SetSessionToken(authToken)
|
agentClient.SetSessionToken(authToken)
|
||||||
|
|
||||||
resp := gitAuthCallback(t, "github", client)
|
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) {
|
srv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
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.NoError(t, err)
|
||||||
require.NotEmpty(t, res.URL)
|
require.NotEmpty(t, res.URL)
|
||||||
|
|
||||||
|
@ -1000,7 +1001,7 @@ func TestWorkspaceAgentsGitAuth(t *testing.T) {
|
||||||
w.WriteHeader(http.StatusForbidden)
|
w.WriteHeader(http.StatusForbidden)
|
||||||
w.Write([]byte("Something went wrong!"))
|
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
|
var apiError *codersdk.Error
|
||||||
require.ErrorAs(t, err, &apiError)
|
require.ErrorAs(t, err, &apiError)
|
||||||
require.Equal(t, http.StatusInternalServerError, apiError.StatusCode())
|
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)
|
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
||||||
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
||||||
|
|
||||||
agentClient := codersdk.New(client.URL)
|
agentClient := agentsdk.New(client.URL)
|
||||||
agentClient.SetSessionToken(authToken)
|
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.NoError(t, err)
|
||||||
require.NotEmpty(t, token.URL)
|
require.NotEmpty(t, token.URL)
|
||||||
|
|
||||||
|
@ -1067,7 +1068,7 @@ func TestWorkspaceAgentsGitAuth(t *testing.T) {
|
||||||
|
|
||||||
// Because the token is expired and `NoRefresh` is specified,
|
// Because the token is expired and `NoRefresh` is specified,
|
||||||
// a redirect URL should be returned again.
|
// 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.NoError(t, err)
|
||||||
require.NotEmpty(t, token.URL)
|
require.NotEmpty(t, token.URL)
|
||||||
})
|
})
|
||||||
|
@ -1110,17 +1111,17 @@ func TestWorkspaceAgentsGitAuth(t *testing.T) {
|
||||||
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
||||||
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
||||||
|
|
||||||
agentClient := codersdk.New(client.URL)
|
agentClient := agentsdk.New(client.URL)
|
||||||
agentClient.SetSessionToken(authToken)
|
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.NoError(t, err)
|
||||||
require.NotEmpty(t, token.URL)
|
require.NotEmpty(t, token.URL)
|
||||||
|
|
||||||
// Start waiting for the token callback...
|
// Start waiting for the token callback...
|
||||||
tokenChan := make(chan codersdk.WorkspaceAgentGitAuthResponse, 1)
|
tokenChan := make(chan agentsdk.GitAuthResponse, 1)
|
||||||
go func() {
|
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)
|
assert.NoError(t, err)
|
||||||
tokenChan <- token
|
tokenChan <- token
|
||||||
}()
|
}()
|
||||||
|
@ -1132,7 +1133,7 @@ func TestWorkspaceAgentsGitAuth(t *testing.T) {
|
||||||
token = <-tokenChan
|
token = <-tokenChan
|
||||||
require.Equal(t, "token", token.Username)
|
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)
|
require.NoError(t, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1173,10 +1174,10 @@ func TestWorkspaceAgentReportStats(t *testing.T) {
|
||||||
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
||||||
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
||||||
|
|
||||||
agentClient := codersdk.New(client.URL)
|
agentClient := agentsdk.New(client.URL)
|
||||||
agentClient.SetSessionToken(authToken)
|
agentClient.SetSessionToken(authToken)
|
||||||
|
|
||||||
_, err := agentClient.PostAgentStats(context.Background(), &codersdk.AgentStats{
|
_, err := agentClient.PostStats(context.Background(), &agentsdk.Stats{
|
||||||
ConnsByProto: map[string]int64{"TCP": 1},
|
ConnsByProto: map[string]int64{"TCP": 1},
|
||||||
NumConns: 1,
|
NumConns: 1,
|
||||||
RxPackets: 1,
|
RxPackets: 1,
|
||||||
|
@ -1206,11 +1207,11 @@ func gitAuthCallback(t *testing.T, id string, client *codersdk.Client) *http.Res
|
||||||
req, err := http.NewRequestWithContext(context.Background(), "GET", oauthURL.String(), nil)
|
req, err := http.NewRequestWithContext(context.Background(), "GET", oauthURL.String(), nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
req.AddCookie(&http.Cookie{
|
req.AddCookie(&http.Cookie{
|
||||||
Name: codersdk.OAuth2StateKey,
|
Name: codersdk.OAuth2StateCookie,
|
||||||
Value: state,
|
Value: state,
|
||||||
})
|
})
|
||||||
req.AddCookie(&http.Cookie{
|
req.AddCookie(&http.Cookie{
|
||||||
Name: codersdk.SessionTokenKey,
|
Name: codersdk.SessionTokenCookie,
|
||||||
Value: client.SessionToken(),
|
Value: client.SessionToken(),
|
||||||
})
|
})
|
||||||
res, err := client.HTTPClient.Do(req)
|
res, err := client.HTTPClient.Do(req)
|
||||||
|
@ -1263,7 +1264,7 @@ func TestWorkspaceAgent_LifecycleState(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
agentClient := codersdk.New(client.URL)
|
agentClient := agentsdk.New(client.URL)
|
||||||
agentClient.SetSessionToken(authToken)
|
agentClient.SetSessionToken(authToken)
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
|
@ -1284,7 +1285,7 @@ func TestWorkspaceAgent_LifecycleState(t *testing.T) {
|
||||||
t.Run(string(tt.state), func(t *testing.T) {
|
t.Run(string(tt.state), func(t *testing.T) {
|
||||||
ctx, _ := testutil.Context(t)
|
ctx, _ := testutil.Context(t)
|
||||||
|
|
||||||
err := agentClient.PostWorkspaceAgentLifecycle(ctx, codersdk.PostWorkspaceAgentLifecycleRequest{
|
err := agentClient.PostLifecycle(ctx, agentsdk.PostLifecycleRequest{
|
||||||
State: tt.state,
|
State: tt.state,
|
||||||
})
|
})
|
||||||
if tt.wantErr {
|
if tt.wantErr {
|
||||||
|
|
|
@ -77,7 +77,7 @@ const (
|
||||||
// @Security CoderSessionToken
|
// @Security CoderSessionToken
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Tags Applications
|
// @Tags Applications
|
||||||
// @Success 200 {object} codersdk.GetAppHostResponse
|
// @Success 200 {object} codersdk.AppHostResponse
|
||||||
// @Router /applications/host [get]
|
// @Router /applications/host [get]
|
||||||
func (api *API) appHost(rw http.ResponseWriter, r *http.Request) {
|
func (api *API) appHost(rw http.ResponseWriter, r *http.Request) {
|
||||||
host := api.AppHostname
|
host := api.AppHostname
|
||||||
|
@ -85,7 +85,7 @@ func (api *API) appHost(rw http.ResponseWriter, r *http.Request) {
|
||||||
host += fmt.Sprintf(":%s", api.AccessURL.Port())
|
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,
|
Host: host,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -862,9 +862,9 @@ func (api *API) proxyWorkspaceApplication(proxyApp proxyApplication, rw http.Res
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if portInt < codersdk.MinimumListeningPort {
|
if portInt < codersdk.WorkspaceAgentMinimumListeningPort {
|
||||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ import (
|
||||||
"github.com/coder/coder/coderd/httpmw"
|
"github.com/coder/coder/coderd/httpmw"
|
||||||
"github.com/coder/coder/coderd/rbac"
|
"github.com/coder/coder/coderd/rbac"
|
||||||
"github.com/coder/coder/codersdk"
|
"github.com/coder/coder/codersdk"
|
||||||
|
"github.com/coder/coder/codersdk/agentsdk"
|
||||||
"github.com/coder/coder/provisioner/echo"
|
"github.com/coder/coder/provisioner/echo"
|
||||||
"github.com/coder/coder/provisionersdk/proto"
|
"github.com/coder/coder/provisionersdk/proto"
|
||||||
"github.com/coder/coder/testutil"
|
"github.com/coder/coder/testutil"
|
||||||
|
@ -96,12 +97,12 @@ func TestGetAppHost(t *testing.T) {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
// Should not leak to unauthenticated users.
|
// Should not leak to unauthenticated users.
|
||||||
host, err := client.GetAppHost(ctx)
|
host, err := client.AppHost(ctx)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
require.Equal(t, "", host.Host)
|
require.Equal(t, "", host.Host)
|
||||||
|
|
||||||
_ = coderdtest.CreateFirstUser(t, client)
|
_ = coderdtest.CreateFirstUser(t, client)
|
||||||
host, err = client.GetAppHost(ctx)
|
host, err = client.AppHost(ctx)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, c.expected, host.Host)
|
require.Equal(t, c.expected, host.Host)
|
||||||
})
|
})
|
||||||
|
@ -134,7 +135,7 @@ func setupProxyTest(t *testing.T, opts *setupProxyTestOpts) (*codersdk.Client, c
|
||||||
server := http.Server{
|
server := http.Server{
|
||||||
ReadHeaderTimeout: time.Minute,
|
ReadHeaderTimeout: time.Minute,
|
||||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
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)
|
assert.ErrorIs(t, err, http.ErrNoCookie)
|
||||||
w.Header().Set("X-Forwarded-For", r.Header.Get("X-Forwarded-For"))
|
w.Header().Set("X-Forwarded-For", r.Header.Get("X-Forwarded-For"))
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
|
@ -252,10 +253,10 @@ func createWorkspaceWithApps(t *testing.T, client *codersdk.Client, orgID uuid.U
|
||||||
user, err := client.User(ctx, codersdk.Me)
|
user, err := client.User(ctx, codersdk.Me)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
agentClient := codersdk.New(client.URL)
|
agentClient := agentsdk.New(client.URL)
|
||||||
agentClient.SetSessionToken(authToken)
|
agentClient.SetSessionToken(authToken)
|
||||||
if appHost != "" {
|
if appHost != "" {
|
||||||
metadata, err := agentClient.WorkspaceAgentMetadata(context.Background())
|
metadata, err := agentClient.Metadata(context.Background())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
proxyURL := fmt.Sprintf(
|
proxyURL := fmt.Sprintf(
|
||||||
"http://{{port}}--%s--%s--%s%s",
|
"http://{{port}}--%s--%s--%s%s",
|
||||||
|
@ -437,7 +438,7 @@ func TestWorkspaceApplicationAuth(t *testing.T) {
|
||||||
// Get the current user and API key.
|
// Get the current user and API key.
|
||||||
user, err := client.User(ctx, codersdk.Me)
|
user, err := client.User(ctx, codersdk.Me)
|
||||||
require.NoError(t, err)
|
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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Try to load the application without authentication.
|
// Try to load the application without authentication.
|
||||||
|
@ -499,7 +500,7 @@ func TestWorkspaceApplicationAuth(t *testing.T) {
|
||||||
apiKey := cookies[0].Value
|
apiKey := cookies[0].Value
|
||||||
|
|
||||||
// Fetch the API key.
|
// 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.NoError(t, err)
|
||||||
require.Equal(t, user.ID, apiKeyInfo.UserID)
|
require.Equal(t, user.ID, apiKeyInfo.UserID)
|
||||||
require.Equal(t, codersdk.LoginTypePassword, apiKeyInfo.LoginType)
|
require.Equal(t, codersdk.LoginTypePassword, apiKeyInfo.LoginType)
|
||||||
|
@ -515,7 +516,7 @@ func TestWorkspaceApplicationAuth(t *testing.T) {
|
||||||
canCreateApplicationConnect = "can-create-application_connect"
|
canCreateApplicationConnect = "can-create-application_connect"
|
||||||
canReadUserMe = "can-read-user-me"
|
canReadUserMe = "can-read-user-me"
|
||||||
)
|
)
|
||||||
authRes, err := appClient.CheckAuthorization(ctx, codersdk.AuthorizationRequest{
|
authRes, err := appClient.AuthCheck(ctx, codersdk.AuthorizationRequest{
|
||||||
Checks: map[string]codersdk.AuthorizationCheck{
|
Checks: map[string]codersdk.AuthorizationCheck{
|
||||||
canCreateApplicationConnect: {
|
canCreateApplicationConnect: {
|
||||||
Object: codersdk.AuthorizationObject{
|
Object: codersdk.AuthorizationObject{
|
||||||
|
@ -546,7 +547,7 @@ func TestWorkspaceApplicationAuth(t *testing.T) {
|
||||||
t.Log("navigating to: ", gotLocation.String())
|
t.Log("navigating to: ", gotLocation.String())
|
||||||
req, err = http.NewRequestWithContext(ctx, "GET", gotLocation.String(), nil)
|
req, err = http.NewRequestWithContext(ctx, "GET", gotLocation.String(), nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
req.Header.Set(codersdk.SessionCustomHeader, apiKey)
|
req.Header.Set(codersdk.SessionTokenHeader, apiKey)
|
||||||
resp, err = doWithRetries(t, client, req)
|
resp, err = doWithRetries(t, client, req)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
|
@ -730,7 +731,7 @@ func TestWorkspaceAppsProxySubdomain(t *testing.T) {
|
||||||
require.NoError(t, err, "get workspaces")
|
require.NoError(t, err, "get workspaces")
|
||||||
require.Len(t, res.Workspaces, 1, "expected 1 workspace")
|
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")
|
require.NoError(t, err, "get app host")
|
||||||
|
|
||||||
subdomain := httpapi.ApplicationURL{
|
subdomain := httpapi.ApplicationURL{
|
||||||
|
@ -858,7 +859,7 @@ func TestWorkspaceAppsProxySubdomain(t *testing.T) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||||
defer cancel()
|
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)
|
resp, err := requestWithRetries(ctx, t, client, http.MethodGet, proxyURL(t, client, port, "/", proxyTestAppQuery), nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
@ -1049,7 +1050,7 @@ func TestAppSubdomainLogout(t *testing.T) {
|
||||||
_, err := client.User(ctx, codersdk.Me)
|
_, err := client.User(ctx, codersdk.Me)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
appHost, err := client.GetAppHost(ctx)
|
appHost, err := client.AppHost(ctx)
|
||||||
require.NoError(t, err, "get app host")
|
require.NoError(t, err, "get app host")
|
||||||
|
|
||||||
if c.cookie == "-" {
|
if c.cookie == "-" {
|
||||||
|
@ -1087,7 +1088,7 @@ func TestAppSubdomainLogout(t *testing.T) {
|
||||||
// The header is prioritized over the devurl cookie if both are
|
// The header is prioritized over the devurl cookie if both are
|
||||||
// set, so this ensures we can trigger the logout code path with
|
// set, so this ensures we can trigger the logout code path with
|
||||||
// bad cookies during tests.
|
// bad cookies during tests.
|
||||||
req.Header.Set(codersdk.SessionCustomHeader, client.SessionToken())
|
req.Header.Set(codersdk.SessionTokenHeader, client.SessionToken())
|
||||||
if c.cookie != "" {
|
if c.cookie != "" {
|
||||||
req.AddCookie(&http.Cookie{
|
req.AddCookie(&http.Cookie{
|
||||||
Name: httpmw.DevURLSessionTokenCookie,
|
Name: httpmw.DevURLSessionTokenCookie,
|
||||||
|
@ -1526,7 +1527,7 @@ func TestWorkspaceAppsNonCanonicalHeaders(t *testing.T) {
|
||||||
secWebSocketKey := "test-dean-was-here"
|
secWebSocketKey := "test-dean-was-here"
|
||||||
req.Header["Sec-WebSocket-Key"] = []string{secWebSocketKey}
|
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)
|
resp, err := doWithRetries(t, client, req)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
@ -1578,7 +1579,7 @@ func TestWorkspaceAppsNonCanonicalHeaders(t *testing.T) {
|
||||||
secWebSocketKey := "test-dean-was-here"
|
secWebSocketKey := "test-dean-was-here"
|
||||||
req.Header["Sec-WebSocket-Key"] = []string{secWebSocketKey}
|
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)
|
resp, err := doWithRetries(t, client, req)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"github.com/coder/coder/coderd/httpapi"
|
"github.com/coder/coder/coderd/httpapi"
|
||||||
"github.com/coder/coder/coderd/provisionerdserver"
|
"github.com/coder/coder/coderd/provisionerdserver"
|
||||||
"github.com/coder/coder/codersdk"
|
"github.com/coder/coder/codersdk"
|
||||||
|
"github.com/coder/coder/codersdk/agentsdk"
|
||||||
|
|
||||||
"github.com/mitchellh/mapstructure"
|
"github.com/mitchellh/mapstructure"
|
||||||
)
|
)
|
||||||
|
@ -26,12 +27,12 @@ import (
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Tags Agents
|
// @Tags Agents
|
||||||
// @Param request body codersdk.AzureInstanceIdentityToken true "Instance identity token"
|
// @Param request body agentsdk.AzureInstanceIdentityToken true "Instance identity token"
|
||||||
// @Success 200 {object} codersdk.WorkspaceAgentAuthenticateResponse
|
// @Success 200 {object} agentsdk.AuthenticateResponse
|
||||||
// @Router /workspaceagents/azure-instance-identity [post]
|
// @Router /workspaceagents/azure-instance-identity [post]
|
||||||
func (api *API) postWorkspaceAuthAzureInstanceIdentity(rw http.ResponseWriter, r *http.Request) {
|
func (api *API) postWorkspaceAuthAzureInstanceIdentity(rw http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
var req codersdk.AzureInstanceIdentityToken
|
var req agentsdk.AzureInstanceIdentityToken
|
||||||
if !httpapi.Read(ctx, rw, r, &req) {
|
if !httpapi.Read(ctx, rw, r, &req) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -56,12 +57,12 @@ func (api *API) postWorkspaceAuthAzureInstanceIdentity(rw http.ResponseWriter, r
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Tags Agents
|
// @Tags Agents
|
||||||
// @Param request body codersdk.AWSInstanceIdentityToken true "Instance identity token"
|
// @Param request body agentsdk.AWSInstanceIdentityToken true "Instance identity token"
|
||||||
// @Success 200 {object} codersdk.WorkspaceAgentAuthenticateResponse
|
// @Success 200 {object} agentsdk.AuthenticateResponse
|
||||||
// @Router /workspaceagents/aws-instance-identity [post]
|
// @Router /workspaceagents/aws-instance-identity [post]
|
||||||
func (api *API) postWorkspaceAuthAWSInstanceIdentity(rw http.ResponseWriter, r *http.Request) {
|
func (api *API) postWorkspaceAuthAWSInstanceIdentity(rw http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
var req codersdk.AWSInstanceIdentityToken
|
var req agentsdk.AWSInstanceIdentityToken
|
||||||
if !httpapi.Read(ctx, rw, r, &req) {
|
if !httpapi.Read(ctx, rw, r, &req) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -86,12 +87,12 @@ func (api *API) postWorkspaceAuthAWSInstanceIdentity(rw http.ResponseWriter, r *
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Tags Agents
|
// @Tags Agents
|
||||||
// @Param request body codersdk.GoogleInstanceIdentityToken true "Instance identity token"
|
// @Param request body agentsdk.GoogleInstanceIdentityToken true "Instance identity token"
|
||||||
// @Success 200 {object} codersdk.WorkspaceAgentAuthenticateResponse
|
// @Success 200 {object} agentsdk.AuthenticateResponse
|
||||||
// @Router /workspaceagents/google-instance-identity [post]
|
// @Router /workspaceagents/google-instance-identity [post]
|
||||||
func (api *API) postWorkspaceAuthGoogleInstanceIdentity(rw http.ResponseWriter, r *http.Request) {
|
func (api *API) postWorkspaceAuthGoogleInstanceIdentity(rw http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
var req codersdk.GoogleInstanceIdentityToken
|
var req agentsdk.GoogleInstanceIdentityToken
|
||||||
if !httpapi.Read(ctx, rw, r, &req) {
|
if !httpapi.Read(ctx, rw, r, &req) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -196,7 +197,7 @@ func (api *API) handleAuthInstanceID(rw http.ResponseWriter, r *http.Request, in
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
httpapi.Write(ctx, rw, http.StatusOK, codersdk.WorkspaceAgentAuthenticateResponse{
|
httpapi.Write(ctx, rw, http.StatusOK, agentsdk.AuthenticateResponse{
|
||||||
SessionToken: agent.AuthToken.String(),
|
SessionToken: agent.AuthToken.String(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
|
|
||||||
"github.com/coder/coder/coderd/coderdtest"
|
"github.com/coder/coder/coderd/coderdtest"
|
||||||
"github.com/coder/coder/codersdk"
|
"github.com/coder/coder/codersdk"
|
||||||
|
"github.com/coder/coder/codersdk/agentsdk"
|
||||||
"github.com/coder/coder/provisioner/echo"
|
"github.com/coder/coder/provisioner/echo"
|
||||||
"github.com/coder/coder/provisionersdk/proto"
|
"github.com/coder/coder/provisionersdk/proto"
|
||||||
"github.com/coder/coder/testutil"
|
"github.com/coder/coder/testutil"
|
||||||
|
@ -50,7 +51,10 @@ func TestPostWorkspaceAuthAzureInstanceIdentity(t *testing.T) {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
client.HTTPClient = metadataClient
|
client.HTTPClient = metadataClient
|
||||||
_, err := client.AuthWorkspaceAzureInstanceIdentity(ctx)
|
agentClient := &agentsdk.Client{
|
||||||
|
SDK: client,
|
||||||
|
}
|
||||||
|
_, err := agentClient.AuthAzureInstanceIdentity(ctx)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,7 +96,10 @@ func TestPostWorkspaceAuthAWSInstanceIdentity(t *testing.T) {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
client.HTTPClient = metadataClient
|
client.HTTPClient = metadataClient
|
||||||
_, err := client.AuthWorkspaceAWSInstanceIdentity(ctx)
|
agentClient := &agentsdk.Client{
|
||||||
|
SDK: client,
|
||||||
|
}
|
||||||
|
_, err := agentClient.AuthAWSInstanceIdentity(ctx)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -110,7 +117,10 @@ func TestPostWorkspaceAuthGoogleInstanceIdentity(t *testing.T) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
_, err := client.AuthWorkspaceGoogleInstanceIdentity(ctx, "", metadata)
|
agentClient := &agentsdk.Client{
|
||||||
|
SDK: client,
|
||||||
|
}
|
||||||
|
_, err := agentClient.AuthGoogleInstanceIdentity(ctx, "", metadata)
|
||||||
var apiErr *codersdk.Error
|
var apiErr *codersdk.Error
|
||||||
require.ErrorAs(t, err, &apiErr)
|
require.ErrorAs(t, err, &apiErr)
|
||||||
require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode())
|
require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode())
|
||||||
|
@ -127,7 +137,10 @@ func TestPostWorkspaceAuthGoogleInstanceIdentity(t *testing.T) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
_, err := client.AuthWorkspaceGoogleInstanceIdentity(ctx, "", metadata)
|
agentClient := &agentsdk.Client{
|
||||||
|
SDK: client,
|
||||||
|
}
|
||||||
|
_, err := agentClient.AuthGoogleInstanceIdentity(ctx, "", metadata)
|
||||||
var apiErr *codersdk.Error
|
var apiErr *codersdk.Error
|
||||||
require.ErrorAs(t, err, &apiErr)
|
require.ErrorAs(t, err, &apiErr)
|
||||||
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
|
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
|
||||||
|
@ -168,7 +181,10 @@ func TestPostWorkspaceAuthGoogleInstanceIdentity(t *testing.T) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
_, err := client.AuthWorkspaceGoogleInstanceIdentity(ctx, "", metadata)
|
agentClient := &agentsdk.Client{
|
||||||
|
SDK: client,
|
||||||
|
}
|
||||||
|
_, err := agentClient.AuthGoogleInstanceIdentity(ctx, "", metadata)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ import (
|
||||||
"github.com/coder/coder/coderd/rbac"
|
"github.com/coder/coder/coderd/rbac"
|
||||||
"github.com/coder/coder/coderd/util/ptr"
|
"github.com/coder/coder/coderd/util/ptr"
|
||||||
"github.com/coder/coder/codersdk"
|
"github.com/coder/coder/codersdk"
|
||||||
|
"github.com/coder/coder/codersdk/agentsdk"
|
||||||
"github.com/coder/coder/cryptorand"
|
"github.com/coder/coder/cryptorand"
|
||||||
"github.com/coder/coder/provisioner/echo"
|
"github.com/coder/coder/provisioner/echo"
|
||||||
"github.com/coder/coder/provisionersdk/proto"
|
"github.com/coder/coder/provisionersdk/proto"
|
||||||
|
@ -900,7 +901,7 @@ func TestWorkspaceFilterManual(t *testing.T) {
|
||||||
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
||||||
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
||||||
|
|
||||||
agentClient := codersdk.New(client.URL)
|
agentClient := agentsdk.New(client.URL)
|
||||||
agentClient.SetSessionToken(authToken)
|
agentClient.SetSessionToken(authToken)
|
||||||
agentCloser := agent.New(agent.Options{
|
agentCloser := agent.New(agent.Options{
|
||||||
Client: agentClient,
|
Client: agentClient,
|
||||||
|
@ -1551,7 +1552,7 @@ func TestWorkspaceWatcher(t *testing.T) {
|
||||||
wait("agent timeout after create")
|
wait("agent timeout after create")
|
||||||
wait("agent timeout after start")
|
wait("agent timeout after start")
|
||||||
|
|
||||||
agentClient := codersdk.New(client.URL)
|
agentClient := agentsdk.New(client.URL)
|
||||||
agentClient.SetSessionToken(authToken)
|
agentClient.SetSessionToken(authToken)
|
||||||
agentCloser := agent.New(agent.Options{
|
agentCloser := agent.New(agent.Options{
|
||||||
Client: agentClient,
|
Client: agentClient,
|
||||||
|
|
|
@ -32,11 +32,11 @@ func New(dialer Dialer, inactiveTimeout time.Duration) *Cache {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dialer creates a new agent connection by ID.
|
// 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.
|
// Conn wraps an agent connection with a reusable HTTP transport.
|
||||||
type Conn struct {
|
type Conn struct {
|
||||||
*codersdk.AgentConn
|
*codersdk.WorkspaceAgentConn
|
||||||
|
|
||||||
locks atomic.Uint64
|
locks atomic.Uint64
|
||||||
timeoutMutex sync.Mutex
|
timeoutMutex sync.Mutex
|
||||||
|
@ -49,8 +49,8 @@ func (c *Conn) HTTPTransport() *http.Transport {
|
||||||
return c.transport
|
return c.transport
|
||||||
}
|
}
|
||||||
|
|
||||||
// CloseWithError ends the HTTP transport if exists, and closes the agent.
|
// Close ends the HTTP transport if exists, and closes the agent.
|
||||||
func (c *Conn) CloseWithError(err error) error {
|
func (c *Conn) Close() error {
|
||||||
if c.transport != nil {
|
if c.transport != nil {
|
||||||
c.transport.CloseIdleConnections()
|
c.transport.CloseIdleConnections()
|
||||||
}
|
}
|
||||||
|
@ -59,7 +59,7 @@ func (c *Conn) CloseWithError(err error) error {
|
||||||
if c.timeout != nil {
|
if c.timeout != nil {
|
||||||
c.timeout.Stop()
|
c.timeout.Stop()
|
||||||
}
|
}
|
||||||
return c.AgentConn.CloseWithError(err)
|
return c.WorkspaceAgentConn.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
type Cache struct {
|
type Cache struct {
|
||||||
|
@ -108,24 +108,20 @@ func (c *Cache) Acquire(r *http.Request, id uuid.UUID) (*Conn, func(), error) {
|
||||||
transport := defaultTransport.Clone()
|
transport := defaultTransport.Clone()
|
||||||
transport.DialContext = agentConn.DialContext
|
transport.DialContext = agentConn.DialContext
|
||||||
conn := &Conn{
|
conn := &Conn{
|
||||||
AgentConn: agentConn,
|
WorkspaceAgentConn: agentConn,
|
||||||
timeoutCancel: timeoutCancelFunc,
|
timeoutCancel: timeoutCancelFunc,
|
||||||
transport: transport,
|
transport: transport,
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
defer c.closeGroup.Done()
|
defer c.closeGroup.Done()
|
||||||
var err error
|
|
||||||
select {
|
select {
|
||||||
case <-timeoutCtx.Done():
|
case <-timeoutCtx.Done():
|
||||||
err = xerrors.New("cache timeout")
|
|
||||||
case <-c.closed:
|
case <-c.closed:
|
||||||
err = xerrors.New("cache closed")
|
|
||||||
case <-conn.Closed():
|
case <-conn.Closed():
|
||||||
}
|
}
|
||||||
|
|
||||||
c.connMap.Delete(id.String())
|
c.connMap.Delete(id.String())
|
||||||
c.connGroup.Forget(id.String())
|
c.connGroup.Forget(id.String())
|
||||||
_ = conn.CloseWithError(err)
|
_ = conn.Close()
|
||||||
}()
|
}()
|
||||||
return conn, nil
|
return conn, nil
|
||||||
})
|
})
|
||||||
|
|
|
@ -26,6 +26,7 @@ import (
|
||||||
"github.com/coder/coder/agent"
|
"github.com/coder/coder/agent"
|
||||||
"github.com/coder/coder/coderd/wsconncache"
|
"github.com/coder/coder/coderd/wsconncache"
|
||||||
"github.com/coder/coder/codersdk"
|
"github.com/coder/coder/codersdk"
|
||||||
|
"github.com/coder/coder/codersdk/agentsdk"
|
||||||
"github.com/coder/coder/tailnet"
|
"github.com/coder/coder/tailnet"
|
||||||
"github.com/coder/coder/tailnet/tailnettest"
|
"github.com/coder/coder/tailnet/tailnettest"
|
||||||
)
|
)
|
||||||
|
@ -38,8 +39,8 @@ func TestCache(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
t.Run("Same", func(t *testing.T) {
|
t.Run("Same", func(t *testing.T) {
|
||||||
t.Parallel()
|
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
|
return setupAgent(t, agentsdk.Metadata{}, 0), nil
|
||||||
}, 0)
|
}, 0)
|
||||||
defer func() {
|
defer func() {
|
||||||
_ = cache.Close()
|
_ = cache.Close()
|
||||||
|
@ -53,9 +54,9 @@ func TestCache(t *testing.T) {
|
||||||
t.Run("Expire", func(t *testing.T) {
|
t.Run("Expire", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
called := atomic.NewInt32(0)
|
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)
|
called.Add(1)
|
||||||
return setupAgent(t, codersdk.WorkspaceAgentMetadata{}, 0), nil
|
return setupAgent(t, agentsdk.Metadata{}, 0), nil
|
||||||
}, time.Microsecond)
|
}, time.Microsecond)
|
||||||
defer func() {
|
defer func() {
|
||||||
_ = cache.Close()
|
_ = cache.Close()
|
||||||
|
@ -72,8 +73,8 @@ func TestCache(t *testing.T) {
|
||||||
})
|
})
|
||||||
t.Run("NoExpireWhenLocked", func(t *testing.T) {
|
t.Run("NoExpireWhenLocked", func(t *testing.T) {
|
||||||
t.Parallel()
|
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
|
return setupAgent(t, agentsdk.Metadata{}, 0), nil
|
||||||
}, time.Microsecond)
|
}, time.Microsecond)
|
||||||
defer func() {
|
defer func() {
|
||||||
_ = cache.Close()
|
_ = cache.Close()
|
||||||
|
@ -105,8 +106,8 @@ func TestCache(t *testing.T) {
|
||||||
}()
|
}()
|
||||||
go server.Serve(random)
|
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
|
return setupAgent(t, agentsdk.Metadata{}, 0), nil
|
||||||
}, time.Microsecond)
|
}, time.Microsecond)
|
||||||
defer func() {
|
defer func() {
|
||||||
_ = cache.Close()
|
_ = cache.Close()
|
||||||
|
@ -144,7 +145,7 @@ func TestCache(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupAgent(t *testing.T, metadata codersdk.WorkspaceAgentMetadata, ptyTimeout time.Duration) *codersdk.AgentConn {
|
func setupAgent(t *testing.T, metadata agentsdk.Metadata, ptyTimeout time.Duration) *codersdk.WorkspaceAgentConn {
|
||||||
metadata.DERPMap = tailnettest.RunDERPAndSTUN(t)
|
metadata.DERPMap = tailnettest.RunDERPAndSTUN(t)
|
||||||
|
|
||||||
coordinator := tailnet.NewCoordinator()
|
coordinator := tailnet.NewCoordinator()
|
||||||
|
@ -182,7 +183,7 @@ func setupAgent(t *testing.T, metadata codersdk.WorkspaceAgentMetadata, ptyTimeo
|
||||||
return conn.UpdateNodes(node)
|
return conn.UpdateNodes(node)
|
||||||
})
|
})
|
||||||
conn.SetNodeCallback(sendNode)
|
conn.SetNodeCallback(sendNode)
|
||||||
return &codersdk.AgentConn{
|
return &codersdk.WorkspaceAgentConn{
|
||||||
Conn: conn,
|
Conn: conn,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -190,15 +191,15 @@ func setupAgent(t *testing.T, metadata codersdk.WorkspaceAgentMetadata, ptyTimeo
|
||||||
type client struct {
|
type client struct {
|
||||||
t *testing.T
|
t *testing.T
|
||||||
agentID uuid.UUID
|
agentID uuid.UUID
|
||||||
metadata codersdk.WorkspaceAgentMetadata
|
metadata agentsdk.Metadata
|
||||||
coordinator tailnet.Coordinator
|
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
|
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()
|
clientConn, serverConn := net.Pipe()
|
||||||
closed := make(chan struct{})
|
closed := make(chan struct{})
|
||||||
c.t.Cleanup(func() {
|
c.t.Cleanup(func() {
|
||||||
|
@ -213,18 +214,18 @@ func (c *client) ListenWorkspaceAgent(_ context.Context) (net.Conn, error) {
|
||||||
return clientConn, nil
|
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
|
return io.NopCloser(strings.NewReader("")), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*client) PostWorkspaceAgentLifecycle(_ context.Context, _ codersdk.PostWorkspaceAgentLifecycleRequest) error {
|
func (*client) PostLifecycle(_ context.Context, _ agentsdk.PostLifecycleRequest) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*client) PostWorkspaceAgentAppHealth(_ context.Context, _ codersdk.PostWorkspaceAppHealthsRequest) error {
|
func (*client) PostAppHealth(_ context.Context, _ agentsdk.PostAppHealthsRequest) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*client) PostWorkspaceAgentVersion(_ context.Context, _ string) error {
|
func (*client) PostVersion(_ context.Context, _ string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
|
@ -23,6 +23,7 @@ type APIKey struct {
|
||||||
LifetimeSeconds int64 `json:"lifetime_seconds" validate:"required"`
|
LifetimeSeconds int64 `json:"lifetime_seconds" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LoginType is the type of login used to create the API key.
|
||||||
type LoginType string
|
type LoginType string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -35,7 +36,10 @@ const (
|
||||||
type APIKeyScope string
|
type APIKeyScope string
|
||||||
|
|
||||||
const (
|
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"
|
APIKeyScopeApplicationConnect APIKeyScope = "application_connect"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -49,7 +53,9 @@ type GenerateAPIKeyResponse struct {
|
||||||
Key string `json:"key"`
|
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) {
|
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)
|
res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/keys/tokens", userID), req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -57,7 +63,7 @@ func (c *Client) CreateToken(ctx context.Context, userID string, req CreateToken
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
if res.StatusCode > http.StatusCreated {
|
if res.StatusCode > http.StatusCreated {
|
||||||
return GenerateAPIKeyResponse{}, readBodyAsError(res)
|
return GenerateAPIKeyResponse{}, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
var apiKey GenerateAPIKeyResponse
|
var apiKey GenerateAPIKeyResponse
|
||||||
|
@ -73,36 +79,36 @@ func (c *Client) CreateAPIKey(ctx context.Context, user string) (GenerateAPIKeyR
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
if res.StatusCode > http.StatusCreated {
|
if res.StatusCode > http.StatusCreated {
|
||||||
return GenerateAPIKeyResponse{}, readBodyAsError(res)
|
return GenerateAPIKeyResponse{}, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
var apiKey GenerateAPIKeyResponse
|
var apiKey GenerateAPIKeyResponse
|
||||||
return apiKey, json.NewDecoder(res.Body).Decode(&apiKey)
|
return apiKey, json.NewDecoder(res.Body).Decode(&apiKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTokens list machine API keys.
|
// Tokens list machine API keys.
|
||||||
func (c *Client) GetTokens(ctx context.Context, userID string) ([]APIKey, error) {
|
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)
|
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/keys/tokens", userID), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
if res.StatusCode > http.StatusOK {
|
if res.StatusCode > http.StatusOK {
|
||||||
return nil, readBodyAsError(res)
|
return nil, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
var apiKey = []APIKey{}
|
var apiKey = []APIKey{}
|
||||||
return apiKey, json.NewDecoder(res.Body).Decode(&apiKey)
|
return apiKey, json.NewDecoder(res.Body).Decode(&apiKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAPIKey returns the api key by id.
|
// APIKey returns the api key by id.
|
||||||
func (c *Client) GetAPIKey(ctx context.Context, userID string, id string) (*APIKey, error) {
|
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)
|
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/keys/%s", userID, id), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
if res.StatusCode > http.StatusCreated {
|
if res.StatusCode > http.StatusCreated {
|
||||||
return nil, readBodyAsError(res)
|
return nil, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
apiKey := &APIKey{}
|
apiKey := &APIKey{}
|
||||||
return apiKey, json.NewDecoder(res.Body).Decode(apiKey)
|
return apiKey, json.NewDecoder(res.Body).Decode(apiKey)
|
||||||
|
@ -116,7 +122,7 @@ func (c *Client) DeleteAPIKey(ctx context.Context, userID string, id string) err
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
if res.StatusCode > http.StatusNoContent {
|
if res.StatusCode > http.StatusNoContent {
|
||||||
return readBodyAsError(res)
|
return ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,43 +0,0 @@
|
||||||
package codersdk
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
type AppearanceConfig struct {
|
|
||||||
LogoURL string `json:"logo_url"`
|
|
||||||
ServiceBanner ServiceBannerConfig `json:"service_banner"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ServiceBannerConfig struct {
|
|
||||||
Enabled bool `json:"enabled"`
|
|
||||||
Message string `json:"message,omitempty"`
|
|
||||||
BackgroundColor string `json:"background_color,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) Appearance(ctx context.Context) (AppearanceConfig, error) {
|
|
||||||
res, err := c.Request(ctx, http.MethodGet, "/api/v2/appearance", nil)
|
|
||||||
if err != nil {
|
|
||||||
return AppearanceConfig{}, err
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
if res.StatusCode != http.StatusOK {
|
|
||||||
return AppearanceConfig{}, readBodyAsError(res)
|
|
||||||
}
|
|
||||||
var cfg AppearanceConfig
|
|
||||||
return cfg, json.NewDecoder(res.Body).Decode(&cfg)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) UpdateAppearance(ctx context.Context, appearance AppearanceConfig) error {
|
|
||||||
res, err := c.Request(ctx, http.MethodPut, "/api/v2/appearance", appearance)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
if res.StatusCode != http.StatusOK {
|
|
||||||
return readBodyAsError(res)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -59,7 +59,7 @@ const (
|
||||||
AuditActionStop AuditAction = "stop"
|
AuditActionStop AuditAction = "stop"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (a AuditAction) FriendlyString() string {
|
func (a AuditAction) Friendly() string {
|
||||||
switch a {
|
switch a {
|
||||||
case AuditActionCreate:
|
case AuditActionCreate:
|
||||||
return "created"
|
return "created"
|
||||||
|
@ -142,7 +142,7 @@ func (c *Client) AuditLogs(ctx context.Context, req AuditLogsRequest) (AuditLogR
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
|
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return AuditLogResponse{}, readBodyAsError(res)
|
return AuditLogResponse{}, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
var logRes AuditLogResponse
|
var logRes AuditLogResponse
|
||||||
|
@ -154,6 +154,8 @@ func (c *Client) AuditLogs(ctx context.Context, req AuditLogsRequest) (AuditLogR
|
||||||
return logRes, nil
|
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 {
|
func (c *Client) CreateTestAuditLog(ctx context.Context, req CreateTestAuditLogRequest) error {
|
||||||
res, err := c.Request(ctx, http.MethodPost, "/api/v2/audit/testgenerate", req)
|
res, err := c.Request(ctx, http.MethodPost, "/api/v2/audit/testgenerate", req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -56,14 +56,16 @@ type AuthorizationObject struct {
|
||||||
ResourceID string `json:"resource_id,omitempty"`
|
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)
|
res, err := c.Request(ctx, http.MethodPost, "/api/v2/authcheck", req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return AuthorizationResponse{}, readBodyAsError(res)
|
return AuthorizationResponse{}, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
var resp AuthorizationResponse
|
var resp AuthorizationResponse
|
||||||
return resp, json.NewDecoder(res.Body).Decode(&resp)
|
return resp, json.NewDecoder(res.Body).Decode(&resp)
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"mime"
|
"mime"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -28,17 +29,23 @@ import (
|
||||||
// shouldn't be likely to conflict with any user-application set cookies.
|
// shouldn't be likely to conflict with any user-application set cookies.
|
||||||
// Be sure to strip additional cookies in httpapi.StripCoderCookies!
|
// Be sure to strip additional cookies in httpapi.StripCoderCookies!
|
||||||
const (
|
const (
|
||||||
// SessionTokenKey represents the name of the cookie or query parameter the API key is stored in.
|
// SessionTokenCookie represents the name of the cookie or query parameter the API key is stored in.
|
||||||
SessionTokenKey = "coder_session_token"
|
SessionTokenCookie = "coder_session_token"
|
||||||
// SessionCustomHeader is the custom header to use for authentication.
|
// SessionTokenHeader is the custom header to use for authentication.
|
||||||
SessionCustomHeader = "Coder-Session-Token"
|
SessionTokenHeader = "Coder-Session-Token"
|
||||||
OAuth2StateKey = "oauth_state"
|
// OAuth2StateCookie is the name of the cookie that stores the oauth2 state.
|
||||||
OAuth2RedirectKey = "oauth_redirect"
|
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
|
// nolint: gosec
|
||||||
BypassRatelimitHeader = "X-Coder-Bypass-Ratelimit"
|
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{}{
|
var loggableMimeTypes = map[string]struct{}{
|
||||||
"application/json": {},
|
"application/json": {},
|
||||||
"text/plain": {},
|
"text/plain": {},
|
||||||
|
@ -63,65 +70,32 @@ type Client struct {
|
||||||
HTTPClient *http.Client
|
HTTPClient *http.Client
|
||||||
URL *url.URL
|
URL *url.URL
|
||||||
|
|
||||||
// Logger can be provided to log requests. Request method, URL and response
|
// Logger is optionally provided to log requests.
|
||||||
// status code will be logged by default.
|
// Method, URL, and response code will be logged by default.
|
||||||
Logger slog.Logger
|
Logger slog.Logger
|
||||||
// LogBodies determines whether the request and response bodies are logged
|
|
||||||
// to the provided Logger. This is useful for debugging or testing.
|
// LogBodies can be enabled to print request and response bodies to the logger.
|
||||||
LogBodies bool
|
LogBodies bool
|
||||||
|
|
||||||
// BypassRatelimits is an optional flag that can be set by the site owner to
|
// Trace can be enabled to propagate tracing spans to the Coder API.
|
||||||
// disable ratelimit checks for the client.
|
// This is useful for tracking a request end-to-end.
|
||||||
BypassRatelimits bool
|
Trace 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SessionToken returns the currently set token for the client.
|
||||||
func (c *Client) SessionToken() string {
|
func (c *Client) SessionToken() string {
|
||||||
c.mu.RLock()
|
c.mu.RLock()
|
||||||
defer c.mu.RUnlock()
|
defer c.mu.RUnlock()
|
||||||
return c.sessionToken
|
return c.sessionToken
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetSessionToken returns the currently set token for the client.
|
||||||
func (c *Client) SetSessionToken(token string) {
|
func (c *Client) SetSessionToken(token string) {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
c.sessionToken = token
|
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
|
// Request performs a HTTP request with the body provided. The caller is
|
||||||
// responsible for closing the response body.
|
// responsible for closing the response body.
|
||||||
func (c *Client) Request(ctx context.Context, method, path string, body interface{}, opts ...RequestOption) (*http.Response, error) {
|
func (c *Client) Request(ctx context.Context, method, path string, body interface{}, opts ...RequestOption) (*http.Response, error) {
|
||||||
|
@ -165,10 +139,7 @@ func (c *Client) Request(ctx context.Context, method, path string, body interfac
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, xerrors.Errorf("create request: %w", err)
|
return nil, xerrors.Errorf("create request: %w", err)
|
||||||
}
|
}
|
||||||
req.Header.Set(SessionCustomHeader, c.SessionToken())
|
req.Header.Set(SessionTokenHeader, c.SessionToken())
|
||||||
if c.BypassRatelimits {
|
|
||||||
req.Header.Set(BypassRatelimitHeader, "true")
|
|
||||||
}
|
|
||||||
|
|
||||||
if r != nil {
|
if r != nil {
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
@ -181,7 +152,7 @@ func (c *Client) Request(ctx context.Context, method, path string, body interfac
|
||||||
span.SetAttributes(semconv.HTTPClientAttributesFromHTTPRequest(req)...)
|
span.SetAttributes(semconv.HTTPClientAttributesFromHTTPRequest(req)...)
|
||||||
|
|
||||||
// Inject tracing headers if enabled.
|
// Inject tracing headers if enabled.
|
||||||
if c.PropagateTracing {
|
if c.Trace {
|
||||||
tmp := otel.GetTextMapPropagator()
|
tmp := otel.GetTextMapPropagator()
|
||||||
hc := propagation.HeaderCarrier(req.Header)
|
hc := propagation.HeaderCarrier(req.Header)
|
||||||
tmp.Inject(ctx, hc)
|
tmp.Inject(ctx, hc)
|
||||||
|
@ -235,28 +206,28 @@ func (c *Client) Request(ctx context.Context, method, path string, body interfac
|
||||||
return resp, err
|
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.
|
// 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 {
|
if res == nil {
|
||||||
return xerrors.Errorf("no body returned")
|
return xerrors.Errorf("no body returned")
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
contentType := res.Header.Get("Content-Type")
|
contentType := res.Header.Get("Content-Type")
|
||||||
|
|
||||||
var method, u string
|
var requestMethod, requestURL string
|
||||||
if res.Request != nil {
|
if res.Request != nil {
|
||||||
method = res.Request.Method
|
requestMethod = res.Request.Method
|
||||||
if res.Request.URL != nil {
|
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 {
|
if res.StatusCode == http.StatusUnauthorized {
|
||||||
// 401 means the user is not logged in
|
// 401 means the user is not logged in
|
||||||
// 403 would mean that the user is not authorized
|
// 403 would mean that the user is not authorized
|
||||||
helper = "Try logging in using 'coder login <url>'."
|
helpMessage = "Try logging in using 'coder login <url>'."
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := io.ReadAll(res.Body)
|
resp, err := io.ReadAll(res.Body)
|
||||||
|
@ -278,7 +249,7 @@ func readBodyAsError(res *http.Response) error {
|
||||||
Message: fmt.Sprintf("unexpected non-JSON response %q", contentType),
|
Message: fmt.Sprintf("unexpected non-JSON response %q", contentType),
|
||||||
Detail: string(resp),
|
Detail: string(resp),
|
||||||
},
|
},
|
||||||
Helper: helper,
|
Helper: helpMessage,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -291,7 +262,7 @@ func readBodyAsError(res *http.Response) error {
|
||||||
Response: Response{
|
Response: Response{
|
||||||
Message: "empty response body",
|
Message: "empty response body",
|
||||||
},
|
},
|
||||||
Helper: helper,
|
Helper: helpMessage,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return xerrors.Errorf("decode body: %w", err)
|
return xerrors.Errorf("decode body: %w", err)
|
||||||
|
@ -307,9 +278,9 @@ func readBodyAsError(res *http.Response) error {
|
||||||
return &Error{
|
return &Error{
|
||||||
Response: m,
|
Response: m,
|
||||||
statusCode: res.StatusCode,
|
statusCode: res.StatusCode,
|
||||||
method: method,
|
method: requestMethod,
|
||||||
url: u,
|
url: requestURL,
|
||||||
Helper: helper,
|
Helper: helpMessage,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -370,3 +341,68 @@ func parseMimeType(contentType string) string {
|
||||||
|
|
||||||
return mimeType
|
return mimeType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Response represents a generic HTTP response.
|
||||||
|
type Response struct {
|
||||||
|
// Message is an actionable message that depicts actions the request took.
|
||||||
|
// These messages should be fully formed sentences with proper punctuation.
|
||||||
|
// Examples:
|
||||||
|
// - "A user has been created."
|
||||||
|
// - "Failed to create a user."
|
||||||
|
Message string `json:"message"`
|
||||||
|
// Detail is a debug message that provides further insight into why the
|
||||||
|
// action failed. This information can be technical and a regular golang
|
||||||
|
// err.Error() text.
|
||||||
|
// - "database: too many open connections"
|
||||||
|
// - "stat: too many open files"
|
||||||
|
Detail string `json:"detail,omitempty"`
|
||||||
|
// Validations are form field-specific friendly error messages. They will be
|
||||||
|
// shown on a form field in the UI. These can also be used to add additional
|
||||||
|
// context if there is a set of errors in the primary 'Message'.
|
||||||
|
Validations []ValidationError `json:"validations,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidationError represents a scoped error to a user input.
|
||||||
|
type ValidationError struct {
|
||||||
|
Field string `json:"field" validate:"required"`
|
||||||
|
Detail string `json:"detail" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ValidationError) Error() string {
|
||||||
|
return fmt.Sprintf("field: %s detail: %s", e.Field, e.Detail)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ error = (*ValidationError)(nil)
|
||||||
|
|
||||||
|
// IsConnectionError is a convenience function for checking if the source of an
|
||||||
|
// error is due to a 'connection refused', 'no such host', etc.
|
||||||
|
func IsConnectionError(err error) bool {
|
||||||
|
var (
|
||||||
|
// E.g. no such host
|
||||||
|
dnsErr *net.DNSError
|
||||||
|
// Eg. connection refused
|
||||||
|
opErr *net.OpError
|
||||||
|
)
|
||||||
|
|
||||||
|
return xerrors.As(err, &dnsErr) || xerrors.As(err, &opErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func AsError(err error) (*Error, bool) {
|
||||||
|
var e *Error
|
||||||
|
return e, xerrors.As(err, &e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequestOption is a function that can be used to modify an http.Request.
|
||||||
|
type RequestOption func(*http.Request)
|
||||||
|
|
||||||
|
// WithQueryParam adds a query parameter to the request.
|
||||||
|
func WithQueryParam(key, value string) RequestOption {
|
||||||
|
return func(r *http.Request) {
|
||||||
|
if value == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
q := r.URL.Query()
|
||||||
|
q.Add(key, value)
|
||||||
|
r.URL.RawQuery = q.Encode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -6,9 +6,11 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -31,6 +33,59 @@ import (
|
||||||
|
|
||||||
const jsonCT = "application/json"
|
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) {
|
func Test_Client(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
@ -43,8 +98,7 @@ func Test_Client(t *testing.T) {
|
||||||
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
assert.Equal(t, method, r.Method)
|
assert.Equal(t, method, r.Method)
|
||||||
assert.Equal(t, path, r.URL.Path)
|
assert.Equal(t, path, r.URL.Path)
|
||||||
assert.Equal(t, token, r.Header.Get(SessionCustomHeader))
|
assert.Equal(t, token, r.Header.Get(SessionTokenHeader))
|
||||||
assert.Equal(t, "true", r.Header.Get(BypassRatelimitHeader))
|
|
||||||
assert.NotEmpty(t, r.Header.Get("Traceparent"))
|
assert.NotEmpty(t, r.Header.Get("Traceparent"))
|
||||||
for k, v := range r.Header {
|
for k, v := range r.Header {
|
||||||
t.Logf("header %q: %q", k, strings.Join(v, ", "))
|
t.Logf("header %q: %q", k, strings.Join(v, ", "))
|
||||||
|
@ -59,7 +113,6 @@ func Test_Client(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
client := New(u)
|
client := New(u)
|
||||||
client.SetSessionToken(token)
|
client.SetSessionToken(token)
|
||||||
client.BypassRatelimits = true
|
|
||||||
|
|
||||||
logBuf := bytes.NewBuffer(nil)
|
logBuf := bytes.NewBuffer(nil)
|
||||||
client.Logger = slog.Make(sloghuman.Sink(logBuf)).Leveled(slog.LevelDebug)
|
client.Logger = slog.Make(sloghuman.Sink(logBuf)).Leveled(slog.LevelDebug)
|
||||||
|
@ -83,7 +136,7 @@ func Test_Client(t *testing.T) {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
otel.SetLogger(logr.Discard())
|
otel.SetLogger(logr.Discard())
|
||||||
client.PropagateTracing = true
|
client.Trace = true
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
@ -240,7 +293,7 @@ func Test_readBodyAsError(t *testing.T) {
|
||||||
|
|
||||||
c.res.Request = c.req
|
c.res.Request = c.req
|
||||||
|
|
||||||
err := readBodyAsError(c.res)
|
err := ReadBodyAsError(c.res)
|
||||||
c.assert(t, err)
|
c.assert(t, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,11 +4,106 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/mod/semver"
|
||||||
"golang.org/x/xerrors"
|
"golang.org/x/xerrors"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Entitlement represents whether a feature is licensed.
|
||||||
|
type Entitlement string
|
||||||
|
|
||||||
|
const (
|
||||||
|
EntitlementEntitled Entitlement = "entitled"
|
||||||
|
EntitlementGracePeriod Entitlement = "grace_period"
|
||||||
|
EntitlementNotEntitled Entitlement = "not_entitled"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FeatureName represents the internal name of a feature.
|
||||||
|
// To add a new feature, add it to this set of enums as well as the FeatureNames
|
||||||
|
// array below.
|
||||||
|
type FeatureName string
|
||||||
|
|
||||||
|
const (
|
||||||
|
FeatureUserLimit FeatureName = "user_limit"
|
||||||
|
FeatureAuditLog FeatureName = "audit_log"
|
||||||
|
FeatureBrowserOnly FeatureName = "browser_only"
|
||||||
|
FeatureSCIM FeatureName = "scim"
|
||||||
|
FeatureTemplateRBAC FeatureName = "template_rbac"
|
||||||
|
FeatureHighAvailability FeatureName = "high_availability"
|
||||||
|
FeatureMultipleGitAuth FeatureName = "multiple_git_auth"
|
||||||
|
FeatureExternalProvisionerDaemons FeatureName = "external_provisioner_daemons"
|
||||||
|
FeatureAppearance FeatureName = "appearance"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FeatureNames must be kept in-sync with the Feature enum above.
|
||||||
|
var FeatureNames = []FeatureName{
|
||||||
|
FeatureUserLimit,
|
||||||
|
FeatureAuditLog,
|
||||||
|
FeatureBrowserOnly,
|
||||||
|
FeatureSCIM,
|
||||||
|
FeatureTemplateRBAC,
|
||||||
|
FeatureHighAvailability,
|
||||||
|
FeatureMultipleGitAuth,
|
||||||
|
FeatureExternalProvisionerDaemons,
|
||||||
|
FeatureAppearance,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Humanize returns the feature name in a human-readable format.
|
||||||
|
func (n FeatureName) Humanize() string {
|
||||||
|
switch n {
|
||||||
|
case FeatureTemplateRBAC:
|
||||||
|
return "Template RBAC"
|
||||||
|
case FeatureSCIM:
|
||||||
|
return "SCIM"
|
||||||
|
default:
|
||||||
|
return strings.Title(strings.ReplaceAll(string(n), "_", " "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AlwaysEnable returns if the feature is always enabled if entitled.
|
||||||
|
// Warning: We don't know if we need this functionality.
|
||||||
|
// This method may disappear at any time.
|
||||||
|
func (n FeatureName) AlwaysEnable() bool {
|
||||||
|
return map[FeatureName]bool{
|
||||||
|
FeatureMultipleGitAuth: true,
|
||||||
|
FeatureExternalProvisionerDaemons: true,
|
||||||
|
FeatureAppearance: true,
|
||||||
|
}[n]
|
||||||
|
}
|
||||||
|
|
||||||
|
type Feature struct {
|
||||||
|
Entitlement Entitlement `json:"entitlement"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Limit *int64 `json:"limit,omitempty"`
|
||||||
|
Actual *int64 `json:"actual,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Entitlements struct {
|
||||||
|
Features map[FeatureName]Feature `json:"features"`
|
||||||
|
Warnings []string `json:"warnings"`
|
||||||
|
Errors []string `json:"errors"`
|
||||||
|
HasLicense bool `json:"has_license"`
|
||||||
|
Trial bool `json:"trial"`
|
||||||
|
|
||||||
|
// DEPRECATED: use Experiments instead.
|
||||||
|
Experimental bool `json:"experimental"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Entitlements(ctx context.Context) (Entitlements, error) {
|
||||||
|
res, err := c.Request(ctx, http.MethodGet, "/api/v2/entitlements", nil)
|
||||||
|
if err != nil {
|
||||||
|
return Entitlements{}, err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
return Entitlements{}, ReadBodyAsError(res)
|
||||||
|
}
|
||||||
|
var ent Entitlements
|
||||||
|
return ent, json.NewDecoder(res.Body).Decode(&ent)
|
||||||
|
}
|
||||||
|
|
||||||
// DeploymentConfig is the central configuration for the coder server.
|
// DeploymentConfig is the central configuration for the coder server.
|
||||||
type DeploymentConfig struct {
|
type DeploymentConfig struct {
|
||||||
AccessURL *DeploymentConfigField[string] `json:"access_url" typescript:",notnull"`
|
AccessURL *DeploymentConfigField[string] `json:"access_url" typescript:",notnull"`
|
||||||
|
@ -234,9 +329,173 @@ func (c *Client) DeploymentConfig(ctx context.Context) (DeploymentConfig, error)
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
|
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return DeploymentConfig{}, readBodyAsError(res)
|
return DeploymentConfig{}, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
var df DeploymentConfig
|
var df DeploymentConfig
|
||||||
return df, json.NewDecoder(res.Body).Decode(&df)
|
return df, json.NewDecoder(res.Body).Decode(&df)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AppearanceConfig struct {
|
||||||
|
LogoURL string `json:"logo_url"`
|
||||||
|
ServiceBanner ServiceBannerConfig `json:"service_banner"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServiceBannerConfig struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
BackgroundColor string `json:"background_color,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Appearance returns the configuration that modifies the visual
|
||||||
|
// display of the dashboard.
|
||||||
|
func (c *Client) Appearance(ctx context.Context) (AppearanceConfig, error) {
|
||||||
|
res, err := c.Request(ctx, http.MethodGet, "/api/v2/appearance", nil)
|
||||||
|
if err != nil {
|
||||||
|
return AppearanceConfig{}, err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
return AppearanceConfig{}, ReadBodyAsError(res)
|
||||||
|
}
|
||||||
|
var cfg AppearanceConfig
|
||||||
|
return cfg, json.NewDecoder(res.Body).Decode(&cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) UpdateAppearance(ctx context.Context, appearance AppearanceConfig) error {
|
||||||
|
res, err := c.Request(ctx, http.MethodPut, "/api/v2/appearance", appearance)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
return ReadBodyAsError(res)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildInfoResponse contains build information for this instance of Coder.
|
||||||
|
type BuildInfoResponse struct {
|
||||||
|
// ExternalURL references the current Coder version.
|
||||||
|
// For production builds, this will link directly to a release. For development builds, this will link to a commit.
|
||||||
|
ExternalURL string `json:"external_url"`
|
||||||
|
// Version returns the semantic version of the build.
|
||||||
|
Version string `json:"version"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CanonicalVersion trims build information from the version.
|
||||||
|
// E.g. 'v0.7.4-devel+11573034' -> 'v0.7.4'.
|
||||||
|
func (b BuildInfoResponse) CanonicalVersion() string {
|
||||||
|
// We do a little hack here to massage the string into a form
|
||||||
|
// that works well with semver.
|
||||||
|
trimmed := strings.ReplaceAll(b.Version, "-devel+", "+devel-")
|
||||||
|
return semver.Canonical(trimmed)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildInfo returns build information for this instance of Coder.
|
||||||
|
func (c *Client) BuildInfo(ctx context.Context) (BuildInfoResponse, error) {
|
||||||
|
res, err := c.Request(ctx, http.MethodGet, "/api/v2/buildinfo", nil)
|
||||||
|
if err != nil {
|
||||||
|
return BuildInfoResponse{}, err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
return BuildInfoResponse{}, ReadBodyAsError(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
var buildInfo BuildInfoResponse
|
||||||
|
return buildInfo, json.NewDecoder(res.Body).Decode(&buildInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Experiment string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ExperimentAuthzQuerier is an internal experiment that enables the ExperimentAuthzQuerier
|
||||||
|
// interface for all RBAC operations. NOT READY FOR PRODUCTION USE.
|
||||||
|
ExperimentAuthzQuerier Experiment = "authz_querier"
|
||||||
|
|
||||||
|
// Add new experiments here!
|
||||||
|
// ExperimentExample Experiment = "example"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ExperimentsAll should include all experiments that are safe for
|
||||||
|
// users to opt-in to via --experimental='*'.
|
||||||
|
// Experiments that are not ready for consumption by all users should
|
||||||
|
// not be included here and will be essentially hidden.
|
||||||
|
ExperimentsAll = Experiments{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Experiments is a list of experiments that are enabled for the deployment.
|
||||||
|
// Multiple experiments may be enabled at the same time.
|
||||||
|
// Experiments are not safe for production use, and are not guaranteed to
|
||||||
|
// be backwards compatible. They may be removed or renamed at any time.
|
||||||
|
type Experiments []Experiment
|
||||||
|
|
||||||
|
func (e Experiments) Enabled(ex Experiment) bool {
|
||||||
|
for _, v := range e {
|
||||||
|
if v == ex {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Experiments(ctx context.Context) (Experiments, error) {
|
||||||
|
res, err := c.Request(ctx, http.MethodGet, "/api/v2/experiments", nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
return nil, ReadBodyAsError(res)
|
||||||
|
}
|
||||||
|
var exp []Experiment
|
||||||
|
return exp, json.NewDecoder(res.Body).Decode(&exp)
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeploymentDAUsResponse struct {
|
||||||
|
Entries []DAUEntry `json:"entries"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) DeploymentDAUs(ctx context.Context) (*DeploymentDAUsResponse, error) {
|
||||||
|
res, err := c.Request(ctx, http.MethodGet, "/api/v2/insights/daus", nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, xerrors.Errorf("execute request: %w", err)
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
return nil, ReadBodyAsError(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp DeploymentDAUsResponse
|
||||||
|
return &resp, json.NewDecoder(res.Body).Decode(&resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
type AppHostResponse struct {
|
||||||
|
// Host is the externally accessible URL for the Coder instance.
|
||||||
|
Host string `json:"host"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppHost returns the site-wide application wildcard hostname without the
|
||||||
|
// leading "*.", e.g. "apps.coder.com". Apps are accessible at:
|
||||||
|
// "<app-name>--<agent-name>--<workspace-name>--<username>.<app-host>", 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)
|
||||||
|
}
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -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))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -30,7 +30,7 @@ func (c *Client) Upload(ctx context.Context, contentType string, content []byte)
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
if res.StatusCode != http.StatusCreated && res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusCreated && res.StatusCode != http.StatusOK {
|
||||||
return UploadResponse{}, readBodyAsError(res)
|
return UploadResponse{}, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
var resp UploadResponse
|
var resp UploadResponse
|
||||||
return resp, json.NewDecoder(res.Body).Decode(&resp)
|
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()
|
defer res.Body.Close()
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return nil, "", readBodyAsError(res)
|
return nil, "", ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
data, err := io.ReadAll(res.Body)
|
data, err := io.ReadAll(res.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -18,11 +18,6 @@ type GitSSHKey struct {
|
||||||
PublicKey string `json:"public_key"`
|
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.
|
// GitSSHKey returns the user's git SSH public key.
|
||||||
func (c *Client) GitSSHKey(ctx context.Context, user string) (GitSSHKey, error) {
|
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)
|
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()
|
defer res.Body.Close()
|
||||||
|
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return GitSSHKey{}, readBodyAsError(res)
|
return GitSSHKey{}, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
var gitsshkey GitSSHKey
|
var gitsshkey GitSSHKey
|
||||||
|
@ -48,25 +43,9 @@ func (c *Client) RegenerateGitSSHKey(ctx context.Context, user string) (GitSSHKe
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
|
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return GitSSHKey{}, readBodyAsError(res)
|
return GitSSHKey{}, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
var gitsshkey GitSSHKey
|
var gitsshkey GitSSHKey
|
||||||
return gitsshkey, json.NewDecoder(res.Body).Decode(&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)
|
|
||||||
}
|
|
||||||
|
|
|
@ -36,7 +36,7 @@ func (c *Client) CreateGroup(ctx context.Context, orgID uuid.UUID, req CreateGro
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
|
|
||||||
if res.StatusCode != http.StatusCreated {
|
if res.StatusCode != http.StatusCreated {
|
||||||
return Group{}, readBodyAsError(res)
|
return Group{}, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
var resp Group
|
var resp Group
|
||||||
return resp, json.NewDecoder(res.Body).Decode(&resp)
|
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()
|
defer res.Body.Close()
|
||||||
|
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return nil, readBodyAsError(res)
|
return nil, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
var groups []Group
|
var groups []Group
|
||||||
|
@ -71,7 +71,7 @@ func (c *Client) GroupByOrgAndName(ctx context.Context, orgID uuid.UUID, name st
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
|
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return Group{}, readBodyAsError(res)
|
return Group{}, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
var resp Group
|
var resp Group
|
||||||
return resp, json.NewDecoder(res.Body).Decode(&resp)
|
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()
|
defer res.Body.Close()
|
||||||
|
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return Group{}, readBodyAsError(res)
|
return Group{}, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
var resp Group
|
var resp Group
|
||||||
return resp, json.NewDecoder(res.Body).Decode(&resp)
|
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()
|
defer res.Body.Close()
|
||||||
|
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return Group{}, readBodyAsError(res)
|
return Group{}, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
var resp Group
|
var resp Group
|
||||||
return resp, json.NewDecoder(res.Body).Decode(&resp)
|
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()
|
defer res.Body.Close()
|
||||||
|
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return readBodyAsError(res)
|
return ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -57,7 +57,7 @@ func (c *Client) AddLicense(ctx context.Context, r AddLicenseRequest) (License,
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
if res.StatusCode != http.StatusCreated {
|
if res.StatusCode != http.StatusCreated {
|
||||||
return License{}, readBodyAsError(res)
|
return License{}, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
var l License
|
var l License
|
||||||
d := json.NewDecoder(res.Body)
|
d := json.NewDecoder(res.Body)
|
||||||
|
@ -72,7 +72,7 @@ func (c *Client) Licenses(ctx context.Context) ([]License, error) {
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return nil, readBodyAsError(res)
|
return nil, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
var licenses []License
|
var licenses []License
|
||||||
d := json.NewDecoder(res.Body)
|
d := json.NewDecoder(res.Body)
|
||||||
|
@ -87,7 +87,7 @@ func (c *Client) DeleteLicense(ctx context.Context, id int32) error {
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return readBodyAsError(res)
|
return ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"`
|
|
||||||
}
|
|
|
@ -32,6 +32,14 @@ type Organization struct {
|
||||||
UpdatedAt time.Time `json:"updated_at" validate:"required" format:"date-time"`
|
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.
|
// CreateTemplateVersionRequest enables callers to create a new Template Version.
|
||||||
type CreateTemplateVersionRequest struct {
|
type CreateTemplateVersionRequest struct {
|
||||||
Name string `json:"name,omitempty" validate:"omitempty,template_name"`
|
Name string `json:"name,omitempty" validate:"omitempty,template_name"`
|
||||||
|
@ -99,7 +107,7 @@ func (c *Client) Organization(ctx context.Context, id uuid.UUID) (Organization,
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
|
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return Organization{}, readBodyAsError(res)
|
return Organization{}, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
var organization Organization
|
var organization Organization
|
||||||
|
@ -118,7 +126,7 @@ func (c *Client) ProvisionerDaemons(ctx context.Context) ([]ProvisionerDaemon, e
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
|
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return nil, readBodyAsError(res)
|
return nil, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
var daemons []ProvisionerDaemon
|
var daemons []ProvisionerDaemon
|
||||||
|
@ -138,7 +146,7 @@ func (c *Client) CreateTemplateVersion(ctx context.Context, organizationID uuid.
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
|
|
||||||
if res.StatusCode != http.StatusCreated {
|
if res.StatusCode != http.StatusCreated {
|
||||||
return TemplateVersion{}, readBodyAsError(res)
|
return TemplateVersion{}, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
var templateVersion TemplateVersion
|
var templateVersion TemplateVersion
|
||||||
|
@ -157,7 +165,7 @@ func (c *Client) TemplateVersionByOrganizationAndName(ctx context.Context, organ
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
|
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return TemplateVersion{}, readBodyAsError(res)
|
return TemplateVersion{}, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
var templateVersion TemplateVersion
|
var templateVersion TemplateVersion
|
||||||
|
@ -176,7 +184,7 @@ func (c *Client) CreateTemplate(ctx context.Context, organizationID uuid.UUID, r
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
|
|
||||||
if res.StatusCode != http.StatusCreated {
|
if res.StatusCode != http.StatusCreated {
|
||||||
return Template{}, readBodyAsError(res)
|
return Template{}, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
var template Template
|
var template Template
|
||||||
|
@ -195,7 +203,7 @@ func (c *Client) TemplatesByOrganization(ctx context.Context, organizationID uui
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
|
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return nil, readBodyAsError(res)
|
return nil, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
var templates []Template
|
var templates []Template
|
||||||
|
@ -214,7 +222,7 @@ func (c *Client) TemplateByName(ctx context.Context, organizationID uuid.UUID, n
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
|
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return Template{}, readBodyAsError(res)
|
return Template{}, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
var template Template
|
var template Template
|
||||||
|
@ -230,7 +238,7 @@ func (c *Client) CreateWorkspace(ctx context.Context, organizationID uuid.UUID,
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
|
|
||||||
if res.StatusCode != http.StatusCreated {
|
if res.StatusCode != http.StatusCreated {
|
||||||
return Workspace{}, readBodyAsError(res)
|
return Workspace{}, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
var workspace Workspace
|
var workspace Workspace
|
||||||
|
|
|
@ -110,7 +110,7 @@ func (c *Client) CreateParameter(ctx context.Context, scope ParameterScope, id u
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
|
|
||||||
if res.StatusCode != http.StatusCreated {
|
if res.StatusCode != http.StatusCreated {
|
||||||
return Parameter{}, readBodyAsError(res)
|
return Parameter{}, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
var param Parameter
|
var param Parameter
|
||||||
|
@ -125,7 +125,7 @@ func (c *Client) DeleteParameter(ctx context.Context, scope ParameterScope, id u
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
|
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return readBodyAsError(res)
|
return ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, _ = io.Copy(io.Discard, res.Body)
|
_, _ = 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()
|
defer res.Body.Close()
|
||||||
|
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return nil, readBodyAsError(res)
|
return nil, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
var parameters []Parameter
|
var parameters []Parameter
|
||||||
|
|
|
@ -104,7 +104,7 @@ func (c *Client) provisionerJobLogsBefore(ctx context.Context, path string, befo
|
||||||
}
|
}
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
return nil, readBodyAsError(res)
|
return nil, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
var logs []ProvisionerJobLog
|
var logs []ProvisionerJobLog
|
||||||
|
@ -126,7 +126,7 @@ func (c *Client) provisionerJobLogsAfter(ctx context.Context, path string, after
|
||||||
return nil, nil, xerrors.Errorf("create cookie jar: %w", err)
|
return nil, nil, xerrors.Errorf("create cookie jar: %w", err)
|
||||||
}
|
}
|
||||||
jar.SetCookies(followURL, []*http.Cookie{{
|
jar.SetCookies(followURL, []*http.Cookie{{
|
||||||
Name: SessionTokenKey,
|
Name: SessionTokenCookie,
|
||||||
Value: c.SessionToken(),
|
Value: c.SessionToken(),
|
||||||
}})
|
}})
|
||||||
httpClient := &http.Client{
|
httpClient := &http.Client{
|
||||||
|
@ -140,7 +140,7 @@ func (c *Client) provisionerJobLogsAfter(ctx context.Context, path string, after
|
||||||
if res == nil {
|
if res == nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
return nil, nil, readBodyAsError(res)
|
return nil, nil, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
logs := make(chan ProvisionerJobLog)
|
logs := make(chan ProvisionerJobLog)
|
||||||
decoder := json.NewDecoder(websocket.NetConn(ctx, conn, websocket.MessageText))
|
decoder := json.NewDecoder(websocket.NetConn(ctx, conn, websocket.MessageText))
|
||||||
|
@ -188,7 +188,7 @@ func (c *Client) ServeProvisionerDaemon(ctx context.Context, organization uuid.U
|
||||||
return nil, xerrors.Errorf("create cookie jar: %w", err)
|
return nil, xerrors.Errorf("create cookie jar: %w", err)
|
||||||
}
|
}
|
||||||
jar.SetCookies(serverURL, []*http.Cookie{{
|
jar.SetCookies(serverURL, []*http.Cookie{{
|
||||||
Name: SessionTokenKey,
|
Name: SessionTokenCookie,
|
||||||
Value: c.SessionToken(),
|
Value: c.SessionToken(),
|
||||||
}})
|
}})
|
||||||
httpClient := &http.Client{
|
httpClient := &http.Client{
|
||||||
|
@ -203,7 +203,7 @@ func (c *Client) ServeProvisionerDaemon(ctx context.Context, organization uuid.U
|
||||||
if res == nil {
|
if res == nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return nil, readBodyAsError(res)
|
return nil, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
// Align with the frame size of yamux.
|
// Align with the frame size of yamux.
|
||||||
conn.SetReadLimit(256 * 1024)
|
conn.SetReadLimit(256 * 1024)
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -36,7 +36,7 @@ func (c *Client) Replicas(ctx context.Context) ([]Replica, error) {
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
|
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return nil, readBodyAsError(res)
|
return nil, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
var replicas []Replica
|
var replicas []Replica
|
||||||
|
|
|
@ -27,7 +27,7 @@ func (c *Client) ListSiteRoles(ctx context.Context) ([]AssignableRoles, error) {
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return nil, readBodyAsError(res)
|
return nil, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
var roles []AssignableRoles
|
var roles []AssignableRoles
|
||||||
return roles, json.NewDecoder(res.Body).Decode(&roles)
|
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()
|
defer res.Body.Close()
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return nil, readBodyAsError(res)
|
return nil, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
var roles []AssignableRoles
|
var roles []AssignableRoles
|
||||||
return roles, json.NewDecoder(res.Body).Decode(&roles)
|
return roles, json.NewDecoder(res.Body).Decode(&roles)
|
||||||
|
|
|
@ -100,7 +100,7 @@ func (c *Client) Template(ctx context.Context, template uuid.UUID) (Template, er
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return Template{}, readBodyAsError(res)
|
return Template{}, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
var resp Template
|
var resp Template
|
||||||
return resp, json.NewDecoder(res.Body).Decode(&resp)
|
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()
|
defer res.Body.Close()
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return readBodyAsError(res)
|
return ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
return nil
|
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")
|
return Template{}, xerrors.New("template metadata not modified")
|
||||||
}
|
}
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return Template{}, readBodyAsError(res)
|
return Template{}, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
var updated Template
|
var updated Template
|
||||||
return updated, json.NewDecoder(res.Body).Decode(&updated)
|
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()
|
defer res.Body.Close()
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return readBodyAsError(res)
|
return ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -153,7 +153,7 @@ func (c *Client) TemplateACL(ctx context.Context, templateID uuid.UUID) (Templat
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return TemplateACL{}, readBodyAsError(res)
|
return TemplateACL{}, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
var acl TemplateACL
|
var acl TemplateACL
|
||||||
return acl, json.NewDecoder(res.Body).Decode(&acl)
|
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()
|
defer res.Body.Close()
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return readBodyAsError(res)
|
return ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -188,7 +188,7 @@ func (c *Client) TemplateVersionsByTemplate(ctx context.Context, req TemplateVer
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return nil, readBodyAsError(res)
|
return nil, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
var templateVersion []TemplateVersion
|
var templateVersion []TemplateVersion
|
||||||
return templateVersion, json.NewDecoder(res.Body).Decode(&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()
|
defer res.Body.Close()
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return TemplateVersion{}, readBodyAsError(res)
|
return TemplateVersion{}, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
var templateVersion TemplateVersion
|
var templateVersion TemplateVersion
|
||||||
return templateVersion, json.NewDecoder(res.Body).Decode(&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()
|
defer res.Body.Close()
|
||||||
|
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return nil, readBodyAsError(res)
|
return nil, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
var resp TemplateDAUsResponse
|
var resp TemplateDAUsResponse
|
||||||
|
@ -258,7 +258,7 @@ func (c *Client) TemplateExamples(ctx context.Context, organizationID uuid.UUID)
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return nil, readBodyAsError(res)
|
return nil, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
var templateExamples []TemplateExample
|
var templateExamples []TemplateExample
|
||||||
return templateExamples, json.NewDecoder(res.Body).Decode(&templateExamples)
|
return templateExamples, json.NewDecoder(res.Body).Decode(&templateExamples)
|
||||||
|
|
|
@ -55,7 +55,7 @@ func (c *Client) TemplateVersion(ctx context.Context, id uuid.UUID) (TemplateVer
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return TemplateVersion{}, readBodyAsError(res)
|
return TemplateVersion{}, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
var version TemplateVersion
|
var version TemplateVersion
|
||||||
return version, json.NewDecoder(res.Body).Decode(&version)
|
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()
|
defer res.Body.Close()
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return readBodyAsError(res)
|
return ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -82,7 +82,7 @@ func (c *Client) TemplateVersionRichParameters(ctx context.Context, version uuid
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return nil, readBodyAsError(res)
|
return nil, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
var params []TemplateVersionParameter
|
var params []TemplateVersionParameter
|
||||||
return params, json.NewDecoder(res.Body).Decode(¶ms)
|
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()
|
defer res.Body.Close()
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return nil, readBodyAsError(res)
|
return nil, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
var params []ParameterSchema
|
var params []ParameterSchema
|
||||||
return params, json.NewDecoder(res.Body).Decode(¶ms)
|
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()
|
defer res.Body.Close()
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return nil, readBodyAsError(res)
|
return nil, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
var params []ComputedParameter
|
var params []ComputedParameter
|
||||||
return params, json.NewDecoder(res.Body).Decode(¶ms)
|
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()
|
defer res.Body.Close()
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return nil, readBodyAsError(res)
|
return nil, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
var resources []WorkspaceResource
|
var resources []WorkspaceResource
|
||||||
return resources, json.NewDecoder(res.Body).Decode(&resources)
|
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()
|
defer res.Body.Close()
|
||||||
if res.StatusCode != http.StatusCreated {
|
if res.StatusCode != http.StatusCreated {
|
||||||
return ProvisionerJob{}, readBodyAsError(res)
|
return ProvisionerJob{}, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
var job ProvisionerJob
|
var job ProvisionerJob
|
||||||
|
@ -173,7 +173,7 @@ func (c *Client) TemplateVersionDryRun(ctx context.Context, version, job uuid.UU
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return ProvisionerJob{}, readBodyAsError(res)
|
return ProvisionerJob{}, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
var j ProvisionerJob
|
var j ProvisionerJob
|
||||||
|
@ -189,7 +189,7 @@ func (c *Client) TemplateVersionDryRunResources(ctx context.Context, version, jo
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return nil, readBodyAsError(res)
|
return nil, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
var resources []WorkspaceResource
|
var resources []WorkspaceResource
|
||||||
|
@ -216,7 +216,7 @@ func (c *Client) CancelTemplateVersionDryRun(ctx context.Context, version, job u
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return readBodyAsError(res)
|
return ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -228,7 +228,7 @@ func (c *Client) PreviousTemplateVersion(ctx context.Context, organization uuid.
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return TemplateVersion{}, readBodyAsError(res)
|
return TemplateVersion{}, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
var version TemplateVersion
|
var version TemplateVersion
|
||||||
return version, json.NewDecoder(res.Body).Decode(&version)
|
return version, json.NewDecoder(res.Body).Decode(&version)
|
||||||
|
|
|
@ -26,7 +26,7 @@ func (c *Client) UpdateCheck(ctx context.Context) (UpdateCheckResponse, error) {
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
|
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return UpdateCheckResponse{}, readBodyAsError(res)
|
return UpdateCheckResponse{}, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
var buildInfo UpdateCheckResponse
|
var buildInfo UpdateCheckResponse
|
||||||
|
|
|
@ -123,7 +123,7 @@ func (c *Client) HasFirstUser(ctx context.Context) (bool, error) {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return false, readBodyAsError(res)
|
return false, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
@ -137,7 +137,7 @@ func (c *Client) CreateFirstUser(ctx context.Context, req CreateFirstUserRequest
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
if res.StatusCode != http.StatusCreated {
|
if res.StatusCode != http.StatusCreated {
|
||||||
return CreateFirstUserResponse{}, readBodyAsError(res)
|
return CreateFirstUserResponse{}, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
var resp CreateFirstUserResponse
|
var resp CreateFirstUserResponse
|
||||||
return resp, json.NewDecoder(res.Body).Decode(&resp)
|
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()
|
defer res.Body.Close()
|
||||||
if res.StatusCode != http.StatusCreated {
|
if res.StatusCode != http.StatusCreated {
|
||||||
return User{}, readBodyAsError(res)
|
return User{}, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
var user User
|
var user User
|
||||||
return user, json.NewDecoder(res.Body).Decode(&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()
|
defer res.Body.Close()
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return readBodyAsError(res)
|
return ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -178,7 +178,7 @@ func (c *Client) UpdateUserProfile(ctx context.Context, user string, req UpdateU
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return User{}, readBodyAsError(res)
|
return User{}, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
var resp User
|
var resp User
|
||||||
return resp, json.NewDecoder(res.Body).Decode(&resp)
|
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()
|
defer res.Body.Close()
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return User{}, readBodyAsError(res)
|
return User{}, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
var resp User
|
var resp User
|
||||||
|
@ -218,7 +218,7 @@ func (c *Client) UpdateUserPassword(ctx context.Context, user string, req Update
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
if res.StatusCode != http.StatusNoContent {
|
if res.StatusCode != http.StatusNoContent {
|
||||||
return readBodyAsError(res)
|
return ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -232,7 +232,7 @@ func (c *Client) UpdateUserRoles(ctx context.Context, user string, req UpdateRol
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return User{}, readBodyAsError(res)
|
return User{}, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
var resp User
|
var resp User
|
||||||
return resp, json.NewDecoder(res.Body).Decode(&resp)
|
return resp, json.NewDecoder(res.Body).Decode(&resp)
|
||||||
|
@ -247,21 +247,21 @@ func (c *Client) UpdateOrganizationMemberRoles(ctx context.Context, organization
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return OrganizationMember{}, readBodyAsError(res)
|
return OrganizationMember{}, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
var member OrganizationMember
|
var member OrganizationMember
|
||||||
return member, json.NewDecoder(res.Body).Decode(&member)
|
return member, json.NewDecoder(res.Body).Decode(&member)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserRoles returns all roles the user has
|
// UserRoles returns all roles the user has
|
||||||
func (c *Client) GetUserRoles(ctx context.Context, user string) (UserRoles, error) {
|
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)
|
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/roles", user), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return UserRoles{}, err
|
return UserRoles{}, err
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return UserRoles{}, readBodyAsError(res)
|
return UserRoles{}, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
var roles UserRoles
|
var roles UserRoles
|
||||||
return roles, json.NewDecoder(res.Body).Decode(&roles)
|
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()
|
defer res.Body.Close()
|
||||||
if res.StatusCode != http.StatusCreated {
|
if res.StatusCode != http.StatusCreated {
|
||||||
return LoginWithPasswordResponse{}, readBodyAsError(res)
|
return LoginWithPasswordResponse{}, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
var resp LoginWithPasswordResponse
|
var resp LoginWithPasswordResponse
|
||||||
err = json.NewDecoder(res.Body).Decode(&resp)
|
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()
|
defer res.Body.Close()
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return User{}, readBodyAsError(res)
|
return User{}, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
var user User
|
var user User
|
||||||
return user, json.NewDecoder(res.Body).Decode(&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()
|
defer res.Body.Close()
|
||||||
|
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return GetUsersResponse{}, readBodyAsError(res)
|
return GetUsersResponse{}, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
var usersRes GetUsersResponse
|
var usersRes GetUsersResponse
|
||||||
|
@ -358,7 +358,7 @@ func (c *Client) OrganizationsByUser(ctx context.Context, user string) ([]Organi
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
if res.StatusCode > http.StatusOK {
|
if res.StatusCode > http.StatusOK {
|
||||||
return nil, readBodyAsError(res)
|
return nil, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
var orgs []Organization
|
var orgs []Organization
|
||||||
return orgs, json.NewDecoder(res.Body).Decode(&orgs)
|
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()
|
defer res.Body.Close()
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return Organization{}, readBodyAsError(res)
|
return Organization{}, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
var org Organization
|
var org Organization
|
||||||
return org, json.NewDecoder(res.Body).Decode(&org)
|
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()
|
defer res.Body.Close()
|
||||||
|
|
||||||
if res.StatusCode != http.StatusCreated {
|
if res.StatusCode != http.StatusCreated {
|
||||||
return Organization{}, readBodyAsError(res)
|
return Organization{}, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
var org Organization
|
var org Organization
|
||||||
|
@ -402,7 +402,7 @@ func (c *Client) AuthMethods(ctx context.Context) (AuthMethods, error) {
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
|
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return AuthMethods{}, readBodyAsError(res)
|
return AuthMethods{}, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
var userAuth AuthMethods
|
var userAuth AuthMethods
|
||||||
|
|
|
@ -24,21 +24,21 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
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
|
// connections from clients to this node. A dynamic address is not required because a Tailnet
|
||||||
// client only dials a single agent at a time.
|
// 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 (
|
const (
|
||||||
TailnetSSHPort = 1
|
WorkspaceAgentSSHPort = 1
|
||||||
TailnetReconnectingPTYPort = 2
|
WorkspaceAgentReconnectingPTYPort = 2
|
||||||
TailnetSpeedtestPort = 3
|
WorkspaceAgentSpeedtestPort = 3
|
||||||
// TailnetStatisticsPort serves a HTTP server with endpoints for gathering
|
// WorkspaceAgentStatisticsPort serves a HTTP server with endpoints for gathering
|
||||||
// agent statistics.
|
// 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
|
// 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
|
// 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
|
// 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
|
// 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
|
// *that* much. The user could bypass this in the CLI by using SSH instead
|
||||||
// anyways.
|
// anyways.
|
||||||
MinimumListeningPort = 9
|
WorkspaceAgentMinimumListeningPort = 9
|
||||||
)
|
)
|
||||||
|
|
||||||
// IgnoredListeningPorts contains a list of ports in the global ignore list.
|
// WorkspaceAgentIgnoredListeningPorts contains a list of ports to ignore when looking for
|
||||||
// This list contains common TCP ports that are not HTTP servers, such as
|
// running applications inside a workspace. We want to ignore non-HTTP servers,
|
||||||
// databases, SSH, FTP, etc.
|
// so we pre-populate this list with common ports that are not HTTP servers.
|
||||||
//
|
//
|
||||||
// This is implemented as a map for fast lookup.
|
// This is implemented as a map for fast lookup.
|
||||||
var IgnoredListeningPorts = map[uint16]struct{}{
|
var WorkspaceAgentIgnoredListeningPorts = map[uint16]struct{}{
|
||||||
0: {},
|
0: {},
|
||||||
// Ports 1-8 are reserved for future use by the Coder agent.
|
// Ports 1-8 are reserved for future use by the Coder agent.
|
||||||
1: {},
|
1: {},
|
||||||
|
@ -111,15 +111,57 @@ var IgnoredListeningPorts = map[uint16]struct{}{
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
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
|
// Add a thousand more ports to the ignore list during tests so it's easier
|
||||||
// to find an available port.
|
// to find an available port.
|
||||||
if strings.HasSuffix(os.Args[0], ".test") {
|
for i := 63000; i < 64000; i++ {
|
||||||
for i := 63000; i < 64000; i++ {
|
WorkspaceAgentIgnoredListeningPorts[uint16(i)] = struct{}{}
|
||||||
IgnoredListeningPorts[uint16(i)] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WorkspaceAgentConn represents a connection to a workspace agent.
|
||||||
|
// @typescript-ignore WorkspaceAgentConn
|
||||||
|
type WorkspaceAgentConn struct {
|
||||||
|
*tailnet.Conn
|
||||||
|
CloseFunc func()
|
||||||
|
}
|
||||||
|
|
||||||
|
// AwaitReachable waits for the agent to be reachable.
|
||||||
|
func (c *WorkspaceAgentConn) AwaitReachable(ctx context.Context) bool {
|
||||||
|
ctx, span := tracing.StartSpan(ctx)
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
return c.Conn.AwaitReachable(ctx, WorkspaceAgentIP)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ping pings the agent and returns the round-trip time.
|
||||||
|
// The bool returns true if the ping was made P2P.
|
||||||
|
func (c *WorkspaceAgentConn) Ping(ctx context.Context) (time.Duration, bool, error) {
|
||||||
|
ctx, span := tracing.StartSpan(ctx)
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
return c.Conn.Ping(ctx, WorkspaceAgentIP)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close ends the connection to the workspace agent.
|
||||||
|
func (c *WorkspaceAgentConn) Close() error {
|
||||||
|
if c.CloseFunc != nil {
|
||||||
|
c.CloseFunc()
|
||||||
|
}
|
||||||
|
return c.Conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// WorkspaceAgentReconnectingPTYInit initializes a new reconnecting PTY session.
|
||||||
|
// @typescript-ignore WorkspaceAgentReconnectingPTYInit
|
||||||
|
type WorkspaceAgentReconnectingPTYInit struct {
|
||||||
|
ID uuid.UUID
|
||||||
|
Height uint16
|
||||||
|
Width uint16
|
||||||
|
Command string
|
||||||
|
}
|
||||||
|
|
||||||
// ReconnectingPTYRequest is sent from the client to the server
|
// ReconnectingPTYRequest is sent from the client to the server
|
||||||
// to pipe data to a PTY.
|
// to pipe data to a PTY.
|
||||||
// @typescript-ignore ReconnectingPTYRequest
|
// @typescript-ignore ReconnectingPTYRequest
|
||||||
|
@ -129,56 +171,18 @@ type ReconnectingPTYRequest struct {
|
||||||
Width uint16 `json:"width"`
|
Width uint16 `json:"width"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// @typescript-ignore AgentConn
|
// ReconnectingPTY spawns a new reconnecting terminal session.
|
||||||
type AgentConn struct {
|
// `ReconnectingPTYRequest` should be JSON marshaled and written to the returned net.Conn.
|
||||||
*tailnet.Conn
|
// Raw terminal output will be read from the returned net.Conn.
|
||||||
CloseFunc func()
|
func (c *WorkspaceAgentConn) ReconnectingPTY(ctx context.Context, id uuid.UUID, height, width uint16, command string) (net.Conn, error) {
|
||||||
}
|
|
||||||
|
|
||||||
func (c *AgentConn) AwaitReachable(ctx context.Context) bool {
|
|
||||||
ctx, span := tracing.StartSpan(ctx)
|
ctx, span := tracing.StartSpan(ctx)
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
return c.Conn.AwaitReachable(ctx, TailnetIP)
|
conn, err := c.DialContextTCP(ctx, netip.AddrPortFrom(WorkspaceAgentIP, WorkspaceAgentReconnectingPTYPort))
|
||||||
}
|
|
||||||
|
|
||||||
// Ping pings the agent and returns the round-trip time.
|
|
||||||
// The bool returns true if the ping was made P2P.
|
|
||||||
func (c *AgentConn) Ping(ctx context.Context) (time.Duration, bool, error) {
|
|
||||||
ctx, span := tracing.StartSpan(ctx)
|
|
||||||
defer span.End()
|
|
||||||
|
|
||||||
return c.Conn.Ping(ctx, TailnetIP)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *AgentConn) CloseWithError(_ error) error {
|
|
||||||
return c.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *AgentConn) Close() error {
|
|
||||||
if c.CloseFunc != nil {
|
|
||||||
c.CloseFunc()
|
|
||||||
}
|
|
||||||
return c.Conn.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// @typescript-ignore ReconnectingPTYInit
|
|
||||||
type ReconnectingPTYInit struct {
|
|
||||||
ID uuid.UUID
|
|
||||||
Height uint16
|
|
||||||
Width uint16
|
|
||||||
Command string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *AgentConn) ReconnectingPTY(ctx context.Context, id uuid.UUID, height, width uint16, command string) (net.Conn, error) {
|
|
||||||
ctx, span := tracing.StartSpan(ctx)
|
|
||||||
defer span.End()
|
|
||||||
|
|
||||||
conn, err := c.DialContextTCP(ctx, netip.AddrPortFrom(TailnetIP, TailnetReconnectingPTYPort))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
data, err := json.Marshal(ReconnectingPTYInit{
|
data, err := json.Marshal(WorkspaceAgentReconnectingPTYInit{
|
||||||
ID: id,
|
ID: id,
|
||||||
Height: height,
|
Height: height,
|
||||||
Width: width,
|
Width: width,
|
||||||
|
@ -199,15 +203,17 @@ func (c *AgentConn) ReconnectingPTY(ctx context.Context, id uuid.UUID, height, w
|
||||||
return conn, nil
|
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)
|
ctx, span := tracing.StartSpan(ctx)
|
||||||
defer span.End()
|
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
|
// SSHClient calls SSH to create a client that uses a weak cipher
|
||||||
// for high throughput.
|
// to improve throughput.
|
||||||
func (c *AgentConn) SSHClient(ctx context.Context) (*ssh.Client, error) {
|
func (c *WorkspaceAgentConn) SSHClient(ctx context.Context) (*ssh.Client, error) {
|
||||||
ctx, span := tracing.StartSpan(ctx)
|
ctx, span := tracing.StartSpan(ctx)
|
||||||
defer span.End()
|
defer span.End()
|
||||||
netConn, err := c.SSH(ctx)
|
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
|
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)
|
ctx, span := tracing.StartSpan(ctx)
|
||||||
defer span.End()
|
defer span.End()
|
||||||
speedConn, err := c.DialContextTCP(ctx, netip.AddrPortFrom(TailnetIP, TailnetSpeedtestPort))
|
speedConn, err := c.DialContextTCP(ctx, netip.AddrPortFrom(WorkspaceAgentIP, WorkspaceAgentSpeedtestPort))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, xerrors.Errorf("dial speedtest: %w", err)
|
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
|
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)
|
ctx, span := tracing.StartSpan(ctx)
|
||||||
defer span.End()
|
defer span.End()
|
||||||
if network == "unix" {
|
if network == "unix" {
|
||||||
|
@ -248,14 +257,62 @@ func (c *AgentConn) DialContext(ctx context.Context, network string, addr string
|
||||||
}
|
}
|
||||||
_, rawPort, _ := net.SplitHostPort(addr)
|
_, rawPort, _ := net.SplitHostPort(addr)
|
||||||
port, _ := strconv.ParseUint(rawPort, 10, 16)
|
port, _ := strconv.ParseUint(rawPort, 10, 16)
|
||||||
ipp := netip.AddrPortFrom(TailnetIP, uint16(port))
|
ipp := netip.AddrPortFrom(WorkspaceAgentIP, uint16(port))
|
||||||
if network == "udp" {
|
if network == "udp" {
|
||||||
return c.Conn.DialContextUDP(ctx, ipp)
|
return c.Conn.DialContextUDP(ctx, ipp)
|
||||||
}
|
}
|
||||||
return c.Conn.DialContextTCP(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{
|
return &http.Client{
|
||||||
Transport: &http.Transport{
|
Transport: &http.Transport{
|
||||||
// Disable keep alives as we're usually only making a single
|
// 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
|
// Verify that host is TailnetIP and port is
|
||||||
// TailnetStatisticsPort.
|
// 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)
|
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 {
|
if err != nil {
|
||||||
return nil, xerrors.Errorf("dial statistics: %w", err)
|
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)
|
|
||||||
}
|
|
|
@ -5,16 +5,13 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/cookiejar"
|
"net/http/cookiejar"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"net/url"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"cloud.google.com/go/compute/metadata"
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"golang.org/x/xerrors"
|
"golang.org/x/xerrors"
|
||||||
"nhooyr.io/websocket"
|
"nhooyr.io/websocket"
|
||||||
|
@ -83,55 +80,11 @@ type WorkspaceAgent struct {
|
||||||
StartupScriptTimeoutSeconds int32 `db:"startup_script_timeout_seconds" json:"startup_script_timeout_seconds"`
|
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 {
|
type DERPRegion struct {
|
||||||
Preferred bool `json:"preferred"`
|
Preferred bool `json:"preferred"`
|
||||||
LatencyMilliseconds float64 `json:"latency_ms"`
|
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
|
// WorkspaceAgentConnectionInfo returns required information for establishing
|
||||||
// a connection with a workspace.
|
// a connection with a workspace.
|
||||||
// @typescript-ignore WorkspaceAgentConnectionInfo
|
// @typescript-ignore WorkspaceAgentConnectionInfo
|
||||||
|
@ -139,272 +92,6 @@ type WorkspaceAgentConnectionInfo struct {
|
||||||
DERPMap *tailcfg.DERPMap `json:"derp_map"`
|
DERPMap *tailcfg.DERPMap `json:"derp_map"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// @typescript-ignore PostWorkspaceAgentVersionRequest
|
|
||||||
// @Description x-apidocgen:skip
|
|
||||||
type PostWorkspaceAgentVersionRequest struct {
|
|
||||||
Version string `json:"version"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// @typescript-ignore WorkspaceAgentMetadata
|
|
||||||
type WorkspaceAgentMetadata struct {
|
|
||||||
// GitAuthConfigs stores the number of Git configurations
|
|
||||||
// the Coder deployment has. If this number is >0, we
|
|
||||||
// set up special configuration in the workspace.
|
|
||||||
GitAuthConfigs int `json:"git_auth_configs"`
|
|
||||||
VSCodePortProxyURI string `json:"vscode_port_proxy_uri"`
|
|
||||||
Apps []WorkspaceApp `json:"apps"`
|
|
||||||
DERPMap *tailcfg.DERPMap `json:"derpmap"`
|
|
||||||
EnvironmentVariables map[string]string `json:"environment_variables"`
|
|
||||||
StartupScript string `json:"startup_script"`
|
|
||||||
StartupScriptTimeout time.Duration `json:"startup_script_timeout"`
|
|
||||||
Directory string `json:"directory"`
|
|
||||||
MOTDFile string `json:"motd_file"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// AuthWorkspaceGoogleInstanceIdentity uses the Google Compute Engine Metadata API to
|
|
||||||
// fetch a signed JWT, and exchange it for a session token for a workspace agent.
|
|
||||||
//
|
|
||||||
// The requesting instance must be registered as a resource in the latest history for a workspace.
|
|
||||||
func (c *Client) AuthWorkspaceGoogleInstanceIdentity(ctx context.Context, serviceAccount string, gcpClient *metadata.Client) (WorkspaceAgentAuthenticateResponse, error) {
|
|
||||||
if serviceAccount == "" {
|
|
||||||
// This is the default name specified by Google.
|
|
||||||
serviceAccount = "default"
|
|
||||||
}
|
|
||||||
if gcpClient == nil {
|
|
||||||
gcpClient = metadata.NewClient(c.HTTPClient)
|
|
||||||
}
|
|
||||||
// "format=full" is required, otherwise the responding payload will be missing "instance_id".
|
|
||||||
jwt, err := gcpClient.Get(fmt.Sprintf("instance/service-accounts/%s/identity?audience=coder&format=full", serviceAccount))
|
|
||||||
if err != nil {
|
|
||||||
return WorkspaceAgentAuthenticateResponse{}, xerrors.Errorf("get metadata identity: %w", err)
|
|
||||||
}
|
|
||||||
res, err := c.Request(ctx, http.MethodPost, "/api/v2/workspaceagents/google-instance-identity", GoogleInstanceIdentityToken{
|
|
||||||
JSONWebToken: jwt,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return WorkspaceAgentAuthenticateResponse{}, err
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
if res.StatusCode != http.StatusOK {
|
|
||||||
return WorkspaceAgentAuthenticateResponse{}, readBodyAsError(res)
|
|
||||||
}
|
|
||||||
var resp WorkspaceAgentAuthenticateResponse
|
|
||||||
return resp, json.NewDecoder(res.Body).Decode(&resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AuthWorkspaceAWSInstanceIdentity uses the Amazon Metadata API to
|
|
||||||
// fetch a signed payload, and exchange it for a session token for a workspace agent.
|
|
||||||
//
|
|
||||||
// The requesting instance must be registered as a resource in the latest history for a workspace.
|
|
||||||
func (c *Client) AuthWorkspaceAWSInstanceIdentity(ctx context.Context) (WorkspaceAgentAuthenticateResponse, error) {
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodPut, "http://169.254.169.254/latest/api/token", nil)
|
|
||||||
if err != nil {
|
|
||||||
return WorkspaceAgentAuthenticateResponse{}, nil
|
|
||||||
}
|
|
||||||
req.Header.Set("X-aws-ec2-metadata-token-ttl-seconds", "21600")
|
|
||||||
res, err := c.HTTPClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return WorkspaceAgentAuthenticateResponse{}, err
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
token, err := io.ReadAll(res.Body)
|
|
||||||
if err != nil {
|
|
||||||
return WorkspaceAgentAuthenticateResponse{}, xerrors.Errorf("read token: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err = http.NewRequestWithContext(ctx, http.MethodGet, "http://169.254.169.254/latest/dynamic/instance-identity/signature", nil)
|
|
||||||
if err != nil {
|
|
||||||
return WorkspaceAgentAuthenticateResponse{}, nil
|
|
||||||
}
|
|
||||||
req.Header.Set("X-aws-ec2-metadata-token", string(token))
|
|
||||||
res, err = c.HTTPClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return WorkspaceAgentAuthenticateResponse{}, err
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
signature, err := io.ReadAll(res.Body)
|
|
||||||
if err != nil {
|
|
||||||
return WorkspaceAgentAuthenticateResponse{}, xerrors.Errorf("read token: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err = http.NewRequestWithContext(ctx, http.MethodGet, "http://169.254.169.254/latest/dynamic/instance-identity/document", nil)
|
|
||||||
if err != nil {
|
|
||||||
return WorkspaceAgentAuthenticateResponse{}, nil
|
|
||||||
}
|
|
||||||
req.Header.Set("X-aws-ec2-metadata-token", string(token))
|
|
||||||
res, err = c.HTTPClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return WorkspaceAgentAuthenticateResponse{}, err
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
document, err := io.ReadAll(res.Body)
|
|
||||||
if err != nil {
|
|
||||||
return WorkspaceAgentAuthenticateResponse{}, xerrors.Errorf("read token: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err = c.Request(ctx, http.MethodPost, "/api/v2/workspaceagents/aws-instance-identity", AWSInstanceIdentityToken{
|
|
||||||
Signature: string(signature),
|
|
||||||
Document: string(document),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return WorkspaceAgentAuthenticateResponse{}, err
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
if res.StatusCode != http.StatusOK {
|
|
||||||
return WorkspaceAgentAuthenticateResponse{}, readBodyAsError(res)
|
|
||||||
}
|
|
||||||
var resp WorkspaceAgentAuthenticateResponse
|
|
||||||
return resp, json.NewDecoder(res.Body).Decode(&resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AuthWorkspaceAzureInstanceIdentity uses the Azure Instance Metadata Service to
|
|
||||||
// fetch a signed payload, and exchange it for a session token for a workspace agent.
|
|
||||||
func (c *Client) AuthWorkspaceAzureInstanceIdentity(ctx context.Context) (WorkspaceAgentAuthenticateResponse, error) {
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://169.254.169.254/metadata/attested/document?api-version=2020-09-01", nil)
|
|
||||||
if err != nil {
|
|
||||||
return WorkspaceAgentAuthenticateResponse{}, nil
|
|
||||||
}
|
|
||||||
req.Header.Set("Metadata", "true")
|
|
||||||
res, err := c.HTTPClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return WorkspaceAgentAuthenticateResponse{}, err
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
|
|
||||||
var token AzureInstanceIdentityToken
|
|
||||||
err = json.NewDecoder(res.Body).Decode(&token)
|
|
||||||
if err != nil {
|
|
||||||
return WorkspaceAgentAuthenticateResponse{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err = c.Request(ctx, http.MethodPost, "/api/v2/workspaceagents/azure-instance-identity", token)
|
|
||||||
if err != nil {
|
|
||||||
return WorkspaceAgentAuthenticateResponse{}, err
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
if res.StatusCode != http.StatusOK {
|
|
||||||
return WorkspaceAgentAuthenticateResponse{}, readBodyAsError(res)
|
|
||||||
}
|
|
||||||
var resp WorkspaceAgentAuthenticateResponse
|
|
||||||
return resp, json.NewDecoder(res.Body).Decode(&resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WorkspaceAgentMetadata fetches metadata for the currently authenticated workspace agent.
|
|
||||||
func (c *Client) WorkspaceAgentMetadata(ctx context.Context) (WorkspaceAgentMetadata, error) {
|
|
||||||
res, err := c.Request(ctx, http.MethodGet, "/api/v2/workspaceagents/me/metadata", nil)
|
|
||||||
if err != nil {
|
|
||||||
return WorkspaceAgentMetadata{}, err
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
if res.StatusCode != http.StatusOK {
|
|
||||||
return WorkspaceAgentMetadata{}, readBodyAsError(res)
|
|
||||||
}
|
|
||||||
var agentMetadata WorkspaceAgentMetadata
|
|
||||||
err = json.NewDecoder(res.Body).Decode(&agentMetadata)
|
|
||||||
if err != nil {
|
|
||||||
return WorkspaceAgentMetadata{}, err
|
|
||||||
}
|
|
||||||
accessingPort := c.URL.Port()
|
|
||||||
if accessingPort == "" {
|
|
||||||
accessingPort = "80"
|
|
||||||
if c.URL.Scheme == "https" {
|
|
||||||
accessingPort = "443"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
accessPort, err := strconv.Atoi(accessingPort)
|
|
||||||
if err != nil {
|
|
||||||
return WorkspaceAgentMetadata{}, xerrors.Errorf("convert accessing port %q: %w", accessingPort, err)
|
|
||||||
}
|
|
||||||
// Agents can provide an arbitrary access URL that may be different
|
|
||||||
// that the globally configured one. This breaks the built-in DERP,
|
|
||||||
// which would continue to reference the global access URL.
|
|
||||||
//
|
|
||||||
// This converts all built-in DERPs to use the access URL that the
|
|
||||||
// metadata request was performed with.
|
|
||||||
for _, region := range agentMetadata.DERPMap.Regions {
|
|
||||||
if !region.EmbeddedRelay {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, node := range region.Nodes {
|
|
||||||
if node.STUNOnly {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
node.HostName = c.URL.Hostname()
|
|
||||||
node.DERPPort = accessPort
|
|
||||||
node.ForceHTTP = c.URL.Scheme == "http"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return agentMetadata, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) ListenWorkspaceAgent(ctx context.Context) (net.Conn, error) {
|
|
||||||
coordinateURL, err := c.URL.Parse("/api/v2/workspaceagents/me/coordinate")
|
|
||||||
if err != nil {
|
|
||||||
return nil, xerrors.Errorf("parse url: %w", err)
|
|
||||||
}
|
|
||||||
jar, err := cookiejar.New(nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, xerrors.Errorf("create cookie jar: %w", err)
|
|
||||||
}
|
|
||||||
jar.SetCookies(coordinateURL, []*http.Cookie{{
|
|
||||||
Name: SessionTokenKey,
|
|
||||||
Value: c.SessionToken(),
|
|
||||||
}})
|
|
||||||
httpClient := &http.Client{
|
|
||||||
Jar: jar,
|
|
||||||
Transport: c.HTTPClient.Transport,
|
|
||||||
}
|
|
||||||
// nolint:bodyclose
|
|
||||||
conn, res, err := websocket.Dial(ctx, coordinateURL.String(), &websocket.DialOptions{
|
|
||||||
HTTPClient: httpClient,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
if res == nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return nil, readBodyAsError(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ping once every 30 seconds to ensure that the websocket is alive. If we
|
|
||||||
// don't get a response within 30s we kill the websocket and reconnect.
|
|
||||||
// See: https://github.com/coder/coder/pull/5824
|
|
||||||
go func() {
|
|
||||||
tick := 30 * time.Second
|
|
||||||
ticker := time.NewTicker(tick)
|
|
||||||
defer ticker.Stop()
|
|
||||||
defer func() {
|
|
||||||
c.Logger.Debug(ctx, "coordinate pinger exited")
|
|
||||||
}()
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
case start := <-ticker.C:
|
|
||||||
ctx, cancel := context.WithTimeout(ctx, tick)
|
|
||||||
|
|
||||||
err := conn.Ping(ctx)
|
|
||||||
if err != nil {
|
|
||||||
c.Logger.Error(ctx, "workspace agent coordinate ping", slog.Error(err))
|
|
||||||
|
|
||||||
err := conn.Close(websocket.StatusGoingAway, "Ping failed")
|
|
||||||
if err != nil {
|
|
||||||
c.Logger.Error(ctx, "close workspace agent coordinate websocket", slog.Error(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
cancel()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Logger.Debug(ctx, "got coordinate pong", slog.F("took", time.Since(start)))
|
|
||||||
cancel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return websocket.NetConn(ctx, conn, websocket.MessageBinary), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// @typescript-ignore DialWorkspaceAgentOptions
|
// @typescript-ignore DialWorkspaceAgentOptions
|
||||||
type DialWorkspaceAgentOptions struct {
|
type DialWorkspaceAgentOptions struct {
|
||||||
Logger slog.Logger
|
Logger slog.Logger
|
||||||
|
@ -413,7 +100,7 @@ type DialWorkspaceAgentOptions struct {
|
||||||
EnableTrafficStats bool
|
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 {
|
if options == nil {
|
||||||
options = &DialWorkspaceAgentOptions{}
|
options = &DialWorkspaceAgentOptions{}
|
||||||
}
|
}
|
||||||
|
@ -423,7 +110,7 @@ func (c *Client) DialWorkspaceAgent(ctx context.Context, agentID uuid.UUID, opti
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return nil, readBodyAsError(res)
|
return nil, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
var connInfo WorkspaceAgentConnectionInfo
|
var connInfo WorkspaceAgentConnectionInfo
|
||||||
err = json.NewDecoder(res.Body).Decode(&connInfo)
|
err = json.NewDecoder(res.Body).Decode(&connInfo)
|
||||||
|
@ -452,7 +139,7 @@ func (c *Client) DialWorkspaceAgent(ctx context.Context, agentID uuid.UUID, opti
|
||||||
return nil, xerrors.Errorf("create cookie jar: %w", err)
|
return nil, xerrors.Errorf("create cookie jar: %w", err)
|
||||||
}
|
}
|
||||||
jar.SetCookies(coordinateURL, []*http.Cookie{{
|
jar.SetCookies(coordinateURL, []*http.Cookie{{
|
||||||
Name: SessionTokenKey,
|
Name: SessionTokenCookie,
|
||||||
Value: c.SessionToken(),
|
Value: c.SessionToken(),
|
||||||
}})
|
}})
|
||||||
httpClient := &http.Client{
|
httpClient := &http.Client{
|
||||||
|
@ -475,7 +162,7 @@ func (c *Client) DialWorkspaceAgent(ctx context.Context, agentID uuid.UUID, opti
|
||||||
})
|
})
|
||||||
if isFirst {
|
if isFirst {
|
||||||
if res != nil && res.StatusCode == http.StatusConflict {
|
if res != nil && res.StatusCode == http.StatusConflict {
|
||||||
first <- readBodyAsError(res)
|
first <- ReadBodyAsError(res)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
isFirst = false
|
isFirst = false
|
||||||
|
@ -513,7 +200,7 @@ func (c *Client) DialWorkspaceAgent(ctx context.Context, agentID uuid.UUID, opti
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &AgentConn{
|
return &WorkspaceAgentConn{
|
||||||
Conn: conn,
|
Conn: conn,
|
||||||
CloseFunc: func() {
|
CloseFunc: func() {
|
||||||
cancelFunc()
|
cancelFunc()
|
||||||
|
@ -530,39 +217,12 @@ func (c *Client) WorkspaceAgent(ctx context.Context, id uuid.UUID) (WorkspaceAge
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return WorkspaceAgent{}, readBodyAsError(res)
|
return WorkspaceAgent{}, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
var workspaceAgent WorkspaceAgent
|
var workspaceAgent WorkspaceAgent
|
||||||
return workspaceAgent, json.NewDecoder(res.Body).Decode(&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.
|
// WorkspaceAgentReconnectingPTY spawns a PTY that reconnects using the token provided.
|
||||||
// It communicates using `agent.ReconnectingPTYRequest` marshaled as JSON.
|
// It communicates using `agent.ReconnectingPTYRequest` marshaled as JSON.
|
||||||
// Responses are PTY output that can be rendered.
|
// Responses are PTY output that can be rendered.
|
||||||
|
@ -583,7 +243,7 @@ func (c *Client) WorkspaceAgentReconnectingPTY(ctx context.Context, agentID, rec
|
||||||
return nil, xerrors.Errorf("create cookie jar: %w", err)
|
return nil, xerrors.Errorf("create cookie jar: %w", err)
|
||||||
}
|
}
|
||||||
jar.SetCookies(serverURL, []*http.Cookie{{
|
jar.SetCookies(serverURL, []*http.Cookie{{
|
||||||
Name: SessionTokenKey,
|
Name: SessionTokenCookie,
|
||||||
Value: c.SessionToken(),
|
Value: c.SessionToken(),
|
||||||
}})
|
}})
|
||||||
httpClient := &http.Client{
|
httpClient := &http.Client{
|
||||||
|
@ -596,114 +256,26 @@ func (c *Client) WorkspaceAgentReconnectingPTY(ctx context.Context, agentID, rec
|
||||||
if res == nil {
|
if res == nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return nil, readBodyAsError(res)
|
return nil, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
return websocket.NetConn(ctx, conn, websocket.MessageBinary), nil
|
return websocket.NetConn(ctx, conn, websocket.MessageBinary), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// WorkspaceAgentListeningPorts returns a list of ports that are currently being
|
// WorkspaceAgentListeningPorts returns a list of ports that are currently being
|
||||||
// listened on inside the workspace agent's network namespace.
|
// 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)
|
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaceagents/%s/listening-ports", agentID), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ListeningPortsResponse{}, err
|
return WorkspaceAgentListeningPortsResponse{}, err
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
if res.StatusCode != http.StatusOK {
|
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)
|
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
|
// GitProvider is a constant that represents the
|
||||||
// type of providers that are supported within Coder.
|
// type of providers that are supported within Coder.
|
||||||
// @typescript-ignore GitProvider
|
// @typescript-ignore GitProvider
|
||||||
|
@ -715,49 +287,3 @@ const (
|
||||||
GitProviderGitLab = "gitlab"
|
GitProviderGitLab = "gitlab"
|
||||||
GitProviderBitBucket = "bitbucket"
|
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
|
|
||||||
}
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ import (
|
||||||
|
|
||||||
"cdr.dev/slog/sloggers/slogtest"
|
"cdr.dev/slog/sloggers/slogtest"
|
||||||
"github.com/coder/coder/coderd/httpapi"
|
"github.com/coder/coder/coderd/httpapi"
|
||||||
"github.com/coder/coder/codersdk"
|
"github.com/coder/coder/codersdk/agentsdk"
|
||||||
"github.com/coder/coder/testutil"
|
"github.com/coder/coder/testutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ func TestWorkspaceAgentMetadata(t *testing.T) {
|
||||||
// This test ensures that the DERP map returned properly
|
// This test ensures that the DERP map returned properly
|
||||||
// mutates built-in DERPs with the client access URL.
|
// mutates built-in DERPs with the client access URL.
|
||||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
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{
|
DERPMap: &tailcfg.DERPMap{
|
||||||
Regions: map[int]*tailcfg.DERPRegion{
|
Regions: map[int]*tailcfg.DERPRegion{
|
||||||
1: {
|
1: {
|
||||||
|
@ -41,8 +41,8 @@ func TestWorkspaceAgentMetadata(t *testing.T) {
|
||||||
}))
|
}))
|
||||||
parsed, err := url.Parse(srv.URL)
|
parsed, err := url.Parse(srv.URL)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
client := codersdk.New(parsed)
|
client := agentsdk.New(parsed)
|
||||||
metadata, err := client.WorkspaceAgentMetadata(context.Background())
|
metadata, err := client.Metadata(context.Background())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
region := metadata.DERPMap.Regions[1]
|
region := metadata.DERPMap.Regions[1]
|
||||||
require.True(t, region.EmbeddedRelay)
|
require.True(t, region.EmbeddedRelay)
|
||||||
|
@ -58,17 +58,17 @@ func TestAgentReportStats(t *testing.T) {
|
||||||
var numReports atomic.Int64
|
var numReports atomic.Int64
|
||||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
numReports.Add(1)
|
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,
|
ReportInterval: 5 * time.Millisecond,
|
||||||
})
|
})
|
||||||
}))
|
}))
|
||||||
parsed, err := url.Parse(srv.URL)
|
parsed, err := url.Parse(srv.URL)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
client := codersdk.New(parsed)
|
client := agentsdk.New(parsed)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
closeStream, err := client.AgentReportStats(ctx, slogtest.Make(t, nil), func() *codersdk.AgentStats {
|
closeStream, err := client.ReportStats(ctx, slogtest.Make(t, nil), func() *agentsdk.Stats {
|
||||||
return &codersdk.AgentStats{}
|
return &agentsdk.Stats{}
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer closeStream.Close()
|
defer closeStream.Close()
|
||||||
|
|
|
@ -56,9 +56,3 @@ type Healthcheck struct {
|
||||||
// Threshold specifies the number of consecutive failed health checks before returning "unhealthy".
|
// Threshold specifies the number of consecutive failed health checks before returning "unhealthy".
|
||||||
Threshold int32 `json:"threshold"`
|
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
|
|
||||||
}
|
|
||||||
|
|
|
@ -110,7 +110,7 @@ func (c *Client) WorkspaceBuild(ctx context.Context, id uuid.UUID) (WorkspaceBui
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return WorkspaceBuild{}, readBodyAsError(res)
|
return WorkspaceBuild{}, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
var workspaceBuild WorkspaceBuild
|
var workspaceBuild WorkspaceBuild
|
||||||
return workspaceBuild, json.NewDecoder(res.Body).Decode(&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()
|
defer res.Body.Close()
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return readBodyAsError(res)
|
return ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -147,7 +147,7 @@ func (c *Client) WorkspaceBuildState(ctx context.Context, build uuid.UUID) ([]by
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return nil, readBodyAsError(res)
|
return nil, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
return io.ReadAll(res.Body)
|
return io.ReadAll(res.Body)
|
||||||
}
|
}
|
||||||
|
@ -159,7 +159,7 @@ func (c *Client) WorkspaceBuildByUsernameAndWorkspaceNameAndBuildNumber(ctx cont
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return WorkspaceBuild{}, readBodyAsError(res)
|
return WorkspaceBuild{}, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
var workspaceBuild WorkspaceBuild
|
var workspaceBuild WorkspaceBuild
|
||||||
return workspaceBuild, json.NewDecoder(res.Body).Decode(&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()
|
defer res.Body.Close()
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return nil, readBodyAsError(res)
|
return nil, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
var params []WorkspaceBuildParameter
|
var params []WorkspaceBuildParameter
|
||||||
return params, json.NewDecoder(res.Body).Decode(¶ms)
|
return params, json.NewDecoder(res.Body).Decode(¶ms)
|
||||||
|
|
|
@ -96,7 +96,7 @@ func (c *Client) getWorkspace(ctx context.Context, id uuid.UUID, opts ...Request
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return Workspace{}, readBodyAsError(res)
|
return Workspace{}, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
var workspace Workspace
|
var workspace Workspace
|
||||||
return workspace, json.NewDecoder(res.Body).Decode(&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()
|
defer res.Body.Close()
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return nil, readBodyAsError(res)
|
return nil, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
var workspaceBuild []WorkspaceBuild
|
var workspaceBuild []WorkspaceBuild
|
||||||
return workspaceBuild, json.NewDecoder(res.Body).Decode(&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()
|
defer res.Body.Close()
|
||||||
if res.StatusCode != http.StatusCreated {
|
if res.StatusCode != http.StatusCreated {
|
||||||
return WorkspaceBuild{}, readBodyAsError(res)
|
return WorkspaceBuild{}, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
var workspaceBuild WorkspaceBuild
|
var workspaceBuild WorkspaceBuild
|
||||||
return workspaceBuild, json.NewDecoder(res.Body).Decode(&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
|
return nil, err
|
||||||
}
|
}
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return nil, readBodyAsError(res)
|
return nil, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
nextEvent := ServerSentEventReader(ctx, res.Body)
|
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()
|
defer res.Body.Close()
|
||||||
if res.StatusCode != http.StatusNoContent {
|
if res.StatusCode != http.StatusNoContent {
|
||||||
return readBodyAsError(res)
|
return ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -218,7 +218,7 @@ func (c *Client) UpdateWorkspaceAutostart(ctx context.Context, id uuid.UUID, req
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
if res.StatusCode != http.StatusNoContent {
|
if res.StatusCode != http.StatusNoContent {
|
||||||
return readBodyAsError(res)
|
return ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -238,7 +238,7 @@ func (c *Client) UpdateWorkspaceTTL(ctx context.Context, id uuid.UUID, req Updat
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
if res.StatusCode != http.StatusNoContent {
|
if res.StatusCode != http.StatusNoContent {
|
||||||
return readBodyAsError(res)
|
return ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -258,7 +258,7 @@ func (c *Client) PutExtendWorkspace(ctx context.Context, id uuid.UUID, req PutEx
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
if res.StatusCode != http.StatusOK && res.StatusCode != http.StatusNotModified {
|
if res.StatusCode != http.StatusOK && res.StatusCode != http.StatusNotModified {
|
||||||
return readBodyAsError(res)
|
return ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -323,7 +323,7 @@ func (c *Client) Workspaces(ctx context.Context, filter WorkspaceFilter) (Worksp
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
|
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return WorkspacesResponse{}, readBodyAsError(res)
|
return WorkspacesResponse{}, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
var wres WorkspacesResponse
|
var wres WorkspacesResponse
|
||||||
|
@ -343,37 +343,29 @@ func (c *Client) WorkspaceByOwnerAndName(ctx context.Context, owner string, name
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
|
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return Workspace{}, readBodyAsError(res)
|
return Workspace{}, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
var workspace Workspace
|
var workspace Workspace
|
||||||
return workspace, json.NewDecoder(res.Body).Decode(&workspace)
|
return workspace, json.NewDecoder(res.Body).Decode(&workspace)
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetAppHostResponse struct {
|
type WorkspaceQuota struct {
|
||||||
// Host is the externally accessible URL for the Coder instance.
|
CreditsConsumed int `json:"credits_consumed"`
|
||||||
Host string `json:"host"`
|
Budget int `json:"budget"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAppHost returns the site-wide application wildcard hostname without the
|
func (c *Client) WorkspaceQuota(ctx context.Context, userID string) (WorkspaceQuota, error) {
|
||||||
// leading "*.", e.g. "apps.coder.com". Apps are accessible at:
|
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspace-quota/%s", userID), nil)
|
||||||
// "<app-name>--<agent-name>--<workspace-name>--<username>.<app-host>", e.g.
|
|
||||||
// "my-app--agent--workspace--username.apps.coder.com".
|
|
||||||
//
|
|
||||||
// If the app host is not set, the response will contain an empty string.
|
|
||||||
func (c *Client) GetAppHost(ctx context.Context) (GetAppHostResponse, error) {
|
|
||||||
res, err := c.Request(ctx, http.MethodGet, "/api/v2/applications/host", nil)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return GetAppHostResponse{}, err
|
return WorkspaceQuota{}, err
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
|
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return GetAppHostResponse{}, readBodyAsError(res)
|
return WorkspaceQuota{}, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
|
var quota WorkspaceQuota
|
||||||
var host GetAppHostResponse
|
return quota, json.NewDecoder(res.Body).Decode("a)
|
||||||
return host, json.NewDecoder(res.Body).Decode(&host)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// WorkspaceNotifyChannel is the PostgreSQL NOTIFY
|
// WorkspaceNotifyChannel is the PostgreSQL NOTIFY
|
||||||
|
|
|
@ -27,7 +27,7 @@ curl -X POST http://coder-server:8080/api/v2/workspaceagents/aws-instance-identi
|
||||||
|
|
||||||
| Name | In | Type | Required | Description |
|
| 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
|
### Example responses
|
||||||
|
|
||||||
|
@ -41,9 +41,9 @@ curl -X POST http://coder-server:8080/api/v2/workspaceagents/aws-instance-identi
|
||||||
|
|
||||||
### Responses
|
### Responses
|
||||||
|
|
||||||
| Status | Meaning | Description | Schema |
|
| Status | Meaning | Description | Schema |
|
||||||
| ------ | ------------------------------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------- |
|
| ------ | ------------------------------------------------------- | ----------- | ------------------------------------------------------------------------ |
|
||||||
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.WorkspaceAgentAuthenticateResponse](schemas.md#codersdkworkspaceagentauthenticateresponse) |
|
| 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).
|
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 |
|
| 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
|
### Example responses
|
||||||
|
|
||||||
|
@ -88,9 +88,9 @@ curl -X POST http://coder-server:8080/api/v2/workspaceagents/azure-instance-iden
|
||||||
|
|
||||||
### Responses
|
### Responses
|
||||||
|
|
||||||
| Status | Meaning | Description | Schema |
|
| Status | Meaning | Description | Schema |
|
||||||
| ------ | ------------------------------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------- |
|
| ------ | ------------------------------------------------------- | ----------- | ------------------------------------------------------------------------ |
|
||||||
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.WorkspaceAgentAuthenticateResponse](schemas.md#codersdkworkspaceagentauthenticateresponse) |
|
| 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).
|
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 |
|
| 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
|
### Example responses
|
||||||
|
|
||||||
|
@ -134,9 +134,9 @@ curl -X POST http://coder-server:8080/api/v2/workspaceagents/google-instance-ide
|
||||||
|
|
||||||
### Responses
|
### Responses
|
||||||
|
|
||||||
| Status | Meaning | Description | Schema |
|
| Status | Meaning | Description | Schema |
|
||||||
| ------ | ------------------------------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------- |
|
| ------ | ------------------------------------------------------- | ----------- | ------------------------------------------------------------------------ |
|
||||||
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.WorkspaceAgentAuthenticateResponse](schemas.md#codersdkworkspaceagentauthenticateresponse) |
|
| 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).
|
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
|
### Parameters
|
||||||
|
|
||||||
| Name | In | Type | Required | Description |
|
| Name | In | Type | Required | Description |
|
||||||
| ------ | ---- | -------------------------------------------------------------------------------------------- | -------- | -------------------------- |
|
| ------ | ---- | -------------------------------------------------------------------------- | -------- | -------------------------- |
|
||||||
| `body` | body | [codersdk.PostWorkspaceAppHealthsRequest](schemas.md#codersdkpostworkspaceapphealthsrequest) | true | Application health request |
|
| `body` | body | [agentsdk.PostAppHealthsRequest](schemas.md#agentsdkpostapphealthsrequest) | true | Application health request |
|
||||||
|
|
||||||
### Responses
|
### Responses
|
||||||
|
|
||||||
|
@ -235,9 +235,9 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/me/gitauth?url=http%
|
||||||
|
|
||||||
### Responses
|
### Responses
|
||||||
|
|
||||||
| Status | Meaning | Description | Schema |
|
| Status | Meaning | Description | Schema |
|
||||||
| ------ | ------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------ |
|
| ------ | ------------------------------------------------------- | ----------- | -------------------------------------------------------------- |
|
||||||
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.WorkspaceAgentGitAuthResponse](schemas.md#codersdkworkspaceagentgitauthresponse) |
|
| 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).
|
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
|
### Responses
|
||||||
|
|
||||||
| Status | Meaning | Description | Schema |
|
| Status | Meaning | Description | Schema |
|
||||||
| ------ | ------------------------------------------------------- | ----------- | ------------------------------------------------------------ |
|
| ------ | ------------------------------------------------------- | ----------- | -------------------------------------------------- |
|
||||||
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.AgentGitSSHKey](schemas.md#codersdkagentgitsshkey) |
|
| 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).
|
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
|
### Responses
|
||||||
|
|
||||||
| Status | Meaning | Description | Schema |
|
| Status | Meaning | Description | Schema |
|
||||||
| ------ | ------------------------------------------------------- | ----------- | ---------------------------------------------------------------------------- |
|
| ------ | ------------------------------------------------------- | ----------- | ------------------------------------------------ |
|
||||||
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.WorkspaceAgentMetadata](schemas.md#codersdkworkspaceagentmetadata) |
|
| 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).
|
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
|
### Parameters
|
||||||
|
|
||||||
| Name | In | Type | Required | Description |
|
| Name | In | Type | Required | Description |
|
||||||
| ------ | ---- | ---------------------------------------------------- | -------- | ------------- |
|
| ------ | ---- | ------------------------------------------ | -------- | ------------- |
|
||||||
| `body` | body | [codersdk.AgentStats](schemas.md#codersdkagentstats) | true | Stats request |
|
| `body` | body | [agentsdk.Stats](schemas.md#agentsdkstats) | true | Stats request |
|
||||||
|
|
||||||
### Example responses
|
### Example responses
|
||||||
|
|
||||||
|
@ -431,9 +431,9 @@ curl -X POST http://coder-server:8080/api/v2/workspaceagents/me/report-stats \
|
||||||
|
|
||||||
### Responses
|
### Responses
|
||||||
|
|
||||||
| Status | Meaning | Description | Schema |
|
| Status | Meaning | Description | Schema |
|
||||||
| ------ | ------------------------------------------------------- | ----------- | -------------------------------------------------------------------- |
|
| ------ | ------------------------------------------------------- | ----------- | ---------------------------------------------------------- |
|
||||||
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.AgentStatsResponse](schemas.md#codersdkagentstatsresponse) |
|
| 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).
|
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||||
|
|
||||||
|
@ -666,7 +666,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/{workspaceagent}/lis
|
||||||
{
|
{
|
||||||
"ports": [
|
"ports": [
|
||||||
{
|
{
|
||||||
"network": "tcp",
|
"network": "string",
|
||||||
"port": 0,
|
"port": 0,
|
||||||
"process_name": "string"
|
"process_name": "string"
|
||||||
}
|
}
|
||||||
|
@ -676,9 +676,9 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/{workspaceagent}/lis
|
||||||
|
|
||||||
### Responses
|
### Responses
|
||||||
|
|
||||||
| Status | Meaning | Description | Schema |
|
| Status | Meaning | Description | Schema |
|
||||||
| ------ | ------------------------------------------------------- | ----------- | ---------------------------------------------------------------------------- |
|
| ------ | ------------------------------------------------------- | ----------- | -------------------------------------------------------------------------------------------------------- |
|
||||||
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.ListeningPortsResponse](schemas.md#codersdklisteningportsresponse) |
|
| 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).
|
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue