Skip to content

Commit

Permalink
Merge pull request #576 from fabriziosestito/feat/webhooks-reconcilia…
Browse files Browse the repository at this point in the history
…tion

feat: reconcile policy webhook configurations
  • Loading branch information
flavio authored Nov 21, 2023
2 parents 3e2246a + 12c7b72 commit b48d736
Show file tree
Hide file tree
Showing 13 changed files with 906 additions and 302 deletions.
7 changes: 6 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
IMG ?= controller:latest
# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary.
ENVTEST_K8S_VERSION = 1.23
# K3S_TESTCONTAINER_VERSION refers to the version of k3s testcontainer to be used by envtest to run integration tests.
K3S_TESTCONTAINER_VERSION = v1.23.2-k3s1
# POLICY_SERVER_VERSION refers to the version of the policy server to be used by integration tests.
POLICY_SERVER_VERSION = v1.9.0
# Binary directory
ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
BIN_DIR := $(abspath $(ROOT_DIR)/bin)
Expand Down Expand Up @@ -111,7 +115,8 @@ unit-tests: manifests generate fmt vet setup-envtest ## Run unit tests.

.PHONY: setup-envtest integration-tests
integration-tests: manifests generate fmt vet setup-envtest ## Run integration tests.
ACK_GINKGO_DEPRECATIONS=2.12.0 KUBEBUILDER_ASSETS="$(KUBEBUILDER_ASSETS)" go test ./pkg/... ./controllers/... -ginkgo.v -ginkgo.progress -test.v -coverprofile cover.out
ACK_GINKGO_DEPRECATIONS=2.12.0 KUBEBUILDER_ASSETS="$(KUBEBUILDER_ASSETS)" go test ./pkg/... -ginkgo.v -ginkgo.progress -test.v -coverprofile cover.out
ACK_GINKGO_DEPRECATIONS=2.12.0 K3S_TESTCONTAINER_VERSION="$(K3S_TESTCONTAINER_VERSION)" POLICY_SERVER_VERSION="$(POLICY_SERVER_VERSION)" go test ./controllers/... -ginkgo.v -ginkgo.progress -test.v -coverprofile cover.out

.PHONY: generate-crds
generate-crds: $(KUSTOMIZE) manifests kustomize ## generate final crds with kustomize. Normally shipped in Helm charts.
Expand Down
48 changes: 47 additions & 1 deletion controllers/admissionpolicy_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"github.com/kubewarden/kubewarden-controller/internal/pkg/constants"
"github.com/kubewarden/kubewarden-controller/internal/pkg/naming"
policiesv1 "github.com/kubewarden/kubewarden-controller/pkg/apis/policies/v1"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
Expand Down Expand Up @@ -88,8 +89,15 @@ func (r *AdmissionPolicyReconciler) SetupWithManager(mgr ctrl.Manager) error {
&policiesv1.PolicyServer{},
handler.EnqueueRequestsFromMapFunc(r.findAdmissionPoliciesForPolicyServer),
).
Watches(
&admissionregistrationv1.ValidatingWebhookConfiguration{},
handler.EnqueueRequestsFromMapFunc(r.findAdmissionPolicyForWebhookConfiguration),
).
Watches(
&admissionregistrationv1.MutatingWebhookConfiguration{},
handler.EnqueueRequestsFromMapFunc(r.findAdmissionPolicyForWebhookConfiguration),
).
Complete(r)

if err != nil {
return errors.Join(errors.New("failed enrolling controller with manager"), err)
}
Expand Down Expand Up @@ -148,3 +156,41 @@ func (r *AdmissionPolicyReconciler) findAdmissionPoliciesForPolicyServer(ctx con
}
return r.findAdmissionPoliciesForConfigMap(&configMap)
}

