Skip to content

feat: implemented artifact cleanup#554

Open
rebEllieous wants to merge 1 commit into
mainfrom
feature/358-artifact-cleanup
Open

feat: implemented artifact cleanup#554
rebEllieous wants to merge 1 commit into
mainfrom
feature/358-artifact-cleanup

Conversation

@rebEllieous
Copy link
Copy Markdown
Contributor

@rebEllieous rebEllieous commented May 27, 2026

What

Clean up rendered OCI artifacts and RenderTasks when SolAr API resources are deleted.
Closes #358

Why

Deleting a ReleaseBinding or Target previously left orphaned RenderTask objects and OCI images in the render registry. This implements the full deletion lifecycle.

Testing

  • Unit tests: RenderArtifactReconciler covered with fake client + injected DeleteTag stub (pkg/controller)
  • Unit tests: TargetReconciler extended for ensureRenderArtifact, ensureRenderBinding, and deleteStaleRenderBindings
  • pkg/ociregistry.DeleteTag tested for success and error paths
  • E2E:
    • 1 artifact + 1 binding created on first render
    • Same artifact reused (2 bindings) when a second Target renders the same release
    • Artifact survives removal of one binding while another remains
    • Artifact GC'd and OCI tag deleted after last binding removed via Target deletion (finalizer path)
    • Artifact GC'd and OCI tag deleted after last ReleaseBinding deleted directly (stale-binding path)

Notes for reviewers

New API resources (CRD-equivalent, served via extension apiserver):

  • RenderArtifact records OCI coordinates of a pushed chart artifact; carries a finalizer; reconciled by RenderArtifactReconciler
  • RenderBinding ref-count record linking a Target to a RenderArtifact; drives GC when the last binding is removed

Helm chart: top-level caBundle block added to mount a CA bundle ConfigMap into the controller-manager pod (SSL_CERT_FILE), required for TLS to self-signed registries in cluster.

Checklist

  • Tests added/updated
  • No breaking changes (or upgrade path documented above)
  • Readable commit history (squashed and cleaned up as desired)
  • AI code review considered and comments resolved

Summary by CodeRabbit

  • New Features
    • Added RenderArtifact and RenderBinding resources for managing and referencing rendered OCI artifacts.
    • Implemented automatic garbage collection of render artifacts when no longer referenced by any bindings.
    • Added CA certificate bundle configuration support in the controller deployment.
    • Extended RBAC permissions for render artifact and binding resource management.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 27, 2026

📝 Walkthrough

Walkthrough

This PR introduces RenderArtifact and RenderBinding Kubernetes custom resources for tracking and managing the lifecycle of successfully rendered OCI artifacts. RenderArtifacts record artifact coordinates (base URL, repository, tag) and their originating RenderTask; RenderBindings link consumer resources to artifacts for reference counting. A new RenderArtifactReconciler implements finalizer-based OCI tag cleanup and garbage collection when binding references are removed, triggered via a field-indexed watch on RenderBindings. The TargetController is extended to create and manage these resources alongside RenderTasks.

Changes

RenderArtifact and RenderBinding resource system

