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

[release-v0.18] fix: Remove namespaceSelector from webhook definition #979

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions controllers/controllers_suite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package controllers

import (
"testing"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

func TestControllers(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Controllers Suite")
}
4 changes: 4 additions & 0 deletions controllers/reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ package controllers

import (
"context"

ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)

type ControllerReconciler interface {
reconcile.Reconciler

Start(ctx context.Context, mgr ctrl.Manager) error
Name() string
}
5 changes: 5 additions & 0 deletions controllers/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,11 @@ func setupManager(ctx context.Context, cancel context.CancelFunc, mgr controller
return fmt.Errorf("error adding service controller: %w", err)
}

webhookConfigController := NewWebhookConfigurationController(mgr.GetClient())
if err = mgr.Add(getRunnable(mgr, webhookConfigController)); err != nil {
return fmt.Errorf("error adding webhook configuration controller: %w", err)
}

if crdWatch.CrdExists(vmCRD) {
vmController, cErr := CreateVmController(mgr)
if cErr != nil {
Expand Down
118 changes: 118 additions & 0 deletions controllers/webhook_controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package controllers

import (
"context"

admissionv1 "k8s.io/api/admissionregistration/v1"
"k8s.io/apimachinery/pkg/api/errors"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"

sspv1beta2 "kubevirt.io/ssp-operator/api/v1beta2"
)

const (
OlmNameLabel = "olm.webhook-description-generate-name"
OlmNameLabelValue = "validation.ssp.kubevirt.io"
)

// +kubebuilder:rbac:groups=admissionregistration.k8s.io,resources=validatingwebhookconfigurations,verbs=get;update

// CreateWebhookConfigurationController creates a controller
// that watches ValidatingWebhookConfiguration created by OLM,
// and removes any namespaceSelector defined in it.
//
// The OLM limits the webhook scope to the namespaces that are defined in the OperatorGroup
// by setting namespaceSelector in the ValidatingWebhookConfiguration.
// We would like our webhook to intercept requests from all namespaces.
//
// The SSP operator already watches all ValidatingWebhookConfigurations, because
// of template validator operand, so this controller is not a performance issue.
func NewWebhookConfigurationController(apiClient client.Client) ControllerReconciler {
return &webhookCtrl{
apiClient: apiClient,
}
}

type webhookCtrl struct {
apiClient client.Client
}

var _ ControllerReconciler = &webhookCtrl{}

var _ reconcile.Reconciler = &webhookCtrl{}

func (w *webhookCtrl) Start(_ context.Context, mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
Named(w.Name()).
For(&admissionv1.ValidatingWebhookConfiguration{}, builder.WithPredicates(
predicate.NewPredicateFuncs(hasExpectedLabel),
)).
Complete(w)
}

func (w *webhookCtrl) Name() string {
return "validating-webhook-controller"
}

func (w *webhookCtrl) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) {
webhookConfig := &admissionv1.ValidatingWebhookConfiguration{}
if err := w.apiClient.Get(ctx, request.NamespacedName, webhookConfig); err != nil {
if errors.IsNotFound(err) {
return reconcile.Result{}, nil
}
return reconcile.Result{}, err
}

if !hasExpectedLabel(webhookConfig) {
return reconcile.Result{}, nil
}

if changed := updateWebhookConfiguration(webhookConfig); !changed {
return reconcile.Result{}, nil
}

err := w.apiClient.Update(ctx, webhookConfig)
return reconcile.Result{}, err
}

func hasExpectedLabel(obj client.Object) bool {
return obj.GetLabels()[OlmNameLabel] == OlmNameLabelValue
}

func updateWebhookConfiguration(webhookConfig *admissionv1.ValidatingWebhookConfiguration) bool {
var changed bool
for i := range webhookConfig.Webhooks {
webhook := &webhookConfig.Webhooks[i]
if webhook.NamespaceSelector == nil {
continue
}
// Check if the webhook reacts to SSP resource.
var hasSspRule bool
for _, rule := range webhook.Rules {
if containsString(rule.APIGroups, sspv1beta2.GroupVersion.Group) {
hasSspRule = true
break
}
}
if !hasSspRule {
continue
}

webhook.NamespaceSelector = nil
changed = true
}
return changed
}

func containsString(slice []string, value string) bool {
for _, s := range slice {
if s == value {
return true
}
}
return false
}
125 changes: 125 additions & 0 deletions controllers/wehook_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package controllers

import (
"context"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"

admissionv1 "k8s.io/api/admissionregistration/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/utils/pointer"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"sigs.k8s.io/controller-runtime/pkg/reconcile"

sspv1beta2 "kubevirt.io/ssp-operator/api/v1beta2"
"kubevirt.io/ssp-operator/internal/common"
)

var _ = Describe("Webhook controller", func() {

var (
webhookConfig *admissionv1.ValidatingWebhookConfiguration
fakeClient client.Client
testController ControllerReconciler
testRequest reconcile.Request
)

BeforeEach(func() {
sideEffectsNone := admissionv1.SideEffectClassNone
webhookConfig = &admissionv1.ValidatingWebhookConfiguration{
TypeMeta: metav1.TypeMeta{
APIVersion: admissionv1.SchemeGroupVersion.String(),
Kind: "ValidatingWebhookConfiguration",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test-webhook",
Labels: map[string]string{
OlmNameLabel: OlmNameLabelValue,
},
},
Webhooks: []admissionv1.ValidatingWebhook{{
Name: "test-ssp-webhook",
ClientConfig: admissionv1.WebhookClientConfig{
Service: &admissionv1.ServiceReference{
Namespace: "test-namespace",
Name: "test-name",
Path: pointer.String("/webhook"),
},
},
Rules: []admissionv1.RuleWithOperations{{
Rule: admissionv1.Rule{
APIGroups: []string{sspv1beta2.GroupVersion.Group},
APIVersions: []string{sspv1beta2.GroupVersion.Version},
Resources: []string{"ssps"},
},
Operations: []admissionv1.OperationType{
admissionv1.Create, admissionv1.Update,
},
}},
NamespaceSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"test-namespace-label": "some-value",
},
},
SideEffects: &sideEffectsNone,
AdmissionReviewVersions: []string{"v1"},
}},
}

fakeClient = fake.NewClientBuilder().WithScheme(common.Scheme).Build()

testController = NewWebhookConfigurationController(fakeClient)

testRequest = reconcile.Request{
NamespacedName: types.NamespacedName{
Name: webhookConfig.Name,
},
}
})

It("should remove namespaceSelector from webhook", func() {
Expect(fakeClient.Create(context.Background(), webhookConfig)).To(Succeed())

_, err := testController.Reconcile(context.Background(), testRequest)
Expect(err).ToNot(HaveOccurred())

updatedConfig := &admissionv1.ValidatingWebhookConfiguration{}
Expect(fakeClient.Get(context.Background(), client.ObjectKeyFromObject(webhookConfig), updatedConfig)).To(Succeed())

Expect(updatedConfig.Webhooks).ToNot(BeEmpty())
for _, webhook := range updatedConfig.Webhooks {
Expect(webhook.NamespaceSelector).To(BeNil())
}
})

It("should not remove namespaceSelector from non SSP webhook", func() {
webhookConfig.Webhooks[0].Rules[0].APIGroups = []string{"non-ssp-api-group"}

Expect(fakeClient.Create(context.Background(), webhookConfig)).To(Succeed())

_, err := testController.Reconcile(context.Background(), testRequest)
Expect(err).ToNot(HaveOccurred())

updatedConfig := &admissionv1.ValidatingWebhookConfiguration{}
Expect(fakeClient.Get(context.Background(), client.ObjectKeyFromObject(webhookConfig), updatedConfig)).To(Succeed())

Expect(updatedConfig).To(Equal(webhookConfig))
})

It("should not remove namespaceSelector from webhook without labels", func() {
webhookConfig.Labels = nil

Expect(fakeClient.Create(context.Background(), webhookConfig)).To(Succeed())

_, err := testController.Reconcile(context.Background(), testRequest)
Expect(err).ToNot(HaveOccurred())

updatedConfig := &admissionv1.ValidatingWebhookConfiguration{}
Expect(fakeClient.Get(context.Background(), client.ObjectKeyFromObject(webhookConfig), updatedConfig)).To(Succeed())

Expect(updatedConfig).To(Equal(webhookConfig))
})
})
95 changes: 95 additions & 0 deletions tests/webhook_controller_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package tests

