rebase: update controller-runtime package to v0.9.2

This commit updates controller-runtime to v0.9.2 and
makes changes in persistentvolume.go to add context to
various functions and function calls made here instead of
context.TODO().

Signed-off-by: Rakshith R <rar@redhat.com>
This commit is contained in:
Rakshith R
2021-06-25 10:32:01 +05:30
committed by mergify[bot]
parent 1b23d78113
commit 9eaa55506f
238 changed files with 19614 additions and 10805 deletions

View File

@ -31,7 +31,7 @@ type Decoder struct {
codecs serializer.CodecFactory
}
// NewDecoder creates a Decoder given the runtime.Scheme
// NewDecoder creates a Decoder given the runtime.Scheme.
func NewDecoder(scheme *runtime.Scheme) (*Decoder, error) {
return &Decoder{codecs: serializer.NewCodecFactory(scheme)}, nil
}
@ -64,11 +64,7 @@ func (d *Decoder) DecodeRaw(rawObj runtime.RawExtension, into runtime.Object) er
}
if unstructuredInto, isUnstructured := into.(*unstructured.Unstructured); isUnstructured {
// unmarshal into unstructured's underlying object to avoid calling the decoder
if err := json.Unmarshal(rawObj.Raw, &unstructuredInto.Object); err != nil {
return err
}
return nil
return json.Unmarshal(rawObj.Raw, &unstructuredInto.Object)
}
deserializer := d.codecs.UniversalDeserializer()

View File

