Skip to content

Commit

Permalink
Merge pull request kubernetes-sigs#29 from abhinavmpandey08/in-place-md
Browse files Browse the repository at this point in the history
✨ Add support for in-place upgrades in MachineDeployments
  • Loading branch information
abhinavmpandey08 committed Jan 25, 2024
2 parents f2c51df + 521849b commit 732048b
Show file tree
Hide file tree
Showing 8 changed files with 173 additions and 1 deletion.
11 changes: 10 additions & 1 deletion api/v1beta1/machinedeployment_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ const (
// i.e. gradually scale down the old MachineSet and scale up the new one.
RollingUpdateMachineDeploymentStrategyType MachineDeploymentStrategyType = "RollingUpdate"

// InPlaceMachineDeploymentStrategyType upgrades the machines within the same MachineSet without rolling out any new nodes.
InPlaceMachineDeploymentStrategyType MachineDeploymentStrategyType = "InPlace"

// OnDeleteMachineDeploymentStrategyType replaces old MachineSets when the deletion of the associated machines are completed.
OnDeleteMachineDeploymentStrategyType MachineDeploymentStrategyType = "OnDelete"

Expand All @@ -54,6 +57,12 @@ const (
// proportions in case the deployment has surge replicas.
MaxReplicasAnnotation = "machinedeployment.clusters.x-k8s.io/max-replicas"

// MachineDeploymentInPlaceUpgradeAnnotation is used to denote that the MachineDeployment needs to be in-place upgraded by an external entity.
// This annotation will be added to the MD object when `strategy.type` is set to `InPlace`.
// The external upgrader entity should watch for the annotation and trigger an upgrade when it's added.
// Once the upgrade is complete, the external upgrade implementer is also responsible for removing this annotation.
MachineDeploymentInPlaceUpgradeAnnotation = "machinedeployment.clusters.x-k8s.io/in-place-upgrade-needed"

// MachineDeploymentUniqueLabel is used to uniquely identify the Machines of a MachineSet.
// The MachineDeployment controller will set this label on a MachineSet when it is created.
// The label is also applied to the Machines of the MachineSet and used in the MachineSet selector.
Expand Down Expand Up @@ -154,7 +163,7 @@ type MachineDeploymentSpec struct {
type MachineDeploymentStrategy struct {
// Type of deployment. Allowed values are RollingUpdate and OnDelete.
// The default is RollingUpdate.
// +kubebuilder:validation:Enum=RollingUpdate;OnDelete
// +kubebuilder:validation:Enum=RollingUpdate;OnDelete;InPlace
// +optional
Type MachineDeploymentStrategyType `json:"type,omitempty"`

Expand Down
1 change: 1 addition & 0 deletions config/crd/bases/cluster.x-k8s.io_clusterclasses.yaml

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

1 change: 1 addition & 0 deletions config/crd/bases/cluster.x-k8s.io_clusters.yaml

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

1 change: 1 addition & 0 deletions config/crd/bases/cluster.x-k8s.io_machinedeployments.yaml

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

Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,10 @@ func (r *Reconciler) reconcile(ctx context.Context, cluster *clusterv1.Cluster,
return r.rolloutRolling(ctx, md, msList)
}

if md.Spec.Strategy.Type == clusterv1.InPlaceMachineDeploymentStrategyType {
return r.rolloutInPlace(ctx, md, msList)
}

if md.Spec.Strategy.Type == clusterv1.OnDeleteMachineDeploymentStrategyType {
return r.rolloutOnDelete(ctx, md, msList)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package machinedeployment

import (
"context"

"github.com/pkg/errors"
kerrors "k8s.io/apimachinery/pkg/util/errors"
ctrl "sigs.k8s.io/controller-runtime"

clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
"sigs.k8s.io/cluster-api/util/annotations"
)

func (r *Reconciler) rolloutInPlace(ctx context.Context, md *clusterv1.MachineDeployment, msList []*clusterv1.MachineSet) (reterr error) {
log := ctrl.LoggerFrom(ctx)

// For in-place upgrade, we shouldn't try to create a new MachineSet as that would trigger a rollout.
// Instead, we should try to get latest MachineSet that matches the MachineDeployment.Spec.Template/
// If no such MachineSet exists yet, this means the MachineSet hasn't been in-place upgraded yet.
// The external in-place upgrade implementer is responsible for updating the latest MachineSet's template
// after in-place upgrade of all worker nodes belonging to the MD is complete.
// Once the MachineSet is updated, this function will return the latest MachineSet that matches the
// MachineDeployment template and thus we can deduce that the in-place upgrade is complete.
newMachineSet, oldMachineSets, err := r.getAllMachineSetsAndSyncRevision(ctx, md, msList, false)
if err != nil {
return err
}

defer func() {
allMSs := append(oldMachineSets, newMachineSet)

// Always attempt to sync the status
err := r.syncDeploymentStatus(allMSs, newMachineSet, md)
reterr = kerrors.NewAggregate([]error{reterr, err})
}()

if newMachineSet == nil {
log.Info("Changes detected, InPlace upgrade strategy detected, adding the annotation")
annotations.AddAnnotations(md, map[string]string{clusterv1.MachineDeploymentInPlaceUpgradeAnnotation: "true"})
return errors.New("new MachineSet not found. This most likely means that the in-place upgrade hasn't finished yet")
}

return r.sync(ctx, md, msList)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package machinedeployment

import (
"testing"

. "github.com/onsi/gomega"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/tools/record"
"k8s.io/utils/pointer"
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)

const (
mdName = "my-md"
msName = "my-ms"
version128 = "v1.28.0"
version129 = "v1.29.0"
)

func getMachineDeployment(name string, version string, replicas int32) *clusterv1.MachineDeployment {
return &clusterv1.MachineDeployment{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Spec: clusterv1.MachineDeploymentSpec{
Strategy: &clusterv1.MachineDeploymentStrategy{
Type: clusterv1.InPlaceMachineDeploymentStrategyType,
},
Replicas: pointer.Int32(replicas),
Template: clusterv1.MachineTemplateSpec{
Spec: clusterv1.MachineSpec{
ClusterName: "my-cluster",
Version: pointer.String(version),
},
},
},
}
}

func getMachineSet(name string, version string, replicas int32) *clusterv1.MachineSet {
return &clusterv1.MachineSet{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Spec: clusterv1.MachineSetSpec{
Replicas: pointer.Int32(replicas),
Template: clusterv1.MachineTemplateSpec{
Spec: clusterv1.MachineSpec{
ClusterName: "my-cluster",
Version: pointer.String(version),
},
},
},
}
}

func TestRolloutInPlace(t *testing.T) {
testCases := []struct {
name string
machineDeployment *clusterv1.MachineDeployment
msList []*clusterv1.MachineSet
annotationExpected bool
expectErr bool
}{
{
name: "MD template matches MS template",
machineDeployment: getMachineDeployment(mdName, version128, 2),
msList: []*clusterv1.MachineSet{getMachineSet(msName, version128, 2)},
annotationExpected: false,
expectErr: false,
},
{
name: "MD template doesn't MS template",
machineDeployment: getMachineDeployment(mdName, version128, 2),
msList: []*clusterv1.MachineSet{getMachineSet(msName, version129, 2)},
annotationExpected: true,
expectErr: true,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
g := NewWithT(t)

resources := []client.Object{
tc.machineDeployment,
}

for key := range tc.msList {
resources = append(resources, tc.msList[key])
}

r := &Reconciler{
Client: fake.NewClientBuilder().WithObjects(resources...).Build(),
recorder: record.NewFakeRecorder(32),
}

err := r.rolloutInPlace(ctx, tc.machineDeployment, tc.msList)
if tc.expectErr {
g.Expect(err).To(HaveOccurred())
}

_, ok := tc.machineDeployment.Annotations[clusterv1.MachineDeploymentInPlaceUpgradeAnnotation]
g.Expect(ok).To(Equal(tc.annotationExpected))
})
}

}
2 changes: 2 additions & 0 deletions internal/controllers/machinedeployment/mdutil/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,8 @@ func NewMSNewReplicas(deployment *clusterv1.MachineDeployment, allMSs []*cluster
// the desired number of replicas in the MachineDeployment
scaleUpCount := *(deployment.Spec.Replicas) - currentMachineCount
return newMSReplicas + scaleUpCount, nil
case clusterv1.InPlaceMachineDeploymentStrategyType:
return 0, nil
default:
return 0, fmt.Errorf("failed to compute replicas: deployment strategy %v isn't supported", deployment.Spec.Strategy.Type)
}
Expand Down

0 comments on commit 732048b

Please sign in to comment.