[cozystack-controller] Ancestor tracking webhook - #1400
Conversation
WalkthroughAdds a LineageControllerWebhook: a controller that builds an in-memory chart→app map from CozystackResourceDefinition CRs and a mutating admission webhook that walks ownership graphs to compute and inject lineage labels; includes lineage traversal utilities, an AppMapper, tests, and Helm templates for webhook TLS/service. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant M as Manager
participant LC as LineageControllerWebhook
participant K8s as API Server
rect rgba(200,220,255,0.15)
note over M,LC: Startup wiring
M->>LC: SetupWithManagerAsController()
M->>LC: SetupWithManagerAsWebhook()
LC->>M: Registered controller + /mutate-lineage webhook
end
rect rgba(220,255,220,0.15)
note over LC,K8s: Controller reconcile updates runtime map
LC->>K8s: List CozystackResourceDefinition (cozy-system)
LC->>LC: Build chart→app map
LC->>LC: Store map (atomic)
end
sequenceDiagram
autonumber
participant AP as API Server
participant WH as LineageControllerWebhook
participant DC as Dynamic Client
participant RM as REST Mapper
participant LU as lineage.WalkOwnershipGraph
rect rgba(255,245,200,0.15)
AP->>WH: AdmissionReview (create/update object)
WH->>WH: decodeUnstructured(req)
WH->>LU: WalkOwnershipGraph(ctx, DC, RM, AppMapper, obj)
LU->>DC: Fetch owner resources (resolve via RM)
LU-->>WH: ancestor ObjectID(s) or none/ambiguous
alt ancestor found
WH->>DC: Get ancestor Unstructured
WH->>WH: computeLabels -> applyLabels
WH-->>AP: Patch response (labels)
else none / ambiguous
WH-->>AP: Allow (may include warning)
end
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Suggested reviewers
Poem
Tip 👮 Agentic pre-merge checks are now available in preview!Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.
Please see the documentation for more information. Example: reviews:
pre_merge_checks:
custom_checks:
- name: "Undocumented Breaking Changes"
mode: "warning"
instructions: |
Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).Please share your feedback with us on this Discord post. Pre-merge checks (2 passed, 1 warning)❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests
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 |
73e5fa6 to
246e0fc
Compare
There was a problem hiding this comment.
Actionable comments posted: 12
🧹 Nitpick comments (13)
pkg/lineage/mapper.go (1)
16-40: Clarify stub-only mapping and avoid drift with the real mapper.Document or gate
stubMapperbehind a build tag to ensure it’s not used in production and doesn’t diverge from the webhook/controller mapping logic.Example doc:
type stubMapper struct{} +// NOTE: stubMapper is intended for local dev/tests only. Production mapping is +// provided by the webhook/controller. Keep entries in sync or remove before release. var stubMapperMap = map[string]string{pkg/lineage/lineage.go (3)
87-91: Use structured logging instead of stderr prints.Library code should not write to stderr directly; use the request-scoped logger.
- if err != nil { - fmt.Fprintf(os.Stderr, "Could not fetch owner %s/%s (%s): %v\n", obj.GetNamespace(), owner.Name, owner.Kind, err) - continue - } + if err != nil { + l.Error(err, "could not fetch owner", + "namespace", obj.GetNamespace(), "name", owner.Name, "kind", owner.Kind) + continue + }
71-77: Clarify variadic-args error message.Current log miscounts and confuses total vs variadic args.
if len(memory) != 0 && len(memory) != 1 { l.Error( fmt.Errorf("invalid argument count"), "could not parse variadic arguments to WalkOwnershipGraph", - "args passed", len(memory)+5, "expected args", "4|5", + "memory args", len(memory), "expected memory args", "0|1", ) return out }
101-130: Flatten the single-iteration loop for readability.Replace the
for { ... break }pattern with early returns/guards to simplify control flow.cmd/cozystack-controller/main.go (1)
41-41: Nit: clearer alias would help readability.Consider aliasing as lineagewebhook or lineage for clarity over lcw.
internal/lineagecontrollerwebhook/types.go (1)
14-21: Use a pointer decoder to follow controller-runtime patterns and avoid unnecessary copies.Change decoder to a pointer; InjectDecoder can then assign directly.
type LineageControllerWebhook struct { Scheme *runtime.Scheme client.Client - decoder admission.Decoder + decoder *admission.Decoder dynClient dynamic.Interface mapper meta.RESTMapper config atomic.Value }internal/lineagecontrollerwebhook/config.go (1)
21-24: Key may collide across namespaces/kinds; consider including SourceRef.Kind/Namespace.Using only SourceRef.Name and Chart risks collisions. Consider a key like "///" and update the controller to match.
internal/lineagecontrollerwebhook/controller.go (2)
31-31: Prefer keyed struct literal for runtimeConfigPrevents accidental field-order bugs if struct changes.
- c.config.Store(&runtimeConfig{newConfig}) + c.config.Store(&runtimeConfig{chartAppMap: newConfig})
22-22: Hard-coded namespace “cozy-system”If CRDs ever move or are cluster-scoped, this will silently miss them.
Make the namespace configurable (env/flag) or handle cluster-scoped CRDs by omitting Namespace in ListOptions.
internal/lineagecontrollerwebhook/webhook.go (4)
66-71: No-op for non-CREATE operations (unless configured otherwise)PR text says “on creation”; guard here in case the webhook config isn’t restricted to CREATE.
warn := make(admission.Warnings, 0) + if req.Operation != admissionv1.Create { + return admission.Allowed("no-op for non-CREATE operations") + }Add import:
- "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + admissionv1 "k8s.io/api/admission/v1"
73-89: Simplify error handling flowThe for-loop is harder to read; a switch is clearer.
- labels, err := h.computeLabels(ctx, obj) - for { - if err != nil && errors.Is(err, NoAncestors) { - return admission.Allowed("object not managed by app") - } - if err != nil && errors.Is(err, AncestryAmbiguous) { - warn = append(warn, "object ancestry ambiguous, using first ancestor found") - break - } - if err != nil { - return admission.Errored(500, fmt.Errorf("error computing lineage labels: %w", err)) - } - if err == nil { - break - } - } + labels, err := h.computeLabels(ctx, obj) + switch { + case err == nil: + case errors.Is(err, NoAncestors): + return admission.Allowed("object not managed by app") + case errors.Is(err, AncestryAmbiguous): + warn = append(warn, "object ancestry ambiguous, using first ancestor found") + default: + return admission.Errored(500, fmt.Errorf("error computing lineage labels: %w", err)) + }
92-97: Generalize log/error text (“pod” → “object”)This webhook handles many kinds, not just Pods.
- mutated, err := json.Marshal(obj) - if err != nil { - return admission.Errored(500, fmt.Errorf("marshal mutated pod: %w", err)) - } - logger.V(1).Info("mutated pod", "namespace", obj.GetNamespace(), "name", obj.GetName()) + mutated, err := json.Marshal(obj) + if err != nil { + return admission.Errored(500, fmt.Errorf("marshal mutated object: %w", err)) + } + logger.V(1).Info("mutated object", "gvk", obj.GroupVersionKind().String(), "namespace", obj.GetNamespace(), "name", obj.GetName())
131-140: Label overwrite policyapplyLabels unconditionally overwrites existing values. If these keys are reserved for this webhook, fine; otherwise consider not overriding.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (8)
cmd/cozystack-controller/main.go(2 hunks)internal/lineagecontrollerwebhook/config.go(1 hunks)internal/lineagecontrollerwebhook/controller.go(1 hunks)internal/lineagecontrollerwebhook/types.go(1 hunks)internal/lineagecontrollerwebhook/webhook.go(1 hunks)pkg/lineage/lineage.go(1 hunks)pkg/lineage/lineage_test.go(1 hunks)pkg/lineage/mapper.go(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (8)
cmd/cozystack-controller/main.go (1)
internal/lineagecontrollerwebhook/types.go (1)
LineageControllerWebhook(14-21)
pkg/lineage/lineage_test.go (1)
pkg/lineage/lineage.go (1)
WalkOwnershipGraph(40-143)
internal/lineagecontrollerwebhook/webhook.go (2)
internal/lineagecontrollerwebhook/types.go (1)
LineageControllerWebhook(14-21)pkg/lineage/lineage.go (1)
WalkOwnershipGraph(40-143)
internal/lineagecontrollerwebhook/config.go (2)
internal/lineagecontrollerwebhook/types.go (1)
LineageControllerWebhook(14-21)api/v1alpha1/cozystackresourcedefinitions_types.go (1)
SourceRef(60-69)
internal/lineagecontrollerwebhook/types.go (1)
pkg/apiserver/apiserver.go (1)
Scheme(41-41)
pkg/lineage/lineage.go (1)
pkg/lineage/mapper.go (1)
AppMapper(10-12)
internal/lineagecontrollerwebhook/controller.go (2)
internal/lineagecontrollerwebhook/types.go (1)
LineageControllerWebhook(14-21)api/v1alpha1/cozystackresourcedefinitions_types.go (3)
CozystackResourceDefinition(26-31)CozystackResourceDefinitionList(36-40)SourceRef(60-69)
pkg/lineage/mapper.go (1)
api/v1alpha1/cozystackresourcedefinitions_types.go (1)
SourceRef(60-69)
🔇 Additional comments (2)
cmd/cozystack-controller/main.go (1)
218-231: Verify webhook init order and readiness behavior.If the webhook receives traffic before its runtime mapping is populated, the handler must allow the request (no-op) rather than error. Please confirm SetupWithManagerAsController populates a default config before SetupWithManagerAsWebhook serves, or that the handler tolerates “uninitialized mapping” gracefully.
internal/lineagecontrollerwebhook/webhook.go (1)
53-56: Correct InjectDecoder signature per controller-runtime API
The admission.DecoderInjector interface expectsInjectDecoder(d *admission.Decoder) error, not a value receiver; with the current signature the method won’t be called andh.decoderremains nil (github.com, chinalhr.github.io)-func (h *LineageControllerWebhook) InjectDecoder(d admission.Decoder) error { +func (h *LineageControllerWebhook) InjectDecoder(d *admission.Decoder) error { h.decoder = d return nil }Also update the struct field in
internal/lineagecontrollerwebhook/types.go:- decoder admission.Decoder + decoder *admission.DecoderLikely an incorrect or invalid review comment.
246e0fc to
053cf59
Compare
There was a problem hiding this comment.
Actionable comments posted: 6
♻️ Duplicate comments (1)
internal/lineagecontrollerwebhook/webhook.go (1)
43-49: Initialize runtime config before registering the webhookPrevent nil deref in Map() during early requests by seeding an empty config before registration.
Apply this diff:
cachedDisco := memory.NewMemCacheClient(discoClient) h.mapper = restmapper.NewDeferredDiscoveryRESTMapper(cachedDisco) +// Safe default: empty mapping to avoid nil deref before the controller populates it. +// Adjust the concrete type/field names if they differ. +h.config.Store(&runtimeConfig{chartAppMap: map[string]string{}}) + // Register HTTP path -> handler. mgr.GetWebhookServer().Register("/mutate-lineage", &admission.Webhook{Handler: h})
🧹 Nitpick comments (6)
internal/lineagecontrollerwebhook/webhook.go (2)
73-88: Simplify error handling; the for-loop is unnecessaryThe loop executes at most once. A switch makes intent clear.
Apply this diff:
- labels, err := h.computeLabels(ctx, obj) - for { - if err != nil && errors.Is(err, NoAncestors) { - return admission.Allowed("object not managed by app") - } - if err != nil && errors.Is(err, AncestryAmbiguous) { - warn = append(warn, "object ancestry ambiguous, using first ancestor found") - break - } - if err != nil { - return admission.Errored(500, fmt.Errorf("error computing lineage labels: %w", err)) - } - if err == nil { - break - } - } + labels, err := h.computeLabels(ctx, obj) + switch { + case err == nil: + // proceed + case errors.Is(err, NoAncestors): + return admission.Allowed("object not managed by app") + case errors.Is(err, AncestryAmbiguous): + warn = append(warn, "object ancestry ambiguous, using first ancestor found") + default: + return admission.Errored(500, fmt.Errorf("error computing lineage labels: %w", err)) + }
92-97: Fix log/message wording: this webhook mutates generic objects, not just podsApply this diff:
- if err != nil { - return admission.Errored(500, fmt.Errorf("marshal mutated pod: %w", err)) - } - logger.V(1).Info("mutated pod", "namespace", obj.GetNamespace(), "name", obj.GetName()) + if err != nil { + return admission.Errored(500, fmt.Errorf("marshal mutated object: %w", err)) + } + logger.V(1).Info("mutated object", "namespace", obj.GetNamespace(), "name", obj.GetName())packages/system/cozystack-controller/templates/service.yaml (1)
10-13: Optional: mark port as HTTPS for claritySet appProtocol for better intent and tooling hints.
Apply this diff:
ports: - port: 443 targetPort: 9443 protocol: TCP name: webhook + appProtocol: httpspackages/system/cozystack-controller/templates/mutatingwebhookconfiguration.yaml (2)
18-23: Expand resources to match PR intent and fix style nitPR mentions labeling “pods, secrets, PVCs, etc.” The rules currently miss secrets. Also fix spacing after comma.
- resources: ["pods","persistentvolumeclaims"] + resources: ["pods", "persistentvolumeclaims", "secrets"]
24-33: Namespace scoping looks good; confirm release ns exclusionYou exclude cozystack.io/system=true and kube-system. Ensure the release namespace is labeled cozystack.io/system=true so webhook won’t mutate its own certs/pods. If not guaranteed, add an explicit NotIn for the release namespace.
packages/system/cozystack-controller/templates/certmanager.yaml (1)
4-7: Template resource names to avoid cross-install collisionsIf this chart can be installed per-tenant or multiple times, hardcoded Issuer/Certificate names may collide. Prefer fullname-based names.
Example pattern:
- name: cozystack-controller-webhook-selfsigned + name: {{ include "cozystack-controller.fullname" . }}-webhook-selfsigned ... - name: cozystack-controller-webhook-ca + name: {{ include "cozystack-controller.fullname" . }}-webhook-ca ... - secretName: cozystack-controller-webhook-ca + secretName: {{ include "cozystack-controller.fullname" . }}-webhook-ca ... - name: cozystack-controller-webhook + name: {{ include "cozystack-controller.fullname" . }}-webhook - secretName: cozystack-controller-webhook-cert + secretName: {{ include "cozystack-controller.fullname" . }}-webhook-certIf you adopt this, also update the inject-ca-from(-secret) and Service tls.secret mounts accordingly in other templates.
Also applies to: 12-20, 25-29, 34-38
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (12)
cmd/cozystack-controller/main.go(2 hunks)internal/lineagecontrollerwebhook/config.go(1 hunks)internal/lineagecontrollerwebhook/controller.go(1 hunks)internal/lineagecontrollerwebhook/types.go(1 hunks)internal/lineagecontrollerwebhook/webhook.go(1 hunks)packages/system/cozystack-controller/templates/certmanager.yaml(1 hunks)packages/system/cozystack-controller/templates/deployment.yaml(1 hunks)packages/system/cozystack-controller/templates/mutatingwebhookconfiguration.yaml(1 hunks)packages/system/cozystack-controller/templates/service.yaml(1 hunks)pkg/lineage/lineage.go(1 hunks)pkg/lineage/lineage_test.go(1 hunks)pkg/lineage/mapper.go(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (7)
- pkg/lineage/lineage_test.go
- internal/lineagecontrollerwebhook/types.go
- internal/lineagecontrollerwebhook/config.go
- cmd/cozystack-controller/main.go
- pkg/lineage/lineage.go
- pkg/lineage/mapper.go
- internal/lineagecontrollerwebhook/controller.go
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-09-10T22:02:47.707Z
Learnt from: lllamnyp
PR: cozystack/cozystack#1380
File: internal/controller/kubeovnplunger/metrics.go:291-296
Timestamp: 2025-09-10T22:02:47.707Z
Learning: In the kubeovnplunger controller, r.lastLeader map is initialized in SetupWithManager method (line 151) with make(map[string]string) before any reconcile operations begin, making defensive nil checks unnecessary in the reconcile methods.
Applied to files:
internal/lineagecontrollerwebhook/webhook.go
📚 Learning: 2025-09-10T22:02:47.707Z
Learnt from: lllamnyp
PR: cozystack/cozystack#1380
File: internal/controller/kubeovnplunger/metrics.go:291-296
Timestamp: 2025-09-10T22:02:47.707Z
Learning: In the kubeovnplunger controller, r.lastLeader map is initialized in SetupWithManager method before any reconcile operations begin, making defensive nil checks unnecessary in the reconcile methods.
Applied to files:
internal/lineagecontrollerwebhook/webhook.go
🧬 Code graph analysis (1)
internal/lineagecontrollerwebhook/webhook.go (2)
internal/lineagecontrollerwebhook/types.go (1)
LineageControllerWebhook(14-21)pkg/lineage/lineage.go (1)
WalkOwnershipGraph(40-143)
🪛 YAMLlint (1.37.1)
packages/system/cozystack-controller/templates/mutatingwebhookconfiguration.yaml
[warning] 22-22: too few spaces after comma
(commas)
[error] 6-6: syntax error: expected , but found ''
(syntax)
⏰ 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 (2)
packages/system/cozystack-controller/templates/deployment.yaml (1)
32-34: Expose webhook port — looks goodContainer port 9443 named "webhook" matches controller-runtime defaults and aligns with the Service.
internal/lineagecontrollerwebhook/webhook.go (1)
46-47: Verify MutatingWebhookConfiguration matches registered webhook path/serviceinternal/lineagecontrollerwebhook/webhook.go (lines 46–47) registers "/mutate-lineage" — scanned packages/ manifests and found MutatingWebhookConfiguration entries but none with clientConfig.service.name="cozystack-controller" and clientConfig.service.path="/mutate-lineage". Confirm the deployed manifests include a MutatingWebhookConfiguration whose clientConfig.service.name is "cozystack-controller" and path is "/mutate-lineage", or update the code/manifests to match.
053cf59 to
e8fa1ac
Compare
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (5)
packages/system/cozystack-controller/templates/mutatingwebhookconfiguration.yaml (2)
23-23: Set failurePolicy to Ignore to avoid cluster-wide CREATE outages if the webhook is down.- failurePolicy: Fail + failurePolicy: Ignore
4-9: Fix CA injection key and quote the value; scope the MWC name to the release (also resolves YAML parse error).
- Use cert-manager’s secret-based injector and reference the actual secret (likely ...-webhook-cert).
- Quote Helm expressions to satisfy yamllint and templating.
- Give the cluster-scoped MutatingWebhookConfiguration a release-scoped name.
metadata: - name: lineage + name: {{ include "cozystack-controller.fullname" . }}-lineage annotations: - cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/cozystack-controller-webhook + cert-manager.io/inject-ca-from-secret: '{{ .Release.Namespace }}/cozystack-controller-webhook-cert' labels: app: cozystack-controllerinternal/lineagecontrollerwebhook/types.go (1)
14-14: Align kubebuilder marker with chart manifest: create-only and Ignore failures.Prevents noisy updates and blocking behavior; keeps generator output consistent with the Helm manifest.
-// +kubebuilder:webhook:path=/mutate-lineage,mutating=true,failurePolicy=Fail,sideEffects=None,groups="",resources=pods,secrets,services,persistentvolumeclaims,verbs=create;update,versions=v1,name=mlineage.cozystack.io,admissionReviewVersions={v1} +// +kubebuilder:webhook:path=/mutate-lineage,mutating=true,failurePolicy=Ignore,sideEffects=None,groups="",resources=pods,secrets,services,persistentvolumeclaims,verbs=create,versions=v1,name=mlineage.cozystack.io,admissionReviewVersions={v1}internal/lineagecontrollerwebhook/config.go (1)
32-39: Guard against nil config to avoid panics on first requests.initConfig isn’t called before Map; Load() can be nil leading to a nil deref.
func (l *LineageControllerWebhook) Map(hr *helmv2.HelmRelease) (string, string, string, error) { - cfg := l.config.Load().(*runtimeConfig).chartAppMap + l.initConfig() + rc := l.config.Load().(*runtimeConfig) + cfg := rc.chartAppMap s := &hr.Spec.Chart.Spec val, ok := cfg[chartRef{s.SourceRef.Name, s.Chart}] if !ok { return "", "", "", fmt.Errorf("cannot map helm release %s/%s to dynamic app", hr.Namespace, hr.Name) } return val.groupVersion, val.kind, val.prefix, nil }pkg/lineage/lineage.go (1)
132-139: Guard against empty HR label; avoid Get(..., "").If the label exists but is empty, this still calls Get with an empty name.
Apply:
- name, ok := labels[HRLabel] - if !ok { + name, ok := labels[HRLabel] + if !ok || name == "" { return }
🧹 Nitpick comments (10)
packages/system/cozystack-controller/templates/mutatingwebhookconfiguration.yaml (2)
11-17: Add reinvocation policy and a shorter timeout.Improves correctness when earlier webhooks mutate objects and prevents long API-server hangs.
admissionReviewVersions: ["v1"] sideEffects: None + reinvocationPolicy: IfNeeded + timeoutSeconds: 5 clientConfig: service: name: cozystack-controller namespace: {{ .Release.Namespace }} path: /mutate-lineage
22-22: Tidy list formatting flagged by yamllint.- resources: ["pods","secrets", "services", "persistentvolumeclaims"] + resources: ["pods", "secrets", "services", "persistentvolumeclaims"]pkg/lineage/lineage.go (8)
96-100: Allow HR-label fallback when owners exist but none could be resolved.The early return prevents falling back to the HR label if all owner fetches failed (RBAC, NotFound, unknown GVK). Return only when you actually appended results from owners.
Apply:
- if len(ownerRefs) > 0 { - return - } + if len(ownerRefs) > 0 && len(out) > 0 { + return + }
85-94: Prefer controller owner when present.Follow the OwnerReference with controller=true first; otherwise fall back to all owners. This reduces branching and avoids non-controller refs.
Apply:
- ownerRefs := obj.GetOwnerReferences() - for _, owner := range ownerRefs { + ownerRefs := obj.GetOwnerReferences() + // prefer controller owner if present + var toVisit []metav1.OwnerReference + for i := range ownerRefs { + if ownerRefs[i].Controller != nil && *ownerRefs[i].Controller { + toVisit = []metav1.OwnerReference{ownerRefs[i]} + break + } + } + if len(toVisit) == 0 { + toVisit = ownerRefs + } + for _, owner := range toVisit { ownerObj, err := getUnstructuredObject(ctx, client, mapper, owner.APIVersion, owner.Kind, obj.GetNamespace(), owner.Name) if err != nil { - fmt.Fprintf(os.Stderr, "Could not fetch owner %s/%s (%s): %v\n", obj.GetNamespace(), owner.Name, owner.Kind, err) + l.Error(err, "Could not fetch owner", "namespace", obj.GetNamespace(), "name", owner.Name, "kind", owner.Kind) continue } out = append(out, WalkOwnershipGraph(ctx, client, mapper, appMapper, ownerObj, visited)...) }
6-6: Use structured logging; drop unused os import.Keep logs consistent with controller-runtime.
Apply:
-import "os"- fmt.Fprintf(os.Stderr, "Could not fetch owner %s/%s (%s): %v\n", obj.GetNamespace(), owner.Name, owner.Kind, err) + l.Error(err, "Could not fetch owner", "namespace", obj.GetNamespace(), "name", owner.Name, "kind", owner.Kind)Also applies to: 89-91
112-121: Validate prefix before TrimPrefix to avoid accidental name truncation.Only strip when HR name actually has the returned prefix.
Apply:
- a, k, p, err := appMapper.Map(hr) + a, k, p, err := appMapper.Map(hr) if err != nil { break } - ownerObj, err := getUnstructuredObject(ctx, client, mapper, a, k, obj.GetNamespace(), strings.TrimPrefix(obj.GetName(), p)) + ownerName := obj.GetName() + if p != "" { + if !strings.HasPrefix(ownerName, p) { + break + } + ownerName = strings.TrimPrefix(ownerName, p) + } + ownerObj, err := getUnstructuredObject(ctx, client, mapper, a, k, obj.GetNamespace(), ownerName)
71-77: Fix log message: expected arg count is 5|6.Apply:
- "args passed", len(memory)+5, "expected args", "4|5", + "args passed", len(memory)+5, "expected args", "5|6",
49-50: Consider using UID in visit-key to harden cycle detection.APIVersion/Kind/Namespace/Name can be reused after deletion; including UID removes ambiguity in edge cases.
If acceptable, extend ObjectID with UID and set it from obj.GetUID() when computing id. I can draft a minimal patch if you want.
Also applies to: 79-84
146-183: Optional: cache REST mappings to cut discovery churn.getUnstructuredObject resolves a RESTMapping for every hop. If this runs per admission, consider a RESTMapper with caching or a small GVK→Resource map in-memory.
40-48: Add tests for failure/fallback paths.Please add unit tests covering: (1) owners present but all fetches fail → HR-label fallback triggers; (2) HR label empty → no Get call; (3) HR Map prefix mismatch → break without panic; (4) controller vs non-controller owners.
I can generate table-driven tests for these cases—want me to open a follow-up?
Also applies to: 132-144
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (12)
cmd/cozystack-controller/main.go(2 hunks)internal/lineagecontrollerwebhook/config.go(1 hunks)internal/lineagecontrollerwebhook/controller.go(1 hunks)internal/lineagecontrollerwebhook/types.go(1 hunks)internal/lineagecontrollerwebhook/webhook.go(1 hunks)packages/system/cozystack-controller/templates/certmanager.yaml(1 hunks)packages/system/cozystack-controller/templates/deployment.yaml(1 hunks)packages/system/cozystack-controller/templates/mutatingwebhookconfiguration.yaml(1 hunks)packages/system/cozystack-controller/templates/service.yaml(1 hunks)pkg/lineage/lineage.go(1 hunks)pkg/lineage/lineage_test.go(1 hunks)pkg/lineage/mapper.go(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (8)
- packages/system/cozystack-controller/templates/service.yaml
- internal/lineagecontrollerwebhook/webhook.go
- cmd/cozystack-controller/main.go
- pkg/lineage/mapper.go
- pkg/lineage/lineage_test.go
- packages/system/cozystack-controller/templates/deployment.yaml
- packages/system/cozystack-controller/templates/certmanager.yaml
- internal/lineagecontrollerwebhook/controller.go
🧰 Additional context used
🧬 Code graph analysis (3)
internal/lineagecontrollerwebhook/types.go (1)
pkg/apiserver/apiserver.go (1)
Scheme(41-41)
internal/lineagecontrollerwebhook/config.go (2)
internal/lineagecontrollerwebhook/types.go (1)
LineageControllerWebhook(15-23)api/v1alpha1/cozystackresourcedefinitions_types.go (1)
SourceRef(60-69)
pkg/lineage/lineage.go (1)
pkg/lineage/mapper.go (1)
AppMapper(10-12)
🪛 YAMLlint (1.37.1)
packages/system/cozystack-controller/templates/mutatingwebhookconfiguration.yaml
[warning] 22-22: too few spaces after comma
(commas)
[error] 6-6: syntax error: expected , but found ''
(syntax)
⏰ 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 (1)
internal/lineagecontrollerwebhook/types.go (1)
18-18: decoder should be a pointer to match admission.DecoderInjector.controller-runtime expects InjectDecoder(d *admission.Decoder); storing a non-pointer will cause assignment/compile issues.
- decoder admission.Decoder + decoder *admission.DecoderLikely an incorrect or invalid review comment.
Many resources created as part of managed apps in cozystack (pods, secrets, etc) do not carry predictable labels that unambiguously indicate which app originally triggered their creation. Some resources are managed by controllers and other custom resources and this indirection can lead to loss of information. Other controllers sometimes simply do not allow setting labels on controlled resources and the latter do not inherit labels from the owner. This patch implements a webhook that sidesteps this problem with a universal solution. On creation of a pod/secret/PVC etc it walks through the owner references until a HelmRelease is found that can be matched with a managed app dynamically registered in the Cozystack API server. The pod is mutated with labels identifying the managed app. ```release-note [cozystack-controller] Add a mutating webhook to identify the Cozystack managed app that ultimately owns low-level resources created in the cluster and label these resources with a reference to said app. ``` Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
e8fa1ac to
e1b97e3
Compare
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (5)
packages/system/cozystack-controller/templates/mutatingwebhookconfiguration.yaml (3)
10-10: Rename webhook to a stable, descriptive DNS namePast feedback still applies; prefer lineage-webhook.cozystack.io for clarity.
Apply:
- - name: lineage.cozystack.io + - name: lineage-webhook.cozystack.io
23-23: Avoid blocking creates if webhook is downSince this webhook only adds labels, prefer failurePolicy: Ignore to prevent outages.
Apply:
- failurePolicy: Fail + failurePolicy: Ignore
6-6: Broken CA injection annotation and templating; also target the secret created by cert-managerThe value is unquoted Helm+literal causing YAML parse errors, and it points to a non-existent object. certmanager.yaml creates cozystack-controller-webhook-cert (a Secret). Use inject-ca-from-secret and quote.
Apply:
- cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/cozystack-controller-webhook + cert-manager.io/inject-ca-from-secret: '{{ .Release.Namespace }}/cozystack-controller-webhook-cert'pkg/lineage/lineage_test.go (2)
29-42: Don’t crash in init; handle config and client errors defensivelyUsing GetConfigOrDie and ignoring errors can crash tests. Initialize conditionally and skip when cluster config isn’t available.
Apply:
-func init() { - cfg := config.GetConfigOrDie() - - dynClient, _ = dynamic.NewForConfig(cfg) - - discoClient, _ := discovery.NewDiscoveryClientForConfig(cfg) - - cachedDisco := memory.NewMemCacheClient(discoClient) - mapper = restmapper.NewDeferredDiscoveryRESTMapper(cachedDisco) - - zapLogger, _ := zap.NewDevelopment() - l = zapr.NewLogger(zapLogger) - ctx = logr.NewContext(context.Background(), l) -} +func TestMain(m *testing.M) { + cfg, err := config.GetConfig() + if err != nil { + // No cluster; skip all tests gracefully. + os.Exit(0) + } + if dynClient, err = dynamic.NewForConfig(cfg); err != nil { + os.Exit(0) + } + discoClient, err := discovery.NewDiscoveryClientForConfig(cfg) + if err != nil { + os.Exit(0) + } + cachedDisco := memory.NewMemCacheClient(discoClient) + mapper = restmapper.NewDeferredDiscoveryRESTMapper(cachedDisco) + zapLogger, err := zap.NewDevelopment() + if err != nil { + os.Exit(0) + } + l = zapr.NewLogger(zapLogger) + ctx = logr.NewContext(context.Background(), l) + os.Exit(m.Run()) +}
44-53: Fix signature mismatch; avoid os.Args; gate as opt-in integration testCall WalkOwnershipGraph with an AppMapper, read namespace/name from env (or flags), and log via t.Logf.
Apply:
-func TestWalkingOwnershipGraph(t *testing.T) { - obj, err := dynClient.Resource(schema.GroupVersionResource{"", "v1", "pods"}).Namespace(os.Args[1]).Get(ctx, os.Args[2], metav1.GetOptions{}) - if err != nil { - t.Fatal(err) - } - nodes := WalkOwnershipGraph(ctx, dynClient, mapper, obj) - for _, node := range nodes { - fmt.Printf("%#v\n", node) - } -} +func TestWalkingOwnershipGraph(t *testing.T) { + if dynClient == nil || mapper == nil { + t.Skip("dynamic client/mapper not configured") + } + if os.Getenv("RUN_LINEAGE_INTEGRATION") != "true" { + t.Skip("opt-in via RUN_LINEAGE_INTEGRATION=true") + } + ns := os.Getenv("E2E_NS") + name := os.Getenv("E2E_NAME") + if ns == "" || name == "" { + t.Skip("set E2E_NS and E2E_NAME") + } + gvr := schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"} + obj, err := dynClient.Resource(gvr).Namespace(ns).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + t.Fatal(err) + } + nodes := WalkOwnershipGraph(ctx, dynClient, mapper, noopMapper{}, obj) + for _, node := range nodes { + t.Logf("%#v", node) + } +}Add this helper in the file:
// Place near the imports. import helmv2 "github.com/fluxcd/helm-controller/api/v2" // Place above TestWalkingOwnershipGraph. type noopMapper struct{} func (noopMapper) Map(*helmv2.HelmRelease) (string, string, string, error) { return "", "", "", fmt.Errorf("no mapping") }
🧹 Nitpick comments (4)
cmd/cozystack-controller/main.go (1)
218-231: Registering both controller and webhook is fine; consider a feature flagLooks good as a dual registrar. Consider a cmd flag (e.g., --enable-lineage-webhook) to allow disabling during staged rollouts.
packages/system/cozystack-controller/templates/mutatingwebhookconfiguration.yaml (3)
4-4: Scope the object name to this chart/releaseBare “lineage” is too generic and risks collisions. Use a chart-scoped name.
Apply either:
- name: lineage + name: cozystack-controller-lineageor (if you have a fullname helper):
- name: lineage + name: {{ include "cozystack-controller.fullname" . }}-lineage
11-17: Harden admission settings (reinvocation, timeout, match policy)Add defensive defaults to avoid long API-server stalls and enable reinvocation.
Apply:
- name: lineage.cozystack.io admissionReviewVersions: ["v1"] sideEffects: None + reinvocationPolicy: IfNeeded + timeoutSeconds: 5 + matchPolicy: Equivalent clientConfig:
22-22: Style nit: add spaces after commasMinor YAMLlint warning.
Apply:
- resources: ["pods","secrets", "services", "persistentvolumeclaims"] + resources: ["pods", "secrets", "services", "persistentvolumeclaims"]
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (12)
cmd/cozystack-controller/main.go(2 hunks)internal/lineagecontrollerwebhook/config.go(1 hunks)internal/lineagecontrollerwebhook/controller.go(1 hunks)internal/lineagecontrollerwebhook/types.go(1 hunks)internal/lineagecontrollerwebhook/webhook.go(1 hunks)packages/system/cozystack-controller/templates/certmanager.yaml(1 hunks)packages/system/cozystack-controller/templates/deployment.yaml(1 hunks)packages/system/cozystack-controller/templates/mutatingwebhookconfiguration.yaml(1 hunks)packages/system/cozystack-controller/templates/service.yaml(1 hunks)pkg/lineage/lineage.go(1 hunks)pkg/lineage/lineage_test.go(1 hunks)pkg/lineage/mapper.go(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (8)
- packages/system/cozystack-controller/templates/service.yaml
- pkg/lineage/mapper.go
- pkg/lineage/lineage.go
- internal/lineagecontrollerwebhook/types.go
- internal/lineagecontrollerwebhook/config.go
- internal/lineagecontrollerwebhook/webhook.go
- internal/lineagecontrollerwebhook/controller.go
- packages/system/cozystack-controller/templates/certmanager.yaml
🧰 Additional context used
🧬 Code graph analysis (2)
pkg/lineage/lineage_test.go (1)
pkg/lineage/lineage.go (1)
WalkOwnershipGraph(40-144)
cmd/cozystack-controller/main.go (1)
internal/lineagecontrollerwebhook/types.go (1)
LineageControllerWebhook(15-23)
🪛 YAMLlint (1.37.1)
packages/system/cozystack-controller/templates/mutatingwebhookconfiguration.yaml
[warning] 22-22: too few spaces after comma
(commas)
[error] 6-6: syntax error: expected , but found ''
(syntax)
⏰ 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 (4)
packages/system/cozystack-controller/templates/deployment.yaml (2)
31-37: Webhook port and cert mount — LGTMPort 9443 and the serving-certs mount align with controller-runtime defaults. No concerns here.
38-42: Fix defaultMode and confirm non-root readability
- File: packages/system/cozystack-controller/templates/deployment.yaml (lines 38–42)
- Replace ambiguous octal literal with decimal:
- defaultMode: 0400 + # 0400 (octal) == 256 (decimal) + defaultMode: 256
- If the pod runs as non-root, either set spec.securityContext.fsGroup to the container's group and use 0440 (288 decimal) so the group can read, or keep defaultMode 256 and run as root. Example (replace with the correct group id):
spec: + securityContext: + fsGroup: <GID> ... - # 0400 (octal) == 256 (decimal) - defaultMode: 256 + # 0440 (octal) == 288 (decimal) + defaultMode: 288cmd/cozystack-controller/main.go (1)
41-41: Import alias for lineage controller/webhook — LGTMClear aliasing; no issues.
packages/system/cozystack-controller/templates/mutatingwebhookconfiguration.yaml (1)
18-22: Confirm resource scope (do you need to mutate Services?)PR text mentions pods/secrets/PVCs “etc.” If Services aren’t mutated by implementation, remove to reduce blast radius.
Reverts #1400 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * New Features * None * Refactor * Removed the lineage mutating admission webhook and its controller logic; objects are no longer auto-labeled/mutated. * Deployment now targets the cozy-system namespace and no longer exposes a webhook port or mounts webhook certs. * Chores * Removed Service and cert-manager resources previously used for webhook TLS; cert-manager is no longer required. * Tests * Removed lineage-related tests. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
What this PR does
Many resources created as part of managed apps in cozystack (pods, secrets, etc) do not carry predictable labels that unambiguously indicate which app originally triggered their creation. Some resources are managed by controllers and other custom resources and this indirection can lead to loss of information. Other controllers sometimes simply do not allow setting labels on controlled resources and the latter do not inherit labels from the owner. This patch implements a webhook that sidesteps this problem with a universal solution. On creation of a pod/secret/PVC etc it walks through the owner references until a HelmRelease is found that can be matched with a managed app dynamically registered in the Cozystack API server. The pod is mutated with labels identifying the managed app.
Release note
Summary by CodeRabbit
New Features
Tests