diff --git a/api/v1alpha1/release_types.go b/api/v1alpha1/release_types.go index a20f1dc73..83e11b78e 100644 --- a/api/v1alpha1/release_types.go +++ b/api/v1alpha1/release_types.go @@ -61,10 +61,10 @@ func (in *Release) ProviderTemplate(name string) string { type ReleaseStatus struct { // Conditions contains details for the current state of the Release Conditions []metav1.Condition `json:"conditions,omitempty"` - // Ready indicates whether HMC is ready to be upgraded to this Release. - Ready bool `json:"ready,omitempty"` // ObservedGeneration is the last observed generation. ObservedGeneration int64 `json:"observedGeneration,omitempty"` + // Ready indicates whether HMC is ready to be upgraded to this Release. + Ready bool `json:"ready,omitempty"` } // +kubebuilder:object:root=true diff --git a/internal/controller/template_controller.go b/internal/controller/template_controller.go index 98374916d..6611a1901 100644 --- a/internal/controller/template_controller.go +++ b/internal/controller/template_controller.go @@ -17,8 +17,12 @@ package controller import ( "context" "encoding/json" + "errors" "fmt" + "slices" + "time" + "github.com/Masterminds/semver/v3" helmcontrollerv2 "github.com/fluxcd/helm-controller/api/v2" sourcev1 "github.com/fluxcd/source-controller/api/v1" "helm.sh/helm/v3/pkg/chart" @@ -26,7 +30,9 @@ import ( apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/predicate" hmc "github.com/Mirantis/hmc/api/v1alpha1" "github.com/Mirantis/hmc/internal/helm" @@ -34,6 +40,8 @@ import ( const ( defaultRepoName = "hmc-templates" + + defaultRequeueTime = 1 * time.Minute ) // TemplateReconciler reconciles a *Template object @@ -73,7 +81,24 @@ func (r *ClusterTemplateReconciler) Reconcile(ctx context.Context, req ctrl.Requ return ctrl.Result{}, err } - return r.ReconcileTemplate(ctx, clusterTemplate) + result, err := r.ReconcileTemplate(ctx, clusterTemplate) + if err != nil { + l.Error(err, "failed to reconcile template") + return result, err + } + + l.Info("Validating template compatibility attributes") + if err := r.validateCompatibilityAttrs(ctx, clusterTemplate); err != nil { + if apierrors.IsNotFound(err) { + l.Info("Validation cannot be performed until Management cluster appears", "requeue in", defaultRequeueTime) + return ctrl.Result{RequeueAfter: defaultRequeueTime}, nil + } + + l.Error(err, "failed to validate compatibility attributes") + return ctrl.Result{}, err + } + + return result, nil } func (r *ServiceTemplateReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { @@ -189,7 +214,7 @@ func (r *TemplateReconciler) ReconcileTemplate(ctx context.Context, template tem } l.Info("Validating Helm chart") - if err = helmChart.Validate(); err != nil { + if err := helmChart.Validate(); err != nil { l.Error(err, "Helm chart validation failed") _ = r.updateStatus(ctx, template, err.Error()) return ctrl.Result{}, err @@ -301,23 +326,115 @@ func (r *TemplateReconciler) getHelmChartFromChartRef(ctx context.Context, chart return helmChart, nil } +func (r *ClusterTemplateReconciler) validateCompatibilityAttrs(ctx context.Context, template *hmc.ClusterTemplate) error { + management := new(hmc.Management) + if err := r.Client.Get(ctx, client.ObjectKey{Name: hmc.ManagementName}, management); err != nil { + if apierrors.IsNotFound(err) { + _ = r.updateStatus(ctx, template, "Waiting for Management creation to complete validation") + return err + } + + err = fmt.Errorf("failed to get Management: %v", err) + _ = r.updateStatus(ctx, template, err.Error()) + return err + } + + exposedProviders, requiredProviders := management.Status.AvailableProviders, template.Status.Providers + + ctrl.LoggerFrom(ctx).V(1).Info("providers to check", "exposed", exposedProviders, "required", requiredProviders) + + var merr error + missing, wrong, parsing := collectMissingProvidersWithWrongVersions("bootstrap", exposedProviders.BootstrapProviders, requiredProviders.BootstrapProviders) + merr = errors.Join(merr, missing, wrong, parsing) + + missing, wrong, parsing = collectMissingProvidersWithWrongVersions("control plane", exposedProviders.ControlPlaneProviders, requiredProviders.ControlPlaneProviders) + merr = errors.Join(merr, missing, wrong, parsing) + + missing, wrong, parsing = collectMissingProvidersWithWrongVersions("infrastructure", exposedProviders.InfrastructureProviders, requiredProviders.InfrastructureProviders) + merr = errors.Join(merr, missing, wrong, parsing) + + if merr != nil { + _ = r.updateStatus(ctx, template, merr.Error()) + return merr + } + + return r.updateStatus(ctx, template, "") +} + +// collectMissingProvidersWithWrongVersions returns collected errors for missing providers, providers with +// wrong versions that do not satisfy the corresponding constraints, and parsing errors respectevly. +func collectMissingProvidersWithWrongVersions(typ string, exposed, required []hmc.ProviderTuple) (missingErr, nonSatisfyingErr, parsingErr error) { + exposedSet := make(map[string]hmc.ProviderTuple, len(exposed)) + for _, v := range exposed { + exposedSet[v.Name] = v + } + + var missing, nonSatisfying []string + for _, reqWithConstraint := range required { + exposedWithExactVer, ok := exposedSet[reqWithConstraint.Name] + if !ok { + missing = append(missing, reqWithConstraint.Name) + continue + } + + version := exposedWithExactVer.VersionOrConstraint + constraint := reqWithConstraint.VersionOrConstraint + + if version == "" || constraint == "" { + continue + } + + exactVer, err := semver.NewVersion(version) + if err != nil { + parsingErr = errors.Join(parsingErr, fmt.Errorf("failed to parse version %s of the provider %s: %w", version, exposedWithExactVer.Name, err)) + continue + } + + requiredC, err := semver.NewConstraint(constraint) + if err != nil { + parsingErr = errors.Join(parsingErr, fmt.Errorf("failed to parse constraint %s of the provider %s: %w", version, exposedWithExactVer.Name, err)) + continue + } + + if !requiredC.Check(exactVer) { + nonSatisfying = append(nonSatisfying, fmt.Sprintf("%s %s !~ %s", reqWithConstraint.Name, version, constraint)) + } + } + + if len(missing) > 0 { + slices.Sort(missing) + missingErr = fmt.Errorf("one or more required %s providers are not deployed yet: %v", typ, missing) + } + + if len(nonSatisfying) > 0 { + slices.Sort(nonSatisfying) + nonSatisfyingErr = fmt.Errorf("one or more required %s providers does not satisfy constraints: %v", typ, nonSatisfying) + } + + if parsingErr != nil { + parsingErr = fmt.Errorf("one or more errors parsing %s providers' versions and constraints : %v", typ, parsingErr) + } + + return missingErr, nonSatisfyingErr, parsingErr +} + // SetupWithManager sets up the controller with the Manager. func (r *ClusterTemplateReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). - For(&hmc.ClusterTemplate{}). + For(&hmc.ClusterTemplate{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})). Complete(r) } // SetupWithManager sets up the controller with the Manager. func (r *ServiceTemplateReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). - For(&hmc.ServiceTemplate{}). + For(&hmc.ServiceTemplate{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})). Complete(r) } // SetupWithManager sets up the controller with the Manager. func (r *ProviderTemplateReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). - For(&hmc.ProviderTemplate{}). + For(&hmc.ProviderTemplate{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})). Complete(r) } diff --git a/internal/controller/template_controller_test.go b/internal/controller/template_controller_test.go index e206ea993..3fec09bbc 100644 --- a/internal/controller/template_controller_test.go +++ b/internal/controller/template_controller_test.go @@ -16,15 +16,18 @@ package controller import ( "context" + "fmt" + "time" helmcontrollerv2 "github.com/fluxcd/helm-controller/api/v2" sourcev1 "github.com/fluxcd/source-controller/api/v1" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "helm.sh/helm/v3/pkg/chart" - "k8s.io/apimachinery/pkg/api/errors" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/reconcile" hmcmirantiscomv1alpha1 "github.com/Mirantis/hmc/api/v1alpha1" @@ -32,11 +35,13 @@ import ( var _ = Describe("Template Controller", func() { Context("When reconciling a resource", func() { - const resourceName = "test-resource" - const helmRepoNamespace = "default" - const helmRepoName = "test-helmrepo" - const helmChartName = "test-helmchart" - const helmChartURL = "http://source-controller.hmc-system.svc.cluster.local./helmchart/hmc-system/test-chart/0.1.0.tar.gz" + const ( + resourceName = "test-resource" + helmRepoNamespace = metav1.NamespaceDefault + helmRepoName = "test-helmrepo" + helmChartName = "test-helmchart" + helmChartURL = "http://source-controller.hmc-system.svc.cluster.local./helmchart/hmc-system/test-chart/0.1.0.tar.gz" + ) fakeDownloadHelmChartFunc := func(context.Context, *sourcev1.Artifact) (*chart.Chart, error) { return &chart.Chart{ @@ -52,7 +57,7 @@ var _ = Describe("Template Controller", func() { typeNamespacedName := types.NamespacedName{ Name: resourceName, - Namespace: "default", + Namespace: metav1.NamespaceDefault, } clusterTemplate := &hmcmirantiscomv1alpha1.ClusterTemplate{} serviceTemplate := &hmcmirantiscomv1alpha1.ServiceTemplate{} @@ -71,7 +76,7 @@ var _ = Describe("Template Controller", func() { BeforeEach(func() { By("creating helm repository") err := k8sClient.Get(ctx, types.NamespacedName{Name: helmRepoName, Namespace: helmRepoNamespace}, helmRepo) - if err != nil && errors.IsNotFound(err) { + if err != nil && apierrors.IsNotFound(err) { helmRepo = &sourcev1.HelmRepository{ ObjectMeta: metav1.ObjectMeta{ Name: helmRepoName, @@ -86,7 +91,7 @@ var _ = Describe("Template Controller", func() { By("creating helm chart") err = k8sClient.Get(ctx, types.NamespacedName{Name: helmChartName, Namespace: helmRepoNamespace}, helmChart) - if err != nil && errors.IsNotFound(err) { + if err != nil && apierrors.IsNotFound(err) { helmChart = &sourcev1.HelmChart{ ObjectMeta: metav1.ObjectMeta{ Name: helmChartName, @@ -112,11 +117,11 @@ var _ = Describe("Template Controller", func() { By("creating the custom resource for the Kind ClusterTemplate") err = k8sClient.Get(ctx, typeNamespacedName, clusterTemplate) - if err != nil && errors.IsNotFound(err) { + if err != nil && apierrors.IsNotFound(err) { resource := &hmcmirantiscomv1alpha1.ClusterTemplate{ ObjectMeta: metav1.ObjectMeta{ Name: resourceName, - Namespace: "default", + Namespace: metav1.NamespaceDefault, }, Spec: hmcmirantiscomv1alpha1.ClusterTemplateSpec{Helm: helmSpec}, } @@ -124,11 +129,11 @@ var _ = Describe("Template Controller", func() { } By("creating the custom resource for the Kind ServiceTemplate") err = k8sClient.Get(ctx, typeNamespacedName, serviceTemplate) - if err != nil && errors.IsNotFound(err) { + if err != nil && apierrors.IsNotFound(err) { resource := &hmcmirantiscomv1alpha1.ServiceTemplate{ ObjectMeta: metav1.ObjectMeta{ Name: resourceName, - Namespace: "default", + Namespace: metav1.NamespaceDefault, }, Spec: hmcmirantiscomv1alpha1.ServiceTemplateSpec{Helm: helmSpec}, } @@ -136,11 +141,10 @@ var _ = Describe("Template Controller", func() { } By("creating the custom resource for the Kind ProviderTemplate") err = k8sClient.Get(ctx, typeNamespacedName, providerTemplate) - if err != nil && errors.IsNotFound(err) { + if err != nil && apierrors.IsNotFound(err) { resource := &hmcmirantiscomv1alpha1.ProviderTemplate{ ObjectMeta: metav1.ObjectMeta{ - Name: resourceName, - Namespace: "default", + Name: resourceName, }, Spec: hmcmirantiscomv1alpha1.ProviderTemplateSpec{Helm: helmSpec}, } @@ -170,6 +174,7 @@ var _ = Describe("Template Controller", func() { By("Cleanup the specific resource instance ClusterTemplate") Expect(k8sClient.Delete(ctx, providerTemplateResource)).To(Succeed()) }) + It("should successfully reconcile the resource", func() { templateReconciler := TemplateReconciler{ Client: k8sClient, @@ -190,5 +195,124 @@ var _ = Describe("Template Controller", func() { _, err = providerTemplateReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) }) + + It("should successfully validate cluster templates providers compatibility attributes", func() { + const ( + clusterTemplateName = "cluster-template-test-name" + mgmtName = hmcmirantiscomv1alpha1.ManagementName + someProviderName = "test-provider-name" + someProviderVersion = "v1.0.0" + someProviderVersionConstraint = ">= 1.0.0 <2.0.0-0" // ^1.0.0 + + timeout = time.Second * 10 + interval = time.Millisecond * 250 + ) + + // NOTE: the cluster template from BeforeEach cannot be reused because spec is immutable + By("Creating cluster template with constrained versions") + clusterTemplate = &hmcmirantiscomv1alpha1.ClusterTemplate{ + ObjectMeta: metav1.ObjectMeta{ + Name: clusterTemplateName, + Namespace: metav1.NamespaceDefault, + }, + Spec: hmcmirantiscomv1alpha1.ClusterTemplateSpec{ + Helm: helmSpec, + Providers: hmcmirantiscomv1alpha1.ProvidersTupled{ + BootstrapProviders: []hmcmirantiscomv1alpha1.ProviderTuple{ + { + Name: someProviderName, + VersionOrConstraint: someProviderVersionConstraint, // constraint + }, + }, + }, + }, + } + Expect(k8sClient.Create(ctx, clusterTemplate)).To(Succeed()) + + By("Checking the cluster template has been updated") + Eventually(func() error { + if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(clusterTemplate), clusterTemplate); err != nil { + return err + } + + if l := len(clusterTemplate.Spec.Providers.BootstrapProviders); l != 1 { + return fmt.Errorf("expected .spec.providers.bootstrapProviders length to be exactly 1, got %d", l) + } + + if v := clusterTemplate.Spec.Providers.BootstrapProviders[0]; v.Name != someProviderName || v.VersionOrConstraint != someProviderVersionConstraint { + return fmt.Errorf("expected .spec.providers.bootstrapProviders[0] to be %s:%s, got %s:%s", someProviderName, someProviderVersionConstraint, v.Name, v.VersionOrConstraint) + } + + return nil + }).WithTimeout(timeout).WithPolling(interval).Should(Succeed()) + + By("Creating a management cluster object with proper required versions in status") + // must set status here since it's controller by another ctrl + mgmt := &hmcmirantiscomv1alpha1.Management{ + ObjectMeta: metav1.ObjectMeta{ + Name: mgmtName, + }, + } + Expect(k8sClient.Create(ctx, mgmt)).To(Succeed()) + mgmt.Status = hmcmirantiscomv1alpha1.ManagementStatus{ + AvailableProviders: hmcmirantiscomv1alpha1.ProvidersTupled{ + BootstrapProviders: []hmcmirantiscomv1alpha1.ProviderTuple{ + { + Name: someProviderName, + VersionOrConstraint: someProviderVersion, // version + }, + }, + }, + } + Expect(k8sClient.Status().Update(ctx, mgmt)).To(Succeed()) + + By("Checking the management cluster appears") + Eventually(func() error { + if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(mgmt), mgmt); err != nil { + return err + } + + if l := len(mgmt.Status.AvailableProviders.BootstrapProviders); l != 1 { + return fmt.Errorf("expected .status.availableProviders.bootstrapProviders length to be exactly 1, got %d", l) + } + + if l := len(mgmt.Status.AvailableProviders.BootstrapProviders); l != 1 { + return fmt.Errorf("expected .status.availableProviders.bootstrapProviders length to be exactly 1, got %d", l) + } + + if v := mgmt.Status.AvailableProviders.BootstrapProviders[0]; v.Name != someProviderName || v.VersionOrConstraint != someProviderVersion { + return fmt.Errorf("expected .status.availableProviders.bootstrapProviders[0] to be %s:%s, got %s:%s", someProviderName, someProviderVersionConstraint, v.Name, v.VersionOrConstraint) + } + + return nil + }).WithTimeout(timeout).WithPolling(interval).Should(Succeed()) + + By("Reconciling the cluster template") + clusterTemplateReconciler := &ClusterTemplateReconciler{TemplateReconciler: TemplateReconciler{ + Client: k8sClient, + downloadHelmChartFunc: fakeDownloadHelmChartFunc, + }} + _, err := clusterTemplateReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{ + Name: clusterTemplateName, + Namespace: metav1.NamespaceDefault, + }}) + Expect(err).NotTo(HaveOccurred()) + + 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.Providers.BootstrapProviders).To(HaveLen(1)) + Expect(clusterTemplate.Status.Providers.BootstrapProviders[0]).To(Equal(hmcmirantiscomv1alpha1.ProviderTuple{Name: someProviderName, VersionOrConstraint: someProviderVersionConstraint})) + + By("Removing the created objects") + Expect(k8sClient.Delete(ctx, mgmt)).To(Succeed()) + Expect(k8sClient.Delete(ctx, clusterTemplate)).To(Succeed()) + + By("Checking the created objects have been removed") + Eventually(func() bool { + return apierrors.IsNotFound(k8sClient.Get(ctx, client.ObjectKeyFromObject(mgmt), &hmcmirantiscomv1alpha1.Management{})) && + apierrors.IsNotFound(k8sClient.Get(ctx, client.ObjectKeyFromObject(clusterTemplate), &hmcmirantiscomv1alpha1.ClusterTemplate{})) + }).WithTimeout(timeout).WithPolling(interval).Should(BeTrue()) + }) }) }) diff --git a/internal/webhook/managedcluster_webhook.go b/internal/webhook/managedcluster_webhook.go index 811765286..21d57237b 100644 --- a/internal/webhook/managedcluster_webhook.go +++ b/internal/webhook/managedcluster_webhook.go @@ -16,10 +16,7 @@ package webhook import ( "context" - "errors" "fmt" - "slices" - "sort" "github.com/Masterminds/semver/v3" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" @@ -65,7 +62,7 @@ func (v *ManagedClusterValidator) ValidateCreate(ctx context.Context, obj runtim return nil, fmt.Errorf("%s: %v", invalidManagedClusterMsg, err) } - if err := v.isTemplateValid(ctx, template); err != nil { + if err := isTemplateValid(template); err != nil { return nil, fmt.Errorf("%s: %v", invalidManagedClusterMsg, err) } @@ -88,7 +85,7 @@ func (v *ManagedClusterValidator) ValidateUpdate(ctx context.Context, _ runtime. return nil, fmt.Errorf("%s: %v", invalidManagedClusterMsg, err) } - if err := v.isTemplateValid(ctx, template); err != nil { + if err := isTemplateValid(template); err != nil { return nil, fmt.Errorf("%s: %v", invalidManagedClusterMsg, err) } @@ -104,16 +101,6 @@ func validateK8sCompatibility(ctx context.Context, cl client.Client, template *h return nil // nothing to do } - svcTpls := new(hmcv1alpha1.ServiceTemplateList) - if err := cl.List(ctx, svcTpls, client.InNamespace(mc.Namespace)); err != nil { - return fmt.Errorf("failed to list ServiceTemplates in %s namespace: %w", mc.Namespace, err) - } - - svcTplName2KConstraint := make(map[string]string, len(svcTpls.Items)) - for _, v := range svcTpls.Items { - svcTplName2KConstraint[v.Name] = v.Status.KubernetesConstraint - } - mcVersion, err := semver.NewVersion(template.Status.KubernetesVersion) if err != nil { // should never happen return fmt.Errorf("failed to parse k8s version %s of the ManagedCluster %s/%s: %w", template.Status.KubernetesVersion, mc.Namespace, mc.Name, err) @@ -124,24 +111,25 @@ func validateK8sCompatibility(ctx context.Context, cl client.Client, template *h continue } - kc, ok := svcTplName2KConstraint[v.Template] - if !ok { - return fmt.Errorf("specified ServiceTemplate %s/%s is missing in the cluster", mc.Namespace, v.Template) + svcTpl := new(hmcv1alpha1.ServiceTemplate) + if err := cl.Get(ctx, client.ObjectKey{Namespace: mc.Namespace, Name: v.Template}, svcTpl); err != nil { + return fmt.Errorf("failed to get ServiceTemplate %s/%s: %w", mc.Namespace, v.Template, err) } - if kc == "" { + constraint := svcTpl.Status.KubernetesConstraint + if constraint == "" { continue } - tplConstraint, err := semver.NewConstraint(kc) + tplConstraint, err := semver.NewConstraint(constraint) if err != nil { // should never happen - return fmt.Errorf("failed to parse k8s constrained version %s of the ServiceTemplate %s/%s: %w", kc, mc.Namespace, v.Template, err) + return fmt.Errorf("failed to parse k8s constrained version %s of the ServiceTemplate %s/%s: %w", constraint, mc.Namespace, v.Template, err) } if !tplConstraint.Check(mcVersion) { return fmt.Errorf("k8s version %s of the ManagedCluster %s/%s does not satisfy constrained version %s from the ServiceTemplate %s/%s", template.Status.KubernetesVersion, mc.Namespace, mc.Name, - kc, mc.Namespace, v.Template) + constraint, mc.Namespace, v.Template) } } @@ -171,7 +159,7 @@ func (v *ManagedClusterValidator) Default(ctx context.Context, obj runtime.Objec return fmt.Errorf("could not get template for the managedcluster: %v", err) } - if err := v.isTemplateValid(ctx, template); err != nil { + if err := isTemplateValid(template); err != nil { return fmt.Errorf("template is invalid: %v", err) } @@ -190,111 +178,10 @@ func (v *ManagedClusterValidator) getManagedClusterTemplate(ctx context.Context, return tpl, v.Get(ctx, client.ObjectKey{Namespace: templateNamespace, Name: templateName}, tpl) } -func (v *ManagedClusterValidator) isTemplateValid(ctx context.Context, template *hmcv1alpha1.ClusterTemplate) error { +func isTemplateValid(template *hmcv1alpha1.ClusterTemplate) error { if !template.Status.Valid { return fmt.Errorf("the template is not valid: %s", template.Status.ValidationError) } - if err := v.verifyProviders(ctx, template); err != nil { - return fmt.Errorf("failed to verify providers: %v", err) - } - - return nil -} - -func (v *ManagedClusterValidator) verifyProviders(ctx context.Context, template *hmcv1alpha1.ClusterTemplate) error { - management := new(hmcv1alpha1.Management) - if err := v.Get(ctx, client.ObjectKey{Name: hmcv1alpha1.ManagementName}, management); err != nil { - return err - } - - const ( - bootstrapProviderType = "bootstrap" - controlPlateProviderType = "control plane" - infraProviderType = "infrastructure" - ) - - var ( - exposedProviders = management.Status.AvailableProviders - requiredProviders = template.Status.Providers - wrongVersionProviders, missingProviders = make(map[string][]string, 3), make(map[string][]string, 3) - - err error - ) - - missingProviders[bootstrapProviderType], wrongVersionProviders[bootstrapProviderType], err = getMissingProvidersWithWrongVersions(exposedProviders.BootstrapProviders, requiredProviders.BootstrapProviders) - if err != nil { - return err - } - - missingProviders[controlPlateProviderType], wrongVersionProviders[controlPlateProviderType], err = getMissingProvidersWithWrongVersions(exposedProviders.ControlPlaneProviders, requiredProviders.ControlPlaneProviders) - if err != nil { - return err - } - - missingProviders[infraProviderType], wrongVersionProviders[infraProviderType], err = getMissingProvidersWithWrongVersions(exposedProviders.InfrastructureProviders, requiredProviders.InfrastructureProviders) - if err != nil { - return err - } - - errs := collectErrors(missingProviders, "one or more required %s providers are not deployed yet: %v") - errs = append(errs, collectErrors(wrongVersionProviders, "one or more required %s providers does not satisfy constraints: %v")...) - if len(errs) > 0 { - sort.Slice(errs, func(i, j int) bool { - return errs[i].Error() < errs[j].Error() - }) - - return errors.Join(errs...) - } - return nil } - -func collectErrors(m map[string][]string, msgFormat string) (errs []error) { - for providerType, missing := range m { - if len(missing) > 0 { - slices.Sort(missing) - errs = append(errs, fmt.Errorf(msgFormat, providerType, missing)) - } - } - - return errs -} - -func getMissingProvidersWithWrongVersions(exposed, required []hmcv1alpha1.ProviderTuple) (missing, nonSatisfying []string, _ error) { - exposedSet := make(map[string]hmcv1alpha1.ProviderTuple, len(exposed)) - for _, v := range exposed { - exposedSet[v.Name] = v - } - - var merr error - for _, reqWithConstraint := range required { - exposedWithExactVer, ok := exposedSet[reqWithConstraint.Name] - if !ok { - missing = append(missing, reqWithConstraint.Name) - continue - } - - if exposedWithExactVer.VersionOrConstraint == "" || reqWithConstraint.VersionOrConstraint == "" { - continue - } - - exactVer, err := semver.NewVersion(exposedWithExactVer.VersionOrConstraint) - if err != nil { - merr = errors.Join(merr, fmt.Errorf("failed to parse version %s of the provider %s: %w", exposedWithExactVer.VersionOrConstraint, exposedWithExactVer.Name, err)) - continue - } - - requiredC, err := semver.NewConstraint(reqWithConstraint.VersionOrConstraint) - if err != nil { - merr = errors.Join(merr, fmt.Errorf("failed to parse constraint %s of the provider %s: %w", exposedWithExactVer.VersionOrConstraint, exposedWithExactVer.Name, err)) - continue - } - - if !requiredC.Check(exactVer) { - nonSatisfying = append(nonSatisfying, fmt.Sprintf("%s %s !~ %s", reqWithConstraint.Name, exposedWithExactVer.VersionOrConstraint, reqWithConstraint.VersionOrConstraint)) - } - } - - return missing, nonSatisfying, merr -} diff --git a/internal/webhook/managedcluster_webhook_test.go b/internal/webhook/managedcluster_webhook_test.go index 82c833dca..b1157f1c3 100644 --- a/internal/webhook/managedcluster_webhook_test.go +++ b/internal/webhook/managedcluster_webhook_test.go @@ -83,28 +83,6 @@ var ( }, err: "the ManagedCluster is invalid: the template is not valid: validation error example", }, - { - name: "should fail if one or more requested providers are not available yet", - managedCluster: managedcluster.NewManagedCluster(managedcluster.WithClusterTemplate(testTemplateName)), - existingObjects: []runtime.Object{ - management.NewManagement( - management.WithAvailableProviders(v1alpha1.ProvidersTupled{ - InfrastructureProviders: []v1alpha1.ProviderTuple{{Name: "aws"}}, - BootstrapProviders: []v1alpha1.ProviderTuple{{Name: "k0s"}}, - }), - ), - template.NewClusterTemplate( - template.WithName(testTemplateName), - template.WithProvidersStatus(v1alpha1.ProvidersTupled{ - InfrastructureProviders: []v1alpha1.ProviderTuple{{Name: "azure"}}, - BootstrapProviders: []v1alpha1.ProviderTuple{{Name: "k0s"}}, - ControlPlaneProviders: []v1alpha1.ProviderTuple{{Name: "k0s"}}, - }), - template.WithValidationStatus(v1alpha1.TemplateValidationStatus{Valid: true}), - ), - }, - err: "the ManagedCluster is invalid: failed to verify providers: one or more required control plane providers are not deployed yet: [k0s]\none or more required infrastructure providers are not deployed yet: [azure]", - }, { name: "should succeed", managedCluster: managedcluster.NewManagedCluster(managedcluster.WithClusterTemplate(testTemplateName)), @@ -112,37 +90,9 @@ var ( mgmt, template.NewClusterTemplate( template.WithName(testTemplateName), - template.WithProvidersStatus(v1alpha1.ProvidersTupled{ - InfrastructureProviders: []v1alpha1.ProviderTuple{{Name: "aws"}}, - BootstrapProviders: []v1alpha1.ProviderTuple{{Name: "k0s"}}, - ControlPlaneProviders: []v1alpha1.ProviderTuple{{Name: "k0s"}}, - }), - template.WithValidationStatus(v1alpha1.TemplateValidationStatus{Valid: true}), - ), - }, - }, - { - name: "provider template versions does not satisfy cluster template constraints", - managedCluster: managedcluster.NewManagedCluster(managedcluster.WithClusterTemplate(testTemplateName)), - existingObjects: []runtime.Object{ - management.NewManagement(management.WithAvailableProviders(v1alpha1.ProvidersTupled{ - InfrastructureProviders: []v1alpha1.ProviderTuple{{Name: "aws", VersionOrConstraint: "v1.0.0"}}, - BootstrapProviders: []v1alpha1.ProviderTuple{{Name: "k0s", VersionOrConstraint: "v1.0.0"}}, - ControlPlaneProviders: []v1alpha1.ProviderTuple{{Name: "k0s", VersionOrConstraint: "v1.0.0"}}, - })), - template.NewClusterTemplate( - template.WithName(testTemplateName), - template.WithProvidersStatus(v1alpha1.ProvidersTupled{ - InfrastructureProviders: []v1alpha1.ProviderTuple{{Name: "aws", VersionOrConstraint: ">=999.0.0"}}, - BootstrapProviders: []v1alpha1.ProviderTuple{{Name: "k0s", VersionOrConstraint: ">=999.0.0"}}, - ControlPlaneProviders: []v1alpha1.ProviderTuple{{Name: "k0s", VersionOrConstraint: ">=999.0.0"}}, - }), template.WithValidationStatus(v1alpha1.TemplateValidationStatus{Valid: true}), ), }, - err: `the ManagedCluster is invalid: failed to verify providers: one or more required bootstrap providers does not satisfy constraints: [k0s v1.0.0 !~ >=999.0.0] -one or more required control plane providers does not satisfy constraints: [k0s v1.0.0 !~ >=999.0.0] -one or more required infrastructure providers does not satisfy constraints: [aws v1.0.0 !~ >=999.0.0]`, }, { name: "cluster template k8s version does not satisfy service template constraints", @@ -158,21 +108,11 @@ one or more required infrastructure providers does not satisfy constraints: [aws })), template.NewClusterTemplate( template.WithName(testTemplateName), - template.WithProvidersStatus(v1alpha1.ProvidersTupled{ - InfrastructureProviders: []v1alpha1.ProviderTuple{{Name: "aws"}}, - BootstrapProviders: []v1alpha1.ProviderTuple{{Name: "k0s"}}, - ControlPlaneProviders: []v1alpha1.ProviderTuple{{Name: "k0s"}}, - }), template.WithValidationStatus(v1alpha1.TemplateValidationStatus{Valid: true}), template.WithClusterStatusK8sVersion("v1.30.0"), ), template.NewServiceTemplate( template.WithName(testTemplateName), - template.WithProvidersStatus(v1alpha1.Providers{ - InfrastructureProviders: []string{"aws"}, - BootstrapProviders: []string{"k0s"}, - ControlPlaneProviders: []string{"k0s"}, - }), template.WithServiceK8sConstraint("<1.30"), template.WithValidationStatus(v1alpha1.TemplateValidationStatus{Valid: true}), ),