mirror of
https://github.com/coder/coder.git
synced 2025-07-13 21:36:50 +00:00
feat: Support config files with viper (#4558)
This commit is contained in:
437
cli/deployment/config.go
Normal file
437
cli/deployment/config.go
Normal file
@ -0,0 +1,437 @@
|
||||
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: <organization-name>/<team-slug>.",
|
||||
Flag: "oauth2-github-allowed-teams",
|
||||
},
|
||||
OAuth2GithubAllowSignups: codersdk.DeploymentConfigField[bool]{
|
||||
Key: "oauth2.github.allow_signups",
|
||||
Usage: "Whether new users can sign up with GitHub.",
|
||||
Flag: "oauth2-github-allow-signups",
|
||||
},
|
||||
OAuth2GithubEnterpriseBaseURL: codersdk.DeploymentConfigField[string]{
|
||||
Key: "oauth2.github.enterprise_base_url",
|
||||
Usage: "Base URL of a GitHub Enterprise deployment to use for Login with GitHub.",
|
||||
Flag: "oauth2-github-enterprise-base-url",
|
||||
},
|
||||
OIDCAllowSignups: codersdk.DeploymentConfigField[bool]{
|
||||
Key: "oidc.allow_signups",
|
||||
Usage: "Whether new users can sign up with OIDC.",
|
||||
Flag: "oidc-allow-signups",
|
||||
Value: true,
|
||||
},
|
||||
OIDCClientID: codersdk.DeploymentConfigField[string]{
|
||||
Key: "oidc.client_id",
|
||||
Usage: "Client ID to use for Login with OIDC.",
|
||||
Flag: "oidc-client-id",
|
||||
},
|
||||
OIDCClientSecret: codersdk.DeploymentConfigField[string]{
|
||||
Key: "oidc.client_secret",
|
||||
Usage: "Client secret to use for Login with OIDC.",
|
||||
Flag: "oidc-client-secret",
|
||||
},
|
||||
OIDCEmailDomain: codersdk.DeploymentConfigField[string]{
|
||||
Key: "oidc.email_domain",
|
||||
Usage: "Email domain that clients logging in with OIDC must match.",
|
||||
Flag: "oidc-email-domain",
|
||||
},
|
||||
OIDCIssuerURL: codersdk.DeploymentConfigField[string]{
|
||||
Key: "oidc.issuer_url",
|
||||
Usage: "Issuer URL to use for Login with OIDC.",
|
||||
Flag: "oidc-issuer-url",
|
||||
},
|
||||
OIDCScopes: codersdk.DeploymentConfigField[[]string]{
|
||||
Key: "oidc.scopes",
|
||||
Usage: "Scopes to grant when authenticating with OIDC.",
|
||||
Flag: "oidc-scopes",
|
||||
Value: []string{oidc.ScopeOpenID, "profile", "email"},
|
||||
},
|
||||
TelemetryEnable: codersdk.DeploymentConfigField[bool]{
|
||||
Key: "telemetry.enable",
|
||||
Usage: "Whether telemetry is enabled or not. Coder collects anonymized usage data to help improve our product.",
|
||||
Flag: "telemetry",
|
||||
Value: flag.Lookup("test.v") == nil,
|
||||
},
|
||||
TelemetryTrace: codersdk.DeploymentConfigField[bool]{
|
||||
Key: "telemetry.trace",
|
||||
Usage: "Whether Opentelemetry traces are sent to Coder. Coder collects anonymized application tracing to help improve our product. Disabling telemetry also disables this option.",
|
||||
Flag: "telemetry-trace",
|
||||
Value: flag.Lookup("test.v") == nil,
|
||||
},
|
||||
TelemetryURL: codersdk.DeploymentConfigField[string]{
|
||||
Key: "telemetry.url",
|
||||
Usage: "URL to send telemetry.",
|
||||
Flag: "telemetry-url",
|
||||
Hidden: true,
|
||||
Value: "https://telemetry.coder.com",
|
||||
},
|
||||
TLSEnable: codersdk.DeploymentConfigField[bool]{
|
||||
Key: "tls.enable",
|
||||
Usage: "Whether TLS will be enabled.",
|
||||
Flag: "tls-enable",
|
||||
},
|
||||
TLSCertFiles: codersdk.DeploymentConfigField[[]string]{
|
||||
Key: "tls.cert_file",
|
||||
Usage: "Path to each certificate for TLS. It requires a PEM-encoded file. To configure the listener to use a CA certificate, concatenate the primary certificate and the CA certificate together. The primary certificate should appear first in the combined file.",
|
||||
Flag: "tls-cert-file",
|
||||
},
|
||||
TLSClientCAFile: codersdk.DeploymentConfigField[string]{
|
||||
Key: "tls.client_ca_file",
|
||||
Usage: "PEM-encoded Certificate Authority file used for checking the authenticity of client",
|
||||
Flag: "tls-client-ca-file",
|
||||
},
|
||||
TLSClientAuth: codersdk.DeploymentConfigField[string]{
|
||||
Key: "tls.client_auth",
|
||||
Usage: "Policy the server will follow for TLS Client Authentication. Accepted values are \"none\", \"request\", \"require-any\", \"verify-if-given\", or \"require-and-verify\".",
|
||||
Flag: "tls-client-auth",
|
||||
Value: "request",
|
||||
},
|
||||
TLSKeyFiles: codersdk.DeploymentConfigField[[]string]{
|
||||
Key: "tls.key_file",
|
||||
Usage: "Paths to the private keys for each of the certificates. It requires a PEM-encoded file.",
|
||||
Flag: "tls-key-file",
|
||||
},
|
||||
TLSMinVersion: codersdk.DeploymentConfigField[string]{
|
||||
Key: "tls.min_version",
|
||||
Usage: "Minimum supported version of TLS. Accepted values are \"tls10\", \"tls11\", \"tls12\" or \"tls13\"",
|
||||
Flag: "tls-min-version",
|
||||
Value: "tls12",
|
||||
},
|
||||
TraceEnable: codersdk.DeploymentConfigField[bool]{
|
||||
Key: "trace",
|
||||
Usage: "Whether application tracing data is collected.",
|
||||
Flag: "trace",
|
||||
},
|
||||
SecureAuthCookie: codersdk.DeploymentConfigField[bool]{
|
||||
Key: "secure_auth_cookie",
|
||||
Usage: "Controls if the 'Secure' property is set on browser session cookies.",
|
||||
Flag: "secure-auth-cookie",
|
||||
},
|
||||
SSHKeygenAlgorithm: codersdk.DeploymentConfigField[string]{
|
||||
Key: "ssh_keygen_algorithm",
|
||||
Usage: "The algorithm to use for generating ssh keys. Accepted values are \"ed25519\", \"ecdsa\", or \"rsa4096\".",
|
||||
Flag: "ssh-keygen-algorithm",
|
||||
Value: "ed25519",
|
||||
},
|
||||
AutoImportTemplates: codersdk.DeploymentConfigField[[]string]{
|
||||
Key: "auto_import_templates",
|
||||
Usage: "Templates to auto-import. Available auto-importable templates are: kubernetes",
|
||||
Flag: "auto-import-template",
|
||||
Hidden: true,
|
||||
},
|
||||
MetricsCacheRefreshInterval: codersdk.DeploymentConfigField[time.Duration]{
|
||||
Key: "metrics_cache_refresh_interval",
|
||||
Usage: "How frequently metrics are refreshed",
|
||||
Flag: "metrics-cache-refresh-interval",
|
||||
Hidden: true,
|
||||
Value: time.Hour,
|
||||
},
|
||||
AgentStatRefreshInterval: codersdk.DeploymentConfigField[time.Duration]{
|
||||
Key: "agent_stat_refresh_interval",
|
||||
Usage: "How frequently agent stats are recorded",
|
||||
Flag: "agent-stats-refresh-interval",
|
||||
Hidden: true,
|
||||
Value: 10 * time.Minute,
|
||||
},
|
||||
AuditLogging: codersdk.DeploymentConfigField[bool]{
|
||||
Key: "audit_logging",
|
||||
Usage: "Specifies whether audit logging is enabled.",
|
||||
Flag: "audit-logging",
|
||||
Value: true,
|
||||
Enterprise: true,
|
||||
},
|
||||
BrowserOnly: codersdk.DeploymentConfigField[bool]{
|
||||
Key: "browser_only",
|
||||
Usage: "Whether Coder only allows connections to workspaces via the browser.",
|
||||
Flag: "browser-only",
|
||||
Enterprise: true,
|
||||
},
|
||||
SCIMAPIKey: codersdk.DeploymentConfigField[string]{
|
||||
Key: "scim_api_key",
|
||||
Usage: "Enables SCIM and sets the authentication header for the built-in SCIM server. New users are automatically created with OIDC authentication.",
|
||||
Flag: "scim-auth-header",
|
||||
Enterprise: true,
|
||||
},
|
||||
UserWorkspaceQuota: codersdk.DeploymentConfigField[int]{
|
||||
Key: "user_workspace_quota",
|
||||
Usage: "Enables and sets a limit on how many workspaces each user can create.",
|
||||
Flag: "user-workspace-quota",
|
||||
Enterprise: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:revive
|
||||
func Config(flagset *pflag.FlagSet, vip *viper.Viper) (codersdk.DeploymentConfig, error) {
|
||||
dc := newConfig()
|
||||
flg, err := flagset.GetString(config.FlagName)
|
||||
if err != nil {
|
||||
return dc, xerrors.Errorf("get global config from flag: %w", err)
|
||||
}
|
||||
vip.SetEnvPrefix("coder")
|
||||
vip.AutomaticEnv()
|
||||
|
||||
if flg != "" {
|
||||
vip.SetConfigFile(flg + "/server.yaml")
|
||||
err = vip.ReadInConfig()
|
||||
if err != nil && !xerrors.Is(err, os.ErrNotExist) {
|
||||
return dc, xerrors.Errorf("reading deployment config: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
dcv := reflect.ValueOf(&dc).Elem()
|
||||
t := dcv.Type()
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
fve := dcv.Field(i)
|
||||
key := fve.FieldByName("Key").String()
|
||||
value := fve.FieldByName("Value").Interface()
|
||||
|
||||
switch value.(type) {
|
||||
case string:
|
||||
fve.FieldByName("Value").SetString(vip.GetString(key))
|
||||
case bool:
|
||||
fve.FieldByName("Value").SetBool(vip.GetBool(key))
|
||||
case int:
|
||||
fve.FieldByName("Value").SetInt(int64(vip.GetInt(key)))
|
||||
case time.Duration:
|
||||
fve.FieldByName("Value").SetInt(int64(vip.GetDuration(key)))
|
||||
case []string:
|
||||
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()
|
||||
|
||||
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")
|
||||
}
|
@ -1,511 +0,0 @@
|
||||
package deployment
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/coder/coder/cli/cliflag"
|
||||
"github.com/coder/coder/cli/cliui"
|
||||
"github.com/coder/coder/codersdk"
|
||||
)
|
||||
|
||||
const (
|
||||
secretValue = "********"
|
||||
)
|
||||
|
||||
func Flags() *codersdk.DeploymentFlags {
|
||||
return &codersdk.DeploymentFlags{
|
||||
AccessURL: &codersdk.StringFlag{
|
||||
Name: "Access URL",
|
||||
Flag: "access-url",
|
||||
EnvVar: "CODER_ACCESS_URL",
|
||||
Description: "External URL to access your deployment. This must be accessible by all provisioned workspaces.",
|
||||
},
|
||||
WildcardAccessURL: &codersdk.StringFlag{
|
||||
Name: "Wildcard Address URL",
|
||||
Flag: "wildcard-access-url",
|
||||
EnvVar: "CODER_WILDCARD_ACCESS_URL",
|
||||
Description: `Specifies the wildcard hostname to use for workspace applications in the form "*.example.com" or "*-suffix.example.com". Ports or schemes should not be included. The scheme will be copied from the access URL.`,
|
||||
},
|
||||
Address: &codersdk.StringFlag{
|
||||
Name: "Bind Address",
|
||||
Flag: "address",
|
||||
EnvVar: "CODER_ADDRESS",
|
||||
Shorthand: "a",
|
||||
Description: "Bind address of the server.",
|
||||
Default: "127.0.0.1:3000",
|
||||
},
|
||||
AutobuildPollInterval: &codersdk.DurationFlag{
|
||||
Name: "Autobuild Poll Interval",
|
||||
Flag: "autobuild-poll-interval",
|
||||
EnvVar: "CODER_AUTOBUILD_POLL_INTERVAL",
|
||||
Description: "Interval to poll for scheduled workspace builds.",
|
||||
Hidden: true,
|
||||
Default: time.Minute,
|
||||
},
|
||||
DerpServerEnable: &codersdk.BoolFlag{
|
||||
Name: "DERP Server Enabled",
|
||||
Flag: "derp-server-enable",
|
||||
EnvVar: "CODER_DERP_SERVER_ENABLE",
|
||||
Description: "Whether to enable or disable the embedded DERP relay server.",
|
||||
Default: true,
|
||||
},
|
||||
DerpServerRegionID: &codersdk.IntFlag{
|
||||
Name: "DERP Server Region ID",
|
||||
Flag: "derp-server-region-id",
|
||||
EnvVar: "CODER_DERP_SERVER_REGION_ID",
|
||||
Description: "Region ID to use for the embedded DERP server.",
|
||||
Default: 999,
|
||||
},
|
||||
DerpServerRegionCode: &codersdk.StringFlag{
|
||||
Name: "DERP Server Region Code",
|
||||
Flag: "derp-server-region-code",
|
||||
EnvVar: "CODER_DERP_SERVER_REGION_CODE",
|
||||
Description: "Region code to use for the embedded DERP server.",
|
||||
Default: "coder",
|
||||
},
|
||||
DerpServerRegionName: &codersdk.StringFlag{
|
||||
Name: "DERP Server Region Name",
|
||||
Flag: "derp-server-region-name",
|
||||
EnvVar: "CODER_DERP_SERVER_REGION_NAME",
|
||||
Description: "Region name that for the embedded DERP server.",
|
||||
Default: "Coder Embedded Relay",
|
||||
},
|
||||
DerpServerSTUNAddresses: &codersdk.StringArrayFlag{
|
||||
Name: "DERP Server STUN Addresses",
|
||||
Flag: "derp-server-stun-addresses",
|
||||
EnvVar: "CODER_DERP_SERVER_STUN_ADDRESSES",
|
||||
Description: "Addresses for STUN servers to establish P2P connections. Set empty to disable P2P connections.",
|
||||
Default: []string{"stun.l.google.com:19302"},
|
||||
},
|
||||
DerpServerRelayAddress: &codersdk.StringFlag{
|
||||
Name: "DERP Server Relay Address",
|
||||
Flag: "derp-server-relay-address",
|
||||
EnvVar: "CODER_DERP_SERVER_RELAY_URL",
|
||||
Description: "An HTTP address that is accessible by other replicas to relay DERP traffic. Required for high availability.",
|
||||
Enterprise: true,
|
||||
},
|
||||
DerpConfigURL: &codersdk.StringFlag{
|
||||
Name: "DERP Config URL",
|
||||
Flag: "derp-config-url",
|
||||
EnvVar: "CODER_DERP_CONFIG_URL",
|
||||
Description: "URL to fetch a DERP mapping on startup. See: https://tailscale.com/kb/1118/custom-derp-servers/",
|
||||
},
|
||||
DerpConfigPath: &codersdk.StringFlag{
|
||||
Name: "DERP Config Path",
|
||||
Flag: "derp-config-path",
|
||||
EnvVar: "CODER_DERP_CONFIG_PATH",
|
||||
Description: "Path to read a DERP mapping from. See: https://tailscale.com/kb/1118/custom-derp-servers/",
|
||||
},
|
||||
PromEnabled: &codersdk.BoolFlag{
|
||||
Name: "Prometheus Enabled",
|
||||
Flag: "prometheus-enable",
|
||||
EnvVar: "CODER_PROMETHEUS_ENABLE",
|
||||
Description: "Serve prometheus metrics on the address defined by `prometheus-address`.",
|
||||
},
|
||||
PromAddress: &codersdk.StringFlag{
|
||||
Name: "Prometheus Address",
|
||||
Flag: "prometheus-address",
|
||||
EnvVar: "CODER_PROMETHEUS_ADDRESS",
|
||||
Description: "The bind address to serve prometheus metrics.",
|
||||
Default: "127.0.0.1:2112",
|
||||
},
|
||||
PprofEnabled: &codersdk.BoolFlag{
|
||||
Name: "pprof Enabled",
|
||||
Flag: "pprof-enable",
|
||||
EnvVar: "CODER_PPROF_ENABLE",
|
||||
Description: "Serve pprof metrics on the address defined by `pprof-address`.",
|
||||
},
|
||||
PprofAddress: &codersdk.StringFlag{
|
||||
Name: "pprof Address",
|
||||
Flag: "pprof-address",
|
||||
EnvVar: "CODER_PPROF_ADDRESS",
|
||||
Description: "The bind address to serve pprof.",
|
||||
Default: "127.0.0.1:6060",
|
||||
},
|
||||
CacheDir: &codersdk.StringFlag{
|
||||
Name: "Cache Directory",
|
||||
Flag: "cache-dir",
|
||||
EnvVar: "CODER_CACHE_DIRECTORY",
|
||||
Description: "The directory to cache temporary files. If unspecified and $CACHE_DIRECTORY is set, it will be used for compatibility with systemd.",
|
||||
Default: defaultCacheDir(),
|
||||
},
|
||||
InMemoryDatabase: &codersdk.BoolFlag{
|
||||
Name: "In-Memory Database",
|
||||
Flag: "in-memory",
|
||||
EnvVar: "CODER_INMEMORY",
|
||||
Description: "Controls whether data will be stored in an in-memory database.",
|
||||
Hidden: true,
|
||||
},
|
||||
ProvisionerDaemonCount: &codersdk.IntFlag{
|
||||
Name: "Provisioner Daemons",
|
||||
Flag: "provisioner-daemons",
|
||||
EnvVar: "CODER_PROVISIONER_DAEMONS",
|
||||
Description: "Number of provisioner daemons to create on start. If builds are stuck in queued state for a long time, consider increasing this.",
|
||||
Default: 3,
|
||||
},
|
||||
PostgresURL: &codersdk.StringFlag{
|
||||
Name: "Postgres URL",
|
||||
Flag: "postgres-url",
|
||||
EnvVar: "CODER_PG_CONNECTION_URL",
|
||||
Description: "URL of a PostgreSQL database. If empty, PostgreSQL binaries will be downloaded from Maven (https://repo1.maven.org/maven2) and store all data in the config root. Access the built-in database with \"coder server postgres-builtin-url\"",
|
||||
Secret: true,
|
||||
},
|
||||
OAuth2GithubClientID: &codersdk.StringFlag{
|
||||
Name: "Oauth2 Github Client ID",
|
||||
Flag: "oauth2-github-client-id",
|
||||
EnvVar: "CODER_OAUTH2_GITHUB_CLIENT_ID",
|
||||
Description: "Client ID for Login with GitHub.",
|
||||
},
|
||||
OAuth2GithubClientSecret: &codersdk.StringFlag{
|
||||
Name: "Oauth2 Github Client Secret",
|
||||
Flag: "oauth2-github-client-secret",
|
||||
EnvVar: "CODER_OAUTH2_GITHUB_CLIENT_SECRET",
|
||||
Description: "Client secret for Login with GitHub.",
|
||||
Secret: true,
|
||||
},
|
||||
OAuth2GithubAllowedOrganizations: &codersdk.StringArrayFlag{
|
||||
Name: "Oauth2 Github Allowed Organizations",
|
||||
Flag: "oauth2-github-allowed-orgs",
|
||||
EnvVar: "CODER_OAUTH2_GITHUB_ALLOWED_ORGS",
|
||||
Description: "Organizations the user must be a member of to Login with GitHub.",
|
||||
Default: []string{},
|
||||
},
|
||||
OAuth2GithubAllowedTeams: &codersdk.StringArrayFlag{
|
||||
Name: "Oauth2 Github Allowed Teams",
|
||||
Flag: "oauth2-github-allowed-teams",
|
||||
EnvVar: "CODER_OAUTH2_GITHUB_ALLOWED_TEAMS",
|
||||
Description: "Teams inside organizations the user must be a member of to Login with GitHub. Structured as: <organization-name>/<team-slug>.",
|
||||
Default: []string{},
|
||||
},
|
||||
OAuth2GithubAllowSignups: &codersdk.BoolFlag{
|
||||
Name: "Oauth2 Github Allow Signups",
|
||||
Flag: "oauth2-github-allow-signups",
|
||||
EnvVar: "CODER_OAUTH2_GITHUB_ALLOW_SIGNUPS",
|
||||
Description: "Whether new users can sign up with GitHub.",
|
||||
},
|
||||
OAuth2GithubEnterpriseBaseURL: &codersdk.StringFlag{
|
||||
Name: "Oauth2 Github Enterprise Base URL",
|
||||
Flag: "oauth2-github-enterprise-base-url",
|
||||
EnvVar: "CODER_OAUTH2_GITHUB_ENTERPRISE_BASE_URL",
|
||||
Description: "Base URL of a GitHub Enterprise deployment to use for Login with GitHub.",
|
||||
},
|
||||
OIDCAllowSignups: &codersdk.BoolFlag{
|
||||
Name: "OIDC Allow Signups",
|
||||
Flag: "oidc-allow-signups",
|
||||
EnvVar: "CODER_OIDC_ALLOW_SIGNUPS",
|
||||
Description: "Whether new users can sign up with OIDC.",
|
||||
Default: true,
|
||||
},
|
||||
OIDCClientID: &codersdk.StringFlag{
|
||||
Name: "OIDC Client ID",
|
||||
Flag: "oidc-client-id",
|
||||
EnvVar: "CODER_OIDC_CLIENT_ID",
|
||||
Description: "Client ID to use for Login with OIDC.",
|
||||
},
|
||||
OIDCClientSecret: &codersdk.StringFlag{
|
||||
Name: "OIDC Client Secret",
|
||||
Flag: "oidc-client-secret",
|
||||
EnvVar: "CODER_OIDC_CLIENT_SECRET",
|
||||
Description: "Client secret to use for Login with OIDC.",
|
||||
Secret: true,
|
||||
},
|
||||
OIDCEmailDomain: &codersdk.StringFlag{
|
||||
Name: "OIDC Email Domain",
|
||||
Flag: "oidc-email-domain",
|
||||
EnvVar: "CODER_OIDC_EMAIL_DOMAIN",
|
||||
Description: "Email domain that clients logging in with OIDC must match.",
|
||||
},
|
||||
OIDCIssuerURL: &codersdk.StringFlag{
|
||||
Name: "OIDC Issuer URL",
|
||||
Flag: "oidc-issuer-url",
|
||||
EnvVar: "CODER_OIDC_ISSUER_URL",
|
||||
Description: "Issuer URL to use for Login with OIDC.",
|
||||
},
|
||||
OIDCScopes: &codersdk.StringArrayFlag{
|
||||
Name: "OIDC Scopes",
|
||||
Flag: "oidc-scopes",
|
||||
EnvVar: "CODER_OIDC_SCOPES",
|
||||
Description: "Scopes to grant when authenticating with OIDC.",
|
||||
Default: []string{oidc.ScopeOpenID, "profile", "email"},
|
||||
},
|
||||
TelemetryEnable: &codersdk.BoolFlag{
|
||||
Name: "Telemetry Enabled",
|
||||
Flag: "telemetry",
|
||||
EnvVar: "CODER_TELEMETRY",
|
||||
Description: "Whether telemetry is enabled or not. Coder collects anonymized usage data to help improve our product.",
|
||||
Default: flag.Lookup("test.v") == nil,
|
||||
},
|
||||
TelemetryTraceEnable: &codersdk.BoolFlag{
|
||||
Name: "Trace Telemetry Enabled",
|
||||
Flag: "telemetry-trace",
|
||||
EnvVar: "CODER_TELEMETRY_TRACE",
|
||||
Shorthand: "",
|
||||
Description: "Whether Opentelemetry traces are sent to Coder. Coder collects anonymized application tracing to help improve our product. Disabling telemetry also disables this option.",
|
||||
Default: flag.Lookup("test.v") == nil,
|
||||
},
|
||||
TelemetryURL: &codersdk.StringFlag{
|
||||
Name: "Telemetry URL",
|
||||
Flag: "telemetry-url",
|
||||
EnvVar: "CODER_TELEMETRY_URL",
|
||||
Description: "URL to send telemetry.",
|
||||
Hidden: true,
|
||||
Default: "https://telemetry.coder.com",
|
||||
},
|
||||
TLSEnable: &codersdk.BoolFlag{
|
||||
Name: "TLS Enabled",
|
||||
Flag: "tls-enable",
|
||||
EnvVar: "CODER_TLS_ENABLE",
|
||||
Description: "Whether TLS will be enabled.",
|
||||
},
|
||||
TLSCertFiles: &codersdk.StringArrayFlag{
|
||||
Name: "TLS Cert Files",
|
||||
Flag: "tls-cert-file",
|
||||
EnvVar: "CODER_TLS_CERT_FILE",
|
||||
Description: "Path to each certificate for TLS. It requires a PEM-encoded file. " +
|
||||
"To configure the listener to use a CA certificate, concatenate the primary certificate " +
|
||||
"and the CA certificate together. The primary certificate should appear first in the combined file.",
|
||||
Default: []string{},
|
||||
},
|
||||
TLSClientCAFile: &codersdk.StringFlag{
|
||||
Name: "TLS Client CA File",
|
||||
Flag: "tls-client-ca-file",
|
||||
EnvVar: "CODER_TLS_CLIENT_CA_FILE",
|
||||
Description: "PEM-encoded Certificate Authority file used for checking the authenticity of client",
|
||||
},
|
||||
TLSClientAuth: &codersdk.StringFlag{
|
||||
Name: "TLS Client Auth",
|
||||
Flag: "tls-client-auth",
|
||||
EnvVar: "CODER_TLS_CLIENT_AUTH",
|
||||
Description: `Policy the server will follow for TLS Client Authentication. ` +
|
||||
`Accepted values are "none", "request", "require-any", "verify-if-given", or "require-and-verify"`,
|
||||
Default: "request",
|
||||
},
|
||||
TLSKeyFiles: &codersdk.StringArrayFlag{
|
||||
Name: "TLS Key Files",
|
||||
Flag: "tls-key-file",
|
||||
EnvVar: "CODER_TLS_KEY_FILE",
|
||||
Description: "Paths to the private keys for each of the certificates. It requires a PEM-encoded file",
|
||||
Default: []string{},
|
||||
},
|
||||
TLSMinVersion: &codersdk.StringFlag{
|
||||
Name: "TLS Min Version",
|
||||
Flag: "tls-min-version",
|
||||
EnvVar: "CODER_TLS_MIN_VERSION",
|
||||
Description: `Minimum supported version of TLS. Accepted values are "tls10", "tls11", "tls12" or "tls13"`,
|
||||
Default: "tls12",
|
||||
},
|
||||
TraceEnable: &codersdk.BoolFlag{
|
||||
Name: "Trace Enabled",
|
||||
Flag: "trace",
|
||||
EnvVar: "CODER_TRACE",
|
||||
Description: "Whether application tracing data is collected.",
|
||||
},
|
||||
SecureAuthCookie: &codersdk.BoolFlag{
|
||||
Name: "Secure Auth Cookie",
|
||||
Flag: "secure-auth-cookie",
|
||||
EnvVar: "CODER_SECURE_AUTH_COOKIE",
|
||||
Description: "Controls if the 'Secure' property is set on browser session cookies",
|
||||
},
|
||||
SSHKeygenAlgorithm: &codersdk.StringFlag{
|
||||
Name: "SSH Keygen Algorithm",
|
||||
Flag: "ssh-keygen-algorithm",
|
||||
EnvVar: "CODER_SSH_KEYGEN_ALGORITHM",
|
||||
Description: "The algorithm to use for generating ssh keys. " +
|
||||
`Accepted values are "ed25519", "ecdsa", or "rsa4096"`,
|
||||
Default: "ed25519",
|
||||
},
|
||||
AutoImportTemplates: &codersdk.StringArrayFlag{
|
||||
Name: "Auto Import Templates",
|
||||
Flag: "auto-import-template",
|
||||
EnvVar: "CODER_TEMPLATE_AUTOIMPORT",
|
||||
Description: "Templates to auto-import. Available auto-importable templates are: kubernetes",
|
||||
Hidden: true,
|
||||
Default: []string{},
|
||||
},
|
||||
MetricsCacheRefreshInterval: &codersdk.DurationFlag{
|
||||
Name: "Metrics Cache Refresh Interval",
|
||||
Flag: "metrics-cache-refresh-interval",
|
||||
EnvVar: "CODER_METRICS_CACHE_REFRESH_INTERVAL",
|
||||
Description: "How frequently metrics are refreshed",
|
||||
Hidden: true,
|
||||
Default: time.Hour,
|
||||
},
|
||||
AgentStatRefreshInterval: &codersdk.DurationFlag{
|
||||
Name: "Agent Stats Refresh Interval",
|
||||
Flag: "agent-stats-refresh-interval",
|
||||
EnvVar: "CODER_AGENT_STATS_REFRESH_INTERVAL",
|
||||
Description: "How frequently agent stats are recorded",
|
||||
Hidden: true,
|
||||
Default: 10 * time.Minute,
|
||||
},
|
||||
Verbose: &codersdk.BoolFlag{
|
||||
Name: "Verbose Logging",
|
||||
Flag: "verbose",
|
||||
EnvVar: "CODER_VERBOSE",
|
||||
Shorthand: "v",
|
||||
Description: "Enables verbose logging.",
|
||||
},
|
||||
AuditLogging: &codersdk.BoolFlag{
|
||||
Name: "Audit Logging",
|
||||
Flag: "audit-logging",
|
||||
EnvVar: "CODER_AUDIT_LOGGING",
|
||||
Description: "Specifies whether audit logging is enabled.",
|
||||
Default: true,
|
||||
Enterprise: true,
|
||||
},
|
||||
BrowserOnly: &codersdk.BoolFlag{
|
||||
Name: "Browser Only",
|
||||
Flag: "browser-only",
|
||||
EnvVar: "CODER_BROWSER_ONLY",
|
||||
Description: "Whether Coder only allows connections to workspaces via the browser.",
|
||||
Enterprise: true,
|
||||
},
|
||||
SCIMAuthHeader: &codersdk.StringFlag{
|
||||
Name: "SCIM Authentication Header",
|
||||
Flag: "scim-auth-header",
|
||||
EnvVar: "CODER_SCIM_API_KEY",
|
||||
Description: "Enables SCIM and sets the authentication header for the built-in SCIM server. New users are automatically created with OIDC authentication.",
|
||||
Secret: true,
|
||||
Enterprise: true,
|
||||
},
|
||||
UserWorkspaceQuota: &codersdk.IntFlag{
|
||||
Name: "User Workspace Quota",
|
||||
Flag: "user-workspace-quota",
|
||||
EnvVar: "CODER_USER_WORKSPACE_QUOTA",
|
||||
Description: "Enables and sets a limit on how many workspaces each user can create.",
|
||||
Default: 0,
|
||||
Enterprise: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func RemoveSensitiveValues(df codersdk.DeploymentFlags) codersdk.DeploymentFlags {
|
||||
v := reflect.ValueOf(&df).Elem()
|
||||
t := v.Type()
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
fv := v.Field(i)
|
||||
if vp, ok := fv.Interface().(*codersdk.StringFlag); ok {
|
||||
if vp.Secret && vp.Value != "" {
|
||||
// Make a copy and remove the value.
|
||||
v := *vp
|
||||
v.Value = secretValue
|
||||
fv.Set(reflect.ValueOf(&v))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return df
|
||||
}
|
||||
|
||||
//nolint:revive
|
||||
func AttachFlags(flagset *pflag.FlagSet, df *codersdk.DeploymentFlags, enterprise bool) {
|
||||
v := reflect.ValueOf(df).Elem()
|
||||
t := v.Type()
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
fv := v.Field(i)
|
||||
fve := fv.Elem()
|
||||
e := fve.FieldByName("Enterprise").Bool()
|
||||
if e != enterprise {
|
||||
continue
|
||||
}
|
||||
if e {
|
||||
d := fve.FieldByName("Description").String()
|
||||
d += cliui.Styles.Keyword.Render(" This is an Enterprise feature. Contact sales@coder.com for licensing")
|
||||
fve.FieldByName("Description").SetString(d)
|
||||
}
|
||||
|
||||
switch v := fv.Interface().(type) {
|
||||
case *codersdk.StringFlag:
|
||||
StringFlag(flagset, v)
|
||||
case *codersdk.StringArrayFlag:
|
||||
StringArrayFlag(flagset, v)
|
||||
case *codersdk.IntFlag:
|
||||
IntFlag(flagset, v)
|
||||
case *codersdk.BoolFlag:
|
||||
BoolFlag(flagset, v)
|
||||
case *codersdk.DurationFlag:
|
||||
DurationFlag(flagset, v)
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown flag type: %T", v))
|
||||
}
|
||||
if fve.FieldByName("Hidden").Bool() {
|
||||
_ = flagset.MarkHidden(fve.FieldByName("Flag").String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func StringFlag(flagset *pflag.FlagSet, fl *codersdk.StringFlag) {
|
||||
cliflag.StringVarP(flagset,
|
||||
&fl.Value,
|
||||
fl.Flag,
|
||||
fl.Shorthand,
|
||||
fl.EnvVar,
|
||||
fl.Default,
|
||||
fl.Description,
|
||||
)
|
||||
}
|
||||
|
||||
func BoolFlag(flagset *pflag.FlagSet, fl *codersdk.BoolFlag) {
|
||||
cliflag.BoolVarP(flagset,
|
||||
&fl.Value,
|
||||
fl.Flag,
|
||||
fl.Shorthand,
|
||||
fl.EnvVar,
|
||||
fl.Default,
|
||||
fl.Description,
|
||||
)
|
||||
}
|
||||
|
||||
func IntFlag(flagset *pflag.FlagSet, fl *codersdk.IntFlag) {
|
||||
cliflag.IntVarP(flagset,
|
||||
&fl.Value,
|
||||
fl.Flag,
|
||||
fl.Shorthand,
|
||||
fl.EnvVar,
|
||||
fl.Default,
|
||||
fl.Description,
|
||||
)
|
||||
}
|
||||
|
||||
func DurationFlag(flagset *pflag.FlagSet, fl *codersdk.DurationFlag) {
|
||||
cliflag.DurationVarP(flagset,
|
||||
&fl.Value,
|
||||
fl.Flag,
|
||||
fl.Shorthand,
|
||||
fl.EnvVar,
|
||||
fl.Default,
|
||||
fl.Description,
|
||||
)
|
||||
}
|
||||
|
||||
func StringArrayFlag(flagset *pflag.FlagSet, fl *codersdk.StringArrayFlag) {
|
||||
cliflag.StringArrayVarP(flagset,
|
||||
&fl.Value,
|
||||
fl.Flag,
|
||||
fl.Shorthand,
|
||||
fl.EnvVar,
|
||||
fl.Default,
|
||||
fl.Description,
|
||||
)
|
||||
}
|
||||
|
||||
func defaultCacheDir() string {
|
||||
defaultCacheDir, err := os.UserCacheDir()
|
||||
if err != nil {
|
||||
defaultCacheDir = os.TempDir()
|
||||
}
|
||||
if dir := os.Getenv("CACHE_DIRECTORY"); dir != "" {
|
||||
// For compatibility with systemd.
|
||||
defaultCacheDir = dir
|
||||
}
|
||||
|
||||
return filepath.Join(defaultCacheDir, "coder")
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
package deployment_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/cli/deployment"
|
||||
)
|
||||
|
||||
func TestFlags(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
df := deployment.Flags()
|
||||
fs := pflag.NewFlagSet("test", pflag.ContinueOnError)
|
||||
deployment.AttachFlags(fs, df, false)
|
||||
|
||||
require.NotNil(t, fs.Lookup("access-url"))
|
||||
require.False(t, fs.Lookup("access-url").Hidden)
|
||||
require.True(t, fs.Lookup("telemetry-url").Hidden)
|
||||
require.NotEmpty(t, fs.Lookup("telemetry-url").DefValue)
|
||||
require.Nil(t, fs.Lookup("audit-logging"))
|
||||
|
||||
df = deployment.Flags()
|
||||
fs = pflag.NewFlagSet("test-enterprise", pflag.ContinueOnError)
|
||||
deployment.AttachFlags(fs, df, true)
|
||||
|
||||
require.Nil(t, fs.Lookup("access-url"))
|
||||
require.NotNil(t, fs.Lookup("audit-logging"))
|
||||
require.Contains(t, fs.Lookup("audit-logging").Usage, "This is an Enterprise feature")
|
||||
}
|
Reference in New Issue
Block a user