mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: use wildcard Host entry in config-ssh (#16096)
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. Depends on #16088.
This commit is contained in:
+76
-159
@@ -3,7 +3,6 @@ package cli
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -12,7 +11,6 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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",
|
||||
}
|
||||
// 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)
|
||||
}
|
||||
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,
|
||||
))
|
||||
}
|
||||
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,
|
||||
))
|
||||
}
|
||||
|
||||
// Create a copy of the options so we can modify them.
|
||||
configOptions := sshConfigOpts
|
||||
configOptions.sshOptions = nil
|
||||
// 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.
|
||||
err := configOptions.addOptions(defaultOptions...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hostBlock := []string{
|
||||
"Host " + sshHostname,
|
||||
}
|
||||
// Prefix with '\t'
|
||||
for _, v := range configOptions.sshOptions {
|
||||
hostBlock = append(hostBlock, "\t"+v)
|
||||
}
|
||||
|
||||
_, _ = buf.WriteString(strings.Join(hostBlock, "\n"))
|
||||
_ = buf.WriteByte('\n')
|
||||
// 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 {
|
||||
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.
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user