fix: Clean up coder config-ssh dry-run behavior (#3660)

This commit also drops old deprecated code.

Fixes #2982
This commit is contained in:
Mathias Fredriksson
2022-08-24 16:58:46 +03:00
committed by GitHub
parent 7a71180ae6
commit 343d1184b2
3 changed files with 40 additions and 263 deletions

View File

@ -137,7 +137,6 @@ func configSSH() *cobra.Command {
sshConfigFile string sshConfigFile string
sshConfigOpts sshConfigOptions sshConfigOpts sshConfigOptions
usePreviousOpts bool usePreviousOpts bool
coderConfigFile string
dryRun bool dryRun bool
skipProxyCommand bool skipProxyCommand bool
wireguard bool wireguard bool
@ -198,15 +197,7 @@ func configSSH() *cobra.Command {
// Parse the previous configuration only if config-ssh // Parse the previous configuration only if config-ssh
// has been run previously. // has been run previously.
var lastConfig *sshConfigOptions var lastConfig *sshConfigOptions
var ok bool if section, ok := sshConfigGetCoderSection(configRaw); ok {
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)) c := sshConfigParseLastOptions(bytes.NewReader(section))
lastConfig = &c lastConfig = &c
} }
@ -237,6 +228,8 @@ func configSSH() *cobra.Command {
} }
// Selecting "no" will use the last config. // Selecting "no" will use the last config.
sshConfigOpts = *lastConfig sshConfigOpts = *lastConfig
} else {
changes = append(changes, "Use new SSH options")
} }
// Only print when prompts are shown. // Only print when prompts are shown.
if yes, _ := cmd.Flags().GetBool("yes"); !yes { if yes, _ := cmd.Flags().GetBool("yes"); !yes {
@ -245,14 +238,6 @@ func configSSH() *cobra.Command {
} }
configModified := configRaw configModified := configRaw
// Check for the presence of the coder Include
// statement is present and add if missing.
// Deprecated: Remove after migration period.
if configModified, ok = removeDeprecatedSSHIncludeStatement(configModified); ok {
changes = append(changes, fmt.Sprintf("Remove %q from %s", "Include coder", sshConfigFile))
}
root := createConfig(cmd) root := createConfig(cmd)
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
@ -313,17 +298,34 @@ func configSSH() *cobra.Command {
_, _ = buf.Write(after) _, _ = buf.Write(after)
if !bytes.Equal(configModified, buf.Bytes()) { if !bytes.Equal(configModified, buf.Bytes()) {
changes = append(changes, fmt.Sprintf("Update coder config section in %s", sshConfigFile)) changes = append(changes, fmt.Sprintf("Update the coder section in %s", sshConfigFile))
configModified = buf.Bytes() configModified = buf.Bytes()
} }
if len(changes) > 0 { if len(changes) == 0 {
dryRunDisclaimer := "" _, _ = fmt.Fprintf(out, "No changes to make.\n")
if dryRun { return nil
dryRunDisclaimer = " (dry-run, no changes will be made)"
} }
if dryRun {
_, _ = fmt.Fprintf(out, "Dry run, the following changes would be made to your SSH configuration:\n\n * %s\n\n", strings.Join(changes, "\n * "))
color := isTTYOut(cmd)
diff, err := diffBytes(sshConfigFile, configRaw, configModified, color)
if err != nil {
return xerrors.Errorf("diff failed: %w", err)
}
if len(diff) > 0 {
// Write diff to stdout.
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s", diff)
}
return nil
}
if len(changes) > 0 {
_, err = cliui.Prompt(cmd, cliui.PromptOptions{ _, 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), Text: fmt.Sprintf("The following changes will be made to your SSH configuration:\n\n * %s\n\n Continue?", strings.Join(changes, "\n * ")),
IsConfirm: true, IsConfirm: true,
}) })
if err != nil { if err != nil {
@ -335,47 +337,18 @@ func configSSH() *cobra.Command {
} }
} }
if dryRun {
color := isTTYOut(cmd)
diffFns := []func() ([]byte, error){
func() ([]byte, error) { return diffBytes(sshConfigFile, configRaw, configModified, color) },
}
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 {
diff, err := diffFn()
if err != nil {
return xerrors.Errorf("diff failed: %w", err)
}
if len(diff) > 0 {
// Write diff to stdout.
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "\n%s", diff)
}
}
} else {
if !bytes.Equal(configRaw, configModified) { if !bytes.Equal(configRaw, configModified) {
err = writeWithTempFileAndMove(sshConfigFile, bytes.NewReader(configModified)) err = writeWithTempFileAndMove(sshConfigFile, bytes.NewReader(configModified))
if err != nil { if err != nil {
return xerrors.Errorf("write ssh config failed: %w", err) return xerrors.Errorf("write ssh config failed: %w", err)
} }
} }
// Deprecated: Remove after migration period.
if len(coderConfigRaw) > 0 {
err = os.Remove(coderConfigFile)
if err != nil {
return xerrors.Errorf("remove coder config failed: %w", err)
}
}
}
if len(workspaceConfigs) > 0 { if len(workspaceConfigs) > 0 {
_, _ = fmt.Fprintln(out, "You should now be able to ssh into your workspace.") _, _ = 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) _, _ = fmt.Fprintf(out, "For example, try running:\n\n\t$ ssh coder.%s\n", workspaceConfigs[0].Name)
} else { } else {
_, _ = fmt.Fprint(out, "You don't have any workspaces yet, try creating one with:\n\n\t$ coder create <workspace>\n\n") _, _ = fmt.Fprint(out, "You don't have any workspaces yet, try creating one with:\n\n\t$ coder create <workspace>\n")
} }
return nil return nil
}, },
@ -389,10 +362,6 @@ func configSSH() *cobra.Command {
cliflag.BoolVarP(cmd.Flags(), &wireguard, "wireguard", "", "CODER_CONFIG_SSH_WIREGUARD", false, "Whether to use Wireguard for SSH tunneling.") cliflag.BoolVarP(cmd.Flags(), &wireguard, "wireguard", "", "CODER_CONFIG_SSH_WIREGUARD", false, "Whether to use Wireguard for SSH tunneling.")
_ = cmd.Flags().MarkHidden("wireguard") _ = cmd.Flags().MarkHidden("wireguard")
// 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")
cliui.AllowSkipPrompt(cmd) cliui.AllowSkipPrompt(cmd)
return cmd return cmd

View File

@ -1,66 +0,0 @@
package cli
import (
"bytes"
"os"
"path/filepath"
"regexp"
"strings"
)
// This file contains config-ssh definitions that are deprecated, they
// will be removed after a migratory period.
const (
sshDefaultCoderConfigFileName = "~/.ssh/coder"
sshCoderConfigHeader = "# This file is managed by coder. DO NOT EDIT."
)
// Regular expressions are used because SSH configs do not have
// meaningful indentation and keywords are case-insensitive.
var (
// Find the semantically correct include statement. Since the user can
// modify their configuration as they see fit, there could be:
// - Leading indentation (space, tab)
// - Trailing indentation (space, tab)
// - Select newline after Include statement for cleaner removal
// In the following cases, we will not recognize the Include statement
// and leave as-is (i.e. they're not supported):
// - User adds another file to the Include statement
// - User adds a comment on the same line as the Include statement
sshCoderIncludedRe = regexp.MustCompile(`(?m)^[\t ]*((?i)Include) coder[\t ]*[\r]?[\n]?$`)
)
// removeDeprecatedSSHIncludeStatement checks for the Include coder statement
// and returns modified = true if it was removed.
func removeDeprecatedSSHIncludeStatement(data []byte) (modifiedData []byte, modified bool) {
coderInclude := sshCoderIncludedRe.FindIndex(data)
if coderInclude == nil {
return data, false
}
// Remove Include statement.
d := append([]byte{}, data[:coderInclude[0]]...)
d = append(d, data[coderInclude[1]:]...)
data = d
return data, true
}
// readDeprecatedCoderConfigFile reads the deprecated split config file.
func readDeprecatedCoderConfigFile(homedir, coderConfigFile string) (name string, data []byte, ok bool) {
if strings.HasPrefix(coderConfigFile, "~/") {
coderConfigFile = filepath.Join(homedir, coderConfigFile[2:])
}
b, err := os.ReadFile(coderConfigFile)
if err != nil {
return coderConfigFile, nil, false
}
if len(b) > 0 {
if !bytes.HasPrefix(b, []byte(sshCoderConfigHeader)) {
return coderConfigFile, nil, false
}
}
return coderConfigFile, b, true
}

View File

@ -6,7 +6,6 @@ import (
"context" "context"
"fmt" "fmt"
"io" "io"
"io/fs"
"net" "net"
"os" "os"
"os/exec" "os/exec"
@ -30,15 +29,14 @@ import (
"github.com/coder/coder/pty/ptytest" "github.com/coder/coder/pty/ptytest"
) )
func sshConfigFileNames(t *testing.T) (sshConfig string, coderConfig string) { func sshConfigFileName(t *testing.T) (sshConfig string) {
t.Helper() t.Helper()
tmpdir := t.TempDir() tmpdir := t.TempDir()
dotssh := filepath.Join(tmpdir, ".ssh") dotssh := filepath.Join(tmpdir, ".ssh")
err := os.Mkdir(dotssh, 0o700) err := os.Mkdir(dotssh, 0o700)
require.NoError(t, err) require.NoError(t, err)
n1 := filepath.Join(dotssh, "config") n := filepath.Join(dotssh, "config")
n2 := filepath.Join(dotssh, "coder") return n
return n1, n2
} }
func sshConfigFileCreate(t *testing.T, name string, data io.Reader) { func sshConfigFileCreate(t *testing.T, name string, data io.Reader) {
@ -135,7 +133,7 @@ func TestConfigSSH(t *testing.T) {
} }
}() }()
sshConfigFile, _ := sshConfigFileNames(t) sshConfigFile := sshConfigFileName(t)
tcpAddr, valid := listener.Addr().(*net.TCPAddr) tcpAddr, valid := listener.Addr().(*net.TCPAddr)
require.True(t, valid) require.True(t, valid)
@ -198,11 +196,9 @@ func TestConfigSSH_FileWriteAndOptionsFlow(t *testing.T) {
type writeConfig struct { type writeConfig struct {
ssh string ssh string
coder string
} }
type wantConfig struct { type wantConfig struct {
ssh string ssh string
coderKept bool
} }
type match struct { type match struct {
match, write string match, write string
@ -514,120 +510,6 @@ func TestConfigSSH_FileWriteAndOptionsFlow(t *testing.T) {
"--yes", "--yes",
}, },
}, },
// Tests for deprecated split coder config.
{
name: "Do not overwrite unknown coder config",
writeConfig: writeConfig{
ssh: strings.Join([]string{
baseHeader,
"",
}, "\n"),
coder: strings.Join([]string{
"We're no strangers to love",
"You know the rules and so do I (do I)",
}, "\n"),
},
wantConfig: wantConfig{
coderKept: true,
},
},
{
name: "Transfer options from coder to ssh config",
writeConfig: writeConfig{
ssh: strings.Join([]string{
"Include coder",
"",
}, "\n"),
coder: strings.Join([]string{
"# This file is managed by coder. DO NOT EDIT.",
"#",
"# You should not hand-edit this file, all changes will be lost when running",
"# \"coder config-ssh\".",
"#",
"# Last config-ssh options:",
"# :ssh-option=ForwardAgent=yes",
"#",
}, "\n"),
},
wantConfig: wantConfig{
ssh: strings.Join([]string{
headerStart,
"# Last config-ssh options:",
"# :ssh-option=ForwardAgent=yes",
"#",
headerEnd,
"",
}, "\n"),
},
matches: []match{
{match: "Use new options?", write: "no"},
{match: "Continue?", write: "yes"},
},
},
{
name: "Allow overwriting previous options from coder config",
writeConfig: writeConfig{
ssh: strings.Join([]string{
"Include coder",
"",
}, "\n"),
coder: strings.Join([]string{
"# This file is managed by coder. DO NOT EDIT.",
"#",
"# You should not hand-edit this file, all changes will be lost when running",
"# \"coder config-ssh\".",
"#",
"# Last config-ssh options:",
"# :ssh-option=ForwardAgent=yes",
"#",
}, "\n"),
},
wantConfig: wantConfig{
ssh: strings.Join([]string{
baseHeader,
"",
}, "\n"),
},
matches: []match{
{match: "Use new options?", write: "yes"},
{match: "Continue?", write: "yes"},
},
},
{
name: "Allow overwriting previous options from coder config when they differ",
writeConfig: writeConfig{
ssh: strings.Join([]string{
"Include coder",
"",
}, "\n"),
coder: strings.Join([]string{
"# This file is managed by coder. DO NOT EDIT.",
"#",
"# You should not hand-edit this file, all changes will be lost when running",
"# \"coder config-ssh\".",
"#",
"# Last config-ssh options:",
"# :ssh-option=ForwardAgent=yes",
"#",
}, "\n"),
},
wantConfig: wantConfig{
ssh: strings.Join([]string{
headerStart,
"# Last config-ssh options:",
"# :ssh-option=ForwardAgent=no",
"#",
headerEnd,
"",
}, "\n"),
},
args: []string{"--ssh-option", "ForwardAgent=no"},
matches: []match{
{match: "Use new options?", write: "yes"},
{match: "Continue?", write: "yes"},
},
},
} }
for _, tt := range tests { for _, tt := range tests {
tt := tt tt := tt
@ -645,18 +527,14 @@ func TestConfigSSH_FileWriteAndOptionsFlow(t *testing.T) {
) )
// Prepare ssh config files. // Prepare ssh config files.
sshConfigName, coderConfigName := sshConfigFileNames(t) sshConfigName := sshConfigFileName(t)
if tt.writeConfig.ssh != "" { if tt.writeConfig.ssh != "" {
sshConfigFileCreate(t, sshConfigName, strings.NewReader(tt.writeConfig.ssh)) sshConfigFileCreate(t, sshConfigName, strings.NewReader(tt.writeConfig.ssh))
} }
if tt.writeConfig.coder != "" {
sshConfigFileCreate(t, coderConfigName, strings.NewReader(tt.writeConfig.coder))
}
args := []string{ args := []string{
"config-ssh", "config-ssh",
"--ssh-config-file", sshConfigName, "--ssh-config-file", sshConfigName,
"--test.ssh-coder-config-file", coderConfigName,
} }
args = append(args, tt.args...) args = append(args, tt.args...)
cmd, root := clitest.New(t, args...) cmd, root := clitest.New(t, args...)
@ -685,10 +563,6 @@ func TestConfigSSH_FileWriteAndOptionsFlow(t *testing.T) {
got := sshConfigFileRead(t, sshConfigName) got := sshConfigFileRead(t, sshConfigName)
assert.Equal(t, tt.wantConfig.ssh, got) assert.Equal(t, tt.wantConfig.ssh, got)
} }
if !tt.wantConfig.coderKept {
_, err := os.ReadFile(coderConfigName)
assert.ErrorIs(t, err, fs.ErrNotExist)
}
}) })
} }
} }
@ -778,7 +652,7 @@ func TestConfigSSH_Hostnames(t *testing.T) {
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
sshConfigFile, _ := sshConfigFileNames(t) sshConfigFile := sshConfigFileName(t)
cmd, root := clitest.New(t, "config-ssh", "--ssh-config-file", sshConfigFile) cmd, root := clitest.New(t, "config-ssh", "--ssh-config-file", sshConfigFile)
clitest.SetupConfig(t, client, root) clitest.SetupConfig(t, client, root)