Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: reconcile policy webhook configurations #576

Merged
merged 7 commits into from
Nov 21, 2023
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
flavio marked this conversation as resolved.
Show resolved Hide resolved
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
Loading