diff --git a/test/e2e/suites/apivalidations/apivalidations_test.go b/test/e2e/suites/apivalidations/apivalidations_test.go new file mode 100644 index 0000000000..13a0f16560 --- /dev/null +++ b/test/e2e/suites/apivalidations/apivalidations_test.go @@ -0,0 +1,126 @@ +/* +Copyright 2024 The Kubernetes Authors. + +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 apivalidations + +import ( + "context" + "fmt" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/metrics/server" + "sigs.k8s.io/controller-runtime/pkg/webhook" + + infrav1 "sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1" +) + +var _ = Describe("APIValidations", func() { + var ( + mgrCancel context.CancelFunc + mgrDone chan struct{} + + namespace corev1.Namespace + ) + + BeforeEach(func() { + By("Setting up manager and webhooks") + mgr, err := ctrl.NewManager(cfg, ctrl.Options{ + Scheme: testScheme, + Metrics: server.Options{ + BindAddress: "0", + }, + WebhookServer: webhook.NewServer(webhook.Options{ + Port: testEnv.WebhookInstallOptions.LocalServingPort, + Host: testEnv.WebhookInstallOptions.LocalServingHost, + CertDir: testEnv.WebhookInstallOptions.LocalServingCertDir, + }), + }) + Expect(err).ToNot(HaveOccurred(), "Manager setup should succeed") + + Expect((&infrav1.OpenStackMachineTemplateWebhook{}).SetupWebhookWithManager(mgr)).To(Succeed(), "OpenStackMachineTemplate webhook should be registered with manager") + Expect((&infrav1.OpenStackMachineTemplateList{}).SetupWebhookWithManager(mgr)).To(Succeed(), "OpenStackMachineTemplateList webhook should be registered with manager") + Expect((&infrav1.OpenStackCluster{}).SetupWebhookWithManager(mgr)).To(Succeed(), "OpenStackCluster webhook should be registered with manager") + Expect((&infrav1.OpenStackClusterTemplate{}).SetupWebhookWithManager(mgr)).To(Succeed(), "OpenStackClusterTemplate webhook should be registered with manager") + Expect((&infrav1.OpenStackMachine{}).SetupWebhookWithManager(mgr)).To(Succeed(), "OpenStackMachine webhook should be registered with manager") + Expect((&infrav1.OpenStackMachineList{}).SetupWebhookWithManager(mgr)).To(Succeed(), "OpenStackMachineList webhook should be registered with manager") + Expect((&infrav1.OpenStackClusterList{}).SetupWebhookWithManager(mgr)).To(Succeed(), "OpenStackClusterList webhook should be registered with manager") + + By("Starting manager") + var mgrCtx context.Context + mgrDone = make(chan struct{}) + mgrCtx, mgrCancel = context.WithCancel(context.Background()) + + go func() { + defer GinkgoRecover() + defer close(mgrDone) + Expect(mgr.Start(mgrCtx)).To(Succeed(), "Manager should start") + }() + DeferCleanup(func() { + By("Tearing down manager") + mgrCancel() + Eventually(mgrDone).Should(BeClosed(), "Manager should stop") + }) + + By("Creating namespace") + namespace = corev1.Namespace{} + namespace.GenerateName = "test-" + Expect(k8sClient.Create(ctx, &namespace)).To(Succeed(), "Namespace creation should succeed") + DeferCleanup(func() { + By("Deleting namespace") + Expect(k8sClient.Delete(ctx, &namespace, client.PropagationPolicy(metav1.DeletePropagationForeground))).To(Succeed(), "Namespace deletion should succeed") + }) + By(fmt.Sprintf("Using namespace %s", namespace.Name)) + }) + + Context("OpenStackCluster", func() { + var cluster *infrav1.OpenStackCluster + + BeforeEach(func() { + // Initialise a basic cluster object in the correct namespace + cluster = &infrav1.OpenStackCluster{} + cluster.Namespace = namespace.Name + cluster.Name = "cluster" + }) + + It("should allow the smallest permissible cluster spec", func() { + Expect(k8sClient.Create(ctx, cluster)).To(Succeed(), "OpenStackCluster creation should succeed") + }) + + It("should only allow controlPlaneEndpoint to be set once", func() { + By("Creating a bare cluster") + Expect(k8sClient.Create(ctx, cluster)).To(Succeed(), "OpenStackCluster creation should succeed") + + By("Setting the control plane endpoint") + cluster.Spec.ControlPlaneEndpoint.Host = "foo" + cluster.Spec.ControlPlaneEndpoint.Port = 1234 + Expect(k8sClient.Update(ctx, cluster)).To(Succeed(), "Setting control plane endpoint should succeed") + + By("Modifying the control plane endpoint") + cluster.Spec.ControlPlaneEndpoint.Host = "bar" + Expect(k8sClient.Update(ctx, cluster)).NotTo(Succeed(), "Updating control plane endpoint should fail") + }) + + It("should allow an empty managed security groups definition", func() { + cluster.Spec.ManagedSecurityGroups = &infrav1.ManagedSecurityGroups{} + Expect(k8sClient.Create(ctx, cluster)).To(Succeed(), "OpenStackCluster creation should succeed") + }) + }) +}) diff --git a/test/e2e/suites/apivalidations/suite_test.go b/test/e2e/suites/apivalidations/suite_test.go new file mode 100644 index 0000000000..1d8da6f510 --- /dev/null +++ b/test/e2e/suites/apivalidations/suite_test.go @@ -0,0 +1,101 @@ +/* +Copyright 2024 The Kubernetes Authors. + +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 apivalidations + +import ( + "context" + "fmt" + "path/filepath" + "strconv" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/discovery" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + "sigs.k8s.io/controller-runtime/pkg/envtest/komega" + + infrav1 "sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1" +) + +var ( + cfg *rest.Config + k8sClient client.Client + testEnv *envtest.Environment + testScheme *runtime.Scheme + ctx = context.Background() +) + +func TestAPIs(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, "Controller Suite") +} + +var _ = BeforeSuite(func() { + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{ + // NOTE: These are the bare CRDs without conversion webhooks + filepath.Join("..", "..", "..", "..", "config", "crd", "bases"), + }, + ErrorIfCRDPathMissing: true, + WebhookInstallOptions: envtest.WebhookInstallOptions{ + Paths: []string{ + filepath.Join("..", "..", "..", "..", "config", "webhook"), + }, + }, + } + + var err error + cfg, err = testEnv.Start() + Expect(err).NotTo(HaveOccurred(), "test environment should start") + Expect(cfg).NotTo(BeNil(), "test environment should return a configuration") + DeferCleanup(func() error { + By("tearing down the test environment") + return testEnv.Stop() + }) + + testScheme = scheme.Scheme + Expect(infrav1.AddToScheme(testScheme)).To(Succeed()) + + //+kubebuilder:scaffold:scheme + + k8sClient, err = client.New(cfg, client.Options{Scheme: testScheme}) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient).NotTo(BeNil()) + + // CEL requires Kube 1.25 and above, so check for the minimum server version. + discoveryClient, err := discovery.NewDiscoveryClientForConfig(cfg) + Expect(err).ToNot(HaveOccurred()) + + serverVersion, err := discoveryClient.ServerVersion() + Expect(err).ToNot(HaveOccurred()) + + Expect(serverVersion.Major).To(Equal("1")) + + minorInt, err := strconv.Atoi(serverVersion.Minor) + Expect(err).ToNot(HaveOccurred()) + Expect(minorInt).To(BeNumerically(">=", 25), fmt.Sprintf("This test suite requires a Kube API server of at least version 1.25, current version is 1.%s", serverVersion.Minor)) + + komega.SetClient(k8sClient) + komega.SetContext(ctx) +})