diff --git a/cli/root.go b/cli/root.go index 3179823a2aae3..ee2d67d788e64 100644 --- a/cli/root.go +++ b/cli/root.go @@ -48,10 +48,12 @@ const ( varNoFeatureWarning = "no-feature-warning" varForceTty = "force-tty" varVerbose = "verbose" + varExperimental = "experimental" notLoggedInMessage = "You are not logged in. Try logging in using 'coder login '." envNoVersionCheck = "CODER_NO_VERSION_WARNING" envNoFeatureWarning = "CODER_NO_FEATURE_WARNING" + envExperimental = "CODER_EXPERIMENTAL" ) var ( @@ -184,6 +186,7 @@ func Root(subcommands []*cobra.Command) *cobra.Command { cmd.PersistentFlags().Bool(varNoOpen, false, "Block automatically opening URLs in the browser.") _ = cmd.PersistentFlags().MarkHidden(varNoOpen) cliflag.Bool(cmd.PersistentFlags(), varVerbose, "v", "CODER_VERBOSE", false, "Enable verbose output.") + cliflag.Bool(cmd.PersistentFlags(), varExperimental, "", envExperimental, false, "Enable experimental features. Experimental features are not ready for production.") return cmd } @@ -598,3 +601,18 @@ func (h *headerTransport) RoundTrip(req *http.Request) (*http.Response, error) { } return h.transport.RoundTrip(req) } + +// ExperimentalEnabled returns if the experimental feature flag is enabled. +func ExperimentalEnabled(cmd *cobra.Command) bool { + return cliflag.IsSetBool(cmd, varExperimental) +} + +// EnsureExperimental will ensure that the experimental feature flag is set if the given flag is set. +func EnsureExperimental(cmd *cobra.Command, name string) error { + _, set := cliflag.IsSet(cmd, name) + if set && !ExperimentalEnabled(cmd) { + return xerrors.Errorf("flag %s is set but requires flag --experimental or environment variable CODER_EXPERIMENTAL=true.", name) + } + + return nil +} diff --git a/cli/root_test.go b/cli/root_test.go index 617d2f90bc327..b95d9932491cf 100644 --- a/cli/root_test.go +++ b/cli/root_test.go @@ -13,6 +13,7 @@ import ( "github.com/coder/coder/buildinfo" "github.com/coder/coder/cli" + "github.com/coder/coder/cli/cliflag" "github.com/coder/coder/cli/clitest" "github.com/coder/coder/codersdk" ) @@ -153,4 +154,19 @@ func TestRoot(t *testing.T) { // This won't succeed, because we're using the login cmd to assert requests. _ = cmd.Execute() }) + + t.Run("Experimental", func(t *testing.T) { + t.Parallel() + + cmd, _ := clitest.New(t, "--experimental") + err := cmd.Execute() + require.NoError(t, err) + require.True(t, cli.ExperimentalEnabled(cmd)) + + cmd, _ = clitest.New(t, "help", "--verbose") + _ = cmd.Execute() + _, set := cliflag.IsSet(cmd, "verbose") + require.True(t, set) + require.ErrorContains(t, cli.EnsureExperimental(cmd, "verbose"), "--experimental") + }) } diff --git a/cli/server.go b/cli/server.go index 0f2def72a01c0..7fbef723a4ae5 100644 --- a/cli/server.go +++ b/cli/server.go @@ -368,6 +368,7 @@ func Server(newAPI func(context.Context, *coderd.Options) (*coderd.API, error)) AutoImportTemplates: validatedAutoImportTemplates, MetricsCacheRefreshInterval: metricsCacheRefreshInterval, AgentStatsRefreshInterval: agentStatRefreshInterval, + Experimental: ExperimentalEnabled(cmd), } if oauth2GithubClientSecret != "" { diff --git a/coderd/coderd.go b/coderd/coderd.go index e79f862efafb4..011f29927d92e 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -81,6 +81,7 @@ type Options struct { MetricsCacheRefreshInterval time.Duration AgentStatsRefreshInterval time.Duration + Experimental bool } // New constructs a Coder API handler. diff --git a/codersdk/features.go b/codersdk/features.go index 0562d9e06c72e..3b57d6eeb3853 100644 --- a/codersdk/features.go +++ b/codersdk/features.go @@ -38,9 +38,10 @@ type Feature struct { } type Entitlements struct { - Features map[string]Feature `json:"features"` - Warnings []string `json:"warnings"` - HasLicense bool `json:"has_license"` + Features map[string]Feature `json:"features"` + Warnings []string `json:"warnings"` + HasLicense bool `json:"has_license"` + Experimental bool `json:"experimental"` } func (c *Client) Entitlements(ctx context.Context) (Entitlements, error) { diff --git a/enterprise/cli/features_test.go b/enterprise/cli/features_test.go index 4621dd07e3def..da2425634cab9 100644 --- a/enterprise/cli/features_test.go +++ b/enterprise/cli/features_test.go @@ -68,5 +68,6 @@ func TestFeaturesList(t *testing.T) { assert.Equal(t, codersdk.EntitlementNotEntitled, entitlements.Features[codersdk.FeatureWorkspaceQuota].Entitlement) assert.False(t, entitlements.HasLicense) + assert.False(t, entitlements.Experimental) }) } diff --git a/enterprise/cli/licenses_test.go b/enterprise/cli/licenses_test.go index b57fca9106186..924590dee984f 100644 --- a/enterprise/cli/licenses_test.go +++ b/enterprise/cli/licenses_test.go @@ -346,8 +346,9 @@ func (*fakeLicenseAPI) entitlements(rw http.ResponseWriter, r *http.Request) { } } httpapi.Write(r.Context(), rw, http.StatusOK, codersdk.Entitlements{ - Features: features, - Warnings: []string{testWarning}, - HasLicense: true, + Features: features, + Warnings: []string{testWarning}, + HasLicense: true, + Experimental: true, }) } diff --git a/enterprise/coderd/coderd.go b/enterprise/coderd/coderd.go index f251f08d7f0d4..34b7ccf0d7026 100644 --- a/enterprise/coderd/coderd.go +++ b/enterprise/coderd/coderd.go @@ -237,9 +237,10 @@ func (api *API) serveEntitlements(rw http.ResponseWriter, r *http.Request) { api.entitlementsMu.RUnlock() resp := codersdk.Entitlements{ - Features: make(map[string]codersdk.Feature), - Warnings: make([]string, 0), - HasLicense: entitlements.hasLicense, + Features: make(map[string]codersdk.Feature), + Warnings: make([]string, 0), + HasLicense: entitlements.hasLicense, + Experimental: api.Experimental, } if entitlements.activeUsers.Limit != nil { diff --git a/site/src/api/api.ts b/site/src/api/api.ts index dada75da0503a..2ee6fac685c00 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -29,6 +29,7 @@ export const defaultEntitlements = (): TypesGen.Entitlements => { features: features, has_license: false, warnings: [], + experimental: false, } } diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 67b5fc5c2f391..5bfe3b788348e 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -244,6 +244,7 @@ export interface Entitlements { readonly features: Record readonly warnings: string[] readonly has_license: boolean + readonly experimental: boolean } // From codersdk/features.go diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 9e085883682f3..febd93652a39e 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -771,11 +771,13 @@ export const MockEntitlements: TypesGen.Entitlements = { warnings: [], has_license: false, features: {}, + experimental: false, } export const MockEntitlementsWithWarnings: TypesGen.Entitlements = { warnings: ["You are over your active user limit.", "And another thing."], has_license: true, + experimental: false, features: { user_limit: { enabled: true, @@ -797,6 +799,7 @@ export const MockEntitlementsWithWarnings: TypesGen.Entitlements = { export const MockEntitlementsWithAuditLog: TypesGen.Entitlements = { warnings: [], has_license: true, + experimental: false, features: { audit_log: { enabled: true, diff --git a/site/src/xServices/entitlements/entitlementsXService.ts b/site/src/xServices/entitlements/entitlementsXService.ts index eb3792bd650e9..1e93c7641aff7 100644 --- a/site/src/xServices/entitlements/entitlementsXService.ts +++ b/site/src/xServices/entitlements/entitlementsXService.ts @@ -23,6 +23,7 @@ const emptyEntitlements = { warnings: [], features: {}, has_license: false, + experimental: false, } export const entitlementsMachine = createMachine(