mirror of https://github.com/coder/coder.git
362 lines
12 KiB
Go
362 lines
12 KiB
Go
//go:build !slim
|
|
|
|
package cli
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"net/http/pprof"
|
|
"os/signal"
|
|
"regexp"
|
|
rpprof "runtime/pprof"
|
|
"time"
|
|
|
|
"github.com/coreos/go-systemd/daemon"
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"github.com/prometheus/client_golang/prometheus/collectors"
|
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
|
"golang.org/x/xerrors"
|
|
|
|
"cdr.dev/slog"
|
|
"github.com/coder/coder/cli"
|
|
"github.com/coder/coder/cli/clibase"
|
|
"github.com/coder/coder/cli/cliui"
|
|
"github.com/coder/coder/coderd/httpapi"
|
|
"github.com/coder/coder/coderd/httpmw"
|
|
"github.com/coder/coder/codersdk"
|
|
"github.com/coder/coder/enterprise/wsproxy"
|
|
)
|
|
|
|
type closers []func()
|
|
|
|
func (c closers) Close() {
|
|
for _, closeF := range c {
|
|
closeF()
|
|
}
|
|
}
|
|
|
|
func (c *closers) Add(f func()) {
|
|
*c = append(*c, f)
|
|
}
|
|
|
|
func (*RootCmd) proxyServer() *clibase.Cmd {
|
|
var (
|
|
cfg = new(codersdk.DeploymentValues)
|
|
// Filter options for only relevant ones.
|
|
opts = cfg.Options().Filter(codersdk.IsWorkspaceProxies)
|
|
|
|
externalProxyOptionGroup = clibase.Group{
|
|
Name: "External Workspace Proxy",
|
|
YAML: "externalWorkspaceProxy",
|
|
}
|
|
proxySessionToken clibase.String
|
|
primaryAccessURL clibase.URL
|
|
)
|
|
opts.Add(
|
|
// Options only for external workspace proxies
|
|
|
|
clibase.Option{
|
|
Name: "Proxy Session Token",
|
|
Description: "Authentication token for the workspace proxy to communicate with coderd.",
|
|
Flag: "proxy-session-token",
|
|
Env: "CODER_PROXY_SESSION_TOKEN",
|
|
YAML: "proxySessionToken",
|
|
Default: "",
|
|
Value: &proxySessionToken,
|
|
Group: &externalProxyOptionGroup,
|
|
Hidden: false,
|
|
},
|
|
|
|
clibase.Option{
|
|
Name: "Coderd (Primary) Access URL",
|
|
Description: "URL to communicate with coderd. This should match the access URL of the Coder deployment.",
|
|
Flag: "primary-access-url",
|
|
Env: "CODER_PRIMARY_ACCESS_URL",
|
|
YAML: "primaryAccessURL",
|
|
Default: "",
|
|
Value: &primaryAccessURL,
|
|
Group: &externalProxyOptionGroup,
|
|
Hidden: false,
|
|
},
|
|
)
|
|
|
|
cmd := &clibase.Cmd{
|
|
Use: "server",
|
|
Short: "Start a workspace proxy server",
|
|
Options: opts,
|
|
Middleware: clibase.Chain(
|
|
cli.WriteConfigMW(cfg),
|
|
cli.PrintDeprecatedOptions(),
|
|
clibase.RequireNArgs(0),
|
|
),
|
|
Handler: func(inv *clibase.Invocation) error {
|
|
if !(primaryAccessURL.Scheme == "http" || primaryAccessURL.Scheme == "https") {
|
|
return xerrors.Errorf("primary access URL must be http or https: url=%s", primaryAccessURL.String())
|
|
}
|
|
|
|
var closers closers
|
|
// Main command context for managing cancellation of running
|
|
// services.
|
|
ctx, topCancel := context.WithCancel(inv.Context())
|
|
defer topCancel()
|
|
closers.Add(topCancel)
|
|
|
|
go cli.DumpHandler(ctx)
|
|
|
|
cli.PrintLogo(inv)
|
|
logger, logCloser, err := cli.BuildLogger(inv, cfg)
|
|
if err != nil {
|
|
return xerrors.Errorf("make logger: %w", err)
|
|
}
|
|
defer logCloser()
|
|
closers.Add(logCloser)
|
|
|
|
logger.Debug(ctx, "started debug logging")
|
|
logger.Sync()
|
|
|
|
// Register signals early on so that graceful shutdown can't
|
|
// be interrupted by additional signals. Note that we avoid
|
|
// shadowing cancel() (from above) here because notifyStop()
|
|
// restores default behavior for the signals. This protects
|
|
// the shutdown sequence from abruptly terminating things
|
|
// like: database migrations, provisioner work, workspace
|
|
// cleanup in dev-mode, etc.
|
|
//
|
|
// To get out of a graceful shutdown, the user can send
|
|
// SIGQUIT with ctrl+\ or SIGKILL with `kill -9`.
|
|
notifyCtx, notifyStop := signal.NotifyContext(ctx, cli.InterruptSignals...)
|
|
defer notifyStop()
|
|
|
|
// Clean up idle connections at the end, e.g.
|
|
// embedded-postgres can leave an idle connection
|
|
// which is caught by goleaks.
|
|
defer http.DefaultClient.CloseIdleConnections()
|
|
closers.Add(http.DefaultClient.CloseIdleConnections)
|
|
|
|
tracer, _, closeTracing := cli.ConfigureTraceProvider(ctx, logger, inv, cfg)
|
|
defer func() {
|
|
logger.Debug(ctx, "closing tracing")
|
|
traceCloseErr := shutdownWithTimeout(closeTracing, 5*time.Second)
|
|
logger.Debug(ctx, "tracing closed", slog.Error(traceCloseErr))
|
|
}()
|
|
|
|
httpServers, err := cli.ConfigureHTTPServers(inv, cfg)
|
|
if err != nil {
|
|
return xerrors.Errorf("configure http(s): %w", err)
|
|
}
|
|
defer httpServers.Close()
|
|
closers.Add(httpServers.Close)
|
|
|
|
// If no access url given, use the local address.
|
|
if cfg.AccessURL.String() == "" {
|
|
// Prefer TLS
|
|
if httpServers.TLSUrl != nil {
|
|
cfg.AccessURL = clibase.URL(*httpServers.TLSUrl)
|
|
} else if httpServers.HTTPUrl != nil {
|
|
cfg.AccessURL = clibase.URL(*httpServers.HTTPUrl)
|
|
}
|
|
}
|
|
|
|
// TODO: @emyrk I find this strange that we add this to the context
|
|
// at the root here.
|
|
ctx, httpClient, err := cli.ConfigureHTTPClient(
|
|
ctx,
|
|
cfg.TLS.ClientCertFile.String(),
|
|
cfg.TLS.ClientKeyFile.String(),
|
|
cfg.TLS.ClientCAFile.String(),
|
|
)
|
|
if err != nil {
|
|
return xerrors.Errorf("configure http client: %w", err)
|
|
}
|
|
defer httpClient.CloseIdleConnections()
|
|
closers.Add(httpClient.CloseIdleConnections)
|
|
|
|
// Warn the user if the access URL appears to be a loopback address.
|
|
isLocal, err := cli.IsLocalURL(ctx, cfg.AccessURL.Value())
|
|
if isLocal || err != nil {
|
|
reason := "could not be resolved"
|
|
if isLocal {
|
|
reason = "isn't externally reachable"
|
|
}
|
|
cliui.Warnf(
|
|
inv.Stderr,
|
|
"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",
|
|
cliui.Styles.Field.Render(cfg.AccessURL.String()), reason,
|
|
)
|
|
}
|
|
|
|
// A newline is added before for visibility in terminal output.
|
|
cliui.Infof(inv.Stdout, "\nView the Web UI: %s", cfg.AccessURL.String())
|
|
|
|
var appHostnameRegex *regexp.Regexp
|
|
appHostname := cfg.WildcardAccessURL.String()
|
|
if appHostname != "" {
|
|
appHostnameRegex, err = httpapi.CompileHostnamePattern(appHostname)
|
|
if err != nil {
|
|
return xerrors.Errorf("parse wildcard access URL %q: %w", appHostname, err)
|
|
}
|
|
}
|
|
|
|
realIPConfig, err := httpmw.ParseRealIPConfig(cfg.ProxyTrustedHeaders, cfg.ProxyTrustedOrigins)
|
|
if err != nil {
|
|
return xerrors.Errorf("parse real ip config: %w", err)
|
|
}
|
|
|
|
if cfg.Pprof.Enable {
|
|
// This prevents the pprof import from being accidentally deleted.
|
|
// pprof has an init function that attaches itself to the default handler.
|
|
// By passing a nil handler to 'serverHandler', it will automatically use
|
|
// the default, which has pprof attached.
|
|
_ = pprof.Handler
|
|
//nolint:revive
|
|
closeFunc := cli.ServeHandler(ctx, logger, nil, cfg.Pprof.Address.String(), "pprof")
|
|
defer closeFunc()
|
|
closers.Add(closeFunc)
|
|
}
|
|
|
|
prometheusRegistry := prometheus.NewRegistry()
|
|
if cfg.Prometheus.Enable {
|
|
prometheusRegistry.MustRegister(collectors.NewGoCollector())
|
|
prometheusRegistry.MustRegister(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}))
|
|
|
|
//nolint:revive
|
|
closeFunc := cli.ServeHandler(ctx, logger, promhttp.InstrumentMetricHandler(
|
|
prometheusRegistry, promhttp.HandlerFor(prometheusRegistry, promhttp.HandlerOpts{}),
|
|
), cfg.Prometheus.Address.String(), "prometheus")
|
|
defer closeFunc()
|
|
closers.Add(closeFunc)
|
|
}
|
|
|
|
proxy, err := wsproxy.New(ctx, &wsproxy.Options{
|
|
Logger: logger,
|
|
HTTPClient: httpClient,
|
|
DashboardURL: primaryAccessURL.Value(),
|
|
AccessURL: cfg.AccessURL.Value(),
|
|
AppHostname: appHostname,
|
|
AppHostnameRegex: appHostnameRegex,
|
|
RealIPConfig: realIPConfig,
|
|
Tracing: tracer,
|
|
PrometheusRegistry: prometheusRegistry,
|
|
APIRateLimit: int(cfg.RateLimit.API.Value()),
|
|
SecureAuthCookie: cfg.SecureAuthCookie.Value(),
|
|
DisablePathApps: cfg.DisablePathApps.Value(),
|
|
ProxySessionToken: proxySessionToken.Value(),
|
|
AllowAllCors: cfg.Dangerous.AllowAllCors.Value(),
|
|
})
|
|
if err != nil {
|
|
return xerrors.Errorf("create workspace proxy: %w", err)
|
|
}
|
|
closers.Add(func() { _ = proxy.Close() })
|
|
|
|
shutdownConnsCtx, shutdownConns := context.WithCancel(ctx)
|
|
defer shutdownConns()
|
|
closers.Add(shutdownConns)
|
|
// ReadHeaderTimeout is purposefully not enabled. It caused some
|
|
// issues with websockets over the dev tunnel.
|
|
// See: https://github.com/coder/coder/pull/3730
|
|
//nolint:gosec
|
|
httpServer := &http.Server{
|
|
// These errors are typically noise like "TLS: EOF". Vault does
|
|
// similar:
|
|
// https://github.com/hashicorp/vault/blob/e2490059d0711635e529a4efcbaa1b26998d6e1c/command/server.go#L2714
|
|
ErrorLog: log.New(io.Discard, "", 0),
|
|
Handler: proxy.Handler,
|
|
BaseContext: func(_ net.Listener) context.Context {
|
|
return shutdownConnsCtx
|
|
},
|
|
}
|
|
defer func() {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
_ = httpServer.Shutdown(ctx)
|
|
}()
|
|
|
|
// TODO: So this obviously is not going to work well.
|
|
errCh := make(chan error, 1)
|
|
go rpprof.Do(ctx, rpprof.Labels("service", "workspace-proxy"), func(ctx context.Context) {
|
|
errCh <- httpServers.Serve(httpServer)
|
|
})
|
|
|
|
cliui.Infof(inv.Stdout, "\n==> Logs will stream in below (press ctrl+c to gracefully exit):")
|
|
|
|
// Updates the systemd status from activating to activated.
|
|
_, err = daemon.SdNotify(false, daemon.SdNotifyReady)
|
|
if err != nil {
|
|
return xerrors.Errorf("notify systemd: %w", err)
|
|
}
|
|
|
|
// Currently there is no way to ask the server to shut
|
|
// itself down, so any exit signal will result in a non-zero
|
|
// exit of the server.
|
|
var exitErr error
|
|
select {
|
|
case exitErr = <-errCh:
|
|
case <-notifyCtx.Done():
|
|
exitErr = notifyCtx.Err()
|
|
_, _ = fmt.Fprintln(inv.Stdout, cliui.Styles.Bold.Render(
|
|
"Interrupt caught, gracefully exiting. Use ctrl+\\ to force quit",
|
|
))
|
|
}
|
|
|
|
if exitErr != nil && !xerrors.Is(exitErr, context.Canceled) {
|
|
cliui.Errorf(inv.Stderr, "Unexpected error, shutting down server: %s\n", exitErr)
|
|
}
|
|
|
|
// Begin clean shut down stage, we try to shut down services
|
|
// gracefully in an order that gives the best experience.
|
|
// This procedure should not differ greatly from the order
|
|
// of `defer`s in this function, but allows us to inform
|
|
// the user about what's going on and handle errors more
|
|
// explicitly.
|
|
|
|
_, err = daemon.SdNotify(false, daemon.SdNotifyStopping)
|
|
if err != nil {
|
|
cliui.Errorf(inv.Stderr, "Notify systemd failed: %s", err)
|
|
}
|
|
|
|
// Stop accepting new connections without interrupting
|
|
// in-flight requests, give in-flight requests 5 seconds to
|
|
// complete.
|
|
cliui.Info(inv.Stdout, "Shutting down API server..."+"\n")
|
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
|
defer cancel()
|
|
err = httpServer.Shutdown(shutdownCtx)
|
|
if err != nil {
|
|
cliui.Errorf(inv.Stderr, "API server shutdown took longer than 3s: %s\n", err)
|
|
} else {
|
|
cliui.Info(inv.Stdout, "Gracefully shut down API server\n")
|
|
}
|
|
// Cancel any remaining in-flight requests.
|
|
shutdownConns()
|
|
|
|
// Trigger context cancellation for any remaining services.
|
|
closers.Close()
|
|
|
|
switch {
|
|
case xerrors.Is(exitErr, context.DeadlineExceeded):
|
|
cliui.Warnf(inv.Stderr, "Graceful shutdown timed out")
|
|
// Errors here cause a significant number of benign CI failures.
|
|
return nil
|
|
case xerrors.Is(exitErr, context.Canceled):
|
|
return nil
|
|
case exitErr != nil:
|
|
return xerrors.Errorf("graceful shutdown: %w", exitErr)
|
|
default:
|
|
return nil
|
|
}
|
|
},
|
|
}
|
|
|
|
return cmd
|
|
}
|
|
|
|
func shutdownWithTimeout(shutdown func(context.Context) error, timeout time.Duration) error {
|
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
|
defer cancel()
|
|
return shutdown(ctx)
|
|
}
|