Skip to content

Conversation

@kvaps
Copy link
Member

@kvaps kvaps commented Dec 24, 2025

depends on #1741

What this PR does

Release note

[cozystack-operator] Add Package and PackageSource reconcilers

Summary by CodeRabbit

Release Notes

  • New Features

    • Automated package and dependency reconciliation with per-dependency readiness tracking
    • New "pks" shorthand alias for PackageSource resources
  • Dependencies

    • Integrated Flux SourceWatcher for enhanced package source management

✏️ Tip: You can customize this high-level summary in your review settings.

@dosubot dosubot bot added size:XXL This PR changes 1000+ lines, ignoring generated files. enhancement New feature or request labels Dec 24, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 24, 2025

Caution

Review failed

The pull request is closed.

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

Two new Kubernetes controllers are introduced to orchestrate Package and PackageSource resource management. The PackageSourceReconciler generates ArtifactGenerators from PackageSource definitions, while the PackageReconciler reconciles Packages by creating HelmReleases and namespaces, managing component dependencies, and tracking readiness status. Integration with Flux's source-watcher API enables reactive watch-based reconciliation.

Changes

Cohort / File(s) Summary
Controller Infrastructure
cmd/cozystack-operator/main.go
Added imports for Flux SourceWatcher v2beta1 and scheme registration. Registered sourcewatcherv1beta1 API types. Wired PackageSourceReconciler and PackageReconciler into manager with client and scheme bindings.
New Reconcilers
internal/operator/package_reconciler.go, internal/operator/packagesource_reconciler.go
Introduced PackageReconciler with core reconciliation loop, HelmRelease management, namespace reconciliation, dependency tracking, and cleanup of orphaned resources. Introduced PackageSourceReconciler to generate ArtifactGenerators, validate SourceRef, construct per-variant library maps, build CopyOperation sequences, and update PackageSource status. Both reconcilers implement watches and status propagation.
API Type Definitions
api/v1alpha1/package_types.go, api/v1alpha1/packagesource_types.go
Added DependencyStatus type with Ready boolean field. Added Dependencies map field to PackageStatus for per-dependency readiness tracking. Updated PackageSource kubebuilder shortName from {pkgsrc, pkgsrcs} to {pks}.
Generated Code
api/v1alpha1/zz_generated.deepcopy.go
Generated DeepCopyInto and DeepCopy methods for new DependencyStatus type. Extended PackageStatus DeepCopyInto to handle new Dependencies map field.
CRD Definitions
packages/core/installer/definitions/cozystack.io_packages.yaml, packages/core/installer/definitions/cozystack.io_packagesources.yaml
Added dependencies schema to Package CRD openAPIV3Schema with DependencyStatus objects containing ready boolean. Updated PackageSource shortNames from [pkgsrc, pkgsrcs] to [pks].
Dependency Management
go.mod
Added github.com/fluxcd/source-watcher/api/v2 v2.0.3 as a new require entry.

Sequence Diagram(s)

sequenceDiagram
    participant M as Manager
    participant PSR as PackageSourceReconciler
    participant K as Kubernetes API
    participant AG as ArtifactGenerator

    M->>PSR: Reconcile PackageSource
    PSR->>K: Fetch PackageSource
    rect rgb(200, 220, 255)
        note right of PSR: Reconcile ArtifactGenerators
        PSR->>PSR: Validate SourceRef
        PSR->>PSR: Build per-variant library map
        PSR->>PSR: Construct CopyOperations
        PSR->>PSR: Derive artifact names
    end
    PSR->>K: Create/Update ArtifactGenerator (server-side apply)
    PSR->>K: Fetch ArtifactGenerator
    rect rgb(200, 220, 255)
        note right of PSR: Mirror Ready condition
        PSR->>PSR: Extract Ready condition
    end
    PSR->>K: Update PackageSource Status
    K-->>M: ArtifactGenerator Ready event
Loading
sequenceDiagram
    participant M as Manager
    participant PR as PackageReconciler
    participant K as Kubernetes API
    participant HR as HelmRelease

    M->>PR: Reconcile Package
    PR->>K: Fetch Package & PackageSource
    PR->>K: Resolve Variant
    rect rgb(200, 220, 255)
        note right of PR: Reconcile Namespaces
        PR->>PR: Derive namespaces from Components
        PR->>K: Create/Update Namespaces (server-side apply)
    end
    rect rgb(200, 220, 255)
        note right of PR: Create/Update HelmReleases
        PR->>PR: Build DependsOn references
        PR->>K: Create/Update HelmRelease resources
    end
    rect rgb(200, 220, 255)
        note right of PR: Track Dependencies
        PR->>PR: Compute variant/component dependencies
        PR->>K: Update Package.Status.Dependencies
    end
    PR->>K: Clean up orphaned HelmReleases
    rect rgb(200, 220, 255)
        note right of PR: Propagate Readiness
        PR->>PR: Enqueue dependent Packages
    end
    K-->>M: Package change event (via watch)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 Hop! Two controllers now take flight,
PackageSource and Package in the night,
Dependencies tracked with care so true,
HelmReleases and namespaces born anew,
The orchestration dance starts here—
Kubernetes magic, crystal clear!

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main changes: adding Package and PackageSource reconcilers to the cozystack-operator, which aligns with the PR's core objective and the substantial changes across multiple files.
Docstring Coverage ✅ Passed Docstring coverage is 88.24% which is sufficient. The required threshold is 80.00%.

📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a3373dc and f28d639.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (9)
  • api/v1alpha1/package_types.go
  • api/v1alpha1/packagesource_types.go
  • api/v1alpha1/zz_generated.deepcopy.go
  • cmd/cozystack-operator/main.go
  • go.mod
  • internal/operator/package_reconciler.go
  • internal/operator/packagesource_reconciler.go
  • packages/core/installer/definitions/cozystack.io_packages.yaml
  • packages/core/installer/definitions/cozystack.io_packagesources.yaml

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 and usage tips.

@kvaps kvaps changed the title cozystack operator reconcilers [cozystack-operator] Add Package and PackageSource reconcilers Dec 24, 2025
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @kvaps, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request lays the foundational framework for the Cozystack operator, enabling declarative management of software packages within a Kubernetes environment. It establishes the core reconciliation loops for PackageSource and Package custom resources, leveraging Flux CD's capabilities for artifact generation and Helm chart deployment. This significantly enhances the platform's ability to define, distribute, and deploy complex application stacks with built-in dependency resolution and lifecycle management.

Highlights

  • Cozystack Operator Core: Introduces the main cozystack-operator executable, responsible for initializing the operator, installing Flux components, applying initial Flux resources, and setting up the PackageSource and Package reconcilers.
  • Flux CD Integration: Implements a mechanism to programmatically install Flux CD components from embedded manifests and apply arbitrary Flux resources (OCIRepository, GitRepository) via command-line flags.
  • PackageSource Reconciler: Adds a new reconciler for PackageSource custom resources, which dynamically generates Flux ArtifactGenerator resources based on the defined variants, components, libraries, and values files within the PackageSource spec.
  • Package Reconciler: Introduces a reconciler for Package custom resources, responsible for creating and managing HelmRelease resources, reconciling necessary namespaces (including privileged settings), handling inter-package dependencies, and cleaning up orphaned HelmRelease objects.
  • Dependency Management: Implements logic for Package resources to declare and validate dependencies on other Package resources, ensuring proper ordering and readiness before deploying components.
  • Makefile Updates: Modifies the flux-aio Makefile to generate Flux manifests into a new embedded location for the operator, streamlining the build process for the integrated Flux installation.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces the core logic for the cozystack-operator, including Package and PackageSource reconcilers. The overall structure is sound, but there are several areas for improvement. I've identified a critical bug in URL parsing, some unreliable patterns like using time.Sleep for synchronization, and inefficiencies in how dependent resources are discovered. Additionally, there are multiple instances of non-atomic Get-then-Update patterns for resource creation/updates, which should be replaced with server-side apply to improve robustness. My review includes suggestions to address these points, aiming to enhance correctness, efficiency, and maintainability.

Copy link
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: 4

🧹 Nitpick comments (9)
internal/fluxinstall/manifests.embed.go (1)

44-44: Consider using more restrictive file permissions.

The file permission 0666 allows world-writable access. For manifest files, 0644 (owner read/write, group/others read-only) is more appropriate and follows the principle of least privilege.