func (r *AdmissionPolicyReconciler) findAdmissionPolicyForWebhookConfiguration(_ context.Context, webhookConfiguration client.Object) []reconcile.Request {
if _, found := webhookConfiguration.GetLabels()["kubewarden"]; !found {
return []reconcile.Request{}
}

policyScope, found := webhookConfiguration.GetLabels()[constants.WebhookConfigurationPolicyScopeLabelKey]
if !found {
r.Log.Error(nil, "Found a webhook configuration without a scope label", "name", webhookConfiguration.GetName())
return []reconcile.Request{}
}

// Filter out ClusterAdmissionPolicies
if policyScope != "namespace" {
return []reconcile.Request{}
}

policyNamespace, found := webhookConfiguration.GetAnnotations()[constants.WebhookConfigurationPolicyNamespaceAnnotationKey]
if !found {
r.Log.Error(nil, "Found a webhook configuration without a namespace annotation", "name", webhookConfiguration.GetName())
return []reconcile.Request{}
}

policyName, found := webhookConfiguration.GetAnnotations()[constants.WebhookConfigurationPolicyNameAnnotationKey]
if !found {
r.Log.Error(nil, "Found webhook configuration without a policy name annotation", "name", webhookConfiguration.GetName())
return []reconcile.Request{}
}

return []reconcile.Request{
{
NamespacedName: client.ObjectKey{
Name: policyName,
Namespace: policyNamespace,
},
},
}
}
283 changes: 214 additions & 69 deletions controllers/admissionpolicy_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,92 +14,237 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

//nolint:dupl
package controllers

