Skip to content

Commit 3c19920

Browse files
Add "server-feature-gates" flag.
Signed-off-by: Siyuan Zhang <[email protected]>
1 parent 0f24953 commit 3c19920

File tree

7 files changed

+156
-9
lines changed

7 files changed

+156
-9
lines changed

pkg/featuregate/feature_gate.go

+19-3
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package featuregate
1717

1818
import (
19+
"flag"
1920
"fmt"
2021
"sort"
2122
"strconv"
@@ -30,7 +31,7 @@ import (
3031
type Feature string
3132

3233
const (
33-
flagName = "feature-gates"
34+
defaultFlagName = "feature-gates"
3435

3536
// allAlphaGate is a global toggle for alpha features. Per-feature key
3637
// values override the default set by allAlphaGate. Examples:
@@ -98,7 +99,7 @@ type MutableFeatureGate interface {
9899
FeatureGate
99100

100101
// AddFlag adds a flag for setting global feature gates to the specified FlagSet.
101-
AddFlag(fs *pflag.FlagSet)
102+
AddFlag(fs *flag.FlagSet, flagName string)
102103
// Set parses and stores flag gates for known features
103104
// from a string like feature1=true,feature2=false,...
104105
Set(value string) error
@@ -121,6 +122,8 @@ type MutableFeatureGate interface {
121122
// overriding its default to true for a limited number of components without simultaneously
122123
// changing its default for all consuming components.
123124
OverrideDefault(name Feature, override bool) error
125+
// SetLogger replaces the logger with the provided logger.
126+
SetLogger(lg *zap.Logger)
124127
}
125128

126129
// featureGate implements FeatureGate as well as pflag.Value for flag parsing.
@@ -165,6 +168,9 @@ func setUnsetBetaGates(known map[Feature]FeatureSpec, enabled map[Feature]bool,
165168
var _ pflag.Value = &featureGate{}
166169

167170
func New(name string, lg *zap.Logger) *featureGate {
171+
if lg == nil {
172+
lg = zap.NewNop()
173+
}
168174
known := map[Feature]FeatureSpec{}
169175
for k, v := range defaultFeatures {
170176
known[k] = v
@@ -349,7 +355,10 @@ func (f *featureGate) Enabled(key Feature) bool {
349355
}
350356

351357
// AddFlag adds a flag for setting global feature gates to the specified FlagSet.
352-
func (f *featureGate) AddFlag(fs *pflag.FlagSet) {
358+
func (f *featureGate) AddFlag(fs *flag.FlagSet, flagName string) {
359+
if flagName == "" {
360+
flagName = defaultFlagName
361+
}
353362
f.lock.Lock()
354363
// TODO(mtaufen): Shouldn't we just close it on the first Set/SetFromMap instead?
355364
// Not all components expose a feature gates flag using this AddFlag method, and
@@ -409,3 +418,10 @@ func (f *featureGate) DeepCopy() MutableFeatureGate {
409418

410419
return fg
411420
}
421+
422+
// SetLogger replaces the logger with the provided logger.
423+
func (f *featureGate) SetLogger(lg *zap.Logger) {
424+
f.lock.Lock()
425+
defer f.lock.Unlock()
426+
f.lg = lg
427+
}

pkg/featuregate/feature_gate_test.go

+6-6
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@
1515
package featuregate
1616

1717
import (
18+
"flag"
1819
"fmt"
1920
"strings"
2021
"testing"
2122

22-
"github.com/spf13/pflag"
2323
"github.com/stretchr/testify/assert"
2424
"go.uber.org/zap/zaptest"
2525
)
@@ -203,15 +203,15 @@ func TestFeatureGateFlag(t *testing.T) {
203203
}
204204
for i, test := range tests {
205205
t.Run(test.arg, func(t *testing.T) {
206-
fs := pflag.NewFlagSet("testfeaturegateflag", pflag.ContinueOnError)
206+
fs := flag.NewFlagSet("testfeaturegateflag", flag.ContinueOnError)
207207
f := New("test", zaptest.NewLogger(t))
208208
f.Add(map[Feature]FeatureSpec{
209209
testAlphaGate: {Default: false, PreRelease: Alpha},
210210
testBetaGate: {Default: false, PreRelease: Beta},
211211
})
212-
f.AddFlag(fs)
212+
f.AddFlag(fs, defaultFlagName)
213213

214-
err := fs.Parse([]string{fmt.Sprintf("--%s=%s", flagName, test.arg)})
214+
err := fs.Parse([]string{fmt.Sprintf("--%s=%s", defaultFlagName, test.arg)})
215215
if test.parseError != "" {
216216
if !strings.Contains(err.Error(), test.parseError) {
217217
t.Errorf("%d: Parse() Expected %v, Got %v", i, test.parseError, err)
@@ -603,8 +603,8 @@ func TestFeatureGateOverrideDefault(t *testing.T) {
603603

604604
t.Run("returns error if already added to flag set", func(t *testing.T) {
605605
f := New("test", zaptest.NewLogger(t))
606-
fs := pflag.NewFlagSet("test", pflag.ContinueOnError)
607-
f.AddFlag(fs)
606+
fs := flag.NewFlagSet("test", flag.ContinueOnError)
607+
f.AddFlag(fs, defaultFlagName)
608608

609609
if err := f.OverrideDefault("TestFeature", true); err == nil {
610610
t.Error("expected a non-nil error to be returned")

server/embed/config.go

+14
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import (
4242
"go.etcd.io/etcd/client/pkg/v3/transport"
4343
"go.etcd.io/etcd/client/pkg/v3/types"
4444
clientv3 "go.etcd.io/etcd/client/v3"
45+
"go.etcd.io/etcd/pkg/v3/featuregate"
4546
"go.etcd.io/etcd/pkg/v3/flags"
4647
"go.etcd.io/etcd/pkg/v3/netutil"
4748
"go.etcd.io/etcd/server/v3/config"
@@ -50,6 +51,7 @@ import (
5051
"go.etcd.io/etcd/server/v3/etcdserver/api/rafthttp"
5152
"go.etcd.io/etcd/server/v3/etcdserver/api/v3compactor"
5253
"go.etcd.io/etcd/server/v3/etcdserver/api/v3discovery"
54+
"go.etcd.io/etcd/server/v3/features"
5355
)
5456

5557
const (
@@ -108,6 +110,8 @@ const (
108110
maxElectionMs = 50000
109111
// backend freelist map type
110112
freelistArrayType = "array"
113+
114+
ServerFeatureGateFlagName = "server-feature-gates"
111115
)
112116

113117
var (
@@ -455,6 +459,9 @@ type Config struct {
455459

456460
// V2Deprecation describes phase of API & Storage V2 support
457461
V2Deprecation config.V2DeprecationEnum `json:"v2-deprecation"`
462+
463+
// ServerFeatureGate is a server level feature gate
464+
ServerFeatureGate featuregate.FeatureGate
458465
}
459466

460467
// configYAML holds the config suitable for yaml parsing
@@ -476,6 +483,8 @@ type configJSON struct {
476483

477484
ClientSecurityJSON securityConfig `json:"client-transport-security"`
478485
PeerSecurityJSON securityConfig `json:"peer-transport-security"`
486+
487+
ServerFeatureGatesJSON string `json:"server-feature-gates"`
479488
}
480489

481490
type securityConfig struct {
@@ -576,6 +585,7 @@ func NewConfig() *Config {
576585
},
577586

578587
AutoCompactionMode: DefaultAutoCompactionMode,
588+
ServerFeatureGate: features.NewDefaultServerFeatureGate(DefaultName, nil),
579589
}
580590
cfg.InitialCluster = cfg.InitialClusterFromName(cfg.Name)
581591
return cfg
@@ -762,6 +772,9 @@ func (cfg *Config) AddFlags(fs *flag.FlagSet) {
762772
// unsafe
763773
fs.BoolVar(&cfg.UnsafeNoFsync, "unsafe-no-fsync", false, "Disables fsync, unsafe, will cause data loss.")
764774
fs.BoolVar(&cfg.ForceNewCluster, "force-new-cluster", false, "Force to create a new one member cluster.")
775+
776+
// featuregate
777+
cfg.ServerFeatureGate.(featuregate.MutableFeatureGate).AddFlag(fs, ServerFeatureGateFlagName)
765778
}
766779

767780
func ConfigFromFile(path string) (*Config, error) {
@@ -907,6 +920,7 @@ func (cfg *Config) Validate() error {
907920
if err := cfg.setupLogging(); err != nil {
908921
return err
909922
}
923+
cfg.ServerFeatureGate.(featuregate.MutableFeatureGate).SetLogger(cfg.logger)
910924
if err := checkBindURLs(cfg.ListenPeerUrls); err != nil {
911925
return err
912926
}

server/embed/etcd.go

+1
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ func StartEtcd(inCfg *Config) (e *Etcd, err error) {
228228
ExperimentalMaxLearners: cfg.ExperimentalMaxLearners,
229229
V2Deprecation: cfg.V2DeprecationEffective(),
230230
ExperimentalLocalAddress: cfg.InferLocalAddr(),
231+
ServerFeatureGate: cfg.ServerFeatureGate,
231232
}
232233

233234
if srvcfg.ExperimentalEnableDistributedTracing {

server/etcdmain/config_test.go

+51
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@ import (
2525

2626
"sigs.k8s.io/yaml"
2727

28+
"go.etcd.io/etcd/pkg/v3/featuregate"
2829
"go.etcd.io/etcd/pkg/v3/flags"
2930
"go.etcd.io/etcd/server/v3/embed"
31+
"go.etcd.io/etcd/server/v3/features"
3032
)
3133

3234
func TestConfigParsingMemberFlags(t *testing.T) {
@@ -395,6 +397,55 @@ func TestFlagsPresentInHelp(t *testing.T) {
395397
})
396398
}
397399

400+
func TestParseFeatureGateFlags(t *testing.T) {
401+
testCases := []struct {
402+
name string
403+
args []string
404+
expectErr bool
405+
expectedFeatures map[featuregate.Feature]bool
406+
}{
407+
{
408+
name: "default",
409+
expectedFeatures: map[featuregate.Feature]bool{
410+
features.DistributedTracing: false,
411+
features.StopGRPCServiceOnDefrag: false,
412+
},
413+
},
414+
{
415+
name: "can set feature gate flag",
416+
args: []string{
417+
"--experimental-stop-grpc-service-on-defrag=false",
418+
"--server-feature-gates=DistributedTracing=true,StopGRPCServiceOnDefrag=true",
419+
},
420+
expectedFeatures: map[featuregate.Feature]bool{
421+
features.DistributedTracing: true,
422+
features.StopGRPCServiceOnDefrag: true,
423+
},
424+
},
425+
}
426+
427+
for _, tc := range testCases {
428+
t.Run(tc.name, func(t *testing.T) {
429+
cfg := newConfig()
430+
err := cfg.parse(tc.args)
431+
if tc.expectErr {
432+
if err == nil {
433+
t.Fatal("expect parse error")
434+
}
435+
return
436+
}
437+
if err != nil {
438+
t.Fatal(err)
439+
}
440+
for k, v := range tc.expectedFeatures {
441+
if cfg.ec.ServerFeatureGate.Enabled(k) != v {
442+
t.Errorf("expected feature gate %s=%v, got %v", k, v, cfg.ec.ServerFeatureGate.Enabled(k))
443+
}
444+
}
445+
})
446+
}
447+
}
448+
398449
func mustCreateCfgFile(t *testing.T, b []byte) *os.File {
399450
tmpfile, err := os.CreateTemp("", "servercfg")
400451
if err != nil {

server/etcdmain/help.go

+4
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@ package etcdmain
1818
import (
1919
"fmt"
2020
"strconv"
21+
"strings"
2122

2223
"golang.org/x/crypto/bcrypt"
2324

2425
cconfig "go.etcd.io/etcd/server/v3/config"
2526
"go.etcd.io/etcd/server/v3/embed"
2627
"go.etcd.io/etcd/server/v3/etcdserver/api/rafthttp"
28+
"go.etcd.io/etcd/server/v3/features"
2729
)
2830

2931
var (
@@ -103,6 +105,8 @@ Member:
103105
Read timeout set on each rafthttp connection
104106
--raft-write-timeout '` + rafthttp.DefaultConnWriteTimeout.String() + `'
105107
Write timeout set on each rafthttp connection
108+
--server-feature-gates ''
109+
A set of key=value pairs that describe server level feature gates for alpha/experimental features. Options are:'` + strings.Join(features.NewDefaultServerFeatureGate("", nil).KnownFeatures(), ",") + `'
106110
107111
Clustering:
108112
--initial-advertise-peer-urls 'http://localhost:2380'

server/features/etcd_features.go

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Copyright 2024 The etcd Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package features
16+
17+
import (
18+
"fmt"
19+
20+
"go.uber.org/zap"
21+
22+
"go.etcd.io/etcd/pkg/v3/featuregate"
23+
)
24+
25+
const (
26+
// Every feature gate should add method here following this template:
27+
//
28+
// // owner: @username
29+
// // kep: https://kep.k8s.io/NNN (or issue: https://github.com/etcd-io/etcd/issues/NNN)
30+
// // alpha: v3.X
31+
// MyFeature featuregate.Feature = "MyFeature"
32+
//
33+
// Feature gates should be listed in alphabetical, case-sensitive
34+
// (upper before any lower case character) order. This reduces the risk
35+
// of code conflicts because changes are more likely to be scattered
36+
// across the file.
37+
38+
// DistributedTracing enables experimental distributed tracing using OpenTelemetry Tracing.
39+
// alpha: v3.5
40+
// issue: https://github.com/etcd-io/etcd/issues/12460
41+
DistributedTracing featuregate.Feature = "DistributedTracing"
42+
// StopGRPCServiceOnDefrag enables etcd gRPC service to stop serving client requests on defragmentation.
43+
// owner: @chaochn47
44+
// alpha: v3.6
45+
StopGRPCServiceOnDefrag featuregate.Feature = "StopGRPCServiceOnDefrag"
46+
)
47+
48+
var (
49+
DefaultEtcdServerFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{
50+
DistributedTracing: {Default: false, PreRelease: featuregate.Alpha},
51+
StopGRPCServiceOnDefrag: {Default: false, PreRelease: featuregate.Alpha},
52+
}
53+
)
54+
55+
func NewDefaultServerFeatureGate(name string, lg *zap.Logger) featuregate.FeatureGate {
56+
fg := featuregate.New(fmt.Sprintf("%sServerFeatureGate", name), lg)
57+
if err := fg.Add(DefaultEtcdServerFeatureGates); err != nil {
58+
panic(err)
59+
}
60+
return fg
61+
}

0 commit comments

Comments
 (0)