import (
"time"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"

admissionv1 "k8s.io/api/admissionregistration/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"

sspv1beta2 "kubevirt.io/ssp-operator/api/v1beta2"
"kubevirt.io/ssp-operator/controllers"
"kubevirt.io/ssp-operator/tests/env"
)

var _ = Describe("Webhook controller", func() {
var webhook *admissionv1.ValidatingWebhookConfiguration

BeforeEach(func() {
sideEffectsNone := admissionv1.SideEffectClassNone
webhook = &admissionv1.ValidatingWebhookConfiguration{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "ssp.kubevirt.io-test-webhook-",
Labels: map[string]string{
controllers.OlmNameLabel: controllers.OlmNameLabelValue,
},
},
Webhooks: []admissionv1.ValidatingWebhook{{
Name: "test.webhook.ssp.kubevirt.io",
ClientConfig: admissionv1.WebhookClientConfig{
Service: &admissionv1.ServiceReference{
Name: "non-existing-service",
Namespace: "non-existing-namespace",
},
},
Rules: []admissionv1.RuleWithOperations{{
// Using "Delete" so it does not conflict with existing SSP webhook
Operations: []admissionv1.OperationType{admissionv1.Delete},
Rule: admissionv1.Rule{
APIGroups: []string{sspv1beta2.GroupVersion.Group},
APIVersions: []string{sspv1beta2.GroupVersion.Version},
Resources: []string{"ssps"},
},
}},
NamespaceSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"ssp-webhook-test-label": "ssp-webhook-test-label-vale",
},
},
SideEffects: &sideEffectsNone,
AdmissionReviewVersions: []string{"v1"},
}},
}
})

