Skip to content

Commit

Permalink
Compatibility rework: addressed comments
Browse files Browse the repository at this point in the history
* change semicolon to comma as a separator
  in the provider templates charts
* capi-contract annotation removed
* unified separated capi contracts struct
  for cluster/provider templates
* capi contracts status in management status
* provider templates: validate capi contracts
  against core capi template
* cluster templates: validate provider contracts
  against provider templates contracts in management status
* renamed k0s -> k0smotron everywhere
  • Loading branch information
zerospiel committed Oct 22, 2024
1 parent b03b9b3 commit 706e684
Show file tree
Hide file tree
Showing 31 changed files with 599 additions and 360 deletions.
20 changes: 17 additions & 3 deletions api/v1alpha1/clustertemplate_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ const (

// ClusterTemplateSpec defines the desired state of ClusterTemplate
type ClusterTemplateSpec struct {
Helm HelmSpec `json:"helm"`
Helm HelmSpec `json:"helm"`
CAPIContracts CompatibilityContracts `json:"capiContracts,omitempty"`
// Kubernetes exact version in the SemVer format provided by this ClusterTemplate.
KubernetesVersion string `json:"k8sVersion,omitempty"`
// Providers represent required CAPI providers with supported contract versions.
Expand All @@ -41,6 +42,7 @@ type ClusterTemplateSpec struct {

// ClusterTemplateStatus defines the observed state of ClusterTemplate
type ClusterTemplateStatus struct {
CAPIContracts CompatibilityContracts `json:"capiContracts,omitempty"`
// Kubernetes exact version in the SemVer format provided by this ClusterTemplate.
KubernetesVersion string `json:"k8sVersion,omitempty"`
// Providers represent required CAPI providers with supported contract versions
Expand All @@ -53,7 +55,14 @@ 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 {
t.Status.Providers = parseProviders(t, annotations)
t.Status.Providers = getProvidersList(t, annotations)

contractsStatus, err := getCAPIContracts(t, annotations)
if err != nil {
return fmt.Errorf("failed to get CAPI contract versions for ClusterTemplate %s/%s: %v", t.GetNamespace(), t.GetName(), err)
}

t.Status.CAPIContracts = contractsStatus

kversion := annotations[ChartAnnotationKubernetesVersion]
if t.Spec.KubernetesVersion != "" {
Expand All @@ -64,14 +73,19 @@ func (t *ClusterTemplate) FillStatusWithProviders(annotations map[string]string)
}

if _, err := semver.NewVersion(kversion); err != nil {
return fmt.Errorf("failed to parse kubernetes version %s: %w", kversion, err)
return fmt.Errorf("failed to parse kubernetes version %s for ClusterTemplate %s/%s: %w", kversion, t.GetNamespace(), t.GetName(), err)
}

t.Status.KubernetesVersion = kversion

return nil
}

// GetContracts returns .spec.capiContracts of the Template.
func (t *ClusterTemplate) GetContracts() CompatibilityContracts {
return t.Spec.CAPIContracts
}

// GetSpecProviders returns .spec.providers of the Template.
func (t *ClusterTemplate) GetSpecProviders() Providers {
return t.Spec.Providers
Expand Down
30 changes: 7 additions & 23 deletions api/v1alpha1/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,23 +36,16 @@ const (
)

type (
// Holds different types of CAPI providers with [compatible contract versions].
//
// [compatible contract versions]: https://cluster-api.sigs.k8s.io/developer/providers/contracts
Providers []NameContract
// Holds different types of CAPI providers.
Providers []string

// Represents name of the provider with its provider compatibility [contract versions].
// Holds key-value pairs with compatibility [contract versions],
// where the key is the core CAPI contract version,
// and the value is an underscore-delimited (_) list of provider contract versions
// supported by the core CAPI.
//
// [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 [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
ContractVersion string `json:"contractVersion,omitempty"`
}
CompatibilityContracts map[string]string
)

const (
Expand Down Expand Up @@ -128,12 +121,3 @@ func ExtractServiceTemplateName(rawObj client.Object) []string {

return templates
}

// Names flattens the underlaying slice to provider names slice and returns it.
func (c Providers) Names() []string {
nn := make([]string, len(c))
for i, v := range c {
nn[i] = v.Name
}
return nn
}
74 changes: 74 additions & 0 deletions api/v1alpha1/compatibility_contract.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// 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 v1alpha1

import (
"strconv"
"strings"
)

// isCAPIContractVersion determines whether a given string
// represents a version in the CAPI contract version format (e.g. v1_v1beta1_v1alpha1, etc.).
func isCAPIContractVersion(version string) bool {
for _, v := range strings.Split(version, "_") {
if !isCAPIContractSingleVersion(v) {
return false
}
}

return true
}

// isCAPIContractSingleVersion determines whether a given string
// represents a single version in the CAPI contract version format (e.g. v1, v1beta1, v1alpha1, etc.).
func isCAPIContractSingleVersion(version string) bool {
if !strings.HasPrefix(version, "v") {
return false
}

parts := strings.Split(version, "v")
if len(parts) != 2 || parts[0] != "" || strings.IndexByte(version, '_') != -1 { // skip v1_v1beta1 list of versions
return false
}

const (
alphaPrefix, betaPrefix = "alpha", "beta"
)

versionNumber := parts[1]
alphaIndex := strings.Index(versionNumber, alphaPrefix)
betaIndex := strings.Index(versionNumber, betaPrefix)

if alphaIndex != -1 {
return isNonMajor(versionNumber, alphaPrefix, alphaIndex)
} else if betaIndex != -1 {
return isNonMajor(versionNumber, betaPrefix, betaIndex)
}

_, err := strconv.Atoi(strings.TrimSpace(versionNumber))
return err == nil
}

func isNonMajor(version, prefix string, prefixIdx int) bool {
majorVer := version[:prefixIdx]
prefixedVer := version[prefixIdx+len(prefix):]

if _, err := strconv.Atoi(majorVer); err != nil {
return false
}

_, err := strconv.Atoi(prefixedVer)
return err == nil
}
72 changes: 72 additions & 0 deletions api/v1alpha1/compatibility_contract_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// 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 v1alpha1

import "testing"

func Test_isCAPIContractVersion(t *testing.T) {
tests := []struct {
version string
isValid bool
}{
{"v1", true},
{"v1alpha1", true},
{"v1beta1", true},
{"v2", true},
{"v3alpha2", true},
{"v33beta22", true},
{"v1alpha1_v1beta1", true},
{"v1alpha1v1alha2_v1beta1", false},
{"v4beta1", true},
{"invalid", false},
{"v1alpha", false},
{"v1beta", false},
{"v1alpha1beta1", false},
}

for _, test := range tests {
result := isCAPIContractVersion(test.version)
if result != test.isValid {
t.Errorf("isValidVersion(%q) = %v, want %v", test.version, result, test.isValid)
}
}
}

func Test_isCAPIContractSingleVersion(t *testing.T) {
tests := []struct {
version string
isValid bool
}{
{"v1", true},
{"v1alpha1", true},
{"v1beta1", true},
{"v2", true},
{"v3alpha2", true},
{"v33beta22", true},
{"v4beta1", true},
{"invalid", false},
{"v1alpha", false},
{"v1beta", false},
{"v1alpha1beta1", false},
{"v1alpha1_v1beta1", false},
}

for _, test := range tests {
result := isCAPIContractSingleVersion(test.version)
if result != test.isValid {
t.Errorf("isValidVersion(%q) = %v, want %v", test.version, result, test.isValid)
}
}
}
2 changes: 2 additions & 0 deletions api/v1alpha1/management_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ func GetDefaultProviders() []Provider {

// ManagementStatus defines the observed state of Management
type ManagementStatus struct {
// TODO:
CAPIContracts map[string]CompatibilityContracts `json:"capiContracts,omitempty"`
// Components indicates the status of installed HMC components and CAPI providers.
Components map[string]ComponentStatus `json:"components,omitempty"`
// Release indicates the current Release object.
Expand Down
41 changes: 15 additions & 26 deletions api/v1alpha1/providertemplate_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,15 @@
package v1alpha1

import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
"fmt"

const (
// ChartAnnotationCAPIContractVersion is an annotation containing the expected core CAPI contract version (e.g. v1beta1) associated with a ProviderTemplate.
ChartAnnotationCAPIContractVersion = "hmc.mirantis.com/capi-version"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// ProviderTemplateSpec defines the desired state of ProviderTemplate
type ProviderTemplateSpec struct {
Helm HelmSpec `json:"helm,omitempty"`
// 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"`
Helm HelmSpec `json:"helm,omitempty"`
CAPIContracts CompatibilityContracts `json:"capiContracts,omitempty"`
// Providers represent exposed CAPI providers with supported contract versions.
// Should be set if not present in the Helm chart metadata.
// Supported contract versions are optional to be defined.
Expand All @@ -40,11 +32,7 @@ type ProviderTemplateSpec struct {

// ProviderTemplateStatus defines the observed state of ProviderTemplate
type ProviderTemplateStatus struct {
// 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"`
CAPIContracts CompatibilityContracts `json:"capiContracts,omitempty"`
// Providers represent exposed CAPI providers with supported contract versions
// if the latter has been given.
Providers Providers `json:"providers,omitempty"`
Expand All @@ -55,22 +43,23 @@ 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 {
t.Status.Providers = parseProviders(t, annotations)
t.Status.Providers = getProvidersList(t, annotations)

if t.Name == CoreCAPIName {
return nil
contractsStatus, err := getCAPIContracts(t, annotations)
if err != nil {
return fmt.Errorf("failed to get CAPI contract versions for ProviderTemplate %s: %v", t.GetName(), err)
}

requiredCAPIContract := annotations[ChartAnnotationCAPIContractVersion]
if t.Spec.CAPIContractVersion != "" {
requiredCAPIContract = t.Spec.CAPIContractVersion
}

t.Status.CAPIContractVersion = requiredCAPIContract
t.Status.CAPIContracts = contractsStatus

return nil
}

// GetContracts returns .spec.capiContracts of the Template.
func (t *ProviderTemplate) GetContracts() CompatibilityContracts {
return t.Spec.CAPIContracts
}

// GetSpecProviders returns .spec.providers of the Template.
func (t *ProviderTemplate) GetSpecProviders() Providers {
return t.Spec.Providers
Expand Down
22 changes: 7 additions & 15 deletions api/v1alpha1/servicetemplate_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ package v1alpha1

import (
"fmt"
"strings"

"github.com/Masterminds/semver/v3"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand Down Expand Up @@ -52,19 +51,7 @@ type ServiceTemplateStatus struct {
// FillStatusWithProviders sets the status of the template with providers
// either from the spec or from the given annotations.
func (t *ServiceTemplate) FillStatusWithProviders(annotations map[string]string) error {
providers := annotations[ChartAnnotationProviderName]
if len(providers) == 0 {
t.Status.Providers = t.Spec.Providers
} else {
splitted := strings.Split(providers, multiProviderSeparator)
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, NameContract{Name: c})
}
}
}
t.Status.Providers = getProvidersList(t, annotations)

kconstraint := annotations[ChartAnnotationKubernetesConstraint]
if t.Spec.KubernetesConstraint != "" {
Expand All @@ -75,14 +62,19 @@ func (t *ServiceTemplate) FillStatusWithProviders(annotations map[string]string)
}

if _, err := semver.NewConstraint(kconstraint); err != nil {
return fmt.Errorf("failed to parse kubernetes constraint %s: %w", kconstraint, err)
return fmt.Errorf("failed to parse kubernetes constraint %s for ServiceTemplate %s/%s: %w", kconstraint, t.GetNamespace(), t.GetName(), err)
}

t.Status.KubernetesConstraint = kconstraint

return nil
}

// GetSpecProviders returns .spec.providers of the Template.
func (t *ServiceTemplate) GetSpecProviders() Providers {
return t.Spec.Providers
}

// GetHelmSpec returns .spec.helm of the Template.
func (t *ServiceTemplate) GetHelmSpec() *HelmSpec {
return &t.Spec.Helm
Expand Down
Loading

0 comments on commit 706e684

Please sign in to comment.