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

Commit 49e3686

Browse files
authored
Merge pull request #17 from cdr/config-ssh
Add config-ssh command
2 parents 1a0c4a1 + ce4bb7d commit 49e3686

File tree

3 files changed

+221
-4
lines changed

3 files changed

+221
-4
lines changed

cmd/coder/config_ssh.go

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io/ioutil"
7+
"net"
8+
"net/url"
9+
"os"
10+
"path/filepath"
11+
"strings"
12+
"time"
13+
14+
"cdr.dev/coder-cli/internal/config"
15+
"cdr.dev/coder-cli/internal/entclient"
16+
"github.com/spf13/pflag"
17+
18+
"go.coder.com/cli"
19+
"go.coder.com/flog"
20+
)
21+
22+
var (
23+
privateKeyFilepath = filepath.Join(os.Getenv("HOME"), ".ssh", "coder_enterprise")
24+
)
25+
26+
type configSSHCmd struct {
27+
filepath string
28+
remove bool
29+
30+
startToken, startMessage, endToken string
31+
}
32+
33+
func (cmd *configSSHCmd) Spec() cli.CommandSpec {
34+
return cli.CommandSpec{
35+
Name: "config-ssh",
36+
Usage: "",
37+
Desc: "add your Coder Enterprise environments to ~/.ssh/config",
38+
}
39+
}
40+
41+
func (cmd *configSSHCmd) RegisterFlags(fl *pflag.FlagSet) {
42+
fl.BoolVar(&cmd.remove, "remove", false, "remove the auto-generated Coder Enterprise ssh config")
43+
home := os.Getenv("HOME")
44+
defaultPath := filepath.Join(home, ".ssh", "config")
45+
fl.StringVar(&cmd.filepath, "config-path", defaultPath, "overide the default path of your ssh config file")
46+
47+
cmd.startToken = "# ------------START-CODER-ENTERPRISE-----------"
48+
cmd.startMessage = `# The following has been auto-generated by "coder config-ssh"
49+
# to make accessing your Coder Enterprise environments easier.
50+
#
51+
# To remove this blob, run:
52+
#
53+
# coder config-ssh --remove
54+
#
55+
# You should not hand-edit this section, unless you are deleting it.`
56+
cmd.endToken = "# ------------END-CODER-ENTERPRISE------------"
57+
}
58+
59+
func (cmd *configSSHCmd) Run(fl *pflag.FlagSet) {
60+
ctx, cancel := context.WithCancel(context.Background())
61+
defer cancel()
62+
63+
currentConfig, err := readStr(cmd.filepath)
64+
if err != nil {
65+
flog.Fatal("failed to read ssh config file %q: %v", cmd.filepath, err)
66+
}
67+
68+
startIndex := strings.Index(currentConfig, cmd.startToken)
69+
endIndex := strings.Index(currentConfig, cmd.endToken)
70+
71+
if cmd.remove {
72+
if startIndex == -1 || endIndex == -1 {
73+
flog.Fatal("the Coder Enterprise ssh configuration section could not be safely deleted or does not exist")
74+
}
75+
currentConfig = currentConfig[:startIndex-1] + currentConfig[endIndex+len(cmd.endToken)+1:]
76+
77+
err = writeStr(cmd.filepath, currentConfig)
78+
if err != nil {
79+
flog.Fatal("failed to write to ssh config file %q: %v", cmd.filepath, err)
80+
}
81+
82+
return
83+
}
84+
85+
entClient := requireAuth()
86+
87+
sshAvailable := cmd.ensureSSHAvailable(ctx)
88+
if !sshAvailable {
89+
flog.Fatal("SSH is disabled or not available for your Coder Enterprise deployment.")
90+
}
91+
92+
me, err := entClient.Me()
93+
if err != nil {
94+
flog.Fatal("failed to fetch username: %v", err)
95+
}
96+
97+
envs := getEnvs(entClient)
98+
if len(envs) < 1 {
99+
flog.Fatal("no environments found")
100+
}
101+
newConfig, err := cmd.makeNewConfigs(me.Username, envs)
102+
if err != nil {
103+
flog.Fatal("failed to make new ssh configurations: %v", err)
104+
}
105+
106+
// if we find the old config, remove those chars from the string
107+
if startIndex != -1 && endIndex != -1 {
108+
currentConfig = currentConfig[:startIndex-1] + currentConfig[endIndex+len(cmd.endToken)+1:]
109+
}
110+
111+
err = writeStr(cmd.filepath, currentConfig+newConfig)
112+
if err != nil {
113+
flog.Fatal("failed to write new configurations to ssh config file %q: %v", cmd.filepath, err)
114+
}
115+
err = writeSSHKey(ctx, entClient)
116+
if err != nil {
117+
flog.Fatal("failed to fetch and write ssh key: %v", err)
118+
}
119+
120+
fmt.Printf("An auto-generated ssh config was written to %q\n", cmd.filepath)
121+
fmt.Printf("Your private ssh key was written to %q\n", privateKeyFilepath)
122+
fmt.Println("You should now be able to ssh into your environment")
123+
fmt.Printf("For example, try running\n\n\t$ ssh coder.%s\n\n", envs[0].Name)
124+
}
125+
126+
func writeSSHKey(ctx context.Context, client *entclient.Client) error {
127+
key, err := client.SSHKey()
128+
if err != nil {
129+
return err
130+
}
131+
err = ioutil.WriteFile(privateKeyFilepath, []byte(key.PrivateKey), 400)
132+
if err != nil {
133+
return err
134+
}
135+
return nil
136+
}
137+
138+
func (cmd *configSSHCmd) makeNewConfigs(userName string, envs []entclient.Environment) (string, error) {
139+
hostname, err := configuredHostname()
140+
if err != nil {
141+
return "", nil
142+
}
143+
144+
newConfig := fmt.Sprintf("\n%s\n%s\n\n", cmd.startToken, cmd.startMessage)
145+
for _, env := range envs {
146+
newConfig += cmd.makeConfig(hostname, userName, env.Name)
147+
}
148+
newConfig += fmt.Sprintf("\n%s\n", cmd.endToken)
149+
150+
return newConfig, nil
151+
}
152+
153+
func (cmd *configSSHCmd) makeConfig(host, userName, envName string) string {
154+
return fmt.Sprintf(
155+
`Host coder.%s
156+
HostName %s
157+
User %s-%s
158+
StrictHostKeyChecking no
159+
ConnectTimeout=0
160+
IdentityFile=%s
161+
`, envName, host, userName, envName, privateKeyFilepath)
162+
}
163+
164+
func (cmd *configSSHCmd) ensureSSHAvailable(ctx context.Context) bool {
165+
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
166+
defer cancel()
167+
168+
host, err := configuredHostname()
169+
if err != nil {
170+
return false
171+
}
172+
173+
var dialer net.Dialer
174+
_, err = dialer.DialContext(ctx, "tcp", net.JoinHostPort(host, "22"))
175+
return err == nil
176+
}
177+
178+
func configuredHostname() (string, error) {
179+
u, err := config.URL.Read()
180+
if err != nil {
181+
return "", err
182+
}
183+
url, err := url.Parse(u)
184+
if err != nil {
185+
return "", err
186+
}
187+
return url.Hostname(), nil
188+
}
189+
190+
func writeStr(filename, data string) error {
191+
return ioutil.WriteFile(filename, []byte(data), 0777)
192+
}
193+
194+
func readStr(filename string) (string, error) {
195+
contents, err := ioutil.ReadFile(filename)
196+
if err != nil {
197+
return "", err
198+
}
199+
return string(contents), nil
200+
}

