2022-02-10 14:33:27 +00:00
package cli
import (
2023-08-14 20:12:17 +00:00
"bufio"
"bytes"
2022-09-08 13:59:28 +00:00
"context"
2023-05-24 16:08:03 +00:00
"encoding/base64"
"encoding/json"
2023-03-23 22:42:20 +00:00
"errors"
2022-05-19 22:35:59 +00:00
"fmt"
2022-10-17 13:43:30 +00:00
"io"
2022-09-12 21:22:05 +00:00
"net/http"
2022-02-10 14:33:27 +00:00
"net/url"
"os"
2023-08-14 20:12:17 +00:00
"os/exec"
2023-01-11 16:22:20 +00:00
"os/signal"
"path/filepath"
2022-12-14 16:36:28 +00:00
"runtime"
2024-03-20 16:53:32 +00:00
"runtime/trace"
2022-05-23 17:19:33 +00:00
"strings"
2024-03-25 19:01:42 +00:00
"sync"
2023-01-11 16:22:20 +00:00
"syscall"
2023-04-07 22:58:21 +00:00
"text/tabwriter"
2022-04-05 01:35:03 +00:00
"time"
2022-02-10 14:33:27 +00:00
"github.com/mattn/go-isatty"
2023-06-07 05:22:58 +00:00
"github.com/mitchellh/go-wordwrap"
2023-06-14 11:52:01 +00:00
"golang.org/x/exp/slices"
2024-03-25 19:01:42 +00:00
"golang.org/x/mod/semver"
2023-06-14 11:52:01 +00:00
"golang.org/x/xerrors"
2022-02-10 14:33:27 +00:00
2023-09-07 21:28:22 +00:00
"github.com/coder/pretty"
2023-08-18 18:55:43 +00:00
"github.com/coder/coder/v2/buildinfo"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/cli/config"
2023-09-01 15:41:22 +00:00
"github.com/coder/coder/v2/cli/gitauth"
2023-09-04 18:42:45 +00:00
"github.com/coder/coder/v2/cli/telemetry"
2023-08-18 18:55:43 +00:00
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/agentsdk"
2024-03-15 16:24:38 +00:00
"github.com/coder/serpent"
2022-02-10 14:33:27 +00:00
)
2022-02-21 18:47:08 +00:00
var (
2023-09-07 21:28:22 +00:00
Caret = pretty . Sprint ( cliui . DefaultStyles . Prompt , "" )
2022-05-09 22:42:02 +00:00
// Applied as annotations to workspace commands
// so they display in a separated "help" section.
workspaceCommand = map [ string ] string {
2022-07-12 20:24:53 +00:00
"workspaces" : "" ,
2022-05-09 22:42:02 +00:00
}
2022-02-21 18:47:08 +00:00
)
2022-02-10 14:33:27 +00:00
const (
2024-02-26 17:38:49 +00:00
varURL = "url"
varToken = "token"
varAgentToken = "agent-token"
varAgentTokenFile = "agent-token-file"
varAgentURL = "agent-url"
varHeader = "header"
varHeaderCommand = "header-command"
varNoOpen = "no-open"
varNoVersionCheck = "no-version-warning"
varNoFeatureWarning = "no-feature-warning"
varForceTty = "force-tty"
varVerbose = "verbose"
varOrganizationSelect = "organization"
varDisableDirect = "disable-direct-connections"
2024-03-11 14:14:19 +00:00
2024-03-25 19:01:42 +00:00
notLoggedInMessage = "You are not logged in. Try logging in using 'coder login <url>'."
2022-08-29 18:30:06 +00:00
envNoVersionCheck = "CODER_NO_VERSION_WARNING"
envNoFeatureWarning = "CODER_NO_FEATURE_WARNING"
2022-10-11 15:58:28 +00:00
envSessionToken = "CODER_SESSION_TOKEN"
2023-03-23 23:38:15 +00:00
//nolint:gosec
envAgentToken = "CODER_AGENT_TOKEN"
2023-10-05 20:41:05 +00:00
//nolint:gosec
envAgentTokenFile = "CODER_AGENT_TOKEN_FILE"
envURL = "CODER_URL"
2022-06-29 22:49:40 +00:00
)
2024-03-25 19:01:42 +00:00
func ( r * RootCmd ) CoreSubcommands ( ) [ ] * serpent . Command {
2022-11-02 18:30:00 +00:00
// Please re-sort this list alphabetically if you change it!
2024-03-17 14:45:26 +00:00
return [ ] * serpent . Command {
2023-03-23 22:42:20 +00:00
r . dotfiles ( ) ,
2023-10-09 23:04:35 +00:00
r . externalAuth ( ) ,
2023-03-23 22:42:20 +00:00
r . login ( ) ,
r . logout ( ) ,
2023-07-11 02:38:02 +00:00
r . netcheck ( ) ,
2023-03-23 22:42:20 +00:00
r . portForward ( ) ,
r . publickey ( ) ,
r . resetPassword ( ) ,
r . state ( ) ,
r . templates ( ) ,
r . tokens ( ) ,
2023-07-11 02:38:02 +00:00
r . users ( ) ,
2023-04-05 12:16:05 +00:00
r . version ( defaultVersionInfo ) ,
2024-02-26 16:03:49 +00:00
r . organizations ( ) ,
2023-03-23 22:42:20 +00:00
// Workspace Commands
2023-11-02 19:41:34 +00:00
r . autoupdate ( ) ,
2023-03-23 22:42:20 +00:00
r . configSSH ( ) ,
r . create ( ) ,
r . deleteWorkspace ( ) ,
2024-01-24 14:05:39 +00:00
r . favorite ( ) ,
2023-03-23 22:42:20 +00:00
r . list ( ) ,
2024-01-02 18:46:18 +00:00
r . open ( ) ,
2023-05-05 09:34:58 +00:00
r . ping ( ) ,
r . rename ( ) ,
2024-01-02 18:46:18 +00:00
r . restart ( ) ,
2023-03-23 22:42:20 +00:00
r . schedules ( ) ,
r . show ( ) ,
r . speedtest ( ) ,
r . ssh ( ) ,
r . start ( ) ,
2024-01-02 18:46:18 +00:00
r . stat ( ) ,
2023-03-23 22:42:20 +00:00
r . stop ( ) ,
2024-01-24 14:05:39 +00:00
r . unfavorite ( ) ,
2023-03-23 22:42:20 +00:00
r . update ( ) ,
// Hidden
r . gitssh ( ) ,
r . vscodeSSH ( ) ,
2023-05-05 09:34:58 +00:00
r . workspaceAgent ( ) ,
2023-07-07 08:10:14 +00:00
r . expCmd ( ) ,
2024-03-01 17:13:50 +00:00
r . support ( ) ,
2023-03-23 22:42:20 +00:00
}
}
2024-03-17 14:45:26 +00:00
func ( r * RootCmd ) AGPL ( ) [ ] * serpent . Command {
2024-03-25 19:01:42 +00:00
all := append ( r . CoreSubcommands ( ) , r . Server ( /* Do not import coderd here. */ nil ) )
2022-08-22 22:02:50 +00:00
return all
}
2024-03-25 19:01:42 +00:00
// RunWithSubcommands runs the root command with the given subcommands.
// It is abstracted to enable the Enterprise code to add commands.
func ( r * RootCmd ) RunWithSubcommands ( subcommands [ ] * serpent . Command ) {
2024-03-20 16:53:32 +00:00
// This configuration is not available as a standard option because we
// want to trace the entire program, including Options parsing.
goTraceFilePath , ok := os . LookupEnv ( "CODER_GO_TRACE" )
if ok {
traceFile , err := os . OpenFile ( goTraceFilePath , os . O_CREATE | os . O_TRUNC | os . O_WRONLY , 0 o644 )
if err != nil {
panic ( fmt . Sprintf ( "failed to open trace file: %v" , err ) )
}
defer traceFile . Close ( )
if err := trace . Start ( traceFile ) ; err != nil {
panic ( fmt . Sprintf ( "failed to start trace: %v" , err ) )
}
defer trace . Stop ( )
}
2023-03-23 22:42:20 +00:00
cmd , err := r . Command ( subcommands )
if err != nil {
panic ( err )
}
err = cmd . Invoke ( ) . WithOS ( ) . Run ( )
if err != nil {
2023-11-24 12:35:56 +00:00
code := 1
var exitErr * exitError
if errors . As ( err , & exitErr ) {
code = exitErr . code
err = exitErr . err
}
2023-03-23 22:42:20 +00:00
if errors . Is ( err , cliui . Canceled ) {
//nolint:revive
2023-11-24 12:35:56 +00:00
os . Exit ( code )
2023-03-23 22:42:20 +00:00
}
2024-04-01 14:19:26 +00:00
f := PrettyErrorFormatter { w : os . Stderr , verbose : r . verbose }
2023-11-24 12:35:56 +00:00
if err != nil {
2024-04-01 14:19:26 +00:00
f . Format ( err )
2023-11-24 12:35:56 +00:00
}
2023-03-23 22:42:20 +00:00
//nolint:revive
2023-11-24 12:35:56 +00:00
os . Exit ( code )
2023-03-23 22:42:20 +00:00
}
}
2022-10-25 00:46:24 +00:00
2024-03-17 14:45:26 +00:00
func ( r * RootCmd ) Command ( subcommands [ ] * serpent . Command ) ( * serpent . Command , error ) {
2022-10-24 16:17:48 +00:00
fmtLong := ` Coder % s — A tool for provisioning self - hosted development environments with Terraform .
`
2024-03-17 14:45:26 +00:00
cmd := & serpent . Command {
2023-03-23 22:42:20 +00:00
Use : "coder [global-flags] <subcommand>" ,
Long : fmt . Sprintf ( fmtLong , buildinfo . Version ( ) ) + formatExamples (
2022-07-11 16:08:09 +00:00
example {
Description : "Start a Coder server" ,
Command : "coder server" ,
} ,
example {
Description : "Get started by creating a template from an example" ,
Command : "coder templates init" ,
} ,
) ,
2024-03-15 16:24:38 +00:00
Handler : func ( i * serpent . Invocation ) error {
2023-10-04 20:37:15 +00:00
if r . versionFlag {
return r . version ( defaultVersionInfo ) . Handler ( i )
}
2023-03-23 22:42:20 +00:00
// The GIT_ASKPASS environment variable must point at
// a binary with no arguments. To prevent writing
// cross-platform scripts to invoke the Coder binary
// with a `gitaskpass` subcommand, we override the entrypoint
// to check if the command was invoked.
if gitauth . CheckCommand ( i . Args , i . Environ . ToOS ( ) ) {
return r . gitAskpass ( ) . Handler ( i )
}
return i . Command . HelpHandler ( i )
} ,
}
cmd . AddSubcommands ( subcommands ... )
// Set default help handler for all commands.
2024-03-17 14:45:26 +00:00
cmd . Walk ( func ( c * serpent . Command ) {
2023-03-23 22:42:20 +00:00
if c . HelpHandler == nil {
c . HelpHandler = helpFn ( )
}
} )
var merr error
// Add [flags] to usage when appropriate.
2024-03-17 14:45:26 +00:00
cmd . Walk ( func ( cmd * serpent . Command ) {
2023-03-23 22:42:20 +00:00
const flags = "[flags]"
if strings . Contains ( cmd . Use , flags ) {
merr = errors . Join (
merr ,
xerrors . Errorf (
"command %q shouldn't have %q in usage since it's automatically populated" ,
cmd . FullUsage ( ) ,
flags ,
) ,
)
return
}
var hasFlag bool
for _ , opt := range cmd . Options {
if opt . Flag != "" {
hasFlag = true
break
}
}
if ! hasFlag {
return
}
// We insert [flags] between the command's name and its arguments.
tokens := strings . SplitN ( cmd . Use , " " , 2 )
if len ( tokens ) == 1 {
cmd . Use = fmt . Sprintf ( "%s %s" , tokens [ 0 ] , flags )
return
}
cmd . Use = fmt . Sprintf ( "%s %s %s" , tokens [ 0 ] , flags , tokens [ 1 ] )
} )
// Add alises when appropriate.
2024-03-17 14:45:26 +00:00
cmd . Walk ( func ( cmd * serpent . Command ) {
2023-03-23 22:42:20 +00:00
// TODO: we should really be consistent about naming.
if cmd . Name ( ) == "delete" || cmd . Name ( ) == "remove" {
if slices . Contains ( cmd . Aliases , "rm" ) {
merr = errors . Join (
merr ,
xerrors . Errorf ( "command %q shouldn't have alias %q since it's added automatically" , cmd . FullName ( ) , "rm" ) ,
)
return
}
cmd . Aliases = append ( cmd . Aliases , "rm" )
}
} )
// Sanity-check command options.
2024-03-17 14:45:26 +00:00
cmd . Walk ( func ( cmd * serpent . Command ) {
2023-03-23 22:42:20 +00:00
for _ , opt := range cmd . Options {
// Verify that every option is configurable.
if opt . Flag == "" && opt . Env == "" {
if cmd . Name ( ) == "server" {
// The server command is funky and has YAML-only options, e.g.
// support links.
return
}
merr = errors . Join (
merr ,
xerrors . Errorf ( "option %q in %q should have a flag or env" , opt . Name , cmd . FullName ( ) ) ,
)
}
}
} )
if merr != nil {
return nil , merr
2022-02-10 14:33:27 +00:00
}
2023-04-07 22:58:21 +00:00
var debugOptions bool
// Add a wrapper to every command to enable debugging options.
2024-03-17 14:45:26 +00:00
cmd . Walk ( func ( cmd * serpent . Command ) {
2023-04-07 22:58:21 +00:00
h := cmd . Handler
2023-07-05 17:20:12 +00:00
if h == nil {
// We should never have a nil handler, but if we do, do not
// wrap it. Wrapping it just hides a nil pointer dereference.
// If a nil handler exists, this is a developer bug. If no handler
// is required for a command such as command grouping (e.g. `users'
// and 'groups'), then the handler should be set to the helper
// function.
2024-03-15 16:24:38 +00:00
// func(inv *serpent.Invocation) error {
2023-07-05 17:20:12 +00:00
// return inv.Command.HelpHandler(inv)
// }
return
}
2024-03-15 16:24:38 +00:00
cmd . Handler = func ( i * serpent . Invocation ) error {
2023-04-07 22:58:21 +00:00
if ! debugOptions {
return h ( i )
}
tw := tabwriter . NewWriter ( i . Stdout , 0 , 0 , 4 , ' ' , 0 )
_ , _ = fmt . Fprintf ( tw , "Option\tValue Source\n" )
for _ , opt := range cmd . Options {
_ , _ = fmt . Fprintf ( tw , "%q\t%v\n" , opt . Name , opt . ValueSource )
}
tw . Flush ( )
return nil
}
} )
2023-03-23 22:42:20 +00:00
if r . agentURL == nil {
r . agentURL = new ( url . URL )
}
if r . clientURL == nil {
r . clientURL = new ( url . URL )
}
2022-03-22 19:17:50 +00:00
2024-03-15 16:24:38 +00:00
globalGroup := & serpent . Group {
2023-03-23 22:42:20 +00:00
Name : "Global" ,
Description : ` Global options are applied to all commands. They can be set using environment variables or flags. ` ,
}
2024-03-15 16:24:38 +00:00
cmd . Options = serpent . OptionSet {
2023-03-23 22:42:20 +00:00
{
Flag : varURL ,
Env : envURL ,
Description : "URL to a deployment." ,
2024-03-15 16:24:38 +00:00
Value : serpent . URLOf ( r . clientURL ) ,
2023-03-23 22:42:20 +00:00
Group : globalGroup ,
} ,
2023-04-07 22:58:21 +00:00
{
Flag : "debug-options" ,
Description : "Print all options, how they're set, then exit." ,
2024-03-15 16:24:38 +00:00
Value : serpent . BoolOf ( & debugOptions ) ,
2023-04-07 22:58:21 +00:00
Group : globalGroup ,
} ,
2023-03-23 22:42:20 +00:00
{
Flag : varToken ,
Env : envSessionToken ,
Description : fmt . Sprintf ( "Specify an authentication token. For security reasons setting %s is preferred." , envSessionToken ) ,
2024-03-15 16:24:38 +00:00
Value : serpent . StringOf ( & r . token ) ,
2023-03-23 22:42:20 +00:00
Group : globalGroup ,
} ,
{
Flag : varAgentToken ,
2023-03-23 23:38:15 +00:00
Env : envAgentToken ,
2023-03-23 22:42:20 +00:00
Description : "An agent authentication token." ,
2024-03-15 16:24:38 +00:00
Value : serpent . StringOf ( & r . agentToken ) ,
2023-03-23 22:42:20 +00:00
Hidden : true ,
Group : globalGroup ,
} ,
2023-10-05 20:41:05 +00:00
{
Flag : varAgentTokenFile ,
Env : envAgentTokenFile ,
Description : "A file containing an agent authentication token." ,
2024-03-15 16:24:38 +00:00
Value : serpent . StringOf ( & r . agentTokenFile ) ,
2023-10-05 20:41:05 +00:00
Hidden : true ,
Group : globalGroup ,
} ,
2023-03-23 22:42:20 +00:00
{
Flag : varAgentURL ,
Env : "CODER_AGENT_URL" ,
Description : "URL for an agent to access your deployment." ,
2024-03-15 16:24:38 +00:00
Value : serpent . URLOf ( r . agentURL ) ,
2023-03-23 22:42:20 +00:00
Hidden : true ,
Group : globalGroup ,
} ,
{
Flag : varNoVersionCheck ,
Env : envNoVersionCheck ,
Description : "Suppress warning when client and server versions do not match." ,
2024-03-15 16:24:38 +00:00
Value : serpent . BoolOf ( & r . noVersionCheck ) ,
2023-03-23 22:42:20 +00:00
Group : globalGroup ,
} ,
{
Flag : varNoFeatureWarning ,
Env : envNoFeatureWarning ,
Description : "Suppress warnings about unlicensed features." ,
2024-03-15 16:24:38 +00:00
Value : serpent . BoolOf ( & r . noFeatureWarning ) ,
2023-03-23 22:42:20 +00:00
Group : globalGroup ,
} ,
{
Flag : varHeader ,
Env : "CODER_HEADER" ,
Description : "Additional HTTP headers added to all requests. Provide as " + ` key=value ` + ". Can be specified multiple times." ,
2024-03-15 16:24:38 +00:00
Value : serpent . StringArrayOf ( & r . header ) ,
2023-03-23 22:42:20 +00:00
Group : globalGroup ,
} ,
2023-08-14 20:12:17 +00:00
{
Flag : varHeaderCommand ,
Env : "CODER_HEADER_COMMAND" ,
Description : "An external command that outputs additional HTTP headers added to all requests. The command must output each header as `key=value` on its own line." ,
2024-03-15 16:24:38 +00:00
Value : serpent . StringOf ( & r . headerCommand ) ,
2023-08-14 20:12:17 +00:00
Group : globalGroup ,
} ,
2023-03-23 22:42:20 +00:00
{
Flag : varNoOpen ,
Env : "CODER_NO_OPEN" ,
Description : "Suppress opening the browser after logging in." ,
2024-03-15 16:24:38 +00:00
Value : serpent . BoolOf ( & r . noOpen ) ,
2023-03-23 22:42:20 +00:00
Hidden : true ,
Group : globalGroup ,
} ,
{
Flag : varForceTty ,
Env : "CODER_FORCE_TTY" ,
Hidden : true ,
Description : "Force the use of a TTY." ,
2024-03-15 16:24:38 +00:00
Value : serpent . BoolOf ( & r . forceTTY ) ,
2023-03-23 22:42:20 +00:00
Group : globalGroup ,
} ,
{
Flag : varVerbose ,
FlagShorthand : "v" ,
Env : "CODER_VERBOSE" ,
Description : "Enable verbose output." ,
2024-03-15 16:24:38 +00:00
Value : serpent . BoolOf ( & r . verbose ) ,
2023-03-23 22:42:20 +00:00
Group : globalGroup ,
} ,
2023-06-21 20:22:43 +00:00
{
Flag : varDisableDirect ,
Env : "CODER_DISABLE_DIRECT_CONNECTIONS" ,
Description : "Disable direct (P2P) connections to workspaces." ,
2024-03-15 16:24:38 +00:00
Value : serpent . BoolOf ( & r . disableDirect ) ,
2023-06-21 20:22:43 +00:00
Group : globalGroup ,
} ,
2023-04-19 16:07:53 +00:00
{
Flag : "debug-http" ,
Description : "Debug codersdk HTTP requests." ,
2024-03-15 16:24:38 +00:00
Value : serpent . BoolOf ( & r . debugHTTP ) ,
2023-04-19 16:07:53 +00:00
Group : globalGroup ,
Hidden : true ,
} ,
2023-03-23 22:42:20 +00:00
{
Flag : config . FlagName ,
Env : "CODER_CONFIG_DIR" ,
Description : "Path to the global `coder` config directory." ,
Default : config . DefaultDir ( ) ,
2024-03-15 16:24:38 +00:00
Value : serpent . StringOf ( & r . globalConfig ) ,
2023-03-23 22:42:20 +00:00
Group : globalGroup ,
} ,
2024-02-26 17:38:49 +00:00
{
Flag : varOrganizationSelect ,
FlagShorthand : "z" ,
Env : "CODER_ORGANIZATION" ,
Description : "Select which organization (uuid or name) to use This overrides what is present in the config file." ,
2024-03-15 16:24:38 +00:00
Value : serpent . StringOf ( & r . organizationSelect ) ,
2024-03-18 14:09:26 +00:00
Hidden : true ,
2024-02-26 17:38:49 +00:00
Group : globalGroup ,
} ,
2023-10-04 20:37:15 +00:00
{
Flag : "version" ,
// This was requested by a customer to assist with their migration.
// They have two Coder CLIs, and want to tell the difference by running
// the same base command.
Description : "Run the version command. Useful for v1 customers migrating to v2." ,
2024-03-15 16:24:38 +00:00
Value : serpent . BoolOf ( & r . versionFlag ) ,
2023-10-04 20:37:15 +00:00
Hidden : true ,
} ,
2023-03-23 22:42:20 +00:00
}
2022-05-09 22:42:02 +00:00
2023-03-23 22:42:20 +00:00
return cmd , nil
2022-02-10 14:33:27 +00:00
}
2023-03-23 22:42:20 +00:00
// RootCmd contains parameters and helpers useful to all commands.
type RootCmd struct {
2024-02-26 17:38:49 +00:00
clientURL * url . URL
token string
globalConfig string
header [ ] string
headerCommand string
agentToken string
agentTokenFile string
agentURL * url . URL
forceTTY bool
noOpen bool
verbose bool
organizationSelect string
versionFlag bool
disableDirect bool
debugHTTP bool
2023-03-23 22:42:20 +00:00
noVersionCheck bool
noFeatureWarning bool
}
2024-03-25 19:01:42 +00:00
// InitClient authenticates the client with files from disk
// and injects header middlewares for telemetry, authentication,
// and version checks.
2024-03-15 16:24:38 +00:00
func ( r * RootCmd ) InitClient ( client * codersdk . Client ) serpent . MiddlewareFunc {
return func ( next serpent . HandlerFunc ) serpent . HandlerFunc {
return func ( inv * serpent . Invocation ) error {
2023-03-23 22:42:20 +00:00
conf := r . createConfig ( )
var err error
2024-03-25 19:01:42 +00:00
// Read the client URL stored on disk.
2023-03-23 22:42:20 +00:00
if r . clientURL == nil || r . clientURL . String ( ) == "" {
rawURL , err := conf . URL ( ) . Read ( )
// If the configuration files are absent, the user is logged out
if os . IsNotExist ( err ) {
2024-03-25 19:01:42 +00:00
return xerrors . New ( notLoggedInMessage )
2023-03-23 22:42:20 +00:00
}
if err != nil {
return err
}
r . clientURL , err = url . Parse ( strings . TrimSpace ( rawURL ) )
if err != nil {
return err
}
2022-05-23 18:51:49 +00:00
}
2024-03-25 19:01:42 +00:00
// Read the token stored on disk.
2023-03-23 22:42:20 +00:00
if r . token == "" {
r . token , err = conf . Session ( ) . Read ( )
2024-03-25 19:01:42 +00:00
// Even if there isn't a token, we don't care.
// Some API routes can be unauthenticated.
if err != nil && ! os . IsNotExist ( err ) {
2023-03-23 22:42:20 +00:00
return err
}
2022-05-23 18:51:49 +00:00
}
2024-03-25 19:01:42 +00:00
err = r . configureClient ( inv . Context ( ) , client , r . clientURL , inv )
2023-03-23 22:42:20 +00:00
if err != nil {
return err
}
client . SetSessionToken ( r . token )
2023-04-19 16:07:53 +00:00
if r . debugHTTP {
client . PlainLogger = os . Stderr
2023-07-06 08:43:39 +00:00
client . SetLogBodies ( true )
2023-04-19 16:07:53 +00:00
}
2023-06-21 20:22:43 +00:00
client . DisableDirectConnections = r . disableDirect
2023-12-21 12:55:34 +00:00
return next ( inv )
}
}
}
2023-04-19 16:07:53 +00:00
2024-03-25 19:01:42 +00:00
// HeaderTransport creates a new transport that executes `--header-command`
// if it is set to add headers for all outbound requests.
2023-12-05 10:13:42 +00:00
func ( r * RootCmd ) HeaderTransport ( ctx context . Context , serverURL * url . URL ) ( * codersdk . HeaderTransport , error ) {
transport := & codersdk . HeaderTransport {
Transport : http . DefaultTransport ,
Header : http . Header { } ,
2022-09-12 21:22:05 +00:00
}
2023-08-14 20:12:17 +00:00
headers := r . header
if r . headerCommand != "" {
shell := "sh"
caller := "-c"
if runtime . GOOS == "windows" {
shell = "cmd.exe"
caller = "/c"
}
var outBuf bytes . Buffer
// #nosec
cmd := exec . CommandContext ( ctx , shell , caller , r . headerCommand )
cmd . Env = append ( os . Environ ( ) , "CODER_URL=" + serverURL . String ( ) )
cmd . Stdout = & outBuf
cmd . Stderr = io . Discard
err := cmd . Run ( )
if err != nil {
2023-12-05 10:13:42 +00:00
return nil , xerrors . Errorf ( "failed to run %v: %w" , cmd . Args , err )
2023-08-14 20:12:17 +00:00
}
scanner := bufio . NewScanner ( & outBuf )
for scanner . Scan ( ) {
headers = append ( headers , scanner . Text ( ) )
}
if err := scanner . Err ( ) ; err != nil {
2023-12-05 10:13:42 +00:00
return nil , xerrors . Errorf ( "scan %v: %w" , cmd . Args , err )
2023-08-14 20:12:17 +00:00
}
}
for _ , header := range headers {
2023-06-14 11:52:01 +00:00
parts := strings . SplitN ( header , "=" , 2 )
if len ( parts ) < 2 {
2023-12-05 10:13:42 +00:00
return nil , xerrors . Errorf ( "split header %q had less than two parts" , header )
2023-06-14 11:52:01 +00:00
}
2023-12-05 10:13:42 +00:00
transport . Header . Add ( parts [ 0 ] , parts [ 1 ] )
}
return transport , nil
}
2024-03-25 19:01:42 +00:00
func ( r * RootCmd ) configureClient ( ctx context . Context , client * codersdk . Client , serverURL * url . URL , inv * serpent . Invocation ) error {
transport := http . DefaultTransport
transport = wrapTransportWithTelemetryHeader ( transport , inv )
if ! r . noVersionCheck {
transport = wrapTransportWithVersionMismatchCheck ( transport , inv , buildinfo . Version ( ) , func ( ctx context . Context ) ( codersdk . BuildInfoResponse , error ) {
// Create a new client without any wrapped transport
// otherwise it creates an infinite loop!
basicClient := codersdk . New ( serverURL )
return basicClient . BuildInfo ( ctx )
} )
}
if ! r . noFeatureWarning {
transport = wrapTransportWithEntitlementsCheck ( transport , inv . Stderr )
}
headerTransport , err := r . HeaderTransport ( ctx , serverURL )
2023-12-05 10:13:42 +00:00
if err != nil {
return xerrors . Errorf ( "create header transport: %w" , err )
2023-06-14 11:52:01 +00:00
}
2024-03-25 19:01:42 +00:00
// The header transport has to come last.
// codersdk checks for the header transport to get headers
// to clone on the DERP client.
headerTransport . Transport = transport
2023-03-23 22:42:20 +00:00
client . HTTPClient = & http . Client {
2024-03-25 19:01:42 +00:00
Transport : headerTransport ,
2023-03-23 22:42:20 +00:00
}
2024-03-25 19:01:42 +00:00
client . URL = serverURL
2023-03-23 22:42:20 +00:00
return nil
}
2024-03-25 19:01:42 +00:00
func ( r * RootCmd ) createUnauthenticatedClient ( ctx context . Context , serverURL * url . URL , inv * serpent . Invocation ) ( * codersdk . Client , error ) {
2023-03-23 22:42:20 +00:00
var client codersdk . Client
2024-03-25 19:01:42 +00:00
err := r . configureClient ( ctx , & client , serverURL , inv )
2023-03-23 22:42:20 +00:00
return & client , err
2022-04-30 16:40:30 +00:00
}
// createAgentClient returns a new client from the command context.
2022-08-23 20:55:39 +00:00
// It works just like CreateClient, but uses the agent token and URL instead.
2023-03-23 22:42:20 +00:00
func ( r * RootCmd ) createAgentClient ( ) ( * agentsdk . Client , error ) {
client := agentsdk . New ( r . agentURL )
client . SetSessionToken ( r . agentToken )
2022-02-12 19:34:04 +00:00
return client , nil
2022-02-10 14:33:27 +00:00
}
2022-10-27 21:49:35 +00:00
// CurrentOrganization returns the currently active organization for the authenticated user.
2024-03-15 16:24:38 +00:00
func CurrentOrganization ( r * RootCmd , inv * serpent . Invocation , client * codersdk . Client ) ( codersdk . Organization , error ) {
2024-02-26 16:03:49 +00:00
conf := r . createConfig ( )
2024-02-26 17:38:49 +00:00
selected := r . organizationSelect
if selected == "" && conf . Organization ( ) . Exists ( ) {
2024-02-26 16:03:49 +00:00
org , err := conf . Organization ( ) . Read ( )
if err != nil {
2024-02-29 18:58:48 +00:00
return codersdk . Organization { } , xerrors . Errorf ( "read selected organization from config file %q: %w" , conf . Organization ( ) , err )
2024-02-26 16:03:49 +00:00
}
selected = org
}
// Verify the org exists and the user is a member
2023-03-23 22:42:20 +00:00
orgs , err := client . OrganizationsByUser ( inv . Context ( ) , codersdk . Me )
2022-02-10 14:33:27 +00:00
if err != nil {
2024-02-26 16:03:49 +00:00
return codersdk . Organization { } , err
2022-02-10 14:33:27 +00:00
}
2024-02-26 16:03:49 +00:00
// User manually selected an organization
if selected != "" {
index := slices . IndexFunc ( orgs , func ( org codersdk . Organization ) bool {
return org . Name == selected || org . ID . String ( ) == selected
} )
if index < 0 {
return codersdk . Organization { } , xerrors . Errorf ( "organization %q not found, are you sure you are a member of this organization?" , selected )
}
return orgs [ index ] , nil
}
// User did not select an organization, so use the default.
index := slices . IndexFunc ( orgs , func ( org codersdk . Organization ) bool {
return org . IsDefault
} )
if index < 0 {
2024-03-14 20:11:29 +00:00
if len ( orgs ) == 1 {
// If there is no "isDefault", but only 1 org is present. We can just
// assume the single organization is correct. This is mainly a helper
// for cli hitting an old instance, or a user that belongs to a single
// org that is not the default.
return orgs [ 0 ] , nil
}
return codersdk . Organization { } , xerrors . Errorf ( "unable to determine current organization. Use 'coder org set <org>' to select an organization to use" )
2024-02-26 16:03:49 +00:00
}
return orgs [ index ] , nil
2022-02-10 14:33:27 +00:00
}
2023-07-14 13:48:02 +00:00
func splitNamedWorkspace ( identifier string ) ( owner string , workspaceName string , err error ) {
2022-06-03 17:47:56 +00:00
parts := strings . Split ( identifier , "/" )
switch len ( parts ) {
case 1 :
owner = codersdk . Me
2023-07-14 13:48:02 +00:00
workspaceName = parts [ 0 ]
2022-06-03 17:47:56 +00:00
case 2 :
owner = parts [ 0 ]
2023-07-14 13:48:02 +00:00
workspaceName = parts [ 1 ]
2022-06-03 17:47:56 +00:00
default :
2023-07-14 13:48:02 +00:00
return "" , "" , xerrors . Errorf ( "invalid workspace name: %q" , identifier )
2022-06-03 17:47:56 +00:00
}
2023-07-14 13:48:02 +00:00
return owner , workspaceName , nil
}
2022-06-03 17:47:56 +00:00
2023-07-14 13:48:02 +00:00
// namedWorkspace fetches and returns a workspace by an identifier, which may be either
// a bare name (for a workspace owned by the current user) or a "user/workspace" combination,
// where user is either a username or UUID.
func namedWorkspace ( ctx context . Context , client * codersdk . Client , identifier string ) ( codersdk . Workspace , error ) {
owner , name , err := splitNamedWorkspace ( identifier )
if err != nil {
return codersdk . Workspace { } , err
}
2023-03-23 22:42:20 +00:00
return client . WorkspaceByOwnerAndName ( ctx , owner , name , codersdk . WorkspaceOptions { } )
2022-06-03 17:47:56 +00:00
}
2022-02-12 19:34:04 +00:00
// createConfig consumes the global configuration flag to produce a config root.
2023-03-23 22:42:20 +00:00
func ( r * RootCmd ) createConfig ( ) config . Root {
return config . Root ( r . globalConfig )
2022-02-10 14:33:27 +00:00
}
// isTTY returns whether the passed reader is a TTY or not.
2024-03-15 16:24:38 +00:00
func isTTY ( inv * serpent . Invocation ) bool {
fix: Run expect tests on Windows with conpty pseudo-terminal (#276)
This brings together a bunch of random, partially implemented packages for support of the new(ish) Windows [`conpty`](https://devblogs.microsoft.com/commandline/windows-command-line-introducing-the-windows-pseudo-console-conpty/) API - such that we can leverage the `expect` style of CLI tests, but in a way that works in Linux/OSX `pty`s and Windows `conpty`.
These include:
- Vendoring the `go-expect` library from Netflix w/ some tweaks to work cross-platform
- Vendoring the `pty` cross-platform implementation from [waypoint-plugin-sdk](https://github.com/hashicorp/waypoint-plugin-sdk/tree/b55c787a65ff9b7d2b32cfae80681b78f8f2275e/internal/pkg/pty)
- Vendoring the `conpty` Windows-specific implementation from [waypoint-plugin-sdk](https://github.com/hashicorp/waypoint-plugin-sdk/tree/b55c787a65ff9b7d2b32cfae80681b78f8f2275e/internal/pkg/conpty)
- Adjusting the `pty` interface to work with `go-expect` + the cross-plat version
There were several limitations with the current packages:
- `go-expect` requires the same `os.File` (TTY) for input / output, but `conhost` requires separate file handles
- `conpty` does not handle input, only output
- The cross-platform `pty` didn't expose the full set of primitives needed for `console`
Therefore, the following changes were made:
- Handling of `stdin` was added to the `conpty` interface
- We weren't using the full extent of the `go-expect` interface, so some portions were removed (ie, exec'ing a process) to simplify our implementation and make it easier to extend cross-platform
- Instead of `console` exposing just a `Tty`, it exposes an `InTty` and `OutTty`, to help encapsulate the difference on Windows (on Linux, these point to the same pipe)
Future improvements:
- The `isatty` implementation doesn't support accurate detection of `conhost` pty's without an associated process. In lieu of a more robust check, I've added a `--force-tty` flag intended for test case use - that forces the CLI to run in tty mode.
- It seems the windows implementation doesn't support setting a deadline. This is needed for the expect.Timeout API, but isn't used by us yet.
Fixes #241
2022-02-15 01:05:40 +00:00
// If the `--force-tty` command is available, and set,
// assume we're in a tty. This is primarily for cases on Windows
// where we may not be able to reliably detect this automatically (ie, tests)
2023-03-23 22:42:20 +00:00
forceTty , err := inv . ParsedFlags ( ) . GetBool ( varForceTty )
fix: Run expect tests on Windows with conpty pseudo-terminal (#276)
This brings together a bunch of random, partially implemented packages for support of the new(ish) Windows [`conpty`](https://devblogs.microsoft.com/commandline/windows-command-line-introducing-the-windows-pseudo-console-conpty/) API - such that we can leverage the `expect` style of CLI tests, but in a way that works in Linux/OSX `pty`s and Windows `conpty`.
These include:
- Vendoring the `go-expect` library from Netflix w/ some tweaks to work cross-platform
- Vendoring the `pty` cross-platform implementation from [waypoint-plugin-sdk](https://github.com/hashicorp/waypoint-plugin-sdk/tree/b55c787a65ff9b7d2b32cfae80681b78f8f2275e/internal/pkg/pty)
- Vendoring the `conpty` Windows-specific implementation from [waypoint-plugin-sdk](https://github.com/hashicorp/waypoint-plugin-sdk/tree/b55c787a65ff9b7d2b32cfae80681b78f8f2275e/internal/pkg/conpty)
- Adjusting the `pty` interface to work with `go-expect` + the cross-plat version
There were several limitations with the current packages:
- `go-expect` requires the same `os.File` (TTY) for input / output, but `conhost` requires separate file handles
- `conpty` does not handle input, only output
- The cross-platform `pty` didn't expose the full set of primitives needed for `console`
Therefore, the following changes were made:
- Handling of `stdin` was added to the `conpty` interface
- We weren't using the full extent of the `go-expect` interface, so some portions were removed (ie, exec'ing a process) to simplify our implementation and make it easier to extend cross-platform
- Instead of `console` exposing just a `Tty`, it exposes an `InTty` and `OutTty`, to help encapsulate the difference on Windows (on Linux, these point to the same pipe)
Future improvements:
- The `isatty` implementation doesn't support accurate detection of `conhost` pty's without an associated process. In lieu of a more robust check, I've added a `--force-tty` flag intended for test case use - that forces the CLI to run in tty mode.
- It seems the windows implementation doesn't support setting a deadline. This is needed for the expect.Timeout API, but isn't used by us yet.
Fixes #241
2022-02-15 01:05:40 +00:00
if forceTty && err == nil {
return true
}
2023-03-23 22:42:20 +00:00
file , ok := inv . Stdin . ( * os . File )
2022-02-10 14:33:27 +00:00
if ! ok {
return false
}
return isatty . IsTerminal ( file . Fd ( ) )
}
2022-04-05 01:35:03 +00:00
2022-06-08 08:45:29 +00:00
// isTTYOut returns whether the passed reader is a TTY or not.
2024-03-15 16:24:38 +00:00
func isTTYOut ( inv * serpent . Invocation ) bool {
2023-03-23 22:42:20 +00:00
return isTTYWriter ( inv , inv . Stdout )
2022-11-07 18:12:39 +00:00
}
// isTTYErr returns whether the passed reader is a TTY or not.
2024-03-15 16:24:38 +00:00
func isTTYErr ( inv * serpent . Invocation ) bool {
2023-03-23 22:42:20 +00:00
return isTTYWriter ( inv , inv . Stderr )
2022-11-07 18:12:39 +00:00
}
2024-03-15 16:24:38 +00:00
func isTTYWriter ( inv * serpent . Invocation , writer io . Writer ) bool {
2022-06-08 08:45:29 +00:00
// If the `--force-tty` command is available, and set,
// assume we're in a tty. This is primarily for cases on Windows
// where we may not be able to reliably detect this automatically (ie, tests)
2023-03-23 22:42:20 +00:00
forceTty , err := inv . ParsedFlags ( ) . GetBool ( varForceTty )
2022-06-08 08:45:29 +00:00
if forceTty && err == nil {
return true
}
2023-03-23 22:42:20 +00:00
file , ok := writer . ( * os . File )
2022-06-08 08:45:29 +00:00
if ! ok {
return false
}
return isatty . IsTerminal ( file . Fd ( ) )
}
2022-07-11 16:08:09 +00:00
// example represents a standard example for command usage, to be used
// with formatExamples.
type example struct {
Description string
Command string
}
2022-08-01 13:29:52 +00:00
// formatExamples formats the examples as width wrapped bulletpoint
2022-07-11 16:08:09 +00:00
// descriptions with the command underneath.
func formatExamples ( examples ... example ) string {
var sb strings . Builder
2023-06-07 05:22:58 +00:00
2023-09-07 21:28:22 +00:00
padStyle := cliui . DefaultStyles . Wrap . With ( pretty . XPad ( 4 , 0 ) )
2022-07-11 16:08:09 +00:00
for i , e := range examples {
if len ( e . Description ) > 0 {
2023-06-07 05:22:58 +00:00
wordwrap . WrapString ( e . Description , 80 )
_ , _ = sb . WriteString (
2023-09-07 21:28:22 +00:00
" - " + pretty . Sprint ( padStyle , e . Description + ":" ) [ 4 : ] + "\n\n " ,
2023-06-07 05:22:58 +00:00
)
2022-07-11 16:08:09 +00:00
}
2023-06-07 09:13:22 +00:00
// We add 1 space here because `cliui.DefaultStyles.Code` adds an extra
2022-07-11 16:08:09 +00:00
// space. This makes the code block align at an even 2 or 6
// spaces for symmetry.
2023-09-07 21:28:22 +00:00
_ , _ = sb . WriteString ( " " + pretty . Sprint ( cliui . DefaultStyles . Code , fmt . Sprintf ( "$ %s" , e . Command ) ) )
2022-07-11 16:08:09 +00:00
if i < len ( examples ) - 1 {
_ , _ = sb . WriteString ( "\n\n" )
}
}
return sb . String ( )
}
2023-08-27 19:46:44 +00:00
// Verbosef logs a message if verbose mode is enabled.
2024-03-15 16:24:38 +00:00
func ( r * RootCmd ) Verbosef ( inv * serpent . Invocation , fmtStr string , args ... interface { } ) {
2023-08-27 19:46:44 +00:00
if r . verbose {
cliui . Infof ( inv . Stdout , fmtStr , args ... )
}
}
2023-04-13 14:07:19 +00:00
// DumpHandler provides a custom SIGQUIT and SIGTRAP handler that dumps the
2023-01-11 16:22:20 +00:00
// stacktrace of all goroutines to stderr and a well-known file in the home
// directory. This is useful for debugging deadlock issues that may occur in
// production in workspaces, since the default Go runtime will only dump to
// stderr (which is often difficult/impossible to read in a workspace).
//
// SIGQUITs will still cause the program to exit (similarly to the default Go
// runtime behavior).
//
// A SIGQUIT handler will not be registered if GOTRACEBACK=crash.
//
// On Windows this immediately returns.
2024-03-04 16:35:33 +00:00
func DumpHandler ( ctx context . Context , name string ) {
2023-01-11 16:22:20 +00:00
if runtime . GOOS == "windows" {
// free up the goroutine since it'll be permanently blocked anyways
return
}
listenSignals := [ ] os . Signal { syscall . SIGTRAP }
if os . Getenv ( "GOTRACEBACK" ) != "crash" {
listenSignals = append ( listenSignals , syscall . SIGQUIT )
}
sigs := make ( chan os . Signal , 1 )
signal . Notify ( sigs , listenSignals ... )
defer signal . Stop ( sigs )
for {
sigStr := ""
select {
case <- ctx . Done ( ) :
return
case sig := <- sigs :
switch sig {
case syscall . SIGQUIT :
sigStr = "SIGQUIT"
case syscall . SIGTRAP :
sigStr = "SIGTRAP"
}
}
// Start with a 1MB buffer and keep doubling it until we can fit the
// entire stacktrace, stopping early once we reach 64MB.
buf := make ( [ ] byte , 1_000_000 )
stacklen := 0
for {
stacklen = runtime . Stack ( buf , true )
if stacklen < len ( buf ) {
break
}
if 2 * len ( buf ) > 64_000_000 {
// Write a message to the end of the buffer saying that it was
// truncated.
const truncatedMsg = "\n\n\nstack trace truncated due to size\n"
copy ( buf [ len ( buf ) - len ( truncatedMsg ) : ] , truncatedMsg )
break
}
buf = make ( [ ] byte , 2 * len ( buf ) )
}
_ , _ = fmt . Fprintf ( os . Stderr , "%s:\n%s\n" , sigStr , buf [ : stacklen ] )
// Write to a well-known file.
dir , err := os . UserHomeDir ( )
if err != nil {
dir = os . TempDir ( )
}
2024-03-04 16:35:33 +00:00
// Make the time filesystem-safe, for example ":" is not
// permitted on many filesystems. Note that Z here only appends
// Z to the string, it does not actually change the time zone.
filesystemSafeTime := time . Now ( ) . UTC ( ) . Format ( "2006-01-02T15-04-05.000Z" )
fpath := filepath . Join ( dir , fmt . Sprintf ( "coder-%s-%s.dump" , name , filesystemSafeTime ) )
2023-01-11 16:22:20 +00:00
_ , _ = fmt . Fprintf ( os . Stderr , "writing dump to %q\n" , fpath )
f , err := os . Create ( fpath )
if err != nil {
_ , _ = fmt . Fprintf ( os . Stderr , "failed to open dump file: %v\n" , err . Error ( ) )
goto done
}
_ , err = f . Write ( buf [ : stacklen ] )
_ = f . Close ( )
if err != nil {
_ , _ = fmt . Fprintf ( os . Stderr , "failed to write dump file: %v\n" , err . Error ( ) )
goto done
}
done :
if sigStr == "SIGQUIT" {
//nolint:revive
os . Exit ( 1 )
}
}
}
2023-01-29 21:47:24 +00:00
2023-11-24 12:35:56 +00:00
type exitError struct {
code int
err error
}
var _ error = ( * exitError ) ( nil )
func ( e * exitError ) Error ( ) string {
if e . err != nil {
return fmt . Sprintf ( "exit code %d: %v" , e . code , e . err )
}
return fmt . Sprintf ( "exit code %d" , e . code )
}
func ( e * exitError ) Unwrap ( ) error {
return e . err
}
// ExitError returns an error that will cause the CLI to exit with the given
// exit code. If err is non-nil, it will be wrapped by the returned error.
func ExitError ( code int , err error ) error {
return & exitError { code : code , err : err }
}
2024-04-01 14:19:26 +00:00
// NewPrettyErrorFormatter creates a new PrettyErrorFormatter.
func NewPrettyErrorFormatter ( w io . Writer , verbose bool ) * PrettyErrorFormatter {
return & PrettyErrorFormatter {
w : w ,
verbose : verbose ,
}
}
type PrettyErrorFormatter struct {
2023-03-28 16:03:34 +00:00
w io . Writer
2023-09-29 21:50:23 +00:00
// verbose turns on more detailed error logs, such as stack traces.
verbose bool
2023-03-23 22:42:20 +00:00
}
2024-04-01 14:19:26 +00:00
// Format formats the error to the writer in PrettyErrorFormatter.
// This error should be human readable.
func ( p * PrettyErrorFormatter ) Format ( err error ) {
2024-01-11 22:55:34 +00:00
output , _ := cliHumanFormatError ( "" , err , & formatOpts {
2023-09-29 21:50:23 +00:00
Verbose : p . verbose ,
} )
// always trail with a newline
_ , _ = p . w . Write ( [ ] byte ( output + "\n" ) )
}
2023-03-23 22:42:20 +00:00
2023-09-29 21:50:23 +00:00
type formatOpts struct {
Verbose bool
}
const indent = " "
// cliHumanFormatError formats an error for the CLI. Newlines and styling are
2024-01-11 22:55:34 +00:00
// included. The second return value is true if the error is special and the error
// chain has custom formatting applied.
//
// If you change this code, you can use the cli "example-errors" tool to
// verify all errors still look ok.
//
// go run main.go exp example-error <type>
// go run main.go exp example-error api
// go run main.go exp example-error cmd
// go run main.go exp example-error multi-error
// go run main.go exp example-error validation
//
//nolint:errorlint
func cliHumanFormatError ( from string , err error , opts * formatOpts ) ( string , bool ) {
2023-09-29 21:50:23 +00:00
if opts == nil {
opts = & formatOpts { }
2023-03-23 22:42:20 +00:00
}
2024-01-11 22:55:34 +00:00
if err == nil {
return "<nil>" , true
}
2023-03-23 22:42:20 +00:00
2023-09-29 21:50:23 +00:00
if multi , ok := err . ( interface { Unwrap ( ) [ ] error } ) ; ok {
multiErrors := multi . Unwrap ( )
if len ( multiErrors ) == 1 {
// Format as a single error
2024-01-11 22:55:34 +00:00
return cliHumanFormatError ( from , multiErrors [ 0 ] , opts )
2023-09-29 21:50:23 +00:00
}
2024-01-11 22:55:34 +00:00
return formatMultiError ( from , multiErrors , opts ) , true
2023-03-23 22:42:20 +00:00
}
2023-09-29 21:50:23 +00:00
// First check for sentinel errors that we want to handle specially.
// Order does matter! We want to check for the most specific errors first.
2024-01-11 22:55:34 +00:00
if sdkError , ok := err . ( * codersdk . Error ) ; ok {
return formatCoderSDKError ( from , sdkError , opts ) , true
2023-09-29 21:50:23 +00:00
}
2024-03-15 16:24:38 +00:00
if cmdErr , ok := err . ( * serpent . RunCommandError ) ; ok {
2024-01-11 22:55:34 +00:00
// no need to pass the "from" context to this since it is always
// top level. We care about what is below this.
return formatRunCommandError ( cmdErr , opts ) , true
2023-09-29 21:50:23 +00:00
}
2024-01-11 22:55:34 +00:00
uw , ok := err . ( interface { Unwrap ( ) error } )
if ok {
msg , special := cliHumanFormatError ( from + traceError ( err ) , uw . Unwrap ( ) , opts )
if special {
return msg , special
}
}
// If we got here, that means that the wrapped error chain does not have
// any special formatting below it. So we want to return the topmost non-special
// error (which is 'err')
2023-09-29 21:50:23 +00:00
// Default just printing the error. Use +v for verbose to handle stack
// traces of xerrors.
if opts . Verbose {
2024-01-11 22:55:34 +00:00
return pretty . Sprint ( headLineStyle ( ) , fmt . Sprintf ( "%+v" , err ) ) , false
2023-09-29 21:50:23 +00:00
}
2024-01-11 22:55:34 +00:00
return pretty . Sprint ( headLineStyle ( ) , fmt . Sprintf ( "%v" , err ) ) , false
2023-09-29 21:50:23 +00:00
}
// formatMultiError formats a multi-error. It formats it as a list of errors.
//
// Multiple Errors:
// <# errors encountered>:
// 1. <heading error message>
// <verbose error message>
// 2. <heading error message>
// <verbose error message>
2024-01-11 22:55:34 +00:00
func formatMultiError ( from string , multi [ ] error , opts * formatOpts ) string {
2023-09-29 21:50:23 +00:00
var errorStrings [ ] string
for _ , err := range multi {
2024-01-11 22:55:34 +00:00
msg , _ := cliHumanFormatError ( "" , err , opts )
errorStrings = append ( errorStrings , msg )
2023-09-29 21:50:23 +00:00
}
// Write errors out
var str strings . Builder
2024-01-11 22:55:34 +00:00
var traceMsg string
if from != "" {
traceMsg = fmt . Sprintf ( "Trace=[%s])" , from )
}
_ , _ = str . WriteString ( pretty . Sprint ( headLineStyle ( ) , fmt . Sprintf ( "%d errors encountered: %s" , len ( multi ) , traceMsg ) ) )
2023-09-29 21:50:23 +00:00
for i , errStr := range errorStrings {
// Indent each error
errStr = strings . ReplaceAll ( errStr , "\n" , "\n" + indent )
// Error now looks like
// | <line>
// | <line>
prefix := fmt . Sprintf ( "%d. " , i + 1 )
if len ( prefix ) < len ( indent ) {
// Indent the prefix to match the indent
prefix = prefix + strings . Repeat ( " " , len ( indent ) - len ( prefix ) )
2023-03-28 16:03:34 +00:00
}
2023-09-29 21:50:23 +00:00
errStr = prefix + errStr
// Now looks like
// |1.<line>
// | <line>
_ , _ = str . WriteString ( "\n" + errStr )
2023-03-23 22:42:20 +00:00
}
2023-09-29 21:50:23 +00:00
return str . String ( )
}
2023-03-23 22:42:20 +00:00
2023-09-29 21:50:23 +00:00
// formatRunCommandError are cli command errors. This kind of error is very
// broad, as it contains all errors that occur when running a command.
// If you know the error is something else, like a codersdk.Error, make a new
// formatter and add it to cliHumanFormatError function.
2024-03-15 16:24:38 +00:00
func formatRunCommandError ( err * serpent . RunCommandError , opts * formatOpts ) string {
2023-09-29 21:50:23 +00:00
var str strings . Builder
2024-03-18 03:17:43 +00:00
_ , _ = str . WriteString ( pretty . Sprint ( headLineStyle ( ) ,
fmt . Sprintf (
` Encountered an error running %q, see "%s --help" for more information ` ,
err . Cmd . FullName ( ) , err . Cmd . FullName ( ) ) ) )
_ , _ = str . WriteString ( pretty . Sprint ( headLineStyle ( ) , "\nerror: " ) )
2023-09-29 21:50:23 +00:00
2024-01-11 22:55:34 +00:00
msgString , special := cliHumanFormatError ( "" , err . Err , opts )
if special {
_ , _ = str . WriteString ( msgString )
} else {
_ , _ = str . WriteString ( pretty . Sprint ( tailLineStyle ( ) , msgString ) )
}
2023-09-29 21:50:23 +00:00
return str . String ( )
}
2023-03-23 22:42:20 +00:00
2023-09-29 21:50:23 +00:00
// formatCoderSDKError come from API requests. In verbose mode, add the
// request debug information.
2024-01-11 22:55:34 +00:00
func formatCoderSDKError ( from string , err * codersdk . Error , opts * formatOpts ) string {
2023-09-29 21:50:23 +00:00
var str strings . Builder
if opts . Verbose {
2024-02-26 17:39:26 +00:00
// If all these fields are empty, then do not print this information.
// This can occur if the error is being used outside the api.
if ! ( err . Method ( ) == "" && err . URL ( ) == "" && err . StatusCode ( ) == 0 ) {
_ , _ = str . WriteString ( pretty . Sprint ( headLineStyle ( ) , fmt . Sprintf ( "API request error to \"%s:%s\". Status code %d" , err . Method ( ) , err . URL ( ) , err . StatusCode ( ) ) ) )
_ , _ = str . WriteString ( "\n" )
}
2023-09-29 21:50:23 +00:00
}
2024-01-11 22:55:34 +00:00
// Always include this trace. Users can ignore this.
if from != "" {
_ , _ = str . WriteString ( pretty . Sprint ( headLineStyle ( ) , fmt . Sprintf ( "Trace=[%s]" , from ) ) )
_ , _ = str . WriteString ( "\n" )
}
2023-03-28 16:03:34 +00:00
2024-04-02 15:02:30 +00:00
// The main error message
2023-09-29 21:50:23 +00:00
_ , _ = str . WriteString ( pretty . Sprint ( headLineStyle ( ) , err . Message ) )
2024-04-02 15:02:30 +00:00
// Validation errors.
if len ( err . Validations ) > 0 {
_ , _ = str . WriteString ( "\n" )
_ , _ = str . WriteString ( pretty . Sprint ( tailLineStyle ( ) , fmt . Sprintf ( "%d validation error(s) found" , len ( err . Validations ) ) ) )
for _ , e := range err . Validations {
_ , _ = str . WriteString ( "\n\t" )
_ , _ = str . WriteString ( pretty . Sprint ( cliui . DefaultStyles . Field , e . Field ) )
_ , _ = str . WriteString ( pretty . Sprintf ( cliui . DefaultStyles . Warn , ": %s" , e . Detail ) )
}
}
2023-09-29 21:50:23 +00:00
if err . Helper != "" {
_ , _ = str . WriteString ( "\n" )
2024-04-02 15:02:30 +00:00
_ , _ = str . WriteString ( pretty . Sprintf ( tailLineStyle ( ) , "Suggestion: %s" , err . Helper ) )
2023-09-29 21:50:23 +00:00
}
// By default we do not show the Detail with the helper.
if opts . Verbose || ( err . Helper == "" && err . Detail != "" ) {
_ , _ = str . WriteString ( "\n" )
_ , _ = str . WriteString ( pretty . Sprint ( tailLineStyle ( ) , err . Detail ) )
2023-03-28 16:03:34 +00:00
}
2023-09-29 21:50:23 +00:00
return str . String ( )
2023-03-28 16:03:34 +00:00
}
2023-03-23 22:42:20 +00:00
2024-01-11 22:55:34 +00:00
// traceError is a helper function that aides developers debugging failed cli
// commands. When we pretty print errors, we lose the context in which they came.
// This function adds the context back. Unfortunately there is no easy way to get
// the prefix to: "error string: %w", so we do a bit of string manipulation.
//
//nolint:errorlint
func traceError ( err error ) string {
if uw , ok := err . ( interface { Unwrap ( ) error } ) ; ok {
a , b := err . Error ( ) , uw . Unwrap ( ) . Error ( )
c := strings . TrimSuffix ( a , b )
return c
}
return err . Error ( )
}
2023-09-29 21:50:23 +00:00
// These styles are arbitrary.
func headLineStyle ( ) pretty . Style {
return cliui . DefaultStyles . Error
}
func tailLineStyle ( ) pretty . Style {
return pretty . Style { pretty . Nop }
2023-03-23 22:42:20 +00:00
}
2023-09-04 16:38:53 +00:00
//nolint:unused
func SlimUnsupported ( w io . Writer , cmd string ) {
2023-09-07 21:28:22 +00:00
_ , _ = fmt . Fprintf ( w , "You are using a 'slim' build of Coder, which does not support the %s subcommand.\n" , pretty . Sprint ( cliui . DefaultStyles . Code , cmd ) )
2023-09-04 16:38:53 +00:00
_ , _ = fmt . Fprintln ( w , "" )
_ , _ = fmt . Fprintln ( w , "Please use a build of Coder from GitHub releases:" )
_ , _ = fmt . Fprintln ( w , " https://github.com/coder/coder/releases" )
//nolint:revive
os . Exit ( 1 )
}
2024-01-30 23:11:37 +00:00
func defaultUpgradeMessage ( version string ) string {
// Our installation script doesn't work on Windows, so instead we direct the user
// to the GitHub release page to download the latest installer.
version = strings . TrimPrefix ( version , "v" )
if runtime . GOOS == "windows" {
return fmt . Sprintf ( "download the server version from: https://github.com/coder/coder/releases/v%s" , version )
}
return fmt . Sprintf ( "download the server version with: 'curl -L https://coder.com/install.sh | sh -s -- --version %s'" , version )
}
2024-03-25 19:01:42 +00:00
// wrapTransportWithEntitlementsCheck adds a middleware to the HTTP transport
// that checks for entitlement warnings and prints them to the user.
func wrapTransportWithEntitlementsCheck ( rt http . RoundTripper , w io . Writer ) http . RoundTripper {
var once sync . Once
return roundTripper ( func ( req * http . Request ) ( * http . Response , error ) {
res , err := rt . RoundTrip ( req )
if err != nil {
return res , err
}
once . Do ( func ( ) {
for _ , warning := range res . Header . Values ( codersdk . EntitlementsWarningHeader ) {
_ , _ = fmt . Fprintln ( w , pretty . Sprint ( cliui . DefaultStyles . Warn , warning ) )
}
} )
return res , err
} )
}
// wrapTransportWithVersionMismatchCheck adds a middleware to the HTTP transport
// that checks for version mismatches between the client and server. If a mismatch
// is detected, a warning is printed to the user.
func wrapTransportWithVersionMismatchCheck ( rt http . RoundTripper , inv * serpent . Invocation , clientVersion string , getBuildInfo func ( ctx context . Context ) ( codersdk . BuildInfoResponse , error ) ) http . RoundTripper {
var once sync . Once
return roundTripper ( func ( req * http . Request ) ( * http . Response , error ) {
res , err := rt . RoundTrip ( req )
if err != nil {
return res , err
}
once . Do ( func ( ) {
serverVersion := res . Header . Get ( codersdk . BuildVersionHeader )
if serverVersion == "" {
return
}
if buildinfo . VersionsMatch ( clientVersion , serverVersion ) {
return
}
upgradeMessage := defaultUpgradeMessage ( semver . Canonical ( serverVersion ) )
serverInfo , err := getBuildInfo ( inv . Context ( ) )
if err == nil && serverInfo . UpgradeMessage != "" {
upgradeMessage = serverInfo . UpgradeMessage
}
fmtWarningText := "version mismatch: client %s, server %s\n%s"
fmtWarn := pretty . Sprint ( cliui . DefaultStyles . Warn , fmtWarningText )
warning := fmt . Sprintf ( fmtWarn , clientVersion , serverVersion , upgradeMessage )
_ , _ = fmt . Fprintln ( inv . Stderr , warning )
} )
return res , err
} )
}
// wrapTransportWithTelemetryHeader adds telemetry headers to report command usage
// to an HTTP transport.
func wrapTransportWithTelemetryHeader ( transport http . RoundTripper , inv * serpent . Invocation ) http . RoundTripper {
var (
value string
once sync . Once
)
return roundTripper ( func ( req * http . Request ) ( * http . Response , error ) {
once . Do ( func ( ) {
// We only want to compute this header once when a request
// first goes out, hence the complexity with locking here.
var topts [ ] telemetry . Option
for _ , opt := range inv . Command . FullOptions ( ) {
if opt . ValueSource == serpent . ValueSourceNone || opt . ValueSource == serpent . ValueSourceDefault {
continue
}
topts = append ( topts , telemetry . Option {
Name : opt . Name ,
ValueSource : string ( opt . ValueSource ) ,
} )
}
ti := telemetry . Invocation {
Command : inv . Command . FullName ( ) ,
Options : topts ,
InvokedAt : time . Now ( ) ,
}
byt , err := json . Marshal ( ti )
if err != nil {
// Should be impossible
panic ( err )
}
s := base64 . StdEncoding . EncodeToString ( byt )
// Don't send the header if it's too long!
if len ( s ) <= 4096 {
value = s
}
} )
if value != "" {
req . Header . Add ( codersdk . CLITelemetryHeader , value )
}
return transport . RoundTrip ( req )
} )
}
type roundTripper func ( req * http . Request ) ( * http . Response , error )
func ( r roundTripper ) RoundTrip ( req * http . Request ) ( * http . Response , error ) {
return r ( req )
}