|
| 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 | +typeBundlestruct { |
| 23 | +DeploymentDeployment`json:"deployment"` |
| 24 | +NetworkNetwork`json:"network"` |
| 25 | +WorkspaceWorkspace`json:"workspace"` |
| 26 | +Logs []string`json:"logs"` |
| 27 | +} |
| 28 | + |
| 29 | +typeDeploymentstruct { |
| 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 | +typeNetworkstruct { |
| 37 | +CoordinatorDebugstring`json:"coordinator_debug"` |
| 38 | +TailnetDebugstring`json:"tailnet_debug"` |
| 39 | +NetcheckLocal*codersdk.WorkspaceAgentConnectionInfo`json:"netcheck_local"` |
| 40 | +NetcheckRemote*codersdk.WorkspaceAgentConnectionInfo`json:"netcheck_remote"` |
| 41 | +} |
| 42 | + |
| 43 | +typeWorkspacestruct { |
| 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 | +typeDepsstruct { |
| 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 | +funcDeploymentInfo(ctx context.Context,client*codersdk.Client,log slog.Logger)Deployment { |
| 64 | +vardDeployment |
| 65 | + |
| 66 | +bi,err:=client.BuildInfo(ctx) |
| 67 | +iferr!=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 | +iferr!=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 | +iferr!=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 | +iferr!=nil { |
| 89 | +log.Error(ctx,"fetch experiments",slog.Error(err)) |
| 90 | +}else { |
| 91 | +d.Experiments=exp |
| 92 | +} |
| 93 | + |
| 94 | +returnd |
| 95 | +} |
| 96 | + |
| 97 | +funcNetworkInfo(ctx context.Context,client*codersdk.Client,log slog.Logger,agentID uuid.UUID)Network { |
| 98 | +varnNetwork |
| 99 | + |
| 100 | +coordResp,err:=client.Request(ctx,http.MethodGet,"/api/v2/debug/coordinator",nil) |
| 101 | +iferr!=nil { |
| 102 | +log.Error(ctx,"fetch coordinator debug page",slog.Error(err)) |
| 103 | +}else { |
| 104 | +defercoordResp.Body.Close() |
| 105 | +bs,err:=io.ReadAll(coordResp.Body) |
| 106 | +iferr!=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 | +iferr!=nil { |
| 115 | +log.Error(ctx,"fetch tailnet debug page",slog.Error(err)) |
| 116 | +}else { |
| 117 | +defertailResp.Body.Close() |
| 118 | +bs,err:=io.ReadAll(tailResp.Body) |
| 119 | +iferr!=nil { |
| 120 | +log.Error(ctx,"read tailnet debug page",slog.Error(err)) |
| 121 | +}else { |
| 122 | +n.TailnetDebug=string(bs) |
| 123 | +} |
| 124 | +} |
| 125 | + |
| 126 | +ifagentID!=uuid.Nil { |
| 127 | +connInfo,err:=client.WorkspaceAgentConnectionInfo(ctx,agentID) |
| 128 | +iferr!=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 | +returnn |
| 138 | +} |
| 139 | + |
| 140 | +funcWorkspaceInfo(ctx context.Context,client*codersdk.Client,log slog.Logger,workspaceID,agentID uuid.UUID)Workspace { |
| 141 | +varwWorkspace |
| 142 | + |
| 143 | +ifworkspaceID==uuid.Nil { |
| 144 | +log.Error(ctx,"no workspace id specified") |
| 145 | +returnw |
| 146 | +} |
| 147 | + |
| 148 | +ifagentID==uuid.Nil { |
| 149 | +log.Error(ctx,"no agent id specified") |
| 150 | +} |
| 151 | + |
| 152 | +ws,err:=client.Workspace(ctx,workspaceID) |
| 153 | +iferr!=nil { |
| 154 | +log.Error(ctx,"fetch workspace",slog.Error(err),slog.F("workspace_id",workspaceID)) |
| 155 | +returnw |
| 156 | +} |
| 157 | + |
| 158 | +w.Workspace=ws |
| 159 | + |
| 160 | +buildLogCh,closer,err:=client.WorkspaceBuildLogsAfter(ctx,ws.LatestBuild.ID,0) |
| 161 | +iferr!=nil { |
| 162 | +log.Error(ctx,"fetch provisioner job logs",slog.Error(err),slog.F("job_id",ws.LatestBuild.Job.ID.String())) |
| 163 | +}else { |
| 164 | +defercloser.Close() |
| 165 | +forlog:=rangebuildLogCh { |
| 166 | +w.BuildLogs=append(w.BuildLogs,log) |
| 167 | +} |
| 168 | +} |
| 169 | + |
| 170 | +iflen(w.Workspace.LatestBuild.Resources)==0 { |
| 171 | +log.Warn(ctx,"workspace build has no resources") |
| 172 | +returnw |
| 173 | +} |
| 174 | + |
| 175 | +agentLogCh,closer,err:=client.WorkspaceAgentLogsAfter(ctx,agentID,0,false) |
| 176 | +iferr!=nil { |
| 177 | +log.Error(ctx,"fetch agent startup logs",slog.Error(err),slog.F("agent_id",agentID.String())) |
| 178 | +}else { |
| 179 | +defercloser.Close() |
| 180 | +forlogChunk:=rangeagentLogCh { |
| 181 | +w.AgentStartupLogs=append(w.AgentStartupLogs,logChunk...) |
| 182 | +} |
| 183 | +} |
| 184 | + |
| 185 | +returnw |
| 186 | +} |
| 187 | + |
| 188 | +// Run generates a support bundle with the given dependencies. |
| 189 | +funcRun(ctx context.Context,d*Deps) (*Bundle,error) { |
| 190 | +varbBundle |
| 191 | +ifd.Client==nil { |
| 192 | +returnnil,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 | +varlogw strings.Builder |
| 206 | +d.Log.AppendSinks(sloghuman.Sink(&logw)) |
| 207 | +d.Client.SetLogger(d.Log) |
| 208 | +deferfunc() { |
| 209 | +b.Logs=strings.Split(logw.String(),"\n") |
| 210 | +}() |
| 211 | + |
| 212 | +authResp,err:=d.Client.AuthCheck(ctx, codersdk.AuthorizationRequest{Checks:authChecks}) |
| 213 | +iferr!=nil { |
| 214 | +return&b,xerrors.Errorf("check authorization: %w",err) |
| 215 | +} |
| 216 | +fork,v:=rangeauthResp { |
| 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 | +} |