Layer / File(s) Summary
API type definitions and registration
api/solar/renderartifact_types.go, api/solar/renderbinding_types.go, api/solar/renderartifact_rest.go, api/solar/renderbinding_rest.go, api/solar/register.go, api/solar/v1alpha1/renderartifact_types.go, api/solar/v1alpha1/renderbinding_types.go, api/solar/v1alpha1/register.go, api/solar/v1alpha1/zz_generated.conversion.go, api/solar/v1alpha1/zz_generated.deepcopy.go, api/solar/zz_generated.deepcopy.go, api/solar/v1alpha1/zz_generated.model_name.go
RenderArtifact embeds OCI coordinates (baseURL, repository, tag) and references the producing RenderTask; optional push secret for GC credentials and registry flavor. RenderArtifactStatus includes optional ChartURL and patch-merge-capable Conditions. RenderBinding links a consumer (ownerKind, ownerName, ownerNamespace) to a RenderArtifact for reference counting. REST/table conversion implements table columns (name, repository/artifact, tag/owner, chart URL/age) and generation management on create/update. Both types are registered in solar (unversioned) and v1alpha1 (public) API groups with bidirectional conversion.
Generated client-go and OpenAPI artifacts
client-go/applyconfigurations/solar/v1alpha1/renderartifact.go, client-go/applyconfigurations/solar/v1alpha1/renderartifactspec.go, client-go/applyconfigurations/solar/v1alpha1/renderartifactstatus.go, client-go/applyconfigurations/solar/v1alpha1/renderbinding.go, client-go/applyconfigurations/solar/v1alpha1/renderbindingspec.go, client-go/applyconfigurations/utils.go, client-go/clientset/..., client-go/informers/..., client-go/listers/..., client-go/openapi/zz_generated.openapi.go
Typed clients, informers, listers, apply configurations, fake clients, and OpenAPI schemas generated for RenderArtifact and RenderBinding; ForKind utility extended to dispatch apply-configuration types.
RenderArtifact reconciler with OCI cleanup
pkg/controller/renderartifact_controller.go
RenderArtifactReconciler implements deletion handling with finalizer-based OCI cleanup via DeleteTag, status.ChartURL synchronization from Spec coordinates, reference-counting garbage collection by querying RenderBindings via field index, and auth resolution from optional push secrets (BasicAuth or Docker config JSON). Deletion failure sets OCICleanup=False condition and emits events while preserving finalizer. SetupWithManager adds watch on RenderBinding resources that enqueues artifact reconciliation when bindings are removed.
OCI tag deletion via go-containerregistry
pkg/ociregistry/deleter.go, pkg/ociregistry/standard.go, pkg/ociregistry/ociregistry_test.go
Exported DeleteTag function delegates to standardDeleter, which parses the OCI reference, configures remote delete options with optional authentication (skipping auth when anonymous), and performs OCI Distribution Spec DELETE. Tests cover invalid references, successful deletion, and registry error handling.
RenderArtifact controller unit tests
pkg/controller/renderartifact_controller_test.go
Ginkgo suite with stubTagDeleter test double verifying finalizer attachment, status.ChartURL population, garbage collection without bindings, retention while bindings exist, and OCI deletion failure handling with OCICleanup=False condition surfacing.
Target controller integration with RenderArtifact/RenderBinding
pkg/controller/target_controller.go, pkg/controller/helpers.go
Extended Target controller to create RenderArtifact (computed from RenderTask chart URL) and RenderBinding (linking target to artifact) when per-release and bootstrap RenderTasks succeed; tracks artifact/binding names in releaseInfo for later cleanup; deletes stale owned RenderBindings by current-expected-names set and all owned bindings during target deletion (triggering OCI GC via RenderArtifactReconciler). RBAC markers extended for renderartifacts/renderbindings permissions. Helper functions compute deterministic artifact/binding names and construct OCI chart URLs with oci:// prefix. Field indexers added for RenderBinding owner and renderArtifactRef lookups.
Target controller RenderArtifact/RenderBinding lifecycle tests
pkg/controller/target_controller_test.go
Integration tests verify artifact creation on RenderTask success, registry flavor propagation, and artifact cleanup when target is deleted.
End-to-end tests for artifact reuse, binding lifecycle, and GC
test/e2e/e2e_test.go, test/e2e/e2e_suite_test.go
E2E suite exercises multi-target artifact reuse (single artifact referenced by multiple bindings), binding retention while at least one binding exists, garbage collection when last binding is removed, and direct stale-binding deletion path. Tests verify OCI tag removal via registry port-forward and edge-case handling (MissingDependencies). Helper functions query artifacts/bindings by repository or renderArtifactRef.
Helm chart RBAC and optional CA bundle support
charts/solar/files/role.yaml, charts/solar/templates/controller/deployment.yaml, charts/solar/values.yaml
ClusterRole updated to grant permissions for renderartifacts, renderbindings, and their status/finalizers sub-resources. Deployment template conditionally mounts CA bundle configMap at /etc/ssl/certs and sets SSL_CERT_FILE environment variable. Values define caBundle.enabled, configMapName, and default key (trust-bundle.pem).
API server registration and documentation
cmd/solar-apiserver/main.go, cmd/solar-controller-manager/main.go, docs/user-guide/api-reference.md
Apiserver builder registers RenderArtifact and RenderBinding resources. Controller manager wires RenderArtifactReconciler. API reference documents RenderArtifact/RenderBinding/List/Spec/Status types including field descriptions and ownership/ref-counting semantics.
Test fixture CA bundle configuration
test/fixtures/solar.values.yaml
Enables caBundle in test fixture (configMapName: root-bundle).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

feature

Suggested reviewers

  • dermorz
  • trevex

🐰 In warrens of code, we weave the threads,
Artifacts tracked, and bindings spread.
When references fade and none remain,
Cleanup sweeps, the registry's reign.
Thus SolAr glows, from push to dust—
Rendered with care, in code we trust. 🎨

✨ 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 feature/358-artifact-cleanup

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🤖 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 `@api/solar/renderartifact_rest.go`:
- Around line 49-56: Add explicit enforcement for required RenderArtifactSpec
fields: in PrepareForCreate ensure o.Spec.BaseURL, o.Spec.Repository, o.Spec.Tag
and o.Spec.RenderTaskRef are non-empty and emit/return a validation error if any
are missing; in PrepareForUpdate, if any of those fields on the incoming o are
empty, copy the values from the old.(*RenderArtifact).Spec to prevent emptying
required fields, and still call incrementGenerationIfNotEqual as before.
Reference RenderArtifact, PrepareForCreate, PrepareForUpdate, RenderArtifactSpec
and its fields baseURL/Repository/Tag/RenderTaskRef when making the changes.

