Skip to content

Commit 1092eb2

Browse files
committed
feat(support): fetch manifest, logs etc. from agent
1 parent 34d25a1 commit 1092eb2

File tree

4 files changed

+160
-5
lines changed

4 files changed

+160
-5
lines changed

cli/support.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,10 @@ func writeBundle(src *support.Bundle, dest *zip.Writer) error {
146146
"network/netcheck_remote.json": src.Network.NetcheckRemote,
147147
"workspace/workspace.json": src.Workspace.Workspace,
148148
"agent/agent.json": src.Agent.Agent,
149+
"agent/listening_ports.json": src.Agent.ListeningPorts,
150+
"agent/manifest.json": src.Agent.Manifest,
151+
"agent/peer_diagnostics.json": src.Agent.PeerDiagnostics,
152+
"agent/ping_result.json": src.Agent.PingResult,
149153
"workspace/template.json": src.Workspace.Template,
150154
"workspace/template_version.json": src.Workspace.TemplateVersion,
151155
"workspace/parameters.json": src.Workspace.Parameters,
@@ -170,6 +174,7 @@ func writeBundle(src *support.Bundle, dest *zip.Writer) error {
170174
"network/coordinator_debug.html": src.Network.CoordinatorDebug,
171175
"network/tailnet_debug.html": src.Network.TailnetDebug,
172176
"workspace/build_logs.txt": humanizeBuildLogs(src.Workspace.BuildLogs),
177+
"agent/logs.txt": string(src.Agent.Logs),
173178
"agent/startup_logs.txt": humanizeAgentLogs(src.Agent.StartupLogs),
174179
"workspace/template_file.zip": string(templateVersionBytes),
175180
"logs.txt": strings.Join(src.Logs, "\n"),

cli/support_test.go

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,19 @@ import (
99
"testing"
1010
"time"
1111

12+
"tailscale.com/ipn/ipnstate"
13+
1214
"github.com/stretchr/testify/require"
1315

16+
"github.com/coder/coder/v2/agent/agenttest"
1417
"github.com/coder/coder/v2/cli/clitest"
1518
"github.com/coder/coder/v2/coderd/coderdtest"
1619
"github.com/coder/coder/v2/coderd/database"
1720
"github.com/coder/coder/v2/coderd/database/dbfake"
1821
"github.com/coder/coder/v2/coderd/database/dbtime"
1922
"github.com/coder/coder/v2/codersdk"
23+
"github.com/coder/coder/v2/codersdk/agentsdk"
24+
"github.com/coder/coder/v2/tailnet"
2025
"github.com/coder/coder/v2/testutil"
2126
)
2227

@@ -37,7 +42,9 @@ func TestSupportBundle(t *testing.T) {
3742
}).WithAgent().Do()
3843
ws, err := client.Workspace(ctx, r.Workspace.ID)
3944
require.NoError(t, err)
40-
agt := ws.LatestBuild.Resources[0].Agents[0]
45+
agt := agenttest.New(t, client.URL, r.AgentToken)
46+
defer agt.Close()
47+
coderdtest.NewWorkspaceAgentWaiter(t, client, r.Workspace.ID).Wait()
4148

4249
// Insert a provisioner job log
4350
_, err = db.InsertProvisionerJobLogs(ctx, database.InsertProvisionerJobLogsParams{
@@ -51,7 +58,7 @@ func TestSupportBundle(t *testing.T) {
5158
require.NoError(t, err)
5259
// Insert an agent log
5360
_, err = db.InsertWorkspaceAgentLogs(ctx, database.InsertWorkspaceAgentLogsParams{
54-
AgentID: agt.ID,
61+
AgentID: ws.LatestBuild.Resources[0].Agents[0].ID,
5562
CreatedAt: dbtime.Now(),
5663
Output: []string{"started up"},
5764
Level: []database.LogLevel{database.LogLevelInfo},
@@ -156,6 +163,25 @@ func assertBundleContents(t *testing.T, path string) {
156163
var v codersdk.WorkspaceAgent
157164
decodeJSONFromZip(t, f, &v)
158165
require.NotEmpty(t, v, "agent should not be empty")
166+
case "agent/listening_ports.json":
167+
var v codersdk.WorkspaceAgentListeningPortsResponse
168+
decodeJSONFromZip(t, f, &v)
169+
require.NotEmpty(t, v, "agent listening ports should not be empty")
170+
case "agent/logs.txt":
171+
bs := readBytesFromZip(t, f)
172+
require.NotEmpty(t, bs, "logs should not be empty")
173+
case "agent/manifest.json":
174+
var v agentsdk.Manifest
175+
decodeJSONFromZip(t, f, &v)
176+
require.NotEmpty(t, v, "agent manifest should not be empty")
177+
case "agent/peer_diagnostics.json":
178+
var v *tailnet.PeerDiagnostics
179+
decodeJSONFromZip(t, f, &v)
180+
require.NotEmpty(t, v, "peer diagnostics should not be empty")
181+
case "agent/ping_result.json":
182+
var v *ipnstate.PingResult
183+
decodeJSONFromZip(t, f, &v)
184+
require.NotEmpty(t, v, "ping result should not be empty")
159185
case "agent/startup_logs.txt":
160186
bs := readBytesFromZip(t, f)
161187
require.Contains(t, string(bs), "started up")
@@ -178,7 +204,7 @@ func assertBundleContents(t *testing.T, path string) {
178204
bs := readBytesFromZip(t, f)
179205
require.NotEmpty(t, bs, "logs should not be empty")
180206
default:
181-
require.Failf(t, "unexpected file in bundle: %q", f.Name)
207+
require.Failf(t, "unexpected file in bundle", f.Name)
182208
}
183209
}
184210
}

support/support.go

Lines changed: 117 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,27 @@
11
package support
22

33
import (
4+
"bytes"
45
"context"
56
"encoding/base64"
7+
"encoding/json"
8+
"errors"
69
"io"
710
"net/http"
811
"strings"
912

1013
"golang.org/x/sync/errgroup"
1114
"golang.org/x/xerrors"
15+
"tailscale.com/ipn/ipnstate"
1216

1317
"github.com/google/uuid"
1418

1519
"cdr.dev/slog"
1620
"cdr.dev/slog/sloggers/sloghuman"
1721
"github.com/coder/coder/v2/coderd/rbac"
1822
"github.com/coder/coder/v2/codersdk"
23+
"github.com/coder/coder/v2/codersdk/agentsdk"
24+
"github.com/coder/coder/v2/tailnet"
1925
)
2026

2127
// Bundle is a set of information discovered about a deployment.
@@ -53,8 +59,13 @@ type Workspace struct {
5359
}
5460

5561
type Agent struct {
56-
Agent codersdk.WorkspaceAgent `json:"agent"`
57-
StartupLogs []codersdk.WorkspaceAgentLog `json:"startup_logs"`
62+
Agent codersdk.WorkspaceAgent `json:"agent"`
63+
ListeningPorts *codersdk.WorkspaceAgentListeningPortsResponse `json:"listening_ports"`
64+
Logs []byte `json:"logs"`
65+
Manifest *agentsdk.Manifest `json:"manifest"`
66+
PeerDiagnostics *tailnet.PeerDiagnostics `json:"peer_diagnostics"`
67+
PingResult *ipnstate.PingResult `json:"ping_result"`
68+
StartupLogs []codersdk.WorkspaceAgentLog `json:"startup_logs"`
5869
}
5970

6071
// 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
303314
return nil
304315
})
305316

317+
conn, err := client.DialWorkspaceAgent(ctx, agentID, &codersdk.DialWorkspaceAgentOptions{
318+
Logger: log.Named("dial-agent"),
319+
BlockEndpoints: false,
320+
})
321+
if err != nil {
322+
log.Error(ctx, "dial agent", slog.Error(err))
323+
} else {
324+
defer conn.Close()
325+
if !conn.AwaitReachable(ctx) {
326+
log.Error(ctx, "timed out waiting for agent")
327+
} else {
328+
eg.Go(func() error {
329+
_, _, pingRes, err := conn.Ping(ctx)
330+
if err != nil {
331+
return xerrors.Errorf("ping agent: %w", err)
332+
}
333+
a.PingResult = pingRes
334+
return nil
335+
})
336+
337+
eg.Go(func() error {
338+
pds := conn.GetPeerDiagnostics()
339+
a.PeerDiagnostics = &pds
340+
return nil
341+
})
342+
343+
eg.Go(func() error {
344+
tokenBytes, err := runCmd(ctx, client, agentID, `echo $CODER_AGENT_TOKEN`)
345+
if err != nil {
346+
return xerrors.Errorf("failed to fetch agent token: %w", err)
347+
}
348+
token := string(bytes.TrimSpace(tokenBytes))
349+
agentClient := agentsdk.New(client.URL)
350+
agentClient.SetSessionToken(token)
351+
manifestRes, err := agentClient.SDK.Request(ctx, http.MethodGet, "/api/v2/workspaceagents/me/manifest", nil)
352+
if err != nil {
353+
return xerrors.Errorf("fetch manifest: %w", err)
354+
}
355+
defer manifestRes.Body.Close()
356+
if err := json.NewDecoder(manifestRes.Body).Decode(&a.Manifest); err != nil {
357+
return xerrors.Errorf("decode agent manifest: %w", err)
358+
}
359+
360+
return nil
361+
})
362+
363+
eg.Go(func() error {
364+
logBytes, err := runCmd(ctx, client, agentID, `tail -n 1000 /tmp/coder-agent.log`)
365+
if err != nil {
366+
return xerrors.Errorf("tail /tmp/coder-agent.log: %w", err)
367+
}
368+
a.Logs = logBytes
369+
return nil
370+
})
371+
372+
eg.Go(func() error {
373+
lps, err := conn.ListeningPorts(ctx)
374+
if err != nil {
375+
return xerrors.Errorf("get listening ports: %w", err)
376+
}
377+
a.ListeningPorts = &lps
378+
return nil
379+
})
380+
}
381+
}
382+
306383
if err := eg.Wait(); err != nil {
307384
log.Error(ctx, "fetch agent information", slog.Error(err))
308385
}
@@ -380,3 +457,41 @@ func sanitizeEnv(kvs map[string]string) {
380457
}
381458
}
382459
}
460+
461+
// TODO: use rpty instead? which is less liable to fail?
462+
func runCmd(ctx context.Context, client *codersdk.Client, agentID uuid.UUID, cmd string) ([]byte, error) {
463+
var err error
464+
var closers []func() error
465+
defer func() {
466+
if err != nil {
467+
for _, c := range closers {
468+
if err2 := c(); err2 != nil {
469+
err = errors.Join(err, err2)
470+
}
471+
}
472+
}
473+
}()
474+
agentConn, err := client.DialWorkspaceAgent(ctx, agentID, &codersdk.DialWorkspaceAgentOptions{})
475+
if err != nil {
476+
return nil, xerrors.Errorf("dial workspace agent: %w", err)
477+
}
478+
closers = append(closers, agentConn.Close)
479+
480+
sshClient, err := agentConn.SSHClient(ctx)
481+
if err != nil {
482+
return nil, xerrors.Errorf("get ssh client: %w", err)
483+
}
484+
closers = append(closers, sshClient.Close)
485+
486+
sshSession, err := sshClient.NewSession()
487+
if err != nil {
488+
return nil, xerrors.Errorf("new ssh session: %w", err)
489+
}
490+
closers = append(closers, sshSession.Close)
491+
492+
cmdBytes, err := sshSession.CombinedOutput(cmd)
493+
if err != nil {
494+
return nil, xerrors.Errorf("shell: %w", err)
495+
}
496+
return cmdBytes, err
497+
}

support/support_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"cdr.dev/slog"
1515
"cdr.dev/slog/sloggers/sloghuman"
1616
"cdr.dev/slog/sloggers/slogtest"
17+
"github.com/coder/coder/v2/agent/agenttest"
1718
"github.com/coder/coder/v2/coderd/coderdtest"
1819
"github.com/coder/coder/v2/coderd/database"
1920
"github.com/coder/coder/v2/coderd/database/dbfake"
@@ -64,6 +65,11 @@ func TestRun(t *testing.T) {
6465
require.NotEmpty(t, bun.Workspace.TemplateFileBase64)
6566
require.NotNil(t, bun.Workspace.Parameters)
6667
require.NotNil(t, bun.Agent.Agent)
68+
require.NotNil(t, bun.Agent.ListeningPorts)
69+
require.NotNil(t, bun.Agent.Logs)
70+
require.NotNil(t, bun.Agent.Manifest)
71+
require.NotNil(t, bun.Agent.PeerDiagnostics)
72+
require.NotNil(t, bun.Agent.PingResult)
6773
require.NotEmpty(t, bun.Agent.StartupLogs)
6874
require.NotEmpty(t, bun.Logs)
6975
})
@@ -200,5 +206,8 @@ func setupWorkspaceAndAgent(ctx context.Context, t *testing.T, client *codersdk.
200206
})
201207
require.NoError(t, err)
202208

209+
_ = agenttest.New(t, client.URL, wbr.AgentToken)
210+
coderdtest.NewWorkspaceAgentWaiter(t, client, wbr.Workspace.ID).Wait()
211+
203212
return ws, agt
204213
}

0 commit comments

Comments
 (0)