diff --git a/go.mod b/go.mod index 55e1a7a772..98721bbab2 100644 --- a/go.mod +++ b/go.mod @@ -40,7 +40,7 @@ require ( github.com/vmihailenco/msgpack/v5 v5.3.5 go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738 go.uber.org/atomic v1.9.0 - go.uber.org/fx v1.10.0 + go.uber.org/fx v1.12.0 go.uber.org/goleak v1.1.10 go.uber.org/zap v1.19.0 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be diff --git a/go.sum b/go.sum index aa99f7719c..03c2f6c43b 100644 --- a/go.sum +++ b/go.sum @@ -339,10 +339,10 @@ go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/dig v1.8.0 h1:1rR6hnL/bu1EVcjnRDN5kx1vbIjEJDTGhSQ2B3ddpcI= -go.uber.org/dig v1.8.0/go.mod h1:X34SnWGr8Fyla9zQNO2GSO2D+TIuqB14OS8JhYocIyw= -go.uber.org/fx v1.10.0 h1:S2K/H8oNied0Je/mLKdWzEWKZfv9jtxSDm8CnwK+5Fg= -go.uber.org/fx v1.10.0/go.mod h1:vLRicqpG/qQEzno4SYU86iCwfT95EZza+Eba0ItuxqY= +go.uber.org/dig v1.9.0 h1:pJTDXKEhRqBI8W7rU7kwT5EgyRZuSMVSFcZolOvKK9U= +go.uber.org/dig v1.9.0/go.mod h1:X34SnWGr8Fyla9zQNO2GSO2D+TIuqB14OS8JhYocIyw= +go.uber.org/fx v1.12.0 h1:+1+3Cz9M0dFMPy9SW9XUIUHye8bnPUm7q7DroNGWYG4= +go.uber.org/fx v1.12.0/go.mod h1:egT3Kyg1JFYQkvKLZ3EsykxkNrZxgXS+gKoKo7abERY= go.uber.org/goleak v0.10.0/go.mod h1:VCZuO8V8mFPlL0F5J5GK1rtHV3DrFcQ1R8ryq7FK0aI= go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go index 60e2c07a8f..c91b408480 100644 --- a/pkg/apiserver/apiserver.go +++ b/pkg/apiserver/apiserver.go @@ -30,6 +30,8 @@ import ( "github.com/pingcap/tidb-dashboard/pkg/apiserver/user/sso" "github.com/pingcap/tidb-dashboard/pkg/apiserver/user/sso/ssoauth" "github.com/pingcap/tidb-dashboard/pkg/tiflash" + "github.com/pingcap/tidb-dashboard/pkg/utils/version" + "github.com/pingcap/tidb-dashboard/util/featureflag" "github.com/pingcap/tidb-dashboard/util/rest" // "github.com/pingcap/tidb-dashboard/pkg/apiserver/__APP_NAME__" @@ -46,7 +48,6 @@ import ( "github.com/pingcap/tidb-dashboard/pkg/tidb" "github.com/pingcap/tidb-dashboard/pkg/tikv" "github.com/pingcap/tidb-dashboard/pkg/utils" - "github.com/pingcap/tidb-dashboard/pkg/utils/version" ) func Handler(s *Service) http.Handler { @@ -99,6 +100,7 @@ func (s *Service) Start(ctx context.Context) error { s.app = fx.New( fx.Logger(utils.NewFxPrinter()), + fx.Supply(featureflag.NewRegistry(s.config.FeatureVersion)), fx.Provide( newAPIHandlerEngine, s.provideLocals, @@ -111,7 +113,6 @@ func (s *Service) Start(ctx context.Context) error { tikv.NewTiKVClient, tiflash.NewTiFlashClient, utils.NewSysSchema, - user.NewAuthService, info.NewService, clusterinfo.NewService, logsearch.NewService, @@ -123,6 +124,7 @@ func (s *Service) Start(ctx context.Context) error { // __APP_NAME__.NewService, // NOTE: Don't remove above comment line, it is a placeholder for code generator ), + user.Module, codeauth.Module, sqlauth.Module, ssoauth.Module, @@ -135,7 +137,6 @@ func (s *Service) Start(ctx context.Context) error { debugapi.Module, fx.Populate(&s.apiHandlerEngine), fx.Invoke( - user.RegisterRouter, info.RegisterRouter, clusterinfo.RegisterRouter, profiling.RegisterRouter, diff --git a/pkg/apiserver/conprof/feature_support.go b/pkg/apiserver/conprof/feature_support.go deleted file mode 100644 index 3871ec0371..0000000000 --- a/pkg/apiserver/conprof/feature_support.go +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2021 PingCAP, Inc. Licensed under Apache-2.0. - -package conprof - -import ( - "github.com/pingcap/tidb-dashboard/pkg/apiserver/utils" - "github.com/pingcap/tidb-dashboard/pkg/config" -) - -var ( - supportedTiDBVersions = []string{">= 5.3.0"} - featureSupported *bool -) - -func IsFeatureSupport(config *config.Config) (supported bool) { - if featureSupported != nil { - return *featureSupported - } - - supported = utils.IsVersionSupport(config.FeatureVersion, supportedTiDBVersions) - featureSupported = &supported - return -} diff --git a/pkg/apiserver/conprof/module.go b/pkg/apiserver/conprof/module.go index d329938382..0e943dadce 100644 --- a/pkg/apiserver/conprof/module.go +++ b/pkg/apiserver/conprof/module.go @@ -2,7 +2,9 @@ package conprof -import "go.uber.org/fx" +import ( + "go.uber.org/fx" +) var Module = fx.Options( fx.Provide(newService), diff --git a/pkg/apiserver/conprof/service.go b/pkg/apiserver/conprof/service.go index c82cfee920..8c45d4d1d7 100644 --- a/pkg/apiserver/conprof/service.go +++ b/pkg/apiserver/conprof/service.go @@ -22,6 +22,7 @@ import ( "github.com/pingcap/tidb-dashboard/pkg/apiserver/utils" "github.com/pingcap/tidb-dashboard/pkg/config" "github.com/pingcap/tidb-dashboard/pkg/utils/topology" + "github.com/pingcap/tidb-dashboard/util/featureflag" "github.com/pingcap/tidb-dashboard/util/rest" ) @@ -44,11 +45,14 @@ type ngMonitoringAddrCacheEntity struct { type ServiceParams struct { fx.In - EtcdClient *clientv3.Client - Config *config.Config + EtcdClient *clientv3.Client + Config *config.Config + FeatureFlags *featureflag.Registry } type Service struct { + FeatureFlagConprof *featureflag.FeatureFlag + params ServiceParams lifecycleCtx context.Context @@ -57,13 +61,18 @@ type Service struct { } func newService(lc fx.Lifecycle, p ServiceParams) *Service { - s := &Service{params: p} + s := &Service{ + FeatureFlagConprof: p.FeatureFlags.Register("conprof", ">= 5.3.0"), + params: p, + } + lc.Append(fx.Hook{ OnStart: func(ctx context.Context) error { s.lifecycleCtx = ctx return nil }, }) + return s } @@ -71,7 +80,7 @@ func newService(lc fx.Lifecycle, p ServiceParams) *Service { func registerRouter(r *gin.RouterGroup, auth *user.AuthService, s *Service) { endpoint := r.Group("/continuous_profiling") - endpoint.Use(utils.MWForbidByFeatureSupport(IsFeatureSupport(s.params.Config))) + endpoint.Use(s.FeatureFlagConprof.VersionGuard()) { endpoint.GET("/config", auth.MWAuthRequired(), s.reverseProxy("/config"), s.conprofConfig) endpoint.POST("/config", auth.MWAuthRequired(), auth.MWRequireWritePriv(), s.reverseProxy("/config"), s.updateConprofConfig) diff --git a/pkg/apiserver/info/info.go b/pkg/apiserver/info/info.go index 39d2095e79..6e610ac4d8 100644 --- a/pkg/apiserver/info/info.go +++ b/pkg/apiserver/info/info.go @@ -11,21 +11,21 @@ import ( "github.com/thoas/go-funk" "go.uber.org/fx" - "github.com/pingcap/tidb-dashboard/pkg/apiserver/conprof" - "github.com/pingcap/tidb-dashboard/pkg/apiserver/nonrootlogin" "github.com/pingcap/tidb-dashboard/pkg/apiserver/user" "github.com/pingcap/tidb-dashboard/pkg/apiserver/utils" "github.com/pingcap/tidb-dashboard/pkg/config" "github.com/pingcap/tidb-dashboard/pkg/dbstore" "github.com/pingcap/tidb-dashboard/pkg/tidb" "github.com/pingcap/tidb-dashboard/pkg/utils/version" + "github.com/pingcap/tidb-dashboard/util/featureflag" ) type ServiceParams struct { fx.In - Config *config.Config - LocalStore *dbstore.DB - TiDBClient *tidb.Client + Config *config.Config + LocalStore *dbstore.DB + TiDBClient *tidb.Client + FeatureFlags *featureflag.Registry } type Service struct { @@ -61,19 +61,11 @@ type InfoResponse struct { //nolint // @Security JwtAuth // @Failure 401 {object} rest.ErrorResponse func (s *Service) infoHandler(c *gin.Context) { - supportedFeatures := []string{} - if conprof.IsFeatureSupport(s.params.Config) { - supportedFeatures = append(supportedFeatures, "conprof") - } - if nonrootlogin.IsFeatureSupport(s.params.Config) { - supportedFeatures = append(supportedFeatures, "nonRootLogin") - } - resp := InfoResponse{ Version: version.GetInfo(), EnableTelemetry: s.params.Config.EnableTelemetry, EnableExperimental: s.params.Config.EnableExperimental, - SupportedFeatures: supportedFeatures, + SupportedFeatures: s.params.FeatureFlags.SupportedFeatures(), } c.JSON(http.StatusOK, resp) } diff --git a/pkg/apiserver/nonrootlogin/feature_support.go b/pkg/apiserver/nonrootlogin/feature_support.go deleted file mode 100644 index b3dbfcf7f4..0000000000 --- a/pkg/apiserver/nonrootlogin/feature_support.go +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2021 PingCAP, Inc. Licensed under Apache-2.0. - -package nonrootlogin - -import ( - "github.com/pingcap/tidb-dashboard/pkg/apiserver/utils" - "github.com/pingcap/tidb-dashboard/pkg/config" -) - -var ( - supportedTiDBVersions = []string{">= 5.3.0"} - featureSupported *bool -) - -func IsFeatureSupport(config *config.Config) (supported bool) { - if featureSupported != nil { - return *featureSupported - } - - supported = utils.IsVersionSupport(config.FeatureVersion, supportedTiDBVersions) - featureSupported = &supported - return -} diff --git a/pkg/apiserver/user/auth.go b/pkg/apiserver/user/auth.go index b31adafd78..6755288b11 100644 --- a/pkg/apiserver/user/auth.go +++ b/pkg/apiserver/user/auth.go @@ -19,6 +19,7 @@ import ( "go.uber.org/zap" "github.com/pingcap/tidb-dashboard/pkg/apiserver/utils" + "github.com/pingcap/tidb-dashboard/util/featureflag" "github.com/pingcap/tidb-dashboard/util/rest" ) @@ -30,6 +31,8 @@ var ( ) type AuthService struct { + FeatureFlagNonRootLogin *featureflag.FeatureFlag + middleware *jwt.GinJWTMiddleware authenticators map[utils.AuthType]Authenticator } @@ -71,7 +74,7 @@ func (a BaseAuthenticator) SignOutInfo(u *utils.SessionUser, redirectURL string) return &SignOutInfo{}, nil } -func NewAuthService() *AuthService { +func newAuthService(featureFlags *featureflag.Registry) *AuthService { var secret *[32]byte secretStr := os.Getenv("DASHBOARD_SESSION_SECRET") @@ -88,8 +91,9 @@ func NewAuthService() *AuthService { } service := &AuthService{ - middleware: nil, - authenticators: map[utils.AuthType]Authenticator{}, + FeatureFlagNonRootLogin: featureFlags.Register("nonRootLogin", ">= 5.3.0"), + middleware: nil, + authenticators: map[utils.AuthType]Authenticator{}, } middleware, err := jwt.New(&jwt.GinJWTMiddleware{ @@ -219,7 +223,7 @@ func (s *AuthService) authForm(f AuthenticateForm) (*utils.SessionUser, error) { return u, nil } -func RegisterRouter(r *gin.RouterGroup, s *AuthService) { +func registerRouter(r *gin.RouterGroup, s *AuthService) { endpoint := r.Group("/user") endpoint.GET("/login_info", s.getLoginInfoHandler) endpoint.POST("/login", s.loginHandler) diff --git a/pkg/apiserver/user/module.go b/pkg/apiserver/user/module.go new file mode 100644 index 0000000000..d3a19b264a --- /dev/null +++ b/pkg/apiserver/user/module.go @@ -0,0 +1,12 @@ +// Copyright 2021 PingCAP, Inc. Licensed under Apache-2.0. + +package user + +import ( + "go.uber.org/fx" +) + +var Module = fx.Options( + fx.Provide(newAuthService), + fx.Invoke(registerRouter), +) diff --git a/pkg/apiserver/utils/error.go b/pkg/apiserver/utils/error.go index e172d90404..d378fefa3e 100644 --- a/pkg/apiserver/utils/error.go +++ b/pkg/apiserver/utils/error.go @@ -9,5 +9,3 @@ import ( var ErrNS = errorx.NewNamespace("error.api") var ErrExpNotEnabled = ErrNS.NewType("experimental_feature_not_enabled") - -var ErrFeatureNotSupported = ErrNS.NewType("feature_not_supported") diff --git a/pkg/apiserver/utils/mw_feature_support.go b/pkg/apiserver/utils/mw_feature_support.go deleted file mode 100644 index de82ff5bcc..0000000000 --- a/pkg/apiserver/utils/mw_feature_support.go +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2021 PingCAP, Inc. Licensed under Apache-2.0. - -package utils - -import ( - "net/http" - - "github.com/gin-gonic/gin" -) - -func MWForbidByFeatureSupport(enabled bool) gin.HandlerFunc { - return func(c *gin.Context) { - if !enabled { - _ = c.Error(ErrFeatureNotSupported.New("The feature is not supported")) - c.Status(http.StatusForbidden) - c.Abort() - return - } - - c.Next() - } -} diff --git a/pkg/apiserver/utils/semver_check.go b/pkg/apiserver/utils/semver_check.go deleted file mode 100644 index 5aef022cc1..0000000000 --- a/pkg/apiserver/utils/semver_check.go +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright 2021 PingCAP, Inc. Licensed under Apache-2.0. - -package utils - -import ( - "strings" - - "github.com/Masterminds/semver" - - "github.com/pingcap/tidb-dashboard/pkg/utils/version" -) - -// IsVersionSupport checks if a semantic version fits within a set of constraints -// pdVersion, standaloneVersion examples: "v5.2.2", "v5.3.0", "v5.4.0-alpha-xxx", "5.3.0" (semver can handle `v` prefix by itself) -// constraints examples: "~5.2.2", ">= 5.3.0", see semver docs to get more information. -func IsVersionSupport(standaloneVersion string, constraints []string) bool { - curVersion := standaloneVersion - if version.Standalone == "No" { - curVersion = version.PDVersion - } - // drop "-alpha-xxx" suffix - versionWithoutSuffix := strings.Split(curVersion, "-")[0] - v, err := semver.NewVersion(versionWithoutSuffix) - if err != nil { - return false - } - for _, ver := range constraints { - c, err := semver.NewConstraint(ver) - if err != nil { - continue - } - if c.Check(v) { - return true - } - } - return false -} diff --git a/pkg/config/config.go b/pkg/config/config.go index 824d942858..1e6069b30c 100755 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -6,6 +6,8 @@ import ( "crypto/tls" "net/url" "strings" + + "github.com/pingcap/tidb-dashboard/pkg/utils/version" ) const ( @@ -40,7 +42,7 @@ func Default() *Config { TiDBTLSConfig: nil, EnableTelemetry: true, EnableExperimental: false, - FeatureVersion: "", + FeatureVersion: version.PDVersion, } } diff --git a/scripts/go.sum b/scripts/go.sum index 302fbcdb21..31a983de77 100644 --- a/scripts/go.sum +++ b/scripts/go.sum @@ -281,8 +281,8 @@ go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/dig v1.8.0/go.mod h1:X34SnWGr8Fyla9zQNO2GSO2D+TIuqB14OS8JhYocIyw= -go.uber.org/fx v1.10.0/go.mod h1:vLRicqpG/qQEzno4SYU86iCwfT95EZza+Eba0ItuxqY= +go.uber.org/dig v1.9.0/go.mod h1:X34SnWGr8Fyla9zQNO2GSO2D+TIuqB14OS8JhYocIyw= +go.uber.org/fx v1.12.0/go.mod h1:egT3Kyg1JFYQkvKLZ3EsykxkNrZxgXS+gKoKo7abERY= go.uber.org/goleak v0.10.0/go.mod h1:VCZuO8V8mFPlL0F5J5GK1rtHV3DrFcQ1R8ryq7FK0aI= go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= diff --git a/util/featureflag/featureflag.go b/util/featureflag/featureflag.go new file mode 100644 index 0000000000..df4689177a --- /dev/null +++ b/util/featureflag/featureflag.go @@ -0,0 +1,72 @@ +// Copyright 2021 PingCAP, Inc. Licensed under Apache-2.0. + +package featureflag + +import ( + "net/http" + "strings" + + "github.com/Masterminds/semver" + "github.com/gin-gonic/gin" + "github.com/joomcode/errorx" + + "github.com/pingcap/tidb-dashboard/util/rest" +) + +var ErrFeatureUnsupported = errorx.CommonErrors.NewType("feature_unsupported") + +type FeatureFlag struct { + name string + constraints []string + isSupported bool +} + +func newFeatureFlag(name, targetVersion string, constraints ...string) *FeatureFlag { + f := &FeatureFlag{name: name, constraints: constraints} + f.isSupported = f.isSupportedIn(targetVersion) + return f +} + +func (f *FeatureFlag) Name() string { + return f.name +} + +func (f *FeatureFlag) IsSupported() bool { + return f.isSupported +} + +// VersionGuard returns gin.HandlerFunc as guard middleware. +// It will determine if features are available in the target version. +func (f *FeatureFlag) VersionGuard() gin.HandlerFunc { + return func(c *gin.Context) { + if !f.isSupported { + _ = c.Error(ErrFeatureUnsupported.New(f.name).WithProperty(rest.HTTPCodeProperty(http.StatusForbidden))) + c.Abort() + return + } + + c.Next() + } +} + +// IsSupportedIn checks if a semantic version fits within a set of constraints +// pdVersion, standaloneVersion examples: "v5.2.2", "v5.3.0", "v5.4.0-alpha-xxx", "5.3.0" (semver can handle `v` prefix by itself) +// constraints examples: "~5.2.2", ">= 5.3.0", see semver docs to get more information. +func (f *FeatureFlag) isSupportedIn(targetVersion string) bool { + // drop "-alpha-xxx" suffix + versionWithoutSuffix := strings.Split(targetVersion, "-")[0] + v, err := semver.NewVersion(versionWithoutSuffix) + if err != nil { + return false + } + for _, ver := range f.constraints { + c, err := semver.NewConstraint(ver) + if err != nil { + continue + } + if c.Check(v) { + return true + } + } + return false +} diff --git a/util/featureflag/featureflag_test.go b/util/featureflag/featureflag_test.go new file mode 100644 index 0000000000..9e75d88188 --- /dev/null +++ b/util/featureflag/featureflag_test.go @@ -0,0 +1,102 @@ +// Copyright 2021 PingCAP, Inc. Licensed under Apache-2.0. + +package featureflag + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "github.com/joomcode/errorx" + "github.com/stretchr/testify/require" + + "github.com/pingcap/tidb-dashboard/util/rest" +) + +func Test_Name(t *testing.T) { + f1 := &FeatureFlag{} + require.Equal(t, f1.Name(), "") + + f2 := newFeatureFlag("testFeature", "v5.3.0", ">= 5.3.0") + require.Equal(t, f2.Name(), "testFeature") +} + +func Test_IsSupported(t *testing.T) { + type Args struct { + target string + constraints []string + } + tests := []struct { + want bool + args Args + }{ + {want: false, args: Args{target: "v4.2.0", constraints: []string{">= 5.3.0"}}}, + {want: false, args: Args{target: "v5.2.0", constraints: []string{">= 5.3.0"}}}, + {want: true, args: Args{target: "v5.3.0", constraints: []string{">= 5.3.0"}}}, + {want: false, args: Args{target: "v5.2.0-alpha-xxx", constraints: []string{">= 5.3.0"}}}, + {want: true, args: Args{target: "v5.3.0-alpha-xxx", constraints: []string{">= 5.3.0"}}}, + {want: true, args: Args{target: "v5.3.0", constraints: []string{"= 5.3.0"}}}, + {want: false, args: Args{target: "v5.3.1", constraints: []string{"= 5.3.0"}}}, + } + + for _, tt := range tests { + ff := newFeatureFlag("testFeature", tt.args.target, tt.args.constraints...) + require.Equal(t, tt.want, ff.IsSupported()) + } +} + +func Test_VersionGuard(t *testing.T) { + r := require.New(t) + f1 := newFeatureFlag("testFeature1", "v5.3.0", ">= 5.3.0") + f2 := newFeatureFlag("testFeature2", "v5.3.0", ">= 5.3.1") + + // success + e := gin.Default() + e.Use(f1.VersionGuard()) + e.GET("/ping", func(c *gin.Context) { + c.String(http.StatusOK, "pong") + }) + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/ping", nil) + e.ServeHTTP(w, req) + + r.Equal(http.StatusOK, w.Code) + r.Equal("pong", w.Body.String()) + + // abort + handled := false + e2 := gin.Default() + e2.Use(func(c *gin.Context) { + c.Next() + + handled = true + r.Equal(true, errorx.IsOfType(c.Errors.Last().Err, ErrFeatureUnsupported)) + }) + e2.Use(f1.VersionGuard(), f2.VersionGuard()) + e2.GET("/ping", func(c *gin.Context) { + c.String(http.StatusOK, "pong") + }) + w2 := httptest.NewRecorder() + req2, _ := http.NewRequest("GET", "/ping", nil) + e2.ServeHTTP(w2, req2) + + r.Equal(true, handled) +} + +func Test_VersionGuardWith_ErrorHandlerFn(t *testing.T) { + r := require.New(t) + f := newFeatureFlag("testFeature", "v5.3.0", ">= 5.3.1") + + e := gin.Default() + e.Use(rest.ErrorHandlerFn()) + e.Use(f.VersionGuard()) + e.GET("/ping", func(c *gin.Context) { + c.String(http.StatusOK, "pong") + }) + w2 := httptest.NewRecorder() + req2, _ := http.NewRequest("GET", "/ping", nil) + e.ServeHTTP(w2, req2) + + r.Equal(http.StatusForbidden, w2.Code) +} diff --git a/util/featureflag/registry.go b/util/featureflag/registry.go new file mode 100644 index 0000000000..b234a18a3f --- /dev/null +++ b/util/featureflag/registry.go @@ -0,0 +1,45 @@ +// Copyright 2021 PingCAP, Inc. Licensed under Apache-2.0. + +package featureflag + +import ( + "sort" +) + +type Registry struct { + version string + flags map[string]*FeatureFlag + supportedFeatures map[string]struct{} +} + +func NewRegistry(version string) *Registry { + return &Registry{ + version: version, + flags: map[string]*FeatureFlag{}, + supportedFeatures: map[string]struct{}{}, + } +} + +// Register create and register feature flag to registry. +func (m *Registry) Register(name string, constraints ...string) *FeatureFlag { + if f, ok := m.flags[name]; ok { + return f + } + + nf := newFeatureFlag(name, m.version, constraints...) + m.flags[name] = nf + if nf.IsSupported() { + m.supportedFeatures[nf.Name()] = struct{}{} + } + return nf +} + +// SupportedFeatures returns supported feature's names. +func (m *Registry) SupportedFeatures() []string { + sf := make([]string, 0, len(m.supportedFeatures)) + for k := range m.supportedFeatures { + sf = append(sf, k) + } + sort.Strings(sf) + return sf +} diff --git a/util/featureflag/registry_test.go b/util/featureflag/registry_test.go new file mode 100644 index 0000000000..3a7e1b33b9 --- /dev/null +++ b/util/featureflag/registry_test.go @@ -0,0 +1,58 @@ +// Copyright 2021 PingCAP, Inc. Licensed under Apache-2.0. + +package featureflag + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_Register(t *testing.T) { + m := NewRegistry("v5.3.0") + tests := []*struct { + supported bool + name string + constraints []string + flag *FeatureFlag + }{ + {supported: true, name: "testFeature1", constraints: []string{">= 5.3.0"}}, + {supported: true, name: "testFeature2", constraints: []string{">= 4.0.0"}}, + {supported: false, name: "testFeature3", constraints: []string{">= 5.3.1"}}, + } + + for _, tt := range tests { + tt.flag = m.Register(tt.name, tt.constraints...) + } + + for _, tt := range tests { + // check whether flag is in flags & supportedFeatures + require.Equal(t, m.flags[tt.flag.name], tt.flag) + _, ok := m.supportedFeatures[tt.flag.name] + require.Equal(t, tt.supported, ok) + } + + // duplicated register + f := m.Register("testFeature3", ">= 5.3.2") + require.Equal(t, f.name, "testFeature3") + require.Equal(t, f.constraints[0], ">= 5.3.1") +} + +func Test_SupportedFeatures(t *testing.T) { + m := NewRegistry("v5.3.0") + tests := []*struct { + supported bool + name string + constraints []string + }{ + {supported: true, name: "testFeature1", constraints: []string{">= 5.3.0"}}, + {supported: true, name: "testFeature2", constraints: []string{">= 4.0.0"}}, + {supported: false, name: "testFeature3", constraints: []string{">= 5.3.1"}}, + } + + for _, tt := range tests { + m.Register(tt.name, tt.constraints...) + } + + require.Equal(t, []string{"testFeature1", "testFeature2"}, m.SupportedFeatures()) +}