mirror of
https://github.com/coder/coder.git
synced 2025-07-08 11:39:50 +00:00
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.
190 lines
6.5 KiB
Go
190 lines
6.5 KiB
Go
package cli
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/cli/safeexec"
|
|
"github.com/spf13/cobra"
|
|
"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/coderd/database"
|
|
"github.com/coder/coder/codersdk"
|
|
)
|
|
|
|
const sshStartToken = "# ------------START-CODER-----------"
|
|
const sshStartMessage = `# This was generated by "coder config-ssh".
|
|
#
|
|
# To remove this blob, run:
|
|
#
|
|
# coder config-ssh --remove
|
|
#
|
|
# You should not hand-edit this section, unless you are deleting it.`
|
|
const sshEndToken = "# ------------END-CODER------------"
|
|
|
|
func configSSH() *cobra.Command {
|
|
var (
|
|
sshConfigFile string
|
|
sshOptions []string
|
|
skipProxyCommand bool
|
|
)
|
|
cmd := &cobra.Command{
|
|
Use: "config-ssh",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
client, err := createClient(cmd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
organization, err := currentOrganization(cmd, client)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if strings.HasPrefix(sshConfigFile, "~/") {
|
|
dirname, _ := os.UserHomeDir()
|
|
sshConfigFile = filepath.Join(dirname, sshConfigFile[2:])
|
|
}
|
|
// Doesn't matter if this fails, because we write the file anyways.
|
|
sshConfigContentRaw, _ := os.ReadFile(sshConfigFile)
|
|
sshConfigContent := string(sshConfigContentRaw)
|
|
startIndex := strings.Index(sshConfigContent, sshStartToken)
|
|
endIndex := strings.Index(sshConfigContent, sshEndToken)
|
|
if startIndex != -1 && endIndex != -1 {
|
|
sshConfigContent = sshConfigContent[:startIndex-1] + sshConfigContent[endIndex+len(sshEndToken):]
|
|
}
|
|
|
|
workspaces, err := client.WorkspacesByOwner(cmd.Context(), organization.ID, codersdk.Me)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(workspaces) == 0 {
|
|
return xerrors.New("You don't have any workspaces!")
|
|
}
|
|
|
|
binaryFile, err := currentBinPath(cmd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
root := createConfig(cmd)
|
|
sshConfigContent += "\n" + sshStartToken + "\n" + sshStartMessage + "\n\n"
|
|
sshConfigContentMutex := sync.Mutex{}
|
|
var errGroup errgroup.Group
|
|
for _, workspace := range workspaces {
|
|
workspace := workspace
|
|
errGroup.Go(func() error {
|
|
resources, err := client.TemplateVersionResources(cmd.Context(), workspace.LatestBuild.TemplateVersionID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, resource := range resources {
|
|
if resource.Transition != database.WorkspaceTransitionStart {
|
|
continue
|
|
}
|
|
for _, agent := range resource.Agents {
|
|
sshConfigContentMutex.Lock()
|
|
hostname := workspace.Name
|
|
if len(resource.Agents) > 1 {
|
|
hostname += "." + agent.Name
|
|
}
|
|
configOptions := []string{
|
|
"Host coder." + hostname,
|
|
}
|
|
for _, option := range sshOptions {
|
|
configOptions = append(configOptions, "\t"+option)
|
|
}
|
|
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 {
|
|
configOptions = append(configOptions, fmt.Sprintf("\tProxyCommand %q --global-config %q ssh --stdio %s", binaryFile, root, hostname))
|
|
}
|
|
sshConfigContent += strings.Join(configOptions, "\n") + "\n"
|
|
sshConfigContentMutex.Unlock()
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
err = errGroup.Wait()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
sshConfigContent += "\n" + sshEndToken
|
|
err = os.MkdirAll(filepath.Dir(sshConfigFile), os.ModePerm)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = os.WriteFile(sshConfigFile, []byte(sshConfigContent), os.ModePerm)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "An auto-generated ssh config was written to %q\n", sshConfigFile)
|
|
_, _ = fmt.Fprintln(cmd.OutOrStdout(), "You should now be able to ssh into your workspace")
|
|
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "For example, try running\n\n\t$ ssh coder.%s\n\n", workspaces[0].Name)
|
|
return nil
|
|
},
|
|
}
|
|
cliflag.StringVarP(cmd.Flags(), &sshConfigFile, "ssh-config-file", "", "CODER_SSH_CONFIG_FILE", "~/.ssh/config", "Specifies the path to an SSH config.")
|
|
cmd.Flags().StringArrayVarP(&sshOptions, "ssh-option", "o", []string{}, "Specifies additional SSH options to embed in each host stanza.")
|
|
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")
|
|
|
|
return cmd
|
|
}
|
|
|
|
// currentBinPath returns the path to the coder binary suitable for use in ssh
|
|
// ProxyCommand.
|
|
func currentBinPath(cmd *cobra.Command) (string, error) {
|
|
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" {
|
|
cliui.Warn(cmd.OutOrStdout(),
|
|
"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),
|
|
)
|
|
// 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) {
|
|
cliui.Warn(cmd.OutOrStdout(),
|
|
"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),
|
|
)
|
|
}
|
|
|
|
return binName, nil
|
|
}
|