Skip to content

KubePatternFind the smells hiding between your resources

Every manifest is valid on its own. The defect shows up only in the relation: an unused definition, a dangling reference, a missing companion. KubePattern finds them across any Kubernetes CRD, with patterns you write as YAML.

How it works

One scan, four steps ​

KubePattern is a Go analyzer that runs inside your cluster. Each scan reads your patterns, fetches what they need, and writes back one Smell per defect it finds.

  1. 1

    Read the patterns

    Patterns are custom resources in your cluster. A linter checks each one first: a malformed pattern is logged and skipped, never run.

  2. 2

    Fetch only what they need

    Kinds are resolved at run time through API discovery, so custom resources unknown at build time work. Only the kinds a pattern names are listed.

  3. 3

    Build the graph

    Resources become the vertices of a graph, built once per run. Each relationship in a pattern is a query over it.

  4. 4

    Write the smells

    Every target that matches becomes a Smell, with a stable identity and a first observation. Fixed smells disappear on the next run.

Pattern-as-Code

A smell is a few lines of YAML ​

A pattern describes the defect, not the rule. Name the resource it is about, the resources that give it context, and the relation that makes it a smell.

  • The target is fixed. A smell may come from several resources, but it is always reported on one, so it is never ambiguous.
  • Absence is a first-class idea. An orphan or a dangling reference is written as matchNone: no instance of the dependency relates to the target.
  • Findings are resources too. Each Smell has a stable name, a severity, a message and the exact target.

Read the Pattern reference

yaml
apiVersion: kubepattern.dev/v1
kind: Pattern
metadata:
  name: cnpg-cluster-without-scheduledbackup
spec:
  displayName: PostgreSQL Cluster Without Scheduled Backup
  category: Reliability
  severity: HIGH
  for: 10m
  message: "Cluster {{target.metadata.name}} has no ScheduledBackup."
  target:
    kind: Cluster
    apiVersion: postgresql.cnpg.io/v1
    plural: clusters
  dependencies:
    - id: schedule
      kind: ScheduledBackup
      apiVersion: postgresql.cnpg.io/v1
      plural: scheduledbackups
  relationships:
    matchNone:
      - with: schedule
        type: custom
        criteria:
          - targetPath: "metadata.name"
            dependencyPath: "spec.cluster.name"
            operator: EQUALS
          - targetPath: "metadata.namespace"
            dependencyPath: "metadata.namespace"
            operator: EQUALS
yaml
apiVersion: kubepattern.dev/v1
kind: Smell
metadata:
  name: cnpg-cluster-without-scheduledbackup-9c1e4d2a-5b7f-4c1e-8a3d-2f6b0e91c7a4
  namespace: orders
  labels:
    kubepattern.dev/phase: Pending
spec:
  name: cnpg-cluster-without-scheduledbackup
  category: Reliability
  severity: HIGH
  phase: Pending
  since: "2026-10-11T09:00:04Z"
  suppress: false
  message: "Cluster pg-orders has no ScheduledBackup."
  target:
    apiVersion: postgresql.cnpg.io/v1
    kind: Cluster
    name: pg-orders
    namespace: orders
    uid: 9c1e4d2a-5b7f-4c1e-8a3d-2f6b0e91c7a4

What it finds

Four families of relational smell ​

Each family is a relation that is missing, or one that should not be there. The same engine finds them in GitOps tools, operators, control planes and platform portals, with no code changes.

  • Unused / orphan. A definition exists, but nothing refers to it, such as a ClusterIssuer that no Certificate uses.
  • Dangling reference. A resource points at something that does not exist, such as a Composition for a kind that no XRD defines.
  • Missing companion. A resource lacks the sibling that should accompany it, such as a PostgreSQL Cluster with no ScheduledBackup.
  • Misconfiguration. A relation exists that should not, such as a Pooler named like a Cluster in the same namespace.

See how a pattern describes them

Built for GitOps timing

Quiet about transient states ​

A GitOps tool that re-creates a referrer makes a healthy resource look unused for a few minutes. Give a pattern a minimum duration and the finding stays Pending until the condition has persisted.

  • First observation is kept. Each Smell records spec.since and keeps its identity across runs.
  • Fixed means gone. When the condition stops holding, the Smell is deleted and the clock starts again.
  • Your call survives. Suppress a false positive once and the suppression holds on later runs.

How the lifecycle works

Hourly CronJob · pattern with for: 90m

A transient state

A GitOps tool deletes and re-creates the only referrer of a resource. For a few minutes the resource looks unused.

RowRun 1Run 2Run 3Run 4Run 5
Condition holdsNoYesNoNoNo
SmellNo smellPendingDeletedNo smellNo smell

Never becomes Active. Nobody is paged for a blip.

A real smell

Nothing refers to the resource, and nothing is going to.

RowRun 1Run 2Run 3Run 4Run 5
Condition holdsNoYesYesYesYes
SmellNo smellPendingPendingActiveActive

Becomes Active at run 4, the first run at least 90 minutes after the first observation.

Run your first scan in a minute ​

Install the chart, apply a pattern, and read the results with kubectl get smells -A.

bash
helm upgrade --install kubepattern \
  oci://ghcr.io/kubepattern/charts/kubepattern \
  --version <VERSION> --namespace kubepattern-system --create-namespace