-
Notifications
You must be signed in to change notification settings - Fork 881
feat(cli): use coder connect in coder ssh --stdio
, if available
#17572
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
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
a862472
feat(cli): use coder connect in `coder ssh`, if available
ethanndickson d8e1c90
fix windows tests
ethanndickson 578ebb0
rename flag, extra test
ethanndickson be118e6
reduce scope
ethanndickson bb75fa2
review
ethanndickson 00f18af
typo
ethanndickson 4ce57b7
fix tests
ethanndickson e46a084
fixup
ethanndickson 76603ed
review
ethanndickson 8c05023
fmt
ethanndickson File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,6 +8,7 @@ import ( | |
"fmt" | ||
"io" | ||
"log" | ||
"net" | ||
"net/http" | ||
"net/url" | ||
"os" | ||
|
@@ -66,6 +67,7 @@ func (r *RootCmd) ssh() *serpent.Command { | |
stdio bool | ||
hostPrefix string | ||
hostnameSuffix string | ||
forceNewTunnel bool | ||
forwardAgent bool | ||
forwardGPG bool | ||
identityAgent string | ||
|
@@ -85,6 +87,7 @@ func (r *RootCmd) ssh() *serpent.Command { | |
containerUser string | ||
) | ||
client := new(codersdk.Client) | ||
wsClient := workspacesdk.New(client) | ||
cmd := &serpent.Command{ | ||
Annotations: workspaceCommand, | ||
Use: "ssh <workspace>", | ||
|
@@ -203,14 +206,14 @@ func (r *RootCmd) ssh() *serpent.Command { | |
parsedEnv = append(parsedEnv, [2]string{k, v}) | ||
} | ||
|
||
deploymentSSHConfig := codersdk.SSHConfigResponse{ | ||
cliConfig := codersdk.SSHConfigResponse{ | ||
HostnamePrefix: hostPrefix, | ||
HostnameSuffix: hostnameSuffix, | ||
} | ||
|
||
workspace, workspaceAgent, err := findWorkspaceAndAgentByHostname( | ||
ctx, inv, client, | ||
inv.Args[0], deploymentSSHConfig, disableAutostart) | ||
inv.Args[0], cliConfig, disableAutostart) | ||
if err != nil { | ||
return err | ||
} | ||
|
@@ -275,10 +278,44 @@ func (r *RootCmd) ssh() *serpent.Command { | |
return err | ||
} | ||
|
||
// If we're in stdio mode, check to see if we can use Coder Connect. | ||
// We don't support Coder Connect over non-stdio coder ssh yet. | ||
if stdio && !forceNewTunnel { | ||
connInfo, err := wsClient.AgentConnectionInfoGeneric(ctx) | ||
if err != nil { | ||
return xerrors.Errorf("get agent connection info: %w", err) | ||
} | ||
coderConnectHost := fmt.Sprintf("%s.%s.%s.%s", | ||
workspaceAgent.Name, workspace.Name, workspace.OwnerName, connInfo.HostnameSuffix) | ||
exists, _ := workspacesdk.ExistsViaCoderConnect(ctx, coderConnectHost) | ||
if exists { | ||
defer cancel() | ||
|
||
if networkInfoDir != "" { | ||
if err := writeCoderConnectNetInfo(ctx, networkInfoDir); err != nil { | ||
ethanndickson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
logger.Error(ctx, "failed to write coder connect net info file", slog.Error(err)) | ||
} | ||
} | ||
|
||
stopPolling := tryPollWorkspaceAutostop(ctx, client, workspace) | ||
defer stopPolling() | ||
|
||
usageAppName := getUsageAppName(usageApp) | ||
if usageAppName != "" { | ||
closeUsage := client.UpdateWorkspaceUsageWithBodyContext(ctx, workspace.ID, codersdk.PostWorkspaceUsageRequest{ | ||
AgentID: workspaceAgent.ID, | ||
AppName: usageAppName, | ||
}) | ||
defer closeUsage() | ||
} | ||
return runCoderConnectStdio(ctx, fmt.Sprintf("%s:22", coderConnectHost), stdioReader, stdioWriter, stack) | ||
} | ||
} | ||
|
||
if r.disableDirect { | ||
_, _ = fmt.Fprintln(inv.Stderr, "Direct connections disabled.") | ||
} | ||
conn, err := workspacesdk.New(client). | ||
conn, err := wsClient. | ||
DialAgent(ctx, workspaceAgent.ID, &workspacesdk.DialAgentOptions{ | ||
Logger: logger, | ||
BlockEndpoints: r.disableDirect, | ||
|
@@ -662,6 +699,12 @@ func (r *RootCmd) ssh() *serpent.Command { | |
Value: serpent.StringOf(&containerUser), | ||
Hidden: true, // Hidden until this features is at least in beta. | ||
}, | ||
{ | ||
Flag: "force-new-tunnel", | ||
ethanndickson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Description: "Force the creation of a new tunnel to the workspace, even if the Coder Connect tunnel is available.", | ||
ethanndickson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Value: serpent.BoolOf(&forceNewTunnel), | ||
Hidden: true, | ||
}, | ||
sshDisableAutostartOption(serpent.BoolOf(&disableAutostart)), | ||
} | ||
return cmd | ||
|
@@ -1374,12 +1417,13 @@ func setStatsCallback( | |
} | ||
|
||
type sshNetworkStats struct { | ||
P2P bool `json:"p2p"` | ||
Latency float64 `json:"latency"` | ||
PreferredDERP string `json:"preferred_derp"` | ||
DERPLatency map[string]float64 `json:"derp_latency"` | ||
UploadBytesSec int64 `json:"upload_bytes_sec"` | ||
DownloadBytesSec int64 `json:"download_bytes_sec"` | ||
P2P bool `json:"p2p"` | ||
Latency float64 `json:"latency"` | ||
PreferredDERP string `json:"preferred_derp"` | ||
DERPLatency map[string]float64 `json:"derp_latency"` | ||
UploadBytesSec int64 `json:"upload_bytes_sec"` | ||
DownloadBytesSec int64 `json:"download_bytes_sec"` | ||
UsingCoderConnect bool `json:"using_coder_connect"` | ||
} | ||
|
||
func collectNetworkStats(ctx context.Context, agentConn *workspacesdk.AgentConn, start, end time.Time, counts map[netlogtype.Connection]netlogtype.Counts) (*sshNetworkStats, error) { | ||
|
@@ -1450,6 +1494,76 @@ func collectNetworkStats(ctx context.Context, agentConn *workspacesdk.AgentConn, | |
}, nil | ||
} | ||
|
||
type coderConnectDialerContextKey struct{} | ||
|
||
type coderConnectDialer interface { | ||
DialContext(ctx context.Context, network, addr string) (net.Conn, error) | ||
} | ||
|
||
func WithTestOnlyCoderConnectDialer(ctx context.Context, dialer coderConnectDialer) context.Context { | ||
return context.WithValue(ctx, coderConnectDialerContextKey{}, dialer) | ||
} | ||
|
||
func testOrDefaultDialer(ctx context.Context) coderConnectDialer { | ||
dialer, ok := ctx.Value(coderConnectDialerContextKey{}).(coderConnectDialer) | ||
if !ok || dialer == nil { | ||
return &net.Dialer{} | ||
} | ||
return dialer | ||
} | ||
|
||
func runCoderConnectStdio(ctx context.Context, addr string, stdin io.Reader, stdout io.Writer, stack *closerStack) error { | ||
dialer := testOrDefaultDialer(ctx) | ||
conn, err := dialer.DialContext(ctx, "tcp", addr) | ||
if err != nil { | ||
return xerrors.Errorf("dial coder connect host: %w", err) | ||
} | ||
if err := stack.push("tcp conn", conn); err != nil { | ||
return err | ||
} | ||
|
||
agentssh.Bicopy(ctx, conn, &StdioRwc{ | ||
Reader: stdin, | ||
Writer: stdout, | ||
}) | ||
|
||
return nil | ||
} | ||
|
||
type StdioRwc struct { | ||
io.Reader | ||
io.Writer | ||
} | ||
|
||
func (*StdioRwc) Close() error { | ||
return nil | ||
} | ||
|
||
func writeCoderConnectNetInfo(ctx context.Context, networkInfoDir string) error { | ||
fs, ok := ctx.Value("fs").(afero.Fs) | ||
if !ok { | ||
fs = afero.NewOsFs() | ||
} | ||
// The VS Code extension obtains the PID of the SSH process to | ||
// find the log file associated with a SSH session. | ||
// | ||
// We get the parent PID because it's assumed `ssh` is calling this | ||
// command via the ProxyCommand SSH option. | ||
networkInfoFilePath := filepath.Join(networkInfoDir, fmt.Sprintf("%d.json", os.Getppid())) | ||
stats := &sshNetworkStats{ | ||
UsingCoderConnect: true, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll put up a PR for the vscode extension to read this. |
||
} | ||
rawStats, err := json.Marshal(stats) | ||
if err != nil { | ||
return xerrors.Errorf("marshal network stats: %w", err) | ||
} | ||
err = afero.WriteFile(fs, networkInfoFilePath, rawStats, 0o600) | ||
if err != nil { | ||
return xerrors.Errorf("write network stats: %w", err) | ||
} | ||
return nil | ||
} | ||
|
||
// Converts workspace name input to owner/workspace.agent format | ||
// Possible valid input formats: | ||
// workspace | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Despite the name, this was always populated from CLI arguments, which in half of the cases are not the deployment SSH config (i.e. for the VS Code extension it's something like
vscode-coder
)