2022-03-22 19:17:50 +00:00
package cli
import (
2022-06-08 08:45:29 +00:00
"bufio"
"bytes"
"context"
"errors"
2022-03-30 22:59:54 +00:00
"fmt"
2022-06-08 08:45:29 +00:00
"io"
"io/fs"
2022-03-30 22:59:54 +00:00
"os"
"path/filepath"
"runtime"
2022-06-08 08:45:29 +00:00
"sort"
2022-03-30 22:59:54 +00:00
"strings"
"github.com/cli/safeexec"
2022-06-08 08:45:29 +00:00
"github.com/pkg/diff"
"github.com/pkg/diff/write"
2022-03-22 19:17:50 +00:00
"github.com/spf13/cobra"
2022-06-08 08:45:29 +00:00
"golang.org/x/exp/slices"
2022-03-30 22:59:54 +00:00
"golang.org/x/sync/errgroup"
"golang.org/x/xerrors"
"github.com/coder/coder/cli/cliflag"
"github.com/coder/coder/cli/cliui"
"github.com/coder/coder/codersdk"
2022-03-22 19:17:50 +00:00
)
2022-06-08 08:45:29 +00:00
const (
2022-06-15 14:22:30 +00:00
sshDefaultConfigFileName = "~/.ssh/config"
sshStartToken = "# ------------START-CODER-----------"
sshEndToken = "# ------------END-CODER------------"
sshConfigSectionHeader = "# This section is managed by coder. DO NOT EDIT."
sshConfigDocsHeader = `
2022-03-30 22:59:54 +00:00
#
2022-06-15 14:22:30 +00:00
# You should not hand - edit this section unless you are removing it , all
# changes will be lost when running "coder config-ssh" .
`
sshConfigOptionsHeader = ` #
2022-06-08 08:45:29 +00:00
# Last config - ssh options :
`
)
2022-06-15 14:22:30 +00:00
// sshConfigOptions represents options that can be stored and read
2022-06-08 08:45:29 +00:00
// from the coder config in ~/.ssh/coder.
2022-06-15 14:22:30 +00:00
type sshConfigOptions struct {
sshOptions [ ] string
2022-06-08 08:45:29 +00:00
}
2022-06-15 14:22:30 +00:00
func ( o sshConfigOptions ) equal ( other sshConfigOptions ) bool {
2022-06-08 08:45:29 +00:00
// Compare without side-effects or regard to order.
opt1 := slices . Clone ( o . sshOptions )
sort . Strings ( opt1 )
opt2 := slices . Clone ( other . sshOptions )
sort . Strings ( opt2 )
2022-06-15 14:22:30 +00:00
return slices . Equal ( opt1 , opt2 )
2022-06-08 08:45:29 +00:00
}
2022-06-15 14:22:30 +00:00
func ( o sshConfigOptions ) asList ( ) ( list [ ] string ) {
2022-06-08 08:45:29 +00:00
for _ , opt := range o . sshOptions {
list = append ( list , fmt . Sprintf ( "ssh-option: %s" , opt ) )
}
return list
}
type sshWorkspaceConfig struct {
Name string
Hosts [ ] string
}
func sshFetchWorkspaceConfigs ( ctx context . Context , client * codersdk . Client ) ( [ ] sshWorkspaceConfig , error ) {
workspaces , err := client . Workspaces ( ctx , codersdk . WorkspaceFilter {
Owner : codersdk . Me ,
} )
if err != nil {
return nil , err
}
var errGroup errgroup . Group
workspaceConfigs := make ( [ ] sshWorkspaceConfig , len ( workspaces ) )
for i , workspace := range workspaces {
i := i
workspace := workspace
errGroup . Go ( func ( ) error {
resources , err := client . TemplateVersionResources ( ctx , workspace . LatestBuild . TemplateVersionID )
if err != nil {
return err
}
wc := sshWorkspaceConfig { Name : workspace . Name }
2022-07-21 19:49:32 +00:00
var agents [ ] codersdk . WorkspaceAgent
2022-06-08 08:45:29 +00:00
for _ , resource := range resources {
if resource . Transition != codersdk . WorkspaceTransitionStart {
continue
}
2022-07-21 19:49:32 +00:00
agents = append ( agents , resource . Agents ... )
}
// handle both WORKSPACE and WORKSPACE.AGENT syntax
if len ( agents ) == 1 {
wc . Hosts = append ( wc . Hosts , workspace . Name )
2022-06-08 08:45:29 +00:00
}
2022-07-21 19:49:32 +00:00
for _ , agent := range agents {
hostname := workspace . Name + "." + agent . Name
wc . Hosts = append ( wc . Hosts , hostname )
}
2022-06-08 08:45:29 +00:00
workspaceConfigs [ i ] = wc
return nil
} )
}
err = errGroup . Wait ( )
if err != nil {
return nil , err
}
return workspaceConfigs , nil
}
func sshPrepareWorkspaceConfigs ( ctx context . Context , client * codersdk . Client ) ( receive func ( ) ( [ ] sshWorkspaceConfig , error ) ) {
wcC := make ( chan [ ] sshWorkspaceConfig , 1 )
errC := make ( chan error , 1 )
go func ( ) {
wc , err := sshFetchWorkspaceConfigs ( ctx , client )
wcC <- wc
errC <- err
} ( )
return func ( ) ( [ ] sshWorkspaceConfig , error ) {
return <- wcC , <- errC
}
}
2022-03-22 19:17:50 +00:00
func configSSH ( ) * cobra . Command {
2022-03-30 22:59:54 +00:00
var (
2022-06-15 14:22:30 +00:00
sshConfigFile string
sshConfigOpts sshConfigOptions
2022-06-17 15:03:15 +00:00
usePreviousOpts bool
2022-06-08 08:45:29 +00:00
coderConfigFile string
2022-06-22 15:33:08 +00:00
dryRun bool
2022-04-11 23:54:30 +00:00
skipProxyCommand bool
2022-06-24 21:21:46 +00:00
wireguard bool
2022-03-30 22:59:54 +00:00
)
2022-03-22 19:17:50 +00:00
cmd := & cobra . Command {
2022-05-09 22:42:02 +00:00
Annotations : workspaceCommand ,
Use : "config-ssh" ,
Short : "Populate your SSH config with Host entries for all of your workspaces" ,
2022-07-11 16:08:09 +00:00
Example : formatExamples (
example {
Description : "You can use -o (or --ssh-option) so set SSH options to be used for all your workspaces" ,
Command : "coder config-ssh -o ForwardAgent=yes" ,
} ,
example {
Description : "You can use --dry-run (or -n) to see the changes that would be made" ,
Command : "coder config-ssh --dry-run" ,
} ,
) ,
2022-08-06 17:56:42 +00:00
Args : cobra . ExactArgs ( 0 ) ,
RunE : func ( cmd * cobra . Command , _ [ ] string ) error {
2022-03-30 22:59:54 +00:00
client , err := createClient ( cmd )
if err != nil {
return err
}
2022-06-08 08:45:29 +00:00
recvWorkspaceConfigs := sshPrepareWorkspaceConfigs ( cmd . Context ( ) , client )
out := cmd . OutOrStdout ( )
2022-06-22 15:33:08 +00:00
if dryRun {
// Print everything except diff to stderr so
// that it's possible to capture the diff.
2022-06-08 08:45:29 +00:00
out = cmd . OutOrStderr ( )
}
binaryFile , err := currentBinPath ( out )
if err != nil {
return err
}
2022-06-15 14:22:30 +00:00
homedir , err := os . UserHomeDir ( )
2022-06-08 08:45:29 +00:00
if err != nil {
return xerrors . Errorf ( "user home dir failed: %w" , err )
}
2022-03-30 22:59:54 +00:00
if strings . HasPrefix ( sshConfigFile , "~/" ) {
2022-06-15 14:22:30 +00:00
sshConfigFile = filepath . Join ( homedir , sshConfigFile [ 2 : ] )
2022-03-30 22:59:54 +00:00
}
2022-06-08 08:45:29 +00:00
// Only allow not-exist errors to avoid trashing
// the users SSH config.
configRaw , err := os . ReadFile ( sshConfigFile )
if err != nil && ! errors . Is ( err , fs . ErrNotExist ) {
return xerrors . Errorf ( "read ssh config failed: %w" , err )
}
2022-06-15 14:22:30 +00:00
// Keep track of changes we are making.
var changes [ ] string
// Parse the previous configuration only if config-ssh
// has been run previously.
var lastConfig * sshConfigOptions
var ok bool
var coderConfigRaw [ ] byte
if coderConfigFile , coderConfigRaw , ok = readDeprecatedCoderConfigFile ( homedir , coderConfigFile ) ; ok {
// Deprecated: Remove after migration period.
changes = append ( changes , fmt . Sprintf ( "Remove old auto-generated coder config file at %s" , coderConfigFile ) )
// Backwards compate, restore old options.
c := sshConfigParseLastOptions ( bytes . NewReader ( coderConfigRaw ) )
lastConfig = & c
} else if section , ok := sshConfigGetCoderSection ( configRaw ) ; ok {
c := sshConfigParseLastOptions ( bytes . NewReader ( section ) )
lastConfig = & c
2022-03-30 22:59:54 +00:00
}
2022-04-11 23:54:30 +00:00
2022-06-08 08:45:29 +00:00
// Avoid prompting in diff mode (unexpected behavior)
// or when a previous config does not exist.
2022-06-17 15:03:15 +00:00
if usePreviousOpts && lastConfig != nil {
sshConfigOpts = * lastConfig
2022-06-22 15:33:08 +00:00
} else if lastConfig != nil && ! sshConfigOpts . equal ( * lastConfig ) {
2022-06-15 14:22:30 +00:00
newOpts := sshConfigOpts . asList ( )
2022-06-08 08:45:29 +00:00
newOptsMsg := "\n\n New options: none"
if len ( newOpts ) > 0 {
newOptsMsg = fmt . Sprintf ( "\n\n New options:\n * %s" , strings . Join ( newOpts , "\n * " ) )
}
2022-06-15 14:22:30 +00:00
oldOpts := lastConfig . asList ( )
2022-06-08 08:45:29 +00:00
oldOptsMsg := "\n\n Previous options: none"
if len ( oldOpts ) > 0 {
oldOptsMsg = fmt . Sprintf ( "\n\n Previous options:\n * %s" , strings . Join ( oldOpts , "\n * " ) )
}
line , err := cliui . Prompt ( cmd , cliui . PromptOptions {
Text : fmt . Sprintf ( "New options differ from previous options:%s%s\n\n Use new options?" , newOptsMsg , oldOptsMsg ) ,
IsConfirm : true ,
} )
if err != nil {
if line == "" && xerrors . Is ( err , cliui . Canceled ) {
return nil
}
// Selecting "no" will use the last config.
2022-06-15 14:22:30 +00:00
sshConfigOpts = * lastConfig
2022-06-08 08:45:29 +00:00
}
2022-06-22 15:33:08 +00:00
// Only print when prompts are shown.
if yes , _ := cmd . Flags ( ) . GetBool ( "yes" ) ; ! yes {
_ , _ = fmt . Fprint ( out , "\n" )
}
2022-06-08 08:45:29 +00:00
}
2022-06-15 14:22:30 +00:00
configModified := configRaw
2022-06-08 08:45:29 +00:00
// Check for the presence of the coder Include
// statement is present and add if missing.
2022-06-15 14:22:30 +00:00
// Deprecated: Remove after migration period.
if configModified , ok = removeDeprecatedSSHIncludeStatement ( configModified ) ; ok {
changes = append ( changes , fmt . Sprintf ( "Remove %q from %s" , "Include coder" , sshConfigFile ) )
2022-03-30 22:59:54 +00:00
}
2022-04-11 23:54:30 +00:00
root := createConfig ( cmd )
2022-06-08 08:45:29 +00:00
buf := & bytes . Buffer { }
2022-06-15 14:22:30 +00:00
before , after := sshConfigSplitOnCoderSection ( configModified )
// Write the first half of the users config file to buf.
_ , _ = buf . Write ( before )
2022-06-08 08:45:29 +00:00
2022-06-15 14:22:30 +00:00
// Write comment and store the provided options as part
2022-06-08 08:45:29 +00:00
// of the config for future (re)use.
2022-06-15 14:22:30 +00:00
newline := len ( before ) > 0
sshConfigWriteSectionHeader ( buf , newline , sshConfigOpts )
2022-06-08 08:45:29 +00:00
workspaceConfigs , err := recvWorkspaceConfigs ( )
if err != nil {
return xerrors . Errorf ( "fetch workspace configs failed: %w" , err )
}
// Ensure stable sorting of output.
slices . SortFunc ( workspaceConfigs , func ( a , b sshWorkspaceConfig ) bool {
return a . Name < b . Name
} )
for _ , wc := range workspaceConfigs {
sort . Strings ( wc . Hosts )
// Write agent configuration.
for _ , hostname := range wc . Hosts {
configOptions := [ ] string {
"Host coder." + hostname ,
2022-03-30 22:59:54 +00:00
}
2022-06-15 14:22:30 +00:00
for _ , option := range sshConfigOpts . sshOptions {
2022-06-08 08:45:29 +00:00
configOptions = append ( configOptions , "\t" + option )
2022-03-30 22:59:54 +00:00
}
2022-06-08 08:45:29 +00:00
configOptions = append ( configOptions ,
"\tHostName coder." + hostname ,
"\tConnectTimeout=0" ,
"\tStrictHostKeyChecking=no" ,
// Without this, the "REMOTE HOST IDENTITY CHANGED"
// message will appear.
"\tUserKnownHostsFile=/dev/null" ,
// This disables the "Warning: Permanently added 'hostname' (RSA) to the list of known hosts."
// message from appearing on every SSH. This happens because we ignore the known hosts.
"\tLogLevel ERROR" ,
)
if ! skipProxyCommand {
2022-06-24 21:21:46 +00:00
if ! wireguard {
configOptions = append ( configOptions , fmt . Sprintf ( "\tProxyCommand %q --global-config %q ssh --stdio %s" , binaryFile , root , hostname ) )
} else {
configOptions = append ( configOptions , fmt . Sprintf ( "\tProxyCommand %q --global-config %q ssh --wireguard --stdio %s" , binaryFile , root , hostname ) )
}
2022-06-08 08:45:29 +00:00
}
_ , _ = buf . WriteString ( strings . Join ( configOptions , "\n" ) )
_ = buf . WriteByte ( '\n' )
}
2022-03-30 22:59:54 +00:00
}
2022-06-08 08:45:29 +00:00
2022-06-15 14:22:30 +00:00
sshConfigWriteSectionEnd ( buf )
// Write the remainder of the users config file to buf.
_ , _ = buf . Write ( after )
if ! bytes . Equal ( configModified , buf . Bytes ( ) ) {
changes = append ( changes , fmt . Sprintf ( "Update coder config section in %s" , sshConfigFile ) )
configModified = buf . Bytes ( )
2022-03-30 22:59:54 +00:00
}
2022-06-08 08:45:29 +00:00
2022-06-22 15:33:08 +00:00
if len ( changes ) > 0 {
dryRunDisclaimer := ""
if dryRun {
dryRunDisclaimer = " (dry-run, no changes will be made)"
}
_ , err = cliui . Prompt ( cmd , cliui . PromptOptions {
Text : fmt . Sprintf ( "The following changes will be made to your SSH configuration:\n\n * %s\n\n Continue?%s" , strings . Join ( changes , "\n * " ) , dryRunDisclaimer ) ,
IsConfirm : true ,
} )
if err != nil {
return nil
2022-06-08 08:45:29 +00:00
}
2022-06-22 15:33:08 +00:00
// Only print when prompts are shown.
if yes , _ := cmd . Flags ( ) . GetBool ( "yes" ) ; ! yes {
_ , _ = fmt . Fprint ( out , "\n" )
}
}
2022-06-08 08:45:29 +00:00
2022-06-22 15:33:08 +00:00
if dryRun {
2022-06-08 08:45:29 +00:00
color := isTTYOut ( cmd )
2022-06-15 14:22:30 +00:00
diffFns := [ ] func ( ) ( [ ] byte , error ) {
2022-06-08 08:45:29 +00:00
func ( ) ( [ ] byte , error ) { return diffBytes ( sshConfigFile , configRaw , configModified , color ) } ,
2022-06-15 14:22:30 +00:00
}
if len ( coderConfigRaw ) > 0 {
// Deprecated: Remove after migration period.
diffFns = append ( diffFns , func ( ) ( [ ] byte , error ) { return diffBytes ( coderConfigFile , coderConfigRaw , nil , color ) } )
}
for _ , diffFn := range diffFns {
2022-06-08 08:45:29 +00:00
diff , err := diffFn ( )
if err != nil {
return xerrors . Errorf ( "diff failed: %w" , err )
}
if len ( diff ) > 0 {
2022-06-22 15:33:08 +00:00
// Write diff to stdout.
2022-06-08 08:45:29 +00:00
_ , _ = fmt . Fprintf ( cmd . OutOrStdout ( ) , "\n%s" , diff )
}
}
2022-06-22 15:33:08 +00:00
} else {
2022-06-08 08:45:29 +00:00
if ! bytes . Equal ( configRaw , configModified ) {
err = writeWithTempFileAndMove ( sshConfigFile , bytes . NewReader ( configModified ) )
if err != nil {
return xerrors . Errorf ( "write ssh config failed: %w" , err )
}
}
2022-06-15 14:22:30 +00:00
// Deprecated: Remove after migration period.
if len ( coderConfigRaw ) > 0 {
err = os . Remove ( coderConfigFile )
2022-06-08 08:45:29 +00:00
if err != nil {
2022-06-15 14:22:30 +00:00
return xerrors . Errorf ( "remove coder config failed: %w" , err )
2022-06-08 08:45:29 +00:00
}
}
}
if len ( workspaceConfigs ) > 0 {
_ , _ = fmt . Fprintln ( out , "You should now be able to ssh into your workspace." )
_ , _ = fmt . Fprintf ( out , "For example, try running:\n\n\t$ ssh coder.%s\n\n" , workspaceConfigs [ 0 ] . Name )
} else {
_ , _ = fmt . Fprint ( out , "You don't have any workspaces yet, try creating one with:\n\n\t$ coder create <workspace>\n\n" )
2022-03-30 22:59:54 +00:00
}
2022-03-22 19:17:50 +00:00
return nil
} ,
}
2022-06-15 14:22:30 +00:00
cliflag . StringVarP ( cmd . Flags ( ) , & sshConfigFile , "ssh-config-file" , "" , "CODER_SSH_CONFIG_FILE" , sshDefaultConfigFileName , "Specifies the path to an SSH config." )
cmd . Flags ( ) . StringArrayVarP ( & sshConfigOpts . sshOptions , "ssh-option" , "o" , [ ] string { } , "Specifies additional SSH options to embed in each host stanza." )
2022-06-22 15:33:08 +00:00
cmd . Flags ( ) . BoolVarP ( & dryRun , "dry-run" , "n" , false , "Perform a trial run with no changes made, showing a diff at the end." )
2022-04-11 23:54:30 +00:00
cmd . Flags ( ) . BoolVarP ( & skipProxyCommand , "skip-proxy-command" , "" , false , "Specifies whether the ProxyCommand option should be skipped. Useful for testing." )
_ = cmd . Flags ( ) . MarkHidden ( "skip-proxy-command" )
2022-06-17 15:03:15 +00:00
cliflag . BoolVarP ( cmd . Flags ( ) , & usePreviousOpts , "use-previous-options" , "" , "CODER_SSH_USE_PREVIOUS_OPTIONS" , false , "Specifies whether or not to keep options from previous run of config-ssh." )
2022-06-24 21:21:46 +00:00
cliflag . BoolVarP ( cmd . Flags ( ) , & wireguard , "wireguard" , "" , "CODER_CONFIG_SSH_WIREGUARD" , false , "Whether to use Wireguard for SSH tunneling." )
_ = cmd . Flags ( ) . MarkHidden ( "wireguard" )
2022-03-22 19:17:50 +00:00
2022-06-15 14:22:30 +00:00
// Deprecated: Remove after migration period.
cmd . Flags ( ) . StringVar ( & coderConfigFile , "test.ssh-coder-config-file" , sshDefaultCoderConfigFileName , "Specifies the path to an Coder SSH config file. Useful for testing." )
_ = cmd . Flags ( ) . MarkHidden ( "test.ssh-coder-config-file" )
2022-06-08 08:45:29 +00:00
2022-06-17 15:03:15 +00:00
cliui . AllowSkipPrompt ( cmd )
2022-06-15 14:22:30 +00:00
return cmd
2022-06-08 08:45:29 +00:00
}
2022-06-15 14:22:30 +00:00
//nolint:revive
func sshConfigWriteSectionHeader ( w io . Writer , addNewline bool , o sshConfigOptions ) {
nl := "\n"
if ! addNewline {
nl = ""
2022-06-08 08:45:29 +00:00
}
2022-06-15 14:22:30 +00:00
_ , _ = fmt . Fprint ( w , nl + sshStartToken + "\n" )
_ , _ = fmt . Fprint ( w , sshConfigSectionHeader )
_ , _ = fmt . Fprint ( w , sshConfigDocsHeader )
if len ( o . sshOptions ) > 0 {
_ , _ = fmt . Fprint ( w , sshConfigOptionsHeader )
for _ , opt := range o . sshOptions {
_ , _ = fmt . Fprintf ( w , "# :%s=%s\n" , "ssh-option" , opt )
}
2022-06-08 08:45:29 +00:00
}
_ , _ = fmt . Fprint ( w , "#\n" )
}
2022-06-15 14:22:30 +00:00
func sshConfigWriteSectionEnd ( w io . Writer ) {
_ , _ = fmt . Fprint ( w , sshEndToken + "\n" )
}
2022-06-08 08:45:29 +00:00
2022-06-15 14:22:30 +00:00
func sshConfigParseLastOptions ( r io . Reader ) ( o sshConfigOptions ) {
2022-06-08 08:45:29 +00:00
s := bufio . NewScanner ( r )
for s . Scan ( ) {
line := s . Text ( )
if strings . HasPrefix ( line , "# :" ) {
line = strings . TrimPrefix ( line , "# :" )
parts := strings . SplitN ( line , "=" , 2 )
switch parts [ 0 ] {
case "ssh-option" :
o . sshOptions = append ( o . sshOptions , parts [ 1 ] )
default :
// Unknown option, ignore.
}
}
}
if err := s . Err ( ) ; err != nil {
panic ( err )
}
return o
}
2022-06-15 14:22:30 +00:00
func sshConfigGetCoderSection ( data [ ] byte ) ( section [ ] byte , ok bool ) {
startIndex := bytes . Index ( data , [ ] byte ( sshStartToken ) )
endIndex := bytes . Index ( data , [ ] byte ( sshEndToken ) )
if startIndex != - 1 && endIndex != - 1 {
return data [ startIndex : endIndex + len ( sshEndToken ) ] , true
}
return nil , false
}
// sshConfigSplitOnCoderSection splits the SSH config into two sections,
// before contains the lines before sshStartToken and after contains the
// lines after sshEndToken.
func sshConfigSplitOnCoderSection ( data [ ] byte ) ( before , after [ ] byte ) {
startIndex := bytes . Index ( data , [ ] byte ( sshStartToken ) )
endIndex := bytes . Index ( data , [ ] byte ( sshEndToken ) )
if startIndex != - 1 && endIndex != - 1 {
// We use -1 and +1 here to also include the preceding
// and trailing newline, where applicable.
start := startIndex
if start > 0 {
start --
}
end := endIndex + len ( sshEndToken )
if end < len ( data ) {
end ++
}
return data [ : start ] , data [ end : ]
}
return data , nil
}
2022-06-08 08:45:29 +00:00
// writeWithTempFileAndMove writes to a temporary file in the same
// directory as path and renames the temp file to the file provided in
// path. This ensure we avoid trashing the file we are writing due to
// unforeseen circumstance like filesystem full, command killed, etc.
func writeWithTempFileAndMove ( path string , r io . Reader ) ( err error ) {
dir := filepath . Dir ( path )
name := filepath . Base ( path )
// Create a tempfile in the same directory for ensuring write
// operation does not fail.
f , err := os . CreateTemp ( dir , fmt . Sprintf ( ".%s." , name ) )
if err != nil {
return xerrors . Errorf ( "create temp file failed: %w" , err )
}
defer func ( ) {
if err != nil {
_ = os . Remove ( f . Name ( ) ) // Cleanup in case a step failed.
}
} ( )
_ , err = io . Copy ( f , r )
if err != nil {
_ = f . Close ( )
return xerrors . Errorf ( "write temp file failed: %w" , err )
}
err = f . Close ( )
if err != nil {
return xerrors . Errorf ( "close temp file failed: %w" , err )
}
err = os . Rename ( f . Name ( ) , path )
if err != nil {
return xerrors . Errorf ( "rename temp file failed: %w" , err )
}
return nil
}
2022-03-30 22:59:54 +00:00
// currentBinPath returns the path to the coder binary suitable for use in ssh
// ProxyCommand.
2022-06-08 08:45:29 +00:00
func currentBinPath ( w io . Writer ) ( string , error ) {
2022-03-30 22:59:54 +00:00
exePath , err := os . Executable ( )
if err != nil {
return "" , xerrors . Errorf ( "get executable path: %w" , err )
}
binName := filepath . Base ( exePath )
// We use safeexec instead of os/exec because os/exec returns paths in
// the current working directory, which we will run into very often when
// looking for our own path.
pathPath , err := safeexec . LookPath ( binName )
// On Windows, the coder-cli executable must be in $PATH for both Msys2/Git
// Bash and OpenSSH for Windows (used by Powershell and VS Code) to function
// correctly. Check if the current executable is in $PATH, and warn the user
// if it isn't.
if err != nil && runtime . GOOS == "windows" {
2022-06-08 08:45:29 +00:00
cliui . Warn ( w ,
2022-03-30 22:59:54 +00:00
"The current executable is not in $PATH." ,
"This may lead to problems connecting to your workspace via SSH." ,
fmt . Sprintf ( "Please move %q to a location in your $PATH (such as System32) and run `%s config-ssh` again." , binName , binName ) ,
)
2022-06-08 08:45:29 +00:00
_ , _ = fmt . Fprint ( w , "\n" )
2022-03-30 22:59:54 +00:00
// Return the exePath so SSH at least works outside of Msys2.
return exePath , nil
}
// Warn the user if the current executable is not the same as the one in
// $PATH.
if filepath . Clean ( pathPath ) != filepath . Clean ( exePath ) {
2022-06-08 08:45:29 +00:00
cliui . Warn ( w ,
2022-03-30 22:59:54 +00:00
"The current executable path does not match the executable path found in $PATH." ,
"This may cause issues connecting to your workspace via SSH." ,
fmt . Sprintf ( "\tCurrent executable path: %q" , exePath ) ,
fmt . Sprintf ( "\tExecutable path in $PATH: %q" , pathPath ) ,
)
2022-06-08 08:45:29 +00:00
_ , _ = fmt . Fprint ( w , "\n" )
2022-03-30 22:59:54 +00:00
}
2022-06-27 17:15:55 +00:00
return exePath , nil
2022-03-30 22:59:54 +00:00
}
2022-06-08 08:45:29 +00:00
// diffBytes takes two byte slices and diffs them as if they were in a
// file named name.
2022-08-21 22:32:53 +00:00
// nolint: revive // Color is an option, not a control coupling.
2022-06-08 08:45:29 +00:00
func diffBytes ( name string , b1 , b2 [ ] byte , color bool ) ( [ ] byte , error ) {
var buf bytes . Buffer
var opts [ ] write . Option
if color {
opts = append ( opts , write . TerminalColor ( ) )
}
2022-06-22 15:33:08 +00:00
err := diff . Text ( name , name , b1 , b2 , & buf , opts ... )
2022-06-08 08:45:29 +00:00
if err != nil {
return nil , err
}
b := buf . Bytes ( )
// Check if diff only output two lines, if yes, there's no diff.
//
// Example:
// --- /home/user/.ssh/config
2022-06-22 15:33:08 +00:00
// +++ /home/user/.ssh/config
2022-06-08 08:45:29 +00:00
if bytes . Count ( b , [ ] byte { '\n' } ) == 2 {
b = nil
}
return b , nil
}