mirror of
https://github.com/etcd-io/etcd.git
synced 2024-09-27 06:25:44 +00:00
server: Move downgrade API logic into version package
This commit is contained in:
parent
1e5e57f268
commit
e47c3c22d2
@ -18,12 +18,14 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/coreos/go-semver/semver"
|
"github.com/coreos/go-semver/semver"
|
||||||
"go.etcd.io/etcd/server/v3/storage/backend"
|
|
||||||
"go.etcd.io/etcd/server/v3/storage/schema"
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
pb "go.etcd.io/etcd/api/v3/etcdserverpb"
|
||||||
|
"go.etcd.io/etcd/api/v3/membershippb"
|
||||||
"go.etcd.io/etcd/api/v3/version"
|
"go.etcd.io/etcd/api/v3/version"
|
||||||
serverversion "go.etcd.io/etcd/server/v3/etcdserver/version"
|
serverversion "go.etcd.io/etcd/server/v3/etcdserver/version"
|
||||||
|
"go.etcd.io/etcd/server/v3/storage/backend"
|
||||||
|
"go.etcd.io/etcd/server/v3/storage/schema"
|
||||||
)
|
)
|
||||||
|
|
||||||
// serverVersionAdapter implements Server interface needed by serverversion.Monitor
|
// serverVersionAdapter implements Server interface needed by serverversion.Monitor
|
||||||
@ -46,12 +48,20 @@ func (s *serverVersionAdapter) UpdateClusterVersion(version string) {
|
|||||||
s.GoAttach(func() { s.updateClusterVersionV2(version) })
|
s.GoAttach(func() { s.updateClusterVersionV2(version) })
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *serverVersionAdapter) DowngradeCancel() {
|
func (s *serverVersionAdapter) LinearizableReadNotify(ctx context.Context) error {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), s.Cfg.ReqTimeout())
|
return s.linearizableReadNotify(ctx)
|
||||||
if _, err := s.downgradeCancel(ctx); err != nil {
|
}
|
||||||
s.lg.Warn("failed to cancel downgrade", zap.Error(err))
|
|
||||||
}
|
func (s *serverVersionAdapter) DowngradeEnable(ctx context.Context, targetVersion *semver.Version) error {
|
||||||
cancel()
|
raftRequest := membershippb.DowngradeInfoSetRequest{Enabled: true, Ver: targetVersion.String()}
|
||||||
|
_, err := s.raftRequest(ctx, pb.InternalRaftRequest{DowngradeInfoSet: &raftRequest})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *serverVersionAdapter) DowngradeCancel(ctx context.Context) error {
|
||||||
|
raftRequest := membershippb.DowngradeInfoSetRequest{Enabled: false}
|
||||||
|
_, err := s.raftRequest(ctx, pb.InternalRaftRequest{DowngradeInfoSet: &raftRequest})
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *serverVersionAdapter) GetClusterVersion() *semver.Version {
|
func (s *serverVersionAdapter) GetClusterVersion() *semver.Version {
|
||||||
|
@ -23,6 +23,7 @@ import (
|
|||||||
"go.etcd.io/etcd/server/v3/auth"
|
"go.etcd.io/etcd/server/v3/auth"
|
||||||
"go.etcd.io/etcd/server/v3/etcdserver"
|
"go.etcd.io/etcd/server/v3/etcdserver"
|
||||||
"go.etcd.io/etcd/server/v3/etcdserver/api/membership"
|
"go.etcd.io/etcd/server/v3/etcdserver/api/membership"
|
||||||
|
"go.etcd.io/etcd/server/v3/etcdserver/version"
|
||||||
"go.etcd.io/etcd/server/v3/lease"
|
"go.etcd.io/etcd/server/v3/lease"
|
||||||
"go.etcd.io/etcd/server/v3/storage/mvcc"
|
"go.etcd.io/etcd/server/v3/storage/mvcc"
|
||||||
|
|
||||||
@ -60,9 +61,9 @@ var toGRPCErrorMap = map[error]error{
|
|||||||
|
|
||||||
etcdserver.ErrClusterVersionUnavailable: rpctypes.ErrGRPCClusterVersionUnavailable,
|
etcdserver.ErrClusterVersionUnavailable: rpctypes.ErrGRPCClusterVersionUnavailable,
|
||||||
etcdserver.ErrWrongDowngradeVersionFormat: rpctypes.ErrGRPCWrongDowngradeVersionFormat,
|
etcdserver.ErrWrongDowngradeVersionFormat: rpctypes.ErrGRPCWrongDowngradeVersionFormat,
|
||||||
etcdserver.ErrInvalidDowngradeTargetVersion: rpctypes.ErrGRPCInvalidDowngradeTargetVersion,
|
version.ErrInvalidDowngradeTargetVersion: rpctypes.ErrGRPCInvalidDowngradeTargetVersion,
|
||||||
etcdserver.ErrDowngradeInProcess: rpctypes.ErrGRPCDowngradeInProcess,
|
version.ErrDowngradeInProcess: rpctypes.ErrGRPCDowngradeInProcess,
|
||||||
etcdserver.ErrNoInflightDowngrade: rpctypes.ErrGRPCNoInflightDowngrade,
|
version.ErrNoInflightDowngrade: rpctypes.ErrGRPCNoInflightDowngrade,
|
||||||
|
|
||||||
lease.ErrLeaseNotFound: rpctypes.ErrGRPCLeaseNotFound,
|
lease.ErrLeaseNotFound: rpctypes.ErrGRPCLeaseNotFound,
|
||||||
lease.ErrLeaseExists: rpctypes.ErrGRPCLeaseExist,
|
lease.ErrLeaseExists: rpctypes.ErrGRPCLeaseExist,
|
||||||
|
@ -41,9 +41,6 @@ var (
|
|||||||
ErrBadLeaderTransferee = errors.New("etcdserver: bad leader transferee")
|
ErrBadLeaderTransferee = errors.New("etcdserver: bad leader transferee")
|
||||||
ErrClusterVersionUnavailable = errors.New("etcdserver: cluster version not found during downgrade")
|
ErrClusterVersionUnavailable = errors.New("etcdserver: cluster version not found during downgrade")
|
||||||
ErrWrongDowngradeVersionFormat = errors.New("etcdserver: wrong downgrade target version format")
|
ErrWrongDowngradeVersionFormat = errors.New("etcdserver: wrong downgrade target version format")
|
||||||
ErrInvalidDowngradeTargetVersion = errors.New("etcdserver: invalid downgrade target version")
|
|
||||||
ErrDowngradeInProcess = errors.New("etcdserver: cluster has a downgrade job in progress")
|
|
||||||
ErrNoInflightDowngrade = errors.New("etcdserver: no inflight downgrade job")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type DiscoveryError struct {
|
type DiscoveryError struct {
|
||||||
|
@ -2359,3 +2359,7 @@ func (s *EtcdServer) IsMemberExist(id types.ID) bool {
|
|||||||
func (s *EtcdServer) raftStatus() raft.Status {
|
func (s *EtcdServer) raftStatus() raft.Status {
|
||||||
return s.r.Node.Status()
|
return s.r.Node.Status()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *EtcdServer) Version() *serverversion.Manager {
|
||||||
|
return serverversion.NewManager(s.Logger(), newServerVersionAdapter(s))
|
||||||
|
}
|
||||||
|
@ -23,12 +23,10 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
pb "go.etcd.io/etcd/api/v3/etcdserverpb"
|
pb "go.etcd.io/etcd/api/v3/etcdserverpb"
|
||||||
"go.etcd.io/etcd/api/v3/membershippb"
|
|
||||||
"go.etcd.io/etcd/pkg/v3/traceutil"
|
"go.etcd.io/etcd/pkg/v3/traceutil"
|
||||||
"go.etcd.io/etcd/raft/v3"
|
"go.etcd.io/etcd/raft/v3"
|
||||||
"go.etcd.io/etcd/server/v3/auth"
|
"go.etcd.io/etcd/server/v3/auth"
|
||||||
"go.etcd.io/etcd/server/v3/etcdserver/api/membership"
|
"go.etcd.io/etcd/server/v3/etcdserver/api/membership"
|
||||||
"go.etcd.io/etcd/server/v3/etcdserver/version"
|
|
||||||
"go.etcd.io/etcd/server/v3/lease"
|
"go.etcd.io/etcd/server/v3/lease"
|
||||||
"go.etcd.io/etcd/server/v3/lease/leasehttp"
|
"go.etcd.io/etcd/server/v3/lease/leasehttp"
|
||||||
"go.etcd.io/etcd/server/v3/storage/mvcc"
|
"go.etcd.io/etcd/server/v3/storage/mvcc"
|
||||||
@ -920,48 +918,27 @@ func (s *EtcdServer) downgradeValidate(ctx context.Context, v string) (*pb.Downg
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// gets leaders commit index and wait for local store to finish applying that index
|
|
||||||
// to avoid using stale downgrade information
|
|
||||||
err = s.linearizableReadNotify(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
cv := s.ClusterVersion()
|
cv := s.ClusterVersion()
|
||||||
if cv == nil {
|
if cv == nil {
|
||||||
return nil, ErrClusterVersionUnavailable
|
return nil, ErrClusterVersionUnavailable
|
||||||
}
|
}
|
||||||
resp.Version = cv.String()
|
resp.Version = cv.String()
|
||||||
|
err = s.Version().DowngradeValidate(ctx, targetVersion)
|
||||||
allowedTargetVersion := version.AllowedDowngradeVersion(cv)
|
if err != nil {
|
||||||
if !targetVersion.Equal(*allowedTargetVersion) {
|
return nil, err
|
||||||
return nil, ErrInvalidDowngradeTargetVersion
|
|
||||||
}
|
}
|
||||||
|
|
||||||
downgradeInfo := s.cluster.DowngradeInfo()
|
|
||||||
if downgradeInfo.Enabled {
|
|
||||||
// Todo: return the downgrade status along with the error msg
|
|
||||||
return nil, ErrDowngradeInProcess
|
|
||||||
}
|
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *EtcdServer) downgradeEnable(ctx context.Context, r *pb.DowngradeRequest) (*pb.DowngradeResponse, error) {
|
func (s *EtcdServer) downgradeEnable(ctx context.Context, r *pb.DowngradeRequest) (*pb.DowngradeResponse, error) {
|
||||||
// validate downgrade capability before starting downgrade
|
|
||||||
v := r.Version
|
|
||||||
lg := s.Logger()
|
lg := s.Logger()
|
||||||
if resp, err := s.downgradeValidate(ctx, v); err != nil {
|
targetVersion, err := convertToClusterVersion(r.Version)
|
||||||
lg.Warn("reject downgrade request", zap.Error(err))
|
|
||||||
return resp, err
|
|
||||||
}
|
|
||||||
targetVersion, err := convertToClusterVersion(v)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
lg.Warn("reject downgrade request", zap.Error(err))
|
lg.Warn("reject downgrade request", zap.Error(err))
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
err = s.Version().DowngradeEnable(ctx, targetVersion)
|
||||||
raftRequest := membershippb.DowngradeInfoSetRequest{Enabled: true, Ver: targetVersion.String()}
|
|
||||||
_, err = s.raftRequest(ctx, pb.InternalRaftRequest{DowngradeInfoSet: &raftRequest})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
lg.Warn("reject downgrade request", zap.Error(err))
|
lg.Warn("reject downgrade request", zap.Error(err))
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -971,21 +948,9 @@ func (s *EtcdServer) downgradeEnable(ctx context.Context, r *pb.DowngradeRequest
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *EtcdServer) downgradeCancel(ctx context.Context) (*pb.DowngradeResponse, error) {
|
func (s *EtcdServer) downgradeCancel(ctx context.Context) (*pb.DowngradeResponse, error) {
|
||||||
// gets leaders commit index and wait for local store to finish applying that index
|
err := s.Version().DowngradeCancel(ctx)
|
||||||
// to avoid using stale downgrade information
|
|
||||||
if err := s.linearizableReadNotify(ctx); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
downgradeInfo := s.cluster.DowngradeInfo()
|
|
||||||
if !downgradeInfo.Enabled {
|
|
||||||
return nil, ErrNoInflightDowngrade
|
|
||||||
}
|
|
||||||
|
|
||||||
raftRequest := membershippb.DowngradeInfoSetRequest{Enabled: false}
|
|
||||||
_, err := s.raftRequest(ctx, pb.InternalRaftRequest{DowngradeInfoSet: &raftRequest})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
s.lg.Warn("failed to cancel downgrade", zap.Error(err))
|
||||||
}
|
}
|
||||||
resp := pb.DowngradeResponse{Version: s.ClusterVersion().String()}
|
resp := pb.DowngradeResponse{Version: s.ClusterVersion().String()}
|
||||||
return &resp, nil
|
return &resp, nil
|
||||||
|
23
server/etcdserver/version/errors.go
Normal file
23
server/etcdserver/version/errors.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
// Copyright 2021 The etcd Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package version
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidDowngradeTargetVersion = errors.New("etcdserver: invalid downgrade target version")
|
||||||
|
ErrDowngradeInProcess = errors.New("etcdserver: cluster has a downgrade job in progress")
|
||||||
|
ErrNoInflightDowngrade = errors.New("etcdserver: no inflight downgrade job")
|
||||||
|
)
|
@ -15,6 +15,8 @@
|
|||||||
package version
|
package version
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
"github.com/coreos/go-semver/semver"
|
"github.com/coreos/go-semver/semver"
|
||||||
"go.etcd.io/etcd/api/v3/version"
|
"go.etcd.io/etcd/api/v3/version"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
@ -32,7 +34,9 @@ type Server interface {
|
|||||||
GetDowngradeInfo() *DowngradeInfo
|
GetDowngradeInfo() *DowngradeInfo
|
||||||
GetMembersVersions() map[string]*version.Versions
|
GetMembersVersions() map[string]*version.Versions
|
||||||
UpdateClusterVersion(string)
|
UpdateClusterVersion(string)
|
||||||
DowngradeCancel()
|
LinearizableReadNotify(ctx context.Context) error
|
||||||
|
DowngradeEnable(ctx context.Context, targetVersion *semver.Version) error
|
||||||
|
DowngradeCancel(ctx context.Context) error
|
||||||
|
|
||||||
GetStorageVersion() *semver.Version
|
GetStorageVersion() *semver.Version
|
||||||
UpdateStorageVersion(semver.Version)
|
UpdateStorageVersion(semver.Version)
|
||||||
@ -101,7 +105,10 @@ func (m *Monitor) CancelDowngradeIfNeeded() {
|
|||||||
v := semver.Must(semver.NewVersion(targetVersion))
|
v := semver.Must(semver.NewVersion(targetVersion))
|
||||||
if m.versionsMatchTarget(v) {
|
if m.versionsMatchTarget(v) {
|
||||||
m.lg.Info("the cluster has been downgraded", zap.String("cluster-version", targetVersion))
|
m.lg.Info("the cluster has been downgraded", zap.String("cluster-version", targetVersion))
|
||||||
m.s.DowngradeCancel()
|
err := m.s.DowngradeCancel(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
m.lg.Warn("failed to cancel downgrade", zap.Error(err))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package version
|
package version
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@ -339,8 +340,17 @@ func (s *storageMock) UpdateClusterVersion(version string) {
|
|||||||
s.clusterVersion = semver.New(version)
|
s.clusterVersion = semver.New(version)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *storageMock) DowngradeCancel() {
|
func (s *storageMock) LinearizableReadNotify(ctx context.Context) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *storageMock) DowngradeEnable(ctx context.Context, targetVersion *semver.Version) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *storageMock) DowngradeCancel(ctx context.Context) error {
|
||||||
s.downgradeInfo = nil
|
s.downgradeInfo = nil
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *storageMock) GetClusterVersion() *semver.Version {
|
func (s *storageMock) GetClusterVersion() *semver.Version {
|
||||||
|
81
server/etcdserver/version/version.go
Normal file
81
server/etcdserver/version/version.go
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
// Copyright 2021 The etcd Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package version
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/coreos/go-semver/semver"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Manager contains logic to manage etcd cluster version downgrade process.
|
||||||
|
type Manager struct {
|
||||||
|
lg *zap.Logger
|
||||||
|
s Server
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewManager returns a new manager instance
|
||||||
|
func NewManager(lg *zap.Logger, s Server) *Manager {
|
||||||
|
return &Manager{
|
||||||
|
lg: lg,
|
||||||
|
s: s,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DowngradeValidate validates if cluster is downloadable to provided target version and returns error if not.
|
||||||
|
func (m *Manager) DowngradeValidate(ctx context.Context, targetVersion *semver.Version) error {
|
||||||
|
// gets leaders commit index and wait for local store to finish applying that index
|
||||||
|
// to avoid using stale downgrade information
|
||||||
|
err := m.s.LinearizableReadNotify(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cv := m.s.GetClusterVersion()
|
||||||
|
allowedTargetVersion := AllowedDowngradeVersion(cv)
|
||||||
|
if !targetVersion.Equal(*allowedTargetVersion) {
|
||||||
|
return ErrInvalidDowngradeTargetVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
downgradeInfo := m.s.GetDowngradeInfo()
|
||||||
|
if downgradeInfo != nil && downgradeInfo.Enabled {
|
||||||
|
// Todo: return the downgrade status along with the error msg
|
||||||
|
return ErrDowngradeInProcess
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DowngradeEnable initiates etcd cluster version downgrade process.
|
||||||
|
func (m *Manager) DowngradeEnable(ctx context.Context, targetVersion *semver.Version) error {
|
||||||
|
// validate downgrade capability before starting downgrade
|
||||||
|
err := m.DowngradeValidate(ctx, targetVersion)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return m.s.DowngradeEnable(context.Background(), targetVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DowngradeCancel cancels ongoing downgrade process.
|
||||||
|
func (m *Manager) DowngradeCancel(ctx context.Context) error {
|
||||||
|
err := m.s.LinearizableReadNotify(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
downgradeInfo := m.s.GetDowngradeInfo()
|
||||||
|
if !downgradeInfo.Enabled {
|
||||||
|
return ErrNoInflightDowngrade
|
||||||
|
}
|
||||||
|
return m.s.DowngradeCancel(ctx)
|
||||||
|
}
|
@ -15,6 +15,7 @@
|
|||||||
package version
|
package version
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"testing"
|
"testing"
|
||||||
@ -140,8 +141,18 @@ func (m *memberMock) UpdateClusterVersion(version string) {
|
|||||||
m.cluster.clusterVersion = *semver.New(version)
|
m.cluster.clusterVersion = *semver.New(version)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *memberMock) DowngradeCancel() {
|
func (m *memberMock) LinearizableReadNotify(ctx context.Context) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memberMock) DowngradeEnable(ctx context.Context, targetVersion *semver.Version) error {
|
||||||
m.cluster.downgradeInfo = nil
|
m.cluster.downgradeInfo = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memberMock) DowngradeCancel(context.Context) error {
|
||||||
|
m.cluster.downgradeInfo = nil
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *memberMock) GetClusterVersion() *semver.Version {
|
func (m *memberMock) GetClusterVersion() *semver.Version {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user