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 }