import (
"fmt"
"time"

policiesv1 "github.com/kubewarden/kubewarden-controller/pkg/apis/policies/v1"
. "github.com/onsi/ginkgo/v2" //nolint:revive
. "github.com/onsi/gomega" //nolint:revive

"github.com/kubewarden/kubewarden-controller/internal/pkg/constants"
policiesv1 "github.com/kubewarden/kubewarden-controller/pkg/apis/policies/v1"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

var _ = Describe("Given an AdmissionPolicy", func() {
var _ = Describe("AdmissionPolicy controller", func() {
policyNamespace := "admission-policy-controller-test"

BeforeEach(func() {
someNamespace := someNamespace.DeepCopy()
Expect(
k8sClient.Create(ctx, someNamespace),
).To(HaveSucceededOrAlreadyExisted())
k8sClient.Create(ctx, &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: policyNamespace,
},
}),
).To(haveSucceededOrAlreadyExisted())
})
When("it does not have a status set", func() {
Context("and it is not deleted", func() {
Context("and it has an empty policy server set on its spec", func() {
var (
policyNamespace = someNamespace.Name
policyName = "unscheduled-policy"

When("creating a validating AdmissionPolicy", func() {
policyServerName := newName("policy-server")
policyName := newName("validating-policy")

It("should set the AdmissionPolicy to active", func() {
By("creating the PolicyServer")
Expect(
k8sClient.Create(ctx, policyServerFactory(policyServerName)),
).To(Succeed())

By("creating the AdmissionPolicy")
Expect(
k8sClient.Create(ctx, admissionPolicyFactory(policyName, policyNamespace, policyServerName, false)),
).To(Succeed())

By("changing the policy status to pending")
Eventually(func(g Gomega) (*policiesv1.AdmissionPolicy, error) {
return getTestAdmissionPolicy(policyNamespace, policyName)
}, timeout, pollInterval).Should(
HaveField("Status.PolicyStatus", Equal(policiesv1.PolicyStatusPending)),
)

By("changing the policy status to active")
Eventually(func(g Gomega) (*policiesv1.AdmissionPolicy, error) {
return getTestAdmissionPolicy(policyNamespace, policyName)
}, timeout, pollInterval).Should(
HaveField("Status.PolicyStatus", Equal(policiesv1.PolicyStatusActive)),
)
})

It("should create the ValidatingWebhookConfiguration", func() {
Eventually(func(g Gomega) {
validatingWebhookConfiguration, err := getTestValidatingWebhookConfiguration(fmt.Sprintf("namespaced-%s-%s", policyNamespace, policyName))

Expect(err).ToNot(HaveOccurred())
Expect(validatingWebhookConfiguration.Labels["kubewarden"]).To(Equal("true"))
Expect(validatingWebhookConfiguration.Labels[constants.WebhookConfigurationPolicyScopeLabelKey]).To(Equal("namespace"))
Expect(validatingWebhookConfiguration.Annotations[constants.WebhookConfigurationPolicyNameAnnotationKey]).To(Equal(policyName))
Expect(validatingWebhookConfiguration.Annotations[constants.WebhookConfigurationPolicyNamespaceAnnotationKey]).To(Equal(policyNamespace))
Expect(validatingWebhookConfiguration.Webhooks).To(HaveLen(1))
Expect(validatingWebhookConfiguration.Webhooks[0].ClientConfig.Service.Name).To(Equal(fmt.Sprintf("policy-server-%s", policyServerName)))
}, timeout, pollInterval).Should(Succeed())
})

When("the ValidatingWebhookConfiguration is changed", func() {
It("should be reconciled to the original state", func() {
By("changing the ValidatingWebhookConfiguration")
validatingWebhookConfiguration, err := getTestValidatingWebhookConfiguration(fmt.Sprintf("namespaced-%s-%s", policyNamespace, policyName))
Expect(err).ToNot(HaveOccurred())
originalValidatingWebhookConfiguration := validatingWebhookConfiguration.DeepCopy()

delete(validatingWebhookConfiguration.Labels, "kubewarden")
validatingWebhookConfiguration.Labels[constants.WebhookConfigurationPolicyScopeLabelKey] = newName("scope")
delete(validatingWebhookConfiguration.Annotations, constants.WebhookConfigurationPolicyNameAnnotationKey)
validatingWebhookConfiguration.Annotations[constants.WebhookConfigurationPolicyNamespaceAnnotationKey] = newName("namespace")
validatingWebhookConfiguration.Webhooks[0].ClientConfig.Service.Name = newName("service")
Expect(
k8sClient.Update(ctx, validatingWebhookConfiguration),
).To(Succeed())

By("reconciling the ValidatingWebhookConfiguration to its original state")
Eventually(func(g Gomega) (*admissionregistrationv1.ValidatingWebhookConfiguration, error) {
return getTestValidatingWebhookConfiguration(fmt.Sprintf("namespaced-%s-%s", policyNamespace, policyName))
}, timeout, pollInterval).Should(
And(
HaveField("Labels", Equal(originalValidatingWebhookConfiguration.Labels)),
HaveField("Annotations", Equal(originalValidatingWebhookConfiguration.Annotations)),
HaveField("Webhooks", Equal(originalValidatingWebhookConfiguration.Webhooks)),
),
)
BeforeEach(func() {
Expect(
k8sClient.Create(ctx, admissionPolicyWithPolicyServerName(policyName, "")),
).To(HaveSucceededOrAlreadyExisted())
})
It(fmt.Sprintf("should set its policy status to %q", policiesv1.PolicyStatusUnscheduled), func() {
Eventually(func(g Gomega) (*policiesv1.AdmissionPolicy, error) {
return getFreshAdmissionPolicy(policyNamespace, policyName)
}).Should(
WithTransform(
func(admissionPolicy *policiesv1.AdmissionPolicy) policiesv1.PolicyStatusEnum {
return admissionPolicy.Status.PolicyStatus
},
Equal(policiesv1.PolicyStatusUnscheduled),
),
)
})
})
Context("and it has a non-empty policy server set on its spec", func() {
var (
policyNamespace = someNamespace.Name
policyName = "scheduled-policy"
policyServerName = "some-policy-server"
})
})

When("creating a mutating AdmissionPolicy", func() {
policyServerName := newName("policy-server")
policyName := newName("mutating-policy")

It("should set the AdmissionPolicy to active", func() {
By("creating the PolicyServer")
Expect(
k8sClient.Create(ctx, policyServerFactory(policyServerName)),
).To(Succeed())

By("creating the AdmissionPolicy")
Expect(
k8sClient.Create(ctx, admissionPolicyFactory(policyName, policyNamespace, policyServerName, true)),
).To(Succeed())

By("changing the policy status to pending")
Eventually(func(g Gomega) (*policiesv1.AdmissionPolicy, error) {
return getTestAdmissionPolicy(policyNamespace, policyName)
}, timeout, pollInterval).Should(
HaveField("Status.PolicyStatus", Equal(policiesv1.PolicyStatusPending)),
)

By("changing the policy status to active")
Eventually(func(g Gomega) (*policiesv1.AdmissionPolicy, error) {
return getTestAdmissionPolicy(policyNamespace, policyName)
}, timeout, pollInterval).Should(
HaveField("Status.PolicyStatus", Equal(policiesv1.PolicyStatusActive)),
)
})

It("should create the MutatingWebhookConfiguration", func() {
Eventually(func(g Gomega) {
mutatingWebhookConfiguration, err := getTestMutatingWebhookConfiguration(fmt.Sprintf("namespaced-%s-%s", policyNamespace, policyName))

Expect(err).ToNot(HaveOccurred())
Expect(mutatingWebhookConfiguration.Labels["kubewarden"]).To(Equal("true"))
Expect(mutatingWebhookConfiguration.Labels[constants.WebhookConfigurationPolicyScopeLabelKey]).To(Equal("namespace"))
Expect(mutatingWebhookConfiguration.Annotations[constants.WebhookConfigurationPolicyNameAnnotationKey]).To(Equal(policyName))
Expect(mutatingWebhookConfiguration.Annotations[constants.WebhookConfigurationPolicyNamespaceAnnotationKey]).To(Equal(policyNamespace))
Expect(mutatingWebhookConfiguration.Webhooks).To(HaveLen(1))
Expect(mutatingWebhookConfiguration.Webhooks[0].ClientConfig.Service.Name).To(Equal(fmt.Sprintf("policy-server-%s", policyServerName)))
}, timeout, pollInterval).Should(Succeed())
})

When("the MutatingWebhookConfiguration is changed", func() {
It("should be reconciled to the original state", func() {
By("changing the MutatingWebhookConfiguration")
mutatingWebhookConfiguration, err := getTestMutatingWebhookConfiguration(fmt.Sprintf("namespaced-%s-%s", policyNamespace, policyName))
Expect(err).ToNot(HaveOccurred())
originalMutatingWebhookConfiguration := mutatingWebhookConfiguration.DeepCopy()

delete(mutatingWebhookConfiguration.Labels, "kubewarden")
mutatingWebhookConfiguration.Labels[constants.WebhookConfigurationPolicyScopeLabelKey] = newName("scope")
delete(mutatingWebhookConfiguration.Annotations, constants.WebhookConfigurationPolicyNameAnnotationKey)
mutatingWebhookConfiguration.Annotations[constants.WebhookConfigurationPolicyNamespaceAnnotationKey] = newName("namespace")
mutatingWebhookConfiguration.Webhooks[0].ClientConfig.Service.Name = newName("service")
Expect(
k8sClient.Update(ctx, mutatingWebhookConfiguration),
).To(Succeed())

By("reconciling the MutatingWebhookConfiguration to its original state")
Eventually(func(g Gomega) (*admissionregistrationv1.MutatingWebhookConfiguration, error) {
return getTestMutatingWebhookConfiguration(fmt.Sprintf("namespaced-%s-%s", policyNamespace, policyName))
}, timeout, pollInterval).Should(
And(
HaveField("Labels", Equal(originalMutatingWebhookConfiguration.Labels)),
HaveField("Annotations", Equal(originalMutatingWebhookConfiguration.Annotations)),
HaveField("Webhooks", Equal(originalMutatingWebhookConfiguration.Webhooks)),
),
)
BeforeEach(func() {
Expect(
k8sClient.Create(ctx, admissionPolicyWithPolicyServerName(policyName, policyServerName)),
).To(HaveSucceededOrAlreadyExisted())
})
It(fmt.Sprintf("should set its policy status to %q", policiesv1.PolicyStatusScheduled), func() {
Eventually(func(g Gomega) (*policiesv1.AdmissionPolicy, error) {
return getFreshAdmissionPolicy(policyNamespace, policyName)
}).Should(
WithTransform(
func(admissionPolicy *policiesv1.AdmissionPolicy) policiesv1.PolicyStatusEnum {
return admissionPolicy.Status.PolicyStatus
},
Equal(policiesv1.PolicyStatusScheduled),
),
)
})
Context("and the targeted policy server is created", func() {
BeforeEach(func() {
Expect(
k8sClient.Create(ctx, policyServer(policyServerName)),
).To(HaveSucceededOrAlreadyExisted())
})
It(fmt.Sprintf("should set its policy status to %q", policiesv1.PolicyStatusPending), func() {
Eventually(func(g Gomega) (*policiesv1.AdmissionPolicy, error) {
return getFreshAdmissionPolicy(policyNamespace, policyName)
}, 30*time.Second, 250*time.Millisecond).Should(
WithTransform(
func(admissionPolicy *policiesv1.AdmissionPolicy) policiesv1.PolicyStatusEnum {
return admissionPolicy.Status.PolicyStatus
},
Equal(policiesv1.PolicyStatusPending),
),
)
})
})
})
})
})

When("creating an AdmissionPolicy without a PolicyServer assigned", func() {
policyName := newName("unscheduled-policy")

It("should set the policy status to unscheduled", func() {
Expect(
k8sClient.Create(ctx, admissionPolicyFactory(policyName, policyNamespace, "", false)),
).To(haveSucceededOrAlreadyExisted())

Eventually(func(g Gomega) (*policiesv1.AdmissionPolicy, error) {
return getTestAdmissionPolicy(policyNamespace, policyName)
}, 30*time.Second, 250*time.Millisecond).Should(
HaveField("Status.PolicyStatus", Equal(policiesv1.PolicyStatusUnscheduled)),
)
})
})

When("creating an AdmissionPolicy with a PolicyServer assigned but not running yet", func() {
var (
policyName = newName("scheduled-policy")
policyServerName = newName("policy-server")
)

It("should set the policy status to scheduled", func() {
Expect(
k8sClient.Create(ctx, admissionPolicyFactory(policyName, policyNamespace, policyServerName, false)),
).To(haveSucceededOrAlreadyExisted())

Eventually(func(g Gomega) (*policiesv1.AdmissionPolicy, error) {
return getTestAdmissionPolicy(policyNamespace, policyName)
}, timeout, pollInterval).Should(
HaveField("Status.PolicyStatus", Equal(policiesv1.PolicyStatusScheduled)),
)
})

It("should set the policy status to active when the PolicyServer is created", func() {
By("creating the PolicyServer")
Expect(
k8sClient.Create(ctx, policyServerFactory(policyServerName)),
).To(haveSucceededOrAlreadyExisted())

By("changing the policy status to pending")
Eventually(func(g Gomega) (*policiesv1.AdmissionPolicy, error) {
return getTestAdmissionPolicy(policyNamespace, policyName)
}, timeout, pollInterval).Should(
HaveField("Status.PolicyStatus", Equal(policiesv1.PolicyStatusPending)),
)

By("changing the policy status to active")
Eventually(func(g Gomega) (*policiesv1.AdmissionPolicy, error) {
return getTestAdmissionPolicy(policyNamespace, policyName)
}, timeout, pollInterval).Should(
HaveField("Status.PolicyStatus", Equal(policiesv1.PolicyStatusActive)),
)
})
})
})
Loading

0 comments on commit b48d736

Please sign in to comment.