From 44e5baf48b5cccd8261505c08d7b0b66b872d8ba Mon Sep 17 00:00:00 2001 From: Andrej Krejcir Date: Mon, 19 Jun 2023 17:51:44 +0200 Subject: [PATCH] fix: Keep API v1beta1 in scheme Keeping the v1beta1 version in scheme allows the operator to decode old objects. Changed webhook to accept both versions. Signed-off-by: Andrej Krejcir --- PROJECT | 14 + .../ssp-cr-template.yaml | 4 +- internal/common/scheme.go | 6 +- tests/tests_suite_test.go | 52 +-- tests/webhook_test.go | 37 ++- .../api/v1beta1/groupversion_info.go | 36 +++ .../ssp-operator/api/v1beta1/ssp_types.go | 171 ++++++++++ .../api/v1beta1/zz_generated.deepcopy.go | 300 ++++++++++++++++++ vendor/modules.txt | 1 + webhooks/ssp_webhook.go | 64 +++- webhooks/ssp_webhook_test.go | 126 +++++--- 11 files changed, 727 insertions(+), 84 deletions(-) create mode 100644 vendor/kubevirt.io/ssp-operator/api/v1beta1/groupversion_info.go create mode 100644 vendor/kubevirt.io/ssp-operator/api/v1beta1/ssp_types.go create mode 100644 vendor/kubevirt.io/ssp-operator/api/v1beta1/zz_generated.deepcopy.go diff --git a/PROJECT b/PROJECT index 2c41f4bfa..91986d824 100644 --- a/PROJECT +++ b/PROJECT @@ -17,6 +17,20 @@ resources: # defaulting: true validation: true webhookVersion: v1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: kubevirt.io + group: ssp + kind: SSP + path: kubevirt.io/ssp-operator/api/v1beta1 + version: v1beta1 + webhooks: + # conversion: true + # defaulting: true + validation: true + webhookVersion: v1 version: "3" plugins: manifests.sdk.operatorframework.io/v2: {} diff --git a/automation/e2e-upgrade-functests/ssp-cr-template.yaml b/automation/e2e-upgrade-functests/ssp-cr-template.yaml index bd3d51ebc..1a305d43a 100644 --- a/automation/e2e-upgrade-functests/ssp-cr-template.yaml +++ b/automation/e2e-upgrade-functests/ssp-cr-template.yaml @@ -1,6 +1,6 @@ # This custom resource is used by upgrade tests -apiVersion: ssp.kubevirt.io/v1beta1 +apiVersion: ssp.kubevirt.io/v1beta2 kind: SSP metadata: name: %%_SSP_NAME_%% @@ -15,5 +15,5 @@ spec: replicas: 2 nodeLabeller: placement: - nodeSelector: + nodeSelector: "test": "test" diff --git a/internal/common/scheme.go b/internal/common/scheme.go index 77ca2ff50..cc1b9a839 100644 --- a/internal/common/scheme.go +++ b/internal/common/scheme.go @@ -11,7 +11,8 @@ import ( utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" instancetypev1alpha2 "kubevirt.io/api/instancetype/v1alpha2" - ssp "kubevirt.io/ssp-operator/api/v1beta2" + sspv1beta1 "kubevirt.io/ssp-operator/api/v1beta1" + sspv1beta2 "kubevirt.io/ssp-operator/api/v1beta2" ) var ( @@ -23,7 +24,8 @@ func init() { utilruntime.Must(clientgoscheme.AddToScheme(Scheme)) utilruntime.Must(extv1.AddToScheme(Scheme)) utilruntime.Must(internalmeta.AddToScheme(Scheme)) - utilruntime.Must(ssp.AddToScheme(Scheme)) + utilruntime.Must(sspv1beta1.AddToScheme(Scheme)) + utilruntime.Must(sspv1beta2.AddToScheme(Scheme)) utilruntime.Must(osconfv1.Install(Scheme)) utilruntime.Must(instancetypev1alpha2.AddToScheme(Scheme)) } diff --git a/tests/tests_suite_test.go b/tests/tests_suite_test.go index 01d08b2ee..7cad1e892 100644 --- a/tests/tests_suite_test.go +++ b/tests/tests_suite_test.go @@ -41,7 +41,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client/config" pipeline "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" - ssp "kubevirt.io/ssp-operator/api/v1beta2" + sspv1beta1 "kubevirt.io/ssp-operator/api/v1beta1" + sspv1beta2 "kubevirt.io/ssp-operator/api/v1beta2" "kubevirt.io/ssp-operator/internal/common" vm_console_proxy "kubevirt.io/ssp-operator/internal/operands/vm-console-proxy" ) @@ -78,7 +79,7 @@ type TestSuiteStrategy interface { } type newSspStrategy struct { - ssp *ssp.SSP + ssp *sspv1beta2.SSP } var _ TestSuiteStrategy = &newSspStrategy{} @@ -102,7 +103,7 @@ func (s *newSspStrategy) Init() { return apiClient.Create(ctx, namespaceObj) }, env.Timeout(), time.Second).ShouldNot(HaveOccurred()) - newSsp := &ssp.SSP{ + newSsp := &sspv1beta2.SSP{ ObjectMeta: metav1.ObjectMeta{ Name: s.GetName(), Namespace: s.GetNamespace(), @@ -118,20 +119,20 @@ func (s *newSspStrategy) Init() { vm_console_proxy.VmConsoleProxyNamespaceAnnotation: s.GetVmConsoleProxyNamespace(), }, }, - Spec: ssp.SSPSpec{ - TemplateValidator: &ssp.TemplateValidator{ + Spec: sspv1beta2.SSPSpec{ + TemplateValidator: &sspv1beta2.TemplateValidator{ Replicas: pointer.Int32(int32(s.GetValidatorReplicas())), }, - CommonTemplates: ssp.CommonTemplates{ + CommonTemplates: sspv1beta2.CommonTemplates{ Namespace: s.GetTemplatesNamespace(), }, - TektonPipelines: &ssp.TektonPipelines{ + TektonPipelines: &sspv1beta2.TektonPipelines{ Namespace: s.GetNamespace(), }, - TektonTasks: &ssp.TektonTasks{ + TektonTasks: &sspv1beta2.TektonTasks{ Namespace: s.GetNamespace(), }, - FeatureGates: &ssp.FeatureGates{ + FeatureGates: &sspv1beta2.FeatureGates{ DeployTektonTaskResources: false, }, }, @@ -154,7 +155,7 @@ func (s *newSspStrategy) Cleanup() { waitForDeletion(client.ObjectKey{ Name: s.GetName(), Namespace: s.GetNamespace(), - }, &ssp.SSP{}) + }, &sspv1beta2.SSP{}) } err1 := apiClient.Delete(ctx, &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: s.GetNamespace()}}) @@ -245,13 +246,13 @@ type existingSspStrategy struct { Name string Namespace string - ssp *ssp.SSP + ssp *sspv1beta2.SSP } var _ TestSuiteStrategy = &existingSspStrategy{} func (s *existingSspStrategy) Init() { - existingSsp := &ssp.SSP{} + existingSsp := &sspv1beta2.SSP{} err := apiClient.Get(ctx, client.ObjectKey{Name: s.Name, Namespace: s.Namespace}, existingSsp) Expect(err).ToNot(HaveOccurred()) @@ -273,11 +274,11 @@ func (s *existingSspStrategy) Init() { if existingSsp.Spec.TemplateValidator != nil && existingSsp.Spec.TemplateValidator.Replicas != nil { newReplicasCount = *existingSsp.Spec.TemplateValidator.Replicas + int32(1) } - updateSsp(func(foundSsp *ssp.SSP) { + updateSsp(func(foundSsp *sspv1beta2.SSP) { if existingSsp.Spec.TemplateValidator != nil { foundSsp.Spec.TemplateValidator.Replicas = &newReplicasCount } else { - foundSsp.Spec.TemplateValidator = &ssp.TemplateValidator{ + foundSsp.Spec.TemplateValidator = &sspv1beta2.TemplateValidator{ Replicas: &newReplicasCount, } } @@ -449,7 +450,8 @@ func expectSuccessOrNotFound(err error) { } func setupApiClient() { - Expect(ssp.AddToScheme(testScheme)).ToNot(HaveOccurred()) + Expect(sspv1beta1.AddToScheme(testScheme)).ToNot(HaveOccurred()) + Expect(sspv1beta2.AddToScheme(testScheme)).ToNot(HaveOccurred()) Expect(promv1.AddToScheme(testScheme)).ToNot(HaveOccurred()) Expect(templatev1.Install(testScheme)).ToNot(HaveOccurred()) Expect(secv1.Install(testScheme)).ToNot(HaveOccurred()) @@ -476,7 +478,7 @@ func setupApiClient() { } func createSspListerWatcher(cfg *rest.Config) cache.ListerWatcher { - sspGvk, err := apiutil.GVKForObject(&ssp.SSP{}, testScheme) + sspGvk, err := apiutil.GVKForObject(&sspv1beta2.SSP{}, testScheme) Expect(err).ToNot(HaveOccurred()) restClient, err := apiutil.RESTClientForGVK(sspGvk, false, cfg, serializer.NewCodecFactory(testScheme)) @@ -485,9 +487,9 @@ func createSspListerWatcher(cfg *rest.Config) cache.ListerWatcher { return cache.NewListWatchFromClient(restClient, "ssps", strategy.GetNamespace(), fields.Everything()) } -func getSsp() *ssp.SSP { +func getSsp() *sspv1beta2.SSP { key := client.ObjectKey{Name: strategy.GetName(), Namespace: strategy.GetNamespace()} - foundSsp := &ssp.SSP{} + foundSsp := &sspv1beta2.SSP{} Expect(apiClient.Get(ctx, key, foundSsp)).ToNot(HaveOccurred()) return foundSsp } @@ -522,10 +524,10 @@ func waitForDeletion(key client.ObjectKey, obj client.Object) { }, env.Timeout(), time.Second).Should(BeTrue()) } -func waitForSspDeletionIfNeeded(sspObj *ssp.SSP) { +func waitForSspDeletionIfNeeded(sspObj *sspv1beta2.SSP) { key := client.ObjectKey{Name: sspObj.Name, Namespace: sspObj.Namespace} Eventually(func() error { - foundSsp := &ssp.SSP{} + foundSsp := &sspv1beta2.SSP{} err := apiClient.Get(ctx, key, foundSsp) if errors.IsNotFound(err) { return nil @@ -546,13 +548,13 @@ func validateDeploymentExists() { env.SspDeploymentName(), env.SspDeploymentNamespace())) } -func createOrUpdateSsp(sspObj *ssp.SSP) { +func createOrUpdateSsp(sspObj *sspv1beta2.SSP) { key := client.ObjectKey{ Name: sspObj.Name, Namespace: sspObj.Namespace, } Eventually(func() error { - foundSsp := &ssp.SSP{} + foundSsp := &sspv1beta2.SSP{} err := apiClient.Get(ctx, key, foundSsp) if err == nil { isEqual := reflect.DeepEqual(foundSsp.Spec, sspObj.Spec) && @@ -567,7 +569,7 @@ func createOrUpdateSsp(sspObj *ssp.SSP) { return apiClient.Update(ctx, foundSsp) } if errors.IsNotFound(err) { - newSsp := &ssp.SSP{ + newSsp := &sspv1beta2.SSP{ ObjectMeta: metav1.ObjectMeta{ Name: sspObj.Name, Namespace: sspObj.Namespace, @@ -583,7 +585,7 @@ func createOrUpdateSsp(sspObj *ssp.SSP) { } func triggerReconciliation() { - updateSsp(func(foundSsp *ssp.SSP) { + updateSsp(func(foundSsp *sspv1beta2.SSP) { if foundSsp.GetAnnotations() == nil { foundSsp.SetAnnotations(map[string]string{}) } @@ -591,7 +593,7 @@ func triggerReconciliation() { foundSsp.GetAnnotations()["forceReconciliation"] = "" }) - updateSsp(func(foundSsp *ssp.SSP) { + updateSsp(func(foundSsp *sspv1beta2.SSP) { delete(foundSsp.GetAnnotations(), "forceReconciliation") }) diff --git a/tests/webhook_test.go b/tests/webhook_test.go index e5e2ff1f3..33458a288 100644 --- a/tests/webhook_test.go +++ b/tests/webhook_test.go @@ -10,10 +10,11 @@ import ( "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + sspv1beta1 "kubevirt.io/ssp-operator/api/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client" "kubevirt.io/controller-lifecycle-operator-sdk/api" - ssp "kubevirt.io/ssp-operator/api/v1beta2" + sspv1beta2 "kubevirt.io/ssp-operator/api/v1beta2" ) // Placement API tests variables @@ -91,7 +92,7 @@ var _ = Describe("Validation webhook", func() { Context("removed existing SSP CR", func() { var ( - newSsp *ssp.SSP + newSsp *sspv1beta2.SSP ) BeforeEach(func() { @@ -99,7 +100,7 @@ var _ = Describe("Validation webhook", func() { foundSsp := getSsp() Expect(apiClient.Delete(ctx, foundSsp)).ToNot(HaveOccurred()) - waitForDeletion(client.ObjectKey{Name: foundSsp.GetName(), Namespace: foundSsp.GetNamespace()}, &ssp.SSP{}) + waitForDeletion(client.ObjectKey{Name: foundSsp.GetName(), Namespace: foundSsp.GetNamespace()}, &sspv1beta2.SSP{}) foundSsp.ObjectMeta = v1.ObjectMeta{ Name: foundSsp.GetName(), @@ -129,7 +130,7 @@ var _ = Describe("Validation webhook", func() { Context("Placement API validation", func() { It("[test_id:5988]should succeed with valid template-validator placement fields", func() { - newSsp.Spec.TemplateValidator = &ssp.TemplateValidator{ + newSsp.Spec.TemplateValidator = &sspv1beta2.TemplateValidator{ Placement: &placementAPIValidationValidPlacement, } @@ -138,7 +139,7 @@ var _ = Describe("Validation webhook", func() { }) It("[test_id:5987]should fail with invalid template-validator placement fields", func() { - newSsp.Spec.TemplateValidator = &ssp.TemplateValidator{ + newSsp.Spec.TemplateValidator = &sspv1beta2.TemplateValidator{ Placement: &placementAPIValidationInvalidPlacement, } @@ -148,18 +149,34 @@ var _ = Describe("Validation webhook", func() { }) It("[test_id:TODO] should fail when DataImportCronTemplate does not have a name", func() { - newSsp.Spec.CommonTemplates.DataImportCronTemplates = []ssp.DataImportCronTemplate{{ + newSsp.Spec.CommonTemplates.DataImportCronTemplates = []sspv1beta2.DataImportCronTemplate{{ ObjectMeta: metav1.ObjectMeta{Name: ""}, }} err := apiClient.Create(ctx, newSsp, client.DryRunAll) Expect(err).To(MatchError(ContainSubstring("missing name in DataImportCronTemplate"))) }) + + It("[test_id:TODO] should accept v1beta1 SSP object", func() { + ssp := &sspv1beta1.SSP{ + ObjectMeta: metav1.ObjectMeta{ + Name: newSsp.GetName(), + Namespace: newSsp.GetNamespace(), + }, + Spec: sspv1beta1.SSPSpec{ + CommonTemplates: sspv1beta1.CommonTemplates{ + Namespace: newSsp.Spec.CommonTemplates.Namespace, + }, + }, + } + + Expect(apiClient.Create(ctx, ssp, client.DryRunAll)).To(Succeed()) + }) }) }) Context("update", func() { var ( - foundSsp *ssp.SSP + foundSsp *sspv1beta2.SSP ) BeforeEach(func() { @@ -174,7 +191,7 @@ var _ = Describe("Validation webhook", func() { It("[test_id:5990]should succeed with valid template-validator placement fields", func() { Eventually(func() error { foundSsp = getSsp() - foundSsp.Spec.TemplateValidator = &ssp.TemplateValidator{ + foundSsp.Spec.TemplateValidator = &sspv1beta2.TemplateValidator{ Placement: &placementAPIValidationValidPlacement, } return apiClient.Update(ctx, foundSsp, client.DryRunAll) @@ -184,7 +201,7 @@ var _ = Describe("Validation webhook", func() { It("[test_id:5989]should fail with invalid template-validator placement fields", func() { Eventually(func() v1.StatusReason { foundSsp = getSsp() - foundSsp.Spec.TemplateValidator = &ssp.TemplateValidator{ + foundSsp.Spec.TemplateValidator = &sspv1beta2.TemplateValidator{ Placement: &placementAPIValidationInvalidPlacement, } err := apiClient.Update(ctx, foundSsp, client.DryRunAll) @@ -196,7 +213,7 @@ var _ = Describe("Validation webhook", func() { It("[test_id:TODO] should fail when DataImportCronTemplate does not have a name", func() { Eventually(func() error { foundSsp = getSsp() - foundSsp.Spec.CommonTemplates.DataImportCronTemplates = []ssp.DataImportCronTemplate{{ + foundSsp.Spec.CommonTemplates.DataImportCronTemplates = []sspv1beta2.DataImportCronTemplate{{ ObjectMeta: metav1.ObjectMeta{Name: ""}, }} return apiClient.Update(ctx, foundSsp, client.DryRunAll) diff --git a/vendor/kubevirt.io/ssp-operator/api/v1beta1/groupversion_info.go b/vendor/kubevirt.io/ssp-operator/api/v1beta1/groupversion_info.go new file mode 100644 index 000000000..6a35a63df --- /dev/null +++ b/vendor/kubevirt.io/ssp-operator/api/v1beta1/groupversion_info.go @@ -0,0 +1,36 @@ +/* + + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package v1beta1 contains API Schema definitions for the ssp v1beta1 API group +// +kubebuilder:object:generate=true +// +groupName=ssp.kubevirt.io +package v1beta1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "ssp.kubevirt.io", Version: "v1beta1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/vendor/kubevirt.io/ssp-operator/api/v1beta1/ssp_types.go b/vendor/kubevirt.io/ssp-operator/api/v1beta1/ssp_types.go new file mode 100644 index 000000000..1f962e17d --- /dev/null +++ b/vendor/kubevirt.io/ssp-operator/api/v1beta1/ssp_types.go @@ -0,0 +1,171 @@ +/* + + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta1 + +import ( + ocpv1 "github.com/openshift/api/config/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + cdiv1beta1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" + lifecycleapi "kubevirt.io/controller-lifecycle-operator-sdk/api" +) + +const ( + OperatorPausedAnnotation = "kubevirt.io/operator.paused" +) + +type TemplateValidator struct { + // Replicas is the number of replicas of the template validator pod + //+kubebuilder:validation:Minimum=0 + //+kubebuilder:default=2 + Replicas *int32 `json:"replicas,omitempty"` + + // Placement describes the node scheduling configuration + Placement *lifecycleapi.NodePlacement `json:"placement,omitempty"` +} + +type CommonTemplates struct { + // Namespace is the k8s namespace where CommonTemplates should be installed + //+kubebuilder:validation:MaxLength=63 + //+kubebuilder:validation:Pattern=^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + Namespace string `json:"namespace"` + + // DataImportCronTemplates defines a list of DataImportCrons managed by the SSP + // Operator. This is intended for images used by CommonTemplates. + DataImportCronTemplates []DataImportCronTemplate `json:"dataImportCronTemplates,omitempty"` +} + +type NodeLabeller struct { + // Placement describes the node scheduling configuration + Placement *lifecycleapi.NodePlacement `json:"placement,omitempty"` +} + +type CommonInstancetypes struct { + // URL of a remote Kustomize target from which to generate and deploy resources. + // + // The following caveats apply to the provided URL: + // + // * Only 'https://' and 'git://' URLs are supported. + // + // * The URL must include '?ref=$ref' or '?version=$ref' pinning it to a specific + // reference. It is recommended that the reference be a specific commit or tag + // to ensure the generated contents does not change over time. As such it is + // recommended not to use branches as the ref for the time being. + // + // * Only VirtualMachineClusterPreference and VirtualMachineClusterInstancetype + // resources generated from the URL are deployed by the operand. + // + // See the following Kustomize documentation for more details: + // + // remote targets + // https://github.com/kubernetes-sigs/kustomize/blob/master/examples/remoteBuild.md + URL *string `json:"url,omitempty"` +} + +// SSPSpec defines the desired state of SSP +type SSPSpec struct { + // TemplateValidator is configuration of the template validator operand + TemplateValidator *TemplateValidator `json:"templateValidator,omitempty"` + + // CommonTemplates is the configuration of the common templates operand + CommonTemplates CommonTemplates `json:"commonTemplates"` + + // NodeLabeller is configuration of the node-labeller operand + NodeLabeller *NodeLabeller `json:"nodeLabeller,omitempty"` + + // TLSSecurityProfile is a configuration for the TLS. + TLSSecurityProfile *ocpv1.TLSSecurityProfile `json:"tlsSecurityProfile,omitempty"` + + // CommonInstancetypes is the configuration of the common-instancetypes operand + CommonInstancetypes *CommonInstancetypes `json:"commonInstancetypes,omitempty"` + + // TektonPipelines is the configuration of the tekton-pipelines operand + TektonPipelines *TektonPipelines `json:"tektonPipelines,omitempty"` + + // TektonTasks is the configuration of the tekton-tasks operand + TektonTasks *TektonTasks `json:"tektonTasks,omitempty"` + + // FeatureGates is the configuration of the tekton operands + FeatureGates *FeatureGates `json:"featureGates,omitempty"` +} + +// TektonPipelines defines the desired state of pipelines +type TektonPipelines struct { + Namespace string `json:"namespace,omitempty"` +} + +// TektonTasks defines variables for configuration of tasks +type TektonTasks struct { + Namespace string `json:"namespace,omitempty"` +} + +// FeatureGates defines feature gate for tto operator +type FeatureGates struct { + DeployTektonTaskResources bool `json:"deployTektonTaskResources,omitempty"` +} + +// DataImportCronTemplate defines the template type for DataImportCrons. +// It requires metadata.name to be specified while leaving namespace as optional. +type DataImportCronTemplate struct { + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec cdiv1beta1.DataImportCronSpec `json:"spec"` +} + +// AsDataImportCron converts the DataImportCronTemplate to a cdiv1beta1.DataImportCron +func (t *DataImportCronTemplate) AsDataImportCron() cdiv1beta1.DataImportCron { + return cdiv1beta1.DataImportCron{ + ObjectMeta: t.ObjectMeta, + Spec: t.Spec, + } +} + +// SSPStatus defines the observed state of SSP +type SSPStatus struct { + lifecycleapi.Status `json:",inline"` + + // Paused is true when the operator notices paused annotation. + Paused bool `json:"paused,omitempty"` + + // ObservedGeneration is the latest generation observed by the operator. + ObservedGeneration int64 `json:"observedGeneration,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:deprecatedversion:warning="ssp.kubevirt.io/v1beta1 ssp is deprecated" +// SSP is the Schema for the ssps API +// +kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase" +type SSP struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec SSPSpec `json:"spec,omitempty"` + Status SSPStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// SSPList contains a list of SSP +type SSPList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []SSP `json:"items"` +} + +func init() { + SchemeBuilder.Register(&SSP{}, &SSPList{}) +} diff --git a/vendor/kubevirt.io/ssp-operator/api/v1beta1/zz_generated.deepcopy.go b/vendor/kubevirt.io/ssp-operator/api/v1beta1/zz_generated.deepcopy.go new file mode 100644 index 000000000..ad8d40cc2 --- /dev/null +++ b/vendor/kubevirt.io/ssp-operator/api/v1beta1/zz_generated.deepcopy.go @@ -0,0 +1,300 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* + + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1beta1 + +import ( + "github.com/openshift/api/config/v1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CommonInstancetypes) DeepCopyInto(out *CommonInstancetypes) { + *out = *in + if in.URL != nil { + in, out := &in.URL, &out.URL + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CommonInstancetypes. +func (in *CommonInstancetypes) DeepCopy() *CommonInstancetypes { + if in == nil { + return nil + } + out := new(CommonInstancetypes) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CommonTemplates) DeepCopyInto(out *CommonTemplates) { + *out = *in + if in.DataImportCronTemplates != nil { + in, out := &in.DataImportCronTemplates, &out.DataImportCronTemplates + *out = make([]DataImportCronTemplate, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CommonTemplates. +func (in *CommonTemplates) DeepCopy() *CommonTemplates { + if in == nil { + return nil + } + out := new(CommonTemplates) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DataImportCronTemplate) DeepCopyInto(out *DataImportCronTemplate) { + *out = *in + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DataImportCronTemplate. +func (in *DataImportCronTemplate) DeepCopy() *DataImportCronTemplate { + if in == nil { + return nil + } + out := new(DataImportCronTemplate) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FeatureGates) DeepCopyInto(out *FeatureGates) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeatureGates. +func (in *FeatureGates) DeepCopy() *FeatureGates { + if in == nil { + return nil + } + out := new(FeatureGates) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NodeLabeller) DeepCopyInto(out *NodeLabeller) { + *out = *in + if in.Placement != nil { + in, out := &in.Placement, &out.Placement + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodeLabeller. +func (in *NodeLabeller) DeepCopy() *NodeLabeller { + if in == nil { + return nil + } + out := new(NodeLabeller) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SSP) DeepCopyInto(out *SSP) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SSP. +func (in *SSP) DeepCopy() *SSP { + if in == nil { + return nil + } + out := new(SSP) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *SSP) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SSPList) DeepCopyInto(out *SSPList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]SSP, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SSPList. +func (in *SSPList) DeepCopy() *SSPList { + if in == nil { + return nil + } + out := new(SSPList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *SSPList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SSPSpec) DeepCopyInto(out *SSPSpec) { + *out = *in + if in.TemplateValidator != nil { + in, out := &in.TemplateValidator, &out.TemplateValidator + *out = new(TemplateValidator) + (*in).DeepCopyInto(*out) + } + in.CommonTemplates.DeepCopyInto(&out.CommonTemplates) + if in.NodeLabeller != nil { + in, out := &in.NodeLabeller, &out.NodeLabeller + *out = new(NodeLabeller) + (*in).DeepCopyInto(*out) + } + if in.TLSSecurityProfile != nil { + in, out := &in.TLSSecurityProfile, &out.TLSSecurityProfile + *out = new(v1.TLSSecurityProfile) + (*in).DeepCopyInto(*out) + } + if in.CommonInstancetypes != nil { + in, out := &in.CommonInstancetypes, &out.CommonInstancetypes + *out = new(CommonInstancetypes) + (*in).DeepCopyInto(*out) + } + if in.TektonPipelines != nil { + in, out := &in.TektonPipelines, &out.TektonPipelines + *out = new(TektonPipelines) + **out = **in + } + if in.TektonTasks != nil { + in, out := &in.TektonTasks, &out.TektonTasks + *out = new(TektonTasks) + **out = **in + } + if in.FeatureGates != nil { + in, out := &in.FeatureGates, &out.FeatureGates + *out = new(FeatureGates) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SSPSpec. +func (in *SSPSpec) DeepCopy() *SSPSpec { + if in == nil { + return nil + } + out := new(SSPSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SSPStatus) DeepCopyInto(out *SSPStatus) { + *out = *in + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SSPStatus. +func (in *SSPStatus) DeepCopy() *SSPStatus { + if in == nil { + return nil + } + out := new(SSPStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TektonPipelines) DeepCopyInto(out *TektonPipelines) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TektonPipelines. +func (in *TektonPipelines) DeepCopy() *TektonPipelines { + if in == nil { + return nil + } + out := new(TektonPipelines) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TektonTasks) DeepCopyInto(out *TektonTasks) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TektonTasks. +func (in *TektonTasks) DeepCopy() *TektonTasks { + if in == nil { + return nil + } + out := new(TektonTasks) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TemplateValidator) DeepCopyInto(out *TemplateValidator) { + *out = *in + if in.Replicas != nil { + in, out := &in.Replicas, &out.Replicas + *out = new(int32) + **out = **in + } + if in.Placement != nil { + in, out := &in.Placement, &out.Placement + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TemplateValidator. +func (in *TemplateValidator) DeepCopy() *TemplateValidator { + if in == nil { + return nil + } + out := new(TemplateValidator) + in.DeepCopyInto(out) + return out +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 2c5668fc9..c2e73ea09 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -847,6 +847,7 @@ kubevirt.io/qe-tools/pkg/ginkgo-reporters kubevirt.io/qe-tools/pkg/polarion-xml # kubevirt.io/ssp-operator/api v0.0.0 => ./api ## explicit; go 1.19 +kubevirt.io/ssp-operator/api/v1beta1 kubevirt.io/ssp-operator/api/v1beta2 # sigs.k8s.io/controller-runtime v0.14.5 ## explicit; go 1.19 diff --git a/webhooks/ssp_webhook.go b/webhooks/ssp_webhook.go index 6883ec367..df970372e 100644 --- a/webhooks/ssp_webhook.go +++ b/webhooks/ssp_webhook.go @@ -24,6 +24,7 @@ import ( apps "k8s.io/api/apps/v1" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/utils/pointer" "kubevirt.io/controller-lifecycle-operator-sdk/api" @@ -32,14 +33,22 @@ import ( logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" - ssp "kubevirt.io/ssp-operator/api/v1beta2" + sspv1beta1 "kubevirt.io/ssp-operator/api/v1beta1" + sspv1beta2 "kubevirt.io/ssp-operator/api/v1beta2" ) var ssplog = logf.Log.WithName("ssp-resource") func Setup(mgr ctrl.Manager) error { + // This is a hack. Using Unstructured allows the webhook to correctly decode different versions of objects. + // Controller-runtime currently does not support a single webhook for multiple versions. + + obj := &unstructured.Unstructured{} + obj.SetAPIVersion(sspv1beta2.GroupVersion.String()) + obj.SetKind("SSP") + return ctrl.NewWebhookManagedBy(mgr). - For(&ssp.SSP{}). + For(obj). WithValidator(newSspValidator(mgr.GetClient())). Complete() } @@ -53,13 +62,16 @@ type sspValidator struct { var _ admission.CustomValidator = &sspValidator{} func (s *sspValidator) ValidateCreate(ctx context.Context, obj runtime.Object) error { - sspObj := obj.(*ssp.SSP) + sspObj, err := getSspWithConversion(obj) + if err != nil { + return err + } - var ssps ssp.SSPList + var ssps sspv1beta2.SSPList // Check if no other SSP resources are present in the cluster ssplog.Info("validate create", "name", sspObj.Name) - err := s.apiClient.List(ctx, &ssps, &client.ListOptions{}) + err = s.apiClient.List(ctx, &ssps, &client.ListOptions{}) if err != nil { return fmt.Errorf("could not list SSPs for validation, please try again: %v", err) } @@ -91,7 +103,10 @@ func (s *sspValidator) ValidateCreate(ctx context.Context, obj runtime.Object) e } func (s *sspValidator) ValidateUpdate(ctx context.Context, _, newObj runtime.Object) error { - newSsp := newObj.(*ssp.SSP) + newSsp, err := getSspWithConversion(newObj) + if err != nil { + return err + } ssplog.Info("validate update", "name", newSsp.Name) @@ -114,7 +129,7 @@ func (s *sspValidator) ValidateDelete(_ context.Context, _ runtime.Object) error return nil } -func (s *sspValidator) validatePlacement(ctx context.Context, ssp *ssp.SSP) error { +func (s *sspValidator) validatePlacement(ctx context.Context, ssp *sspv1beta2.SSP) error { if ssp.Spec.TemplateValidator == nil { return nil } @@ -172,8 +187,39 @@ func (s *sspValidator) validateOperandPlacement(ctx context.Context, namespace s return s.apiClient.Create(ctx, deployment, &client.CreateOptions{DryRun: []string{metav1.DryRunAll}}) } +func getSspWithConversion(obj runtime.Object) (*sspv1beta2.SSP, error) { + unstructuredSsp, ok := obj.(*unstructured.Unstructured) + if !ok { + return nil, fmt.Errorf("expected unstructured object, got %T", obj) + } + + if unstructuredSsp.GetKind() != "SSP" { + return nil, fmt.Errorf("expected SSP kind, got %s", unstructuredSsp.GetKind()) + } + + switch unstructuredSsp.GetAPIVersion() { + case sspv1beta1.GroupVersion.String(): + // Currently the two versions differ only in one removed field. + // We can decode the v1beta1 object into v1beta2. + // TODO: Use proper conversion logic. + unstructuredSsp.SetAPIVersion(sspv1beta2.GroupVersion.String()) + + case sspv1beta2.GroupVersion.String(): + break + default: + return nil, fmt.Errorf("unexpected group version %s", unstructuredSsp.GetAPIVersion()) + } + + ssp := &sspv1beta2.SSP{} + err := runtime.DefaultUnstructuredConverter.FromUnstructured(unstructuredSsp.UnstructuredContent(), ssp) + if err != nil { + return nil, fmt.Errorf("error converting unstructured object to SSP: %w", err) + } + return ssp, nil +} + // TODO: also validate DataImportCronTemplates in general once CDI exposes its own validation -func validateDataImportCronTemplates(ssp *ssp.SSP) error { +func validateDataImportCronTemplates(ssp *sspv1beta2.SSP) error { for _, cron := range ssp.Spec.CommonTemplates.DataImportCronTemplates { if cron.Name == "" { return fmt.Errorf("missing name in DataImportCronTemplate") @@ -182,7 +228,7 @@ func validateDataImportCronTemplates(ssp *ssp.SSP) error { return nil } -func validateCommonInstancetypes(ssp *ssp.SSP) error { +func validateCommonInstancetypes(ssp *sspv1beta2.SSP) error { if ssp.Spec.CommonInstancetypes == nil || ssp.Spec.CommonInstancetypes.URL == nil { return nil } diff --git a/webhooks/ssp_webhook_test.go b/webhooks/ssp_webhook_test.go index 308408a3d..e5c3289a5 100644 --- a/webhooks/ssp_webhook_test.go +++ b/webhooks/ssp_webhook_test.go @@ -18,6 +18,7 @@ package webhooks import ( "context" + "fmt" "testing" . "github.com/onsi/ginkgo/v2" @@ -25,13 +26,15 @@ import ( v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "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/webhook/admission" - ssp "kubevirt.io/ssp-operator/api/v1beta2" + sspv1beta1 "kubevirt.io/ssp-operator/api/v1beta1" + sspv1beta2 "kubevirt.io/ssp-operator/api/v1beta2" "kubevirt.io/ssp-operator/internal" ) @@ -47,7 +50,8 @@ var _ = Describe("SSP Validation", func() { JustBeforeEach(func() { scheme := runtime.NewScheme() // add our own scheme - Expect(ssp.SchemeBuilder.AddToScheme(scheme)).To(Succeed()) + Expect(sspv1beta2.SchemeBuilder.AddToScheme(scheme)).To(Succeed()) + Expect(sspv1beta1.SchemeBuilder.AddToScheme(scheme)).To(Succeed()) // add more schemes Expect(v1.AddToScheme(scheme)).To(Succeed()) @@ -78,14 +82,14 @@ var _ = Describe("SSP Validation", func() { Context("when one is already present", func() { BeforeEach(func() { // add an SSP CR to fake client - objects = append(objects, &ssp.SSP{ + objects = append(objects, &sspv1beta2.SSP{ ObjectMeta: metav1.ObjectMeta{ Name: "test-ssp", Namespace: "test-ns", ResourceVersion: "1", }, - Spec: ssp.SSPSpec{ - CommonTemplates: ssp.CommonTemplates{ + Spec: sspv1beta2.SSPSpec{ + CommonTemplates: sspv1beta2.CommonTemplates{ Namespace: templatesNamespace, }, }, @@ -93,18 +97,19 @@ var _ = Describe("SSP Validation", func() { }) It("should be rejected", func() { - ssp := &ssp.SSP{ + ssp := &sspv1beta2.SSP{ ObjectMeta: metav1.ObjectMeta{ Name: "test-ssp2", Namespace: "test-ns2", }, - Spec: ssp.SSPSpec{ - CommonTemplates: ssp.CommonTemplates{ + Spec: sspv1beta2.SSPSpec{ + CommonTemplates: sspv1beta2.CommonTemplates{ Namespace: templatesNamespace, }, }, } - err := validator.ValidateCreate(ctx, ssp) + + err := validator.ValidateCreate(ctx, toUnstructured(ssp)) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("creation failed, an SSP CR already exists in namespace test-ns: test-ssp")) }) @@ -112,31 +117,63 @@ var _ = Describe("SSP Validation", func() { It("should fail if template namespace does not exist", func() { const nonexistingNamespace = "nonexisting-namespace" - ssp := &ssp.SSP{ + ssp := &sspv1beta2.SSP{ ObjectMeta: metav1.ObjectMeta{ Name: "test-ssp", Namespace: "test-ns", }, - Spec: ssp.SSPSpec{ - CommonTemplates: ssp.CommonTemplates{ + Spec: sspv1beta2.SSPSpec{ + CommonTemplates: sspv1beta2.CommonTemplates{ Namespace: nonexistingNamespace, }, }, } - err := validator.ValidateCreate(ctx, ssp) + err := validator.ValidateCreate(ctx, toUnstructured(ssp)) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("creation failed, the configured namespace for common templates does not exist: " + nonexistingNamespace)) }) + + It("should accept old v1beta1 SSP CR", func() { + ssp := &sspv1beta1.SSP{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-ssp", + Namespace: "test-ns", + }, + Spec: sspv1beta1.SSPSpec{ + TemplateValidator: &sspv1beta1.TemplateValidator{ + Replicas: pointer.Int32(2), + }, + CommonTemplates: sspv1beta1.CommonTemplates{ + Namespace: templatesNamespace, + }, + NodeLabeller: &sspv1beta1.NodeLabeller{}, + CommonInstancetypes: &sspv1beta1.CommonInstancetypes{ + URL: pointer.String("https://foo.com/bar?ref=1234"), + }, + TektonPipelines: &sspv1beta1.TektonPipelines{ + Namespace: "test-pipelines-ns", + }, + TektonTasks: &sspv1beta1.TektonTasks{ + Namespace: "test-tasks-ns", + }, + FeatureGates: &sspv1beta1.FeatureGates{ + DeployTektonTaskResources: true, + }, + }, + } + + Expect(validator.ValidateCreate(ctx, toUnstructured(ssp))).To(Succeed()) + }) }) It("should allow update of commonTemplates.namespace", func() { - oldSsp := &ssp.SSP{ + oldSsp := &sspv1beta2.SSP{ ObjectMeta: metav1.ObjectMeta{ Name: "test-ssp", Namespace: "test-ns", }, - Spec: ssp.SSPSpec{ - CommonTemplates: ssp.CommonTemplates{ + Spec: sspv1beta2.SSPSpec{ + CommonTemplates: sspv1beta2.CommonTemplates{ Namespace: "old-ns", }, }, @@ -145,7 +182,7 @@ var _ = Describe("SSP Validation", func() { newSsp := oldSsp.DeepCopy() newSsp.Spec.CommonTemplates.Namespace = "new-ns" - err := validator.ValidateUpdate(ctx, oldSsp, newSsp) + err := validator.ValidateUpdate(ctx, toUnstructured(oldSsp), toUnstructured(newSsp)) Expect(err).ToNot(HaveOccurred()) }) @@ -155,8 +192,8 @@ var _ = Describe("SSP Validation", func() { ) var ( - oldSSP *ssp.SSP - newSSP *ssp.SSP + oldSSP *sspv1beta2.SSP + newSSP *sspv1beta2.SSP ) BeforeEach(func() { @@ -167,15 +204,15 @@ var _ = Describe("SSP Validation", func() { }, }) - oldSSP = &ssp.SSP{ + oldSSP = &sspv1beta2.SSP{ ObjectMeta: metav1.ObjectMeta{ Name: "test-ssp", Namespace: "test-ns", }, - Spec: ssp.SSPSpec{ - CommonTemplates: ssp.CommonTemplates{ + Spec: sspv1beta2.SSPSpec{ + CommonTemplates: sspv1beta2.CommonTemplates{ Namespace: templatesNamespace, - DataImportCronTemplates: []ssp.DataImportCronTemplate{ + DataImportCronTemplates: []sspv1beta2.DataImportCronTemplate{ { ObjectMeta: metav1.ObjectMeta{ Namespace: internal.GoldenImagesNamespace, @@ -194,15 +231,15 @@ var _ = Describe("SSP Validation", func() { }) It("should validate dataImportCronTemplates on create", func() { - Expect(validator.ValidateCreate(ctx, newSSP)).To(HaveOccurred()) + Expect(validator.ValidateCreate(ctx, toUnstructured(newSSP))).To(HaveOccurred()) newSSP.Spec.CommonTemplates.DataImportCronTemplates[0].Name = "test-name" - Expect(validator.ValidateCreate(ctx, newSSP)).ToNot(HaveOccurred()) + Expect(validator.ValidateCreate(ctx, toUnstructured(newSSP))).ToNot(HaveOccurred()) }) It("should validate dataImportCronTemplates on update", func() { - Expect(validator.ValidateUpdate(ctx, oldSSP, newSSP)).To(HaveOccurred()) + Expect(validator.ValidateUpdate(ctx, toUnstructured(oldSSP), toUnstructured(newSSP))).To(HaveOccurred()) newSSP.Spec.CommonTemplates.DataImportCronTemplates[0].Name = "test-name" - Expect(validator.ValidateUpdate(ctx, oldSSP, newSSP)).ToNot(HaveOccurred()) + Expect(validator.ValidateUpdate(ctx, toUnstructured(oldSSP), toUnstructured(newSSP))).ToNot(HaveOccurred()) }) }) @@ -212,7 +249,7 @@ var _ = Describe("SSP Validation", func() { templatesNamespace = "test-templates-ns" ) - var sspObj *ssp.SSP + var sspObj *sspv1beta2.SSP BeforeEach(func() { objects = append(objects, &v1.Namespace{ @@ -221,15 +258,15 @@ var _ = Describe("SSP Validation", func() { ResourceVersion: "1", }, }) - sspObj = &ssp.SSP{ + sspObj = &sspv1beta2.SSP{ ObjectMeta: metav1.ObjectMeta{ Name: "ssp", }, - Spec: ssp.SSPSpec{ - CommonTemplates: ssp.CommonTemplates{ + Spec: sspv1beta2.SSPSpec{ + CommonTemplates: sspv1beta2.CommonTemplates{ Namespace: templatesNamespace, }, - CommonInstancetypes: &ssp.CommonInstancetypes{}, + CommonInstancetypes: &sspv1beta2.CommonInstancetypes{}, }, } }) @@ -240,17 +277,17 @@ var _ = Describe("SSP Validation", func() { It("should reject URL without https:// or ssh://", func() { sspObj.Spec.CommonInstancetypes.URL = pointer.String("file://foo/bar") - Expect(validator.ValidateCreate(ctx, sspObj)).ShouldNot(Succeed()) + Expect(validator.ValidateCreate(ctx, toUnstructured(sspObj))).ShouldNot(Succeed()) }) It("should reject URL without ?ref= or ?version=", func() { sspObj.Spec.CommonInstancetypes.URL = pointer.String("https://foo.com/bar") - Expect(validator.ValidateCreate(ctx, sspObj)).ShouldNot(Succeed()) + Expect(validator.ValidateCreate(ctx, toUnstructured(sspObj))).ShouldNot(Succeed()) }) DescribeTable("should accept a valid remote kustomize target URL", func(url string) { sspObj.Spec.CommonInstancetypes.URL = pointer.String(url) - Expect(validator.ValidateCreate(ctx, sspObj)).Should(Succeed()) + Expect(validator.ValidateCreate(ctx, toUnstructured(sspObj))).Should(Succeed()) }, Entry("https:// with ?ref=", "https://foo.com/bar?ref=1234"), Entry("https:// with ?target=", "https://foo.com/bar?version=1234"), @@ -259,11 +296,28 @@ var _ = Describe("SSP Validation", func() { ) It("should accept when no URL is provided", func() { - Expect(validator.ValidateCreate(ctx, sspObj)).Should(Succeed()) + Expect(validator.ValidateCreate(ctx, toUnstructured(sspObj))).Should(Succeed()) }) }) }) +func toUnstructured(obj runtime.Object) *unstructured.Unstructured { + switch t := obj.(type) { + case *sspv1beta1.SSP: + t.APIVersion = sspv1beta1.GroupVersion.String() + t.Kind = "SSP" + case *sspv1beta2.SSP: + t.APIVersion = sspv1beta2.GroupVersion.String() + t.Kind = "SSP" + } + + data, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj) + if err != nil { + panic(fmt.Sprintf("cannot convert object to unstructured: %s", err)) + } + return &unstructured.Unstructured{Object: data} +} + func TestWebhook(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "API Suite")