diff --git a/api/v1alpha1/common.go b/api/v1alpha1/common.go index ddd02be9..ddf27cd8 100644 --- a/api/v1alpha1/common.go +++ b/api/v1alpha1/common.go @@ -79,6 +79,10 @@ func SetupIndexers(ctx context.Context, mgr ctrl.Manager) error { return err } + if err := SetupMultiClusterServiceServicesIndexer(ctx, mgr); err != nil { + return err + } + if err := SetupClusterTemplateChainIndexer(ctx, mgr); err != nil { return err } @@ -131,10 +135,10 @@ func ExtractReleaseTemplates(rawObj client.Object) []string { const ServicesTemplateKey = ".spec.services[].Template" func SetupManagedClusterServicesIndexer(ctx context.Context, mgr ctrl.Manager) error { - return mgr.GetFieldIndexer().IndexField(ctx, &ManagedCluster{}, ServicesTemplateKey, ExtractServiceTemplateName) + return mgr.GetFieldIndexer().IndexField(ctx, &ManagedCluster{}, ServicesTemplateKey, ExtractServiceTemplateFromManagedCluster) } -func ExtractServiceTemplateName(rawObj client.Object) []string { +func ExtractServiceTemplateFromManagedCluster(rawObj client.Object) []string { cluster, ok := rawObj.(*ManagedCluster) if !ok { return nil @@ -148,6 +152,24 @@ func ExtractServiceTemplateName(rawObj client.Object) []string { return templates } +func SetupMultiClusterServiceServicesIndexer(ctx context.Context, mgr ctrl.Manager) error { + return mgr.GetFieldIndexer().IndexField(ctx, &MultiClusterService{}, ServicesTemplateKey, ExtractServiceTemplateFromMultiClusterService) +} + +func ExtractServiceTemplateFromMultiClusterService(rawObj client.Object) []string { + cluster, ok := rawObj.(*MultiClusterService) + if !ok { + return nil + } + + templates := []string{} + for _, s := range cluster.Spec.Services { + templates = append(templates, s.Template) + } + + return templates +} + const SupportedTemplateKey = ".spec.supportedTemplates[].Name" func SetupClusterTemplateChainIndexer(ctx context.Context, mgr ctrl.Manager) error { diff --git a/api/v1alpha1/multiclusterservice_types.go b/api/v1alpha1/multiclusterservice_types.go index 31be9c0d..fe575294 100644 --- a/api/v1alpha1/multiclusterservice_types.go +++ b/api/v1alpha1/multiclusterservice_types.go @@ -100,7 +100,7 @@ type ServiceStatus struct { type MultiClusterServiceStatus struct { // Services contains details for the state of services. Services []ServiceStatus `json:"services,omitempty"` - // Conditions contains details for the current state of the ManagedCluster + // Conditions contains details for the current state of the MultiClusterService. Conditions []metav1.Condition `json:"conditions,omitempty"` // ObservedGeneration is the last observed generation. ObservedGeneration int64 `json:"observedGeneration,omitempty"` diff --git a/cmd/main.go b/cmd/main.go index acf62c37..5ee887fb 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -296,7 +296,8 @@ func main() { } if err = (&controller.MultiClusterServiceReconciler{ - Client: mgr.GetClient(), + Client: mgr.GetClient(), + SystemNamespace: currentNamespace, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "MultiClusterService") os.Exit(1) @@ -331,6 +332,10 @@ func setupWebhooks(mgr ctrl.Manager, currentNamespace string) error { setupLog.Error(err, "unable to create webhook", "webhook", "ManagedCluster") return err } + if err := (&hmcwebhook.MultiClusterServiceValidator{SystemNamespace: currentNamespace}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "MultiClusterService") + return err + } if err := (&hmcwebhook.ManagementValidator{}).SetupWebhookWithManager(mgr); err != nil { setupLog.Error(err, "unable to create webhook", "webhook", "Management") return err @@ -351,7 +356,7 @@ func setupWebhooks(mgr ctrl.Manager, currentNamespace string) error { setupLog.Error(err, "unable to create webhook", "webhook", "ClusterTemplate") return err } - if err := (&hmcwebhook.ServiceTemplateValidator{}).SetupWebhookWithManager(mgr); err != nil { + if err := (&hmcwebhook.ServiceTemplateValidator{SystemNamespace: currentNamespace}).SetupWebhookWithManager(mgr); err != nil { setupLog.Error(err, "unable to create webhook", "webhook", "ServiceTemplate") return err } diff --git a/internal/controller/managedcluster_controller_test.go b/internal/controller/managedcluster_controller_test.go index 2182968f..ac37d190 100644 --- a/internal/controller/managedcluster_controller_test.go +++ b/internal/controller/managedcluster_controller_test.go @@ -27,6 +27,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/reconcile" hmc "github.com/Mirantis/hmc/api/v1alpha1" @@ -38,8 +39,9 @@ var _ = Describe("ManagedCluster Controller", func() { managedClusterName = "test-managed-cluster" managedClusterNamespace = "test" - templateName = "test-template" - credentialName = "test-credential" + templateName = "test-template" + svcTemplateName = "test-svc-template" + credentialName = "test-credential" ) ctx := context.Background() @@ -50,6 +52,7 @@ var _ = Describe("ManagedCluster Controller", func() { } managedCluster := &hmc.ManagedCluster{} template := &hmc.ClusterTemplate{} + svcTemplate := &hmc.ServiceTemplate{} management := &hmc.Management{} credential := &hmc.Credential{} namespace := &corev1.Namespace{} @@ -66,7 +69,7 @@ var _ = Describe("ManagedCluster Controller", func() { Expect(k8sClient.Create(ctx, namespace)).To(Succeed()) } - By("creating the custom resource for the Kind Template") + By("creating the custom resource for the Kind ClusterTemplate") err = k8sClient.Get(ctx, typeNamespacedName, template) if err != nil && errors.IsNotFound(err) { template = &hmc.ClusterTemplate{ @@ -99,6 +102,35 @@ var _ = Describe("ManagedCluster Controller", func() { Expect(k8sClient.Status().Update(ctx, template)).To(Succeed()) } + By("creating the custom resource for the Kind ServiceTemplate") + err = k8sClient.Get(ctx, client.ObjectKey{Namespace: managedClusterNamespace, Name: svcTemplateName}, svcTemplate) + if err != nil && errors.IsNotFound(err) { + svcTemplate = &hmc.ServiceTemplate{ + ObjectMeta: metav1.ObjectMeta{ + Name: svcTemplateName, + Namespace: managedClusterNamespace, + }, + Spec: hmc.ServiceTemplateSpec{ + Helm: hmc.HelmSpec{ + ChartRef: &hcv2.CrossNamespaceSourceReference{ + Kind: "HelmChart", + Name: "ref-test", + Namespace: "default", + }, + }, + }, + } + Expect(k8sClient.Create(ctx, svcTemplate)).To(Succeed()) + svcTemplate.Status = hmc.ServiceTemplateStatus{ + TemplateStatusCommon: hmc.TemplateStatusCommon{ + TemplateValidationStatus: hmc.TemplateValidationStatus{ + Valid: true, + }, + }, + } + Expect(k8sClient.Status().Update(ctx, svcTemplate)).To(Succeed()) + } + By("creating the custom resource for the Kind Management") err = k8sClient.Get(ctx, typeNamespacedName, management) if err != nil && errors.IsNotFound(err) { @@ -148,6 +180,12 @@ var _ = Describe("ManagedCluster Controller", func() { Spec: hmc.ManagedClusterSpec{ Template: templateName, Credential: credentialName, + Services: []hmc.ServiceSpec{ + { + Template: svcTemplateName, + Name: "test-svc-name", + }, + }, }, } Expect(k8sClient.Create(ctx, managedCluster)).To(Succeed()) diff --git a/internal/controller/multiclusterservice_controller.go b/internal/controller/multiclusterservice_controller.go index 38136cf9..12905445 100644 --- a/internal/controller/multiclusterservice_controller.go +++ b/internal/controller/multiclusterservice_controller.go @@ -44,6 +44,7 @@ import ( // MultiClusterServiceReconciler reconciles a MultiClusterService object type MultiClusterServiceReconciler struct { client.Client + SystemNamespace string } // Reconcile reconciles a MultiClusterService object. @@ -114,9 +115,9 @@ func (r *MultiClusterServiceReconciler) reconcileUpdate(ctx context.Context, mcs return ctrl.Result{Requeue: true}, nil } - // By using DefaultSystemNamespace we are enforcing that MultiClusterService - // may only use ServiceTemplates that are present in the hmc-system namespace. - opts, err := helmChartOpts(ctx, r.Client, utils.DefaultSystemNamespace, mcs.Spec.Services) + // We are enforcing that MultiClusterService may only use + // ServiceTemplates that are present in the system namespace. + opts, err := helmChartOpts(ctx, r.Client, r.SystemNamespace, mcs.Spec.Services) if err != nil { return ctrl.Result{}, err } @@ -179,7 +180,7 @@ func helmChartOpts(ctx context.Context, c client.Client, namespace string, servi // Here we can use the same namespace for all services // because if the services slice is part of: // 1. ManagedCluster: Then the referred template must be in its own namespace. - // 2. MultiClusterService: Then the referred template must be in hmc-system namespace. + // 2. MultiClusterService: Then the referred template must be in system namespace. tmplRef := client.ObjectKey{Name: svc.Template, Namespace: namespace} if err := c.Get(ctx, tmplRef, tmpl); err != nil { return nil, fmt.Errorf("failed to get ServiceTemplate %s: %w", tmplRef.String(), err) @@ -356,7 +357,7 @@ func requeueSveltosProfileForClusterSummary(ctx context.Context, obj client.Obje cs, ok := obj.(*sveltosv1beta1.ClusterSummary) if !ok { - l.Error(errors.New("request is not for a ClusterSummary object"), msg, "ClusterSummary.Name", obj.GetName(), "ClusterSummary.Namespace", obj.GetNamespace()) + l.Error(errors.New("request is not for a ClusterSummary object"), msg, "Requested.Name", obj.GetName(), "Requested.Namespace", obj.GetNamespace()) return []ctrl.Request{} } diff --git a/internal/controller/multiclusterservice_controller_test.go b/internal/controller/multiclusterservice_controller_test.go index 245f5b55..817eba6f 100644 --- a/internal/controller/multiclusterservice_controller_test.go +++ b/internal/controller/multiclusterservice_controller_test.go @@ -31,13 +31,11 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" hmc "github.com/Mirantis/hmc/api/v1alpha1" - "github.com/Mirantis/hmc/internal/utils" ) var _ = Describe("MultiClusterService Controller", func() { Context("When reconciling a resource", func() { const ( - testNamespace = utils.DefaultSystemNamespace serviceTemplateName = "test-service-0-1-0" helmRepoName = "test-helmrepo" helmChartName = "test-helmchart" @@ -66,31 +64,31 @@ var _ = Describe("MultiClusterService Controller", func() { multiClusterService := &hmc.MultiClusterService{} clusterProfile := &sveltosv1beta1.ClusterProfile{} - helmRepositoryRef := types.NamespacedName{Namespace: testNamespace, Name: helmRepoName} - helmChartRef := types.NamespacedName{Namespace: testNamespace, Name: helmChartName} - serviceTemplateRef := types.NamespacedName{Namespace: testNamespace, Name: serviceTemplateName} + helmRepositoryRef := types.NamespacedName{Namespace: testSystemNamespace, Name: helmRepoName} + helmChartRef := types.NamespacedName{Namespace: testSystemNamespace, Name: helmChartName} + serviceTemplateRef := types.NamespacedName{Namespace: testSystemNamespace, Name: serviceTemplateName} multiClusterServiceRef := types.NamespacedName{Name: multiClusterServiceName} clusterProfileRef := types.NamespacedName{Name: multiClusterServiceName} BeforeEach(func() { By("creating Namespace") - err := k8sClient.Get(ctx, types.NamespacedName{Name: testNamespace}, namespace) + err := k8sClient.Get(ctx, types.NamespacedName{Name: testSystemNamespace}, namespace) if err != nil && apierrors.IsNotFound(err) { namespace = &corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ - Name: testNamespace, + Name: testSystemNamespace, }, } Expect(k8sClient.Create(ctx, namespace)).To(Succeed()) } By("creating HelmRepository") - err = k8sClient.Get(ctx, types.NamespacedName{Name: helmRepoName, Namespace: testNamespace}, helmRepo) + err = k8sClient.Get(ctx, types.NamespacedName{Name: helmRepoName, Namespace: testSystemNamespace}, helmRepo) if err != nil && apierrors.IsNotFound(err) { helmRepo = &sourcev1.HelmRepository{ ObjectMeta: metav1.ObjectMeta{ Name: helmRepoName, - Namespace: testNamespace, + Namespace: testSystemNamespace, }, Spec: sourcev1.HelmRepositorySpec{ URL: "oci://test/helmrepo", @@ -100,12 +98,12 @@ var _ = Describe("MultiClusterService Controller", func() { } By("creating HelmChart") - err = k8sClient.Get(ctx, types.NamespacedName{Name: helmChartName, Namespace: testNamespace}, helmChart) + err = k8sClient.Get(ctx, types.NamespacedName{Name: helmChartName, Namespace: testSystemNamespace}, helmChart) if err != nil && apierrors.IsNotFound(err) { helmChart = &sourcev1.HelmChart{ ObjectMeta: metav1.ObjectMeta{ Name: helmChartName, - Namespace: testNamespace, + Namespace: testSystemNamespace, }, Spec: sourcev1.HelmChartSpec{ SourceRef: sourcev1.LocalHelmChartSourceReference{ @@ -131,7 +129,7 @@ var _ = Describe("MultiClusterService Controller", func() { serviceTemplate = &hmc.ServiceTemplate{ ObjectMeta: metav1.ObjectMeta{ Name: serviceTemplateName, - Namespace: testNamespace, + Namespace: testSystemNamespace, Labels: map[string]string{ hmc.HMCManagedLabelKey: "true", }, @@ -142,7 +140,7 @@ var _ = Describe("MultiClusterService Controller", func() { ChartRef: &helmcontrollerv2.CrossNamespaceSourceReference{ Kind: "HelmChart", Name: helmChartName, - Namespace: testNamespace, + Namespace: testSystemNamespace, }, }, }, @@ -150,6 +148,20 @@ var _ = Describe("MultiClusterService Controller", func() { } Expect(k8sClient.Create(ctx, serviceTemplate)).To(Succeed()) + By("reconciling ServiceTemplate used by MultiClusterService") + templateReconciler := TemplateReconciler{ + Client: k8sClient, + downloadHelmChartFunc: fakeDownloadHelmChartFunc, + } + serviceTemplateReconciler := &ServiceTemplateReconciler{TemplateReconciler: templateReconciler} + _, err = serviceTemplateReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: serviceTemplateRef}) + Expect(err).NotTo(HaveOccurred()) + + By("having the valid service template status") + Expect(k8sClient.Get(ctx, serviceTemplateRef, serviceTemplate)).To(Succeed()) + Expect(serviceTemplate.Status.Valid).To(BeTrue()) + Expect(serviceTemplate.Status.ValidationError).To(BeEmpty()) + By("creating MultiClusterService") err = k8sClient.Get(ctx, multiClusterServiceRef, multiClusterService) if err != nil && apierrors.IsNotFound(err) { @@ -181,7 +193,7 @@ var _ = Describe("MultiClusterService Controller", func() { multiClusterServiceResource := &hmc.MultiClusterService{} Expect(k8sClient.Get(ctx, multiClusterServiceRef, multiClusterServiceResource)).NotTo(HaveOccurred()) - reconciler := &MultiClusterServiceReconciler{Client: k8sClient} + reconciler := &MultiClusterServiceReconciler{Client: k8sClient, SystemNamespace: testSystemNamespace} Expect(k8sClient.Delete(ctx, multiClusterService)).To(Succeed()) // Running reconcile to remove the finalizer and delete the MultiClusterService _, err := reconciler.Reconcile(ctx, reconcile.Request{NamespacedName: multiClusterServiceRef}) @@ -204,19 +216,10 @@ var _ = Describe("MultiClusterService Controller", func() { }) It("should successfully reconcile the resource", func() { - By("reconciling ServiceTemplate used by MultiClusterService") - templateReconciler := TemplateReconciler{ - Client: k8sClient, - downloadHelmChartFunc: fakeDownloadHelmChartFunc, - } - serviceTemplateReconciler := &ServiceTemplateReconciler{TemplateReconciler: templateReconciler} - _, err := serviceTemplateReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: serviceTemplateRef}) - Expect(err).NotTo(HaveOccurred()) - By("reconciling MultiClusterService") - multiClusterServiceReconciler := &MultiClusterServiceReconciler{Client: k8sClient} + multiClusterServiceReconciler := &MultiClusterServiceReconciler{Client: k8sClient, SystemNamespace: testSystemNamespace} - _, err = multiClusterServiceReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: multiClusterServiceRef}) + _, err := multiClusterServiceReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: multiClusterServiceRef}) Expect(err).NotTo(HaveOccurred()) Eventually(k8sClient.Get, 1*time.Minute, 5*time.Second).WithArguments(ctx, clusterProfileRef, clusterProfile).ShouldNot(HaveOccurred()) diff --git a/internal/controller/suite_test.go b/internal/controller/suite_test.go index 395afc88..1cc10a19 100644 --- a/internal/controller/suite_test.go +++ b/internal/controller/suite_test.go @@ -54,6 +54,7 @@ import ( const ( mutatingWebhookKind = "MutatingWebhookConfiguration" validatingWebhookKind = "ValidatingWebhookConfiguration" + testSystemNamespace = "test-system-namespace" ) var ( @@ -150,6 +151,9 @@ var _ = BeforeSuite(func() { err = (&hmcwebhook.ManagedClusterValidator{}).SetupWebhookWithManager(mgr) Expect(err).NotTo(HaveOccurred()) + err = (&hmcwebhook.MultiClusterServiceValidator{SystemNamespace: testSystemNamespace}).SetupWebhookWithManager(mgr) + Expect(err).NotTo(HaveOccurred()) + err = (&hmcwebhook.ManagementValidator{}).SetupWebhookWithManager(mgr) Expect(err).NotTo(HaveOccurred()) @@ -165,7 +169,7 @@ var _ = BeforeSuite(func() { err = (&hmcwebhook.ClusterTemplateValidator{}).SetupWebhookWithManager(mgr) Expect(err).NotTo(HaveOccurred()) - err = (&hmcwebhook.ServiceTemplateValidator{}).SetupWebhookWithManager(mgr) + err = (&hmcwebhook.ServiceTemplateValidator{SystemNamespace: testSystemNamespace}).SetupWebhookWithManager(mgr) Expect(err).NotTo(HaveOccurred()) err = (&hmcwebhook.ProviderTemplateValidator{}).SetupWebhookWithManager(mgr) diff --git a/internal/controller/template_controller_test.go b/internal/controller/template_controller_test.go index 373046f7..ceddb620 100644 --- a/internal/controller/template_controller_test.go +++ b/internal/controller/template_controller_test.go @@ -312,7 +312,8 @@ var _ = Describe("Template Controller", func() { By("Having the valid cluster template status") Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(clusterTemplate), clusterTemplate)).To(Succeed()) - Expect(clusterTemplate.Status.Valid && clusterTemplate.Status.ValidationError == "").To(BeTrue()) + Expect(clusterTemplate.Status.Valid).To(BeTrue()) + Expect(clusterTemplate.Status.ValidationError).To(BeEmpty()) Expect(clusterTemplate.Status.Providers).To(HaveLen(2)) Expect(clusterTemplate.Status.ProviderContracts).To(HaveLen(2)) Expect(clusterTemplate.Status.Providers[0]).To(Equal(someProviderName)) diff --git a/internal/webhook/managedcluster_webhook.go b/internal/webhook/managedcluster_webhook.go index 5ad95f98..866a2b9b 100644 --- a/internal/webhook/managedcluster_webhook.go +++ b/internal/webhook/managedcluster_webhook.go @@ -67,7 +67,7 @@ func (v *ManagedClusterValidator) ValidateCreate(ctx context.Context, obj runtim return nil, fmt.Errorf("%s: %w", invalidManagedClusterMsg, err) } - if err := isTemplateValid(template); err != nil { + if err := isTemplateValid(template.GetCommonStatus()); err != nil { return nil, fmt.Errorf("%s: %w", invalidManagedClusterMsg, err) } @@ -79,6 +79,10 @@ func (v *ManagedClusterValidator) ValidateCreate(ctx context.Context, obj runtim return nil, fmt.Errorf("%s: %w", invalidManagedClusterMsg, err) } + if err := validateServices(ctx, v.Client, managedCluster.Namespace, managedCluster.Spec.Services); err != nil { + return nil, fmt.Errorf("%s: %w", invalidManagedClusterMsg, err) + } + return nil, nil } @@ -106,7 +110,7 @@ func (v *ManagedClusterValidator) ValidateUpdate(ctx context.Context, oldObj, ne return admission.Warnings{msg}, errClusterUpgradeForbidden } - if err := isTemplateValid(template); err != nil { + if err := isTemplateValid(template.GetCommonStatus()); err != nil { return nil, fmt.Errorf("%s: %w", invalidManagedClusterMsg, err) } @@ -119,6 +123,10 @@ func (v *ManagedClusterValidator) ValidateUpdate(ctx context.Context, oldObj, ne return nil, fmt.Errorf("%s: %w", invalidManagedClusterMsg, err) } + if err := validateServices(ctx, v.Client, newManagedCluster.Namespace, newManagedCluster.Spec.Services); err != nil { + return nil, fmt.Errorf("%s: %w", invalidManagedClusterMsg, err) + } + return nil, nil } @@ -185,7 +193,7 @@ func (v *ManagedClusterValidator) Default(ctx context.Context, obj runtime.Objec return fmt.Errorf("could not get template for the managedcluster: %w", err) } - if err := isTemplateValid(template); err != nil { + if err := isTemplateValid(template.GetCommonStatus()); err != nil { return fmt.Errorf("template is invalid: %w", err) } @@ -216,9 +224,9 @@ func (v *ManagedClusterValidator) getManagedClusterCredential(ctx context.Contex return cred, nil } -func isTemplateValid(template *hmcv1alpha1.ClusterTemplate) error { - if !template.Status.Valid { - return fmt.Errorf("the template is not valid: %s", template.Status.ValidationError) +func isTemplateValid(status *hmcv1alpha1.TemplateStatusCommon) error { + if !status.Valid { + return fmt.Errorf("the template is not valid: %s", status.ValidationError) } return nil diff --git a/internal/webhook/managedcluster_webhook_test.go b/internal/webhook/managedcluster_webhook_test.go index 4d8cc84a..87a1ed14 100644 --- a/internal/webhook/managedcluster_webhook_test.go +++ b/internal/webhook/managedcluster_webhook_test.go @@ -97,6 +97,32 @@ func TestManagedClusterValidateCreate(t *testing.T) { }, err: fmt.Sprintf("the ManagedCluster is invalid: clustertemplates.hmc.mirantis.com \"%s\" not found", testTemplateName), }, + { + name: "should fail if the ServiceTemplates are not found in same namespace", + managedCluster: managedcluster.NewManagedCluster( + managedcluster.WithClusterTemplate(testTemplateName), + managedcluster.WithCredential(testCredentialName), + managedcluster.WithServiceTemplate(testSvcTemplate1Name), + ), + existingObjects: []runtime.Object{ + mgmt, + cred, + template.NewClusterTemplate( + template.WithName(testTemplateName), + template.WithProvidersStatus(v1alpha1.Providers{ + "infrastructure-aws", + "control-plane-k0smotron", + "bootstrap-k0smotron", + }), + template.WithValidationStatus(v1alpha1.TemplateValidationStatus{Valid: true}), + ), + template.NewServiceTemplate( + template.WithName(testSvcTemplate1Name), + template.WithNamespace("othernamespace"), + ), + }, + err: fmt.Sprintf("the ManagedCluster is invalid: servicetemplates.hmc.mirantis.com \"%s\" not found", testSvcTemplate1Name), + }, { name: "should fail if the cluster template was found but is invalid (some validation error)", managedCluster: managedcluster.NewManagedCluster( @@ -116,11 +142,41 @@ func TestManagedClusterValidateCreate(t *testing.T) { }, err: "the ManagedCluster is invalid: the template is not valid: validation error example", }, + { + name: "should fail if the service templates were found but are invalid (some validation error)", + managedCluster: managedcluster.NewManagedCluster( + managedcluster.WithClusterTemplate(testTemplateName), + managedcluster.WithCredential(testCredentialName), + managedcluster.WithServiceTemplate(testSvcTemplate1Name), + ), + existingObjects: []runtime.Object{ + mgmt, + cred, + template.NewClusterTemplate( + template.WithName(testTemplateName), + template.WithProvidersStatus(v1alpha1.Providers{ + "infrastructure-aws", + "control-plane-k0smotron", + "bootstrap-k0smotron", + }), + template.WithValidationStatus(v1alpha1.TemplateValidationStatus{Valid: true}), + ), + template.NewServiceTemplate( + template.WithName(testSvcTemplate1Name), + template.WithValidationStatus(v1alpha1.TemplateValidationStatus{ + Valid: false, + ValidationError: "validation error example", + }), + ), + }, + err: "the ManagedCluster is invalid: the template is not valid: validation error example", + }, { name: "should succeed", managedCluster: managedcluster.NewManagedCluster( managedcluster.WithClusterTemplate(testTemplateName), managedcluster.WithCredential(testCredentialName), + managedcluster.WithServiceTemplate(testSvcTemplate1Name), ), existingObjects: []runtime.Object{ mgmt, @@ -134,6 +190,10 @@ func TestManagedClusterValidateCreate(t *testing.T) { }), template.WithValidationStatus(v1alpha1.TemplateValidationStatus{Valid: true}), ), + template.NewServiceTemplate( + template.WithName(testSvcTemplate1Name), + template.WithValidationStatus(v1alpha1.TemplateValidationStatus{Valid: true}), + ), }, }, { @@ -394,6 +454,148 @@ func TestManagedClusterValidateUpdate(t *testing.T) { ), }, }, + { + name: "should succeed if serviceTemplates are added", + oldManagedCluster: managedcluster.NewManagedCluster( + managedcluster.WithClusterTemplate(testTemplateName), + managedcluster.WithConfig(`{"foo":"bar"}`), + managedcluster.WithCredential(testCredentialName), + ), + newManagedCluster: managedcluster.NewManagedCluster( + managedcluster.WithClusterTemplate(testTemplateName), + managedcluster.WithConfig(`{"a":"b"}`), + managedcluster.WithCredential(testCredentialName), + managedcluster.WithServiceTemplate(testSvcTemplate1Name), + ), + existingObjects: []runtime.Object{ + mgmt, + cred, + template.NewClusterTemplate( + template.WithName(testTemplateName), + template.WithValidationStatus(v1alpha1.TemplateValidationStatus{ + Valid: false, + ValidationError: "validation error example", + }), + template.WithProvidersStatus(v1alpha1.Providers{ + "infrastructure-aws", + "control-plane-k0smotron", + "bootstrap-k0smotron", + }), + ), + template.NewServiceTemplate( + template.WithName(testSvcTemplate1Name), + template.WithValidationStatus(v1alpha1.TemplateValidationStatus{Valid: true}), + ), + }, + }, + { + name: "should succeed if serviceTemplates are removed", + oldManagedCluster: managedcluster.NewManagedCluster( + managedcluster.WithClusterTemplate(testTemplateName), + managedcluster.WithConfig(`{"foo":"bar"}`), + managedcluster.WithCredential(testCredentialName), + managedcluster.WithServiceTemplate(testSvcTemplate1Name), + ), + newManagedCluster: managedcluster.NewManagedCluster( + managedcluster.WithClusterTemplate(testTemplateName), + managedcluster.WithConfig(`{"a":"b"}`), + managedcluster.WithCredential(testCredentialName), + ), + existingObjects: []runtime.Object{ + mgmt, + cred, + template.NewClusterTemplate( + template.WithName(testTemplateName), + template.WithValidationStatus(v1alpha1.TemplateValidationStatus{ + Valid: false, + ValidationError: "validation error example", + }), + template.WithProvidersStatus(v1alpha1.Providers{ + "infrastructure-aws", + "control-plane-k0smotron", + "bootstrap-k0smotron", + }), + ), + template.NewServiceTemplate( + template.WithName(testSvcTemplate1Name), + template.WithValidationStatus(v1alpha1.TemplateValidationStatus{Valid: true}), + ), + }, + }, + { + name: "should fail if serviceTemplates are not in the same namespace", + oldManagedCluster: managedcluster.NewManagedCluster( + managedcluster.WithClusterTemplate(testTemplateName), + managedcluster.WithConfig(`{"foo":"bar"}`), + managedcluster.WithCredential(testCredentialName), + ), + newManagedCluster: managedcluster.NewManagedCluster( + managedcluster.WithClusterTemplate(testTemplateName), + managedcluster.WithConfig(`{"a":"b"}`), + managedcluster.WithCredential(testCredentialName), + managedcluster.WithServiceTemplate(testSvcTemplate1Name), + ), + existingObjects: []runtime.Object{ + mgmt, + cred, + template.NewClusterTemplate( + template.WithName(testTemplateName), + template.WithValidationStatus(v1alpha1.TemplateValidationStatus{ + Valid: false, + ValidationError: "validation error example", + }), + template.WithProvidersStatus(v1alpha1.Providers{ + "infrastructure-aws", + "control-plane-k0smotron", + "bootstrap-k0smotron", + }), + ), + template.NewServiceTemplate( + template.WithName(testSvcTemplate1Name), + template.WithNamespace("othernamespace"), + template.WithValidationStatus(v1alpha1.TemplateValidationStatus{Valid: true}), + ), + }, + err: fmt.Sprintf("the ManagedCluster is invalid: servicetemplates.hmc.mirantis.com \"%s\" not found", testSvcTemplate1Name), + }, + { + name: "should fail if the ServiceTemplates were found but are invalid", + oldManagedCluster: managedcluster.NewManagedCluster( + managedcluster.WithClusterTemplate(testTemplateName), + managedcluster.WithConfig(`{"foo":"bar"}`), + managedcluster.WithCredential(testCredentialName), + ), + newManagedCluster: managedcluster.NewManagedCluster( + managedcluster.WithClusterTemplate(testTemplateName), + managedcluster.WithConfig(`{"a":"b"}`), + managedcluster.WithCredential(testCredentialName), + managedcluster.WithServiceTemplate(testSvcTemplate1Name), + ), + existingObjects: []runtime.Object{ + mgmt, + cred, + template.NewClusterTemplate( + template.WithName(testTemplateName), + template.WithValidationStatus(v1alpha1.TemplateValidationStatus{ + Valid: false, + ValidationError: "validation error example", + }), + template.WithProvidersStatus(v1alpha1.Providers{ + "infrastructure-aws", + "control-plane-k0smotron", + "bootstrap-k0smotron", + }), + ), + template.NewServiceTemplate( + template.WithName(testSvcTemplate1Name), + template.WithValidationStatus(v1alpha1.TemplateValidationStatus{ + Valid: false, + ValidationError: "validation error example", + }), + ), + }, + err: "the ManagedCluster is invalid: the template is not valid: validation error example", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/internal/webhook/multiclusterservice_webhook.go b/internal/webhook/multiclusterservice_webhook.go new file mode 100644 index 00000000..fd84a77b --- /dev/null +++ b/internal/webhook/multiclusterservice_webhook.go @@ -0,0 +1,109 @@ +// Copyright 2024 +// +// 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 webhook + +import ( + "context" + "errors" + "fmt" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + "github.com/Mirantis/hmc/api/v1alpha1" +) + +type MultiClusterServiceValidator struct { + client.Client + SystemNamespace string +} + +const invalidMultiClusterServiceMsg = "the MultiClusterService is invalid" + +// SetupWebhookWithManager will setup the manager to manage the webhooks +func (v *MultiClusterServiceValidator) SetupWebhookWithManager(mgr ctrl.Manager) error { + v.Client = mgr.GetClient() + return ctrl.NewWebhookManagedBy(mgr). + For(&v1alpha1.MultiClusterService{}). + WithValidator(v). + WithDefaulter(v). + Complete() +} + +var ( + _ webhook.CustomValidator = &MultiClusterServiceValidator{} + _ webhook.CustomDefaulter = &MultiClusterServiceValidator{} +) + +// Default implements webhook.Defaulter so a webhook will be registered for the type. +func (*MultiClusterServiceValidator) Default(_ context.Context, _ runtime.Object) error { + return nil +} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type. +func (v *MultiClusterServiceValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + mcs, ok := obj.(*v1alpha1.MultiClusterService) + if !ok { + return nil, apierrors.NewBadRequest(fmt.Sprintf("expected MultiClusterService but got a %T", obj)) + } + + if err := validateServices(ctx, v.Client, v.SystemNamespace, mcs.Spec.Services); err != nil { + return nil, fmt.Errorf("%s: %w", invalidMultiClusterServiceMsg, err) + } + + return nil, nil +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type. +func (v *MultiClusterServiceValidator) ValidateUpdate(ctx context.Context, _, newObj runtime.Object) (admission.Warnings, error) { + mcs, ok := newObj.(*v1alpha1.MultiClusterService) + if !ok { + return nil, apierrors.NewBadRequest(fmt.Sprintf("expected MultiClusterService but got a %T", newObj)) + } + + if err := validateServices(ctx, v.Client, v.SystemNamespace, mcs.Spec.Services); err != nil { + return nil, fmt.Errorf("%s: %w", invalidMultiClusterServiceMsg, err) + } + + return nil, nil +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type. +func (*MultiClusterServiceValidator) ValidateDelete(_ context.Context, _ runtime.Object) (admission.Warnings, error) { + return nil, nil +} + +func getServiceTemplate(ctx context.Context, c client.Client, templateNamespace, templateName string) (tpl *v1alpha1.ServiceTemplate, err error) { + tpl = new(v1alpha1.ServiceTemplate) + return tpl, c.Get(ctx, client.ObjectKey{Namespace: templateNamespace, Name: templateName}, tpl) +} + +func validateServices(ctx context.Context, c client.Client, namespace string, services []v1alpha1.ServiceSpec) (errs error) { + for _, svc := range services { + tpl, err := getServiceTemplate(ctx, c, namespace, svc.Template) + if err != nil { + errs = errors.Join(errs, err) + continue + } + + errs = errors.Join(errs, isTemplateValid(tpl.GetCommonStatus())) + } + + return errs +} diff --git a/internal/webhook/multiclusterservice_webhook_test.go b/internal/webhook/multiclusterservice_webhook_test.go new file mode 100644 index 00000000..38905431 --- /dev/null +++ b/internal/webhook/multiclusterservice_webhook_test.go @@ -0,0 +1,263 @@ +// Copyright 2024 +// +// 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 webhook + +import ( + "context" + "fmt" + "testing" + + . "github.com/onsi/gomega" + admissionv1 "k8s.io/api/admission/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + "github.com/Mirantis/hmc/api/v1alpha1" + "github.com/Mirantis/hmc/test/objects/multiclusterservice" + "github.com/Mirantis/hmc/test/objects/template" + "github.com/Mirantis/hmc/test/scheme" +) + +const ( + testMCSName = "testmcs" + testSvcTemplate1Name = "test-servicetemplate-1" + testSvcTemplate2Name = "test-servicetemplate-2" + testSystemNamespace = "test-system-namespace" +) + +func TestMultiClusterServiceValidateCreate(t *testing.T) { + ctx := admission.NewContextWithRequest(context.Background(), admission.Request{ + AdmissionRequest: admissionv1.AdmissionRequest{ + Operation: admissionv1.Create, + }, + }) + + tests := []struct { + name string + mcs *v1alpha1.MultiClusterService + existingObjects []runtime.Object + err string + warnings admission.Warnings + }{ + { + name: "should fail if the ServiceTemplates are not found in system namespace", + mcs: multiclusterservice.NewMultiClusterService( + multiclusterservice.WithName(testMCSName), + multiclusterservice.WithServiceTemplate(testSvcTemplate1Name), + ), + existingObjects: []runtime.Object{ + template.NewServiceTemplate( + template.WithName(testSvcTemplate1Name), + template.WithNamespace("othernamespace"), + template.WithValidationStatus(v1alpha1.TemplateValidationStatus{Valid: true}), + ), + }, + err: fmt.Sprintf("the MultiClusterService is invalid: servicetemplates.hmc.mirantis.com \"%s\" not found", testSvcTemplate1Name), + }, + { + name: "should fail if the ServiceTemplates were found but are invalid", + mcs: multiclusterservice.NewMultiClusterService( + multiclusterservice.WithName(testMCSName), + multiclusterservice.WithServiceTemplate(testSvcTemplate1Name), + ), + existingObjects: []runtime.Object{ + template.NewServiceTemplate( + template.WithName(testSvcTemplate1Name), + template.WithNamespace(testSystemNamespace), + template.WithValidationStatus(v1alpha1.TemplateValidationStatus{ + Valid: false, + ValidationError: "validation error example", + }), + ), + }, + err: "the MultiClusterService is invalid: the template is not valid: validation error example", + }, + { + name: "should succeed", + mcs: multiclusterservice.NewMultiClusterService( + multiclusterservice.WithName(testMCSName), + multiclusterservice.WithServiceTemplate(testSvcTemplate1Name), + ), + existingObjects: []runtime.Object{ + template.NewServiceTemplate( + template.WithName(testSvcTemplate1Name), + template.WithNamespace(testSystemNamespace), + template.WithValidationStatus(v1alpha1.TemplateValidationStatus{Valid: true}), + ), + }, + }, + { + name: "should succeed with multiple serviceTemplates", + mcs: multiclusterservice.NewMultiClusterService( + multiclusterservice.WithName(testMCSName), + multiclusterservice.WithServiceTemplate(testSvcTemplate1Name), + multiclusterservice.WithServiceTemplate(testSvcTemplate2Name), + ), + existingObjects: []runtime.Object{ + template.NewServiceTemplate( + template.WithName(testSvcTemplate1Name), + template.WithNamespace(testSystemNamespace), + template.WithValidationStatus(v1alpha1.TemplateValidationStatus{Valid: true}), + ), + template.NewServiceTemplate( + template.WithName(testSvcTemplate2Name), + template.WithNamespace(testSystemNamespace), + template.WithValidationStatus(v1alpha1.TemplateValidationStatus{Valid: true}), + ), + }, + }, + { + name: "should succeed without any serviceTemplates", + mcs: multiclusterservice.NewMultiClusterService( + multiclusterservice.WithName(testMCSName), + ), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + c := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithRuntimeObjects(tt.existingObjects...).Build() + validator := &MultiClusterServiceValidator{Client: c, SystemNamespace: testSystemNamespace} + warn, err := validator.ValidateCreate(ctx, tt.mcs) + if tt.err != "" { + g.Expect(err).To(MatchError(tt.err)) + } else { + g.Expect(err).To(Succeed()) + } + + if len(tt.warnings) > 0 { + g.Expect(warn).To(Equal(tt.warnings)) + } else { + g.Expect(warn).To(BeEmpty()) + } + }) + } +} + +func TestMultiClusterServiceValidateUpdate(t *testing.T) { + ctx := admission.NewContextWithRequest(context.Background(), admission.Request{ + AdmissionRequest: admissionv1.AdmissionRequest{ + Operation: admissionv1.Update, + }, + }) + + oldMCS := multiclusterservice.NewMultiClusterService( + multiclusterservice.WithName(testMCSName), + multiclusterservice.WithServiceTemplate(testSvcTemplate1Name), + ) + + tests := []struct { + name string + newMCS *v1alpha1.MultiClusterService + existingObjects []runtime.Object + err string + warnings admission.Warnings + }{ + { + name: "should fail if the ServiceTemplates are not found in system namespace", + newMCS: multiclusterservice.NewMultiClusterService( + multiclusterservice.WithName(testMCSName), + multiclusterservice.WithServiceTemplate(testSvcTemplate1Name), + multiclusterservice.WithServiceTemplate(testSvcTemplate2Name), + ), + existingObjects: []runtime.Object{ + template.NewServiceTemplate( + template.WithName(testSvcTemplate1Name), + template.WithNamespace(testSystemNamespace), + template.WithValidationStatus(v1alpha1.TemplateValidationStatus{Valid: true}), + ), + template.NewServiceTemplate( + template.WithName(testSvcTemplate2Name), + template.WithNamespace("othernamespace"), + template.WithValidationStatus(v1alpha1.TemplateValidationStatus{Valid: true}), + ), + }, + err: fmt.Sprintf("the MultiClusterService is invalid: servicetemplates.hmc.mirantis.com \"%s\" not found", testSvcTemplate2Name), + }, + { + name: "should fail if the ServiceTemplates were found but are invalid", + newMCS: multiclusterservice.NewMultiClusterService( + multiclusterservice.WithName(testMCSName), + multiclusterservice.WithServiceTemplate(testSvcTemplate1Name), + multiclusterservice.WithServiceTemplate(testSvcTemplate2Name), + ), + existingObjects: []runtime.Object{ + template.NewServiceTemplate( + template.WithName(testSvcTemplate1Name), + template.WithNamespace(testSystemNamespace), + template.WithValidationStatus(v1alpha1.TemplateValidationStatus{Valid: true}), + ), + template.NewServiceTemplate( + template.WithName(testSvcTemplate2Name), + template.WithNamespace(testSystemNamespace), + template.WithValidationStatus(v1alpha1.TemplateValidationStatus{ + Valid: false, + ValidationError: "validation error example", + }), + ), + }, + err: "the MultiClusterService is invalid: the template is not valid: validation error example", + }, + { + name: "should succeed if another template is added", + newMCS: multiclusterservice.NewMultiClusterService( + multiclusterservice.WithName(oldMCS.Name), + multiclusterservice.WithServiceTemplate(testSvcTemplate1Name), + multiclusterservice.WithServiceTemplate(testSvcTemplate2Name), + ), + existingObjects: []runtime.Object{ + template.NewServiceTemplate( + template.WithName(testSvcTemplate1Name), + template.WithNamespace(testSystemNamespace), + template.WithValidationStatus(v1alpha1.TemplateValidationStatus{Valid: true}), + ), + template.NewServiceTemplate( + template.WithName(testSvcTemplate2Name), + template.WithNamespace(testSystemNamespace), + template.WithValidationStatus(v1alpha1.TemplateValidationStatus{Valid: true}), + ), + }, + }, + { + name: "should succeed if all templates removed", + newMCS: multiclusterservice.NewMultiClusterService( + multiclusterservice.WithName(oldMCS.Name), + ), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + c := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithRuntimeObjects(tt.existingObjects...).Build() + validator := &MultiClusterServiceValidator{Client: c, SystemNamespace: testSystemNamespace} + warn, err := validator.ValidateUpdate(ctx, oldMCS, tt.newMCS) + if tt.err != "" { + g.Expect(err).To(MatchError(tt.err)) + } else { + g.Expect(err).To(Succeed()) + } + + if len(tt.warnings) > 0 { + g.Expect(warn).To(Equal(tt.warnings)) + } else { + g.Expect(warn).To(BeEmpty()) + } + }) + } +} diff --git a/internal/webhook/template_webhook.go b/internal/webhook/template_webhook.go index b2a0dacd..7f4dda04 100644 --- a/internal/webhook/template_webhook.go +++ b/internal/webhook/template_webhook.go @@ -88,6 +88,7 @@ func (*ClusterTemplateValidator) Default(context.Context, runtime.Object) error type ServiceTemplateValidator struct { client.Client + SystemNamespace string } func (v *ServiceTemplateValidator) SetupWebhookWithManager(mgr ctrl.Manager) error { @@ -133,6 +134,20 @@ func (v *ServiceTemplateValidator) ValidateDelete(ctx context.Context, obj runti return admission.Warnings{"The ServiceTemplate object can't be removed if ManagedCluster objects referencing it still exist"}, errTemplateDeletionForbidden } + // MultiClusterServices can only refer to serviceTemplates in system namespace. + if tmpl.Namespace == v.SystemNamespace { + multiSvcClusters := &v1alpha1.MultiClusterServiceList{} + if err := v.Client.List(ctx, multiSvcClusters, + client.MatchingFields{v1alpha1.ServicesTemplateKey: tmpl.Name}, + client.Limit(1)); err != nil { + return nil, err + } + + if len(multiSvcClusters.Items) > 0 { + return admission.Warnings{"The ServiceTemplate object can't be removed if MultiClusterService objects referencing it still exist"}, errTemplateDeletionForbidden + } + } + return nil, nil } diff --git a/internal/webhook/template_webhook_test.go b/internal/webhook/template_webhook_test.go index 44938a4c..8672d8b3 100644 --- a/internal/webhook/template_webhook_test.go +++ b/internal/webhook/template_webhook_test.go @@ -25,6 +25,7 @@ import ( "github.com/Mirantis/hmc/api/v1alpha1" "github.com/Mirantis/hmc/test/objects/managedcluster" + "github.com/Mirantis/hmc/test/objects/multiclusterservice" "github.com/Mirantis/hmc/test/objects/template" "github.com/Mirantis/hmc/test/scheme" ) @@ -136,6 +137,18 @@ func TestServiceTemplateValidateDelete(t *testing.T) { template: tmpl, existingObjects: []runtime.Object{managedcluster.NewManagedCluster()}, }, + { + title: "should fail if a MultiClusterService is referencing serviceTemplate in system namespace", + template: template.NewServiceTemplate(template.WithNamespace(testSystemNamespace), template.WithName(tmpl.Name)), + existingObjects: []runtime.Object{ + multiclusterservice.NewMultiClusterService( + multiclusterservice.WithName("mymulticlusterservice"), + multiclusterservice.WithServiceTemplate(tmpl.Name), + ), + }, + warnings: admission.Warnings{"The ServiceTemplate object can't be removed if MultiClusterService objects referencing it still exist"}, + err: errTemplateDeletionForbidden.Error(), + }, } for _, tt := range tests { @@ -146,9 +159,10 @@ func TestServiceTemplateValidateDelete(t *testing.T) { NewClientBuilder(). WithScheme(scheme.Scheme). WithRuntimeObjects(tt.existingObjects...). - WithIndex(tt.existingObjects[0], v1alpha1.ServicesTemplateKey, v1alpha1.ExtractServiceTemplateName). + WithIndex(&v1alpha1.ManagedCluster{}, v1alpha1.ServicesTemplateKey, v1alpha1.ExtractServiceTemplateFromManagedCluster). + WithIndex(&v1alpha1.MultiClusterService{}, v1alpha1.ServicesTemplateKey, v1alpha1.ExtractServiceTemplateFromMultiClusterService). Build() - validator := &ServiceTemplateValidator{Client: c} + validator := &ServiceTemplateValidator{Client: c, SystemNamespace: testSystemNamespace} warn, err := validator.ValidateDelete(ctx, tt.template) if tt.err != "" { g.Expect(err).To(MatchError(tt.err)) diff --git a/templates/provider/hmc/templates/crds/hmc.mirantis.com_multiclusterservices.yaml b/templates/provider/hmc/templates/crds/hmc.mirantis.com_multiclusterservices.yaml index 1cbbc344..e15a1e11 100644 --- a/templates/provider/hmc/templates/crds/hmc.mirantis.com_multiclusterservices.yaml +++ b/templates/provider/hmc/templates/crds/hmc.mirantis.com_multiclusterservices.yaml @@ -145,7 +145,7 @@ spec: properties: conditions: description: Conditions contains details for the current state of - the ManagedCluster + the MultiClusterService. items: description: Condition contains details for one aspect of the current state of this API Resource. diff --git a/templates/provider/hmc/templates/webhooks.yaml b/templates/provider/hmc/templates/webhooks.yaml index b9faed01..b0e41200 100644 --- a/templates/provider/hmc/templates/webhooks.yaml +++ b/templates/provider/hmc/templates/webhooks.yaml @@ -81,6 +81,28 @@ webhooks: resources: - managedclusters sideEffects: None + - admissionReviewVersions: + - v1 + - v1beta1 + clientConfig: + service: + name: {{ include "hmc.webhook.serviceName" . }} + namespace: {{ include "hmc.webhook.serviceNamespace" . }} + path: /validate-hmc-mirantis-com-v1alpha1-multiclusterservice + failurePolicy: Fail + matchPolicy: Equivalent + name: validation.multiclusterservice.hmc.mirantis.com + rules: + - apiGroups: + - hmc.mirantis.com + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - multiclusterservices + sideEffects: None - admissionReviewVersions: - v1 - v1beta1 diff --git a/test/objects/multiclusterservice/multiclusterservice.go b/test/objects/multiclusterservice/multiclusterservice.go new file mode 100644 index 00000000..cc6aec97 --- /dev/null +++ b/test/objects/multiclusterservice/multiclusterservice.go @@ -0,0 +1,54 @@ +// Copyright 2024 +// +// 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 multiclusterservice + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/Mirantis/hmc/api/v1alpha1" +) + +const ( + DefaultName = "multiclusterservice" +) + +type Opt func(multiClusterService *v1alpha1.MultiClusterService) + +func NewMultiClusterService(opts ...Opt) *v1alpha1.MultiClusterService { + p := &v1alpha1.MultiClusterService{ + ObjectMeta: metav1.ObjectMeta{ + Name: DefaultName, + }, + } + + for _, opt := range opts { + opt(p) + } + return p +} + +func WithName(name string) Opt { + return func(p *v1alpha1.MultiClusterService) { + p.Name = name + } +} + +func WithServiceTemplate(templateName string) Opt { + return func(p *v1alpha1.MultiClusterService) { + p.Spec.Services = append(p.Spec.Services, v1alpha1.ServiceSpec{ + Template: templateName, + }) + } +}