From 545f2babb9ae468d068e0fbf2d99536ae5b655ad Mon Sep 17 00:00:00 2001 From: zerospiel Date: Tue, 8 Oct 2024 15:17:25 +0200 Subject: [PATCH] Addressed comments on compatibility attrs * amends to descs and fix typos * correctly parse providers * changed providers anno separator * enforce CAPI version check in providertemplates * amends to the API regarding CAPI version compatibility --- api/v1alpha1/clustertemplate_types.go | 21 ++- api/v1alpha1/common.go | 16 +- api/v1alpha1/managedcluster_types.go | 7 +- api/v1alpha1/providertemplate_types.go | 70 ++++++-- api/v1alpha1/servicetemplate_types.go | 26 ++- api/v1alpha1/templates_common.go | 13 +- api/v1alpha1/zz_generated.deepcopy.go | 1 - .../controller/managedcluster_controller.go | 3 +- internal/controller/release_controller.go | 8 +- internal/controller/template_controller.go | 6 +- internal/helm/release.go | 2 +- internal/webhook/managedcluster_webhook.go | 75 +++----- .../webhook/managedcluster_webhook_test.go | 80 ++++----- internal/webhook/management_webhook.go | 80 ++++++++- internal/webhook/management_webhook_test.go | 165 ++++++++++++++++-- .../hmc.mirantis.com_clustertemplates.yaml | 92 +++++----- .../hmc.mirantis.com_managedclusters.yaml | 56 +----- .../crds/hmc.mirantis.com_managements.yaml | 36 ++-- .../hmc.mirantis.com_providertemplates.yaml | 105 +++++++---- .../hmc.mirantis.com_servicetemplates.yaml | 7 +- templates/provider/k0smotron/Chart.yaml | 2 +- test/kubeclient/kubeclient.go | 4 +- test/objects/managedcluster/managedcluster.go | 6 - test/objects/management/management.go | 6 + test/objects/release/release.go | 70 ++++++++ test/objects/template/template.go | 35 +++- 26 files changed, 654 insertions(+), 338 deletions(-) create mode 100644 test/objects/release/release.go diff --git a/api/v1alpha1/clustertemplate_types.go b/api/v1alpha1/clustertemplate_types.go index a10368b7b..024cc2d77 100644 --- a/api/v1alpha1/clustertemplate_types.go +++ b/api/v1alpha1/clustertemplate_types.go @@ -31,17 +31,20 @@ const ( // ClusterTemplateSpec defines the desired state of ClusterTemplate type ClusterTemplateSpec struct { Helm HelmSpec `json:"helm"` - // Compatible K8S version of the cluster set in the SemVer format. - KubertenesVersion string `json:"k8sVersion,omitempty"` - // Providers represent required CAPI providers with constrainted compatibility versions set. Should be set if not present in the Helm chart metadata. + // Kubernetes exact version in the SemVer format provided by this ClusterTemplate. + KubernetesVersion string `json:"k8sVersion,omitempty"` + // Providers represent required CAPI providers with constrained compatibility versions set. + // Should be set if not present in the Helm chart metadata. + // Compatibility attributes are optional to be defined. Providers ProvidersTupled `json:"providers,omitempty"` } // ClusterTemplateStatus defines the observed state of ClusterTemplate type ClusterTemplateStatus struct { - // Compatible K8S version of the cluster set in the SemVer format. - KubertenesVersion string `json:"k8sVersion,omitempty"` - // Providers represent exposed CAPI providers with constrainted compatibility versions set. + // Kubernetes exact version in the SemVer format provided by this ClusterTemplate. + KubernetesVersion string `json:"k8sVersion,omitempty"` + // Providers represent required CAPI providers with constrained compatibility versions set + // if the latter has been given. Providers ProvidersTupled `json:"providers,omitempty"` TemplateStatusCommon `json:",inline"` @@ -67,8 +70,8 @@ func (t *ClusterTemplate) FillStatusWithProviders(annotations map[string]string) } kversion := annotations[ChartAnnotationKubernetesVersion] - if t.Spec.KubertenesVersion != "" { - kversion = t.Spec.KubertenesVersion + if t.Spec.KubernetesVersion != "" { + kversion = t.Spec.KubernetesVersion } if kversion == "" { return nil @@ -78,7 +81,7 @@ func (t *ClusterTemplate) FillStatusWithProviders(annotations map[string]string) return fmt.Errorf("failed to parse kubernetes version %s: %w", kversion, err) } - t.Status.KubertenesVersion = kversion + t.Status.KubernetesVersion = kversion return nil } diff --git a/api/v1alpha1/common.go b/api/v1alpha1/common.go index f100a4bfc..0a25b4a70 100644 --- a/api/v1alpha1/common.go +++ b/api/v1alpha1/common.go @@ -33,22 +33,26 @@ type ( } // Holds different types of CAPI providers with either - // an exact or constrainted version in the SemVer format. The requirement + // an exact or constrained version in the SemVer format. The requirement // is determined by a consumer of this type. ProvidersTupled struct { - // List of CAPI infrastructure providers with either an exact or constrainted version in the SemVer format. + // List of CAPI infrastructure providers with either an exact or constrained version in the SemVer format. + // Compatibility attributes are optional to be defined. InfrastructureProviders []ProviderTuple `json:"infrastructure,omitempty"` - // List of CAPI bootstrap providers with either an exact or constrainted version in the SemVer format. + // List of CAPI bootstrap providers with either an exact or constrained version in the SemVer format. + // Compatibility attributes are optional to be defined. BootstrapProviders []ProviderTuple `json:"bootstrap,omitempty"` - // List of CAPI control plane providers with either an exact or constrainted version in the SemVer format. + // List of CAPI control plane providers with either an exact or constrained version in the SemVer format. + // Compatibility attributes are optional to be defined. ControlPlaneProviders []ProviderTuple `json:"controlPlane,omitempty"` } - // Represents name of the provider with either an exact or constrainted version in the SemVer format. + // Represents name of the provider with either an exact or constrained version in the SemVer format. ProviderTuple struct { // Name of the provider. Name string `json:"name,omitempty"` - // Compatibility restriction in the SemVer format (exact or constrainted version) + // Compatibility restriction in the SemVer format (exact or constrained version). + // Optional to be defined. VersionOrConstraint string `json:"versionOrConstraint,omitempty"` } ) diff --git a/api/v1alpha1/managedcluster_types.go b/api/v1alpha1/managedcluster_types.go index 225ce290a..befb3122a 100644 --- a/api/v1alpha1/managedcluster_types.go +++ b/api/v1alpha1/managedcluster_types.go @@ -101,12 +101,9 @@ type ManagedClusterSpec struct { // ManagedClusterStatus defines the observed state of ManagedCluster type ManagedClusterStatus struct { - // Currently compatible K8S version of the cluster. Being set only if + // Currently compatible exact Kubernetes version of the cluster. Being set only if // provided by the corresponding ClusterTemplate. - KubertenesVersion string `json:"k8sVersion,omitempty"` - // Providers represent exposed CAPI providers with constrainted compatibility versions set. - // Propagated from the corresponding ClusterTemplate. - Providers ProvidersTupled `json:"providers,omitempty"` + KubernetesVersion string `json:"k8sVersion,omitempty"` // Conditions contains details for the current state of the ManagedCluster Conditions []metav1.Condition `json:"conditions,omitempty"` // ObservedGeneration is the last observed generation. diff --git a/api/v1alpha1/providertemplate_types.go b/api/v1alpha1/providertemplate_types.go index c1326b71c..b71f020cd 100644 --- a/api/v1alpha1/providertemplate_types.go +++ b/api/v1alpha1/providertemplate_types.go @@ -21,23 +21,39 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// ChartAnnotationCAPIVersion is an annotation containing the CAPI exact version in the SemVer format associated with a ProviderTemplate. -const ChartAnnotationCAPIVersion = "hmc.mirantis.com/capi-version" +const ( + // ChartAnnotationCAPIVersion is an annotation containing the CAPI exact version in the SemVer format associated with a ProviderTemplate. + ChartAnnotationCAPIVersion = "hmc.mirantis.com/capi-version" + // ChartAnnotationCAPIVersionConstraint is an annotation containing the CAPI version constraint in the SemVer format associated with a ProviderTemplate. + ChartAnnotationCAPIVersionConstraint = "hmc.mirantis.com/capi-version-constraint" +) + +// +kubebuilder:validation:XValidation:rule="!(has(self.capiVersion) && has(self.capiVersionConstraint))", message="Either capiVersion or capiVersionConstraint may be set, but not both" // ProviderTemplateSpec defines the desired state of ProviderTemplate type ProviderTemplateSpec struct { - Helm HelmSpec `json:"helm"` - // Compatible CAPI provider version set in the SemVer format. + Helm HelmSpec `json:"helm,omitempty"` + // CAPI exact version in the SemVer format. + // Applicable only for the cluster-api ProviderTemplate itself. CAPIVersion string `json:"capiVersion,omitempty"` - // Represents required CAPI providers with exact compatibility versions set. Should be set if not present in the Helm chart metadata. + // CAPI version constraint in the SemVer format indicating compatibility with the core CAPI. + // Not applicable for the cluster-api ProviderTemplate. + CAPIVersionConstraint string `json:"capiVersionConstraint,omitempty"` + // Providers represent exposed CAPI providers with exact compatibility versions set. + // Should be set if not present in the Helm chart metadata. + // Compatibility attributes are optional to be defined. Providers ProvidersTupled `json:"providers,omitempty"` } // ProviderTemplateStatus defines the observed state of ProviderTemplate type ProviderTemplateStatus struct { - // Compatible CAPI provider version in the SemVer format. + // CAPI exact version in the SemVer format. + // Applicable only for the capi Template itself. CAPIVersion string `json:"capiVersion,omitempty"` - // Providers represent exposed CAPI providers with exact compatibility versions set. + // CAPI version constraint in the SemVer format indicating compatibility with the core CAPI. + CAPIVersionConstraint string `json:"capiVersionConstraint,omitempty"` + // Providers represent exposed CAPI providers with exact compatibility versions set + // if the latter has been given. Providers ProvidersTupled `json:"providers,omitempty"` TemplateStatusCommon `json:",inline"` @@ -62,19 +78,35 @@ func (t *ProviderTemplate) FillStatusWithProviders(annotations map[string]string return fmt.Errorf("failed to parse ProviderTemplate infrastructure providers: %v", err) } - capiVersion := annotations[ChartAnnotationCAPIVersion] - if t.Spec.CAPIVersion != "" { - capiVersion = t.Spec.CAPIVersion + if t.Name == CoreCAPIName { + capiVersion := annotations[ChartAnnotationCAPIVersion] + if t.Spec.CAPIVersion != "" { + capiVersion = t.Spec.CAPIVersion + } + if capiVersion == "" { + return nil + } + + if _, err := semver.NewVersion(capiVersion); err != nil { + return fmt.Errorf("failed to parse CAPI version %s: %w", capiVersion, err) + } + + t.Status.CAPIVersion = capiVersion + } else { + capiConstraint := annotations[ChartAnnotationCAPIVersionConstraint] + if t.Spec.CAPIVersionConstraint != "" { + capiConstraint = t.Spec.CAPIVersionConstraint + } + if capiConstraint == "" { + return nil + } + + if _, err := semver.NewConstraint(capiConstraint); err != nil { + return fmt.Errorf("failed to parse CAPI version constraint %s: %w", capiConstraint, err) + } + + t.Status.CAPIVersionConstraint = capiConstraint } - if capiVersion == "" { - return nil - } - - if _, err := semver.NewVersion(capiVersion); err != nil { - return fmt.Errorf("failed to parse CAPI version %s: %w", capiVersion, err) - } - - t.Status.CAPIVersion = capiVersion return nil } diff --git a/api/v1alpha1/servicetemplate_types.go b/api/v1alpha1/servicetemplate_types.go index bf2890ccd..540c33eb6 100644 --- a/api/v1alpha1/servicetemplate_types.go +++ b/api/v1alpha1/servicetemplate_types.go @@ -25,7 +25,7 @@ import ( const ( // Denotes the servicetemplate resource Kind. ServiceTemplateKind = "ServiceTemplate" - // ChartAnnotationKubernetesConstraint is an annotation containing the Kubernetes constrainted version in the SemVer format associated with a ServiceTemplate. + // ChartAnnotationKubernetesConstraint is an annotation containing the Kubernetes constrained version in the SemVer format associated with a ServiceTemplate. ChartAnnotationKubernetesConstraint = "hmc.mirantis.com/k8s-version-constraint" ) @@ -33,16 +33,17 @@ const ( type ServiceTemplateSpec struct { Helm HelmSpec `json:"helm"` // Constraint describing compatible K8S versions of the cluster set in the SemVer format. - KubertenesConstraint string `json:"k8sConstraint,omitempty"` - // Represents required CAPI providers. Should be set if not present in the Helm chart metadata. + KubernetesConstraint string `json:"k8sConstraint,omitempty"` + // Providers represent requested CAPI providers. + // Should be set if not present in the Helm chart metadata. Providers Providers `json:"providers,omitempty"` } // ServiceTemplateStatus defines the observed state of ServiceTemplate type ServiceTemplateStatus struct { // Constraint describing compatible K8S versions of the cluster set in the SemVer format. - KubertenesConstraint string `json:"k8sConstraint,omitempty"` - // Represents exposed CAPI providers. + KubernetesConstraint string `json:"k8sConstraint,omitempty"` + // Providers represent requested CAPI providers. Providers Providers `json:"providers,omitempty"` TemplateStatusCommon `json:",inline"` @@ -65,17 +66,14 @@ func (t *ServiceTemplate) FillStatusWithProviders(annotations map[string]string) pspec, anno = t.Spec.Providers.InfrastructureProviders, ChartAnnotationInfraProviders } - if len(pspec) > 0 { - return pspec - } - providers := annotations[anno] if len(providers) == 0 { - return []string{} + return pspec } - splitted := strings.Split(providers, ",") + splitted := strings.Split(providers, multiProviderSeparator) result := make([]string, 0, len(splitted)) + result = append(result, pspec...) for _, v := range splitted { if c := strings.TrimSpace(v); c != "" { result = append(result, c) @@ -90,8 +88,8 @@ func (t *ServiceTemplate) FillStatusWithProviders(annotations map[string]string) t.Status.Providers.InfrastructureProviders = parseProviders(infrastructureProvidersType) kconstraint := annotations[ChartAnnotationKubernetesConstraint] - if t.Spec.KubertenesConstraint != "" { - kconstraint = t.Spec.KubertenesConstraint + if t.Spec.KubernetesConstraint != "" { + kconstraint = t.Spec.KubernetesConstraint } if kconstraint == "" { return nil @@ -101,7 +99,7 @@ func (t *ServiceTemplate) FillStatusWithProviders(annotations map[string]string) return fmt.Errorf("failed to parse kubernetes constraint %s: %w", kconstraint, err) } - t.Status.KubertenesConstraint = kconstraint + t.Status.KubernetesConstraint = kconstraint return nil } diff --git a/api/v1alpha1/templates_common.go b/api/v1alpha1/templates_common.go index 36fd1f167..3f1abaf92 100644 --- a/api/v1alpha1/templates_common.go +++ b/api/v1alpha1/templates_common.go @@ -85,22 +85,23 @@ const ( infrastructureProvidersType ) +const multiProviderSeparator = ";" + func parseProviders[T any](providersGetter interface{ GetSpecProviders() ProvidersTupled }, typ providersType, annotations map[string]string, validationFn func(string) (T, error)) ([]ProviderTuple, error) { pspec, anno := getProvidersSpecAnno(providersGetter, typ) - if len(pspec) > 0 { - return pspec, nil - } providers := annotations[anno] if len(providers) == 0 { - return []ProviderTuple{}, nil + return pspec, nil } var ( - splitted = strings.Split(providers, ",") - pstatus = make([]ProviderTuple, 0, len(splitted)) + splitted = strings.Split(providers, multiProviderSeparator) + pstatus = make([]ProviderTuple, 0, len(splitted)+len(pspec)) merr error ) + pstatus = append(pstatus, pspec...) + for _, v := range splitted { v = strings.TrimSpace(v) nVerOrC := strings.SplitN(v, " ", 2) diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 0060cf256..a0d3c518a 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -488,7 +488,6 @@ func (in *ManagedClusterSpec) DeepCopy() *ManagedClusterSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ManagedClusterStatus) DeepCopyInto(out *ManagedClusterStatus) { *out = *in - in.Providers.DeepCopyInto(&out.Providers) if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions *out = make([]metav1.Condition, len(*in)) diff --git a/internal/controller/managedcluster_controller.go b/internal/controller/managedcluster_controller.go index 86a05fb4c..e29981f24 100644 --- a/internal/controller/managedcluster_controller.go +++ b/internal/controller/managedcluster_controller.go @@ -229,8 +229,7 @@ func (r *ManagedClusterReconciler) Update(ctx context.Context, managedCluster *h return ctrl.Result{}, errors.New(errMsg) } // template is ok, propagate data from it - managedCluster.Status.KubertenesVersion = template.Status.KubertenesVersion - managedCluster.Status.Providers = template.Status.Providers + managedCluster.Status.KubernetesVersion = template.Status.KubernetesVersion apimeta.SetStatusCondition(managedCluster.GetConditions(), metav1.Condition{ Type: hmc.TemplateReadyCondition, diff --git a/internal/controller/release_controller.go b/internal/controller/release_controller.go index b341d84fc..825126e29 100644 --- a/internal/controller/release_controller.go +++ b/internal/controller/release_controller.go @@ -51,14 +51,14 @@ type ReleaseReconciler struct { Config *rest.Config - CreateManagement bool - CreateRelease bool - CreateTemplates bool - HMCTemplatesChartName string SystemNamespace string DefaultRegistryConfig helm.DefaultRegistryConfig + + CreateManagement bool + CreateRelease bool + CreateTemplates bool } func (r *ReleaseReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { diff --git a/internal/controller/template_controller.go b/internal/controller/template_controller.go index e92b8eeab..98374916d 100644 --- a/internal/controller/template_controller.go +++ b/internal/controller/template_controller.go @@ -39,11 +39,11 @@ const ( // TemplateReconciler reconciles a *Template object type TemplateReconciler struct { client.Client - SystemNamespace string - - DefaultRegistryConfig helm.DefaultRegistryConfig downloadHelmChartFunc func(context.Context, *sourcev1.Artifact) (*chart.Chart, error) + + SystemNamespace string + DefaultRegistryConfig helm.DefaultRegistryConfig } type ClusterTemplateReconciler struct { diff --git a/internal/helm/release.go b/internal/helm/release.go index e89df8182..dd63a666c 100644 --- a/internal/helm/release.go +++ b/internal/helm/release.go @@ -38,8 +38,8 @@ type ReconcileHelmReleaseOpts struct { OwnerReference *metav1.OwnerReference ChartRef *hcv2.CrossNamespaceSourceReference ReconcileInterval *time.Duration - DependsOn []meta.NamespacedObjectReference TargetNamespace string + DependsOn []meta.NamespacedObjectReference CreateNamespace bool } diff --git a/internal/webhook/managedcluster_webhook.go b/internal/webhook/managedcluster_webhook.go index c4dd0ac21..811765286 100644 --- a/internal/webhook/managedcluster_webhook.go +++ b/internal/webhook/managedcluster_webhook.go @@ -22,7 +22,6 @@ import ( "sort" "github.com/Masterminds/semver/v3" - admissionv1 "k8s.io/api/admission/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" @@ -70,6 +69,10 @@ func (v *ManagedClusterValidator) ValidateCreate(ctx context.Context, obj runtim return nil, fmt.Errorf("%s: %v", invalidManagedClusterMsg, err) } + if err := validateK8sCompatibility(ctx, v.Client, template, managedCluster); err != nil { + return admission.Warnings{"Failed to validate k8s version compatibility with ServiceTemplates"}, fmt.Errorf("failed to validate k8s compatibility: %v", err) + } + return nil, nil } @@ -89,16 +92,16 @@ func (v *ManagedClusterValidator) ValidateUpdate(ctx context.Context, _ runtime. return nil, fmt.Errorf("%s: %v", invalidManagedClusterMsg, err) } - if err := validateK8sCompatibility(ctx, v.Client, newManagedCluster); err != nil { + if err := validateK8sCompatibility(ctx, v.Client, template, newManagedCluster); err != nil { return admission.Warnings{"Failed to validate k8s version compatibility with ServiceTemplates"}, fmt.Errorf("failed to validate k8s compatibility: %v", err) } return nil, nil } -func validateK8sCompatibility(ctx context.Context, cl client.Client, mc *hmcv1alpha1.ManagedCluster) error { - if len(mc.Spec.Services) == 0 || mc.Status.KubertenesVersion == "" { - return nil +func validateK8sCompatibility(ctx context.Context, cl client.Client, template *hmcv1alpha1.ClusterTemplate, mc *hmcv1alpha1.ManagedCluster) error { + if len(mc.Spec.Services) == 0 || template.Status.KubernetesVersion == "" { + return nil // nothing to do } svcTpls := new(hmcv1alpha1.ServiceTemplateList) @@ -108,12 +111,12 @@ func validateK8sCompatibility(ctx context.Context, cl client.Client, mc *hmcv1al svcTplName2KConstraint := make(map[string]string, len(svcTpls.Items)) for _, v := range svcTpls.Items { - svcTplName2KConstraint[v.Name] = v.Status.KubertenesConstraint + svcTplName2KConstraint[v.Name] = v.Status.KubernetesConstraint } - mcVersion, err := semver.NewVersion(mc.Status.KubertenesVersion) + 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", mc.Status.KubertenesVersion, mc.Namespace, mc.Name, err) + return fmt.Errorf("failed to parse k8s version %s of the ManagedCluster %s/%s: %w", template.Status.KubernetesVersion, mc.Namespace, mc.Name, err) } for _, v := range mc.Spec.Services { @@ -132,12 +135,12 @@ func validateK8sCompatibility(ctx context.Context, cl client.Client, mc *hmcv1al tplConstraint, err := semver.NewConstraint(kc) if err != nil { // should never happen - return fmt.Errorf("failed to parse k8s constrainted 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", kc, mc.Namespace, v.Template, err) } if !tplConstraint.Check(mcVersion) { - return fmt.Errorf("k8s version %s of the ManagedCluster %s/%s does not satisfy constrainted version %s from the ServiceTemplate %s/%s", - mc.Status.KubertenesVersion, mc.Namespace, mc.Name, + 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) } } @@ -212,43 +215,26 @@ func (v *ManagedClusterValidator) verifyProviders(ctx context.Context, template ) var ( - exposedProviders = management.Status.AvailableProviders - requiredProviders = template.Status.Providers + exposedProviders = management.Status.AvailableProviders + requiredProviders = template.Status.Providers + wrongVersionProviders, missingProviders = make(map[string][]string, 3), make(map[string][]string, 3) - missingBootstrap, missingCP, missingInfra []string - wrongVersionProviders map[string][]string + err error ) - // on update we have to validate versions between exact the provider tpl and constraints from the cluster tpl - if req, _ := admission.RequestFromContext(ctx); req.Operation == admissionv1.Update { - wrongVersionProviders = make(map[string][]string, 3) - missing, wrongVers, err := getMissingProvidersWithWrongVersions(exposedProviders.BootstrapProviders, requiredProviders.BootstrapProviders) - if err != nil { - return err - } - wrongVersionProviders[bootstrapProviderType], missingBootstrap = wrongVers, missing - - missing, wrongVers, err = getMissingProvidersWithWrongVersions(exposedProviders.ControlPlaneProviders, requiredProviders.ControlPlaneProviders) - if err != nil { - return err - } - wrongVersionProviders[controlPlateProviderType], missingCP = wrongVers, missing + missingProviders[bootstrapProviderType], wrongVersionProviders[bootstrapProviderType], err = getMissingProvidersWithWrongVersions(exposedProviders.BootstrapProviders, requiredProviders.BootstrapProviders) + if err != nil { + return err + } - missing, wrongVers, err = getMissingProvidersWithWrongVersions(exposedProviders.InfrastructureProviders, requiredProviders.InfrastructureProviders) - if err != nil { - return err - } - wrongVersionProviders[infraProviderType], missingInfra = wrongVers, missing - } else { - missingBootstrap = getMissingProviders(exposedProviders.BootstrapProviders, requiredProviders.BootstrapProviders) - missingCP = getMissingProviders(exposedProviders.ControlPlaneProviders, requiredProviders.ControlPlaneProviders) - missingInfra = getMissingProviders(exposedProviders.InfrastructureProviders, requiredProviders.InfrastructureProviders) + missingProviders[controlPlateProviderType], wrongVersionProviders[controlPlateProviderType], err = getMissingProvidersWithWrongVersions(exposedProviders.ControlPlaneProviders, requiredProviders.ControlPlaneProviders) + if err != nil { + return err } - missingProviders := map[string][]string{ - bootstrapProviderType: missingBootstrap, - controlPlateProviderType: missingCP, - infraProviderType: missingInfra, + 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") @@ -275,11 +261,6 @@ func collectErrors(m map[string][]string, msgFormat string) (errs []error) { return errs } -func getMissingProviders(exposed, required []hmcv1alpha1.ProviderTuple) (missing []string) { - missing, _, _ = getMissingProvidersWithWrongVersions(exposed, required) - return missing -} - func getMissingProvidersWithWrongVersions(exposed, required []hmcv1alpha1.ProviderTuple) (missing, nonSatisfying []string, _ error) { exposedSet := make(map[string]hmcv1alpha1.ProviderTuple, len(exposed)) for _, v := range exposed { diff --git a/internal/webhook/managedcluster_webhook_test.go b/internal/webhook/managedcluster_webhook_test.go index e5945223e..82c833dca 100644 --- a/internal/webhook/managedcluster_webhook_test.go +++ b/internal/webhook/managedcluster_webhook_test.go @@ -121,47 +121,6 @@ var ( ), }, }, - } -) - -func TestManagedClusterValidateCreate(t *testing.T) { - g := NewWithT(t) - - ctx := admission.NewContextWithRequest(context.Background(), admission.Request{ - AdmissionRequest: admissionv1.AdmissionRequest{ - Operation: admissionv1.Create, - }, - }) - 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) - if tt.err != "" { - g.Expect(err).To(HaveOccurred()) - if err.Error() != tt.err { - t.Fatalf("expected error '%s', got error: %s", tt.err, err.Error()) - } - } else { - g.Expect(err).To(Succeed()) - } - - g.Expect(warn).To(Equal(tt.warnings)) - }) - } -} - -func TestManagedClusterValidateUpdate(t *testing.T) { - g := NewWithT(t) - - updateTests := append(createAndUpdateTests[:0:0], createAndUpdateTests...) - updateTests = append(updateTests, []struct { - name string - managedCluster *v1alpha1.ManagedCluster - existingObjects []runtime.Object - err string - warnings admission.Warnings - }{ { name: "provider template versions does not satisfy cluster template constraints", managedCluster: managedcluster.NewManagedCluster(managedcluster.WithClusterTemplate(testTemplateName)), @@ -189,7 +148,6 @@ one or more required infrastructure providers does not satisfy constraints: [aws name: "cluster template k8s version does not satisfy service template constraints", managedCluster: managedcluster.NewManagedCluster( managedcluster.WithClusterTemplate(testTemplateName), - managedcluster.WithK8sVersionStatus("v1.30.0"), managedcluster.WithServiceTemplate(testTemplateName), ), existingObjects: []runtime.Object{ @@ -206,6 +164,7 @@ one or more required infrastructure providers does not satisfy constraints: [aws ControlPlaneProviders: []v1alpha1.ProviderTuple{{Name: "k0s"}}, }), template.WithValidationStatus(v1alpha1.TemplateValidationStatus{Valid: true}), + template.WithClusterStatusK8sVersion("v1.30.0"), ), template.NewServiceTemplate( template.WithName(testTemplateName), @@ -218,17 +177,48 @@ one or more required infrastructure providers does not satisfy constraints: [aws template.WithValidationStatus(v1alpha1.TemplateValidationStatus{Valid: true}), ), }, - err: fmt.Sprintf(`failed to validate k8s compatibility: k8s version v1.30.0 of the ManagedCluster default/managedcluster does not satisfy constrainted version <1.30 from the ServiceTemplate default/%s`, testTemplateName), + err: fmt.Sprintf(`failed to validate k8s compatibility: k8s version v1.30.0 of the ManagedCluster default/%s does not satisfy constrained version <1.30 from the ServiceTemplate default/%s`, managedcluster.DefaultName, testTemplateName), warnings: admission.Warnings{"Failed to validate k8s version compatibility with ServiceTemplates"}, }, - }...) + } +) + +func TestManagedClusterValidateCreate(t *testing.T) { + g := NewWithT(t) + + ctx := admission.NewContextWithRequest(context.Background(), admission.Request{ + AdmissionRequest: admissionv1.AdmissionRequest{ + Operation: admissionv1.Create, + }, + }) + 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) + if tt.err != "" { + g.Expect(err).To(HaveOccurred()) + if err.Error() != tt.err { + t.Fatalf("expected error '%s', got error: %s", tt.err, err.Error()) + } + } else { + g.Expect(err).To(Succeed()) + } + + g.Expect(warn).To(Equal(tt.warnings)) + }) + } +} + +func TestManagedClusterValidateUpdate(t *testing.T) { + g := NewWithT(t) ctx := admission.NewContextWithRequest(context.Background(), admission.Request{ AdmissionRequest: admissionv1.AdmissionRequest{ Operation: admissionv1.Update, }, }) - for _, tt := range updateTests { + 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} diff --git a/internal/webhook/management_webhook.go b/internal/webhook/management_webhook.go index f1784cd73..b0c0f1704 100644 --- a/internal/webhook/management_webhook.go +++ b/internal/webhook/management_webhook.go @@ -17,14 +17,17 @@ 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" + "github.com/Masterminds/semver/v3" + hmcv1alpha1 "github.com/Mirantis/hmc/api/v1alpha1" ) type ManagementValidator struct { @@ -36,7 +39,7 @@ var errManagementDeletionForbidden = errors.New("management deletion is forbidde func (v *ManagementValidator) SetupWebhookWithManager(mgr ctrl.Manager) error { v.Client = mgr.GetClient() return ctrl.NewWebhookManagedBy(mgr). - For(&v1alpha1.Management{}). + For(&hmcv1alpha1.Management{}). WithValidator(v). WithDefaulter(v). Complete() @@ -53,13 +56,82 @@ func (*ManagementValidator) ValidateCreate(_ context.Context, _ runtime.Object) } // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type. -func (*ManagementValidator) ValidateUpdate(_ context.Context, _ runtime.Object, _ runtime.Object) (admission.Warnings, error) { +func (v *ManagementValidator) ValidateUpdate(ctx context.Context, _, newObj runtime.Object) (admission.Warnings, error) { + const invalidMgmtMsg = "the Management is invalid" + + mgmt, ok := newObj.(*hmcv1alpha1.Management) + if !ok { + return nil, apierrors.NewBadRequest(fmt.Sprintf("expected Management but got a %T", newObj)) + } + + release := new(hmcv1alpha1.Release) + if err := v.Get(ctx, client.ObjectKey{Name: mgmt.Spec.Release}, release); err != nil { + // TODO: probably we do not want this skip if extra checks will be introduced + if apierrors.IsNotFound(err) && (mgmt.Spec.Core == nil || mgmt.Spec.Core.CAPI.Template == "") { + return nil, nil // nothing to do + } + return nil, fmt.Errorf("failed to get Release %s: %w", mgmt.Spec.Release, err) + } + + capiTplName := release.Spec.CAPI.Template + if mgmt.Spec.Core != nil && mgmt.Spec.Core.CAPI.Template != "" { + capiTplName = mgmt.Spec.Core.CAPI.Template + } + + capiTpl := new(hmcv1alpha1.ProviderTemplate) + if err := v.Get(ctx, client.ObjectKey{Name: capiTplName}, capiTpl); err != nil { + return nil, fmt.Errorf("failed to get ProviderTemplate %s: %w", capiTplName, err) + } + + if capiTpl.Status.CAPIVersion == "" { + return nil, nil // nothing to validate against + } + + capiRequiredVersion, err := semver.NewVersion(capiTpl.Status.CAPIVersion) + if err != nil { // should never happen + return nil, fmt.Errorf("%s: invalid CAPI version %s in the ProviderTemplate %s to be validated against: %v", invalidMgmtMsg, capiTpl.Status.CAPIVersion, capiTpl.Name, err) + } + + var wrongVersions error + for _, p := range mgmt.Spec.Providers { + tplName := p.Template + if tplName == "" { + tplName = release.ProviderTemplate(p.Name) + } + + if tplName == capiTpl.Name { // skip capi itself + continue + } + + pTpl := new(hmcv1alpha1.ProviderTemplate) + if err := v.Get(ctx, client.ObjectKey{Name: tplName}, pTpl); err != nil { + return nil, fmt.Errorf("failed to get ProviderTemplate %s: %w", tplName, err) + } + + if pTpl.Status.CAPIVersionConstraint == "" { + continue + } + + constraint, err := semver.NewConstraint(pTpl.Status.CAPIVersionConstraint) + if err != nil { // should never happen + return nil, fmt.Errorf("%s: invalid CAPI version constraint %s in the ProviderTemplate %s: %v", invalidMgmtMsg, pTpl.Status.CAPIVersionConstraint, pTpl.Name, err) + } + + if !constraint.Check(capiRequiredVersion) { + wrongVersions = errors.Join(wrongVersions, fmt.Errorf("core CAPI version %s does not satisfy ProviderTemplate %s constraint %s", capiRequiredVersion, pTpl.Name, constraint)) + } + } + + if wrongVersions != nil { + return admission.Warnings{"The Management object has incompatible CAPI versions ProviderTemplates"}, fmt.Errorf("%s: %s", invalidMgmtMsg, wrongVersions) + } + return nil, nil } // ValidateDelete implements webhook.Validator so a webhook will be registered for the type. func (v *ManagementValidator) ValidateDelete(ctx context.Context, _ runtime.Object) (admission.Warnings, error) { - managedClusters := &v1alpha1.ManagedClusterList{} + managedClusters := &hmcv1alpha1.ManagedClusterList{} err := v.Client.List(ctx, managedClusters, client.Limit(1)) if err != nil { return nil, err diff --git a/internal/webhook/management_webhook_test.go b/internal/webhook/management_webhook_test.go index a8bd18036..13bb2942d 100644 --- a/internal/webhook/management_webhook_test.go +++ b/internal/webhook/management_webhook_test.go @@ -16,9 +16,11 @@ 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" @@ -26,13 +28,160 @@ import ( "github.com/Mirantis/hmc/api/v1alpha1" "github.com/Mirantis/hmc/test/objects/managedcluster" "github.com/Mirantis/hmc/test/objects/management" + "github.com/Mirantis/hmc/test/objects/release" + "github.com/Mirantis/hmc/test/objects/template" "github.com/Mirantis/hmc/test/scheme" ) +func TestManagementValidateUpdate(t *testing.T) { + g := NewWithT(t) + + ctx := admission.NewContextWithRequest(context.Background(), admission.Request{AdmissionRequest: admissionv1.AdmissionRequest{Operation: admissionv1.Update}}) + + const ( + versionOne = "1.0.0" + constraintVerOne, constraintVerTwo = "^1.0.0", "~2.0.0" + invalidVersion = "invalid-ver" + invalidConstraint = "invalid-constraint" + ) + + providerAwsDefaultTpl := v1alpha1.Provider{ + Name: "aws", + Component: v1alpha1.Component{ + Template: template.DefaultName, + }, + } + + tests := []struct { + name string + management *v1alpha1.Management + existingObjects []runtime.Object + err string + warnings admission.Warnings + }{ + { + name: "no release and no core capi tpl set, should succeed", + management: management.NewManagement(), + }, + { + name: "no capi providertemplate, should fail", + management: management.NewManagement(management.WithRelease(release.DefaultName)), + existingObjects: []runtime.Object{release.New()}, + err: fmt.Sprintf(`failed to get ProviderTemplate %s: providertemplates.hmc.mirantis.com "%s" not found`, release.DefaultCAPITemplateName, release.DefaultCAPITemplateName), + }, + { + name: "capi providertemplate without capi version set, should succeed", + management: management.NewManagement(management.WithRelease(release.DefaultName)), + existingObjects: []runtime.Object{ + release.New(), + template.NewProviderTemplate(template.WithName(release.DefaultCAPITemplateName)), + }, + }, + { + name: "capi providertemplate with wrong capi semver set, should fail", + management: management.NewManagement(management.WithRelease(release.DefaultName)), + existingObjects: []runtime.Object{ + release.New(), + template.NewProviderTemplate( + template.WithName(release.DefaultCAPITemplateName), + template.WithProviderStatusCAPIVersion(invalidVersion), + ), + }, + err: fmt.Sprintf("the Management is invalid: invalid CAPI version %s in the ProviderTemplate %s to be validated against: Invalid Semantic Version", invalidVersion, release.DefaultCAPITemplateName), + }, + { + name: "providertemplates without specified capi constraints, should succeed", + management: management.NewManagement( + management.WithRelease(release.DefaultName), + management.WithProviders([]v1alpha1.Provider{providerAwsDefaultTpl}), + ), + existingObjects: []runtime.Object{ + release.New(), + template.NewProviderTemplate( + template.WithName(release.DefaultCAPITemplateName), + template.WithProviderStatusCAPIVersion(versionOne), + ), + template.NewProviderTemplate(), + }, + }, + { + name: "providertemplates with invalid specified capi semver, should fail", + management: management.NewManagement( + management.WithRelease(release.DefaultName), + management.WithProviders([]v1alpha1.Provider{providerAwsDefaultTpl}), + ), + existingObjects: []runtime.Object{ + release.New(), + template.NewProviderTemplate( + template.WithName(release.DefaultCAPITemplateName), + template.WithProviderStatusCAPIVersion(versionOne), + ), + template.NewProviderTemplate( + template.WithProviderStatusCAPIConstraint(invalidConstraint), + ), + }, + err: fmt.Sprintf("the Management is invalid: invalid CAPI version constraint %s in the ProviderTemplate %s: improper constraint: %s", invalidConstraint, template.DefaultName, invalidConstraint), + }, + { + name: "providertemplates do not match capi version, should fail", + management: management.NewManagement( + management.WithRelease(release.DefaultName), + management.WithProviders([]v1alpha1.Provider{providerAwsDefaultTpl}), + ), + existingObjects: []runtime.Object{ + release.New(), + template.NewProviderTemplate( + template.WithName(release.DefaultCAPITemplateName), + template.WithProviderStatusCAPIVersion(versionOne), + ), + template.NewProviderTemplate( + template.WithProviderStatusCAPIConstraint(constraintVerTwo), + ), + }, + warnings: admission.Warnings{"The Management object has incompatible CAPI versions ProviderTemplates"}, + err: fmt.Sprintf("the Management is invalid: core CAPI version %s does not satisfy ProviderTemplate %s constraint %s", versionOne, template.DefaultName, constraintVerTwo), + }, + { + name: "providertemplates match capi version, should succeed", + management: management.NewManagement( + management.WithRelease(release.DefaultName), + management.WithProviders([]v1alpha1.Provider{providerAwsDefaultTpl}), + ), + existingObjects: []runtime.Object{ + release.New(), + template.NewProviderTemplate( + template.WithName(release.DefaultCAPITemplateName), + template.WithProviderStatusCAPIVersion(versionOne), + ), + template.NewProviderTemplate( + template.WithProviderStatusCAPIConstraint(constraintVerOne), + ), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(_ *testing.T) { + c := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithRuntimeObjects(tt.existingObjects...).Build() + validator := &ManagementValidator{Client: c} + + warnings, err := validator.ValidateUpdate(ctx, nil, tt.management) + if tt.err != "" { + g.Expect(err).To(HaveOccurred()) + g.Expect(err).To(MatchError(tt.err)) + } else { + g.Expect(err).To(Succeed()) + } + + g.Expect(warnings).To(Equal(tt.warnings)) + }) + } +} + func TestManagementValidateDelete(t *testing.T) { g := NewWithT(t) - ctx := context.Background() + ctx := admission.NewContextWithRequest(context.Background(), admission.Request{AdmissionRequest: admissionv1.AdmissionRequest{Operation: admissionv1.Delete}}) tests := []struct { name string @@ -55,23 +204,19 @@ func TestManagementValidateDelete(t *testing.T) { } for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { + t.Run(tt.name, func(_ *testing.T) { c := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithRuntimeObjects(tt.existingObjects...).Build() validator := &ManagementValidator{Client: c} + warn, err := validator.ValidateDelete(ctx, tt.management) if tt.err != "" { g.Expect(err).To(HaveOccurred()) - if err.Error() != tt.err { - t.Fatalf("expected error '%s', got error: %s", tt.err, err.Error()) - } + 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()) - } + + g.Expect(warn).To(Equal(tt.warnings)) }) } } diff --git a/templates/provider/hmc/templates/crds/hmc.mirantis.com_clustertemplates.yaml b/templates/provider/hmc/templates/crds/hmc.mirantis.com_clustertemplates.yaml index 91bcd8de5..51fc2e84e 100644 --- a/templates/provider/hmc/templates/crds/hmc.mirantis.com_clustertemplates.yaml +++ b/templates/provider/hmc/templates/crds/hmc.mirantis.com_clustertemplates.yaml @@ -104,59 +104,66 @@ spec: rule: (has(self.chartName) && !has(self.chartRef)) || (!has(self.chartName) && has(self.chartRef)) k8sVersion: - description: Compatible K8S version of the cluster set in the SemVer - format. + description: Kubernetes exact version in the SemVer format provided + by this ClusterTemplate. type: string providers: - description: Providers represent required CAPI providers with constrainted - compatibility versions set. Should be set if not present in the - Helm chart metadata. + description: |- + Providers represent required CAPI providers with constrained compatibility versions set. + Should be set if not present in the Helm chart metadata. + Compatibility attributes are optional to be defined. properties: bootstrap: - description: List of CAPI bootstrap providers with either an exact - or constrainted version in the SemVer format. + description: |- + List of CAPI bootstrap providers with either an exact or constrained version in the SemVer format. + Compatibility attributes are optional to be defined. items: description: Represents name of the provider with either an - exact or constrainted version in the SemVer format. + exact or constrained version in the SemVer format. properties: name: description: Name of the provider. type: string versionOrConstraint: - description: Compatibility restriction in the SemVer format - (exact or constrainted version) + description: |- + Compatibility restriction in the SemVer format (exact or constrained version). + Optional to be defined. type: string type: object type: array controlPlane: - description: List of CAPI control plane providers with either - an exact or constrainted version in the SemVer format. + description: |- + List of CAPI control plane providers with either an exact or constrained version in the SemVer format. + Compatibility attributes are optional to be defined. items: description: Represents name of the provider with either an - exact or constrainted version in the SemVer format. + exact or constrained version in the SemVer format. properties: name: description: Name of the provider. type: string versionOrConstraint: - description: Compatibility restriction in the SemVer format - (exact or constrainted version) + description: |- + Compatibility restriction in the SemVer format (exact or constrained version). + Optional to be defined. type: string type: object type: array infrastructure: - description: List of CAPI infrastructure providers with either - an exact or constrainted version in the SemVer format. + description: |- + List of CAPI infrastructure providers with either an exact or constrained version in the SemVer format. + Compatibility attributes are optional to be defined. items: description: Represents name of the provider with either an - exact or constrainted version in the SemVer format. + exact or constrained version in the SemVer format. properties: name: description: Name of the provider. type: string versionOrConstraint: - description: Compatibility restriction in the SemVer format - (exact or constrainted version) + description: |- + Compatibility restriction in the SemVer format (exact or constrained version). + Optional to be defined. type: string type: object type: array @@ -209,62 +216,69 @@ spec: description: Description contains information about the template. type: string k8sVersion: - description: Compatible K8S version of the cluster set in the SemVer - format. + description: Kubernetes exact version in the SemVer format provided + by this ClusterTemplate. type: string observedGeneration: description: ObservedGeneration is the last observed generation. format: int64 type: integer providers: - description: Providers represent exposed CAPI providers with constrainted - compatibility versions set. + description: |- + Providers represent required CAPI providers with constrained compatibility versions set + if the latter has been given. properties: bootstrap: - description: List of CAPI bootstrap providers with either an exact - or constrainted version in the SemVer format. + description: |- + List of CAPI bootstrap providers with either an exact or constrained version in the SemVer format. + Compatibility attributes are optional to be defined. items: description: Represents name of the provider with either an - exact or constrainted version in the SemVer format. + exact or constrained version in the SemVer format. properties: name: description: Name of the provider. type: string versionOrConstraint: - description: Compatibility restriction in the SemVer format - (exact or constrainted version) + description: |- + Compatibility restriction in the SemVer format (exact or constrained version). + Optional to be defined. type: string type: object type: array controlPlane: - description: List of CAPI control plane providers with either - an exact or constrainted version in the SemVer format. + description: |- + List of CAPI control plane providers with either an exact or constrained version in the SemVer format. + Compatibility attributes are optional to be defined. items: description: Represents name of the provider with either an - exact or constrainted version in the SemVer format. + exact or constrained version in the SemVer format. properties: name: description: Name of the provider. type: string versionOrConstraint: - description: Compatibility restriction in the SemVer format - (exact or constrainted version) + description: |- + Compatibility restriction in the SemVer format (exact or constrained version). + Optional to be defined. type: string type: object type: array infrastructure: - description: List of CAPI infrastructure providers with either - an exact or constrainted version in the SemVer format. + description: |- + List of CAPI infrastructure providers with either an exact or constrained version in the SemVer format. + Compatibility attributes are optional to be defined. items: description: Represents name of the provider with either an - exact or constrainted version in the SemVer format. + exact or constrained version in the SemVer format. properties: name: description: Name of the provider. type: string versionOrConstraint: - description: Compatibility restriction in the SemVer format - (exact or constrainted version) + description: |- + Compatibility restriction in the SemVer format (exact or constrained version). + Optional to be defined. type: string type: object type: array diff --git a/templates/provider/hmc/templates/crds/hmc.mirantis.com_managedclusters.yaml b/templates/provider/hmc/templates/crds/hmc.mirantis.com_managedclusters.yaml index e1fa118ea..6c33ac0bb 100644 --- a/templates/provider/hmc/templates/crds/hmc.mirantis.com_managedclusters.yaml +++ b/templates/provider/hmc/templates/crds/hmc.mirantis.com_managedclusters.yaml @@ -191,67 +191,13 @@ spec: type: array k8sVersion: description: |- - Currently compatible K8S version of the cluster. Being set only if + Currently compatible exact Kubernetes version of the cluster. Being set only if provided by the corresponding ClusterTemplate. type: string observedGeneration: description: ObservedGeneration is the last observed generation. format: int64 type: integer - providers: - description: |- - Providers represent exposed CAPI providers with constrainted compatibility versions set. - Propagated from the corresponding ClusterTemplate. - properties: - bootstrap: - description: List of CAPI bootstrap providers with either an exact - or constrainted version in the SemVer format. - items: - description: Represents name of the provider with either an - exact or constrainted version in the SemVer format. - properties: - name: - description: Name of the provider. - type: string - versionOrConstraint: - description: Compatibility restriction in the SemVer format - (exact or constrainted version) - type: string - type: object - type: array - controlPlane: - description: List of CAPI control plane providers with either - an exact or constrainted version in the SemVer format. - items: - description: Represents name of the provider with either an - exact or constrainted version in the SemVer format. - properties: - name: - description: Name of the provider. - type: string - versionOrConstraint: - description: Compatibility restriction in the SemVer format - (exact or constrainted version) - type: string - type: object - type: array - infrastructure: - description: List of CAPI infrastructure providers with either - an exact or constrainted version in the SemVer format. - items: - description: Represents name of the provider with either an - exact or constrainted version in the SemVer format. - properties: - name: - description: Name of the provider. - type: string - versionOrConstraint: - description: Compatibility restriction in the SemVer format - (exact or constrainted version) - type: string - type: object - type: array - type: object type: object type: object served: true diff --git a/templates/provider/hmc/templates/crds/hmc.mirantis.com_managements.yaml b/templates/provider/hmc/templates/crds/hmc.mirantis.com_managements.yaml index 5f9d66007..8ffcde13e 100644 --- a/templates/provider/hmc/templates/crds/hmc.mirantis.com_managements.yaml +++ b/templates/provider/hmc/templates/crds/hmc.mirantis.com_managements.yaml @@ -117,50 +117,56 @@ spec: their exact compatibility versions if specified in ProviderTemplates on the Management cluster. properties: bootstrap: - description: List of CAPI bootstrap providers with either an exact - or constrainted version in the SemVer format. + description: |- + List of CAPI bootstrap providers with either an exact or constrained version in the SemVer format. + Compatibility attributes are optional to be defined. items: description: Represents name of the provider with either an - exact or constrainted version in the SemVer format. + exact or constrained version in the SemVer format. properties: name: description: Name of the provider. type: string versionOrConstraint: - description: Compatibility restriction in the SemVer format - (exact or constrainted version) + description: |- + Compatibility restriction in the SemVer format (exact or constrained version). + Optional to be defined. type: string type: object type: array controlPlane: - description: List of CAPI control plane providers with either - an exact or constrainted version in the SemVer format. + description: |- + List of CAPI control plane providers with either an exact or constrained version in the SemVer format. + Compatibility attributes are optional to be defined. items: description: Represents name of the provider with either an - exact or constrainted version in the SemVer format. + exact or constrained version in the SemVer format. properties: name: description: Name of the provider. type: string versionOrConstraint: - description: Compatibility restriction in the SemVer format - (exact or constrainted version) + description: |- + Compatibility restriction in the SemVer format (exact or constrained version). + Optional to be defined. type: string type: object type: array infrastructure: - description: List of CAPI infrastructure providers with either - an exact or constrainted version in the SemVer format. + description: |- + List of CAPI infrastructure providers with either an exact or constrained version in the SemVer format. + Compatibility attributes are optional to be defined. items: description: Represents name of the provider with either an - exact or constrainted version in the SemVer format. + exact or constrained version in the SemVer format. properties: name: description: Name of the provider. type: string versionOrConstraint: - description: Compatibility restriction in the SemVer format - (exact or constrainted version) + description: |- + Compatibility restriction in the SemVer format (exact or constrained version). + Optional to be defined. type: string type: object type: array diff --git a/templates/provider/hmc/templates/crds/hmc.mirantis.com_providertemplates.yaml b/templates/provider/hmc/templates/crds/hmc.mirantis.com_providertemplates.yaml index e00635e13..39ddece49 100644 --- a/templates/provider/hmc/templates/crds/hmc.mirantis.com_providertemplates.yaml +++ b/templates/provider/hmc/templates/crds/hmc.mirantis.com_providertemplates.yaml @@ -57,7 +57,14 @@ spec: description: ProviderTemplateSpec defines the desired state of ProviderTemplate properties: capiVersion: - description: Compatible CAPI provider version set in the SemVer format. + description: |- + CAPI exact version in the SemVer format. + Applicable only for the cluster-api ProviderTemplate itself. + type: string + capiVersionConstraint: + description: |- + CAPI version constraint in the SemVer format indicating compatibility with the core CAPI. + Not applicable for the cluster-api ProviderTemplate. type: string helm: description: HelmSpec references a Helm chart representing the HMC @@ -107,69 +114,84 @@ spec: rule: (has(self.chartName) && !has(self.chartRef)) || (!has(self.chartName) && has(self.chartRef)) providers: - description: Represents required CAPI providers with exact compatibility - versions set. Should be set if not present in the Helm chart metadata. + description: |- + Providers represent exposed CAPI providers with exact compatibility versions set. + Should be set if not present in the Helm chart metadata. + Compatibility attributes are optional to be defined. properties: bootstrap: - description: List of CAPI bootstrap providers with either an exact - or constrainted version in the SemVer format. + description: |- + List of CAPI bootstrap providers with either an exact or constrained version in the SemVer format. + Compatibility attributes are optional to be defined. items: description: Represents name of the provider with either an - exact or constrainted version in the SemVer format. + exact or constrained version in the SemVer format. properties: name: description: Name of the provider. type: string versionOrConstraint: - description: Compatibility restriction in the SemVer format - (exact or constrainted version) + description: |- + Compatibility restriction in the SemVer format (exact or constrained version). + Optional to be defined. type: string type: object type: array controlPlane: - description: List of CAPI control plane providers with either - an exact or constrainted version in the SemVer format. + description: |- + List of CAPI control plane providers with either an exact or constrained version in the SemVer format. + Compatibility attributes are optional to be defined. items: description: Represents name of the provider with either an - exact or constrainted version in the SemVer format. + exact or constrained version in the SemVer format. properties: name: description: Name of the provider. type: string versionOrConstraint: - description: Compatibility restriction in the SemVer format - (exact or constrainted version) + description: |- + Compatibility restriction in the SemVer format (exact or constrained version). + Optional to be defined. type: string type: object type: array infrastructure: - description: List of CAPI infrastructure providers with either - an exact or constrainted version in the SemVer format. + description: |- + List of CAPI infrastructure providers with either an exact or constrained version in the SemVer format. + Compatibility attributes are optional to be defined. items: description: Represents name of the provider with either an - exact or constrainted version in the SemVer format. + exact or constrained version in the SemVer format. properties: name: description: Name of the provider. type: string versionOrConstraint: - description: Compatibility restriction in the SemVer format - (exact or constrainted version) + description: |- + Compatibility restriction in the SemVer format (exact or constrained version). + Optional to be defined. type: string type: object type: array type: object - required: - - helm type: object x-kubernetes-validations: - message: Spec is immutable rule: self == oldSelf + - message: Either capiVersion or capiVersionConstraint may be set, but + not both + rule: '!(has(self.capiVersion) && has(self.capiVersionConstraint))' status: description: ProviderTemplateStatus defines the observed state of ProviderTemplate properties: capiVersion: - description: Compatible CAPI provider version in the SemVer format. + description: |- + CAPI exact version in the SemVer format. + Applicable only for the capi Template itself. + type: string + capiVersionConstraint: + description: CAPI version constraint in the SemVer format indicating + compatibility with the core CAPI. type: string chartRef: description: |- @@ -214,54 +236,61 @@ spec: format: int64 type: integer providers: - description: Providers represent exposed CAPI providers with exact - compatibility versions set. + description: |- + Providers represent exposed CAPI providers with exact compatibility versions set + if the latter has been given. properties: bootstrap: - description: List of CAPI bootstrap providers with either an exact - or constrainted version in the SemVer format. + description: |- + List of CAPI bootstrap providers with either an exact or constrained version in the SemVer format. + Compatibility attributes are optional to be defined. items: description: Represents name of the provider with either an - exact or constrainted version in the SemVer format. + exact or constrained version in the SemVer format. properties: name: description: Name of the provider. type: string versionOrConstraint: - description: Compatibility restriction in the SemVer format - (exact or constrainted version) + description: |- + Compatibility restriction in the SemVer format (exact or constrained version). + Optional to be defined. type: string type: object type: array controlPlane: - description: List of CAPI control plane providers with either - an exact or constrainted version in the SemVer format. + description: |- + List of CAPI control plane providers with either an exact or constrained version in the SemVer format. + Compatibility attributes are optional to be defined. items: description: Represents name of the provider with either an - exact or constrainted version in the SemVer format. + exact or constrained version in the SemVer format. properties: name: description: Name of the provider. type: string versionOrConstraint: - description: Compatibility restriction in the SemVer format - (exact or constrainted version) + description: |- + Compatibility restriction in the SemVer format (exact or constrained version). + Optional to be defined. type: string type: object type: array infrastructure: - description: List of CAPI infrastructure providers with either - an exact or constrainted version in the SemVer format. + description: |- + List of CAPI infrastructure providers with either an exact or constrained version in the SemVer format. + Compatibility attributes are optional to be defined. items: description: Represents name of the provider with either an - exact or constrainted version in the SemVer format. + exact or constrained version in the SemVer format. properties: name: description: Name of the provider. type: string versionOrConstraint: - description: Compatibility restriction in the SemVer format - (exact or constrainted version) + description: |- + Compatibility restriction in the SemVer format (exact or constrained version). + Optional to be defined. type: string type: object type: array diff --git a/templates/provider/hmc/templates/crds/hmc.mirantis.com_servicetemplates.yaml b/templates/provider/hmc/templates/crds/hmc.mirantis.com_servicetemplates.yaml index 465f57233..967e71ac7 100644 --- a/templates/provider/hmc/templates/crds/hmc.mirantis.com_servicetemplates.yaml +++ b/templates/provider/hmc/templates/crds/hmc.mirantis.com_servicetemplates.yaml @@ -108,8 +108,9 @@ spec: cluster set in the SemVer format. type: string providers: - description: Represents required CAPI providers. Should be set if - not present in the Helm chart metadata. + description: |- + Providers represent requested CAPI providers. + Should be set if not present in the Helm chart metadata. properties: bootstrap: description: BootstrapProviders is the list of CAPI bootstrap @@ -186,7 +187,7 @@ spec: format: int64 type: integer providers: - description: Represents exposed CAPI providers. + description: Providers represent requested CAPI providers. properties: bootstrap: description: BootstrapProviders is the list of CAPI bootstrap diff --git a/templates/provider/k0smotron/Chart.yaml b/templates/provider/k0smotron/Chart.yaml index ab31a4d89..c453ca418 100644 --- a/templates/provider/k0smotron/Chart.yaml +++ b/templates/provider/k0smotron/Chart.yaml @@ -22,4 +22,4 @@ appVersion: "1.0.4" annotations: hmc.mirantis.com/infrastructure-providers: k0smotron hmc.mirantis.com/bootstrap-providers: k0s - hmc.mirantis.com/control-plane-providers: k0s,k0smotron + hmc.mirantis.com/control-plane-providers: k0s; k0smotron diff --git a/test/kubeclient/kubeclient.go b/test/kubeclient/kubeclient.go index 459e797a2..34edc413c 100644 --- a/test/kubeclient/kubeclient.go +++ b/test/kubeclient/kubeclient.go @@ -34,11 +34,11 @@ import ( ) type KubeClient struct { - Namespace string - Client kubernetes.Interface ExtendedClient apiextensionsclientset.Interface Config *rest.Config + + Namespace string } // NewFromLocal creates a new instance of KubeClient from a given namespace diff --git a/test/objects/managedcluster/managedcluster.go b/test/objects/managedcluster/managedcluster.go index 15a7d1525..4204576a6 100644 --- a/test/objects/managedcluster/managedcluster.go +++ b/test/objects/managedcluster/managedcluster.go @@ -66,12 +66,6 @@ func WithClusterTemplate(templateName string) Opt { } } -func WithK8sVersionStatus(v string) Opt { - return func(managedCluster *v1alpha1.ManagedCluster) { - managedCluster.Status.KubertenesVersion = v - } -} - func WithConfig(config string) Opt { return func(p *v1alpha1.ManagedCluster) { p.Spec.Config = &apiextensionsv1.JSON{ diff --git a/test/objects/management/management.go b/test/objects/management/management.go index 98c5ade15..a6861c804 100644 --- a/test/objects/management/management.go +++ b/test/objects/management/management.go @@ -75,3 +75,9 @@ func WithComponentsStatus(components map[string]v1alpha1.ComponentStatus) Opt { p.Status.Components = components } } + +func WithRelease(v string) Opt { + return func(management *v1alpha1.Management) { + management.Spec.Release = v + } +} diff --git a/test/objects/release/release.go b/test/objects/release/release.go new file mode 100644 index 000000000..ab92e5b4e --- /dev/null +++ b/test/objects/release/release.go @@ -0,0 +1,70 @@ +// 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 release + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/Mirantis/hmc/api/v1alpha1" +) + +const ( + DefaultName = "release-test-0-0-1" + + DefaultCAPITemplateName = "cluster-api-test-0-0-1" + DefaultHMCTemplateName = "hmc-test-0-0-1" +) + +type Opt func(*v1alpha1.Release) + +func New(opts ...Opt) *v1alpha1.Release { + release := &v1alpha1.Release{ + ObjectMeta: metav1.ObjectMeta{ + Name: DefaultName, + }, + Spec: v1alpha1.ReleaseSpec{ + HMC: v1alpha1.CoreProviderTemplate{ + Template: DefaultHMCTemplateName, + }, + CAPI: v1alpha1.CoreProviderTemplate{ + Template: DefaultCAPITemplateName, + }, + }, + } + + for _, opt := range opts { + opt(release) + } + + return release +} + +func WithName(name string) Opt { + return func(r *v1alpha1.Release) { + r.Name = name + } +} + +func WithHMCTemplateName(v string) Opt { + return func(r *v1alpha1.Release) { + r.Spec.HMC.Template = v + } +} + +func WithCAPITemplateName(v string) Opt { + return func(r *v1alpha1.Release) { + r.Spec.CAPI.Template = v + } +} diff --git a/test/objects/template/template.go b/test/objects/template/template.go index 52fcf56fd..f53877503 100644 --- a/test/objects/template/template.go +++ b/test/objects/template/template.go @@ -72,8 +72,7 @@ func NewServiceTemplate(opts ...Opt) *v1alpha1.ServiceTemplate { func NewProviderTemplate(opts ...Opt) *v1alpha1.ProviderTemplate { t := &v1alpha1.ProviderTemplate{ ObjectMeta: metav1.ObjectMeta{ - Name: DefaultName, - Namespace: DefaultNamespace, + Name: DefaultName, }, } @@ -127,7 +126,7 @@ func WithServiceK8sConstraint(v string) Opt { return func(template Template) { switch tt := template.(type) { case *v1alpha1.ServiceTemplate: - tt.Status.KubertenesConstraint = v + tt.Status.KubernetesConstraint = v default: panic(fmt.Sprintf("unexpected obj typed %T, expected *ServiceTemplate", tt)) } @@ -174,3 +173,33 @@ func WithConfigStatus(config string) Opt { } } } + +func WithProviderStatusCAPIVersion(v string) Opt { + return func(template Template) { + pt, ok := template.(*v1alpha1.ProviderTemplate) + if !ok { + panic(fmt.Sprintf("unexpected type %T, expected ProviderTemplate", template)) + } + pt.Status.CAPIVersion = v + } +} + +func WithProviderStatusCAPIConstraint(v string) Opt { + return func(template Template) { + pt, ok := template.(*v1alpha1.ProviderTemplate) + if !ok { + panic(fmt.Sprintf("unexpected type %T, expected ProviderTemplate", template)) + } + pt.Status.CAPIVersionConstraint = v + } +} + +func WithClusterStatusK8sVersion(v string) Opt { + return func(template Template) { + ct, ok := template.(*v1alpha1.ClusterTemplate) + if !ok { + panic(fmt.Sprintf("unexpected type %T, expected ClusterTemplate", template)) + } + ct.Status.KubernetesVersion = v + } +}