In `@api/solar/renderbinding_rest.go`:
- Around line 42-49: Add explicit validation in RenderBinding.PrepareForCreate
and RenderBinding.PrepareForUpdate to ensure RenderBindingSpec required fields
are non-empty: check o.Spec.RenderArtifactRef.Name, o.Spec.OwnerKind,
o.Spec.OwnerName, and o.Spec.OwnerNamespace; on create, validate the new object
and on update compare against the old object (or := old.(*RenderBinding)) and
validate the incoming o.Spec before incrementing Generation. If any required
field is empty, surface an API validation error (use the repo's existing
validation/error helpers or return an apiserver validation error consistent with
other api/solar/*_rest.go lifecycles) so creation/update is rejected.

In `@charts/solar/templates/controller/deployment.yaml`:
- Around line 123-129: When caBundle.enabled is true the template blindly uses
caBundle.configMapName which may be empty; update the controller/deployment.yaml
fragment to guard or require caBundle.configMapName before rendering the
ConfigMap volume: either wrap the volume block with an additional condition that
checks (ne .Values.caBundle.configMapName "") or replace the direct usage with
Helm's required function (e.g. required "message"
.Values.caBundle.configMapName) so that caBundle.configMapName (and optionally
caBundle.key) are validated and you never emit name: "" for the ca-bundle
volume.

In `@pkg/controller/renderartifact_controller.go`:
- Around line 150-151: The code constructs rawRef from artifact.Spec.BaseURL
without normalizing, which can include "oci://" prefixes or trailing slashes and
cause mismatched auth host lookups; normalize BaseURL once (strip "oci://"
prefix and any trailing slashes) into a local variable (e.g., normalizedBase)
and use that when building rawRef and when performing Docker/OCI auth host
lookups, replacing usages of artifact.Spec.BaseURL with normalizedBase (ensure
rawRef becomes normalizedBase + "/" + artifact.Spec.Repository + ":" +
artifact.Spec.Tag and any auth map key lookups use normalizedBase or the
host-only form derived from it).
- Line 160: The OCI tag deletion (DeleteTag) is a network call invoked via
deleteFn and needs a local timeout to avoid hanging the reconcile worker; wrap
the call to deleteFn(ctx, rawRef, auth) with a context.WithTimeout (choose a
sensible timeout like 15–30s), use the derived ctx (and defer cancel()) when
calling deleteFn, and ensure the context and time packages are imported so the
DeleteTag/backing network call is cancelled if it exceeds the timeout.

In `@pkg/controller/target_controller.go`:
- Around line 491-498: The bootstrap ensure errors are being logged but
swallowed; change the reconcile flow so failures from
r.ensureRenderArtifact(ctx, bootstrapArtifactName, bootstrapRT,
registry.Spec.Flavor) and r.ensureRenderBinding(ctx, target,
bootstrapArtifactName, bootstrapBindingName) are returned to the caller instead
of only logged. Locate the block using
bootstrapArtifactName/bootstrapBindingName and the calls to ensureRenderArtifact
and ensureRenderBinding in target_controller.go and replace the current log-only
branches with error propagation (return err) so reconcile fails when these
ensures fail, preserving the original error context in the returned error.
- Around line 228-234: The no-bindings branch currently swallows errors from
cleanup by only logging them; change it to return the error so the reconcile is
requeued and retries cleanup. Specifically, in the section that calls
r.deleteStaleRenderTasks(ctx, target, map[string]struct{}{}) and
r.deleteStaleRenderBindings(ctx, target, map[string]struct{}{}), replace the
log-only handling with error returns (e.g., if err != nil { log.Error(err,
"..."); return ctrl.Result{}, err }) so failures in deleteStaleRenderTasks and
deleteStaleRenderBindings cause the controller to retry reconciliation.

In `@test/e2e/e2e_test.go`:
- Around line 1022-1028: The test currently treats any error from
repo.FetchReference as success, which can hide transient failures; update the
Eventually block that calls zotClient.Repository and repo.FetchReference to
assert that the returned error is specifically a not-found sentinel by using
errors.Is(err, errdef.ErrNotFound) (importing errors and errdef if not present)
instead of g.Expect(err).To(HaveOccurred()); ensure the failure message still
references artTag and releaseRepo() but only consider the check succeeded when
errors.Is indicates ErrNotFound.
🪄 Autofix (Beta)

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

Run ID: fd3f4d1b-0e95-4b59-8dfa-5a4598b5904b

📥 Commits

Reviewing files that changed from the base of the PR and between ce1ad46 and ba638e8.

📒 Files selected for processing (51)
  • api/solar/register.go
  • api/solar/renderartifact_rest.go
  • api/solar/renderartifact_types.go
  • api/solar/renderbinding_rest.go
  • api/solar/renderbinding_types.go
  • api/solar/v1alpha1/register.go
  • api/solar/v1alpha1/renderartifact_types.go
  • api/solar/v1alpha1/renderbinding_types.go
  • api/solar/v1alpha1/zz_generated.conversion.go
  • api/solar/v1alpha1/zz_generated.deepcopy.go
  • api/solar/v1alpha1/zz_generated.model_name.go
  • api/solar/zz_generated.deepcopy.go
  • charts/solar/files/role.yaml
  • charts/solar/templates/controller/deployment.yaml
  • charts/solar/values.yaml
  • client-go/applyconfigurations/solar/v1alpha1/renderartifact.go
  • client-go/applyconfigurations/solar/v1alpha1/renderartifactspec.go
  • client-go/applyconfigurations/solar/v1alpha1/renderartifactstatus.go
  • client-go/applyconfigurations/solar/v1alpha1/renderbinding.go
  • client-go/applyconfigurations/solar/v1alpha1/renderbindingspec.go
  • client-go/applyconfigurations/utils.go
  • client-go/clientset/versioned/typed/solar/v1alpha1/fake/fake_renderartifact.go
  • client-go/clientset/versioned/typed/solar/v1alpha1/fake/fake_renderbinding.go
  • client-go/clientset/versioned/typed/solar/v1alpha1/fake/fake_solar_client.go
  • client-go/clientset/versioned/typed/solar/v1alpha1/generated_expansion.go
  • client-go/clientset/versioned/typed/solar/v1alpha1/renderartifact.go
  • client-go/clientset/versioned/typed/solar/v1alpha1/renderbinding.go
  • client-go/clientset/versioned/typed/solar/v1alpha1/solar_client.go
  • client-go/informers/externalversions/generic.go
  • client-go/informers/externalversions/solar/v1alpha1/interface.go
  • client-go/informers/externalversions/solar/v1alpha1/renderartifact.go
  • client-go/informers/externalversions/solar/v1alpha1/renderbinding.go
  • client-go/listers/solar/v1alpha1/expansion_generated.go
  • client-go/listers/solar/v1alpha1/renderartifact.go
  • client-go/listers/solar/v1alpha1/renderbinding.go
  • client-go/openapi/zz_generated.openapi.go
  • cmd/solar-apiserver/main.go
  • cmd/solar-controller-manager/main.go
  • docs/user-guide/api-reference.md
  • pkg/controller/helpers.go
  • pkg/controller/renderartifact_controller.go
  • pkg/controller/renderartifact_controller_test.go
  • pkg/controller/suite_test.go
  • pkg/controller/target_controller.go
  • pkg/controller/target_controller_test.go
  • pkg/ociregistry/deleter.go
  • pkg/ociregistry/ociregistry_test.go
  • pkg/ociregistry/standard.go
  • test/e2e/e2e_suite_test.go
  • test/e2e/e2e_test.go
  • test/fixtures/solar.values.yaml

Comment on lines +49 to +56
func (o *RenderArtifact) PrepareForCreate(ctx context.Context) {
o.Generation = 1
}

func (o *RenderArtifact) PrepareForUpdate(ctx context.Context, old runtime.Object) {
or := old.(*RenderArtifact)
incrementGenerationIfNotEqual(o, o.Spec, or.Spec)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Enforce RenderArtifactSpec invariants in lifecycle validation.

Line 49 and Line 53 only manage generation; they do not enforce non-empty required spec values (baseURL, repository, tag, renderTaskRef). That allows invalid artifacts to be stored and fail later during reconciliation/GC.

Based on learnings: field-level API validation in this repo is enforced via custom logic in lifecycle methods like PrepareForCreate/PrepareForUpdate in api/solar/*_rest.go.

🤖 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 `@api/solar/renderartifact_rest.go` around lines 49 - 56, Add explicit
enforcement for required RenderArtifactSpec fields: in PrepareForCreate ensure
o.Spec.BaseURL, o.Spec.Repository, o.Spec.Tag and o.Spec.RenderTaskRef are
non-empty and emit/return a validation error if any are missing; in
PrepareForUpdate, if any of those fields on the incoming o are empty, copy the
values from the old.(*RenderArtifact).Spec to prevent emptying required fields,
and still call incrementGenerationIfNotEqual as before. Reference
RenderArtifact, PrepareForCreate, PrepareForUpdate, RenderArtifactSpec and its
fields baseURL/Repository/Tag/RenderTaskRef when making the changes.

Comment on lines +42 to +49
func (o *RenderBinding) PrepareForCreate(ctx context.Context) {
o.Generation = 1
}

func (o *RenderBinding) PrepareForUpdate(ctx context.Context, old runtime.Object) {
or := old.(*RenderBinding)
incrementGenerationIfNotEqual(o, o.Spec, or.Spec)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add create/update validation for RenderBindingSpec required fields.

Line 42 and Line 46 currently only set/increment generation. There is no enforcement for required non-empty identity fields (renderArtifactRef.name, ownerKind, ownerName, ownerNamespace), which can break binding-based reference counting.

Based on learnings: field-level API validation in this repo is enforced via custom logic in lifecycle methods like PrepareForCreate/PrepareForUpdate in api/solar/*_rest.go.

🤖 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 `@api/solar/renderbinding_rest.go` around lines 42 - 49, Add explicit
validation in RenderBinding.PrepareForCreate and RenderBinding.PrepareForUpdate
to ensure RenderBindingSpec required fields are non-empty: check
o.Spec.RenderArtifactRef.Name, o.Spec.OwnerKind, o.Spec.OwnerName, and
o.Spec.OwnerNamespace; on create, validate the new object and on update compare
against the old object (or := old.(*RenderBinding)) and validate the incoming
o.Spec before incrementing Generation. If any required field is empty, surface
an API validation error (use the repo's existing validation/error helpers or
return an apiserver validation error consistent with other api/solar/*_rest.go
lifecycles) so creation/update is rejected.

Comment on lines +123 to +129
{{- if .Values.caBundle.enabled }}
- name: ca-bundle
configMap:
name: {{ .Values.caBundle.configMapName }}
items:
- key: {{ .Values.caBundle.key }}
path: ca-bundle.pem
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify default is empty and template is currently unguarded.
rg -n 'caBundle:' charts/solar/values.yaml -A8 -B1
rg -n 'name:\s*\{\{\s*\.Values\.caBundle\.configMapName\s*\}\}' charts/solar/templates/controller/deployment.yaml -A3 -B2

Repository: opendefensecloud/solution-arsenal

Length of output: 662


Guard caBundle inputs to prevent invalid ConfigMap volume at runtime.

caBundle.enabled only gates rendering; when it’s true, the template uses caBundle.configMapName directly. In charts/solar/values.yaml, caBundle.configMapName defaults to "", so Helm can emit name: "" for the ca-bundle ConfigMap volume, causing a runtime deploy failure. Add required guards for at least caBundle.configMapName (guarding caBundle.key is optional since it already defaults to "trust-bundle.pem").

Suggested fix
         - name: ca-bundle
           configMap:
-            name: {{ .Values.caBundle.configMapName }}
+            name: {{ required "caBundle.configMapName must be set when caBundle.enabled=true" .Values.caBundle.configMapName }}
             items:
-              - key: {{ .Values.caBundle.key }}
+              - key: {{ required "caBundle.key must be set when caBundle.enabled=true" .Values.caBundle.key }}
                 path: ca-bundle.pem
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{{- if .Values.caBundle.enabled }}
- name: ca-bundle
configMap:
name: {{ .Values.caBundle.configMapName }}
items:
- key: {{ .Values.caBundle.key }}
path: ca-bundle.pem
{{- if .Values.caBundle.enabled }}
- name: ca-bundle
configMap:
name: {{ required "caBundle.configMapName must be set when caBundle.enabled=true" .Values.caBundle.configMapName }}
items:
- key: {{ required "caBundle.key must be set when caBundle.enabled=true" .Values.caBundle.key }}
path: ca-bundle.pem
🤖 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 `@charts/solar/templates/controller/deployment.yaml` around lines 123 - 129,
When caBundle.enabled is true the template blindly uses caBundle.configMapName
which may be empty; update the controller/deployment.yaml fragment to guard or
require caBundle.configMapName before rendering the ConfigMap volume: either
wrap the volume block with an additional condition that checks (ne
.Values.caBundle.configMapName "") or replace the direct usage with Helm's
required function (e.g. required "message" .Values.caBundle.configMapName) so
that caBundle.configMapName (and optionally caBundle.key) are validated and you
never emit name: "" for the ca-bundle volume.

Comment on lines +150 to +151
rawRef := artifact.Spec.BaseURL + "/" + artifact.Spec.Repository + ":" + artifact.Spec.Tag
log.V(1).Info("Attempting OCI tag cleanup", "ref", rawRef)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Normalize BaseURL before building the OCI delete ref and auth lookup host.

Using artifact.Spec.BaseURL directly can break cleanup when it carries oci:// or trailing slashes, and can also miss Docker auth map keys. Normalize once and use the normalized host consistently.

Proposed fix
 import (
 	"context"
 	"encoding/json"
 	"slices"
+	"strings"
@@
 func (r *RenderArtifactReconciler) cleanupOCIArtifact(ctx context.Context, artifact *solarv1alpha1.RenderArtifact) error {
 	log := ctrl.LoggerFrom(ctx)
 
-	rawRef := artifact.Spec.BaseURL + "/" + artifact.Spec.Repository + ":" + artifact.Spec.Tag
+	registryHost := strings.TrimPrefix(strings.TrimSuffix(artifact.Spec.BaseURL, "/"), "oci://")
+	rawRef := registryHost + "/" + strings.TrimPrefix(artifact.Spec.Repository, "/") + ":" + artifact.Spec.Tag
 	log.V(1).Info("Attempting OCI tag cleanup", "ref", rawRef)
@@
-	auth := r.resolveAuth(ctx, artifact)
+	auth := r.resolveAuth(ctx, artifact, registryHost)
@@
-func (r *RenderArtifactReconciler) resolveAuth(ctx context.Context, artifact *solarv1alpha1.RenderArtifact) authn.Authenticator {
+func (r *RenderArtifactReconciler) resolveAuth(ctx context.Context, artifact *solarv1alpha1.RenderArtifact, registryHost string) authn.Authenticator {
@@
-	return ociAuthFromSecret(secret, artifact.Spec.BaseURL)
+	return ociAuthFromSecret(secret, registryHost)
 }

Also applies to: 211-212

🤖 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 `@pkg/controller/renderartifact_controller.go` around lines 150 - 151, The code
constructs rawRef from artifact.Spec.BaseURL without normalizing, which can
include "oci://" prefixes or trailing slashes and cause mismatched auth host
lookups; normalize BaseURL once (strip "oci://" prefix and any trailing slashes)
into a local variable (e.g., normalizedBase) and use that when building rawRef
and when performing Docker/OCI auth host lookups, replacing usages of
artifact.Spec.BaseURL with normalizedBase (ensure rawRef becomes normalizedBase
+ "/" + artifact.Spec.Repository + ":" + artifact.Spec.Tag and any auth map key
lookups use normalizedBase or the host-only form derived from it).


auth := r.resolveAuth(ctx, artifact)

if err := deleteFn(ctx, rawRef, auth); err != nil {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add a timeout around OCI tag deletion calls.

DeleteTag is an external network call on the reconcile path; without a local timeout, a hung registry can stall workers.

Proposed fix
 import (
 	"context"
 	"encoding/json"
 	"slices"
+	"time"
@@
-	if err := deleteFn(ctx, rawRef, auth); err != nil {
+	deleteCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
+	defer cancel()
+	if err := deleteFn(deleteCtx, rawRef, auth); err != nil {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if err := deleteFn(ctx, rawRef, auth); err != nil {
deleteCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
if err := deleteFn(deleteCtx, rawRef, auth); err != nil {
🤖 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 `@pkg/controller/renderartifact_controller.go` at line 160, The OCI tag
deletion (DeleteTag) is a network call invoked via deleteFn and needs a local
timeout to avoid hanging the reconcile worker; wrap the call to deleteFn(ctx,
rawRef, auth) with a context.WithTimeout (choose a sensible timeout like
15–30s), use the derived ctx (and defer cancel()) when calling deleteFn, and
ensure the context and time packages are imported so the DeleteTag/backing
network call is cancelled if it exceeds the timeout.

Comment on lines +228 to +234
// Clean up any stale RenderTasks and RenderBindings left from prior reconciles.
if err := r.deleteStaleRenderTasks(ctx, target, map[string]struct{}{}); err != nil {
log.Error(err, "failed to clean up stale RenderTasks after all bindings removed")
}
if err := r.deleteStaleRenderBindings(ctx, target, map[string]struct{}{}); err != nil {
log.Error(err, "failed to clean up stale RenderBindings after all bindings removed")
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Retry when stale cleanup fails in the no-bindings branch.

Only logging cleanup failures here can leave stale RenderTasks/RenderBindings indefinitely if no further events requeue the Target.

Proposed fix
-		if err := r.deleteStaleRenderTasks(ctx, target, map[string]struct{}{}); err != nil {
-			log.Error(err, "failed to clean up stale RenderTasks after all bindings removed")
-		}
-		if err := r.deleteStaleRenderBindings(ctx, target, map[string]struct{}{}); err != nil {
-			log.Error(err, "failed to clean up stale RenderBindings after all bindings removed")
-		}
+		if err := r.deleteStaleRenderTasks(ctx, target, map[string]struct{}{}); err != nil {
+			return ctrl.Result{}, errLogAndWrap(log, err, "failed to clean up stale RenderTasks after all bindings removed")
+		}
+		if err := r.deleteStaleRenderBindings(ctx, target, map[string]struct{}{}); err != nil {
+			return ctrl.Result{}, errLogAndWrap(log, err, "failed to clean up stale RenderBindings after all bindings removed")
+		}
🤖 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 `@pkg/controller/target_controller.go` around lines 228 - 234, The no-bindings
branch currently swallows errors from cleanup by only logging them; change it to
return the error so the reconcile is requeued and retries cleanup. Specifically,
in the section that calls r.deleteStaleRenderTasks(ctx, target,
map[string]struct{}{}) and r.deleteStaleRenderBindings(ctx, target,
map[string]struct{}{}), replace the log-only handling with error returns (e.g.,
if err != nil { log.Error(err, "..."); return ctrl.Result{}, err }) so failures
in deleteStaleRenderTasks and deleteStaleRenderBindings cause the controller to
retry reconciliation.

Comment on lines +491 to +498
// Ensure RenderArtifact + RenderBinding exist for the bootstrap chart.
bootstrapArtifactName := renderArtifactName(target.Namespace, bootstrapRT.Spec.BaseURL, bootstrapRT.Spec.Repository, bootstrapRT.Spec.Tag)
bootstrapBindingName := renderBindingName(bootstrapArtifactName, target.Name)
if err := r.ensureRenderArtifact(ctx, bootstrapArtifactName, bootstrapRT, registry.Spec.Flavor); err != nil {
log.Error(err, "failed to ensure RenderArtifact for bootstrap")
} else if err := r.ensureRenderBinding(ctx, target, bootstrapArtifactName, bootstrapBindingName); err != nil {
log.Error(err, "failed to ensure RenderBinding for bootstrap")
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not swallow bootstrap artifact/binding ensure errors.

If these calls fail and reconcile still returns success, bootstrap tracking can drift and GC/reference-count behavior becomes nondeterministic.

Proposed fix
 		if err := r.ensureRenderArtifact(ctx, bootstrapArtifactName, bootstrapRT, registry.Spec.Flavor); err != nil {
-			log.Error(err, "failed to ensure RenderArtifact for bootstrap")
-		} else if err := r.ensureRenderBinding(ctx, target, bootstrapArtifactName, bootstrapBindingName); err != nil {
-			log.Error(err, "failed to ensure RenderBinding for bootstrap")
+			return ctrl.Result{}, errLogAndWrap(log, err, "failed to ensure RenderArtifact for bootstrap")
+		}
+		if err := r.ensureRenderBinding(ctx, target, bootstrapArtifactName, bootstrapBindingName); err != nil {
+			return ctrl.Result{}, errLogAndWrap(log, err, "failed to ensure RenderBinding for bootstrap")
 		}
🤖 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 `@pkg/controller/target_controller.go` around lines 491 - 498, The bootstrap
ensure errors are being logged but swallowed; change the reconcile flow so
failures from r.ensureRenderArtifact(ctx, bootstrapArtifactName, bootstrapRT,
registry.Spec.Flavor) and r.ensureRenderBinding(ctx, target,
bootstrapArtifactName, bootstrapBindingName) are returned to the caller instead
of only logged. Locate the block using
bootstrapArtifactName/bootstrapBindingName and the calls to ensureRenderArtifact
and ensureRenderBinding in target_controller.go and replace the current log-only
branches with error propagation (return err) so reconcile fails when these
ensures fail, preserving the original error context in the returned error.

Comment thread test/e2e/e2e_test.go
Comment on lines +1022 to +1028
Eventually(func(g Gomega) {
repo, err := zotClient.Repository(ociCtx, releaseRepo())
g.Expect(err).NotTo(HaveOccurred(), "should connect to zot-deploy")
_, _, err = repo.FetchReference(ociCtx, artTag)
g.Expect(err).To(HaveOccurred(),
"OCI tag %s in %s should no longer exist after GC", artTag, releaseRepo())
}).Should(Succeed())
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the file
ls -la test/e2e/e2e_test.go

# Show relevant line ranges around 1020-1040
sed -n '1000,1060p' test/e2e/e2e_test.go | nl -ba | sed -n '1,120p'

# Show relevant line ranges around 1125-1155
sed -n '1100,1160p' test/e2e/e2e_test.go | nl -ba | sed -n '1,140p'

# Also show imports section to see current imports around errors/errdef
# Print top of file through import block
sed -n '1,120p' test/e2e/e2e_test.go | nl -ba | sed -n '1,140p'

Repository: opendefensecloud/solution-arsenal

Length of output: 209


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the file
ls -la test/e2e/e2e_test.go

# Show relevant line ranges around 1020-1040
sed -n '1000,1060p' test/e2e/e2e_test.go | nl -ba | sed -n '1,120p'

# Show relevant line ranges around 1125-1155
sed -n '1100,1160p' test/e2e/e2e_test.go | nl -ba | sed -n '1,140p'

# Also show imports section to see current imports around errors/errdef
sed -n '1,140p' test/e2e/e2e_test.go | nl -ba | sed -n '1,200p'

Repository: opendefensecloud/solution-arsenal

Length of output: 209


🌐 Web query:

oras.land oras-go v2 errdef ErrNotFound FetchReference

💡 Result:

In oras.land/oras-go/v2, a FetchReference call returns the package-level sentinel error errdef.ErrNotFound when the registry responds with HTTP 404 (status not found). That sentinel error is defined as errors.New("missing reference"). [1][2] How FetchReference fits into ORAS v2: - oras-go v2 has an internal content.Fetch path that checks whether the target implements registry.ReferenceFetcher; if so, it calls refFetcher.FetchReference(ctx, reference). [3] - The oras-go documentation/API describes Targets: Resolve resolves a tag string to a descriptor, while Fetch retrieves content identified by a descriptor from the CAS; ReferenceFetcher.FetchReference is the optimized path that fetches by reference directly when supported. [4][3] Therefore, encountering "oras-go v2 ErrNotFound FetchReference" typically means ORAS attempted to fetch a manifest/artifact by reference (tag/digest string) and the reference was missing on the registry, resulting in ErrNotFound/"missing reference". [1][2]

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail
cat go.mod | rg -n "oras\.land/oras-go/v2" || true

Repository: opendefensecloud/solution-arsenal

Length of output: 109


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "FetchReference\\(" -S test/e2e/e2e_test.go

Repository: opendefensecloud/solution-arsenal

Length of output: 366


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "errdef|ErrNotFound|not found" test/e2e/e2e_test.go

Repository: opendefensecloud/solution-arsenal

Length of output: 59


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "not[- ]found|ErrNotFound|Err.*NotFound|errors\\.Is\\(err" test/e2e/e2e_test.go

Repository: opendefensecloud/solution-arsenal

Length of output: 645


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
p = Path("test/e2e/e2e_test.go")
lines = p.read_text().splitlines()
start, end = 980, 1060
for i in range(start, end+1):
    if i < len(lines):
        print(f"{i+1:5d}:\t{lines[i]}")
PY

Repository: opendefensecloud/solution-arsenal

Length of output: 4355


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
p = Path("test/e2e/e2e_test.go")
lines = p.read_text().splitlines()
start, end = 1090, 1170
for i in range(start, end+1):
    if i < len(lines):
        print(f"{i+1:5d}:\t{lines[i]}")
PY

Repository: opendefensecloud/solution-arsenal

Length of output: 4387


🏁 Script executed:

#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
p = Path("test/e2e/e2e_test.go")
lines = p.read_text().splitlines()
# print import block area
for i in range(0, 180):
    if i < len(lines):
        print(f"{i+1:5d}:\t{lines[i]}")
PY

Repository: opendefensecloud/solution-arsenal

Length of output: 8028


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "errdef|ErrNotFound|errors\\.Is\\(err" test/e2e/e2e_test.go || true

Repository: opendefensecloud/solution-arsenal

Length of output: 59


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
p = Path("test/e2e/e2e_test.go")
lines = p.read_text().splitlines()
start, end = 980, 1060
for i in range(start, end+1):
    if i < len(lines):
        print(f"{i+1:5d}:\t{lines[i]}")
PY

Repository: opendefensecloud/solution-arsenal

Length of output: 4355


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
p = Path("test/e2e/e2e_test.go")
lines = p.read_text().splitlines()
start, end = 1090, 1170
for i in range(start, end+1):
    if i < len(lines):
        print(f"{i+1:5d}:\t{lines[i]}")
PY

Repository: opendefensecloud/solution-arsenal

Length of output: 4387


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
p = Path("test/e2e/e2e_test.go")
lines = p.read_text().splitlines()
for i in range(1, 220):
    if i <= len(lines):
        print(f"{i:5d}:\t{lines[i-1]}")
PY

Repository: opendefensecloud/solution-arsenal

Length of output: 9729


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "errdef|ErrNotFound|errors\\.Is\\(err" test/e2e/e2e_test.go || true

Repository: opendefensecloud/solution-arsenal

Length of output: 59


Assert OCI tag “not found” specifically for GC checks.

test/e2e/e2e_test.go lines 1026 and 1137 (1133-1139) currently treat any repo.FetchReference error as success, which can mask transient/auth/network issues. Match the ORAS sentinel not-found error (errdef.ErrNotFound) via errors.Is.

Suggested fix
 import (
 	"context"
+	"errors"
 	"fmt"
 	"os"
 	"os/exec"
 	"path/filepath"
 	"strings"
 	"time"
 
 	. "github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/gomega"
+	"oras.land/oras-go/v2/errdef"
 )
@@
-					_, _, err = repo.FetchReference(ociCtx, artTag)
-					g.Expect(err).To(HaveOccurred(),
+					_, _, err = repo.FetchReference(ociCtx, artTag)
+					g.Expect(errors.Is(err, errdef.ErrNotFound)).To(BeTrue(),
 						"OCI tag %s in %s should no longer exist after GC", artTag, releaseRepo())
@@
-					_, _, err = repo.FetchReference(ociCtx, artTag3)
-					g.Expect(err).To(HaveOccurred(),
+					_, _, err = repo.FetchReference(ociCtx, artTag3)
+					g.Expect(errors.Is(err, errdef.ErrNotFound)).To(BeTrue(),
 						"OCI tag %s in %s should no longer exist after GC via stale-binding path", artTag3, releaseRepo())
🤖 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 `@test/e2e/e2e_test.go` around lines 1022 - 1028, The test currently treats any
error from repo.FetchReference as success, which can hide transient failures;
update the Eventually block that calls zotClient.Repository and
repo.FetchReference to assert that the returned error is specifically a
not-found sentinel by using errors.Is(err, errdef.ErrNotFound) (importing errors
and errdef if not present) instead of g.Expect(err).To(HaveOccurred()); ensure
the failure message still references artTag and releaseRepo() but only consider
the check succeeded when errors.Is indicates ErrNotFound.

@coveralls
Copy link
Copy Markdown

Coverage Report for CI Build 26530885563

Warning

No base build found for commit ce1ad46 on main.
Coverage changes can't be calculated without a base build.
If a base build is processing, this comment will update automatically when it completes.

Coverage: 71.664%

Details

  • Patch coverage: 100 uncovered changes across 5 files (263 of 363 lines covered, 72.45%).

Uncovered Changes

File Changed Covered %
pkg/controller/renderartifact_controller.go 162 117 72.22%
pkg/controller/target_controller.go 123 91 73.98%
pkg/controller/helpers.go 60 41 68.33%
cmd/solar-apiserver/main.go 2 0 0.0%
pkg/ociregistry/standard.go 13 11 84.62%
Total (6 files) 363 263 72.45%

Coverage Regressions

Requires a base build to compare against. How to fix this →


Coverage Stats

Coverage Status
Relevant Lines: 3642
Covered Lines: 2610
Line Coverage: 71.66%
Coverage Strength: 24.17 hits per line

💛 - Coveralls

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.

Cleanup rendered resources when API resources are deleted

3 participants