2024-03-01 17:13:50 +00:00
package cli
import (
"archive/zip"
"bytes"
2024-03-07 14:43:46 +00:00
"encoding/base64"
2024-03-01 17:13:50 +00:00
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"text/tabwriter"
"time"
"golang.org/x/xerrors"
"cdr.dev/slog"
"cdr.dev/slog/sloggers/sloghuman"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/support"
2024-03-15 16:24:38 +00:00
"github.com/coder/serpent"
2024-03-01 17:13:50 +00:00
)
2024-03-15 16:24:38 +00:00
func ( r * RootCmd ) support ( ) * serpent . Cmd {
supportCmd := & serpent . Cmd {
2024-03-01 17:13:50 +00:00
Use : "support" ,
Short : "Commands for troubleshooting issues with a Coder deployment." ,
2024-03-15 16:24:38 +00:00
Handler : func ( inv * serpent . Invocation ) error {
2024-03-01 17:13:50 +00:00
return inv . Command . HelpHandler ( inv )
} ,
Hidden : true , // TODO: un-hide once the must-haves from #12160 are completed.
2024-03-15 16:24:38 +00:00
Children : [ ] * serpent . Cmd {
2024-03-01 17:13:50 +00:00
r . supportBundle ( ) ,
} ,
}
return supportCmd
}
2024-03-15 16:24:38 +00:00
func ( r * RootCmd ) supportBundle ( ) * serpent . Cmd {
2024-03-01 17:13:50 +00:00
var outputPath string
client := new ( codersdk . Client )
2024-03-15 16:24:38 +00:00
cmd := & serpent . Cmd {
2024-03-01 17:13:50 +00:00
Use : "bundle <workspace> [<agent>]" ,
Short : "Generate a support bundle to troubleshoot issues connecting to a workspace." ,
Long : ` This command generates a file containing detailed troubleshooting information about the Coder deployment and workspace connections. You must specify a single workspace (and optionally an agent name). ` ,
2024-03-15 16:24:38 +00:00
Middleware : serpent . Chain (
serpent . RequireRangeArgs ( 0 , 2 ) ,
2024-03-01 17:13:50 +00:00
r . InitClient ( client ) ,
) ,
2024-03-15 16:24:38 +00:00
Handler : func ( inv * serpent . Invocation ) error {
2024-03-01 17:13:50 +00:00
var (
log = slog . Make ( sloghuman . Sink ( inv . Stderr ) ) .
Leveled ( slog . LevelDebug )
deps = support . Deps {
Client : client ,
Log : log ,
}
)
if len ( inv . Args ) == 0 {
return xerrors . Errorf ( "must specify workspace name" )
}
ws , err := namedWorkspace ( inv . Context ( ) , client , inv . Args [ 0 ] )
if err != nil {
return xerrors . Errorf ( "invalid workspace: %w" , err )
}
deps . WorkspaceID = ws . ID
agentName := ""
if len ( inv . Args ) > 1 {
agentName = inv . Args [ 1 ]
}
agt , found := findAgent ( agentName , ws . LatestBuild . Resources )
if ! found {
return xerrors . Errorf ( "could not find agent named %q for workspace" , agentName )
}
deps . AgentID = agt . ID
if outputPath == "" {
cwd , err := filepath . Abs ( "." )
if err != nil {
return xerrors . Errorf ( "could not determine current working directory: %w" , err )
}
fname := fmt . Sprintf ( "coder-support-%d.zip" , time . Now ( ) . Unix ( ) )
outputPath = filepath . Join ( cwd , fname )
}
w , err := os . Create ( outputPath )
if err != nil {
return xerrors . Errorf ( "create output file: %w" , err )
}
zwr := zip . NewWriter ( w )
defer zwr . Close ( )
bun , err := support . Run ( inv . Context ( ) , & deps )
if err != nil {
_ = os . Remove ( outputPath ) // best effort
return xerrors . Errorf ( "create support bundle: %w" , err )
}
if err := writeBundle ( bun , zwr ) ; err != nil {
_ = os . Remove ( outputPath ) // best effort
return xerrors . Errorf ( "write support bundle to %s: %w" , outputPath , err )
}
return nil
} ,
}
2024-03-15 16:24:38 +00:00
cmd . Options = serpent . OptionSet {
2024-03-01 17:13:50 +00:00
{
Flag : "output" ,
FlagShorthand : "o" ,
Env : "CODER_SUPPORT_BUNDLE_OUTPUT" ,
Description : "File path for writing the generated support bundle. Defaults to coder-support-$(date +%s).zip." ,
2024-03-15 16:24:38 +00:00
Value : serpent . StringOf ( & outputPath ) ,
2024-03-01 17:13:50 +00:00
} ,
}
return cmd
}
func findAgent ( agentName string , haystack [ ] codersdk . WorkspaceResource ) ( * codersdk . WorkspaceAgent , bool ) {
for _ , res := range haystack {
for _ , agt := range res . Agents {
if agentName == "" {
// just return the first
return & agt , true
}
if agt . Name == agentName {
return & agt , true
}
}
}
return nil , false
}
func writeBundle ( src * support . Bundle , dest * zip . Writer ) error {
2024-03-15 11:01:39 +00:00
// We JSON-encode the following:
2024-03-01 17:13:50 +00:00
for k , v := range map [ string ] any {
2024-03-07 14:43:46 +00:00
"deployment/buildinfo.json" : src . Deployment . BuildInfo ,
"deployment/config.json" : src . Deployment . Config ,
"deployment/experiments.json" : src . Deployment . Experiments ,
"deployment/health.json" : src . Deployment . HealthReport ,
2024-03-15 11:01:39 +00:00
"network/netcheck.json" : src . Network . Netcheck ,
2024-03-07 14:43:46 +00:00
"workspace/workspace.json" : src . Workspace . Workspace ,
2024-03-15 11:01:39 +00:00
"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 ,
2024-03-07 14:43:46 +00:00
"workspace/template.json" : src . Workspace . Template ,
"workspace/template_version.json" : src . Workspace . TemplateVersion ,
"workspace/parameters.json" : src . Workspace . Parameters ,
2024-03-01 17:13:50 +00:00
} {
f , err := dest . Create ( k )
if err != nil {
return xerrors . Errorf ( "create file %q in archive: %w" , k , err )
}
enc := json . NewEncoder ( f )
enc . SetIndent ( "" , " " )
if err := enc . Encode ( v ) ; err != nil {
return xerrors . Errorf ( "write json to %q: %w" , k , err )
}
}
2024-03-07 14:43:46 +00:00
templateVersionBytes , err := base64 . StdEncoding . DecodeString ( src . Workspace . TemplateFileBase64 )
if err != nil {
return xerrors . Errorf ( "decode template zip from base64" )
}
2024-03-15 11:01:39 +00:00
// The below we just write as we have them:
2024-03-01 17:13:50 +00:00
for k , v := range map [ string ] string {
2024-03-15 11:01:39 +00:00
"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 ) ,
2024-03-15 15:33:49 +00:00
"agent/agent_magicsock.html" : string ( src . Agent . AgentMagicsockHTML ) ,
"agent/client_magicsock.html" : string ( src . Agent . ClientMagicsockHTML ) ,
2024-03-15 11:01:39 +00:00
"agent/startup_logs.txt" : humanizeAgentLogs ( src . Agent . StartupLogs ) ,
2024-03-15 15:33:49 +00:00
"agent/prometheus.txt" : string ( src . Agent . Prometheus ) ,
2024-03-15 11:01:39 +00:00
"workspace/template_file.zip" : string ( templateVersionBytes ) ,
"logs.txt" : strings . Join ( src . Logs , "\n" ) ,
2024-03-01 17:13:50 +00:00
} {
f , err := dest . Create ( k )
if err != nil {
return xerrors . Errorf ( "create file %q in archive: %w" , k , err )
}
if _ , err := f . Write ( [ ] byte ( v ) ) ; err != nil {
return xerrors . Errorf ( "write file %q in archive: %w" , k , err )
}
}
if err := dest . Close ( ) ; err != nil {
return xerrors . Errorf ( "close zip file: %w" , err )
}
return nil
}
func humanizeAgentLogs ( ls [ ] codersdk . WorkspaceAgentLog ) string {
var buf bytes . Buffer
tw := tabwriter . NewWriter ( & buf , 0 , 2 , 1 , ' ' , 0 )
for _ , l := range ls {
_ , _ = fmt . Fprintf ( tw , "%s\t[%s]\t%s\n" ,
l . CreatedAt . Format ( "2006-01-02 15:04:05.000" ) , // for consistency with slog
string ( l . Level ) ,
l . Output ,
)
}
_ = tw . Flush ( )
return buf . String ( )
}
func humanizeBuildLogs ( ls [ ] codersdk . ProvisionerJobLog ) string {
var buf bytes . Buffer
tw := tabwriter . NewWriter ( & buf , 0 , 2 , 1 , ' ' , 0 )
for _ , l := range ls {
_ , _ = fmt . Fprintf ( tw , "%s\t[%s]\t%s\t%s\t%s\n" ,
l . CreatedAt . Format ( "2006-01-02 15:04:05.000" ) , // for consistency with slog
string ( l . Level ) ,
string ( l . Source ) ,
l . Stage ,
l . Output ,
)
}
_ = tw . Flush ( )
return buf . String ( )
}