github.com/percona/percona-xtradb-cluster-operator@v1.14.0/pkg/webhook/hook.go (about)

     1  package webhook
     2  
     3  import (
     4  	"context"
     5  	"io"
     6  	"net/http"
     7  	"os"
     8  
     9  	"github.com/go-logr/logr"
    10  	"github.com/go-logr/zapr"
    11  	"github.com/pkg/errors"
    12  	"go.uber.org/zap"
    13  	admission "k8s.io/api/admission/v1"
    14  	admissionregistration "k8s.io/api/admissionregistration/v1"
    15  	appsv1 "k8s.io/api/apps/v1"
    16  	corev1 "k8s.io/api/core/v1"
    17  	k8serrors "k8s.io/apimachinery/pkg/api/errors"
    18  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    19  	"k8s.io/apimachinery/pkg/runtime"
    20  	"k8s.io/apimachinery/pkg/types"
    21  	"k8s.io/apimachinery/pkg/util/intstr"
    22  	"k8s.io/client-go/util/cert"
    23  	"sigs.k8s.io/controller-runtime/pkg/client"
    24  	"sigs.k8s.io/controller-runtime/pkg/manager"
    25  
    26  	v1 "github.com/percona/percona-xtradb-cluster-operator/pkg/apis/pxc/v1"
    27  	"github.com/percona/percona-xtradb-cluster-operator/pkg/k8s"
    28  	"github.com/percona/percona-xtradb-cluster-operator/pkg/pxctls"
    29  	"github.com/percona/percona-xtradb-cluster-operator/pkg/webhook/json"
    30  )
    31  
    32  const certPath = "/tmp/k8s-webhook-server/serving-certs/"
    33  
    34  var hookPath = "/validate-percona-xtradbcluster"
    35  
    36  type hook struct {
    37  	cl        client.Client
    38  	scheme    *runtime.Scheme
    39  	caBundle  []byte
    40  	namespace string
    41  	log       logr.Logger
    42  }
    43  
    44  func (h *hook) Start(ctx context.Context) error {
    45  	err := h.setup(ctx)
    46  	if err != nil {
    47  		h.log.Info("failed to setup webhook", "err", err.Error())
    48  	}
    49  	<-ctx.Done()
    50  	return nil
    51  }
    52  
    53  func (h *hook) setup(ctx context.Context) error {
    54  	operatorDeployment, err := h.operatorDeployment()
    55  	if err != nil {
    56  		return errors.Wrap(err, "failed to get operator deployment")
    57  	}
    58  
    59  	ref, err := k8s.OwnerRef(operatorDeployment, h.scheme)
    60  	if err != nil {
    61  		return errors.Wrap(err, "failed to get deployment owner ref")
    62  	}
    63  
    64  	err = h.createService(ctx, ref)
    65  	if err != nil {
    66  		return errors.Wrap(err, "Can't create service")
    67  	}
    68  
    69  	err = h.createWebhook(ref)
    70  	if err != nil {
    71  		return errors.Wrap(err, "can't create webhook")
    72  	}
    73  	return nil
    74  }
    75  
    76  func (h *hook) createService(ctx context.Context, ownerRef metav1.OwnerReference) error {
    77  	opPod, err := k8s.OperatorPod(ctx, h.cl)
    78  	if err != nil {
    79  		return errors.Wrap(err, "get operator pod")
    80  	}
    81  	svc := &corev1.Service{
    82  		ObjectMeta: metav1.ObjectMeta{
    83  			Name:            "percona-xtradb-cluster-operator",
    84  			Namespace:       h.namespace,
    85  			Labels:          map[string]string{"name": "percona-xtradb-cluster-operator"},
    86  			OwnerReferences: []metav1.OwnerReference{ownerRef},
    87  		},
    88  		Spec: corev1.ServiceSpec{
    89  			Ports:    []corev1.ServicePort{{Port: 443, TargetPort: intstr.FromInt(9443)}},
    90  			Selector: opPod.Labels,
    91  		},
    92  	}
    93  	err = h.cl.Create(context.TODO(), svc)
    94  	if err != nil {
    95  		if k8serrors.IsAlreadyExists(err) {
    96  			service := &corev1.Service{}
    97  			err = h.cl.Get(context.TODO(), types.NamespacedName{
    98  				Name:      "percona-xtradb-cluster-operator",
    99  				Namespace: h.namespace,
   100  			}, service)
   101  			if err != nil {
   102  				return err
   103  			}
   104  
   105  			service.Spec.Selector = opPod.Labels
   106  			service.ObjectMeta.OwnerReferences = []metav1.OwnerReference{ownerRef}
   107  			return h.cl.Update(context.TODO(), service)
   108  		}
   109  		return err
   110  	}
   111  	return nil
   112  }
   113  
   114  func (h *hook) createWebhook(ownerRef metav1.OwnerReference) error {
   115  	failPolicy := admissionregistration.Fail
   116  	sideEffects := admissionregistration.SideEffectClassNone
   117  	hook := &admissionregistration.ValidatingWebhookConfiguration{
   118  		ObjectMeta: metav1.ObjectMeta{
   119  			Name:            "percona-xtradbcluster-webhook",
   120  			OwnerReferences: []metav1.OwnerReference{ownerRef},
   121  		},
   122  		Webhooks: []admissionregistration.ValidatingWebhook{
   123  			{
   124  				AdmissionReviewVersions: []string{"v1"},
   125  				Name:                    "validationwebhook.pxc.percona.com",
   126  				ClientConfig: admissionregistration.WebhookClientConfig{
   127  					Service: &admissionregistration.ServiceReference{
   128  						Namespace: h.namespace,
   129  						Name:      "percona-xtradb-cluster-operator",
   130  						Path:      &hookPath,
   131  					},
   132  					CABundle: h.caBundle,
   133  				},
   134  				SideEffects:   &sideEffects,
   135  				FailurePolicy: &failPolicy,
   136  				Rules: []admissionregistration.RuleWithOperations{
   137  					{
   138  						Rule: admissionregistration.Rule{
   139  							APIGroups:   []string{"pxc.percona.com"},
   140  							APIVersions: []string{"*"},
   141  							Resources:   []string{"perconaxtradbclusters/*"},
   142  						},
   143  						Operations: []admissionregistration.OperationType{"CREATE", "UPDATE"},
   144  					},
   145  				},
   146  			},
   147  		},
   148  	}
   149  
   150  	err := h.cl.Create(context.TODO(), hook)
   151  	if k8serrors.IsForbidden(err) {
   152  		return nil
   153  	}
   154  
   155  	if err != nil && k8serrors.IsAlreadyExists(err) {
   156  		hook := &admissionregistration.ValidatingWebhookConfiguration{}
   157  		err := h.cl.Get(context.TODO(), types.NamespacedName{
   158  			Name: "percona-xtradbcluster-webhook",
   159  		}, hook)
   160  		if err != nil {
   161  			return err
   162  		}
   163  
   164  		hook.Webhooks[0].ClientConfig.CABundle = h.caBundle
   165  		hook.ObjectMeta.OwnerReferences = []metav1.OwnerReference{ownerRef}
   166  		return h.cl.Update(context.TODO(), hook)
   167  	}
   168  	return err
   169  }
   170  
   171  // SetupWebhook prepares certificates for webhook and
   172  // create ValidatingWebhookConfiguration k8s object
   173  func SetupWebhook(mgr manager.Manager) error {
   174  	err := admissionregistration.AddToScheme(mgr.GetScheme())
   175  	if err != nil {
   176  		return errors.Wrap(err, "add admissionregistration to scheme")
   177  	}
   178  
   179  	namespace, err := k8s.GetOperatorNamespace()
   180  	if err != nil {
   181  		return errors.Wrap(err, "get operator namespace")
   182  	}
   183  
   184  	ca, err := setupCertificates(mgr.GetAPIReader(), namespace)
   185  	if err != nil {
   186  		return errors.Wrap(err, "prepare hook tls certs")
   187  	}
   188  
   189  	zapLog, err := zap.NewProduction()
   190  	if err != nil {
   191  		return errors.Wrap(err, "create logger")
   192  	}
   193  
   194  	h := &hook{
   195  		cl:        mgr.GetClient(),
   196  		scheme:    mgr.GetScheme(),
   197  		caBundle:  ca,
   198  		namespace: namespace,
   199  		log:       zapr.NewLogger(zapLog),
   200  	}
   201  
   202  	mgr.GetWebhookServer().Register(hookPath, h)
   203  
   204  	err = mgr.Add(h)
   205  	if err != nil {
   206  		return errors.Wrap(err, "add webhook creator to manager")
   207  	}
   208  
   209  	return nil
   210  }
   211  
   212  func setupCertificates(cl client.Reader, namespace string) ([]byte, error) {
   213  	certSecret := &corev1.Secret{}
   214  	err := cl.Get(context.TODO(), types.NamespacedName{
   215  		Namespace: namespace,
   216  		Name:      "pxc-webhook-ssl",
   217  	}, certSecret)
   218  	if err != nil && !k8serrors.IsNotFound(err) {
   219  		return nil, err
   220  	}
   221  
   222  	var ca, crt, key []byte
   223  
   224  	if k8serrors.IsNotFound(err) {
   225  		ca, crt, key, err = pxctls.Issue([]string{"percona-xtradb-cluster-operator." + namespace + ".svc"})
   226  		if err != nil {
   227  			return nil, errors.Wrap(err, "issue tls certificates")
   228  		}
   229  	} else {
   230  		ca, crt, key = certSecret.Data["ca.crt"], certSecret.Data["tls.crt"], certSecret.Data["tls.key"]
   231  	}
   232  
   233  	return ca, writeCerts(crt, key)
   234  }
   235  
   236  func writeCerts(crt, key []byte) error {
   237  	err := cert.WriteCert(certPath+"tls.crt", crt)
   238  	if err != nil {
   239  		return errors.Wrap(err, "write tls.crt")
   240  	}
   241  	err = cert.WriteCert(certPath+"tls.key", key)
   242  	if err != nil {
   243  		return errors.Wrap(err, "write tls.key")
   244  	}
   245  	return nil
   246  }
   247  
   248  func (h *hook) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   249  	defer r.Body.Close()
   250  	req := &admission.AdmissionReview{}
   251  
   252  	bytes, err := io.ReadAll(r.Body)
   253  	if err != nil {
   254  		h.log.Error(err, "can't read request body")
   255  		return
   256  	}
   257  
   258  	if err := json.Decode(bytes, req, true); err != nil {
   259  		h.log.Error(err, "Can't decode admission review request")
   260  		return
   261  	}
   262  
   263  	if req.Request.Kind.Group == "autoscaling" && req.Request.Kind.Kind == "Scale" {
   264  		if err = sendResponse(req.Request.UID, req.TypeMeta, w, nil); err != nil {
   265  			h.log.Error(err, "Can't send validation response")
   266  		}
   267  		return
   268  	}
   269  
   270  	cr := &v1.PerconaXtraDBCluster{}
   271  	if err := json.Decode(req.Request.Object.Raw, cr, true); err != nil {
   272  		err = sendResponse(req.Request.UID, req.TypeMeta, w, err)
   273  		if err != nil {
   274  			h.log.Error(err, "Can't send validation response")
   275  		}
   276  		return
   277  	}
   278  
   279  	if cr.Spec.EnableCRValidationWebhook == nil || !*cr.Spec.EnableCRValidationWebhook {
   280  		err = sendResponse(req.Request.UID, req.TypeMeta, w, nil)
   281  		if err != nil {
   282  			h.log.Error(err, "Can't send validation response")
   283  		}
   284  		return
   285  	}
   286  
   287  	err = sendResponse(req.Request.UID, req.TypeMeta, w, cr.Validate())
   288  	if err != nil {
   289  		h.log.Error(err, "Can't send validation response")
   290  	}
   291  }
   292  
   293  func sendResponse(uid types.UID, meta metav1.TypeMeta, w http.ResponseWriter, err error) error {
   294  	resp := &admission.AdmissionReview{
   295  		TypeMeta: meta,
   296  		Response: &admission.AdmissionResponse{
   297  			UID:     uid,
   298  			Allowed: true,
   299  		},
   300  	}
   301  	if err != nil {
   302  		resp.Response.Allowed = false
   303  		resp.Response.Result = &metav1.Status{
   304  			Message: err.Error(),
   305  			Code:    403,
   306  		}
   307  	}
   308  	data, err := json.Marshal(resp)
   309  	if err != nil {
   310  		return errors.Wrap(err, "marshall response")
   311  	}
   312  	w.Header().Add("Content-Type", "application/json")
   313  	_, err = w.Write(data)
   314  	if err != nil {
   315  		return errors.Wrap(err, "write response")
   316  	}
   317  	return nil
   318  }
   319  
   320  func (h *hook) operatorDeployment() (*appsv1.Deployment, error) {
   321  	operatorDeploymentName := os.Getenv("OPERATOR_NAME")
   322  	if operatorDeploymentName == "" {
   323  		operatorDeploymentName = "percona-xtradb-cluster-operator"
   324  	}
   325  	deployment := &appsv1.Deployment{}
   326  	err := h.cl.Get(context.TODO(), types.NamespacedName{
   327  		Name:      operatorDeploymentName,
   328  		Namespace: h.namespace,
   329  	}, deployment)
   330  	return deployment, err
   331  }