🔎 Suggested fix
-		if err := os.WriteFile(outputPath, data, 0666); err != nil {
+		if err := os.WriteFile(outputPath, data, 0644); err != nil {
packages/core/flux-aio/Makefile (1)

15-15: Add PHONY declaration for the update target.

Per static analysis, the update target should be declared as .PHONY since it doesn't produce a file with that name.

🔎 Suggested fix

Add at the top of the file or before the update target:

.PHONY: show apply diff update update-old update-new
internal/fluxinstall/install.go (2)

184-186: Hardcoded sleep for CRD registration may be unreliable.

A fixed 2-second wait may be insufficient under load or too long in fast environments. Consider polling for CRD readiness or using a retry mechanism with exponential backoff.


63-75: Error variable shadowing in fallback YAML discovery.

The err variable on line 65 shadows the outer err from line 63. While functionally correct here, this pattern can cause confusion. Also, if fluxcd.yaml doesn't exist due to an error other than "not found," the code silently falls back.

🔎 Suggested fix
 	manifestPath := filepath.Join(manifestsDir, "fluxcd.yaml")
-	if _, err := os.Stat(manifestPath); err != nil {
+	if _, statErr := os.Stat(manifestPath); statErr != nil {
 		// Try to find any YAML file if fluxcd.yaml doesn't exist
-		entries, err := os.ReadDir(manifestsDir)
-		if err != nil {
+		entries, readErr := os.ReadDir(manifestsDir)
+		if readErr != nil {
 			return fmt.Errorf("failed to read manifests directory: %w", err)
 		}
internal/operator/package_reconciler.go (2)

182-182: Use time.Duration constants for readability.

The raw nanosecond calculation 5 * 60 * 1000000000 is error-prone. Use time.Minute for clarity.

🔎 Suggested fix
+import "time"
+
 			Spec: helmv2.HelmReleaseSpec{
-				Interval: metav1.Duration{Duration: 5 * 60 * 1000000000}, // 5m
+				Interval: metav1.Duration{Duration: 5 * time.Minute},

829-886: Watch handler lists all Packages on every Package change.

This EnqueueRequestsFromMapFunc iterates all Packages and their PackageSources on every Package event. With N packages, this is O(N) per event, potentially O(N²) overall. Consider using an index or caching the dependency graph.

For controller-runtime, you can use field indexers to efficiently query dependent packages:

// In SetupWithManager, add an indexer
mgr.GetFieldIndexer().IndexField(ctx, &cozyv1alpha1.Package{}, "spec.dependsOn", func(obj client.Object) []string {
    // Return package names this package depends on
})
cmd/cozystack-operator/main.go (2)

231-293: Consider extracting common resource apply logic.

installFluxResourcesFunc and installPlatformSourceResource share similar create-or-update logic for unstructured objects. Consider extracting a helper to reduce duplication.

Also applies to: 297-358


460-481: GitRepository ref always uses branch, but ref could be a tag or commit.

The generateGitRepository function always sets ref.branch, but the parsed ref from the URL could be a tag or commit SHA. Consider detecting the ref type.

🔎 Suggested enhancement
 func generateGitRepository(name, repoURL, ref string) (*unstructured.Unstructured, error) {
+	refSpec := map[string]interface{}{}
+	// Detect if ref looks like a commit SHA (40 hex chars) or semver tag
+	if len(ref) == 40 && isHexString(ref) {
+		refSpec["commit"] = ref
+	} else if strings.HasPrefix(ref, "v") && strings.Contains(ref, ".") {
+		refSpec["tag"] = ref
+	} else {
+		refSpec["branch"] = ref
+	}
+
 	spec := map[string]interface{}{
 		"interval": "5m0s",
 		"url":      repoURL,
-		"ref": map[string]interface{}{
-			"branch": ref,
-		},
+		"ref":      refSpec,
 	}
internal/operator/packagesource_reconciler.go (1)

309-364: Generic createOrUpdate contains type-specific logic.

The createOrUpdate method has special handling for ArtifactGenerator embedded within it (lines 347-361). This reduces reusability. Consider either making it ArtifactGenerator-specific or extracting the type-specific logic.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 59f62b7 and 12f6336.

📒 Files selected for processing (7)
  • cmd/cozystack-operator/main.go
  • internal/fluxinstall/install.go
  • internal/fluxinstall/manifests.embed.go
  • internal/fluxinstall/manifests/fluxcd.yaml
  • internal/operator/package_reconciler.go
  • internal/operator/packagesource_reconciler.go
  • packages/core/flux-aio/Makefile
🧰 Additional context used
📓 Path-based instructions (1)
**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

Use Controller-runtime patterns and kubebuilder style for Go code

Files:

  • internal/fluxinstall/install.go
  • internal/fluxinstall/manifests.embed.go
  • internal/operator/packagesource_reconciler.go
  • cmd/cozystack-operator/main.go
  • internal/operator/package_reconciler.go
🧬 Code graph analysis (2)
internal/operator/packagesource_reconciler.go (3)
pkg/apiserver/apiserver.go (1)
  • Scheme (53-53)
api/v1alpha1/cozystackresourcedefinitions_types.go (1)
  • SourceRef (71-80)
internal/controller/dashboard/manager.go (1)
  • Manager (45-48)
internal/operator/package_reconciler.go (2)
pkg/apiserver/apiserver.go (1)
  • Scheme (53-53)
internal/controller/dashboard/manager.go (1)
  • Manager (45-48)
🪛 checkmake (0.2.2)
packages/core/flux-aio/Makefile

[warning] 15-15: Target "update" should be declared PHONY.

(phonydeclared)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build
🔇 Additional comments (3)
internal/operator/package_reconciler.go (1)

155-164: Inconsistent namespace handling between HelmRelease and reconcileNamespaces.

Here, empty namespace defaults to "cozy-system", but reconcileNamespaces (line 562) returns an error if namespace is empty. This inconsistency could cause confusion—if a component has no namespace specified, HelmRelease creation succeeds but namespace reconciliation would fail if the check were applied there.

Please verify this is the intended behavior. If components without an explicit namespace should use cozy-system, the error in reconcileNamespaces should also apply this default.

cmd/cozystack-operator/main.go (1)

157-157: Using mgr.GetClient() before manager is started.

The manager's client is being used before mgr.Start() is called. While the client is available, the cache won't be started yet, so reads may go directly to the API server. This is likely intentional for installation steps, but be aware of potential rate limiting if many resources are read.

Verify this is acceptable for the installation workflow. The client will bypass the cache and hit the API server directly.

internal/operator/packagesource_reconciler.go (1)

441-467: Well-structured controller setup with efficient watch.

The watch on ArtifactGenerator correctly uses ownerReference lookup to find the owning PackageSource, which is efficient and follows controller-runtime best practices. Good use of EnqueueRequestsFromMapFunc.

@kvaps kvaps force-pushed the cozystack-operator-reconcilers branch from 12f6336 to 72e3cac Compare December 25, 2025 01:04
Copy link
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: 2

♻️ Duplicate comments (9)
cmd/cozystack-operator/main.go (3)

89-89: Unused cozystackVersion flag.

The cozystackVersion flag is parsed but never used. Consider logging it during startup or removing the flag if not needed.

🔎 Suggested fix
 	setupLog.Info("Starting controller manager")
+	setupLog.Info("Cozystack version", "version", cozystackVersion)

263-293: Consider server-side apply for consistency.

The Get/Create/Update pattern here is not atomic. Since internal/fluxinstall/install.go already uses server-side apply, consider using the same approach for consistency and robustness.

🔎 Server-side apply pattern
-		// Use server-side apply or create/update
-		existing := &unstructured.Unstructured{}
-		existing.SetGroupVersionKind(obj.GroupVersionKind())
-		key := client.ObjectKey{
-			Name:      obj.GetName(),
-			Namespace: obj.GetNamespace(),
-		}
-
-		err := k8sClient.Get(ctx, key, existing)
-		if err != nil {
-			if client.IgnoreNotFound(err) == nil {
-				// Resource doesn't exist, create it
-				if err := k8sClient.Create(ctx, &obj); err != nil {
-					return fmt.Errorf("failed to create resource %s/%s: %w", obj.GetKind(), obj.GetName(), err)
-				}
-				logger.Info("Created Flux resource", "kind", obj.GetKind(), "name", obj.GetName())
-			} else {
-				return fmt.Errorf("failed to check if resource exists: %w", err)
-			}
-		} else {
-			// Resource exists, update it
-			obj.SetResourceVersion(existing.GetResourceVersion())
-			if err := k8sClient.Update(ctx, &obj); err != nil {
-				return fmt.Errorf("failed to update resource %s/%s: %w", obj.GetKind(), obj.GetName(), err)
-			}
-			logger.Info("Updated Flux resource", "kind", obj.GetKind(), "name", obj.GetName())
-		}
+		// Use server-side apply
+		if err := k8sClient.Patch(ctx, &obj, client.Apply, client.FieldOwner("cozystack-operator")); err != nil {
+			return fmt.Errorf("failed to apply resource %s/%s: %w", obj.GetKind(), obj.GetName(), err)
+		}
+		logger.Info("Applied Flux resource", "kind", obj.GetKind(), "name", obj.GetName())

376-378: Use strings.LastIndex for robust digest parsing.

The comment correctly states that LastIndex should be used to handle paths with @ symbols, but the code uses strings.Index. This could incorrectly split the URL if the repository path contains @sha256:.

🔎 Proposed fix
-		sha256Idx := strings.Index(rest, "@sha256:")
+		sha256Idx := strings.LastIndex(rest, "@sha256:")
internal/operator/packagesource_reconciler.go (2)

87-103: Library name collision across variants.

If two variants define libraries with the same name but different paths, the last one overwrites the first without warning. This could cause unexpected behavior.

🔎 Suggested fix: log collision warning
 	for _, variant := range packageSource.Spec.Variants {
 		for _, lib := range variant.Libraries {
 			libName := lib.Name
 			if libName == "" {
 				libName = r.getPackageNameFromPath(lib.Path)
 			}
 			if libName != "" {
+				if existing, ok := libraryMap[libName]; ok && existing.Path != lib.Path {
+					logger.Info("library name collision detected, overwriting",
+						"libraryName", libName,
+						"existingPath", existing.Path,
+						"newPath", lib.Path,
+						"variant", variant.Name)
+				}
 				libraryMap[libName] = lib
 			}
 		}
 	}

309-364: Consider server-side apply for atomic updates.

The Get/Create/Update pattern is not atomic and can lead to race conditions. Server-side apply would be more robust and would simplify the label/annotation merging logic.

🔎 Server-side apply pattern
 func (r *PackageSourceReconciler) createOrUpdate(ctx context.Context, obj client.Object) error {
-	existing := obj.DeepCopyObject().(client.Object)
-	key := client.ObjectKeyFromObject(obj)
-
-	err := r.Get(ctx, key, existing)
-	if apierrors.IsNotFound(err) {
-		return r.Create(ctx, obj)
-	} else if err != nil {
-		return err
-	}
-	// ... rest of update logic
+	return r.Patch(ctx, obj, client.Apply, client.FieldOwner("cozystack-packagesource"), client.ForceOwnership)
 }

Note: Server-side apply requires the object to have GVK set and may need adjustments for owner references.

internal/operator/package_reconciler.go (4)

400-435: Duplicated variant lookup logic.

The pattern of fetching Package → PackageSource → finding Variant is repeated in buildDependsOn, validateVariantDependencies, and triggerDependentPackages. Consider extracting this into a helper function.

🔎 Suggested helper
// getPackageVariant returns the Package, PackageSource, and effective Variant for a package name
func (r *PackageReconciler) getPackageVariant(ctx context.Context, packageName string) (*cozyv1alpha1.Package, *cozyv1alpha1.PackageSource, *cozyv1alpha1.Variant, error) {
	pkg := &cozyv1alpha1.Package{}
	if err := r.Get(ctx, types.NamespacedName{Name: packageName}, pkg); err != nil {
		return nil, nil, nil, err
	}

	ps := &cozyv1alpha1.PackageSource{}
	if err := r.Get(ctx, types.NamespacedName{Name: packageName}, ps); err != nil {
		return nil, nil, nil, err
	}

	variantName := pkg.Spec.Variant
	if variantName == "" {
		variantName = "default"
	}

	for i := range ps.Spec.Variants {
		if ps.Spec.Variants[i].Name == variantName {
			return pkg, ps, &ps.Spec.Variants[i], nil
		}
	}

	return nil, nil, nil, fmt.Errorf("variant %s not found in PackageSource %s", variantName, packageName)
}

275-279: Redundant dependent triggering via annotation updates.

The triggerDependentPackages function updates annotations on dependent Packages to trigger reconciliation. However, the watch handler on Package (lines 830-887) already enqueues reconcile requests for dependent Packages when a Package changes. This annotation update is redundant and could cause cascading updates.

🔎 Suggested fix

Remove the triggerDependentPackages call since the watch handles this:

 	logger.Info("reconciled Package", "name", pkg.Name, "helmReleaseCount", helmReleaseCount)

-	// Trigger reconcile for Packages that depend on this Package
-	if err := r.triggerDependentPackages(ctx, pkg.Name); err != nil {
-		logger.Error(err, "failed to trigger dependent Packages", "package", pkg.Name)
-		// Don't return error, this is best-effort
-	}
-
 	return ctrl.Result{}, nil

284-333: Get/Create/Update pattern in createOrUpdateHelmRelease.

Same atomicity concern as other reconcilers. Server-side apply would be more robust.


612-652: Get/Create/Update pattern in createOrUpdateNamespace.

Same atomicity concern as other helper functions. Server-side apply would be more robust.

🧹 Nitpick comments (6)
cmd/cozystack-operator/main.go (2)

150-192: Context cancellation with multiple sequential timeouts.

The code creates multiple context.WithTimeout calls with deferred cancels. While functionally correct (each defer captures its own cancel function), the pattern is unusual. Since each operation completes before the next begins, consider calling cancel immediately after each operation completes to release resources promptly.

🔎 Suggested refactor
 	// Install Flux before starting reconcile loop
 	if installFlux {
 		setupLog.Info("Installing Flux components before starting reconcile loop")
 		installCtx, installCancel := context.WithTimeout(context.Background(), 5*time.Minute)
-		defer installCancel()
 
 		// The namespace will be automatically extracted from the embedded manifests
 		if err := fluxinstall.Install(installCtx, mgr.GetClient(), fluxinstall.WriteEmbeddedManifests); err != nil {
 			setupLog.Error(err, "failed to install Flux, continuing anyway")
 		} else {
 			setupLog.Info("Flux installation completed successfully")
 		}
+		installCancel()
 	}

Apply the same pattern to the other timeout contexts.


468-474: Git ref always treated as branch.

The ref is always set as a branch, but the parsed reference could be a tag. Consider supporting tag references for Git repositories.

🔎 Suggested improvement

You could detect tags (e.g., semver patterns) or add a flag to specify the ref type:

 	spec := map[string]interface{}{
 		"interval": "5m0s",
 		"url":      repoURL,
-		"ref": map[string]interface{}{
-			"branch": ref,
-		},
 	}
+	// Determine if ref is a tag (starts with 'v' followed by digit, or contains dots)
+	if strings.HasPrefix(ref, "v") || strings.Contains(ref, ".") {
+		spec["ref"] = map[string]interface{}{
+			"tag": ref,
+		}
+	} else {
+		spec["ref"] = map[string]interface{}{
+			"branch": ref,
+		}
+	}

Alternatively, support explicit syntax like git://repo@branch:main vs git://repo@tag:v1.0.0.

internal/operator/packagesource_reconciler.go (2)

211-220: OwnerReference may have empty APIVersion/Kind.

When using r.Get(), the returned object may not have APIVersion and Kind populated (depends on the client configuration). Consider using ctrl.SetControllerReference() which handles this properly.

🔎 Suggested fix
-	// Set ownerReference
-	ag.OwnerReferences = []metav1.OwnerReference{
-		{
-			APIVersion: packageSource.APIVersion,
-			Kind:       packageSource.Kind,
-			Name:       packageSource.Name,
-			UID:        packageSource.UID,
-			Controller: func() *bool { b := true; return &b }(),
-		},
-	}
+	// Set ownerReference
+	if err := ctrl.SetControllerReference(packageSource, ag, r.Scheme); err != nil {
+		return fmt.Errorf("failed to set controller reference: %w", err)
+	}

423-431: LastTransitionTime copied from ArtifactGenerator may be misleading.

Copying LastTransitionTime from the ArtifactGenerator while setting ObservedGeneration from the PackageSource creates a mixed condition. Consider using the current time or documenting this behavior.

internal/operator/package_reconciler.go (2)

201-210: OwnerReference may have empty APIVersion/Kind.

Same issue as in PackageSourceReconciler. Consider using ctrl.SetControllerReference() for proper GVK handling.

🔎 Suggested fix
-	// Set ownerReference
-	hr.OwnerReferences = []metav1.OwnerReference{
-		{
-			APIVersion: pkg.APIVersion,
-			Kind:       pkg.Kind,
-			Name:       pkg.Name,
-			UID:        pkg.UID,
-			Controller: func() *bool { b := true; return &b }(),
-		},
-	}
+	// Set ownerReference
+	if err := ctrl.SetControllerReference(pkg, hr, r.Scheme); err != nil {
+		return ctrl.Result{}, fmt.Errorf("failed to set controller reference: %w", err)
+	}

830-888: O(n) enumeration in watch handler could be optimized at scale.

The watch handler lists all Packages on every Package change and iterates through them to find dependents. While acceptable for small deployments, consider using controller-runtime field indexes for more efficient lookups as the cluster scales.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 12f6336 and 72e3cac.

📒 Files selected for processing (3)
  • cmd/cozystack-operator/main.go
  • internal/operator/package_reconciler.go
  • internal/operator/packagesource_reconciler.go
🧰 Additional context used
📓 Path-based instructions (1)
**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

Use Controller-runtime patterns and kubebuilder style for Go code

Files:

  • internal/operator/packagesource_reconciler.go
  • cmd/cozystack-operator/main.go
  • internal/operator/package_reconciler.go
🧠 Learnings (1)
📚 Learning: 2025-11-27T11:21:45.808Z
Learnt from: CR
Repo: cozystack/cozystack PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-27T11:21:45.808Z
Learning: Applies to **/*.go : Use Controller-runtime patterns and kubebuilder style for Go code

Applied to files:

  • cmd/cozystack-operator/main.go
🧬 Code graph analysis (3)
internal/operator/packagesource_reconciler.go (1)
internal/controller/dashboard/manager.go (1)
  • Manager (45-48)
cmd/cozystack-operator/main.go (2)
internal/fluxinstall/install.go (1)
  • Install (41-108)
internal/fluxinstall/manifests.embed.go (1)
  • WriteEmbeddedManifests (31-50)
internal/operator/package_reconciler.go (2)
pkg/apiserver/apiserver.go (1)
  • Scheme (53-53)
internal/controller/dashboard/manager.go (1)
  • Manager (45-48)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build
🔇 Additional comments (6)
cmd/cozystack-operator/main.go (2)

72-80: LGTM!

The scheme registration follows standard kubebuilder patterns and correctly registers all required API types.


194-228: LGTM!

The controller setup and manager wiring follow standard controller-runtime patterns. Health and readiness probes are correctly configured.

internal/operator/packagesource_reconciler.go (3)

48-74: LGTM!

The Reconcile method follows the standard controller-runtime pattern with proper error handling. The decision to not return an error on status update failure is reasonable since status is informational.


110-179: LGTM!

The OutputArtifact construction logic is well-structured. The valuesFiles merge strategy (Overwrite for first, Merge for subsequent) and Kubernetes-compliant naming are correctly implemented.


441-467: LGTM!

The controller setup correctly watches ArtifactGenerator resources and enqueues reconcile requests for the owning PackageSource based on owner references. This follows controller-runtime best practices.

internal/operator/package_reconciler.go (1)

52-108: LGTM!

The main Reconcile entry point correctly fetches the Package and PackageSource, handles not-found cases gracefully, and sets appropriate status conditions when resources are missing.

@kvaps kvaps force-pushed the cozystack-operator-reconcilers branch from 72e3cac to 9ef1d32 Compare December 25, 2025 09:51
Copy link
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: 0

♻️ Duplicate comments (12)
cmd/cozystack-operator/main.go (3)

89-89: Unused variable: cozystackVersion.

The cozystackVersion flag is parsed but never used. Consider logging it during startup or removing the flag.

🔎 Suggested fix

Add to startup logging:

 	setupLog.Info("Starting controller manager")
+	setupLog.Info("Cozystack version", "version", cozystackVersion)

271-289: Prefer server-side apply for atomic resource installation.

Both installFluxResourcesFunc and installPlatformSourceResource use Get followed by Create or Update, which is not atomic. Using server-side apply (client.Patch with client.Apply) would be more robust and align with the pattern used in internal/fluxinstall/install.go.

🔎 Suggested refactor

Replace the Get/Create/Update logic with:

-	err := k8sClient.Get(ctx, key, existing)
-	if err != nil {
-		if client.IgnoreNotFound(err) == nil {
-			if err := k8sClient.Create(ctx, &obj); err != nil {
-				return fmt.Errorf("failed to create resource: %w", err)
-			}
-		} else {
-			return fmt.Errorf("failed to check if resource exists: %w", err)
-		}
-	} else {
-		obj.SetResourceVersion(existing.GetResourceVersion())
-		if err := k8sClient.Update(ctx, &obj); err != nil {
-			return fmt.Errorf("failed to update resource: %w", err)
-		}
-	}
+	patchOptions := &client.PatchOptions{
+		FieldManager: "cozystack-operator",
+		Force:        func() *bool { b := true; return &b }(),
+	}
+	if err := k8sClient.Patch(ctx, &obj, client.Apply, patchOptions); err != nil {
+		return fmt.Errorf("failed to apply resource: %w", err)
+	}

Also applies to: 337-355


443-451: Incorrect handling of OCI tags vs digests.

The code unconditionally prepends sha256: to any non-empty digest. However, if the ref is a tag (e.g., @v1.0.0), this produces an invalid reference like sha256:v1.0.0. This will break OCI repositories using tags instead of digests.

🔎 Suggested fix

Distinguish between tags and digests:

 	if digest != "" {
-		// Ensure digest starts with sha256:
-		if !strings.HasPrefix(digest, "sha256:") {
-			digest = "sha256:" + digest
+		// Only treat as digest if it looks like one
+		if strings.HasPrefix(digest, "sha256:") {
+			spec["ref"] = map[string]interface{}{
+				"digest": digest,
+			}
+		} else {
+			// Treat as tag
+			spec["ref"] = map[string]interface{}{
+				"tag": digest,
+			}
 		}
-		spec["ref"] = map[string]interface{}{
-			"digest": digest,
-		}
 	}
internal/operator/package_reconciler.go (6)

613-652: Prefer server-side apply for Namespace updates.

The createOrUpdateNamespace function uses Get/Create/Update which is not atomic. Server-side apply would be more robust.

🔎 Suggested refactor

Replace with server-side apply:

 func (r *PackageReconciler) createOrUpdateNamespace(ctx context.Context, namespace *corev1.Namespace) error {
-	existing := &corev1.Namespace{}
-	key := types.NamespacedName{Name: namespace.Name}
-	err := r.Get(ctx, key, existing)
-	if apierrors.IsNotFound(err) {
-		return r.Create(ctx, namespace)
-	} else if err != nil {
-		return err
-	}
-	// ... merge logic ...
-	return r.Update(ctx, namespace)
+	patchOptions := &client.PatchOptions{
+		FieldManager: "cozystack-operator",
+		Force:        func() *bool { b := true; return &b }(),
+	}
+	return r.Patch(ctx, namespace, client.Apply, patchOptions)
 }

714-801: Inefficient dependent Package lookup.

The triggerDependentPackages function lists all Packages in the cluster and iterates through them. This can be inefficient with many Packages. Consider using a controller-runtime index on dependency fields for efficient lookup.


275-279: Reconsider triggering dependent Packages via annotation updates.

Updating the cozystack.io/trigger-reconcile annotation on dependent Packages triggers their reconciliation, which in turn may call triggerDependentPackages again, potentially causing cascading updates across the dependency graph. The existing watch handler (lines 830-888) already enqueues dependent Packages when a Package changes, making this annotation-based trigger redundant.

🔎 Suggested alternative

Consider removing the triggerDependentPackages call since the watch-based approach already handles this:

 	logger.Info("reconciled Package", "name", pkg.Name, "helmReleaseCount", helmReleaseCount)
-
-	// Trigger reconcile for Packages that depend on this Package
-	if err := r.triggerDependentPackages(ctx, pkg.Name); err != nil {
-		logger.Error(err, "failed to trigger dependent Packages", "package", pkg.Name)
-		// Don't return error, this is best-effort
-	}
 
 	return ctrl.Result{}, nil

400-435: Refactor duplicated variant lookup logic.

The logic for finding the correct variant for a Package is repeated in buildDependsOn, validateVariantDependencies, and triggerDependentPackages. This code duplication reduces maintainability.

🔎 Suggested refactor

Extract into a helper function:

func (r *PackageReconciler) getVariantForPackage(ctx context.Context, packageName string) (*cozyv1alpha1.Variant, error) {
	pkg := &cozyv1alpha1.Package{}
	if err := r.Get(ctx, types.NamespacedName{Name: packageName}, pkg); err != nil {
		return nil, err
	}
	
	variantName := pkg.Spec.Variant
	if variantName == "" {
		variantName = "default"
	}
	
	packageSource := &cozyv1alpha1.PackageSource{}
	if err := r.Get(ctx, types.NamespacedName{Name: packageName}, packageSource); err != nil {
		return nil, err
	}
	
	for i := range packageSource.Spec.Variants {
		if packageSource.Spec.Variants[i].Name == variantName {
			return &packageSource.Spec.Variants[i], nil
		}
	}
	
	return nil, fmt.Errorf("variant %s not found in PackageSource %s", variantName, packageName)
}

Then use it in all three places.

Also applies to: 494-529, 740-757


285-333: Prefer server-side apply for HelmRelease updates.

The createOrUpdateHelmRelease function uses Get followed by Create or Update, which can lead to race conditions. Server-side apply is the recommended approach for operators.

🔎 Suggested refactor

Replace with server-side apply:

 func (r *PackageReconciler) createOrUpdateHelmRelease(ctx context.Context, hr *helmv2.HelmRelease) error {
-	existing := &helmv2.HelmRelease{}
-	key := types.NamespacedName{
-		Name:      hr.Name,
-		Namespace: hr.Namespace,
-	}
-	err := r.Get(ctx, key, existing)
-	if apierrors.IsNotFound(err) {
-		return r.Create(ctx, hr)
-	} else if err != nil {
-		return err
-	}
-	// ... merge logic ...
-	return r.Update(ctx, existing)
+	patchOptions := &client.PatchOptions{
+		FieldManager: "cozystack-operator",
+		Force:        func() *bool { b := true; return &b }(),
+	}
+	return r.Patch(ctx, hr, client.Apply, patchOptions)
 }

559-563: Inconsistent namespace defaulting.

The reconcileNamespaces function errors when component.Install.Namespace is empty, but HelmRelease creation (lines 156-159) defaults empty namespaces to cozy-system. This inconsistency could cause namespace reconciliation to fail for components relying on the default.

🔎 Suggested fix

Apply the same defaulting logic:

 		namespace := component.Install.Namespace
 		if namespace == "" {
-			return fmt.Errorf("component %s has empty namespace in Install section", component.Name)
+			namespace = "cozy-system"
 		}
internal/operator/packagesource_reconciler.go (2)

90-103: Potential library name collision across variants.

The library map is built from all variants, but if two variants define libraries with the same name but different paths, the last one silently overwrites earlier entries. This could cause components to reference incorrect library paths.

🔎 Suggested fix

Consider keying by variant+library name, or detecting and logging collisions:

 	for _, variant := range packageSource.Spec.Variants {
 		for _, lib := range variant.Libraries {
 			libName := lib.Name
 			if libName == "" {
 				libName = r.getPackageNameFromPath(lib.Path)
 			}
 			if libName != "" {
+				if existing, ok := libraryMap[libName]; ok && existing.Path != lib.Path {
+					logger.Info("library name collision detected, last one wins",
+						"name", libName,
+						"existingPath", existing.Path,
+						"newPath", lib.Path)
+				}
 				libraryMap[libName] = lib
 			}
 		}
 	}

310-364: Prefer server-side apply for atomic resource updates.

The createOrUpdate function uses Get followed by Create or Update, which is not atomic and can lead to race conditions. Server-side apply (client.Patch with client.Apply) is more robust and handles field ownership declaratively.

🔎 Suggested refactor

Replace with server-side apply:

 func (r *PackageSourceReconciler) createOrUpdate(ctx context.Context, obj client.Object) error {
-	existing := obj.DeepCopyObject().(client.Object)
-	key := client.ObjectKeyFromObject(obj)
-
-	err := r.Get(ctx, key, existing)
-	if apierrors.IsNotFound(err) {
-		return r.Create(ctx, obj)
-	} else if err != nil {
-		return err
-	}
-	// ... merge logic ...
-	return r.Update(ctx, obj)
+	patchOptions := &client.PatchOptions{
+		FieldManager: "cozystack-operator",
+		Force:        func() *bool { b := true; return &b }(),
+	}
+	return r.Patch(ctx, obj, client.Apply, patchOptions)
 }
internal/fluxinstall/install.go (1)

181-182: Unreliable CRD registration wait using time.Sleep.

Using time.Sleep to wait for CRDs is unreliable on slow or busy clusters. The 2-second duration may not be sufficient, leading to failures when applying resources that depend on those CRDs.

🔎 Suggested fix

Poll the API server to check if CRDs are established:

 	if len(stageOne) > 0 {
 		logger.Info("Applying cluster definitions", "count", len(stageOne))
 		if err := applyObjects(ctx, k8sClient, stageOne); err != nil {
 			return fmt.Errorf("failed to apply cluster definitions: %w", err)
 		}
-		// Wait a bit for CRDs to be registered
-		time.Sleep(2 * time.Second)
+		// Wait for CRDs to be established
+		for _, obj := range stageOne {
+			if obj.GetKind() == "CustomResourceDefinition" {
+				if err := waitForCRDEstablished(ctx, k8sClient, obj.GetName(), 30*time.Second); err != nil {
+					return fmt.Errorf("CRD %s not established: %w", obj.GetName(), err)
+				}
+			}
+		}
 	}

You'll need to implement waitForCRDEstablished to poll the CRD's status.

🧹 Nitpick comments (3)
packages/core/flux-aio/Makefile (2)

15-15: Declare target as .PHONY for robustness.

The update target should be declared as .PHONY to ensure it runs even if a file named "update" exists in the directory.

🔎 Suggested fix

Add at the top of the Makefile or before the target:

+.PHONY: update update-old update-new
 update: update-old update-new

Based on static analysis hints.


33-34: TODO: Resolve DNS configuration for tenant clusters.

The commented-out dnsPolicy insertion indicates an unresolved DNS issue when using hostNetwork in tenant Kubernetes clusters. This could affect Flux's ability to install HelmReleases in tenant environments.

Do you want me to help investigate DNS configuration options or open an issue to track this work?

internal/fluxinstall/install.go (1)

255-309: Consider logging all environment injection failures.

The injectKubernetesServiceEnv function continues processing after errors and only returns the first error encountered. While this provides resilience, subsequent errors are silently skipped without any log output, making troubleshooting difficult.

🔎 Suggested improvement

Log each error as it occurs:

 		spec, found, err := unstructured.NestedMap(obj.Object, "spec", "template", "spec")
 		if !found {
 			continue
 		}
 		if err != nil {
+			logger.V(1).Info("failed to get spec for environment injection", "kind", kind, "name", obj.GetName(), "error", err)
 			if firstErr == nil {
 				firstErr = fmt.Errorf("failed to get spec for %s/%s: %w", kind, obj.GetName(), err)
 			}
 			continue
 		}

Apply similar logging for other error cases.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 72e3cac and 9ef1d32.

📒 Files selected for processing (7)
  • cmd/cozystack-operator/main.go
  • internal/fluxinstall/install.go
  • internal/fluxinstall/manifests.embed.go
  • internal/fluxinstall/manifests/fluxcd.yaml
  • internal/operator/package_reconciler.go
  • internal/operator/packagesource_reconciler.go
  • packages/core/flux-aio/Makefile
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/fluxinstall/manifests.embed.go
🧰 Additional context used
📓 Path-based instructions (4)
cmd/**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

Follow controller-runtime patterns and kubebuilder style for Go code in cmd directory

Files:

  • cmd/cozystack-operator/main.go
**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

Do NOT manually edit vendor/ directory or auto-generated files (zz_generated.*.go)

Files:

  • cmd/cozystack-operator/main.go
  • internal/fluxinstall/install.go
  • internal/operator/packagesource_reconciler.go
  • internal/operator/package_reconciler.go
internal/**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

Follow controller-runtime patterns and kubebuilder style for Go code in internal directory

Files:

  • internal/fluxinstall/install.go
  • internal/operator/packagesource_reconciler.go
  • internal/operator/package_reconciler.go
packages/core/**

📄 CodeRabbit inference engine (AGENTS.md)

Use Helm Chart umbrella pattern with vendored upstream charts in charts/ directory

Files:

  • packages/core/flux-aio/Makefile
🧠 Learnings (3)
📚 Learning: 2025-12-25T09:45:26.511Z
Learnt from: CR
Repo: cozystack/cozystack PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-25T09:45:26.511Z
Learning: Applies to cmd/**/*.go : Follow controller-runtime patterns and kubebuilder style for Go code in cmd directory

Applied to files:

  • cmd/cozystack-operator/main.go
📚 Learning: 2025-12-25T09:45:26.511Z
Learnt from: CR
Repo: cozystack/cozystack PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-25T09:45:26.511Z
Learning: Applies to internal/**/*.go : Follow controller-runtime patterns and kubebuilder style for Go code in internal directory

Applied to files:

  • cmd/cozystack-operator/main.go
  • internal/operator/packagesource_reconciler.go
  • internal/operator/package_reconciler.go
📚 Learning: 2025-12-25T09:45:26.511Z
Learnt from: CR
Repo: cozystack/cozystack PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-25T09:45:26.511Z
Learning: Applies to pkg/**/*.go : Follow controller-runtime patterns and kubebuilder style for Go code in pkg directory

Applied to files:

  • cmd/cozystack-operator/main.go
  • internal/operator/packagesource_reconciler.go
  • internal/operator/package_reconciler.go
🧬 Code graph analysis (2)
cmd/cozystack-operator/main.go (4)
internal/fluxinstall/install.go (1)
  • Install (39-106)
internal/fluxinstall/manifests.embed.go (1)
  • WriteEmbeddedManifests (31-50)
internal/operator/packagesource_reconciler.go (1)
  • PackageSourceReconciler (39-42)
internal/operator/package_reconciler.go (1)
  • PackageReconciler (40-43)
internal/operator/packagesource_reconciler.go (2)
api/v1alpha1/packagesource_types.go (2)
  • PackageSource (31-37)
  • Library (85-93)
api/v1alpha1/cozystackresourcedefinitions_types.go (1)
  • SourceRef (71-80)
🪛 checkmake (0.2.2)
packages/core/flux-aio/Makefile

[warning] 15-15: Target "update" should be declared PHONY.

(phonydeclared)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build

@kvaps kvaps force-pushed the cozystack-operator-reconcilers branch 2 times, most recently from af02b8d to 2ae2670 Compare December 25, 2025 11:48
Copy link
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: 2

♻️ Duplicate comments (9)
cmd/cozystack-operator/main.go (3)

76-77: Unused cozystackVersion variable.

The cozystackVersion flag is parsed but never used. Consider logging it at startup for operational visibility.

🔎 Suggested fix
 	ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
+	setupLog.Info("Cozystack operator starting", "version", cozystackVersion)
 
 	config := ctrl.GetConfigOrDie()

242-260: Consider using server-side apply for consistency.

The current Get/Create/Update pattern is acceptable for bootstrap operations but could be simplified using server-side apply, which would also be consistent with the approach in internal/fluxinstall/install.go.

🔎 Suggested refactor using SSA
-	existing := &unstructured.Unstructured{}
-	existing.SetGroupVersionKind(obj.GroupVersionKind())
-	key := client.ObjectKey{
-		Name:      obj.GetName(),
-		Namespace: obj.GetNamespace(),
-	}
-
-	err = k8sClient.Get(ctx, key, existing)
-	if err != nil {
-		if client.IgnoreNotFound(err) == nil {
-			// Resource doesn't exist, create it
-			if err := k8sClient.Create(ctx, obj); err != nil {
-				return fmt.Errorf("failed to create resource %s/%s: %w", obj.GetKind(), obj.GetName(), err)
-			}
-			logger.Info("Created platform source resource", "kind", obj.GetKind(), "name", obj.GetName())
-		} else {
-			return fmt.Errorf("failed to check if resource exists: %w", err)
-		}
-	} else {
-		// Resource exists, update it
-		obj.SetResourceVersion(existing.GetResourceVersion())
-		if err := k8sClient.Update(ctx, obj); err != nil {
-			return fmt.Errorf("failed to update resource %s/%s: %w", obj.GetKind(), obj.GetName(), err)
-		}
-		logger.Info("Updated platform source resource", "kind", obj.GetKind(), "name", obj.GetName())
-	}
+	patchOptions := &client.PatchOptions{
+		FieldManager: "cozystack-operator",
+		Force:        func() *bool { b := true; return &b }(),
+	}
+	if err := k8sClient.Patch(ctx, obj, client.Apply, patchOptions); err != nil {
+		return fmt.Errorf("failed to apply resource %s/%s: %w", obj.GetKind(), obj.GetName(), err)
+	}
+	logger.Info("Applied platform source resource", "kind", obj.GetKind(), "name", obj.GetName())

348-356: Potential incorrect handling of OCI tags vs digests.

The code prepends sha256: to any ref that doesn't already have it. If the ref is a tag (e.g., v1.0.0), this produces an invalid reference like sha256:v1.0.0. The parsing logic at lines 289-293 can return non-digest refs.

🔎 Suggested fix
 	if digest != "" {
-		// Ensure digest starts with sha256:
-		if !strings.HasPrefix(digest, "sha256:") {
-			digest = "sha256:" + digest
+		// Check if it looks like a digest (sha256:... or 64-char hex)
+		if strings.HasPrefix(digest, "sha256:") {
+			spec["ref"] = map[string]interface{}{
+				"digest": digest,
+			}
+		} else if len(digest) == 64 && isHexString(digest) {
+			spec["ref"] = map[string]interface{}{
+				"digest": "sha256:" + digest,
+			}
+		} else {
+			// Treat as tag
+			spec["ref"] = map[string]interface{}{
+				"tag": digest,
+			}
 		}
-		spec["ref"] = map[string]interface{}{
-			"digest": digest,
-		}
 	}

Add helper function:

func isHexString(s string) bool {
	for _, c := range s {
		if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {
			return false
		}
	}
	return true
}
internal/fluxinstall/install.go (1)

181-183: time.Sleep for CRD registration is unreliable.

A 2-second sleep may not be sufficient on slow or busy clusters. Consider polling the API server for CRD Established condition instead.

🔎 Suggested improvement
// waitForCRDs polls until CRDs are established or timeout
func waitForCRDs(ctx context.Context, k8sClient client.Client, crds []*unstructured.Unstructured) error {
	for _, crd := range crds {
		if crd.GetKind() != "CustomResourceDefinition" {
			continue
		}
		name := crd.GetName()
		for {
			select {
			case <-ctx.Done():
				return ctx.Err()
			default:
			}
			existing := &unstructured.Unstructured{}
			existing.SetGroupVersionKind(crd.GroupVersionKind())
			if err := k8sClient.Get(ctx, client.ObjectKey{Name: name}, existing); err != nil {
				time.Sleep(500 * time.Millisecond)
				continue
			}
			conditions, found, _ := unstructured.NestedSlice(existing.Object, "status", "conditions")
			if found {
				for _, c := range conditions {
					cond, _ := c.(map[string]interface{})
					if cond["type"] == "Established" && cond["status"] == "True" {
						goto nextCRD
					}
				}
			}
			time.Sleep(500 * time.Millisecond)
		}
	nextCRD:
	}
	return nil
}
internal/operator/packagesource_reconciler.go (2)

90-103: Potential library name collision across variants.

When building the library map, if two variants define libraries with the same name but different paths, the last one silently overwrites the first. This could cause unexpected behavior.

🔎 Suggested fix - log collision warning
 	for _, variant := range packageSource.Spec.Variants {
 		for _, lib := range variant.Libraries {
 			libName := lib.Name
 			if libName == "" {
 				libName = r.getPackageNameFromPath(lib.Path)
 			}
 			if libName != "" {
+				if existing, ok := libraryMap[libName]; ok && existing.Path != lib.Path {
+					logger.Info("library name collision detected, overwriting",
+						"name", libName,
+						"existingPath", existing.Path,
+						"newPath", lib.Path,
+						"variant", variant.Name)
+				}
 				libraryMap[libName] = lib
 			}
 		}
 	}

309-364: Consider using server-side apply instead of Get/Create/Update.

The current pattern is not atomic and the label/annotation merging logic could be simplified by using server-side apply. This would also be consistent with internal/fluxinstall/install.go.

internal/operator/package_reconciler.go (3)

294-343: Consider using server-side apply and extracting shared utility.

The Get/Create/Update pattern is duplicated here and in packagesource_reconciler.go. Consider:

  1. Using server-side apply for atomicity
  2. Extracting a shared utility function to avoid duplication

763-803: Consider using server-side apply for namespace management.

This is another instance of the Get/Create/Update pattern that could benefit from server-side apply.


710-714: Inconsistent handling of empty namespace.

In reconcileNamespaces, an empty Install.Namespace returns an error. However, in the HelmRelease creation loop (lines 162-165), an empty namespace defaults to cozy-system. This inconsistency could cause namespace reconciliation to fail for components that rely on the default.

🔎 Suggested fix - apply same defaulting
 		// Namespace must be set
 		namespace := component.Install.Namespace
 		if namespace == "" {
-			return fmt.Errorf("component %s has empty namespace in Install section", component.Name)
+			namespace = "cozy-system"
 		}
🧹 Nitpick comments (5)
packages/core/flux-aio/Makefile (1)

15-15: Add .PHONY declaration for the new targets.

The static analysis tool correctly flagged that update, update-old, and update-new should be declared as .PHONY targets since they don't produce files with those names.

🔎 Suggested fix
+.PHONY: show apply diff update update-old update-new
+
 NAME=flux-aio
 NAMESPACE=cozy-$(NAME)
internal/fluxinstall/install.go (2)

234-312: Error handling captures first error but continues.

The function design allows partial success - if env injection fails for one workload, others still get processed. This is intentional per the docstring, but consider logging each failure rather than just returning the first error to aid debugging.

🔎 Consider logging each error
+import "sigs.k8s.io/controller-runtime/pkg/log"
+
 func injectKubernetesServiceEnv(objects []*unstructured.Unstructured) error {
+	logger := log.Log.WithName("fluxinstall")
 	// ...
 		if err != nil {
 			if firstErr == nil {
 				firstErr = fmt.Errorf("failed to get spec for %s/%s: %w", kind, obj.GetName(), err)
 			}
+			logger.V(1).Info("failed to get spec", "kind", kind, "name", obj.GetName(), "error", err)
 			continue
 		}

59-73: Fallback YAML file selection could be made more explicit.

If fluxcd.yaml doesn't exist, the code picks the first .yaml file found. Consider logging a warning when using the fallback, or being more specific about expected filenames.

🔎 Suggested improvement
 	if _, err := os.Stat(manifestPath); err != nil {
 		// Try to find any YAML file if fluxcd.yaml doesn't exist
 		entries, err := os.ReadDir(manifestsDir)
 		if err != nil {
 			return fmt.Errorf("failed to read manifests directory: %w", err)
 		}
+		logger.Info("fluxcd.yaml not found, searching for alternative manifest files")
 		for _, entry := range entries {
 			if strings.HasSuffix(entry.Name(), ".yaml") {
 				manifestPath = filepath.Join(manifestsDir, entry.Name())
+				logger.Info("using alternative manifest file", "file", entry.Name())
 				break
 			}
 		}
 	}
internal/operator/package_reconciler.go (2)

586-684: Listing all Packages on every reconcile could be expensive.

The updateDependentPackagesDependencies function lists all Packages in the cluster. For clusters with many packages, this could become a performance bottleneck.

Consider using a controller-runtime index to efficiently find dependent packages:

// In SetupWithManager, add an index:
if err := mgr.GetFieldIndexer().IndexField(ctx, &cozyv1alpha1.Package{}, "spec.dependencies", func(obj client.Object) []string {
    pkg := obj.(*cozyv1alpha1.Package)
    // Return list of dependency package names
    // (requires resolving variant dependencies)
    return dependencyNames
}); err != nil {
    return err
}

// Then use:
r.List(ctx, packageList, client.MatchingFields{"spec.dependencies": packageName})

891-949: Watch handler lists all Packages, which may impact performance.

The watch handler for Package changes lists all Packages to find dependents. This duplicates the inefficient pattern from updateDependentPackagesDependencies. For better performance, consider:

  1. Using an index (as suggested above)
  2. Or caching dependency relationships
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9ef1d32 and 2ae2670.

📒 Files selected for processing (8)
  • api/v1alpha1/package_types.go
  • cmd/cozystack-operator/main.go
  • internal/fluxinstall/install.go
  • internal/fluxinstall/manifests.embed.go
  • internal/fluxinstall/manifests/fluxcd.yaml
  • internal/operator/package_reconciler.go
  • internal/operator/packagesource_reconciler.go
  • packages/core/flux-aio/Makefile
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/fluxinstall/manifests.embed.go
🧰 Additional context used
📓 Path-based instructions (5)
api/**

📄 CodeRabbit inference engine (AGENTS.md)

Define Kubernetes CRDs in the api directory following kubebuilder conventions

Files:

  • api/v1alpha1/package_types.go
**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

Do NOT manually edit vendor/ directory or auto-generated files (zz_generated.*.go)

Files:

  • api/v1alpha1/package_types.go
  • cmd/cozystack-operator/main.go
  • internal/fluxinstall/install.go
  • internal/operator/packagesource_reconciler.go
  • internal/operator/package_reconciler.go
cmd/**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

Follow controller-runtime patterns and kubebuilder style for Go code in cmd directory

Files:

  • cmd/cozystack-operator/main.go
internal/**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

Follow controller-runtime patterns and kubebuilder style for Go code in internal directory

Files:

  • internal/fluxinstall/install.go
  • internal/operator/packagesource_reconciler.go
  • internal/operator/package_reconciler.go
packages/core/**

📄 CodeRabbit inference engine (AGENTS.md)

Use Helm Chart umbrella pattern with vendored upstream charts in charts/ directory

Files:

  • packages/core/flux-aio/Makefile
🧠 Learnings (3)
📚 Learning: 2025-12-25T09:45:26.511Z
Learnt from: CR
Repo: cozystack/cozystack PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-25T09:45:26.511Z
Learning: Applies to cmd/**/*.go : Follow controller-runtime patterns and kubebuilder style for Go code in cmd directory

Applied to files:

  • cmd/cozystack-operator/main.go
📚 Learning: 2025-12-25T09:45:26.511Z
Learnt from: CR
Repo: cozystack/cozystack PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-25T09:45:26.511Z
Learning: Applies to internal/**/*.go : Follow controller-runtime patterns and kubebuilder style for Go code in internal directory

Applied to files:

  • cmd/cozystack-operator/main.go
  • internal/operator/packagesource_reconciler.go
📚 Learning: 2025-12-25T09:45:26.511Z
Learnt from: CR
Repo: cozystack/cozystack PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-25T09:45:26.511Z
Learning: Applies to pkg/**/*.go : Follow controller-runtime patterns and kubebuilder style for Go code in pkg directory

Applied to files:

  • cmd/cozystack-operator/main.go
  • internal/operator/packagesource_reconciler.go
  • internal/operator/package_reconciler.go
🪛 checkmake (0.2.2)
packages/core/flux-aio/Makefile

[warning] 15-15: Target "update" should be declared PHONY.

(phonydeclared)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build
🔇 Additional comments (8)
api/v1alpha1/package_types.go (1)

89-100: LGTM! Clean API extension for dependency tracking.

The new DependencyStatus type and Dependencies field in PackageStatus follow kubebuilder conventions with appropriate json tags and documentation.

cmd/cozystack-operator/main.go (2)

136-147: LGTM! Flux installation with proper timeout handling.

The 5-minute timeout is reasonable for applying Flux manifests, and the error handling properly logs and exits on failure.


163-179: LGTM! Standard controller-runtime reconciler setup.

The reconciler initialization follows kubebuilder conventions correctly, passing the client and scheme from the manager.

internal/fluxinstall/install.go (1)

196-211: LGTM! Correct server-side apply implementation.

The applyObjects function properly uses server-side apply with a field manager and force ownership, which is the recommended approach for operator-managed resources.

internal/operator/packagesource_reconciler.go (1)

441-467: LGTM! Watch setup follows controller-runtime patterns.

The watch handler correctly enqueues reconcile requests for the owning PackageSource when ArtifactGenerator changes. Consider using Owns(&sourcewatcherv1beta1.ArtifactGenerator{}) as a simpler alternative if cross-namespace ownership isn't needed.

internal/operator/package_reconciler.go (2)

52-137: LGTM! Core reconciliation logic follows controller-runtime patterns.

The main reconcile flow properly handles:

  • Resource not found (returns without error for cleanup by ownerReference)
  • PackageSource lookup with status condition update
  • Variant resolution with appropriate error conditions
  • Dependency validation before creating HelmReleases

805-862: LGTM! Orphan cleanup logic is well-implemented.

The cleanup function correctly:

  • Builds a set of desired HelmReleases including disabled-component logic
  • Uses label selector for efficient filtering
  • Handles delete errors gracefully (ignoring NotFound)
packages/core/flux-aio/Makefile (1)

27-34: No action needed. The internal/fluxinstall/manifests directory already exists in the repository with the fluxcd.yaml file present, so the update-new target will execute successfully without requiring a mkdir -p command.

Likely an incorrect or invalid review comment.

@kvaps kvaps force-pushed the cozystack-operator-reconcilers branch 2 times, most recently from ef72cd2 to af63ae0 Compare December 25, 2025 12:52
Copy link
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: 0

♻️ Duplicate comments (9)
cmd/cozystack-operator/main.go (3)

76-77: cozystackVersion flag is parsed but never used.

Consider logging it during startup for debugging/observability, or remove if not needed.

🔎 Suggested fix
 	setupLog.Info("Starting controller manager")
+	setupLog.Info("Cozystack version", "version", cozystackVersion)

242-260: Consider using server-side apply for resource creation.

The Get/Create/Update pattern is not atomic. While this is a startup-time single-shot operation with lower race condition risk, using client.Patch with client.Apply would be more robust and consistent with modern controller patterns.


348-356: Potential incorrect handling of OCI tags vs digests persists.

If parsePlatformSource extracts a tag (e.g., v1.0.0 from oci://registry/repo@v1.0.0), this code will prepend sha256: producing an invalid reference sha256:v1.0.0.

🔎 Suggested fix

Distinguish between tags and digests based on format:

 	if digest != "" {
-		// Ensure digest starts with sha256:
-		if !strings.HasPrefix(digest, "sha256:") {
-			digest = "sha256:" + digest
+		// Only treat as digest if it looks like a sha256 digest
+		if strings.HasPrefix(digest, "sha256:") || isHexDigest(digest) {
+			if !strings.HasPrefix(digest, "sha256:") {
+				digest = "sha256:" + digest
+			}
+			spec["ref"] = map[string]interface{}{
+				"digest": digest,
+			}
+		} else {
+			// Treat as tag
+			spec["ref"] = map[string]interface{}{
+				"tag": digest,
+			}
 		}
-		spec["ref"] = map[string]interface{}{
-			"digest": digest,
-		}
 	}

Add a helper to check for hex digest format:

func isHexDigest(s string) bool {
	if len(s) != 64 {
		return false
	}
	for _, c := range s {
		if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) {
			return false
		}
	}
	return true
}
internal/operator/packagesource_reconciler.go (3)

90-103: Potential library name collision across variants.

If two variants define libraries with the same name but different paths, the last one silently overwrites the first. Consider logging a warning when a collision is detected.

🔎 Suggested fix
 	for _, variant := range packageSource.Spec.Variants {
 		for _, lib := range variant.Libraries {
 			libName := lib.Name
 			if libName == "" {
 				libName = r.getPackageNameFromPath(lib.Path)
 			}
 			if libName != "" {
+				if existing, ok := libraryMap[libName]; ok && existing.Path != lib.Path {
+					logger.Info("library name collision, overwriting",
+						"name", libName,
+						"existingPath", existing.Path,
+						"newPath", lib.Path)
+				}
 				libraryMap[libName] = lib
 			}
 		}
 	}

309-364: Consider using server-side apply for resource creation.

The createOrUpdate function uses Get/Create/Update which is not atomic and can lead to race conditions. Server-side apply with client.Patch(ctx, obj, client.Apply, ...) is more robust and declarative.


211-220: Owner reference may have empty APIVersion/Kind.

When packageSource is fetched from the API server, TypeMeta (APIVersion/Kind) is often not populated. Using packageSource.APIVersion and packageSource.Kind directly may result in empty values in the owner reference.

🔎 Suggested fix using GVK from scheme
+	gvk, err := apiutil.GVKForObject(packageSource, r.Scheme)
+	if err != nil {
+		return fmt.Errorf("failed to get GVK for PackageSource: %w", err)
+	}
 	ag.OwnerReferences = []metav1.OwnerReference{
 		{
-			APIVersion: packageSource.APIVersion,
-			Kind:       packageSource.Kind,
+			APIVersion: gvk.GroupVersion().String(),
+			Kind:       gvk.Kind,
 			Name:       packageSource.Name,
 			UID:        packageSource.UID,
 			Controller: func() *bool { b := true; return &b }(),
 		},
 	}

Add import:

import "sigs.k8s.io/controller-runtime/pkg/client/apiutil"
internal/operator/package_reconciler.go (3)

294-343: Consider using server-side apply for HelmRelease management.

The createOrUpdateHelmRelease function uses Get/Create/Update which is not atomic. Server-side apply with client.Patch(ctx, hr, client.Apply, ...) would be more robust.


207-216: Owner reference may have empty APIVersion/Kind.

Same issue as in packagesource_reconciler.go. When pkg is fetched from the API server, TypeMeta is often not populated. Use apiutil.GVKForObject to reliably get the GVK.

🔎 Suggested fix
+	gvk, err := apiutil.GVKForObject(pkg, r.Scheme)
+	if err != nil {
+		return ctrl.Result{}, fmt.Errorf("failed to get GVK for Package: %w", err)
+	}
 	hr.OwnerReferences = []metav1.OwnerReference{
 		{
-			APIVersion: pkg.APIVersion,
-			Kind:       pkg.Kind,
+			APIVersion: gvk.GroupVersion().String(),
+			Kind:       gvk.Kind,
 			Name:       pkg.Name,
 			UID:        pkg.UID,
 			Controller: func() *bool { b := true; return &b }(),
 		},
 	}

Add import:

import "sigs.k8s.io/controller-runtime/pkg/client/apiutil"

710-714: Inconsistent handling of empty namespace.

In reconcileNamespaces, an empty Install.Namespace causes an error. However, in the HelmRelease creation loop (lines 162-165), an empty namespace defaults to cozy-system. This inconsistency could cause namespace reconciliation to fail for components that rely on the default.

🔎 Suggested fix

Apply the same defaulting logic:

 		// Namespace must be set
 		namespace := component.Install.Namespace
 		if namespace == "" {
-			return fmt.Errorf("component %s has empty namespace in Install section", component.Name)
+			namespace = "cozy-system"
 		}
🧹 Nitpick comments (3)
cmd/cozystack-operator/main.go (1)

373-379: Git ref always treated as branch.

The ref is always set as branch, but it could be a tag (e.g., git://repo@v1.0.0). While Flux GitRepository can often resolve branches by name even if they're tags, consider supporting explicit tag refs for correctness.

internal/operator/package_reconciler.go (2)

891-949: Watch handler lists all Packages for dependency lookup.

The handler lists all Package resources to find dependents, which can be inefficient in clusters with many Packages. Consider using a controller-runtime index for more efficient lookups.

🔎 Suggested approach

Add an index on Package dependencies:

// In SetupWithManager:
if err := mgr.GetFieldIndexer().IndexField(ctx, &cozyv1alpha1.Package{}, "spec.dependsOn", func(o client.Object) []string {
    pkg := o.(*cozyv1alpha1.Package)
    // Get variant dependencies from PackageSource
    // Return list of dependency package names
    return getDependencies(pkg)
}); err != nil {
    return err
}

// Then use client.MatchingFields in the handler instead of iterating all Packages

Note: This requires resolving the PackageSource at index time, which may not be straightforward. An alternative is to accept the current approach for initial implementation and optimize later if performance becomes an issue.


586-684: updateDependentPackagesDependencies duplicates watch handler logic.

This function lists all Packages and updates their dependency status, but the watch handler on lines 891-949 already enqueues dependent Packages when a Package changes. The status update here may be redundant, and updating pkg.Status.Dependencies (line 672-675) will trigger another reconcile via the watch.

Consider relying solely on the watch-based mechanism and letting each Package update its own dependencies status during its reconcile, rather than having the dependency push updates.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2ae2670 and af63ae0.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (7)
  • api/v1alpha1/package_types.go
  • api/v1alpha1/zz_generated.deepcopy.go
  • cmd/cozystack-operator/main.go
  • go.mod
  • internal/operator/package_reconciler.go
  • internal/operator/packagesource_reconciler.go
  • packages/core/installer/definitions/cozystack.io_packages.yaml
🧰 Additional context used
📓 Path-based instructions (7)
go.mod

📄 CodeRabbit inference engine (AGENTS.md)

Do NOT modify go.mod manually; use go get command instead

Files:

  • go.mod
internal/**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

Follow controller-runtime patterns and kubebuilder style for Go code in internal directory

Files:

  • internal/operator/packagesource_reconciler.go
  • internal/operator/package_reconciler.go
**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

Do NOT manually edit vendor/ directory or auto-generated files (zz_generated.*.go)

Files:

  • internal/operator/packagesource_reconciler.go
  • api/v1alpha1/package_types.go
  • api/v1alpha1/zz_generated.deepcopy.go
  • internal/operator/package_reconciler.go
  • cmd/cozystack-operator/main.go
api/**

📄 CodeRabbit inference engine (AGENTS.md)

Define Kubernetes CRDs in the api directory following kubebuilder conventions

Files:

  • api/v1alpha1/package_types.go
  • api/v1alpha1/zz_generated.deepcopy.go
packages/core/**

📄 CodeRabbit inference engine (AGENTS.md)

Use Helm Chart umbrella pattern with vendored upstream charts in charts/ directory

Files:

  • packages/core/installer/definitions/cozystack.io_packages.yaml
**/*.yaml

📄 CodeRabbit inference engine (AGENTS.md)

Do NOT directly edit upstream Helm charts in vendored charts directory; use proper chart vendoring mechanisms

Files:

  • packages/core/installer/definitions/cozystack.io_packages.yaml
cmd/**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

Follow controller-runtime patterns and kubebuilder style for Go code in cmd directory

Files:

  • cmd/cozystack-operator/main.go
🧠 Learnings (3)
📚 Learning: 2025-12-25T09:45:26.511Z
Learnt from: CR
Repo: cozystack/cozystack PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-25T09:45:26.511Z
Learning: Applies to pkg/**/*.go : Follow controller-runtime patterns and kubebuilder style for Go code in pkg directory

Applied to files:

  • go.mod
  • internal/operator/packagesource_reconciler.go
  • internal/operator/package_reconciler.go
  • cmd/cozystack-operator/main.go
📚 Learning: 2025-12-25T09:45:26.511Z
Learnt from: CR
Repo: cozystack/cozystack PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-25T09:45:26.511Z
Learning: Applies to cmd/**/*.go : Follow controller-runtime patterns and kubebuilder style for Go code in cmd directory

Applied to files:

  • cmd/cozystack-operator/main.go
📚 Learning: 2025-12-25T09:45:26.511Z
Learnt from: CR
Repo: cozystack/cozystack PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-25T09:45:26.511Z
Learning: Applies to internal/**/*.go : Follow controller-runtime patterns and kubebuilder style for Go code in internal directory

Applied to files:

  • cmd/cozystack-operator/main.go
🧬 Code graph analysis (2)
api/v1alpha1/zz_generated.deepcopy.go (1)
api/v1alpha1/package_types.go (1)
  • DependencyStatus (97-100)
internal/operator/package_reconciler.go (2)
api/v1alpha1/package_types.go (2)
  • Package (32-38)
  • DependencyStatus (97-100)
api/v1alpha1/packagesource_types.go (3)
  • PackageSource (31-37)
  • Variant (65-82)
  • Component (138-159)
🔇 Additional comments (7)
go.mod (1)

9-10: LGTM on new Flux dependencies.

The addition of source-controller/api and source-watcher/api/v2 aligns with the new operator functionality for managing Flux resources. The file header indicates this is generated, which aligns with the coding guideline to use go get rather than manual edits.

packages/core/installer/definitions/cozystack.io_packages.yaml (1)

151-165: LGTM on the dependencies status field.

The CRD schema correctly defines the new dependencies map with DependencyStatus containing a required ready boolean. This enables per-dependency readiness tracking in the Package status.

api/v1alpha1/package_types.go (1)

89-100: LGTM on the new dependency tracking types.

The Dependencies map with DependencyStatus follows kubebuilder conventions. Using a simple bool without pointer for Ready is appropriate since the field is required and doesn't need omitempty semantics.

api/v1alpha1/zz_generated.deepcopy.go (1)

311-324: Auto-generated deepcopy code looks correct.

This file is generated by controller-gen. The deepcopy logic for DependencyStatus and the map copying for PackageStatus.Dependencies follows the expected patterns.

Also applies to: 585-591

internal/operator/packagesource_reconciler.go (1)

441-467: SetupWithManager watch handler looks good.

The watch on ArtifactGenerator correctly enqueues the owning PackageSource by checking owner references. This follows controller-runtime patterns.

internal/operator/package_reconciler.go (2)

51-292: Reconcile flow is well-structured.

The reconciliation follows a logical order: namespace creation → dependency status update → dependency check → HelmRelease creation → cleanup → status update. The early return when dependencies aren't ready (lines 122-137) prevents creating HelmReleases prematurely.


805-862: Orphan cleanup logic is correct.

The cleanup correctly builds a set of desired releases, lists by label selector (efficient), and deletes only orphaned releases. Error handling properly ignores NotFound errors.

@kvaps kvaps force-pushed the cozystack-operator-reconcilers branch from af63ae0 to 21af008 Compare December 25, 2025 13:16
Copy link
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: 0

♻️ Duplicate comments (4)
cmd/cozystack-operator/main.go (2)

76-76: Unused flag: cozystackVersion is never consumed.

The cozystackVersion flag is parsed but never used in the operator. Either log it during startup or remove the flag definition.

🔎 Suggested fix

Add to startup logging:

 	setupLog.Info("Starting controller manager")
+	setupLog.Info("Cozystack version", "version", cozystackVersion)

242-260: Prefer server-side apply over Get/Create/Update.

The current Get→Create/Update pattern is not atomic and can lead to race conditions. Use server-side apply (client.Patch with client.Apply) as done in internal/operator/packagesource_reconciler.go:313-327 for consistency and robustness.

🔎 Proposed fix
-	err = k8sClient.Get(ctx, key, existing)
-	if err != nil {
-		if client.IgnoreNotFound(err) == nil {
-			// Resource doesn't exist, create it
-			if err := k8sClient.Create(ctx, obj); err != nil {
-				return fmt.Errorf("failed to create resource %s/%s: %w", obj.GetKind(), obj.GetName(), err)
-			}
-			logger.Info("Created platform source resource", "kind", obj.GetKind(), "name", obj.GetName())
-		} else {
-			return fmt.Errorf("failed to check if resource exists: %w", err)
-		}
-	} else {
-		// Resource exists, update it
-		obj.SetResourceVersion(existing.GetResourceVersion())
-		if err := k8sClient.Update(ctx, obj); err != nil {
-			return fmt.Errorf("failed to update resource %s/%s: %w", obj.GetKind(), obj.GetName(), err)
-		}
-		logger.Info("Updated platform source resource", "kind", obj.GetKind(), "name", obj.GetName())
-	}
-
-	return nil
+	// Use server-side apply for atomic create-or-update
+	if err := k8sClient.Patch(ctx, obj, client.Apply, client.FieldOwner("cozystack-operator-platform-source")); err != nil {
+		return fmt.Errorf("failed to apply platform source resource %s/%s: %w", obj.GetKind(), obj.GetName(), err)
+	}
+	logger.Info("Applied platform source resource", "kind", obj.GetKind(), "name", obj.GetName())
+	return nil
internal/operator/packagesource_reconciler.go (1)

98-113: Potential library name collision across variants.

The libraryMap is built by iterating through all variants. If two variants define libraries with the same name but different paths, the last one processed will overwrite previous entries, potentially causing incorrect library paths to be used.

🔎 Suggested fix

Consider one of these approaches:

Option 1: Detect and warn about collisions

 		for _, lib := range variant.Libraries {
 			libName := lib.Name
 			if libName == "" {
 				libName = r.getPackageNameFromPath(lib.Path)
 			}
 			if libName != "" {
+				if existing, ok := libraryMap[libName]; ok && existing.Path != lib.Path {
+					logger.Info("library name collision detected, last variant wins",
+						"name", libName,
+						"existingPath", existing.Path,
+						"newPath", lib.Path,
+						"variant", variant.Name)
+				}
 				libraryMap[libName] = lib
 			}
 		}

Option 2: Namespace by variant

-		libraryMap := make(map[string]cozyv1alpha1.Library)
+		libraryMap := make(map[string]cozyv1alpha1.Library)
 		for _, lib := range variant.Libraries {
 			libName := lib.Name
 			if libName == "" {
 				libName = r.getPackageNameFromPath(lib.Path)
 			}
 			if libName != "" {
-				libraryMap[libName] = lib
+				// Namespace by variant to avoid collisions
+				libraryMap[variant.Name+":"+libName] = lib
 			}
 		}

Note: If using option 2, update the lookup on line 144 accordingly.

internal/operator/package_reconciler.go (1)

319-368: Prefer server-side apply over Get/Create/Update.

The current Get→Create/Update pattern is not atomic and can lead to race conditions. Server-side apply automatically handles label/annotation merging and is more robust. This would also align with createOrUpdateNamespace (line 795), which already uses server-side apply.

🔎 Proposed fix
 func (r *PackageReconciler) createOrUpdateHelmRelease(ctx context.Context, hr *helmv2.HelmRelease) error {
-	existing := &helmv2.HelmRelease{}
-	key := types.NamespacedName{
-		Name:      hr.Name,
-		Namespace: hr.Namespace,
-	}
-
-	err := r.Get(ctx, key, existing)
-	if apierrors.IsNotFound(err) {
-		return r.Create(ctx, hr)
-	} else if err != nil {
-		return err
-	}
-
-	// Preserve resource version
-	hr.SetResourceVersion(existing.GetResourceVersion())
-
-	// Merge labels
-	labels := hr.GetLabels()
-	if labels == nil {
-		labels = make(map[string]string)
-	}
-	for k, v := range existing.GetLabels() {
-		if _, ok := labels[k]; !ok {
-			labels[k] = v
-		}
-	}
-	hr.SetLabels(labels)
-
-	// Merge annotations
-	annotations := hr.GetAnnotations()
-	if annotations == nil {
-		annotations = make(map[string]string)
-	}
-	for k, v := range existing.GetAnnotations() {
-		if _, ok := annotations[k]; !ok {
-			annotations[k] = v
-		}
-	}
-	hr.SetAnnotations(annotations)
-
-	// Update Spec
-	existing.Spec = hr.Spec
-	existing.SetLabels(hr.GetLabels())
-	existing.SetAnnotations(hr.GetAnnotations())
-	existing.SetOwnerReferences(hr.GetOwnerReferences())
-
-	return r.Update(ctx, existing)
+	// Ensure TypeMeta is set for server-side apply
+	gvk, err := apiutil.GVKForObject(hr, r.Scheme)
+	if err != nil {
+		return fmt.Errorf("failed to get GVK for HelmRelease: %w", err)
+	}
+	hr.SetGroupVersionKind(gvk)
+
+	// Use server-side apply with field manager
+	return r.Patch(ctx, hr, client.Apply, client.FieldOwner("cozystack-package-controller"))
 }
🧹 Nitpick comments (2)
cmd/cozystack-operator/main.go (1)

348-356: Validate digest format before prepending sha256:.

If the ref is a tag (e.g., @v1.0.0) rather than a digest, prepending sha256: produces an invalid digest like sha256:v1.0.0. Consider validating that the ref is actually a SHA256 digest before prepending the prefix, or treating non-digest refs as tags.

🔎 Proposed fix
 	if digest != "" {
-		// Ensure digest starts with sha256:
-		if !strings.HasPrefix(digest, "sha256:") {
-			digest = "sha256:" + digest
+		// Only treat as digest if it looks like a SHA256 hash
+		if strings.HasPrefix(digest, "sha256:") {
+			spec["ref"] = map[string]interface{}{
+				"digest": digest,
+			}
+		} else if len(digest) == 64 {
+			// Assume bare 64-char hex string is a SHA256 digest
+			spec["ref"] = map[string]interface{}{
+				"digest": "sha256:" + digest,
+			}
+		} else {
+			// Treat as tag
+			spec["ref"] = map[string]interface{}{
+				"tag": digest,
+			}
 		}
-		spec["ref"] = map[string]interface{}{
-			"digest": digest,
-		}
 	}
internal/operator/package_reconciler.go (1)

858-935: LGTM!

The controller setup with watches correctly implements dependency-driven reconciliation. The watch handler that lists all Packages to find dependents is acceptable for moderate cluster sizes.

Optional: Consider indexing for efficiency.

If the cluster has a large number of Packages, consider adding a controller-runtime index on dependencies to avoid listing all Packages. However, this optimization is not critical for most deployments.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between af63ae0 and 21af008.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (7)
  • api/v1alpha1/package_types.go
  • api/v1alpha1/zz_generated.deepcopy.go
  • cmd/cozystack-operator/main.go
  • go.mod
  • internal/operator/package_reconciler.go
  • internal/operator/packagesource_reconciler.go
  • packages/core/installer/definitions/cozystack.io_packages.yaml
🚧 Files skipped from review as they are similar to previous changes (3)
  • api/v1alpha1/package_types.go
  • packages/core/installer/definitions/cozystack.io_packages.yaml
  • go.mod
🧰 Additional context used
📓 Path-based instructions (4)
cmd/**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

Follow controller-runtime patterns and kubebuilder style for Go code in cmd directory

Files:

  • cmd/cozystack-operator/main.go
**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

Do NOT manually edit vendor/ directory or auto-generated files (zz_generated.*.go)

Files:

  • cmd/cozystack-operator/main.go
  • internal/operator/packagesource_reconciler.go
  • api/v1alpha1/zz_generated.deepcopy.go
  • internal/operator/package_reconciler.go
internal/**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

Follow controller-runtime patterns and kubebuilder style for Go code in internal directory

Files:

  • internal/operator/packagesource_reconciler.go
  • internal/operator/package_reconciler.go
api/**

📄 CodeRabbit inference engine (AGENTS.md)

Define Kubernetes CRDs in the api directory following kubebuilder conventions

Files:

  • api/v1alpha1/zz_generated.deepcopy.go
🧠 Learnings (3)
📚 Learning: 2025-12-25T09:45:26.511Z
Learnt from: CR
Repo: cozystack/cozystack PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-25T09:45:26.511Z
Learning: Applies to cmd/**/*.go : Follow controller-runtime patterns and kubebuilder style for Go code in cmd directory

Applied to files:

  • cmd/cozystack-operator/main.go
📚 Learning: 2025-12-25T09:45:26.511Z
Learnt from: CR
Repo: cozystack/cozystack PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-25T09:45:26.511Z
Learning: Applies to internal/**/*.go : Follow controller-runtime patterns and kubebuilder style for Go code in internal directory

Applied to files:

  • cmd/cozystack-operator/main.go
📚 Learning: 2025-12-25T09:45:26.511Z
Learnt from: CR
Repo: cozystack/cozystack PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-25T09:45:26.511Z
Learning: Applies to pkg/**/*.go : Follow controller-runtime patterns and kubebuilder style for Go code in pkg directory

Applied to files:

  • cmd/cozystack-operator/main.go
  • internal/operator/package_reconciler.go
🧬 Code graph analysis (2)
cmd/cozystack-operator/main.go (4)
internal/fluxinstall/install.go (1)
  • Install (39-106)
internal/fluxinstall/manifests.embed.go (1)
  • WriteEmbeddedManifests (31-50)
internal/operator/packagesource_reconciler.go (1)
  • PackageSourceReconciler (40-43)
internal/operator/package_reconciler.go (1)
  • PackageReconciler (41-44)
api/v1alpha1/zz_generated.deepcopy.go (1)
api/v1alpha1/package_types.go (1)
  • DependencyStatus (97-100)
🔇 Additional comments (14)
cmd/cozystack-operator/main.go (5)

136-147: LGTM!

The Flux installation logic is well-structured with appropriate timeout and error handling.


149-161: LGTM!

The platform source resource installation is properly structured with timeout and error handling.


163-179: LGTM!

The reconciler setup follows controller-runtime patterns correctly.


265-333: LGTM!

The URL parsing logic correctly handles both OCI and Git sources, with proper use of LastIndex to handle @ symbols in paths.


365-386: LGTM!

The GitRepository generation is straightforward and correctly sets the branch reference.

internal/operator/packagesource_reconciler.go (4)

50-75: LGTM!

The main Reconcile function follows controller-runtime patterns correctly with appropriate error handling.


313-327: LGTM!

The createOrUpdate function correctly uses server-side apply, which is atomic and avoids race conditions. This is the recommended pattern for operators.


329-402: LGTM!

The status update logic properly propagates the Ready condition from ArtifactGenerator to PackageSource with appropriate error handling.


404-430: LGTM!

The controller setup with watches correctly uses owner references to enqueue reconciles for the owning PackageSource.

internal/operator/package_reconciler.go (5)

52-317: LGTM!

The main Reconcile function is comprehensive and well-structured, following controller-runtime patterns with proper error handling and status updates.


411-523: LGTM!

The buildDependsOn function correctly builds dependencies from both component-level and variant-level sources with appropriate validation.


525-599: LGTM!

The dependency status update logic properly maintains the status map, cleaning up old entries and updating current dependencies.


709-784: LGTM!

The namespace reconciliation logic is consistent with the main Reconcile function—both properly validate that namespace is not empty.


786-796: LGTM!

The namespace creation correctly uses server-side apply for atomic create-or-update operations.

@kvaps kvaps force-pushed the cozystack-operator-reconcilers branch from 21af008 to a3373dc Compare December 25, 2025 13:37
Copy link
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: 1

♻️ Duplicate comments (5)
cmd/cozystack-operator/main.go (3)

76-76: Unused cozystackVersion flag.

The cozystackVersion flag is parsed but never used. Either log it during startup or remove the flag definition to avoid dead code.

Also applies to: 90-91


242-260: Use server-side apply instead of Get/Create/Update.

The Get followed by Create or Update pattern is not atomic and can lead to race conditions. Use client.Patch with client.Apply for a more robust approach, similar to the pattern already adopted in packagesource_reconciler.go line 329.


348-356: Validate digest format before prepending sha256:.

Blindly prepending sha256: to any non-empty digest breaks valid tag references. For example, oci://registry/repo@v1.0.0 would produce an invalid sha256:v1.0.0 digest.

🔎 Suggested validation logic
 	if digest != "" {
-		// Ensure digest starts with sha256:
-		if !strings.HasPrefix(digest, "sha256:") {
-			digest = "sha256:" + digest
+		// Only treat as digest if it looks like a sha256 digest
+		if strings.HasPrefix(digest, "sha256:") {
+			spec["ref"] = map[string]interface{}{
+				"digest": digest,
+			}
+		} else if len(digest) == 64 {
+			// Bare 64-char hex string, prepend sha256:
+			spec["ref"] = map[string]interface{}{
+				"digest": "sha256:" + digest,
+			}
+		} else {
+			// Treat as tag
+			spec["ref"] = map[string]interface{}{
+				"tag": digest,
+			}
 		}
-		spec["ref"] = map[string]interface{}{
-			"digest": digest,
-		}
 	}
internal/operator/package_reconciler.go (2)

886-934: Consider indexing for improved watch handler performance.

The watch handler lists all Packages to find dependents (line 894-896), which works but may not scale well with hundreds of Packages. Consider adding a controller-runtime index on the dependency field for more efficient lookups in large deployments.


320-369: Refactor to use server-side apply.

This function still uses the Get/Create/Update pattern with manual label/annotation merging, which is not atomic. Use client.Patch with client.Apply for consistency with createOrUpdateNamespace (line 796) and packagesource_reconciler.go line 329.

🧹 Nitpick comments (2)
internal/operator/packagesource_reconciler.go (2)

98-113: Consider detecting library name collisions within a variant.

Within a single variant, if two libraries resolve to the same name (either from explicit Name or extracted from Path), the second will silently overwrite the first in the map. This could lead to unexpected behavior where a component references a library by name but gets the wrong path.

🔎 Suggested enhancement to log collisions
 		for _, lib := range variant.Libraries {
 			libName := lib.Name
 			if libName == "" {
 				// If library name is not set, extract from path
 				libName = r.getPackageNameFromPath(lib.Path)
 			}
 			if libName != "" {
+				if existing, ok := libraryMap[libName]; ok {
+					logger.Info("library name collision within variant, overwriting",
+						"variant", variant.Name,
+						"name", libName,
+						"existingPath", existing.Path,
+						"newPath", lib.Path)
+				}
 				// Store library with the resolved name
 				libraryMap[libName] = lib
 			}
 		}

Note: The previous review concern about collisions across variants is not applicable since libraryMap is correctly scoped per variant (created on line 102 inside the variant loop).


313-330: LGTM! Race condition resolved with server-side apply.

The method now correctly uses server-side apply (client.Apply), which is atomic and avoids the race condition from the Get/Create/Update pattern flagged in previous reviews.

For future maintainability, consider:

  1. Extract the field owner string to a constant:

    const FieldOwnerPackageSource = "cozystack-packagesource-controller"
  2. Deduplicate with package_reconciler.go: As noted in previous reviews, this createOrUpdate logic is duplicated. Consider extracting it to a shared utility package (e.g., internal/operator/util.go) to reduce maintenance burden.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 21af008 and a3373dc.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (9)
  • api/v1alpha1/package_types.go
  • api/v1alpha1/packagesource_types.go
  • api/v1alpha1/zz_generated.deepcopy.go
  • cmd/cozystack-operator/main.go
  • go.mod
  • internal/operator/package_reconciler.go
  • internal/operator/packagesource_reconciler.go
  • packages/core/installer/definitions/cozystack.io_packages.yaml
  • packages/core/installer/definitions/cozystack.io_packagesources.yaml
🚧 Files skipped from review as they are similar to previous changes (3)
  • api/v1alpha1/package_types.go
  • packages/core/installer/definitions/cozystack.io_packages.yaml
  • go.mod
🧰 Additional context used
📓 Path-based instructions (6)
api/**

📄 CodeRabbit inference engine (AGENTS.md)

Define Kubernetes CRDs in the api directory following kubebuilder conventions

Files:

  • api/v1alpha1/zz_generated.deepcopy.go
  • api/v1alpha1/packagesource_types.go
**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

Do NOT manually edit vendor/ directory or auto-generated files (zz_generated.*.go)

Files:

  • api/v1alpha1/zz_generated.deepcopy.go
  • cmd/cozystack-operator/main.go
  • internal/operator/packagesource_reconciler.go
  • api/v1alpha1/packagesource_types.go
  • internal/operator/package_reconciler.go
cmd/**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

Follow controller-runtime patterns and kubebuilder style for Go code in cmd directory

Files:

  • cmd/cozystack-operator/main.go
internal/**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

Follow controller-runtime patterns and kubebuilder style for Go code in internal directory

Files:

  • internal/operator/packagesource_reconciler.go
  • internal/operator/package_reconciler.go
packages/core/**

📄 CodeRabbit inference engine (AGENTS.md)

Use Helm Chart umbrella pattern with vendored upstream charts in charts/ directory

Files:

  • packages/core/installer/definitions/cozystack.io_packagesources.yaml
**/*.yaml

📄 CodeRabbit inference engine (AGENTS.md)

Do NOT directly edit upstream Helm charts in vendored charts directory; use proper chart vendoring mechanisms

Files:

  • packages/core/installer/definitions/cozystack.io_packagesources.yaml
🧠 Learnings (4)
📚 Learning: 2025-12-25T09:45:26.511Z
Learnt from: CR
Repo: cozystack/cozystack PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-25T09:45:26.511Z
Learning: Applies to cmd/**/*.go : Follow controller-runtime patterns and kubebuilder style for Go code in cmd directory

Applied to files:

  • cmd/cozystack-operator/main.go
📚 Learning: 2025-12-25T09:45:26.511Z
Learnt from: CR
Repo: cozystack/cozystack PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-25T09:45:26.511Z
Learning: Applies to internal/**/*.go : Follow controller-runtime patterns and kubebuilder style for Go code in internal directory

Applied to files:

  • cmd/cozystack-operator/main.go
  • internal/operator/packagesource_reconciler.go
📚 Learning: 2025-12-25T09:45:26.511Z
Learnt from: CR
Repo: cozystack/cozystack PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-25T09:45:26.511Z
Learning: Applies to pkg/**/*.go : Follow controller-runtime patterns and kubebuilder style for Go code in pkg directory

Applied to files:

  • cmd/cozystack-operator/main.go
  • internal/operator/packagesource_reconciler.go
  • internal/operator/package_reconciler.go
📚 Learning: 2025-07-11T06:11:25.438Z
Learnt from: lllamnyp
Repo: cozystack/cozystack PR: 1130
File: hack/e2e-apps/kubernetes.bats:101-101
Timestamp: 2025-07-11T06:11:25.438Z
Learning: In cozystack, the plural form for the Kubernetes custom resource is `kuberneteses.apps.cozystack.io`, not `kubernetes.apps.cozystack.io`. This is defined in the API schema even though it's not grammatically perfect.

Applied to files:

  • packages/core/installer/definitions/cozystack.io_packagesources.yaml
🧬 Code graph analysis (3)
api/v1alpha1/zz_generated.deepcopy.go (1)
api/v1alpha1/package_types.go (1)
  • DependencyStatus (97-100)
cmd/cozystack-operator/main.go (1)
internal/fluxinstall/install.go (1)
  • Install (39-106)
internal/operator/packagesource_reconciler.go (4)
pkg/apiserver/apiserver.go (1)
  • Scheme (53-53)
api/v1alpha1/packagesource_types.go (2)
  • PackageSource (31-37)
  • Library (85-93)
api/v1alpha1/cozystackresourcedefinitions_types.go (1)
  • SourceRef (71-80)
internal/controller/dashboard/manager.go (1)
  • Manager (45-48)
🔇 Additional comments (15)
api/v1alpha1/zz_generated.deepcopy.go (2)

311-324: Autogenerated deepcopy methods look correct.

The generated DeepCopy methods for DependencyStatus are correct for the simple struct containing only a boolean field.


585-591: Map deepcopy implementation is correct.

The Dependencies map deepcopy correctly allocates a new map and copies values. Since DependencyStatus is a simple struct with no pointer fields, value copy is appropriate.

packages/core/installer/definitions/cozystack.io_packagesources.yaml (1)

15-15: CRD shortName change aligns with API.

The CRD shortName change matches the API annotation change, maintaining consistency.

cmd/cozystack-operator/main.go (1)

163-179: Reconciler setup follows controller-runtime patterns.

The reconciler setup correctly follows kubebuilder and controller-runtime conventions with proper error handling.

internal/operator/package_reconciler.go (5)

218-241: Owner reference correctly uses GVK from scheme.

The fix using apiutil.GVKForObject (lines 219-220) correctly addresses the empty TypeMeta issue flagged in past reviews.


163-176: Empty namespace validation is consistent.

Both the main Reconcile function and reconcileNamespaces consistently reject empty namespaces, ensuring components must explicitly specify their target namespace.


787-797: Namespace creation uses server-side apply correctly.

The implementation correctly uses client.Patch with client.Apply, avoiding race conditions and ensuring atomic updates.


631-708: Dependency status updates avoid cascading annotation updates.

The implementation correctly updates only the status.Dependencies field rather than mutating annotations, avoiding the reconciliation storm issue flagged in past reviews. The watch handler at lines 886-934 properly enqueues dependent packages.


371-410: Variant lookup refactored into shared helper.

The getVariantForPackage helper eliminates code duplication flagged in past reviews, being reused in buildDependsOn (line 487), updateDependentPackagesDependencies (line 663), and the watch handler (line 904).

internal/operator/packagesource_reconciler.go (6)

1-47: LGTM!

The controller structure, imports, and RBAC annotations follow standard controller-runtime and kubebuilder patterns correctly.


49-75: LGTM!

The reconcile flow follows standard controller patterns with appropriate error handling.


211-224: LGTM! Owner reference issue resolved.

The owner reference now correctly uses apiutil.GVKForObject to obtain GVK from the scheme instead of relying on potentially empty TypeMeta fields, addressing the concern from previous reviews.


237-311: LGTM!

The helper functions properly handle path construction, normalization, and default values for different source kinds.


332-405: LGTM!

The status update logic correctly:

  • Mirrors variant names from spec to status
  • Handles missing SourceRef and ArtifactGenerator gracefully
  • Propagates the Ready condition from ArtifactGenerator with proper ObservedGeneration tracking

407-433: Code is correct for cluster-scoped PackageSource.

The reconcile request on line 424 correctly omits the namespace because PackageSource is a cluster-scoped resource. ArtifactGenerator (the watched resource) is namespaced, but can reference the cluster-scoped PackageSource via owner reference without including a namespace in the reconcile request.

@kvaps kvaps force-pushed the cozystack-operator-reconcilers branch from a3373dc to 2d6c008 Compare December 25, 2025 16:04
@lllamnyp lllamnyp self-assigned this Dec 29, 2025
@lllamnyp lllamnyp changed the base branch from main to cozystack-operator-core December 29, 2025 11:36
@kvaps kvaps force-pushed the cozystack-operator-core branch from d1dcc1f to db8a006 Compare December 29, 2025 13:38
@dosubot dosubot bot added the lgtm This PR has been approved by a maintainer label Dec 30, 2025
Base automatically changed from cozystack-operator-core to main December 30, 2025 10:48
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
@kvaps kvaps force-pushed the cozystack-operator-reconcilers branch from 2d6c008 to f28d639 Compare December 30, 2025 10:55
@kvaps kvaps merged commit c06f399 into main Dec 30, 2025
4 of 6 checks passed
@kvaps kvaps deleted the cozystack-operator-reconcilers branch December 30, 2025 10:55
kvaps added a commit that referenced this pull request Jan 8, 2026
depends on #1741

## What this PR does


### Release note

<!--  Write a release note:
- Explain what has changed internally and for users.
- Start with the same [label] as in the PR title
- Follow the guidelines at
https://github.com/kubernetes/community/blob/master/contributors/guide/release-notes.md.
-->

```release-note
[cozystack-operator] Add Package and PackageSource reconcilers
```

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Launches CozyStack operator with CLI-configurable runtime
(metrics/health/leader election/HTTP2), optional pre-reconcile steps,
optional automatic Flux install, and platform-source URL support
(OCI/Git).
* Adds PackageSource and Package controllers for artifact generation,
variant resolution, dependency tracking, HelmRelease orchestration,
namespace reconciliation, orphan cleanup, and status propagation.
  * Package status now exposes per-dependency readiness.

* **Chores**
* Embeds Flux manifests and adds a local Flux install workflow; updates
manifest generation path and Makefile targets.
* **Other**
  * PackageSource CRD shortName simplified to "pks".

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
kvaps added a commit that referenced this pull request Jan 16, 2026
…d backup system (#1867)

## What this PR does

Update changelog for v1.0.0-alpha.1 to include missing features:
- **Cozystack Operator**: New operator for Package and PackageSource
management (#1740, #1741, #1755, #1756, #1760, #1761)
- **Backup System**: Comprehensive backup functionality with Velero
integration (#1640, #1685, #1687, #1708, #1719, #1720, #1737, #1762)
- Add @androndo to contributors
- Update Full Changelog link to v0.38.0...v1.0.0-alpha.1

### Release note

```release-note
[docs] Update changelog for v1.0.0-alpha.1: add cozystack-operator and backup system
```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request lgtm This PR has been approved by a maintainer size:XXL This PR changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants