mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-25 12:43:23 +00:00
implement healthz
for controller managers.
This commit is contained in:
parent
42405442e7
commit
15e0336de2
68
staging/src/k8s.io/controller-manager/pkg/healthz/handler.go
Normal file
68
staging/src/k8s.io/controller-manager/pkg/healthz/handler.go
Normal file
@ -0,0 +1,68 @@
|
||||
/*
|
||||
Copyright 2021 The Kubernetes 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 healthz
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"k8s.io/apiserver/pkg/server/healthz"
|
||||
"k8s.io/apiserver/pkg/server/mux"
|
||||
)
|
||||
|
||||
// MutableHealthzHandler returns a http.Handler that handles "/healthz"
|
||||
// following the standard healthz mechanism.
|
||||
//
|
||||
// This handler can register health checks after its creation, which
|
||||
// is originally not allowed with standard healthz handler.
|
||||
type MutableHealthzHandler struct {
|
||||
// handler is the underlying handler that will be replaced every time
|
||||
// new checks are added.
|
||||
handler http.Handler
|
||||
// mutex is a RWMutex that allows concurrent health checks (read)
|
||||
// but disallow replacing the handler at the same time (write).
|
||||
mutex sync.RWMutex
|
||||
checks []healthz.HealthChecker
|
||||
}
|
||||
|
||||
func (h *MutableHealthzHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
||||
h.mutex.RLock()
|
||||
defer h.mutex.RUnlock()
|
||||
|
||||
h.handler.ServeHTTP(writer, request)
|
||||
}
|
||||
|
||||
// AddHealthChecker adds health check(s) to the handler.
|
||||
//
|
||||
// Every time this function is called, the handler have to be re-initiated.
|
||||
// It is advised to add as many checks at once as possible.
|
||||
func (h *MutableHealthzHandler) AddHealthChecker(checks ...healthz.HealthChecker) {
|
||||
h.mutex.Lock()
|
||||
defer h.mutex.Unlock()
|
||||
|
||||
h.checks = append(h.checks, checks...)
|
||||
newMux := mux.NewPathRecorderMux("healthz")
|
||||
healthz.InstallHandler(newMux, h.checks...)
|
||||
h.handler = newMux
|
||||
}
|
||||
|
||||
func NewMutableHealthzHandler(checks ...healthz.HealthChecker) *MutableHealthzHandler {
|
||||
h := &MutableHealthzHandler{}
|
||||
h.AddHealthChecker(checks...)
|
||||
|
||||
return h
|
||||
}
|
@ -0,0 +1,174 @@
|
||||
/*
|
||||
Copyright 2021 The Kubernetes 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 healthz
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"k8s.io/apiserver/pkg/server/healthz"
|
||||
)
|
||||
|
||||
func TestMutableHealthzHandler(t *testing.T) {
|
||||
badChecker := healthz.NamedCheck("bad", func(r *http.Request) error {
|
||||
return fmt.Errorf("bad")
|
||||
})
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
checkBatches [][]healthz.HealthChecker
|
||||
appendBad bool // appends bad check after batches above, and see if it fails afterwards
|
||||
path string
|
||||
expectedBody string
|
||||
expectedStatus int
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
checkBatches: [][]healthz.HealthChecker{},
|
||||
path: "/healthz",
|
||||
expectedBody: "ok",
|
||||
expectedStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "good",
|
||||
checkBatches: [][]healthz.HealthChecker{
|
||||
{NamedPingChecker("good")},
|
||||
},
|
||||
path: "/healthz",
|
||||
expectedBody: "ok",
|
||||
expectedStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "good verbose", // verbose only applies for successful checks
|
||||
checkBatches: [][]healthz.HealthChecker{
|
||||
{NamedPingChecker("good")}, // batch 1: good
|
||||
},
|
||||
path: "/healthz?verbose=true",
|
||||
expectedBody: "[+]good ok\nhealthz check passed\n",
|
||||
expectedStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "good and bad, same batch",
|
||||
checkBatches: [][]healthz.HealthChecker{
|
||||
{NamedPingChecker("good"), badChecker}, // batch 1: good, bad
|
||||
},
|
||||
path: "/healthz",
|
||||
expectedBody: "[+]good ok\n[-]bad failed: reason withheld\nhealthz check failed\n",
|
||||
expectedStatus: http.StatusInternalServerError,
|
||||
},
|
||||
{
|
||||
name: "good and bad, two batches",
|
||||
checkBatches: [][]healthz.HealthChecker{
|
||||
{NamedPingChecker("good")}, // batch 1: good
|
||||
{badChecker}, // batch 2: bad
|
||||
},
|
||||
path: "/healthz",
|
||||
expectedBody: "[+]good ok\n[-]bad failed: reason withheld\nhealthz check failed\n",
|
||||
expectedStatus: http.StatusInternalServerError,
|
||||
},
|
||||
{
|
||||
name: "two checks and append bad",
|
||||
checkBatches: [][]healthz.HealthChecker{
|
||||
{NamedPingChecker("foo"), NamedPingChecker("bar")},
|
||||
},
|
||||
path: "/healthz",
|
||||
expectedBody: "ok",
|
||||
expectedStatus: http.StatusOK,
|
||||
appendBad: true,
|
||||
},
|
||||
{
|
||||
name: "subcheck",
|
||||
checkBatches: [][]healthz.HealthChecker{
|
||||
{NamedPingChecker("good")}, // batch 1: good
|
||||
{badChecker}, // batch 2: bad
|
||||
},
|
||||
path: "/healthz/good",
|
||||
expectedBody: "ok",
|
||||
expectedStatus: http.StatusOK,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
h := NewMutableHealthzHandler()
|
||||
for _, batch := range tc.checkBatches {
|
||||
h.AddHealthChecker(batch...)
|
||||
}
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf("https://example.com%v", tc.path), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
h.ServeHTTP(w, req)
|
||||
if w.Code != tc.expectedStatus {
|
||||
t.Errorf("unexpected status: expected %v, got %v", tc.expectedStatus, w.Result().StatusCode)
|
||||
}
|
||||
if w.Body.String() != tc.expectedBody {
|
||||
t.Errorf("unexpected body: expected %v, got %v", tc.expectedBody, w.Body.String())
|
||||
}
|
||||
if tc.appendBad {
|
||||
h.AddHealthChecker(badChecker)
|
||||
w := httptest.NewRecorder()
|
||||
h.ServeHTTP(w, req)
|
||||
// should fail
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Errorf("did not fail after adding bad checker")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestConcurrentChecks tests that the handler would not block on concurrent healthz requests.
|
||||
func TestConcurrentChecks(t *testing.T) {
|
||||
const N = 5
|
||||
stopChan := make(chan interface{})
|
||||
defer close(stopChan) // always close no matter passing or not
|
||||
concurrentChan := make(chan interface{}, N)
|
||||
var concurrentCount int32
|
||||
pausingCheck := healthz.NamedCheck("pausing", func(r *http.Request) error {
|
||||
atomic.AddInt32(&concurrentCount, 1)
|
||||
concurrentChan <- nil
|
||||
<-stopChan
|
||||
return nil
|
||||
})
|
||||
|
||||
h := NewMutableHealthzHandler(pausingCheck)
|
||||
for i := 0; i < N; i++ {
|
||||
go func() {
|
||||
req, _ := http.NewRequest(http.MethodGet, "https://example.com/healthz", nil)
|
||||
w := httptest.NewRecorder()
|
||||
h.ServeHTTP(w, req)
|
||||
}()
|
||||
}
|
||||
|
||||
giveUp := time.After(1 * time.Second) // should take <1ms if passing
|
||||
for i := 0; i < N; i++ {
|
||||
select {
|
||||
case <-giveUp:
|
||||
t.Errorf("given up waiting for concurrent checks to start.")
|
||||
return
|
||||
case <-concurrentChan:
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if concurrentCount != N {
|
||||
t.Errorf("expected %v concurrency, got %v", N, concurrentCount)
|
||||
}
|
||||
}
|
43
staging/src/k8s.io/controller-manager/pkg/healthz/healthz.go
Normal file
43
staging/src/k8s.io/controller-manager/pkg/healthz/healthz.go
Normal file
@ -0,0 +1,43 @@
|
||||
/*
|
||||
Copyright 2021 The Kubernetes 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 healthz
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"k8s.io/apiserver/pkg/server/healthz"
|
||||
)
|
||||
|
||||
// NamedPingChecker returns a health check with given name
|
||||
// that returns no error when checked.
|
||||
func NamedPingChecker(name string) healthz.HealthChecker {
|
||||
return NamedHealthChecker(name, healthz.PingHealthz)
|
||||
}
|
||||
|
||||
// NamedHealthChecker creates a named health check from
|
||||
// an unnamed one.
|
||||
func NamedHealthChecker(name string, check UnnamedHealthChecker) healthz.HealthChecker {
|
||||
return healthz.NamedCheck(name, check.Check)
|
||||
}
|
||||
|
||||
// UnnamedHealthChecker is an unnamed healthz checker.
|
||||
// The name of the check can be set by the controller manager.
|
||||
type UnnamedHealthChecker interface {
|
||||
Check(req *http.Request) error
|
||||
}
|
||||
|
||||
var _ UnnamedHealthChecker = (healthz.HealthChecker)(nil)
|
@ -0,0 +1,41 @@
|
||||
/*
|
||||
Copyright 2021 The Kubernetes 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 healthz
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type checkWithMessage struct {
|
||||
message string
|
||||
}
|
||||
|
||||
func (c *checkWithMessage) Check(_ *http.Request) error {
|
||||
return fmt.Errorf("%s", c.message)
|
||||
}
|
||||
|
||||
func TestNamedHealthChecker(t *testing.T) {
|
||||
named := NamedHealthChecker("foo", &checkWithMessage{message: "hello"})
|
||||
if named.Name() != "foo" {
|
||||
t.Errorf("expected: %v, got: %v", "foo", named.Name())
|
||||
}
|
||||
if err := named.Check(nil); err.Error() != "hello" {
|
||||
t.Errorf("expected: %v, got: %v", "hello", err.Error())
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user