cmd/coder/main.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
package main
22

33
import (
4-
"github.com/spf13/pflag"
5-
"go.coder.com/cli"
64
"log"
75
"net/http"
86
_ "net/http/pprof"
97
"os"
8+
9+
"github.com/spf13/pflag"
10+
"go.coder.com/cli"
1011
)
1112

1213
var (
@@ -36,6 +37,7 @@ func (r *rootCmd) Subcommands() []cli.Command {
3637
&syncCmd{},
3738
&urlCmd{},
3839
&versionCmd{},
40+
&configSSHCmd{},
3941
}
4042
}
4143

internal/entclient/me.go

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
package entclient
22

33
type User struct {
4-
ID string `json:"id"`
5-
Email string `json:"email"`
4+
ID string `json:"id"`
5+
Email string `json:"email"`
6+
Username string `json:"username"`
67
}
78

89
func (c Client) Me() (*User, error) {
@@ -13,3 +14,17 @@ func (c Client) Me() (*User, error) {
1314
}
1415
return &u, nil
1516
}
17+
18+
type SSHKey struct {
19+
PublicKey string `json:"public_key"`
20+
PrivateKey string `json:"private_key"`
21+
}
22+
23+
func (c Client) SSHKey() (*SSHKey, error) {
24+
var key SSHKey
25+
err := c.requestBody("GET", "/api/users/me/sshkey", nil, &key)
26+
if err != nil {
27+
return nil, err
28+
}
29+
return &key, nil
30+
}

0 commit comments

Comments
 (0)