Skip to content

Commit 9dbfb4f

Browse files
committed
Adds command and client for the clusterctl alpha rollout undo
for MachineDeployment.
1 parent 046ab29 commit 9dbfb4f

15 files changed

+564
-14
lines changed

cmd/clusterctl/client/alpha/rollout.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,16 @@ import (
2121
"sigs.k8s.io/cluster-api/cmd/clusterctl/internal/util"
2222
)
2323

24-
const machineDeployment = "machinedeployment"
24+
const MachineDeployment = "machinedeployment"
2525

26-
var validResourceTypes = []string{machineDeployment}
26+
var validResourceTypes = []string{MachineDeployment}
2727

2828
// Rollout defines the behavior of a rollout implementation.
2929
type Rollout interface {
3030
ObjectRestarter(cluster.Proxy, util.ResourceTuple, string) error
3131
ObjectPauser(cluster.Proxy, util.ResourceTuple, string) error
3232
ObjectResumer(cluster.Proxy, util.ResourceTuple, string) error
33+
ObjectRollbacker(cluster.Proxy, util.ResourceTuple, string, int64) error
3334
}
3435

3536
var _ Rollout = &rollout{}

cmd/clusterctl/client/alpha/rollout_pauser.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import (
2929
// ObjectPauser will issue a pause on the specified cluster-api resource.
3030
func (r *rollout) ObjectPauser(proxy cluster.Proxy, tuple util.ResourceTuple, namespace string) error {
3131
switch tuple.Resource {
32-
case machineDeployment:
32+
case MachineDeployment:
3333
deployment, err := getMachineDeployment(proxy, tuple.Name, namespace)
3434
if err != nil || deployment == nil {
3535
return errors.Wrapf(err, "failed to fetch %v/%v", tuple.Resource, tuple.Name)

cmd/clusterctl/client/alpha/rollout_pauser_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ func Test_ObjectPauser(t *testing.T) {
5555
},
5656
},
5757
tuple: util.ResourceTuple{
58-
Resource: "machinedeployment",
58+
Resource: MachineDeployment,
5959
Name: "md-1",
6060
},
6161
namespace: "default",
@@ -81,7 +81,7 @@ func Test_ObjectPauser(t *testing.T) {
8181
},
8282
},
8383
tuple: util.ResourceTuple{
84-
Resource: "machinedeployment",
84+
Resource: MachineDeployment,
8585
Name: "md-1",
8686
},
8787
namespace: "default",

cmd/clusterctl/client/alpha/rollout_restarter.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import (
3232
// ObjectRestarter will issue a restart on the specified cluster-api resource.
3333
func (r *rollout) ObjectRestarter(proxy cluster.Proxy, tuple util.ResourceTuple, namespace string) error {
3434
switch tuple.Resource {
35-
case "machinedeployment":
35+
case MachineDeployment:
3636
deployment, err := getMachineDeployment(proxy, tuple.Name, namespace)
3737
if err != nil || deployment == nil {
3838
return errors.Wrapf(err, "failed to fetch %v/%v", tuple.Resource, tuple.Name)

cmd/clusterctl/client/alpha/rollout_restarter_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ func Test_ObjectRestarter(t *testing.T) {
5656
},
5757
},
5858
tuple: util.ResourceTuple{
59-
Resource: "machinedeployment",
59+
Resource: MachineDeployment,
6060
Name: "md-1",
6161
},
6262
namespace: "default",
@@ -83,7 +83,7 @@ func Test_ObjectRestarter(t *testing.T) {
8383
},
8484
},
8585
tuple: util.ResourceTuple{
86-
Resource: "machinedeployment",
86+
Resource: MachineDeployment,
8787
Name: "md-1",
8888
},
8989
namespace: "default",

cmd/clusterctl/client/alpha/rollout_resumer.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import (
2929
// ObjectResumer will issue a resume on the specified cluster-api resource.
3030
func (r *rollout) ObjectResumer(proxy cluster.Proxy, tuple util.ResourceTuple, namespace string) error {
3131
switch tuple.Resource {
32-
case "machinedeployment":
32+
case MachineDeployment:
3333
deployment, err := getMachineDeployment(proxy, tuple.Name, namespace)
3434
if err != nil || deployment == nil {
3535
return errors.Wrapf(err, "failed to fetch %v/%v", tuple.Resource, tuple.Name)

cmd/clusterctl/client/alpha/rollout_resumer_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ func Test_ObjectResumer(t *testing.T) {
5858
},
5959
},
6060
tuple: util.ResourceTuple{
61-
Resource: "machinedeployment",
61+
Resource: MachineDeployment,
6262
Name: "md-1",
6363
},
6464
namespace: "default",
@@ -84,7 +84,7 @@ func Test_ObjectResumer(t *testing.T) {
8484
},
8585
},
8686
tuple: util.ResourceTuple{
87-
Resource: "machinedeployment",
87+
Resource: MachineDeployment,
8888
Name: "md-1",
8989
},
9090
namespace: "default",
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
/*
2+
Copyright 2020 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package alpha
18+
19+
import (
20+
"context"
21+
22+
"github.com/pkg/errors"
23+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
24+
"k8s.io/apimachinery/pkg/labels"
25+
clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha4"
26+
"sigs.k8s.io/cluster-api/cmd/clusterctl/client/cluster"
27+
"sigs.k8s.io/cluster-api/cmd/clusterctl/internal/util"
28+
logf "sigs.k8s.io/cluster-api/cmd/clusterctl/log"
29+
"sigs.k8s.io/cluster-api/controllers/mdutil"
30+
"sigs.k8s.io/cluster-api/util/patch"
31+
"sigs.k8s.io/controller-runtime/pkg/client"
32+
)
33+
34+
// ObjectRollbacker will issue a rollback on the specified cluster-api resource.
35+
func (r *rollout) ObjectRollbacker(proxy cluster.Proxy, tuple util.ResourceTuple, namespace string, toRevision int64) error {
36+
switch tuple.Resource {
37+
case MachineDeployment:
38+
deployment, err := getMachineDeployment(proxy, tuple.Name, namespace)
39+
if err != nil || deployment == nil {
40+
return errors.Wrapf(err, "failed to get %v/%v", tuple.Resource, tuple.Name)
41+
}
42+
if deployment.Spec.Paused {
43+
return errors.Errorf("can't rollback a paused MachineDeployment: please run 'clusterctl rollout resume %v/%v' first", tuple.Resource, tuple.Name)
44+
}
45+
if err := rollbackMachineDeployment(proxy, deployment, tuple.Name, toRevision); err != nil {
46+
return err
47+
}
48+
default:
49+
return errors.Errorf("invalid resource type %q, valid values are %v", tuple.Resource, validResourceTypes)
50+
}
51+
return nil
52+
}
53+
54+
// rollbackMachineDeployment will rollback to a previous MachineSet revision used by this MachineDeployment.
55+
func rollbackMachineDeployment(proxy cluster.Proxy, d *clusterv1.MachineDeployment, name string, toRevision int64) error {
56+
log := logf.Log
57+
c, err := proxy.NewClient()
58+
if err != nil {
59+
return err
60+
}
61+
62+
if toRevision < 0 {
63+
return errors.Errorf("revision number cannot be negative: %v", toRevision)
64+
}
65+
msList, err := getMachineSetsForDeployment(proxy, d)
66+
if err != nil {
67+
return err
68+
}
69+
log.V(7).Info("Found MachineSets", "count", len(msList))
70+
msForRevision, err := findMachineDeploymentRevision(toRevision, msList)
71+
if err != nil {
72+
return err
73+
}
74+
log.V(7).Info("Found revision", "revision", msForRevision)
75+
patchHelper, err := patch.NewHelper(d, c)
76+
if err != nil {
77+
return err
78+
}
79+
// Copy template into the machinedeployment (excluding the hash)
80+
revMSTemplate := *msForRevision.Spec.Template.DeepCopy()
81+
delete(revMSTemplate.Labels, mdutil.DefaultMachineDeploymentUniqueLabelKey)
82+
83+
d.Spec.Template = revMSTemplate
84+
return patchHelper.Patch(context.TODO(), d)
85+
}
86+
87+
// findMachineDeploymentRevision finds the specific revision in the machine sets
88+
func findMachineDeploymentRevision(toRevision int64, allMSs []*clusterv1.MachineSet) (*clusterv1.MachineSet, error) {
89+
var (
90+
latestMachineSet *clusterv1.MachineSet
91+
latestRevision = int64(-1)
92+
previousMachineSet *clusterv1.MachineSet
93+
previousRevision = int64(-1)
94+
)
95+
for _, ms := range allMSs {
96+
if v, err := mdutil.Revision(ms); err == nil {
97+
if toRevision == 0 {
98+
if latestRevision < v {
99+
// newest one we've seen so far
100+
previousRevision = latestRevision
101+
previousMachineSet = latestMachineSet
102+
latestRevision = v
103+
latestMachineSet = ms
104+
} else if previousRevision < v {
105+
// second newest one we've seen so far
106+
previousRevision = v
107+
previousMachineSet = ms
108+
}
109+
} else if toRevision == v {
110+
return ms, nil
111+
}
112+
}
113+
}
114+
115+
if toRevision > 0 {
116+
return nil, errors.Errorf("unable to find specified revision: %v", toRevision)
117+
}
118+
119+
if previousMachineSet == nil {
120+
return nil, errors.Errorf("no rollout history found")
121+
}
122+
return previousMachineSet, nil
123+
124+
}
125+
126+
// getMachineSetsForDeployment returns a list of MachineSets associated with a MachineDeployment.
127+
func getMachineSetsForDeployment(proxy cluster.Proxy, d *clusterv1.MachineDeployment) ([]*clusterv1.MachineSet, error) {
128+
log := logf.Log
129+
c, err := proxy.NewClient()
130+
if err != nil {
131+
return nil, err
132+
}
133+
// List all MachineSets to find those we own but that no longer match our selector.
134+
machineSets := &clusterv1.MachineSetList{}
135+
if err := c.List(context.TODO(), machineSets, client.InNamespace(d.Namespace)); err != nil {
136+
return nil, err
137+
}
138+
139+
filtered := make([]*clusterv1.MachineSet, 0, len(machineSets.Items))
140+
for idx := range machineSets.Items {
141+
ms := &machineSets.Items[idx]
142+
143+
// Skip this MachineSet if its controller ref is not pointing to this MachineDeployment
144+
if !metav1.IsControlledBy(ms, d) {
145+
log.V(5).Info("Skipping MachineSet, controller ref does not match MachineDeployment", "machineset", ms.Name)
146+
continue
147+
}
148+
149+
selector, err := metav1.LabelSelectorAsSelector(&d.Spec.Selector)
150+
if err != nil {
151+
log.V(5).Info("Skipping MachineSet, failed to get label selector from spec selector", "machineset", ms.Name)
152+
continue
153+
}
154+
// If a MachineDeployment with a nil or empty selector creeps in, it should match nothing, not everything.
155+
if selector.Empty() {
156+
log.V(5).Info("Skipping MachineSet as the selector is empty", "machineset", ms.Name)
157+
continue
158+
}
159+
// Skip this MachineSet if selector does not match
160+
if !selector.Matches(labels.Set(ms.Labels)) {
161+
log.V(5).Info("Skipping MachineSet, label mismatch", "machineset", ms.Name)
162+
continue
163+
}
164+
filtered = append(filtered, ms)
165+
}
166+
167+
return filtered, nil
168+
}

0 commit comments

Comments
 (0)