Skip to content

Commit d3f233a

Browse files
committed
readd util/deprecated/v1beta1/paused
1 parent 352aa46 commit d3f233a

File tree

2 files changed

+271
-0
lines changed

2 files changed

+271
-0
lines changed
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/*
2+
Copyright 2024 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 paused implements paused helper functions.
18+
package paused
19+
20+
import (
21+
"context"
22+
"fmt"
23+
"strings"
24+
25+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
26+
"k8s.io/apimachinery/pkg/runtime"
27+
ctrl "sigs.k8s.io/controller-runtime"
28+
"sigs.k8s.io/controller-runtime/pkg/client"
29+
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
30+
31+
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
32+
"sigs.k8s.io/cluster-api/util/annotations"
33+
v1beta2conditions "sigs.k8s.io/cluster-api/util/conditions/v1beta2"
34+
"sigs.k8s.io/cluster-api/util/patch"
35+
)
36+
37+
// ConditionSetter combines the client.Object and Setter interface.
38+
type ConditionSetter interface {
39+
v1beta2conditions.Setter
40+
client.Object
41+
}
42+
43+
// EnsurePausedCondition sets the paused condition on the object and returns if it should be considered as paused.
44+
func EnsurePausedCondition(ctx context.Context, c client.Client, cluster *clusterv1.Cluster, obj ConditionSetter) (isPaused bool, requeue bool, err error) {
45+
oldCondition := v1beta2conditions.Get(obj, clusterv1.PausedV1Beta2Condition)
46+
newCondition := pausedCondition(c.Scheme(), cluster, obj, clusterv1.PausedV1Beta2Condition)
47+
48+
isPaused = newCondition.Status == metav1.ConditionTrue
49+
pausedStatusChanged := oldCondition == nil || oldCondition.Status != newCondition.Status
50+
51+
log := ctrl.LoggerFrom(ctx)
52+
53+
switch {
54+
case pausedStatusChanged && isPaused:
55+
log.V(4).Info("Pausing reconciliation for this object", "reason", newCondition.Message)
56+
case pausedStatusChanged && !isPaused:
57+
log.V(4).Info("Unpausing reconciliation for this object")
58+
case !pausedStatusChanged && isPaused:
59+
log.V(6).Info("Reconciliation is paused for this object", "reason", newCondition.Message)
60+
}
61+
62+
if oldCondition != nil {
63+
// Return early if the paused condition did not change at all.
64+
if v1beta2conditions.HasSameState(oldCondition, &newCondition) {
65+
return isPaused, false, nil
66+
}
67+
68+
// Set condition and return early if only observed generation changed and obj is not paused.
69+
// In this case we want to avoid the additional reconcile that we would get by requeueing.
70+
if v1beta2conditions.HasSameStateExceptObservedGeneration(oldCondition, &newCondition) && !isPaused {
71+
v1beta2conditions.Set(obj, newCondition)
72+
return isPaused, false, nil
73+
}
74+
}
75+
76+
patchHelper, err := patch.NewHelper(obj, c)
77+
if err != nil {
78+
return isPaused, false, err
79+
}
80+
81+
v1beta2conditions.Set(obj, newCondition)
82+
83+
if err := patchHelper.Patch(ctx, obj, patch.WithOwnedV1Beta2Conditions{Conditions: []string{
84+
clusterv1.PausedV1Beta2Condition,
85+
}}); err != nil {
86+
return isPaused, false, err
87+
}
88+
89+
return isPaused, true, nil
90+
}
91+
92+
// pausedCondition sets the paused condition on the object and returns if it should be considered as paused.
93+
func pausedCondition(scheme *runtime.Scheme, cluster *clusterv1.Cluster, obj ConditionSetter, targetConditionType string) metav1.Condition {
94+
if (cluster != nil && cluster.Spec.Paused) || annotations.HasPaused(obj) {
95+
var messages []string
96+
if cluster != nil && cluster.Spec.Paused {
97+
messages = append(messages, "Cluster spec.paused is set to true")
98+
}
99+
if annotations.HasPaused(obj) {
100+
kind := "Object"
101+
if gvk, err := apiutil.GVKForObject(obj, scheme); err == nil {
102+
kind = gvk.Kind
103+
}
104+
messages = append(messages, fmt.Sprintf("%s has the cluster.x-k8s.io/paused annotation", kind))
105+
}
106+
107+
return metav1.Condition{
108+
Type: targetConditionType,
109+
Status: metav1.ConditionTrue,
110+
Reason: clusterv1.PausedV1Beta2Reason,
111+
Message: strings.Join(messages, ", "),
112+
ObservedGeneration: obj.GetGeneration(),
113+
}
114+
}
115+
116+
return metav1.Condition{
117+
Type: targetConditionType,
118+
Status: metav1.ConditionFalse,
119+
Reason: clusterv1.NotPausedV1Beta2Reason,
120+
ObservedGeneration: obj.GetGeneration(),
121+
}
122+
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/*
2+
Copyright 2024 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 paused implements paused helper functions.
18+
package paused
19+
20+
import (
21+
"context"
22+
"testing"
23+
24+
. "github.com/onsi/gomega"
25+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
26+
"k8s.io/apimachinery/pkg/runtime"
27+
"sigs.k8s.io/controller-runtime/pkg/client"
28+
"sigs.k8s.io/controller-runtime/pkg/client/fake"
29+
30+
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
31+
v1beta2conditions "sigs.k8s.io/cluster-api/util/conditions/v1beta2"
32+
"sigs.k8s.io/cluster-api/util/test/builder"
33+
)
34+
35+
func TestEnsurePausedCondition(t *testing.T) {
36+
g := NewWithT(t)
37+
38+
scheme := runtime.NewScheme()
39+
g.Expect(builder.AddTransitionV1Beta2ToScheme(scheme)).To(Succeed())
40+
g.Expect(clusterv1.AddToScheme(scheme)).To(Succeed())
41+
42+
// Cluster Case 1: unpaused
43+
normalCluster := &clusterv1.Cluster{
44+
ObjectMeta: metav1.ObjectMeta{
45+
Name: "some-cluster",
46+
Namespace: "default",
47+
},
48+
}
49+
50+
// Cluster Case 2: paused
51+
pausedCluster := normalCluster.DeepCopy()
52+
pausedCluster.Spec.Paused = true
53+
54+
// Object case 1: unpaused
55+
obj := &builder.Phase1Obj{ObjectMeta: metav1.ObjectMeta{
56+
Name: "some-object",
57+
Namespace: "default",
58+
Generation: 1,
59+
}}
60+
61+
// Object case 2: paused
62+
pausedObj := obj.DeepCopy()
63+
pausedObj.SetAnnotations(map[string]string{clusterv1.PausedAnnotation: ""})
64+
65+
tests := []struct {
66+
name string
67+
cluster *clusterv1.Cluster
68+
object ConditionSetter
69+
wantIsPaused bool
70+
wantRequeueAfterGenerationChange bool
71+
}{
72+
{
73+
name: "unpaused cluster and unpaused object",
74+
cluster: normalCluster.DeepCopy(),
75+
object: obj.DeepCopy(),
76+
wantIsPaused: false,
77+
wantRequeueAfterGenerationChange: false, // We don't want a requeue in this case to avoid additional reconciles.
78+
},
79+
{
80+
name: "paused cluster and unpaused object",
81+
cluster: pausedCluster.DeepCopy(),
82+
object: obj.DeepCopy(),
83+
wantIsPaused: true,
84+
wantRequeueAfterGenerationChange: true,
85+
},
86+
{
87+
name: "unpaused cluster and paused object",
88+
cluster: normalCluster.DeepCopy(),
89+
object: pausedObj.DeepCopy(),
90+
wantIsPaused: true,
91+
wantRequeueAfterGenerationChange: true,
92+
},
93+
{
94+
name: "paused cluster and paused object",
95+
cluster: pausedCluster.DeepCopy(),
96+
object: pausedObj.DeepCopy(),
97+
wantIsPaused: true,
98+
wantRequeueAfterGenerationChange: true,
99+
},
100+
}
101+
for _, tt := range tests {
102+
t.Run(tt.name, func(t *testing.T) {
103+
g := NewWithT(t)
104+
ctx := context.Background()
105+
106+
c := fake.NewClientBuilder().WithScheme(scheme).WithStatusSubresource(&clusterv1.Cluster{}, &builder.Phase1Obj{}).
107+
WithObjects(tt.object, tt.cluster).Build()
108+
109+
g.Expect(c.Get(ctx, client.ObjectKeyFromObject(tt.object), tt.object)).To(Succeed())
110+
111+
// The first run should set the condition.
112+
gotIsPaused, gotRequeue, err := EnsurePausedCondition(ctx, c, tt.cluster, tt.object)
113+
g.Expect(err).ToNot(HaveOccurred())
114+
g.Expect(gotRequeue).To(BeTrue(), "The first reconcile should return requeue=true because it set the Paused condition")
115+
g.Expect(gotIsPaused).To(Equal(tt.wantIsPaused))
116+
assertCondition(g, tt.object, tt.wantIsPaused)
117+
118+
// The second reconcile should be a no-op.
119+
gotIsPaused, gotRequeue, err = EnsurePausedCondition(ctx, c, tt.cluster, tt.object)
120+
g.Expect(err).ToNot(HaveOccurred())
121+
g.Expect(gotRequeue).To(BeFalse(), "The second reconcile should return requeue=false as the Paused condition was not changed")
122+
g.Expect(gotIsPaused).To(Equal(tt.wantIsPaused))
123+
assertCondition(g, tt.object, tt.wantIsPaused)
124+
125+
// The third reconcile reconciles a generation change, condition should be updated and requeue=true
126+
// should only be returned if the object is paused.
127+
tt.object.SetGeneration(tt.object.GetGeneration() + 1)
128+
gotIsPaused, gotRequeue, err = EnsurePausedCondition(ctx, c, tt.cluster, tt.object)
129+
g.Expect(err).ToNot(HaveOccurred())
130+
g.Expect(gotRequeue).To(Equal(tt.wantRequeueAfterGenerationChange))
131+
g.Expect(gotIsPaused).To(Equal(tt.wantIsPaused))
132+
assertCondition(g, tt.object, tt.wantIsPaused)
133+
})
134+
}
135+
}
136+
137+
func assertCondition(g Gomega, object ConditionSetter, wantIsPaused bool) {
138+
condition := v1beta2conditions.Get(object, clusterv1.PausedV1Beta2Condition)
139+
g.Expect(condition.ObservedGeneration).To(Equal(object.GetGeneration()))
140+
if wantIsPaused {
141+
g.Expect(condition.Status).To(Equal(metav1.ConditionTrue))
142+
g.Expect(condition.Reason).To(Equal(clusterv1.PausedV1Beta2Reason))
143+
g.Expect(condition.Message).ToNot(BeEmpty())
144+
} else {
145+
g.Expect(condition.Status).To(Equal(metav1.ConditionFalse))
146+
g.Expect(condition.Reason).To(Equal(clusterv1.NotPausedV1Beta2Reason))
147+
g.Expect(condition.Message).To(BeEmpty())
148+
}
149+
}

0 commit comments

Comments
 (0)