diff --git a/server/embed/config.go b/server/embed/config.go index 12be43d4d6b..f54a8a44363 100644 --- a/server/embed/config.go +++ b/server/embed/config.go @@ -483,6 +483,8 @@ type configJSON struct { ClientSecurityJSON securityConfig `json:"client-transport-security"` PeerSecurityJSON securityConfig `json:"peer-transport-security"` + + ServerFeatureGatesJSON string `json:"feature-gates"` } type securityConfig struct { @@ -796,6 +798,13 @@ func (cfg *configYAML) configFromFile(path string) error { return err } + if cfg.configJSON.ServerFeatureGatesJSON != "" { + err = cfg.Config.ServerFeatureGate.(featuregate.MutableFeatureGate).Set(cfg.configJSON.ServerFeatureGatesJSON) + if err != nil { + return err + } + } + if cfg.configJSON.ListenPeerURLs != "" { u, err := types.NewURLs(strings.Split(cfg.configJSON.ListenPeerURLs, ",")) if err != nil { diff --git a/server/embed/config_test.go b/server/embed/config_test.go index 9153feb06e6..3e76b35b1d3 100644 --- a/server/embed/config_test.go +++ b/server/embed/config_test.go @@ -31,6 +31,8 @@ import ( "go.etcd.io/etcd/client/pkg/v3/srv" "go.etcd.io/etcd/client/pkg/v3/transport" "go.etcd.io/etcd/client/pkg/v3/types" + "go.etcd.io/etcd/pkg/v3/featuregate" + "go.etcd.io/etcd/server/v3/features" ) func notFoundErr(service, domain string) error { @@ -89,6 +91,78 @@ func TestConfigFileOtherFields(t *testing.T) { assert.Equal(t, false, cfg.SocketOpts.ReuseAddress, "ReuseAddress does not match") } +func TestConfigFileFeatureGates(t *testing.T) { + testCases := []struct { + name string + serverFeatureGatesJSON string + expectErr bool + expectedFeatures map[featuregate.Feature]bool + }{ + { + name: "default", + expectedFeatures: map[featuregate.Feature]bool{ + features.DistributedTracing: false, + features.StopGRPCServiceOnDefrag: false, + }, + }, + { + name: "set StopGRPCServiceOnDefrag to true", + serverFeatureGatesJSON: "StopGRPCServiceOnDefrag=true", + expectedFeatures: map[featuregate.Feature]bool{ + features.DistributedTracing: false, + features.StopGRPCServiceOnDefrag: true, + }, + }, + { + name: "set both features to true", + serverFeatureGatesJSON: "DistributedTracing=true,StopGRPCServiceOnDefrag=true", + expectedFeatures: map[featuregate.Feature]bool{ + features.DistributedTracing: true, + features.StopGRPCServiceOnDefrag: true, + }, + }, + { + name: "error setting unrecognized feature", + serverFeatureGatesJSON: "DistributedTracing=true,StopGRPCServiceOnDefragExp=true", + expectErr: true, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + yc := struct { + ExperimentalStopGRPCServiceOnDefrag *bool `json:"experimental-stop-grpc-service-on-defrag,omitempty"` + ServerFeatureGatesJSON string `json:"feature-gates"` + }{ + ServerFeatureGatesJSON: tc.serverFeatureGatesJSON, + } + + b, err := yaml.Marshal(&yc) + if err != nil { + t.Fatal(err) + } + + tmpfile := mustCreateCfgFile(t, b) + defer os.Remove(tmpfile.Name()) + + cfg, err := ConfigFromFile(tmpfile.Name()) + if tc.expectErr { + if err == nil { + t.Fatal("expect parse error") + } + return + } + if err != nil { + t.Fatal(err) + } + for k, v := range tc.expectedFeatures { + if cfg.ServerFeatureGate.Enabled(k) != v { + t.Errorf("expected feature gate %s=%v, got %v", k, v, cfg.ServerFeatureGate.Enabled(k)) + } + } + }) + } +} + // TestUpdateDefaultClusterFromName ensures that etcd can start with 'etcd --name=abc'. func TestUpdateDefaultClusterFromName(t *testing.T) { cfg := NewConfig()