Skip to content

Commit db1abdc

Browse files
committed
Adds command and client for the clusterctl alpha rollout undo
for MachineDeployment.
1 parent 168bdc7 commit db1abdc

File tree

8 files changed

+567
-3
lines changed

8 files changed

+567
-3
lines changed

cmd/clusterctl/client/alpha/rollout.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ 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{}
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
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 fetch %v/%v", tuple.Resource, tuple.Name)
41+
}
42+
if deployment.Spec.Paused {
43+
return errors.Errorf("can't rollback a paused MachineDeployment (run rollout resume first): %v/%v\n", tuple.Resource, tuple.Name)
44+
}
45+
if err := rollbackMachineDeployment(proxy, deployment, tuple.Name, namespace, 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, namespace 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+
90+
var (
91+
latestMachineSet *clusterv1.MachineSet
92+
latestRevision = int64(-1)
93+
previousMachineSet *clusterv1.MachineSet
94+
previousRevision = int64(-1)
95+
)
96+
for _, ms := range allMSs {
97+
if v, err := mdutil.Revision(ms); err == nil {
98+
if toRevision == 0 {
99+
if latestRevision < v {
100+
// newest one we've seen so far
101+
previousRevision = latestRevision
102+
previousMachineSet = latestMachineSet
103+
latestRevision = v
104+
latestMachineSet = ms
105+
} else if previousRevision < v {
106+
// second newest one we've seen so far
107+
previousRevision = v
108+
previousMachineSet = ms
109+
}
110+
} else if toRevision == v {
111+
return ms, nil
112+
}
113+
}
114+
}
115+
116+
if toRevision > 0 {
117+
return nil, errors.Errorf("unable to find specified revision: %v", toRevision)
118+
}
119+
120+
if previousMachineSet == nil {
121+
return nil, errors.Errorf("no rollout history found")
122+
}
123+
return previousMachineSet, nil
124+
125+
}
126+
127+
// getMachineSetsForDeployment returns a list of MachineSets associated with a MachineDeployment.
128+
func getMachineSetsForDeployment(proxy cluster.Proxy, d *clusterv1.MachineDeployment) ([]*clusterv1.MachineSet, error) {
129+
log := logf.Log
130+
c, err := proxy.NewClient()
131+
if err != nil {
132+
return nil, err
133+
}
134+
// List all MachineSets to find those we own but that no longer match our selector.
135+
machineSets := &clusterv1.MachineSetList{}
136+
if err := c.List(context.TODO(), machineSets, client.InNamespace(d.Namespace)); err != nil {
137+
return nil, err
138+
}
139+
140+
filtered := make([]*clusterv1.MachineSet, 0, len(machineSets.Items))
141+
for idx := range machineSets.Items {
142+
ms := &machineSets.Items[idx]
143+
144+
selector, err := metav1.LabelSelectorAsSelector(&d.Spec.Selector)
145+
if err != nil {
146+
log.V(5).Info("Skipping MachineSet, failed to get label selector from spec selector", "machineset", ms.Name)
147+
continue
148+
}
149+
150+
// If a MachineDeployment with a nil or empty selector creeps in, it should match nothing, not everything.
151+
if selector.Empty() {
152+
log.V(5).Info("Skipping MachineSet as the selector is empty", "machineset", ms.Name)
153+
continue
154+
}
155+
156+
// Skip this MachineSet if selector does not matche
157+
if !selector.Matches(labels.Set(ms.Labels)) {
158+
log.V(5).Info("Skipping MachineSet, label mismatch", "machineset", ms.Name)
159+
continue
160+
}
161+
// Skip this MachineSet if its controller ref is not pointing to this MachineDeployment
162+
if !metav1.IsControlledBy(ms, d) {
163+
log.V(5).Info("Skipping MachineSet, controller ref does not match MachineDeployment", "machineset", ms.Name)
164+
continue
165+
}
166+
167+
filtered = append(filtered, ms)
168+
}
169+
170+
return filtered, nil
171+
}

0 commit comments

Comments
 (0)