-
Notifications
You must be signed in to change notification settings - Fork 136
[cozystack-operator] Add Package and PackageSource reconcilers #1755
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
Caution Review failedThe pull request is closed. Note Other AI code review bot(s) detectedCodeRabbit 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. 📝 WalkthroughWalkthroughTwo 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
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
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)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
📜 Recent review detailsConfiguration used: defaults Review profile: CHILL Plan: Pro ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (9)
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. Comment |
Summary of ChangesHello @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 Highlights
🧠 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 AssistThe 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
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 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
|
There was a problem hiding this 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.
There was a problem hiding this 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
0666allows 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 theupdatetarget.Per static analysis, the
updatetarget should be declared as.PHONYsince it doesn't produce a file with that name.🔎 Suggested fix
Add at the top of the file or before the
updatetarget:.PHONY: show apply diff update update-old update-newinternal/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
errvariable on line 65 shadows the outererrfrom line 63. While functionally correct here, this pattern can cause confusion. Also, iffluxcd.yamldoesn'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: Usetime.Durationconstants for readability.The raw nanosecond calculation
5 * 60 * 1000000000is error-prone. Usetime.Minutefor 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
EnqueueRequestsFromMapFunciterates 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.
installFluxResourcesFuncandinstallPlatformSourceResourceshare 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 usesbranch, but ref could be a tag or commit.The
generateGitRepositoryfunction always setsref.branch, but the parsedreffrom 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: GenericcreateOrUpdatecontains type-specific logic.The
createOrUpdatemethod has special handling forArtifactGeneratorembedded 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
📒 Files selected for processing (7)
cmd/cozystack-operator/main.gointernal/fluxinstall/install.gointernal/fluxinstall/manifests.embed.gointernal/fluxinstall/manifests/fluxcd.yamlinternal/operator/package_reconciler.gointernal/operator/packagesource_reconciler.gopackages/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.gointernal/fluxinstall/manifests.embed.gointernal/operator/packagesource_reconciler.gocmd/cozystack-operator/main.gointernal/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", butreconcileNamespaces(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 inreconcileNamespacesshould also apply this default.cmd/cozystack-operator/main.go (1)
157-157: Usingmgr.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
ArtifactGeneratorcorrectly uses ownerReference lookup to find the owning PackageSource, which is efficient and follows controller-runtime best practices. Good use ofEnqueueRequestsFromMapFunc.
12f6336 to
72e3cac
Compare
There was a problem hiding this 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: UnusedcozystackVersionflag.The
cozystackVersionflag 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.goalready 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: Usestrings.LastIndexfor robust digest parsing.The comment correctly states that
LastIndexshould be used to handle paths with@symbols, but the code usesstrings.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, andtriggerDependentPackages. 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
triggerDependentPackagesfunction 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
triggerDependentPackagescall 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.WithTimeoutcalls with deferred cancels. While functionally correct (eachdefercaptures 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
refis 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:mainvsgit://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 haveAPIVersionandKindpopulated (depends on the client configuration). Consider usingctrl.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
LastTransitionTimefrom the ArtifactGenerator while settingObservedGenerationfrom 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
📒 Files selected for processing (3)
cmd/cozystack-operator/main.gointernal/operator/package_reconciler.gointernal/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.gocmd/cozystack-operator/main.gointernal/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.
72e3cac to
9ef1d32
Compare
There was a problem hiding this 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
cozystackVersionflag 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
installFluxResourcesFuncandinstallPlatformSourceResourceuse Get followed by Create or Update, which is not atomic. Using server-side apply (client.Patchwithclient.Apply) would be more robust and align with the pattern used ininternal/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 likesha256: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
createOrUpdateNamespacefunction 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
triggerDependentPackagesfunction 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-reconcileannotation on dependent Packages triggers their reconciliation, which in turn may calltriggerDependentPackagesagain, 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
triggerDependentPackagescall 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, andtriggerDependentPackages. 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
createOrUpdateHelmReleasefunction 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
reconcileNamespacesfunction errors whencomponent.Install.Namespaceis empty, but HelmRelease creation (lines 156-159) defaults empty namespaces tocozy-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
createOrUpdatefunction uses Get followed by Create or Update, which is not atomic and can lead to race conditions. Server-side apply (client.Patchwithclient.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.Sleepto 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
waitForCRDEstablishedto poll the CRD's status.
🧹 Nitpick comments (3)
packages/core/flux-aio/Makefile (2)
15-15: Declare target as .PHONY for robustness.The
updatetarget should be declared as.PHONYto 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-newBased on static analysis hints.
33-34: TODO: Resolve DNS configuration for tenant clusters.The commented-out
dnsPolicyinsertion indicates an unresolved DNS issue when usinghostNetworkin 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
injectKubernetesServiceEnvfunction 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
📒 Files selected for processing (7)
cmd/cozystack-operator/main.gointernal/fluxinstall/install.gointernal/fluxinstall/manifests.embed.gointernal/fluxinstall/manifests/fluxcd.yamlinternal/operator/package_reconciler.gointernal/operator/packagesource_reconciler.gopackages/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.gointernal/fluxinstall/install.gointernal/operator/packagesource_reconciler.gointernal/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.gointernal/operator/packagesource_reconciler.gointernal/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.gointernal/operator/packagesource_reconciler.gointernal/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.gointernal/operator/packagesource_reconciler.gointernal/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
af02b8d to
2ae2670
Compare
There was a problem hiding this 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: UnusedcozystackVersionvariable.The
cozystackVersionflag 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 likesha256: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.Sleepfor CRD registration is unreliable.A 2-second sleep may not be sufficient on slow or busy clusters. Consider polling the API server for CRD
Establishedcondition 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:
- Using server-side apply for atomicity
- 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 emptyInstall.Namespacereturns an error. However, in the HelmRelease creation loop (lines 162-165), an empty namespace defaults tocozy-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.PHONYdeclaration for the new targets.The static analysis tool correctly flagged that
update,update-old, andupdate-newshould be declared as.PHONYtargets 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.yamldoesn't exist, the code picks the first.yamlfile 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
updateDependentPackagesDependenciesfunction 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:
- Using an index (as suggested above)
- Or caching dependency relationships
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (8)
api/v1alpha1/package_types.gocmd/cozystack-operator/main.gointernal/fluxinstall/install.gointernal/fluxinstall/manifests.embed.gointernal/fluxinstall/manifests/fluxcd.yamlinternal/operator/package_reconciler.gointernal/operator/packagesource_reconciler.gopackages/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.gocmd/cozystack-operator/main.gointernal/fluxinstall/install.gointernal/operator/packagesource_reconciler.gointernal/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.gointernal/operator/packagesource_reconciler.gointernal/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.gointernal/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.gointernal/operator/packagesource_reconciler.gointernal/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
DependencyStatustype andDependenciesfield inPackageStatusfollow 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
applyObjectsfunction 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. Theinternal/fluxinstall/manifestsdirectory already exists in the repository with thefluxcd.yamlfile present, so theupdate-newtarget will execute successfully without requiring amkdir -pcommand.Likely an incorrect or invalid review comment.
ef72cd2 to
af63ae0
Compare
There was a problem hiding this 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:cozystackVersionflag 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.Patchwithclient.Applywould be more robust and consistent with modern controller patterns.
348-356: Potential incorrect handling of OCI tags vs digests persists.If
parsePlatformSourceextracts a tag (e.g.,v1.0.0fromoci://registry/repo@v1.0.0), this code will prependsha256:producing an invalid referencesha256: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
createOrUpdatefunction uses Get/Create/Update which is not atomic and can lead to race conditions. Server-side apply withclient.Patch(ctx, obj, client.Apply, ...)is more robust and declarative.
211-220: Owner reference may have empty APIVersion/Kind.When
packageSourceis fetched from the API server,TypeMeta(APIVersion/Kind) is often not populated. UsingpackageSource.APIVersionandpackageSource.Kinddirectly 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
createOrUpdateHelmReleasefunction uses Get/Create/Update which is not atomic. Server-side apply withclient.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. Whenpkgis fetched from the API server,TypeMetais often not populated. Useapiutil.GVKForObjectto 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 emptyInstall.Namespacecauses an error. However, in the HelmRelease creation loop (lines 162-165), an empty namespace defaults tocozy-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
refis always set asbranch, 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
Packageresources 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 PackagesNote: 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:updateDependentPackagesDependenciesduplicates 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
⛔ Files ignored due to path filters (1)
go.sumis excluded by!**/*.sum
📒 Files selected for processing (7)
api/v1alpha1/package_types.goapi/v1alpha1/zz_generated.deepcopy.gocmd/cozystack-operator/main.gogo.modinternal/operator/package_reconciler.gointernal/operator/packagesource_reconciler.gopackages/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 getcommand 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.gointernal/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.goapi/v1alpha1/package_types.goapi/v1alpha1/zz_generated.deepcopy.gointernal/operator/package_reconciler.gocmd/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.goapi/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.modinternal/operator/packagesource_reconciler.gointernal/operator/package_reconciler.gocmd/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/apiandsource-watcher/api/v2aligns with the new operator functionality for managing Flux resources. The file header indicates this is generated, which aligns with the coding guideline to usego getrather than manual edits.packages/core/installer/definitions/cozystack.io_packages.yaml (1)
151-165: LGTM on thedependenciesstatus field.The CRD schema correctly defines the new
dependenciesmap withDependencyStatuscontaining a requiredreadyboolean. 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
Dependenciesmap withDependencyStatusfollows kubebuilder conventions. Using a simpleboolwithout pointer forReadyis 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
DependencyStatusand the map copying forPackageStatus.Dependenciesfollows 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.
af63ae0 to
21af008
Compare
There was a problem hiding this 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:cozystackVersionis never consumed.The
cozystackVersionflag 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.Patchwithclient.Apply) as done ininternal/operator/packagesource_reconciler.go:313-327for 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 nilinternal/operator/packagesource_reconciler.go (1)
98-113: Potential library name collision across variants.The
libraryMapis 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 prependingsha256:.If the ref is a tag (e.g.,
@v1.0.0) rather than a digest, prependingsha256:produces an invalid digest likesha256: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
⛔ Files ignored due to path filters (1)
go.sumis excluded by!**/*.sum
📒 Files selected for processing (7)
api/v1alpha1/package_types.goapi/v1alpha1/zz_generated.deepcopy.gocmd/cozystack-operator/main.gogo.modinternal/operator/package_reconciler.gointernal/operator/packagesource_reconciler.gopackages/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.gointernal/operator/packagesource_reconciler.goapi/v1alpha1/zz_generated.deepcopy.gointernal/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.gointernal/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.gointernal/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
LastIndexto 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
createOrUpdatefunction 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
buildDependsOnfunction 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.
21af008 to
a3373dc
Compare
There was a problem hiding this 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
cozystackVersionflag 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.Patchwithclient.Applyfor a more robust approach, similar to the pattern already adopted inpackagesource_reconciler.goline 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.0would produce an invalidsha256:v1.0.0digest.🔎 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.Patchwithclient.Applyfor consistency withcreateOrUpdateNamespace(line 796) andpackagesource_reconciler.goline 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
Nameor extracted fromPath), 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
libraryMapis 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:
Extract the field owner string to a constant:
const FieldOwnerPackageSource = "cozystack-packagesource-controller"Deduplicate with
package_reconciler.go: As noted in previous reviews, thiscreateOrUpdatelogic 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
⛔ Files ignored due to path filters (1)
go.sumis excluded by!**/*.sum
📒 Files selected for processing (9)
api/v1alpha1/package_types.goapi/v1alpha1/packagesource_types.goapi/v1alpha1/zz_generated.deepcopy.gocmd/cozystack-operator/main.gogo.modinternal/operator/package_reconciler.gointernal/operator/packagesource_reconciler.gopackages/core/installer/definitions/cozystack.io_packages.yamlpackages/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.goapi/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.gocmd/cozystack-operator/main.gointernal/operator/packagesource_reconciler.goapi/v1alpha1/packagesource_types.gointernal/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.gointernal/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.gointernal/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.gointernal/operator/packagesource_reconciler.gointernal/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.Patchwithclient.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
getVariantForPackagehelper 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.GVKForObjectto 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.
a3373dc to
2d6c008
Compare
d1dcc1f to
db8a006
Compare
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2d6c008 to
f28d639
Compare
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 -->
…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 ```
depends on #1741
What this PR does
Release note
Summary by CodeRabbit
Release Notes
New Features
Dependencies
✏️ Tip: You can customize this high-level summary in your review settings.