From 9c8a9e354e8128e191feb4f9de3ef674857a86a1 Mon Sep 17 00:00:00 2001 From: Ekaterina Kazakova Date: Tue, 24 Sep 2024 15:32:36 +0400 Subject: [PATCH] Validate available upgrades for managed clusters Closes #372 --- api/v1alpha1/clustertemplatechain_types.go | 4 + api/v1alpha1/servicetemplatechain_types.go | 4 + api/v1alpha1/templates_common.go | 3 + internal/controller/template_controller.go | 22 +-- .../templatemanagement_controller.go | 14 +- internal/templateutil/interface.go | 32 ++++ internal/templateutil/state.go | 149 +++++++++++------ internal/webhook/managedcluster_webhook.go | 26 ++- .../webhook/managedcluster_webhook_test.go | 156 +++++++++++++----- test/objects/template/template.go | 9 + 10 files changed, 308 insertions(+), 111 deletions(-) create mode 100644 internal/templateutil/interface.go diff --git a/api/v1alpha1/clustertemplatechain_types.go b/api/v1alpha1/clustertemplatechain_types.go index f20b4bf5..c14a03b0 100644 --- a/api/v1alpha1/clustertemplatechain_types.go +++ b/api/v1alpha1/clustertemplatechain_types.go @@ -18,6 +18,10 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +func (c *ClusterTemplateChain) GetSupportedTemplates() []SupportedTemplate { + return c.Spec.SupportedTemplates +} + // +kubebuilder:object:root=true // +kubebuilder:resource:scope=Cluster diff --git a/api/v1alpha1/servicetemplatechain_types.go b/api/v1alpha1/servicetemplatechain_types.go index 96eb4eb0..45c0db69 100644 --- a/api/v1alpha1/servicetemplatechain_types.go +++ b/api/v1alpha1/servicetemplatechain_types.go @@ -18,6 +18,10 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +func (c *ServiceTemplateChain) GetSupportedTemplates() []SupportedTemplate { + return c.Spec.SupportedTemplates +} + // +kubebuilder:object:root=true // +kubebuilder:resource:scope=Cluster diff --git a/api/v1alpha1/templates_common.go b/api/v1alpha1/templates_common.go index 98bd9ccf..1282953b 100644 --- a/api/v1alpha1/templates_common.go +++ b/api/v1alpha1/templates_common.go @@ -26,6 +26,9 @@ const ( ChartAnnotationBootstrapProviders = "hmc.mirantis.com/bootstrap-providers" // ChartAnnotationControlPlaneProviders is an annotation containing the CAPI control plane providers associated with Template. ChartAnnotationControlPlaneProviders = "hmc.mirantis.com/control-plane-providers" + + ClusterTemplateKind string = "ClusterTemplate" + ServiceTemplateKind string = "ServiceTemplate" ) // TemplateSpecCommon is a Template configuration common for all Template types diff --git a/internal/controller/template_controller.go b/internal/controller/template_controller.go index b2171e88..56465a4b 100644 --- a/internal/controller/template_controller.go +++ b/internal/controller/template_controller.go @@ -36,6 +36,7 @@ import ( hmc "github.com/Mirantis/hmc/api/v1alpha1" "github.com/Mirantis/hmc/internal/helm" + "github.com/Mirantis/hmc/internal/templateutil" ) const ( @@ -123,14 +124,7 @@ func (r *ProviderTemplateReconciler) Reconcile(ctx context.Context, req ctrl.Req return r.ReconcileTemplate(ctx, providerTemplate) } -// Template is the interface defining a list of methods to interact with templates -type Template interface { - client.Object - GetSpec() *hmc.TemplateSpecCommon - GetStatus() *hmc.TemplateStatusCommon -} - -func (r *TemplateReconciler) ReconcileTemplate(ctx context.Context, template Template) (ctrl.Result, error) { +func (r *TemplateReconciler) ReconcileTemplate(ctx context.Context, template templateutil.Template) (ctrl.Result, error) { l := log.FromContext(ctx) spec := template.GetSpec() @@ -149,7 +143,7 @@ func (r *TemplateReconciler) ReconcileTemplate(ctx context.Context, template Tem l.Error(err, "invalid helm chart reference") return ctrl.Result{}, err } - if template.GetNamespace() == r.SystemNamespace || !templateManagedByHMC(template) { + if template.GetNamespace() == r.SystemNamespace || !templateutil.IsManagedByHMC(template) { err := r.reconcileDefaultHelmRepository(ctx, template.GetNamespace()) if err != nil { l.Error(err, "Failed to reconcile default HelmRepository", "namespace", template.GetNamespace()) @@ -222,11 +216,7 @@ func (r *TemplateReconciler) ReconcileTemplate(ctx context.Context, template Tem return ctrl.Result{}, r.updateStatus(ctx, template, "") } -func templateManagedByHMC(template Template) bool { - return template.GetLabels()[hmc.HMCManagedLabelKey] == hmc.HMCManagedLabelValue -} - -func parseChartMetadata(template Template, inChart *chart.Chart) error { +func parseChartMetadata(template templateutil.Template, inChart *chart.Chart) error { if inChart.Metadata == nil { return fmt.Errorf("chart metadata is empty") } @@ -261,7 +251,7 @@ func parseChartMetadata(template Template, inChart *chart.Chart) error { return nil } -func (r *TemplateReconciler) updateStatus(ctx context.Context, template Template, validationError string) error { +func (r *TemplateReconciler) updateStatus(ctx context.Context, template templateutil.Template, validationError string) error { status := template.GetStatus() status.ObservedGeneration = template.GetGeneration() status.ValidationError = validationError @@ -312,7 +302,7 @@ func (r *TemplateReconciler) reconcileDefaultHelmRepository(ctx context.Context, return nil } -func (r *TemplateReconciler) reconcileHelmChart(ctx context.Context, template Template) (*sourcev1.HelmChart, error) { +func (r *TemplateReconciler) reconcileHelmChart(ctx context.Context, template templateutil.Template) (*sourcev1.HelmChart, error) { spec := template.GetSpec() namespace := template.GetNamespace() if namespace == "" { diff --git a/internal/controller/templatemanagement_controller.go b/internal/controller/templatemanagement_controller.go index 745b6464..ca9b133f 100644 --- a/internal/controller/templatemanagement_controller.go +++ b/internal/controller/templatemanagement_controller.go @@ -75,11 +75,11 @@ func (r *TemplateManagementReconciler) Reconcile(ctx context.Context, req ctrl.R } var errs error - err = r.distributeTemplates(ctx, expectedState.ClusterTemplatesState, templateutil.ClusterTemplateKind) + err = r.distributeTemplates(ctx, expectedState.ClusterTemplatesState, hmc.ClusterTemplateKind) if err != nil { errs = errors.Join(errs, err) } - err = r.distributeTemplates(ctx, expectedState.ServiceTemplatesState, templateutil.ServiceTemplateKind) + err = r.distributeTemplates(ctx, expectedState.ServiceTemplatesState, hmc.ServiceTemplateKind) if err != nil { errs = errors.Join(errs, err) } @@ -125,7 +125,7 @@ func (r *TemplateManagementReconciler) applyTemplates(ctx context.Context, kind chartName := "" switch kind { - case templateutil.ClusterTemplateKind: + case hmc.ClusterTemplateKind: source := &hmc.ClusterTemplate{} err := r.Get(ctx, client.ObjectKey{ Namespace: r.SystemNamespace, @@ -137,7 +137,7 @@ func (r *TemplateManagementReconciler) applyTemplates(ctx context.Context, kind } else if !apierrors.IsNotFound(err) { return err } - case templateutil.ServiceTemplateKind: + case hmc.ServiceTemplateKind: source := &hmc.ServiceTemplate{} err := r.Get(ctx, client.ObjectKey{ Namespace: r.SystemNamespace, @@ -150,7 +150,7 @@ func (r *TemplateManagementReconciler) applyTemplates(ctx context.Context, kind return err } default: - return fmt.Errorf("invalid kind %s. Only %s or %s kinds are supported", kind, templateutil.ClusterTemplateKind, templateutil.ServiceTemplateKind) + return fmt.Errorf("invalid kind %s. Only %s or %s kinds are supported", kind, hmc.ClusterTemplateKind, hmc.ServiceTemplateKind) } spec := hmc.TemplateSpecCommon{ @@ -166,10 +166,10 @@ func (r *TemplateManagementReconciler) applyTemplates(ctx context.Context, kind for ns, keep := range namespaces { var target client.Object meta.Namespace = ns - if kind == templateutil.ClusterTemplateKind { + if kind == hmc.ClusterTemplateKind { target = &hmc.ClusterTemplate{ObjectMeta: meta, Spec: hmc.ClusterTemplateSpec{TemplateSpecCommon: spec}} } - if kind == templateutil.ServiceTemplateKind { + if kind == hmc.ServiceTemplateKind { target = &hmc.ServiceTemplate{ObjectMeta: meta, Spec: hmc.ServiceTemplateSpec{TemplateSpecCommon: spec}} } if keep { diff --git a/internal/templateutil/interface.go b/internal/templateutil/interface.go new file mode 100644 index 00000000..a5be15b4 --- /dev/null +++ b/internal/templateutil/interface.go @@ -0,0 +1,32 @@ +// 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 templateutil + +import ( + "sigs.k8s.io/controller-runtime/pkg/client" + + hmc "github.com/Mirantis/hmc/api/v1alpha1" +) + +// Template is the interface defining a list of methods to interact with templates +type Template interface { + client.Object + GetSpec() *hmc.TemplateSpecCommon + GetStatus() *hmc.TemplateStatusCommon +} + +func IsManagedByHMC(template Template) bool { + return template.GetLabels()[hmc.HMCManagedLabelKey] == hmc.HMCManagedLabelValue +} diff --git a/internal/templateutil/state.go b/internal/templateutil/state.go index 279dd039..8844d93b 100644 --- a/internal/templateutil/state.go +++ b/internal/templateutil/state.go @@ -16,24 +16,18 @@ package templateutil import ( "context" - "errors" "fmt" + "slices" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" hmc "github.com/Mirantis/hmc/api/v1alpha1" ) -const ( - ClusterTemplateKind = "ClusterTemplate" - ServiceTemplateKind = "ServiceTemplate" -) - type State struct { // ClusterTemplatesState is a map where keys are ClusterTemplate names and values is the map of namespaces // where this ClusterTemplate should be distributed @@ -50,7 +44,7 @@ func GetCurrentTemplatesState(ctx context.Context, cl client.Client, systemNames } clusterTemplatesList, serviceTemplatesList := &metav1.PartialObjectMetadataList{}, &metav1.PartialObjectMetadataList{} - for _, kind := range []string{ClusterTemplateKind, ServiceTemplateKind} { + for _, kind := range []string{hmc.ClusterTemplateKind, hmc.ServiceTemplateKind} { partialList := &metav1.PartialObjectMetadataList{} partialList.SetGroupVersionKind(schema.GroupVersionKind{ Group: hmc.GroupVersion.Group, @@ -61,10 +55,10 @@ func GetCurrentTemplatesState(ctx context.Context, cl client.Client, systemNames if err != nil { return State{}, err } - if kind == ClusterTemplateKind { + if kind == hmc.ClusterTemplateKind { clusterTemplatesList = partialList } - if kind == ServiceTemplateKind { + if kind == hmc.ServiceTemplateKind { serviceTemplatesList = partialList } } @@ -104,53 +98,33 @@ func ParseAccessRules(ctx context.Context, cl client.Client, rules []hmc.AccessR if expectedState.ServiceTemplatesState == nil { expectedState.ServiceTemplatesState = make(map[string]map[string]bool) } + ctChains, err := getTemplateChains(ctx, cl, hmc.ClusterTemplateKind) + if err != nil { + return State{}, err + } + stChains, err := getTemplateChains(ctx, cl, hmc.ServiceTemplateKind) + if err != nil { + return State{}, err + } for _, rule := range rules { - var clusterTemplates []string - var serviceTemplates []string - for _, ctChainName := range rule.ClusterTemplateChains { - ctChain := &hmc.ClusterTemplateChain{} - err := cl.Get(ctx, types.NamespacedName{ - Name: ctChainName, - }, ctChain) - if err != nil { - errs = errors.Join(errs, err) - continue - } - for _, supportedTemplate := range ctChain.Spec.SupportedTemplates { - clusterTemplates = append(clusterTemplates, supportedTemplate.Name) - } - } - for _, stChainName := range rule.ServiceTemplateChains { - stChain := &hmc.ServiceTemplateChain{} - err := cl.Get(ctx, types.NamespacedName{ - Name: stChainName, - }, stChain) - if err != nil { - errs = errors.Join(errs, err) - continue - } - for _, supportedTemplate := range stChain.Spec.SupportedTemplates { - serviceTemplates = append(serviceTemplates, supportedTemplate.Name) - } - } namespaces, err := getTargetNamespaces(ctx, cl, rule.TargetNamespaces) if err != nil { return State{}, err } - for _, ct := range clusterTemplates { - if expectedState.ClusterTemplatesState[ct] == nil { - expectedState.ClusterTemplatesState[ct] = make(map[string]bool) + for _, ct := range getSupportedTemplates(ctChains, rule.ClusterTemplateChains) { + if expectedState.ClusterTemplatesState[ct.Name] == nil { + expectedState.ClusterTemplatesState[ct.Name] = make(map[string]bool) } for _, ns := range namespaces { - expectedState.ClusterTemplatesState[ct][ns] = true + expectedState.ClusterTemplatesState[ct.Name][ns] = true } } - for _, st := range serviceTemplates { - if expectedState.ServiceTemplatesState[st] == nil { - expectedState.ServiceTemplatesState[st] = make(map[string]bool) + for _, st := range getSupportedTemplates(stChains, rule.ServiceTemplateChains) { + if expectedState.ServiceTemplatesState[st.Name] == nil { + expectedState.ServiceTemplatesState[st.Name] = make(map[string]bool) } for _, ns := range namespaces { - expectedState.ServiceTemplatesState[st][ns] = true + expectedState.ServiceTemplatesState[st.Name][ns] = true } } } @@ -160,6 +134,43 @@ func ParseAccessRules(ctx context.Context, cl client.Client, rules []hmc.AccessR return expectedState, nil } +func IsAvailableForUpgrade(ctx context.Context, cl client.Client, templateKind string, source, target string) (bool, error) { + tmList := &hmc.TemplateManagementList{} + listOpts := &client.ListOptions{Limit: 1} + err := cl.List(ctx, tmList, listOpts) + if err != nil { + return false, err + } + if len(tmList.Items) == 0 { + return false, fmt.Errorf("TemplateManagement is not found") + } + allChains, err := getTemplateChains(ctx, cl, templateKind) + if err != nil { + return false, err + } + for _, accessRule := range tmList.Items[0].Spec.AccessRules { + var accessRuleChains []string + switch templateKind { + case hmc.ClusterTemplateKind: + accessRuleChains = accessRule.ClusterTemplateChains + case hmc.ServiceTemplateKind: + accessRuleChains = accessRule.ServiceTemplateChains + default: + return false, fmt.Errorf("invalid template kind. Supported values: %s and %s", hmc.ClusterTemplateKind, hmc.ServiceTemplateKind) + } + for _, supportedTemplate := range getSupportedTemplates(allChains, accessRuleChains) { + if source == supportedTemplate.Name { + if slices.ContainsFunc(supportedTemplate.AvailableUpgrades, func(au hmc.AvailableUpgrade) bool { + return target == au.Name + }) { + return true, nil + } + } + } + } + return false, nil +} + func getTargetNamespaces(ctx context.Context, cl client.Client, targetNamespaces hmc.TargetNamespaces) ([]string, error) { if len(targetNamespaces.List) > 0 { return targetNamespaces.List, nil @@ -193,3 +204,49 @@ func getTargetNamespaces(ctx context.Context, cl client.Client, targetNamespaces } return result, nil } + +func getSupportedTemplates(allChains []templateChain, accessRuleChains []string) []hmc.SupportedTemplate { + supportedTemplates := make(map[string][]hmc.SupportedTemplate) + for _, chain := range allChains { + supportedTemplates[chain.GetName()] = chain.GetSupportedTemplates() + } + var result []hmc.SupportedTemplate + for _, chainName := range accessRuleChains { + result = append(result, supportedTemplates[chainName]...) + } + return result +} + +type templateChain interface { + client.Object + GetSupportedTemplates() []hmc.SupportedTemplate +} + +func getTemplateChains(ctx context.Context, cl client.Client, kind string) ([]templateChain, error) { + switch kind { + case hmc.ClusterTemplateKind: + ctChains := &hmc.ClusterTemplateChainList{} + err := cl.List(ctx, ctChains) + if err != nil { + return nil, err + } + templateChains := make([]templateChain, len(ctChains.Items)) + for i, chain := range ctChains.Items { + templateChains[i] = &chain + } + return templateChains, nil + case hmc.ServiceTemplateKind: + stChains := &hmc.ServiceTemplateChainList{} + err := cl.List(ctx, stChains) + if err != nil { + return nil, err + } + templateChains := make([]templateChain, len(stChains.Items)) + for i, chain := range stChains.Items { + templateChains[i] = &chain + } + return templateChains, nil + default: + return nil, fmt.Errorf("invalid template kind. Supported values: %s and %s", hmc.ClusterTemplateKind, hmc.ServiceTemplateKind) + } +} diff --git a/internal/webhook/managedcluster_webhook.go b/internal/webhook/managedcluster_webhook.go index 48b5e221..d11555a4 100644 --- a/internal/webhook/managedcluster_webhook.go +++ b/internal/webhook/managedcluster_webhook.go @@ -30,6 +30,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook/admission" "github.com/Mirantis/hmc/api/v1alpha1" + "github.com/Mirantis/hmc/internal/templateutil" "github.com/Mirantis/hmc/internal/utils" ) @@ -37,7 +38,10 @@ type ManagedClusterValidator struct { client.Client } -var errInvalidManagedCluster = errors.New("the ManagedCluster is invalid") +var ( + errInvalidManagedCluster = errors.New("the ManagedCluster is invalid") + errUpgradeForbidden = errors.New("the ManagedCluster upgrade is forbidden") +) func (v *ManagedClusterValidator) SetupWebhookWithManager(mgr ctrl.Manager) error { v.Client = mgr.GetClient() @@ -71,11 +75,20 @@ func (v *ManagedClusterValidator) ValidateCreate(ctx context.Context, obj runtim } // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type. -func (v *ManagedClusterValidator) ValidateUpdate(ctx context.Context, _ runtime.Object, newObj runtime.Object) (admission.Warnings, error) { +func (v *ManagedClusterValidator) ValidateUpdate(ctx context.Context, oldObj runtime.Object, newObj runtime.Object) (admission.Warnings, error) { + oldManagedCluster, ok := oldObj.(*v1alpha1.ManagedCluster) + if !ok { + return nil, apierrors.NewBadRequest(fmt.Sprintf("expected ManagedCluster but got a %T", oldObj)) + } newManagedCluster, ok := newObj.(*v1alpha1.ManagedCluster) if !ok { return nil, apierrors.NewBadRequest(fmt.Sprintf("expected ManagedCluster but got a %T", newObj)) } + oldTemplateName := oldManagedCluster.Spec.Template + newTemplateName := newManagedCluster.Spec.Template + if oldTemplateName == newTemplateName { + return nil, nil + } template, err := v.getManagedClusterTemplate(ctx, newManagedCluster.Namespace, newManagedCluster.Spec.Template) if err != nil { return nil, fmt.Errorf("%s: %v", errInvalidManagedCluster, err) @@ -84,6 +97,15 @@ func (v *ManagedClusterValidator) ValidateUpdate(ctx context.Context, _ runtime. if err != nil { return nil, fmt.Errorf("%s: %v", errInvalidManagedCluster, err) } + if !newManagedCluster.Spec.DryRun && templateutil.IsManagedByHMC(template) { + availableForUpgrade, err := templateutil.IsAvailableForUpgrade(ctx, v.Client, v1alpha1.ClusterTemplateKind, oldTemplateName, newTemplateName) + if err != nil { + return nil, fmt.Errorf("%s: failed to check if the ClusterTemplate %s is available for the upgrade from %s: %w", errUpgradeForbidden, newTemplateName, oldTemplateName, err) + } + if !availableForUpgrade { + return nil, fmt.Errorf("%s: ClusterTemplate %s is not available for the upgrade from %s", errUpgradeForbidden, newTemplateName, oldTemplateName) + } + } return nil, nil } diff --git a/internal/webhook/managedcluster_webhook_test.go b/internal/webhook/managedcluster_webhook_test.go index 61e5a02d..379a505b 100644 --- a/internal/webhook/managedcluster_webhook_test.go +++ b/internal/webhook/managedcluster_webhook_test.go @@ -28,13 +28,31 @@ import ( "github.com/Mirantis/hmc/test/objects/managedcluster" "github.com/Mirantis/hmc/test/objects/management" "github.com/Mirantis/hmc/test/objects/template" + chain "github.com/Mirantis/hmc/test/objects/templatechain" + tm "github.com/Mirantis/hmc/test/objects/templatemanagement" "github.com/Mirantis/hmc/test/scheme" ) -var ( - testTemplateName = "template-test" - testNamespace = "test" +type testCase struct { + name string + oldManagedCluster *v1alpha1.ManagedCluster + newManagedCluster *v1alpha1.ManagedCluster + existingObjects []runtime.Object + err string + warnings admission.Warnings +} + +const ( + testNamespace = "test" + + testTemplateName = "template-test" + templateUpgradeSource = "template-1-0-0" + templateUpgradeTarget = "template-1-0-1" + ctChainName = "test" +) + +var ( mgmt = management.NewManagement( management.WithAvailableProviders(v1alpha1.Providers{ InfrastructureProviders: []string{"aws"}, @@ -43,37 +61,57 @@ var ( }), ) - createAndUpdateTests = []struct { - name string - managedCluster *v1alpha1.ManagedCluster - existingObjects []runtime.Object - err string - warnings admission.Warnings - }{ + accessRules = []v1alpha1.AccessRule{ + { + ClusterTemplateChains: []string{ctChainName}, + }, + } + + supportedTemplates = []v1alpha1.SupportedTemplate{ { - name: "should fail if the template is unset", - managedCluster: managedcluster.NewManagedCluster(), - err: "the ManagedCluster is invalid: clustertemplates.hmc.mirantis.com \"\" not found", + Name: testTemplateName, }, { - name: "should fail if the ClusterTemplate is not found in the ManagedCluster's namespace", - managedCluster: managedcluster.NewManagedCluster(managedcluster.WithTemplate(testTemplateName)), + Name: templateUpgradeSource, + AvailableUpgrades: []v1alpha1.AvailableUpgrade{ + { + Name: templateUpgradeTarget, + }, + }, + }, + } + + withAWSK0sProviders = template.WithProvidersStatus(v1alpha1.Providers{ + InfrastructureProviders: []string{"aws"}, + BootstrapProviders: []string{"k0s"}, + ControlPlaneProviders: []string{"k0s"}, + }) + withValidStatus = template.WithValidationStatus(v1alpha1.TemplateValidationStatus{Valid: true}) + managedByHMC = template.ManagedByHMC() + + tmObj = tm.NewTemplateManagement(tm.WithAccessRules(accessRules)) + ctChain = chain.NewClusterTemplateChain(chain.WithName(ctChainName), chain.WithSupportedTemplates(supportedTemplates)) + + createAndUpdateTests = []testCase{ + { + name: "should fail if the ClusterTemplate is not found in the ManagedCluster's namespace", + oldManagedCluster: managedcluster.NewManagedCluster(managedcluster.WithTemplate(templateUpgradeSource)), + newManagedCluster: managedcluster.NewManagedCluster(managedcluster.WithTemplate(templateUpgradeTarget)), existingObjects: []runtime.Object{ mgmt, - template.NewClusterTemplate( - template.WithName(testTemplateName), - template.WithNamespace(testNamespace), - ), + template.NewClusterTemplate(template.WithName(templateUpgradeSource), template.WithNamespace(testNamespace)), + template.NewClusterTemplate(template.WithName(templateUpgradeTarget), template.WithNamespace(testNamespace)), }, - err: fmt.Sprintf("the ManagedCluster is invalid: clustertemplates.hmc.mirantis.com \"%s\" not found", testTemplateName), + err: fmt.Sprintf("the ManagedCluster is invalid: clustertemplates.hmc.mirantis.com \"%s\" not found", templateUpgradeTarget), }, { - name: "should fail if the cluster template was found but is invalid (some validation error)", - managedCluster: managedcluster.NewManagedCluster(managedcluster.WithTemplate(testTemplateName)), + name: "should fail if the cluster template was found but is invalid (some validation error)", + oldManagedCluster: managedcluster.NewManagedCluster(managedcluster.WithTemplate(templateUpgradeSource)), + newManagedCluster: managedcluster.NewManagedCluster(managedcluster.WithTemplate(templateUpgradeTarget)), existingObjects: []runtime.Object{ mgmt, template.NewClusterTemplate( - template.WithName(testTemplateName), + template.WithName(templateUpgradeTarget), template.WithValidationStatus(v1alpha1.TemplateValidationStatus{ Valid: false, ValidationError: "validation error example", @@ -83,8 +121,9 @@ 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.WithTemplate(testTemplateName)), + name: "should fail if one or more requested providers are not available yet", + oldManagedCluster: managedcluster.NewManagedCluster(managedcluster.WithTemplate(templateUpgradeSource)), + newManagedCluster: managedcluster.NewManagedCluster(managedcluster.WithTemplate(templateUpgradeTarget)), existingObjects: []runtime.Object{ management.NewManagement( management.WithAvailableProviders(v1alpha1.Providers{ @@ -93,7 +132,7 @@ var ( }), ), template.NewClusterTemplate( - template.WithName(testTemplateName), + template.WithName(templateUpgradeTarget), template.WithProvidersStatus(v1alpha1.Providers{ InfrastructureProviders: []string{"azure"}, BootstrapProviders: []string{"k0s"}, @@ -105,19 +144,12 @@ var ( err: "the ManagedCluster is invalid: providers verification failed: 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.WithTemplate(testTemplateName)), + name: "should succeed", + oldManagedCluster: managedcluster.NewManagedCluster(managedcluster.WithTemplate(templateUpgradeSource)), + newManagedCluster: managedcluster.NewManagedCluster(managedcluster.WithTemplate(templateUpgradeTarget)), existingObjects: []runtime.Object{ mgmt, - template.NewClusterTemplate( - template.WithName(testTemplateName), - template.WithProvidersStatus(v1alpha1.Providers{ - InfrastructureProviders: []string{"aws"}, - BootstrapProviders: []string{"k0s"}, - ControlPlaneProviders: []string{"k0s"}, - }), - template.WithValidationStatus(v1alpha1.TemplateValidationStatus{Valid: true}), - ), + template.NewClusterTemplate(template.WithName(templateUpgradeTarget), withAWSK0sProviders, withValidStatus), }, }, } @@ -127,11 +159,12 @@ func TestManagedClusterValidateCreate(t *testing.T) { g := NewWithT(t) ctx := context.Background() + for _, tt := range createAndUpdateTests { t.Run(tt.name, func(t *testing.T) { c := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithRuntimeObjects(tt.existingObjects...).Build() validator := &ManagedClusterValidator{Client: c} - warn, err := validator.ValidateCreate(ctx, tt.managedCluster) + warn, err := validator.ValidateCreate(ctx, tt.newManagedCluster) if tt.err != "" { g.Expect(err).To(HaveOccurred()) if err.Error() != tt.err { @@ -153,11 +186,54 @@ func TestManagedClusterValidateUpdate(t *testing.T) { g := NewWithT(t) ctx := context.Background() - for _, tt := range createAndUpdateTests { + + tests := []testCase{ + { + name: "should fail if the target template is not available for update", + oldManagedCluster: managedcluster.NewManagedCluster(managedcluster.WithTemplate(templateUpgradeSource)), + newManagedCluster: managedcluster.NewManagedCluster(managedcluster.WithTemplate(testTemplateName)), + existingObjects: []runtime.Object{ + mgmt, + template.NewClusterTemplate(template.WithName(testTemplateName), managedByHMC, withAWSK0sProviders, withValidStatus), + }, + err: fmt.Sprintf("the ManagedCluster upgrade is forbidden: ClusterTemplate %s is not available for the upgrade from %s", testTemplateName, templateUpgradeSource), + }, + { + name: "should succeed - template is not managed by HMC, skipping available upgrades check", + oldManagedCluster: managedcluster.NewManagedCluster(managedcluster.WithTemplate(templateUpgradeSource)), + newManagedCluster: managedcluster.NewManagedCluster(managedcluster.WithTemplate(testTemplateName)), + existingObjects: []runtime.Object{ + mgmt, + template.NewClusterTemplate(template.WithName(testTemplateName), withAWSK0sProviders, withValidStatus), + }, + }, + { + name: "should succeed - cluster is in dry run mode, skipping available upgrades check", + oldManagedCluster: managedcluster.NewManagedCluster(managedcluster.WithTemplate(templateUpgradeSource), managedcluster.WithDryRun(true)), + newManagedCluster: managedcluster.NewManagedCluster(managedcluster.WithTemplate(testTemplateName), managedcluster.WithDryRun(true)), + existingObjects: []runtime.Object{ + mgmt, + template.NewClusterTemplate(template.WithName(testTemplateName), managedByHMC, withAWSK0sProviders, withValidStatus), + }, + }, + { + name: "should succeed - template is available for upgrade and valid", + oldManagedCluster: managedcluster.NewManagedCluster(managedcluster.WithTemplate(templateUpgradeSource)), + newManagedCluster: managedcluster.NewManagedCluster(managedcluster.WithTemplate(templateUpgradeTarget)), + existingObjects: []runtime.Object{ + mgmt, + template.NewClusterTemplate(template.WithName(templateUpgradeTarget), managedByHMC, withAWSK0sProviders, withValidStatus), + }, + }, + } + tests = append(tests, createAndUpdateTests...) + + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + tt.existingObjects = append(tt.existingObjects, tmObj, ctChain) c := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithRuntimeObjects(tt.existingObjects...).Build() validator := &ManagedClusterValidator{Client: c} - warn, err := validator.ValidateUpdate(ctx, managedcluster.NewManagedCluster(), tt.managedCluster) + warn, err := validator.ValidateUpdate(ctx, tt.oldManagedCluster, tt.newManagedCluster) if tt.err != "" { g.Expect(err).To(HaveOccurred()) if err.Error() != tt.err { diff --git a/test/objects/template/template.go b/test/objects/template/template.go index 668f4019..f3184be3 100644 --- a/test/objects/template/template.go +++ b/test/objects/template/template.go @@ -92,6 +92,15 @@ func WithLabels(labels map[string]string) Opt { } } +func ManagedByHMC() Opt { + return func(t *Template) { + if t.Labels == nil { + t.Labels = make(map[string]string) + } + t.Labels[v1alpha1.HMCManagedLabelKey] = v1alpha1.HMCManagedLabelValue + } +} + func WithHelmSpec(helmSpec v1alpha1.HelmSpec) Opt { return func(t *Template) { t.Spec.Helm = helmSpec