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"
2024-03-21 17:06:28 +00:00
"net/url"
2024-03-01 17:13:50 +00:00
"os"
"path/filepath"
"strings"
"text/tabwriter"
"time"
2024-04-11 09:09:10 +00:00
"github.com/google/uuid"
2024-03-01 17:13:50 +00:00
"golang.org/x/xerrors"
"cdr.dev/slog"
"cdr.dev/slog/sloggers/sloghuman"
2024-03-21 17:06:28 +00:00
"github.com/coder/coder/v2/cli/cliui"
2024-03-01 17:13:50 +00:00
"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-17 14:45:26 +00:00
func ( r * RootCmd ) support ( ) * serpent . Command {
supportCmd := & serpent . Command {
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 )
} ,
2024-03-17 14:45:26 +00:00
Children : [ ] * serpent . Command {
2024-03-01 17:13:50 +00:00
r . supportBundle ( ) ,
} ,
}
return supportCmd
}
2024-03-21 17:06:28 +00:00
var supportBundleBlurb = cliui . Bold ( "This will collect the following information:\n" ) +
` - Coder deployment version
2024-03-25 15:14:27 +00:00
- Coder deployment Configuration ( sanitized ) , including enabled experiments
2024-03-21 17:06:28 +00:00
- Coder deployment health snapshot
- Coder deployment Network troubleshooting information
- Workspace configuration , parameters , and build logs
- Template version and source code for the given workspace
- Agent details ( with environment variable sanitized )
- Agent network diagnostics
- Agent logs
` + cliui . Bold ( "Note: " ) +
2024-03-25 16:51:48 +00:00
cliui . Wrap ( "While we try to sanitize sensitive data from support bundles, we cannot guarantee that they do not contain information that you or your organization may consider sensitive.\n" ) +
2024-03-21 17:06:28 +00:00
cliui . Bold ( "Please confirm that you will:\n" ) +
" - Review the support bundle before distribution\n" +
" - Only distribute it via trusted channels\n" +
cliui . Bold ( "Continue? " )
2024-03-17 14:45:26 +00:00
func ( r * RootCmd ) supportBundle ( ) * serpent . Command {
2024-03-01 17:13:50 +00:00
var outputPath string
2024-03-21 17:06:28 +00:00
var coderURLOverride string
2024-03-01 17:13:50 +00:00
client := new ( codersdk . Client )
2024-03-17 14:45:26 +00:00
cmd := & serpent . Command {
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-21 17:06:28 +00:00
var cliLogBuf bytes . Buffer
cliLogW := sloghuman . Sink ( & cliLogBuf )
cliLog := slog . Make ( cliLogW ) . Leveled ( slog . LevelDebug )
if r . verbose {
cliLog = cliLog . AppendSinks ( sloghuman . Sink ( inv . Stderr ) )
}
ans , err := cliui . Prompt ( inv , cliui . PromptOptions {
Text : supportBundleBlurb ,
Secret : false ,
IsConfirm : true ,
} )
if err != nil || ans != cliui . ConfirmYes {
return err
}
if skip , _ := inv . ParsedFlags ( ) . GetBool ( "yes" ) ; skip {
cliLog . Debug ( inv . Context ( ) , "user auto-confirmed" )
} else {
cliLog . Debug ( inv . Context ( ) , "user confirmed manually" , slog . F ( "answer" , ans ) )
}
vi := defaultVersionInfo ( )
cliLog . Debug ( inv . Context ( ) , "version info" ,
slog . F ( "version" , vi . Version ) ,
slog . F ( "build_time" , vi . BuildTime ) ,
slog . F ( "external_url" , vi . ExternalURL ) ,
slog . F ( "slim" , vi . Slim ) ,
slog . F ( "agpl" , vi . AGPL ) ,
slog . F ( "boring_crypto" , vi . BoringCrypto ) ,
2024-03-01 17:13:50 +00:00
)
2024-03-21 17:06:28 +00:00
cliLog . Debug ( inv . Context ( ) , "invocation" , slog . F ( "args" , strings . Join ( os . Args , " " ) ) )
// Check if we're running inside a workspace
if val , found := os . LookupEnv ( "CODER" ) ; found && val == "true" {
2024-04-15 16:10:49 +00:00
cliui . Warn ( inv . Stderr , "Running inside Coder workspace; this can affect results!" )
2024-03-21 17:06:28 +00:00
cliLog . Debug ( inv . Context ( ) , "running inside coder workspace" )
}
if coderURLOverride != "" && coderURLOverride != client . URL . String ( ) {
u , err := url . Parse ( coderURLOverride )
if err != nil {
return xerrors . Errorf ( "invalid value for Coder URL override: %w" , err )
}
_ , _ = fmt . Fprintf ( inv . Stderr , "Overrode Coder URL to %q; this can affect results!\n" , coderURLOverride )
cliLog . Debug ( inv . Context ( ) , "coder url overridden" , slog . F ( "url" , coderURLOverride ) )
client . URL = u
}
2024-03-01 17:13:50 +00:00
2024-04-11 09:09:10 +00:00
var (
wsID uuid . UUID
agtID uuid . UUID
2024-03-21 17:06:28 +00:00
)
2024-03-01 17:13:50 +00:00
2024-04-11 09:09:10 +00:00
if len ( inv . Args ) == 0 {
cliLog . Warn ( inv . Context ( ) , "no workspace specified" )
2024-04-15 16:10:49 +00:00
cliui . Warn ( inv . Stderr , "No workspace specified. This will result in incomplete information." )
2024-04-11 09:09:10 +00:00
} else {
ws , err := namedWorkspace ( inv . Context ( ) , client , inv . Args [ 0 ] )
if err != nil {
return xerrors . Errorf ( "invalid workspace: %w" , err )
}
cliLog . Debug ( inv . Context ( ) , "found workspace" ,
slog . F ( "workspace_name" , ws . Name ) ,
slog . F ( "workspace_id" , ws . ID ) ,
)
wsID = ws . ID
agentName := ""
if len ( inv . Args ) > 1 {
agentName = inv . Args [ 1 ]
}
2024-03-01 17:13:50 +00:00
2024-04-11 09:09:10 +00:00
agt , found := findAgent ( agentName , ws . LatestBuild . Resources )
if ! found {
cliLog . Warn ( inv . Context ( ) , "could not find agent in workspace" , slog . F ( "agent_name" , agentName ) )
} else {
cliLog . Debug ( inv . Context ( ) , "found workspace agent" ,
slog . F ( "agent_name" , agt . Name ) ,
slog . F ( "agent_id" , agt . ID ) ,
)
agtID = agt . ID
}
2024-03-01 17:13:50 +00:00
}
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 )
}
2024-03-21 17:06:28 +00:00
cliLog . Debug ( inv . Context ( ) , "output path" , slog . F ( "path" , outputPath ) )
2024-03-01 17:13:50 +00:00
w , err := os . Create ( outputPath )
if err != nil {
return xerrors . Errorf ( "create output file: %w" , err )
}
zwr := zip . NewWriter ( w )
defer zwr . Close ( )
2024-03-21 17:06:28 +00:00
clientLog := slog . Make ( ) . Leveled ( slog . LevelDebug )
if r . verbose {
clientLog . AppendSinks ( sloghuman . Sink ( inv . Stderr ) )
}
deps := support . Deps {
Client : client ,
// Support adds a sink so we don't need to supply one ourselves.
Log : clientLog ,
2024-04-11 09:09:10 +00:00
WorkspaceID : wsID ,
AgentID : agtID ,
2024-03-21 17:06:28 +00:00
}
2024-03-01 17:13:50 +00:00
bun , err := support . Run ( inv . Context ( ) , & deps )
if err != nil {
_ = os . Remove ( outputPath ) // best effort
return xerrors . Errorf ( "create support bundle: %w" , err )
}
2024-04-16 15:21:09 +00:00
docsURL := bun . Deployment . Config . Values . DocsURL . String ( )
deployHealthSummary := bun . Deployment . HealthReport . Summarize ( docsURL )
2024-04-16 12:31:56 +00:00
if len ( deployHealthSummary ) > 0 {
cliui . Warn ( inv . Stdout , "Deployment health issues detected:" , deployHealthSummary ... )
}
2024-04-16 15:21:09 +00:00
clientNetcheckSummary := bun . Network . Netcheck . Summarize ( "Client netcheck:" , docsURL )
2024-04-16 12:31:56 +00:00
if len ( clientNetcheckSummary ) > 0 {
cliui . Warn ( inv . Stdout , "Networking issues detected:" , deployHealthSummary ... )
}
2024-03-21 17:06:28 +00:00
bun . CLILogs = cliLogBuf . Bytes ( )
2024-03-01 17:13:50 +00:00
if err := writeBundle ( bun , zwr ) ; err != nil {
_ = os . Remove ( outputPath ) // best effort
return xerrors . Errorf ( "write support bundle to %s: %w" , outputPath , err )
}
2024-03-21 17:06:28 +00:00
_ , _ = fmt . Fprintln ( inv . Stderr , "Wrote support bundle to " + outputPath )
2024-04-15 16:10:49 +00:00
2024-03-01 17:13:50 +00:00
return nil
} ,
}
2024-03-15 16:24:38 +00:00
cmd . Options = serpent . OptionSet {
2024-03-21 17:06:28 +00:00
cliui . SkipPromptOption ( ) ,
2024-03-01 17:13:50 +00:00
{
2024-03-21 17:06:28 +00:00
Flag : "output-file" ,
FlagShorthand : "O" ,
Env : "CODER_SUPPORT_BUNDLE_OUTPUT_FILE" ,
2024-03-01 17:13:50 +00:00
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
} ,
2024-03-21 17:06:28 +00:00
{
Flag : "url-override" ,
Env : "CODER_SUPPORT_BUNDLE_URL_OVERRIDE" ,
Description : "Override the URL to your Coder deployment. This may be useful, for example, if you need to troubleshoot a specific Coder replica." ,
Value : serpent . StringOf ( & coderURLOverride ) ,
} ,
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-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-04-12 08:40:04 +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 ,
"network/connection_info.json" : src . Network . ConnectionInfo ,
"network/netcheck.json" : src . Network . Netcheck ,
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-04-12 08:40:04 +00:00
"workspace/workspace.json" : src . Workspace . Workspace ,
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
"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-21 17:06:28 +00:00
"cli_logs.txt" : string ( src . CLILogs ) ,
2024-04-12 08:40:04 +00:00
"logs.txt" : strings . Join ( src . Logs , "\n" ) ,
"network/coordinator_debug.html" : src . Network . CoordinatorDebug ,
"network/tailnet_debug.html" : src . Network . TailnetDebug ,
"workspace/build_logs.txt" : humanizeBuildLogs ( src . Workspace . BuildLogs ) ,
"workspace/template_file.zip" : string ( templateVersionBytes ) ,
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 ( )
}