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

Validate available upgrades for managed clusters #391

Merged
merged 4 commits into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
39 changes: 38 additions & 1 deletion api/v1alpha1/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,15 @@ func SetupIndexers(ctx context.Context, mgr ctrl.Manager) error {
return err
}

return SetupManagedClusterServicesIndexer(ctx, mgr)
if err := SetupManagedClusterServicesIndexer(ctx, mgr); err != nil {
return err
}

if err := SetupClusterTemplateChainIndexer(ctx, mgr); err != nil {
return err
}

return SetupServiceTemplateChainIndexer(ctx, mgr)
}

const TemplateKey = ".spec.template"
Expand Down Expand Up @@ -121,3 +129,32 @@ func ExtractServiceTemplateName(rawObj client.Object) []string {

return templates
}

const SupportedTemplateKey = ".spec.supportedTemplates[].Name"

func SetupClusterTemplateChainIndexer(ctx context.Context, mgr ctrl.Manager) error {
return mgr.GetFieldIndexer().IndexField(ctx, &ClusterTemplateChain{}, SupportedTemplateKey, ExtractSupportedTemplatesNames)
}

func SetupServiceTemplateChainIndexer(ctx context.Context, mgr ctrl.Manager) error {
return mgr.GetFieldIndexer().IndexField(ctx, &ServiceTemplateChain{}, SupportedTemplateKey, ExtractSupportedTemplatesNames)
}

func ExtractSupportedTemplatesNames(rawObj client.Object) []string {
chainSpec := TemplateChainSpec{}
switch chain := rawObj.(type) {
case *ClusterTemplateChain:
chainSpec = chain.Spec
case *ServiceTemplateChain:
chainSpec = chain.Spec
default:
return nil
}

supportedTemplates := make([]string, 0, len(chainSpec.SupportedTemplates))
for _, t := range chainSpec.SupportedTemplates {
supportedTemplates = append(supportedTemplates, t.Name)
}

return supportedTemplates
}
7 changes: 6 additions & 1 deletion api/v1alpha1/managedcluster_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,13 @@ type ManagedClusterStatus struct {
// Currently compatible exact Kubernetes version of the cluster. Being set only if
// provided by the corresponding ClusterTemplate.
KubernetesVersion string `json:"k8sVersion,omitempty"`
// Conditions contains details for the current state of the ManagedCluster
// Conditions contains details for the current state of the ManagedCluster.
Conditions []metav1.Condition `json:"conditions,omitempty"`

// AvailableUpgrades is the list of ClusterTemplate names to which
// this cluster can be upgraded. It can be an empty array, which means no upgrades are
// available.
AvailableUpgrades []AvailableUpgrade `json:"availableUpgrades,omitempty"`
eromanova marked this conversation as resolved.
Show resolved Hide resolved
// ObservedGeneration is the last observed generation.
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
}
Expand Down
5 changes: 5 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

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

74 changes: 71 additions & 3 deletions internal/controller/managedcluster_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import (
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/dynamic"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
Expand Down Expand Up @@ -180,11 +181,12 @@ func (r *ManagedClusterReconciler) Update(ctx context.Context, managedCluster *h
managedCluster.InitConditions()
}

template := &hmc.ClusterTemplate{}

defer func() {
err = errors.Join(err, r.updateStatus(ctx, managedCluster))
err = errors.Join(err, r.updateStatus(ctx, managedCluster, template))
}()

template := &hmc.ClusterTemplate{}
templateRef := client.ObjectKey{Name: managedCluster.Spec.Template, Namespace: managedCluster.Namespace}
if err := r.Get(ctx, templateRef, template); err != nil {
l.Error(err, "Failed to get Template")
Expand Down Expand Up @@ -419,7 +421,7 @@ func validateReleaseWithValues(ctx context.Context, actionConfig *action.Configu
return nil
}

func (r *ManagedClusterReconciler) updateStatus(ctx context.Context, managedCluster *hmc.ManagedCluster) error {
func (r *ManagedClusterReconciler) updateStatus(ctx context.Context, managedCluster *hmc.ManagedCluster, template *hmc.ClusterTemplate) error {
managedCluster.Status.ObservedGeneration = managedCluster.Generation
warnings := ""
errs := ""
Expand Down Expand Up @@ -451,6 +453,11 @@ func (r *ManagedClusterReconciler) updateStatus(ctx context.Context, managedClus
condition.Message = errs
}
apimeta.SetStatusCondition(managedCluster.GetConditions(), condition)

err := r.setAvailableUpgrades(ctx, managedCluster, template)
if err != nil {
return errors.New("failed to set available upgrades")
}
if err := r.Status().Update(ctx, managedCluster); err != nil {
return fmt.Errorf("failed to update status for managedCluster %s/%s: %w", managedCluster.Namespace, managedCluster.Name, err)
}
Expand Down Expand Up @@ -997,6 +1004,38 @@ func setIdentityHelmValues(values *apiextensionsv1.JSON, idRef *corev1.ObjectRef
}, nil
}

func (r *ManagedClusterReconciler) setAvailableUpgrades(ctx context.Context, managedCluster *hmc.ManagedCluster, template *hmc.ClusterTemplate) error {
if template == nil {
return nil
}
chains := &hmc.ClusterTemplateChainList{}
err := r.List(ctx, chains,
client.InNamespace(template.Namespace),
client.MatchingFields{hmc.SupportedTemplateKey: template.GetName()},
)
if err != nil {
return err
}

availableUpgradesMap := make(map[string]hmc.AvailableUpgrade)
for _, chain := range chains.Items {
for _, supportedTemplate := range chain.Spec.SupportedTemplates {
if supportedTemplate.Name == template.Name {
for _, availableUpgrade := range supportedTemplate.AvailableUpgrades {
availableUpgradesMap[availableUpgrade.Name] = availableUpgrade
}
}
}
}
availableUpgrades := make([]hmc.AvailableUpgrade, 0, len(availableUpgradesMap))
for _, availableUpgrade := range availableUpgradesMap {
availableUpgrades = append(availableUpgrades, availableUpgrade)
}

managedCluster.Status.AvailableUpgrades = availableUpgrades
return nil
}

// SetupWithManager sets up the controller with the Manager.
func (r *ManagedClusterReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
Expand All @@ -1019,5 +1058,34 @@ func (r *ManagedClusterReconciler) SetupWithManager(mgr ctrl.Manager) error {
}
}),
).
Watches(&hmc.ClusterTemplateChain{},
handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, o client.Object) []ctrl.Request {
chain := &hmc.ClusterTemplateChain{}
err := r.Client.Get(ctx, types.NamespacedName{Namespace: o.GetNamespace(), Name: o.GetName()}, chain)
if err != nil {
return []ctrl.Request{}
}
eromanova marked this conversation as resolved.
Show resolved Hide resolved

var req []ctrl.Request
for _, template := range getTemplateNamesManagedByChain(chain) {
managedClusters := &hmc.ManagedClusterList{}
err = r.Client.List(ctx, managedClusters,
client.InNamespace(o.GetNamespace()),
client.MatchingFields{hmc.TemplateKey: template})
if err != nil {
return []ctrl.Request{}
}
for _, cluster := range managedClusters.Items {
req = append(req, reconcile.Request{
NamespacedName: client.ObjectKey{
Namespace: cluster.Namespace,
Name: cluster.Name,
},
})
}
}
return req
}),
).
zerospiel marked this conversation as resolved.
Show resolved Hide resolved
Complete(r)
}
8 changes: 8 additions & 0 deletions internal/controller/templatechain_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,14 @@ func getCurrentTemplates(ctx context.Context, cl client.Client, templateKind, sy
return systemTemplates, managedTemplates, nil
}

func getTemplateNamesManagedByChain(chain templateChain) []string {
result := make([]string, 0, len(chain.GetSpec().SupportedTemplates))
for _, template := range chain.GetSpec().SupportedTemplates {
result = append(result, template.Name)
}
return result
}

// SetupWithManager sets up the controller with the Manager.
func (r *ClusterTemplateChainReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
Expand Down
22 changes: 20 additions & 2 deletions internal/webhook/managedcluster_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"context"
"errors"
"fmt"
"slices"
"strings"

"github.com/Masterminds/semver/v3"
Expand All @@ -38,6 +39,8 @@ type ManagedClusterValidator struct {

const invalidManagedClusterMsg = "the ManagedCluster is invalid"

var errClusterUpgradeForbidden = errors.New("cluster upgrade is forbidden")

func (v *ManagedClusterValidator) SetupWebhookWithManager(mgr ctrl.Manager) error {
v.Client = mgr.GetClient()
return ctrl.NewWebhookManagedBy(mgr).
Expand Down Expand Up @@ -89,12 +92,21 @@ func (v *ManagedClusterValidator) ValidateUpdate(ctx context.Context, oldObj, ne
if !ok {
return nil, apierrors.NewBadRequest(fmt.Sprintf("expected ManagedCluster but got a %T", newObj))
}
template, err := v.getManagedClusterTemplate(ctx, newManagedCluster.Namespace, newManagedCluster.Spec.Template)
oldTemplate := oldManagedCluster.Spec.Template
newTemplate := newManagedCluster.Spec.Template

template, err := v.getManagedClusterTemplate(ctx, newManagedCluster.Namespace, newTemplate)
if err != nil {
return nil, fmt.Errorf("%s: %v", invalidManagedClusterMsg, err)
}

if oldManagedCluster.Spec.Template != newManagedCluster.Spec.Template {
if oldTemplate != newTemplate {
isUpgradeAvailable := validateAvailableUpgrade(oldManagedCluster, newTemplate)
if !isUpgradeAvailable {
msg := fmt.Sprintf("Cluster can't be upgraded from %s to %s. This upgrade sequence is not allowed", oldTemplate, newTemplate)
return admission.Warnings{msg}, errClusterUpgradeForbidden
}

if err := isTemplateValid(template); err != nil {
return nil, fmt.Errorf("%s: %v", invalidManagedClusterMsg, err)
}
Expand All @@ -111,6 +123,12 @@ func (v *ManagedClusterValidator) ValidateUpdate(ctx context.Context, oldObj, ne
return nil, nil
}

func validateAvailableUpgrade(oldManagedCluster *hmcv1alpha1.ManagedCluster, newTemplate string) bool {
return slices.ContainsFunc(oldManagedCluster.Status.AvailableUpgrades, func(au hmcv1alpha1.AvailableUpgrade) bool {
return newTemplate == au.Name
})
}

func validateK8sCompatibility(ctx context.Context, cl client.Client, template *hmcv1alpha1.ClusterTemplate, mc *hmcv1alpha1.ManagedCluster) error {
if len(mc.Spec.Services) == 0 || template.Status.KubernetesVersion == "" {
return nil // nothing to do
Expand Down
82 changes: 79 additions & 3 deletions internal/webhook/managedcluster_webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,11 @@ func TestManagedClusterValidateCreate(t *testing.T) {
}

func TestManagedClusterValidateUpdate(t *testing.T) {
const (
upgradeTargetTemplateName = "upgrade-target-template"
unmanagedByHMCTemplateName = "unmanaged-template"
)

g := NewWithT(t)

ctx := admission.NewContextWithRequest(context.Background(), admission.Request{
Expand All @@ -274,8 +279,11 @@ func TestManagedClusterValidateUpdate(t *testing.T) {
warnings admission.Warnings
}{
{
name: "should fail if the new cluster template was found but is invalid (some validation error)",
oldManagedCluster: managedcluster.NewManagedCluster(managedcluster.WithClusterTemplate(testTemplateName)),
name: "update spec.template: should fail if the new cluster template was found but is invalid (some validation error)",
oldManagedCluster: managedcluster.NewManagedCluster(
managedcluster.WithClusterTemplate(testTemplateName),
managedcluster.WithAvailableUpgrades([]v1alpha1.AvailableUpgrade{{Name: newTemplateName}}),
),
newManagedCluster: managedcluster.NewManagedCluster(managedcluster.WithClusterTemplate(newTemplateName)),
existingObjects: []runtime.Object{
mgmt,
Expand All @@ -290,7 +298,75 @@ func TestManagedClusterValidateUpdate(t *testing.T) {
err: "the ManagedCluster is invalid: the template is not valid: validation error example",
},
{
name: "should succeed if template is not changed",
name: "update spec.template: should fail if the template is not in the list of available",
oldManagedCluster: managedcluster.NewManagedCluster(
managedcluster.WithClusterTemplate(testTemplateName),
managedcluster.WithCredential(testCredentialName),
managedcluster.WithAvailableUpgrades([]v1alpha1.AvailableUpgrade{}),
),
newManagedCluster: managedcluster.NewManagedCluster(
managedcluster.WithClusterTemplate(upgradeTargetTemplateName),
managedcluster.WithCredential(testCredentialName),
),
existingObjects: []runtime.Object{
mgmt, cred,
template.NewClusterTemplate(
template.WithName(testTemplateName),
template.WithValidationStatus(v1alpha1.TemplateValidationStatus{Valid: true}),
template.WithProvidersStatus(v1alpha1.Providers{
"infrastructure-aws",
"control-plane-k0smotron",
"bootstrap-k0smotron",
}),
),
template.NewClusterTemplate(
template.WithName(upgradeTargetTemplateName),
template.WithValidationStatus(v1alpha1.TemplateValidationStatus{Valid: true}),
template.WithProvidersStatus(v1alpha1.Providers{
"infrastructure-aws",
"control-plane-k0smotron",
"bootstrap-k0smotron",
}),
),
},
warnings: admission.Warnings{fmt.Sprintf("Cluster can't be upgraded from %s to %s. This upgrade sequence is not allowed", testTemplateName, upgradeTargetTemplateName)},
err: "cluster upgrade is forbidden",
},
{
name: "update spec.template: should succeed if the template is in the list of available",
oldManagedCluster: managedcluster.NewManagedCluster(
managedcluster.WithClusterTemplate(testTemplateName),
managedcluster.WithCredential(testCredentialName),
managedcluster.WithAvailableUpgrades([]v1alpha1.AvailableUpgrade{{Name: newTemplateName}}),
),
newManagedCluster: managedcluster.NewManagedCluster(
managedcluster.WithClusterTemplate(newTemplateName),
managedcluster.WithCredential(testCredentialName),
),
existingObjects: []runtime.Object{
mgmt, cred,
template.NewClusterTemplate(
template.WithName(testTemplateName),
template.WithValidationStatus(v1alpha1.TemplateValidationStatus{Valid: true}),
template.WithProvidersStatus(v1alpha1.Providers{
"infrastructure-aws",
"control-plane-k0smotron",
"bootstrap-k0smotron",
}),
),
template.NewClusterTemplate(
template.WithName(newTemplateName),
template.WithValidationStatus(v1alpha1.TemplateValidationStatus{Valid: true}),
template.WithProvidersStatus(v1alpha1.Providers{
"infrastructure-aws",
"control-plane-k0smotron",
"bootstrap-k0smotron",
}),
),
},
},
{
name: "should succeed if spec.template is not changed",
oldManagedCluster: managedcluster.NewManagedCluster(
managedcluster.WithClusterTemplate(testTemplateName),
managedcluster.WithConfig(`{"foo":"bar"}`),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,26 @@ spec:
status:
description: ManagedClusterStatus defines the observed state of ManagedCluster
properties:
availableUpgrades:
description: |-
AvailableUpgrades is the list of ClusterTemplate names to which
this cluster can be upgraded. It can be an empty array, which means no upgrades are
available.
items:
description: AvailableUpgrade is the definition of the available
upgrade for the Template
properties:
name:
description: Name is the name of the Template to which the upgrade
is available.
type: string
required:
- name
type: object
type: array
conditions:
description: Conditions contains details for the current state of
the ManagedCluster
the ManagedCluster.
items:
description: Condition contains details for one aspect of the current
state of this API Resource.
Expand Down
Loading