From c06ec16e718ba7d0a889d88f99e7c19594cd8c4d Mon Sep 17 00:00:00 2001 From: pvsune Date: Mon, 5 Sep 2022 09:39:04 +0800 Subject: [PATCH] operator: Add defaulting webhook for Enterprise fields - Update Console validating webhook to mutating webhook - Refactor required keys used for validating/mutating to vars --- src/go/k8s/config/webhook/manifests.yaml | 30 ++++----- .../redpanda/console_controller_test.go | 2 +- src/go/k8s/main.go | 2 +- src/go/k8s/pkg/console/configmap.go | 21 +++++-- .../k8s/webhooks/redpanda/console_webhook.go | 63 ++++++++++++++++--- .../webhooks/redpanda/validate_enterprise.go | 48 ++++++++++++++ 6 files changed, 134 insertions(+), 32 deletions(-) create mode 100644 src/go/k8s/webhooks/redpanda/validate_enterprise.go diff --git a/src/go/k8s/config/webhook/manifests.yaml b/src/go/k8s/config/webhook/manifests.yaml index 1932178c3e369..bc50121fc5694 100644 --- a/src/go/k8s/config/webhook/manifests.yaml +++ b/src/go/k8s/config/webhook/manifests.yaml @@ -27,24 +27,15 @@ webhooks: resources: - clusters sideEffects: None - ---- -apiVersion: admissionregistration.k8s.io/v1 -kind: ValidatingWebhookConfiguration -metadata: - creationTimestamp: null - name: validating-webhook-configuration -webhooks: - admissionReviewVersions: - v1 - - v1beta1 clientConfig: service: name: webhook-service namespace: system - path: /validate-redpanda-vectorized-io-v1alpha1-cluster + path: /mutate-redpanda-vectorized-io-v1alpha1-console failurePolicy: Fail - name: vcluster.kb.io + name: mconsole.kb.io rules: - apiGroups: - redpanda.vectorized.io @@ -54,17 +45,26 @@ webhooks: - CREATE - UPDATE resources: - - clusters + - consoles sideEffects: None + +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + creationTimestamp: null + name: validating-webhook-configuration +webhooks: - admissionReviewVersions: - v1 + - v1beta1 clientConfig: service: name: webhook-service namespace: system - path: /validate-redpanda-vectorized-io-v1alpha1-console + path: /validate-redpanda-vectorized-io-v1alpha1-cluster failurePolicy: Fail - name: vconsole.kb.io + name: vcluster.kb.io rules: - apiGroups: - redpanda.vectorized.io @@ -74,5 +74,5 @@ webhooks: - CREATE - UPDATE resources: - - consoles + - clusters sideEffects: None diff --git a/src/go/k8s/controllers/redpanda/console_controller_test.go b/src/go/k8s/controllers/redpanda/console_controller_test.go index 6151e3fd1c83b..30c5208e3e376 100644 --- a/src/go/k8s/controllers/redpanda/console_controller_test.go +++ b/src/go/k8s/controllers/redpanda/console_controller_test.go @@ -212,7 +212,7 @@ var _ = Describe("Console controller", func() { It("Should create Enterprise fields in ConfigMap", func() { var ( rbacName = fmt.Sprintf("%s-rbac", ConsoleName) - rbacDataKey = "rbac.yaml" + rbacDataKey = consolepkg.EnterpriseRBACDataKey rbacDataVal = `roleBindings: - roleName: admin metadata: diff --git a/src/go/k8s/main.go b/src/go/k8s/main.go index 3bb16eb397223..1c3c6ed829855 100644 --- a/src/go/k8s/main.go +++ b/src/go/k8s/main.go @@ -140,7 +140,7 @@ func main() { os.Exit(1) } hookServer := mgr.GetWebhookServer() - hookServer.Register("/validate-redpanda-vectorized-io-v1alpha1-console", &webhook.Admission{Handler: &redpandawebhooks.ConsoleValidator{Client: mgr.GetClient()}}) + hookServer.Register("/mutate-redpanda-vectorized-io-v1alpha1-console", &webhook.Admission{Handler: &redpandawebhooks.ConsoleHandler{Client: mgr.GetClient()}}) } if err = (&redpandacontrollers.ConsoleReconciler{ diff --git a/src/go/k8s/pkg/console/configmap.go b/src/go/k8s/pkg/console/configmap.go index 688680533ec56..c942919c0e084 100644 --- a/src/go/k8s/pkg/console/configmap.go +++ b/src/go/k8s/pkg/console/configmap.go @@ -161,7 +161,7 @@ func (cm *ConfigMap) genEnterprise() (e Enterprise) { return Enterprise{ RBAC: EnterpriseRBAC{ Enabled: cm.consoleobj.Spec.Enterprise.RBAC.Enabled, - RoleBindingsFilepath: fmt.Sprintf("%s/%s", enterpriseRBACMountPath, "rbac.yaml"), + RoleBindingsFilepath: fmt.Sprintf("%s/%s", enterpriseRBACMountPath, EnterpriseRBACDataKey), }, } } @@ -169,8 +169,17 @@ func (cm *ConfigMap) genEnterprise() (e Enterprise) { } var ( - defaultLicenseSecretKey = "license" - defaultJWTSecretKey = "jwt" + // DefaultLicenseSecretKey is the defautl key in Enterprise License SecretKeyRef + DefaultLicenseSecretKey = "license" + + // DefaultJWTSecretKey is the defautl key in Enterprise JWT SecretKeyRef + DefaultJWTSecretKey = "jwt" + + // EnterpriseRBACDataKey is the required key in Enterprise RBAC + EnterpriseRBACDataKey = "rbac.yaml" + + // EnterpriseGoogleSADataKey is the required key in EnterpriseLoginGoogle SA + EnterpriseGoogleSADataKey = "sa.json" ) func (cm *ConfigMap) genLogin(ctx context.Context) (e EnterpriseLogin, err error) { @@ -183,7 +192,7 @@ func (cm *ConfigMap) genLogin(ctx context.Context) (e EnterpriseLogin, err error if err != nil { return e, err } - jwt, err := provider.JWTSecretRef.GetValue(jwtSecret, defaultJWTSecretKey) + jwt, err := provider.JWTSecretRef.GetValue(jwtSecret, DefaultJWTSecretKey) if err != nil { return e, err } @@ -212,7 +221,7 @@ func (cm *ConfigMap) genLogin(ctx context.Context) (e EnterpriseLogin, err error } if dir := provider.Google.Directory; dir != nil { enterpriseLogin.Google.Directory = &EnterpriseLoginGoogleDirectory{ - ServiceAccountFilepath: fmt.Sprintf("%s/%s", enterpriseGoogleSAMountPath, "sa.json"), + ServiceAccountFilepath: fmt.Sprintf("%s/%s", enterpriseGoogleSAMountPath, EnterpriseGoogleSADataKey), TargetPrincipal: provider.Google.Directory.TargetPrincipal, } } @@ -228,7 +237,7 @@ func (cm *ConfigMap) genLicense(ctx context.Context) (string, error) { if err != nil { return "", err } - licenseValue, err := license.GetValue(licenseSecret, defaultLicenseSecretKey) + licenseValue, err := license.GetValue(licenseSecret, DefaultLicenseSecretKey) if err != nil { return "", err } diff --git a/src/go/k8s/webhooks/redpanda/console_webhook.go b/src/go/k8s/webhooks/redpanda/console_webhook.go index 123c140864a50..cab30f3c1a0f3 100644 --- a/src/go/k8s/webhooks/redpanda/console_webhook.go +++ b/src/go/k8s/webhooks/redpanda/console_webhook.go @@ -3,31 +3,35 @@ package redpanda import ( "context" + "encoding/json" + "errors" "fmt" "net/http" redpandav1alpha1 "github.com/redpanda-data/redpanda/src/go/k8s/apis/redpanda/v1alpha1" + consolepkg "github.com/redpanda-data/redpanda/src/go/k8s/pkg/console" apierrors "k8s.io/apimachinery/pkg/api/errors" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" ) -// +kubebuilder:webhook:path=/validate-redpanda-vectorized-io-v1alpha1-console,mutating=false,failurePolicy=fail,sideEffects=None,groups="redpanda.vectorized.io",resources=consoles,verbs=create;update,versions=v1alpha1,name=vconsole.kb.io,admissionReviewVersions=v1 +// +kubebuilder:webhook:path=/mutate-redpanda-vectorized-io-v1alpha1-console,mutating=true,failurePolicy=fail,sideEffects=None,groups="redpanda.vectorized.io",resources=consoles,verbs=create;update,versions=v1alpha1,name=mconsole.kb.io,admissionReviewVersions=v1 -// ConsoleValidator validates Consoles -type ConsoleValidator struct { +// ConsoleHandler implements admission.Handler +// It creates a webhook by using the lower level webhook handler +// REF https://github.com/kubernetes-sigs/kubebuilder/issues/1216 +type ConsoleHandler struct { Client client.Client decoder *admission.Decoder } // Handle processes admission for Console -func (v *ConsoleValidator) Handle( +func (v *ConsoleHandler) Handle( ctx context.Context, req admission.Request, // nolint:gocritic // interface not require pointer ) admission.Response { console := &redpandav1alpha1.Console{} - err := v.decoder.Decode(req, console) - if err != nil { + if err := v.decoder.Decode(req, console); err != nil { return admission.Errored(http.StatusBadRequest, err) } @@ -43,14 +47,55 @@ func (v *ConsoleValidator) Handle( return admission.Errored(http.StatusBadRequest, err) } - return admission.Allowed("") + if err := ValidateEnterpriseRBAC(ctx, v.Client, console); err != nil { + if errors.Is(err, ErrEnterpriseRBACDataKeyNotFound) { + return admission.Denied(fmt.Sprintf("configmap %s/%s %s", console.GetNamespace(), console.Spec.Enterprise.RBAC.RoleBindingsRef.Name, err)) + } + return admission.Errored(http.StatusBadRequest, err) + } + + if err := ValidateEnterpriseGoogleSA(ctx, v.Client, console); err != nil { + if errors.Is(err, ErrEnterpriseGoogleSADataKeyNotFound) { + return admission.Denied(fmt.Sprintf("configmap %s/%s %s", console.GetNamespace(), console.Spec.Login.Google.Directory.ServiceAccountRef.Name, err)) + } + return admission.Errored(http.StatusBadRequest, err) + } + + // Implement defaulting + response, err := Default(console) + if err != nil { + return admission.Errored(http.StatusBadRequest, err) + } + return *response +} + +// Default implements admission defaulting +func Default(console *redpandav1alpha1.Console) (*admission.Response, error) { + original, err := json.Marshal(console.DeepCopy()) + if err != nil { + return nil, err + } + + if login := console.Spec.Login; login != nil && login.JWTSecretRef.Key == "" { + login.JWTSecretRef.Key = consolepkg.DefaultJWTSecretKey + } + if license := console.Spec.LicenseRef; license != nil && license.Key == "" { + license.Key = consolepkg.DefaultLicenseSecretKey + } + + current, err := json.Marshal(console) + if err != nil { + return nil, err + } + response := admission.PatchResponseFromRaw(original, current) + return &response, nil } -// ConsoleValidator implements admission.DecoderInjector. +// ConsoleHandler implements admission.DecoderInjector. // A decoder will be automatically injected. // InjectDecoder injects the decoder. -func (v *ConsoleValidator) InjectDecoder(d *admission.Decoder) error { +func (v *ConsoleHandler) InjectDecoder(d *admission.Decoder) error { v.decoder = d return nil } diff --git a/src/go/k8s/webhooks/redpanda/validate_enterprise.go b/src/go/k8s/webhooks/redpanda/validate_enterprise.go new file mode 100644 index 0000000000000..69eec95cb78b0 --- /dev/null +++ b/src/go/k8s/webhooks/redpanda/validate_enterprise.go @@ -0,0 +1,48 @@ +// Package redpanda defines Webhooks for redpanda API group +package redpanda + +import ( + "context" + "fmt" + + redpandav1alpha1 "github.com/redpanda-data/redpanda/src/go/k8s/apis/redpanda/v1alpha1" + consolepkg "github.com/redpanda-data/redpanda/src/go/k8s/pkg/console" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var ( + // ErrEnterpriseRBACDataKeyNotFound error when "rbac.yaml" not found in RBAC ref + ErrEnterpriseRBACDataKeyNotFound = fmt.Errorf("must contain key '%s'", consolepkg.EnterpriseRBACDataKey) + + // ErrEnterpriseGoogleSADataKeyNotFound error when "sa.json" not found in Google SA ref + ErrEnterpriseGoogleSADataKeyNotFound = fmt.Errorf("must contain key '%s'", consolepkg.EnterpriseGoogleSADataKey) +) + +// ValidateEnterpriseRBAC validates the referenced RBAC ConfigMap +func ValidateEnterpriseRBAC(ctx context.Context, cl client.Client, console *redpandav1alpha1.Console) error { + if enterprise := console.Spec.Enterprise; enterprise != nil { + configmap := &corev1.ConfigMap{} + if err := cl.Get(ctx, client.ObjectKey{Namespace: console.GetNamespace(), Name: enterprise.RBAC.RoleBindingsRef.Name}, configmap); err != nil { + return err + } + if _, ok := configmap.Data[consolepkg.EnterpriseRBACDataKey]; !ok { + return ErrEnterpriseRBACDataKeyNotFound + } + } + return nil +} + +// ValidateEnterpriseGoogleSA validates the referenced Google SA ConfigMap +func ValidateEnterpriseGoogleSA(ctx context.Context, cl client.Client, console *redpandav1alpha1.Console) error { + if login := console.Spec.Login; login != nil && login.Google != nil && login.Google.Directory != nil { + configmap := &corev1.ConfigMap{} + if err := cl.Get(ctx, client.ObjectKey{Namespace: console.GetNamespace(), Name: login.Google.Directory.ServiceAccountRef.Name}, configmap); err != nil { + return err + } + if _, ok := configmap.Data[consolepkg.EnterpriseGoogleSADataKey]; !ok { + return ErrEnterpriseGoogleSADataKeyNotFound + } + } + return nil +}