Skip to content

feat(PL-6661): implement release pruning respecting prune annotation - #35

Merged
davidmdm merged 1 commit into
masterfrom
feat/PL-6661/add-release-pruning
Aug 10, 2026
Merged

feat(PL-6661): implement release pruning respecting prune annotation#35
davidmdm merged 1 commit into
masterfrom
feat/PL-6661/add-release-pruning

Conversation

@davidmdm

@davidmdm davidmdm commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Context: https://nestoca.atlassian.net/browse/PLT-6661

This PR implements pruning on releases because its a desirable feature, but also so that we have a mechanism via which we can remove previews from clusters.

Summary by CodeRabbit

  • New Features

    • Releases marked for pruning now automatically clean up their associated deployment applications when deleted.
    • Pull-based environments now support automated resource pruning.
  • Bug Fixes

    • Improved handling of concurrent updates and resources that are being deleted.
    • Prevented stale deployment applications from remaining after release removal.
  • Tests

    • Added coverage for release pruning and improved deployment readiness validation.

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9144c651-f960-4fd3-aa7b-4ec71b4f0606

📥 Commits

Reviewing files that changed from the base of the PR and between 15e0b7c and aa0ae49.

📒 Files selected for processing (2)
  • cmd/operator/main_test.go
  • cmd/operator/reconciler_release.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • cmd/operator/main_test.go
  • cmd/operator/reconciler_release.go

📝 Walkthrough

Walkthrough

The operator now enables Argo CD pruning, safely deletes Applications during annotated Release removal, and manages a cleanup finalizer. Integration tests use server-side apply and verify pruning behavior. Kubernetes and related Go dependencies were updated.

Changes

Pruning lifecycle

Layer / File(s) Summary
Release cleanup and application naming
cmd/operator/reconciler_release.go, go.mod
The Release reconciler handles terminating Releases, manages the prune finalizer, deletes associated Applications, centralizes Application naming, and updates Go dependencies.
Environment Application pruning
cmd/operator/reconciler_environment.go
Pull-mode Applications enable pruning. Non-pull cleanup retries conflicts, disables pruning, and deletes with a resource-version precondition.
Integration validation and apply setup
cmd/operator/main_test.go
Integration setup uses server-side apply. Readiness waits are extended, pruning is asserted, and TestReleasePruning verifies Application cleanup.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant KubernetesRelease
  participant ReleaseReconciler
  participant ArgoCDApplication
  KubernetesRelease->>ReleaseReconciler: report annotated Release deletion
  ReleaseReconciler->>ArgoCDApplication: delete associated Application
  ArgoCDApplication-->>ReleaseReconciler: confirm deletion or not found
  ReleaseReconciler->>KubernetesRelease: remove prune-release finalizer
Loading

Suggested reviewers: nwaller-nesto

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes implementing release pruning based on the prune annotation, which is the main change.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/PL-6661/add-release-pruning

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@davidmdm
davidmdm requested a review from nwaller-nesto August 10, 2026 14:10
@davidmdm
davidmdm force-pushed the feat/PL-6661/add-release-pruning branch 3 times, most recently from adb8d63 to 2ddd1b0 Compare August 10, 2026 16:15
@davidmdm
davidmdm force-pushed the feat/PL-6661/add-release-pruning branch from 2ddd1b0 to 15e0b7c Compare August 10, 2026 17:05
@davidmdm
davidmdm marked this pull request as ready for review August 10, 2026 17:17

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (4)
cmd/operator/main_test.go (3)

837-837: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Rename the shadowing variable.

Line 837 assigns an argocd.Application to a variable named release, which shadows the v1alpha1.Release from line 791. Line 845 then reports "expected application" while checking release.DeletionTimestamp. Name it app.

♻️ Proposed change
-			release, err := appsIntf.Get(t.Context(), "staging-test", metav1.GetOptions{})
+			app, err := appsIntf.Get(t.Context(), "staging-test", metav1.GetOptions{})
 			if err != nil {
 				if kerrors.IsNotFound(err) {
 					return nil
 				}
 				return fmt.Errorf("expected app to be not found but got: %v", err)
 			}
