Skip to content
This repository was archived by the owner on Aug 30, 2024. It is now read-only.

Add coder tunnel command #313

Merged
merged 2 commits into from
Apr 8, 2021
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
1 change: 1 addition & 0 deletions docs/coder_config-ssh.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ coder config-ssh [flags]
```
--filepath string override the default path of your ssh config file (default "~/.ssh/config")
-h, --help help for config-ssh
--p2p (experimental) uses coder tunnel to proxy ssh connection
--remove remove the auto-generated Coder ssh config
```

Expand Down
48 changes: 34 additions & 14 deletions internal/cmd/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import (
"golang.org/x/xerrors"
"nhooyr.io/websocket"

"cdr.dev/coder-cli/internal/x/xcobra"
"cdr.dev/coder-cli/internal/x/xwebrtc"
"cdr.dev/coder-cli/pkg/proto"
)
Expand All @@ -40,31 +39,42 @@ func agentCmd() *cobra.Command {

func startCmd() *cobra.Command {
var (
token string
token string
coderURL string
)
cmd := &cobra.Command{
Use: "start [coderURL] --token=[token]",
Args: xcobra.ExactArgs(1),
Use: "start --coder-url=[coder_url] --token=[token]",
Short: "starts the coder agent",
Long: "starts the coder agent",
Example: `# start the agent and connect with a Coder agent token
Example: `# start the agent and use CODER_URL and CODER_AGENT_TOKEN env vars

coder agent start https://my-coder.com --token xxxx-xxxx
coder agent start

# start the agent and use CODER_AGENT_TOKEN env var for auth token
# start the agent and connect with a specified url and agent token

coder agent start https://my-coder.com
coder agent start --coder-url https://my-coder.com --token xxxx-xxxx
`,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
log := slog.Make(sloghuman.Sink(cmd.OutOrStdout()))

// Pull the URL from the args and do some sanity check.
rawURL := args[0]
if rawURL == "" || !strings.HasPrefix(rawURL, "http") {
if coderURL == "" {
var ok bool
token, ok = os.LookupEnv("CODER_URL")
if !ok {
client, err := newClient(ctx)
if err != nil {
return xerrors.New("must login, pass --coder-url flag, or set the CODER_URL env variable")
}
burl := client.BaseURL()
coderURL = burl.String()
}
}

if !strings.HasPrefix(coderURL, "http") {
return xerrors.Errorf("invalid URL")
}
u, err := url.Parse(rawURL)
u, err := url.Parse(coderURL)
if err != nil {
return xerrors.Errorf("parse url: %w", err)
}
Expand All @@ -79,18 +89,26 @@ coder agent start https://my-coder.com
}
}

if token == "" {
var ok bool
token, ok = os.LookupEnv("CODER_AGENT_TOKEN")
if !ok {
return xerrors.New("must pass --token or set the CODER_AGENT_TOKEN env variable")
}
}

q := u.Query()
q.Set("service_token", token)
u.RawQuery = q.Encode()

ctx, cancelFunc := context.WithTimeout(ctx, time.Second*15)
defer cancelFunc()
log.Info(ctx, "connecting to broker", slog.F("url", u.String()))
conn, res, err := websocket.Dial(ctx, u.String(), nil)
// nolint: bodyclose
conn, _, err := websocket.Dial(ctx, u.String(), nil)
if err != nil {
return fmt.Errorf("dial: %w", err)
}
_ = res.Body.Close()
nc := websocket.NetConn(context.Background(), conn, websocket.MessageBinary)
session, err := yamux.Server(nc, nil)
if err != nil {
Expand All @@ -112,6 +130,8 @@ coder agent start https://my-coder.com
}

cmd.Flags().StringVar(&token, "token", "", "coder agent token")
cmd.Flags().StringVar(&coderURL, "coder-url", "", "coder access url")

return cmd
}

Expand Down
1 change: 1 addition & 0 deletions internal/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ func Make() *cobra.Command {
providersCmd(),
genDocsCmd(app),
agentCmd(),
tunnelCmd(),
)
app.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "show verbose output")
return app
Expand Down
27 changes: 21 additions & 6 deletions internal/cmd/configssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,21 +35,23 @@ func configSSHCmd() *cobra.Command {
var (
configpath string
remove = false
p2p = false
)

cmd := &cobra.Command{
Use: "config-ssh",
Short: "Configure SSH to access Coder environments",
Long: "Inject the proper OpenSSH configuration into your local SSH config file.",
RunE: configSSH(&configpath, &remove),
RunE: configSSH(&configpath, &remove, &p2p),
}
cmd.Flags().StringVar(&configpath, "filepath", filepath.Join("~", ".ssh", "config"), "override the default path of your ssh config file")
cmd.Flags().BoolVar(&remove, "remove", false, "remove the auto-generated Coder ssh config")
cmd.Flags().BoolVar(&p2p, "p2p", false, "(experimental) uses coder tunnel to proxy ssh connection")

return cmd
}

func configSSH(configpath *string, remove *bool) func(cmd *cobra.Command, _ []string) error {
func configSSH(configpath *string, remove *bool, p2p *bool) func(cmd *cobra.Command, _ []string) error {
return func(cmd *cobra.Command, _ []string) error {
ctx := cmd.Context()
usr, err := user.Current()
Expand Down Expand Up @@ -113,7 +115,7 @@ func configSSH(configpath *string, remove *bool) func(cmd *cobra.Command, _ []st
return xerrors.New("SSH is disabled or not available for any environments in your Coder deployment.")
}

newConfig := makeNewConfigs(user.Username, envsWithProviders, privateKeyFilepath)
newConfig := makeNewConfigs(user.Username, envsWithProviders, privateKeyFilepath, *p2p)

err = os.MkdirAll(filepath.Dir(*configpath), os.ModePerm)
if err != nil {
Expand Down Expand Up @@ -174,7 +176,7 @@ func writeSSHKey(ctx context.Context, client coder.Client, privateKeyPath string
return ioutil.WriteFile(privateKeyPath, []byte(key.PrivateKey), 0600)
}

func makeNewConfigs(userName string, envs []coderutil.EnvWithWorkspaceProvider, privateKeyFilepath string) string {
func makeNewConfigs(userName string, envs []coderutil.EnvWithWorkspaceProvider, privateKeyFilepath string, p2p bool) string {
newConfig := fmt.Sprintf("\n%s\n%s\n\n", sshStartToken, sshStartMessage)

sort.Slice(envs, func(i, j int) bool { return envs[i].Env.Name < envs[j].Env.Name })
Expand All @@ -192,14 +194,27 @@ func makeNewConfigs(userName string, envs []coderutil.EnvWithWorkspaceProvider,
clog.LogWarn("invalid access url", clog.Causef("malformed url: %q", env.WorkspaceProvider.EnvproxyAccessURL))
continue
}
newConfig += makeSSHConfig(u.Host, userName, env.Env.Name, privateKeyFilepath)
newConfig += makeSSHConfig(u.Host, userName, env.Env.Name, privateKeyFilepath, p2p)
}
newConfig += fmt.Sprintf("\n%s\n", sshEndToken)

return newConfig
}

func makeSSHConfig(host, userName, envName, privateKeyFilepath string) string {
func makeSSHConfig(host, userName, envName, privateKeyFilepath string, p2p bool) string {
if p2p {
return fmt.Sprintf(
`Host coder.%s
HostName localhost
ProxyCommand coder tunnel %s 22 stdio
StrictHostKeyChecking no
ConnectTimeout=0
IdentityFile="%s"
ServerAliveInterval 60
ServerAliveCountMax 3
`, envName, envName, privateKeyFilepath)
}

return fmt.Sprintf(
`Host coder.%s
HostName %s
Expand Down
Loading