feat: Support config files with viper (#4696)

This commit is contained in:
Kyle Carberry
2022-10-21 17:08:23 -05:00
committed by GitHub
parent adc5c1a131
commit 7bc5b89f7a
36 changed files with 1099 additions and 1093 deletions
+8
View File
@@ -6,6 +6,10 @@ import (
"path/filepath"
)
const (
FlagName = "global-config"
)
// Root represents the configuration directory.
type Root string
@@ -42,6 +46,10 @@ func (r Root) PostgresPort() File {
return File(filepath.Join(r.PostgresPath(), "port"))
}
func (r Root) DeploymentConfigPath() string {
return filepath.Join(string(r), "server.yaml")
}
// File provides convenience methods for interacting with *os.File.
type File string
+450
View File
@@ -0,0 +1,450 @@
package deployment
import (
"flag"
"fmt"
"os"
"path/filepath"
"reflect"
"strings"
"time"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/spf13/pflag"
"github.com/spf13/viper"
"golang.org/x/xerrors"
"github.com/coder/coder/cli/cliui"
"github.com/coder/coder/cli/config"
"github.com/coder/coder/codersdk"
)
func newConfig() codersdk.DeploymentConfig {
return codersdk.DeploymentConfig{
AccessURL: codersdk.DeploymentConfigField[string]{
Key: "access_url",
Usage: "External URL to access your deployment. This must be accessible by all provisioned workspaces.",
Flag: "access-url",
},
WildcardAccessURL: codersdk.DeploymentConfigField[string]{
Key: "wildcard_access_url",
Usage: "Specifies the wildcard hostname to use for workspace applications in the form \"*.example.com\".",
Flag: "wildcard-access-url",
},
Address: codersdk.DeploymentConfigField[string]{
Key: "address",
Usage: "Bind address of the server.",
Flag: "address",
Shorthand: "a",
Value: "127.0.0.1:3000",
},
AutobuildPollInterval: codersdk.DeploymentConfigField[time.Duration]{
Key: "autobuild_poll_interval",
Usage: "Interval to poll for scheduled workspace builds.",
Flag: "autobuild-poll-interval",
Hidden: true,
Value: time.Minute,
},
DERPServerEnable: codersdk.DeploymentConfigField[bool]{
Key: "derp.server.enable",
Usage: "Whether to enable or disable the embedded DERP relay server.",
Flag: "derp-server-enable",
Value: true,
},
DERPServerRegionID: codersdk.DeploymentConfigField[int]{
Key: "derp.server.region_id",
Usage: "Region ID to use for the embedded DERP server.",
Flag: "derp-server-region-id",
Value: 999,
},
DERPServerRegionCode: codersdk.DeploymentConfigField[string]{
Key: "derp.server.region_code",
Usage: "Region code to use for the embedded DERP server.",
Flag: "derp-server-region-code",
Value: "coder",
},
DERPServerRegionName: codersdk.DeploymentConfigField[string]{
Key: "derp.server.region_name",
Usage: "Region name that for the embedded DERP server.",
Flag: "derp-server-region-name",
Value: "Coder Embedded Relay",
},
DERPServerSTUNAddresses: codersdk.DeploymentConfigField[[]string]{
Key: "derp.server.stun_addresses",
Usage: "Addresses for STUN servers to establish P2P connections. Set empty to disable P2P connections.",
Flag: "derp-server-stun-addresses",
Value: []string{"stun.l.google.com:19302"},
},
DERPServerRelayURL: codersdk.DeploymentConfigField[string]{
Key: "derp.server.relay_url",
Usage: "An HTTP URL that is accessible by other replicas to relay DERP traffic. Required for high availability.",
Flag: "derp-server-relay-url",
Enterprise: true,
},
DERPConfigURL: codersdk.DeploymentConfigField[string]{
Key: "derp.config.url",
Usage: "URL to fetch a DERP mapping on startup. See: https://tailscale.com/kb/1118/custom-derp-servers/",
Flag: "derp-config-url",
},
DERPConfigPath: codersdk.DeploymentConfigField[string]{
Key: "derp.config.path",
Usage: "Path to read a DERP mapping from. See: https://tailscale.com/kb/1118/custom-derp-servers/",
Flag: "derp-config-path",
},
PrometheusEnable: codersdk.DeploymentConfigField[bool]{
Key: "prometheus.enable",
Usage: "Serve prometheus metrics on the address defined by prometheus address.",
Flag: "prometheus-enable",
},
PrometheusAddress: codersdk.DeploymentConfigField[string]{
Key: "prometheus.address",
Usage: "The bind address to serve prometheus metrics.",
Flag: "prometheus-address",
Value: "127.0.0.1:2112",
},
PprofEnable: codersdk.DeploymentConfigField[bool]{
Key: "pprof.enable",
Usage: "Serve pprof metrics on the address defined by pprof address.",
Flag: "pprof-enable",
},
PprofAddress: codersdk.DeploymentConfigField[string]{
Key: "pprof.address",
Usage: "The bind address to serve pprof.",
Flag: "pprof-address",
Value: "127.0.0.1:6060",
},
CacheDirectory: codersdk.DeploymentConfigField[string]{
Key: "cache_directory",
Usage: "The directory to cache temporary files. If unspecified and $CACHE_DIRECTORY is set, it will be used for compatibility with systemd.",
Flag: "cache-dir",
Value: defaultCacheDir(),
},
InMemoryDatabase: codersdk.DeploymentConfigField[bool]{
Key: "in_memory_database",
Usage: "Controls whether data will be stored in an in-memory database.",
Flag: "in-memory",
Hidden: true,
},
ProvisionerDaemons: codersdk.DeploymentConfigField[int]{
Key: "provisioner.daemons",
Usage: "Number of provisioner daemons to create on start. If builds are stuck in queued state for a long time, consider increasing this.",
Flag: "provisioner-daemons",
Value: 3,
},
PostgresURL: codersdk.DeploymentConfigField[string]{
Key: "pg_connection_url",
Usage: "URL of a PostgreSQL database. If empty, PostgreSQL binaries will be downloaded from Maven (https://repo1.maven.org/maven2) and store all data in the config root. Access the built-in database with \"coder server postgres-builtin-url\".",
Flag: "postgres-url",
},
OAuth2GithubClientID: codersdk.DeploymentConfigField[string]{
Key: "oauth2.github.client_id",
Usage: "Client ID for Login with GitHub.",
Flag: "oauth2-github-client-id",
},
OAuth2GithubClientSecret: codersdk.DeploymentConfigField[string]{
Key: "oauth2.github.client_secret",
Usage: "Client secret for Login with GitHub.",
Flag: "oauth2-github-client-secret",
},
OAuth2GithubAllowedOrgs: codersdk.DeploymentConfigField[[]string]{
Key: "oauth2.github.allowed_orgs",
Usage: "Organizations the user must be a member of to Login with GitHub.",
Flag: "oauth2-github-allowed-orgs",
},
OAuth2GithubAllowedTeams: codersdk.DeploymentConfigField[[]string]{
Key: "oauth2.github.allowed_teams",
Usage: "Teams inside organizations the user must be a member of to Login with GitHub. Structured as: <organization-name>/<team-slug>.",
Flag: "oauth2-github-allowed-teams",
},
OAuth2GithubAllowSignups: codersdk.DeploymentConfigField[bool]{
Key: "oauth2.github.allow_signups",
Usage: "Whether new users can sign up with GitHub.",
Flag: "oauth2-github-allow-signups",
},
OAuth2GithubEnterpriseBaseURL: codersdk.DeploymentConfigField[string]{
Key: "oauth2.github.enterprise_base_url",
Usage: "Base URL of a GitHub Enterprise deployment to use for Login with GitHub.",
Flag: "oauth2-github-enterprise-base-url",
},
OIDCAllowSignups: codersdk.DeploymentConfigField[bool]{
Key: "oidc.allow_signups",
Usage: "Whether new users can sign up with OIDC.",
Flag: "oidc-allow-signups",
Value: true,
},
OIDCClientID: codersdk.DeploymentConfigField[string]{
Key: "oidc.client_id",
Usage: "Client ID to use for Login with OIDC.",
Flag: "oidc-client-id",
},
OIDCClientSecret: codersdk.DeploymentConfigField[string]{
Key: "oidc.client_secret",
Usage: "Client secret to use for Login with OIDC.",
Flag: "oidc-client-secret",
},
OIDCEmailDomain: codersdk.DeploymentConfigField[string]{
Key: "oidc.email_domain",
Usage: "Email domain that clients logging in with OIDC must match.",
Flag: "oidc-email-domain",
},
OIDCIssuerURL: codersdk.DeploymentConfigField[string]{
Key: "oidc.issuer_url",
Usage: "Issuer URL to use for Login with OIDC.",
Flag: "oidc-issuer-url",
},
OIDCScopes: codersdk.DeploymentConfigField[[]string]{
Key: "oidc.scopes",
Usage: "Scopes to grant when authenticating with OIDC.",
Flag: "oidc-scopes",
Value: []string{oidc.ScopeOpenID, "profile", "email"},
},
TelemetryEnable: codersdk.DeploymentConfigField[bool]{
Key: "telemetry.enable",
Usage: "Whether telemetry is enabled or not. Coder collects anonymized usage data to help improve our product.",
Flag: "telemetry",
Value: flag.Lookup("test.v") == nil,
},
TelemetryTrace: codersdk.DeploymentConfigField[bool]{
Key: "telemetry.trace",
Usage: "Whether Opentelemetry traces are sent to Coder. Coder collects anonymized application tracing to help improve our product. Disabling telemetry also disables this option.",
Flag: "telemetry-trace",
Value: flag.Lookup("test.v") == nil,
},
TelemetryURL: codersdk.DeploymentConfigField[string]{
Key: "telemetry.url",
Usage: "URL to send telemetry.",
Flag: "telemetry-url",
Hidden: true,
Value: "https://telemetry.coder.com",
},
TLSEnable: codersdk.DeploymentConfigField[bool]{
Key: "tls.enable",
Usage: "Whether TLS will be enabled.",
Flag: "tls-enable",
},
TLSCertFiles: codersdk.DeploymentConfigField[[]string]{
Key: "tls.cert_file",
Usage: "Path to each certificate for TLS. It requires a PEM-encoded file. To configure the listener to use a CA certificate, concatenate the primary certificate and the CA certificate together. The primary certificate should appear first in the combined file.",
Flag: "tls-cert-file",
},
TLSClientCAFile: codersdk.DeploymentConfigField[string]{
Key: "tls.client_ca_file",
Usage: "PEM-encoded Certificate Authority file used for checking the authenticity of client",
Flag: "tls-client-ca-file",
},
TLSClientAuth: codersdk.DeploymentConfigField[string]{
Key: "tls.client_auth",
Usage: "Policy the server will follow for TLS Client Authentication. Accepted values are \"none\", \"request\", \"require-any\", \"verify-if-given\", or \"require-and-verify\".",
Flag: "tls-client-auth",
Value: "request",
},
TLSKeyFiles: codersdk.DeploymentConfigField[[]string]{
Key: "tls.key_file",
Usage: "Paths to the private keys for each of the certificates. It requires a PEM-encoded file.",
Flag: "tls-key-file",
},
TLSMinVersion: codersdk.DeploymentConfigField[string]{
Key: "tls.min_version",
Usage: "Minimum supported version of TLS. Accepted values are \"tls10\", \"tls11\", \"tls12\" or \"tls13\"",
Flag: "tls-min-version",
Value: "tls12",
},
TraceEnable: codersdk.DeploymentConfigField[bool]{
Key: "trace",
Usage: "Whether application tracing data is collected.",
Flag: "trace",
},
SecureAuthCookie: codersdk.DeploymentConfigField[bool]{
Key: "secure_auth_cookie",
Usage: "Controls if the 'Secure' property is set on browser session cookies.",
Flag: "secure-auth-cookie",
},
SSHKeygenAlgorithm: codersdk.DeploymentConfigField[string]{
Key: "ssh_keygen_algorithm",
Usage: "The algorithm to use for generating ssh keys. Accepted values are \"ed25519\", \"ecdsa\", or \"rsa4096\".",
Flag: "ssh-keygen-algorithm",
Value: "ed25519",
},
AutoImportTemplates: codersdk.DeploymentConfigField[[]string]{
Key: "auto_import_templates",
Usage: "Templates to auto-import. Available auto-importable templates are: kubernetes",
Flag: "auto-import-template",
Hidden: true,
},
MetricsCacheRefreshInterval: codersdk.DeploymentConfigField[time.Duration]{
Key: "metrics_cache_refresh_interval",
Usage: "How frequently metrics are refreshed",
Flag: "metrics-cache-refresh-interval",
Hidden: true,
Value: time.Hour,
},
AgentStatRefreshInterval: codersdk.DeploymentConfigField[time.Duration]{
Key: "agent_stat_refresh_interval",
Usage: "How frequently agent stats are recorded",
Flag: "agent-stats-refresh-interval",
Hidden: true,
Value: 10 * time.Minute,
},
AuditLogging: codersdk.DeploymentConfigField[bool]{
Key: "audit_logging",
Usage: "Specifies whether audit logging is enabled.",
Flag: "audit-logging",
Value: true,
Enterprise: true,
},
BrowserOnly: codersdk.DeploymentConfigField[bool]{
Key: "browser_only",
Usage: "Whether Coder only allows connections to workspaces via the browser.",
Flag: "browser-only",
Enterprise: true,
},
SCIMAPIKey: codersdk.DeploymentConfigField[string]{
Key: "scim_api_key",
Usage: "Enables SCIM and sets the authentication header for the built-in SCIM server. New users are automatically created with OIDC authentication.",
Flag: "scim-auth-header",
Enterprise: true,
},
UserWorkspaceQuota: codersdk.DeploymentConfigField[int]{
Key: "user_workspace_quota",
Usage: "Enables and sets a limit on how many workspaces each user can create.",
Flag: "user-workspace-quota",
Enterprise: true,
},
}
}
//nolint:revive
func Config(flagset *pflag.FlagSet, vip *viper.Viper) (codersdk.DeploymentConfig, error) {
dc := newConfig()
flg, err := flagset.GetString(config.FlagName)
if err != nil {
return dc, xerrors.Errorf("get global config from flag: %w", err)
}
vip.SetEnvPrefix("coder")
vip.AutomaticEnv()
if flg != "" {
vip.SetConfigFile(flg + "/server.yaml")
err = vip.ReadInConfig()
if err != nil && !xerrors.Is(err, os.ErrNotExist) {
return dc, xerrors.Errorf("reading deployment config: %w", err)
}
}
dcv := reflect.ValueOf(&dc).Elem()
t := dcv.Type()
for i := 0; i < t.NumField(); i++ {
fve := dcv.Field(i)
key := fve.FieldByName("Key").String()
value := fve.FieldByName("Value").Interface()
switch value.(type) {
case string:
fve.FieldByName("Value").SetString(vip.GetString(key))
case bool:
fve.FieldByName("Value").SetBool(vip.GetBool(key))
case int:
fve.FieldByName("Value").SetInt(int64(vip.GetInt(key)))
case time.Duration:
fve.FieldByName("Value").SetInt(int64(vip.GetDuration(key)))
case []string:
// As of October 21st, 2022 we supported delimiting a string
// with a comma, but Viper only supports with a space. This
// is a small hack around it!
rawSlice := reflect.ValueOf(vip.GetStringSlice(key)).Interface()
slice, ok := rawSlice.([]string)
if !ok {
return dc, xerrors.Errorf("string slice is of type %T", rawSlice)
}
value := make([]string, 0, len(slice))
for _, entry := range slice {
value = append(value, strings.Split(entry, ",")...)
}
fve.FieldByName("Value").Set(reflect.ValueOf(value))
default:
return dc, xerrors.Errorf("unsupported type %T", value)
}
}
return dc, nil
}
func NewViper() *viper.Viper {
dc := newConfig()
v := viper.New()
v.SetEnvPrefix("coder")
v.AutomaticEnv()
v.SetEnvKeyReplacer(strings.NewReplacer("-", "_", ".", "_"))
dcv := reflect.ValueOf(dc)
t := dcv.Type()
for i := 0; i < t.NumField(); i++ {
fv := dcv.Field(i)
key := fv.FieldByName("Key").String()
value := fv.FieldByName("Value").Interface()
v.SetDefault(key, value)
}
return v
}
//nolint:revive
func AttachFlags(flagset *pflag.FlagSet, vip *viper.Viper, enterprise bool) {
dc := newConfig()
dcv := reflect.ValueOf(dc)
t := dcv.Type()
for i := 0; i < t.NumField(); i++ {
fv := dcv.Field(i)
isEnt := fv.FieldByName("Enterprise").Bool()
if enterprise != isEnt {
continue
}
key := fv.FieldByName("Key").String()
flg := fv.FieldByName("Flag").String()
if flg == "" {
continue
}
usage := fv.FieldByName("Usage").String()
usage = fmt.Sprintf("%s\n%s", usage, cliui.Styles.Placeholder.Render("Consumes $"+formatEnv(key)))
shorthand := fv.FieldByName("Shorthand").String()
hidden := fv.FieldByName("Hidden").Bool()
value := fv.FieldByName("Value").Interface()
switch value.(type) {
case string:
_ = flagset.StringP(flg, shorthand, vip.GetString(key), usage)
case bool:
_ = flagset.BoolP(flg, shorthand, vip.GetBool(key), usage)
case int:
_ = flagset.IntP(flg, shorthand, vip.GetInt(key), usage)
case time.Duration:
_ = flagset.DurationP(flg, shorthand, vip.GetDuration(key), usage)
case []string:
_ = flagset.StringSliceP(flg, shorthand, vip.GetStringSlice(key), usage)
default:
continue
}
_ = vip.BindPFlag(key, flagset.Lookup(flg))
if hidden {
_ = flagset.MarkHidden(flg)
}
}
}
func formatEnv(key string) string {
return "CODER_" + strings.ToUpper(strings.NewReplacer("-", "_", ".", "_").Replace(key))
}
func defaultCacheDir() string {
defaultCacheDir, err := os.UserCacheDir()
if err != nil {
defaultCacheDir = os.TempDir()
}
if dir := os.Getenv("CACHE_DIRECTORY"); dir != "" {
// For compatibility with systemd.
defaultCacheDir = dir
}
return filepath.Join(defaultCacheDir, "coder")
}
+162
View File
@@ -0,0 +1,162 @@
package deployment_test
import (
"testing"
"github.com/spf13/pflag"
"github.com/stretchr/testify/require"
"github.com/coder/coder/cli/config"
"github.com/coder/coder/cli/deployment"
"github.com/coder/coder/codersdk"
)
// nolint:paralleltest
func TestConfig(t *testing.T) {
viper := deployment.NewViper()
flagSet := pflag.NewFlagSet("", pflag.ContinueOnError)
flagSet.String(config.FlagName, "", "")
deployment.AttachFlags(flagSet, viper, true)
for _, tc := range []struct {
Name string
Env map[string]string
Valid func(config codersdk.DeploymentConfig)
}{{
Name: "Deployment",
Env: map[string]string{
"CODER_ADDRESS": "0.0.0.0:8443",
"CODER_ACCESS_URL": "https://dev.coder.com",
"CODER_PG_CONNECTION_URL": "some-url",
"CODER_PPROF_ADDRESS": "something",
"CODER_PPROF_ENABLE": "true",
"CODER_PROMETHEUS_ADDRESS": "hello-world",
"CODER_PROMETHEUS_ENABLE": "true",
"CODER_PROVISIONER_DAEMONS": "5",
"CODER_SECURE_AUTH_COOKIE": "true",
"CODER_SSH_KEYGEN_ALGORITHM": "potato",
"CODER_TELEMETRY": "false",
"CODER_TELEMETRY_TRACE": "false",
"CODER_WILDCARD_ACCESS_URL": "something-wildcard.com",
},
Valid: func(config codersdk.DeploymentConfig) {
require.Equal(t, config.Address.Value, "0.0.0.0:8443")
require.Equal(t, config.AccessURL.Value, "https://dev.coder.com")
require.Equal(t, config.PostgresURL.Value, "some-url")
require.Equal(t, config.PprofAddress.Value, "something")
require.Equal(t, config.PprofEnable.Value, true)
require.Equal(t, config.PrometheusAddress.Value, "hello-world")
require.Equal(t, config.PrometheusEnable.Value, true)
require.Equal(t, config.ProvisionerDaemons.Value, 5)
require.Equal(t, config.SecureAuthCookie.Value, true)
require.Equal(t, config.SSHKeygenAlgorithm.Value, "potato")
require.Equal(t, config.TelemetryEnable.Value, false)
require.Equal(t, config.TelemetryTrace.Value, false)
require.Equal(t, config.WildcardAccessURL.Value, "something-wildcard.com")
},
}, {
Name: "DERP",
Env: map[string]string{
"CODER_DERP_CONFIG_PATH": "/example/path",
"CODER_DERP_CONFIG_URL": "https://google.com",
"CODER_DERP_SERVER_ENABLE": "false",
"CODER_DERP_SERVER_REGION_CODE": "something",
"CODER_DERP_SERVER_REGION_ID": "123",
"CODER_DERP_SERVER_REGION_NAME": "Code-Land",
"CODER_DERP_SERVER_RELAY_URL": "1.1.1.1",
"CODER_DERP_SERVER_STUN_ADDRESSES": "google.org",
},
Valid: func(config codersdk.DeploymentConfig) {
require.Equal(t, config.DERPConfigPath.Value, "/example/path")
require.Equal(t, config.DERPConfigURL.Value, "https://google.com")
require.Equal(t, config.DERPServerEnable.Value, false)
require.Equal(t, config.DERPServerRegionCode.Value, "something")
require.Equal(t, config.DERPServerRegionID.Value, 123)
require.Equal(t, config.DERPServerRegionName.Value, "Code-Land")
require.Equal(t, config.DERPServerRelayURL.Value, "1.1.1.1")
require.Equal(t, config.DERPServerSTUNAddresses.Value, []string{"google.org"})
},
}, {
Name: "Enterprise",
Env: map[string]string{
"CODER_AUDIT_LOGGING": "false",
"CODER_BROWSER_ONLY": "true",
"CODER_SCIM_API_KEY": "some-key",
"CODER_USER_WORKSPACE_QUOTA": "10",
},
Valid: func(config codersdk.DeploymentConfig) {
require.Equal(t, config.AuditLogging.Value, false)
require.Equal(t, config.BrowserOnly.Value, true)
require.Equal(t, config.SCIMAPIKey.Value, "some-key")
require.Equal(t, config.UserWorkspaceQuota.Value, 10)
},
}, {
Name: "TLS",
Env: map[string]string{
"CODER_TLS_CERT_FILE": "/etc/acme-sh/dev.coder.com,/etc/acme-sh/*.dev.coder.com",
"CODER_TLS_KEY_FILE": "/etc/acme-sh/dev.coder.com,/etc/acme-sh/*.dev.coder.com",
"CODER_TLS_CLIENT_AUTH": "/some/path",
"CODER_TLS_CLIENT_CA_FILE": "/some/path",
"CODER_TLS_ENABLE": "true",
"CODER_TLS_MIN_VERSION": "tls10",
},
Valid: func(config codersdk.DeploymentConfig) {
require.Len(t, config.TLSCertFiles.Value, 2)
require.Equal(t, config.TLSCertFiles.Value[0], "/etc/acme-sh/dev.coder.com")
require.Equal(t, config.TLSCertFiles.Value[1], "/etc/acme-sh/*.dev.coder.com")
require.Len(t, config.TLSKeyFiles.Value, 2)
require.Equal(t, config.TLSKeyFiles.Value[0], "/etc/acme-sh/dev.coder.com")
require.Equal(t, config.TLSKeyFiles.Value[1], "/etc/acme-sh/*.dev.coder.com")
require.Equal(t, config.TLSClientAuth.Value, "/some/path")
require.Equal(t, config.TLSClientCAFile.Value, "/some/path")
require.Equal(t, config.TLSEnable.Value, true)
require.Equal(t, config.TLSMinVersion.Value, "tls10")
},
}, {
Name: "OIDC",
Env: map[string]string{
"CODER_OIDC_ISSUER_URL": "https://accounts.google.com",
"CODER_OIDC_EMAIL_DOMAIN": "coder.com",
"CODER_OIDC_CLIENT_ID": "client",
"CODER_OIDC_CLIENT_SECRET": "secret",
"CODER_OIDC_ALLOW_SIGNUPS": "false",
"CODER_OIDC_SCOPES": "something,here",
},
Valid: func(config codersdk.DeploymentConfig) {
require.Equal(t, config.OIDCIssuerURL.Value, "https://accounts.google.com")
require.Equal(t, config.OIDCEmailDomain.Value, "coder.com")
require.Equal(t, config.OIDCClientID.Value, "client")
require.Equal(t, config.OIDCClientSecret.Value, "secret")
require.Equal(t, config.OIDCAllowSignups.Value, false)
require.Equal(t, config.OIDCScopes.Value, []string{"something", "here"})
},
}, {
Name: "GitHub",
Env: map[string]string{
"CODER_OAUTH2_GITHUB_CLIENT_ID": "client",
"CODER_OAUTH2_GITHUB_CLIENT_SECRET": "secret",
"CODER_OAUTH2_GITHUB_ALLOWED_ORGS": "coder",
"CODER_OAUTH2_GITHUB_ALLOWED_TEAMS": "coder",
"CODER_OAUTH2_GITHUB_ALLOW_SIGNUPS": "true",
},
Valid: func(config codersdk.DeploymentConfig) {
require.Equal(t, config.OAuth2GithubClientID.Value, "client")
require.Equal(t, config.OAuth2GithubClientSecret.Value, "secret")
require.Equal(t, []string{"coder"}, config.OAuth2GithubAllowedOrgs.Value)
require.Equal(t, []string{"coder"}, config.OAuth2GithubAllowedTeams.Value)
require.Equal(t, config.OAuth2GithubAllowSignups.Value, true)
},
}} {
tc := tc
t.Run(tc.Name, func(t *testing.T) {
for key, value := range tc.Env {
t.Setenv(key, value)
}
config, err := deployment.Config(flagSet, viper)
require.NoError(t, err)
tc.Valid(config)
})
}
}
-511
View File
@@ -1,511 +0,0 @@
package deployment
import (
"flag"
"fmt"
"os"
"path/filepath"
"reflect"
"time"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/spf13/pflag"
"github.com/coder/coder/cli/cliflag"
"github.com/coder/coder/cli/cliui"
"github.com/coder/coder/codersdk"
)
const (
secretValue = "********"
)
func Flags() *codersdk.DeploymentFlags {
return &codersdk.DeploymentFlags{
AccessURL: &codersdk.StringFlag{
Name: "Access URL",
Flag: "access-url",
EnvVar: "CODER_ACCESS_URL",
Description: "External URL to access your deployment. This must be accessible by all provisioned workspaces.",
},
WildcardAccessURL: &codersdk.StringFlag{
Name: "Wildcard Address URL",
Flag: "wildcard-access-url",
EnvVar: "CODER_WILDCARD_ACCESS_URL",
Description: `Specifies the wildcard hostname to use for workspace applications in the form "*.example.com" or "*-suffix.example.com". Ports or schemes should not be included. The scheme will be copied from the access URL.`,
},
Address: &codersdk.StringFlag{
Name: "Bind Address",
Flag: "address",
EnvVar: "CODER_ADDRESS",
Shorthand: "a",
Description: "Bind address of the server.",
Default: "127.0.0.1:3000",
},
AutobuildPollInterval: &codersdk.DurationFlag{
Name: "Autobuild Poll Interval",
Flag: "autobuild-poll-interval",
EnvVar: "CODER_AUTOBUILD_POLL_INTERVAL",
Description: "Interval to poll for scheduled workspace builds.",
Hidden: true,
Default: time.Minute,
},
DerpServerEnable: &codersdk.BoolFlag{
Name: "DERP Server Enabled",
Flag: "derp-server-enable",
EnvVar: "CODER_DERP_SERVER_ENABLE",
Description: "Whether to enable or disable the embedded DERP relay server.",
Default: true,
},
DerpServerRegionID: &codersdk.IntFlag{
Name: "DERP Server Region ID",
Flag: "derp-server-region-id",
EnvVar: "CODER_DERP_SERVER_REGION_ID",
Description: "Region ID to use for the embedded DERP server.",
Default: 999,
},
DerpServerRegionCode: &codersdk.StringFlag{
Name: "DERP Server Region Code",
Flag: "derp-server-region-code",
EnvVar: "CODER_DERP_SERVER_REGION_CODE",
Description: "Region code to use for the embedded DERP server.",
Default: "coder",
},
DerpServerRegionName: &codersdk.StringFlag{
Name: "DERP Server Region Name",
Flag: "derp-server-region-name",
EnvVar: "CODER_DERP_SERVER_REGION_NAME",
Description: "Region name that for the embedded DERP server.",
Default: "Coder Embedded Relay",
},
DerpServerSTUNAddresses: &codersdk.StringArrayFlag{
Name: "DERP Server STUN Addresses",
Flag: "derp-server-stun-addresses",
EnvVar: "CODER_DERP_SERVER_STUN_ADDRESSES",
Description: "Addresses for STUN servers to establish P2P connections. Set empty to disable P2P connections.",
Default: []string{"stun.l.google.com:19302"},
},
DerpServerRelayAddress: &codersdk.StringFlag{
Name: "DERP Server Relay Address",
Flag: "derp-server-relay-address",
EnvVar: "CODER_DERP_SERVER_RELAY_URL",
Description: "An HTTP address that is accessible by other replicas to relay DERP traffic. Required for high availability.",
Enterprise: true,
},
DerpConfigURL: &codersdk.StringFlag{
Name: "DERP Config URL",
Flag: "derp-config-url",
EnvVar: "CODER_DERP_CONFIG_URL",
Description: "URL to fetch a DERP mapping on startup. See: https://tailscale.com/kb/1118/custom-derp-servers/",
},
DerpConfigPath: &codersdk.StringFlag{
Name: "DERP Config Path",
Flag: "derp-config-path",
EnvVar: "CODER_DERP_CONFIG_PATH",
Description: "Path to read a DERP mapping from. See: https://tailscale.com/kb/1118/custom-derp-servers/",
},
PromEnabled: &codersdk.BoolFlag{
Name: "Prometheus Enabled",
Flag: "prometheus-enable",
EnvVar: "CODER_PROMETHEUS_ENABLE",
Description: "Serve prometheus metrics on the address defined by `prometheus-address`.",
},
PromAddress: &codersdk.StringFlag{
Name: "Prometheus Address",
Flag: "prometheus-address",
EnvVar: "CODER_PROMETHEUS_ADDRESS",
Description: "The bind address to serve prometheus metrics.",
Default: "127.0.0.1:2112",
},
PprofEnabled: &codersdk.BoolFlag{
Name: "pprof Enabled",
Flag: "pprof-enable",
EnvVar: "CODER_PPROF_ENABLE",
Description: "Serve pprof metrics on the address defined by `pprof-address`.",
},
PprofAddress: &codersdk.StringFlag{
Name: "pprof Address",
Flag: "pprof-address",
EnvVar: "CODER_PPROF_ADDRESS",
Description: "The bind address to serve pprof.",
Default: "127.0.0.1:6060",
},
CacheDir: &codersdk.StringFlag{
Name: "Cache Directory",
Flag: "cache-dir",
EnvVar: "CODER_CACHE_DIRECTORY",
Description: "The directory to cache temporary files. If unspecified and $CACHE_DIRECTORY is set, it will be used for compatibility with systemd.",
Default: defaultCacheDir(),
},
InMemoryDatabase: &codersdk.BoolFlag{
Name: "In-Memory Database",
Flag: "in-memory",
EnvVar: "CODER_INMEMORY",
Description: "Controls whether data will be stored in an in-memory database.",
Hidden: true,
},
ProvisionerDaemonCount: &codersdk.IntFlag{
Name: "Provisioner Daemons",
Flag: "provisioner-daemons",
EnvVar: "CODER_PROVISIONER_DAEMONS",
Description: "Number of provisioner daemons to create on start. If builds are stuck in queued state for a long time, consider increasing this.",
Default: 3,
},
PostgresURL: &codersdk.StringFlag{
Name: "Postgres URL",
Flag: "postgres-url",
EnvVar: "CODER_PG_CONNECTION_URL",
Description: "URL of a PostgreSQL database. If empty, PostgreSQL binaries will be downloaded from Maven (https://repo1.maven.org/maven2) and store all data in the config root. Access the built-in database with \"coder server postgres-builtin-url\"",
Secret: true,
},
OAuth2GithubClientID: &codersdk.StringFlag{
Name: "Oauth2 Github Client ID",
Flag: "oauth2-github-client-id",
EnvVar: "CODER_OAUTH2_GITHUB_CLIENT_ID",
Description: "Client ID for Login with GitHub.",
},
OAuth2GithubClientSecret: &codersdk.StringFlag{
Name: "Oauth2 Github Client Secret",
Flag: "oauth2-github-client-secret",
EnvVar: "CODER_OAUTH2_GITHUB_CLIENT_SECRET",
Description: "Client secret for Login with GitHub.",
Secret: true,
},
OAuth2GithubAllowedOrganizations: &codersdk.StringArrayFlag{
Name: "Oauth2 Github Allowed Organizations",
Flag: "oauth2-github-allowed-orgs",
EnvVar: "CODER_OAUTH2_GITHUB_ALLOWED_ORGS",
Description: "Organizations the user must be a member of to Login with GitHub.",
Default: []string{},
},
OAuth2GithubAllowedTeams: &codersdk.StringArrayFlag{
Name: "Oauth2 Github Allowed Teams",
Flag: "oauth2-github-allowed-teams",
EnvVar: "CODER_OAUTH2_GITHUB_ALLOWED_TEAMS",
Description: "Teams inside organizations the user must be a member of to Login with GitHub. Structured as: <organization-name>/<team-slug>.",
Default: []string{},
},
OAuth2GithubAllowSignups: &codersdk.BoolFlag{
Name: "Oauth2 Github Allow Signups",
Flag: "oauth2-github-allow-signups",
EnvVar: "CODER_OAUTH2_GITHUB_ALLOW_SIGNUPS",
Description: "Whether new users can sign up with GitHub.",
},
OAuth2GithubEnterpriseBaseURL: &codersdk.StringFlag{
Name: "Oauth2 Github Enterprise Base URL",
Flag: "oauth2-github-enterprise-base-url",
EnvVar: "CODER_OAUTH2_GITHUB_ENTERPRISE_BASE_URL",
Description: "Base URL of a GitHub Enterprise deployment to use for Login with GitHub.",
},
OIDCAllowSignups: &codersdk.BoolFlag{
Name: "OIDC Allow Signups",
Flag: "oidc-allow-signups",
EnvVar: "CODER_OIDC_ALLOW_SIGNUPS",
Description: "Whether new users can sign up with OIDC.",
Default: true,
},
OIDCClientID: &codersdk.StringFlag{
Name: "OIDC Client ID",
Flag: "oidc-client-id",
EnvVar: "CODER_OIDC_CLIENT_ID",
Description: "Client ID to use for Login with OIDC.",
},
OIDCClientSecret: &codersdk.StringFlag{
Name: "OIDC Client Secret",
Flag: "oidc-client-secret",
EnvVar: "CODER_OIDC_CLIENT_SECRET",
Description: "Client secret to use for Login with OIDC.",
Secret: true,
},
OIDCEmailDomain: &codersdk.StringFlag{
Name: "OIDC Email Domain",
Flag: "oidc-email-domain",
EnvVar: "CODER_OIDC_EMAIL_DOMAIN",
Description: "Email domain that clients logging in with OIDC must match.",
},
OIDCIssuerURL: &codersdk.StringFlag{
Name: "OIDC Issuer URL",
Flag: "oidc-issuer-url",
EnvVar: "CODER_OIDC_ISSUER_URL",
Description: "Issuer URL to use for Login with OIDC.",
},
OIDCScopes: &codersdk.StringArrayFlag{
Name: "OIDC Scopes",
Flag: "oidc-scopes",
EnvVar: "CODER_OIDC_SCOPES",
Description: "Scopes to grant when authenticating with OIDC.",
Default: []string{oidc.ScopeOpenID, "profile", "email"},
},
TelemetryEnable: &codersdk.BoolFlag{
Name: "Telemetry Enabled",
Flag: "telemetry",
EnvVar: "CODER_TELEMETRY",
Description: "Whether telemetry is enabled or not. Coder collects anonymized usage data to help improve our product.",
Default: flag.Lookup("test.v") == nil,
},
TelemetryTraceEnable: &codersdk.BoolFlag{
Name: "Trace Telemetry Enabled",
Flag: "telemetry-trace",
EnvVar: "CODER_TELEMETRY_TRACE",
Shorthand: "",
Description: "Whether Opentelemetry traces are sent to Coder. Coder collects anonymized application tracing to help improve our product. Disabling telemetry also disables this option.",
Default: flag.Lookup("test.v") == nil,
},
TelemetryURL: &codersdk.StringFlag{
Name: "Telemetry URL",
Flag: "telemetry-url",
EnvVar: "CODER_TELEMETRY_URL",
Description: "URL to send telemetry.",
Hidden: true,
Default: "https://telemetry.coder.com",
},
TLSEnable: &codersdk.BoolFlag{
Name: "TLS Enabled",
Flag: "tls-enable",
EnvVar: "CODER_TLS_ENABLE",
Description: "Whether TLS will be enabled.",
},
TLSCertFiles: &codersdk.StringArrayFlag{
Name: "TLS Cert Files",
Flag: "tls-cert-file",
EnvVar: "CODER_TLS_CERT_FILE",
Description: "Path to each certificate for TLS. It requires a PEM-encoded file. " +
"To configure the listener to use a CA certificate, concatenate the primary certificate " +
"and the CA certificate together. The primary certificate should appear first in the combined file.",
Default: []string{},
},
TLSClientCAFile: &codersdk.StringFlag{
Name: "TLS Client CA File",
Flag: "tls-client-ca-file",
EnvVar: "CODER_TLS_CLIENT_CA_FILE",
Description: "PEM-encoded Certificate Authority file used for checking the authenticity of client",
},
TLSClientAuth: &codersdk.StringFlag{
Name: "TLS Client Auth",
Flag: "tls-client-auth",
EnvVar: "CODER_TLS_CLIENT_AUTH",
Description: `Policy the server will follow for TLS Client Authentication. ` +
`Accepted values are "none", "request", "require-any", "verify-if-given", or "require-and-verify"`,
Default: "request",
},
TLSKeyFiles: &codersdk.StringArrayFlag{
Name: "TLS Key Files",
Flag: "tls-key-file",
EnvVar: "CODER_TLS_KEY_FILE",
Description: "Paths to the private keys for each of the certificates. It requires a PEM-encoded file",
Default: []string{},
},
TLSMinVersion: &codersdk.StringFlag{
Name: "TLS Min Version",
Flag: "tls-min-version",
EnvVar: "CODER_TLS_MIN_VERSION",
Description: `Minimum supported version of TLS. Accepted values are "tls10", "tls11", "tls12" or "tls13"`,
Default: "tls12",
},
TraceEnable: &codersdk.BoolFlag{
Name: "Trace Enabled",
Flag: "trace",
EnvVar: "CODER_TRACE",
Description: "Whether application tracing data is collected.",
},
SecureAuthCookie: &codersdk.BoolFlag{
Name: "Secure Auth Cookie",
Flag: "secure-auth-cookie",
EnvVar: "CODER_SECURE_AUTH_COOKIE",
Description: "Controls if the 'Secure' property is set on browser session cookies",
},
SSHKeygenAlgorithm: &codersdk.StringFlag{
Name: "SSH Keygen Algorithm",
Flag: "ssh-keygen-algorithm",
EnvVar: "CODER_SSH_KEYGEN_ALGORITHM",
Description: "The algorithm to use for generating ssh keys. " +
`Accepted values are "ed25519", "ecdsa", or "rsa4096"`,
Default: "ed25519",
},
AutoImportTemplates: &codersdk.StringArrayFlag{
Name: "Auto Import Templates",
Flag: "auto-import-template",
EnvVar: "CODER_TEMPLATE_AUTOIMPORT",
Description: "Templates to auto-import. Available auto-importable templates are: kubernetes",
Hidden: true,
Default: []string{},
},
MetricsCacheRefreshInterval: &codersdk.DurationFlag{
Name: "Metrics Cache Refresh Interval",
Flag: "metrics-cache-refresh-interval",
EnvVar: "CODER_METRICS_CACHE_REFRESH_INTERVAL",
Description: "How frequently metrics are refreshed",
Hidden: true,
Default: time.Hour,
},
AgentStatRefreshInterval: &codersdk.DurationFlag{
Name: "Agent Stats Refresh Interval",
Flag: "agent-stats-refresh-interval",
EnvVar: "CODER_AGENT_STATS_REFRESH_INTERVAL",
Description: "How frequently agent stats are recorded",
Hidden: true,
Default: 10 * time.Minute,
},
Verbose: &codersdk.BoolFlag{
Name: "Verbose Logging",
Flag: "verbose",
EnvVar: "CODER_VERBOSE",
Shorthand: "v",
Description: "Enables verbose logging.",
},
AuditLogging: &codersdk.BoolFlag{
Name: "Audit Logging",
Flag: "audit-logging",
EnvVar: "CODER_AUDIT_LOGGING",
Description: "Specifies whether audit logging is enabled.",
Default: true,
Enterprise: true,
},
BrowserOnly: &codersdk.BoolFlag{
Name: "Browser Only",
Flag: "browser-only",
EnvVar: "CODER_BROWSER_ONLY",
Description: "Whether Coder only allows connections to workspaces via the browser.",
Enterprise: true,
},
SCIMAuthHeader: &codersdk.StringFlag{
Name: "SCIM Authentication Header",
Flag: "scim-auth-header",
EnvVar: "CODER_SCIM_API_KEY",
Description: "Enables SCIM and sets the authentication header for the built-in SCIM server. New users are automatically created with OIDC authentication.",
Secret: true,
Enterprise: true,
},
UserWorkspaceQuota: &codersdk.IntFlag{
Name: "User Workspace Quota",
Flag: "user-workspace-quota",
EnvVar: "CODER_USER_WORKSPACE_QUOTA",
Description: "Enables and sets a limit on how many workspaces each user can create.",
Default: 0,
Enterprise: true,
},
}
}
func RemoveSensitiveValues(df codersdk.DeploymentFlags) codersdk.DeploymentFlags {
v := reflect.ValueOf(&df).Elem()
t := v.Type()
for i := 0; i < t.NumField(); i++ {
fv := v.Field(i)
if vp, ok := fv.Interface().(*codersdk.StringFlag); ok {
if vp.Secret && vp.Value != "" {
// Make a copy and remove the value.
v := *vp
v.Value = secretValue
fv.Set(reflect.ValueOf(&v))
}
}
}
return df
}
//nolint:revive
func AttachFlags(flagset *pflag.FlagSet, df *codersdk.DeploymentFlags, enterprise bool) {
v := reflect.ValueOf(df).Elem()
t := v.Type()
for i := 0; i < t.NumField(); i++ {
fv := v.Field(i)
fve := fv.Elem()
e := fve.FieldByName("Enterprise").Bool()
if e != enterprise {
continue
}
if e {
d := fve.FieldByName("Description").String()
d += cliui.Styles.Keyword.Render(" This is an Enterprise feature. Contact sales@coder.com for licensing")
fve.FieldByName("Description").SetString(d)
}
switch v := fv.Interface().(type) {
case *codersdk.StringFlag:
StringFlag(flagset, v)
case *codersdk.StringArrayFlag:
StringArrayFlag(flagset, v)
case *codersdk.IntFlag:
IntFlag(flagset, v)
case *codersdk.BoolFlag:
BoolFlag(flagset, v)
case *codersdk.DurationFlag:
DurationFlag(flagset, v)
default:
panic(fmt.Sprintf("unknown flag type: %T", v))
}
if fve.FieldByName("Hidden").Bool() {
_ = flagset.MarkHidden(fve.FieldByName("Flag").String())
}
}
}
func StringFlag(flagset *pflag.FlagSet, fl *codersdk.StringFlag) {
cliflag.StringVarP(flagset,
&fl.Value,
fl.Flag,
fl.Shorthand,
fl.EnvVar,
fl.Default,
fl.Description,
)
}
func BoolFlag(flagset *pflag.FlagSet, fl *codersdk.BoolFlag) {
cliflag.BoolVarP(flagset,
&fl.Value,
fl.Flag,
fl.Shorthand,
fl.EnvVar,
fl.Default,
fl.Description,
)
}
func IntFlag(flagset *pflag.FlagSet, fl *codersdk.IntFlag) {
cliflag.IntVarP(flagset,
&fl.Value,
fl.Flag,
fl.Shorthand,
fl.EnvVar,
fl.Default,
fl.Description,
)
}
func DurationFlag(flagset *pflag.FlagSet, fl *codersdk.DurationFlag) {
cliflag.DurationVarP(flagset,
&fl.Value,
fl.Flag,
fl.Shorthand,
fl.EnvVar,
fl.Default,
fl.Description,
)
}
func StringArrayFlag(flagset *pflag.FlagSet, fl *codersdk.StringArrayFlag) {
cliflag.StringArrayVarP(flagset,
&fl.Value,
fl.Flag,
fl.Shorthand,
fl.EnvVar,
fl.Default,
fl.Description,
)
}
func defaultCacheDir() string {
defaultCacheDir, err := os.UserCacheDir()
if err != nil {
defaultCacheDir = os.TempDir()
}
if dir := os.Getenv("CACHE_DIRECTORY"); dir != "" {
// For compatibility with systemd.
defaultCacheDir = dir
}
return filepath.Join(defaultCacheDir, "coder")
}
-32
View File
@@ -1,32 +0,0 @@
package deployment_test
import (
"testing"
"github.com/spf13/pflag"
"github.com/stretchr/testify/require"
"github.com/coder/coder/cli/deployment"
)
func TestFlags(t *testing.T) {
t.Parallel()
df := deployment.Flags()
fs := pflag.NewFlagSet("test", pflag.ContinueOnError)
deployment.AttachFlags(fs, df, false)
require.NotNil(t, fs.Lookup("access-url"))
require.False(t, fs.Lookup("access-url").Hidden)
require.True(t, fs.Lookup("telemetry-url").Hidden)
require.NotEmpty(t, fs.Lookup("telemetry-url").DefValue)
require.Nil(t, fs.Lookup("audit-logging"))
df = deployment.Flags()
fs = pflag.NewFlagSet("test-enterprise", pflag.ContinueOnError)
deployment.AttachFlags(fs, df, true)
require.Nil(t, fs.Lookup("access-url"))
require.NotNil(t, fs.Lookup("audit-logging"))
require.Contains(t, fs.Lookup("audit-logging").Usage, "This is an Enterprise feature")
}
+3 -4
View File
@@ -43,7 +43,6 @@ const (
varToken = "token"
varAgentToken = "agent-token"
varAgentURL = "agent-url"
varGlobalConfig = "global-config"
varHeader = "header"
varNoOpen = "no-open"
varNoVersionCheck = "no-version-warning"
@@ -101,7 +100,7 @@ func Core() []*cobra.Command {
}
func AGPL() []*cobra.Command {
all := append(Core(), Server(deployment.Flags(), func(_ context.Context, o *coderd.Options) (*coderd.API, io.Closer, error) {
all := append(Core(), Server(deployment.NewViper(), func(_ context.Context, o *coderd.Options) (*coderd.API, io.Closer, error) {
api := coderd.New(o)
return api, api, nil
}))
@@ -184,7 +183,7 @@ func Root(subcommands []*cobra.Command) *cobra.Command {
_ = cmd.PersistentFlags().MarkHidden(varAgentToken)
cliflag.String(cmd.PersistentFlags(), varAgentURL, "", "CODER_AGENT_URL", "", "URL for an agent to access your deployment.")
_ = cmd.PersistentFlags().MarkHidden(varAgentURL)
cliflag.String(cmd.PersistentFlags(), varGlobalConfig, "", "CODER_CONFIG_DIR", configdir.LocalConfig("coderv2"), "Path to the global `coder` config directory.")
cliflag.String(cmd.PersistentFlags(), config.FlagName, "", "CODER_CONFIG_DIR", configdir.LocalConfig("coderv2"), "Path to the global `coder` config directory.")
cliflag.StringArray(cmd.PersistentFlags(), varHeader, "", "CODER_HEADER", []string{}, "HTTP headers added to all requests. Provide as \"Key=Value\"")
cmd.PersistentFlags().Bool(varForceTty, false, "Force the `coder` command to run as if connected to a TTY.")
_ = cmd.PersistentFlags().MarkHidden(varForceTty)
@@ -362,7 +361,7 @@ func namedWorkspace(cmd *cobra.Command, client *codersdk.Client, identifier stri
// createConfig consumes the global configuration flag to produce a config root.
func createConfig(cmd *cobra.Command) config.Root {
globalRoot, err := cmd.Flags().GetString(varGlobalConfig)
globalRoot, err := cmd.Flags().GetString(config.FlagName)
if err != nil {
panic(err)
}
+86 -82
View File
@@ -32,6 +32,7 @@ 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"
@@ -70,14 +71,18 @@ import (
)
// nolint:gocyclo
func Server(dflags *codersdk.DeploymentFlags, newAPI func(context.Context, *coderd.Options) (*coderd.API, io.Closer, error)) *cobra.Command {
func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*coderd.API, io.Closer, error)) *cobra.Command {
root := &cobra.Command{
Use: "server",
Short: "Start a Coder server",
RunE: func(cmd *cobra.Command, args []string) error {
cfg, err := deployment.Config(cmd.Flags(), vip)
if err != nil {
return xerrors.Errorf("getting deployment config: %w", err)
}
printLogo(cmd)
logger := slog.Make(sloghuman.Sink(cmd.ErrOrStderr()))
if dflags.Verbose.Value {
if ok, _ := cmd.Flags().GetBool(varVerbose); ok {
logger = logger.Leveled(slog.LevelDebug)
}
@@ -106,22 +111,21 @@ func Server(dflags *codersdk.DeploymentFlags, newAPI func(context.Context, *code
var (
tracerProvider trace.TracerProvider
err error
sqlDriver = "postgres"
)
// Coder tracing should be disabled if telemetry is disabled unless
// --telemetry-trace was explicitly provided.
shouldCoderTrace := dflags.TelemetryEnable.Value && !isTest()
shouldCoderTrace := cfg.TelemetryEnable.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 = dflags.TelemetryTraceEnable.Value
shouldCoderTrace = cfg.TelemetryTrace.Value
}
if dflags.TraceEnable.Value || shouldCoderTrace {
if cfg.TraceEnable.Value || shouldCoderTrace {
sdkTracerProvider, closeTracing, err := tracing.TracerProvider(ctx, "coderd", tracing.TracerOpts{
Default: dflags.TraceEnable.Value,
Default: cfg.TraceEnable.Value,
Coder: shouldCoderTrace,
})
if err != nil {
@@ -146,10 +150,10 @@ func Server(dflags *codersdk.DeploymentFlags, newAPI func(context.Context, *code
config := createConfig(cmd)
builtinPostgres := false
// Only use built-in if PostgreSQL URL isn't specified!
if !dflags.InMemoryDatabase.Value && dflags.PostgresURL.Value == "" {
if !cfg.InMemoryDatabase.Value && cfg.PostgresURL.Value == "" {
var closeFunc func() error
cmd.Printf("Using built-in PostgreSQL (%s)\n", config.PostgresPath())
dflags.PostgresURL.Value, closeFunc, err = startBuiltinPostgres(ctx, config, logger)
cfg.PostgresURL.Value, closeFunc, err = startBuiltinPostgres(ctx, config, logger)
if err != nil {
return err
}
@@ -162,20 +166,20 @@ func Server(dflags *codersdk.DeploymentFlags, newAPI func(context.Context, *code
}()
}
listener, err := net.Listen("tcp", dflags.Address.Value)
listener, err := net.Listen("tcp", cfg.Address.Value)
if err != nil {
return xerrors.Errorf("listen %q: %w", dflags.Address.Value, err)
return xerrors.Errorf("listen %q: %w", cfg.Address.Value, err)
}
defer listener.Close()
var tlsConfig *tls.Config
if dflags.TLSEnable.Value {
if cfg.TLSEnable.Value {
tlsConfig, err = configureTLS(
dflags.TLSMinVersion.Value,
dflags.TLSClientAuth.Value,
dflags.TLSCertFiles.Value,
dflags.TLSKeyFiles.Value,
dflags.TLSClientCAFile.Value,
cfg.TLSMinVersion.Value,
cfg.TLSClientAuth.Value,
cfg.TLSCertFiles.Value,
cfg.TLSKeyFiles.Value,
cfg.TLSClientCAFile.Value,
)
if err != nil {
return xerrors.Errorf("configure tls: %w", err)
@@ -197,7 +201,7 @@ func Server(dflags *codersdk.DeploymentFlags, newAPI func(context.Context, *code
Scheme: "http",
Host: tcpAddr.String(),
}
if dflags.TLSEnable.Value {
if cfg.TLSEnable.Value {
localURL.Scheme = "https"
}
@@ -210,26 +214,26 @@ func Server(dflags *codersdk.DeploymentFlags, newAPI func(context.Context, *code
// If the access URL is empty, we attempt to run a reverse-proxy
// tunnel to make the initial setup really simple.
if dflags.AccessURL.Value == "" {
if cfg.AccessURL.Value == "" {
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)
}
dflags.AccessURL.Value = tunnel.URL
cfg.AccessURL.Value = tunnel.URL
if dflags.WildcardAccessURL.Value == "" {
if cfg.WildcardAccessURL.Value == "" {
u, err := parseURL(ctx, tunnel.URL)
if err != nil {
return xerrors.Errorf("parse tunnel url: %w", err)
}
// Suffixed wildcard access URL.
dflags.WildcardAccessURL.Value = fmt.Sprintf("*--%s", u.Hostname())
cfg.WildcardAccessURL.Value = fmt.Sprintf("*--%s", u.Hostname())
}
}
accessURLParsed, err := parseURL(ctx, dflags.AccessURL.Value)
accessURLParsed, err := parseURL(ctx, cfg.AccessURL.Value)
if err != nil {
return xerrors.Errorf("parse URL: %w", err)
}
@@ -264,17 +268,17 @@ func Server(dflags *codersdk.DeploymentFlags, newAPI func(context.Context, *code
return err
}
sshKeygenAlgorithm, err := gitsshkey.ParseAlgorithm(dflags.SSHKeygenAlgorithm.Value)
sshKeygenAlgorithm, err := gitsshkey.ParseAlgorithm(cfg.SSHKeygenAlgorithm.Value)
if err != nil {
return xerrors.Errorf("parse ssh keygen algorithm %s: %w", dflags.SSHKeygenAlgorithm.Value, err)
return xerrors.Errorf("parse ssh keygen algorithm %s: %w", cfg.SSHKeygenAlgorithm.Value, err)
}
// Validate provided auto-import templates.
var (
validatedAutoImportTemplates = make([]coderd.AutoImportTemplate, len(dflags.AutoImportTemplates.Value))
seenValidatedAutoImportTemplates = make(map[coderd.AutoImportTemplate]struct{}, len(dflags.AutoImportTemplates.Value))
validatedAutoImportTemplates = make([]coderd.AutoImportTemplate, len(cfg.AutoImportTemplates.Value))
seenValidatedAutoImportTemplates = make(map[coderd.AutoImportTemplate]struct{}, len(cfg.AutoImportTemplates.Value))
)
for i, autoImportTemplate := range dflags.AutoImportTemplates.Value {
for i, autoImportTemplate := range cfg.AutoImportTemplates.Value {
var v coderd.AutoImportTemplate
switch autoImportTemplate {
case "kubernetes":
@@ -292,27 +296,27 @@ func Server(dflags *codersdk.DeploymentFlags, newAPI func(context.Context, *code
defaultRegion := &tailcfg.DERPRegion{
EmbeddedRelay: true,
RegionID: dflags.DerpServerRegionID.Value,
RegionCode: dflags.DerpServerRegionCode.Value,
RegionName: dflags.DerpServerRegionName.Value,
RegionID: cfg.DERPServerRegionID.Value,
RegionCode: cfg.DERPServerRegionCode.Value,
RegionName: cfg.DERPServerRegionName.Value,
Nodes: []*tailcfg.DERPNode{{
Name: fmt.Sprintf("%db", dflags.DerpServerRegionID.Value),
RegionID: dflags.DerpServerRegionID.Value,
Name: fmt.Sprintf("%db", cfg.DERPServerRegionID.Value),
RegionID: cfg.DERPServerRegionID.Value,
HostName: accessURLParsed.Hostname(),
DERPPort: accessURLPort,
STUNPort: -1,
ForceHTTP: accessURLParsed.Scheme == "http",
}},
}
if !dflags.DerpServerEnable.Value {
if !cfg.DERPServerEnable.Value {
defaultRegion = nil
}
derpMap, err := tailnet.NewDERPMap(ctx, defaultRegion, dflags.DerpServerSTUNAddresses.Value, dflags.DerpConfigURL.Value, dflags.DerpConfigPath.Value)
derpMap, err := tailnet.NewDERPMap(ctx, defaultRegion, cfg.DERPServerSTUNAddresses.Value, cfg.DERPConfigURL.Value, cfg.DERPConfigPath.Value)
if err != nil {
return xerrors.Errorf("create derp map: %w", err)
}
appHostname := strings.TrimSpace(dflags.WildcardAccessURL.Value)
appHostname := strings.TrimSpace(cfg.WildcardAccessURL.Value)
var appHostnameRegex *regexp.Regexp
if appHostname != "" {
appHostnameRegex, err = httpapi.CompileHostnamePattern(appHostname)
@@ -329,45 +333,45 @@ func Server(dflags *codersdk.DeploymentFlags, newAPI func(context.Context, *code
Database: databasefake.New(),
DERPMap: derpMap,
Pubsub: database.NewPubsubInMemory(),
CacheDir: dflags.CacheDir.Value,
CacheDir: cfg.CacheDirectory.Value,
GoogleTokenValidator: googleTokenValidator,
SecureAuthCookie: dflags.SecureAuthCookie.Value,
SecureAuthCookie: cfg.SecureAuthCookie.Value,
SSHKeygenAlgorithm: sshKeygenAlgorithm,
TracerProvider: tracerProvider,
Telemetry: telemetry.NewNoop(),
AutoImportTemplates: validatedAutoImportTemplates,
MetricsCacheRefreshInterval: dflags.MetricsCacheRefreshInterval.Value,
AgentStatsRefreshInterval: dflags.AgentStatRefreshInterval.Value,
MetricsCacheRefreshInterval: cfg.MetricsCacheRefreshInterval.Value,
AgentStatsRefreshInterval: cfg.AgentStatRefreshInterval.Value,
Experimental: ExperimentalEnabled(cmd),
DeploymentFlags: dflags,
DeploymentConfig: &cfg,
}
if tlsConfig != nil {
options.TLSCertificates = tlsConfig.Certificates
}
if dflags.OAuth2GithubClientSecret.Value != "" {
if cfg.OAuth2GithubClientSecret.Value != "" {
options.GithubOAuth2Config, err = configureGithubOAuth2(accessURLParsed,
dflags.OAuth2GithubClientID.Value,
dflags.OAuth2GithubClientSecret.Value,
dflags.OAuth2GithubAllowSignups.Value,
dflags.OAuth2GithubAllowedOrganizations.Value,
dflags.OAuth2GithubAllowedTeams.Value,
dflags.OAuth2GithubEnterpriseBaseURL.Value,
cfg.OAuth2GithubClientID.Value,
cfg.OAuth2GithubClientSecret.Value,
cfg.OAuth2GithubAllowSignups.Value,
cfg.OAuth2GithubAllowedOrgs.Value,
cfg.OAuth2GithubAllowedTeams.Value,
cfg.OAuth2GithubEnterpriseBaseURL.Value,
)
if err != nil {
return xerrors.Errorf("configure github oauth2: %w", err)
}
}
if dflags.OIDCClientSecret.Value != "" {
if dflags.OIDCClientID.Value == "" {
if cfg.OIDCClientSecret.Value != "" {
if cfg.OIDCClientID.Value == "" {
return xerrors.Errorf("OIDC client ID be set!")
}
if dflags.OIDCIssuerURL.Value == "" {
if cfg.OIDCIssuerURL.Value == "" {
return xerrors.Errorf("OIDC issuer URL must be set!")
}
oidcProvider, err := oidc.NewProvider(ctx, dflags.OIDCIssuerURL.Value)
oidcProvider, err := oidc.NewProvider(ctx, cfg.OIDCIssuerURL.Value)
if err != nil {
return xerrors.Errorf("configure oidc provider: %w", err)
}
@@ -377,25 +381,25 @@ func Server(dflags *codersdk.DeploymentFlags, newAPI func(context.Context, *code
}
options.OIDCConfig = &coderd.OIDCConfig{
OAuth2Config: &oauth2.Config{
ClientID: dflags.OIDCClientID.Value,
ClientSecret: dflags.OIDCClientSecret.Value,
ClientID: cfg.OIDCClientID.Value,
ClientSecret: cfg.OIDCClientSecret.Value,
RedirectURL: redirectURL.String(),
Endpoint: oidcProvider.Endpoint(),
Scopes: dflags.OIDCScopes.Value,
Scopes: cfg.OIDCScopes.Value,
},
Verifier: oidcProvider.Verifier(&oidc.Config{
ClientID: dflags.OIDCClientID.Value,
ClientID: cfg.OIDCClientID.Value,
}),
EmailDomain: dflags.OIDCEmailDomain.Value,
AllowSignups: dflags.OIDCAllowSignups.Value,
EmailDomain: cfg.OIDCEmailDomain.Value,
AllowSignups: cfg.OIDCAllowSignups.Value,
}
}
if dflags.InMemoryDatabase.Value {
if cfg.InMemoryDatabase.Value {
options.Database = databasefake.New()
options.Pubsub = database.NewPubsubInMemory()
} else {
sqlDB, err := sql.Open(sqlDriver, dflags.PostgresURL.Value)
sqlDB, err := sql.Open(sqlDriver, cfg.PostgresURL.Value)
if err != nil {
return xerrors.Errorf("dial postgres: %w", err)
}
@@ -427,7 +431,7 @@ func Server(dflags *codersdk.DeploymentFlags, newAPI func(context.Context, *code
return xerrors.Errorf("migrate up: %w", err)
}
options.Database = database.New(sqlDB)
options.Pubsub, err = database.NewPubsub(ctx, sqlDB, dflags.PostgresURL.Value)
options.Pubsub, err = database.NewPubsub(ctx, sqlDB, cfg.PostgresURL.Value)
if err != nil {
return xerrors.Errorf("create pubsub: %w", err)
}
@@ -450,26 +454,26 @@ func Server(dflags *codersdk.DeploymentFlags, newAPI func(context.Context, *code
}
// Parse the raw telemetry URL!
telemetryURL, err := parseURL(ctx, dflags.TelemetryURL.Value)
telemetryURL, err := parseURL(ctx, cfg.TelemetryURL.Value)
if err != nil {
return xerrors.Errorf("parse telemetry url: %w", err)
}
// Disable telemetry if the in-memory database is used unless explicitly defined!
if dflags.InMemoryDatabase.Value && !cmd.Flags().Changed(dflags.TelemetryEnable.Flag) {
dflags.TelemetryEnable.Value = false
if cfg.InMemoryDatabase.Value && !cmd.Flags().Changed(cfg.TelemetryEnable.Flag) {
cfg.TelemetryEnable.Value = false
}
if dflags.TelemetryEnable.Value {
if cfg.TelemetryEnable.Value {
options.Telemetry, err = telemetry.New(telemetry.Options{
BuiltinPostgres: builtinPostgres,
DeploymentID: deploymentID,
Database: options.Database,
Logger: logger.Named("telemetry"),
URL: telemetryURL,
GitHubOAuth: dflags.OAuth2GithubClientID.Value != "",
OIDCAuth: dflags.OIDCClientID.Value != "",
OIDCIssuerURL: dflags.OIDCIssuerURL.Value,
Prometheus: dflags.PromEnabled.Value,
STUN: len(dflags.DerpServerSTUNAddresses.Value) != 0,
GitHubOAuth: cfg.OAuth2GithubClientID.Value != "",
OIDCAuth: cfg.OIDCClientID.Value != "",
OIDCIssuerURL: cfg.OIDCIssuerURL.Value,
Prometheus: cfg.PrometheusEnable.Value,
STUN: len(cfg.DERPServerSTUNAddresses.Value) != 0,
Tunnel: tunnel != nil,
})
if err != nil {
@@ -480,11 +484,11 @@ func Server(dflags *codersdk.DeploymentFlags, newAPI func(context.Context, *code
// This prevents the pprof import from being accidentally deleted.
_ = pprof.Handler
if dflags.PprofEnabled.Value {
if cfg.PprofEnable.Value {
//nolint:revive
defer serveHandler(ctx, logger, nil, dflags.PprofAddress.Value, "pprof")()
defer serveHandler(ctx, logger, nil, cfg.PprofAddress.Value, "pprof")()
}
if dflags.PromEnabled.Value {
if cfg.PrometheusEnable.Value {
options.PrometheusRegistry = prometheus.NewRegistry()
closeUsersFunc, err := prometheusmetrics.ActiveUsers(ctx, options.PrometheusRegistry, options.Database, 0)
if err != nil {
@@ -501,7 +505,7 @@ func Server(dflags *codersdk.DeploymentFlags, newAPI func(context.Context, *code
//nolint:revive
defer serveHandler(ctx, logger, promhttp.InstrumentMetricHandler(
options.PrometheusRegistry, promhttp.HandlerFor(options.PrometheusRegistry, promhttp.HandlerOpts{}),
), dflags.PromAddress.Value, "prometheus")()
), cfg.PrometheusAddress.Value, "prometheus")()
}
// We use a separate coderAPICloser so the Enterprise API
@@ -513,7 +517,7 @@ func Server(dflags *codersdk.DeploymentFlags, newAPI func(context.Context, *code
}
client := codersdk.New(localURL)
if dflags.TLSEnable.Value {
if cfg.TLSEnable.Value {
// Secure transport isn't needed for locally communicating!
client.HTTPClient.Transport = &http.Transport{
TLSClientConfig: &tls.Config{
@@ -537,8 +541,8 @@ func Server(dflags *codersdk.DeploymentFlags, newAPI func(context.Context, *code
_ = daemon.Close()
}
}()
for i := 0; i < dflags.ProvisionerDaemonCount.Value; i++ {
daemon, err := newProvisionerDaemon(ctx, coderAPI, logger, dflags.CacheDir.Value, errCh, false)
for i := 0; i < cfg.ProvisionerDaemons.Value; i++ {
daemon, err := newProvisionerDaemon(ctx, coderAPI, logger, cfg.CacheDirectory.Value, errCh, false)
if err != nil {
return xerrors.Errorf("create provisioner daemon: %w", err)
}
@@ -604,7 +608,7 @@ func Server(dflags *codersdk.DeploymentFlags, newAPI func(context.Context, *code
return xerrors.Errorf("notify systemd: %w", err)
}
autobuildPoller := time.NewTicker(dflags.AutobuildPollInterval.Value)
autobuildPoller := time.NewTicker(cfg.AutobuildPollInterval.Value)
defer autobuildPoller.Stop()
autobuildExecutor := executor.New(ctx, options.Database, logger, autobuildPoller.C)
autobuildExecutor.Run()
@@ -669,7 +673,7 @@ func Server(dflags *codersdk.DeploymentFlags, newAPI func(context.Context, *code
go func() {
defer wg.Done()
if dflags.Verbose.Value {
if ok, _ := cmd.Flags().GetBool(varVerbose); ok {
cmd.Printf("Shutting down provisioner daemon %d...\n", id)
}
err := shutdownWithTimeout(provisionerDaemon.Shutdown, 5*time.Second)
@@ -682,7 +686,7 @@ func Server(dflags *codersdk.DeploymentFlags, newAPI func(context.Context, *code
cmd.PrintErrf("Close provisioner daemon %d: %s\n", id, err)
return
}
if dflags.Verbose.Value {
if ok, _ := cmd.Flags().GetBool(varVerbose); ok {
cmd.Printf("Gracefully shut down provisioner daemon %d\n", id)
}
}()
@@ -734,7 +738,7 @@ func Server(dflags *codersdk.DeploymentFlags, newAPI func(context.Context, *code
RunE: func(cmd *cobra.Command, args []string) error {
cfg := createConfig(cmd)
logger := slog.Make(sloghuman.Sink(cmd.ErrOrStderr()))
if dflags.Verbose.Value {
if ok, _ := cmd.Flags().GetBool(varVerbose); ok {
logger = logger.Leveled(slog.LevelDebug)
}
@@ -755,7 +759,7 @@ func Server(dflags *codersdk.DeploymentFlags, newAPI func(context.Context, *code
},
})
deployment.AttachFlags(root.Flags(), dflags, false)
deployment.AttachFlags(root.Flags(), vip, false)
return root
}