-			if release.DeletionTimestamp.IsZero() {
+			if app.DeletionTimestamp.IsZero() {

Also applies to: 844-844

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/operator/main_test.go` at line 837, Rename the variable assigned by
appsIntf.Get in the affected test from release to app, and update its later
references, including the DeletionTimestamp check and related error message,
while preserving the v1alpha1.Release variable named release.

253-253: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use a test-specific field manager instead of joyOperator.

These applies use the same field manager as the operator. Server-side apply then treats test writes and operator writes as one owner. Two effects follow:

  1. Ownership conflicts between the test fixture and the operator are never reported, even with Force absent.
  2. The operator can remove fields that the test set, and the test cannot detect it.

TestMain already uses a distinct manager at line 171 ("operator-tests"). Use the same value here.

♻️ Proposed change (apply to each call site)
-		metav1.ApplyOptions{FieldManager: joyOperator},
+		metav1.ApplyOptions{FieldManager: "operator-tests"},

Also applies to: 289-289, 311-311, 355-355, 632-632

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/operator/main_test.go` at line 253, Replace joyOperator with the
test-specific field manager "operator-tests" in every metav1.ApplyOptions call
identified in the diff, matching the manager already used by TestMain. Keep the
existing apply behavior and options unchanged.

688-712: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

TestReleasePruning reuses cluster-wide names and leaves state behind.

The test applies project/test, catalog/catalog and environment/staging. TestHappyReconciliations uses the same names, and TestEnvironmentSourcePattern also applies catalog/catalog and runs helm upgrade with a changed EnvironmentSourcePattern. All three tests share one kind cluster. The tests therefore depend on execution order and on the leftover state of earlier tests.

The test also creates no cleanup. The Project, Catalog and Environment stay in the cluster after the test ends.

Add t.Cleanup calls that delete the applied resources, or use unique names per test.

Also applies to: 720-748, 756-770

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/operator/main_test.go` around lines 688 - 712, Update TestReleasePruning
and the related resource-application blocks to avoid shared cluster-wide names
by generating unique Project, Catalog, and Environment names per test, or
register t.Cleanup callbacks that delete every applied resource. Apply the same
isolation to the blocks around the referenced symbols so
TestHappyReconciliations and TestEnvironmentSourcePattern cannot depend on
leftover state or execution order.
cmd/operator/reconciler_environment.go (1)

124-124: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Use retry.DefaultRetry and return conflict errors unwrapped.

Two points about the retry helper:

  1. DefaultBackoff uses 4 steps starting at 10 ms with factor 5.0. DefaultRetry is documented as the recommended retry for a conflict where multiple clients are making changes to the same resource, which matches this case, because the Argo CD application controller updates the same Application.
  2. The client-go example states that the callback must return err itself, not wrapped inside another error, so that RetryOnConflict can identify it correctly. Lines 127 and 137 wrap the error with fmt.Errorf. Recent apimachinery unwraps through errors.As, so this most likely still works, but the wrapping also affects the kerrors.IsNotFound(err) check at line 142. Confirm the behavior of the vendored apimachinery version, or return the raw errors and wrap once at line 143.

Also applies to: 127-127

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/operator/reconciler_environment.go` at line 124, Update the
RetryOnConflict call in the reconciler to use retry.DefaultRetry instead of
retry.DefaultBackoff. Within its callback, return conflict errors directly
without fmt.Errorf wrapping, including the error paths around the referenced
lines, so conflict detection and kerrors.IsNotFound remain reliable; if context
is needed, wrap only once at the outer error return.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmd/operator/main_test.go`:
- Around line 832-853: Extend the test around the release deletion to first
retrieve the Release and assert its metadata includes the
joy.nesto.ca/prune-release finalizer before calling releaseIntf.Delete. After
deletion, add an Eventually check using releaseIntf.Get that succeeds only when
the Release returns a NotFound error, while preserving the existing Argo CD
Application deletion assertion.

In `@cmd/operator/reconciler_release.go`:
- Around line 49-59: Update cmd/operator/reconciler_release.go lines 49-59 and
the finalizer writes in that reconciler to copy the cached release before
mutation, then apply a minimal object containing only apiVersion, kind,
metadata.name, metadata.namespace, and metadata.finalizers. Update
cmd/operator/reconciler_environment.go line 135 to avoid applying the full
object returned by appIntf.Get; apply only the identity fields and
spec.syncPolicy, or use a JSON patch.
- Around line 80-117: Handle deleted releases before environment/project lookups
so missing environments cannot prevent finalizer cleanup. In the deletion path,
delete the Application only when shouldPrune is true, then remove
finalizerPruneRelease whenever hasPruneReleaseFinalizer regardless of the
annotation, and return without re-applying the Application. Use
release.Namespace when constructing the Application name via appName, then keep
environment/project lookup and normal Application apply exclusively for
non-deleted releases.

In `@go.mod`:
- Around line 12-17: Update the indirect google.golang.org/protobuf dependency
in go.mod from the pseudo-version to the v1.36.12 release by running go get
google.golang.org/protobuf@latest, then commit the resulting go.mod and reduced
go.sum changes.

---

Nitpick comments:
In `@cmd/operator/main_test.go`:
- Line 837: Rename the variable assigned by appsIntf.Get in the affected test
from release to app, and update its later references, including the
DeletionTimestamp check and related error message, while preserving the
v1alpha1.Release variable named release.
- Line 253: Replace joyOperator with the test-specific field manager
"operator-tests" in every metav1.ApplyOptions call identified in the diff,
matching the manager already used by TestMain. Keep the existing apply behavior
and options unchanged.
- Around line 688-712: Update TestReleasePruning and the related
resource-application blocks to avoid shared cluster-wide names by generating
unique Project, Catalog, and Environment names per test, or register t.Cleanup
callbacks that delete every applied resource. Apply the same isolation to the
blocks around the referenced symbols so TestHappyReconciliations and
TestEnvironmentSourcePattern cannot depend on leftover state or execution order.

In `@cmd/operator/reconciler_environment.go`:
- Line 124: Update the RetryOnConflict call in the reconciler to use
retry.DefaultRetry instead of retry.DefaultBackoff. Within its callback, return
conflict errors directly without fmt.Errorf wrapping, including the error paths
around the referenced lines, so conflict detection and kerrors.IsNotFound remain
reliable; if context is needed, wrap only once at the outer error return.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 10898b77-72bf-4eb7-a24f-da4801c28284

📥 Commits

Reviewing files that changed from the base of the PR and between 352d2ea and 15e0b7c.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (4)
  • cmd/operator/main_test.go
  • cmd/operator/reconciler_environment.go
  • cmd/operator/reconciler_release.go
  • go.mod

Comment thread cmd/operator/main_test.go
Comment on lines +49 to +59
releaseIntf := k8s.TypedInterface[v1alpha1.Release](
ctrl.Client(ctx),
schema.GroupVersionResource{Group: v1alpha1.Group, Version: v1alpha1.Version, Resource: "releases"},
).Namespace(event.Namespace)

release, err := releaseCache.Get(event.Name)
if kerrors.IsNotFound(err) {
// If the release is no longer in the cache, a deletion event happened. However, that doesn't mean that the release doesn't yet
// still exist in etcd with a deletion timestamp and finalizer. In order to handle cleanup, we need to check its live state.
release, err = releaseIntf.Get(ctx, event.Name, metav1.GetOptions{})
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Server-side apply of a full live-read object claims ownership of every field. Both reconcilers read an object with Get, mutate one field, then send the whole object through Apply with FieldManager: joyOperator and Force: true. Server-side apply treats every field present in the request body as owned by that manager, and Force: true takes fields away from their current owners. The operator then owns fields it never intended to manage, and later removals by the real owner no longer take effect.

  • cmd/operator/reconciler_release.go#L49-L59: for the finalizer writes at lines 90, 96 and 111, build a minimal object that carries only apiVersion, kind, metadata.name, metadata.namespace and metadata.finalizers. Also copy the object before mutating release.Finalizers, because releaseCache.Get can return a pointer into the shared informer cache.
  • cmd/operator/reconciler_environment.go#L135-L135: replace the full-object apply of the value returned by appIntf.Get with a minimal apply that carries only the identity fields and spec.syncPolicy, or use a JSON patch.
📍 Affects 2 files
  • cmd/operator/reconciler_release.go#L49-L59 (this comment)
  • cmd/operator/reconciler_environment.go#L135-L135
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/operator/reconciler_release.go` around lines 49 - 59, Update
cmd/operator/reconciler_release.go lines 49-59 and the finalizer writes in that
reconciler to copy the cached release before mutation, then apply a minimal
object containing only apiVersion, kind, metadata.name, metadata.namespace, and
metadata.finalizers. Update cmd/operator/reconciler_environment.go line 135 to
avoid applying the full object returned by appIntf.Get; apply only the identity
fields and spec.syncPolicy, or use a JSON patch.

Comment thread cmd/operator/reconciler_release.go
Comment thread go.mod
Comment on lines +12 to +17
github.com/yokecd/yoke v0.20.25
go.yaml.in/yaml/v3 v3.0.5
k8s.io/api v0.36.3
k8s.io/apiextensions-apiserver v0.36.3
k8s.io/apimachinery v0.36.3
k8s.io/client-go v0.36.3

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
for mod in github.com/yokecd/yoke k8s.io/api k8s.io/client-go google.golang.org/protobuf; do
  echo "== $mod"
  curl -s "https://proxy.golang.org/${mod}/@latest" | head -c 400; echo
done
grep -n 'protobuf' go.mod go.sum | head -20
gh api graphql -f query='
{
  securityVulnerabilities(first: 5, ecosystem: GO, package: "google.golang.org/protobuf") {
    nodes { advisory { summary severity publishedAt } vulnerableVersionRange firstPatchedVersion { identifier } }
  }
}'

Repository: nestoca/joy-operator

Length of output: 1942


Replace the indirect protobuf pseudo-version with a release.

google.golang.org/protobuf is pinned via an indirect requirement to v1.36.12-0.20260120151049-f2248ac996af; a go.sum entry is present, so it was not hand-edited. Run go get google.golang.org/protobuf@latest to prefer v1.36.12 and commit the reduced go.sum diff. The bumped versions exist and are outside the known advisory ranges.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@go.mod` around lines 12 - 17, Update the indirect google.golang.org/protobuf
dependency in go.mod from the pseudo-version to the v1.36.12 release by running
go get google.golang.org/protobuf@latest, then commit the resulting go.mod and
reduced go.sum changes.

@davidmdm
davidmdm force-pushed the feat/PL-6661/add-release-pruning branch from 15e0b7c to c68b8ef Compare August 10, 2026 17:30
@davidmdm
davidmdm force-pushed the feat/PL-6661/add-release-pruning branch from c68b8ef to aa0ae49 Compare August 10, 2026 18:55
@davidmdm
davidmdm merged commit 61ec71a into master Aug 10, 2026
7 checks passed
@davidmdm
davidmdm deleted the feat/PL-6661/add-release-pruning branch August 10, 2026 20:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants