diff --git a/api/v3rpc/rpctypes/error.go b/api/v3rpc/rpctypes/error.go index 5ea2cf88dd7..b9f4842da25 100644 --- a/api/v3rpc/rpctypes/error.go +++ b/api/v3rpc/rpctypes/error.go @@ -65,6 +65,7 @@ var ( ErrGRPCAuthNotEnabled = status.New(codes.FailedPrecondition, "etcdserver: authentication is not enabled").Err() ErrGRPCInvalidAuthToken = status.New(codes.Unauthenticated, "etcdserver: invalid auth token").Err() ErrGRPCInvalidAuthMgmt = status.New(codes.InvalidArgument, "etcdserver: invalid auth management").Err() + ErrGRPCAuthOldRevision = status.New(codes.InvalidArgument, "etcdserver: revision of auth store is old").Err() ErrGRPCNoLeader = status.New(codes.Unavailable, "etcdserver: no leader").Err() ErrGRPCNotLeader = status.New(codes.FailedPrecondition, "etcdserver: not leader").Err() @@ -131,6 +132,7 @@ var ( ErrorDesc(ErrGRPCAuthNotEnabled): ErrGRPCAuthNotEnabled, ErrorDesc(ErrGRPCInvalidAuthToken): ErrGRPCInvalidAuthToken, ErrorDesc(ErrGRPCInvalidAuthMgmt): ErrGRPCInvalidAuthMgmt, + ErrorDesc(ErrGRPCAuthOldRevision): ErrGRPCAuthOldRevision, ErrorDesc(ErrGRPCNoLeader): ErrGRPCNoLeader, ErrorDesc(ErrGRPCNotLeader): ErrGRPCNotLeader, @@ -195,6 +197,7 @@ var ( ErrPermissionNotGranted = Error(ErrGRPCPermissionNotGranted) ErrAuthNotEnabled = Error(ErrGRPCAuthNotEnabled) ErrInvalidAuthToken = Error(ErrGRPCInvalidAuthToken) + ErrAuthOldRevision = Error(ErrGRPCAuthOldRevision) ErrInvalidAuthMgmt = Error(ErrGRPCInvalidAuthMgmt) ErrNoLeader = Error(ErrGRPCNoLeader) diff --git a/client/v3/retry_interceptor.go b/client/v3/retry_interceptor.go index 7198f1450e4..04f157a1dcb 100644 --- a/client/v3/retry_interceptor.go +++ b/client/v3/retry_interceptor.go @@ -156,7 +156,9 @@ func (c *Client) shouldRefreshToken(err error, callOpts *options) bool { // which is possible when the client token is cleared somehow return c.authTokenBundle != nil // equal to c.Username != "" && c.Password != "" } - return callOpts.retryAuth && rpctypes.Error(err) == rpctypes.ErrInvalidAuthToken + + return callOpts.retryAuth && + (rpctypes.Error(err) == rpctypes.ErrInvalidAuthToken || rpctypes.Error(err) == rpctypes.ErrAuthOldRevision) } // type serverStreamingRetryingStream is the implementation of grpc.ClientStream that acts as a diff --git a/client/v3/retry_interceptor_test.go b/client/v3/retry_interceptor_test.go new file mode 100644 index 00000000000..b850b56e0f5 --- /dev/null +++ b/client/v3/retry_interceptor_test.go @@ -0,0 +1,124 @@ +package clientv3 + +import ( + "go.etcd.io/etcd/api/v3/v3rpc/rpctypes" + "go.etcd.io/etcd/client/v3/credentials" + grpccredentials "google.golang.org/grpc/credentials" + "testing" +) + +type dummyAuthTokenBundle struct{} + +func (d dummyAuthTokenBundle) TransportCredentials() grpccredentials.TransportCredentials { + return nil +} + +func (d dummyAuthTokenBundle) PerRPCCredentials() grpccredentials.PerRPCCredentials { + return nil +} + +func (d dummyAuthTokenBundle) NewWithMode(mode string) (grpccredentials.Bundle, error) { + return nil, nil +} + +func (d dummyAuthTokenBundle) UpdateAuthToken(token string) { +} + +func TestClientShouldRefreshToken(t *testing.T) { + type fields struct { + authTokenBundle credentials.Bundle + } + type args struct { + err error + callOpts *options + } + + optsWithTrue := &options{ + retryAuth: true, + } + optsWithFalse := &options{ + retryAuth: false, + } + + tests := []struct { + name string + fields fields + args args + want bool + }{ + { + name: "ErrUserEmpty and non nil authTokenBundle", + fields: fields{ + authTokenBundle: &dummyAuthTokenBundle{}, + }, + args: args{rpctypes.ErrGRPCUserEmpty, optsWithTrue}, + want: true, + }, + { + name: "ErrUserEmpty and nil authTokenBundle", + fields: fields{ + authTokenBundle: nil, + }, + args: args{rpctypes.ErrGRPCUserEmpty, optsWithTrue}, + want: false, + }, + { + name: "ErrGRPCInvalidAuthToken and retryAuth", + fields: fields{ + authTokenBundle: nil, + }, + args: args{rpctypes.ErrGRPCInvalidAuthToken, optsWithTrue}, + want: true, + }, + { + name: "ErrGRPCInvalidAuthToken and !retryAuth", + fields: fields{ + authTokenBundle: nil, + }, + args: args{rpctypes.ErrGRPCInvalidAuthToken, optsWithFalse}, + want: false, + }, + { + name: "ErrGRPCAuthOldRevision and retryAuth", + fields: fields{ + authTokenBundle: nil, + }, + args: args{rpctypes.ErrGRPCAuthOldRevision, optsWithTrue}, + want: true, + }, + { + name: "ErrGRPCAuthOldRevision and !retryAuth", + fields: fields{ + authTokenBundle: nil, + }, + args: args{rpctypes.ErrGRPCAuthOldRevision, optsWithFalse}, + want: false, + }, + { + name: "Other error and retryAuth", + fields: fields{ + authTokenBundle: nil, + }, + args: args{rpctypes.ErrGRPCAuthFailed, optsWithTrue}, + want: false, + }, + { + name: "Other error and !retryAuth", + fields: fields{ + authTokenBundle: nil, + }, + args: args{rpctypes.ErrGRPCAuthFailed, optsWithFalse}, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Client{ + authTokenBundle: tt.fields.authTokenBundle, + } + if got := c.shouldRefreshToken(tt.args.err, tt.args.callOpts); got != tt.want { + t.Errorf("shouldRefreshToken() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/server/etcdserver/api/v3rpc/util.go b/server/etcdserver/api/v3rpc/util.go index f61fae03b96..91072b70358 100644 --- a/server/etcdserver/api/v3rpc/util.go +++ b/server/etcdserver/api/v3rpc/util.go @@ -84,6 +84,7 @@ var toGRPCErrorMap = map[error]error{ auth.ErrAuthNotEnabled: rpctypes.ErrGRPCAuthNotEnabled, auth.ErrInvalidAuthToken: rpctypes.ErrGRPCInvalidAuthToken, auth.ErrInvalidAuthMgmt: rpctypes.ErrGRPCInvalidAuthMgmt, + auth.ErrAuthOldRevision: rpctypes.ErrGRPCAuthOldRevision, // In sync with status.FromContextError context.Canceled: rpctypes.ErrGRPCCanceled,