@ -24,7 +24,7 @@ import (
"k8s.io/apimachinery/pkg/runtime"
)
// Defaulter defines functions for setting defaults on resources
// Defaulter defines functions for setting defaults on resources.
type Defaulter interface {
runtime.Object
Default()
@ -58,8 +58,7 @@ func (h *mutatingHandler) Handle(ctx context.Context, req Request) Response {
// Get the object in the request
obj := h.defaulter.DeepCopyObject().(Defaulter)
err := h.decoder.Decode(req, obj)
if err != nil {
if err := h.decoder.Decode(req, obj); err != nil {
return Errored(http.StatusBadRequest, err)
}

View File

@ -24,9 +24,10 @@ import (
"io/ioutil"
"net/http"
v1 "k8s.io/api/admission/v1"
"k8s.io/api/admission/v1beta1"
admissionv1beta1 "k8s.io/api/admission/v1beta1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
)
@ -35,7 +36,8 @@ var admissionScheme = runtime.NewScheme()
var admissionCodecs = serializer.NewCodecFactory(admissionScheme)
func init() {
utilruntime.Must(admissionv1beta1.AddToScheme(admissionScheme))
utilruntime.Must(v1.AddToScheme(admissionScheme))
utilruntime.Must(v1beta1.AddToScheme(admissionScheme))
}
var _ http.Handler = &Webhook{}
@ -43,16 +45,13 @@ var _ http.Handler = &Webhook{}
func (wh *Webhook) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var body []byte
var err error
ctx := r.Context()
if wh.WithContextFunc != nil {
ctx = wh.WithContextFunc(ctx, r)
}
var reviewResponse Response
if r.Body != nil {
if body, err = ioutil.ReadAll(r.Body); err != nil {
wh.log.Error(err, "unable to read the body from the incoming request")
reviewResponse = Errored(http.StatusBadRequest, err)
wh.writeResponse(w, reviewResponse)
return
}
} else {
if r.Body == nil {
err = errors.New("request body is empty")
wh.log.Error(err, "bad request")
reviewResponse = Errored(http.StatusBadRequest, err)
@ -60,9 +59,16 @@ func (wh *Webhook) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
defer r.Body.Close()
if body, err = ioutil.ReadAll(r.Body); err != nil {
wh.log.Error(err, "unable to read the body from the incoming request")
reviewResponse = Errored(http.StatusBadRequest, err)
wh.writeResponse(w, reviewResponse)
return
}
// verify the content type is accurate
contentType := r.Header.Get("Content-Type")
if contentType != "application/json" {
if contentType := r.Header.Get("Content-Type"); contentType != "application/json" {
err = fmt.Errorf("contentType=%s, expected application/json", contentType)
wh.log.Error(err, "unable to process a request with an unknown content type", "content type", contentType)
reviewResponse = Errored(http.StatusBadRequest, err)
@ -70,12 +76,19 @@ func (wh *Webhook) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
// Both v1 and v1beta1 AdmissionReview types are exactly the same, so the v1beta1 type can
// be decoded into the v1 type. However the runtime codec's decoder guesses which type to
// decode into by type name if an Object's TypeMeta isn't set. By setting TypeMeta of an
// unregistered type to the v1 GVK, the decoder will coerce a v1beta1 AdmissionReview to v1.
// The actual AdmissionReview GVK will be used to write a typed response in case the
// webhook config permits multiple versions, otherwise this response will fail.
req := Request{}
ar := v1beta1.AdmissionReview{
// avoid an extra copy
Request: &req.AdmissionRequest,
}
if _, _, err := admissionCodecs.UniversalDeserializer().Decode(body, nil, &ar); err != nil {
ar := unversionedAdmissionReview{}
// avoid an extra copy
ar.Request = &req.AdmissionRequest
ar.SetGroupVersionKind(v1.SchemeGroupVersion.WithKind("AdmissionReview"))
_, actualAdmRevGVK, err := admissionCodecs.UniversalDeserializer().Decode(body, nil, &ar)
if err != nil {
wh.log.Error(err, "unable to decode the request")
reviewResponse = Errored(http.StatusBadRequest, err)
wh.writeResponse(w, reviewResponse)
@ -83,22 +96,51 @@ func (wh *Webhook) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
wh.log.V(1).Info("received request", "UID", req.UID, "kind", req.Kind, "resource", req.Resource)
// TODO: add panic-recovery for Handle
reviewResponse = wh.Handle(r.Context(), req)
wh.writeResponse(w, reviewResponse)
reviewResponse = wh.Handle(ctx, req)
wh.writeResponseTyped(w, reviewResponse, actualAdmRevGVK)
}
// writeResponse writes response to w generically, i.e. without encoding GVK information.
func (wh *Webhook) writeResponse(w io.Writer, response Response) {
encoder := json.NewEncoder(w)
responseAdmissionReview := v1beta1.AdmissionReview{
wh.writeAdmissionResponse(w, v1.AdmissionReview{Response: &response.AdmissionResponse})
}
// writeResponseTyped writes response to w with GVK set to admRevGVK, which is necessary
// if multiple AdmissionReview versions are permitted by the webhook.
func (wh *Webhook) writeResponseTyped(w io.Writer, response Response, admRevGVK *schema.GroupVersionKind) {
ar := v1.AdmissionReview{
Response: &response.AdmissionResponse,
}
err := encoder.Encode(responseAdmissionReview)
if err != nil {
// Default to a v1 AdmissionReview, otherwise the API server may not recognize the request
// if multiple AdmissionReview versions are permitted by the webhook config.
// TODO(estroz): this should be configurable since older API servers won't know about v1.
if admRevGVK == nil || *admRevGVK == (schema.GroupVersionKind{}) {
ar.SetGroupVersionKind(v1.SchemeGroupVersion.WithKind("AdmissionReview"))
} else {
ar.SetGroupVersionKind(*admRevGVK)
}
wh.writeAdmissionResponse(w, ar)
}
// writeAdmissionResponse writes ar to w.
func (wh *Webhook) writeAdmissionResponse(w io.Writer, ar v1.AdmissionReview) {
if err := json.NewEncoder(w).Encode(ar); err != nil {
wh.log.Error(err, "unable to encode the response")
wh.writeResponse(w, Errored(http.StatusInternalServerError, err))
} else {
res := responseAdmissionReview.Response
wh.log.V(1).Info("wrote response", "UID", res.UID, "allowed", res.Allowed, "result", res.Result)
res := ar.Response
if log := wh.log; log.V(1).Enabled() {
if res.Result != nil {
log = log.WithValues("code", res.Result.Code, "reason", res.Result.Reason)
}
log.V(1).Info("wrote response", "UID", res.UID, "allowed", res.Allowed)
}
}
}
// unversionedAdmissionReview is used to decode both v1 and v1beta1 AdmissionReview types.
type unversionedAdmissionReview struct {
v1.AdmissionReview
}
var _ runtime.Object = &unversionedAdmissionReview{}

View File

@ -22,9 +22,10 @@ import (
"fmt"
"net/http"
"gomodules.xyz/jsonpatch/v2"
admissionv1beta1 "k8s.io/api/admission/v1beta1"
jsonpatch "gomodules.xyz/jsonpatch/v2"
admissionv1 "k8s.io/api/admission/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/runtime/inject"
)
@ -37,10 +38,10 @@ func (hs multiMutating) Handle(ctx context.Context, req Request) Response {
if !resp.Allowed {
return resp
}
if resp.PatchType != nil && *resp.PatchType != admissionv1beta1.PatchTypeJSONPatch {
if resp.PatchType != nil && *resp.PatchType != admissionv1.PatchTypeJSONPatch {
return Errored(http.StatusInternalServerError,
fmt.Errorf("unexpected patch type returned by the handler: %v, only allow: %v",
resp.PatchType, admissionv1beta1.PatchTypeJSONPatch))
resp.PatchType, admissionv1.PatchTypeJSONPatch))
}
patches = append(patches, resp.Patches...)
}
@ -50,13 +51,13 @@ func (hs multiMutating) Handle(ctx context.Context, req Request) Response {
return Errored(http.StatusBadRequest, fmt.Errorf("error when marshaling the patch: %w", err))
}
return Response{
AdmissionResponse: admissionv1beta1.AdmissionResponse{
AdmissionResponse: admissionv1.AdmissionResponse{
Allowed: true,
Result: &metav1.Status{
Code: http.StatusOK,
},
Patch: marshaledPatch,
PatchType: func() *admissionv1beta1.PatchType { pt := admissionv1beta1.PatchTypeJSONPatch; return &pt }(),
PatchType: func() *admissionv1.PatchType { pt := admissionv1.PatchTypeJSONPatch; return &pt }(),
},
}
}
@ -76,6 +77,16 @@ func (hs multiMutating) InjectFunc(f inject.Func) error {
return nil
}
// InjectDecoder injects the decoder into the handlers.
func (hs multiMutating) InjectDecoder(d *Decoder) error {
for _, handler := range hs {
if _, err := InjectDecoderInto(d, handler); err != nil {
return err
}
}
return nil
}
// MultiMutatingHandler combines multiple mutating webhook handlers into a single
// mutating webhook handler. Handlers are called in sequential order, and the first
// `allowed: false` response may short-circuit the rest. Users must take care to
@ -94,7 +105,7 @@ func (hs multiValidating) Handle(ctx context.Context, req Request) Response {
}
}
return Response{
AdmissionResponse: admissionv1beta1.AdmissionResponse{
AdmissionResponse: admissionv1.AdmissionResponse{
Allowed: true,
Result: &metav1.Status{
Code: http.StatusOK,
@ -124,3 +135,13 @@ func (hs multiValidating) InjectFunc(f inject.Func) error {
return nil
}
// InjectDecoder injects the decoder into the handlers.
func (hs multiValidating) InjectDecoder(d *Decoder) error {
for _, handler := range hs {
if _, err := InjectDecoderInto(d, handler); err != nil {
return err
}
}
return nil
}

View File

@ -19,9 +19,8 @@ package admission
import (
"net/http"
"gomodules.xyz/jsonpatch/v2"
admissionv1beta1 "k8s.io/api/admission/v1beta1"
jsonpatch "gomodules.xyz/jsonpatch/v2"
admissionv1 "k8s.io/api/admission/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
@ -50,7 +49,7 @@ func Patched(reason string, patches ...jsonpatch.JsonPatchOperation) Response {
// Errored creates a new Response for error-handling a request.
func Errored(code int32, err error) Response {
return Response{
AdmissionResponse: admissionv1beta1.AdmissionResponse{
AdmissionResponse: admissionv1.AdmissionResponse{
Allowed: false,
Result: &metav1.Status{
Code: code,
@ -67,7 +66,7 @@ func ValidationResponse(allowed bool, reason string) Response {
code = http.StatusOK
}
resp := Response{
AdmissionResponse: admissionv1beta1.AdmissionResponse{
AdmissionResponse: admissionv1.AdmissionResponse{
Allowed: allowed,
Result: &metav1.Status{
Code: int32(code),
@ -90,9 +89,33 @@ func PatchResponseFromRaw(original, current []byte) Response {
}
return Response{
Patches: patches,
AdmissionResponse: admissionv1beta1.AdmissionResponse{
Allowed: true,
PatchType: func() *admissionv1beta1.PatchType { pt := admissionv1beta1.PatchTypeJSONPatch; return &pt }(),
AdmissionResponse: admissionv1.AdmissionResponse{
Allowed: true,
PatchType: func() *admissionv1.PatchType {
if len(patches) == 0 {
return nil
}
pt := admissionv1.PatchTypeJSONPatch
return &pt
}(),
},
}
}
// validationResponseFromStatus returns a response for admitting a request with provided Status object.
func validationResponseFromStatus(allowed bool, status metav1.Status) Response {
resp := Response{
AdmissionResponse: admissionv1.AdmissionResponse{
Allowed: allowed,
Result: &status,
},
}
return resp
}
// WithWarnings adds the given warnings to the Response.
// If any warnings were already given, they will not be overwritten.
func (r Response) WithWarnings(warnings ...string) Response {
r.AdmissionResponse.Warnings = append(r.AdmissionResponse.Warnings, warnings...)
return r
}

View File

@ -18,13 +18,15 @@ package admission
import (
"context"
goerrors "errors"
"net/http"
"k8s.io/api/admission/v1beta1"
v1 "k8s.io/api/admission/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
)
// Validator defines functions for validating an operation
// Validator defines functions for validating an operation.
type Validator interface {
runtime.Object
ValidateCreate() error
@ -60,7 +62,7 @@ func (h *validatingHandler) Handle(ctx context.Context, req Request) Response {
// Get the object in the request
obj := h.validator.DeepCopyObject().(Validator)
if req.Operation == v1beta1.Create {
if req.Operation == v1.Create {
err := h.decoder.Decode(req, obj)
if err != nil {
return Errored(http.StatusBadRequest, err)
@ -68,11 +70,15 @@ func (h *validatingHandler) Handle(ctx context.Context, req Request) Response {
err = obj.ValidateCreate()
if err != nil {
var apiStatus apierrors.APIStatus
if goerrors.As(err, &apiStatus) {
return validationResponseFromStatus(false, apiStatus.Status())
}
return Denied(err.Error())
}
}
if req.Operation == v1beta1.Update {
if req.Operation == v1.Update {
oldObj := obj.DeepCopyObject()
err := h.decoder.DecodeRaw(req.Object, obj)
@ -86,11 +92,15 @@ func (h *validatingHandler) Handle(ctx context.Context, req Request) Response {
err = obj.ValidateUpdate(oldObj)
if err != nil {
var apiStatus apierrors.APIStatus
if goerrors.As(err, &apiStatus) {
return validationResponseFromStatus(false, apiStatus.Status())
}
return Denied(err.Error())
}
}
if req.Operation == v1beta1.Delete {
if req.Operation == v1.Delete {
// In reference to PR: https://github.com/kubernetes/kubernetes/pull/76346
// OldObject contains the object being deleted
err := h.decoder.DecodeRaw(req.OldObject, obj)
@ -100,6 +110,10 @@ func (h *validatingHandler) Handle(ctx context.Context, req Request) Response {
err = obj.ValidateDelete()
if err != nil {
var apiStatus apierrors.APIStatus
if goerrors.As(err, &apiStatus) {
return validationResponseFromStatus(false, apiStatus.Status())
}
return Denied(err.Error())
}
}

View File

@ -22,13 +22,16 @@ import (
"net/http"
"github.com/go-logr/logr"
"gomodules.xyz/jsonpatch/v2"
admissionv1beta1 "k8s.io/api/admission/v1beta1"
jsonpatch "gomodules.xyz/jsonpatch/v2"
admissionv1 "k8s.io/api/admission/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/json"
"k8s.io/client-go/kubernetes/scheme"
logf "sigs.k8s.io/controller-runtime/pkg/internal/log"
"sigs.k8s.io/controller-runtime/pkg/runtime/inject"
"sigs.k8s.io/controller-runtime/pkg/webhook/internal/metrics"
)
var (
@ -41,7 +44,7 @@ var (
// name, namespace), as well as the operation in question
// (e.g. Get, Create, etc), and the object itself.
type Request struct {
admissionv1beta1.AdmissionRequest
admissionv1.AdmissionRequest
}
// Response is the output of an admission handler.
@ -57,7 +60,7 @@ type Response struct {
Patches []jsonpatch.JsonPatchOperation
// AdmissionResponse is the raw admission response.
// The Patch field in it will be overwritten by the listed patches.
admissionv1beta1.AdmissionResponse
admissionv1.AdmissionResponse
}
// Complete populates any fields that are yet to be set in
@ -84,7 +87,7 @@ func (r *Response) Complete(req Request) error {
if err != nil {
return err
}
patchType := admissionv1beta1.PatchTypeJSONPatch
patchType := admissionv1.PatchTypeJSONPatch
r.PatchType = &patchType
return nil
@ -110,11 +113,19 @@ func (f HandlerFunc) Handle(ctx context.Context, req Request) Response {
}
// Webhook represents each individual webhook.
//
// It must be registered with a webhook.Server or
// populated by StandaloneWebhook to be ran on an arbitrary HTTP server.
type Webhook struct {
// Handler actually processes an admission request returning whether it was allowed or denied,
// and potentially patches to apply to the handler.
Handler Handler
// WithContextFunc will allow you to take the http.Request.Context() and
// add any additional information such as passing the request path or
// headers thus allowing you to read them from within the handler
WithContextFunc func(context.Context, *http.Request) context.Context
// decoder is constructed on receiving a scheme and passed down to then handler
decoder *Decoder
@ -122,8 +133,8 @@ type Webhook struct {
}
// InjectLogger gets a handle to a logging instance, hopefully with more info about this particular webhook.
func (w *Webhook) InjectLogger(l logr.Logger) error {
w.log = l
func (wh *Webhook) InjectLogger(l logr.Logger) error {
wh.log = l
return nil
}
@ -131,10 +142,10 @@ func (w *Webhook) InjectLogger(l logr.Logger) error {
// If the webhook is mutating type, it delegates the AdmissionRequest to each handler and merge the patches.
// If the webhook is validating type, it delegates the AdmissionRequest to each handler and
// deny the request if anyone denies.
func (w *Webhook) Handle(ctx context.Context, req Request) Response {
resp := w.Handler.Handle(ctx, req)
func (wh *Webhook) Handle(ctx context.Context, req Request) Response {
resp := wh.Handler.Handle(ctx, req)
if err := resp.Complete(req); err != nil {
w.log.Error(err, "unable to encode response")
wh.log.Error(err, "unable to encode response")
return Errored(http.StatusInternalServerError, errUnableToEncodeResponse)
}
@ -142,19 +153,19 @@ func (w *Webhook) Handle(ctx context.Context, req Request) Response {
}
// InjectScheme injects a scheme into the webhook, in order to construct a Decoder.
func (w *Webhook) InjectScheme(s *runtime.Scheme) error {
func (wh *Webhook) InjectScheme(s *runtime.Scheme) error {
// TODO(directxman12): we should have a better way to pass this down
var err error
w.decoder, err = NewDecoder(s)
wh.decoder, err = NewDecoder(s)
if err != nil {
return err
}
// inject the decoder here too, just in case the order of calling this is not
// scheme first, then inject func
if w.Handler != nil {
if _, err := InjectDecoderInto(w.GetDecoder(), w.Handler); err != nil {
if wh.Handler != nil {
if _, err := InjectDecoderInto(wh.GetDecoder(), wh.Handler); err != nil {
return err
}
}
@ -164,12 +175,12 @@ func (w *Webhook) InjectScheme(s *runtime.Scheme) error {
// GetDecoder returns a decoder to decode the objects embedded in admission requests.
// It may be nil if we haven't received a scheme to use to determine object types yet.
func (w *Webhook) GetDecoder() *Decoder {
return w.decoder
func (wh *Webhook) GetDecoder() *Decoder {
return wh.decoder
}
// InjectFunc injects the field setter into the webhook.
func (w *Webhook) InjectFunc(f inject.Func) error {
func (wh *Webhook) InjectFunc(f inject.Func) error {
// inject directly into the handlers. It would be more correct
// to do this in a sync.Once in Handle (since we don't have some
// other start/finalize-type method), but it's more efficient to
@ -189,12 +200,56 @@ func (w *Webhook) InjectFunc(f inject.Func) error {
return err
}
if _, err := InjectDecoderInto(w.GetDecoder(), target); err != nil {
if _, err := InjectDecoderInto(wh.GetDecoder(), target); err != nil {
return err
}
return nil
}
return setFields(w.Handler)
return setFields(wh.Handler)
}
// StandaloneOptions let you configure a StandaloneWebhook.
type StandaloneOptions struct {
// Scheme is the scheme used to resolve runtime.Objects to GroupVersionKinds / Resources
// Defaults to the kubernetes/client-go scheme.Scheme, but it's almost always better
// idea to pass your own scheme in. See the documentation in pkg/scheme for more information.
Scheme *runtime.Scheme
// Logger to be used by the webhook.
// If none is set, it defaults to log.Log global logger.
Logger logr.Logger
// MetricsPath is used for labelling prometheus metrics
// by the path is served on.
// If none is set, prometheus metrics will not be generated.
MetricsPath string
}
// StandaloneWebhook prepares a webhook for use without a webhook.Server,
// passing in the information normally populated by webhook.Server
// and instrumenting the webhook with metrics.
//
// Use this to attach your webhook to an arbitrary HTTP server or mux.
//
// Note that you are responsible for terminating TLS if you use StandaloneWebhook
// in your own server/mux. In order to be accessed by a kubernetes cluster,
// all webhook servers require TLS.
func StandaloneWebhook(hook *Webhook, opts StandaloneOptions) (http.Handler, error) {
if opts.Scheme == nil {
opts.Scheme = scheme.Scheme
}
if err := hook.InjectScheme(opts.Scheme); err != nil {
return nil, err
}
if opts.Logger == nil {
opts.Logger = logf.RuntimeLog.WithName("webhook")
}
hook.log = opts.Logger
if opts.MetricsPath == "" {
return hook, nil
}
return metrics.InstrumentedHook(opts.MetricsPath, hook), nil
}

View File

@ -23,10 +23,10 @@ import (
// define some aliases for common bits of the webhook functionality
// Defaulter defines functions for setting defaults on resources
// Defaulter defines functions for setting defaults on resources.
type Defaulter = admission.Defaulter
// Validator defines functions for validating an operation
// Validator defines functions for validating an operation.
type Validator = admission.Validator
// AdmissionRequest defines the input for an admission handler.

View File

@ -1,162 +0,0 @@
/*
Copyright 2019 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 certwatcher
import (
"crypto/tls"
"sync"
"gopkg.in/fsnotify.v1"
logf "sigs.k8s.io/controller-runtime/pkg/internal/log"
)
var log = logf.RuntimeLog.WithName("certwatcher")
// CertWatcher watches certificate and key files for changes. When either file
// changes, it reads and parses both and calls an optional callback with the new
// certificate.
type CertWatcher struct {
sync.Mutex
currentCert *tls.Certificate
watcher *fsnotify.Watcher
certPath string
keyPath string
}
// New returns a new CertWatcher watching the given certificate and key.
func New(certPath, keyPath string) (*CertWatcher, error) {
var err error
cw := &CertWatcher{
certPath: certPath,
keyPath: keyPath,
}
// Initial read of certificate and key.
if err := cw.ReadCertificate(); err != nil {
return nil, err
}
cw.watcher, err = fsnotify.NewWatcher()
if err != nil {
return nil, err
}
return cw, nil
}
// GetCertificate fetches the currently loaded certificate, which may be nil.
func (cw *CertWatcher) GetCertificate(_ *tls.ClientHelloInfo) (*tls.Certificate, error) {
cw.Lock()
defer cw.Unlock()
return cw.currentCert, nil
}
// Start starts the watch on the certificate and key files.
func (cw *CertWatcher) Start(stopCh <-chan struct{}) error {
files := []string{cw.certPath, cw.keyPath}
for _, f := range files {
if err := cw.watcher.Add(f); err != nil {
return err
}
}
go cw.Watch()
log.Info("Starting certificate watcher")
// Block until the stop channel is closed.
<-stopCh
return cw.watcher.Close()
}
// Watch reads events from the watcher's channel and reacts to changes.
func (cw *CertWatcher) Watch() {
for {
select {
case event, ok := <-cw.watcher.Events:
// Channel is closed.
if !ok {
return
}
cw.handleEvent(event)
case err, ok := <-cw.watcher.Errors:
// Channel is closed.
if !ok {
return
}
log.Error(err, "certificate watch error")
}
}
}
// ReadCertificate reads the certificate and key files from disk, parses them,
// and updates the current certificate on the watcher. If a callback is set, it
// is invoked with the new certificate.
func (cw *CertWatcher) ReadCertificate() error {
cert, err := tls.LoadX509KeyPair(cw.certPath, cw.keyPath)
if err != nil {
return err
}
cw.Lock()
cw.currentCert = &cert
cw.Unlock()
log.Info("Updated current TLS certificate")
return nil
}
func (cw *CertWatcher) handleEvent(event fsnotify.Event) {
// Only care about events which may modify the contents of the file.
if !(isWrite(event) || isRemove(event) || isCreate(event)) {
return
}
log.V(1).Info("certificate event", "event", event)
// If the file was removed, re-add the watch.
if isRemove(event) {
if err := cw.watcher.Add(event.Name); err != nil {
log.Error(err, "error re-watching file")
}
}
if err := cw.ReadCertificate(); err != nil {
log.Error(err, "error re-reading certificate")
}
}
func isWrite(event fsnotify.Event) bool {
return event.Op&fsnotify.Write == fsnotify.Write
}
func isCreate(event fsnotify.Event) bool {
return event.Op&fsnotify.Create == fsnotify.Create
}
func isRemove(event fsnotify.Event) bool {
return event.Op&fsnotify.Remove == fsnotify.Remove
}

View File

@ -17,7 +17,10 @@ limitations under the License.
package metrics
import (
"net/http"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"sigs.k8s.io/controller-runtime/pkg/metrics"
)
@ -32,9 +35,51 @@ var (
},
[]string{"webhook"},
)
// RequestTotal is a prometheus metric which is a counter of the total processed admission requests.
RequestTotal = func() *prometheus.CounterVec {
return prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "controller_runtime_webhook_requests_total",
Help: "Total number of admission requests by HTTP status code.",
},
[]string{"webhook", "code"},
)
}()
// RequestInFlight is a prometheus metric which is a gauge of the in-flight admission requests.
RequestInFlight = func() *prometheus.GaugeVec {
return prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "controller_runtime_webhook_requests_in_flight",
Help: "Current number of admission requests being served.",
},
[]string{"webhook"},
)
}()
)
func init() {
metrics.Registry.MustRegister(
RequestLatency)
metrics.Registry.MustRegister(RequestLatency, RequestTotal, RequestInFlight)
}
// InstrumentedHook adds some instrumentation on top of the given webhook.
func InstrumentedHook(path string, hookRaw http.Handler) http.Handler {
lbl := prometheus.Labels{"webhook": path}
lat := RequestLatency.MustCurryWith(lbl)
cnt := RequestTotal.MustCurryWith(lbl)
gge := RequestInFlight.With(lbl)
// Initialize the most likely HTTP status codes.
cnt.WithLabelValues("200")
cnt.WithLabelValues("500")
return promhttp.InstrumentHandlerDuration(
lat,
promhttp.InstrumentHandlerCounter(
cnt,
promhttp.InstrumentHandlerInFlight(gge, hookRaw),
),
)
}

View File

@ -28,25 +28,32 @@ import (
"path/filepath"
"strconv"
"sync"
"time"
"k8s.io/apimachinery/pkg/runtime"
kscheme "k8s.io/client-go/kubernetes/scheme"
"sigs.k8s.io/controller-runtime/pkg/certwatcher"
"sigs.k8s.io/controller-runtime/pkg/runtime/inject"
"sigs.k8s.io/controller-runtime/pkg/webhook/internal/certwatcher"
"sigs.k8s.io/controller-runtime/pkg/webhook/internal/metrics"
)
// DefaultPort is the default port that the webhook server serves.
var DefaultPort = 443
var DefaultPort = 9443
// Server is an admission webhook server that can serve traffic and
// generates related k8s resources for deploying.
//
// TLS is required for a webhook to be accessed by kubernetes, so
// you must provide a CertName and KeyName or have valid cert/key
// at the default locations (tls.crt and tls.key). If you do not
// want to configure TLS (i.e for testing purposes) run an
// admission.StandaloneWebhook in your own server.
type Server struct {
// Host is the address that the server will listen on.
// Defaults to "" - all addresses.
Host string
// Port is the port number that the server will serve.
// It will be defaulted to 443 if unspecified.
// It will be defaulted to 9443 if unspecified.
Port int
// CertDir is the directory that contains the server key and certificate. The
@ -63,6 +70,10 @@ type Server struct {
// Defaults to "", which means server does not verify client's certificate.
ClientCAName string
// TLSVersion is the minimum version of TLS supported. Accepts
// "", "1.0", "1.1", "1.2" and "1.3" only ("" is equivalent to "1.0" for backwards compatibility)
TLSMinVersion string
// WebhookMux is the multiplexer that handles different webhooks.
WebhookMux *http.ServeMux
@ -75,6 +86,9 @@ type Server struct {
// defaultingOnce ensures that the default fields are only ever set once.
defaultingOnce sync.Once
// mu protects access to the webhook map & setFields for Start, Register, etc
mu sync.Mutex
}
// setDefaults does defaulting for the Server.
@ -110,50 +124,87 @@ func (*Server) NeedLeaderElection() bool {
// Register marks the given webhook as being served at the given path.
// It panics if two hooks are registered on the same path.
func (s *Server) Register(path string, hook http.Handler) {
s.mu.Lock()
defer s.mu.Unlock()
s.defaultingOnce.Do(s.setDefaults)
_, found := s.webhooks[path]
if found {
if _, found := s.webhooks[path]; found {
panic(fmt.Errorf("can't register duplicate path: %v", path))
}
// TODO(directxman12): call setfields if we've already started the server
s.webhooks[path] = hook
s.WebhookMux.Handle(path, instrumentedHook(path, hook))
log.Info("registering webhook", "path", path)
}
s.WebhookMux.Handle(path, metrics.InstrumentedHook(path, hook))
// instrumentedHook adds some instrumentation on top of the given webhook.
func instrumentedHook(path string, hookRaw http.Handler) http.Handler {
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
startTS := time.Now()
defer func() { metrics.RequestLatency.WithLabelValues(path).Observe(time.Since(startTS).Seconds()) }()
hookRaw.ServeHTTP(resp, req)
regLog := log.WithValues("path", path)
regLog.Info("registering webhook")
// TODO(directxman12): add back in metric about total requests broken down by result?
})
}
// Start runs the server.
// It will install the webhook related resources depend on the server configuration.
func (s *Server) Start(stop <-chan struct{}) error {
s.defaultingOnce.Do(s.setDefaults)
baseHookLog := log.WithName("webhooks")
baseHookLog.Info("starting webhook server")
// inject fields here as opposed to in Register so that we're certain to have our setFields
// function available.
for hookPath, webhook := range s.webhooks {
if err := s.setFields(webhook); err != nil {
return err
// we've already been "started", inject dependencies here.
// Otherwise, InjectFunc will do this for us later.
if s.setFields != nil {
if err := s.setFields(hook); err != nil {
// TODO(directxman12): swallowing this error isn't great, but we'd have to
// change the signature to fix that
regLog.Error(err, "unable to inject fields into webhook during registration")
}
baseHookLog := log.WithName("webhooks")
// NB(directxman12): we don't propagate this further by wrapping setFields because it's
// unclear if this is how we want to deal with log propagation. In this specific instance,
// we want to be able to pass a logger to webhooks because they don't know their own path.
if _, err := inject.LoggerInto(baseHookLog.WithValues("webhook", hookPath), webhook); err != nil {
return err
if _, err := inject.LoggerInto(baseHookLog.WithValues("webhook", path), hook); err != nil {
regLog.Error(err, "unable to logger into webhook during registration")
}
}
}
// StartStandalone runs a webhook server without
// a controller manager.
func (s *Server) StartStandalone(ctx context.Context, scheme *runtime.Scheme) error {
// Use the Kubernetes client-go scheme if none is specified
if scheme == nil {
scheme = kscheme.Scheme
}
if err := s.InjectFunc(func(i interface{}) error {
if _, err := inject.SchemeInto(scheme, i); err != nil {
return err
}
return nil
}); err != nil {
return err
}
return s.Start(ctx)
}
// tlsVersion converts from human-readable TLS version (for example "1.1")
// to the values accepted by tls.Config (for example 0x301).
func tlsVersion(version string) (uint16, error) {
switch version {
// default is previous behaviour
case "":
return tls.VersionTLS10, nil
case "1.0":
return tls.VersionTLS10, nil
case "1.1":
return tls.VersionTLS11, nil
case "1.2":
return tls.VersionTLS12, nil
case "1.3":
return tls.VersionTLS13, nil
default:
return 0, fmt.Errorf("invalid TLSMinVersion %v: expects 1.0, 1.1, 1.2, 1.3 or empty", version)
}
}
// Start runs the server.
// It will install the webhook related resources depend on the server configuration.
func (s *Server) Start(ctx context.Context) error {
s.defaultingOnce.Do(s.setDefaults)
baseHookLog := log.WithName("webhooks")
baseHookLog.Info("starting webhook server")
certPath := filepath.Join(s.CertDir, s.CertName)
keyPath := filepath.Join(s.CertDir, s.KeyName)
@ -164,14 +215,20 @@ func (s *Server) Start(stop <-chan struct{}) error {
}
go func() {
if err := certWatcher.Start(stop); err != nil {
if err := certWatcher.Start(ctx); err != nil {
log.Error(err, "certificate watcher error")
}
}()
cfg := &tls.Config{
tlsMinVersion, err := tlsVersion(s.TLSMinVersion)
if err != nil {
return err
}
cfg := &tls.Config{ //nolint:gosec
NextProtos: []string{"h2"},
GetCertificate: certWatcher.GetCertificate,
MinVersion: tlsMinVersion,
}
// load CA to verify client certificate
@ -191,7 +248,7 @@ func (s *Server) Start(stop <-chan struct{}) error {
cfg.ClientAuth = tls.RequireAndVerifyClientCert
}
listener, err := tls.Listen("tcp", net.JoinHostPort(s.Host, strconv.Itoa(int(s.Port))), cfg)
listener, err := tls.Listen("tcp", net.JoinHostPort(s.Host, strconv.Itoa(s.Port)), cfg)
if err != nil {
return err
}
@ -204,7 +261,7 @@ func (s *Server) Start(stop <-chan struct{}) error {
idleConnsClosed := make(chan struct{})
go func() {
<-stop
<-ctx.Done()
log.Info("shutting down webhook server")
// TODO: use a context with reasonable timeout
@ -215,8 +272,7 @@ func (s *Server) Start(stop <-chan struct{}) error {
close(idleConnsClosed)
}()
err = srv.Serve(listener)
if err != nil && err != http.ErrServerClosed {
if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed {
return err
}
@ -227,5 +283,20 @@ func (s *Server) Start(stop <-chan struct{}) error {
// InjectFunc injects the field setter into the server.
func (s *Server) InjectFunc(f inject.Func) error {
s.setFields = f
// inject fields here that weren't injected in Register because we didn't have setFields yet.
baseHookLog := log.WithName("webhooks")
for hookPath, webhook := range s.webhooks {
if err := s.setFields(webhook); err != nil {
return err
}
// NB(directxman12): we don't propagate this further by wrapping setFields because it's
// unclear if this is how we want to deal with log propagation. In this specific instance,
// we want to be able to pass a logger to webhooks because they don't know their own path.
if _, err := inject.LoggerInto(baseHookLog.WithValues("webhook", hookPath), webhook); err != nil {
return err
}
}
return nil
}