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

Skip to content

Commit 0d9da63

Browse files
committed
Fix slim cmd to include extra proxy cmds
1 parent 5f05cbf commit 0d9da63

File tree

3 files changed

+367
-357
lines changed

3 files changed

+367
-357
lines changed

enterprise/cli/proxyserver.go

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

enterprise/cli/workspaceproxy_slim.go renamed to enterprise/cli/proxyserver_slim.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ import (
1111
"github.com/coder/coder/cli/cliui"
1212
)
1313

14-
func (r *RootCmd) workspaceProxy() *clibase.Cmd {
14+
func (r *RootCmd) proxyServer() *clibase.Cmd {
1515
root := &clibase.Cmd{
16-
Use: "workspace-proxy",
17-
Short: "Manage workspace proxies",
18-
Aliases: []string{"proxy"},
16+
Use: "server",
17+
Short: "Start a workspace proxy server",
18+
Aliases: []string{},
1919
// We accept RawArgs so all commands and flags are accepted.
2020
RawArgs: true,
2121
Hidden: true,

0 commit comments

Comments
 (0)