2022-02-10 14:33:27 +00:00
package cli
import (
2022-09-08 13:59:28 +00:00
"context"
2022-07-26 13:27:48 +00:00
"flag"
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-01-11 16:22:20 +00:00
"os/signal"
"path/filepath"
2022-12-14 16:36:28 +00:00
"runtime"
2022-05-23 17:19:33 +00:00
"strings"
2023-01-11 16:22:20 +00:00
"syscall"
2022-07-12 20:24:53 +00:00
"text/template"
2022-04-05 01:35:03 +00:00
"time"
2022-02-10 14:33:27 +00:00
2022-05-23 18:51:49 +00:00
"golang.org/x/xerrors"
2022-06-29 22:49:40 +00:00
"github.com/charmbracelet/lipgloss"
2022-02-10 14:33:27 +00:00
"github.com/kirsle/configdir"
"github.com/mattn/go-isatty"
"github.com/spf13/cobra"
2022-04-07 17:18:58 +00:00
"github.com/coder/coder/buildinfo"
2022-04-30 16:40:30 +00:00
"github.com/coder/coder/cli/cliflag"
2022-03-22 19:17:50 +00:00
"github.com/coder/coder/cli/cliui"
2022-02-10 14:33:27 +00:00
"github.com/coder/coder/cli/config"
2022-10-10 19:04:15 +00:00
"github.com/coder/coder/cli/deployment"
2022-08-22 22:02:50 +00:00
"github.com/coder/coder/coderd"
2022-10-25 00:46:24 +00:00
"github.com/coder/coder/coderd/gitauth"
2022-02-10 14:33:27 +00:00
"github.com/coder/coder/codersdk"
)
2022-02-21 18:47:08 +00:00
var (
2022-10-27 21:49:35 +00:00
Caret = cliui . Styles . Prompt . String ( )
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 (
2022-08-29 18:30:06 +00:00
varURL = "url"
varToken = "token"
varAgentToken = "agent-token"
varAgentURL = "agent-url"
2022-09-12 21:22:05 +00:00
varHeader = "header"
2022-08-29 18:30:06 +00:00
varNoOpen = "no-open"
varNoVersionCheck = "no-version-warning"
varNoFeatureWarning = "no-feature-warning"
varForceTty = "force-tty"
varVerbose = "verbose"
notLoggedInMessage = "You are not logged in. Try logging in using 'coder login <url>'."
envNoVersionCheck = "CODER_NO_VERSION_WARNING"
envNoFeatureWarning = "CODER_NO_FEATURE_WARNING"
2022-10-11 15:58:28 +00:00
envSessionToken = "CODER_SESSION_TOKEN"
envURL = "CODER_URL"
2022-06-29 22:49:40 +00:00
)
var (
errUnauthenticated = xerrors . New ( notLoggedInMessage )
2022-02-10 14:33:27 +00:00
)
2022-05-18 14:10:40 +00:00
func init ( ) {
2022-07-12 20:24:53 +00:00
// Set cobra template functions in init to avoid conflicts in tests.
cobra . AddTemplateFuncs ( templateFunctions )
2022-05-18 14:10:40 +00:00
}
2022-08-22 22:02:50 +00:00
func Core ( ) [ ] * cobra . Command {
2022-11-02 18:30:00 +00:00
// Please re-sort this list alphabetically if you change it!
2022-08-22 22:02:50 +00:00
return [ ] * cobra . Command {
configSSH ( ) ,
create ( ) ,
deleteWorkspace ( ) ,
dotfiles ( ) ,
gitssh ( ) ,
list ( ) ,
login ( ) ,
logout ( ) ,
parameters ( ) ,
portForward ( ) ,
publickey ( ) ,
2022-11-02 18:30:00 +00:00
rename ( ) ,
2022-08-22 22:02:50 +00:00
resetPassword ( ) ,
2022-12-15 15:04:24 +00:00
scaletest ( ) ,
2022-08-22 22:02:50 +00:00
schedules ( ) ,
show ( ) ,
2022-09-05 22:15:49 +00:00
speedtest ( ) ,
2022-11-02 18:30:00 +00:00
ssh ( ) ,
2022-08-22 22:02:50 +00:00
start ( ) ,
state ( ) ,
stop ( ) ,
templates ( ) ,
2022-11-02 18:30:00 +00:00
tokens ( ) ,
2022-08-22 22:02:50 +00:00
update ( ) ,
users ( ) ,
versionCmd ( ) ,
2023-01-10 04:23:17 +00:00
vscodeSSH ( ) ,
2023-01-07 02:57:25 +00:00
workspaceAgent ( ) ,
2022-08-22 22:02:50 +00:00
}
}
func AGPL ( ) [ ] * cobra . Command {
2022-10-21 22:08:23 +00:00
all := append ( Core ( ) , Server ( deployment . NewViper ( ) , func ( _ context . Context , o * coderd . Options ) ( * coderd . API , io . Closer , error ) {
2022-10-17 13:43:30 +00:00
api := coderd . New ( o )
return api , api , nil
2022-09-20 04:11:01 +00:00
} ) )
2022-08-22 22:02:50 +00:00
return all
}
func Root ( subcommands [ ] * cobra . Command ) * cobra . Command {
2022-10-25 00:46:24 +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.
isGitAskpass := false
2022-10-24 16:17:48 +00:00
fmtLong := ` Coder % s — A tool for provisioning self - hosted development environments with Terraform .
`
2022-02-10 14:33:27 +00:00
cmd := & cobra . Command {
2022-04-11 23:54:30 +00:00
Use : "coder" ,
SilenceErrors : true ,
SilenceUsage : true ,
2022-10-25 00:46:24 +00:00
Long : fmt . Sprintf ( fmtLong , buildinfo . Version ( ) ) ,
Args : func ( cmd * cobra . Command , args [ ] string ) error {
if gitauth . CheckCommand ( args , os . Environ ( ) ) {
isGitAskpass = true
return nil
}
return cobra . NoArgs ( cmd , args )
} ,
RunE : func ( cmd * cobra . Command , args [ ] string ) error {
if isGitAskpass {
return gitAskpass ( ) . RunE ( cmd , args )
}
return cmd . Help ( )
} ,
2022-07-11 16:08:09 +00:00
Example : formatExamples (
example {
Description : "Start a Coder server" ,
Command : "coder server" ,
} ,
example {
Description : "Get started by creating a template from an example" ,
Command : "coder templates init" ,
} ,
) ,
2022-02-10 14:33:27 +00:00
}
2022-08-22 22:02:50 +00:00
cmd . AddCommand ( subcommands ... )
2022-09-20 12:31:38 +00:00
fixUnknownSubcommandError ( cmd . Commands ( ) )
2022-03-22 19:17:50 +00:00
2022-05-09 22:42:02 +00:00
cmd . SetUsageTemplate ( usageTemplate ( ) )
2022-10-11 15:58:28 +00:00
cliflag . String ( cmd . PersistentFlags ( ) , varURL , "" , envURL , "" , "URL to a deployment." )
2022-07-20 20:17:51 +00:00
cliflag . Bool ( cmd . PersistentFlags ( ) , varNoVersionCheck , "" , envNoVersionCheck , false , "Suppress warning when client and server versions do not match." )
2022-08-29 18:30:06 +00:00
cliflag . Bool ( cmd . PersistentFlags ( ) , varNoFeatureWarning , "" , envNoFeatureWarning , false , "Suppress warnings about unlicensed features." )
2022-06-24 23:50:35 +00:00
cliflag . String ( cmd . PersistentFlags ( ) , varToken , "" , envSessionToken , "" , fmt . Sprintf ( "Specify an authentication token. For security reasons setting %s is preferred." , envSessionToken ) )
2022-09-19 16:36:18 +00:00
cliflag . String ( cmd . PersistentFlags ( ) , varAgentToken , "" , "CODER_AGENT_TOKEN" , "" , "An agent authentication token." )
2022-05-10 20:57:07 +00:00
_ = cmd . PersistentFlags ( ) . MarkHidden ( varAgentToken )
2022-09-19 16:36:18 +00:00
cliflag . String ( cmd . PersistentFlags ( ) , varAgentURL , "" , "CODER_AGENT_URL" , "" , "URL for an agent to access your deployment." )
2022-05-10 20:57:07 +00:00
_ = cmd . PersistentFlags ( ) . MarkHidden ( varAgentURL )
2022-10-21 22:08:23 +00:00
cliflag . String ( cmd . PersistentFlags ( ) , config . FlagName , "" , "CODER_CONFIG_DIR" , configdir . LocalConfig ( "coderv2" ) , "Path to the global `coder` config directory." )
2022-09-12 21:22:05 +00:00
cliflag . StringArray ( cmd . PersistentFlags ( ) , varHeader , "" , "CODER_HEADER" , [ ] string { } , "HTTP headers added to all requests. Provide as \"Key=Value\"" )
2022-04-30 16:40:30 +00:00
cmd . PersistentFlags ( ) . Bool ( varForceTty , false , "Force the `coder` command to run as if connected to a TTY." )
_ = cmd . PersistentFlags ( ) . MarkHidden ( varForceTty )
2022-02-21 17:10:05 +00:00
cmd . PersistentFlags ( ) . Bool ( varNoOpen , false , "Block automatically opening URLs in the browser." )
2022-04-30 16:40:30 +00:00
_ = cmd . PersistentFlags ( ) . MarkHidden ( varNoOpen )
2022-09-19 16:36:18 +00:00
cliflag . Bool ( cmd . PersistentFlags ( ) , varVerbose , "v" , "CODER_VERBOSE" , false , "Enable verbose output." )
2022-02-10 14:33:27 +00:00
return cmd
}
2022-09-20 12:31:38 +00:00
// fixUnknownSubcommandError modifies the provided commands so that the
// ones with subcommands output the correct error message when an
// unknown subcommand is invoked.
//
// Example:
//
// unknown command "bad" for "coder templates"
func fixUnknownSubcommandError ( commands [ ] * cobra . Command ) {
for _ , sc := range commands {
if sc . HasSubCommands ( ) {
if sc . Run == nil && sc . RunE == nil {
if sc . Args != nil {
// In case the developer does not know about this
// behavior in Cobra they must verify correct
// behavior. For instance, settings Args to
// `cobra.ExactArgs(0)` will not give the same
// message as `cobra.NoArgs`. Likewise, omitting the
// run function will not give the wanted error.
panic ( "developer error: subcommand has subcommands and Args but no Run or RunE" )
}
sc . Args = cobra . NoArgs
sc . Run = func ( * cobra . Command , [ ] string ) { }
}
fixUnknownSubcommandError ( sc . Commands ( ) )
}
}
}
2022-06-06 22:38:51 +00:00
// versionCmd prints the coder version
func versionCmd ( ) * cobra . Command {
return & cobra . Command {
2022-07-11 16:08:09 +00:00
Use : "version" ,
Short : "Show coder version" ,
2022-06-06 22:38:51 +00:00
RunE : func ( cmd * cobra . Command , args [ ] string ) error {
var str strings . Builder
2023-01-17 16:58:00 +00:00
_ , _ = str . WriteString ( "Coder " )
if buildinfo . IsAGPL ( ) {
_ , _ = str . WriteString ( "(AGPL) " )
}
_ , _ = str . WriteString ( buildinfo . Version ( ) )
2022-06-06 22:38:51 +00:00
buildTime , valid := buildinfo . Time ( )
if valid {
_ , _ = str . WriteString ( " " + buildTime . Format ( time . UnixDate ) )
}
2023-01-17 16:58:00 +00:00
_ , _ = str . WriteString ( "\r\n" + buildinfo . ExternalURL ( ) + "\r\n\r\n" )
if buildinfo . IsSlim ( ) {
_ , _ = str . WriteString ( fmt . Sprintf ( "Slim build of Coder, does not support the %s subcommand.\n" , cliui . Styles . Code . Render ( "server" ) ) )
} else {
_ , _ = str . WriteString ( fmt . Sprintf ( "Full build of Coder, supports the %s subcommand.\n" , cliui . Styles . Code . Render ( "server" ) ) )
}
2022-06-06 22:38:51 +00:00
_ , _ = fmt . Fprint ( cmd . OutOrStdout ( ) , str . String ( ) )
return nil
} ,
}
}
2022-07-26 13:27:48 +00:00
func isTest ( ) bool {
return flag . Lookup ( "test.v" ) != nil
}
2022-08-23 20:55:39 +00:00
// CreateClient returns a new client from the command context.
2022-04-30 16:40:30 +00:00
// It reads from global configuration files if flags are not set.
2022-08-23 20:55:39 +00:00
func CreateClient ( cmd * cobra . Command ) ( * codersdk . Client , error ) {
2022-02-10 14:33:27 +00:00
root := createConfig ( cmd )
2022-04-30 16:40:30 +00:00
rawURL , err := cmd . Flags ( ) . GetString ( varURL )
if err != nil || rawURL == "" {
rawURL , err = root . URL ( ) . Read ( )
if err != nil {
2022-05-23 18:51:49 +00:00
// If the configuration files are absent, the user is logged out
if os . IsNotExist ( err ) {
2022-06-29 22:49:40 +00:00
return nil , errUnauthenticated
2022-05-23 18:51:49 +00:00
}
2022-04-30 16:40:30 +00:00
return nil , err
}
}
2022-05-23 17:19:33 +00:00
serverURL , err := url . Parse ( strings . TrimSpace ( rawURL ) )
2022-04-30 16:40:30 +00:00
if err != nil {
return nil , err
}
token , err := cmd . Flags ( ) . GetString ( varToken )
if err != nil || token == "" {
token , err = root . Session ( ) . Read ( )
if err != nil {
2022-05-23 18:51:49 +00:00
// If the configuration files are absent, the user is logged out
if os . IsNotExist ( err ) {
2022-06-29 22:49:40 +00:00
return nil , errUnauthenticated
2022-05-23 18:51:49 +00:00
}
2022-04-30 16:40:30 +00:00
return nil , err
}
}
2022-09-12 21:22:05 +00:00
client , err := createUnauthenticatedClient ( cmd , serverURL )
if err != nil {
return nil , err
}
2022-11-09 13:31:24 +00:00
client . SetSessionToken ( token )
2022-12-02 21:40:23 +00:00
// We send these requests in parallel to minimize latency.
var (
versionErr = make ( chan error )
warningErr = make ( chan error )
)
go func ( ) {
versionErr <- checkVersions ( cmd , client )
close ( versionErr )
} ( )
go func ( ) {
warningErr <- checkWarnings ( cmd , client )
close ( warningErr )
} ( )
if err = <- versionErr ; err != nil {
// Just log the error here. We never want to fail a command
// due to a pre-run.
_ , _ = fmt . Fprintf ( cmd . ErrOrStderr ( ) ,
cliui . Styles . Warn . Render ( "check versions error: %s" ) , err )
_ , _ = fmt . Fprintln ( cmd . ErrOrStderr ( ) )
}
if err = <- warningErr ; err != nil {
// Same as above
_ , _ = fmt . Fprintf ( cmd . ErrOrStderr ( ) ,
cliui . Styles . Warn . Render ( "check entitlement warnings error: %s" ) , err )
_ , _ = fmt . Fprintln ( cmd . ErrOrStderr ( ) )
}
2022-09-12 21:22:05 +00:00
return client , nil
}
func createUnauthenticatedClient ( cmd * cobra . Command , serverURL * url . URL ) ( * codersdk . Client , error ) {
2022-04-30 16:40:30 +00:00
client := codersdk . New ( serverURL )
2022-09-12 21:22:05 +00:00
headers , err := cmd . Flags ( ) . GetStringArray ( varHeader )
if err != nil {
return nil , err
}
transport := & headerTransport {
transport : http . DefaultTransport ,
headers : map [ string ] string { } ,
}
for _ , header := range headers {
parts := strings . SplitN ( header , "=" , 2 )
if len ( parts ) < 2 {
return nil , xerrors . Errorf ( "split header %q had less than two parts" , header )
}
transport . headers [ parts [ 0 ] ] = parts [ 1 ]
}
client . HTTPClient . Transport = transport
2022-04-30 16:40:30 +00:00
return client , nil
}
// 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.
2022-04-30 16:40:30 +00:00
func createAgentClient ( cmd * cobra . Command ) ( * codersdk . Client , error ) {
rawURL , err := cmd . Flags ( ) . GetString ( varAgentURL )
2022-02-10 14:33:27 +00:00
if err != nil {
return nil , err
}
serverURL , err := url . Parse ( rawURL )
if err != nil {
return nil , err
}
2022-04-30 16:40:30 +00:00
token , err := cmd . Flags ( ) . GetString ( varAgentToken )
2022-02-10 14:33:27 +00:00
if err != nil {
return nil , err
}
client := codersdk . New ( serverURL )
2022-11-09 13:31:24 +00:00
client . SetSessionToken ( token )
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.
func CurrentOrganization ( cmd * cobra . Command , client * codersdk . Client ) ( codersdk . Organization , error ) {
2022-04-01 19:42:36 +00:00
orgs , err := client . OrganizationsByUser ( cmd . Context ( ) , codersdk . Me )
2022-02-10 14:33:27 +00:00
if err != nil {
2022-03-22 19:17:50 +00:00
return codersdk . Organization { } , nil
2022-02-10 14:33:27 +00:00
}
// For now, we won't use the config to set this.
// Eventually, we will support changing using "coder switch <org>"
return orgs [ 0 ] , nil
}
2022-06-03 17:47:56 +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.
2022-06-03 19:36:08 +00:00
func namedWorkspace ( cmd * cobra . Command , client * codersdk . Client , identifier string ) ( codersdk . Workspace , error ) {
2022-06-03 17:47:56 +00:00
parts := strings . Split ( identifier , "/" )
var owner , name string
switch len ( parts ) {
case 1 :
owner = codersdk . Me
name = parts [ 0 ]
case 2 :
owner = parts [ 0 ]
name = parts [ 1 ]
default :
return codersdk . Workspace { } , xerrors . Errorf ( "invalid workspace name: %q" , identifier )
}
2022-06-10 14:58:42 +00:00
return client . WorkspaceByOwnerAndName ( cmd . Context ( ) , 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.
2022-02-10 14:33:27 +00:00
func createConfig ( cmd * cobra . Command ) config . Root {
2022-10-21 22:08:23 +00:00
globalRoot , err := cmd . Flags ( ) . GetString ( config . FlagName )
2022-02-10 14:33:27 +00:00
if err != nil {
panic ( err )
}
return config . Root ( globalRoot )
}
// isTTY returns whether the passed reader is a TTY or not.
// This accepts a reader to work with Cobra's "InOrStdin"
// function for simple testing.
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
func isTTY ( cmd * cobra . Command ) bool {
// 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)
forceTty , err := cmd . Flags ( ) . GetBool ( varForceTty )
if forceTty && err == nil {
return true
}
2022-03-22 19:17:50 +00:00
file , ok := cmd . InOrStdin ( ) . ( * 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.
// This accepts a reader to work with Cobra's "OutOrStdout"
// function for simple testing.
func isTTYOut ( cmd * cobra . Command ) bool {
2022-11-07 18:12:39 +00:00
return isTTYWriter ( cmd , cmd . OutOrStdout )
}
// isTTYErr returns whether the passed reader is a TTY or not.
// This accepts a reader to work with Cobra's "ErrOrStderr"
// function for simple testing.
func isTTYErr ( cmd * cobra . Command ) bool {
return isTTYWriter ( cmd , cmd . ErrOrStderr )
}
func isTTYWriter ( cmd * cobra . Command , writer func ( ) 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)
forceTty , err := cmd . Flags ( ) . GetBool ( varForceTty )
if forceTty && err == nil {
return true
}
2022-11-07 18:12:39 +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-12 20:24:53 +00:00
var templateFunctions = template . FuncMap {
"usageHeader" : usageHeader ,
"isWorkspaceCommand" : isWorkspaceCommand ,
}
func usageHeader ( s string ) string {
// Customizes the color of headings to make subcommands more visually
// appealing.
return cliui . Styles . Placeholder . Render ( s )
}
func isWorkspaceCommand ( cmd * cobra . Command ) bool {
if _ , ok := cmd . Annotations [ "workspaces" ] ; ok {
return true
}
var ws bool
cmd . VisitParents ( func ( cmd * cobra . Command ) {
if _ , ok := cmd . Annotations [ "workspaces" ] ; ok {
ws = true
}
} )
return ws
}
2022-05-09 22:42:02 +00:00
func usageTemplate ( ) string {
2022-05-18 14:10:40 +00:00
// usageHeader is defined in init().
2022-05-16 17:01:42 +00:00
return ` { { usageHeader "Usage:" } }
{ { - if . Runnable } }
{ { . UseLine } }
{ { end } }
{ { - if . HasAvailableSubCommands } }
{ { . CommandPath } } [ command ]
{ { end } }
{ { - if gt ( len . Aliases ) 0 } }
{ { usageHeader "Aliases:" } }
{ { . NameAndAliases } }
{ { end } }
{ { - if . HasExample } }
{ { usageHeader "Get Started:" } }
2022-05-09 22:42:02 +00:00
{ { . Example } }
2022-05-16 17:01:42 +00:00
{ { end } }
2022-07-12 20:24:53 +00:00
{ { - $ isRootHelp := ( not . HasParent ) } }
2022-05-16 17:01:42 +00:00
{ { - if . HasAvailableSubCommands } }
{ { usageHeader "Commands:" } }
{ { - range . Commands } }
2022-07-12 20:24:53 +00:00
{ { - $ isRootWorkspaceCommand := ( and $ isRootHelp ( isWorkspaceCommand . ) ) } }
{ { - if ( or ( and . IsAvailableCommand ( not $ isRootWorkspaceCommand ) ) ( eq . Name "help" ) ) } }
2022-05-16 17:01:42 +00:00
{ { rpad . Name . NamePadding } } { { . Short } }
{ { - end } }
{ { - end } }
{ { end } }
2022-07-12 20:24:53 +00:00
{ { - if ( and $ isRootHelp . HasAvailableSubCommands ) } }
2022-05-16 17:01:42 +00:00
{ { usageHeader "Workspace Commands:" } }
{ { - range . Commands } }
2022-07-12 20:24:53 +00:00
{ { - if ( and . IsAvailableCommand ( isWorkspaceCommand . ) ) } }
2022-05-16 17:01:42 +00:00
{ { rpad . Name . NamePadding } } { { . Short } }
{ { - end } }
{ { - end } }
{ { end } }
2022-05-09 22:42:02 +00:00
2022-05-16 17:01:42 +00:00
{ { - if . HasAvailableLocalFlags } }
{ { usageHeader "Flags:" } }
2022-07-12 20:24:53 +00:00
{ { . LocalFlags . FlagUsagesWrapped 100 | trimTrailingWhitespaces } }
2022-05-16 17:01:42 +00:00
{ { end } }
2022-05-09 22:42:02 +00:00
2022-05-16 17:01:42 +00:00
{ { - if . HasAvailableInheritedFlags } }
{ { usageHeader "Global Flags:" } }
2022-07-12 20:24:53 +00:00
{ { . InheritedFlags . FlagUsagesWrapped 100 | trimTrailingWhitespaces } }
2022-05-16 17:01:42 +00:00
{ { end } }
2022-05-09 22:42:02 +00:00
2022-05-16 17:01:42 +00:00
{ { - if . HasHelpSubCommands } }
{ { usageHeader "Additional help topics:" } }
{ { - range . Commands } }
{ { - if . IsAdditionalHelpTopicCommand } }
{ { rpad . CommandPath . CommandPathPadding } } { { . Short } }
{ { - end } }
{ { - end } }
{ { end } }
2022-05-09 22:42:02 +00:00
2022-05-16 17:01:42 +00:00
{ { - if . HasAvailableSubCommands } }
2022-05-09 22:42:02 +00:00
Use "{{.CommandPath}} [command] --help" for more information about a command .
2022-05-16 17:01:42 +00:00
{ { end } } `
2022-05-09 22:42:02 +00:00
}
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 {
wrap := cliui . Styles . Wrap . Copy ( )
wrap . PaddingLeft ( 4 )
var sb strings . Builder
for i , e := range examples {
if len ( e . Description ) > 0 {
_ , _ = sb . WriteString ( " - " + wrap . Render ( e . Description + ":" ) [ 4 : ] + "\n\n " )
}
// We add 1 space here because `cliui.Styles.Code` adds an extra
// space. This makes the code block align at an even 2 or 6
// spaces for symmetry.
_ , _ = sb . WriteString ( " " + cliui . Styles . Code . Render ( fmt . Sprintf ( "$ %s" , e . Command ) ) )
if i < len ( examples ) - 1 {
_ , _ = sb . WriteString ( "\n\n" )
}
}
return sb . String ( )
}
2022-05-19 22:35:59 +00:00
// FormatCobraError colorizes and adds "--help" docs to cobra commands.
func FormatCobraError ( err error , cmd * cobra . Command ) string {
2022-05-20 09:42:56 +00:00
helpErrMsg := fmt . Sprintf ( "Run '%s --help' for usage." , cmd . CommandPath ( ) )
2022-07-20 20:17:51 +00:00
var (
httpErr * codersdk . Error
output strings . Builder
)
if xerrors . As ( err , & httpErr ) {
_ , _ = fmt . Fprintln ( & output , httpErr . Friendly ( ) )
}
// If the httpErr is nil then we just have a regular error in which
// case we want to print out what's happening.
if httpErr == nil || cliflag . IsSetBool ( cmd , varVerbose ) {
_ , _ = fmt . Fprintln ( & output , err . Error ( ) )
}
_ , _ = fmt . Fprint ( & output , helpErrMsg )
return cliui . Styles . Error . Render ( output . String ( ) )
2022-05-19 22:35:59 +00:00
}
2022-06-29 22:49:40 +00:00
func checkVersions ( cmd * cobra . Command , client * codersdk . Client ) error {
2022-07-20 20:17:51 +00:00
if cliflag . IsSetBool ( cmd , varNoVersionCheck ) {
2022-06-29 22:49:40 +00:00
return nil
}
2022-09-08 13:59:28 +00:00
ctx , cancel := context . WithTimeout ( cmd . Context ( ) , 10 * time . Second )
defer cancel ( )
2022-06-29 22:49:40 +00:00
2022-09-08 13:59:28 +00:00
clientVersion := buildinfo . Version ( )
info , err := client . BuildInfo ( ctx )
2022-07-21 19:28:24 +00:00
// Avoid printing errors that are connection-related.
if codersdk . IsConnectionErr ( err ) {
return nil
}
2022-06-29 22:49:40 +00:00
if err != nil {
return xerrors . Errorf ( "build info: %w" , err )
}
fmtWarningText := ` version mismatch : client % s , server % s
`
2022-12-14 16:36:28 +00:00
// Our installation script doesn't work on Windows, so instead we direct the user
// to the GitHub release page to download the latest installer.
if runtime . GOOS == "windows" {
fmtWarningText += ` download the server version from: https://github.com/coder/coder/releases/v%s `
} else {
fmtWarningText += ` download the server version with: 'curl -L https://coder.com/install.sh | sh -s -- --version %s' `
}
2022-06-29 22:49:40 +00:00
if ! buildinfo . VersionsMatch ( clientVersion , info . Version ) {
warn := cliui . Styles . Warn . Copy ( ) . Align ( lipgloss . Left )
2022-07-26 20:47:12 +00:00
_ , _ = fmt . Fprintf ( cmd . ErrOrStderr ( ) , warn . Render ( fmtWarningText ) , clientVersion , info . Version , strings . TrimPrefix ( info . CanonicalVersion ( ) , "v" ) )
_ , _ = fmt . Fprintln ( cmd . ErrOrStderr ( ) )
2022-06-29 22:49:40 +00:00
}
return nil
}
2022-08-29 18:30:06 +00:00
2022-09-08 13:59:28 +00:00
func checkWarnings ( cmd * cobra . Command , client * codersdk . Client ) error {
2022-08-29 18:30:06 +00:00
if cliflag . IsSetBool ( cmd , varNoFeatureWarning ) {
2022-09-08 13:59:28 +00:00
return nil
2022-08-29 18:30:06 +00:00
}
2022-09-08 13:59:28 +00:00
ctx , cancel := context . WithTimeout ( cmd . Context ( ) , 10 * time . Second )
defer cancel ( )
entitlements , err := client . Entitlements ( ctx )
2022-09-20 04:11:01 +00:00
if err == nil {
for _ , w := range entitlements . Warnings {
_ , _ = fmt . Fprintln ( cmd . ErrOrStderr ( ) , cliui . Styles . Warn . Render ( w ) )
}
2022-08-29 18:30:06 +00:00
}
2022-09-08 13:59:28 +00:00
return nil
2022-08-29 18:30:06 +00:00
}
2022-09-12 21:22:05 +00:00
type headerTransport struct {
transport http . RoundTripper
headers map [ string ] string
}
func ( h * headerTransport ) RoundTrip ( req * http . Request ) ( * http . Response , error ) {
for k , v := range h . headers {
req . Header . Add ( k , v )
}
return h . transport . RoundTrip ( req )
}
2023-01-11 16:22:20 +00:00
// dumpHandler provides a custom SIGQUIT and SIGTRAP handler that dumps the
// 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.
func dumpHandler ( ctx context . Context ) {
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 ( )
}
fpath := filepath . Join ( dir , fmt . Sprintf ( "coder-agent-%s.dump" , time . Now ( ) . Format ( "2006-01-02T15:04:05.000Z" ) ) )
_ , _ = 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 )
}
}
}