mirror of https://github.com/knative/pkg.git
Add health checks (#2671)
* add health checks * updates * make probes more flexible * simplify * minor * internal only * fixes
This commit is contained in:
parent
33e6b88010
commit
4a80605786
|
@ -0,0 +1,109 @@
|
||||||
|
/*
|
||||||
|
Copyright 2023 The Knative 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 injection
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"knative.dev/pkg/logging"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HealthCheckDefaultPort defines the default port number for health probes
|
||||||
|
const HealthCheckDefaultPort = 8080
|
||||||
|
|
||||||
|
// ServeHealthProbes sets up liveness and readiness probes.
|
||||||
|
// If user sets no probes explicitly via the context then defaults are added.
|
||||||
|
func ServeHealthProbes(ctx context.Context, port int) error {
|
||||||
|
logger := logging.FromContext(ctx)
|
||||||
|
server := http.Server{ReadHeaderTimeout: time.Minute, Handler: muxWithHandles(ctx), Addr: ":" + strconv.Itoa(port)}
|
||||||
|
go func() {
|
||||||
|
<-ctx.Done()
|
||||||
|
_ = server.Shutdown(ctx)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// start the web server on port and accept requests
|
||||||
|
logger.Infof("Probes server listening on port %s", port)
|
||||||
|
if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func muxWithHandles(ctx context.Context) *http.ServeMux {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
readiness := getReadinessHandleOrDefault(ctx)
|
||||||
|
liveness := getLivenessHandleOrDefault(ctx)
|
||||||
|
mux.HandleFunc("/readiness", *readiness)
|
||||||
|
mux.HandleFunc("/health", *liveness)
|
||||||
|
return mux
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDefaultProbesHandle(sigCtx context.Context) http.HandlerFunc {
|
||||||
|
logger := logging.FromContext(sigCtx)
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
f := func() error {
|
||||||
|
select {
|
||||||
|
// When we get SIGTERM (sigCtx done), let readiness probes start failing.
|
||||||
|
case <-sigCtx.Done():
|
||||||
|
logger.Info("Signal context canceled")
|
||||||
|
return errors.New("received SIGTERM from kubelet")
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := f(); err != nil {
|
||||||
|
logger.Errorf("Healthcheck failed: %v", err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
} else {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type addReadinessKey struct{}
|
||||||
|
|
||||||
|
// AddReadiness signals to probe setup logic to add a user provided probe handler
|
||||||
|
func AddReadiness(ctx context.Context, handlerFunc http.HandlerFunc) context.Context {
|
||||||
|
return context.WithValue(ctx, addReadinessKey{}, &handlerFunc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getReadinessHandleOrDefault(ctx context.Context) *http.HandlerFunc {
|
||||||
|
if ctx.Value(addReadinessKey{}) != nil {
|
||||||
|
return ctx.Value(addReadinessKey{}).(*http.HandlerFunc)
|
||||||
|
}
|
||||||
|
defaultHandle := newDefaultProbesHandle(ctx)
|
||||||
|
return &defaultHandle
|
||||||
|
}
|
||||||
|
|
||||||
|
type addLivenessKey struct{}
|
||||||
|
|
||||||
|
// AddLiveness signals to probe setup logic to add a user provided probe handler
|
||||||
|
func AddLiveness(ctx context.Context, handlerFunc http.HandlerFunc) context.Context {
|
||||||
|
return context.WithValue(ctx, addLivenessKey{}, &handlerFunc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getLivenessHandleOrDefault(ctx context.Context) *http.HandlerFunc {
|
||||||
|
if ctx.Value(addLivenessKey{}) != nil {
|
||||||
|
return ctx.Value(addLivenessKey{}).(*http.HandlerFunc)
|
||||||
|
}
|
||||||
|
defaultHandle := newDefaultProbesHandle(ctx)
|
||||||
|
return &defaultHandle
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
/*
|
||||||
|
Copyright 2023 The Knative 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 injection
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHealthCheckHandler(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
ctx context.Context
|
||||||
|
expectedReadiness int
|
||||||
|
expectedLiveness int
|
||||||
|
}{{
|
||||||
|
name: "user provided no handlers, default health check handlers are used",
|
||||||
|
ctx: context.Background(),
|
||||||
|
expectedReadiness: http.StatusOK,
|
||||||
|
expectedLiveness: http.StatusOK,
|
||||||
|
}, {
|
||||||
|
name: "user provided custom readiness health check handler, liveness default handler is used",
|
||||||
|
ctx: AddReadiness(context.Background(), testHandler()),
|
||||||
|
expectedReadiness: http.StatusBadGateway,
|
||||||
|
expectedLiveness: http.StatusOK,
|
||||||
|
}, {
|
||||||
|
name: "user provided custom liveness health check handler, readiness default handler is used",
|
||||||
|
ctx: AddLiveness(context.Background(), testHandler()),
|
||||||
|
expectedReadiness: http.StatusOK,
|
||||||
|
expectedLiveness: http.StatusBadGateway,
|
||||||
|
}, {
|
||||||
|
name: "user provided custom health check handlers",
|
||||||
|
ctx: AddReadiness(AddLiveness(context.Background(), testHandler()), testHandler()),
|
||||||
|
expectedReadiness: http.StatusBadGateway,
|
||||||
|
expectedLiveness: http.StatusBadGateway,
|
||||||
|
}}
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
mux := muxWithHandles(tc.ctx)
|
||||||
|
reqReadiness := http.Request{
|
||||||
|
URL: &url.URL{
|
||||||
|
Path: "/readiness",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
resp := httptest.NewRecorder()
|
||||||
|
mux.ServeHTTP(resp, &reqReadiness)
|
||||||
|
if got, want := resp.Code, tc.expectedReadiness; got != want {
|
||||||
|
t.Errorf("Probe status = %d, wanted %d", got, want)
|
||||||
|
}
|
||||||
|
reqLiveness := http.Request{
|
||||||
|
URL: &url.URL{
|
||||||
|
Path: "/health",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
resp = httptest.NewRecorder()
|
||||||
|
mux.ServeHTTP(resp, &reqLiveness)
|
||||||
|
if got, want := resp.Code, tc.expectedLiveness; got != want {
|
||||||
|
t.Errorf("Probe status = %d, wanted %d", got, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testHandler() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
http.Error(w, "test", http.StatusBadGateway)
|
||||||
|
}
|
||||||
|
}
|
|
@ -313,6 +313,13 @@ func MainWithConfig(ctx context.Context, component string, cfg *rest.Config, cto
|
||||||
return controller.StartAll(ctx, controllers...)
|
return controller.StartAll(ctx, controllers...)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Setup default health checks to catch issues with cache sync etc.
|
||||||
|
if !healthProbesDisabled(ctx) {
|
||||||
|
eg.Go(func() error {
|
||||||
|
return injection.ServeHealthProbes(ctx, injection.HealthCheckDefaultPort)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// This will block until either a signal arrives or one of the grouped functions
|
// This will block until either a signal arrives or one of the grouped functions
|
||||||
// returns an error.
|
// returns an error.
|
||||||
<-egCtx.Done()
|
<-egCtx.Done()
|
||||||
|
@ -324,6 +331,17 @@ func MainWithConfig(ctx context.Context, component string, cfg *rest.Config, cto
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type healthProbesDisabledKey struct{}
|
||||||
|
|
||||||
|
// WithHealthProbesDisabled signals to MainWithContext that it should disable default probes (readiness and liveness).
|
||||||
|
func WithHealthProbesDisabled(ctx context.Context) context.Context {
|
||||||
|
return context.WithValue(ctx, healthProbesDisabledKey{}, struct{}{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func healthProbesDisabled(ctx context.Context) bool {
|
||||||
|
return ctx.Value(healthProbesDisabledKey{}) != nil
|
||||||
|
}
|
||||||
|
|
||||||
func flush(logger *zap.SugaredLogger) {
|
func flush(logger *zap.SugaredLogger) {
|
||||||
logger.Sync()
|
logger.Sync()
|
||||||
metrics.FlushExporter()
|
metrics.FlushExporter()
|
||||||
|
|
Loading…
Reference in New Issue