From 7182fd2950c32196eb03d307a9006598f3ec5c6a Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Fri, 21 Oct 2022 17:07:08 -0400 Subject: [PATCH] Revert "Revert "Revert "feat: Support config files with viper"" (#4693)" This reverts commit 372fb1f3458e71bf45f1e45f328c3044c2983660. --- cli/config/file.go | 8 - cli/deployment/config.go | 438 --------------- cli/deployment/flags.go | 511 ++++++++++++++++++ cli/deployment/flags_test.go | 32 ++ cli/root.go | 7 +- cli/server.go | 168 +++--- coderd/coderd.go | 6 +- coderd/coderdtest/coderdtest.go | 4 +- coderd/deploymentconfig.go | 17 - coderd/deploymentconfig_test.go | 47 -- coderd/flags.go | 18 + coderd/flags_test.go | 47 ++ coderd/rbac/object.go | 6 +- codersdk/deploymentconfig.go | 97 ---- codersdk/flags.go | 142 +++++ docs/install/kubernetes.md | 2 +- enterprise/cli/server.go | 25 +- go.mod | 11 +- go.sum | 14 - helm/README.md | 4 +- scripts/apitypings/main.go | 4 - site/src/AppRouter.tsx | 8 +- site/src/api/api.ts | 6 +- site/src/api/typesGenerated.ts | 176 ++++-- .../DeploySettingsLayout.tsx | 14 +- .../DeploySettingsLayout/OptionsTable.tsx | 4 +- site/src/components/Navbar/Navbar.tsx | 2 +- .../DeploySettingsPage/AuthSettingsPage.tsx | 30 +- .../GeneralSettingsPage.tsx | 8 +- .../NetworkSettingsPage.tsx | 10 +- .../SecuritySettingsPage.tsx | 14 +- site/src/xServices/StateContext.tsx | 6 +- site/src/xServices/auth/authXService.ts | 4 +- .../deploymentConfigMachine.ts | 62 --- .../deploymentFlags/deploymentFlagsMachine.ts | 62 +++ 35 files changed, 1091 insertions(+), 923 deletions(-) delete mode 100644 cli/deployment/config.go create mode 100644 cli/deployment/flags.go create mode 100644 cli/deployment/flags_test.go delete mode 100644 coderd/deploymentconfig.go delete mode 100644 coderd/deploymentconfig_test.go create mode 100644 coderd/flags.go create mode 100644 coderd/flags_test.go delete mode 100644 codersdk/deploymentconfig.go create mode 100644 codersdk/flags.go delete mode 100644 site/src/xServices/deploymentConfig/deploymentConfigMachine.ts create mode 100644 site/src/xServices/deploymentFlags/deploymentFlagsMachine.ts diff --git a/cli/config/file.go b/cli/config/file.go index 26af6896c14b3..388ce0881f304 100644 --- a/cli/config/file.go +++ b/cli/config/file.go @@ -6,10 +6,6 @@ import ( "path/filepath" ) -const ( - FlagName = "global-config" -) - // Root represents the configuration directory. type Root string @@ -46,10 +42,6 @@ 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 diff --git a/cli/deployment/config.go b/cli/deployment/config.go deleted file mode 100644 index 01bc7ce1ddc75..0000000000000 --- a/cli/deployment/config.go +++ /dev/null @@ -1,438 +0,0 @@ -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"}, - }, - DERPServerRelayAddress: codersdk.DeploymentConfigField[string]{ - Key: "derp.server.relay_address", - Usage: "An HTTP address that is accessible by other replicas to relay DERP traffic. Required for high availability.", - Flag: "derp-server-relay-address", - 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", - }, - OAuth2GithubAllowedOrganizations: codersdk.DeploymentConfigField[[]string]{ - Key: "oauth2.github.allowed_organizations", - 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: /.", - 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: - fve.FieldByName("Value").Set(reflect.ValueOf(vip.GetStringSlice(key))) - 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") -} diff --git a/cli/deployment/flags.go b/cli/deployment/flags.go new file mode 100644 index 0000000000000..792051f805f70 --- /dev/null +++ b/cli/deployment/flags.go @@ -0,0 +1,511 @@ +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: /.", + 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") +} diff --git a/cli/deployment/flags_test.go b/cli/deployment/flags_test.go new file mode 100644 index 0000000000000..8b411068dba28 --- /dev/null +++ b/cli/deployment/flags_test.go @@ -0,0 +1,32 @@ +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") +} diff --git a/cli/root.go b/cli/root.go index c40bd927e2b90..ea803cdfa5d81 100644 --- a/cli/root.go +++ b/cli/root.go @@ -43,6 +43,7 @@ const ( varToken = "token" varAgentToken = "agent-token" varAgentURL = "agent-url" + varGlobalConfig = "global-config" varHeader = "header" varNoOpen = "no-open" varNoVersionCheck = "no-version-warning" @@ -100,7 +101,7 @@ func Core() []*cobra.Command { } func AGPL() []*cobra.Command { - all := append(Core(), Server(deployment.NewViper(), func(_ context.Context, o *coderd.Options) (*coderd.API, io.Closer, error) { + all := append(Core(), Server(deployment.Flags(), func(_ context.Context, o *coderd.Options) (*coderd.API, io.Closer, error) { api := coderd.New(o) return api, api, nil })) @@ -183,7 +184,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(), config.FlagName, "", "CODER_CONFIG_DIR", configdir.LocalConfig("coderv2"), "Path to the global `coder` config directory.") + cliflag.String(cmd.PersistentFlags(), varGlobalConfig, "", "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) @@ -361,7 +362,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(config.FlagName) + globalRoot, err := cmd.Flags().GetString(varGlobalConfig) if err != nil { panic(err) } diff --git a/cli/server.go b/cli/server.go index 8f3ce62921c80..99f23e9278315 100644 --- a/cli/server.go +++ b/cli/server.go @@ -32,7 +32,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" @@ -71,18 +70,14 @@ import ( ) // nolint:gocyclo -func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*coderd.API, io.Closer, error)) *cobra.Command { +func Server(dflags *codersdk.DeploymentFlags, 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 ok, _ := cmd.Flags().GetBool(varVerbose); ok { + if dflags.Verbose.Value { logger = logger.Leveled(slog.LevelDebug) } @@ -111,21 +106,22 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co var ( tracerProvider trace.TracerProvider + err error sqlDriver = "postgres" ) // Coder tracing should be disabled if telemetry is disabled unless // --telemetry-trace was explicitly provided. - shouldCoderTrace := cfg.TelemetryEnable.Value && !isTest() + shouldCoderTrace := dflags.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 = cfg.TelemetryTrace.Value + shouldCoderTrace = dflags.TelemetryTraceEnable.Value } - if cfg.TraceEnable.Value || shouldCoderTrace { + if dflags.TraceEnable.Value || shouldCoderTrace { sdkTracerProvider, closeTracing, err := tracing.TracerProvider(ctx, "coderd", tracing.TracerOpts{ - Default: cfg.TraceEnable.Value, + Default: dflags.TraceEnable.Value, Coder: shouldCoderTrace, }) if err != nil { @@ -150,10 +146,10 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co config := createConfig(cmd) builtinPostgres := false // Only use built-in if PostgreSQL URL isn't specified! - if !cfg.InMemoryDatabase.Value && cfg.PostgresURL.Value == "" { + if !dflags.InMemoryDatabase.Value && dflags.PostgresURL.Value == "" { var closeFunc func() error cmd.Printf("Using built-in PostgreSQL (%s)\n", config.PostgresPath()) - cfg.PostgresURL.Value, closeFunc, err = startBuiltinPostgres(ctx, config, logger) + dflags.PostgresURL.Value, closeFunc, err = startBuiltinPostgres(ctx, config, logger) if err != nil { return err } @@ -166,20 +162,20 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co }() } - listener, err := net.Listen("tcp", cfg.Address.Value) + listener, err := net.Listen("tcp", dflags.Address.Value) if err != nil { - return xerrors.Errorf("listen %q: %w", cfg.Address.Value, err) + return xerrors.Errorf("listen %q: %w", dflags.Address.Value, err) } defer listener.Close() var tlsConfig *tls.Config - if cfg.TLSEnable.Value { + if dflags.TLSEnable.Value { tlsConfig, err = configureTLS( - cfg.TLSMinVersion.Value, - cfg.TLSClientAuth.Value, - cfg.TLSCertFiles.Value, - cfg.TLSKeyFiles.Value, - cfg.TLSClientCAFile.Value, + dflags.TLSMinVersion.Value, + dflags.TLSClientAuth.Value, + dflags.TLSCertFiles.Value, + dflags.TLSKeyFiles.Value, + dflags.TLSClientCAFile.Value, ) if err != nil { return xerrors.Errorf("configure tls: %w", err) @@ -201,7 +197,7 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co Scheme: "http", Host: tcpAddr.String(), } - if cfg.TLSEnable.Value { + if dflags.TLSEnable.Value { localURL.Scheme = "https" } @@ -214,26 +210,26 @@ 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 dflags.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) } - cfg.AccessURL.Value = tunnel.URL + dflags.AccessURL.Value = tunnel.URL - if cfg.WildcardAccessURL.Value == "" { + if dflags.WildcardAccessURL.Value == "" { u, err := parseURL(ctx, 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()) + dflags.WildcardAccessURL.Value = fmt.Sprintf("*--%s", u.Hostname()) } } - accessURLParsed, err := parseURL(ctx, cfg.AccessURL.Value) + accessURLParsed, err := parseURL(ctx, dflags.AccessURL.Value) if err != nil { return xerrors.Errorf("parse URL: %w", err) } @@ -268,17 +264,17 @@ 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(dflags.SSHKeygenAlgorithm.Value) 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", dflags.SSHKeygenAlgorithm.Value, err) } // Validate provided auto-import templates. var ( - validatedAutoImportTemplates = make([]coderd.AutoImportTemplate, len(cfg.AutoImportTemplates.Value)) - seenValidatedAutoImportTemplates = make(map[coderd.AutoImportTemplate]struct{}, len(cfg.AutoImportTemplates.Value)) + validatedAutoImportTemplates = make([]coderd.AutoImportTemplate, len(dflags.AutoImportTemplates.Value)) + seenValidatedAutoImportTemplates = make(map[coderd.AutoImportTemplate]struct{}, len(dflags.AutoImportTemplates.Value)) ) - for i, autoImportTemplate := range cfg.AutoImportTemplates.Value { + for i, autoImportTemplate := range dflags.AutoImportTemplates.Value { var v coderd.AutoImportTemplate switch autoImportTemplate { case "kubernetes": @@ -296,27 +292,27 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co defaultRegion := &tailcfg.DERPRegion{ EmbeddedRelay: true, - RegionID: cfg.DERPServerRegionID.Value, - RegionCode: cfg.DERPServerRegionCode.Value, - RegionName: cfg.DERPServerRegionName.Value, + RegionID: dflags.DerpServerRegionID.Value, + RegionCode: dflags.DerpServerRegionCode.Value, + RegionName: dflags.DerpServerRegionName.Value, Nodes: []*tailcfg.DERPNode{{ - Name: fmt.Sprintf("%db", cfg.DERPServerRegionID.Value), - RegionID: cfg.DERPServerRegionID.Value, + Name: fmt.Sprintf("%db", dflags.DerpServerRegionID.Value), + RegionID: dflags.DerpServerRegionID.Value, HostName: accessURLParsed.Hostname(), DERPPort: accessURLPort, STUNPort: -1, ForceHTTP: accessURLParsed.Scheme == "http", }}, } - if !cfg.DERPServerEnable.Value { + if !dflags.DerpServerEnable.Value { defaultRegion = nil } - derpMap, err := tailnet.NewDERPMap(ctx, defaultRegion, cfg.DERPServerSTUNAddresses.Value, cfg.DERPConfigURL.Value, cfg.DERPConfigPath.Value) + derpMap, err := tailnet.NewDERPMap(ctx, defaultRegion, dflags.DerpServerSTUNAddresses.Value, dflags.DerpConfigURL.Value, dflags.DerpConfigPath.Value) if err != nil { return xerrors.Errorf("create derp map: %w", err) } - appHostname := strings.TrimSpace(cfg.WildcardAccessURL.Value) + appHostname := strings.TrimSpace(dflags.WildcardAccessURL.Value) var appHostnameRegex *regexp.Regexp if appHostname != "" { appHostnameRegex, err = httpapi.CompileHostnamePattern(appHostname) @@ -333,45 +329,45 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co Database: databasefake.New(), DERPMap: derpMap, Pubsub: database.NewPubsubInMemory(), - CacheDir: cfg.CacheDirectory.Value, + CacheDir: dflags.CacheDir.Value, GoogleTokenValidator: googleTokenValidator, - SecureAuthCookie: cfg.SecureAuthCookie.Value, + SecureAuthCookie: dflags.SecureAuthCookie.Value, SSHKeygenAlgorithm: sshKeygenAlgorithm, TracerProvider: tracerProvider, Telemetry: telemetry.NewNoop(), AutoImportTemplates: validatedAutoImportTemplates, - MetricsCacheRefreshInterval: cfg.MetricsCacheRefreshInterval.Value, - AgentStatsRefreshInterval: cfg.AgentStatRefreshInterval.Value, + MetricsCacheRefreshInterval: dflags.MetricsCacheRefreshInterval.Value, + AgentStatsRefreshInterval: dflags.AgentStatRefreshInterval.Value, Experimental: ExperimentalEnabled(cmd), - DeploymentConfig: &cfg, + DeploymentFlags: dflags, } if tlsConfig != nil { options.TLSCertificates = tlsConfig.Certificates } - if cfg.OAuth2GithubClientSecret.Value != "" { + if dflags.OAuth2GithubClientSecret.Value != "" { options.GithubOAuth2Config, err = configureGithubOAuth2(accessURLParsed, - cfg.OAuth2GithubClientID.Value, - cfg.OAuth2GithubClientSecret.Value, - cfg.OAuth2GithubAllowSignups.Value, - cfg.OAuth2GithubAllowedOrganizations.Value, - cfg.OAuth2GithubAllowedTeams.Value, - cfg.OAuth2GithubEnterpriseBaseURL.Value, + dflags.OAuth2GithubClientID.Value, + dflags.OAuth2GithubClientSecret.Value, + dflags.OAuth2GithubAllowSignups.Value, + dflags.OAuth2GithubAllowedOrganizations.Value, + dflags.OAuth2GithubAllowedTeams.Value, + dflags.OAuth2GithubEnterpriseBaseURL.Value, ) if err != nil { return xerrors.Errorf("configure github oauth2: %w", err) } } - if cfg.OIDCClientSecret.Value != "" { - if cfg.OIDCClientID.Value == "" { + if dflags.OIDCClientSecret.Value != "" { + if dflags.OIDCClientID.Value == "" { return xerrors.Errorf("OIDC client ID be set!") } - if cfg.OIDCIssuerURL.Value == "" { + if dflags.OIDCIssuerURL.Value == "" { return xerrors.Errorf("OIDC issuer URL must be set!") } - oidcProvider, err := oidc.NewProvider(ctx, cfg.OIDCIssuerURL.Value) + oidcProvider, err := oidc.NewProvider(ctx, dflags.OIDCIssuerURL.Value) if err != nil { return xerrors.Errorf("configure oidc provider: %w", err) } @@ -381,25 +377,25 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co } options.OIDCConfig = &coderd.OIDCConfig{ OAuth2Config: &oauth2.Config{ - ClientID: cfg.OIDCClientID.Value, - ClientSecret: cfg.OIDCClientSecret.Value, + ClientID: dflags.OIDCClientID.Value, + ClientSecret: dflags.OIDCClientSecret.Value, RedirectURL: redirectURL.String(), Endpoint: oidcProvider.Endpoint(), - Scopes: cfg.OIDCScopes.Value, + Scopes: dflags.OIDCScopes.Value, }, Verifier: oidcProvider.Verifier(&oidc.Config{ - ClientID: cfg.OIDCClientID.Value, + ClientID: dflags.OIDCClientID.Value, }), - EmailDomain: cfg.OIDCEmailDomain.Value, - AllowSignups: cfg.OIDCAllowSignups.Value, + EmailDomain: dflags.OIDCEmailDomain.Value, + AllowSignups: dflags.OIDCAllowSignups.Value, } } - if cfg.InMemoryDatabase.Value { + if dflags.InMemoryDatabase.Value { options.Database = databasefake.New() options.Pubsub = database.NewPubsubInMemory() } else { - sqlDB, err := sql.Open(sqlDriver, cfg.PostgresURL.Value) + sqlDB, err := sql.Open(sqlDriver, dflags.PostgresURL.Value) if err != nil { return xerrors.Errorf("dial postgres: %w", err) } @@ -431,7 +427,7 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co return xerrors.Errorf("migrate up: %w", err) } options.Database = database.New(sqlDB) - options.Pubsub, err = database.NewPubsub(ctx, sqlDB, cfg.PostgresURL.Value) + options.Pubsub, err = database.NewPubsub(ctx, sqlDB, dflags.PostgresURL.Value) if err != nil { return xerrors.Errorf("create pubsub: %w", err) } @@ -454,26 +450,26 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co } // Parse the raw telemetry URL! - telemetryURL, err := parseURL(ctx, cfg.TelemetryURL.Value) + telemetryURL, err := parseURL(ctx, dflags.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 cfg.InMemoryDatabase.Value && !cmd.Flags().Changed(cfg.TelemetryEnable.Flag) { - cfg.TelemetryEnable.Value = false + if dflags.InMemoryDatabase.Value && !cmd.Flags().Changed(dflags.TelemetryEnable.Flag) { + dflags.TelemetryEnable.Value = false } - if cfg.TelemetryEnable.Value { + if dflags.TelemetryEnable.Value { options.Telemetry, err = telemetry.New(telemetry.Options{ BuiltinPostgres: builtinPostgres, DeploymentID: deploymentID, Database: options.Database, Logger: logger.Named("telemetry"), URL: telemetryURL, - GitHubOAuth: cfg.OAuth2GithubClientID.Value != "", - OIDCAuth: cfg.OIDCClientID.Value != "", - OIDCIssuerURL: cfg.OIDCIssuerURL.Value, - Prometheus: cfg.PrometheusEnable.Value, - STUN: len(cfg.DERPServerSTUNAddresses.Value) != 0, + GitHubOAuth: dflags.OAuth2GithubClientID.Value != "", + OIDCAuth: dflags.OIDCClientID.Value != "", + OIDCIssuerURL: dflags.OIDCIssuerURL.Value, + Prometheus: dflags.PromEnabled.Value, + STUN: len(dflags.DerpServerSTUNAddresses.Value) != 0, Tunnel: tunnel != nil, }) if err != nil { @@ -484,11 +480,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.PprofEnable.Value { + if dflags.PprofEnabled.Value { //nolint:revive - defer serveHandler(ctx, logger, nil, cfg.PprofAddress.Value, "pprof")() + defer serveHandler(ctx, logger, nil, dflags.PprofAddress.Value, "pprof")() } - if cfg.PrometheusEnable.Value { + if dflags.PromEnabled.Value { options.PrometheusRegistry = prometheus.NewRegistry() closeUsersFunc, err := prometheusmetrics.ActiveUsers(ctx, options.PrometheusRegistry, options.Database, 0) if err != nil { @@ -505,7 +501,7 @@ 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.PrometheusAddress.Value, "prometheus")() + ), dflags.PromAddress.Value, "prometheus")() } // We use a separate coderAPICloser so the Enterprise API @@ -517,7 +513,7 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co } client := codersdk.New(localURL) - if cfg.TLSEnable.Value { + if dflags.TLSEnable.Value { // Secure transport isn't needed for locally communicating! client.HTTPClient.Transport = &http.Transport{ TLSClientConfig: &tls.Config{ @@ -541,8 +537,8 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co _ = daemon.Close() } }() - for i := 0; i < cfg.ProvisionerDaemons.Value; i++ { - daemon, err := newProvisionerDaemon(ctx, coderAPI, logger, cfg.CacheDirectory.Value, errCh, false) + for i := 0; i < dflags.ProvisionerDaemonCount.Value; i++ { + daemon, err := newProvisionerDaemon(ctx, coderAPI, logger, dflags.CacheDir.Value, errCh, false) if err != nil { return xerrors.Errorf("create provisioner daemon: %w", err) } @@ -608,7 +604,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(dflags.AutobuildPollInterval.Value) defer autobuildPoller.Stop() autobuildExecutor := executor.New(ctx, options.Database, logger, autobuildPoller.C) autobuildExecutor.Run() @@ -673,7 +669,7 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co go func() { defer wg.Done() - if ok, _ := cmd.Flags().GetBool(varVerbose); ok { + if dflags.Verbose.Value { cmd.Printf("Shutting down provisioner daemon %d...\n", id) } err := shutdownWithTimeout(provisionerDaemon.Shutdown, 5*time.Second) @@ -686,7 +682,7 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co cmd.PrintErrf("Close provisioner daemon %d: %s\n", id, err) return } - if ok, _ := cmd.Flags().GetBool(varVerbose); ok { + if dflags.Verbose.Value { cmd.Printf("Gracefully shut down provisioner daemon %d\n", id) } }() @@ -738,7 +734,7 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co RunE: func(cmd *cobra.Command, args []string) error { cfg := createConfig(cmd) logger := slog.Make(sloghuman.Sink(cmd.ErrOrStderr())) - if ok, _ := cmd.Flags().GetBool(varVerbose); ok { + if dflags.Verbose.Value { logger = logger.Leveled(slog.LevelDebug) } @@ -759,7 +755,7 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co }, }) - deployment.AttachFlags(root.Flags(), vip, false) + deployment.AttachFlags(root.Flags(), dflags, false) return root } diff --git a/coderd/coderd.go b/coderd/coderd.go index 1aed8417b0ba8..75263926d329c 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -92,7 +92,7 @@ type Options struct { MetricsCacheRefreshInterval time.Duration AgentStatsRefreshInterval time.Duration Experimental bool - DeploymentConfig *codersdk.DeploymentConfig + DeploymentFlags *codersdk.DeploymentFlags } // New constructs a Coder API handler. @@ -286,9 +286,9 @@ func New(options *Options) *API { }) }) }) - r.Route("/config", func(r chi.Router) { + r.Route("/flags", func(r chi.Router) { r.Use(apiKeyMiddleware) - r.Get("/deployment", api.deploymentConfig) + r.Get("/deployment", api.deploymentFlags) }) r.Route("/audit", func(r chi.Router) { r.Use( diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index 4a4198e43c45b..05e3d6a27d2d4 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -91,7 +91,7 @@ type Options struct { IncludeProvisionerDaemon bool MetricsCacheRefreshInterval time.Duration AgentStatsRefreshInterval time.Duration - DeploymentConfig *codersdk.DeploymentConfig + DeploymentFlags *codersdk.DeploymentFlags // Overriding the database is heavily discouraged. // It should only be used in cases where multiple Coder @@ -268,7 +268,7 @@ func NewOptions(t *testing.T, options *Options) (func(http.Handler), context.Can AutoImportTemplates: options.AutoImportTemplates, MetricsCacheRefreshInterval: options.MetricsCacheRefreshInterval, AgentStatsRefreshInterval: options.AgentStatsRefreshInterval, - DeploymentConfig: options.DeploymentConfig, + DeploymentFlags: options.DeploymentFlags, } } diff --git a/coderd/deploymentconfig.go b/coderd/deploymentconfig.go deleted file mode 100644 index d68332c9089f7..0000000000000 --- a/coderd/deploymentconfig.go +++ /dev/null @@ -1,17 +0,0 @@ -package coderd - -import ( - "net/http" - - "github.com/coder/coder/coderd/httpapi" - "github.com/coder/coder/coderd/rbac" -) - -func (api *API) deploymentConfig(rw http.ResponseWriter, r *http.Request) { - if !api.Authorize(r, rbac.ActionRead, rbac.ResourceDeploymentConfig) { - httpapi.Forbidden(rw) - return - } - - httpapi.Write(r.Context(), rw, http.StatusOK, api.DeploymentConfig) -} diff --git a/coderd/deploymentconfig_test.go b/coderd/deploymentconfig_test.go deleted file mode 100644 index 2f03e655c671d..0000000000000 --- a/coderd/deploymentconfig_test.go +++ /dev/null @@ -1,47 +0,0 @@ -package coderd_test - -import ( - "context" - "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/coderd/coderdtest" - "github.com/coder/coder/testutil" -) - -func TestDeploymentConfig(t *testing.T) { - t.Parallel() - hi := "hi" - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() - vip := deployment.NewViper() - fs := pflag.NewFlagSet("test", pflag.ContinueOnError) - fs.String(config.FlagName, hi, "usage") - cfg, err := deployment.Config(fs, vip) - require.NoError(t, err) - // values should be returned - cfg.AccessURL.Value = hi - // values should not be returned - cfg.OAuth2GithubClientSecret.Value = hi - cfg.OIDCClientSecret.Value = hi - cfg.PostgresURL.Value = hi - cfg.SCIMAPIKey.Value = hi - - client := coderdtest.New(t, &coderdtest.Options{ - DeploymentConfig: &cfg, - }) - _ = coderdtest.CreateFirstUser(t, client) - scrubbed, err := client.DeploymentConfig(ctx) - require.NoError(t, err) - // ensure normal values pass through - require.EqualValues(t, hi, scrubbed.AccessURL.Value) - // ensure secrets are removed - require.Empty(t, scrubbed.OAuth2GithubClientSecret.Value) - require.Empty(t, scrubbed.OIDCClientSecret.Value) - require.Empty(t, scrubbed.PostgresURL.Value) - require.Empty(t, scrubbed.SCIMAPIKey.Value) -} diff --git a/coderd/flags.go b/coderd/flags.go new file mode 100644 index 0000000000000..7e2f1376df116 --- /dev/null +++ b/coderd/flags.go @@ -0,0 +1,18 @@ +package coderd + +import ( + "net/http" + + "github.com/coder/coder/cli/deployment" + "github.com/coder/coder/coderd/httpapi" + "github.com/coder/coder/coderd/rbac" +) + +func (api *API) deploymentFlags(rw http.ResponseWriter, r *http.Request) { + if !api.Authorize(r, rbac.ActionRead, rbac.ResourceDeploymentFlags) { + httpapi.Forbidden(rw) + return + } + + httpapi.Write(r.Context(), rw, http.StatusOK, deployment.RemoveSensitiveValues(*api.DeploymentFlags)) +} diff --git a/coderd/flags_test.go b/coderd/flags_test.go new file mode 100644 index 0000000000000..dbff5992385d4 --- /dev/null +++ b/coderd/flags_test.go @@ -0,0 +1,47 @@ +package coderd_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/coder/coder/cli/deployment" + "github.com/coder/coder/coderd/coderdtest" + "github.com/coder/coder/testutil" +) + +const ( + secretValue = "********" +) + +func TestDeploymentFlagSecrets(t *testing.T) { + t.Parallel() + hi := "hi" + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + df := deployment.Flags() + // check if copy works for non-secret values + df.AccessURL.Value = hi + // check if secrets are removed + df.OAuth2GithubClientSecret.Value = hi + df.OIDCClientSecret.Value = hi + df.PostgresURL.Value = hi + df.SCIMAuthHeader.Value = hi + + client := coderdtest.New(t, &coderdtest.Options{ + DeploymentFlags: df, + }) + _ = coderdtest.CreateFirstUser(t, client) + scrubbed, err := client.DeploymentFlags(ctx) + require.NoError(t, err) + // ensure df is unchanged + require.EqualValues(t, hi, df.OAuth2GithubClientSecret.Value) + // ensure normal values pass through + require.EqualValues(t, hi, scrubbed.AccessURL.Value) + // ensure secrets are removed + require.EqualValues(t, secretValue, scrubbed.OAuth2GithubClientSecret.Value) + require.EqualValues(t, secretValue, scrubbed.OIDCClientSecret.Value) + require.EqualValues(t, secretValue, scrubbed.PostgresURL.Value) + require.EqualValues(t, secretValue, scrubbed.SCIMAuthHeader.Value) +} diff --git a/coderd/rbac/object.go b/coderd/rbac/object.go index 4852a4c269638..1a8861c984ce9 100644 --- a/coderd/rbac/object.go +++ b/coderd/rbac/object.go @@ -142,9 +142,9 @@ var ( Type: "license", } - // ResourceDeploymentConfig - ResourceDeploymentConfig = Object{ - Type: "deployment_config", + // ResourceDeploymentFlags + ResourceDeploymentFlags = Object{ + Type: "deployment_flags", } ResourceReplicas = Object{ diff --git a/codersdk/deploymentconfig.go b/codersdk/deploymentconfig.go deleted file mode 100644 index 174152e4a29f2..0000000000000 --- a/codersdk/deploymentconfig.go +++ /dev/null @@ -1,97 +0,0 @@ -package codersdk - -import ( - "context" - "encoding/json" - "net/http" - "time" - - "golang.org/x/xerrors" -) - -// DeploymentConfig is the central configuration for the coder server. -// Secret values should specify `json:"-"` to prevent them from being returned by the API. -type DeploymentConfig struct { - AccessURL DeploymentConfigField[string] `json:"access_url"` - WildcardAccessURL DeploymentConfigField[string] `json:"wildcard_access_url"` - Address DeploymentConfigField[string] `json:"address"` - AutobuildPollInterval DeploymentConfigField[time.Duration] `json:"autobuild_poll_interval"` - DERPServerEnable DeploymentConfigField[bool] `json:"derp_server_enabled"` - DERPServerRegionID DeploymentConfigField[int] `json:"derp_server_region_id"` - DERPServerRegionCode DeploymentConfigField[string] `json:"derp_server_region_code"` - DERPServerRegionName DeploymentConfigField[string] `json:"derp_server_region_name"` - DERPServerSTUNAddresses DeploymentConfigField[[]string] `json:"derp_server_stun_address"` - DERPServerRelayAddress DeploymentConfigField[string] `json:"derp_server_relay_address"` - DERPConfigURL DeploymentConfigField[string] `json:"derp_config_url"` - DERPConfigPath DeploymentConfigField[string] `json:"derp_config_path"` - PrometheusEnable DeploymentConfigField[bool] `json:"prometheus_enabled"` - PrometheusAddress DeploymentConfigField[string] `json:"prometheus_address"` - PprofEnable DeploymentConfigField[bool] `json:"pprof_enabled"` - PprofAddress DeploymentConfigField[string] `json:"pprof_address"` - CacheDirectory DeploymentConfigField[string] `json:"cache_directory"` - InMemoryDatabase DeploymentConfigField[bool] `json:"in_memory_database"` - ProvisionerDaemons DeploymentConfigField[int] `json:"provisioner_daemon_count"` - PostgresURL DeploymentConfigField[string] `json:"-"` - OAuth2GithubClientID DeploymentConfigField[string] `json:"oauth2_github_client_id"` - OAuth2GithubClientSecret DeploymentConfigField[string] `json:"-"` - OAuth2GithubAllowedOrganizations DeploymentConfigField[[]string] `json:"oauth2_github_allowed_organizations"` - OAuth2GithubAllowedTeams DeploymentConfigField[[]string] `json:"oauth2_github_allowed_teams"` - OAuth2GithubAllowSignups DeploymentConfigField[bool] `json:"oauth2_github_allow_signups"` - OAuth2GithubEnterpriseBaseURL DeploymentConfigField[string] `json:"oauth2_github_enterprise_base_url"` - OIDCAllowSignups DeploymentConfigField[bool] `json:"oidc_allow_signups"` - OIDCClientID DeploymentConfigField[string] `json:"oidc_client_id"` - OIDCClientSecret DeploymentConfigField[string] `json:"-"` - OIDCEmailDomain DeploymentConfigField[string] `json:"oidc_email_domain"` - OIDCIssuerURL DeploymentConfigField[string] `json:"oidc_issuer_url"` - OIDCScopes DeploymentConfigField[[]string] `json:"oidc_scopes"` - TelemetryEnable DeploymentConfigField[bool] `json:"telemetry_enable"` - TelemetryTrace DeploymentConfigField[bool] `json:"telemetry_trace_enable"` - TelemetryURL DeploymentConfigField[string] `json:"telemetry_url"` - TLSEnable DeploymentConfigField[bool] `json:"tls_enable"` - TLSCertFiles DeploymentConfigField[[]string] `json:"tls_cert_files"` - TLSClientCAFile DeploymentConfigField[string] `json:"tls_client_ca_file"` - TLSClientAuth DeploymentConfigField[string] `json:"tls_client_auth"` - TLSKeyFiles DeploymentConfigField[[]string] `json:"tls_key_files"` - TLSMinVersion DeploymentConfigField[string] `json:"tls_min_version"` - TraceEnable DeploymentConfigField[bool] `json:"trace_enable"` - SecureAuthCookie DeploymentConfigField[bool] `json:"secure_auth_cookie"` - SSHKeygenAlgorithm DeploymentConfigField[string] `json:"ssh_keygen_algorithm"` - AutoImportTemplates DeploymentConfigField[[]string] `json:"auto_import_templates"` - MetricsCacheRefreshInterval DeploymentConfigField[time.Duration] `json:"metrics_cache_refresh_interval"` - AgentStatRefreshInterval DeploymentConfigField[time.Duration] `json:"agent_stat_refresh_interval"` - AuditLogging DeploymentConfigField[bool] `json:"audit_logging"` - BrowserOnly DeploymentConfigField[bool] `json:"browser_only"` - SCIMAPIKey DeploymentConfigField[string] `json:"-"` - UserWorkspaceQuota DeploymentConfigField[int] `json:"user_workspace_quota"` -} - -type Flaggable interface { - string | bool | int | time.Duration | []string -} - -type DeploymentConfigField[T Flaggable] struct { - Key string `json:"key"` - Name string `json:"name"` - Usage string `json:"usage"` - Flag string `json:"flag"` - Shorthand string `json:"shorthand"` - Enterprise bool `json:"enterprise"` - Hidden bool `json:"hidden"` - Value T `json:"value"` -} - -// DeploymentConfig returns the deployment config for the coder server. -func (c *Client) DeploymentConfig(ctx context.Context) (DeploymentConfig, error) { - res, err := c.Request(ctx, http.MethodGet, "/api/v2/config/deployment", nil) - if err != nil { - return DeploymentConfig{}, xerrors.Errorf("execute request: %w", err) - } - defer res.Body.Close() - - if res.StatusCode != http.StatusOK { - return DeploymentConfig{}, readBodyAsError(res) - } - - var df DeploymentConfig - return df, json.NewDecoder(res.Body).Decode(&df) -} diff --git a/codersdk/flags.go b/codersdk/flags.go new file mode 100644 index 0000000000000..bf407760bbfb8 --- /dev/null +++ b/codersdk/flags.go @@ -0,0 +1,142 @@ +package codersdk + +import ( + "context" + "encoding/json" + "net/http" + "time" + + "golang.org/x/xerrors" +) + +type DeploymentFlags struct { + AccessURL *StringFlag `json:"access_url" typescript:",notnull"` + WildcardAccessURL *StringFlag `json:"wildcard_access_url" typescript:",notnull"` + Address *StringFlag `json:"address" typescript:",notnull"` + AutobuildPollInterval *DurationFlag `json:"autobuild_poll_interval" typescript:",notnull"` + DerpServerEnable *BoolFlag `json:"derp_server_enabled" typescript:",notnull"` + DerpServerRegionID *IntFlag `json:"derp_server_region_id" typescript:",notnull"` + DerpServerRegionCode *StringFlag `json:"derp_server_region_code" typescript:",notnull"` + DerpServerRegionName *StringFlag `json:"derp_server_region_name" typescript:",notnull"` + DerpServerSTUNAddresses *StringArrayFlag `json:"derp_server_stun_address" typescript:",notnull"` + DerpServerRelayAddress *StringFlag `json:"derp_server_relay_address" typescript:",notnull"` + DerpConfigURL *StringFlag `json:"derp_config_url" typescript:",notnull"` + DerpConfigPath *StringFlag `json:"derp_config_path" typescript:",notnull"` + PromEnabled *BoolFlag `json:"prom_enabled" typescript:",notnull"` + PromAddress *StringFlag `json:"prom_address" typescript:",notnull"` + PprofEnabled *BoolFlag `json:"pprof_enabled" typescript:",notnull"` + PprofAddress *StringFlag `json:"pprof_address" typescript:",notnull"` + CacheDir *StringFlag `json:"cache_dir" typescript:",notnull"` + InMemoryDatabase *BoolFlag `json:"in_memory_database" typescript:",notnull"` + ProvisionerDaemonCount *IntFlag `json:"provisioner_daemon_count" typescript:",notnull"` + PostgresURL *StringFlag `json:"postgres_url" typescript:",notnull"` + OAuth2GithubClientID *StringFlag `json:"oauth2_github_client_id" typescript:",notnull"` + OAuth2GithubClientSecret *StringFlag `json:"oauth2_github_client_secret" typescript:",notnull"` + OAuth2GithubAllowedOrganizations *StringArrayFlag `json:"oauth2_github_allowed_organizations" typescript:",notnull"` + OAuth2GithubAllowedTeams *StringArrayFlag `json:"oauth2_github_allowed_teams" typescript:",notnull"` + OAuth2GithubAllowSignups *BoolFlag `json:"oauth2_github_allow_signups" typescript:",notnull"` + OAuth2GithubEnterpriseBaseURL *StringFlag `json:"oauth2_github_enterprise_base_url" typescript:",notnull"` + OIDCAllowSignups *BoolFlag `json:"oidc_allow_signups" typescript:",notnull"` + OIDCClientID *StringFlag `json:"oidc_client_id" typescript:",notnull"` + OIDCClientSecret *StringFlag `json:"oidc_client_secret" typescript:",notnull"` + OIDCEmailDomain *StringFlag `json:"oidc_email_domain" typescript:",notnull"` + OIDCIssuerURL *StringFlag `json:"oidc_issuer_url" typescript:",notnull"` + OIDCScopes *StringArrayFlag `json:"oidc_scopes" typescript:",notnull"` + TelemetryEnable *BoolFlag `json:"telemetry_enable" typescript:",notnull"` + TelemetryTraceEnable *BoolFlag `json:"telemetry_trace_enable" typescript:",notnull"` + TelemetryURL *StringFlag `json:"telemetry_url" typescript:",notnull"` + TLSEnable *BoolFlag `json:"tls_enable" typescript:",notnull"` + TLSCertFiles *StringArrayFlag `json:"tls_cert_files" typescript:",notnull"` + TLSClientCAFile *StringFlag `json:"tls_client_ca_file" typescript:",notnull"` + TLSClientAuth *StringFlag `json:"tls_client_auth" typescript:",notnull"` + TLSKeyFiles *StringArrayFlag `json:"tls_key_files" typescript:",notnull"` + TLSMinVersion *StringFlag `json:"tls_min_version" typescript:",notnull"` + TraceEnable *BoolFlag `json:"trace_enable" typescript:",notnull"` + SecureAuthCookie *BoolFlag `json:"secure_auth_cookie" typescript:",notnull"` + SSHKeygenAlgorithm *StringFlag `json:"ssh_keygen_algorithm" typescript:",notnull"` + AutoImportTemplates *StringArrayFlag `json:"auto_import_templates" typescript:",notnull"` + MetricsCacheRefreshInterval *DurationFlag `json:"metrics_cache_refresh_interval" typescript:",notnull"` + AgentStatRefreshInterval *DurationFlag `json:"agent_stat_refresh_interval" typescript:",notnull"` + Verbose *BoolFlag `json:"verbose" typescript:",notnull"` + AuditLogging *BoolFlag `json:"audit_logging" typescript:",notnull"` + BrowserOnly *BoolFlag `json:"browser_only" typescript:",notnull"` + SCIMAuthHeader *StringFlag `json:"scim_auth_header" typescript:",notnull"` + UserWorkspaceQuota *IntFlag `json:"user_workspace_quota" typescript:",notnull"` +} + +type StringFlag struct { + Name string `json:"name"` + Flag string `json:"flag"` + EnvVar string `json:"env_var"` + Shorthand string `json:"shorthand"` + Description string `json:"description"` + Enterprise bool `json:"enterprise"` + Secret bool `json:"secret"` + Hidden bool `json:"hidden"` + Default string `json:"default"` + Value string `json:"value"` +} + +type BoolFlag struct { + Name string `json:"name"` + Flag string `json:"flag"` + EnvVar string `json:"env_var"` + Shorthand string `json:"shorthand"` + Description string `json:"description"` + Enterprise bool `json:"enterprise"` + Hidden bool `json:"hidden"` + Default bool `json:"default"` + Value bool `json:"value"` +} + +type IntFlag struct { + Name string `json:"name"` + Flag string `json:"flag"` + EnvVar string `json:"env_var"` + Shorthand string `json:"shorthand"` + Description string `json:"description"` + Enterprise bool `json:"enterprise"` + Hidden bool `json:"hidden"` + Default int `json:"default"` + Value int `json:"value"` +} + +type DurationFlag struct { + Name string `json:"name"` + Flag string `json:"flag"` + EnvVar string `json:"env_var"` + Shorthand string `json:"shorthand"` + Description string `json:"description"` + Enterprise bool `json:"enterprise"` + Hidden bool `json:"hidden"` + Default time.Duration `json:"default"` + Value time.Duration `json:"value"` +} + +type StringArrayFlag struct { + Name string `json:"name"` + Flag string `json:"flag"` + EnvVar string `json:"env_var"` + Shorthand string `json:"shorthand"` + Description string `json:"description"` + Enterprise bool `json:"enterprise"` + Hidden bool `json:"hidden"` + Default []string `json:"default"` + Value []string `json:"value"` +} + +// DeploymentFlags returns the deployment level flags for the coder server. +func (c *Client) DeploymentFlags(ctx context.Context) (DeploymentFlags, error) { + res, err := c.Request(ctx, http.MethodGet, "/api/v2/flags/deployment", nil) + if err != nil { + return DeploymentFlags{}, xerrors.Errorf("execute request: %w", err) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return DeploymentFlags{}, readBodyAsError(res) + } + + var df DeploymentFlags + return df, json.NewDecoder(res.Body).Decode(&df) +} diff --git a/docs/install/kubernetes.md b/docs/install/kubernetes.md index 81958e964878c..ef5d5fb9e90d2 100644 --- a/docs/install/kubernetes.md +++ b/docs/install/kubernetes.md @@ -102,7 +102,7 @@ to log in and manage templates. # This env variable controls whether or not to auto-import the # "kubernetes" template on first startup. This will not work unless # coder.serviceAccount.workspacePerms is true. - - name: CODER_AUTO_IMPORT_TEMPLATES + - name: CODER_TEMPLATE_AUTOIMPORT value: "kubernetes" #tls: diff --git a/enterprise/cli/server.go b/enterprise/cli/server.go index cd04a3be93972..9459cc6906a84 100644 --- a/enterprise/cli/server.go +++ b/enterprise/cli/server.go @@ -24,10 +24,10 @@ import ( ) func server() *cobra.Command { - vip := deployment.NewViper() - cmd := agpl.Server(vip, func(ctx context.Context, options *agplcoderd.Options) (*agplcoderd.API, io.Closer, error) { - if options.DeploymentConfig.DERPServerRelayAddress.Value != "" { - _, err := url.Parse(options.DeploymentConfig.DERPServerRelayAddress.Value) + dflags := deployment.Flags() + cmd := agpl.Server(dflags, func(ctx context.Context, options *agplcoderd.Options) (*agplcoderd.API, io.Closer, error) { + if dflags.DerpServerRelayAddress.Value != "" { + _, err := url.Parse(dflags.DerpServerRelayAddress.Value) if err != nil { return nil, nil, xerrors.Errorf("derp-server-relay-address must be a valid HTTP URL: %w", err) } @@ -50,7 +50,7 @@ func server() *cobra.Command { } options.DERPServer.SetMeshKey(meshKey) - if options.DeploymentConfig.AuditLogging.Value { + if dflags.AuditLogging.Value { options.Auditor = audit.NewAuditor(audit.DefaultFilter, backends.NewPostgres(options.Database, true), backends.NewSlog(options.Logger), @@ -58,13 +58,13 @@ func server() *cobra.Command { } o := &coderd.Options{ - AuditLogging: options.DeploymentConfig.AuditLogging.Value, - BrowserOnly: options.DeploymentConfig.BrowserOnly.Value, - SCIMAPIKey: []byte(options.DeploymentConfig.SCIMAPIKey.Value), - UserWorkspaceQuota: options.DeploymentConfig.UserWorkspaceQuota.Value, + AuditLogging: dflags.AuditLogging.Value, + BrowserOnly: dflags.BrowserOnly.Value, + SCIMAPIKey: []byte(dflags.SCIMAuthHeader.Value), + UserWorkspaceQuota: dflags.UserWorkspaceQuota.Value, RBAC: true, - DERPServerRelayAddress: options.DeploymentConfig.DERPServerRelayAddress.Value, - DERPServerRegionID: options.DeploymentConfig.DERPServerRegionID.Value, + DERPServerRelayAddress: dflags.DerpServerRelayAddress.Value, + DERPServerRegionID: dflags.DerpServerRegionID.Value, Options: options, } @@ -76,7 +76,6 @@ func server() *cobra.Command { return api.AGPL, api, nil }) - deployment.AttachFlags(cmd.Flags(), vip, true) - + deployment.AttachFlags(cmd.Flags(), dflags, true) return cmd } diff --git a/go.mod b/go.mod index 0030ac753610d..caf155fffb690 100644 --- a/go.mod +++ b/go.mod @@ -166,15 +166,6 @@ require github.com/jmoiron/sqlx v1.3.5 require github.com/cakturk/go-netstat v0.0.0-20200220111822-e5b49efee7a5 -require ( - github.com/fsnotify/fsnotify v1.5.4 // indirect - github.com/magiconair/properties v1.8.6 // indirect - github.com/pelletier/go-toml v1.9.5 // indirect - github.com/spf13/viper v1.13.0 // indirect - github.com/subosito/gotenv v1.4.1 // indirect - gopkg.in/ini.v1 v1.67.0 // indirect -) - require ( filippo.io/edwards25519 v1.0.0-rc.1 // indirect github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect @@ -261,7 +252,7 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.0.3-0.20220114050600-8b9d41f48198 // indirect github.com/opencontainers/runc v1.1.2 // indirect - github.com/pelletier/go-toml/v2 v2.0.5 // indirect + github.com/pelletier/go-toml/v2 v2.0.4 // indirect github.com/pion/transport v0.13.1 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect diff --git a/go.sum b/go.sum index bee1e8e048568..4ffdf1a75d2bd 100644 --- a/go.sum +++ b/go.sum @@ -594,8 +594,6 @@ github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= -github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= -github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/fsouza/fake-gcs-server v1.17.0/go.mod h1:D1rTE4YCyHFNa99oyJJ5HyclvN/0uQR+pM/VdlL83bw= github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa h1:RDBNVkRviHZtvDvId8XSGPu3rmpmSe+wKRcEWNgsfWU= github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA= @@ -1234,8 +1232,6 @@ github.com/mafredri/udp v0.1.2-0.20220805105907-b2872e92e98d/go.mod h1:GUd681aT3 github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= -github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= -github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= @@ -1489,12 +1485,8 @@ github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAv github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= -github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.0.4 h1:MHHO+ZUPwPZQ6BmnnT81iQg5cuurp78CRH7rNsguSMk= github.com/pelletier/go-toml/v2 v2.0.4/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas= -github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg= -github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/phayes/checkstyle v0.0.0-20170904204023-bfd46e6a821d/go.mod h1:3OzsM7FXDQlpCiw2j81fOmAwQLnZnLGXVKUzeKQXIAw= github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= @@ -1686,8 +1678,6 @@ github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/y github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= github.com/spf13/viper v1.9.0/go.mod h1:+i6ajR7OX2XaiBkrcZJFK21htRk7eDeLg7+O6bhUPP4= -github.com/spf13/viper v1.13.0 h1:BWSJ/M+f+3nmdz9bxB+bWX28kkALN2ok11D0rSo8EJU= -github.com/spf13/viper v1.13.0/go.mod h1:Icm2xNL3/8uyh/wFuB1jI7TiTNKp8632Nwegu+zgdYw= github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I= github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= @@ -1711,8 +1701,6 @@ github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= -github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/swaggest/assertjson v1.7.0 h1:SKw5Rn0LQs6UvmGrIdaKQbMR1R3ncXm5KNon+QJ7jtw= github.com/sylvia7788/contextcheck v1.0.4/go.mod h1:vuPKJMQ7MQ91ZTqfdyreNKwZjyUg6KO+IebVyQDedZQ= github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= @@ -2718,8 +2706,6 @@ gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.63.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= diff --git a/helm/README.md b/helm/README.md index 95b4ecffe3ed8..0fa51303c71a4 100644 --- a/helm/README.md +++ b/helm/README.md @@ -47,10 +47,10 @@ coder: # This env variable controls whether or not to auto-import the "kubernetes" # template on first startup. This will not work unless # coder.serviceAccount.workspacePerms is true. - - name: CODER_AUTO_IMPORT_TEMPLATES + - name: CODER_TEMPLATE_AUTOIMPORT value: "kubernetes" tls: - secretNames: + secretNames: - my-tls-secret-name ``` diff --git a/scripts/apitypings/main.go b/scripts/apitypings/main.go index 20ff1ba1f8250..720a828f100bf 100644 --- a/scripts/apitypings/main.go +++ b/scripts/apitypings/main.go @@ -441,10 +441,6 @@ func (g *Generator) buildStruct(obj types.Object, st *types.Struct) (string, err jsonOptional bool ) if err == nil { - if jsonTag.Name == "-" { - // Completely ignore this field. - continue - } jsonName = jsonTag.Name if len(jsonTag.Options) > 0 && jsonTag.Options[0] == "omitempty" { jsonOptional = true diff --git a/site/src/AppRouter.tsx b/site/src/AppRouter.tsx index feecf7dd6ab72..c0759aecafdaf 100644 --- a/site/src/AppRouter.tsx +++ b/site/src/AppRouter.tsx @@ -256,7 +256,7 @@ export const AppRouter: FC = () => { element={ @@ -270,7 +270,7 @@ export const AppRouter: FC = () => { element={ @@ -284,7 +284,7 @@ export const AppRouter: FC = () => { element={ @@ -298,7 +298,7 @@ export const AppRouter: FC = () => { element={ diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 8bb9303f97fa2..ded0d3d3c89ba 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -667,9 +667,9 @@ export const getAgentListeningPorts = async ( return response.data } -export const getDeploymentConfig = - async (): Promise => { - const response = await axios.get(`/api/v2/config/deployment`) +export const getDeploymentFlags = + async (): Promise => { + const response = await axios.get(`/api/v2/flags/deployment`) return response.data } diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 0729f24e784f8..c8540c31888a9 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -126,6 +126,19 @@ export interface AzureInstanceIdentityToken { readonly encoding: string } +// From codersdk/flags.go +export interface BoolFlag { + readonly name: string + readonly flag: string + readonly env_var: string + readonly shorthand: string + readonly description: string + readonly enterprise: boolean + readonly hidden: boolean + readonly default: boolean + readonly value: boolean +} + // From codersdk/buildinfo.go export interface BuildInfoResponse { readonly external_url: string @@ -251,67 +264,75 @@ export interface DERPRegion { readonly latency_ms: number } -// From codersdk/deploymentconfig.go -export interface DeploymentConfig { - readonly access_url: DeploymentConfigField - readonly wildcard_access_url: DeploymentConfigField - readonly address: DeploymentConfigField - readonly autobuild_poll_interval: DeploymentConfigField - readonly derp_server_enabled: DeploymentConfigField - readonly derp_server_region_id: DeploymentConfigField - readonly derp_server_region_code: DeploymentConfigField - readonly derp_server_region_name: DeploymentConfigField - readonly derp_server_stun_address: DeploymentConfigField - readonly derp_server_relay_address: DeploymentConfigField - readonly derp_config_url: DeploymentConfigField - readonly derp_config_path: DeploymentConfigField - readonly prometheus_enabled: DeploymentConfigField - readonly prometheus_address: DeploymentConfigField - readonly pprof_enabled: DeploymentConfigField - readonly pprof_address: DeploymentConfigField - readonly cache_directory: DeploymentConfigField - readonly in_memory_database: DeploymentConfigField - readonly provisioner_daemon_count: DeploymentConfigField - readonly oauth2_github_client_id: DeploymentConfigField - readonly oauth2_github_allowed_organizations: DeploymentConfigField - readonly oauth2_github_allowed_teams: DeploymentConfigField - readonly oauth2_github_allow_signups: DeploymentConfigField - readonly oauth2_github_enterprise_base_url: DeploymentConfigField - readonly oidc_allow_signups: DeploymentConfigField - readonly oidc_client_id: DeploymentConfigField - readonly oidc_email_domain: DeploymentConfigField - readonly oidc_issuer_url: DeploymentConfigField - readonly oidc_scopes: DeploymentConfigField - readonly telemetry_enable: DeploymentConfigField - readonly telemetry_trace_enable: DeploymentConfigField - readonly telemetry_url: DeploymentConfigField - readonly tls_enable: DeploymentConfigField - readonly tls_cert_files: DeploymentConfigField - readonly tls_client_ca_file: DeploymentConfigField - readonly tls_client_auth: DeploymentConfigField - readonly tls_key_files: DeploymentConfigField - readonly tls_min_version: DeploymentConfigField - readonly trace_enable: DeploymentConfigField - readonly secure_auth_cookie: DeploymentConfigField - readonly ssh_keygen_algorithm: DeploymentConfigField - readonly auto_import_templates: DeploymentConfigField - readonly metrics_cache_refresh_interval: DeploymentConfigField - readonly agent_stat_refresh_interval: DeploymentConfigField - readonly audit_logging: DeploymentConfigField - readonly browser_only: DeploymentConfigField - readonly user_workspace_quota: DeploymentConfigField -} - -// From codersdk/deploymentconfig.go -export interface DeploymentConfigField { - readonly key: string +// From codersdk/flags.go +export interface DeploymentFlags { + readonly access_url: StringFlag + readonly wildcard_access_url: StringFlag + readonly address: StringFlag + readonly autobuild_poll_interval: DurationFlag + readonly derp_server_enabled: BoolFlag + readonly derp_server_region_id: IntFlag + readonly derp_server_region_code: StringFlag + readonly derp_server_region_name: StringFlag + readonly derp_server_stun_address: StringArrayFlag + readonly derp_server_relay_address: StringFlag + readonly derp_config_url: StringFlag + readonly derp_config_path: StringFlag + readonly prom_enabled: BoolFlag + readonly prom_address: StringFlag + readonly pprof_enabled: BoolFlag + readonly pprof_address: StringFlag + readonly cache_dir: StringFlag + readonly in_memory_database: BoolFlag + readonly provisioner_daemon_count: IntFlag + readonly postgres_url: StringFlag + readonly oauth2_github_client_id: StringFlag + readonly oauth2_github_client_secret: StringFlag + readonly oauth2_github_allowed_organizations: StringArrayFlag + readonly oauth2_github_allowed_teams: StringArrayFlag + readonly oauth2_github_allow_signups: BoolFlag + readonly oauth2_github_enterprise_base_url: StringFlag + readonly oidc_allow_signups: BoolFlag + readonly oidc_client_id: StringFlag + readonly oidc_client_secret: StringFlag + readonly oidc_email_domain: StringFlag + readonly oidc_issuer_url: StringFlag + readonly oidc_scopes: StringArrayFlag + readonly telemetry_enable: BoolFlag + readonly telemetry_trace_enable: BoolFlag + readonly telemetry_url: StringFlag + readonly tls_enable: BoolFlag + readonly tls_cert_files: StringArrayFlag + readonly tls_client_ca_file: StringFlag + readonly tls_client_auth: StringFlag + readonly tls_key_files: StringArrayFlag + readonly tls_min_version: StringFlag + readonly trace_enable: BoolFlag + readonly secure_auth_cookie: BoolFlag + readonly ssh_keygen_algorithm: StringFlag + readonly auto_import_templates: StringArrayFlag + readonly metrics_cache_refresh_interval: DurationFlag + readonly agent_stat_refresh_interval: DurationFlag + readonly verbose: BoolFlag + readonly audit_logging: BoolFlag + readonly browser_only: BoolFlag + readonly scim_auth_header: StringFlag + readonly user_workspace_quota: IntFlag +} + +// From codersdk/flags.go +export interface DurationFlag { readonly name: string - readonly usage: string readonly flag: string + readonly env_var: string readonly shorthand: string + readonly description: string readonly enterprise: boolean readonly hidden: boolean - readonly value: T + // This is likely an enum in an external package ("time.Duration") + readonly default: number + // This is likely an enum in an external package ("time.Duration") + readonly value: number } // From codersdk/features.go @@ -366,6 +387,19 @@ export interface Healthcheck { readonly threshold: number } +// From codersdk/flags.go +export interface IntFlag { + readonly name: string + readonly flag: string + readonly env_var: string + readonly shorthand: string + readonly description: string + readonly enterprise: boolean + readonly hidden: boolean + readonly default: number + readonly value: number +} + // From codersdk/licenses.go export interface License { readonly id: number @@ -530,6 +564,33 @@ export interface ServerSentEvent { readonly data: any } +// From codersdk/flags.go +export interface StringArrayFlag { + readonly name: string + readonly flag: string + readonly env_var: string + readonly shorthand: string + readonly description: string + readonly enterprise: boolean + readonly hidden: boolean + readonly default: string[] + readonly value: string[] +} + +// From codersdk/flags.go +export interface StringFlag { + readonly name: string + readonly flag: string + readonly env_var: string + readonly shorthand: string + readonly description: string + readonly enterprise: boolean + readonly secret: boolean + readonly hidden: boolean + readonly default: string + readonly value: string +} + // From codersdk/templates.go export interface Template { readonly id: string @@ -938,6 +999,3 @@ export type WorkspaceStatus = // From codersdk/workspacebuilds.go export type WorkspaceTransition = "delete" | "start" | "stop" - -// From codersdk/deploymentconfig.go -export type Flaggable = string | boolean | number | string[] diff --git a/site/src/components/DeploySettingsLayout/DeploySettingsLayout.tsx b/site/src/components/DeploySettingsLayout/DeploySettingsLayout.tsx index f9294d4108f7d..b6abb27841f83 100644 --- a/site/src/components/DeploySettingsLayout/DeploySettingsLayout.tsx +++ b/site/src/components/DeploySettingsLayout/DeploySettingsLayout.tsx @@ -11,9 +11,9 @@ import React, { import { useActor } from "@xstate/react" import { XServiceContext } from "xServices/StateContext" import { Loader } from "components/Loader/Loader" -import { DeploymentConfig } from "api/typesGenerated" +import { DeploymentFlags } from "api/typesGenerated" -type DeploySettingsContextValue = { deploymentConfig: DeploymentConfig } +type DeploySettingsContextValue = { deploymentFlags: DeploymentFlags } const DeploySettingsContext = createContext< DeploySettingsContextValue | undefined @@ -33,9 +33,9 @@ export const DeploySettingsLayout: React.FC = ({ children, }) => { const xServices = useContext(XServiceContext) - const [state, send] = useActor(xServices.deploymentConfigXService) + const [state, send] = useActor(xServices.deploymentFlagsXService) const styles = useStyles() - const { deploymentConfig } = state.context + const { deploymentFlags } = state.context useEffect(() => { if (state.matches("idle")) { @@ -48,10 +48,8 @@ export const DeploySettingsLayout: React.FC = ({
- {deploymentConfig ? ( - + {deploymentFlags ? ( + {children} ) : ( diff --git a/site/src/components/DeploySettingsLayout/OptionsTable.tsx b/site/src/components/DeploySettingsLayout/OptionsTable.tsx index 8b7fbd111d110..d54c614b48949 100644 --- a/site/src/components/DeploySettingsLayout/OptionsTable.tsx +++ b/site/src/components/DeploySettingsLayout/OptionsTable.tsx @@ -5,7 +5,7 @@ import TableCell from "@material-ui/core/TableCell" import TableContainer from "@material-ui/core/TableContainer" import TableHead from "@material-ui/core/TableHead" import TableRow from "@material-ui/core/TableRow" -import { DeploymentConfig } from "api/typesGenerated" +import { DeploymentFlags } from "api/typesGenerated" import { OptionDescription, OptionName, @@ -13,7 +13,7 @@ import { } from "components/DeploySettingsLayout/Option" import React from "react" -const OptionsTable: React.FC<{ options: Partial }> = ({ +const OptionsTable: React.FC<{ options: Partial }> = ({ options, }) => { const styles = useStyles() diff --git a/site/src/components/Navbar/Navbar.tsx b/site/src/components/Navbar/Navbar.tsx index 56d763b4a66f1..95355e5a1dbe7 100644 --- a/site/src/components/Navbar/Navbar.tsx +++ b/site/src/components/Navbar/Navbar.tsx @@ -22,7 +22,7 @@ export const Navbar: React.FC = () => { featureVisibility[FeatureNames.AuditLog] && Boolean(permissions?.viewAuditLog) const canViewDeployment = - experimental && Boolean(permissions?.viewDeploymentConfig) + experimental && Boolean(permissions?.viewDeploymentFlags) const onSignOut = () => authSend("SIGN_OUT") return ( diff --git a/site/src/pages/DeploySettingsPage/AuthSettingsPage.tsx b/site/src/pages/DeploySettingsPage/AuthSettingsPage.tsx index 150c0fb006afb..008f9f25cbe22 100644 --- a/site/src/pages/DeploySettingsPage/AuthSettingsPage.tsx +++ b/site/src/pages/DeploySettingsPage/AuthSettingsPage.tsx @@ -12,7 +12,7 @@ import { Helmet } from "react-helmet-async" import { pageTitle } from "util/page" const AuthSettingsPage: React.FC = () => { - const { deploymentConfig: deploymentConfig } = useDeploySettings() + const { deploymentFlags } = useDeploySettings() return ( <> @@ -32,7 +32,7 @@ const AuthSettingsPage: React.FC = () => { /> - {deploymentConfig.oidc_client_id.value ? ( + {deploymentFlags.oidc_client_id.value ? ( ) : ( @@ -41,12 +41,12 @@ const AuthSettingsPage: React.FC = () => { @@ -60,7 +60,7 @@ const AuthSettingsPage: React.FC = () => { /> - {deploymentConfig.oauth2_github_client_id.value ? ( + {deploymentFlags.oauth2_github_client_id.value ? ( ) : ( @@ -69,17 +69,17 @@ const AuthSettingsPage: React.FC = () => { diff --git a/site/src/pages/DeploySettingsPage/GeneralSettingsPage.tsx b/site/src/pages/DeploySettingsPage/GeneralSettingsPage.tsx index 59fd23e7a3308..ebf523fb0e5a7 100644 --- a/site/src/pages/DeploySettingsPage/GeneralSettingsPage.tsx +++ b/site/src/pages/DeploySettingsPage/GeneralSettingsPage.tsx @@ -6,7 +6,7 @@ import { Helmet } from "react-helmet-async" import { pageTitle } from "util/page" const GeneralSettingsPage: React.FC = () => { - const { deploymentConfig: deploymentConfig } = useDeploySettings() + const { deploymentFlags } = useDeploySettings() return ( <> @@ -22,9 +22,9 @@ const GeneralSettingsPage: React.FC = () => { diff --git a/site/src/pages/DeploySettingsPage/NetworkSettingsPage.tsx b/site/src/pages/DeploySettingsPage/NetworkSettingsPage.tsx index 23e87a284e4b2..7bcf9cdede202 100644 --- a/site/src/pages/DeploySettingsPage/NetworkSettingsPage.tsx +++ b/site/src/pages/DeploySettingsPage/NetworkSettingsPage.tsx @@ -6,7 +6,7 @@ import { Helmet } from "react-helmet-async" import { pageTitle } from "util/page" const NetworkSettingsPage: React.FC = () => { - const { deploymentConfig: deploymentConfig } = useDeploySettings() + const { deploymentFlags } = useDeploySettings() return ( <> @@ -22,10 +22,10 @@ const NetworkSettingsPage: React.FC = () => { diff --git a/site/src/pages/DeploySettingsPage/SecuritySettingsPage.tsx b/site/src/pages/DeploySettingsPage/SecuritySettingsPage.tsx index 0a7e3afdccae9..987c61b93c85f 100644 --- a/site/src/pages/DeploySettingsPage/SecuritySettingsPage.tsx +++ b/site/src/pages/DeploySettingsPage/SecuritySettingsPage.tsx @@ -16,7 +16,7 @@ import { pageTitle } from "util/page" import { XServiceContext } from "xServices/StateContext" const SecuritySettingsPage: React.FC = () => { - const { deploymentConfig: deploymentConfig } = useDeploySettings() + const { deploymentFlags } = useDeploySettings() const xServices = useContext(XServiceContext) const [entitlementsState] = useActor(xServices.entitlementsXService) @@ -34,8 +34,8 @@ const SecuritySettingsPage: React.FC = () => { @@ -89,10 +89,10 @@ const SecuritySettingsPage: React.FC = () => { diff --git a/site/src/xServices/StateContext.tsx b/site/src/xServices/StateContext.tsx index 8df5003aff85e..565ca3023a5b9 100644 --- a/site/src/xServices/StateContext.tsx +++ b/site/src/xServices/StateContext.tsx @@ -3,7 +3,7 @@ import { createContext, FC, ReactNode } from "react" import { ActorRefFrom } from "xstate" import { authMachine } from "./auth/authXService" import { buildInfoMachine } from "./buildInfo/buildInfoXService" -import { deploymentConfigMachine } from "./deploymentConfig/deploymentConfigMachine" +import { deploymentFlagsMachine } from "./deploymentFlags/deploymentFlagsMachine" import { entitlementsMachine } from "./entitlements/entitlementsXService" import { siteRolesMachine } from "./roles/siteRolesXService" @@ -13,7 +13,7 @@ interface XServiceContextType { entitlementsXService: ActorRefFrom siteRolesXService: ActorRefFrom // Since the info here is used by multiple deployment settings page and we don't want to refetch them every time - deploymentConfigXService: ActorRefFrom + deploymentFlagsXService: ActorRefFrom } /** @@ -34,7 +34,7 @@ export const XServiceProvider: FC<{ children: ReactNode }> = ({ children }) => { buildInfoXService: useInterpret(buildInfoMachine), entitlementsXService: useInterpret(entitlementsMachine), siteRolesXService: useInterpret(siteRolesMachine), - deploymentConfigXService: useInterpret(deploymentConfigMachine), + deploymentFlagsXService: useInterpret(deploymentFlagsMachine), }} > {children} diff --git a/site/src/xServices/auth/authXService.ts b/site/src/xServices/auth/authXService.ts index 79439dbc298a4..af25518f8bcaa 100644 --- a/site/src/xServices/auth/authXService.ts +++ b/site/src/xServices/auth/authXService.ts @@ -16,7 +16,7 @@ export const checks = { createTemplates: "createTemplates", deleteTemplates: "deleteTemplates", viewAuditLog: "viewAuditLog", - viewDeploymentConfig: "viewDeploymentConfig", + viewDeploymentFlags: "viewDeploymentFlags", createGroup: "createGroup", } as const @@ -57,7 +57,7 @@ export const permissionsToCheck = { }, action: "read", }, - [checks.viewDeploymentConfig]: { + [checks.viewDeploymentFlags]: { object: { resource_type: "deployment_flags", }, diff --git a/site/src/xServices/deploymentConfig/deploymentConfigMachine.ts b/site/src/xServices/deploymentConfig/deploymentConfigMachine.ts deleted file mode 100644 index d5fd0f18bdc3b..0000000000000 --- a/site/src/xServices/deploymentConfig/deploymentConfigMachine.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { getDeploymentConfig } from "api/api" -import { DeploymentConfig } from "api/typesGenerated" -import { createMachine, assign } from "xstate" - -export const deploymentConfigMachine = createMachine( - { - id: "deploymentConfigMachine", - predictableActionArguments: true, - initial: "idle", - schema: { - context: {} as { - deploymentConfig?: DeploymentConfig - getDeploymentConfigError?: unknown - }, - events: {} as { type: "LOAD" }, - services: {} as { - getDeploymentConfig: { - data: DeploymentConfig - } - }, - }, - tsTypes: {} as import("./deploymentConfigMachine.typegen").Typegen0, - states: { - idle: { - on: { - LOAD: { - target: "loading", - }, - }, - }, - loading: { - invoke: { - src: "getDeploymentConfig", - onDone: { - target: "loaded", - actions: ["assignDeploymentConfig"], - }, - onError: { - target: "idle", - actions: ["assignGetDeploymentConfigError"], - }, - }, - }, - loaded: { - type: "final", - }, - }, - }, - { - services: { - getDeploymentConfig: getDeploymentConfig, - }, - actions: { - assignDeploymentConfig: assign({ - deploymentConfig: (_, { data }) => data, - }), - assignGetDeploymentConfigError: assign({ - getDeploymentConfigError: (_, { data }) => data, - }), - }, - }, -) diff --git a/site/src/xServices/deploymentFlags/deploymentFlagsMachine.ts b/site/src/xServices/deploymentFlags/deploymentFlagsMachine.ts new file mode 100644 index 0000000000000..aa18e9d179ce1 --- /dev/null +++ b/site/src/xServices/deploymentFlags/deploymentFlagsMachine.ts @@ -0,0 +1,62 @@ +import { getDeploymentFlags } from "api/api" +import { DeploymentFlags } from "api/typesGenerated" +import { createMachine, assign } from "xstate" + +export const deploymentFlagsMachine = createMachine( + { + id: "deploymentFlagsMachine", + predictableActionArguments: true, + initial: "idle", + schema: { + context: {} as { + deploymentFlags?: DeploymentFlags + getDeploymentFlagsError?: unknown + }, + events: {} as { type: "LOAD" }, + services: {} as { + getDeploymentFlags: { + data: DeploymentFlags + } + }, + }, + tsTypes: {} as import("./deploymentFlagsMachine.typegen").Typegen0, + states: { + idle: { + on: { + LOAD: { + target: "loading", + }, + }, + }, + loading: { + invoke: { + src: "getDeploymentFlags", + onDone: { + target: "loaded", + actions: ["assignDeploymentFlags"], + }, + onError: { + target: "idle", + actions: ["assignGetDeploymentFlagsError"], + }, + }, + }, + loaded: { + type: "final", + }, + }, + }, + { + services: { + getDeploymentFlags, + }, + actions: { + assignDeploymentFlags: assign({ + deploymentFlags: (_, { data }) => data, + }), + assignGetDeploymentFlagsError: assign({ + getDeploymentFlagsError: (_, { data }) => data, + }), + }, + }, +)