Skip to content

Commit 56b9965

Browse files
authored
feat: add --experiments flag to replace --experimental (#5767)
- Deprecates the --experimental flag - Adds a new flag --experiments which supports passing multiple comma-separated values or a wildcard value. - Exposes a new endpoint /api/v2/experiments that returns the list of enabled experiments. - Deprecates the field Features.Experimental in favour of this new API. - Updates apidocgen to support type aliases (shoutout to @mtojek). - Modifies apitypings to support generating slice types. - Updates develop.sh to pass additional args after -- to $CODERD_SHIM.
1 parent 47c3d72 commit 56b9965

File tree

29 files changed

+593
-41
lines changed

29 files changed

+593
-41
lines changed

cli/deployment/config.go

+15-6
Original file line numberDiff line numberDiff line change
@@ -446,10 +446,19 @@ func newConfig() *codersdk.DeploymentConfig {
446446
Default: 512,
447447
},
448448
},
449+
// DEPRECATED: use Experiments instead.
449450
Experimental: &codersdk.DeploymentConfigField[bool]{
450-
Name: "Experimental",
451-
Usage: "Enable experimental features. Experimental features are not ready for production.",
452-
Flag: "experimental",
451+
Name: "Experimental",
452+
Usage: "Enable experimental features. Experimental features are not ready for production.",
453+
Flag: "experimental",
454+
Default: false,
455+
Hidden: true,
456+
},
457+
Experiments: &codersdk.DeploymentConfigField[[]string]{
458+
Name: "Experiments",
459+
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.",
460+
Flag: "experiments",
461+
Default: []string{},
453462
},
454463
UpdateCheck: &codersdk.DeploymentConfigField[bool]{
455464
Name: "Update Check",
@@ -557,12 +566,12 @@ func setConfig(prefix string, vip *viper.Viper, target interface{}) {
557566
// with a comma, but Viper only supports with a space. This
558567
// is a small hack around it!
559568
rawSlice := reflect.ValueOf(vip.GetStringSlice(prefix)).Interface()
560-
slice, ok := rawSlice.([]string)
569+
stringSlice, ok := rawSlice.([]string)
561570
if !ok {
562571
panic(fmt.Sprintf("string slice is of type %T", rawSlice))
563572
}
564-
value := make([]string, 0, len(slice))
565-
for _, entry := range slice {
573+
value := make([]string, 0, len(stringSlice))
574+
for _, entry := range stringSlice {
566575
value = append(value, strings.Split(entry, ",")...)
567576
}
568577
val.FieldByName("Value").Set(reflect.ValueOf(value))

cli/deployment/config_test.go

+17
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,23 @@ func TestConfig(t *testing.T) {
232232
require.Equal(t, config.Prometheus.Enable.Value, true)
233233
require.Equal(t, config.Prometheus.Address.Value, config.Prometheus.Address.Default)
234234
},
235+
}, {
236+
Name: "Experiments - no features",
237+
Env: map[string]string{
238+
"CODER_EXPERIMENTS": "",
239+
},
240+
Valid: func(config *codersdk.DeploymentConfig) {
241+
require.Empty(t, config.Experiments.Value)
242+
},
243+
}, {
244+
Name: "Experiments - multiple features",
245+
Env: map[string]string{
246+
"CODER_EXPERIMENTS": "foo,bar",
247+
},
248+
Valid: func(config *codersdk.DeploymentConfig) {
249+
expected := []string{"foo", "bar"}
250+
require.ElementsMatch(t, expected, config.Experiments.Value)
251+
},
235252
}} {
236253
tc := tc
237254
t.Run(tc.Name, func(t *testing.T) {

cli/testdata/coder_server_--help.golden

+6-4
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,12 @@ Flags:
6161
Consumes
6262
$CODER_DERP_SERVER_STUN_ADDRESSES
6363
(default [stun.l.google.com:19302])
64-
--experimental Enable experimental features.
65-
Experimental features are not ready for
66-
production.
67-
Consumes $CODER_EXPERIMENTAL
64+
--experiments strings Enable one or more experiments. These are
65+
not ready for production. Separate
66+
multiple experiments with commas, or
67+
enter '*' to opt-in to all available
68+
experiments.
69+
Consumes $CODER_EXPERIMENTS
6870
-h, --help help for server
6971
--http-address string HTTP bind address of the server. Unset to
7072
disable the HTTP endpoint.

coderd/apidoc/docs.go

+47-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apidoc/swagger.json

+39-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/coderd.go

+35
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"net/url"
1212
"path/filepath"
1313
"regexp"
14+
"strings"
1415
"sync"
1516
"sync/atomic"
1617
"time"
@@ -52,6 +53,7 @@ import (
5253
"github.com/coder/coder/coderd/telemetry"
5354
"github.com/coder/coder/coderd/tracing"
5455
"github.com/coder/coder/coderd/updatecheck"
56+
"github.com/coder/coder/coderd/util/slice"
5557
"github.com/coder/coder/coderd/wsconncache"
5658
"github.com/coder/coder/codersdk"
5759
"github.com/coder/coder/provisionerd/proto"
@@ -220,6 +222,7 @@ func New(options *Options) *API {
220222
},
221223
metricsCache: metricsCache,
222224
Auditor: atomic.Pointer[audit.Auditor]{},
225+
Experiments: initExperiments(options.Logger, options.DeploymentConfig.Experiments.Value, options.DeploymentConfig.Experimental.Value),
223226
}
224227
if options.UpdateCheckOptions != nil {
225228
api.updateChecker = updatecheck.New(
@@ -348,6 +351,10 @@ func New(options *Options) *API {
348351
r.Post("/csp/reports", api.logReportCSPViolations)
349352

350353
r.Get("/buildinfo", buildInfo)
354+
r.Route("/experiments", func(r chi.Router) {
355+
r.Use(apiKeyMiddleware)
356+
r.Get("/", api.handleExperimentsGet)
357+
})
351358
r.Get("/updatecheck", api.updateCheck)
352359
r.Route("/config", func(r chi.Router) {
353360
r.Use(apiKeyMiddleware)
@@ -646,6 +653,10 @@ type API struct {
646653
metricsCache *metricscache.Cache
647654
workspaceAgentCache *wsconncache.Cache
648655
updateChecker *updatecheck.Checker
656+
657+
// Experiments contains the list of experiments currently enabled.
658+
// This is used to gate features that are not yet ready for production.
659+
Experiments codersdk.Experiments
649660
}
650661

651662
// Close waits for all WebSocket connections to drain before returning.
@@ -752,3 +763,27 @@ func (api *API) CreateInMemoryProvisionerDaemon(ctx context.Context, debounce ti
752763

753764
return proto.NewDRPCProvisionerDaemonClient(clientSession), nil
754765
}
766+
767+
// nolint:revive
768+
func initExperiments(log slog.Logger, raw []string, legacyAll bool) codersdk.Experiments {
769+
exps := make([]codersdk.Experiment, 0, len(raw))
770+
for _, v := range raw {
771+
switch v {
772+
case "*":
773+
exps = append(exps, codersdk.ExperimentsAll...)
774+
default:
775+
ex := codersdk.Experiment(strings.ToLower(v))
776+
if !slice.Contains(codersdk.ExperimentsAll, ex) {
777+
log.Warn(context.Background(), "🐉 HERE BE DRAGONS: opting into hidden experiment", slog.F("experiment", ex))
778+
}
779+
exps = append(exps, ex)
780+
}
781+
}
782+
783+
// --experiments takes precedence over --experimental. It's deprecated.
784+
if legacyAll && len(raw) == 0 {
785+
log.Warn(context.Background(), "--experimental is deprecated, use --experiments='*' instead")
786+
exps = append(exps, codersdk.ExperimentsAll...)
787+
}
788+
return exps
789+
}

coderd/coderdtest/authorize.go

+1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ func AGPLRoutes(a *AuthTester) (map[string]string, map[string]RouteCheck) {
4848
"GET:/healthz": {NoAuthorize: true},
4949
"GET:/api/v2": {NoAuthorize: true},
5050
"GET:/api/v2/buildinfo": {NoAuthorize: true},
51+
"GET:/api/v2/experiments": {NoAuthorize: true}, // This route requires AuthN, but not AuthZ.
5152
"GET:/api/v2/updatecheck": {NoAuthorize: true},
5253
"GET:/api/v2/users/first": {NoAuthorize: true},
5354
"POST:/api/v2/users/first": {NoAuthorize: true},

coderd/coderdtest/coderdtest.go

-1
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,6 @@ type Options struct {
8585
AppHostname string
8686
AWSCertificates awsidentity.Certificates
8787
Authorizer rbac.Authorizer
88-
Experimental bool
8988
AzureCertificates x509.VerifyOptions
9089
GithubOAuth2Config *coderd.GithubOAuth2Config
9190
RealIPConfig *httpmw.RealIPConfig

coderd/experiments.go

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package coderd
2+
3+
import (
4+
"net/http"
5+
6+
"github.com/coder/coder/coderd/httpapi"
7+
)
8+
9+
// @Summary Get experiments
10+
// @ID get-experiments
11+
// @Security CoderSessionToken
12+
// @Produce json
13+
// @Tags General
14+
// @Success 200 {array} codersdk.Experiment
15+
// @Router /experiments [get]
16+
func (api *API) handleExperimentsGet(rw http.ResponseWriter, r *http.Request) {
17+
ctx := r.Context()
18+
httpapi.Write(ctx, rw, http.StatusOK, api.Experiments)
19+
}

0 commit comments

Comments
 (0)