Skip to content

Commit 2136d8d

Browse files
authored
chore: use access url from env resource pool (coder#216)
1 parent cf6e030 commit 2136d8d

File tree

10 files changed

+136
-67
lines changed

10 files changed

+136
-67
lines changed

coder-sdk/env.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package coder
33
import (
44
"context"
55
"net/http"
6+
"net/url"
67
"time"
78

89
"cdr.dev/wsep"
@@ -32,7 +33,6 @@ type Environment struct {
3233
LastOpenedAt time.Time `json:"last_opened_at" table:"-"`
3334
LastConnectionAt time.Time `json:"last_connection_at" table:"-"`
3435
AutoOffThreshold Duration `json:"auto_off_threshold" table:"-"`
35-
SSHAvailable bool `json:"ssh_available" table:"-"`
3636
UseContainerVM bool `json:"use_container_vm" table:"CVM"`
3737
ResourcePoolID string `json:"resource_pool_id" table:"-"`
3838
}
@@ -147,22 +147,22 @@ func (c Client) EditEnvironment(ctx context.Context, envID string, req UpdateEnv
147147

148148
// DialWsep dials an environments command execution interface
149149
// See https://github.com/cdr/wsep for details.
150-
func (c Client) DialWsep(ctx context.Context, envID string) (*websocket.Conn, error) {
151-
return c.dialWebsocket(ctx, "/proxy/environments/"+envID+"/wsep")
150+
func (c Client) DialWsep(ctx context.Context, baseURL *url.URL, envID string) (*websocket.Conn, error) {
151+
return c.dialWebsocket(ctx, "/proxy/environments/"+envID+"/wsep", withBaseURL(baseURL))
152152
}
153153

154154
// DialExecutor gives a remote execution interface for performing commands inside an environment.
155-
func (c Client) DialExecutor(ctx context.Context, envID string) (wsep.Execer, error) {
156-
ws, err := c.DialWsep(ctx, envID)
155+
func (c Client) DialExecutor(ctx context.Context, baseURL *url.URL, envID string) (wsep.Execer, error) {
156+
ws, err := c.DialWsep(ctx, baseURL, envID)
157157
if err != nil {
158158
return nil, err
159159
}
160160
return wsep.RemoteExecer(ws), nil
161161
}
162162

163163
// DialIDEStatus opens a websocket connection for cpu load metrics on the environment.
164-
func (c Client) DialIDEStatus(ctx context.Context, envID string) (*websocket.Conn, error) {
165-
return c.dialWebsocket(ctx, "/proxy/environments/"+envID+"/ide/api/status")
164+
func (c Client) DialIDEStatus(ctx context.Context, baseURL *url.URL, envID string) (*websocket.Conn, error) {
165+
return c.dialWebsocket(ctx, "/proxy/environments/"+envID+"/ide/api/status", withBaseURL(baseURL))
166166
}
167167

168168
// DialEnvironmentBuildLog opens a websocket connection for the environment build log messages.

coder-sdk/request.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,21 @@ import (
1212
)
1313

1414
// request is a helper to set the cookie, marshal the payload and execute the request.
15-
func (c Client) request(ctx context.Context, method, path string, in interface{}) (*http.Response, error) {
15+
func (c Client) request(ctx context.Context, method, path string, in interface{}, options ...requestOption) (*http.Response, error) {
1616
// Create a default http client with the auth in the cookie.
1717
client, err := c.newHTTPClient()
1818
if err != nil {
1919
return nil, xerrors.Errorf("new http client: %w", err)
2020
}
21+
url := *c.BaseURL
22+
23+
var config requestOptions
24+
for _, o := range options {
25+
o(&config)
26+
}
27+
if config.BaseURLOverride != nil {
28+
url = *config.BaseURLOverride
29+
}
2130

2231
// If we have incoming data, encode it as json.
2332
var payload io.Reader
@@ -30,7 +39,7 @@ func (c Client) request(ctx context.Context, method, path string, in interface{}
3039
}
3140

3241
// Create the http request.
33-
req, err := http.NewRequestWithContext(ctx, method, c.BaseURL.String()+path, payload)
42+
req, err := http.NewRequestWithContext(ctx, method, url.String()+path, payload)
3443
if err != nil {
3544
return nil, xerrors.Errorf("create request: %w", err)
3645
}

coder-sdk/resourcepools.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ type ResourcePool struct {
1717
DevurlHost string `json:"devurl_host"`
1818
NamespaceWhitelist []string `json:"namespace_whitelist"`
1919
OrgWhitelist []string `json:"org_whitelist"`
20+
SSHEnabled bool `json:"ssh_enabled"`
21+
AccessURL string `json:"envproxy_access_url"`
2022
}
2123

2224
// ResourcePoolByID fetches a resource pool entity by its unique ID.

coder-sdk/ws.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,35 @@ package coder
33
import (
44
"context"
55
"net/http"
6+
"net/url"
67

78
"nhooyr.io/websocket"
89
)
910

11+
type requestOptions struct {
12+
BaseURLOverride *url.URL
13+
}
14+
15+
type requestOption func(*requestOptions)
16+
17+
func withBaseURL(base *url.URL) func(o *requestOptions) {
18+
return func(o *requestOptions) {
19+
o.BaseURLOverride = base
20+
}
21+
}
22+
1023
// dialWebsocket establish the websocket connection while setting the authentication header.
11-
func (c Client) dialWebsocket(ctx context.Context, path string) (*websocket.Conn, error) {
24+
func (c Client) dialWebsocket(ctx context.Context, path string, options ...requestOption) (*websocket.Conn, error) {
1225
// Make a copy of the url so we can update the scheme to ws(s) without mutating the state.
1326
url := *c.BaseURL
27+
var config requestOptions
28+
for _, o := range options {
29+
o(&config)
30+
}
31+
if config.BaseURLOverride != nil {
32+
url = *config.BaseURLOverride
33+
}
34+
1435
if url.Scheme == "https" {
1536
url.Scheme = "wss"
1637
} else {

internal/cmd/configssh.go

Lines changed: 23 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,16 @@ import (
44
"context"
55
"fmt"
66
"io/ioutil"
7-
"net"
87
"net/url"
98
"os"
109
"os/user"
1110
"path/filepath"
1211
"strings"
13-
"time"
1412

1513
"cdr.dev/coder-cli/pkg/clog"
1614

1715
"cdr.dev/coder-cli/coder-sdk"
16+
"cdr.dev/coder-cli/internal/coderutil"
1817
"cdr.dev/coder-cli/internal/config"
1918
"github.com/spf13/cobra"
2019
"golang.org/x/xerrors"
@@ -103,20 +102,17 @@ func configSSH(configpath *string, remove *bool) func(cmd *cobra.Command, _ []st
103102
return xerrors.New("no environments found")
104103
}
105104

106-
if !sshAvailable(envs) {
107-
return xerrors.New("SSH is disabled or not available for any environments in your Coder Enterprise deployment.")
108-
}
109-
110-
err = canConnectSSH(ctx)
105+
envsWithPools, err := coderutil.EnvsWithPool(ctx, client, envs)
111106
if err != nil {
112-
return xerrors.Errorf("check if SSH is available: unable to connect to SSH endpoint: %w", err)
107+
return xerrors.Errorf("resolve env pools: %w", err)
113108
}
114109

115-
newConfig, err := makeNewConfigs(user.Username, envs, privateKeyFilepath)
116-
if err != nil {
117-
return xerrors.Errorf("make new ssh configurations: %w", err)
110+
if !sshAvailable(envsWithPools) {
111+
return xerrors.New("SSH is disabled or not available for any environments in your Coder Enterprise deployment.")
118112
}
119113

114+
newConfig := makeNewConfigs(user.Username, envsWithPools, privateKeyFilepath)
115+
120116
err = os.MkdirAll(filepath.Dir(*configpath), os.ModePerm)
121117
if err != nil {
122118
return xerrors.Errorf("make configuration directory: %w", err)
@@ -159,42 +155,15 @@ func removeOldConfig(config string) (string, bool) {
159155
}
160156

161157
// sshAvailable returns true if SSH is available for at least one environment.
162-
func sshAvailable(envs []coder.Environment) bool {
158+
func sshAvailable(envs []coderutil.EnvWithPool) bool {
163159
for _, env := range envs {
164-
if env.SSHAvailable {
160+
if env.Pool.SSHEnabled {
165161
return true
166162
}
167163
}
168-
169164
return false
170165
}
171166

172-
// canConnectSSH returns an error if we cannot dial the SSH port.
173-
func canConnectSSH(ctx context.Context) error {
174-
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
175-
defer cancel()
176-
177-
host, err := configuredHostname()
178-
if err != nil {
179-
return xerrors.Errorf("get configured manager hostname: %w", err)
180-
}
181-
182-
var (
183-
dialer net.Dialer
184-
hostPort = net.JoinHostPort(host, "22")
185-
)
186-
conn, err := dialer.DialContext(ctx, "tcp", hostPort)
187-
if err != nil {
188-
if err == context.DeadlineExceeded {
189-
err = xerrors.New("timed out after 3 seconds")
190-
}
191-
return xerrors.Errorf("dial tcp://%v: %w", hostPort, err)
192-
}
193-
conn.Close()
194-
195-
return nil
196-
}
197-
198167
func writeSSHKey(ctx context.Context, client *coder.Client, privateKeyPath string) error {
199168
key, err := client.SSHKey(ctx)
200169
if err != nil {
@@ -203,23 +172,26 @@ func writeSSHKey(ctx context.Context, client *coder.Client, privateKeyPath strin
203172
return ioutil.WriteFile(privateKeyPath, []byte(key.PrivateKey), 0400)
204173
}
205174

206-
func makeNewConfigs(userName string, envs []coder.Environment, privateKeyFilepath string) (string, error) {
207-
hostname, err := configuredHostname()
208-
if err != nil {
209-
return "", err
210-
}
211-
175+
func makeNewConfigs(userName string, envs []coderutil.EnvWithPool, privateKeyFilepath string) string {
212176
newConfig := fmt.Sprintf("\n%s\n%s\n\n", sshStartToken, sshStartMessage)
213177
for _, env := range envs {
214-
if !env.SSHAvailable {
178+
if !env.Pool.SSHEnabled {
179+
clog.LogWarn(fmt.Sprintf("SSH is not enabled for pool %q", env.Pool.Name),
180+
clog.BlankLine,
181+
clog.Tipf("ask an infrastructure administrator to enable SSH for this resource pool"),
182+
)
215183
continue
216184
}
217-
218-
newConfig += makeSSHConfig(hostname, userName, env.Name, privateKeyFilepath)
185+
u, err := url.Parse(env.Pool.AccessURL)
186+
if err != nil {
187+
clog.LogWarn("invalid access url", clog.Causef("malformed url: %q", env.Pool.AccessURL))
188+
continue
189+
}
190+
newConfig += makeSSHConfig(u.Host, userName, env.Env.Name, privateKeyFilepath)
219191
}
220192
newConfig += fmt.Sprintf("\n%s\n", sshEndToken)
221193

222-
return newConfig, nil
194+
return newConfig
223195
}
224196

225197
func makeSSHConfig(host, userName, envName, privateKeyFilepath string) string {
@@ -235,6 +207,7 @@ func makeSSHConfig(host, userName, envName, privateKeyFilepath string) string {
235207
`, envName, host, userName, envName, privateKeyFilepath)
236208
}
237209

210+
//nolint:deadcode,unused
238211
func configuredHostname() (string, error) {
239212
u, err := config.URL.Read()
240213
if err != nil {

internal/cmd/shell.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616

1717
"cdr.dev/coder-cli/coder-sdk"
1818
"cdr.dev/coder-cli/internal/activity"
19+
"cdr.dev/coder-cli/internal/coderutil"
1920
"cdr.dev/coder-cli/internal/x/xterminal"
2021
"cdr.dev/coder-cli/pkg/clog"
2122
"cdr.dev/wsep"
@@ -161,9 +162,9 @@ func runCommand(ctx context.Context, envName, command string, args []string) err
161162
ctx, cancel := context.WithCancel(ctx)
162163
defer cancel()
163164

164-
conn, err := client.DialWsep(ctx, env.ID)
165+
conn, err := coderutil.DialEnvWsep(ctx, client, env)
165166
if err != nil {
166-
return xerrors.Errorf("dial websocket: %w", err)
167+
return xerrors.Errorf("dial executor: %w", err)
167168
}
168169
go heartbeat(ctx, conn, 15*time.Second)
169170

internal/coderutil/doc.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Package coderutil providers utilities for high-level operations on coder-sdk entities.
2+
package coderutil

internal/coderutil/env.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package coderutil
2+
3+
import (
4+
"context"
5+
"net/url"
6+
7+
"cdr.dev/coder-cli/coder-sdk"
8+
"golang.org/x/xerrors"
9+
"nhooyr.io/websocket"
10+
)
11+
12+
// DialEnvWsep dials the executor endpoint using the https://github.com/cdr/wsep message protocol.
13+
// The proper resource pool access URL is used.
14+
func DialEnvWsep(ctx context.Context, client *coder.Client, env *coder.Environment) (*websocket.Conn, error) {
15+
resourcePool, err := client.ResourcePoolByID(ctx, env.ResourcePoolID)
16+
if err != nil {
17+
return nil, xerrors.Errorf("get env resource pool: %w", err)
18+
}
19+
accessURL, err := url.Parse(resourcePool.AccessURL)
20+
if err != nil {
21+
return nil, xerrors.Errorf("invalid resource pool access url: %w", err)
22+
}
23+
24+
conn, err := client.DialWsep(ctx, accessURL, env.ID)
25+
if err != nil {
26+
return nil, xerrors.Errorf("dial websocket: %w", err)
27+
}
28+
return conn, nil
29+
}
30+
31+
// EnvWithPool composes an Environment entity with its associated ResourcePool.
32+
type EnvWithPool struct {
33+
Env coder.Environment
34+
Pool coder.ResourcePool
35+
}
36+
37+
// EnvsWithPool performs the composition of each Environment with its associated ResourcePool.
38+
func EnvsWithPool(ctx context.Context, client *coder.Client, envs []coder.Environment) ([]EnvWithPool, error) {
39+
pooledEnvs := make([]EnvWithPool, len(envs))
40+
pools, err := client.ResourcePools(ctx)
41+
if err != nil {
42+
return nil, err
43+
}
44+
poolMap := make(map[string]coder.ResourcePool, len(pools))
45+
for _, p := range pools {
46+
poolMap[p.ID] = p
47+
}
48+
for _, e := range envs {
49+
envPool, ok := poolMap[e.ResourcePoolID]
50+
if !ok {
51+
return nil, xerrors.Errorf("fetch env resource pool: %w", coder.ErrNotFound)
52+
}
53+
pooledEnvs = append(pooledEnvs, EnvWithPool{
54+
Env: e,
55+
Pool: envPool,
56+
})
57+
}
58+
return pooledEnvs, nil
59+
}

internal/sync/singlefile.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,15 @@ import (
1010
"strings"
1111

1212
"cdr.dev/coder-cli/coder-sdk"
13+
"cdr.dev/coder-cli/internal/coderutil"
1314
"cdr.dev/wsep"
1415
"golang.org/x/xerrors"
1516
"nhooyr.io/websocket"
1617
)
1718

1819
// SingleFile copies the given file into the remote dir or remote path of the given coder.Environment.
1920
func SingleFile(ctx context.Context, local, remoteDir string, env *coder.Environment, client *coder.Client) error {
20-
conn, err := client.DialWsep(ctx, env.ID)
21+
conn, err := coderutil.DialEnvWsep(ctx, client, env)
2122
if err != nil {
2223
return xerrors.Errorf("dial remote execer: %w", err)
2324
}

internal/sync/sync.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424

2525
"cdr.dev/coder-cli/coder-sdk"
2626
"cdr.dev/coder-cli/internal/activity"
27+
"cdr.dev/coder-cli/internal/coderutil"
2728
"cdr.dev/coder-cli/pkg/clog"
2829
"cdr.dev/wsep"
2930
)
@@ -89,9 +90,9 @@ func (s Sync) syncPaths(delete bool, local, remote string) error {
8990
}
9091

9192
func (s Sync) remoteCmd(ctx context.Context, prog string, args ...string) error {
92-
conn, err := s.Client.DialWsep(ctx, s.Env.ID)
93+
conn, err := coderutil.DialEnvWsep(ctx, s.Client, &s.Env)
9394
if err != nil {
94-
return xerrors.Errorf("dial websocket: %w", err)
95+
return xerrors.Errorf("dial executor: %w", err)
9596
}
9697
defer func() { _ = conn.Close(websocket.CloseNormalClosure, "") }() // Best effort.
9798

@@ -270,9 +271,9 @@ func (s Sync) Version() (string, error) {
270271
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
271272
defer cancel()
272273

273-
conn, err := s.Client.DialWsep(ctx, s.Env.ID)
274+
conn, err := coderutil.DialEnvWsep(ctx, s.Client, &s.Env)
274275
if err != nil {
275-
return "", err
276+
return "", xerrors.Errorf("dial env executor: %w", err)
276277
}
277278
defer func() { _ = conn.Close(websocket.CloseNormalClosure, "") }() // Best effort.
278279

0 commit comments

Comments
 (0)