Skip to content

feat: use wildcard Host entry in config-ssh #16096

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 our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 14, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
feat: use wildcard Host entry in config-ssh
Rather than create a separate Host entry for every workspace, configure
a wildcard such as `coder.*` which can accomodate all of a user's
workspaces.
  • Loading branch information
aaronlehmann committed Jan 11, 2025
commit 34377578fa8efbb9c3d7a0b5ec69cfd5592cde1c
233 changes: 75 additions & 158 deletions cli/configssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package cli
import (
"bufio"
"bytes"
"context"
"errors"
"fmt"
"io"
Expand All @@ -12,7 +11,6 @@ import (
"os"
"path/filepath"
"runtime"
"sort"
"strconv"
"strings"

Expand All @@ -22,11 +20,9 @@ import (
"github.com/pkg/diff/write"
"golang.org/x/exp/constraints"
"golang.org/x/exp/slices"
"golang.org/x/sync/errgroup"
"golang.org/x/xerrors"

"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/coderd/util/slice"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/serpent"
)
Expand Down Expand Up @@ -139,74 +135,6 @@ func (o sshConfigOptions) asList() (list []string) {
return list
}

type sshWorkspaceConfig struct {
Name string
Hosts []string
}

func sshFetchWorkspaceConfigs(ctx context.Context, client *codersdk.Client) ([]sshWorkspaceConfig, error) {
res, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
Owner: codersdk.Me,
})
if err != nil {
return nil, err
}

var errGroup errgroup.Group
workspaceConfigs := make([]sshWorkspaceConfig, len(res.Workspaces))
for i, workspace := range res.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}
var agents []codersdk.WorkspaceAgent
for _, resource := range resources {
if resource.Transition != codersdk.WorkspaceTransitionStart {
continue
}
agents = append(agents, resource.Agents...)
}

// handle both WORKSPACE and WORKSPACE.AGENT syntax
if len(agents) == 1 {
wc.Hosts = append(wc.Hosts, workspace.Name)
}
for _, agent := range agents {
hostname := workspace.Name + "." + agent.Name
wc.Hosts = append(wc.Hosts, hostname)
}

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
}
}

func (r *RootCmd) configSSH() *serpent.Command {
var (
sshConfigFile string
Expand Down Expand Up @@ -254,8 +182,6 @@ func (r *RootCmd) configSSH() *serpent.Command {
// warning at any time.
_, _ = client.BuildInfo(ctx)

recvWorkspaceConfigs := sshPrepareWorkspaceConfigs(ctx, client)

out := inv.Stdout
if dryRun {
// Print everything except diff to stderr so
Expand Down Expand Up @@ -371,11 +297,6 @@ func (r *RootCmd) configSSH() *serpent.Command {
newline := len(before) > 0
sshConfigWriteSectionHeader(buf, newline, sshConfigOpts)

workspaceConfigs, err := recvWorkspaceConfigs()
if err != nil {
return xerrors.Errorf("fetch workspace configs failed: %w", err)
}

coderdConfig, err := client.SSHConfiguration(ctx)
if err != nil {
// If the error is 404, this deployment does not support
Expand All @@ -394,91 +315,79 @@ func (r *RootCmd) configSSH() *serpent.Command {
coderdConfig.HostnamePrefix = sshConfigOpts.userHostPrefix
}

// Ensure stable sorting of output.
slices.SortFunc(workspaceConfigs, func(a, b sshWorkspaceConfig) int {
return slice.Ascending(a.Name, b.Name)
})
for _, wc := range workspaceConfigs {
sort.Strings(wc.Hosts)
// Write agent configuration.
for _, workspaceHostname := range wc.Hosts {
sshHostname := fmt.Sprintf("%s%s", coderdConfig.HostnamePrefix, workspaceHostname)
defaultOptions := []string{
"HostName " + sshHostname,
"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"
}
defaultOptions = append(defaultOptions, fmt.Sprintf(
"ProxyCommand %s %s ssh --stdio%s %s",
escapedCoderBinary, rootFlags, flags, workspaceHostname,
))
}
// 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 := 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)
}
}
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)
}

// 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)
}
}
flags := ""
if sshConfigOpts.waitEnum != "auto" {
flags += " --wait=" + sshConfigOpts.waitEnum
}
if sshConfigOpts.disableAutostart {
flags += " --disable-autostart=true"
}
defaultOptions = append(defaultOptions, fmt.Sprintf(
"ProxyCommand %s %s ssh --stdio%s --ssh-host-prefix %s %%h",
escapedCoderBinary, rootFlags, flags, coderdConfig.HostnamePrefix,
))
}

// Finally, add the standard options.
err := configOptions.addOptions(defaultOptions...)
if err != nil {
return err
}
// Create a copy of the options so we can modify them.
configOptions := sshConfigOpts
configOptions.sshOptions = nil

hostBlock := []string{
"Host " + sshHostname,
}
// Prefix with '\t'
for _, v := range configOptions.sshOptions {
hostBlock = append(hostBlock, "\t"+v)
}
// 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)
}
}

_, _ = buf.WriteString(strings.Join(hostBlock, "\n"))
_ = buf.WriteByte('\n')
// 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 {
return err
}

hostBlock := []string{
"Host " + coderdConfig.HostnamePrefix + "*",
}
// Prefix with '\t'
for _, v := range configOptions.sshOptions {
hostBlock = append(hostBlock, "\t"+v)
}

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

sshConfigWriteSectionEnd(buf)

// Write the remainder of the users config file to buf.
Expand Down Expand Up @@ -532,9 +441,17 @@ func (r *RootCmd) configSSH() *serpent.Command {
_, _ = fmt.Fprintf(out, "Updated %q\n", sshConfigFile)
}

if len(workspaceConfigs) > 0 {
res, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
Owner: codersdk.Me,
Limit: 1,
})
if err != nil {
return xerrors.Errorf("fetch workspaces failed: %w", err)
}

if len(res.Workspaces) > 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 %s%s\n", coderdConfig.HostnamePrefix, workspaceConfigs[0].Name)
_, _ = fmt.Fprintf(out, "For example, try running:\n\n\t$ ssh %s%s\n", coderdConfig.HostnamePrefix, res.Workspaces[0].Name)
} else {
_, _ = fmt.Fprint(out, "You don't have any workspaces yet, try creating one with:\n\n\t$ coder create <workspace>\n")
}
Expand Down
Loading
Loading