Repository: etcd-io/etcd (language: go)

Issue:
Make the readiness check fail for learner
### What would you like to be added?

RPC read and write operations are not supported for learners, so the `readyz` check should fail for the learner.



### Why is this needed?

When the etcd cluster is running in Kubernetes, the learner's  readiness probe should fail. #16007


Discussion / hints:
Thanks @gojoy for raising the issue. Would you be able to write a PR for this? It is pretty straightforward to add a check in 
https://github.com/etcd-io/etcd/blob/3cf550d3a47ecee743eaadec1ebd183a0fa88383/server/etcdserver/api/etcdhttp/health.go#L246
based on the server method 
https://github.com/etcd-io/etcd/blob/3cf550d3a47ecee743eaadec1ebd183a0fa88383/server/etcdserver/server.go#L2485

and then backport to 3.5, 3.4
ok, please assign this to me @siyuanfoundation 

Can admin please assign this issue to me @siyuanfoundation 
/assign @gojoy 

Fix patch:
diff --git a/server/etcdserver/api/etcdhttp/health.go b/server/etcdserver/api/etcdhttp/health.go
index 15655e580cc..fe22fb42c8a 100644
--- a/server/etcdserver/api/etcdhttp/health.go
+++ b/server/etcdserver/api/etcdhttp/health.go
@@ -53,6 +53,7 @@ type ServerHealth interface {
 	Range(context.Context, *pb.RangeRequest) (*pb.RangeResponse, error)
 	Config() config.ServerConfig
 	AuthStore() auth.AuthStore
+	IsLearner() bool
 }
 
 type serverHealthV2V3 interface {
@@ -288,6 +289,8 @@ func installReadyzEndpoints(lg *zap.Logger, mux *http.ServeMux, server ServerHea
 	reg.Register("serializable_read", readCheck(server, true))
 	// linearizable_read check would be replaced by read_index check in 3.6
 	reg.Register("linearizable_read", readCheck(server, false))
+	// check if local is learner
+	reg.Register("non_learner", learnerCheck(server))
 	reg.InstallHttpEndpoints(lg, mux)
 }
 
@@ -458,3 +461,12 @@ func readCheck(srv ServerHealth, serializable bool) func(ctx context.Context) er
 		return err
 	}
 }
+
+func learnerCheck(srv ServerHealth) func(ctx context.Context) error {
+	return func(ctx context.Context) error {
+		if srv.IsLearner() {
+			return fmt.Errorf("not supported for learner")
+		}
+		return nil
+	}
+}


Test patch:
diff --git a/server/etcdserver/api/etcdhttp/health_test.go b/server/etcdserver/api/etcdhttp/health_test.go
index 0019121755a..fae41761d84 100644
--- a/server/etcdserver/api/etcdhttp/health_test.go
+++ b/server/etcdserver/api/etcdhttp/health_test.go
@@ -27,6 +27,7 @@ type fakeHealthServer struct {
 	linearizableReadError error
 	missingLeader         bool
 	authStore             auth.AuthStore
+	isLearner             bool
 }
 
 func (s *fakeHealthServer) Range(_ context.Context, req *pb.RangeRequest) (*pb.RangeResponse, error) {
@@ -36,6 +37,10 @@ func (s *fakeHealthServer) Range(_ context.Context, req *pb.RangeRequest) (*pb.R
 	return nil, s.linearizableReadError
 }
 
+func (s *fakeHealthServer) IsLearner() bool {
+	return s.isLearner
+}
+
 func (s *fakeHealthServer) Config() config.ServerConfig {
 	return config.ServerConfig{}
 }
@@ -61,6 +66,7 @@ type healthTestCase struct {
 	alarms        []*pb.AlarmMember
 	apiError      error
 	missingLeader bool
+	isLearner     bool
 }
 
 func TestHealthHandler(t *testing.T) {
@@ -165,6 +171,11 @@ func TestHttpSubPath(t *testing.T) {
 			expectStatusCode: http.StatusServiceUnavailable,
 			notInResult:      []string{"data_corruption"},
 		},
+		{
+			name:             "/readyz/learner ok",
+			healthCheckURL:   "/readyz/non_learner",
+			expectStatusCode: http.StatusOK,
+		},
 		{
 			name:             "/readyz/non_exist 404",
 			healthCheckURL:   "/readyz/non_exist",
@@ -328,6 +339,42 @@ func TestLinearizableReadCheck(t *testing.T) {
 	}
 }
 
+func TestLearnerReadyCheck(t *testing.T) {
+	be, _ := betesting.NewDefaultTmpBackend(t)
+	defer betesting.Close(t, be)
+	tests := []healthTestCase{
+		{
+			name:             "readyz normal",
+			healthCheckURL:   "/readyz",
+			expectStatusCode: http.StatusOK,
+			isLearner:        false,
+		},
+		{
+			name:             "not ready because member is learner",
+			healthCheckURL:   "/readyz",
+			expectStatusCode: http.StatusServiceUnavailable,
+			isLearner:        true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			mux := http.NewServeMux()
+			logger := zaptest.NewLogger(t)
+			s := &fakeHealthServer{
+				linearizableReadError: tt.apiError,
+				authStore:             auth.NewAuthStore(logger, be, nil, 0),
+			}
+			s.isLearner = tt.isLearner
+			HandleHealth(logger, mux, s)
+			ts := httptest.NewServer(mux)
+			defer ts.Close()
+			checkHttpResponse(t, ts, tt.healthCheckURL, tt.expectStatusCode, tt.inResult, tt.notInResult)
+			checkMetrics(t, tt.healthCheckURL, "linearizable_read", tt.expectStatusCode)
+		})
+	}
+}
+
 func checkHttpResponse(t *testing.T, ts *httptest.Server, url string, expectStatusCode int, inResult []string, notInResult []string) {
 	res, err := ts.Client().Do(&http.Request{Method: http.MethodGet, URL: testutil.MustNewURL(t, ts.URL+url)})