From 34d25a1672de5ffc2ed3237b9f2715cec8ca57c0 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Tue, 12 Mar 2024 17:38:56 +0000 Subject: [PATCH 01/10] refactor(support): move agent-related info to top level --- cli/support.go | 14 +++---- cli/support_test.go | 4 +- support/support.go | 89 +++++++++++++++++++++++++---------------- support/support_test.go | 4 +- 4 files changed, 65 insertions(+), 46 deletions(-) diff --git a/cli/support.go b/cli/support.go index ac0e2041cb6ae..a996a029ec4a0 100644 --- a/cli/support.go +++ b/cli/support.go @@ -145,7 +145,7 @@ func writeBundle(src *support.Bundle, dest *zip.Writer) error { "network/netcheck_local.json": src.Network.NetcheckLocal, "network/netcheck_remote.json": src.Network.NetcheckRemote, "workspace/workspace.json": src.Workspace.Workspace, - "workspace/agent.json": src.Workspace.Agent, + "agent/agent.json": src.Agent.Agent, "workspace/template.json": src.Workspace.Template, "workspace/template_version.json": src.Workspace.TemplateVersion, "workspace/parameters.json": src.Workspace.Parameters, @@ -167,12 +167,12 @@ func writeBundle(src *support.Bundle, dest *zip.Writer) error { } for k, v := range map[string]string{ - "network/coordinator_debug.html": src.Network.CoordinatorDebug, - "network/tailnet_debug.html": src.Network.TailnetDebug, - "workspace/build_logs.txt": humanizeBuildLogs(src.Workspace.BuildLogs), - "workspace/agent_startup_logs.txt": humanizeAgentLogs(src.Workspace.AgentStartupLogs), - "workspace/template_file.zip": string(templateVersionBytes), - "logs.txt": strings.Join(src.Logs, "\n"), + "network/coordinator_debug.html": src.Network.CoordinatorDebug, + "network/tailnet_debug.html": src.Network.TailnetDebug, + "workspace/build_logs.txt": humanizeBuildLogs(src.Workspace.BuildLogs), + "agent/startup_logs.txt": humanizeAgentLogs(src.Agent.StartupLogs), + "workspace/template_file.zip": string(templateVersionBytes), + "logs.txt": strings.Join(src.Logs, "\n"), } { f, err := dest.Create(k) if err != nil { diff --git a/cli/support_test.go b/cli/support_test.go index b36d17ad7c695..0cfe60638cd0c 100644 --- a/cli/support_test.go +++ b/cli/support_test.go @@ -152,11 +152,11 @@ func assertBundleContents(t *testing.T, path string) { case "workspace/build_logs.txt": bs := readBytesFromZip(t, f) require.Contains(t, string(bs), "provision done") - case "workspace/agent.json": + case "agent/agent.json": var v codersdk.WorkspaceAgent decodeJSONFromZip(t, f, &v) require.NotEmpty(t, v, "agent should not be empty") - case "workspace/agent_startup_logs.txt": + case "agent/startup_logs.txt": bs := readBytesFromZip(t, f) require.Contains(t, string(bs), "started up") case "workspace/template.json": diff --git a/support/support.go b/support/support.go index 30aa77eef2ff7..7ea1c742e4d79 100644 --- a/support/support.go +++ b/support/support.go @@ -25,6 +25,7 @@ type Bundle struct { Deployment Deployment `json:"deployment"` Network Network `json:"network"` Workspace Workspace `json:"workspace"` + Agent Agent `json:"agent"` Logs []string `json:"logs"` } @@ -49,8 +50,11 @@ type Workspace struct { TemplateVersion codersdk.TemplateVersion `json:"template_version"` TemplateFileBase64 string `json:"template_file_base64"` BuildLogs []codersdk.ProvisionerJobLog `json:"build_logs"` - Agent codersdk.WorkspaceAgent `json:"agent"` - AgentStartupLogs []codersdk.WorkspaceAgentLog `json:"startup_logs"` +} + +type Agent struct { + Agent codersdk.WorkspaceAgent `json:"agent"` + StartupLogs []codersdk.WorkspaceAgentLog `json:"startup_logs"` } // Deps is a set of dependencies for discovering information @@ -170,7 +174,7 @@ func NetworkInfo(ctx context.Context, client *codersdk.Client, log slog.Logger, return n } -func WorkspaceInfo(ctx context.Context, client *codersdk.Client, log slog.Logger, workspaceID, agentID uuid.UUID) Workspace { +func WorkspaceInfo(ctx context.Context, client *codersdk.Client, log slog.Logger, workspaceID uuid.UUID) Workspace { var ( w Workspace eg errgroup.Group @@ -181,10 +185,6 @@ func WorkspaceInfo(ctx context.Context, client *codersdk.Client, log slog.Logger return w } - if agentID == uuid.Nil { - log.Error(ctx, "no agent id specified") - } - // dependency, cannot fetch concurrently ws, err := client.Workspace(ctx, workspaceID) if err != nil { @@ -198,15 +198,6 @@ func WorkspaceInfo(ctx context.Context, client *codersdk.Client, log slog.Logger } w.Workspace = ws - eg.Go(func() error { - agt, err := client.WorkspaceAgent(ctx, agentID) - if err != nil { - return xerrors.Errorf("fetch workspace agent: %w", err) - } - w.Agent = agt - return nil - }) - eg.Go(func() error { buildLogCh, closer, err := client.WorkspaceBuildLogsAfter(ctx, ws.LatestBuild.ID, 0) if err != nil { @@ -221,24 +212,6 @@ func WorkspaceInfo(ctx context.Context, client *codersdk.Client, log slog.Logger return nil }) - eg.Go(func() error { - if len(w.Workspace.LatestBuild.Resources) == 0 { - log.Warn(ctx, "workspace build has no resources") - return nil - } - agentLogCh, closer, err := client.WorkspaceAgentLogsAfter(ctx, agentID, 0, false) - if err != nil { - return xerrors.Errorf("fetch agent startup logs: %w", err) - } - defer closer.Close() - var logs []codersdk.WorkspaceAgentLog - for logChunk := range agentLogCh { - logs = append(w.AgentStartupLogs, logChunk...) - } - w.AgentStartupLogs = logs - return nil - }) - eg.Go(func() error { if w.Workspace.TemplateActiveVersionID == uuid.Nil { return xerrors.Errorf("workspace has nil template active version id") @@ -296,6 +269,47 @@ func WorkspaceInfo(ctx context.Context, client *codersdk.Client, log slog.Logger return w } +func AgentInfo(ctx context.Context, client *codersdk.Client, log slog.Logger, agentID uuid.UUID) Agent { + var ( + a Agent + eg errgroup.Group + ) + + if agentID == uuid.Nil { + log.Error(ctx, "no agent id specified") + return a + } + + eg.Go(func() error { + agt, err := client.WorkspaceAgent(ctx, agentID) + if err != nil { + return xerrors.Errorf("fetch workspace agent: %w", err) + } + a.Agent = agt + return nil + }) + + eg.Go(func() error { + agentLogCh, closer, err := client.WorkspaceAgentLogsAfter(ctx, agentID, 0, false) + if err != nil { + return xerrors.Errorf("fetch agent startup logs: %w", err) + } + defer closer.Close() + var logs []codersdk.WorkspaceAgentLog + for logChunk := range agentLogCh { + logs = append(logs, logChunk...) + } + a.StartupLogs = logs + return nil + }) + + if err := eg.Wait(); err != nil { + log.Error(ctx, "fetch agent information", slog.Error(err)) + } + + return a +} + // Run generates a support bundle with the given dependencies. func Run(ctx context.Context, d *Deps) (*Bundle, error) { var b Bundle @@ -337,7 +351,7 @@ func Run(ctx context.Context, d *Deps) (*Bundle, error) { return nil }) eg.Go(func() error { - wi := WorkspaceInfo(ctx, d.Client, d.Log, d.WorkspaceID, d.AgentID) + wi := WorkspaceInfo(ctx, d.Client, d.Log, d.WorkspaceID) b.Workspace = wi return nil }) @@ -346,6 +360,11 @@ func Run(ctx context.Context, d *Deps) (*Bundle, error) { b.Network = ni return nil }) + eg.Go(func() error { + ai := AgentInfo(ctx, d.Client, d.Log, d.AgentID) + b.Agent = ai + return nil + }) _ = eg.Wait() diff --git a/support/support_test.go b/support/support_test.go index ce21bfe999c06..c7f8b0e941b42 100644 --- a/support/support_test.go +++ b/support/support_test.go @@ -59,12 +59,12 @@ func TestRun(t *testing.T) { require.NotNil(t, bun.Workspace.Workspace) assertSanitizedWorkspace(t, bun.Workspace.Workspace) require.NotEmpty(t, bun.Workspace.BuildLogs) - require.NotNil(t, bun.Workspace.Agent) - require.NotEmpty(t, bun.Workspace.AgentStartupLogs) require.NotEmpty(t, bun.Workspace.Template) require.NotEmpty(t, bun.Workspace.TemplateVersion) require.NotEmpty(t, bun.Workspace.TemplateFileBase64) require.NotNil(t, bun.Workspace.Parameters) + require.NotNil(t, bun.Agent.Agent) + require.NotEmpty(t, bun.Agent.StartupLogs) require.NotEmpty(t, bun.Logs) }) From 1092eb25e78fc8ed0675aa087deed20fd6794197 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Tue, 12 Mar 2024 21:31:57 +0000 Subject: [PATCH 02/10] feat(support): fetch manifest, logs etc. from agent --- cli/support.go | 5 ++ cli/support_test.go | 32 ++++++++++- support/support.go | 119 +++++++++++++++++++++++++++++++++++++++- support/support_test.go | 9 +++ 4 files changed, 160 insertions(+), 5 deletions(-) diff --git a/cli/support.go b/cli/support.go index a996a029ec4a0..f6978a44d6299 100644 --- a/cli/support.go +++ b/cli/support.go @@ -146,6 +146,10 @@ func writeBundle(src *support.Bundle, dest *zip.Writer) error { "network/netcheck_remote.json": src.Network.NetcheckRemote, "workspace/workspace.json": src.Workspace.Workspace, "agent/agent.json": src.Agent.Agent, + "agent/listening_ports.json": src.Agent.ListeningPorts, + "agent/manifest.json": src.Agent.Manifest, + "agent/peer_diagnostics.json": src.Agent.PeerDiagnostics, + "agent/ping_result.json": src.Agent.PingResult, "workspace/template.json": src.Workspace.Template, "workspace/template_version.json": src.Workspace.TemplateVersion, "workspace/parameters.json": src.Workspace.Parameters, @@ -170,6 +174,7 @@ func writeBundle(src *support.Bundle, dest *zip.Writer) error { "network/coordinator_debug.html": src.Network.CoordinatorDebug, "network/tailnet_debug.html": src.Network.TailnetDebug, "workspace/build_logs.txt": humanizeBuildLogs(src.Workspace.BuildLogs), + "agent/logs.txt": string(src.Agent.Logs), "agent/startup_logs.txt": humanizeAgentLogs(src.Agent.StartupLogs), "workspace/template_file.zip": string(templateVersionBytes), "logs.txt": strings.Join(src.Logs, "\n"), diff --git a/cli/support_test.go b/cli/support_test.go index 0cfe60638cd0c..3c169c6985bb2 100644 --- a/cli/support_test.go +++ b/cli/support_test.go @@ -9,14 +9,19 @@ import ( "testing" "time" + "tailscale.com/ipn/ipnstate" + "github.com/stretchr/testify/require" + "github.com/coder/coder/v2/agent/agenttest" "github.com/coder/coder/v2/cli/clitest" "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbfake" "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/codersdk/agentsdk" + "github.com/coder/coder/v2/tailnet" "github.com/coder/coder/v2/testutil" ) @@ -37,7 +42,9 @@ func TestSupportBundle(t *testing.T) { }).WithAgent().Do() ws, err := client.Workspace(ctx, r.Workspace.ID) require.NoError(t, err) - agt := ws.LatestBuild.Resources[0].Agents[0] + agt := agenttest.New(t, client.URL, r.AgentToken) + defer agt.Close() + coderdtest.NewWorkspaceAgentWaiter(t, client, r.Workspace.ID).Wait() // Insert a provisioner job log _, err = db.InsertProvisionerJobLogs(ctx, database.InsertProvisionerJobLogsParams{ @@ -51,7 +58,7 @@ func TestSupportBundle(t *testing.T) { require.NoError(t, err) // Insert an agent log _, err = db.InsertWorkspaceAgentLogs(ctx, database.InsertWorkspaceAgentLogsParams{ - AgentID: agt.ID, + AgentID: ws.LatestBuild.Resources[0].Agents[0].ID, CreatedAt: dbtime.Now(), Output: []string{"started up"}, Level: []database.LogLevel{database.LogLevelInfo}, @@ -156,6 +163,25 @@ func assertBundleContents(t *testing.T, path string) { var v codersdk.WorkspaceAgent decodeJSONFromZip(t, f, &v) require.NotEmpty(t, v, "agent should not be empty") + case "agent/listening_ports.json": + var v codersdk.WorkspaceAgentListeningPortsResponse + decodeJSONFromZip(t, f, &v) + require.NotEmpty(t, v, "agent listening ports should not be empty") + case "agent/logs.txt": + bs := readBytesFromZip(t, f) + require.NotEmpty(t, bs, "logs should not be empty") + case "agent/manifest.json": + var v agentsdk.Manifest + decodeJSONFromZip(t, f, &v) + require.NotEmpty(t, v, "agent manifest should not be empty") + case "agent/peer_diagnostics.json": + var v *tailnet.PeerDiagnostics + decodeJSONFromZip(t, f, &v) + require.NotEmpty(t, v, "peer diagnostics should not be empty") + case "agent/ping_result.json": + var v *ipnstate.PingResult + decodeJSONFromZip(t, f, &v) + require.NotEmpty(t, v, "ping result should not be empty") case "agent/startup_logs.txt": bs := readBytesFromZip(t, f) require.Contains(t, string(bs), "started up") @@ -178,7 +204,7 @@ func assertBundleContents(t *testing.T, path string) { bs := readBytesFromZip(t, f) require.NotEmpty(t, bs, "logs should not be empty") default: - require.Failf(t, "unexpected file in bundle: %q", f.Name) + require.Failf(t, "unexpected file in bundle", f.Name) } } } diff --git a/support/support.go b/support/support.go index 7ea1c742e4d79..24cd7d7b301bb 100644 --- a/support/support.go +++ b/support/support.go @@ -1,14 +1,18 @@ package support import ( + "bytes" "context" "encoding/base64" + "encoding/json" + "errors" "io" "net/http" "strings" "golang.org/x/sync/errgroup" "golang.org/x/xerrors" + "tailscale.com/ipn/ipnstate" "github.com/google/uuid" @@ -16,6 +20,8 @@ import ( "cdr.dev/slog/sloggers/sloghuman" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/codersdk/agentsdk" + "github.com/coder/coder/v2/tailnet" ) // Bundle is a set of information discovered about a deployment. @@ -53,8 +59,13 @@ type Workspace struct { } type Agent struct { - Agent codersdk.WorkspaceAgent `json:"agent"` - StartupLogs []codersdk.WorkspaceAgentLog `json:"startup_logs"` + Agent codersdk.WorkspaceAgent `json:"agent"` + ListeningPorts *codersdk.WorkspaceAgentListeningPortsResponse `json:"listening_ports"` + Logs []byte `json:"logs"` + Manifest *agentsdk.Manifest `json:"manifest"` + PeerDiagnostics *tailnet.PeerDiagnostics `json:"peer_diagnostics"` + PingResult *ipnstate.PingResult `json:"ping_result"` + StartupLogs []codersdk.WorkspaceAgentLog `json:"startup_logs"` } // Deps is a set of dependencies for discovering information @@ -303,6 +314,72 @@ func AgentInfo(ctx context.Context, client *codersdk.Client, log slog.Logger, ag return nil }) + conn, err := client.DialWorkspaceAgent(ctx, agentID, &codersdk.DialWorkspaceAgentOptions{ + Logger: log.Named("dial-agent"), + BlockEndpoints: false, + }) + if err != nil { + log.Error(ctx, "dial agent", slog.Error(err)) + } else { + defer conn.Close() + if !conn.AwaitReachable(ctx) { + log.Error(ctx, "timed out waiting for agent") + } else { + eg.Go(func() error { + _, _, pingRes, err := conn.Ping(ctx) + if err != nil { + return xerrors.Errorf("ping agent: %w", err) + } + a.PingResult = pingRes + return nil + }) + + eg.Go(func() error { + pds := conn.GetPeerDiagnostics() + a.PeerDiagnostics = &pds + return nil + }) + + eg.Go(func() error { + tokenBytes, err := runCmd(ctx, client, agentID, `echo $CODER_AGENT_TOKEN`) + if err != nil { + return xerrors.Errorf("failed to fetch agent token: %w", err) + } + token := string(bytes.TrimSpace(tokenBytes)) + agentClient := agentsdk.New(client.URL) + agentClient.SetSessionToken(token) + manifestRes, err := agentClient.SDK.Request(ctx, http.MethodGet, "/api/v2/workspaceagents/me/manifest", nil) + if err != nil { + return xerrors.Errorf("fetch manifest: %w", err) + } + defer manifestRes.Body.Close() + if err := json.NewDecoder(manifestRes.Body).Decode(&a.Manifest); err != nil { + return xerrors.Errorf("decode agent manifest: %w", err) + } + + return nil + }) + + eg.Go(func() error { + logBytes, err := runCmd(ctx, client, agentID, `tail -n 1000 /tmp/coder-agent.log`) + if err != nil { + return xerrors.Errorf("tail /tmp/coder-agent.log: %w", err) + } + a.Logs = logBytes + return nil + }) + + eg.Go(func() error { + lps, err := conn.ListeningPorts(ctx) + if err != nil { + return xerrors.Errorf("get listening ports: %w", err) + } + a.ListeningPorts = &lps + return nil + }) + } + } + if err := eg.Wait(); err != nil { log.Error(ctx, "fetch agent information", slog.Error(err)) } @@ -380,3 +457,41 @@ func sanitizeEnv(kvs map[string]string) { } } } + +// TODO: use rpty instead? which is less liable to fail? +func runCmd(ctx context.Context, client *codersdk.Client, agentID uuid.UUID, cmd string) ([]byte, error) { + var err error + var closers []func() error + defer func() { + if err != nil { + for _, c := range closers { + if err2 := c(); err2 != nil { + err = errors.Join(err, err2) + } + } + } + }() + agentConn, err := client.DialWorkspaceAgent(ctx, agentID, &codersdk.DialWorkspaceAgentOptions{}) + if err != nil { + return nil, xerrors.Errorf("dial workspace agent: %w", err) + } + closers = append(closers, agentConn.Close) + + sshClient, err := agentConn.SSHClient(ctx) + if err != nil { + return nil, xerrors.Errorf("get ssh client: %w", err) + } + closers = append(closers, sshClient.Close) + + sshSession, err := sshClient.NewSession() + if err != nil { + return nil, xerrors.Errorf("new ssh session: %w", err) + } + closers = append(closers, sshSession.Close) + + cmdBytes, err := sshSession.CombinedOutput(cmd) + if err != nil { + return nil, xerrors.Errorf("shell: %w", err) + } + return cmdBytes, err +} diff --git a/support/support_test.go b/support/support_test.go index c7f8b0e941b42..ed0e2b445613a 100644 --- a/support/support_test.go +++ b/support/support_test.go @@ -14,6 +14,7 @@ import ( "cdr.dev/slog" "cdr.dev/slog/sloggers/sloghuman" "cdr.dev/slog/sloggers/slogtest" + "github.com/coder/coder/v2/agent/agenttest" "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbfake" @@ -64,6 +65,11 @@ func TestRun(t *testing.T) { require.NotEmpty(t, bun.Workspace.TemplateFileBase64) require.NotNil(t, bun.Workspace.Parameters) require.NotNil(t, bun.Agent.Agent) + require.NotNil(t, bun.Agent.ListeningPorts) + require.NotNil(t, bun.Agent.Logs) + require.NotNil(t, bun.Agent.Manifest) + require.NotNil(t, bun.Agent.PeerDiagnostics) + require.NotNil(t, bun.Agent.PingResult) require.NotEmpty(t, bun.Agent.StartupLogs) require.NotEmpty(t, bun.Logs) }) @@ -200,5 +206,8 @@ func setupWorkspaceAndAgent(ctx context.Context, t *testing.T, client *codersdk. }) require.NoError(t, err) + _ = agenttest.New(t, client.URL, wbr.AgentToken) + coderdtest.NewWorkspaceAgentWaiter(t, client, wbr.Workspace.ID).Wait() + return ws, agt } From bef4023699c904be9af51a16a14b62c0636c7d5d Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 13 Mar 2024 13:11:41 +0000 Subject: [PATCH 03/10] chore(support): add goleak detection --- support/support_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/support/support_test.go b/support/support_test.go index ed0e2b445613a..af3fd81dcf5df 100644 --- a/support/support_test.go +++ b/support/support_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.uber.org/goleak" "cdr.dev/slog" "cdr.dev/slog/sloggers/sloghuman" @@ -25,6 +26,10 @@ import ( "github.com/coder/coder/v2/testutil" ) +func TestMain(m *testing.M) { + goleak.VerifyTestMain(m) +} + func TestRun(t *testing.T) { t.Parallel() From f4331057e85a7304321b57e43d21c04adc2b1e77 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 13 Mar 2024 13:43:12 +0000 Subject: [PATCH 04/10] increase test timeout --- support/support_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/support/support_test.go b/support/support_test.go index af3fd81dcf5df..045b1ba369550 100644 --- a/support/support_test.go +++ b/support/support_test.go @@ -37,7 +37,7 @@ func TestRun(t *testing.T) { t.Parallel() cfg := coderdtest.DeploymentValues(t) cfg.Experiments = []string{"foo"} - ctx := testutil.Context(t, testutil.WaitShort) + ctx := testutil.Context(t, testutil.WaitLong) client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{ DeploymentValues: cfg, Logger: ptr.Ref(slog.Make(sloghuman.Sink(io.Discard))), @@ -83,7 +83,7 @@ func TestRun(t *testing.T) { t.Parallel() cfg := coderdtest.DeploymentValues(t) cfg.Experiments = []string{"foo"} - ctx := testutil.Context(t, testutil.WaitShort) + ctx := testutil.Context(t, testutil.WaitLong) client := coderdtest.New(t, &coderdtest.Options{ DeploymentValues: cfg, Logger: ptr.Ref(slog.Make(sloghuman.Sink(io.Discard))), @@ -110,7 +110,7 @@ func TestRun(t *testing.T) { t.Run("NoAuth", func(t *testing.T) { t.Parallel() - ctx := testutil.Context(t, testutil.WaitShort) + ctx := testutil.Context(t, testutil.WaitLong) client := coderdtest.New(t, &coderdtest.Options{ Logger: ptr.Ref(slog.Make(sloghuman.Sink(io.Discard))), }) @@ -128,7 +128,7 @@ func TestRun(t *testing.T) { t.Run("MissingPrivilege", func(t *testing.T) { t.Parallel() - ctx := testutil.Context(t, testutil.WaitShort) + ctx := testutil.Context(t, testutil.WaitLong) client := coderdtest.New(t, &coderdtest.Options{ Logger: ptr.Ref(slog.Make(sloghuman.Sink(io.Discard))), }) From d5fb6190b7a78d7dc49d659c0196a19a2914de07 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 13 Mar 2024 13:44:12 +0000 Subject: [PATCH 05/10] feat(support): add agent magicsock HTML --- cli/support.go | 1 + cli/support_test.go | 3 +++ support/support.go | 10 ++++++++++ support/support_test.go | 1 + 4 files changed, 15 insertions(+) diff --git a/cli/support.go b/cli/support.go index f6978a44d6299..c2f8c2457f159 100644 --- a/cli/support.go +++ b/cli/support.go @@ -175,6 +175,7 @@ func writeBundle(src *support.Bundle, dest *zip.Writer) error { "network/tailnet_debug.html": src.Network.TailnetDebug, "workspace/build_logs.txt": humanizeBuildLogs(src.Workspace.BuildLogs), "agent/logs.txt": string(src.Agent.Logs), + "agent/magicsock.html": string(src.Agent.MagicsockHTML), "agent/startup_logs.txt": humanizeAgentLogs(src.Agent.StartupLogs), "workspace/template_file.zip": string(templateVersionBytes), "logs.txt": strings.Join(src.Logs, "\n"), diff --git a/cli/support_test.go b/cli/support_test.go index 3c169c6985bb2..43be15822f330 100644 --- a/cli/support_test.go +++ b/cli/support_test.go @@ -170,6 +170,9 @@ func assertBundleContents(t *testing.T, path string) { case "agent/logs.txt": bs := readBytesFromZip(t, f) require.NotEmpty(t, bs, "logs should not be empty") + case "agent/magicsock.html": + bs := readBytesFromZip(t, f) + require.NotEmpty(t, bs, "agent magicsock should not be empty") case "agent/manifest.json": var v agentsdk.Manifest decodeJSONFromZip(t, f, &v) diff --git a/support/support.go b/support/support.go index 24cd7d7b301bb..feb739432d6e8 100644 --- a/support/support.go +++ b/support/support.go @@ -62,6 +62,7 @@ type Agent struct { Agent codersdk.WorkspaceAgent `json:"agent"` ListeningPorts *codersdk.WorkspaceAgentListeningPortsResponse `json:"listening_ports"` Logs []byte `json:"logs"` + MagicsockHTML []byte `json:"magicsock_html"` Manifest *agentsdk.Manifest `json:"manifest"` PeerDiagnostics *tailnet.PeerDiagnostics `json:"peer_diagnostics"` PingResult *ipnstate.PingResult `json:"ping_result"` @@ -340,6 +341,15 @@ func AgentInfo(ctx context.Context, client *codersdk.Client, log slog.Logger, ag return nil }) + eg.Go(func() error { + msBytes, err := conn.DebugMagicsock(ctx) + if err != nil { + return xerrors.Errorf("get agent magicsock page: %w", err) + } + a.MagicsockHTML = msBytes + return nil + }) + eg.Go(func() error { tokenBytes, err := runCmd(ctx, client, agentID, `echo $CODER_AGENT_TOKEN`) if err != nil { diff --git a/support/support_test.go b/support/support_test.go index 045b1ba369550..14369f9b573be 100644 --- a/support/support_test.go +++ b/support/support_test.go @@ -72,6 +72,7 @@ func TestRun(t *testing.T) { require.NotNil(t, bun.Agent.Agent) require.NotNil(t, bun.Agent.ListeningPorts) require.NotNil(t, bun.Agent.Logs) + require.NotNil(t, bun.Agent.MagicsockHTML) require.NotNil(t, bun.Agent.Manifest) require.NotNil(t, bun.Agent.PeerDiagnostics) require.NotNil(t, bun.Agent.PingResult) From 8022a437e14e802facc787d5879652c480958c87 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 13 Mar 2024 17:36:08 +0000 Subject: [PATCH 06/10] remove unused field --- cli/support.go | 3 +-- cli/support_test.go | 8 ++++---- support/support.go | 5 ++--- support/support_test.go | 2 +- 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/cli/support.go b/cli/support.go index c2f8c2457f159..8e453dee5f7df 100644 --- a/cli/support.go +++ b/cli/support.go @@ -142,8 +142,7 @@ func writeBundle(src *support.Bundle, dest *zip.Writer) error { "deployment/config.json": src.Deployment.Config, "deployment/experiments.json": src.Deployment.Experiments, "deployment/health.json": src.Deployment.HealthReport, - "network/netcheck_local.json": src.Network.NetcheckLocal, - "network/netcheck_remote.json": src.Network.NetcheckRemote, + "network/netcheck.json": src.Network.Netcheck, "workspace/workspace.json": src.Workspace.Workspace, "agent/agent.json": src.Agent.Agent, "agent/listening_ports.json": src.Agent.ListeningPorts, diff --git a/cli/support_test.go b/cli/support_test.go index 43be15822f330..90529becdd5f4 100644 --- a/cli/support_test.go +++ b/cli/support_test.go @@ -148,10 +148,10 @@ func assertBundleContents(t *testing.T, path string) { case "network/tailnet_debug.html": bs := readBytesFromZip(t, f) require.NotEmpty(t, bs, "tailnet debug should not be empty") - case "network/netcheck_local.json", "network/netcheck_remote.json": - // TODO: setup fake agent? - bs := readBytesFromZip(t, f) - require.NotEmpty(t, bs, "netcheck should not be empty") + case "network/netcheck.json": + var v codersdk.WorkspaceAgentConnectionInfo + decodeJSONFromZip(t, f, &v) + require.NotEmpty(t, v, "connection info should not be empty") case "workspace/workspace.json": var v codersdk.Workspace decodeJSONFromZip(t, f, &v) diff --git a/support/support.go b/support/support.go index feb739432d6e8..d28507fc7c98d 100644 --- a/support/support.go +++ b/support/support.go @@ -45,8 +45,7 @@ type Deployment struct { type Network struct { CoordinatorDebug string `json:"coordinator_debug"` TailnetDebug string `json:"tailnet_debug"` - NetcheckLocal *codersdk.WorkspaceAgentConnectionInfo `json:"netcheck_local"` - NetcheckRemote *codersdk.WorkspaceAgentConnectionInfo `json:"netcheck_remote"` + Netcheck *codersdk.WorkspaceAgentConnectionInfo `json:"netcheck"` } type Workspace struct { @@ -175,7 +174,7 @@ func NetworkInfo(ctx context.Context, client *codersdk.Client, log slog.Logger, if err != nil { return xerrors.Errorf("fetch agent conn info: %w", err) } - n.NetcheckLocal = &connInfo + n.Netcheck = &connInfo return nil }) diff --git a/support/support_test.go b/support/support_test.go index 14369f9b573be..908494d41d576 100644 --- a/support/support_test.go +++ b/support/support_test.go @@ -61,7 +61,7 @@ func TestRun(t *testing.T) { require.NotEmpty(t, bun.Deployment.Experiments) require.NotEmpty(t, bun.Network.CoordinatorDebug) require.NotEmpty(t, bun.Network.TailnetDebug) - require.NotNil(t, bun.Network.NetcheckLocal) + require.NotNil(t, bun.Network.Netcheck) require.NotNil(t, bun.Workspace.Workspace) assertSanitizedWorkspace(t, bun.Workspace.Workspace) require.NotEmpty(t, bun.Workspace.BuildLogs) From 779b90e951dfe0ec723b8c6858df07a3bd677492 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 13 Mar 2024 17:50:57 +0000 Subject: [PATCH 07/10] wait for conn to close --- support/support.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/support/support.go b/support/support.go index d28507fc7c98d..561d1b277b513 100644 --- a/support/support.go +++ b/support/support.go @@ -314,14 +314,21 @@ func AgentInfo(ctx context.Context, client *codersdk.Client, log slog.Logger, ag return nil }) - conn, err := client.DialWorkspaceAgent(ctx, agentID, &codersdk.DialWorkspaceAgentOptions{ + dialCtx, dialCancel := context.WithCancel(ctx) + defer dialCancel() + conn, err := client.DialWorkspaceAgent(dialCtx, agentID, &codersdk.DialWorkspaceAgentOptions{ Logger: log.Named("dial-agent"), BlockEndpoints: false, }) if err != nil { log.Error(ctx, "dial agent", slog.Error(err)) } else { - defer conn.Close() + defer func() { + if err := conn.Close(); err != nil { + log.Error(ctx, "failed to close agent connection", slog.Error(err)) + } + <-conn.Closed() + }() if !conn.AwaitReachable(ctx) { log.Error(ctx, "timed out waiting for agent") } else { From 4ccb82b5c9f5c28f84bf7c3d385c196fe3b838aa Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 14 Mar 2024 11:44:28 +0000 Subject: [PATCH 08/10] actually close connections properly --- support/support.go | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/support/support.go b/support/support.go index 561d1b277b513..cc0344f1a0cf4 100644 --- a/support/support.go +++ b/support/support.go @@ -5,7 +5,6 @@ import ( "context" "encoding/base64" "encoding/json" - "errors" "io" "net/http" "strings" @@ -476,34 +475,23 @@ func sanitizeEnv(kvs map[string]string) { // TODO: use rpty instead? which is less liable to fail? func runCmd(ctx context.Context, client *codersdk.Client, agentID uuid.UUID, cmd string) ([]byte, error) { - var err error - var closers []func() error - defer func() { - if err != nil { - for _, c := range closers { - if err2 := c(); err2 != nil { - err = errors.Join(err, err2) - } - } - } - }() agentConn, err := client.DialWorkspaceAgent(ctx, agentID, &codersdk.DialWorkspaceAgentOptions{}) if err != nil { return nil, xerrors.Errorf("dial workspace agent: %w", err) } - closers = append(closers, agentConn.Close) + defer agentConn.Close() sshClient, err := agentConn.SSHClient(ctx) if err != nil { return nil, xerrors.Errorf("get ssh client: %w", err) } - closers = append(closers, sshClient.Close) + defer sshClient.Close() sshSession, err := sshClient.NewSession() if err != nil { return nil, xerrors.Errorf("new ssh session: %w", err) } - closers = append(closers, sshSession.Close) + defer sshSession.Close() cmdBytes, err := sshSession.CombinedOutput(cmd) if err != nil { From df184a09eafb6c89cd5ecb04b66ffbe75a0259a6 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 14 Mar 2024 16:36:34 +0000 Subject: [PATCH 09/10] use workspaceagentconn methods added in #12593 --- cli/support_test.go | 9 ++++++++- support/support.go | 43 ++++------------------------------------- support/support_test.go | 10 +++++++++- 3 files changed, 21 insertions(+), 41 deletions(-) diff --git a/cli/support_test.go b/cli/support_test.go index 90529becdd5f4..b19cbaebc57f9 100644 --- a/cli/support_test.go +++ b/cli/support_test.go @@ -4,6 +4,7 @@ import ( "archive/zip" "encoding/json" "io" + "os" "path/filepath" "runtime" "testing" @@ -13,6 +14,7 @@ import ( "github.com/stretchr/testify/require" + "github.com/coder/coder/v2/agent" "github.com/coder/coder/v2/agent/agenttest" "github.com/coder/coder/v2/cli/clitest" "github.com/coder/coder/v2/coderd/coderdtest" @@ -42,7 +44,12 @@ func TestSupportBundle(t *testing.T) { }).WithAgent().Do() ws, err := client.Workspace(ctx, r.Workspace.ID) require.NoError(t, err) - agt := agenttest.New(t, client.URL, r.AgentToken) + tempDir := t.TempDir() + logPath := filepath.Join(tempDir, "coder-agent.log") + require.NoError(t, os.WriteFile(logPath, []byte("hello from the agent"), 0o600)) + agt := agenttest.New(t, client.URL, r.AgentToken, func(o *agent.Options) { + o.LogDir = tempDir + }) defer agt.Close() coderdtest.NewWorkspaceAgentWaiter(t, client, r.Workspace.ID).Wait() diff --git a/support/support.go b/support/support.go index cc0344f1a0cf4..966c75b5ec9bf 100644 --- a/support/support.go +++ b/support/support.go @@ -356,19 +356,11 @@ func AgentInfo(ctx context.Context, client *codersdk.Client, log slog.Logger, ag }) eg.Go(func() error { - tokenBytes, err := runCmd(ctx, client, agentID, `echo $CODER_AGENT_TOKEN`) - if err != nil { - return xerrors.Errorf("failed to fetch agent token: %w", err) - } - token := string(bytes.TrimSpace(tokenBytes)) - agentClient := agentsdk.New(client.URL) - agentClient.SetSessionToken(token) - manifestRes, err := agentClient.SDK.Request(ctx, http.MethodGet, "/api/v2/workspaceagents/me/manifest", nil) + manifestRes, err := conn.DebugManifest(ctx) if err != nil { return xerrors.Errorf("fetch manifest: %w", err) } - defer manifestRes.Body.Close() - if err := json.NewDecoder(manifestRes.Body).Decode(&a.Manifest); err != nil { + if err := json.NewDecoder(bytes.NewReader(manifestRes)).Decode(&a.Manifest); err != nil { return xerrors.Errorf("decode agent manifest: %w", err) } @@ -376,9 +368,9 @@ func AgentInfo(ctx context.Context, client *codersdk.Client, log slog.Logger, ag }) eg.Go(func() error { - logBytes, err := runCmd(ctx, client, agentID, `tail -n 1000 /tmp/coder-agent.log`) + logBytes, err := conn.DebugLogs(ctx) if err != nil { - return xerrors.Errorf("tail /tmp/coder-agent.log: %w", err) + return xerrors.Errorf("fetch coder agent logs: %w", err) } a.Logs = logBytes return nil @@ -472,30 +464,3 @@ func sanitizeEnv(kvs map[string]string) { } } } - -// TODO: use rpty instead? which is less liable to fail? -func runCmd(ctx context.Context, client *codersdk.Client, agentID uuid.UUID, cmd string) ([]byte, error) { - agentConn, err := client.DialWorkspaceAgent(ctx, agentID, &codersdk.DialWorkspaceAgentOptions{}) - if err != nil { - return nil, xerrors.Errorf("dial workspace agent: %w", err) - } - defer agentConn.Close() - - sshClient, err := agentConn.SSHClient(ctx) - if err != nil { - return nil, xerrors.Errorf("get ssh client: %w", err) - } - defer sshClient.Close() - - sshSession, err := sshClient.NewSession() - if err != nil { - return nil, xerrors.Errorf("new ssh session: %w", err) - } - defer sshSession.Close() - - cmdBytes, err := sshSession.CombinedOutput(cmd) - if err != nil { - return nil, xerrors.Errorf("shell: %w", err) - } - return cmdBytes, err -} diff --git a/support/support_test.go b/support/support_test.go index 908494d41d576..17ce6100eb129 100644 --- a/support/support_test.go +++ b/support/support_test.go @@ -5,6 +5,8 @@ import ( "context" "io" "net/http" + "os" + "path/filepath" "testing" "time" @@ -15,6 +17,7 @@ import ( "cdr.dev/slog" "cdr.dev/slog/sloggers/sloghuman" "cdr.dev/slog/sloggers/slogtest" + "github.com/coder/coder/v2/agent" "github.com/coder/coder/v2/agent/agenttest" "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/database" @@ -212,7 +215,12 @@ func setupWorkspaceAndAgent(ctx context.Context, t *testing.T, client *codersdk. }) require.NoError(t, err) - _ = agenttest.New(t, client.URL, wbr.AgentToken) + tempDir := t.TempDir() + logPath := filepath.Join(tempDir, "coder-agent.log") + require.NoError(t, os.WriteFile(logPath, []byte("hello from the agent"), 0o600)) + _ = agenttest.New(t, client.URL, wbr.AgentToken, func(o *agent.Options) { + o.LogDir = tempDir + }) coderdtest.NewWorkspaceAgentWaiter(t, client, wbr.Workspace.ID).Wait() return ws, agt From c29d922be19b372853bd858f3d5ccbec0a859e3f Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Fri, 15 Mar 2024 10:54:50 +0000 Subject: [PATCH 10/10] address PR comments, beef up test assertions --- cli/support.go | 2 ++ support/support.go | 8 ++--- support/support_test.go | 80 ++++++++++++++++++++++------------------- 3 files changed, 49 insertions(+), 41 deletions(-) diff --git a/cli/support.go b/cli/support.go index 8e453dee5f7df..ab6687e442967 100644 --- a/cli/support.go +++ b/cli/support.go @@ -137,6 +137,7 @@ func findAgent(agentName string, haystack []codersdk.WorkspaceResource) (*coders } func writeBundle(src *support.Bundle, dest *zip.Writer) error { + // We JSON-encode the following: for k, v := range map[string]any{ "deployment/buildinfo.json": src.Deployment.BuildInfo, "deployment/config.json": src.Deployment.Config, @@ -169,6 +170,7 @@ func writeBundle(src *support.Bundle, dest *zip.Writer) error { return xerrors.Errorf("decode template zip from base64") } + // The below we just write as we have them: for k, v := range map[string]string{ "network/coordinator_debug.html": src.Network.CoordinatorDebug, "network/tailnet_debug.html": src.Network.TailnetDebug, diff --git a/support/support.go b/support/support.go index 966c75b5ec9bf..2a2c3c55f8a2c 100644 --- a/support/support.go +++ b/support/support.go @@ -57,7 +57,7 @@ type Workspace struct { } type Agent struct { - Agent codersdk.WorkspaceAgent `json:"agent"` + Agent *codersdk.WorkspaceAgent `json:"agent"` ListeningPorts *codersdk.WorkspaceAgentListeningPortsResponse `json:"listening_ports"` Logs []byte `json:"logs"` MagicsockHTML []byte `json:"magicsock_html"` @@ -295,7 +295,7 @@ func AgentInfo(ctx context.Context, client *codersdk.Client, log slog.Logger, ag if err != nil { return xerrors.Errorf("fetch workspace agent: %w", err) } - a.Agent = agt + a.Agent = &agt return nil }) @@ -313,9 +313,7 @@ func AgentInfo(ctx context.Context, client *codersdk.Client, log slog.Logger, ag return nil }) - dialCtx, dialCancel := context.WithCancel(ctx) - defer dialCancel() - conn, err := client.DialWorkspaceAgent(dialCtx, agentID, &codersdk.DialWorkspaceAgentOptions{ + conn, err := client.DialWorkspaceAgent(ctx, agentID, &codersdk.DialWorkspaceAgentOptions{ Logger: log.Named("dial-agent"), BlockEndpoints: false, }) diff --git a/support/support_test.go b/support/support_test.go index 17ce6100eb129..6876bafbc50c2 100644 --- a/support/support_test.go +++ b/support/support_test.go @@ -55,35 +55,34 @@ func TestRun(t *testing.T) { AgentID: agt.ID, }) require.NoError(t, err) - require.NotEmpty(t, bun) - require.NotEmpty(t, bun.Deployment.BuildInfo) - require.NotEmpty(t, bun.Deployment.Config) - require.NotEmpty(t, bun.Deployment.Config.Options) + assertNotNilNotEmpty(t, bun, "bundle should be present") + assertNotNilNotEmpty(t, bun.Deployment.BuildInfo, "deployment build info should be present") + assertNotNilNotEmpty(t, bun.Deployment.Config, "deployment config should be present") + assertNotNilNotEmpty(t, bun.Deployment.Config.Options, "deployment config should be present") assertSanitizedDeploymentConfig(t, bun.Deployment.Config) - require.NotEmpty(t, bun.Deployment.HealthReport) - require.NotEmpty(t, bun.Deployment.Experiments) - require.NotEmpty(t, bun.Network.CoordinatorDebug) - require.NotEmpty(t, bun.Network.TailnetDebug) - require.NotNil(t, bun.Network.Netcheck) - require.NotNil(t, bun.Workspace.Workspace) + assertNotNilNotEmpty(t, bun.Deployment.HealthReport, "deployment health report should be present") + assertNotNilNotEmpty(t, bun.Deployment.Experiments, "deployment experiments should be present") + assertNotNilNotEmpty(t, bun.Network.CoordinatorDebug, "network coordinator debug should be present") + assertNotNilNotEmpty(t, bun.Network.TailnetDebug, "network tailnet debug should be present") + assertNotNilNotEmpty(t, bun.Network.Netcheck, "network netcheck should be present") + assertNotNilNotEmpty(t, bun.Workspace.Workspace, "workspace should be present") assertSanitizedWorkspace(t, bun.Workspace.Workspace) - require.NotEmpty(t, bun.Workspace.BuildLogs) - require.NotEmpty(t, bun.Workspace.Template) - require.NotEmpty(t, bun.Workspace.TemplateVersion) - require.NotEmpty(t, bun.Workspace.TemplateFileBase64) - require.NotNil(t, bun.Workspace.Parameters) - require.NotNil(t, bun.Agent.Agent) - require.NotNil(t, bun.Agent.ListeningPorts) - require.NotNil(t, bun.Agent.Logs) - require.NotNil(t, bun.Agent.MagicsockHTML) - require.NotNil(t, bun.Agent.Manifest) - require.NotNil(t, bun.Agent.PeerDiagnostics) - require.NotNil(t, bun.Agent.PingResult) - require.NotEmpty(t, bun.Agent.StartupLogs) - require.NotEmpty(t, bun.Logs) + assertNotNilNotEmpty(t, bun.Workspace.BuildLogs, "workspace build logs should be present") + assertNotNilNotEmpty(t, bun.Workspace.Template, "workspace template should be present") + assertNotNilNotEmpty(t, bun.Workspace.TemplateVersion, "workspace template version should be present") + assertNotNilNotEmpty(t, bun.Workspace.TemplateFileBase64, "workspace template file should be present") + require.NotNil(t, bun.Workspace.Parameters, "workspace parameters should be present") + assertNotNilNotEmpty(t, bun.Agent.Agent, "agent should be present") + assertNotNilNotEmpty(t, bun.Agent.ListeningPorts, "agent listening ports should be present") + assertNotNilNotEmpty(t, bun.Agent.Logs, "agent logs should be present") + assertNotNilNotEmpty(t, bun.Agent.MagicsockHTML, "agent magicsock should be present") + assertNotNilNotEmpty(t, bun.Agent.PeerDiagnostics, "agent peer diagnostics should be present") + assertNotNilNotEmpty(t, bun.Agent.PingResult, "agent ping result should be present") + assertNotNilNotEmpty(t, bun.Agent.StartupLogs, "agent startup logs should be present") + assertNotNilNotEmpty(t, bun.Logs, "bundle logs should be present") }) - t.Run("OK_NoAgent", func(t *testing.T) { + t.Run("OK_NoWorkspace", func(t *testing.T) { t.Parallel() cfg := coderdtest.DeploymentValues(t) cfg.Experiments = []string{"foo"} @@ -98,18 +97,19 @@ func TestRun(t *testing.T) { Log: slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Named("bundle").Leveled(slog.LevelDebug), }) require.NoError(t, err) - require.NotEmpty(t, bun) - require.NotEmpty(t, bun.Deployment.BuildInfo) - require.NotEmpty(t, bun.Deployment.Config) - require.NotEmpty(t, bun.Deployment.Config.Options) + assertNotNilNotEmpty(t, bun, "bundle should be present") + assertNotNilNotEmpty(t, bun.Deployment.BuildInfo, "deployment build info should be present") + assertNotNilNotEmpty(t, bun.Deployment.Config, "deployment config should be present") + assertNotNilNotEmpty(t, bun.Deployment.Config.Options, "deployment config should be present") assertSanitizedDeploymentConfig(t, bun.Deployment.Config) - require.NotEmpty(t, bun.Deployment.HealthReport) - require.NotEmpty(t, bun.Deployment.Experiments) - require.NotEmpty(t, bun.Network.CoordinatorDebug) - require.NotEmpty(t, bun.Network.TailnetDebug) - require.NotNil(t, bun.Workspace) - assertSanitizedWorkspace(t, bun.Workspace.Workspace) - require.NotEmpty(t, bun.Logs) + assertNotNilNotEmpty(t, bun.Deployment.HealthReport, "deployment health report should be present") + assertNotNilNotEmpty(t, bun.Deployment.Experiments, "deployment experiments should be present") + assertNotNilNotEmpty(t, bun.Network.CoordinatorDebug, "network coordinator debug should be present") + assertNotNilNotEmpty(t, bun.Network.TailnetDebug, "network tailnet debug should be present") + assert.Empty(t, bun.Network.Netcheck, "did not expect netcheck to be present") + assert.Empty(t, bun.Workspace.Workspace, "did not expect workspace to be present") + assert.Empty(t, bun.Agent, "did not expect agent to be present") + assertNotNilNotEmpty(t, bun.Logs, "bundle logs should be present") }) t.Run("NoAuth", func(t *testing.T) { @@ -225,3 +225,11 @@ func setupWorkspaceAndAgent(ctx context.Context, t *testing.T, client *codersdk. return ws, agt } + +func assertNotNilNotEmpty[T any](t *testing.T, v T, msg string) { + t.Helper() + + if assert.NotNil(t, v, msg+" but was nil") { + assert.NotEmpty(t, v, msg+" but was empty") + } +}