Skip to content

Commit c3245fa

Browse files
feat: support for krateo.io/gracefully-paused annotation
1 parent 1262ca0 commit c3245fa

11 files changed

Lines changed: 431 additions & 21 deletions

File tree

go.mod

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ require (
2020
k8s.io/apimachinery v0.33.3
2121
k8s.io/cli-runtime v0.33.3
2222
k8s.io/client-go v0.33.3
23-
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738
2423
sigs.k8s.io/e2e-framework v0.6.0
2524
sigs.k8s.io/kustomize/kyaml v0.19.0
2625
sigs.k8s.io/yaml v1.4.0
@@ -137,10 +136,13 @@ require (
137136
k8s.io/klog/v2 v2.130.1 // indirect
138137
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect
139138
k8s.io/kubectl v0.33.2 // indirect
139+
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
140140
oras.land/oras-go/v2 v2.6.0 // indirect
141141
sigs.k8s.io/controller-runtime v0.20.0 // indirect
142142
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
143143
sigs.k8s.io/kustomize/api v0.19.0 // indirect
144144
sigs.k8s.io/randfill v1.0.0 // indirect
145145
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect
146146
)
147+
148+
replace github.com/krateoplatformops/unstructured-runtime => /Users/matteogastaldello/Documents/unstructured-runtime

go.sum

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,8 +179,6 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
179179
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
180180
github.com/krateoplatformops/plumbing v0.6.1 h1:UxQjxvJwj6ORso/RJePQv7PRC75tCcupCpYZz98BaRI=
181181
github.com/krateoplatformops/plumbing v0.6.1/go.mod h1:mQ/sm0viyKgfR2ARzHuwCpY0rcyMKqCv8a8SOu52yYQ=
182-
github.com/krateoplatformops/unstructured-runtime v0.2.1 h1:KQ//tYaSknQByKi1GuJf83ZNi+2jDell3E/6dK5o2H0=
183-
github.com/krateoplatformops/unstructured-runtime v0.2.1/go.mod h1:hY1WdGSugozFs5p/MTlQot3yM71Il6/B+UkZv43FL9E=
184182
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
185183
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
186184
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw=

internal/composition/composition.go

Lines changed: 81 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"net/http"
77
"os"
88
"path/filepath"
9+
"time"
910

1011
compositionMeta "github.com/krateoplatformops/composition-dynamic-controller/internal/meta"
1112

@@ -40,6 +41,8 @@ var (
4041
)
4142

4243
const (
44+
reasonReconciliationGracefullyPaused event.Reason = "ReconciliationGracefullyPaused"
45+
4346
// Event reasons
4447
reasonCreated = "CompositionCreated"
4548
reasonDeleted = "CompositionDeleted"
@@ -98,6 +101,24 @@ func (h *handler) Observe(ctx context.Context, mg *unstructured.Unstructured) (c
98101
WithValues("namespace", mg.GetNamespace())
99102

100103
compositionMeta.SetReleaseName(mg, mg.GetName())
104+
if _, p := compositionMeta.GetGracefullyPausedTime(mg); p && compositionMeta.IsGracefullyPaused(mg) {
105+
log.Debug("Composition is gracefully paused, skipping observe.")
106+
h.eventRecorder.Event(mg, event.Normal(reasonReconciliationGracefullyPaused, "Observe", "Reconciliation is paused via the gracefully paused annotation."))
107+
return controller.ExternalObservation{
108+
ResourceExists: true,
109+
ResourceUpToDate: true,
110+
}, nil
111+
}
112+
// Immediately remove the gracefully paused time annotation if the composition is not gracefully paused.
113+
meta.RemoveAnnotations(mg, compositionMeta.AnnotationKeyReconciliationGracefullyPausedTime)
114+
mg, err := tools.Update(ctx, mg, tools.UpdateOptions{
115+
Pluralizer: h.pluralizer,
116+
DynamicClient: h.dynamicClient,
117+
})
118+
if err != nil {
119+
log.Debug("Updating composition", "error", err)
120+
return controller.ExternalObservation{}, fmt.Errorf("updating composition: %w", err)
121+
}
101122

102123
if h.packageInfoGetter == nil {
103124
return controller.ExternalObservation{}, fmt.Errorf("helm chart package info getter must be specified")
@@ -220,21 +241,18 @@ func (h *handler) Observe(ctx context.Context, mg *unstructured.Unstructured) (c
220241
}, nil
221242
}
222243

223-
err = setAvaibleStatus(mg, pkg, "Composition up-to-date")
244+
err = setAvaibleStatus(mg, pkg, "Composition up-to-date", false)
224245
if err != nil {
225246
log.Debug("Setting available status", "error", err)
226247
return controller.ExternalObservation{}, err
227248
}
228-
mg, err = tools.UpdateStatus(ctx, mg, tools.UpdateOptions{
229-
Pluralizer: h.pluralizer,
230-
DynamicClient: h.dynamicClient,
231-
})
232249
if err != nil {
233250
log.Debug("Updating cr status with condition", "error", err, "condition", condition.Available())
234251
return controller.ExternalObservation{}, err
235252
}
236253

237254
log.Debug("Composition Observed - installed", "package", pkg.URL)
255+
238256
return controller.ExternalObservation{
239257
ResourceExists: true,
240258
ResourceUpToDate: true,
@@ -248,6 +266,12 @@ func (h *handler) Create(ctx context.Context, mg *unstructured.Unstructured) err
248266
WithValues("name", mg.GetName()).
249267
WithValues("namespace", mg.GetNamespace())
250268

269+
if _, p := compositionMeta.GetGracefullyPausedTime(mg); p && compositionMeta.IsGracefullyPaused(mg) {
270+
log.Debug("Composition is gracefully paused, skipping create.")
271+
h.eventRecorder.Event(mg, event.Normal(reasonReconciliationGracefullyPaused, "Update", "Reconciliation is paused via the gracefully paused annotation."))
272+
return nil
273+
}
274+
251275
compositionMeta.SetReleaseName(mg, mg.GetName())
252276
mg, err := tools.Update(ctx, mg, tools.UpdateOptions{
253277
Pluralizer: h.pluralizer,
@@ -343,12 +367,14 @@ func (h *handler) Create(ctx context.Context, mg *unstructured.Unstructured) err
343367
h.logger.Debug("Composition created.", "package", pkg.URL)
344368

345369
h.eventRecorder.Event(mg, event.Normal(reasonCreated, "Create", fmt.Sprintf("Composition created: %s", mg.GetName())))
346-
err = setAvaibleStatus(mg, pkg, "Composition created")
370+
err = setAvaibleStatus(mg, pkg, "Composition created", true)
347371
if err != nil {
348372
log.Debug("Setting available status", "error", err)
349373
return err
350374
}
351-
_, err = tools.UpdateStatus(ctx, mg, tools.UpdateOptions{
375+
376+
meta.RemoveAnnotations(mg, compositionMeta.AnnotationKeyReconciliationGracefullyPausedTime)
377+
_, err = tools.Update(ctx, mg, tools.UpdateOptions{
352378
Pluralizer: h.pluralizer,
353379
DynamicClient: h.dynamicClient,
354380
})
@@ -362,6 +388,12 @@ func (h *handler) Update(ctx context.Context, mg *unstructured.Unstructured) err
362388
WithValues("name", mg.GetName()).
363389
WithValues("namespace", mg.GetNamespace())
364390

391+
if _, p := compositionMeta.GetGracefullyPausedTime(mg); p && compositionMeta.IsGracefullyPaused(mg) {
392+
log.Debug("Composition is gracefully paused, skipping update.")
393+
h.eventRecorder.Event(mg, event.Normal(reasonReconciliationGracefullyPaused, "Update", "Reconciliation is paused via the gracefully paused annotation."))
394+
return nil
395+
}
396+
365397
log.Debug("Handling composition values update.")
366398

367399
if h.packageInfoGetter == nil {
@@ -402,16 +434,36 @@ func (h *handler) Update(ctx context.Context, mg *unstructured.Unstructured) err
402434
log.Debug("Composition values updated.", "package", pkg.URL)
403435

404436
h.eventRecorder.Event(mg, event.Normal(reasonUpdated, "Update", fmt.Sprintf("Updated composition: %s", mg.GetName())))
405-
err = setAvaibleStatus(mg, pkg, "Composition values updated.")
406-
if err != nil {
407-
log.Debug("Setting available status", "error", err)
408-
return err
437+
438+
if compositionMeta.IsGracefullyPaused(mg) {
439+
err = setGracefullyPausedCondition(mg, pkg)
440+
if err != nil {
441+
log.Debug("Setting gracefully paused condition", "error", err)
442+
return err
443+
}
444+
compositionMeta.SetGracefullyPausedTime(mg, time.Now())
445+
log.Debug("Composition is gracefully paused.")
446+
h.eventRecorder.Event(mg, event.Normal(reasonReconciliationGracefullyPaused, "Update", "Reconciliation is paused via the gracefully paused annotation."))
447+
} else {
448+
// Normal behavior, set available status
449+
err = setAvaibleStatus(mg, pkg, "Composition values updated.", true)
450+
if err != nil {
451+
log.Debug("Setting available status", "error", err)
452+
return err
453+
}
454+
455+
meta.RemoveAnnotations(mg, compositionMeta.AnnotationKeyReconciliationGracefullyPausedTime)
409456
}
410-
_, err = tools.UpdateStatus(ctx, mg, tools.UpdateOptions{
457+
458+
_, err = tools.Update(ctx, mg, tools.UpdateOptions{
411459
Pluralizer: h.pluralizer,
412460
DynamicClient: h.dynamicClient,
413461
})
414-
return err
462+
if err != nil {
463+
log.Debug("Updating cr with values", "error", err)
464+
return err
465+
}
466+
return nil
415467
}
416468

417469
func (h *handler) Delete(ctx context.Context, mg *unstructured.Unstructured) error {
@@ -421,6 +473,12 @@ func (h *handler) Delete(ctx context.Context, mg *unstructured.Unstructured) err
421473
WithValues("name", mg.GetName()).
422474
WithValues("namespace", mg.GetNamespace())
423475

476+
if _, p := compositionMeta.GetGracefullyPausedTime(mg); p && compositionMeta.IsGracefullyPaused(mg) {
477+
log.Debug("Composition is gracefully paused, skipping delete.")
478+
h.eventRecorder.Event(mg, event.Normal(reasonReconciliationGracefullyPaused, "Delete", "Reconciliation is paused via the gracefully paused annotation."))
479+
return nil
480+
}
481+
424482
if h.packageInfoGetter == nil {
425483
return fmt.Errorf("helm chart package info getter must be specified")
426484
}
@@ -497,6 +555,16 @@ func (h *handler) Delete(ctx context.Context, mg *unstructured.Unstructured) err
497555

498556
h.eventRecorder.Event(mg, event.Normal(reasonDeleted, "Delete", fmt.Sprintf("Deleted composition: %s", mg.GetName())))
499557
log.Debug("Composition package removed.", "package", pkg.URL)
558+
meta.RemoveAnnotations(mg, compositionMeta.AnnotationKeyReconciliationGracefullyPausedTime)
559+
560+
_, err = tools.Update(ctx, mg, tools.UpdateOptions{
561+
Pluralizer: h.pluralizer,
562+
DynamicClient: h.dynamicClient,
563+
})
564+
if err != nil {
565+
log.Debug("Updating cr with values", "error", err)
566+
return fmt.Errorf("updating cr with values: %w", err)
567+
}
500568

501569
return nil
502570
}

internal/composition/support.go

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package composition
33
import (
44
"fmt"
55

6+
compositionCondition "github.com/krateoplatformops/composition-dynamic-controller/internal/condition"
7+
68
"github.com/krateoplatformops/composition-dynamic-controller/internal/tools/helmchart/archive"
79
"github.com/krateoplatformops/unstructured-runtime/pkg/controller/objectref"
810
"github.com/krateoplatformops/unstructured-runtime/pkg/pluralizer"
@@ -19,19 +21,22 @@ type ManagedResource struct {
1921
Namespace string `json:"namespace"`
2022
}
2123

22-
func setAvaibleStatus(mg *unstructured.Unstructured, pkg *archive.Info, message string) error {
24+
func setAvaibleStatus(mg *unstructured.Unstructured, pkg *archive.Info, message string, force bool) error {
2325
if pkg == nil {
2426
return fmt.Errorf("package info is nil")
2527
}
2628

2729
unstructured.SetNestedField(mg.Object, pkg.Version, "status", "helmChartVersion")
2830
unstructured.SetNestedField(mg.Object, pkg.URL, "status", "helmChartUrl")
2931

30-
currentCondition := unstructuredtools.GetCondition(mg, condition.Available().Type, condition.Available().Reason)
32+
if !force {
33+
currentCondition := unstructuredtools.GetCondition(mg, condition.Available().Type, condition.Available().Reason)
3134

32-
if currentCondition != nil && currentCondition.Message == message {
33-
return nil
35+
if currentCondition != nil && currentCondition.Message == message {
36+
return nil
37+
}
3438
}
39+
3540
cond := condition.Available()
3641
cond.Message = message
3742
err := unstructuredtools.SetConditions(mg, cond)
@@ -41,6 +46,25 @@ func setAvaibleStatus(mg *unstructured.Unstructured, pkg *archive.Info, message
4146
return nil
4247
}
4348

49+
func setGracefullyPausedCondition(mg *unstructured.Unstructured, pkg *archive.Info) error {
50+
if pkg == nil {
51+
return fmt.Errorf("package info is nil")
52+
}
53+
54+
currentCondition := unstructuredtools.GetCondition(mg, compositionCondition.ReconcileGracefullyPaused().Type, compositionCondition.ReconcileGracefullyPaused().Reason)
55+
56+
if currentCondition != nil && currentCondition.Message == "Composition is gracefully paused." {
57+
return nil
58+
}
59+
cond := compositionCondition.ReconcileGracefullyPaused()
60+
cond.Message = "Composition is gracefully paused."
61+
err := unstructuredtools.SetConditions(mg, cond)
62+
if err != nil {
63+
return fmt.Errorf("setting condition: %w", err)
64+
}
65+
return nil
66+
}
67+
4468
func setManagedResources(mg *unstructured.Unstructured, managed []interface{}) {
4569
status := mg.Object["status"]
4670
if status == nil {

internal/condition/condition.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package condition
2+
3+
import (
4+
"github.com/krateoplatformops/unstructured-runtime/pkg/tools/unstructured/condition"
5+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
6+
)
7+
8+
const (
9+
ReasonReconcileGracefullyPaused = "ReconcileGracefullyPaused"
10+
)
11+
12+
// ReconcilePaused returns a condition that indicates reconciliation on
13+
// the managed resource is paused via the pause annotation.
14+
func ReconcileGracefullyPaused() metav1.Condition {
15+
return metav1.Condition{
16+
Type: condition.TypeReady,
17+
Status: metav1.ConditionTrue,
18+
LastTransitionTime: metav1.Now(),
19+
Reason: ReasonReconcileGracefullyPaused,
20+
}
21+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package condition
2+
3+
import (
4+
"testing"
5+
6+
"github.com/krateoplatformops/unstructured-runtime/pkg/tools/unstructured/condition"
7+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
8+
)
9+
10+
func TestReconcileGracefullyPaused(t *testing.T) {
11+
result := ReconcileGracefullyPaused()
12+
13+
// Test Type
14+
if result.Type != condition.TypeSynced {
15+
t.Errorf("Expected Type to be %s, got %s", condition.TypeSynced, result.Type)
16+
}
17+
18+
// Test Status
19+
if result.Status != metav1.ConditionFalse {
20+
t.Errorf("Expected Status to be %s, got %s", metav1.ConditionFalse, result.Status)
21+
}
22+
23+
// Test Reason
24+
if result.Reason != ReasonReconcileGracefullyPaused {
25+
t.Errorf("Expected Reason to be %s, got %s", ReasonReconcileGracefullyPaused, result.Reason)
26+
}
27+
28+
// Test LastTransitionTime is set (not zero)
29+
if result.LastTransitionTime.IsZero() {
30+
t.Error("Expected LastTransitionTime to be set, got zero time")
31+
}
32+
33+
// Test Message is empty (default)
34+
if result.Message != "" {
35+
t.Errorf("Expected Message to be empty, got %s", result.Message)
36+
}
37+
}
38+
39+
func TestReasonReconcileGracefullyPausedConstant(t *testing.T) {
40+
expected := "ReconcileGracefullyPaused"
41+
if ReasonReconcileGracefullyPaused != expected {
42+
t.Errorf("Expected constant to be %s, got %s", expected, ReasonReconcileGracefullyPaused)
43+
}
44+
}

internal/meta/meta.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package meta
22

33
import (
4+
"time"
5+
6+
"github.com/krateoplatformops/unstructured-runtime/pkg/meta"
47
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
58
"k8s.io/apimachinery/pkg/runtime/schema"
69
)
@@ -15,6 +18,15 @@ const (
1518

1619
// ReleaseNameLabel is the label used to identify the release name of a Helm chart that can be different from the name of the resource.
1720
ReleaseNameLabel = "krateo.io/release-name"
21+
22+
// AnnotationKeyReconciliationGracefullyPaused is the key in the annotations map
23+
// that indicates whether the reconciliation of the resource is gracefully paused.
24+
AnnotationKeyReconciliationGracefullyPaused = "krateo.io/gracefully-paused"
25+
26+
// AnnotationKeyReconciliationGracefullyPausedTime is the key in the annotations map
27+
// that indicates the time when the reconciliation was gracefully paused.
28+
// This is used to track how long the resource has been paused.
29+
AnnotationKeyReconciliationGracefullyPausedTime = "krateo.io/gracefully-paused-time"
1830
)
1931

2032
func GetReleaseName(o metav1.Object) string {
@@ -58,3 +70,25 @@ func SetCompositionDefinitionLabels(o metav1.Object, cdInfo CompositionDefinitio
5870

5971
o.SetLabels(labels)
6072
}
73+
74+
// IsGracefullyPaused returns true if the object has the AnnotationKeyGracefullyPaused
75+
// annotation set to `true`.
76+
func IsGracefullyPaused(o metav1.Object) bool {
77+
return o.GetAnnotations()[AnnotationKeyReconciliationGracefullyPaused] == "true"
78+
}
79+
80+
func SetGracefullyPausedTime(o metav1.Object, t time.Time) {
81+
meta.AddAnnotations(o, map[string]string{AnnotationKeyReconciliationGracefullyPausedTime: t.Format(time.RFC3339)})
82+
}
83+
84+
func GetGracefullyPausedTime(o metav1.Object) (time.Time, bool) {
85+
t, ok := o.GetAnnotations()[AnnotationKeyReconciliationGracefullyPausedTime]
86+
if !ok {
87+
return time.Time{}, false
88+
}
89+
pausedTime, err := time.Parse(time.RFC3339, t)
90+
if err != nil {
91+
return time.Time{}, false
92+
}
93+
return pausedTime, true
94+
}

0 commit comments

Comments
 (0)