Skip to content

Commit

Permalink
Update templates with compatibility attributes
Browse files Browse the repository at this point in the history
* added core CAPI contracts
* added providertemplates capi
  and crds versions
* added compatibility to the
  clustertemplates
* runtime: added support for
  multiproviders via multi-annotations
  to allow set provider crds contracts
  separately per provider
  only for clustertemplates
  • Loading branch information
zerospiel committed Oct 24, 2024
1 parent 59a0e13 commit c16aa2e
Show file tree
Hide file tree
Showing 21 changed files with 171 additions and 113 deletions.
29 changes: 18 additions & 11 deletions api/v1alpha1/clustertemplate_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,14 @@ const (

// ClusterTemplateSpec defines the desired state of ClusterTemplate
type ClusterTemplateSpec struct {
Helm HelmSpec `json:"helm"`
CAPIContracts CompatibilityContracts `json:"capiContracts,omitempty"`
Helm HelmSpec `json:"helm"`
// Holds key-value pairs with compatibility [contract versions],
// where the key is the name of the provider,
// and the value is the provider contract version
// required to be supported by the provider.
//
// [contract versions]: https://cluster-api.sigs.k8s.io/developer/providers/contracts
ProviderContracts CompatibilityContracts `json:"providerContracts,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 @@ -42,7 +48,13 @@ type ClusterTemplateSpec struct {

// ClusterTemplateStatus defines the observed state of ClusterTemplate
type ClusterTemplateStatus struct {
CAPIContracts CompatibilityContracts `json:"capiContracts,omitempty"`
// Holds key-value pairs with compatibility [contract versions],
// where the key is the name of the provider,
// and the value is the provider contract version
// required to be supported by the provider.
//
// [contract versions]: https://cluster-api.sigs.k8s.io/developer/providers/contracts
ProviderContracts CompatibilityContracts `json:"providerContracts,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 @@ -55,14 +67,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 = getProvidersList(t, annotations)
t.Status.Providers = getProvidersList(t.Spec.Providers, annotations)

contractsStatus, err := getCAPIContracts(t, annotations)
contractsStatus, err := getCAPIContracts(t.Kind, t.Spec.ProviderContracts, 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
t.Status.ProviderContracts = contractsStatus

kversion := annotations[ChartAnnotationKubernetesVersion]
if t.Spec.KubernetesVersion != "" {
Expand All @@ -81,11 +93,6 @@ func (t *ClusterTemplate) FillStatusWithProviders(annotations map[string]string)
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
17 changes: 5 additions & 12 deletions api/v1alpha1/providertemplate_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// ProviderTemplateKind denotes the providertemplate resource Kind.
const ProviderTemplateKind = "ProviderTemplate"

// ProviderTemplateSpec defines the desired state of ProviderTemplate
type ProviderTemplateSpec struct {
Helm HelmSpec `json:"helm,omitempty"`
Expand All @@ -43,9 +46,9 @@ 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 = getProvidersList(t, annotations)
t.Status.Providers = getProvidersList(t.Spec.Providers, annotations)

contractsStatus, err := getCAPIContracts(t, annotations)
contractsStatus, err := getCAPIContracts(t.Kind, t.Spec.CAPIContracts, annotations)
if err != nil {
return fmt.Errorf("failed to get CAPI contract versions for ProviderTemplate %s: %v", t.GetName(), err)
}
Expand All @@ -55,16 +58,6 @@ func (t *ProviderTemplate) FillStatusWithProviders(annotations map[string]string
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
}

// GetHelmSpec returns .spec.helm of the Template.
func (t *ProviderTemplate) GetHelmSpec() *HelmSpec {
return &t.Spec.Helm
Expand Down
2 changes: 1 addition & 1 deletion api/v1alpha1/servicetemplate_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +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 {
t.Status.Providers = getProvidersList(t, annotations)
t.Status.Providers = getProvidersList(t.Spec.Providers, annotations)

kconstraint := annotations[ChartAnnotationKubernetesConstraint]
if t.Spec.KubernetesConstraint != "" {
Expand Down
56 changes: 34 additions & 22 deletions api/v1alpha1/templates_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,21 +77,22 @@ type TemplateValidationStatus struct {
Valid bool `json:"valid"`
}

func getProvidersList(providersGetter interface{ GetSpecProviders() Providers }, annotations map[string]string) Providers {
func getProvidersList(providers Providers, annotations map[string]string) Providers {
const multiProviderSeparator = ","

if spec := providersGetter.GetSpecProviders(); len(spec) > 0 {
slices.Sort(spec)
return slices.Compact(spec)
if len(providers) > 0 {
res := slices.Clone(providers)
slices.Sort(res)
return slices.Compact(res)
}

providers := annotations[ChartAnnotationProviderName]
if len(providers) == 0 {
providersFromAnno := annotations[ChartAnnotationProviderName]
if len(providersFromAnno) == 0 {
return Providers{}
}

var (
splitted = strings.Split(providers, multiProviderSeparator)
splitted = strings.Split(providersFromAnno, multiProviderSeparator)
pstatus = make([]string, 0, len(splitted))
)
for _, v := range splitted {
Expand All @@ -104,23 +105,30 @@ func getProvidersList(providersGetter interface{ GetSpecProviders() Providers },
return slices.Compact(pstatus)
}

func getCAPIContracts(contractsGetter interface{ GetContracts() CompatibilityContracts }, annotations map[string]string) (_ CompatibilityContracts, merr error) {
func getCAPIContracts(kind string, contracts CompatibilityContracts, annotations map[string]string) (_ CompatibilityContracts, merr error) {
contractsStatus := make(map[string]string)

// spec preceding the annos
if contracts := contractsGetter.GetContracts(); len(contracts) > 0 {
for capiContract, providerContract := range contracts {
if !isCAPIContractSingleVersion(capiContract) {
merr = errors.Join(merr, fmt.Errorf("incorrect CAPI contract version %s in the spec", capiContract))
if len(contracts) > 0 {
for key, providerContract := range contracts { // key is either CAPI contract version or the name of a provider
// for provider templates the key must be contract version
// for cluster template the key must be the name of a provider
if kind == ProviderTemplateKind && !isCAPIContractSingleVersion(key) {
merr = errors.Join(merr, fmt.Errorf("incorrect CAPI contract version %s in the spec", key))
continue
}

if providerContract != "" && !isCAPIContractVersion(providerContract) { // special case for either CAPI or deliberately set empty
merr = errors.Join(merr, fmt.Errorf("incorrect provider contract version %s in the spec for the %s CAPI contract version", providerContract, capiContract))
// for provider templates it is allowed to have a list of contract versions, or be empty for the core CAPI case
// for cluster templates the contract versions should be single
if kind == ProviderTemplateKind && providerContract != "" && !isCAPIContractVersion(providerContract) {
merr = errors.Join(merr, fmt.Errorf("incorrect provider contract version %s in the spec for the %s CAPI contract version", providerContract, key))
continue
} else if kind == ClusterTemplateKind && !isCAPIContractSingleVersion(providerContract) {
merr = errors.Join(merr, fmt.Errorf("incorrect provider contract version %s in the spec for the %s provider name", providerContract, key))
continue
}

contractsStatus[capiContract] = providerContract
contractsStatus[key] = providerContract
}

return contractsStatus, merr
Expand All @@ -132,19 +140,23 @@ func getCAPIContracts(contractsGetter interface{ GetContracts() CompatibilityCon
continue
}

capiContract := k[idx+len(chartAnnoCAPIPrefix):]
if isCAPIContractSingleVersion(capiContract) {
if providerContract == "" { // special case for either CAPI or deliberately set empty
contractsStatus[capiContract] = ""
capiContractOrProviderName := k[idx+len(chartAnnoCAPIPrefix):]
if (kind == ProviderTemplateKind && isCAPIContractSingleVersion(capiContractOrProviderName)) ||
(kind == ClusterTemplateKind && (strings.HasPrefix(capiContractOrProviderName, "bootstrap-") ||
strings.HasPrefix(capiContractOrProviderName, "control-plane-") ||
strings.HasPrefix(capiContractOrProviderName, "infrastructure-"))) {
if kind == ProviderTemplateKind && providerContract == "" { // special case for the core CAPI
contractsStatus[capiContractOrProviderName] = ""
continue
}

if isCAPIContractVersion(providerContract) {
contractsStatus[capiContract] = providerContract
if (kind == ProviderTemplateKind && isCAPIContractVersion(providerContract)) ||
(kind == ClusterTemplateKind && isCAPIContractSingleVersion(providerContract)) {
contractsStatus[capiContractOrProviderName] = providerContract
} else {
// since we parsed capi contract version,
// then treat the provider's invalid version as an error
merr = errors.Join(merr, fmt.Errorf("incorrect provider contract version %s given for the %s CAPI contract version annotation", providerContract, k))
merr = errors.Join(merr, fmt.Errorf("incorrect provider contract version %s given for the %s annotation", providerContract, k))
}
}
}
Expand Down
8 changes: 4 additions & 4 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion internal/controller/management_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,7 @@ func updateComponentsStatus(
*providers = slices.Compact(*providers)

for _, v := range templateProviders {
capiContracts[v] = templateContracts // TODO (zerospiel): not sure whether it's okay to overwrite if the same provider
capiContracts[v] = templateContracts
}
}
}
Expand Down
32 changes: 16 additions & 16 deletions internal/controller/template_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -341,8 +341,9 @@ func (r *ClusterTemplateReconciler) validateCompatibilityAttrs(ctx context.Conte

exposedProviders, requiredProviders := management.Status.AvailableProviders, template.Status.Providers

ctrl.LoggerFrom(ctx).V(1).Info("providers to check", "exposed", exposedProviders, "required", requiredProviders,
"exposed_capi_contract_versions", management.Status.CAPIContracts, "required_capi_contract_versions", template.Status.CAPIContracts)
l := ctrl.LoggerFrom(ctx)
l.V(1).Info("providers to check", "exposed", exposedProviders, "required", requiredProviders,
"exposed_capi_contract_versions", management.Status.CAPIContracts, "required_provider_contract_versions", template.Status.ProviderContracts)

var (
merr error
Expand All @@ -354,26 +355,25 @@ func (r *ClusterTemplateReconciler) validateCompatibilityAttrs(ctx context.Conte
missing = append(missing, v)
continue
}
}

// already validated contract versions format
for providerName, requiredContract := range template.Status.ProviderContracts {
l.V(1).Info("validating contracts", "exposed_provider_capi_contracts", management.Status.CAPIContracts, "required_provider_name", providerName)

providerCAPIContracts, ok := management.Status.CAPIContracts[v]
providerCAPIContracts, ok := management.Status.CAPIContracts[providerName] // capi_version: provider_version(s)
if !ok {
continue // both the provider and cluster templates contract versions must be set for the validation
}

// already validated contract versions format
for capi, providerReq := range template.Status.CAPIContracts {
providerSupported, ok := providerCAPIContracts[capi]
if !ok {
// TODO (zerospiel): should we also consider it as a missing error? capi req from cluster missing in provider tpl
continue
}
var exposedProviderContracts []string
for _, supportedVersions := range providerCAPIContracts {
exposedProviderContracts = append(exposedProviderContracts, strings.Split(supportedVersions, "_")...)
}

providerSupportedContracts := strings.Split(providerSupported, "_")
for _, v := range strings.Split(providerReq, "_") {
if !slices.Contains(providerSupportedContracts, v) {
nonSatisfying = append(nonSatisfying, v)
}
}
l.V(1).Info("checking if contract is supported", "exposed_provider_contracts_final_list", exposedProviderContracts, "required_contract", requiredContract)
if !slices.Contains(exposedProviderContracts, requiredContract) {
nonSatisfying = append(nonSatisfying, "provider "+providerName+" does not support "+requiredContract)
}
}

Expand Down
Loading

0 comments on commit c16aa2e

Please sign in to comment.