Skip to content

Commit

Permalink
Introduce compatibility based on contracts
Browse files Browse the repository at this point in the history
* removed version/constraints from compatibility
  attributes
* compatibility is now based on CAPI contracts
  versions
* unified providers structure
* corresponding adjustments

Closes Mirantis#496
  • Loading branch information
zerospiel committed Oct 18, 2024
1 parent 8367e72 commit 94e7838
Show file tree
Hide file tree
Showing 20 changed files with 260 additions and 361 deletions.
16 changes: 6 additions & 10 deletions api/v1alpha1/clustertemplate_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,31 +33,27 @@ 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"`
}

// 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 != "" {
Expand All @@ -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
}

Expand Down
27 changes: 9 additions & 18 deletions api/v1alpha1/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,29 +36,20 @@ 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

// 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
// [compatible contract versions]: https://cluster-api.sigs.k8s.io/developer/providers/contracts
Providers []NameContract

// Represents name of the provider with either an exact or constrained version in the SemVer format.
ProviderTuple struct {
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"`
}
)

Expand Down Expand Up @@ -137,7 +128,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
Expand Down
2 changes: 1 addition & 1 deletion api/v1alpha1/management_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ type ManagementStatus struct {
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"`
AvailableProviders Providers `json:"availableProviders,omitempty"`
// ObservedGeneration is the last observed generation.
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
}
Expand Down
90 changes: 32 additions & 58 deletions api/v1alpha1/providertemplate_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,94 +15,68 @@
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"`
}

// 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
}

if requiredCAPIContract == "" {
return nil
}

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
}

Expand Down
4 changes: 2 additions & 2 deletions api/v1alpha1/servicetemplate_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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})
}
}
}
Expand Down
35 changes: 10 additions & 25 deletions api/v1alpha1/templates_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@
package v1alpha1

import (
"errors"
"fmt"
"strings"

helmcontrollerv2 "github.com/fluxcd/helm-controller/api/v2"
Expand Down Expand Up @@ -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
}
Loading

0 comments on commit 94e7838

Please sign in to comment.