From 6c1e20647f700bdf3d851cc79839c581a46d41cb Mon Sep 17 00:00:00 2001 From: ruromero Date: Tue, 24 Nov 2020 10:29:26 +0100 Subject: [PATCH] Skip validatingWebhookConfiguration when configured Signed-off-by: ruromero --- README.md | 28 ++++++++ api/v1alpha1/gatekeeper_types.go | 1 + .../operator.gatekeeper.sh_gatekeepers.yaml | 1 + controllers/gatekeeper_controller.go | 49 ++++++-------- controllers/gatekeeper_controller_test.go | 35 +++++++--- pkg/bindata/bindata.go | 64 +++++++++---------- pkg/util/util.go | 25 ++++++++ test/gatekeeper_controller_test.go | 46 ++++++++++--- 8 files changed, 171 insertions(+), 78 deletions(-) diff --git a/README.md b/README.md index ed9328ad..cc1b2f99 100644 --- a/README.md +++ b/README.md @@ -113,3 +113,31 @@ If you would like to deploy Operator using OLM, you'll need to build and push th source: gatekeeper-operator sourceNamespace: gatekeeper-system ``` + +## Usage + +Before using Gatekeeper you have to create a `gatekeeper` resource that will be consumed by the operator and create all the necessary resources for you. + +Here you can find an example of a `gatekeeper` resource definition: + +```yaml +apiVersion: operator.gatekeeper.sh/v1alpha1 +kind: Gatekeeper +metadata: + name: gatekeeper +spec: + # Add fields here + audit: + replicas: 1 + logLevel: ERROR +``` + +If nothing is defined in the `spec`, the default values will be used. In the example above the number of replicas for the audit pod is set to `1` and the logLevel to `ERROR` where the default is `INFO`. + +The default behaviour for the `ValidatingWebhookConfiguration` is `ENABLED`, that means that it will be created. To disable the `ValidatingWebhookConfiguration` deployment, set the `validatingWebhook` spec property to `DISABLED`. + +In order to create an instance of gatekeeper in the specified namespace you can start from one of the [sample configurations](config/samples). + +```shell +kubectl create -f config/samples/operator_v1alpha1_gatekeeper.yaml +``` diff --git a/api/v1alpha1/gatekeeper_types.go b/api/v1alpha1/gatekeeper_types.go index 1e962c28..ff95ec5b 100644 --- a/api/v1alpha1/gatekeeper_types.go +++ b/api/v1alpha1/gatekeeper_types.go @@ -34,6 +34,7 @@ type GatekeeperSpec struct { Image *ImageConfig `json:"image,omitempty"` // +optional Audit *AuditConfig `json:"audit,omitempty"` + // +kubebuilder:default:=Enabled // +optional ValidatingWebhook *WebhookMode `json:"validatingWebhook,omitempty"` // +optional diff --git a/config/crd/bases/operator.gatekeeper.sh_gatekeepers.yaml b/config/crd/bases/operator.gatekeeper.sh_gatekeepers.yaml index 8d545eca..97ec6cc3 100644 --- a/config/crd/bases/operator.gatekeeper.sh_gatekeepers.yaml +++ b/config/crd/bases/operator.gatekeeper.sh_gatekeepers.yaml @@ -750,6 +750,7 @@ spec: type: object type: array validatingWebhook: + default: Enabled enum: - Enabled - Disabled diff --git a/controllers/gatekeeper_controller.go b/controllers/gatekeeper_controller.go index f8009e7e..997d5244 100644 --- a/controllers/gatekeeper_controller.go +++ b/controllers/gatekeeper_controller.go @@ -18,10 +18,8 @@ package controllers import ( "context" - "encoding/json" "fmt" "strconv" - "strings" "time" "github.com/RHsyseng/operator-utils/pkg/utils/openshift" @@ -202,7 +200,7 @@ func (r *GatekeeperReconciler) SetupWithManager(mgr ctrl.Manager) error { } func (r *GatekeeperReconciler) deployGatekeeperResources(gatekeeper *operatorv1alpha1.Gatekeeper, platformName string) error { - for _, a := range orderedStaticAssets { + for _, a := range getStaticAssets(gatekeeper) { if a == RoleFile && platformName == "OpenShift" { a = openshiftAssetsDir + a } @@ -221,6 +219,19 @@ func (r *GatekeeperReconciler) deployGatekeeperResources(gatekeeper *operatorv1a return nil } +func getStaticAssets(gatekeeper *operatorv1alpha1.Gatekeeper) []string { + if gatekeeper.Spec.ValidatingWebhook == nil || *gatekeeper.Spec.ValidatingWebhook == operatorv1alpha1.WebhookEnabled { + return orderedStaticAssets + } + assets := make([]string, 0) + for _, a := range orderedStaticAssets { + if a != ValidatingWebhookConfiguration { + assets = append(assets, a) + } + } + return assets +} + func (r *GatekeeperReconciler) updateOrCreateResource(manifest *manifest.Manifest, gatekeeper *operatorv1alpha1.Gatekeeper) error { var err error ctx := context.Background() @@ -506,7 +517,7 @@ func setFailurePolicy(obj *unstructured.Unstructured, failurePolicy *admregv1.Fa func setAffinity(obj *unstructured.Unstructured, spec operatorv1alpha1.GatekeeperSpec) error { if spec.Affinity != nil { - if err := unstructured.SetNestedField(obj.Object, toMap(spec.Affinity), "spec", "template", "spec", "affinity"); err != nil { + if err := unstructured.SetNestedField(obj.Object, util.ToMap(spec.Affinity), "spec", "template", "spec", "affinity"); err != nil { return errors.Wrapf(err, "Failed to set affinity value") } } @@ -535,7 +546,7 @@ func setTolerations(obj *unstructured.Unstructured, spec operatorv1alpha1.Gateke if spec.Tolerations != nil { tolerations := make([]interface{}, len(spec.Tolerations)) for i, t := range spec.Tolerations { - tolerations[i] = toMap(t) + tolerations[i] = util.ToMap(t) } if err := unstructured.SetNestedSlice(obj.Object, tolerations, "spec", "template", "spec", "tolerations"); err != nil { return errors.Wrapf(err, "Failed to set container tolerations") @@ -548,7 +559,7 @@ func setTolerations(obj *unstructured.Unstructured, spec operatorv1alpha1.Gateke func setResources(container map[string]interface{}, spec operatorv1alpha1.GatekeeperSpec) error { if spec.Resources != nil { - if err := unstructured.SetNestedField(container, toMap(spec.Resources), "resources"); err != nil { + if err := unstructured.SetNestedField(container, util.ToMap(spec.Resources), "resources"); err != nil { return errors.Wrapf(err, "Failed to set container resources") } } @@ -601,35 +612,15 @@ func setContainerArg(obj *unstructured.Unstructured, containerName, argName stri } exists := false for i, arg := range args { - n, _ := fromArg(arg) + n, _ := util.FromArg(arg) if n == argName { - args[i] = ToArg(argName, argValue) + args[i] = util.ToArg(argName, argValue) exists = true } } if !exists { - args = append(args, ToArg(argName, argValue)) + args = append(args, util.ToArg(argName, argValue)) } return unstructured.SetNestedStringSlice(container, args, "args") }) } - -// toMap Convenience method to convert any struct into a map -func toMap(obj interface{}) map[string]interface{} { - var result map[string]interface{} - resultRec, _ := json.Marshal(obj) - json.Unmarshal(resultRec, &result) - return result -} - -func ToArg(name, value string) string { - return name + "=" + value -} - -func fromArg(arg string) (key, value string) { - parts := strings.Split(arg, "=") - if len(parts) == 1 { - return parts[0], "" - } - return parts[0], parts[1] -} diff --git a/controllers/gatekeeper_controller_test.go b/controllers/gatekeeper_controller_test.go index 15e211ea..ca548ff8 100644 --- a/controllers/gatekeeper_controller_test.go +++ b/controllers/gatekeeper_controller_test.go @@ -31,6 +31,23 @@ import ( test "github.com/gatekeeper/gatekeeper-operator/test/util" ) +func TestDeployValidatingWebhookConfig(t *testing.T) { + g := NewWithT(t) + gatekeeper := &operatorv1alpha1.Gatekeeper{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "testns", + }, + } + g.Expect(getStaticAssets(gatekeeper)).To(ContainElement(ValidatingWebhookConfiguration)) + webhookMode := operatorv1alpha1.WebhookEnabled + gatekeeper.Spec.ValidatingWebhook = &webhookMode + g.Expect(getStaticAssets(gatekeeper)).To(ContainElement(ValidatingWebhookConfiguration)) + webhookMode = operatorv1alpha1.WebhookDisabled + gatekeeper.Spec.ValidatingWebhook = &webhookMode + g.Expect(getStaticAssets(gatekeeper)).NotTo(ContainElement(ValidatingWebhookConfiguration)) +} + func TestReplicas(t *testing.T) { g := NewWithT(t) auditReplicaOverride := int32(4) @@ -152,7 +169,7 @@ func assertWebhookAffinity(g *WithT, manifest *manifest.Manifest, expected *core } func assertAffinity(g *WithT, expected *corev1.Affinity, current interface{}) { - g.Expect(toMap(expected)).To(BeEquivalentTo(toMap(current))) + g.Expect(util.ToMap(expected)).To(BeEquivalentTo(util.ToMap(current))) } func TestNodeSelector(t *testing.T) { @@ -312,7 +329,7 @@ func assertTolerations(g *WithT, manifest *manifest.Manifest, expected []corev1. g.Expect(found).To(BeFalse()) } else { for i, toleration := range expected { - g.Expect(toMap(toleration)).To(BeEquivalentTo(current[i])) + g.Expect(util.ToMap(toleration)).To(BeEquivalentTo(current[i])) } } } @@ -369,7 +386,7 @@ func assertResources(g *WithT, manifest *manifest.Manifest, expected *corev1.Res g.Expect(found).To(BeTrue()) for _, c := range containers { - current, found, err := unstructured.NestedMap(toMap(c), "resources") + current, found, err := unstructured.NestedMap(util.ToMap(c), "resources") g.Expect(err).ToNot(HaveOccurred()) g.Expect(found).To(BeTrue()) if expected == nil { @@ -436,7 +453,7 @@ func assertImage(g *WithT, manifest *manifest.Manifest, expected *operatorv1alph g.Expect(found).To(BeTrue()) for _, c := range containers { - currentImage, found, err := unstructured.NestedString(toMap(c), "image") + currentImage, found, err := unstructured.NestedString(util.ToMap(c), "image") g.Expect(err).ToNot(HaveOccurred()) g.Expect(found).To(BeTrue()) if expected == nil { @@ -444,7 +461,7 @@ func assertImage(g *WithT, manifest *manifest.Manifest, expected *operatorv1alph } else { g.Expect(*expected.Image).To(BeEquivalentTo(currentImage)) } - currentImagePullPolicy, found, err := unstructured.NestedString(toMap(c), "imagePullPolicy") + currentImagePullPolicy, found, err := unstructured.NestedString(util.ToMap(c), "imagePullPolicy") g.Expect(err).ToNot(HaveOccurred()) g.Expect(found).To(BeTrue()) if expected == nil { @@ -512,7 +529,7 @@ func assertFailurePolicy(g *WithT, manifest *manifest.Manifest, expected *admreg for _, w := range webhooks { webhook := w.(map[string]interface{}) if webhook["name"] == ValidationGatekeeperWebhook { - current, found, err := unstructured.NestedString(toMap(w), "failurePolicy") + current, found, err := unstructured.NestedString(util.ToMap(w), "failurePolicy") g.Expect(err).ToNot(HaveOccurred()) g.Expect(found).To(BeTrue()) if expected == nil { @@ -859,16 +876,16 @@ func getContainerArguments(g *WithT, containerName string, manifest *manifest.Ma g.Expect(found).To(BeTrue()) for _, c := range containers { - cName, found, err := unstructured.NestedString(toMap(c), "name") + cName, found, err := unstructured.NestedString(util.ToMap(c), "name") g.Expect(err).ToNot(HaveOccurred()) g.Expect(found).To(BeTrue()) if cName == containerName { - args, found, err := unstructured.NestedStringSlice(toMap(c), "args") + args, found, err := unstructured.NestedStringSlice(util.ToMap(c), "args") g.Expect(err).ToNot(HaveOccurred()) g.Expect(found).To(BeTrue()) argsMap := make(map[string]string) for _, arg := range args { - key, value := fromArg(arg) + key, value := util.FromArg(arg) argsMap[key] = value } return argsMap diff --git a/pkg/bindata/bindata.go b/pkg/bindata/bindata.go index 6f94dd3b..d9472d29 100644 --- a/pkg/bindata/bindata.go +++ b/pkg/bindata/bindata.go @@ -1004,38 +1004,6 @@ func configGatekeeperRbacAuthorizationK8sIo_v1_clusterrolebinding_gatekeeperMana return a, nil } -var _configGatekeeperRbacAuthorizationK8sIo_v1_rolebinding_gatekeeperManagerRolebindingYaml = []byte(`apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - labels: - gatekeeper.sh/system: "yes" - name: gatekeeper-manager-rolebinding - namespace: gatekeeper-system -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: gatekeeper-manager-role -subjects: -- kind: ServiceAccount - name: gatekeeper-admin - namespace: gatekeeper-system -`) - -func configGatekeeperRbacAuthorizationK8sIo_v1_rolebinding_gatekeeperManagerRolebindingYamlBytes() ([]byte, error) { - return _configGatekeeperRbacAuthorizationK8sIo_v1_rolebinding_gatekeeperManagerRolebindingYaml, nil -} - -func configGatekeeperRbacAuthorizationK8sIo_v1_rolebinding_gatekeeperManagerRolebindingYaml() (*asset, error) { - bytes, err := configGatekeeperRbacAuthorizationK8sIo_v1_rolebinding_gatekeeperManagerRolebindingYamlBytes() - if err != nil { - return nil, err - } - - info := bindataFileInfo{name: "config/gatekeeper/rbac.authorization.k8s.io_v1_rolebinding_gatekeeper-manager-rolebinding.yaml", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} - a := &asset{bytes: bytes, info: info} - return a, nil -} - var _configGatekeeperRbacAuthorizationK8sIo_v1_role_gatekeeperManagerRoleYaml = []byte(`apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: @@ -1081,6 +1049,38 @@ func configGatekeeperRbacAuthorizationK8sIo_v1_role_gatekeeperManagerRoleYaml() return a, nil } +var _configGatekeeperRbacAuthorizationK8sIo_v1_rolebinding_gatekeeperManagerRolebindingYaml = []byte(`apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + gatekeeper.sh/system: "yes" + name: gatekeeper-manager-rolebinding + namespace: gatekeeper-system +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: gatekeeper-manager-role +subjects: +- kind: ServiceAccount + name: gatekeeper-admin + namespace: gatekeeper-system +`) + +func configGatekeeperRbacAuthorizationK8sIo_v1_rolebinding_gatekeeperManagerRolebindingYamlBytes() ([]byte, error) { + return _configGatekeeperRbacAuthorizationK8sIo_v1_rolebinding_gatekeeperManagerRolebindingYaml, nil +} + +func configGatekeeperRbacAuthorizationK8sIo_v1_rolebinding_gatekeeperManagerRolebindingYaml() (*asset, error) { + bytes, err := configGatekeeperRbacAuthorizationK8sIo_v1_rolebinding_gatekeeperManagerRolebindingYamlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "config/gatekeeper/rbac.authorization.k8s.io_v1_rolebinding_gatekeeper-manager-rolebinding.yaml", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + var _configGatekeeperV1_secret_gatekeeperWebhookServerCertYaml = []byte(`apiVersion: v1 kind: Secret metadata: diff --git a/pkg/util/util.go b/pkg/util/util.go index 3e1de3f6..8bf61f9a 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -15,6 +15,9 @@ limitations under the License. package util import ( + "encoding/json" + "strings" + "github.com/openshift/library-go/pkg/manifest" "github.com/pkg/errors" @@ -39,3 +42,25 @@ func GetManifest(asset string) (*manifest.Manifest, error) { } return manifest, nil } + +// ToMap Convenience method to convert any struct into a map +func ToMap(obj interface{}) map[string]interface{} { + var result map[string]interface{} + resultRec, _ := json.Marshal(obj) + json.Unmarshal(resultRec, &result) + return result +} + +// ToArg Converts a key, value pair into a valid container argument. e.g. '--argName', 'argValue' returns '--argName=argValue' +func ToArg(name, value string) string { + return name + "=" + value +} + +// FromArg Converts a container argument into a key, value pair. e.g. '--argName=argValue' returns '--argName', 'argValue' +func FromArg(arg string) (key, value string) { + parts := strings.Split(arg, "=") + if len(parts) == 1 { + return parts[0], "" + } + return parts[0], parts[1] +} diff --git a/test/gatekeeper_controller_test.go b/test/gatekeeper_controller_test.go index 3e310e71..b610af29 100644 --- a/test/gatekeeper_controller_test.go +++ b/test/gatekeeper_controller_test.go @@ -130,6 +130,13 @@ var _ = Describe("Gatekeeper", func() { return getDeploymentReadyReplicas(ctx, auditName, gkDeployment) }, waitTimeout, pollInterval).Should(Equal(*gatekeeper.Spec.Audit.Replicas)) }) + + By("Checking validatingWebhookConfiguration is deployed", func() { + validatingWebhookConfiguration := &admregv1.ValidatingWebhookConfiguration{} + Eventually(func() error { + return K8sClient.Get(ctx, validatingWebhookName, validatingWebhookConfiguration) + }, waitTimeout, pollInterval).ShouldNot(HaveOccurred()) + }) }) }) }) @@ -316,51 +323,74 @@ var _ = Describe("Gatekeeper", func() { By("Checking expected audit interval", func() { value, found := getContainerArg(auditDeployment.Spec.Template.Spec.Containers[0].Args, controllers.AuditIntervalArg) Expect(found).To(BeTrue()) - Expect(value).To(Equal(controllers.ToArg(controllers.AuditIntervalArg, "10"))) + Expect(value).To(Equal(util.ToArg(controllers.AuditIntervalArg, "10"))) }) By("Checking expected audit log level", func() { value, found := getContainerArg(auditDeployment.Spec.Template.Spec.Containers[0].Args, controllers.LogLevelArg) Expect(found).To(BeTrue()) - Expect(value).To(Equal(controllers.ToArg(controllers.LogLevelArg, "DEBUG"))) + Expect(value).To(Equal(util.ToArg(controllers.LogLevelArg, "DEBUG"))) }) By("Checking expected audit constraint violation limit", func() { value, found := getContainerArg(auditDeployment.Spec.Template.Spec.Containers[0].Args, controllers.ConstraintViolationLimitArg) Expect(found).To(BeTrue()) - Expect(value).To(Equal(controllers.ToArg(controllers.ConstraintViolationLimitArg, "55"))) + Expect(value).To(Equal(util.ToArg(controllers.ConstraintViolationLimitArg, "55"))) }) By("Checking expected audit chunk size", func() { value, found := getContainerArg(auditDeployment.Spec.Template.Spec.Containers[0].Args, controllers.AuditChunkSizeArg) Expect(found).To(BeTrue()) - Expect(value).To(Equal(controllers.ToArg(controllers.AuditChunkSizeArg, "66"))) + Expect(value).To(Equal(util.ToArg(controllers.AuditChunkSizeArg, "66"))) }) By("Checking expected audit from cache", func() { value, found := getContainerArg(auditDeployment.Spec.Template.Spec.Containers[0].Args, controllers.AuditFromCacheArg) Expect(found).To(BeTrue()) - Expect(value).To(Equal(controllers.ToArg(controllers.AuditFromCacheArg, "true"))) + Expect(value).To(Equal(util.ToArg(controllers.AuditFromCacheArg, "true"))) }) By("Checking expected emit audit events", func() { value, found := getContainerArg(auditDeployment.Spec.Template.Spec.Containers[0].Args, controllers.EmitAuditEventsArg) Expect(found).To(BeTrue()) - Expect(value).To(Equal(controllers.ToArg(controllers.EmitAuditEventsArg, "true"))) + Expect(value).To(Equal(util.ToArg(controllers.EmitAuditEventsArg, "true"))) }) By("Checking expected emit admission events", func() { value, found := getContainerArg(webhookDeployment.Spec.Template.Spec.Containers[0].Args, controllers.EmitAdmissionEventsArg) Expect(found).To(BeTrue()) - Expect(value).To(Equal(controllers.ToArg(controllers.EmitAdmissionEventsArg, "true"))) + Expect(value).To(Equal(util.ToArg(controllers.EmitAdmissionEventsArg, "true"))) }) By("Checking expected webhook log level", func() { value, found := getContainerArg(webhookDeployment.Spec.Template.Spec.Containers[0].Args, controllers.LogLevelArg) Expect(found).To(BeTrue()) - Expect(value).To(Equal(controllers.ToArg(controllers.LogLevelArg, "ERROR"))) + Expect(value).To(Equal(util.ToArg(controllers.LogLevelArg, "ERROR"))) }) }) + + It("Does not deploy the validatingWebhookConfiguration", func() { + gatekeeper := emptyGatekeeper() + webhookMode := v1alpha1.WebhookDisabled + gatekeeper.Spec.ValidatingWebhook = &webhookMode + Expect(K8sClient.Create(ctx, gatekeeper)).Should(Succeed()) + + auditDeployment := &appsv1.Deployment{} + Eventually(func() error { + return K8sClient.Get(ctx, auditName, auditDeployment) + }, waitTimeout, pollInterval).ShouldNot(HaveOccurred()) + + webhookDeployment := &appsv1.Deployment{} + Eventually(func() error { + return K8sClient.Get(ctx, controllerManagerName, webhookDeployment) + }, waitTimeout, pollInterval).ShouldNot(HaveOccurred()) + + validatingWebhookConfiguration := &admregv1.ValidatingWebhookConfiguration{} + Eventually(func() bool { + err := K8sClient.Get(ctx, validatingWebhookName, validatingWebhookConfiguration) + return apierrors.IsNotFound(err) + }, waitTimeout, pollInterval).Should(BeTrue()) + }) }) })