It("[test_id:TODO] should delete namespaceSelector from webhook configuration with OLM label", func() {
Expect(apiClient.Create(ctx, webhook)).To(Succeed())
DeferCleanup(func() {
Expect(apiClient.Delete(ctx, webhook)).To(Succeed())
})

Eventually(func(g Gomega) {
updatedWebhook := &admissionv1.ValidatingWebhookConfiguration{}
g.Expect(apiClient.Get(ctx, client.ObjectKeyFromObject(webhook), updatedWebhook)).To(Succeed())
g.Expect(updatedWebhook.Webhooks).To(HaveLen(1))

namespaceSelector := updatedWebhook.Webhooks[0].NamespaceSelector
if namespaceSelector != nil {
g.Expect(namespaceSelector.MatchLabels).To(BeEmpty())
g.Expect(namespaceSelector.MatchExpressions).To(BeEmpty())
}
}, env.ShortTimeout(), time.Second).Should(Succeed())
})

It("[test_id:TODO] should not delete namespaceSelector from webhook configuration without OLM label", func() {
webhook.Labels = nil

Expect(apiClient.Create(ctx, webhook)).To(Succeed())
DeferCleanup(func() {
Expect(apiClient.Delete(ctx, webhook)).To(Succeed())
})

Consistently(func(g Gomega) {
updatedWebhook := &admissionv1.ValidatingWebhookConfiguration{}
g.Expect(apiClient.Get(ctx, client.ObjectKeyFromObject(webhook), updatedWebhook)).To(Succeed())
g.Expect(updatedWebhook.Webhooks).To(HaveLen(1))

namespaceSelector := updatedWebhook.Webhooks[0].NamespaceSelector
g.Expect(namespaceSelector).ToNot(BeNil())
g.Expect(namespaceSelector.MatchLabels).ToNot(BeEmpty())
}, 10*time.Second, time.Second).Should(Succeed())
})
})
Loading