Skip to content

[FEATURE] Per-rule objectSelector on (Cluster)WatchRule rules #146

Description

@sunib

This request comes up more than once in my conversations with operators — I'm filing it with the Cozystack TenantSecret situation as a worked example, but the underlying need is general: "capture this curated, label-tagged subset of native objects, not the whole kind."

Is your feature request related to a problem? Please describe.

Today WatchRule.spec.rules[] and ClusterWatchRule.spec.rules[] only support a namespaceSelector for scoping. There is no per-rule object-level label selector. That makes some natural "watch the curated subset" use cases impossible to express without dragging the whole kind along.

Concrete worked example (Cozystack):

  • Tenant namespaces typically contain ~40+ native Secrets (Helm release state, ServiceAccount tokens, cert-manager material, operator-internal credentials, ...).
  • Cozystack tags the ~5 user-facing handoff credentials with internal.cozystack.io/tenantresource=true (and exposes them through the TenantSecret aggregated kind, which is a label-gated facade).
  • We want to capture those 5 into git as the GitOps source-of-truth for the tenant. Watching v1/secrets captures all ~40+; the natural fix is "filter by that label."

I've heard structurally the same ask from operators with totally different stacks (ExternalSecrets-managed Secrets, app.kubernetes.io/part-of=…-tagged ConfigMaps, etc.). It is not a Cozystack-specific need.

Describe the solution you'd like

A per-rule objectSelector (or labelSelector) field on WatchRuleSpec.rules[] and ClusterWatchRuleSpec.rules[], accepting a standard metav1.LabelSelector. Semantics:

  • If unset (the default), behaves identically to today.
  • If set, an inbound audit event matches the rule only if the underlying object's metadata.labels matches the selector. The selector is applied by the watch/audit manager when filtering events, the same place namespaceSelector is applied today.
  • The selector composes with namespaceSelector via AND, matching how Kubernetes label-selector semantics typically work.

Example yaml shape:

apiVersion: configbutler.ai/v1alpha1
kind: ClusterWatchRule
metadata:
  name: tenant-handoff-secrets
spec:
  targetRef:
    name: cozystack-example
    namespace: gitops-reverser
  rules:
    - scope: Namespaced
      operations: [CREATE, UPDATE, DELETE]
      apiGroups: [""]
      apiVersions: ["v1"]
      resources: ["secrets"]
      objectSelector:
        matchLabels:
          internal.cozystack.io/tenantresource: "true"

Describe alternatives you've considered

  • Go through an aggregated-API facade that pre-filters by label (the TenantSecret route). Works for Cozystack today but: (a) requires inserting apiservice-audit-proxy in front of the aggregated API to recover body-rich audit events, (b) the relevant operator-driven rotations actually happen at the v1/secrets layer and bypass the facade, so a watch on the aggregated kind is mostly silent in steady state, (c) doesn't generalise to other platforms.
  • Post-filter in the BranchWorker / commit pipeline instead of in the rule. Works structurally but pushes platform-specific knowledge into the data path and gives the operator no way to express the curated set declaratively.
  • One rule per object name — impractical when objects are dynamic (rotating credentials, autogenerated names) and bounds the number of selector requirements unnaturally.

Additional context

Implementation considerations

  • The LabelSelector lives on each rules[] entry, not at the spec root, so different rules in the same (Cluster)WatchRule can carry different selectors.
  • The watch manager applies it after the audit event is decoded enough to read metadata.labels. For aggregated APIs routed through apiservice-audit-proxy, metadata.labels arrives in the enriched body alongside the rest of the object; for native APIs it's already present in the canonical audit event.
  • Conversion to labels.Selector happens once at rule reconciliation, not per event; non-selectable selectors (e.g. labels.Nothing()) should be a validation error on the rule.
  • Default behaviour (no objectSelector) is structurally identical to today, so the field is backwards-compatible.

Priority

  • Medium
  • Low / High / Critical

(Medium because there are workarounds — aggregated-API facade, post-filter — but they all carry real costs, and this is a fairly common shape.)

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions