Skip to content

Commit

Permalink
Make Auto Upgrade Semantic Version Aware (#162)
Browse files Browse the repository at this point in the history
A breakging change was recently added to an application bundle, sadly we
needed to re-instate the old bundle version for various reasons.
However, the main problem here is any resources using the old bundle
will get upgraded to the new one, and bascially destroy the resource due
to the breaking change.  How do we prevent this?  Well everything has a
semantic version, and major versions represent breaking changes, so when
finding a potential upgrade target, only consider bundles with the same
semantic version as the current one.
  • Loading branch information
spjmurray authored Jan 2, 2025
1 parent d0ba941 commit 50a2ae2
Show file tree
Hide file tree
Showing 2 changed files with 74 additions and 46 deletions.
60 changes: 37 additions & 23 deletions pkg/monitor/upgrade/cluster/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,23 +42,18 @@ func New(client client.Client) *Checker {
}
}

func (c *Checker) upgradeResource(ctx context.Context, resource *unikornv1.KubernetesCluster, bundles *unikornv1.KubernetesClusterApplicationBundleList, target *unikornv1.KubernetesClusterApplicationBundle) error {
func (c *Checker) upgradeResource(ctx context.Context, resource *unikornv1.KubernetesCluster, current, target *unikornv1.KubernetesClusterApplicationBundle) error {
logger := log.FromContext(ctx)

bundle := bundles.Get(*resource.Spec.ApplicationBundle)
if bundle == nil {
return fmt.Errorf("%w: %s", errors.ErrMissingBundle, *resource.Spec.ApplicationBundle)
}

// If the current bundle is in preview, then don't offer to upgrade.
if bundle.Spec.Preview != nil && *bundle.Spec.Preview {
if current.Spec.Preview != nil && *current.Spec.Preview {
logger.Info("bundle in preview, ignoring")

return nil
}

// If the current bundle is the best option already, we are done.
if bundle.Name == target.Name {
if current.Name == target.Name {
logger.Info("bundle already latest, ignoring")

return nil
Expand All @@ -67,7 +62,7 @@ func (c *Checker) upgradeResource(ctx context.Context, resource *unikornv1.Kuber
upgradable := util.UpgradeableResource(resource)

if resource.Spec.ApplicationBundleAutoUpgrade == nil {
if bundle.Spec.EndOfLife == nil || time.Now().Before(bundle.Spec.EndOfLife.Time) {
if current.Spec.EndOfLife == nil || time.Now().Before(current.Spec.EndOfLife.Time) {
logger.Info("resource auto-upgrade disabled, ignoring")

return nil
Expand All @@ -88,7 +83,7 @@ func (c *Checker) upgradeResource(ctx context.Context, resource *unikornv1.Kuber
return nil
}

logger.Info("bundle upgrading", "from", bundle.Spec.Version, "to", target.Spec.Version)
logger.Info("bundle upgrading")

resource.Spec.ApplicationBundle = &target.Name

Expand All @@ -110,18 +105,6 @@ func (c *Checker) Check(ctx context.Context) error {
return err
}

// Extract the potential upgrade target bundles, these are sorted by version, so
// the newest is on the top, we shall see why later...
bundles := allBundles.Upgradable()
if len(bundles.Items) == 0 {
return errors.ErrNoBundles
}

slices.SortStableFunc(bundles.Items, unikornv1.CompareKubernetesClusterApplicationBundle)

// Pick the most recent as our upgrade target.
upgradeTarget := &bundles.Items[len(bundles.Items)-1]

resources := &unikornv1.KubernetesClusterList{}

if err := c.client.List(ctx, resources); err != nil {
Expand All @@ -131,13 +114,44 @@ func (c *Checker) Check(ctx context.Context) error {
for i := range resources.Items {
resource := &resources.Items[i]

// What we need to do is respect semantic versioning, e.g. a major is a breaking
// change therefore auto-upgrade is not allowed.
currentBundle := allBundles.Get(*resource.Spec.ApplicationBundle)
if currentBundle == nil {
return fmt.Errorf("%w: %s", errors.ErrMissingBundle, *resource.Spec.ApplicationBundle)
}

discard := func(bundle unikornv1.KubernetesClusterApplicationBundle) bool {
return bundle.Spec.Version.Version.Major() != currentBundle.Spec.Version.Major()
}

allowedBundles := &unikornv1.KubernetesClusterApplicationBundleList{
Items: slices.Clone(allBundles.Items),
}

allowedBundles.Items = slices.DeleteFunc(allowedBundles.Items, discard)

// Extract the potential upgrade target bundles, these are sorted by version, so
// the newest is on the top, we shall see why later...
upgradeTagetBundles := allowedBundles.Upgradable()
if len(upgradeTagetBundles.Items) == 0 {
return errors.ErrNoBundles
}

slices.SortStableFunc(upgradeTagetBundles.Items, unikornv1.CompareKubernetesClusterApplicationBundle)

// Pick the most recent as our upgrade target.
upgradeTarget := &upgradeTagetBundles.Items[len(upgradeTagetBundles.Items)-1]

logger := logger.WithValues(
"organization", resource.Labels[constants.OrganizationLabel],
"project", resource.Labels[constants.ProjectLabel],
"cluster", resource.Name,
"from", currentBundle.Spec.Version,
"to", upgradeTarget.Spec.Version,
)

if err := c.upgradeResource(log.IntoContext(ctx, logger), resource, allBundles, upgradeTarget); err != nil {
if err := c.upgradeResource(log.IntoContext(ctx, logger), resource, currentBundle, upgradeTarget); err != nil {
return err
}
}
Expand Down
60 changes: 37 additions & 23 deletions pkg/monitor/upgrade/clustermanager/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,23 +42,18 @@ func New(client client.Client) *Checker {
}
}

func (c *Checker) upgradeResource(ctx context.Context, resource *unikornv1.ClusterManager, bundles *unikornv1.ClusterManagerApplicationBundleList, target *unikornv1.ClusterManagerApplicationBundle) error {
func (c *Checker) upgradeResource(ctx context.Context, resource *unikornv1.ClusterManager, current, target *unikornv1.ClusterManagerApplicationBundle) error {
logger := log.FromContext(ctx)

bundle := bundles.Get(*resource.Spec.ApplicationBundle)
if bundle == nil {
return fmt.Errorf("%w: %s", errors.ErrMissingBundle, *resource.Spec.ApplicationBundle)
}

// If the current bundle is in preview, then don't offer to upgrade.
if bundle.Spec.Preview != nil && *bundle.Spec.Preview {
if current.Spec.Preview != nil && *current.Spec.Preview {
logger.Info("bundle in preview, ignoring")

return nil
}

// If the current bundle is the best option already, we are done.
if bundle.Name == target.Name {
if current.Name == target.Name {
logger.Info("bundle already latest, ignoring")

return nil
Expand All @@ -67,7 +62,7 @@ func (c *Checker) upgradeResource(ctx context.Context, resource *unikornv1.Clust
upgradable := util.UpgradeableResource(resource)

if resource.Spec.ApplicationBundleAutoUpgrade == nil {
if bundle.Spec.EndOfLife == nil || time.Now().Before(bundle.Spec.EndOfLife.Time) {
if current.Spec.EndOfLife == nil || time.Now().Before(current.Spec.EndOfLife.Time) {
logger.Info("resource auto-upgrade disabled, ignoring")

return nil
Expand All @@ -88,7 +83,7 @@ func (c *Checker) upgradeResource(ctx context.Context, resource *unikornv1.Clust
return nil
}

logger.Info("bundle upgrading", "from", bundle.Spec.Version, "to", target.Spec.Version)
logger.Info("bundle upgrading")

resource.Spec.ApplicationBundle = &target.Name

Expand All @@ -106,18 +101,6 @@ func (c *Checker) Check(ctx context.Context) error {
return err
}

// Extract the potential upgrade target bundles, these are sorted by version, so
// the newest is on the top, we shall see why later...
bundles := allBundles.Upgradable()
if len(bundles.Items) == 0 {
return errors.ErrNoBundles
}

slices.SortStableFunc(bundles.Items, unikornv1.CompareClusterManagerApplicationBundle)

// Pick the most recent as our upgrade target.
upgradeTarget := &bundles.Items[len(bundles.Items)-1]

resources := &unikornv1.ClusterManagerList{}

if err := c.client.List(ctx, resources, &client.ListOptions{}); err != nil {
Expand All @@ -127,13 +110,44 @@ func (c *Checker) Check(ctx context.Context) error {
for i := range resources.Items {
resource := &resources.Items[i]

// What we need to do is respect semantic versioning, e.g. a major is a breaking
// change therefore auto-upgrade is not allowed.
currentBundle := allBundles.Get(*resource.Spec.ApplicationBundle)
if currentBundle == nil {
return fmt.Errorf("%w: %s", errors.ErrMissingBundle, *resource.Spec.ApplicationBundle)
}

discard := func(bundle unikornv1.ClusterManagerApplicationBundle) bool {
return bundle.Spec.Version.Version.Major() != currentBundle.Spec.Version.Major()
}

allowedBundles := &unikornv1.ClusterManagerApplicationBundleList{
Items: slices.Clone(allBundles.Items),
}

allowedBundles.Items = slices.DeleteFunc(allowedBundles.Items, discard)

// Extract the potential upgrade target bundles, these are sorted by version, so
// the newest is on the top, we shall see why later...
upgradeTagetBundles := allowedBundles.Upgradable()
if len(upgradeTagetBundles.Items) == 0 {
return errors.ErrNoBundles
}

slices.SortStableFunc(upgradeTagetBundles.Items, unikornv1.CompareClusterManagerApplicationBundle)

// Pick the most recent as our upgrade target.
upgradeTarget := &upgradeTagetBundles.Items[len(upgradeTagetBundles.Items)-1]

logger := logger.WithValues(
"organization", resource.Labels[constants.OrganizationLabel],
"project", resource.Labels[constants.ProjectLabel],
"clustermanager", resource.Name,
"from", currentBundle.Spec.Version,
"to", upgradeTarget.Spec.Version,
)

if err := c.upgradeResource(log.IntoContext(ctx, logger), resource, allBundles, upgradeTarget); err != nil {
if err := c.upgradeResource(log.IntoContext(ctx, logger), resource, currentBundle, upgradeTarget); err != nil {
return err
}
}
Expand Down

0 comments on commit 50a2ae2

Please sign in to comment.