feat: refactor deployment config (#6347)

This commit is contained in:
Ammar Bandukwala
2023-03-07 15:10:01 -06:00
committed by GitHub
parent bb0a996fc2
commit 3b73321a6c
102 changed files with 5643 additions and 6682 deletions
+371 -160
View File
@@ -12,6 +12,7 @@ import (
"database/sql"
"encoding/hex"
"errors"
"flag"
"fmt"
"io"
"log"
@@ -25,6 +26,7 @@ import (
"os/user"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
"sync"
@@ -40,7 +42,6 @@ import (
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/spf13/afero"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.opentelemetry.io/otel/trace"
"golang.org/x/mod/semver"
"golang.org/x/oauth2"
@@ -49,6 +50,7 @@ import (
"golang.org/x/xerrors"
"google.golang.org/api/idtoken"
"google.golang.org/api/option"
"gopkg.in/yaml.v3"
"tailscale.com/tailcfg"
"cdr.dev/slog"
@@ -56,9 +58,9 @@ import (
"cdr.dev/slog/sloggers/slogjson"
"cdr.dev/slog/sloggers/slogstackdriver"
"github.com/coder/coder/buildinfo"
"github.com/coder/coder/cli/clibase"
"github.com/coder/coder/cli/cliui"
"github.com/coder/coder/cli/config"
"github.com/coder/coder/cli/deployment"
"github.com/coder/coder/coderd"
"github.com/coder/coder/coderd/autobuild/executor"
"github.com/coder/coder/coderd/database"
@@ -84,48 +86,221 @@ import (
"github.com/coder/coder/tailnet"
)
// ReadGitAuthProvidersFromEnv is provided for compatibility purposes with the
// viper CLI.
// DEPRECATED
func ReadGitAuthProvidersFromEnv(environ []string) ([]codersdk.GitAuthConfig, error) {
// The index numbers must be in-order.
sort.Strings(environ)
var providers []codersdk.GitAuthConfig
for _, v := range clibase.EnvsWithPrefix(environ, envPrefix+"GITAUTH_") {
tokens := strings.SplitN(v.Name, "_", 2)
if len(tokens) != 2 {
return nil, xerrors.Errorf("invalid env var: %s", v.Name)
}
providerNum, err := strconv.Atoi(tokens[0])
if err != nil {
return nil, xerrors.Errorf("parse number: %s", v.Name)
}
var provider codersdk.GitAuthConfig
switch {
case len(providers) < providerNum:
return nil, xerrors.Errorf(
"provider num %v skipped: %s",
len(providers),
v.Name,
)
case len(providers) == providerNum:
// At the next next provider.
providers = append(providers, provider)
case len(providers) == providerNum+1:
// At the current provider.
provider = providers[providerNum]
}
key := tokens[1]
switch key {
case "ID":
provider.ID = v.Value
case "TYPE":
provider.Type = v.Value
case "CLIENT_ID":
provider.ClientID = v.Value
case "CLIENT_SECRET":
provider.ClientSecret = v.Value
case "AUTH_URL":
provider.AuthURL = v.Value
case "TOKEN_URL":
provider.TokenURL = v.Value
case "VALIDATE_URL":
provider.ValidateURL = v.Value
case "REGEX":
provider.Regex = v.Value
case "NO_REFRESH":
b, err := strconv.ParseBool(key)
if err != nil {
return nil, xerrors.Errorf("parse bool: %s", v.Value)
}
provider.NoRefresh = b
case "SCOPES":
provider.Scopes = strings.Split(v.Value, " ")
}
providers[providerNum] = provider
}
return providers, nil
}
// nolint:gocyclo
func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*coderd.API, io.Closer, error)) *cobra.Command {
func Server(newAPI func(context.Context, *coderd.Options) (*coderd.API, io.Closer, error)) *cobra.Command {
root := &cobra.Command{
Use: "server",
Short: "Start a Coder server",
Use: "server",
Short: "Start a Coder server",
DisableFlagParsing: true,
RunE: func(cmd *cobra.Command, args []string) error {
// Main command context for managing cancellation of running
// services.
ctx, cancel := context.WithCancel(cmd.Context())
defer cancel()
cfg := &codersdk.DeploymentValues{}
cliOpts := cfg.Options()
var configDir clibase.String
// This is a hack to get around the fact that the Cobra-defined
// flags are not available.
cliOpts.Add(clibase.Option{
Name: "Global Config",
Flag: config.FlagName,
Description: "Global Config is ignored in server mode.",
Hidden: true,
Default: config.DefaultDir(),
Value: &configDir,
})
err := cliOpts.SetDefaults()
if err != nil {
return xerrors.Errorf("set defaults: %w", err)
}
err = cliOpts.ParseEnv(envPrefix, os.Environ())
if err != nil {
return xerrors.Errorf("parse env: %w", err)
}
flagSet := cliOpts.FlagSet()
// These parents and children will be moved once we convert the
// rest of the `cli` package to clibase.
flagSet.Usage = usageFn(cmd.ErrOrStderr(), &clibase.Cmd{
Parent: &clibase.Cmd{
Use: "coder",
},
Children: []*clibase.Cmd{
{
Use: "postgres-builtin-url",
Short: "Output the connection URL for the built-in PostgreSQL deployment.",
},
{
Use: "postgres-builtin-serve",
Short: "Run the built-in PostgreSQL deployment.",
},
},
Use: "server [flags]",
Short: "Start a Coder server",
Long: `
The server provides the Coder dashboard, API, and provisioners.
If no options are provided, the server will start with a built-in postgres
and an access URL provided by Coder's cloud service.
Use the following command to print the built-in postgres URL:
$ coder server postgres-builtin-url
Use the following command to manually run the built-in postgres:
$ coder server postgres-builtin-serve
Options may be provided via environment variables prefixed with "CODER_",
flags, and YAML configuration. The precedence is as follows:
1. Defaults
2. YAML configuration
3. Environment variables
4. Flags
`,
Options: cliOpts,
})
err = flagSet.Parse(args)
if err != nil {
return xerrors.Errorf("parse flags: %w", err)
}
if cfg.WriteConfig {
// TODO: this should output to a file.
n, err := cliOpts.ToYAML()
if err != nil {
return xerrors.Errorf("generate yaml: %w", err)
}
enc := yaml.NewEncoder(cmd.ErrOrStderr())
err = enc.Encode(n)
if err != nil {
return xerrors.Errorf("encode yaml: %w", err)
}
err = enc.Close()
if err != nil {
return xerrors.Errorf("close yaml encoder: %w", err)
}
return nil
}
// Print deprecation warnings.
for _, opt := range cliOpts {
if opt.UseInstead == nil {
continue
}
warnStr := opt.Name + " is deprecated, please use "
for i, use := range opt.UseInstead {
warnStr += use.Name + " "
if i != len(opt.UseInstead)-1 {
warnStr += "and "
}
}
warnStr += "instead.\n"
cmd.PrintErr(
cliui.Styles.Warn.Render("WARN: ") + warnStr,
)
}
go dumpHandler(ctx)
cfg, err := deployment.Config(cmd.Flags(), vip)
if err != nil {
return xerrors.Errorf("getting deployment config: %w", err)
}
// Validate bind addresses.
if cfg.Address.Value != "" {
cmd.PrintErr(cliui.Styles.Warn.Render("WARN:") + " --address and -a are deprecated, please use --http-address and --tls-address instead")
if cfg.TLS.Enable.Value {
cfg.HTTPAddress.Value = ""
cfg.TLS.Address.Value = cfg.Address.Value
if cfg.Address.String() != "" {
if cfg.TLS.Enable {
cfg.HTTPAddress = ""
cfg.TLS.Address = cfg.Address
} else {
cfg.HTTPAddress.Value = cfg.Address.Value
cfg.TLS.Address.Value = ""
_ = cfg.HTTPAddress.Set(cfg.Address.String())
cfg.TLS.Address.Host = ""
cfg.TLS.Address.Port = ""
}
}
if cfg.TLS.Enable.Value && cfg.TLS.Address.Value == "" {
if cfg.TLS.Enable && cfg.TLS.Address.String() == "" {
return xerrors.Errorf("TLS address must be set if TLS is enabled")
}
if !cfg.TLS.Enable.Value && cfg.HTTPAddress.Value == "" {
if !cfg.TLS.Enable && cfg.HTTPAddress.String() == "" {
return xerrors.Errorf("TLS is disabled. Enable with --tls-enable or specify a HTTP address")
}
if cfg.AccessURL.String() != "" && cfg.AccessURL.Scheme == "" {
return xerrors.Errorf("access-url must include a scheme (e.g. 'http://' or 'https://)")
}
// Disable rate limits if the `--dangerous-disable-rate-limits` flag
// was specified.
loginRateLimit := 60
filesRateLimit := 12
if cfg.RateLimit.DisableAll.Value {
cfg.RateLimit.API.Value = -1
if cfg.RateLimit.DisableAll {
cfg.RateLimit.API = -1
loginRateLimit = -1
filesRateLimit = -1
}
@@ -137,6 +312,10 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
}
defer logCloser()
// This line is helpful in tests.
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()
@@ -151,7 +330,7 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
defer notifyStop()
// Ensure we have a unique cache directory for this process.
cacheDir := filepath.Join(cfg.CacheDirectory.Value, uuid.NewString())
cacheDir := filepath.Join(cfg.CacheDir.String(), uuid.NewString())
err = os.MkdirAll(cacheDir, 0o700)
if err != nil {
return xerrors.Errorf("create cache directory: %w", err)
@@ -170,18 +349,18 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
// Coder tracing should be disabled if telemetry is disabled unless
// --telemetry-trace was explicitly provided.
shouldCoderTrace := cfg.Telemetry.Enable.Value && !isTest()
shouldCoderTrace := cfg.Telemetry.Enable.Value() && !isTest()
// Only override if telemetryTraceEnable was specifically set.
// By default we want it to be controlled by telemetryEnable.
if cmd.Flags().Changed("telemetry-trace") {
shouldCoderTrace = cfg.Telemetry.Trace.Value
shouldCoderTrace = cfg.Telemetry.Trace.Value()
}
if cfg.Trace.Enable.Value || shouldCoderTrace || cfg.Trace.HoneycombAPIKey.Value != "" {
if cfg.Trace.Enable.Value() || shouldCoderTrace || cfg.Trace.HoneycombAPIKey != "" {
sdkTracerProvider, closeTracing, err := tracing.TracerProvider(ctx, "coderd", tracing.TracerOpts{
Default: cfg.Trace.Enable.Value,
Default: cfg.Trace.Enable.Value(),
Coder: shouldCoderTrace,
Honeycomb: cfg.Trace.HoneycombAPIKey.Value,
Honeycomb: cfg.Trace.HoneycombAPIKey.String(),
})
if err != nil {
logger.Warn(ctx, "start telemetry exporter", slog.Error(err))
@@ -202,13 +381,18 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
}
}
config := createConfig(cmd)
config := config.Root(configDir)
builtinPostgres := false
// Only use built-in if PostgreSQL URL isn't specified!
if !cfg.InMemoryDatabase.Value && cfg.PostgresURL.Value == "" {
if !cfg.InMemoryDatabase && cfg.PostgresURL == "" {
var closeFunc func() error
cmd.Printf("Using built-in PostgreSQL (%s)\n", config.PostgresPath())
cfg.PostgresURL.Value, closeFunc, err = startBuiltinPostgres(ctx, config, logger)
pgURL, closeFunc, err := startBuiltinPostgres(ctx, config, logger)
if err != nil {
return err
}
err = cfg.PostgresURL.Set(pgURL)
if err != nil {
return err
}
@@ -228,10 +412,10 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
httpListener net.Listener
httpURL *url.URL
)
if cfg.HTTPAddress.Value != "" {
httpListener, err = net.Listen("tcp", cfg.HTTPAddress.Value)
if cfg.HTTPAddress.String() != "" {
httpListener, err = net.Listen("tcp", cfg.HTTPAddress.String())
if err != nil {
return xerrors.Errorf("listen %q: %w", cfg.HTTPAddress.Value, err)
return xerrors.Errorf("listen %q: %w", cfg.HTTPAddress.String(), err)
}
defer httpListener.Close()
@@ -240,7 +424,7 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
// 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.Value, "0.0.0.0") {
if strings.Contains(cfg.HTTPAddress.String(), "0.0.0.0") {
listenAddrStr = strings.ReplaceAll(listenAddrStr, "[::]", "0.0.0.0")
}
@@ -267,8 +451,8 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
httpsListener net.Listener
httpsURL *url.URL
)
if cfg.TLS.Enable.Value {
if cfg.TLS.Address.Value == "" {
if cfg.TLS.Enable {
if cfg.TLS.Address.String() == "" {
return xerrors.New("tls address must be set if tls is enabled")
}
@@ -276,22 +460,22 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
// It made more sense to have the redirect be opt-in.
if os.Getenv("CODER_TLS_REDIRECT_HTTP") == "true" || cmd.Flags().Changed("tls-redirect-http-to-https") {
cmd.PrintErr(cliui.Styles.Warn.Render("WARN:") + " --tls-redirect-http-to-https is deprecated, please use --redirect-to-access-url instead\n")
cfg.RedirectToAccessURL.Value = cfg.TLS.RedirectHTTP.Value
cfg.RedirectToAccessURL = cfg.TLS.RedirectHTTP
}
tlsConfig, err = configureTLS(
cfg.TLS.MinVersion.Value,
cfg.TLS.ClientAuth.Value,
cfg.TLS.CertFiles.Value,
cfg.TLS.KeyFiles.Value,
cfg.TLS.ClientCAFile.Value,
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.Value)
httpsListenerInner, err := net.Listen("tcp", cfg.TLS.Address.String())
if err != nil {
return xerrors.Errorf("listen %q: %w", cfg.TLS.Address.Value, err)
return xerrors.Errorf("listen %q: %w", cfg.TLS.Address.String(), err)
}
defer httpsListenerInner.Close()
@@ -304,7 +488,7 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
// 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.Value, "0.0.0.0") {
if strings.Contains(cfg.HTTPAddress.String(), "0.0.0.0") {
listenAddrStr = strings.ReplaceAll(listenAddrStr, "[::]", "0.0.0.0")
}
@@ -340,9 +524,9 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
ctx, httpClient, err := configureHTTPClient(
ctx,
cfg.TLS.ClientCertFile.Value,
cfg.TLS.ClientKeyFile.Value,
cfg.TLS.ClientCAFile.Value,
cfg.TLS.ClientCertFile.String(),
cfg.TLS.ClientKeyFile.String(),
cfg.TLS.ClientCAFile.String(),
)
if err != nil {
return xerrors.Errorf("configure http client: %w", err)
@@ -357,53 +541,60 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
// If the access URL is empty, we attempt to run a reverse-proxy
// tunnel to make the initial setup really simple.
if cfg.AccessURL.Value == "" {
if cfg.AccessURL.String() == "" {
cmd.Printf("Opening tunnel so workspaces can connect to your deployment. For production scenarios, specify an external access URL\n")
tunnel, tunnelErr, err = devtunnel.New(ctxTunnel, logger.Named("devtunnel"))
if err != nil {
return xerrors.Errorf("create tunnel: %w", err)
}
cfg.AccessURL.Value = tunnel.URL
err = cfg.AccessURL.Set(tunnel.URL)
if err != nil {
return xerrors.Errorf("set access url: %w", err)
}
if cfg.WildcardAccessURL.Value == "" {
if cfg.WildcardAccessURL.String() == "" {
u, err := parseURL(tunnel.URL)
if err != nil {
return xerrors.Errorf("parse tunnel url: %w", err)
}
// Suffixed wildcard access URL.
cfg.WildcardAccessURL.Value = fmt.Sprintf("*--%s", u.Hostname())
u, err = url.Parse(fmt.Sprintf("*--%s", u.Hostname()))
if err != nil {
return xerrors.Errorf("parse wildcard url: %w", err)
}
cfg.WildcardAccessURL = clibase.URL(*u)
}
}
accessURLParsed, err := parseURL(cfg.AccessURL.Value)
if err != nil {
return xerrors.Errorf("parse URL: %w", err)
}
accessURLPortRaw := accessURLParsed.Port()
_, accessURLPortRaw, _ := net.SplitHostPort(cfg.AccessURL.Host)
if accessURLPortRaw == "" {
accessURLPortRaw = "80"
if accessURLParsed.Scheme == "https" {
if cfg.AccessURL.Scheme == "https" {
accessURLPortRaw = "443"
}
}
accessURLPort, err := strconv.Atoi(accessURLPortRaw)
if err != nil {
return xerrors.Errorf("parse access URL port: %w", err)
}
// Warn the user if the access URL appears to be a loopback address.
isLocal, err := isLocalURL(ctx, accessURLParsed)
isLocal, err := isLocalURL(ctx, cfg.AccessURL.Value())
if isLocal || err != nil {
reason := "could not be resolved"
if isLocal {
reason = "isn't externally reachable"
}
cmd.Printf("%s 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.Warn.Render("Warning:"), cliui.Styles.Field.Render(accessURLParsed.String()), reason)
cmd.Printf(
"%s 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.Warn.Render("Warning:"), cliui.Styles.Field.Render(cfg.AccessURL.String()), reason,
)
}
// A newline is added before for visibility in terminal output.
cmd.Printf("\nView the Web UI: %s\n", accessURLParsed.String())
cmd.Printf("\nView the Web UI: %s\n", cfg.AccessURL.String())
// Used for zero-trust instance identity with Google Cloud.
googleTokenValidator, err := idtoken.NewValidator(ctx, option.WithoutAuthentication())
@@ -411,34 +602,37 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
return err
}
sshKeygenAlgorithm, err := gitsshkey.ParseAlgorithm(cfg.SSHKeygenAlgorithm.Value)
sshKeygenAlgorithm, err := gitsshkey.ParseAlgorithm(cfg.SSHKeygenAlgorithm.String())
if err != nil {
return xerrors.Errorf("parse ssh keygen algorithm %s: %w", cfg.SSHKeygenAlgorithm.Value, err)
return xerrors.Errorf("parse ssh keygen algorithm %s: %w", cfg.SSHKeygenAlgorithm, err)
}
defaultRegion := &tailcfg.DERPRegion{
EmbeddedRelay: true,
RegionID: cfg.DERP.Server.RegionID.Value,
RegionCode: cfg.DERP.Server.RegionCode.Value,
RegionName: cfg.DERP.Server.RegionName.Value,
RegionID: int(cfg.DERP.Server.RegionID.Value()),
RegionCode: cfg.DERP.Server.RegionCode.String(),
RegionName: cfg.DERP.Server.RegionName.String(),
Nodes: []*tailcfg.DERPNode{{
Name: fmt.Sprintf("%db", cfg.DERP.Server.RegionID.Value),
RegionID: cfg.DERP.Server.RegionID.Value,
HostName: accessURLParsed.Hostname(),
Name: fmt.Sprintf("%db", cfg.DERP.Server.RegionID),
RegionID: int(cfg.DERP.Server.RegionID.Value()),
HostName: cfg.AccessURL.Host,
DERPPort: accessURLPort,
STUNPort: -1,
ForceHTTP: accessURLParsed.Scheme == "http",
ForceHTTP: cfg.AccessURL.Scheme == "http",
}},
}
if !cfg.DERP.Server.Enable.Value {
if !cfg.DERP.Server.Enable {
defaultRegion = nil
}
derpMap, err := tailnet.NewDERPMap(ctx, defaultRegion, cfg.DERP.Server.STUNAddresses.Value, cfg.DERP.Config.URL.Value, cfg.DERP.Config.Path.Value)
derpMap, err := tailnet.NewDERPMap(
ctx, defaultRegion, cfg.DERP.Server.STUNAddresses,
cfg.DERP.Config.URL.String(), cfg.DERP.Config.Path.String(),
)
if err != nil {
return xerrors.Errorf("create derp map: %w", err)
}
appHostname := strings.TrimSpace(cfg.WildcardAccessURL.Value)
appHostname := cfg.WildcardAccessURL.String()
var appHostnameRegex *regexp.Regexp
if appHostname != "" {
appHostnameRegex, err = httpapi.CompileHostnamePattern(appHostname)
@@ -447,18 +641,32 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
}
}
gitAuthConfigs, err := gitauth.ConvertConfig(cfg.GitAuth.Value, accessURLParsed)
gitAuthEnv, err := ReadGitAuthProvidersFromEnv(os.Environ())
if err != nil {
return xerrors.Errorf("parse git auth config: %w", err)
return xerrors.Errorf("read git auth providers from env: %w", err)
}
realIPConfig, err := httpmw.ParseRealIPConfig(cfg.ProxyTrustedHeaders.Value, cfg.ProxyTrustedOrigins.Value)
gitAuthConfigs, err := gitauth.ConvertConfig(
append(cfg.GitAuthProviders.Value, gitAuthEnv...),
cfg.AccessURL.Value(),
)
if err != nil {
return xerrors.Errorf("convert git auth config: %w", err)
}
for _, c := range gitAuthConfigs {
logger.Debug(
ctx, "loaded git auth config",
slog.F("id", c.ID),
)
}
realIPConfig, err := httpmw.ParseRealIPConfig(cfg.ProxyTrustedHeaders, cfg.ProxyTrustedOrigins)
if err != nil {
return xerrors.Errorf("parse real ip config: %w", err)
}
options := &coderd.Options{
AccessURL: accessURLParsed,
AccessURL: cfg.AccessURL.Value(),
AppHostname: appHostname,
AppHostnameRegex: appHostnameRegex,
Logger: logger.Named("coderd"),
@@ -469,15 +677,15 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
GoogleTokenValidator: googleTokenValidator,
GitAuthConfigs: gitAuthConfigs,
RealIPConfig: realIPConfig,
SecureAuthCookie: cfg.SecureAuthCookie.Value,
SecureAuthCookie: cfg.SecureAuthCookie.Value(),
SSHKeygenAlgorithm: sshKeygenAlgorithm,
TracerProvider: tracerProvider,
Telemetry: telemetry.NewNoop(),
MetricsCacheRefreshInterval: cfg.MetricsCacheRefreshInterval.Value,
AgentStatsRefreshInterval: cfg.AgentStatRefreshInterval.Value,
DeploymentConfig: cfg,
MetricsCacheRefreshInterval: cfg.MetricsCacheRefreshInterval.Value(),
AgentStatsRefreshInterval: cfg.AgentStatRefreshInterval.Value(),
DeploymentValues: cfg,
PrometheusRegistry: prometheus.NewRegistry(),
APIRateLimit: cfg.RateLimit.API.Value,
APIRateLimit: int(cfg.RateLimit.API.Value()),
LoginRateLimit: loginRateLimit,
FilesRateLimit: filesRateLimit,
HTTPClient: httpClient,
@@ -486,14 +694,16 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
options.TLSCertificates = tlsConfig.Certificates
}
if cfg.StrictTransportSecurity.Value > 0 {
options.StrictTransportSecurityCfg, err = httpmw.HSTSConfigOptions(cfg.StrictTransportSecurity.Value, cfg.StrictTransportSecurityOptions.Value)
if cfg.StrictTransportSecurity > 0 {
options.StrictTransportSecurityCfg, err = httpmw.HSTSConfigOptions(
int(cfg.StrictTransportSecurity.Value()), cfg.StrictTransportSecurityOptions,
)
if err != nil {
return xerrors.Errorf("coderd: setting hsts header failed (options: %v): %w", cfg.StrictTransportSecurityOptions.Value, err)
return xerrors.Errorf("coderd: setting hsts header failed (options: %v): %w", cfg.StrictTransportSecurityOptions, err)
}
}
if cfg.UpdateCheck.Value {
if cfg.UpdateCheck {
options.UpdateCheckOptions = &updatecheck.Options{
// Avoid spamming GitHub API checking for updates.
Interval: 24 * time.Hour,
@@ -512,67 +722,69 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
}
}
if cfg.OAuth2.Github.ClientSecret.Value != "" {
options.GithubOAuth2Config, err = configureGithubOAuth2(accessURLParsed,
cfg.OAuth2.Github.ClientID.Value,
cfg.OAuth2.Github.ClientSecret.Value,
cfg.OAuth2.Github.AllowSignups.Value,
cfg.OAuth2.Github.AllowEveryone.Value,
cfg.OAuth2.Github.AllowedOrgs.Value,
cfg.OAuth2.Github.AllowedTeams.Value,
cfg.OAuth2.Github.EnterpriseBaseURL.Value,
if cfg.OAuth2.Github.ClientSecret != "" {
options.GithubOAuth2Config, err = configureGithubOAuth2(cfg.AccessURL.Value(),
cfg.OAuth2.Github.ClientID.String(),
cfg.OAuth2.Github.ClientSecret.String(),
cfg.OAuth2.Github.AllowSignups.Value(),
cfg.OAuth2.Github.AllowEveryone.Value(),
cfg.OAuth2.Github.AllowedOrgs,
cfg.OAuth2.Github.AllowedTeams,
cfg.OAuth2.Github.EnterpriseBaseURL.String(),
)
if err != nil {
return xerrors.Errorf("configure github oauth2: %w", err)
}
}
if cfg.OIDC.ClientSecret.Value != "" {
if cfg.OIDC.ClientID.Value == "" {
if cfg.OIDC.ClientSecret != "" {
if cfg.OIDC.ClientID == "" {
return xerrors.Errorf("OIDC client ID be set!")
}
if cfg.OIDC.IssuerURL.Value == "" {
if cfg.OIDC.IssuerURL == "" {
return xerrors.Errorf("OIDC issuer URL must be set!")
}
if cfg.OIDC.IgnoreEmailVerified.Value {
if cfg.OIDC.IgnoreEmailVerified {
logger.Warn(ctx, "coder will not check email_verified for OIDC logins")
}
oidcProvider, err := oidc.NewProvider(ctx, cfg.OIDC.IssuerURL.Value)
oidcProvider, err := oidc.NewProvider(
ctx, cfg.OIDC.IssuerURL.String(),
)
if err != nil {
return xerrors.Errorf("configure oidc provider: %w", err)
}
redirectURL, err := accessURLParsed.Parse("/api/v2/users/oidc/callback")
redirectURL, err := cfg.AccessURL.Value().Parse("/api/v2/users/oidc/callback")
if err != nil {
return xerrors.Errorf("parse oidc oauth callback url: %w", err)
}
options.OIDCConfig = &coderd.OIDCConfig{
OAuth2Config: &oauth2.Config{
ClientID: cfg.OIDC.ClientID.Value,
ClientSecret: cfg.OIDC.ClientSecret.Value,
ClientID: cfg.OIDC.ClientID.String(),
ClientSecret: cfg.OIDC.ClientSecret.String(),
RedirectURL: redirectURL.String(),
Endpoint: oidcProvider.Endpoint(),
Scopes: cfg.OIDC.Scopes.Value,
Scopes: cfg.OIDC.Scopes,
},
Provider: oidcProvider,
Verifier: oidcProvider.Verifier(&oidc.Config{
ClientID: cfg.OIDC.ClientID.Value,
ClientID: cfg.OIDC.ClientID.String(),
}),
EmailDomain: cfg.OIDC.EmailDomain.Value,
AllowSignups: cfg.OIDC.AllowSignups.Value,
UsernameField: cfg.OIDC.UsernameField.Value,
SignInText: cfg.OIDC.SignInText.Value,
IconURL: cfg.OIDC.IconURL.Value,
IgnoreEmailVerified: cfg.OIDC.IgnoreEmailVerified.Value,
EmailDomain: cfg.OIDC.EmailDomain,
AllowSignups: cfg.OIDC.AllowSignups.Value(),
UsernameField: cfg.OIDC.UsernameField.String(),
SignInText: cfg.OIDC.SignInText.String(),
IconURL: cfg.OIDC.IconURL.String(),
IgnoreEmailVerified: cfg.OIDC.IgnoreEmailVerified.Value(),
}
}
if cfg.InMemoryDatabase.Value {
if cfg.InMemoryDatabase {
options.Database = dbfake.New()
options.Pubsub = database.NewPubsubInMemory()
} else {
sqlDB, err := connectToPostgres(ctx, logger, sqlDriver, cfg.PostgresURL.Value)
sqlDB, err := connectToPostgres(ctx, logger, sqlDriver, cfg.PostgresURL.String())
if err != nil {
return xerrors.Errorf("connect to postgres: %w", err)
}
@@ -581,7 +793,7 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
}()
options.Database = database.New(sqlDB)
options.Pubsub, err = database.NewPubsub(ctx, sqlDB, cfg.PostgresURL.Value)
options.Pubsub, err = database.NewPubsub(ctx, sqlDB, cfg.PostgresURL.String())
if err != nil {
return xerrors.Errorf("create pubsub: %w", err)
}
@@ -646,21 +858,13 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
return err
}
// Disable telemetry if the in-memory database is used unless explicitly defined!
if cfg.InMemoryDatabase.Value && !cmd.Flags().Changed(cfg.Telemetry.Enable.Flag) {
cfg.Telemetry.Enable.Value = false
}
if cfg.Telemetry.Enable.Value {
// Parse the raw telemetry URL!
telemetryURL, err := parseURL(cfg.Telemetry.URL.Value)
if err != nil {
return xerrors.Errorf("parse telemetry url: %w", err)
}
if cfg.Telemetry.Enable {
gitAuth := make([]telemetry.GitAuth, 0)
// TODO:
var gitAuthConfigs []codersdk.GitAuthConfig
for _, cfg := range gitAuthConfigs {
gitAuth = append(gitAuth, telemetry.GitAuth{
Type: string(cfg.Type),
Type: cfg.Type,
})
}
@@ -669,15 +873,15 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
DeploymentID: deploymentID,
Database: options.Database,
Logger: logger.Named("telemetry"),
URL: telemetryURL,
Wildcard: cfg.WildcardAccessURL.Value != "",
DERPServerRelayURL: cfg.DERP.Server.RelayURL.Value,
URL: cfg.Telemetry.URL.Value(),
Wildcard: cfg.WildcardAccessURL.String() != "",
DERPServerRelayURL: cfg.DERP.Server.RelayURL.String(),
GitAuth: gitAuth,
GitHubOAuth: cfg.OAuth2.Github.ClientID.Value != "",
OIDCAuth: cfg.OIDC.ClientID.Value != "",
OIDCIssuerURL: cfg.OIDC.IssuerURL.Value,
Prometheus: cfg.Prometheus.Enable.Value,
STUN: len(cfg.DERP.Server.STUNAddresses.Value) != 0,
GitHubOAuth: cfg.OAuth2.Github.ClientID != "",
OIDCAuth: cfg.OIDC.ClientID != "",
OIDCIssuerURL: cfg.OIDC.IssuerURL.String(),
Prometheus: cfg.Prometheus.Enable.Value(),
STUN: len(cfg.DERP.Server.STUNAddresses) != 0,
Tunnel: tunnel != nil,
})
if err != nil {
@@ -688,11 +892,11 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
// This prevents the pprof import from being accidentally deleted.
_ = pprof.Handler
if cfg.Pprof.Enable.Value {
if cfg.Pprof.Enable {
//nolint:revive
defer serveHandler(ctx, logger, nil, cfg.Pprof.Address.Value, "pprof")()
defer serveHandler(ctx, logger, nil, cfg.Pprof.Address.String(), "pprof")()
}
if cfg.Prometheus.Enable.Value {
if cfg.Prometheus.Enable {
options.PrometheusRegistry.MustRegister(collectors.NewGoCollector())
options.PrometheusRegistry.MustRegister(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}))
@@ -711,11 +915,11 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
//nolint:revive
defer serveHandler(ctx, logger, promhttp.InstrumentMetricHandler(
options.PrometheusRegistry, promhttp.HandlerFor(options.PrometheusRegistry, promhttp.HandlerOpts{}),
), cfg.Prometheus.Address.Value, "prometheus")()
), cfg.Prometheus.Address.String(), "prometheus")()
}
if cfg.Swagger.Enable.Value {
options.SwaggerEndpoint = cfg.Swagger.Enable.Value
if cfg.Swagger.Enable {
options.SwaggerEndpoint = cfg.Swagger.Enable.Value()
}
// We use a separate coderAPICloser so the Enterprise API
@@ -742,7 +946,10 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
// This is helpful for tests, but can be silently ignored.
// Coder may be ran as users that don't have permission to write in the homedir,
// such as via the systemd service.
_ = config.URL().Write(client.URL.String())
err = config.URL().Write(client.URL.String())
if err != nil && flag.Lookup("test.v") != nil {
return xerrors.Errorf("write config url: %w", err)
}
// Since errCh only has one buffered slot, all routines
// sending on it must be wrapped in a select/default to
@@ -759,7 +966,7 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
}
}()
provisionerdMetrics := provisionerd.NewMetrics(options.PrometheusRegistry)
for i := 0; i < cfg.Provisioner.Daemons.Value; i++ {
for i := int64(0); i < cfg.Provisioner.Daemons.Value(); i++ {
daemonCacheDir := filepath.Join(cacheDir, fmt.Sprintf("provisioner-%d", i))
daemon, err := newProvisionerDaemon(ctx, coderAPI, provisionerdMetrics, logger, cfg, daemonCacheDir, errCh, false)
if err != nil {
@@ -774,8 +981,8 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
// Wrap the server in middleware that redirects to the access URL if
// the request is not to a local IP.
var handler http.Handler = coderAPI.RootHandler
if cfg.RedirectToAccessURL.Value {
handler = redirectToAccessURL(handler, accessURLParsed, tunnel != nil, appHostnameRegex)
if cfg.RedirectToAccessURL {
handler = redirectToAccessURL(handler, cfg.AccessURL.Value(), tunnel != nil, appHostnameRegex)
}
// ReadHeaderTimeout is purposefully not enabled. It caused some
@@ -842,7 +1049,7 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
cmd.Println("\nFailed to check for the first user: " + err.Error())
} else if !hasFirstUser {
cmd.Println("\nGet started by creating the first user (in a new terminal):")
cmd.Println(cliui.Styles.Code.Render("coder login " + accessURLParsed.String()))
cmd.Println(cliui.Styles.Code.Render("coder login " + cfg.AccessURL.String()))
}
cmd.Println("\n==> Logs will stream in below (press ctrl+c to gracefully exit):")
@@ -853,7 +1060,7 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
return xerrors.Errorf("notify systemd: %w", err)
}
autobuildPoller := time.NewTicker(cfg.AutobuildPollInterval.Value)
autobuildPoller := time.NewTicker(cfg.AutobuildPollInterval.Value())
defer autobuildPoller.Stop()
autobuildExecutor := executor.New(ctx, options.Database, logger, autobuildPoller.C)
autobuildExecutor.Run()
@@ -1011,10 +1218,11 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
postgresBuiltinServeCmd.Flags().BoolVar(&pgRawURL, "raw-url", false, "Output the raw connection URL instead of a psql command.")
createAdminUserCommand := newCreateAdminUserCommand()
root.SetHelpFunc(func(cmd *cobra.Command, args []string) {
// Help is handled by clibase in command body.
})
root.AddCommand(postgresBuiltinURLCmd, postgresBuiltinServeCmd, createAdminUserCommand)
deployment.AttachFlags(root.Flags(), vip, false)
return root
}
@@ -1063,7 +1271,7 @@ func newProvisionerDaemon(
coderAPI *coderd.API,
metrics provisionerd.Metrics,
logger slog.Logger,
cfg *codersdk.DeploymentConfig,
cfg *codersdk.DeploymentValues,
cacheDir string,
errCh chan error,
dev bool,
@@ -1140,11 +1348,11 @@ func newProvisionerDaemon(
return coderAPI.CreateInMemoryProvisionerDaemon(ctx, debounce)
}, &provisionerd.Options{
Logger: logger,
JobPollInterval: cfg.Provisioner.DaemonPollInterval.Value,
JobPollJitter: cfg.Provisioner.DaemonPollJitter.Value,
JobPollInterval: cfg.Provisioner.DaemonPollInterval.Value(),
JobPollJitter: cfg.Provisioner.DaemonPollJitter.Value(),
JobPollDebounce: debounce,
UpdateInterval: 500 * time.Millisecond,
ForceCancelInterval: cfg.Provisioner.ForceCancelInterval.Value,
ForceCancelInterval: cfg.Provisioner.ForceCancelInterval.Value(),
Provisioners: provisioners,
WorkDirectory: tempDir,
TracerProvider: coderAPI.TracerProvider,
@@ -1172,7 +1380,11 @@ func loadCertificates(tlsCertFiles, tlsKeyFiles []string) ([]tls.Certificate, er
certFile, keyFile := tlsCertFiles[i], tlsKeyFiles[i]
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
return nil, xerrors.Errorf("load TLS key pair %d (%q, %q): %w", i, certFile, keyFile, err)
return nil, xerrors.Errorf(
"load TLS key pair %d (%q, %q): %w\ncertFiles: %+v\nkeyFiles: %+v",
i, certFile, keyFile, err,
tlsCertFiles, tlsKeyFiles,
)
}
certs[i] = cert
@@ -1554,7 +1766,7 @@ func isLocalhost(host string) bool {
return host == "localhost" || host == "127.0.0.1" || host == "::1"
}
func buildLogger(cmd *cobra.Command, cfg *codersdk.DeploymentConfig) (slog.Logger, func(), error) {
func buildLogger(cmd *cobra.Command, cfg *codersdk.DeploymentValues) (slog.Logger, func(), error) {
var (
sinks = []slog.Sink{}
closers = []func() error{}
@@ -1575,32 +1787,31 @@ func buildLogger(cmd *cobra.Command, cfg *codersdk.DeploymentConfig) (slog.Logge
if err != nil {
return xerrors.Errorf("open log file %q: %w", loc, err)
}
closers = append(closers, fi.Close)
sinks = append(sinks, sinkFn(fi))
}
return nil
}
err := addSinkIfProvided(sloghuman.Sink, cfg.Logging.Human.Value)
err := addSinkIfProvided(sloghuman.Sink, cfg.Logging.Human.String())
if err != nil {
return slog.Logger{}, nil, xerrors.Errorf("add human sink: %w", err)
}
err = addSinkIfProvided(slogjson.Sink, cfg.Logging.JSON.Value)
err = addSinkIfProvided(slogjson.Sink, cfg.Logging.JSON.String())
if err != nil {
return slog.Logger{}, nil, xerrors.Errorf("add json sink: %w", err)
}
err = addSinkIfProvided(slogstackdriver.Sink, cfg.Logging.Stackdriver.Value)
err = addSinkIfProvided(slogstackdriver.Sink, cfg.Logging.Stackdriver.String())
if err != nil {
return slog.Logger{}, nil, xerrors.Errorf("add stackdriver sink: %w", err)
}
if cfg.Trace.CaptureLogs.Value {
if cfg.Trace.CaptureLogs {
sinks = append(sinks, tracing.SlogSink{})
}
level := slog.LevelInfo
if ok, _ := cmd.Flags().GetBool(varVerbose); ok {
if cfg.Verbose {
level = slog.LevelDebug
}