Skip to content

Commit ffcd0e0

Browse files
feat(clusterctl): block move when Cluster or ClusterClass is paused
Add checks that ensure clusters and cluster classes are not paused before starting move operations. Signed-off-by: alexandre.vilain <[email protected]>
1 parent 0e860e7 commit ffcd0e0

File tree

4 files changed

+160
-5
lines changed

4 files changed

+160
-5
lines changed

cmd/clusterctl/client/cluster/mover.go

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import (
4141
clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2"
4242
clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3"
4343
logf "sigs.k8s.io/cluster-api/cmd/clusterctl/log"
44+
"sigs.k8s.io/cluster-api/util/annotations"
4445
"sigs.k8s.io/cluster-api/util/conditions"
4546
"sigs.k8s.io/cluster-api/util/patch"
4647
"sigs.k8s.io/cluster-api/util/yaml"
@@ -297,6 +298,25 @@ func getClusterObj(ctx context.Context, proxy Proxy, cluster *node, clusterObj *
297298
return nil
298299
}
299300

301+
// getClusterClassObj retrieves the clusterClassObj corresponding to a node with type ClusterClass.
302+
func getClusterClassObj(ctx context.Context, proxy Proxy, cluster *node, clusterClassObj *clusterv1.ClusterClass) error {
303+
c, err := proxy.NewClient(ctx)
304+
if err != nil {
305+
return err
306+
}
307+
308+
clusterClassObjKey := client.ObjectKey{
309+
Namespace: cluster.identity.Namespace,
310+
Name: cluster.identity.Name,
311+
}
312+
313+
if err := c.Get(ctx, clusterClassObjKey, clusterClassObj); err != nil {
314+
return errors.Wrapf(err, "error reading ClusterClass %s/%s",
315+
cluster.identity.Namespace, cluster.identity.Name)
316+
}
317+
return nil
318+
}
319+
300320
// getMachineObj retrieves the machineObj corresponding to a node with type Machine.
301321
func getMachineObj(ctx context.Context, proxy Proxy, machine *node, machineObj *clusterv1.Machine) error {
302322
c, err := proxy.NewClient(ctx)
@@ -320,9 +340,17 @@ func (o *objectMover) move(ctx context.Context, graph *objectGraph, toProxy Prox
320340
log := logf.Log
321341

322342
clusters := graph.getClusters()
343+
if err := checkClustersNotPaused(ctx, o.fromProxy, clusters); err != nil {
344+
return err
345+
}
346+
323347
log.Info("Moving Cluster API objects", "Clusters", len(clusters))
324348

325349
clusterClasses := graph.getClusterClasses()
350+
if err := checkClusterClassesNotPaused(ctx, o.fromProxy, clusterClasses); err != nil {
351+
return err
352+
}
353+
326354
log.Info("Moving Cluster API objects", "ClusterClasses", len(clusterClasses))
327355

328356
// Sets the pause field on the Cluster object in the source management cluster, so the controllers stop reconciling it.
@@ -395,9 +423,17 @@ func (o *objectMover) toDirectory(ctx context.Context, graph *objectGraph, direc
395423
log := logf.Log
396424

397425
clusters := graph.getClusters()
426+
if err := checkClustersNotPaused(ctx, o.fromProxy, clusters); err != nil {
427+
return err
428+
}
429+
398430
log.Info("Starting move of Cluster API objects", "Clusters", len(clusters))
399431

400432
clusterClasses := graph.getClusterClasses()
433+
if err := checkClusterClassesNotPaused(ctx, o.fromProxy, clusterClasses); err != nil {
434+
return err
435+
}
436+
401437
log.Info("Moving Cluster API objects", "ClusterClasses", len(clusterClasses))
402438

403439
// Sets the pause field on the Cluster object in the source management cluster, so the controllers stop reconciling it.
@@ -611,6 +647,40 @@ func setClusterClassPause(ctx context.Context, proxy Proxy, clusterclasses []*no
611647
return nil
612648
}
613649

650+
// checkClustersNotPaused checks that no cluster in the graph is paused before proceeding.
651+
func checkClustersNotPaused(ctx context.Context, proxy Proxy, clusters []*node) error {
652+
for i := range clusters {
653+
cluster := clusters[i]
654+
clusterObj := &clusterv1.Cluster{}
655+
if err := getClusterObj(ctx, proxy, cluster, clusterObj); err != nil {
656+
return err
657+
}
658+
659+
if ptr.Deref(clusterObj.Spec.Paused, false) || annotations.HasPaused(clusterObj) {
660+
return errors.Errorf("cannot start operation while Cluster %s/%s is paused", clusterObj.Namespace, clusterObj.Name)
661+
}
662+
}
663+
664+
return nil
665+
}
666+
667+
// checkClusterClassesNotPaused checks that no clusterClass in the graph is paused before proceeding.
668+
func checkClusterClassesNotPaused(ctx context.Context, proxy Proxy, clusterClasses []*node) error {
669+
for i := range clusterClasses {
670+
clusterClass := clusterClasses[i]
671+
clusterClassObj := &clusterv1.ClusterClass{}
672+
if err := getClusterClassObj(ctx, proxy, clusterClass, clusterClassObj); err != nil {
673+
return err
674+
}
675+
676+
if annotations.HasPaused(clusterClassObj) {
677+
return errors.Errorf("cannot start operation while ClusterClass %s/%s is paused", clusterClassObj.Namespace, clusterClassObj.Name)
678+
}
679+
}
680+
681+
return nil
682+
}
683+
614684
func waitReadyForMove(ctx context.Context, proxy Proxy, nodes []*node, dryRun bool, backoff wait.Backoff) error {
615685
if dryRun {
616686
return nil
@@ -723,7 +793,8 @@ func pauseClusterClass(ctx context.Context, proxy Proxy, n *node, pause bool, mu
723793
ObjectMeta: metav1.ObjectMeta{
724794
Name: n.identity.Name,
725795
Namespace: n.identity.Namespace,
726-
}}, mutators...)
796+
},
797+
}, mutators...)
727798
if err != nil {
728799
return err
729800
}
@@ -1072,7 +1143,7 @@ func (o *objectMover) backupTargetObject(ctx context.Context, nodeToCreate *node
10721143
}
10731144
}
10741145

1075-
err = os.WriteFile(objectFile, byObj, 0600)
1146+
err = os.WriteFile(objectFile, byObj, 0o600)
10761147
if err != nil {
10771148
return err
10781149
}
@@ -1173,7 +1244,6 @@ func (o *objectMover) deleteGroup(ctx context.Context, group moveGroup) error {
11731244
err := retryWithExponentialBackoff(ctx, deleteSourceObjectBackoff, func(ctx context.Context) error {
11741245
return o.deleteSourceObject(ctx, nodeToDelete)
11751246
})
1176-
11771247
if err != nil {
11781248
errList = append(errList, err)
11791249
}

cmd/clusterctl/client/cluster/mover_test.go

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,40 @@ var moveTests = []struct {
9898
},
9999
wantErr: false,
100100
},
101+
{
102+
name: "Paused Cluster",
103+
fields: moveTestsFields{
104+
objs: test.NewFakeCluster("ns1", "foo").WithPaused().Objs(),
105+
},
106+
wantMoveGroups: [][]string{
107+
{ // group 1
108+
clusterv1.GroupVersion.String() + ", Kind=Cluster, ns1/foo",
109+
},
110+
{ // group 2 (objects with ownerReferences in group 1)
111+
// owned by Clusters
112+
"/v1, Kind=Secret, ns1/foo-ca",
113+
"/v1, Kind=Secret, ns1/foo-kubeconfig",
114+
clusterv1.GroupVersionInfrastructure.String() + ", Kind=GenericInfrastructureCluster, ns1/foo",
115+
},
116+
},
117+
wantErr: true,
118+
},
119+
{
120+
name: "Paused ClusterClass",
121+
fields: moveTestsFields{
122+
objs: test.NewFakeClusterClass("ns1", "class1").WithPaused().Objs(),
123+
},
124+
wantMoveGroups: [][]string{
125+
{ // group 1
126+
clusterv1.GroupVersion.String() + ", Kind=ClusterClass, ns1/class1",
127+
},
128+
{ // group 2
129+
clusterv1.GroupVersionInfrastructure.String() + ", Kind=GenericInfrastructureClusterTemplate, ns1/class1",
130+
clusterv1.GroupVersionControlPlane.String() + ", Kind=GenericControlPlaneTemplate, ns1/class1",
131+
},
132+
},
133+
wantErr: true,
134+
},
101135
{
102136
name: "Cluster with cloud config secret with the force move label",
103137
fields: moveTestsFields{
@@ -923,8 +957,29 @@ func Test_objectMover_restoreTargetObject(t *testing.T) {
923957
}
924958

925959
func Test_objectMover_toDirectory(t *testing.T) {
926-
// NB. we are testing the move and move sequence using the same set of moveTests, but checking the results at different stages of the move process
927-
for _, tt := range backupRestoreTests {
960+
tests := []struct {
961+
name string
962+
fields moveTestsFields
963+
files map[string]string
964+
wantErr bool
965+
}{
966+
{
967+
name: "Cluster is paused",
968+
fields: moveTestsFields{
969+
objs: test.NewFakeCluster("ns1", "foo").WithPaused().Objs(),
970+
},
971+
wantErr: true,
972+
},
973+
{
974+
name: "ClusterClass is paused",
975+
fields: moveTestsFields{
976+
objs: test.NewFakeClusterClass("ns1", "foo").WithPaused().Objs(),
977+
},
978+
wantErr: true,
979+
},
980+
}
981+
tests = append(tests, backupRestoreTests...)
982+
for _, tt := range tests {
928983
t.Run(tt.name, func(t *testing.T) {
929984
g := NewWithT(t)
930985

cmd/clusterctl/internal/test/fake_objects.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import (
4848
type FakeCluster struct {
4949
namespace string
5050
name string
51+
paused bool
5152
controlPlane *FakeControlPlane
5253
machinePools []*FakeMachinePool
5354
machineDeployments []*FakeMachineDeployment
@@ -117,6 +118,11 @@ func (f *FakeCluster) WithTopologyClassNamespace(namespace string) *FakeCluster
117118
return f
118119
}
119120

121+
func (f *FakeCluster) WithPaused() *FakeCluster {
122+
f.paused = true
123+
return f
124+
}
125+
120126
func (f *FakeCluster) Objs() []client.Object {
121127
clusterInfrastructure := &fakeinfrastructure.GenericInfrastructureCluster{
122128
TypeMeta: metav1.TypeMeta{
@@ -161,6 +167,10 @@ func (f *FakeCluster) Objs() []client.Object {
161167
}
162168
}
163169

170+
if f.paused {
171+
cluster.Spec.Paused = ptr.To(true)
172+
}
173+
164174
// Ensure the cluster gets a UID to be used by dependant objects for creating OwnerReferences.
165175
setUID(cluster)
166176

@@ -1486,6 +1496,7 @@ func FakeCRDList() []*apiextensionsv1.CustomResourceDefinition {
14861496
type FakeClusterClass struct {
14871497
namespace string
14881498
name string
1499+
paused bool
14891500
infrastructureClusterTemplate *unstructured.Unstructured
14901501
controlPlaneTemplate *unstructured.Unstructured
14911502
controlPlaneInfrastructureMachineTemplate *unstructured.Unstructured
@@ -1519,6 +1530,11 @@ func (f *FakeClusterClass) WithWorkerMachineDeploymentClasses(classes []*FakeMac
15191530
return f
15201531
}
15211532

1533+
func (f *FakeClusterClass) WithPaused() *FakeClusterClass {
1534+
f.paused = true
1535+
return f
1536+
}
1537+
15221538
func (f *FakeClusterClass) Objs() []client.Object {
15231539
// objMap map where the key is the object to which the owner reference to the cluster class should be added
15241540
// and the value dictates if the onwner ref needs to be added.
@@ -1546,6 +1562,10 @@ func (f *FakeClusterClass) Objs() []client.Object {
15461562
objMap[f.controlPlaneInfrastructureMachineTemplate] = true
15471563
}
15481564

1565+
if f.paused {
1566+
clusterClassBuilder.WithAnnotations(map[string]string{clusterv1.PausedAnnotation: "true"})
1567+
}
1568+
15491569
if len(f.workerMachineDeploymentClasses) > 0 {
15501570
mdClasses := []clusterv1.MachineDeploymentClass{}
15511571
for _, fakeMDClass := range f.workerMachineDeploymentClasses {

util/test/builder/builders.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,7 @@ func (m *MachinePoolTopologyBuilder) Build() clusterv1.MachinePoolTopology {
342342
type ClusterClassBuilder struct {
343343
namespace string
344344
name string
345+
annotations map[string]string
345346
infrastructureClusterTemplate *unstructured.Unstructured
346347
controlPlaneMetadata *clusterv1.ObjectMeta
347348
controlPlaneReadinessGates []clusterv1.MachineReadinessGate
@@ -370,6 +371,12 @@ func ClusterClass(namespace, name string) *ClusterClassBuilder {
370371
}
371372
}
372373

374+
// WithAnnotations adds the passed annotations to the ClusterClassBuilder.
375+
func (c *ClusterClassBuilder) WithAnnotations(annotations map[string]string) *ClusterClassBuilder {
376+
c.annotations = annotations
377+
return c
378+
}
379+
373380
// WithInfrastructureClusterTemplate adds the passed InfrastructureClusterTemplate to the ClusterClassBuilder.
374381
func (c *ClusterClassBuilder) WithInfrastructureClusterTemplate(t *unstructured.Unstructured) *ClusterClassBuilder {
375382
c.infrastructureClusterTemplate = t
@@ -502,6 +509,9 @@ func (c *ClusterClassBuilder) Build() *clusterv1.ClusterClass {
502509
Variables: c.statusVariables,
503510
},
504511
}
512+
if c.annotations != nil {
513+
obj.Annotations = c.annotations
514+
}
505515
if c.conditions != nil {
506516
obj.Status.Conditions = c.conditions
507517
}

0 commit comments

Comments
 (0)