From 5c0d401f33a7c1693c6d5e81ab811f2349ac04d8 Mon Sep 17 00:00:00 2001 From: zerospiel Date: Fri, 18 Oct 2024 17:24:42 +0200 Subject: [PATCH] Introduce compatibility based on contracts * removed version/constraints from compatibility attributes * compatibility is now based on CAPI contracts versions * unified providers structure * corresponding adjustments Closes #496 --- api/v1alpha1/clustertemplate_types.go | 16 ++-- api/v1alpha1/common.go | 31 +++---- api/v1alpha1/management_types.go | 4 +- api/v1alpha1/providertemplate_types.go | 86 ++++++------------- api/v1alpha1/servicetemplate_types.go | 4 +- api/v1alpha1/templates_common.go | 35 +++----- api/v1alpha1/zz_generated.deepcopy.go | 59 +++++-------- .../managedcluster_controller_test.go | 4 +- internal/controller/management_controller.go | 12 ++- internal/controller/template_controller.go | 55 +++++------- .../controller/template_controller_test.go | 32 +++---- .../webhook/managedcluster_webhook_test.go | 22 ++--- internal/webhook/management_webhook.go | 44 +++++----- internal/webhook/management_webhook_test.go | 68 ++++----------- .../hmc.mirantis.com_clustertemplates.yaml | 40 +++++---- .../crds/hmc.mirantis.com_managements.yaml | 20 +++-- .../hmc.mirantis.com_providertemplates.yaml | 71 +++++++-------- .../hmc.mirantis.com_servicetemplates.yaml | 34 +++++++- test/objects/management/management.go | 2 +- test/objects/template/template.go | 20 ++--- 20 files changed, 285 insertions(+), 374 deletions(-) diff --git a/api/v1alpha1/clustertemplate_types.go b/api/v1alpha1/clustertemplate_types.go index e8b211ba8..315d8c47e 100644 --- a/api/v1alpha1/clustertemplate_types.go +++ b/api/v1alpha1/clustertemplate_types.go @@ -33,19 +33,19 @@ type ClusterTemplateSpec struct { Helm HelmSpec `json:"helm"` // 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. + // Providers represent required CAPI providers with supported contract versions. // Should be set if not present in the Helm chart metadata. // Compatibility attributes are optional to be defined. - Providers ProvidersTupled `json:"providers,omitempty"` + Providers Providers `json:"providers,omitempty"` } // ClusterTemplateStatus defines the observed state of ClusterTemplate type ClusterTemplateStatus struct { // 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 + // Providers represent required CAPI providers with supported contract versions // if the latter has been given. - Providers ProvidersTupled `json:"providers,omitempty"` + Providers Providers `json:"providers,omitempty"` TemplateStatusCommon `json:",inline"` } @@ -53,11 +53,7 @@ type ClusterTemplateStatus struct { // FillStatusWithProviders sets the status of the template with providers // either from the spec or from the given annotations. func (t *ClusterTemplate) FillStatusWithProviders(annotations map[string]string) error { - var err error - t.Status.Providers, err = parseProviders(t, annotations, semver.NewConstraint) - if err != nil { - return fmt.Errorf("failed to parse ClusterTemplate providers: %v", err) - } + t.Status.Providers = parseProviders(t, annotations) kversion := annotations[ChartAnnotationKubernetesVersion] if t.Spec.KubernetesVersion != "" { @@ -77,7 +73,7 @@ func (t *ClusterTemplate) FillStatusWithProviders(annotations map[string]string) } // GetSpecProviders returns .spec.providers of the Template. -func (t *ClusterTemplate) GetSpecProviders() ProvidersTupled { +func (t *ClusterTemplate) GetSpecProviders() Providers { return t.Spec.Providers } diff --git a/api/v1alpha1/common.go b/api/v1alpha1/common.go index fdba2d2f6..3f6c8232a 100644 --- a/api/v1alpha1/common.go +++ b/api/v1alpha1/common.go @@ -36,29 +36,22 @@ const ( ) type ( - // TODO (zerospiel): unite with the versioned as part of the [Contracts support]. + // Holds different types of CAPI providers with [compatible contract versions]. // - // [Contracts support]: https://github.com/Mirantis/hmc/issues/496 + // [compatible contract versions]: https://cluster-api.sigs.k8s.io/developer/providers/contracts + Providers []NameContract - // Providers hold different types of CAPI providers. - Providers []string - - // Holds different types of CAPI providers with either - // an exact or constrained version in the SemVer format. The requirement - // is determined by a consumer of this type. - - // Holds different types of CAPI providers with either - // an exact or constrained version in the SemVer format. The requirement - // is determined by a consumer of this type. - ProvidersTupled []ProviderTuple - - // Represents name of the provider with either an exact or constrained version in the SemVer format. - ProviderTuple struct { + // Represents name of the provider with its provider compatibility [contract versions]. + // + // [contract versions]: https://cluster-api.sigs.k8s.io/developer/providers/contracts + NameContract struct { // Name of the provider. Name string `json:"name,omitempty"` - // Compatibility restriction in the SemVer format (exact or constrained version). + // Compatibility restriction in the [CAPI provider format]. The value is an underscore-delimited (_) list of versions. // Optional to be defined. - VersionOrConstraint string `json:"versionOrConstraint,omitempty"` + // + // [CAPI provider format]: https://cluster-api.sigs.k8s.io/developer/providers/contracts#api-version-labels + ContractVersion string `json:"contractVersion,omitempty"` } ) @@ -137,7 +130,7 @@ func ExtractServiceTemplateName(rawObj client.Object) []string { } // Names flattens the underlaying slice to provider names slice and returns it. -func (c ProvidersTupled) Names() []string { +func (c Providers) Names() []string { nn := make([]string, len(c)) for i, v := range c { nn[i] = v.Name diff --git a/api/v1alpha1/management_types.go b/api/v1alpha1/management_types.go index 7c036ab3b..bc1adf74b 100644 --- a/api/v1alpha1/management_types.go +++ b/api/v1alpha1/management_types.go @@ -92,8 +92,8 @@ type ManagementStatus struct { // Release indicates the current Release object. Release string `json:"release,omitempty"` // AvailableProviders holds all CAPI providers available along with - // their exact compatibility versions if specified in ProviderTemplates on the Management cluster. - AvailableProviders ProvidersTupled `json:"availableProviders,omitempty"` + // their supported contract versions, if specified in ProviderTemplates, on the Management cluster. + AvailableProviders Providers `json:"availableProviders,omitempty"` // ObservedGeneration is the last observed generation. ObservedGeneration int64 `json:"observedGeneration,omitempty"` } diff --git a/api/v1alpha1/providertemplate_types.go b/api/v1alpha1/providertemplate_types.go index ca7ebd6cf..15f682a8e 100644 --- a/api/v1alpha1/providertemplate_types.go +++ b/api/v1alpha1/providertemplate_types.go @@ -15,46 +15,39 @@ package v1alpha1 import ( - "fmt" - - "github.com/Masterminds/semver/v3" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) 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" + // ChartAnnotationCAPIContractVersion is an annotation containing the expected core CAPI contract version (e.g. v1beta1) associated with a ProviderTemplate. + ChartAnnotationCAPIContractVersion = "hmc.mirantis.com/capi-version" ) -// +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,omitempty"` - // CAPI exact version in the SemVer format. - // Applicable only for the cluster-api ProviderTemplate itself. - CAPIVersion string `json:"capiVersion,omitempty"` - // 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. + // CAPI [contract version] indicating compatibility with the core CAPI. + // Currently supported versions: v1alpha3_v1alpha4_v1beta1. + // The field is not applicable for the cluster-api ProviderTemplate. + // + // [contract version]: https://cluster-api.sigs.k8s.io/developer/providers/contracts + CAPIContractVersion string `json:"capiContractVersion,omitempty"` + // Providers represent exposed CAPI providers with supported contract versions. // Should be set if not present in the Helm chart metadata. - // Compatibility attributes are optional to be defined. - Providers ProvidersTupled `json:"providers,omitempty"` + // Supported contract versions are optional to be defined. + Providers Providers `json:"providers,omitempty"` } // ProviderTemplateStatus defines the observed state of ProviderTemplate type ProviderTemplateStatus struct { - // CAPI exact version in the SemVer format. - // Applicable only for the capi Template itself. - CAPIVersion string `json:"capiVersion,omitempty"` - // 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 + // CAPI [contract version] indicating compatibility with the core CAPI. + // Currently supported versions: v1alpha3_v1alpha4_v1beta1. + // + // [contract version]: https://cluster-api.sigs.k8s.io/developer/providers/contracts + CAPIContractVersion string `json:"capiContractVersion,omitempty"` + // Providers represent exposed CAPI providers with supported contract versions // if the latter has been given. - Providers ProvidersTupled `json:"providers,omitempty"` + Providers Providers `json:"providers,omitempty"` TemplateStatusCommon `json:",inline"` } @@ -62,47 +55,24 @@ type ProviderTemplateStatus struct { // FillStatusWithProviders sets the status of the template with providers // either from the spec or from the given annotations. func (t *ProviderTemplate) FillStatusWithProviders(annotations map[string]string) error { - var err error - t.Status.Providers, err = parseProviders(t, annotations, semver.NewVersion) - if err != nil { - return fmt.Errorf("failed to parse ProviderTemplate providers: %v", err) - } + t.Status.Providers = parseProviders(t, annotations) 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 + return nil } + requiredCAPIContract := annotations[ChartAnnotationCAPIContractVersion] + if t.Spec.CAPIContractVersion != "" { + requiredCAPIContract = t.Spec.CAPIContractVersion + } + + t.Status.CAPIContractVersion = requiredCAPIContract + return nil } // GetSpecProviders returns .spec.providers of the Template. -func (t *ProviderTemplate) GetSpecProviders() ProvidersTupled { +func (t *ProviderTemplate) GetSpecProviders() Providers { return t.Spec.Providers } diff --git a/api/v1alpha1/servicetemplate_types.go b/api/v1alpha1/servicetemplate_types.go index 70aaf781d..7bf0d5142 100644 --- a/api/v1alpha1/servicetemplate_types.go +++ b/api/v1alpha1/servicetemplate_types.go @@ -57,11 +57,11 @@ func (t *ServiceTemplate) FillStatusWithProviders(annotations map[string]string) t.Status.Providers = t.Spec.Providers } else { splitted := strings.Split(providers, multiProviderSeparator) - t.Status.Providers = make([]string, 0, len(splitted)) + t.Status.Providers = make(Providers, 0, len(splitted)) t.Status.Providers = append(t.Status.Providers, t.Spec.Providers...) for _, v := range splitted { if c := strings.TrimSpace(v); c != "" { - t.Status.Providers = append(t.Status.Providers, c) + t.Status.Providers = append(t.Status.Providers, NameContract{Name: c}) } } } diff --git a/api/v1alpha1/templates_common.go b/api/v1alpha1/templates_common.go index 688b28e86..5fe0ebfe8 100644 --- a/api/v1alpha1/templates_common.go +++ b/api/v1alpha1/templates_common.go @@ -15,8 +15,6 @@ package v1alpha1 import ( - "errors" - "fmt" "strings" helmcontrollerv2 "github.com/fluxcd/helm-controller/api/v2" @@ -74,49 +72,36 @@ type TemplateValidationStatus struct { Valid bool `json:"valid"` } -// TODO (zerospiel): change to comma as part of the [Contracts support]. -// -// [Contracts support]: https://github.com/Mirantis/hmc/issues/496 -const multiProviderSeparator = ";" +const multiProviderSeparator = "," -// TODO (zerospiel): move to the template-ctrl? -func parseProviders[T any](providersGetter interface{ GetSpecProviders() ProvidersTupled }, annotations map[string]string, validationFn func(string) (T, error)) ([]ProviderTuple, error) { +func parseProviders(providersGetter interface{ GetSpecProviders() Providers }, annotations map[string]string) []NameContract { providers := annotations[ChartAnnotationProviderName] if len(providers) == 0 { - return providersGetter.GetSpecProviders(), nil + return providersGetter.GetSpecProviders() } var ( ps = providersGetter.GetSpecProviders() splitted = strings.Split(providers, multiProviderSeparator) - pstatus = make([]ProviderTuple, 0, len(splitted)+len(ps)) - merr error + pstatus = make([]NameContract, 0, len(splitted)+len(ps)) ) pstatus = append(pstatus, ps...) for _, v := range splitted { v = strings.TrimSpace(v) - nVerOrC := strings.SplitN(v, " ", 2) - if len(nVerOrC) == 0 { // BCE (bound check elimination) + nameContract := strings.SplitN(v, " ", 2) + if len(nameContract) == 0 { // BCE (bound check elimination) continue } - n := ProviderTuple{Name: nVerOrC[0]} - if len(nVerOrC) < 2 { - pstatus = append(pstatus, n) - continue - } - - ver := strings.TrimSpace(nVerOrC[1]) - if _, err := validationFn(ver); err != nil { // validation - merr = errors.Join(merr, fmt.Errorf("failed to parse %s in the %s: %v", ver, v, err)) - continue + n := NameContract{Name: nameContract[0]} + if len(nameContract) > 1 { + n.ContractVersion = nameContract[1] } - n.VersionOrConstraint = ver pstatus = append(pstatus, n) } - return pstatus, merr + return pstatus } diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 706a72b05..543a99e4d 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -190,7 +190,7 @@ func (in *ClusterTemplateSpec) DeepCopyInto(out *ClusterTemplateSpec) { in.Helm.DeepCopyInto(&out.Helm) if in.Providers != nil { in, out := &in.Providers, &out.Providers - *out = make(ProvidersTupled, len(*in)) + *out = make(Providers, len(*in)) copy(*out, *in) } } @@ -210,7 +210,7 @@ func (in *ClusterTemplateStatus) DeepCopyInto(out *ClusterTemplateStatus) { *out = *in if in.Providers != nil { in, out := &in.Providers, &out.Providers - *out = make(ProvidersTupled, len(*in)) + *out = make(Providers, len(*in)) copy(*out, *in) } in.TemplateStatusCommon.DeepCopyInto(&out.TemplateStatusCommon) @@ -613,7 +613,7 @@ func (in *ManagementStatus) DeepCopyInto(out *ManagementStatus) { } if in.AvailableProviders != nil { in, out := &in.AvailableProviders, &out.AvailableProviders - *out = make(ProvidersTupled, len(*in)) + *out = make(Providers, len(*in)) copy(*out, *in) } } @@ -725,6 +725,21 @@ func (in *MultiClusterServiceStatus) DeepCopy() *MultiClusterServiceStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NameContract) DeepCopyInto(out *NameContract) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NameContract. +func (in *NameContract) DeepCopy() *NameContract { + if in == nil { + return nil + } + out := new(NameContract) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NamedProviderTemplate) DeepCopyInto(out *NamedProviderTemplate) { *out = *in @@ -822,7 +837,7 @@ func (in *ProviderTemplateSpec) DeepCopyInto(out *ProviderTemplateSpec) { in.Helm.DeepCopyInto(&out.Helm) if in.Providers != nil { in, out := &in.Providers, &out.Providers - *out = make(ProvidersTupled, len(*in)) + *out = make(Providers, len(*in)) copy(*out, *in) } } @@ -842,7 +857,7 @@ func (in *ProviderTemplateStatus) DeepCopyInto(out *ProviderTemplateStatus) { *out = *in if in.Providers != nil { in, out := &in.Providers, &out.Providers - *out = make(ProvidersTupled, len(*in)) + *out = make(Providers, len(*in)) copy(*out, *in) } in.TemplateStatusCommon.DeepCopyInto(&out.TemplateStatusCommon) @@ -858,21 +873,6 @@ func (in *ProviderTemplateStatus) DeepCopy() *ProviderTemplateStatus { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ProviderTuple) DeepCopyInto(out *ProviderTuple) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderTuple. -func (in *ProviderTuple) DeepCopy() *ProviderTuple { - if in == nil { - return nil - } - out := new(ProviderTuple) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in Providers) DeepCopyInto(out *Providers) { { @@ -892,25 +892,6 @@ func (in Providers) DeepCopy() Providers { return *out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in ProvidersTupled) DeepCopyInto(out *ProvidersTupled) { - { - in := &in - *out = make(ProvidersTupled, len(*in)) - copy(*out, *in) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProvidersTupled. -func (in ProvidersTupled) DeepCopy() ProvidersTupled { - if in == nil { - return nil - } - out := new(ProvidersTupled) - in.DeepCopyInto(out) - return *out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Release) DeepCopyInto(out *Release) { *out = *in diff --git a/internal/controller/managedcluster_controller_test.go b/internal/controller/managedcluster_controller_test.go index 029b544a9..9a8f9bfd0 100644 --- a/internal/controller/managedcluster_controller_test.go +++ b/internal/controller/managedcluster_controller_test.go @@ -94,7 +94,7 @@ var _ = Describe("ManagedCluster Controller", func() { Raw: []byte(`{"foo":"bar"}`), }, }, - Providers: hmc.ProvidersTupled{{Name: "infrastructure-aws"}}, + Providers: hmc.Providers{{Name: "infrastructure-aws"}}, } Expect(k8sClient.Status().Update(ctx, template)).To(Succeed()) } @@ -110,7 +110,7 @@ var _ = Describe("ManagedCluster Controller", func() { } Expect(k8sClient.Create(ctx, management)).To(Succeed()) management.Status = hmc.ManagementStatus{ - AvailableProviders: hmc.ProvidersTupled{{Name: "infrastructure-aws"}}, + AvailableProviders: hmc.Providers{{Name: "infrastructure-aws"}}, } Expect(k8sClient.Status().Update(ctx, management)).To(Succeed()) } diff --git a/internal/controller/management_controller.go b/internal/controller/management_controller.go index 537dc1e43..dc75f9d83 100644 --- a/internal/controller/management_controller.go +++ b/internal/controller/management_controller.go @@ -107,7 +107,7 @@ func (r *ManagementReconciler) Update(ctx context.Context, management *hmc.Manag } var errs error - detectedProviders := hmc.ProvidersTupled{} + detectedProviders := hmc.Providers{} detectedComponents := make(map[string]hmc.ComponentStatus) err := r.enableAdditionalComponents(ctx, management) @@ -414,10 +414,10 @@ func (r *ManagementReconciler) enableAdditionalComponents(ctx context.Context, m func updateComponentsStatus( components map[string]hmc.ComponentStatus, - providers *hmc.ProvidersTupled, + providers *hmc.Providers, componentName string, templateName string, - templateProviders hmc.ProvidersTupled, + templateProviders hmc.Providers, err string, ) { components[componentName] = hmc.ComponentStatus{ @@ -426,9 +426,13 @@ func updateComponentsStatus( Template: templateName, } + // TODO: do we actually need to partially include supported versions? The validation is taking + // place only when the actual provider template had been created, so either we have to perform + // validation here in the controller or change templates status via the validation webhook. + // Or just use the fixed versions, so no partially supported contracts would exist. if err == "" { *providers = append(*providers, templateProviders...) - slices.SortFunc(*providers, func(a, b hmc.ProviderTuple) int { return strings.Compare(a.Name, b.Name) }) + slices.SortFunc(*providers, func(a, b hmc.NameContract) int { return strings.Compare(a.Name, b.Name) }) *providers = slices.Compact(*providers) } } diff --git a/internal/controller/template_controller.go b/internal/controller/template_controller.go index 9e0c403a9..73f218354 100644 --- a/internal/controller/template_controller.go +++ b/internal/controller/template_controller.go @@ -20,9 +20,9 @@ import ( "errors" "fmt" "slices" + "strings" "time" - "github.com/Masterminds/semver/v3" helmcontrollerv2 "github.com/fluxcd/helm-controller/api/v2" sourcev1 "github.com/fluxcd/source-controller/api/v1" "helm.sh/helm/v3/pkg/chart" @@ -344,8 +344,8 @@ func (r *ClusterTemplateReconciler) validateCompatibilityAttrs(ctx context.Conte ctrl.LoggerFrom(ctx).V(1).Info("providers to check", "exposed", exposedProviders, "required", requiredProviders) var merr error - missing, wrong, parsing := collectMissingProvidersWithWrongVersions(exposedProviders, requiredProviders) - merr = errors.Join(merr, missing, wrong, parsing) + missing, wrong := collectMissingProvidersWithWrongVersions(exposedProviders, requiredProviders) + merr = errors.Join(merr, missing, wrong) if merr != nil { _ = r.updateStatus(ctx, template, merr.Error()) @@ -355,43 +355,36 @@ func (r *ClusterTemplateReconciler) validateCompatibilityAttrs(ctx context.Conte return r.updateStatus(ctx, template, "") } -// collectMissingProvidersWithWrongVersions returns collected errors for missing providers, providers with -// wrong versions that do not satisfy the corresponding constraints, and parsing errors respectevly. -func collectMissingProvidersWithWrongVersions(exposed, required []hmc.ProviderTuple) (missingErr, nonSatisfyingErr, parsingErr error) { - exposedSet := make(map[string]hmc.ProviderTuple, len(exposed)) +// collectMissingProvidersWithWrongVersions returns collected errors for missing providers and providers with +// unsatisfying contract versions. +func collectMissingProvidersWithWrongVersions(exposed, required []hmc.NameContract) (missingErr, nonSatisfyingErr error) { + exposed2Contracts := make(map[string][]string, len(exposed)) for _, v := range exposed { - exposedSet[v.Name] = v + if v.ContractVersion != "" { + exposed2Contracts[v.Name] = strings.Split(v.ContractVersion, "_") + } else { + exposed2Contracts[v.Name] = nil + } } var missing, nonSatisfying []string - for _, reqWithConstraint := range required { - exposedWithExactVer, ok := exposedSet[reqWithConstraint.Name] + for _, requiredProvider := range required { + exposedContracts, ok := exposed2Contracts[requiredProvider.Name] if !ok { - missing = append(missing, reqWithConstraint.Name) - continue - } - - version := exposedWithExactVer.VersionOrConstraint - constraint := reqWithConstraint.VersionOrConstraint - - if version == "" || constraint == "" { + missing = append(missing, requiredProvider.Name) continue } - exactVer, err := semver.NewVersion(version) - if err != nil { - parsingErr = errors.Join(parsingErr, fmt.Errorf("failed to parse version %s of the provider %s: %w", version, exposedWithExactVer.Name, err)) + if len(exposedContracts) == 0 || requiredProvider.ContractVersion == "" { continue } - requiredC, err := semver.NewConstraint(constraint) - if err != nil { - parsingErr = errors.Join(parsingErr, fmt.Errorf("failed to parse constraint %s of the provider %s: %w", version, exposedWithExactVer.Name, err)) - continue - } + requiredContracts := strings.Split(requiredProvider.ContractVersion, "_") // must be only one, but better to check twice - if !requiredC.Check(exactVer) { - nonSatisfying = append(nonSatisfying, fmt.Sprintf("%s %s !~ %s", reqWithConstraint.Name, version, constraint)) + for _, rc := range requiredContracts { + if !slices.Contains(exposedContracts, rc) { + nonSatisfying = append(nonSatisfying, fmt.Sprintf("%s %s !~ %v", requiredProvider.Name, rc, exposedContracts)) + } } } @@ -405,11 +398,7 @@ func collectMissingProvidersWithWrongVersions(exposed, required []hmc.ProviderTu nonSatisfyingErr = fmt.Errorf("one or more required providers does not satisfy constraints: %v", nonSatisfying) } - if parsingErr != nil { - parsingErr = fmt.Errorf("one or more errors parsing providers' versions and constraints : %v", parsingErr) - } - - return missingErr, nonSatisfyingErr, parsingErr + return missingErr, nonSatisfyingErr } // SetupWithManager sets up the controller with the Manager. diff --git a/internal/controller/template_controller_test.go b/internal/controller/template_controller_test.go index fe6cf6d4a..5d011426f 100644 --- a/internal/controller/template_controller_test.go +++ b/internal/controller/template_controller_test.go @@ -198,11 +198,11 @@ var _ = Describe("Template Controller", func() { It("should successfully validate cluster templates providers compatibility attributes", func() { const ( - clusterTemplateName = "cluster-template-test-name" - mgmtName = hmcmirantiscomv1alpha1.ManagementName - someProviderName = "test-provider-name" - someProviderVersion = "v1.0.0" - someProviderVersionConstraint = ">= 1.0.0 <2.0.0-0" // ^1.0.0 + clusterTemplateName = "cluster-template-test-name" + mgmtName = hmcmirantiscomv1alpha1.ManagementName + someProviderName = "test-provider-name" + someRequiredContract = "v1beta1" + someExposedContract = "v1beta1_v1beta2" timeout = time.Second * 10 interval = time.Millisecond * 250 @@ -217,10 +217,10 @@ var _ = Describe("Template Controller", func() { }, Spec: hmcmirantiscomv1alpha1.ClusterTemplateSpec{ Helm: helmSpec, - Providers: []hmcmirantiscomv1alpha1.ProviderTuple{ + Providers: []hmcmirantiscomv1alpha1.NameContract{ { - Name: someProviderName, - VersionOrConstraint: someProviderVersionConstraint, // constraint + Name: someProviderName, + ContractVersion: someRequiredContract, }, }, }, @@ -237,8 +237,8 @@ var _ = Describe("Template Controller", func() { return fmt.Errorf("expected .spec.providers length to be exactly 1, got %d", l) } - if v := clusterTemplate.Spec.Providers[0]; v.Name != someProviderName || v.VersionOrConstraint != someProviderVersionConstraint { - return fmt.Errorf("expected .spec.providers[0] to be %s:%s, got %s:%s", someProviderName, someProviderVersionConstraint, v.Name, v.VersionOrConstraint) + if v := clusterTemplate.Spec.Providers[0]; v.Name != someProviderName || v.ContractVersion != someRequiredContract { + return fmt.Errorf("expected .spec.providers[0] to be %s:%s, got %s:%s", someProviderName, someRequiredContract, v.Name, v.ContractVersion) } return nil @@ -253,10 +253,10 @@ var _ = Describe("Template Controller", func() { } Expect(k8sClient.Create(ctx, mgmt)).To(Succeed()) mgmt.Status = hmcmirantiscomv1alpha1.ManagementStatus{ - AvailableProviders: []hmcmirantiscomv1alpha1.ProviderTuple{ + AvailableProviders: []hmcmirantiscomv1alpha1.NameContract{ { - Name: someProviderName, - VersionOrConstraint: someProviderVersion, // version + Name: someProviderName, + ContractVersion: someExposedContract, }, }, } @@ -272,8 +272,8 @@ var _ = Describe("Template Controller", func() { return fmt.Errorf("expected .status.availableProviders length to be exactly 1, got %d", l) } - if v := mgmt.Status.AvailableProviders[0]; v.Name != someProviderName || v.VersionOrConstraint != someProviderVersion { - return fmt.Errorf("expected .status.availableProviders[0] to be %s:%s, got %s:%s", someProviderName, someProviderVersionConstraint, v.Name, v.VersionOrConstraint) + if v := mgmt.Status.AvailableProviders[0]; v.Name != someProviderName || v.ContractVersion != someExposedContract { + return fmt.Errorf("expected .status.availableProviders[0] to be %s:%s, got %s:%s", someProviderName, someExposedContract, v.Name, v.ContractVersion) } return nil @@ -294,7 +294,7 @@ var _ = Describe("Template Controller", func() { Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(clusterTemplate), clusterTemplate)).To(Succeed()) Expect(clusterTemplate.Status.Valid && clusterTemplate.Status.ValidationError == "").To(BeTrue()) Expect(clusterTemplate.Status.Providers).To(HaveLen(1)) - Expect(clusterTemplate.Status.Providers[0]).To(Equal(hmcmirantiscomv1alpha1.ProviderTuple{Name: someProviderName, VersionOrConstraint: someProviderVersionConstraint})) + Expect(clusterTemplate.Status.Providers[0]).To(Equal(hmcmirantiscomv1alpha1.NameContract{Name: someProviderName, ContractVersion: someRequiredContract})) By("Removing the created objects") Expect(k8sClient.Delete(ctx, mgmt)).To(Succeed()) diff --git a/internal/webhook/managedcluster_webhook_test.go b/internal/webhook/managedcluster_webhook_test.go index ad8261de6..398f3a507 100644 --- a/internal/webhook/managedcluster_webhook_test.go +++ b/internal/webhook/managedcluster_webhook_test.go @@ -42,7 +42,7 @@ var ( testNamespace = "test" mgmt = management.NewManagement( - management.WithAvailableProviders(v1alpha1.ProvidersTupled{ + management.WithAvailableProviders(v1alpha1.Providers{ {Name: "infrastructure-aws"}, {Name: "control-plane-k0s"}, {Name: "bootstrap-k0s"}, @@ -127,7 +127,7 @@ func TestManagedClusterValidateCreate(t *testing.T) { cred, template.NewClusterTemplate( template.WithName(testTemplateName), - template.WithProvidersStatus(v1alpha1.ProvidersTupled{ + template.WithProvidersStatus(v1alpha1.Providers{ {Name: "infrastructure-aws"}, {Name: "control-plane-k0s"}, {Name: "bootstrap-k0s"}, @@ -144,10 +144,10 @@ func TestManagedClusterValidateCreate(t *testing.T) { ), existingObjects: []runtime.Object{ cred, - management.NewManagement(management.WithAvailableProviders(v1alpha1.ProvidersTupled{ - {Name: "infrastructure-aws", VersionOrConstraint: "v1.0.0"}, - {Name: "control-plane-k0s", VersionOrConstraint: "v1.0.0"}, - {Name: "bootstrap-k0s", VersionOrConstraint: "v1.0.0"}, + management.NewManagement(management.WithAvailableProviders(v1alpha1.Providers{ + {Name: "infrastructure-aws"}, + {Name: "control-plane-k0s"}, + {Name: "bootstrap-k0s"}, })), template.NewClusterTemplate( template.WithName(testTemplateName), @@ -170,7 +170,7 @@ func TestManagedClusterValidateCreate(t *testing.T) { mgmt, template.NewClusterTemplate( template.WithName(testTemplateName), - template.WithProvidersStatus(v1alpha1.ProvidersTupled{ + template.WithProvidersStatus(v1alpha1.Providers{ {Name: "infrastructure-aws"}, {Name: "control-plane-k0s"}, {Name: "bootstrap-k0s"}, @@ -199,7 +199,7 @@ func TestManagedClusterValidateCreate(t *testing.T) { ), template.NewClusterTemplate( template.WithName(testTemplateName), - template.WithProvidersStatus(v1alpha1.ProvidersTupled{ + template.WithProvidersStatus(v1alpha1.Providers{ {Name: "infrastructure-aws"}, {Name: "control-plane-k0s"}, {Name: "bootstrap-k0s"}, @@ -218,7 +218,7 @@ func TestManagedClusterValidateCreate(t *testing.T) { existingObjects: []runtime.Object{ cred, management.NewManagement( - management.WithAvailableProviders(v1alpha1.ProvidersTupled{ + management.WithAvailableProviders(v1alpha1.Providers{ {Name: "infrastructure-azure"}, {Name: "control-plane-k0s"}, {Name: "bootstrap-k0s"}, @@ -226,7 +226,7 @@ func TestManagedClusterValidateCreate(t *testing.T) { ), template.NewClusterTemplate( template.WithName(testTemplateName), - template.WithProvidersStatus(v1alpha1.ProvidersTupled{ + template.WithProvidersStatus(v1alpha1.Providers{ {Name: "infrastructure-azure"}, {Name: "control-plane-k0s"}, {Name: "bootstrap-k0s"}, @@ -310,7 +310,7 @@ func TestManagedClusterValidateUpdate(t *testing.T) { Valid: false, ValidationError: "validation error example", }), - template.WithProvidersStatus(v1alpha1.ProvidersTupled{ + template.WithProvidersStatus(v1alpha1.Providers{ {Name: "infrastructure-aws"}, {Name: "control-plane-k0s"}, {Name: "bootstrap-k0s"}, diff --git a/internal/webhook/management_webhook.go b/internal/webhook/management_webhook.go index b0c0f1704..c15f38cfd 100644 --- a/internal/webhook/management_webhook.go +++ b/internal/webhook/management_webhook.go @@ -18,6 +18,8 @@ import ( "context" "errors" "fmt" + "slices" + "strings" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" @@ -26,7 +28,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" - "github.com/Masterminds/semver/v3" hmcv1alpha1 "github.com/Mirantis/hmc/api/v1alpha1" ) @@ -78,19 +79,18 @@ func (v *ManagementValidator) ValidateUpdate(ctx context.Context, _, newObj runt 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 - } + supportedCAPIContractVersions := []string{"v1alpha3", "v1alpha4", "v1beta1"} + // TODO: i think it's better to just have a simple list instead of parcing a CRD on each update event + less groups for the rbac + // clusterCAPI := new(crdv1.CustomResourceDefinition) + // if err := v.Get(ctx, client.ObjectKey{Name: "clusters.cluster.x-k8s.io"}, clusterCAPI); err != nil { + // return nil, fmt.Errorf("failed to get Cluster CRD: %w", err) + // } - 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) - } + // supportedCAPIContractVersions := make([]string, len(clusterCAPI.Spec.Versions)) + // for _, v := range clusterCAPI.Spec.Versions { + // // TODO: we can actually skip the depreceted just in case + // supportedCAPIContractVersions = append(supportedCAPIContractVersions, v.Name) + // } var wrongVersions error for _, p := range mgmt.Spec.Providers { @@ -99,7 +99,7 @@ func (v *ManagementValidator) ValidateUpdate(ctx context.Context, _, newObj runt tplName = release.ProviderTemplate(p.Name) } - if tplName == capiTpl.Name { // skip capi itself + if tplName == capiTplName { // skip capi itself continue } @@ -108,22 +108,20 @@ func (v *ManagementValidator) ValidateUpdate(ctx context.Context, _, newObj runt return nil, fmt.Errorf("failed to get ProviderTemplate %s: %w", tplName, err) } - if pTpl.Status.CAPIVersionConstraint == "" { + if pTpl.Status.CAPIContractVersion == "" { 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)) + expectedCAPIContractVersions := strings.Split(pTpl.Status.CAPIContractVersion, "_") + for _, ev := range expectedCAPIContractVersions { + if !slices.Contains(supportedCAPIContractVersions, ev) { + wrongVersions = errors.Join(wrongVersions, fmt.Errorf("core CAPI contract versions %v does not support ProviderTemplate %s contract %s", supportedCAPIContractVersions, pTpl.Name, ev)) + } } } if wrongVersions != nil { - return admission.Warnings{"The Management object has incompatible CAPI versions ProviderTemplates"}, fmt.Errorf("%s: %s", invalidMgmtMsg, wrongVersions) + return admission.Warnings{"The Management object has incompatible CAPI contract versions in ProviderTemplates"}, fmt.Errorf("%s: %s", invalidMgmtMsg, wrongVersions) } return nil, nil diff --git a/internal/webhook/management_webhook_test.go b/internal/webhook/management_webhook_test.go index a3d311475..e56eccf5a 100644 --- a/internal/webhook/management_webhook_test.go +++ b/internal/webhook/management_webhook_test.go @@ -39,10 +39,8 @@ func TestManagementValidateUpdate(t *testing.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" + contractVersionValid = "v1alpha4_v1beta1" + contractVersionInvalid = "v1alpha1_v1alpha4_v1beta1" ) providerAwsDefaultTpl := v1alpha1.Provider{ @@ -52,6 +50,8 @@ func TestManagementValidateUpdate(t *testing.T) { }, } + supportedCAPIContractVersions := []string{"v1alpha3", "v1alpha4", "v1beta1"} + tests := []struct { name string management *v1alpha1.Management @@ -64,48 +64,16 @@ func TestManagementValidateUpdate(t *testing.T) { 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", + name: "no providertemplates having providers in mgmt spec, 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(), - }, + existingObjects: []runtime.Object{release.New()}, + err: fmt.Sprintf(`failed to get ProviderTemplate %s: providertemplates.hmc.mirantis.com "%s" not found`, template.DefaultName, template.DefaultName), }, { - name: "providertemplates with invalid specified capi semver, should fail", + name: "providertemplates without specified capi contracts, should succeed", management: management.NewManagement( management.WithRelease(release.DefaultName), management.WithProviders([]v1alpha1.Provider{providerAwsDefaultTpl}), @@ -114,16 +82,12 @@ func TestManagementValidateUpdate(t *testing.T) { release.New(), template.NewProviderTemplate( template.WithName(release.DefaultCAPITemplateName), - template.WithProviderStatusCAPIVersion(versionOne), - ), - template.NewProviderTemplate( - template.WithProviderStatusCAPIConstraint(invalidConstraint), ), + template.NewProviderTemplate(), }, - 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", + name: "providertemplates do not match capi contracts, should fail", management: management.NewManagement( management.WithRelease(release.DefaultName), management.WithProviders([]v1alpha1.Provider{providerAwsDefaultTpl}), @@ -132,17 +96,16 @@ func TestManagementValidateUpdate(t *testing.T) { release.New(), template.NewProviderTemplate( template.WithName(release.DefaultCAPITemplateName), - template.WithProviderStatusCAPIVersion(versionOne), ), template.NewProviderTemplate( - template.WithProviderStatusCAPIConstraint(constraintVerTwo), + template.WithProviderStatusCAPIContract(contractVersionInvalid), ), }, - 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), + warnings: admission.Warnings{"The Management object has incompatible CAPI contract versions in ProviderTemplates"}, + err: fmt.Sprintf("the Management is invalid: core CAPI contract versions %v does not support ProviderTemplate %s contract %s", supportedCAPIContractVersions, template.DefaultName, "v1alpha1"), }, { - name: "providertemplates match capi version, should succeed", + name: "providertemplates match capi contracts, should succeed", management: management.NewManagement( management.WithRelease(release.DefaultName), management.WithProviders([]v1alpha1.Provider{providerAwsDefaultTpl}), @@ -151,10 +114,9 @@ func TestManagementValidateUpdate(t *testing.T) { release.New(), template.NewProviderTemplate( template.WithName(release.DefaultCAPITemplateName), - template.WithProviderStatusCAPIVersion(versionOne), ), template.NewProviderTemplate( - template.WithProviderStatusCAPIConstraint(constraintVerOne), + template.WithProviderStatusCAPIContract(contractVersionValid), ), }, }, 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 bd386db7d..fafcd2e0e 100644 --- a/templates/provider/hmc/templates/crds/hmc.mirantis.com_clustertemplates.yaml +++ b/templates/provider/hmc/templates/crds/hmc.mirantis.com_clustertemplates.yaml @@ -109,20 +109,24 @@ spec: type: string providers: description: |- - Providers represent required CAPI providers with constrained compatibility versions set. + Providers represent required CAPI providers with supported contract versions. Should be set if not present in the Helm chart metadata. Compatibility attributes are optional to be defined. items: - description: Represents name of the provider with either an exact - or constrained version in the SemVer format. + description: |- + Represents name of the provider with its provider compatibility [contract versions]. + + [contract versions]: https://cluster-api.sigs.k8s.io/developer/providers/contracts properties: - name: - description: Name of the provider. - type: string - versionOrConstraint: + contractVersion: description: |- - Compatibility restriction in the SemVer format (exact or constrained version). + Compatibility restriction in the [CAPI provider format]. The value is an underscore-delimited (_) list of versions. Optional to be defined. + + [CAPI provider format]: https://cluster-api.sigs.k8s.io/developer/providers/contracts#api-version-labels + type: string + name: + description: Name of the provider. type: string type: object type: array @@ -183,19 +187,23 @@ spec: type: integer providers: description: |- - Providers represent required CAPI providers with constrained compatibility versions set + Providers represent required CAPI providers with supported contract versions if the latter has been given. items: - description: Represents name of the provider with either an exact - or constrained version in the SemVer format. + description: |- + Represents name of the provider with its provider compatibility [contract versions]. + + [contract versions]: https://cluster-api.sigs.k8s.io/developer/providers/contracts properties: - name: - description: Name of the provider. - type: string - versionOrConstraint: + contractVersion: description: |- - Compatibility restriction in the SemVer format (exact or constrained version). + Compatibility restriction in the [CAPI provider format]. The value is an underscore-delimited (_) list of versions. Optional to be defined. + + [CAPI provider format]: https://cluster-api.sigs.k8s.io/developer/providers/contracts#api-version-labels + type: string + name: + description: Name of the provider. type: string type: object type: array 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 d69574975..f8df43e48 100644 --- a/templates/provider/hmc/templates/crds/hmc.mirantis.com_managements.yaml +++ b/templates/provider/hmc/templates/crds/hmc.mirantis.com_managements.yaml @@ -114,18 +114,22 @@ spec: availableProviders: description: |- AvailableProviders holds all CAPI providers available along with - their exact compatibility versions if specified in ProviderTemplates on the Management cluster. + their supported contract versions, if specified in ProviderTemplates, on the Management cluster. items: - description: Represents name of the provider with either an exact - or constrained version in the SemVer format. + description: |- + Represents name of the provider with its provider compatibility [contract versions]. + + [contract versions]: https://cluster-api.sigs.k8s.io/developer/providers/contracts properties: - name: - description: Name of the provider. - type: string - versionOrConstraint: + contractVersion: description: |- - Compatibility restriction in the SemVer format (exact or constrained version). + Compatibility restriction in the [CAPI provider format]. The value is an underscore-delimited (_) list of versions. Optional to be defined. + + [CAPI provider format]: https://cluster-api.sigs.k8s.io/developer/providers/contracts#api-version-labels + type: string + name: + description: Name of the provider. 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 1e584155f..a50fbeb2d 100644 --- a/templates/provider/hmc/templates/crds/hmc.mirantis.com_providertemplates.yaml +++ b/templates/provider/hmc/templates/crds/hmc.mirantis.com_providertemplates.yaml @@ -56,15 +56,13 @@ spec: spec: description: ProviderTemplateSpec defines the desired state of ProviderTemplate properties: - capiVersion: + capiContractVersion: 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. + CAPI [contract version] indicating compatibility with the core CAPI. + Currently supported versions: v1alpha3_v1alpha4_v1beta1. + The field is not applicable for the cluster-api ProviderTemplate. + + [contract version]: https://cluster-api.sigs.k8s.io/developer/providers/contracts type: string helm: description: HelmSpec references a Helm chart representing the HMC @@ -115,20 +113,24 @@ spec: && has(self.chartRef)) providers: description: |- - Providers represent exposed CAPI providers with exact compatibility versions set. + Providers represent exposed CAPI providers with supported contract versions. Should be set if not present in the Helm chart metadata. - Compatibility attributes are optional to be defined. + Supported contract versions are optional to be defined. items: - description: Represents name of the provider with either an exact - or constrained version in the SemVer format. + description: |- + Represents name of the provider with its provider compatibility [contract versions]. + + [contract versions]: https://cluster-api.sigs.k8s.io/developer/providers/contracts properties: - name: - description: Name of the provider. - type: string - versionOrConstraint: + contractVersion: description: |- - Compatibility restriction in the SemVer format (exact or constrained version). + Compatibility restriction in the [CAPI provider format]. The value is an underscore-delimited (_) list of versions. Optional to be defined. + + [CAPI provider format]: https://cluster-api.sigs.k8s.io/developer/providers/contracts#api-version-labels + type: string + name: + description: Name of the provider. type: string type: object type: array @@ -136,20 +138,15 @@ spec: 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: + capiContractVersion: 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. + CAPI [contract version] indicating compatibility with the core CAPI. + Currently supported versions: v1alpha3_v1alpha4_v1beta1. + + [contract version]: https://cluster-api.sigs.k8s.io/developer/providers/contracts type: string chartRef: description: |- @@ -195,19 +192,23 @@ spec: type: integer providers: description: |- - Providers represent exposed CAPI providers with exact compatibility versions set + Providers represent exposed CAPI providers with supported contract versions if the latter has been given. items: - description: Represents name of the provider with either an exact - or constrained version in the SemVer format. + description: |- + Represents name of the provider with its provider compatibility [contract versions]. + + [contract versions]: https://cluster-api.sigs.k8s.io/developer/providers/contracts properties: - name: - description: Name of the provider. - type: string - versionOrConstraint: + contractVersion: description: |- - Compatibility restriction in the SemVer format (exact or constrained version). + Compatibility restriction in the [CAPI provider format]. The value is an underscore-delimited (_) list of versions. Optional to be defined. + + [CAPI provider format]: https://cluster-api.sigs.k8s.io/developer/providers/contracts#api-version-labels + type: string + name: + description: Name of the provider. 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 84c17f57f..48800b1fe 100644 --- a/templates/provider/hmc/templates/crds/hmc.mirantis.com_servicetemplates.yaml +++ b/templates/provider/hmc/templates/crds/hmc.mirantis.com_servicetemplates.yaml @@ -112,7 +112,22 @@ spec: Providers represent requested CAPI providers. Should be set if not present in the Helm chart metadata. items: - type: string + description: |- + Represents name of the provider with its provider compatibility [contract versions]. + + [contract versions]: https://cluster-api.sigs.k8s.io/developer/providers/contracts + properties: + contractVersion: + description: |- + Compatibility restriction in the [CAPI provider format]. The value is an underscore-delimited (_) list of versions. + Optional to be defined. + + [CAPI provider format]: https://cluster-api.sigs.k8s.io/developer/providers/contracts#api-version-labels + type: string + name: + description: Name of the provider. + type: string + type: object type: array required: - helm @@ -172,7 +187,22 @@ spec: providers: description: Providers represent requested CAPI providers. items: - type: string + description: |- + Represents name of the provider with its provider compatibility [contract versions]. + + [contract versions]: https://cluster-api.sigs.k8s.io/developer/providers/contracts + properties: + contractVersion: + description: |- + Compatibility restriction in the [CAPI provider format]. The value is an underscore-delimited (_) list of versions. + Optional to be defined. + + [CAPI provider format]: https://cluster-api.sigs.k8s.io/developer/providers/contracts#api-version-labels + type: string + name: + description: Name of the provider. + type: string + type: object type: array valid: description: Valid indicates whether the template passed validation diff --git a/test/objects/management/management.go b/test/objects/management/management.go index a6861c804..b120c6be5 100644 --- a/test/objects/management/management.go +++ b/test/objects/management/management.go @@ -64,7 +64,7 @@ func WithProviders(providers []v1alpha1.Provider) Opt { } } -func WithAvailableProviders(providers v1alpha1.ProvidersTupled) Opt { +func WithAvailableProviders(providers v1alpha1.Providers) Opt { return func(p *v1alpha1.Management) { p.Status.AvailableProviders = providers } diff --git a/test/objects/template/template.go b/test/objects/template/template.go index f53877503..9ac4cf412 100644 --- a/test/objects/template/template.go +++ b/test/objects/template/template.go @@ -140,18 +140,18 @@ func WithValidationStatus(validationStatus v1alpha1.TemplateValidationStatus) Op } } -func WithProvidersStatus[T v1alpha1.Providers | v1alpha1.ProvidersTupled](providers T) Opt { +func WithProvidersStatus(providers v1alpha1.Providers) Opt { return func(t Template) { switch v := t.(type) { case *v1alpha1.ClusterTemplate: var ok bool - v.Status.Providers, ok = any(providers).(v1alpha1.ProvidersTupled) + v.Status.Providers, ok = any(providers).(v1alpha1.Providers) if !ok { panic(fmt.Sprintf("unexpected type %T", providers)) } case *v1alpha1.ProviderTemplate: var ok bool - v.Status.Providers, ok = any(providers).(v1alpha1.ProvidersTupled) + v.Status.Providers, ok = any(providers).(v1alpha1.Providers) if !ok { panic(fmt.Sprintf("unexpected type %T", providers)) } @@ -174,23 +174,13 @@ 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 { +func WithProviderStatusCAPIContract(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 + pt.Status.CAPIContractVersion = v } }