diff --git a/webhooks/ssp_webhook_test.go b/webhooks/ssp_webhook_test.go index 84dfe3374..5481b90b0 100644 --- a/webhooks/ssp_webhook_test.go +++ b/webhooks/ssp_webhook_test.go @@ -23,112 +23,81 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + apps "k8s.io/api/apps/v1" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" + "k8s.io/utils/ptr" + "kubevirt.io/controller-lifecycle-operator-sdk/api" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/client/interceptor" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" sspv1beta2 "kubevirt.io/ssp-operator/api/v1beta2" "kubevirt.io/ssp-operator/internal" + "kubevirt.io/ssp-operator/internal/common" ) var _ = Describe("SSP Validation", func() { + var ( - client client.Client - objects = make([]runtime.Object, 0) + apiClient client.Client + createIntercept func(ctx context.Context, client client.WithWatch, obj client.Object, opts ...client.CreateOption) error validator admission.CustomValidator ctx context.Context ) - JustBeforeEach(func() { - scheme := runtime.NewScheme() - // add our own scheme - Expect(sspv1beta2.SchemeBuilder.AddToScheme(scheme)).To(Succeed()) - // add more schemes - Expect(v1.AddToScheme(scheme)).To(Succeed()) + BeforeEach(func() { + createIntercept = func(ctx context.Context, client client.WithWatch, obj client.Object, opts ...client.CreateOption) error { + return client.Create(ctx, obj, opts...) + } - client = fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(objects...).Build() + apiClient = fake.NewClientBuilder(). + WithScheme(common.Scheme). + WithInterceptorFuncs(interceptor.Funcs{ + Create: func(ctx context.Context, client client.WithWatch, obj client.Object, opts ...client.CreateOption) error { + return createIntercept(ctx, client, obj, opts...) + }, + }). + Build() - validator = newSspValidator(client) - ctx = context.Background() + validator = newSspValidator(apiClient) }) Context("creating SSP CR", func() { - const ( - templatesNamespace = "test-templates-ns" - ) - BeforeEach(func() { - objects = append(objects, &v1.Namespace{ + err := apiClient.Create(ctx, &sspv1beta2.SSP{ ObjectMeta: metav1.ObjectMeta{ - Name: templatesNamespace, - ResourceVersion: "1", + Name: "test-ssp", + Namespace: "test-ns", }, + Spec: sspv1beta2.SSPSpec{}, }) + Expect(err).ToNot(HaveOccurred()) }) - AfterEach(func() { - objects = make([]runtime.Object, 0) - }) - - Context("when one is already present", func() { - BeforeEach(func() { - // add an SSP CR to fake client - objects = append(objects, &sspv1beta2.SSP{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-ssp", - Namespace: "test-ns", - ResourceVersion: "1", - }, - Spec: sspv1beta2.SSPSpec{ - CommonTemplates: sspv1beta2.CommonTemplates{ - Namespace: templatesNamespace, - }, - }, - }) - }) - - It("should be rejected", func() { - ssp := &sspv1beta2.SSP{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-ssp2", - Namespace: "test-ns2", - }, - Spec: sspv1beta2.SSPSpec{ - CommonTemplates: sspv1beta2.CommonTemplates{ - Namespace: templatesNamespace, - }, - }, - } + It("should reject SSP when one is already present", func() { + ssp := &sspv1beta2.SSP{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-ssp2", + Namespace: "test-ns2", + }, + Spec: sspv1beta2.SSPSpec{}, + } - _, err := validator.ValidateCreate(ctx, ssp) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("creation failed, an SSP CR already exists in namespace test-ns: test-ssp")) - }) + _, err := validator.ValidateCreate(ctx, ssp) + Expect(err).To(MatchError(ContainSubstring("creation failed, an SSP CR already exists in namespace test-ns: test-ssp"))) }) }) Context("DataImportCronTemplates", func() { - const ( - templatesNamespace = "test-templates-ns" - ) - var ( oldSSP *sspv1beta2.SSP newSSP *sspv1beta2.SSP ) BeforeEach(func() { - objects = append(objects, &v1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: templatesNamespace, - ResourceVersion: "1", - }, - }) - oldSSP = &sspv1beta2.SSP{ ObjectMeta: metav1.ObjectMeta{ Name: "test-ssp", @@ -136,7 +105,7 @@ var _ = Describe("SSP Validation", func() { }, Spec: sspv1beta2.SSPSpec{ CommonTemplates: sspv1beta2.CommonTemplates{ - Namespace: templatesNamespace, + Namespace: "test-templates-ns", DataImportCronTemplates: []sspv1beta2.DataImportCronTemplate{ { ObjectMeta: metav1.ObjectMeta{ @@ -151,13 +120,9 @@ var _ = Describe("SSP Validation", func() { newSSP = oldSSP.DeepCopy() }) - AfterEach(func() { - objects = make([]runtime.Object, 0) - }) - It("should validate dataImportCronTemplates on create", func() { _, err := validator.ValidateCreate(ctx, newSSP) - Expect(err).To(HaveOccurred()) + Expect(err).To(MatchError(ContainSubstring("missing name in DataImportCronTemplate"))) newSSP.Spec.CommonTemplates.DataImportCronTemplates[0].Name = "test-name" @@ -167,7 +132,7 @@ var _ = Describe("SSP Validation", func() { It("should validate dataImportCronTemplates on update", func() { _, err := validator.ValidateUpdate(ctx, oldSSP, newSSP) - Expect(err).To(HaveOccurred()) + Expect(err).To(MatchError(ContainSubstring("missing name in DataImportCronTemplate"))) newSSP.Spec.CommonTemplates.DataImportCronTemplates[0].Name = "test-name" @@ -176,9 +141,97 @@ var _ = Describe("SSP Validation", func() { }) }) + Context("validate placement", func() { + It("should not call create API, if placement is nil", func() { + createIntercept = func(_ context.Context, _ client.WithWatch, _ client.Object, _ ...client.CreateOption) error { + Fail("Called create API") + return nil + } + + ssp := &sspv1beta2.SSP{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-ssp", + Namespace: "test-ns", + }, + Spec: sspv1beta2.SSPSpec{ + TemplateValidator: &sspv1beta2.TemplateValidator{ + Replicas: ptr.To(int32(2)), + Placement: nil, + }, + }, + } + + _, err := validator.ValidateCreate(ctx, ssp) + Expect(err).ToNot(HaveOccurred()) + }) + + It("should call create API dry run", func() { + placement := &api.NodePlacement{ + NodeSelector: map[string]string{ + "test-label": "test-value", + }, + Affinity: &v1.Affinity{ + PodAffinity: &v1.PodAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: []v1.PodAffinityTerm{{ + LabelSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "foo": "bar", + }, + }, + }}, + }, + }, + Tolerations: []v1.Toleration{{ + Key: "key", + Value: "value", + }}, + } + + var createWasCalled bool + createIntercept = func(ctx context.Context, cli client.WithWatch, obj client.Object, opts ...client.CreateOption) error { + deployment, ok := obj.(*apps.Deployment) + if !ok { + Fail("Expected created object to be Deployment.") + } + + createOptions := &client.CreateOptions{} + for _, opt := range opts { + opt.ApplyToCreate(createOptions) + } + + if len(createOptions.DryRun) != 1 || createOptions.DryRun[0] != metav1.DryRunAll { + Fail("Create call should be dry run.") + } + + Expect(deployment.Spec.Template.Spec.NodeSelector).To(Equal(placement.NodeSelector)) + Expect(deployment.Spec.Template.Spec.Affinity).To(Equal(placement.Affinity)) + Expect(deployment.Spec.Template.Spec.Tolerations).To(Equal(placement.Tolerations)) + createWasCalled = true + return nil + } + + ssp := &sspv1beta2.SSP{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-ssp", + Namespace: "test-ns", + }, + Spec: sspv1beta2.SSPSpec{ + TemplateValidator: &sspv1beta2.TemplateValidator{ + Replicas: ptr.To(int32(2)), + Placement: placement, + }, + }, + } + + _, err := validator.ValidateCreate(ctx, ssp) + Expect(err).ToNot(HaveOccurred()) + + Expect(createWasCalled).To(BeTrue()) + }) + }) }) func TestWebhook(t *testing.T) { RegisterFailHandler(Fail) - RunSpecs(t, "API Suite") + RunSpecs(t, "Webhook Suite") }