diff --git a/apis/core/v1beta1/openstackcontrolplane_webhook.go b/apis/core/v1beta1/openstackcontrolplane_webhook.go index 8b4edaf23..99464fbf1 100644 --- a/apis/core/v1beta1/openstackcontrolplane_webhook.go +++ b/apis/core/v1beta1/openstackcontrolplane_webhook.go @@ -17,6 +17,7 @@ limitations under the License. package v1beta1 import ( + "context" "fmt" "strings" @@ -26,11 +27,14 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/validation/field" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/webhook" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" ) +var ctlplaneWebhookClient client.Client + // OpenStackControlPlaneDefaults - type OpenStackControlPlaneDefaults struct { RabbitMqImageURL string @@ -49,6 +53,10 @@ func SetupOpenStackControlPlaneDefaults(defaults OpenStackControlPlaneDefaults) // SetupWebhookWithManager sets up the Webhook with the Manager. func (r *OpenStackControlPlane) SetupWebhookWithManager(mgr ctrl.Manager) error { + if ctlplaneWebhookClient == nil { + ctlplaneWebhookClient = mgr.GetClient() + } + return ctrl.NewWebhookManagedBy(mgr). For(r). Complete() @@ -64,6 +72,38 @@ func (r *OpenStackControlPlane) ValidateCreate() (admission.Warnings, error) { var allErrs field.ErrorList basePath := field.NewPath("spec") + + ctlplaneList := &OpenStackControlPlaneList{} + listOpts := []client.ListOption{ + client.InNamespace(r.Namespace), + } + if err := ctlplaneWebhookClient.List(context.TODO(), ctlplaneList, listOpts...); err != nil { + return nil, apierrors.NewForbidden( + schema.GroupResource{ + Group: GroupVersion.WithKind("OpenStackControlPlane").Group, + Resource: GroupVersion.WithKind("OpenStackControlPlane").Kind, + }, r.GetName(), &field.Error{ + Type: field.ErrorTypeForbidden, + Field: "", + BadValue: r.Name, + Detail: err.Error(), + }, + ) + } + if len(ctlplaneList.Items) >= 1 { + return nil, apierrors.NewForbidden( + schema.GroupResource{ + Group: GroupVersion.WithKind("OpenStackControlPlane").Group, + Resource: GroupVersion.WithKind("OpenStackControlPlane").Kind, + }, r.GetName(), &field.Error{ + Type: field.ErrorTypeForbidden, + Field: "", + BadValue: r.Name, + Detail: "Only one OpenStackControlPlane instance per namespace is supported at this time.", + }, + ) + } + if err := r.ValidateCreateServices(basePath); err != nil { allErrs = append(allErrs, err...) } diff --git a/tests/functional/openstackoperator_controller_test.go b/tests/functional/openstackoperator_controller_test.go index cc0c66535..0f089a15c 100644 --- a/tests/functional/openstackoperator_controller_test.go +++ b/tests/functional/openstackoperator_controller_test.go @@ -1673,6 +1673,41 @@ var _ = Describe("OpenStackOperator controller", func() { var _ = Describe("OpenStackOperator Webhook", func() { + It("Blocks creating multiple ctlplane CRs in the same namespace", func() { + spec := GetDefaultOpenStackControlPlaneSpec() + spec["tls"] = GetTLSPublicSpec() + DeferCleanup( + th.DeleteInstance, + CreateOpenStackControlPlane(names.OpenStackControlplaneName, spec), + ) + + OSCtlplane := GetOpenStackControlPlane(names.OpenStackControlplaneName) + Expect(OSCtlplane.Labels).Should(Not(BeNil())) + Expect(OSCtlplane.Labels).Should(HaveKeyWithValue("core.openstack.org/openstackcontrolplane", "")) + + raw := map[string]interface{}{ + "apiVersion": "core.openstack.org/v1beta1", + "kind": "OpenStackControlPlane", + "metadata": map[string]interface{}{ + "name": "foo", + "namespace": OSCtlplane.GetNamespace(), + }, + "spec": spec, + } + + unstructuredObj := &unstructured.Unstructured{Object: raw} + _, err := controllerutil.CreateOrPatch( + th.Ctx, th.K8sClient, unstructuredObj, func() error { return nil }) + Expect(err).Should(HaveOccurred()) + var statusError *k8s_errors.StatusError + Expect(errors.As(err, &statusError)).To(BeTrue()) + Expect(statusError.ErrStatus.Details.Kind).To(Equal("OpenStackControlPlane")) + Expect(statusError.ErrStatus.Message).To( + ContainSubstring( + "Forbidden: Only one OpenStackControlPlane instance per namespace is supported at this time."), + ) + }) + It("Adds default label via defaulting webhook", func() { spec := GetDefaultOpenStackControlPlaneSpec() spec["tls"] = GetTLSPublicSpec()