@@ -15,7 +15,6 @@ import (
1515 appsv1 "k8s.io/api/apps/v1"
1616 apierrors "k8s.io/apimachinery/pkg/api/errors"
1717 "k8s.io/apimachinery/pkg/runtime"
18- "k8s.io/apimachinery/pkg/runtime/schema"
1918 "k8s.io/apimachinery/pkg/types"
2019 ctrl "sigs.k8s.io/controller-runtime"
2120 "sigs.k8s.io/controller-runtime/pkg/client"
@@ -40,19 +39,6 @@ func buildRevisionSnapshot(spec v2alpha1.DatadogAgentSpec, allAnnotations map[st
4039 return json .Marshal (snap )
4140}
4241
43- // skipRevisionBump returns true when the revision bump should be suppressed.
44- // During experiment rollback the spec is restored to an older revision; bumping
45- // its revision number to "latest" would make it appear newer than the experiment
46- // revision, causing findRollbackTarget to return the experiment revision on the
47- // next rollback attempt instead of the pre-experiment revision.
48- func skipRevisionBump (newStatus * v2alpha1.DatadogAgentStatus ) bool {
49- if newStatus == nil || newStatus .Experiment == nil {
50- return false
51- }
52- phase := newStatus .Experiment .Phase
53- return phase == v2alpha1 .ExperimentPhaseTerminated
54- }
55-
5642// manageRevision creates a ControllerRevision snapshot of the current spec and
5743// garbage collects old revisions. Must be called after manageExperiment.
5844//
@@ -61,16 +47,54 @@ func skipRevisionBump(newStatus *v2alpha1.DatadogAgentStatus) bool {
6147// still used for labels, annotations, and object identity, which are
6248// unaffected by defaulting.
6349func (r * Reconciler ) manageRevision (ctx context.Context , instance * v2alpha1.DatadogAgent , rawSpec v2alpha1.DatadogAgentSpec , revList []appsv1.ControllerRevision , newStatus * v2alpha1.DatadogAgentStatus ) error {
64- revName , err := r .ensureRevision (ctx , instance , rawSpec , revList , skipRevisionBump ( newStatus ) )
50+ revName , err := r .ensureRevision (ctx , instance , rawSpec , revList )
6551 if err != nil {
6652 return err
6753 }
68- if err := r .gcOldRevisions (ctx , map [string ]bool {revName : true }, revList ); err != nil {
54+
55+ pins := map [string ]bool {revName : true }
56+ var checkpoint * v2alpha1.ExperimentCheckpoint
57+ var phase v2alpha1.ExperimentPhase
58+ if newStatus != nil && newStatus .Experiment != nil {
59+ checkpoint = newStatus .Experiment .Checkpoint
60+ phase = newStatus .Experiment .Phase
61+ }
62+ if checkpoint != nil {
63+ if ! isTerminalPhase (phase ) {
64+ pins [checkpoint .RollbackTargetRevision ] = true
65+ }
66+ } else if pending := instance .GetAnnotations ()[v2alpha1 .AnnotationExperimentRollbackTargetRevision ]; pending != "" {
67+ // A start or rollback signal is pending and status has not copied
68+ // the checkpoint yet; keep the nominated baseline alive so it isn't
69+ // GC'd out from under the in-flight signal.
70+ pins [pending ] = true
71+ }
72+
73+ if err := r .gcOldRevisions (ctx , pins , revList ); err != nil {
6974 ctrl .LoggerFrom (ctx ).Error (err , "Failed to garbage collect old ControllerRevisions, will retry on next reconcile" )
7075 }
7176 return nil
7277}
7378
79+ // ownedByDDA reports whether rev is a ControllerRevision owned by dda: same
80+ // namespace, the agent-name label matches, and a controller owner reference
81+ // points at dda's UID. A revision left behind by a deleted-and-recreated DDA
82+ // (same name, new UID) or a foreign/spoofed object fails this check.
83+ func ownedByDDA (rev * appsv1.ControllerRevision , dda * v2alpha1.DatadogAgent ) bool {
84+ if rev .Namespace != dda .GetNamespace () {
85+ return false
86+ }
87+ if rev .Labels [apicommon .DatadogAgentNameLabelKey ] != dda .GetName () {
88+ return false
89+ }
90+ for _ , ref := range rev .OwnerReferences {
91+ if ref .Controller != nil && * ref .Controller && ref .UID == dda .GetUID () {
92+ return true
93+ }
94+ }
95+ return false
96+ }
97+
7498// publishCurrentRevisionBarrier ensures a ControllerRevision exists for the
7599// current raw spec plus Datadog-owned annotations, then durably publishes the
76100// resulting pointer to status.currentRevision (plus its freshness fields) via
@@ -88,7 +112,7 @@ func (r *Reconciler) publishCurrentRevisionBarrier(
88112 rawSpec v2alpha1.DatadogAgentSpec ,
89113 revList []appsv1.ControllerRevision ,
90114) (revName string , annotationsHash string , err error ) {
91- revName , err = r .ensureRevision (ctx , instance , rawSpec , revList , false )
115+ revName , err = r .ensureRevision (ctx , instance , rawSpec , revList )
92116 if err != nil {
93117 return "" , "" , err
94118 }
@@ -144,11 +168,8 @@ func (r *Reconciler) listRevisions(ctx context.Context, instance *v2alpha1.Datad
144168 // mistaken for the current owner's history.
145169 owned := revList .Items [:0 ]
146170 for i := range revList .Items {
147- for _ , ref := range revList .Items [i ].OwnerReferences {
148- if ref .Controller != nil && * ref .Controller && ref .UID == instance .GetUID () {
149- owned = append (owned , revList .Items [i ])
150- break
151- }
171+ if ownedByDDA (& revList .Items [i ], instance ) {
172+ owned = append (owned , revList .Items [i ])
152173 }
153174 }
154175 revList .Items = owned
@@ -161,14 +182,12 @@ func (r *Reconciler) listRevisions(ctx context.Context, instance *v2alpha1.Datad
161182// rawSpec (not instance.Spec, which may carry in-memory defaults) is what
162183// gets stored, so that revisions reflect only user-intended changes.
163184//
164- // The Revision field is a monotonic creation counter. If skipBump is true the
165- // existing revision is returned as-is without bumping its Revision number.
185+ // The Revision field is a monotonic creation counter.
166186func (r * Reconciler ) ensureRevision (
167187 ctx context.Context ,
168188 instance * v2alpha1.DatadogAgent ,
169189 rawSpec v2alpha1.DatadogAgentSpec ,
170190 revList []appsv1.ControllerRevision ,
171- skipBump bool ,
172191) (string , error ) {
173192 logger := ctrl .LoggerFrom (ctx )
174193
@@ -219,17 +238,9 @@ func (r *Reconciler) ensureRevision(
219238 "object.name" , matchingRev .Name ,
220239 )
221240
222- if revisionExperimentState (matchingRev ) == experimentRevisionStateRolledBack && ! skipBump {
223- return r .recreateRevision (ctx , matchingRev , instance , gvks [0 ], labels , data , maxRevision )
224- }
225-
226241 // Identical content already snapshotted. Bump Revision to max+1 if it
227242 // has been superseded (e.g. after a revert) so ordering stays correct.
228- // Skip the bump during experiment rollback: bumping the pre-experiment
229- // revision above the experiment revision would cause findRollbackTarget
230- // to select the experiment revision as the rollback target on the next
231- // stopped signal, reversing the rollback.
232- if matchingRev .Revision < maxRevision && ! skipBump {
243+ if matchingRev .Revision < maxRevision {
233244 objLogger .Info ("Bumping ControllerRevision to latest" )
234245 patch := fmt .Appendf (nil , `{"revision":%d}` , maxRevision + 1 )
235246 if err := r .client .Patch (ctx , matchingRev , client .RawPatch (types .MergePatchType , patch )); err != nil && ! apierrors .IsConflict (err ) {
@@ -272,46 +283,6 @@ func (r *Reconciler) ensureRevision(
272283 return rev .Name , nil
273284}
274285
275- // recreateRevision deletes a rolled-back ControllerRevision and creates a
276- // fresh one with the same content but a new CreationTimestamp. This prevents
277- // an immediate timeout when the same experiment spec is re-applied, since
278- // CreationTimestamp is immutable in Kubernetes.
279- //
280- // Failure recovery:
281- // - Delete fails: error returned, next reconcile retries.
282- // - Delete succeeds, Create fails (or operator crashes): the revision is
283- // gone, so the next reconcile's ensureRevision takes the normal "no
284- // matching revision" path and creates a fresh one.
285- func (r * Reconciler ) recreateRevision (
286- ctx context.Context ,
287- old * appsv1.ControllerRevision ,
288- instance * v2alpha1.DatadogAgent ,
289- gvk schema.GroupVersionKind ,
290- labels map [string ]string ,
291- data runtime.RawExtension ,
292- maxRevision int64 ,
293- ) (string , error ) {
294- logger := ctrl .LoggerFrom (ctx ).WithValues (
295- "object.kind" , "ControllerRevision" ,
296- "object.namespace" , old .Namespace ,
297- "object.name" , old .Name ,
298- )
299- logger .Info ("Recreating rolled-back ControllerRevision with fresh timestamp" )
300-
301- if err := r .client .Delete (ctx , old ); err != nil && ! apierrors .IsNotFound (err ) {
302- return "" , fmt .Errorf ("failed to delete rolled-back ControllerRevision %s: %w" , old .Name , err )
303- }
304-
305- fresh := controllerrevisions .NewControllerRevision (instance , gvk , labels , data , maxRevision + 1 , nil )
306- if err := r .client .Create (ctx , fresh ); err != nil {
307- if apierrors .IsAlreadyExists (err ) {
308- return fresh .Name , nil
309- }
310- return "" , fmt .Errorf ("failed to recreate ControllerRevision %s: %w" , fresh .Name , err )
311- }
312- return fresh .Name , nil
313- }
314-
315286// datadogAnnotations returns a copy of annotations filtered to only those
316287// with `.datadoghq.com/` in the key, which are used for preview features.
317288// Experiment signal annotations (experiment.datadoghq.com/) are excluded
@@ -332,16 +303,13 @@ func datadogAnnotations(all map[string]string) map[string]string {
332303}
333304
334305// gcOldRevisions deletes all but the pinned revisions and the single most
335- // recent unpinned one (kept as "previous"). Stale experiment revisions
336- // (marked with the rollback annotation) are kept here — they are handled by
337- // ensureRevision which recreates them with a fresh timestamp when the same
338- // spec is re-applied.
306+ // recent unpinned one (kept as "previous").
339307//
340- // pins is a set of revision names that must never be deleted. Today it only
341- // carries status.currentRevision; later commits add the active experiment
342- // checkpoint's rollback target and a pending start signal's rollback-target
343- // annotation, once those states exist, without needing to reshape this
344- // signature again .
308+ // pins is a set of revision names that must never be deleted: the caller
309+ // includes status.currentRevision, the active experiment checkpoint's
310+ // rollback target (while non-terminal), and a pending start/ rollback
311+ // signal's rollback-target annotation before status has copied the
312+ // checkpoint .
345313func (r * Reconciler ) gcOldRevisions (
346314 ctx context.Context ,
347315 pins map [string ]bool ,
0 commit comments