Files
coder/support/support.go
T
2024-03-12 15:23:11 +00:00

364 lines
9.6 KiB
Go

package support
import (
"context"
"encoding/base64"
"io"
"net/http"
"strings"
"golang.org/x/sync/errgroup"
"golang.org/x/xerrors"
"github.com/google/uuid"
"cdr.dev/slog"
"cdr.dev/slog/sloggers/sloghuman"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/codersdk"
)
// Bundle is a set of information discovered about a deployment.
// Even though we do attempt to sanitize data, it may still contain
// sensitive information and should thus be treated as secret.
type Bundle struct {
Deployment Deployment `json:"deployment"`
Network Network `json:"network"`
Workspace Workspace `json:"workspace"`
Logs []string `json:"logs"`
}
type Deployment struct {
BuildInfo *codersdk.BuildInfoResponse `json:"build"`
Config *codersdk.DeploymentConfig `json:"config"`
Experiments codersdk.Experiments `json:"experiments"`
HealthReport *codersdk.HealthcheckReport `json:"health_report"`
}
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"`
}
type Workspace struct {
Workspace codersdk.Workspace `json:"workspace"`
Parameters []codersdk.WorkspaceBuildParameter `json:"parameters"`
Template codersdk.Template `json:"template"`
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"`
}
// Deps is a set of dependencies for discovering information
type Deps struct {
// Source from which to obtain information.
Client *codersdk.Client
// Log is where to log any informational or warning messages.
Log slog.Logger
// WorkspaceID is the optional workspace against which to run connection tests.
WorkspaceID uuid.UUID
// AgentID is the optional agent ID against which to run connection tests.
// Defaults to the first agent of the workspace, if not specified.
AgentID uuid.UUID
}
func DeploymentInfo(ctx context.Context, client *codersdk.Client, log slog.Logger) Deployment {
// Note: each goroutine assigns to a different struct field, hence no mutex.
var (
d Deployment
eg errgroup.Group
)
eg.Go(func() error {
bi, err := client.BuildInfo(ctx)
if err != nil {
return xerrors.Errorf("fetch build info: %w", err)
}
d.BuildInfo = &bi
return nil
})
eg.Go(func() error {
dc, err := client.DeploymentConfig(ctx)
if err != nil {
return xerrors.Errorf("fetch deployment config: %w", err)
}
d.Config = dc
return nil
})
eg.Go(func() error {
hr, err := client.DebugHealth(ctx)
if err != nil {
return xerrors.Errorf("fetch health report: %w", err)
}
d.HealthReport = &hr
return nil
})
eg.Go(func() error {
exp, err := client.Experiments(ctx)
if err != nil {
return xerrors.Errorf("fetch experiments: %w", err)
}
d.Experiments = exp
return nil
})
if err := eg.Wait(); err != nil {
log.Error(ctx, "fetch deployment information", slog.Error(err))
}
return d
}
func NetworkInfo(ctx context.Context, client *codersdk.Client, log slog.Logger, agentID uuid.UUID) Network {
var (
n Network
eg errgroup.Group
)
eg.Go(func() error {
coordResp, err := client.Request(ctx, http.MethodGet, "/api/v2/debug/coordinator", nil)
if err != nil {
return xerrors.Errorf("fetch coordinator debug page: %w", err)
}
defer coordResp.Body.Close()
bs, err := io.ReadAll(coordResp.Body)
if err != nil {
return xerrors.Errorf("read coordinator debug page: %w", err)
}
n.CoordinatorDebug = string(bs)
return nil
})
eg.Go(func() error {
tailResp, err := client.Request(ctx, http.MethodGet, "/api/v2/debug/tailnet", nil)
if err != nil {
return xerrors.Errorf("fetch tailnet debug page: %w", err)
}
defer tailResp.Body.Close()
bs, err := io.ReadAll(tailResp.Body)
if err != nil {
return xerrors.Errorf("read tailnet debug page: %w", err)
}
n.TailnetDebug = string(bs)
return nil
})
eg.Go(func() error {
if agentID == uuid.Nil {
log.Warn(ctx, "agent id required for agent connection info")
return nil
}
connInfo, err := client.WorkspaceAgentConnectionInfo(ctx, agentID)
if err != nil {
return xerrors.Errorf("fetch agent conn info: %w", err)
}
n.NetcheckLocal = &connInfo
return nil
})
if err := eg.Wait(); err != nil {
log.Error(ctx, "fetch network information", slog.Error(err))
}
return n
}
func WorkspaceInfo(ctx context.Context, client *codersdk.Client, log slog.Logger, workspaceID, agentID uuid.UUID) Workspace {
var (
w Workspace
eg errgroup.Group
)
if workspaceID == uuid.Nil {
log.Error(ctx, "no workspace id specified")
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 {
log.Error(ctx, "fetch workspace", slog.Error(err), slog.F("workspace_id", workspaceID))
return w
}
for _, res := range ws.LatestBuild.Resources {
for _, agt := range res.Agents {
sanitizeEnv(agt.EnvironmentVariables)
}
}
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 {
return xerrors.Errorf("fetch provisioner job logs: %w", err)
}
defer closer.Close()
var logs []codersdk.ProvisionerJobLog
for log := range buildLogCh {
logs = append(w.BuildLogs, log)
}
w.BuildLogs = logs
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")
}
tv, err := client.TemplateVersion(ctx, w.Workspace.TemplateActiveVersionID)
if err != nil {
return xerrors.Errorf("fetch template active version id")
}
w.TemplateVersion = tv
if tv.Job.FileID == uuid.Nil {
return xerrors.Errorf("template file id is nil")
}
raw, ctype, err := client.DownloadWithFormat(ctx, tv.Job.FileID, codersdk.FormatZip)
if err != nil {
return err
}
if ctype != codersdk.ContentTypeZip {
return xerrors.Errorf("expected content-type %s, got %s", codersdk.ContentTypeZip, ctype)
}
b64encoded := base64.StdEncoding.EncodeToString(raw)
w.TemplateFileBase64 = b64encoded
return nil
})
eg.Go(func() error {
if w.Workspace.TemplateID == uuid.Nil {
return xerrors.Errorf("workspace has nil version id")
}
tpl, err := client.Template(ctx, w.Workspace.TemplateID)
if err != nil {
return xerrors.Errorf("fetch template")
}
w.Template = tpl
return nil
})
eg.Go(func() error {
if ws.LatestBuild.ID == uuid.Nil {
return xerrors.Errorf("workspace has nil latest build id")
}
params, err := client.WorkspaceBuildParameters(ctx, ws.LatestBuild.ID)
if err != nil {
return xerrors.Errorf("fetch workspace build parameters: %w", err)
}
w.Parameters = params
return nil
})
if err := eg.Wait(); err != nil {
log.Error(ctx, "fetch workspace information", slog.Error(err))
}
return w
}
// Run generates a support bundle with the given dependencies.
func Run(ctx context.Context, d *Deps) (*Bundle, error) {
var b Bundle
if d.Client == nil {
return nil, xerrors.Errorf("developer error: missing client!")
}
authChecks := map[string]codersdk.AuthorizationCheck{
"Read DeploymentValues": {
Object: codersdk.AuthorizationObject{
ResourceType: codersdk.ResourceDeploymentValues,
},
Action: string(rbac.ActionRead),
},
}
// Ensure we capture logs from the client.
var logw strings.Builder
d.Log = d.Log.AppendSinks(sloghuman.Sink(&logw))
d.Client.SetLogger(d.Log)
defer func() {
b.Logs = strings.Split(logw.String(), "\n")
}()
authResp, err := d.Client.AuthCheck(ctx, codersdk.AuthorizationRequest{Checks: authChecks})
if err != nil {
return &b, xerrors.Errorf("check authorization: %w", err)
}
for k, v := range authResp {
if !v {
return &b, xerrors.Errorf("failed authorization check: cannot %s", k)
}
}
var eg errgroup.Group
eg.Go(func() error {
di := DeploymentInfo(ctx, d.Client, d.Log)
b.Deployment = di
return nil
})
eg.Go(func() error {
wi := WorkspaceInfo(ctx, d.Client, d.Log, d.WorkspaceID, d.AgentID)
b.Workspace = wi
return nil
})
eg.Go(func() error {
ni := NetworkInfo(ctx, d.Client, d.Log, d.AgentID)
b.Network = ni
return nil
})
_ = eg.Wait()
return &b, nil
}
// sanitizeEnv modifies kvs in place and replaces the values all non-empty keys
// with the string ***REDACTED***
func sanitizeEnv(kvs map[string]string) {
for k, v := range kvs {
if v != "" {
kvs[k] = "***REDACTED***"
}
}
}