Skip to content

Commit e57c101

Browse files
authored
feat: add support package and accompanying tests (#12289)
1 parent 2bf3c72 commit e57c101

File tree

3 files changed

+409
-0
lines changed

3 files changed

+409
-0
lines changed

codersdk/health.go

+13
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,19 @@ type UpdateHealthSettings struct {
4444
DismissedHealthchecks []HealthSection `json:"dismissed_healthchecks"`
4545
}
4646

47+
func (c *Client) DebugHealth(ctx context.Context) (HealthcheckReport, error) {
48+
res, err := c.Request(ctx, http.MethodGet, "/api/v2/debug/health", nil)
49+
if err != nil {
50+
return HealthcheckReport{}, err
51+
}
52+
defer res.Body.Close()
53+
if res.StatusCode != http.StatusOK {
54+
return HealthcheckReport{}, ReadBodyAsError(res)
55+
}
56+
var rpt HealthcheckReport
57+
return rpt, json.NewDecoder(res.Body).Decode(&rpt)
58+
}
59+
4760
func (c *Client) HealthSettings(ctx context.Context) (HealthSettings, error) {
4861
res, err := c.Request(ctx, http.MethodGet, "/api/v2/debug/health/settings", nil)
4962
if err != nil {

support/support.go

+227
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
package support
2+
3+
import (
4+
"context"
5+
"io"
6+
"net/http"
7+
"strings"
8+
9+
"golang.org/x/xerrors"
10+
11+
"github.com/google/uuid"
12+
13+
"cdr.dev/slog"
14+
"cdr.dev/slog/sloggers/sloghuman"
15+
"github.com/coder/coder/v2/coderd/rbac"
16+
"github.com/coder/coder/v2/codersdk"
17+
)
18+
19+
// Bundle is a set of information discovered about a deployment.
20+
// Even though we do attempt to sanitize data, it may still contain
21+
// sensitive information and should thus be treated as secret.
22+
type Bundle struct {
23+
Deployment Deployment `json:"deployment"`
24+
Network Network `json:"network"`
25+
Workspace Workspace `json:"workspace"`
26+
Logs []string `json:"logs"`
27+
}
28+
29+
type Deployment struct {
30+
BuildInfo *codersdk.BuildInfoResponse `json:"build"`
31+
Config *codersdk.DeploymentConfig `json:"config"`
32+
Experiments codersdk.Experiments `json:"experiments"`
33+
HealthReport *codersdk.HealthcheckReport `json:"health_report"`
34+
}
35+
36+
type Network struct {
37+
CoordinatorDebug string `json:"coordinator_debug"`
38+
TailnetDebug string `json:"tailnet_debug"`
39+
NetcheckLocal *codersdk.WorkspaceAgentConnectionInfo `json:"netcheck_local"`
40+
NetcheckRemote *codersdk.WorkspaceAgentConnectionInfo `json:"netcheck_remote"`
41+
}
42+
43+
type Workspace struct {
44+
Workspace codersdk.Workspace `json:"workspace"`
45+
BuildLogs []codersdk.ProvisionerJobLog `json:"build_logs"`
46+
Agent codersdk.WorkspaceAgent `json:"agent"`
47+
AgentStartupLogs []codersdk.WorkspaceAgentLog `json:"startup_logs"`
48+
}
49+
50+
// Deps is a set of dependencies for discovering information
51+
type Deps struct {
52+
// Source from which to obtain information.
53+
Client *codersdk.Client
54+
// Log is where to log any informational or warning messages.
55+
Log slog.Logger
56+
// WorkspaceID is the optional workspace against which to run connection tests.
57+
WorkspaceID uuid.UUID
58+
// AgentID is the optional agent ID against which to run connection tests.
59+
// Defaults to the first agent of the workspace, if not specified.
60+
AgentID uuid.UUID
61+
}
62+
63+
func DeploymentInfo(ctx context.Context, client *codersdk.Client, log slog.Logger) Deployment {
64+
var d Deployment
65+
66+
bi, err := client.BuildInfo(ctx)
67+
if err != nil {
68+
log.Error(ctx, "fetch build info", slog.Error(err))
69+
} else {
70+
d.BuildInfo = &bi
71+
}
72+
73+
dc, err := client.DeploymentConfig(ctx)
74+
if err != nil {
75+
log.Error(ctx, "fetch deployment config", slog.Error(err))
76+
} else {
77+
d.Config = dc
78+
}
79+
80+
hr, err := client.DebugHealth(ctx)
81+
if err != nil {
82+
log.Error(ctx, "fetch health report", slog.Error(err))
83+
} else {
84+
d.HealthReport = &hr
85+
}
86+
87+
exp, err := client.Experiments(ctx)
88+
if err != nil {
89+
log.Error(ctx, "fetch experiments", slog.Error(err))
90+
} else {
91+
d.Experiments = exp
92+
}
93+
94+
return d
95+
}
96+
97+
func NetworkInfo(ctx context.Context, client *codersdk.Client, log slog.Logger, agentID uuid.UUID) Network {
98+
var n Network
99+
100+
coordResp, err := client.Request(ctx, http.MethodGet, "/api/v2/debug/coordinator", nil)
101+
if err != nil {
102+
log.Error(ctx, "fetch coordinator debug page", slog.Error(err))
103+
} else {
104+
defer coordResp.Body.Close()
105+
bs, err := io.ReadAll(coordResp.Body)
106+
if err != nil {
107+
log.Error(ctx, "read coordinator debug page", slog.Error(err))
108+
} else {
109+
n.CoordinatorDebug = string(bs)
110+
}
111+
}
112+
113+
tailResp, err := client.Request(ctx, http.MethodGet, "/api/v2/debug/tailnet", nil)
114+
if err != nil {
115+
log.Error(ctx, "fetch tailnet debug page", slog.Error(err))
116+
} else {
117+
defer tailResp.Body.Close()
118+
bs, err := io.ReadAll(tailResp.Body)
119+
if err != nil {
120+
log.Error(ctx, "read tailnet debug page", slog.Error(err))
121+
} else {
122+
n.TailnetDebug = string(bs)
123+
}
124+
}
125+
126+
if agentID != uuid.Nil {
127+
connInfo, err := client.WorkspaceAgentConnectionInfo(ctx, agentID)
128+
if err != nil {
129+
log.Error(ctx, "fetch agent conn info", slog.Error(err), slog.F("agent_id", agentID.String()))
130+
} else {
131+
n.NetcheckLocal = &connInfo
132+
}
133+
} else {
134+
log.Warn(ctx, "agent id required for agent connection info")
135+
}
136+
137+
return n
138+
}
139+
140+
func WorkspaceInfo(ctx context.Context, client *codersdk.Client, log slog.Logger, workspaceID, agentID uuid.UUID) Workspace {
141+
var w Workspace
142+
143+
if workspaceID == uuid.Nil {
144+
log.Error(ctx, "no workspace id specified")
145+
return w
146+
}
147+
148+
if agentID == uuid.Nil {
149+
log.Error(ctx, "no agent id specified")
150+
}
151+
152+
ws, err := client.Workspace(ctx, workspaceID)
153+
if err != nil {
154+
log.Error(ctx, "fetch workspace", slog.Error(err), slog.F("workspace_id", workspaceID))
155+
return w
156+
}
157+
158+
w.Workspace = ws
159+
160+
buildLogCh, closer, err := client.WorkspaceBuildLogsAfter(ctx, ws.LatestBuild.ID, 0)
161+
if err != nil {
162+
log.Error(ctx, "fetch provisioner job logs", slog.Error(err), slog.F("job_id", ws.LatestBuild.Job.ID.String()))
163+
} else {
164+
defer closer.Close()
165+
for log := range buildLogCh {
166+
w.BuildLogs = append(w.BuildLogs, log)
167+
}
168+
}
169+
170+
if len(w.Workspace.LatestBuild.Resources) == 0 {
171+
log.Warn(ctx, "workspace build has no resources")
172+
return w
173+
}
174+
175+
agentLogCh, closer, err := client.WorkspaceAgentLogsAfter(ctx, agentID, 0, false)
176+
if err != nil {
177+
log.Error(ctx, "fetch agent startup logs", slog.Error(err), slog.F("agent_id", agentID.String()))
178+
} else {
179+
defer closer.Close()
180+
for logChunk := range agentLogCh {
181+
w.AgentStartupLogs = append(w.AgentStartupLogs, logChunk...)
182+
}
183+
}
184+
185+
return w
186+
}
187+
188+
// Run generates a support bundle with the given dependencies.
189+
func Run(ctx context.Context, d *Deps) (*Bundle, error) {
190+
var b Bundle
191+
if d.Client == nil {
192+
return nil, xerrors.Errorf("developer error: missing client!")
193+
}
194+
195+
authChecks := map[string]codersdk.AuthorizationCheck{
196+
"Read DeploymentValues": {
197+
Object: codersdk.AuthorizationObject{
198+
ResourceType: codersdk.ResourceDeploymentValues,
199+
},
200+
Action: string(rbac.ActionRead),
201+
},
202+
}
203+
204+
// Ensure we capture logs from the client.
205+
var logw strings.Builder
206+
d.Log.AppendSinks(sloghuman.Sink(&logw))
207+
d.Client.SetLogger(d.Log)
208+
defer func() {
209+
b.Logs = strings.Split(logw.String(), "\n")
210+
}()
211+
212+
authResp, err := d.Client.AuthCheck(ctx, codersdk.AuthorizationRequest{Checks: authChecks})
213+
if err != nil {
214+
return &b, xerrors.Errorf("check authorization: %w", err)
215+
}
216+
for k, v := range authResp {
217+
if !v {
218+
return &b, xerrors.Errorf("failed authorization check: cannot %s", k)
219+
}
220+
}
221+
222+
b.Deployment = DeploymentInfo(ctx, d.Client, d.Log)
223+
b.Workspace = WorkspaceInfo(ctx, d.Client, d.Log, d.WorkspaceID, d.AgentID)
224+
b.Network = NetworkInfo(ctx, d.Client, d.Log, d.AgentID)
225+
226+
return &b, nil
227+
}

0 commit comments

Comments
 (0)