From 76c7b914ede984b464464ef898cb18e68c448786 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Mon, 16 Jan 2023 21:44:57 +0000 Subject: [PATCH 01/17] feat: support multiple values for --experimental --- cli/deployment/config.go | 39 +++++++-- cli/deployment/config_test.go | 17 ++++ cli/testdata/coder_server_--help.golden | 8 +- coderd/apidoc/docs.go | 64 +++++++++++++- coderd/apidoc/swagger.json | 60 ++++++++++++- coderd/coderd.go | 1 + coderd/coderdtest/authorize.go | 1 + coderd/coderdtest/coderdtest.go | 1 - coderd/experiments.go | 18 ++++ coderd/experiments_test.go | 84 +++++++++++++++++++ codersdk/deploymentconfig.go | 4 +- codersdk/experiments.go | 48 +++++++++++ codersdk/features.go | 13 +-- docs/api/general.md | 32 ++++++- docs/api/schemas.md | 54 +++++++++--- enterprise/coderd/coderd.go | 2 +- .../apidocgen/markdown-template/responses.def | 3 + scripts/apitypings/main.go | 4 +- site/src/api/api.ts | 12 +++ site/src/api/types.ts | 16 ++++ site/src/api/typesGenerated.ts | 13 ++- .../WorkspacePage/WorkspaceReadyPage.tsx | 10 ++- site/src/testHelpers/entities.ts | 2 + site/src/testHelpers/handlers.ts | 5 ++ site/src/xServices/StateContext.tsx | 3 + .../entitlements/entitlementsXService.ts | 1 + .../experiments/experimentsMachine.ts | 74 ++++++++++++++++ 27 files changed, 543 insertions(+), 46 deletions(-) create mode 100644 coderd/experiments.go create mode 100644 coderd/experiments_test.go create mode 100644 codersdk/experiments.go create mode 100644 site/src/xServices/experiments/experimentsMachine.ts diff --git a/cli/deployment/config.go b/cli/deployment/config.go index 02b24f371ce6f..b54f2d5d6c20e 100644 --- a/cli/deployment/config.go +++ b/cli/deployment/config.go @@ -446,10 +446,11 @@ func newConfig() *codersdk.DeploymentConfig { Default: 512, }, }, - Experimental: &codersdk.DeploymentConfigField[bool]{ - Name: "Experimental", - Usage: "Enable experimental features. Experimental features are not ready for production.", - Flag: "experimental", + Experimental: &codersdk.DeploymentConfigField[codersdk.Experiments]{ + Name: "Experimental", + Usage: "Enable one or more experiments. These are not ready for production. Separate multiple experiments with commas, or enter '*' to opt-in to all available experiments.", + Flag: "experimental", + Default: []string{}, }, UpdateCheck: &codersdk.DeploymentConfigField[bool]{ Name: "Update Check", @@ -557,15 +558,37 @@ func setConfig(prefix string, vip *viper.Viper, target interface{}) { // with a comma, but Viper only supports with a space. This // is a small hack around it! rawSlice := reflect.ValueOf(vip.GetStringSlice(prefix)).Interface() - slice, ok := rawSlice.([]string) + stringSlice, ok := rawSlice.([]string) if !ok { panic(fmt.Sprintf("string slice is of type %T", rawSlice)) } - value := make([]string, 0, len(slice)) - for _, entry := range slice { + value := make([]string, 0, len(stringSlice)) + for _, entry := range stringSlice { value = append(value, strings.Split(entry, ",")...) } val.FieldByName("Value").Set(reflect.ValueOf(value)) + case codersdk.Experiments: + // As []string above, but we support setting wildcard values + // '*' or 'true' to enable experiments listed in codersdk.ExperimentsAll. + // Experiments not listed in codersdk.ExperimentsAll must be enabled + // explicitly. + vip.MustBindEnv(prefix, env) + rawSlice := reflect.ValueOf(vip.GetStringSlice(prefix)).Interface() + stringSlice, ok := rawSlice.([]string) + if !ok { + panic(fmt.Sprintf("string slice is of type %T", rawSlice)) + } + value := make([]string, 0, len(stringSlice)) + for _, entry := range stringSlice { + for _, val := range strings.Split(entry, ",") { + if val == "*" || val == "true" { + value = append(value, codersdk.ExperimentsAll...) + } else { + value = append(value, val) + } + } + } + val.FieldByName("Value").Set(reflect.ValueOf(codersdk.Experiments(value))) case []codersdk.GitAuthConfig: // Do not bind to CODER_GITAUTH, instead bind to CODER_GITAUTH_0_*, etc. values := readSliceFromViper[codersdk.GitAuthConfig](vip, prefix, value) @@ -743,7 +766,7 @@ func setFlags(prefix string, flagset *pflag.FlagSet, vip *viper.Viper, target in _ = flagset.IntP(flg, shorthand, vip.GetInt(prefix), usage) case time.Duration: _ = flagset.DurationP(flg, shorthand, vip.GetDuration(prefix), usage) - case []string: + case []string, codersdk.Experiments: _ = flagset.StringSliceP(flg, shorthand, vip.GetStringSlice(prefix), usage) case []codersdk.GitAuthConfig: // Ignore this one! diff --git a/cli/deployment/config_test.go b/cli/deployment/config_test.go index bced504e01fab..51dc560d5f3a3 100644 --- a/cli/deployment/config_test.go +++ b/cli/deployment/config_test.go @@ -232,6 +232,23 @@ func TestConfig(t *testing.T) { require.Equal(t, config.Prometheus.Enable.Value, true) require.Equal(t, config.Prometheus.Address.Value, config.Prometheus.Address.Default) }, + }, { + Name: "Experimental - no features", + Env: map[string]string{ + "CODER_EXPERIMENTAL": "", + }, + Valid: func(config *codersdk.DeploymentConfig) { + require.Empty(t, config.Experimental.Value) + }, + }, { + Name: "Experimental - multiple features", + Env: map[string]string{ + "CODER_EXPERIMENTAL": "foo,bar", + }, + Valid: func(config *codersdk.DeploymentConfig) { + expected := []string{"foo", "bar"} + require.ElementsMatch(t, expected, config.Experimental.Value) + }, }} { tc := tc t.Run(tc.Name, func(t *testing.T) { diff --git a/cli/testdata/coder_server_--help.golden b/cli/testdata/coder_server_--help.golden index 0424c2c8dd9f1..1936d07a31e3a 100644 --- a/cli/testdata/coder_server_--help.golden +++ b/cli/testdata/coder_server_--help.golden @@ -61,9 +61,11 @@ Flags: Consumes $CODER_DERP_SERVER_STUN_ADDRESSES (default [stun.l.google.com:19302]) - --experimental Enable experimental features. - Experimental features are not ready for - production. + --experimental strings Enable one or more experiments. These are + not ready for production. Separate + multiple experiments with commas, or + enter '*' to opt-in to all available + experiments. Consumes $CODER_EXPERIMENTAL -h, --help help for server --http-address string HTTP bind address of the server. Unset to diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 177c4230c94a1..cc13d9a3d7dff 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -387,6 +387,29 @@ const docTemplate = `{ } } }, + "/experiments": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "General" + ], + "summary": "Get experiments", + "operationId": "get-experiments", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + }, "/files": { "post": { "security": [ @@ -5740,7 +5763,7 @@ const docTemplate = `{ "$ref": "#/definitions/codersdk.DERP" }, "experimental": { - "$ref": "#/definitions/codersdk.DeploymentConfigField-bool" + "$ref": "#/definitions/codersdk.DeploymentConfigField-codersdk_Experiments" }, "gitauth": { "$ref": "#/definitions/codersdk.DeploymentConfigField-array_codersdk_GitAuthConfig" @@ -5924,6 +5947,44 @@ const docTemplate = `{ } } }, + "codersdk.DeploymentConfigField-codersdk_Experiments": { + "type": "object", + "properties": { + "default": { + "type": "array", + "items": { + "type": "string" + } + }, + "enterprise": { + "type": "boolean" + }, + "flag": { + "type": "string" + }, + "hidden": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "secret": { + "type": "boolean" + }, + "shorthand": { + "type": "string" + }, + "usage": { + "type": "string" + }, + "value": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, "codersdk.DeploymentConfigField-int": { "type": "object", "properties": { @@ -6043,6 +6104,7 @@ const docTemplate = `{ } }, "experimental": { + "description": "DEPRECATED: use Experiments instead.", "type": "boolean" }, "features": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index b9a34ede0b0f3..c868c8fd2d590 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -329,6 +329,25 @@ } } }, + "/experiments": { + "get": { + "produces": ["application/json"], + "tags": ["General"], + "summary": "Get experiments", + "operationId": "get-experiments", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + }, "/files": { "post": { "security": [ @@ -5093,7 +5112,7 @@ "$ref": "#/definitions/codersdk.DERP" }, "experimental": { - "$ref": "#/definitions/codersdk.DeploymentConfigField-bool" + "$ref": "#/definitions/codersdk.DeploymentConfigField-codersdk_Experiments" }, "gitauth": { "$ref": "#/definitions/codersdk.DeploymentConfigField-array_codersdk_GitAuthConfig" @@ -5277,6 +5296,44 @@ } } }, + "codersdk.DeploymentConfigField-codersdk_Experiments": { + "type": "object", + "properties": { + "default": { + "type": "array", + "items": { + "type": "string" + } + }, + "enterprise": { + "type": "boolean" + }, + "flag": { + "type": "string" + }, + "hidden": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "secret": { + "type": "boolean" + }, + "shorthand": { + "type": "string" + }, + "usage": { + "type": "string" + }, + "value": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, "codersdk.DeploymentConfigField-int": { "type": "object", "properties": { @@ -5392,6 +5449,7 @@ } }, "experimental": { + "description": "DEPRECATED: use Experiments instead.", "type": "boolean" }, "features": { diff --git a/coderd/coderd.go b/coderd/coderd.go index c374b6ac60b92..27752ce672134 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -348,6 +348,7 @@ func New(options *Options) *API { r.Post("/csp/reports", api.logReportCSPViolations) r.Get("/buildinfo", buildInfo) + r.Get("/experiments", api.handleExperimentsGet) r.Get("/updatecheck", api.updateCheck) r.Route("/config", func(r chi.Router) { r.Use(apiKeyMiddleware) diff --git a/coderd/coderdtest/authorize.go b/coderd/coderdtest/authorize.go index f03370376c7cb..78747257acb9a 100644 --- a/coderd/coderdtest/authorize.go +++ b/coderd/coderdtest/authorize.go @@ -48,6 +48,7 @@ func AGPLRoutes(a *AuthTester) (map[string]string, map[string]RouteCheck) { "GET:/healthz": {NoAuthorize: true}, "GET:/api/v2": {NoAuthorize: true}, "GET:/api/v2/buildinfo": {NoAuthorize: true}, + "GET:/api/v2/experiments": {NoAuthorize: true}, "GET:/api/v2/updatecheck": {NoAuthorize: true}, "GET:/api/v2/users/first": {NoAuthorize: true}, "POST:/api/v2/users/first": {NoAuthorize: true}, diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index 1d4a268b86137..cf28f4d2492d9 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -85,7 +85,6 @@ type Options struct { AppHostname string AWSCertificates awsidentity.Certificates Authorizer rbac.Authorizer - Experimental bool AzureCertificates x509.VerifyOptions GithubOAuth2Config *coderd.GithubOAuth2Config RealIPConfig *httpmw.RealIPConfig diff --git a/coderd/experiments.go b/coderd/experiments.go new file mode 100644 index 0000000000000..32efe9cca2a8f --- /dev/null +++ b/coderd/experiments.go @@ -0,0 +1,18 @@ +package coderd + +import ( + "net/http" + + "github.com/coder/coder/coderd/httpapi" +) + +// @Summary Get experiments +// @ID get-experiments +// @Produce json +// @Tags General +// @Success 200 {array} string +// @Router /experiments [get] +func (api *API) handleExperimentsGet(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + httpapi.Write(ctx, rw, http.StatusOK, api.DeploymentConfig.Experimental.Value) +} diff --git a/coderd/experiments_test.go b/coderd/experiments_test.go new file mode 100644 index 0000000000000..ee49cce319176 --- /dev/null +++ b/coderd/experiments_test.go @@ -0,0 +1,84 @@ +package coderd_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/coder/coder/coderd/coderdtest" + "github.com/coder/coder/codersdk" + "github.com/coder/coder/testutil" +) + +// nolint:paralleltest // t.Setenv +func Test_Experiments(t *testing.T) { + // In these tests we set the environment variable directly, rather than + // modifying the deployment config. This corresponds more directly to what + // an end user would do. Also, modifying the deployment config doesn't work + // for expanding the wildcard because of how viper binds stuff. + t.Run("empty", func(t *testing.T) { + client := coderdtest.New(t, nil) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + experiments, err := client.Experiments(ctx) + require.NoError(t, err) + require.NotNil(t, experiments) + require.Empty(t, experiments) + require.False(t, experiments.Enabled(codersdk.ExperimentVSCodeLocal)) + require.False(t, experiments.Enabled("foo")) + }) + + t.Run("multiple features", func(t *testing.T) { + t.Setenv("CODER_EXPERIMENTAL", "foo,bar") + client := coderdtest.New(t, nil) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + experiments, err := client.Experiments(ctx) + require.NoError(t, err) + require.NotNil(t, experiments) + require.ElementsMatch(t, []string{"foo", "bar"}, experiments) + require.True(t, experiments.Enabled("foo")) + require.True(t, experiments.Enabled("bar")) + require.False(t, experiments.Enabled("baz")) + }) + + t.Run("wildcard", func(t *testing.T) { + t.Setenv("CODER_EXPERIMENTAL", "*") + client := coderdtest.New(t, nil) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + experiments, err := client.Experiments(ctx) + require.NoError(t, err) + require.NotNil(t, experiments) + require.ElementsMatch(t, codersdk.ExperimentsAll, experiments) + for _, ex := range codersdk.ExperimentsAll { + require.True(t, experiments.Enabled(ex)) + } + require.False(t, experiments.Enabled("danger")) + }) + + t.Run("alternate wildcard with manual opt-in", func(t *testing.T) { + t.Setenv("CODER_EXPERIMENTAL", "true,danger") + client := coderdtest.New(t, nil) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + experiments, err := client.Experiments(ctx) + require.NoError(t, err) + require.NotNil(t, experiments) + require.ElementsMatch(t, append(codersdk.ExperimentsAll, "danger"), experiments) + for _, ex := range codersdk.ExperimentsAll { + require.True(t, experiments.Enabled(ex)) + } + require.True(t, experiments.Enabled("danger")) + require.False(t, experiments.Enabled("herebedragons")) + }) +} diff --git a/codersdk/deploymentconfig.go b/codersdk/deploymentconfig.go index 477a27b013e6c..983e6ace37299 100644 --- a/codersdk/deploymentconfig.go +++ b/codersdk/deploymentconfig.go @@ -41,7 +41,7 @@ type DeploymentConfig struct { SCIMAPIKey *DeploymentConfigField[string] `json:"scim_api_key" typescript:",notnull"` Provisioner *ProvisionerConfig `json:"provisioner" typescript:",notnull"` RateLimit *RateLimitConfig `json:"rate_limit" typescript:",notnull"` - Experimental *DeploymentConfigField[bool] `json:"experimental" typescript:",notnull"` + Experimental *DeploymentConfigField[Experiments] `json:"experimental" typescript:",notnull"` UpdateCheck *DeploymentConfigField[bool] `json:"update_check" typescript:",notnull"` MaxTokenLifetime *DeploymentConfigField[time.Duration] `json:"max_token_lifetime" typescript:",notnull"` Swagger *SwaggerConfig `json:"swagger" typescript:",notnull"` @@ -163,7 +163,7 @@ type LoggingConfig struct { } type Flaggable interface { - string | time.Duration | bool | int | []string | []GitAuthConfig + string | time.Duration | bool | int | []string | []GitAuthConfig | Experiments } type DeploymentConfigField[T Flaggable] struct { diff --git a/codersdk/experiments.go b/codersdk/experiments.go new file mode 100644 index 0000000000000..25558e86bc7b3 --- /dev/null +++ b/codersdk/experiments.go @@ -0,0 +1,48 @@ +package codersdk + +import ( + "context" + "encoding/json" + "net/http" +) + +var ( + // ExperimentVSCodeLocal enables a workspace button to launch VSCode + // and connect using the local VSCode extension. + ExperimentVSCodeLocal = "vscode_local" + // ExperimentsAll should include all experiments that are safe for + // users to opt-in to via --experimental='*'. + // Experiments that are not ready for consumption by all users should + // not be included here. + ExperimentsAll Experiments = Experiments{ + ExperimentVSCodeLocal, + } +) + +// Experiments is a list of experiments that are enabled for the deployment. +// Multiple experiments may be enabled at the same time. +// Experiments are not safe for production use, and are not guaranteed to +// be backwards compatible. They may be removed or renamed at any time. +type Experiments []string + +func (e Experiments) Enabled(ex string) bool { + for _, v := range e { + if v == ex { + return true + } + } + return false +} + +func (c *Client) Experiments(ctx context.Context) (Experiments, error) { + res, err := c.Request(ctx, http.MethodGet, "/api/v2/experiments", nil) + if err != nil { + return []string{}, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return []string{}, readBodyAsError(res) + } + var exp []string + return exp, json.NewDecoder(res.Body).Decode(&exp) +} diff --git a/codersdk/features.go b/codersdk/features.go index 916df4db6110d..239b5800e4cb6 100644 --- a/codersdk/features.go +++ b/codersdk/features.go @@ -75,12 +75,13 @@ type Feature struct { } type Entitlements struct { - Features map[FeatureName]Feature `json:"features"` - Warnings []string `json:"warnings"` - Errors []string `json:"errors"` - HasLicense bool `json:"has_license"` - Experimental bool `json:"experimental"` - Trial bool `json:"trial"` + Features map[FeatureName]Feature `json:"features"` + Warnings []string `json:"warnings"` + Errors []string `json:"errors"` + HasLicense bool `json:"has_license"` + // DEPRECATED: use Experiments instead. + Experimental bool `json:"experimental"` + Trial bool `json:"trial"` } func (c *Client) Entitlements(ctx context.Context) (Entitlements, error) { diff --git a/docs/api/general.md b/docs/api/general.md index 461226c00a998..802b3fc45be00 100644 --- a/docs/api/general.md +++ b/docs/api/general.md @@ -266,7 +266,7 @@ curl -X GET http://coder-server:8080/api/v2/config/deployment \ } }, "experimental": { - "default": true, + "default": ["string"], "enterprise": true, "flag": "string", "hidden": true, @@ -274,7 +274,7 @@ curl -X GET http://coder-server:8080/api/v2/config/deployment \ "secret": true, "shorthand": "string", "usage": "string", - "value": true + "value": ["string"] }, "gitauth": { "default": [ @@ -1008,6 +1008,34 @@ curl -X POST http://coder-server:8080/api/v2/csp/reports \ To perform this operation, you must be authenticated. [Learn more](authentication.md). +## Get experiments + +### Code samples + +```shell +# Example request using curl +curl -X GET http://coder-server:8080/api/v2/experiments \ + -H 'Accept: application/json' +``` + +`GET /experiments` + +### Example responses + +> 200 Response + +```json +["string"] +``` + +### Responses + +| Status | Meaning | Description | Schema | +| ------ | ------------------------------------------------------- | ----------- | --------------- | +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | array of string | + +

Response Schema

+ ## Update check ### Code samples diff --git a/docs/api/schemas.md b/docs/api/schemas.md index 91cb8f816960f..dff22580a172d 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -1346,7 +1346,7 @@ CreateParameterRequest is a structure used to create a new parameter value for a } }, "experimental": { - "default": true, + "default": ["string"], "enterprise": true, "flag": "string", "hidden": true, @@ -1354,7 +1354,7 @@ CreateParameterRequest is a structure used to create a new parameter value for a "secret": true, "shorthand": "string", "usage": "string", - "value": true + "value": ["string"] }, "gitauth": { "default": [ @@ -2058,7 +2058,7 @@ CreateParameterRequest is a structure used to create a new parameter value for a | `browser_only` | [codersdk.DeploymentConfigField-bool](#codersdkdeploymentconfigfield-bool) | false | | | | `cache_directory` | [codersdk.DeploymentConfigField-string](#codersdkdeploymentconfigfield-string) | false | | | | `derp` | [codersdk.DERP](#codersdkderp) | false | | | -| `experimental` | [codersdk.DeploymentConfigField-bool](#codersdkdeploymentconfigfield-bool) | false | | | +| `experimental` | [codersdk.DeploymentConfigField-codersdk_Experiments](#codersdkdeploymentconfigfield-codersdk_experiments) | false | | | | `gitauth` | [codersdk.DeploymentConfigField-array_codersdk_GitAuthConfig](#codersdkdeploymentconfigfield-array_codersdk_gitauthconfig) | false | | | | `http_address` | [codersdk.DeploymentConfigField-string](#codersdkdeploymentconfigfield-string) | false | | | | `in_memory_database` | [codersdk.DeploymentConfigField-bool](#codersdkdeploymentconfigfield-bool) | false | | | @@ -2198,6 +2198,36 @@ CreateParameterRequest is a structure used to create a new parameter value for a | `usage` | string | false | | | | `value` | boolean | false | | | +## codersdk.DeploymentConfigField-codersdk_Experiments + +```json +{ + "default": ["string"], + "enterprise": true, + "flag": "string", + "hidden": true, + "name": "string", + "secret": true, + "shorthand": "string", + "usage": "string", + "value": ["string"] +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| ------------ | --------------- | -------- | ------------ | ----------- | +| `default` | array of string | false | | | +| `enterprise` | boolean | false | | | +| `flag` | string | false | | | +| `hidden` | boolean | false | | | +| `name` | string | false | | | +| `secret` | boolean | false | | | +| `shorthand` | string | false | | | +| `usage` | string | false | | | +| `value` | array of string | false | | | + ## codersdk.DeploymentConfigField-int ```json @@ -2332,15 +2362,15 @@ CreateParameterRequest is a structure used to create a new parameter value for a ### Properties -| Name | Type | Required | Restrictions | Description | -| ------------------ | ------------------------------------ | -------- | ------------ | ----------- | -| `errors` | array of string | false | | | -| `experimental` | boolean | false | | | -| `features` | object | false | | | -| ยป `[any property]` | [codersdk.Feature](#codersdkfeature) | false | | | -| `has_license` | boolean | false | | | -| `trial` | boolean | false | | | -| `warnings` | array of string | false | | | +| Name | Type | Required | Restrictions | Description | +| ------------------ | ------------------------------------ | -------- | ------------ | ------------------------------------- | +| `errors` | array of string | false | | | +| `experimental` | boolean | false | | Experimental use Experiments instead. | +| `features` | object | false | | | +| ยป `[any property]` | [codersdk.Feature](#codersdkfeature) | false | | | +| `has_license` | boolean | false | | | +| `trial` | boolean | false | | | +| `warnings` | array of string | false | | | ## codersdk.Feature diff --git a/enterprise/coderd/coderd.go b/enterprise/coderd/coderd.go index 5ad7460b1dd26..d916f813e43cc 100644 --- a/enterprise/coderd/coderd.go +++ b/enterprise/coderd/coderd.go @@ -250,7 +250,7 @@ func (api *API) updateEntitlements(ctx context.Context) error { if err != nil { return err } - entitlements.Experimental = api.DeploymentConfig.Experimental.Value + entitlements.Experimental = len(api.DeploymentConfig.Experimental.Value) > 0 featureChanged := func(featureName codersdk.FeatureName) (changed bool, enabled bool) { if api.entitlements.Features == nil { diff --git a/scripts/apidocgen/markdown-template/responses.def b/scripts/apidocgen/markdown-template/responses.def index 117320d16e976..02421e11c5ea5 100644 --- a/scripts/apidocgen/markdown-template/responses.def +++ b/scripts/apidocgen/markdown-template/responses.def @@ -13,6 +13,9 @@ if (!ref) { ref = content.schema.items["x-widdershins-oldRef"]; } + if (!ref) { + return content.schema.items.type; + } const aType = ref.replace("#/components/schemas/",""); const href = aType.replace(".","").toLowerCase(); return "[" + aType + "](schemas.md#" + href + ")"; diff --git a/scripts/apitypings/main.go b/scripts/apitypings/main.go index 1d068795c0eb4..765c8dd510d70 100644 --- a/scripts/apitypings/main.go +++ b/scripts/apitypings/main.go @@ -288,7 +288,7 @@ func (g *Generator) generateOne(m *Maps, obj types.Object) error { // type string // These are enums. Store to expand later. m.Enums[obj.Name()] = obj - case *types.Map: + case *types.Map, *types.Array, *types.Slice: // Declared maps that are not structs are still valid codersdk objects. // Handle them custom by calling 'typescriptType' directly instead of // iterating through each struct field. @@ -308,8 +308,6 @@ func (g *Generator) generateOne(m *Maps, obj types.Object) error { // Use similar output syntax to enums. str.WriteString(fmt.Sprintf("export type %s = %s\n", obj.Name(), ts.ValueType)) m.Structs[obj.Name()] = str.String() - case *types.Array, *types.Slice: - // TODO: @emyrk if you need this, follow the same design as "*types.Map" case. case *types.Interface: // Interfaces are used as generics. Non-generic interfaces are // not supported. diff --git a/site/src/api/api.ts b/site/src/api/api.ts index fb0cac686b734..52e069cfd5541 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -636,6 +636,18 @@ export const getEntitlements = async (): Promise => { } } +export const getExperiments = async (): Promise => { + try { + const response = await axios.get("/api/v2/experiments") + return response.data + } catch (error) { + if (axios.isAxiosError(error) && error.response?.status === 404) { + return [] + } + throw error + } +} + export const getAuditLogs = async ( options: TypesGen.AuditLogsRequest, ): Promise => { diff --git a/site/src/api/types.ts b/site/src/api/types.ts index daf4e451ac5e8..7e3876711597f 100644 --- a/site/src/api/types.ts +++ b/site/src/api/types.ts @@ -14,3 +14,19 @@ export interface ReconnectingPTYRequest { export type WorkspaceBuildTransition = "start" | "stop" | "delete" export type Message = { message: string } + +// Keep up to date with coder/codersdk/features.go +export enum FeatureNames { + AuditLog = "audit_log", + UserLimit = "user_limit", + BrowserOnly = "browser_only", + SCIM = "scim", + TemplateRBAC = "template_rbac", + HighAvailability = "high_availability", + Appearance = "appearance", +} + +// Keep up to date with coder/codersdk/experiments.go +export enum Experiments { + VSCodeLocal = "vscode_local", +} diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 7d098c55b9138..75741046ce3c0 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -311,7 +311,7 @@ export interface DeploymentConfig { readonly scim_api_key: DeploymentConfigField readonly provisioner: ProvisionerConfig readonly rate_limit: RateLimitConfig - readonly experimental: DeploymentConfigField + readonly experimental: DeploymentConfigField readonly update_check: DeploymentConfigField readonly max_token_lifetime: DeploymentConfigField readonly swagger: SwaggerConfig @@ -341,6 +341,9 @@ export interface Entitlements { readonly trial: boolean } +// From codersdk/experiments.go +export type Experiments = string[] + // From codersdk/features.go export interface Feature { readonly entitlement: Entitlement @@ -1277,4 +1280,10 @@ export const WorkspaceTransitions: WorkspaceTransition[] = [ ] // From codersdk/deploymentconfig.go -export type Flaggable = string | number | boolean | string[] | GitAuthConfig[] +export type Flaggable = + | string + | number + | boolean + | string[] + | GitAuthConfig[] + | Experiments diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index 3c8f6324ac35b..8754879b65f8c 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -1,4 +1,5 @@ import { useActor, useSelector } from "@xstate/react" +import { Experiments, FeatureNames } from "api/types" import dayjs from "dayjs" import { useContext, useEffect } from "react" import { Helmet } from "react-helmet-async" @@ -41,9 +42,9 @@ export const WorkspaceReadyPage = ({ workspaceState.children["scheduleBannerMachine"], ) const xServices = useContext(XServiceContext) - const experimental = useSelector( - xServices.entitlementsXService, - (state) => state.context.entitlements.experimental, + const experiments = useSelector( + xServices.experimentsXService, + (state) => state.context.experiments || [], ) const featureVisibility = useSelector( xServices.entitlementsXService, @@ -124,7 +125,8 @@ export const WorkspaceReadyPage = ({ canUpdateWorkspace={canUpdateWorkspace} hideSSHButton={featureVisibility["browser_only"]} hideVSCodeDesktopButton={ - !experimental || featureVisibility["browser_only"] + !experiments.includes(Experiments.VSCodeLocal) || + featureVisibility[FeatureNames.BrowserOnly] } workspaceErrors={{ [WorkspaceErrors.GET_RESOURCES_ERROR]: refreshWorkspaceWarning, diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index dc57d1857327d..42216689b68f6 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -982,6 +982,8 @@ export const MockEntitlementsWithAuditLog: TypesGen.Entitlements = { }), } +export const MockExperiments: string[] = [Types.Experiments.VSCodeLocal] + export const MockAuditLog: TypesGen.AuditLog = { id: "fbd2116a-8961-4954-87ae-e4575bd29ce0", request_id: "53bded77-7b9d-4e82-8771-991a34d759f9", diff --git a/site/src/testHelpers/handlers.ts b/site/src/testHelpers/handlers.ts index 92d3d8e8e6d69..98a02293218bb 100644 --- a/site/src/testHelpers/handlers.ts +++ b/site/src/testHelpers/handlers.ts @@ -15,6 +15,11 @@ export const handlers = [ return res(ctx.status(200), ctx.json(M.MockBuildInfo)) }), + // experiments + rest.get("/api/v2/experiments", async (req, res, ctx) => { + return res(ctx.status(200), ctx.json(M.MockExperiments)) + }), + // update check rest.get("/api/v2/updatecheck", async (req, res, ctx) => { return res(ctx.status(200), ctx.json(M.MockUpdateCheck)) diff --git a/site/src/xServices/StateContext.tsx b/site/src/xServices/StateContext.tsx index d77c94defb09e..e7e958e50409c 100644 --- a/site/src/xServices/StateContext.tsx +++ b/site/src/xServices/StateContext.tsx @@ -4,12 +4,14 @@ import { ActorRefFrom } from "xstate" import { authMachine } from "./auth/authXService" import { buildInfoMachine } from "./buildInfo/buildInfoXService" import { entitlementsMachine } from "./entitlements/entitlementsXService" +import { experimentsMachine } from "./experiments/experimentsMachine" import { appearanceMachine } from "./appearance/appearanceXService" interface XServiceContextType { authXService: ActorRefFrom buildInfoXService: ActorRefFrom entitlementsXService: ActorRefFrom + experimentsXService: ActorRefFrom appearanceXService: ActorRefFrom } @@ -30,6 +32,7 @@ export const XServiceProvider: FC<{ children: ReactNode }> = ({ children }) => { authXService: useInterpret(authMachine), buildInfoXService: useInterpret(buildInfoMachine), entitlementsXService: useInterpret(entitlementsMachine), + experimentsXService: useInterpret(experimentsMachine), appearanceXService: useInterpret(appearanceMachine), }} > diff --git a/site/src/xServices/entitlements/entitlementsXService.ts b/site/src/xServices/entitlements/entitlementsXService.ts index 52bceac1b2967..96b175897c70b 100644 --- a/site/src/xServices/entitlements/entitlementsXService.ts +++ b/site/src/xServices/entitlements/entitlementsXService.ts @@ -22,6 +22,7 @@ const emptyEntitlements = { features: withDefaultFeatures({}), has_license: false, experimental: false, + experimental_features: [], trial: false, } diff --git a/site/src/xServices/experiments/experimentsMachine.ts b/site/src/xServices/experiments/experimentsMachine.ts new file mode 100644 index 0000000000000..76176bd833836 --- /dev/null +++ b/site/src/xServices/experiments/experimentsMachine.ts @@ -0,0 +1,74 @@ +import { getExperiments } from "api/api" +import { createMachine, assign } from "xstate" + +export interface ExperimentsContext { + experiments?: string[] + getExperimentsError?: Error | unknown +} + +export const experimentsMachine = createMachine( + { + id: "experimentsState", + predictableActionArguments: true, + tsTypes: {} as import("./experimentsMachine.typegen").Typegen0, + schema: { + context: {} as ExperimentsContext, + services: {} as { + getExperiments: { + data: string[] + } + }, + }, + context: { + experiments: undefined, + }, + initial: "gettingExperiments", + states: { + gettingExperiments: { + invoke: { + src: "getExperiments", + id: "getExperiments", + onDone: [ + { + actions: ["assignExperiments", "clearGetExperimentsError"], + target: "#experimentsState.success", + } + ], + onError: [ + { + actions: ["assignGetExperimentsError", "clearExperiments"], + target: "#experimentsState.failure", + } + ], + }, + }, + success: { + type: "final", + }, + failure: { + type: "final", + } + }, + }, + { + services: { + getExperiments: getExperiments, + }, + actions: { + assignExperiments: assign({ + experiments: (_, event) => event.data, + }), + clearExperiments: assign((context: ExperimentsContext) => ({ + ...context, + experiments: undefined, + })), + assignGetExperimentsError: assign({ + getExperimentsError: (_, event) => event.data, + }), + clearGetExperimentsError: assign((context: ExperimentsContext) => ({ + ...context, + getExperimentsError: undefined, + })), + }, + }, +) From 626d81bdda06280e6bf063c51baecce0040d10fb Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 18 Jan 2023 11:29:04 +0000 Subject: [PATCH 02/17] develop.sh: allow passing additional args to backend API via -- arg1=val1... --- scripts/develop.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/develop.sh b/scripts/develop.sh index 3ee90f5a9db93..9620af0a76dc6 100755 --- a/scripts/develop.sh +++ b/scripts/develop.sh @@ -121,7 +121,7 @@ fatal() { trap 'fatal "Script encountered an error"' ERR cdroot - start_cmd API "" "${CODER_DEV_SHIM}" server --http-address 0.0.0.0:3000 --swagger-enable --access-url "http://127.0.0.1:3000" + start_cmd API "" "${CODER_DEV_SHIM}" server --http-address 0.0.0.0:3000 --swagger-enable --access-url "http://127.0.0.1:3000" "$@" echo '== Waiting for Coder to become ready' # Start the timeout in the background so interrupting this script From d93a3734cefaa5b0e151fc7634748f6ed9a596c4 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 18 Jan 2023 11:58:10 +0000 Subject: [PATCH 03/17] fix swaggerparser test --- coderd/coderdtest/swaggerparser.go | 1 + 1 file changed, 1 insertion(+) diff --git a/coderd/coderdtest/swaggerparser.go b/coderd/coderdtest/swaggerparser.go index c5d19cf088966..a7dba90ffef43 100644 --- a/coderd/coderdtest/swaggerparser.go +++ b/coderd/coderdtest/swaggerparser.go @@ -297,6 +297,7 @@ func assertPathParametersDefined(t *testing.T, comment SwaggerComment) { func assertSecurityDefined(t *testing.T, comment SwaggerComment) { if comment.router == "/updatecheck" || comment.router == "/buildinfo" || + comment.router == "/experiments" || comment.router == "/" { return // endpoints do not require authorization } From e82c306cce6bb42012638f146820e444e4e61b1a Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 18 Jan 2023 11:58:23 +0000 Subject: [PATCH 04/17] make fmt --- .../xServices/experiments/experimentsMachine.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/site/src/xServices/experiments/experimentsMachine.ts b/site/src/xServices/experiments/experimentsMachine.ts index 76176bd833836..99709fb12b379 100644 --- a/site/src/xServices/experiments/experimentsMachine.ts +++ b/site/src/xServices/experiments/experimentsMachine.ts @@ -30,15 +30,15 @@ export const experimentsMachine = createMachine( id: "getExperiments", onDone: [ { - actions: ["assignExperiments", "clearGetExperimentsError"], - target: "#experimentsState.success", - } + actions: ["assignExperiments", "clearGetExperimentsError"], + target: "#experimentsState.success", + }, ], onError: [ { - actions: ["assignGetExperimentsError", "clearExperiments"], - target: "#experimentsState.failure", - } + actions: ["assignGetExperimentsError", "clearExperiments"], + target: "#experimentsState.failure", + }, ], }, }, @@ -47,7 +47,7 @@ export const experimentsMachine = createMachine( }, failure: { type: "final", - } + }, }, }, { From 047771c524de2d2e27a89b87f2f748fbf8022414 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 18 Jan 2023 13:34:45 +0000 Subject: [PATCH 05/17] require AuthN on /api/v2/experiments --- coderd/apidoc/docs.go | 5 +++++ coderd/apidoc/swagger.json | 5 +++++ coderd/coderd.go | 5 ++++- coderd/coderdtest/authorize.go | 2 +- coderd/experiments.go | 1 + coderd/experiments_test.go | 17 +++++++++++++++++ docs/api/general.md | 5 ++++- 7 files changed, 37 insertions(+), 3 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index cc13d9a3d7dff..dd377d09eaa98 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -389,6 +389,11 @@ const docTemplate = `{ }, "/experiments": { "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], "produces": [ "application/json" ], diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index c868c8fd2d590..cd7a421e32978 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -331,6 +331,11 @@ }, "/experiments": { "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], "produces": ["application/json"], "tags": ["General"], "summary": "Get experiments", diff --git a/coderd/coderd.go b/coderd/coderd.go index 27752ce672134..4459ec76689e9 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -348,7 +348,10 @@ func New(options *Options) *API { r.Post("/csp/reports", api.logReportCSPViolations) r.Get("/buildinfo", buildInfo) - r.Get("/experiments", api.handleExperimentsGet) + r.Route("/experiments", func(r chi.Router) { + r.Use(apiKeyMiddleware) + r.Get("/", api.handleExperimentsGet) + }) r.Get("/updatecheck", api.updateCheck) r.Route("/config", func(r chi.Router) { r.Use(apiKeyMiddleware) diff --git a/coderd/coderdtest/authorize.go b/coderd/coderdtest/authorize.go index 78747257acb9a..1541fa2b119ae 100644 --- a/coderd/coderdtest/authorize.go +++ b/coderd/coderdtest/authorize.go @@ -48,7 +48,7 @@ func AGPLRoutes(a *AuthTester) (map[string]string, map[string]RouteCheck) { "GET:/healthz": {NoAuthorize: true}, "GET:/api/v2": {NoAuthorize: true}, "GET:/api/v2/buildinfo": {NoAuthorize: true}, - "GET:/api/v2/experiments": {NoAuthorize: true}, + "GET:/api/v2/experiments": {NoAuthorize: true}, // This route requires AuthN, but not AuthZ. "GET:/api/v2/updatecheck": {NoAuthorize: true}, "GET:/api/v2/users/first": {NoAuthorize: true}, "POST:/api/v2/users/first": {NoAuthorize: true}, diff --git a/coderd/experiments.go b/coderd/experiments.go index 32efe9cca2a8f..565b79b283bbb 100644 --- a/coderd/experiments.go +++ b/coderd/experiments.go @@ -8,6 +8,7 @@ import ( // @Summary Get experiments // @ID get-experiments +// @Security CoderSessionToken // @Produce json // @Tags General // @Success 200 {array} string diff --git a/coderd/experiments_test.go b/coderd/experiments_test.go index ee49cce319176..a3a84c3ac9f76 100644 --- a/coderd/experiments_test.go +++ b/coderd/experiments_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/require" "github.com/coder/coder/coderd/coderdtest" + "github.com/coder/coder/coderd/httpmw" "github.com/coder/coder/codersdk" "github.com/coder/coder/testutil" ) @@ -19,6 +20,7 @@ func Test_Experiments(t *testing.T) { // for expanding the wildcard because of how viper binds stuff. t.Run("empty", func(t *testing.T) { client := coderdtest.New(t, nil) + _ = coderdtest.CreateFirstUser(t, client) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() @@ -34,6 +36,7 @@ func Test_Experiments(t *testing.T) { t.Run("multiple features", func(t *testing.T) { t.Setenv("CODER_EXPERIMENTAL", "foo,bar") client := coderdtest.New(t, nil) + _ = coderdtest.CreateFirstUser(t, client) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() @@ -50,6 +53,7 @@ func Test_Experiments(t *testing.T) { t.Run("wildcard", func(t *testing.T) { t.Setenv("CODER_EXPERIMENTAL", "*") client := coderdtest.New(t, nil) + _ = coderdtest.CreateFirstUser(t, client) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() @@ -67,6 +71,7 @@ func Test_Experiments(t *testing.T) { t.Run("alternate wildcard with manual opt-in", func(t *testing.T) { t.Setenv("CODER_EXPERIMENTAL", "true,danger") client := coderdtest.New(t, nil) + _ = coderdtest.CreateFirstUser(t, client) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() @@ -81,4 +86,16 @@ func Test_Experiments(t *testing.T) { require.True(t, experiments.Enabled("danger")) require.False(t, experiments.Enabled("herebedragons")) }) + + t.Run("Unauthorized", func(t *testing.T) { + t.Setenv("CODER_EXPERIMENTAL", "foo,bar") + client := coderdtest.New(t, nil) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + _, err := client.Experiments(ctx) + require.Error(t, err) + require.ErrorContains(t, err, httpmw.SignedOutErrorMessage) + }) } diff --git a/docs/api/general.md b/docs/api/general.md index 802b3fc45be00..19ce6c3346157 100644 --- a/docs/api/general.md +++ b/docs/api/general.md @@ -1015,7 +1015,8 @@ To perform this operation, you must be authenticated. [Learn more](authenticatio ```shell # Example request using curl curl -X GET http://coder-server:8080/api/v2/experiments \ - -H 'Accept: application/json' + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' ``` `GET /experiments` @@ -1036,6 +1037,8 @@ curl -X GET http://coder-server:8080/api/v2/experiments \

Response Schema

+To perform this operation, you must be authenticated. [Learn more](authentication.md). + ## Update check ### Code samples From 3ba27ea4e38c9e0e1f0c15ba11708abe29ee4a42 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 18 Jan 2023 13:36:59 +0000 Subject: [PATCH 06/17] fixup! require AuthN on /api/v2/experiments --- coderd/coderdtest/swaggerparser.go | 1 - 1 file changed, 1 deletion(-) diff --git a/coderd/coderdtest/swaggerparser.go b/coderd/coderdtest/swaggerparser.go index a7dba90ffef43..c5d19cf088966 100644 --- a/coderd/coderdtest/swaggerparser.go +++ b/coderd/coderdtest/swaggerparser.go @@ -297,7 +297,6 @@ func assertPathParametersDefined(t *testing.T, comment SwaggerComment) { func assertSecurityDefined(t *testing.T, comment SwaggerComment) { if comment.router == "/updatecheck" || comment.router == "/buildinfo" || - comment.router == "/experiments" || comment.router == "/" { return // endpoints do not require authorization } From 3ebb778a145f12495b62cfa2db14bd5c85541f46 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 18 Jan 2023 13:40:02 +0000 Subject: [PATCH 07/17] fix nits --- coderd/experiments_test.go | 2 ++ codersdk/experiments.go | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/coderd/experiments_test.go b/coderd/experiments_test.go index a3a84c3ac9f76..7c2cfaa0990ff 100644 --- a/coderd/experiments_test.go +++ b/coderd/experiments_test.go @@ -90,6 +90,8 @@ func Test_Experiments(t *testing.T) { t.Run("Unauthorized", func(t *testing.T) { t.Setenv("CODER_EXPERIMENTAL", "foo,bar") client := coderdtest.New(t, nil) + // Explicitly omit creating a user so we're unauthorized. + // _ = coderdtest.CreateFirstUser(t, client) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() diff --git a/codersdk/experiments.go b/codersdk/experiments.go index 25558e86bc7b3..f0da7ee618c3a 100644 --- a/codersdk/experiments.go +++ b/codersdk/experiments.go @@ -37,11 +37,11 @@ func (e Experiments) Enabled(ex string) bool { func (c *Client) Experiments(ctx context.Context) (Experiments, error) { res, err := c.Request(ctx, http.MethodGet, "/api/v2/experiments", nil) if err != nil { - return []string{}, err + return nil, err } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return []string{}, readBodyAsError(res) + return nil, readBodyAsError(res) } var exp []string return exp, json.NewDecoder(res.Body).Decode(&exp) From 2aeb93ef2f5c5301488af48c801617451c836957 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 18 Jan 2023 14:03:48 +0000 Subject: [PATCH 08/17] add type alias for codersdk.Experiment --- cli/deployment/config.go | 6 +-- cli/deployment/config_test.go | 2 +- coderd/apidoc/docs.go | 13 +++++- coderd/apidoc/swagger.json | 9 +++- coderd/experiments_test.go | 2 +- codersdk/experiments.go | 17 ++++--- docs/api/general.md | 4 +- docs/api/schemas.md | 44 ++++++++++++------- site/src/api/api.ts | 2 +- site/src/api/typesGenerated.ts | 6 ++- .../experiments/experimentsMachine.ts | 5 ++- 11 files changed, 74 insertions(+), 36 deletions(-) diff --git a/cli/deployment/config.go b/cli/deployment/config.go index b54f2d5d6c20e..66c0db20361e2 100644 --- a/cli/deployment/config.go +++ b/cli/deployment/config.go @@ -450,7 +450,7 @@ func newConfig() *codersdk.DeploymentConfig { Name: "Experimental", Usage: "Enable one or more experiments. These are not ready for production. Separate multiple experiments with commas, or enter '*' to opt-in to all available experiments.", Flag: "experimental", - Default: []string{}, + Default: []codersdk.Experiment{}, }, UpdateCheck: &codersdk.DeploymentConfigField[bool]{ Name: "Update Check", @@ -578,13 +578,13 @@ func setConfig(prefix string, vip *viper.Viper, target interface{}) { if !ok { panic(fmt.Sprintf("string slice is of type %T", rawSlice)) } - value := make([]string, 0, len(stringSlice)) + value := make([]codersdk.Experiment, 0, len(stringSlice)) for _, entry := range stringSlice { for _, val := range strings.Split(entry, ",") { if val == "*" || val == "true" { value = append(value, codersdk.ExperimentsAll...) } else { - value = append(value, val) + value = append(value, codersdk.Experiment(val)) } } } diff --git a/cli/deployment/config_test.go b/cli/deployment/config_test.go index 51dc560d5f3a3..31ad58c7fa7a2 100644 --- a/cli/deployment/config_test.go +++ b/cli/deployment/config_test.go @@ -246,7 +246,7 @@ func TestConfig(t *testing.T) { "CODER_EXPERIMENTAL": "foo,bar", }, Valid: func(config *codersdk.DeploymentConfig) { - expected := []string{"foo", "bar"} + expected := []codersdk.Experiment{"foo", "bar"} require.ElementsMatch(t, expected, config.Experimental.Value) }, }} { diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index dd377d09eaa98..bcf671852e4fc 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -5958,7 +5958,7 @@ const docTemplate = `{ "default": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/codersdk.Experiment" } }, "enterprise": { @@ -5985,7 +5985,7 @@ const docTemplate = `{ "value": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/codersdk.Experiment" } } } @@ -6132,6 +6132,15 @@ const docTemplate = `{ } } }, + "codersdk.Experiment": { + "type": "string", + "enum": [ + "vscode_local" + ], + "x-enum-varnames": [ + "ExperimentVSCodeLocal" + ] + }, "codersdk.Feature": { "type": "object", "properties": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index cd7a421e32978..bc6f1f622cdd9 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -5307,7 +5307,7 @@ "default": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/codersdk.Experiment" } }, "enterprise": { @@ -5334,7 +5334,7 @@ "value": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/codersdk.Experiment" } } } @@ -5477,6 +5477,11 @@ } } }, + "codersdk.Experiment": { + "type": "string", + "enum": ["vscode_local"], + "x-enum-varnames": ["ExperimentVSCodeLocal"] + }, "codersdk.Feature": { "type": "object", "properties": { diff --git a/coderd/experiments_test.go b/coderd/experiments_test.go index 7c2cfaa0990ff..529add45a64b5 100644 --- a/coderd/experiments_test.go +++ b/coderd/experiments_test.go @@ -44,7 +44,7 @@ func Test_Experiments(t *testing.T) { experiments, err := client.Experiments(ctx) require.NoError(t, err) require.NotNil(t, experiments) - require.ElementsMatch(t, []string{"foo", "bar"}, experiments) + require.ElementsMatch(t, []codersdk.Experiment{"foo", "bar"}, experiments) require.True(t, experiments.Enabled("foo")) require.True(t, experiments.Enabled("bar")) require.False(t, experiments.Enabled("baz")) diff --git a/codersdk/experiments.go b/codersdk/experiments.go index f0da7ee618c3a..0a90ba2dea988 100644 --- a/codersdk/experiments.go +++ b/codersdk/experiments.go @@ -6,15 +6,20 @@ import ( "net/http" ) -var ( +type Experiment string + +const ( // ExperimentVSCodeLocal enables a workspace button to launch VSCode // and connect using the local VSCode extension. - ExperimentVSCodeLocal = "vscode_local" + ExperimentVSCodeLocal Experiment = "vscode_local" +) + +var ( // ExperimentsAll should include all experiments that are safe for // users to opt-in to via --experimental='*'. // Experiments that are not ready for consumption by all users should // not be included here. - ExperimentsAll Experiments = Experiments{ + ExperimentsAll = Experiments{ ExperimentVSCodeLocal, } ) @@ -23,9 +28,9 @@ var ( // Multiple experiments may be enabled at the same time. // Experiments are not safe for production use, and are not guaranteed to // be backwards compatible. They may be removed or renamed at any time. -type Experiments []string +type Experiments []Experiment -func (e Experiments) Enabled(ex string) bool { +func (e Experiments) Enabled(ex Experiment) bool { for _, v := range e { if v == ex { return true @@ -43,6 +48,6 @@ func (c *Client) Experiments(ctx context.Context) (Experiments, error) { if res.StatusCode != http.StatusOK { return nil, readBodyAsError(res) } - var exp []string + var exp []Experiment return exp, json.NewDecoder(res.Body).Decode(&exp) } diff --git a/docs/api/general.md b/docs/api/general.md index 19ce6c3346157..a0d29f315f00a 100644 --- a/docs/api/general.md +++ b/docs/api/general.md @@ -266,7 +266,7 @@ curl -X GET http://coder-server:8080/api/v2/config/deployment \ } }, "experimental": { - "default": ["string"], + "default": ["vscode_local"], "enterprise": true, "flag": "string", "hidden": true, @@ -274,7 +274,7 @@ curl -X GET http://coder-server:8080/api/v2/config/deployment \ "secret": true, "shorthand": "string", "usage": "string", - "value": ["string"] + "value": ["vscode_local"] }, "gitauth": { "default": [ diff --git a/docs/api/schemas.md b/docs/api/schemas.md index dff22580a172d..37016d98c0ff8 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -1346,7 +1346,7 @@ CreateParameterRequest is a structure used to create a new parameter value for a } }, "experimental": { - "default": ["string"], + "default": ["vscode_local"], "enterprise": true, "flag": "string", "hidden": true, @@ -1354,7 +1354,7 @@ CreateParameterRequest is a structure used to create a new parameter value for a "secret": true, "shorthand": "string", "usage": "string", - "value": ["string"] + "value": ["vscode_local"] }, "gitauth": { "default": [ @@ -2202,7 +2202,7 @@ CreateParameterRequest is a structure used to create a new parameter value for a ```json { - "default": ["string"], + "default": ["vscode_local"], "enterprise": true, "flag": "string", "hidden": true, @@ -2210,23 +2210,23 @@ CreateParameterRequest is a structure used to create a new parameter value for a "secret": true, "shorthand": "string", "usage": "string", - "value": ["string"] + "value": ["vscode_local"] } ``` ### Properties -| Name | Type | Required | Restrictions | Description | -| ------------ | --------------- | -------- | ------------ | ----------- | -| `default` | array of string | false | | | -| `enterprise` | boolean | false | | | -| `flag` | string | false | | | -| `hidden` | boolean | false | | | -| `name` | string | false | | | -| `secret` | boolean | false | | | -| `shorthand` | string | false | | | -| `usage` | string | false | | | -| `value` | array of string | false | | | +| Name | Type | Required | Restrictions | Description | +| ------------ | --------------------------------------------------- | -------- | ------------ | ----------- | +| `default` | array of [codersdk.Experiment](#codersdkexperiment) | false | | | +| `enterprise` | boolean | false | | | +| `flag` | string | false | | | +| `hidden` | boolean | false | | | +| `name` | string | false | | | +| `secret` | boolean | false | | | +| `shorthand` | string | false | | | +| `usage` | string | false | | | +| `value` | array of [codersdk.Experiment](#codersdkexperiment) | false | | | ## codersdk.DeploymentConfigField-int @@ -2372,6 +2372,20 @@ CreateParameterRequest is a structure used to create a new parameter value for a | `trial` | boolean | false | | | | `warnings` | array of string | false | | | +## codersdk.Experiment + +```json +"vscode_local" +``` + +### Properties + +#### Enumerated Values + +| Value | +| -------------- | +| `vscode_local` | + ## codersdk.Feature ```json diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 52e069cfd5541..f1798e2886f33 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -636,7 +636,7 @@ export const getEntitlements = async (): Promise => { } } -export const getExperiments = async (): Promise => { +export const getExperiments = async (): Promise => { try { const response = await axios.get("/api/v2/experiments") return response.data diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 75741046ce3c0..fa14353b28259 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -342,7 +342,7 @@ export interface Entitlements { } // From codersdk/experiments.go -export type Experiments = string[] +export type Experiments = Experiment[] // From codersdk/features.go export interface Feature { @@ -1082,6 +1082,10 @@ export const Entitlements: Entitlement[] = [ "not_entitled", ] +// From codersdk/experiments.go +export type Experiment = "vscode_local" +export const Experiments: Experiment[] = ["vscode_local"] + // From codersdk/features.go export type FeatureName = | "appearance" diff --git a/site/src/xServices/experiments/experimentsMachine.ts b/site/src/xServices/experiments/experimentsMachine.ts index 99709fb12b379..25acab364bf0b 100644 --- a/site/src/xServices/experiments/experimentsMachine.ts +++ b/site/src/xServices/experiments/experimentsMachine.ts @@ -1,8 +1,9 @@ import { getExperiments } from "api/api" +import { Experiment } from "api/typesGenerated" import { createMachine, assign } from "xstate" export interface ExperimentsContext { - experiments?: string[] + experiments?: Experiment[] getExperimentsError?: Error | unknown } @@ -15,7 +16,7 @@ export const experimentsMachine = createMachine( context: {} as ExperimentsContext, services: {} as { getExperiments: { - data: string[] + data: Experiment[] } }, }, From 53d92d7f4b47169fa4eec0625eb9c98300545ea2 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 18 Jan 2023 14:05:30 +0000 Subject: [PATCH 09/17] make it clear that Entitlements.Experimental is deprecated --- codersdk/features.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/codersdk/features.go b/codersdk/features.go index 239b5800e4cb6..9ad04ea79afee 100644 --- a/codersdk/features.go +++ b/codersdk/features.go @@ -79,9 +79,10 @@ type Entitlements struct { Warnings []string `json:"warnings"` Errors []string `json:"errors"` HasLicense bool `json:"has_license"` + Trial bool `json:"trial"` + // DEPRECATED: use Experiments instead. Experimental bool `json:"experimental"` - Trial bool `json:"trial"` } func (c *Client) Entitlements(ctx context.Context) (Entitlements, error) { From ef0cbc329e89d9e1d8343177c9228c66f46376d4 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 18 Jan 2023 15:34:43 +0000 Subject: [PATCH 10/17] remove unnecessary types as they are now autogenerated --- site/src/api/types.ts | 16 ---------------- .../pages/WorkspacePage/WorkspaceReadyPage.tsx | 5 ++--- 2 files changed, 2 insertions(+), 19 deletions(-) diff --git a/site/src/api/types.ts b/site/src/api/types.ts index 7e3876711597f..daf4e451ac5e8 100644 --- a/site/src/api/types.ts +++ b/site/src/api/types.ts @@ -14,19 +14,3 @@ export interface ReconnectingPTYRequest { export type WorkspaceBuildTransition = "start" | "stop" | "delete" export type Message = { message: string } - -// Keep up to date with coder/codersdk/features.go -export enum FeatureNames { - AuditLog = "audit_log", - UserLimit = "user_limit", - BrowserOnly = "browser_only", - SCIM = "scim", - TemplateRBAC = "template_rbac", - HighAvailability = "high_availability", - Appearance = "appearance", -} - -// Keep up to date with coder/codersdk/experiments.go -export enum Experiments { - VSCodeLocal = "vscode_local", -} diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index 8754879b65f8c..0041901b01897 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -1,5 +1,4 @@ import { useActor, useSelector } from "@xstate/react" -import { Experiments, FeatureNames } from "api/types" import dayjs from "dayjs" import { useContext, useEffect } from "react" import { Helmet } from "react-helmet-async" @@ -125,8 +124,8 @@ export const WorkspaceReadyPage = ({ canUpdateWorkspace={canUpdateWorkspace} hideSSHButton={featureVisibility["browser_only"]} hideVSCodeDesktopButton={ - !experiments.includes(Experiments.VSCodeLocal) || - featureVisibility[FeatureNames.BrowserOnly] + !experiments.includes("vscode_local") || + featureVisibility["browser_only"] } workspaceErrors={{ [WorkspaceErrors.GET_RESOURCES_ERROR]: refreshWorkspaceWarning, From 8fdb21b5debb0a9d0b366f1b77ec3766495037fa Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 18 Jan 2023 15:47:03 +0000 Subject: [PATCH 11/17] apply suggestions from code review --- cli/deployment/config.go | 36 +++++--------- cli/deployment/config_test.go | 14 +++--- cli/testdata/coder_server_--help.golden | 4 +- coderd/apidoc/docs.go | 57 ++++----------------- coderd/apidoc/swagger.json | 53 ++++---------------- coderd/coderd.go | 24 +++++++++ coderd/experiments.go | 2 +- coderd/experiments_test.go | 66 +++++++++++++++++++------ codersdk/deploymentconfig.go | 5 +- codersdk/experiments.go | 2 +- docs/api/general.md | 15 +++++- docs/api/schemas.md | 62 ++++++----------------- enterprise/coderd/coderd.go | 2 +- site/src/api/typesGenerated.ts | 5 +- 14 files changed, 152 insertions(+), 195 deletions(-) diff --git a/cli/deployment/config.go b/cli/deployment/config.go index 66c0db20361e2..77b1130a9ded1 100644 --- a/cli/deployment/config.go +++ b/cli/deployment/config.go @@ -446,11 +446,19 @@ func newConfig() *codersdk.DeploymentConfig { Default: 512, }, }, - Experimental: &codersdk.DeploymentConfigField[codersdk.Experiments]{ + // DEPRECATED: use Experiments instead. + Experimental: &codersdk.DeploymentConfigField[bool]{ Name: "Experimental", - Usage: "Enable one or more experiments. These are not ready for production. Separate multiple experiments with commas, or enter '*' to opt-in to all available experiments.", + Usage: "Enable experimental features. Experimental features are not ready for production.", Flag: "experimental", - Default: []codersdk.Experiment{}, + Default: false, + Hidden: true, + }, + Experiments: &codersdk.DeploymentConfigField[[]string]{ + Name: "Experiments", + Usage: "Enable one or more experiments. These are not ready for production. Separate multiple experiments with commas, or enter '*' to opt-in to all available experiments.", + Flag: "experiments", + Default: []string{}, }, UpdateCheck: &codersdk.DeploymentConfigField[bool]{ Name: "Update Check", @@ -567,28 +575,6 @@ func setConfig(prefix string, vip *viper.Viper, target interface{}) { value = append(value, strings.Split(entry, ",")...) } val.FieldByName("Value").Set(reflect.ValueOf(value)) - case codersdk.Experiments: - // As []string above, but we support setting wildcard values - // '*' or 'true' to enable experiments listed in codersdk.ExperimentsAll. - // Experiments not listed in codersdk.ExperimentsAll must be enabled - // explicitly. - vip.MustBindEnv(prefix, env) - rawSlice := reflect.ValueOf(vip.GetStringSlice(prefix)).Interface() - stringSlice, ok := rawSlice.([]string) - if !ok { - panic(fmt.Sprintf("string slice is of type %T", rawSlice)) - } - value := make([]codersdk.Experiment, 0, len(stringSlice)) - for _, entry := range stringSlice { - for _, val := range strings.Split(entry, ",") { - if val == "*" || val == "true" { - value = append(value, codersdk.ExperimentsAll...) - } else { - value = append(value, codersdk.Experiment(val)) - } - } - } - val.FieldByName("Value").Set(reflect.ValueOf(codersdk.Experiments(value))) case []codersdk.GitAuthConfig: // Do not bind to CODER_GITAUTH, instead bind to CODER_GITAUTH_0_*, etc. values := readSliceFromViper[codersdk.GitAuthConfig](vip, prefix, value) diff --git a/cli/deployment/config_test.go b/cli/deployment/config_test.go index 31ad58c7fa7a2..d6d5b93d98043 100644 --- a/cli/deployment/config_test.go +++ b/cli/deployment/config_test.go @@ -233,21 +233,21 @@ func TestConfig(t *testing.T) { require.Equal(t, config.Prometheus.Address.Value, config.Prometheus.Address.Default) }, }, { - Name: "Experimental - no features", + Name: "Experiments - no features", Env: map[string]string{ - "CODER_EXPERIMENTAL": "", + "CODER_EXPERIMENTS": "", }, Valid: func(config *codersdk.DeploymentConfig) { - require.Empty(t, config.Experimental.Value) + require.Empty(t, config.Experiments.Value) }, }, { - Name: "Experimental - multiple features", + Name: "Experiments - multiple features", Env: map[string]string{ - "CODER_EXPERIMENTAL": "foo,bar", + "CODER_EXPERIMENTS": "foo,bar", }, Valid: func(config *codersdk.DeploymentConfig) { - expected := []codersdk.Experiment{"foo", "bar"} - require.ElementsMatch(t, expected, config.Experimental.Value) + expected := []string{"foo", "bar"} + require.ElementsMatch(t, expected, config.Experiments.Value) }, }} { tc := tc diff --git a/cli/testdata/coder_server_--help.golden b/cli/testdata/coder_server_--help.golden index 1936d07a31e3a..797ca82d5d8be 100644 --- a/cli/testdata/coder_server_--help.golden +++ b/cli/testdata/coder_server_--help.golden @@ -61,12 +61,12 @@ Flags: Consumes $CODER_DERP_SERVER_STUN_ADDRESSES (default [stun.l.google.com:19302]) - --experimental strings Enable one or more experiments. These are + --experiments strings Enable one or more experiments. These are not ready for production. Separate multiple experiments with commas, or enter '*' to opt-in to all available experiments. - Consumes $CODER_EXPERIMENTAL + Consumes $CODER_EXPERIMENTS -h, --help help for server --http-address string HTTP bind address of the server. Unset to disable the HTTP endpoint. diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index bcf671852e4fc..1cd63992f22b2 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -5768,7 +5768,15 @@ const docTemplate = `{ "$ref": "#/definitions/codersdk.DERP" }, "experimental": { - "$ref": "#/definitions/codersdk.DeploymentConfigField-codersdk_Experiments" + "description": "DEPRECATED: Use Experiments instead.", + "allOf": [ + { + "$ref": "#/definitions/codersdk.DeploymentConfigField-bool" + } + ] + }, + "experiments": { + "$ref": "#/definitions/codersdk.DeploymentConfigField-array_string" }, "gitauth": { "$ref": "#/definitions/codersdk.DeploymentConfigField-array_codersdk_GitAuthConfig" @@ -5952,44 +5960,6 @@ const docTemplate = `{ } } }, - "codersdk.DeploymentConfigField-codersdk_Experiments": { - "type": "object", - "properties": { - "default": { - "type": "array", - "items": { - "$ref": "#/definitions/codersdk.Experiment" - } - }, - "enterprise": { - "type": "boolean" - }, - "flag": { - "type": "string" - }, - "hidden": { - "type": "boolean" - }, - "name": { - "type": "string" - }, - "secret": { - "type": "boolean" - }, - "shorthand": { - "type": "string" - }, - "usage": { - "type": "string" - }, - "value": { - "type": "array", - "items": { - "$ref": "#/definitions/codersdk.Experiment" - } - } - } - }, "codersdk.DeploymentConfigField-int": { "type": "object", "properties": { @@ -6132,15 +6102,6 @@ const docTemplate = `{ } } }, - "codersdk.Experiment": { - "type": "string", - "enum": [ - "vscode_local" - ], - "x-enum-varnames": [ - "ExperimentVSCodeLocal" - ] - }, "codersdk.Feature": { "type": "object", "properties": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index bc6f1f622cdd9..68a4b96850a00 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -5117,7 +5117,15 @@ "$ref": "#/definitions/codersdk.DERP" }, "experimental": { - "$ref": "#/definitions/codersdk.DeploymentConfigField-codersdk_Experiments" + "description": "DEPRECATED: Use Experiments instead.", + "allOf": [ + { + "$ref": "#/definitions/codersdk.DeploymentConfigField-bool" + } + ] + }, + "experiments": { + "$ref": "#/definitions/codersdk.DeploymentConfigField-array_string" }, "gitauth": { "$ref": "#/definitions/codersdk.DeploymentConfigField-array_codersdk_GitAuthConfig" @@ -5301,44 +5309,6 @@ } } }, - "codersdk.DeploymentConfigField-codersdk_Experiments": { - "type": "object", - "properties": { - "default": { - "type": "array", - "items": { - "$ref": "#/definitions/codersdk.Experiment" - } - }, - "enterprise": { - "type": "boolean" - }, - "flag": { - "type": "string" - }, - "hidden": { - "type": "boolean" - }, - "name": { - "type": "string" - }, - "secret": { - "type": "boolean" - }, - "shorthand": { - "type": "string" - }, - "usage": { - "type": "string" - }, - "value": { - "type": "array", - "items": { - "$ref": "#/definitions/codersdk.Experiment" - } - } - } - }, "codersdk.DeploymentConfigField-int": { "type": "object", "properties": { @@ -5477,11 +5447,6 @@ } } }, - "codersdk.Experiment": { - "type": "string", - "enum": ["vscode_local"], - "x-enum-varnames": ["ExperimentVSCodeLocal"] - }, "codersdk.Feature": { "type": "object", "properties": { diff --git a/coderd/coderd.go b/coderd/coderd.go index 4459ec76689e9..7a8b3f4c3cf8b 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -220,6 +220,7 @@ func New(options *Options) *API { }, metricsCache: metricsCache, Auditor: atomic.Pointer[audit.Auditor]{}, + Experiments: initExperiments(options.DeploymentConfig.Experiments.Value, options.DeploymentConfig.Experimental.Value), } if options.UpdateCheckOptions != nil { api.updateChecker = updatecheck.New( @@ -650,6 +651,10 @@ type API struct { metricsCache *metricscache.Cache workspaceAgentCache *wsconncache.Cache updateChecker *updatecheck.Checker + + // Experiments contains the list of experiments currently enabled. + // This is used to gate features that are not yet ready for production. + Experiments codersdk.Experiments } // Close waits for all WebSocket connections to drain before returning. @@ -756,3 +761,22 @@ func (api *API) CreateInMemoryProvisionerDaemon(ctx context.Context, debounce ti return proto.NewDRPCProvisionerDaemonClient(clientSession), nil } + +// nolint:revive +func initExperiments(raw []string, legacyAll bool) codersdk.Experiments { + exp := make([]codersdk.Experiment, 0, len(raw)) + for _, v := range raw { + switch v { + case "*": + exp = append(exp, codersdk.ExperimentsAll...) + default: + exp = append(exp, codersdk.Experiment(v)) + } + } + + // --experiments takes precedence over --experimental. It's deprecated. + if legacyAll && len(raw) == 0 { + exp = append(exp, codersdk.ExperimentsAll...) + } + return exp +} diff --git a/coderd/experiments.go b/coderd/experiments.go index 565b79b283bbb..27816e25c2eaa 100644 --- a/coderd/experiments.go +++ b/coderd/experiments.go @@ -15,5 +15,5 @@ import ( // @Router /experiments [get] func (api *API) handleExperimentsGet(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() - httpapi.Write(ctx, rw, http.StatusOK, api.DeploymentConfig.Experimental.Value) + httpapi.Write(ctx, rw, http.StatusOK, api.Experiments) } diff --git a/coderd/experiments_test.go b/coderd/experiments_test.go index 529add45a64b5..a042d1bba472e 100644 --- a/coderd/experiments_test.go +++ b/coderd/experiments_test.go @@ -12,14 +12,14 @@ import ( "github.com/coder/coder/testutil" ) -// nolint:paralleltest // t.Setenv func Test_Experiments(t *testing.T) { - // In these tests we set the environment variable directly, rather than - // modifying the deployment config. This corresponds more directly to what - // an end user would do. Also, modifying the deployment config doesn't work - // for expanding the wildcard because of how viper binds stuff. + t.Parallel() t.Run("empty", func(t *testing.T) { - client := coderdtest.New(t, nil) + t.Parallel() + cfg := coderdtest.DeploymentConfig(t) + client := coderdtest.New(t, &coderdtest.Options{ + DeploymentConfig: cfg, + }) _ = coderdtest.CreateFirstUser(t, client) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) @@ -34,8 +34,12 @@ func Test_Experiments(t *testing.T) { }) t.Run("multiple features", func(t *testing.T) { - t.Setenv("CODER_EXPERIMENTAL", "foo,bar") - client := coderdtest.New(t, nil) + t.Parallel() + cfg := coderdtest.DeploymentConfig(t) + cfg.Experiments.Value = []string{"foo", "bar"} + client := coderdtest.New(t, &coderdtest.Options{ + DeploymentConfig: cfg, + }) _ = coderdtest.CreateFirstUser(t, client) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) @@ -51,8 +55,12 @@ func Test_Experiments(t *testing.T) { }) t.Run("wildcard", func(t *testing.T) { - t.Setenv("CODER_EXPERIMENTAL", "*") - client := coderdtest.New(t, nil) + t.Parallel() + cfg := coderdtest.DeploymentConfig(t) + cfg.Experiments.Value = []string{"*"} + client := coderdtest.New(t, &coderdtest.Options{ + DeploymentConfig: cfg, + }) _ = coderdtest.CreateFirstUser(t, client) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) @@ -69,8 +77,12 @@ func Test_Experiments(t *testing.T) { }) t.Run("alternate wildcard with manual opt-in", func(t *testing.T) { - t.Setenv("CODER_EXPERIMENTAL", "true,danger") - client := coderdtest.New(t, nil) + t.Parallel() + cfg := coderdtest.DeploymentConfig(t) + cfg.Experiments.Value = []string{"*", "danger"} + client := coderdtest.New(t, &coderdtest.Options{ + DeploymentConfig: cfg, + }) _ = coderdtest.CreateFirstUser(t, client) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) @@ -87,9 +99,35 @@ func Test_Experiments(t *testing.T) { require.False(t, experiments.Enabled("herebedragons")) }) + t.Run("legacy wildcard", func(t *testing.T) { + t.Parallel() + cfg := coderdtest.DeploymentConfig(t) + cfg.Experimental.Value = true + client := coderdtest.New(t, &coderdtest.Options{ + DeploymentConfig: cfg, + }) + _ = coderdtest.CreateFirstUser(t, client) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + experiments, err := client.Experiments(ctx) + require.NoError(t, err) + require.NotNil(t, experiments) + require.ElementsMatch(t, codersdk.ExperimentsAll, experiments) + for _, ex := range codersdk.ExperimentsAll { + require.True(t, experiments.Enabled(ex)) + } + require.False(t, experiments.Enabled("danger")) + }) + t.Run("Unauthorized", func(t *testing.T) { - t.Setenv("CODER_EXPERIMENTAL", "foo,bar") - client := coderdtest.New(t, nil) + t.Parallel() + cfg := coderdtest.DeploymentConfig(t) + cfg.Experiments.Value = []string{"*"} + client := coderdtest.New(t, &coderdtest.Options{ + DeploymentConfig: cfg, + }) // Explicitly omit creating a user so we're unauthorized. // _ = coderdtest.CreateFirstUser(t, client) diff --git a/codersdk/deploymentconfig.go b/codersdk/deploymentconfig.go index 983e6ace37299..b129a75f7456a 100644 --- a/codersdk/deploymentconfig.go +++ b/codersdk/deploymentconfig.go @@ -41,11 +41,14 @@ type DeploymentConfig struct { SCIMAPIKey *DeploymentConfigField[string] `json:"scim_api_key" typescript:",notnull"` Provisioner *ProvisionerConfig `json:"provisioner" typescript:",notnull"` RateLimit *RateLimitConfig `json:"rate_limit" typescript:",notnull"` - Experimental *DeploymentConfigField[Experiments] `json:"experimental" typescript:",notnull"` + Experiments *DeploymentConfigField[[]string] `json:"experiments" typescript:",notnull"` UpdateCheck *DeploymentConfigField[bool] `json:"update_check" typescript:",notnull"` MaxTokenLifetime *DeploymentConfigField[time.Duration] `json:"max_token_lifetime" typescript:",notnull"` Swagger *SwaggerConfig `json:"swagger" typescript:",notnull"` Logging *LoggingConfig `json:"logging" typescript:",notnull"` + + // DEPRECATED: Use Experiments instead. + Experimental *DeploymentConfigField[bool] `json:"experimental" typescript:",notnull"` } type DERP struct { diff --git a/codersdk/experiments.go b/codersdk/experiments.go index 0a90ba2dea988..0d6f1b78f5582 100644 --- a/codersdk/experiments.go +++ b/codersdk/experiments.go @@ -18,7 +18,7 @@ var ( // ExperimentsAll should include all experiments that are safe for // users to opt-in to via --experimental='*'. // Experiments that are not ready for consumption by all users should - // not be included here. + // not be included here and will be essentially hidden. ExperimentsAll = Experiments{ ExperimentVSCodeLocal, } diff --git a/docs/api/general.md b/docs/api/general.md index a0d29f315f00a..822ba9153c822 100644 --- a/docs/api/general.md +++ b/docs/api/general.md @@ -266,7 +266,18 @@ curl -X GET http://coder-server:8080/api/v2/config/deployment \ } }, "experimental": { - "default": ["vscode_local"], + "default": true, + "enterprise": true, + "flag": "string", + "hidden": true, + "name": "string", + "secret": true, + "shorthand": "string", + "usage": "string", + "value": true + }, + "experiments": { + "default": ["string"], "enterprise": true, "flag": "string", "hidden": true, @@ -274,7 +285,7 @@ curl -X GET http://coder-server:8080/api/v2/config/deployment \ "secret": true, "shorthand": "string", "usage": "string", - "value": ["vscode_local"] + "value": ["string"] }, "gitauth": { "default": [ diff --git a/docs/api/schemas.md b/docs/api/schemas.md index 37016d98c0ff8..2859909c87997 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -1346,7 +1346,18 @@ CreateParameterRequest is a structure used to create a new parameter value for a } }, "experimental": { - "default": ["vscode_local"], + "default": true, + "enterprise": true, + "flag": "string", + "hidden": true, + "name": "string", + "secret": true, + "shorthand": "string", + "usage": "string", + "value": true + }, + "experiments": { + "default": ["string"], "enterprise": true, "flag": "string", "hidden": true, @@ -1354,7 +1365,7 @@ CreateParameterRequest is a structure used to create a new parameter value for a "secret": true, "shorthand": "string", "usage": "string", - "value": ["vscode_local"] + "value": ["string"] }, "gitauth": { "default": [ @@ -2058,7 +2069,8 @@ CreateParameterRequest is a structure used to create a new parameter value for a | `browser_only` | [codersdk.DeploymentConfigField-bool](#codersdkdeploymentconfigfield-bool) | false | | | | `cache_directory` | [codersdk.DeploymentConfigField-string](#codersdkdeploymentconfigfield-string) | false | | | | `derp` | [codersdk.DERP](#codersdkderp) | false | | | -| `experimental` | [codersdk.DeploymentConfigField-codersdk_Experiments](#codersdkdeploymentconfigfield-codersdk_experiments) | false | | | +| `experimental` | [codersdk.DeploymentConfigField-bool](#codersdkdeploymentconfigfield-bool) | false | | Experimental Use Experiments instead. | +| `experiments` | [codersdk.DeploymentConfigField-array_string](#codersdkdeploymentconfigfield-array_string) | false | | | | `gitauth` | [codersdk.DeploymentConfigField-array_codersdk_GitAuthConfig](#codersdkdeploymentconfigfield-array_codersdk_gitauthconfig) | false | | | | `http_address` | [codersdk.DeploymentConfigField-string](#codersdkdeploymentconfigfield-string) | false | | | | `in_memory_database` | [codersdk.DeploymentConfigField-bool](#codersdkdeploymentconfigfield-bool) | false | | | @@ -2198,36 +2210,6 @@ CreateParameterRequest is a structure used to create a new parameter value for a | `usage` | string | false | | | | `value` | boolean | false | | | -## codersdk.DeploymentConfigField-codersdk_Experiments - -```json -{ - "default": ["vscode_local"], - "enterprise": true, - "flag": "string", - "hidden": true, - "name": "string", - "secret": true, - "shorthand": "string", - "usage": "string", - "value": ["vscode_local"] -} -``` - -### Properties - -| Name | Type | Required | Restrictions | Description | -| ------------ | --------------------------------------------------- | -------- | ------------ | ----------- | -| `default` | array of [codersdk.Experiment](#codersdkexperiment) | false | | | -| `enterprise` | boolean | false | | | -| `flag` | string | false | | | -| `hidden` | boolean | false | | | -| `name` | string | false | | | -| `secret` | boolean | false | | | -| `shorthand` | string | false | | | -| `usage` | string | false | | | -| `value` | array of [codersdk.Experiment](#codersdkexperiment) | false | | | - ## codersdk.DeploymentConfigField-int ```json @@ -2372,20 +2354,6 @@ CreateParameterRequest is a structure used to create a new parameter value for a | `trial` | boolean | false | | | | `warnings` | array of string | false | | | -## codersdk.Experiment - -```json -"vscode_local" -``` - -### Properties - -#### Enumerated Values - -| Value | -| -------------- | -| `vscode_local` | - ## codersdk.Feature ```json diff --git a/enterprise/coderd/coderd.go b/enterprise/coderd/coderd.go index d916f813e43cc..f5d34df4f6e3e 100644 --- a/enterprise/coderd/coderd.go +++ b/enterprise/coderd/coderd.go @@ -250,7 +250,7 @@ func (api *API) updateEntitlements(ctx context.Context) error { if err != nil { return err } - entitlements.Experimental = len(api.DeploymentConfig.Experimental.Value) > 0 + entitlements.Experimental = api.DeploymentConfig.Experimental.Value || len(api.AGPL.Experiments) != 0 featureChanged := func(featureName codersdk.FeatureName) (changed bool, enabled bool) { if api.entitlements.Features == nil { diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index fa14353b28259..63ffce045f4a1 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -311,11 +311,12 @@ export interface DeploymentConfig { readonly scim_api_key: DeploymentConfigField readonly provisioner: ProvisionerConfig readonly rate_limit: RateLimitConfig - readonly experimental: DeploymentConfigField + readonly experiments: DeploymentConfigField readonly update_check: DeploymentConfigField readonly max_token_lifetime: DeploymentConfigField readonly swagger: SwaggerConfig readonly logging: LoggingConfig + readonly experimental: DeploymentConfigField } // From codersdk/deploymentconfig.go @@ -337,8 +338,8 @@ export interface Entitlements { readonly warnings: string[] readonly errors: string[] readonly has_license: boolean - readonly experimental: boolean readonly trial: boolean + readonly experimental: boolean } // From codersdk/experiments.go From 0cb9a6a4c3f577a7234d2c43d5ac3dae3e48f94e Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 18 Jan 2023 16:21:25 +0000 Subject: [PATCH 12/17] fixup! apply suggestions from code review --- site/src/testHelpers/entities.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 42216689b68f6..1b79af3fcdd0a 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -982,7 +982,7 @@ export const MockEntitlementsWithAuditLog: TypesGen.Entitlements = { }), } -export const MockExperiments: string[] = [Types.Experiments.VSCodeLocal] +export const MockExperiments: TypesGen.Experiment[] = ["vscode_local"] export const MockAuditLog: TypesGen.AuditLog = { id: "fbd2116a-8961-4954-87ae-e4575bd29ce0", From 44e963854a51e106b75552c15ff1d7518b338cc9 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 18 Jan 2023 16:34:40 +0000 Subject: [PATCH 13/17] warn about dragons --- coderd/coderd.go | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/coderd/coderd.go b/coderd/coderd.go index 7a8b3f4c3cf8b..614ccdbb3b64a 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -52,6 +52,7 @@ import ( "github.com/coder/coder/coderd/telemetry" "github.com/coder/coder/coderd/tracing" "github.com/coder/coder/coderd/updatecheck" + "github.com/coder/coder/coderd/util/slice" "github.com/coder/coder/coderd/wsconncache" "github.com/coder/coder/codersdk" "github.com/coder/coder/provisionerd/proto" @@ -220,7 +221,7 @@ func New(options *Options) *API { }, metricsCache: metricsCache, Auditor: atomic.Pointer[audit.Auditor]{}, - Experiments: initExperiments(options.DeploymentConfig.Experiments.Value, options.DeploymentConfig.Experimental.Value), + Experiments: initExperiments(options.Logger, options.DeploymentConfig.Experiments.Value, options.DeploymentConfig.Experimental.Value), } if options.UpdateCheckOptions != nil { api.updateChecker = updatecheck.New( @@ -763,20 +764,25 @@ func (api *API) CreateInMemoryProvisionerDaemon(ctx context.Context, debounce ti } // nolint:revive -func initExperiments(raw []string, legacyAll bool) codersdk.Experiments { - exp := make([]codersdk.Experiment, 0, len(raw)) +func initExperiments(log slog.Logger, raw []string, legacyAll bool) codersdk.Experiments { + exps := make([]codersdk.Experiment, 0, len(raw)) for _, v := range raw { switch v { case "*": - exp = append(exp, codersdk.ExperimentsAll...) + exps = append(exps, codersdk.ExperimentsAll...) default: - exp = append(exp, codersdk.Experiment(v)) + ex := codersdk.Experiment(v) + if !slice.Contains(codersdk.ExperimentsAll, ex) { + log.Warn(context.Background(), "๐Ÿ‰ HERE BE DRAGONS: opting into hidden experiment", slog.F("experiment", ex)) + } + exps = append(exps, ex) } } // --experiments takes precedence over --experimental. It's deprecated. if legacyAll && len(raw) == 0 { - exp = append(exp, codersdk.ExperimentsAll...) + log.Warn(context.Background(), "--experimental is deprecated, use --experiments='*' instead") + exps = append(exps, codersdk.ExperimentsAll...) } - return exp + return exps } From 6c32aa57555deff683fd256aa2e17c5ca7aead4d Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 18 Jan 2023 16:37:28 +0000 Subject: [PATCH 14/17] fix apidoc type --- coderd/apidoc/docs.go | 11 ++++++++++- coderd/apidoc/swagger.json | 7 ++++++- coderd/experiments.go | 2 +- docs/api/general.md | 14 ++++++++++---- docs/api/schemas.md | 14 ++++++++++++++ 5 files changed, 41 insertions(+), 7 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 1cd63992f22b2..3355084edd85a 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -408,7 +408,7 @@ const docTemplate = `{ "schema": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/codersdk.Experiment" } } } @@ -6102,6 +6102,15 @@ const docTemplate = `{ } } }, + "codersdk.Experiment": { + "type": "string", + "enum": [ + "vscode_local" + ], + "x-enum-varnames": [ + "ExperimentVSCodeLocal" + ] + }, "codersdk.Feature": { "type": "object", "properties": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 68a4b96850a00..50339d9e3e44b 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -346,7 +346,7 @@ "schema": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/codersdk.Experiment" } } } @@ -5447,6 +5447,11 @@ } } }, + "codersdk.Experiment": { + "type": "string", + "enum": ["vscode_local"], + "x-enum-varnames": ["ExperimentVSCodeLocal"] + }, "codersdk.Feature": { "type": "object", "properties": { diff --git a/coderd/experiments.go b/coderd/experiments.go index 27816e25c2eaa..651ae3155c369 100644 --- a/coderd/experiments.go +++ b/coderd/experiments.go @@ -11,7 +11,7 @@ import ( // @Security CoderSessionToken // @Produce json // @Tags General -// @Success 200 {array} string +// @Success 200 {array} codersdk.Experiment // @Router /experiments [get] func (api *API) handleExperimentsGet(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() diff --git a/docs/api/general.md b/docs/api/general.md index 822ba9153c822..447e6dde907ad 100644 --- a/docs/api/general.md +++ b/docs/api/general.md @@ -1037,17 +1037,23 @@ curl -X GET http://coder-server:8080/api/v2/experiments \ > 200 Response ```json -["string"] +["vscode_local"] ``` ### Responses -| Status | Meaning | Description | Schema | -| ------ | ------------------------------------------------------- | ----------- | --------------- | -| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | array of string | +| Status | Meaning | Description | Schema | +| ------ | ------------------------------------------------------- | ----------- | ------------------------------------------------------------- | +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | array of [codersdk.Experiment](schemas.md#codersdkexperiment) |

Response Schema

+Status Code **200** + +| Name | Type | Required | Restrictions | Description | +| -------------- | ----- | -------- | ------------ | ----------- | +| `[array item]` | array | false | | | + To perform this operation, you must be authenticated. [Learn more](authentication.md). ## Update check diff --git a/docs/api/schemas.md b/docs/api/schemas.md index 2859909c87997..83cb1ba074c80 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -2354,6 +2354,20 @@ CreateParameterRequest is a structure used to create a new parameter value for a | `trial` | boolean | false | | | | `warnings` | array of string | false | | | +## codersdk.Experiment + +```json +"vscode_local" +``` + +### Properties + +#### Enumerated Values + +| Value | +| -------------- | +| `vscode_local` | + ## codersdk.Feature ```json From 1cbaaf72237baf7b12c023d85a732054f0fd1d4c Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 18 Jan 2023 16:47:13 +0000 Subject: [PATCH 15/17] codersdk.Experiments no longer needs be flaggable or special-cased in DeploymentConfig reflection --- cli/deployment/config.go | 2 +- codersdk/deploymentconfig.go | 2 +- site/src/api/typesGenerated.ts | 8 +------- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/cli/deployment/config.go b/cli/deployment/config.go index 77b1130a9ded1..3a36218277c7e 100644 --- a/cli/deployment/config.go +++ b/cli/deployment/config.go @@ -752,7 +752,7 @@ func setFlags(prefix string, flagset *pflag.FlagSet, vip *viper.Viper, target in _ = flagset.IntP(flg, shorthand, vip.GetInt(prefix), usage) case time.Duration: _ = flagset.DurationP(flg, shorthand, vip.GetDuration(prefix), usage) - case []string, codersdk.Experiments: + case []string: _ = flagset.StringSliceP(flg, shorthand, vip.GetStringSlice(prefix), usage) case []codersdk.GitAuthConfig: // Ignore this one! diff --git a/codersdk/deploymentconfig.go b/codersdk/deploymentconfig.go index b129a75f7456a..1b7c9ddf072a3 100644 --- a/codersdk/deploymentconfig.go +++ b/codersdk/deploymentconfig.go @@ -166,7 +166,7 @@ type LoggingConfig struct { } type Flaggable interface { - string | time.Duration | bool | int | []string | []GitAuthConfig | Experiments + string | time.Duration | bool | int | []string | []GitAuthConfig } type DeploymentConfigField[T Flaggable] struct { diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 63ffce045f4a1..6793d66ca0fbf 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1285,10 +1285,4 @@ export const WorkspaceTransitions: WorkspaceTransition[] = [ ] // From codersdk/deploymentconfig.go -export type Flaggable = - | string - | number - | boolean - | string[] - | GitAuthConfig[] - | Experiments +export type Flaggable = string | number | boolean | string[] | GitAuthConfig[] From 67d07f3f905a21d2251009f599900d593227a4d8 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 18 Jan 2023 17:16:04 +0000 Subject: [PATCH 16/17] lower-case when ingesting and comparing experiments --- coderd/coderd.go | 3 ++- coderd/experiments_test.go | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/coderd/coderd.go b/coderd/coderd.go index 614ccdbb3b64a..5429eeee9cad9 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -11,6 +11,7 @@ import ( "net/url" "path/filepath" "regexp" + "strings" "sync" "sync/atomic" "time" @@ -771,7 +772,7 @@ func initExperiments(log slog.Logger, raw []string, legacyAll bool) codersdk.Exp case "*": exps = append(exps, codersdk.ExperimentsAll...) default: - ex := codersdk.Experiment(v) + ex := codersdk.Experiment(strings.ToLower(v)) if !slice.Contains(codersdk.ExperimentsAll, ex) { log.Warn(context.Background(), "๐Ÿ‰ HERE BE DRAGONS: opting into hidden experiment", slog.F("experiment", ex)) } diff --git a/coderd/experiments_test.go b/coderd/experiments_test.go index a042d1bba472e..7a9da5370703a 100644 --- a/coderd/experiments_test.go +++ b/coderd/experiments_test.go @@ -36,7 +36,7 @@ func Test_Experiments(t *testing.T) { t.Run("multiple features", func(t *testing.T) { t.Parallel() cfg := coderdtest.DeploymentConfig(t) - cfg.Experiments.Value = []string{"foo", "bar"} + cfg.Experiments.Value = []string{"foo", "BAR"} client := coderdtest.New(t, &coderdtest.Options{ DeploymentConfig: cfg, }) @@ -48,6 +48,7 @@ func Test_Experiments(t *testing.T) { experiments, err := client.Experiments(ctx) require.NoError(t, err) require.NotNil(t, experiments) + // Should be lower-cased. require.ElementsMatch(t, []codersdk.Experiment{"foo", "bar"}, experiments) require.True(t, experiments.Enabled("foo")) require.True(t, experiments.Enabled("bar")) @@ -79,7 +80,7 @@ func Test_Experiments(t *testing.T) { t.Run("alternate wildcard with manual opt-in", func(t *testing.T) { t.Parallel() cfg := coderdtest.DeploymentConfig(t) - cfg.Experiments.Value = []string{"*", "danger"} + cfg.Experiments.Value = []string{"*", "dAnGeR"} client := coderdtest.New(t, &coderdtest.Options{ DeploymentConfig: cfg, }) From 196b263c24147994e5ca17644ed8cb8eedb55349 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 18 Jan 2023 17:34:09 +0000 Subject: [PATCH 17/17] update apitypings unit tests --- scripts/apitypings/testdata/enums/enums.go | 1 + scripts/apitypings/testdata/enums/enums.ts | 3 +++ 2 files changed, 4 insertions(+) diff --git a/scripts/apitypings/testdata/enums/enums.go b/scripts/apitypings/testdata/enums/enums.go index 7e6a5e71673f3..baf665c62dedd 100644 --- a/scripts/apitypings/testdata/enums/enums.go +++ b/scripts/apitypings/testdata/enums/enums.go @@ -1,6 +1,7 @@ package enums type Enum string +type Enums []Enum const ( EnumFoo Enum = "foo" diff --git a/scripts/apitypings/testdata/enums/enums.ts b/scripts/apitypings/testdata/enums/enums.ts index 21b6f1934e499..bf9863da23dfe 100644 --- a/scripts/apitypings/testdata/enums/enums.ts +++ b/scripts/apitypings/testdata/enums/enums.ts @@ -1,5 +1,8 @@ // Code generated by 'make site/src/api/typesGenerated.ts'. DO NOT EDIT. +// From codersdk/enums.go +export type Enums = Enum[] + // From codersdk/enums.go export type Enum = "bar" | "baz" | "foo" | "qux" export const Enums: Enum[] = ["bar", "baz", "foo", "qux"]