Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 1f77074

Browse files
committed
feat: Add workspace proxy enterprise cli commands
1 parent 22aadf1 commit 1f77074

File tree

3 files changed

+400
-0
lines changed

3 files changed

+400
-0
lines changed

enterprise/cli/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ type RootCmd struct {
1212
func (r *RootCmd) enterpriseOnly() []*clibase.Cmd {
1313
return []*clibase.Cmd{
1414
r.server(),
15+
r.workspaceProxy(),
1516
r.features(),
1617
r.licenses(),
1718
r.groups(),

enterprise/cli/workspaceproxy.go

Lines changed: 362 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,362 @@
1+
//go:build !slim
2+
3+
package cli
4+
5+
import (
6+
"context"
7+
"fmt"
8+
"io"
9+
"log"
10+
"net"
11+
"net/http"
12+
"net/http/pprof"
13+
"net/url"
14+
"os/signal"
15+
"regexp"
16+
rpprof "runtime/pprof"
17+
"time"
18+
19+
"github.com/coder/coder/coderd/httpapi"
20+
"github.com/coder/coder/coderd/httpmw"
21+
"github.com/prometheus/client_golang/prometheus"
22+
"github.com/prometheus/client_golang/prometheus/collectors"
23+
"github.com/prometheus/client_golang/prometheus/promhttp"
24+
25+
"github.com/coreos/go-systemd/daemon"
26+
27+
"github.com/coder/coder/cli/cliui"
28+
"golang.org/x/xerrors"
29+
30+
"github.com/coder/coder/cli"
31+
"github.com/coder/coder/coderd/workspaceapps"
32+
"github.com/coder/coder/enterprise/wsproxy"
33+
34+
"github.com/coder/coder/cli/clibase"
35+
"github.com/coder/coder/codersdk"
36+
)
37+
38+
func (r *RootCmd) workspaceProxy() *clibase.Cmd {
39+
cmd := &clibase.Cmd{
40+
Use: "workspace-proxy",
41+
Short: "Manage workspace proxies",
42+
Aliases: []string{"proxy"},
43+
Hidden: true,
44+
Handler: func(inv *clibase.Invocation) error {
45+
return inv.Command.HelpHandler(inv)
46+
},
47+
Children: []*clibase.Cmd{
48+
r.proxyServer(),
49+
r.registerProxy(),
50+
},
51+
}
52+
53+
return cmd
54+
}
55+
56+
func (r *RootCmd) registerProxy() *clibase.Cmd {
57+
client := new(codersdk.Client)
58+
cmd := &clibase.Cmd{
59+
Use: "register",
60+
Short: "Register a workspace proxy",
61+
Middleware: clibase.Chain(
62+
clibase.RequireNArgs(1),
63+
r.InitClient(client),
64+
),
65+
Handler: func(i *clibase.Invocation) error {
66+
ctx := i.Context()
67+
name := i.Args[0]
68+
// TODO: Fix all this
69+
resp, err := client.CreateWorkspaceProxy(ctx, codersdk.CreateWorkspaceProxyRequest{
70+
Name: name,
71+
DisplayName: name,
72+
Icon: "whocares.png",
73+
URL: "http://localhost:6005",
74+
WildcardHostname: "",
75+
})
76+
if err != nil {
77+
return xerrors.Errorf("create workspace proxy: %w", err)
78+
}
79+
80+
fmt.Println(resp.ProxyToken)
81+
return nil
82+
},
83+
}
84+
return cmd
85+
}
86+
87+
type closers []func()
88+
89+
func (c closers) Close() {
90+
for _, closeF := range c {
91+
closeF()
92+
}
93+
}
94+
95+
func (c *closers) Add(f func()) {
96+
*c = append(*c, f)
97+
}
98+
99+
func (r *RootCmd) proxyServer() *clibase.Cmd {
100+
var (
101+
// TODO: Remove options that we do not need
102+
cfg = new(codersdk.DeploymentValues)
103+
opts = cfg.Options()
104+
)
105+
var _ = opts
106+
107+
client := new(codersdk.Client)
108+
cmd := &clibase.Cmd{
109+
Use: "server",
110+
Short: "Start a workspace proxy server",
111+
Options: opts,
112+
Middleware: clibase.Chain(
113+
cli.WriteConfigMW(cfg),
114+
cli.PrintDeprecatedOptions(),
115+
clibase.RequireNArgs(0),
116+
// We need a client to connect with the primary coderd instance.
117+
r.InitClient(client),
118+
),
119+
Handler: func(inv *clibase.Invocation) error {
120+
var closers closers
121+
// Main command context for managing cancellation of running
122+
// services.
123+
ctx, topCancel := context.WithCancel(inv.Context())
124+
defer topCancel()
125+
closers.Add(topCancel)
126+
127+
go cli.DumpHandler(ctx)
128+
129+
cli.PrintLogo(inv)
130+
logger, logCloser, err := cli.BuildLogger(inv, cfg)
131+
if err != nil {
132+
return xerrors.Errorf("make logger: %w", err)
133+
}
134+
defer logCloser()
135+
closers.Add(logCloser)
136+
137+
logger.Debug(ctx, "started debug logging")
138+
logger.Sync()
139+
140+
// Register signals early on so that graceful shutdown can't
141+
// be interrupted by additional signals. Note that we avoid
142+
// shadowing cancel() (from above) here because notifyStop()
143+
// restores default behavior for the signals. This protects
144+
// the shutdown sequence from abruptly terminating things
145+
// like: database migrations, provisioner work, workspace
146+
// cleanup in dev-mode, etc.
147+
//
148+
// To get out of a graceful shutdown, the user can send
149+
// SIGQUIT with ctrl+\ or SIGKILL with `kill -9`.
150+
notifyCtx, notifyStop := signal.NotifyContext(ctx, cli.InterruptSignals...)
151+
defer notifyStop()
152+
153+
// Clean up idle connections at the end, e.g.
154+
// embedded-postgres can leave an idle connection
155+
// which is caught by goleaks.
156+
defer http.DefaultClient.CloseIdleConnections()
157+
closers.Add(http.DefaultClient.CloseIdleConnections)
158+
159+
tracer, _ := cli.ConfigureTraceProvider(ctx, logger, inv, cfg)
160+
161+
httpServers, err := cli.ConfigureHTTPServers(inv, cfg)
162+
if err != nil {
163+
return xerrors.Errorf("configure http(s): %w", err)
164+
}
165+
defer httpServers.Close()
166+
closers.Add(httpServers.Close)
167+
168+
// TODO: @emyrk I find this strange that we add this to the context
169+
// at the root here.
170+
ctx, httpClient, err := cli.ConfigureHTTPClient(
171+
ctx,
172+
cfg.TLS.ClientCertFile.String(),
173+
cfg.TLS.ClientKeyFile.String(),
174+
cfg.TLS.ClientCAFile.String(),
175+
)
176+
if err != nil {
177+
return xerrors.Errorf("configure http client: %w", err)
178+
}
179+
defer httpClient.CloseIdleConnections()
180+
closers.Add(httpClient.CloseIdleConnections)
181+
182+
// Warn the user if the access URL appears to be a loopback address.
183+
isLocal, err := cli.IsLocalURL(ctx, cfg.AccessURL.Value())
184+
if isLocal || err != nil {
185+
reason := "could not be resolved"
186+
if isLocal {
187+
reason = "isn't externally reachable"
188+
}
189+
cliui.Warnf(
190+
inv.Stderr,
191+
"The access URL %s %s, this may cause unexpected problems when creating workspaces. Generate a unique *.try.coder.app URL by not specifying an access URL.\n",
192+
cliui.Styles.Field.Render(cfg.AccessURL.String()), reason,
193+
)
194+
}
195+
196+
// A newline is added before for visibility in terminal output.
197+
cliui.Infof(inv.Stdout, "\nView the Web UI: %s", cfg.AccessURL.String())
198+
199+
var appHostnameRegex *regexp.Regexp
200+
appHostname := cfg.WildcardAccessURL.String()
201+
if appHostname != "" {
202+
appHostnameRegex, err = httpapi.CompileHostnamePattern(appHostname)
203+
if err != nil {
204+
return xerrors.Errorf("parse wildcard access URL %q: %w", appHostname, err)
205+
}
206+
}
207+
208+
realIPConfig, err := httpmw.ParseRealIPConfig(cfg.ProxyTrustedHeaders, cfg.ProxyTrustedOrigins)
209+
if err != nil {
210+
return xerrors.Errorf("parse real ip config: %w", err)
211+
}
212+
213+
if cfg.Pprof.Enable {
214+
// This prevents the pprof import from being accidentally deleted.
215+
// pprof has an init function that attaches itself to the default handler.
216+
// By passing a nil handler to 'serverHandler', it will automatically use
217+
// the default, which has pprof attached.
218+
_ = pprof.Handler
219+
//nolint:revive
220+
closeFunc := cli.ServeHandler(ctx, logger, nil, cfg.Pprof.Address.String(), "pprof")
221+
defer closeFunc()
222+
closers.Add(closeFunc)
223+
}
224+
225+
prometheusRegistry := prometheus.NewRegistry()
226+
if cfg.Prometheus.Enable {
227+
prometheusRegistry.MustRegister(collectors.NewGoCollector())
228+
prometheusRegistry.MustRegister(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}))
229+
230+
//nolint:revive
231+
closeFunc := cli.ServeHandler(ctx, logger, promhttp.InstrumentMetricHandler(
232+
prometheusRegistry, promhttp.HandlerFor(prometheusRegistry, promhttp.HandlerOpts{}),
233+
), cfg.Prometheus.Address.String(), "prometheus")
234+
defer closeFunc()
235+
closers.Add(closeFunc)
236+
}
237+
238+
pu, _ := url.Parse("http://localhost:3000")
239+
proxy, err := wsproxy.New(&wsproxy.Options{
240+
Logger: logger,
241+
// TODO: PrimaryAccessURL
242+
PrimaryAccessURL: pu,
243+
AccessURL: cfg.AccessURL.Value(),
244+
AppHostname: appHostname,
245+
AppHostnameRegex: appHostnameRegex,
246+
RealIPConfig: realIPConfig,
247+
// TODO: AppSecurityKey
248+
AppSecurityKey: workspaceapps.SecurityKey{},
249+
Tracing: tracer,
250+
PrometheusRegistry: prometheusRegistry,
251+
APIRateLimit: int(cfg.RateLimit.API.Value()),
252+
SecureAuthCookie: cfg.SecureAuthCookie.Value(),
253+
// TODO: DisablePathApps
254+
DisablePathApps: false,
255+
// TODO: ProxySessionToken
256+
ProxySessionToken: "",
257+
})
258+
if err != nil {
259+
return xerrors.Errorf("create workspace proxy: %w", err)
260+
}
261+
262+
shutdownConnsCtx, shutdownConns := context.WithCancel(ctx)
263+
defer shutdownConns()
264+
closers.Add(shutdownConns)
265+
// ReadHeaderTimeout is purposefully not enabled. It caused some
266+
// issues with websockets over the dev tunnel.
267+
// See: https://github.com/coder/coder/pull/3730
268+
//nolint:gosec
269+
httpServer := &http.Server{
270+
// These errors are typically noise like "TLS: EOF". Vault does
271+
// similar:
272+
// https://github.com/hashicorp/vault/blob/e2490059d0711635e529a4efcbaa1b26998d6e1c/command/server.go#L2714
273+
ErrorLog: log.New(io.Discard, "", 0),
274+
Handler: proxy.Handler,
275+
BaseContext: func(_ net.Listener) context.Context {
276+
return shutdownConnsCtx
277+
},
278+
}
279+
defer func() {
280+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
281+
defer cancel()
282+
_ = httpServer.Shutdown(ctx)
283+
}()
284+
285+
// TODO: So this obviously is not going to work well.
286+
errCh := make(chan error, 1)
287+
go rpprof.Do(ctx, rpprof.Labels("service", "workspace-proxy"), func(ctx context.Context) {
288+
errCh <- httpServers.Serve(httpServer)
289+
})
290+
291+
cliui.Infof(inv.Stdout, "\n==> Logs will stream in below (press ctrl+c to gracefully exit):")
292+
293+
// Updates the systemd status from activating to activated.
294+
_, err = daemon.SdNotify(false, daemon.SdNotifyReady)
295+
if err != nil {
296+
return xerrors.Errorf("notify systemd: %w", err)
297+
}
298+
299+
// Currently there is no way to ask the server to shut
300+
// itself down, so any exit signal will result in a non-zero
301+
// exit of the server.
302+
var exitErr error
303+
select {
304+
case exitErr = <-errCh:
305+
case <-notifyCtx.Done():
306+
exitErr = notifyCtx.Err()
307+
_, _ = fmt.Fprintln(inv.Stdout, cliui.Styles.Bold.Render(
308+
"Interrupt caught, gracefully exiting. Use ctrl+\\ to force quit",
309+
))
310+
}
311+
312+
if exitErr != nil && !xerrors.Is(exitErr, context.Canceled) {
313+
cliui.Errorf(inv.Stderr, "Unexpected error, shutting down server: %s\n", exitErr)
314+
}
315+
316+
// Begin clean shut down stage, we try to shut down services
317+
// gracefully in an order that gives the best experience.
318+
// This procedure should not differ greatly from the order
319+
// of `defer`s in this function, but allows us to inform
320+
// the user about what's going on and handle errors more
321+
// explicitly.
322+
323+
_, err = daemon.SdNotify(false, daemon.SdNotifyStopping)
324+
if err != nil {
325+
cliui.Errorf(inv.Stderr, "Notify systemd failed: %s", err)
326+
}
327+
328+
// Stop accepting new connections without interrupting
329+
// in-flight requests, give in-flight requests 5 seconds to
330+
// complete.
331+
cliui.Info(inv.Stdout, "Shutting down API server..."+"\n")
332+
shutdownCtx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
333+
defer cancel()
334+
err = httpServer.Shutdown(shutdownCtx)
335+
if err != nil {
336+
cliui.Errorf(inv.Stderr, "API server shutdown took longer than 3s: %s\n", err)
337+
} else {
338+
cliui.Info(inv.Stdout, "Gracefully shut down API server\n")
339+
}
340+
// Cancel any remaining in-flight requests.
341+
shutdownConns()
342+
343+
// Trigger context cancellation for any remaining services.
344+
closers.Close()
345+
346+
switch {
347+
case xerrors.Is(exitErr, context.DeadlineExceeded):
348+
cliui.Warnf(inv.Stderr, "Graceful shutdown timed out")
349+
// Errors here cause a significant number of benign CI failures.
350+
return nil
351+
case xerrors.Is(exitErr, context.Canceled):
352+
return nil
353+
case exitErr != nil:
354+
return xerrors.Errorf("graceful shutdown: %w", exitErr)
355+
default:
356+
return nil
357+
}
358+
},
359+
}
360+
361+
return cmd
362+
}

0 commit comments

Comments
 (0)