chore: Export all functions used by server cmd (#7118)

* chore: Export all functions used by server cmd

Required to make workspace proxy cmd
* Factor out httpservers and tracer
This commit is contained in:
Steven Masley
2023-04-13 09:07:19 -05:00
committed by GitHub
parent 87fe16cde9
commit fa64c58e56
3 changed files with 242 additions and 192 deletions
+236 -186
View File
@@ -169,8 +169,8 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
Short: "Start a Coder server",
Options: opts,
Middleware: clibase.Chain(
writeConfigMW(cfg),
printDeprecatedOptions(),
WriteConfigMW(cfg),
PrintDeprecatedOptions(),
clibase.RequireNArgs(0),
),
Handler: func(inv *clibase.Invocation) error {
@@ -183,7 +183,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
cliui.Warnf(inv.Stderr, "YAML support is experimental and offers no compatibility guarantees.")
}
go dumpHandler(ctx)
go DumpHandler(ctx)
// Validate bind addresses.
if cfg.Address.String() != "" {
@@ -218,8 +218,8 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
filesRateLimit = -1
}
printLogo(inv)
logger, logCloser, err := buildLogger(inv, cfg)
PrintLogo(inv)
logger, logCloser, err := BuildLogger(inv, cfg)
if err != nil {
return xerrors.Errorf("make logger: %w", err)
}
@@ -255,44 +255,12 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
// which is caught by goleaks.
defer http.DefaultClient.CloseIdleConnections()
var (
tracerProvider trace.TracerProvider
sqlDriver = "postgres"
)
// Coder tracing should be disabled if telemetry is disabled unless
// --telemetry-trace was explicitly provided.
shouldCoderTrace := cfg.Telemetry.Enable.Value() && !isTest()
// Only override if telemetryTraceEnable was specifically set.
// By default we want it to be controlled by telemetryEnable.
if inv.ParsedFlags().Changed("telemetry-trace") {
shouldCoderTrace = cfg.Telemetry.Trace.Value()
}
if cfg.Trace.Enable.Value() || shouldCoderTrace || cfg.Trace.HoneycombAPIKey != "" {
sdkTracerProvider, closeTracing, err := tracing.TracerProvider(ctx, "coderd", tracing.TracerOpts{
Default: cfg.Trace.Enable.Value(),
Coder: shouldCoderTrace,
Honeycomb: cfg.Trace.HoneycombAPIKey.String(),
})
if err != nil {
logger.Warn(ctx, "start telemetry exporter", slog.Error(err))
} else {
// allow time for traces to flush even if command context is canceled
defer func() {
_ = shutdownWithTimeout(closeTracing, 5*time.Second)
}()
d, err := tracing.PostgresDriver(sdkTracerProvider, "coderd.database")
if err != nil {
logger.Warn(ctx, "start postgres tracing driver", slog.Error(err))
} else {
sqlDriver = d
}
tracerProvider = sdkTracerProvider
}
tracerProvider, sqlDriver := ConfigureTraceProvider(ctx, logger, inv, cfg)
httpServers, err := ConfigureHTTPServers(inv, cfg)
if err != nil {
return xerrors.Errorf("configure http(s): %w", err)
}
defer httpServers.Close()
config := r.createConfig()
@@ -322,121 +290,13 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
}()
}
var (
httpListener net.Listener
httpURL *url.URL
)
if cfg.HTTPAddress.String() != "" {
httpListener, err = net.Listen("tcp", cfg.HTTPAddress.String())
if err != nil {
return err
}
defer httpListener.Close()
listenAddrStr := httpListener.Addr().String()
// For some reason if 0.0.0.0:x is provided as the http address,
// httpListener.Addr().String() likes to return it as an ipv6
// address (i.e. [::]:x). If the input ip is 0.0.0.0, try to
// coerce the output back to ipv4 to make it less confusing.
if strings.Contains(cfg.HTTPAddress.String(), "0.0.0.0") {
listenAddrStr = strings.ReplaceAll(listenAddrStr, "[::]", "0.0.0.0")
}
// We want to print out the address the user supplied, not the
// loopback device.
_, _ = fmt.Fprintf(inv.Stdout, "Started HTTP listener at %s\n", (&url.URL{Scheme: "http", Host: listenAddrStr}).String())
// Set the http URL we want to use when connecting to ourselves.
tcpAddr, tcpAddrValid := httpListener.Addr().(*net.TCPAddr)
if !tcpAddrValid {
return xerrors.Errorf("invalid TCP address type %T", httpListener.Addr())
}
if tcpAddr.IP.IsUnspecified() {
tcpAddr.IP = net.IPv4(127, 0, 0, 1)
}
httpURL = &url.URL{
Scheme: "http",
Host: tcpAddr.String(),
}
}
var (
tlsConfig *tls.Config
httpsListener net.Listener
httpsURL *url.URL
)
if cfg.TLS.Enable {
if cfg.TLS.Address.String() == "" {
return xerrors.New("tls address must be set if tls is enabled")
}
// DEPRECATED: This redirect used to default to true.
// It made more sense to have the redirect be opt-in.
if inv.Environ.Get("CODER_TLS_REDIRECT_HTTP") == "true" || inv.ParsedFlags().Changed("tls-redirect-http-to-https") {
cliui.Warn(inv.Stderr, "--tls-redirect-http-to-https is deprecated, please use --redirect-to-access-url instead")
cfg.RedirectToAccessURL = cfg.TLS.RedirectHTTP
}
tlsConfig, err = configureTLS(
cfg.TLS.MinVersion.String(),
cfg.TLS.ClientAuth.String(),
cfg.TLS.CertFiles,
cfg.TLS.KeyFiles,
cfg.TLS.ClientCAFile.String(),
)
if err != nil {
return xerrors.Errorf("configure tls: %w", err)
}
httpsListenerInner, err := net.Listen("tcp", cfg.TLS.Address.String())
if err != nil {
return err
}
defer httpsListenerInner.Close()
httpsListener = tls.NewListener(httpsListenerInner, tlsConfig)
defer httpsListener.Close()
listenAddrStr := httpsListener.Addr().String()
// For some reason if 0.0.0.0:x is provided as the https
// address, httpsListener.Addr().String() likes to return it as
// an ipv6 address (i.e. [::]:x). If the input ip is 0.0.0.0,
// try to coerce the output back to ipv4 to make it less
// confusing.
if strings.Contains(cfg.HTTPAddress.String(), "0.0.0.0") {
listenAddrStr = strings.ReplaceAll(listenAddrStr, "[::]", "0.0.0.0")
}
// We want to print out the address the user supplied, not the
// loopback device.
_, _ = fmt.Fprintf(inv.Stdout, "Started TLS/HTTPS listener at %s\n", (&url.URL{Scheme: "https", Host: listenAddrStr}).String())
// Set the https URL we want to use when connecting to
// ourselves.
tcpAddr, tcpAddrValid := httpsListener.Addr().(*net.TCPAddr)
if !tcpAddrValid {
return xerrors.Errorf("invalid TCP address type %T", httpsListener.Addr())
}
if tcpAddr.IP.IsUnspecified() {
tcpAddr.IP = net.IPv4(127, 0, 0, 1)
}
httpsURL = &url.URL{
Scheme: "https",
Host: tcpAddr.String(),
}
}
// Sanity check that at least one listener was started.
if httpListener == nil && httpsListener == nil {
return xerrors.New("must listen on at least one address")
}
// Prefer HTTP because it's less prone to TLS errors over localhost.
localURL := httpsURL
if httpURL != nil {
localURL = httpURL
localURL := httpServers.TLSUrl
if httpServers.HTTPUrl != nil {
localURL = httpServers.HTTPUrl
}
ctx, httpClient, err := configureHTTPClient(
ctx, httpClient, err := ConfigureHTTPClient(
ctx,
cfg.TLS.ClientCertFile.String(),
cfg.TLS.ClientKeyFile.String(),
@@ -486,7 +346,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
}
// Warn the user if the access URL appears to be a loopback address.
isLocal, err := isLocalURL(ctx, cfg.AccessURL.Value())
isLocal, err := IsLocalURL(ctx, cfg.AccessURL.Value())
if isLocal || err != nil {
reason := "could not be resolved"
if isLocal {
@@ -607,8 +467,8 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
SSHConfigOptions: configSSHOptions,
},
}
if tlsConfig != nil {
options.TLSCertificates = tlsConfig.Certificates
if httpServers.TLSConfig != nil {
options.TLSCertificates = httpServers.TLSConfig.Certificates
}
if cfg.StrictTransportSecurity > 0 {
@@ -826,7 +686,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
_ = pprof.Handler
if cfg.Pprof.Enable {
//nolint:revive
defer serveHandler(ctx, logger, nil, cfg.Pprof.Address.String(), "pprof")()
defer ServeHandler(ctx, logger, nil, cfg.Pprof.Address.String(), "pprof")()
}
if cfg.Prometheus.Enable {
options.PrometheusRegistry.MustRegister(collectors.NewGoCollector())
@@ -845,7 +705,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
defer closeWorkspacesFunc()
//nolint:revive
defer serveHandler(ctx, logger, promhttp.InstrumentMetricHandler(
defer ServeHandler(ctx, logger, promhttp.InstrumentMetricHandler(
options.PrometheusRegistry, promhttp.HandlerFor(options.PrometheusRegistry, promhttp.HandlerOpts{}),
), cfg.Prometheus.Address.String(), "prometheus")()
}
@@ -872,7 +732,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
}
client := codersdk.New(localURL)
if localURL.Scheme == "https" && isLocalhost(localURL.Hostname()) {
if localURL.Scheme == "https" && IsLocalhost(localURL.Hostname()) {
// The certificate will likely be self-signed or for a different
// hostname, so we need to skip verification.
client.HTTPClient.Transport = &http.Transport{
@@ -956,30 +816,17 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
// We call this in the routine so we can kill the other listeners if
// one of them fails.
closeListenersNow := func() {
if httpListener != nil {
_ = httpListener.Close()
}
if httpsListener != nil {
_ = httpsListener.Close()
}
httpServers.Close()
if tunnel != nil {
_ = tunnel.Listener.Close()
}
}
eg := errgroup.Group{}
if httpListener != nil {
eg.Go(func() error {
defer closeListenersNow()
return httpServer.Serve(httpListener)
})
}
if httpsListener != nil {
eg.Go(func() error {
defer closeListenersNow()
return httpServer.Serve(httpsListener)
})
}
eg.Go(func() error {
defer closeListenersNow()
return httpServers.Serve(httpServer)
})
if tunnel != nil {
eg.Go(func() error {
defer closeListenersNow()
@@ -1186,7 +1033,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
// printDeprecatedOptions loops through all command options, and prints
// a warning for usage of deprecated options.
func printDeprecatedOptions() clibase.MiddlewareFunc {
func PrintDeprecatedOptions() clibase.MiddlewareFunc {
return func(next clibase.HandlerFunc) clibase.HandlerFunc {
return func(inv *clibase.Invocation) error {
opts := inv.Command.Options
@@ -1222,7 +1069,7 @@ func printDeprecatedOptions() clibase.MiddlewareFunc {
// writeConfigMW will prevent the main command from running if the write-config
// flag is set. Instead, it will marshal the command options to YAML and write
// them to stdout.
func writeConfigMW(cfg *codersdk.DeploymentValues) clibase.MiddlewareFunc {
func WriteConfigMW(cfg *codersdk.DeploymentValues) clibase.MiddlewareFunc {
return func(next clibase.HandlerFunc) clibase.HandlerFunc {
return func(inv *clibase.Invocation) error {
if !cfg.WriteConfig {
@@ -1251,7 +1098,7 @@ func writeConfigMW(cfg *codersdk.DeploymentValues) clibase.MiddlewareFunc {
// isLocalURL returns true if the hostname of the provided URL appears to
// resolve to a loopback address.
func isLocalURL(ctx context.Context, u *url.URL) (bool, error) {
func IsLocalURL(ctx context.Context, u *url.URL) (bool, error) {
resolver := &net.Resolver{}
ips, err := resolver.LookupIPAddr(ctx, u.Hostname())
if err != nil {
@@ -1377,7 +1224,7 @@ func newProvisionerDaemon(
}
// nolint: revive
func printLogo(inv *clibase.Invocation) {
func PrintLogo(inv *clibase.Invocation) {
// Only print the logo in TTYs.
if !isTTYOut(inv) {
return
@@ -1723,7 +1570,7 @@ func startBuiltinPostgres(ctx context.Context, cfg config.Root, logger slog.Logg
return connectionURL, ep.Stop, nil
}
func configureHTTPClient(ctx context.Context, clientCertFile, clientKeyFile string, tlsClientCAFile string) (context.Context, *http.Client, error) {
func ConfigureHTTPClient(ctx context.Context, clientCertFile, clientKeyFile string, tlsClientCAFile string) (context.Context, *http.Client, error) {
if clientCertFile != "" && clientKeyFile != "" {
certificates, err := loadCertificates([]string{clientCertFile}, []string{clientKeyFile})
if err != nil {
@@ -1783,13 +1630,13 @@ func redirectToAccessURL(handler http.Handler, accessURL *url.URL, tunnel bool,
})
}
// isLocalhost returns true if the host points to the local machine. Intended to
// IsLocalhost returns true if the host points to the local machine. Intended to
// be called with `u.Hostname()`.
func isLocalhost(host string) bool {
func IsLocalhost(host string) bool {
return host == "localhost" || host == "127.0.0.1" || host == "::1"
}
func buildLogger(inv *clibase.Invocation, cfg *codersdk.DeploymentValues) (slog.Logger, func(), error) {
func BuildLogger(inv *clibase.Invocation, cfg *codersdk.DeploymentValues) (slog.Logger, func(), error) {
var (
sinks = []slog.Sink{}
closers = []func() error{}
@@ -1916,3 +1763,206 @@ func connectToPostgres(ctx context.Context, logger slog.Logger, driver string, d
ok = true
return sqlDB, nil
}
type HTTPServers struct {
HTTPUrl *url.URL
HTTPListener net.Listener
// TLS
TLSUrl *url.URL
TLSListener net.Listener
TLSConfig *tls.Config
}
// Serve acts just like http.Serve. It is a blocking call until the server
// is closed, and an error is returned if any underlying Serve call fails.
func (s *HTTPServers) Serve(srv *http.Server) error {
eg := errgroup.Group{}
if s.HTTPListener != nil {
eg.Go(func() error {
defer s.Close() // close all listeners on error
return srv.Serve(s.HTTPListener)
})
}
if s.TLSListener != nil {
eg.Go(func() error {
defer s.Close() // close all listeners on error
return srv.Serve(s.TLSListener)
})
}
return eg.Wait()
}
func (s *HTTPServers) Close() {
if s.HTTPListener != nil {
_ = s.HTTPListener.Close()
}
if s.TLSListener != nil {
_ = s.TLSListener.Close()
}
}
func ConfigureTraceProvider(ctx context.Context, logger slog.Logger, inv *clibase.Invocation, cfg *codersdk.DeploymentValues) (trace.TracerProvider, string) {
var (
tracerProvider trace.TracerProvider
sqlDriver = "postgres"
)
// Coder tracing should be disabled if telemetry is disabled unless
// --telemetry-trace was explicitly provided.
shouldCoderTrace := cfg.Telemetry.Enable.Value() && !isTest()
// Only override if telemetryTraceEnable was specifically set.
// By default we want it to be controlled by telemetryEnable.
if inv.ParsedFlags().Changed("telemetry-trace") {
shouldCoderTrace = cfg.Telemetry.Trace.Value()
}
if cfg.Trace.Enable.Value() || shouldCoderTrace || cfg.Trace.HoneycombAPIKey != "" {
sdkTracerProvider, closeTracing, err := tracing.TracerProvider(ctx, "coderd", tracing.TracerOpts{
Default: cfg.Trace.Enable.Value(),
Coder: shouldCoderTrace,
Honeycomb: cfg.Trace.HoneycombAPIKey.String(),
})
if err != nil {
logger.Warn(ctx, "start telemetry exporter", slog.Error(err))
} else {
// allow time for traces to flush even if command context is canceled
defer func() {
_ = shutdownWithTimeout(closeTracing, 5*time.Second)
}()
d, err := tracing.PostgresDriver(sdkTracerProvider, "coderd.database")
if err != nil {
logger.Warn(ctx, "start postgres tracing driver", slog.Error(err))
} else {
sqlDriver = d
}
tracerProvider = sdkTracerProvider
}
}
return tracerProvider, sqlDriver
}
func ConfigureHTTPServers(inv *clibase.Invocation, cfg *codersdk.DeploymentValues) (_ *HTTPServers, err error) {
httpServers := &HTTPServers{}
defer func() {
if err != nil {
// Always close the listeners if we fail.
httpServers.Close()
}
}()
// Validate bind addresses.
if cfg.Address.String() != "" {
if cfg.TLS.Enable {
cfg.HTTPAddress = ""
cfg.TLS.Address = cfg.Address
} else {
_ = cfg.HTTPAddress.Set(cfg.Address.String())
cfg.TLS.Address.Host = ""
cfg.TLS.Address.Port = ""
}
}
if cfg.TLS.Enable && cfg.TLS.Address.String() == "" {
return nil, xerrors.Errorf("TLS address must be set if TLS is enabled")
}
if !cfg.TLS.Enable && cfg.HTTPAddress.String() == "" {
return nil, xerrors.Errorf("TLS is disabled. Enable with --tls-enable or specify a HTTP address")
}
if cfg.AccessURL.String() != "" &&
!(cfg.AccessURL.Scheme == "http" || cfg.AccessURL.Scheme == "https") {
return nil, xerrors.Errorf("access-url must include a scheme (e.g. 'http://' or 'https://)")
}
addrString := func(l net.Listener) string {
listenAddrStr := l.Addr().String()
// For some reason if 0.0.0.0:x is provided as the https
// address, httpsListener.Addr().String() likes to return it as
// an ipv6 address (i.e. [::]:x). If the input ip is 0.0.0.0,
// try to coerce the output back to ipv4 to make it less
// confusing.
if strings.Contains(cfg.HTTPAddress.String(), "0.0.0.0") {
listenAddrStr = strings.ReplaceAll(listenAddrStr, "[::]", "0.0.0.0")
}
return listenAddrStr
}
if cfg.HTTPAddress.String() != "" {
httpServers.HTTPListener, err = net.Listen("tcp", cfg.HTTPAddress.String())
if err != nil {
return nil, err
}
// We want to print out the address the user supplied, not the
// loopback device.
_, _ = fmt.Fprintf(inv.Stdout, "Started HTTP listener at %s\n", (&url.URL{Scheme: "http", Host: addrString(httpServers.HTTPListener)}).String())
// Set the http URL we want to use when connecting to ourselves.
tcpAddr, tcpAddrValid := httpServers.HTTPListener.Addr().(*net.TCPAddr)
if !tcpAddrValid {
return nil, xerrors.Errorf("invalid TCP address type %T", httpServers.HTTPListener.Addr())
}
if tcpAddr.IP.IsUnspecified() {
tcpAddr.IP = net.IPv4(127, 0, 0, 1)
}
httpServers.HTTPUrl = &url.URL{
Scheme: "http",
Host: tcpAddr.String(),
}
}
if cfg.TLS.Enable {
if cfg.TLS.Address.String() == "" {
return nil, xerrors.New("tls address must be set if tls is enabled")
}
// DEPRECATED: This redirect used to default to true.
// It made more sense to have the redirect be opt-in.
if inv.Environ.Get("CODER_TLS_REDIRECT_HTTP") == "true" || inv.ParsedFlags().Changed("tls-redirect-http-to-https") {
cliui.Warn(inv.Stderr, "--tls-redirect-http-to-https is deprecated, please use --redirect-to-access-url instead")
cfg.RedirectToAccessURL = cfg.TLS.RedirectHTTP
}
tlsConfig, err := configureTLS(
cfg.TLS.MinVersion.String(),
cfg.TLS.ClientAuth.String(),
cfg.TLS.CertFiles,
cfg.TLS.KeyFiles,
cfg.TLS.ClientCAFile.String(),
)
if err != nil {
return nil, xerrors.Errorf("configure tls: %w", err)
}
httpsListenerInner, err := net.Listen("tcp", cfg.TLS.Address.String())
if err != nil {
return nil, err
}
httpServers.TLSConfig = tlsConfig
httpServers.TLSListener = tls.NewListener(httpsListenerInner, tlsConfig)
// We want to print out the address the user supplied, not the
// loopback device.
_, _ = fmt.Fprintf(inv.Stdout, "Started TLS/HTTPS listener at %s\n", (&url.URL{Scheme: "https", Host: addrString(httpServers.TLSListener)}).String())
// Set the https URL we want to use when connecting to
// ourselves.
tcpAddr, tcpAddrValid := httpServers.TLSListener.Addr().(*net.TCPAddr)
if !tcpAddrValid {
return nil, xerrors.Errorf("invalid TCP address type %T", httpServers.TLSListener.Addr())
}
if tcpAddr.IP.IsUnspecified() {
tcpAddr.IP = net.IPv4(127, 0, 0, 1)
}
httpServers.TLSUrl = &url.URL{
Scheme: "https",
Host: tcpAddr.String(),
}
}
if httpServers.HTTPListener == nil && httpServers.TLSListener == nil {
return nil, xerrors.New("must listen on at least one address")
}
return httpServers, nil
}