Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

feat: modify config-ssh to check for Coder Connect#17419

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
spikecurtis merged 1 commit intomainfromspike/config-ssh-match-suffix
Apr 17, 2025
Merged
Show file tree
Hide file tree
Changes fromall commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
283 changes: 155 additions & 128 deletionscli/configssh.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -48,13 +48,17 @@ const (
type sshConfigOptions struct {
waitEnum string
// Deprecated: moving away from prefix to hostnameSuffix
userHostPrefix string
hostnameSuffix string
sshOptions []string
disableAutostart bool
header []string
headerCommand string
removedKeys map[string]bool
userHostPrefix string
hostnameSuffix string
sshOptions []string
disableAutostart bool
header []string
headerCommand string
removedKeys map[string]bool
globalConfigPath string
coderBinaryPath string
skipProxyCommand bool
forceUnixSeparators bool
}

// addOptions expects options in the form of "option=value" or "option value".
Expand DownExpand Up@@ -107,6 +111,80 @@ func (o sshConfigOptions) equal(other sshConfigOptions) bool {
o.hostnameSuffix == other.hostnameSuffix
}

func (o sshConfigOptions) writeToBuffer(buf *bytes.Buffer) error {
escapedCoderBinary, err := sshConfigExecEscape(o.coderBinaryPath, o.forceUnixSeparators)
if err != nil {
return xerrors.Errorf("escape coder binary for ssh failed: %w", err)
}

escapedGlobalConfig, err := sshConfigExecEscape(o.globalConfigPath, o.forceUnixSeparators)
if err != nil {
return xerrors.Errorf("escape global config for ssh failed: %w", err)
}

rootFlags := fmt.Sprintf("--global-config %s", escapedGlobalConfig)
for _, h := range o.header {
rootFlags += fmt.Sprintf(" --header %q", h)
}
if o.headerCommand != "" {
rootFlags += fmt.Sprintf(" --header-command %q", o.headerCommand)
}

flags := ""
if o.waitEnum != "auto" {
flags += " --wait=" + o.waitEnum
}
if o.disableAutostart {
flags += " --disable-autostart=true"
}

// Prefix block:
if o.userHostPrefix != "" {
_, _ = buf.WriteString("Host")

_, _ = buf.WriteString(" ")
_, _ = buf.WriteString(o.userHostPrefix)
_, _ = buf.WriteString("*\n")

for _, v := range o.sshOptions {
_, _ = buf.WriteString("\t")
_, _ = buf.WriteString(v)
_, _ = buf.WriteString("\n")
}
if !o.skipProxyCommand && o.userHostPrefix != "" {
_, _ = buf.WriteString("\t")
_, _ = fmt.Fprintf(buf,
"ProxyCommand %s %s ssh --stdio%s --ssh-host-prefix %s %%h",
escapedCoderBinary, rootFlags, flags, o.userHostPrefix,
)
_, _ = buf.WriteString("\n")
}
}

// Suffix block
if o.hostnameSuffix == "" {
return nil
}
_, _ = fmt.Fprintf(buf, "\nHost *.%s\n", o.hostnameSuffix)
for _, v := range o.sshOptions {
_, _ = buf.WriteString("\t")
_, _ = buf.WriteString(v)
_, _ = buf.WriteString("\n")
}
// the ^^ options should always apply, but we only want to use the proxy command if Coder Connect is not running.
if !o.skipProxyCommand {
_, _ = fmt.Fprintf(buf, "\nMatch host *.%s !exec \"%s connect exists %%h\"\n",
o.hostnameSuffix, escapedCoderBinary)
_, _ = buf.WriteString("\t")
_, _ = fmt.Fprintf(buf,
"ProxyCommand %s %s ssh --stdio%s --hostname-suffix %s %%h",
escapedCoderBinary, rootFlags, flags, o.hostnameSuffix,
)
_, _ = buf.WriteString("\n")
}
return nil
}

// slicesSortedEqual compares two slices without side-effects or regard to order.
func slicesSortedEqual[S ~[]E, E constraints.Ordered](a, b S) bool {
if len(a) != len(b) {
Expand DownExpand Up@@ -147,13 +225,11 @@ func (o sshConfigOptions) asList() (list []string) {

func (r *RootCmd) configSSH() *serpent.Command {
var (
sshConfigFile string
sshConfigOpts sshConfigOptions
usePreviousOpts bool
dryRun bool
skipProxyCommand bool
forceUnixSeparators bool
coderCliPath string
sshConfigFile string
sshConfigOpts sshConfigOptions
usePreviousOpts bool
dryRun bool
coderCliPath string
)
client := new(codersdk.Client)
cmd := &serpent.Command{
Expand All@@ -177,7 +253,7 @@ func (r *RootCmd) configSSH() *serpent.Command {
Handler: func(inv *serpent.Invocation) error {
ctx := inv.Context()

if sshConfigOpts.waitEnum != "auto" && skipProxyCommand {
if sshConfigOpts.waitEnum != "auto" &&sshConfigOpts.skipProxyCommand {
// The wait option is applied to the ProxyCommand. If the user
// specifies skip-proxy-command, then wait cannot be applied.
return xerrors.Errorf("cannot specify both --skip-proxy-command and --wait")
Expand DownExpand Up@@ -207,18 +283,7 @@ func (r *RootCmd) configSSH() *serpent.Command {
return err
}
}

escapedCoderBinary, err := sshConfigExecEscape(coderBinary, forceUnixSeparators)
if err != nil {
return xerrors.Errorf("escape coder binary for ssh failed: %w", err)
}

root := r.createConfig()
escapedGlobalConfig, err := sshConfigExecEscape(string(root), forceUnixSeparators)
if err != nil {
return xerrors.Errorf("escape global config for ssh failed: %w", err)
}

homedir, err := os.UserHomeDir()
if err != nil {
return xerrors.Errorf("user home dir failed: %w", err)
Expand DownExpand Up@@ -320,94 +385,15 @@ func (r *RootCmd) configSSH() *serpent.Command {
coderdConfig.HostnamePrefix = "coder."
}

if sshConfigOpts.userHostPrefix != "" {
// Override with user flag.
coderdConfig.HostnamePrefix = sshConfigOpts.userHostPrefix
}
if sshConfigOpts.hostnameSuffix != "" {
// Override with user flag.
coderdConfig.HostnameSuffix = sshConfigOpts.hostnameSuffix
}

// Write agent configuration.
defaultOptions := []string{
"ConnectTimeout=0",
"StrictHostKeyChecking=no",
// Without this, the "REMOTE HOST IDENTITY CHANGED"
// message will appear.
"UserKnownHostsFile=/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.
"LogLevel ERROR",
}

if !skipProxyCommand {
rootFlags := fmt.Sprintf("--global-config %s", escapedGlobalConfig)
for _, h := range sshConfigOpts.header {
rootFlags += fmt.Sprintf(" --header %q", h)
}
if sshConfigOpts.headerCommand != "" {
rootFlags += fmt.Sprintf(" --header-command %q", sshConfigOpts.headerCommand)
}

flags := ""
if sshConfigOpts.waitEnum != "auto" {
flags += " --wait=" + sshConfigOpts.waitEnum
}
if sshConfigOpts.disableAutostart {
flags += " --disable-autostart=true"
}
if coderdConfig.HostnamePrefix != "" {
flags += " --ssh-host-prefix " + coderdConfig.HostnamePrefix
}
if coderdConfig.HostnameSuffix != "" {
flags += " --hostname-suffix " + coderdConfig.HostnameSuffix
}
defaultOptions = append(defaultOptions, fmt.Sprintf(
"ProxyCommand %s %s ssh --stdio%s %%h",
escapedCoderBinary, rootFlags, flags,
))
}

// Create a copy of the options so we can modify them.
configOptions := sshConfigOpts
configOptions.sshOptions = nil

// User options first (SSH only uses the first
// option unless it can be given multiple times)
for _, opt := range sshConfigOpts.sshOptions {
err := configOptions.addOptions(opt)
if err != nil {
return xerrors.Errorf("add flag config option %q: %w", opt, err)
}
}

// Deployment options second, allow them to
// override standard options.
for k, v := range coderdConfig.SSHConfigOptions {
opt := fmt.Sprintf("%s %s", k, v)
err := configOptions.addOptions(opt)
if err != nil {
return xerrors.Errorf("add coderd config option %q: %w", opt, err)
}
}

// Finally, add the standard options.
if err := configOptions.addOptions(defaultOptions...); err != nil {
configOptions, err := mergeSSHOptions(sshConfigOpts, coderdConfig, string(root), coderBinary)
if err != nil {
return err
}

hostBlock := []string{
sshConfigHostLinePatterns(coderdConfig),
}
// Prefix with '\t'
for _, v := range configOptions.sshOptions {
hostBlock = append(hostBlock, "\t"+v)
err = configOptions.writeToBuffer(buf)
if err != nil {
return err
}

_, _ = buf.WriteString(strings.Join(hostBlock, "\n"))
_ = buf.WriteByte('\n')

sshConfigWriteSectionEnd(buf)

// Write the remainder of the users config file to buf.
Expand DownExpand Up@@ -523,7 +509,7 @@ func (r *RootCmd) configSSH() *serpent.Command {
Flag: "skip-proxy-command",
Env: "CODER_SSH_SKIP_PROXY_COMMAND",
Description: "Specifies whether the ProxyCommand option should be skipped. Useful for testing.",
Value: serpent.BoolOf(&skipProxyCommand),
Value: serpent.BoolOf(&sshConfigOpts.skipProxyCommand),
Hidden: true,
},
{
Expand DownExpand Up@@ -564,7 +550,7 @@ func (r *RootCmd) configSSH() *serpent.Command {
Description: "By default, 'config-ssh' uses the os path separator when writing the ssh config. " +
"This might be an issue in Windows machine that use a unix-like shell. " +
"This flag forces the use of unix file paths (the forward slash '/').",
Value: serpent.BoolOf(&forceUnixSeparators),
Value: serpent.BoolOf(&sshConfigOpts.forceUnixSeparators),
// On non-windows showing this command is useless because it is a noop.
// Hide vs disable it though so if a command is copied from a Windows
// machine to a unix machine it will still work and not throw an
Expand All@@ -577,6 +563,63 @@ func (r *RootCmd) configSSH() *serpent.Command {
return cmd
}

func mergeSSHOptions(
user sshConfigOptions, coderd codersdk.SSHConfigResponse, globalConfigPath, coderBinaryPath string,
) (
sshConfigOptions, error,
) {
// Write agent configuration.
defaultOptions := []string{
"ConnectTimeout=0",
"StrictHostKeyChecking=no",
// Without this, the "REMOTE HOST IDENTITY CHANGED"
// message will appear.
"UserKnownHostsFile=/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.
"LogLevel ERROR",
}

// Create a copy of the options so we can modify them.
configOptions := user
configOptions.sshOptions = nil

configOptions.globalConfigPath = globalConfigPath
configOptions.coderBinaryPath = coderBinaryPath
// user config takes precedence
if user.userHostPrefix == "" {
configOptions.userHostPrefix = coderd.HostnamePrefix
}
if user.hostnameSuffix == "" {
configOptions.hostnameSuffix = coderd.HostnameSuffix
}

// User options first (SSH only uses the first
// option unless it can be given multiple times)
for _, opt := range user.sshOptions {
err := configOptions.addOptions(opt)
if err != nil {
return sshConfigOptions{}, xerrors.Errorf("add flag config option %q: %w", opt, err)
}
}

// Deployment options second, allow them to
// override standard options.
for k, v := range coderd.SSHConfigOptions {
opt := fmt.Sprintf("%s %s", k, v)
err := configOptions.addOptions(opt)
if err != nil {
return sshConfigOptions{}, xerrors.Errorf("add coderd config option %q: %w", opt, err)
}
}

// Finally, add the standard options.
if err := configOptions.addOptions(defaultOptions...); err != nil {
return sshConfigOptions{}, err
}
return configOptions, nil
}

//nolint:revive
func sshConfigWriteSectionHeader(w io.Writer, addNewline bool, o sshConfigOptions) {
nl := "\n"
Expand DownExpand Up@@ -844,19 +887,3 @@ func diffBytes(name string, b1, b2 []byte, color bool) ([]byte, error) {
}
return b, nil
}

func sshConfigHostLinePatterns(config codersdk.SSHConfigResponse) string {
builder := strings.Builder{}
// by inspection, WriteString always returns nil error
_, _ = builder.WriteString("Host")
if config.HostnamePrefix != "" {
_, _ = builder.WriteString(" ")
_, _ = builder.WriteString(config.HostnamePrefix)
_, _ = builder.WriteString("*")
}
if config.HostnameSuffix != "" {
_, _ = builder.WriteString(" *.")
_, _ = builder.WriteString(config.HostnameSuffix)
}
return builder.String()
}
15 changes: 11 additions & 4 deletionscli/configssh_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -615,13 +615,21 @@ func TestConfigSSH_FileWriteAndOptionsFlow(t *testing.T) {
name: "Hostname Suffix",
args: []string{
"--yes",
"--ssh-option", "Foo=bar",
"--hostname-suffix", "testy",
},
wantErr: false,
hasAgent: true,
wantConfig: wantConfig{
ssh: []string{"Host coder.* *.testy"},
regexMatch: `ProxyCommand .* ssh .* --hostname-suffix testy %h`,
ssh: []string{
"Host *.testy",
"Foo=bar",
"ConnectTimeout=0",
"StrictHostKeyChecking=no",
"UserKnownHostsFile=/dev/null",
"LogLevel ERROR",
},
regexMatch: `Match host \*\.testy !exec ".* connect exists %h"\n\tProxyCommand .* ssh .* --hostname-suffix testy %h`,
},
},
{
Expand All@@ -634,8 +642,7 @@ func TestConfigSSH_FileWriteAndOptionsFlow(t *testing.T) {
wantErr: false,
hasAgent: true,
wantConfig: wantConfig{
ssh: []string{"Host presto.* *.testy"},
regexMatch: `ProxyCommand .* ssh .* --ssh-host-prefix presto\. --hostname-suffix testy %h`,
ssh: []string{"Host presto.*", "Match host *.testy !exec"},
},
},
}
Expand Down
Loading

[8]ページ先頭

©2009-2025 Movatter.jp