Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update templates with compatibility attributes #549

Merged
merged 1 commit into from
Oct 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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