Skip to content

Patterns ​

A Pattern is a custom resource that describes one relational smell. It is the core building block of KubePattern: you codify what a defect looks like, and the analysis engine reports every resource that matches it.

Describe the defect, not the rule

A pattern states the condition that a defective resource satisfies, so every match is a finding. Write what is wrong, not what would be right.

This condition is not always a negative one. An orphan or a dangling reference is an absence (matchNone). A pooler named like another database cluster is the presence of a relation (matchAll).

Not in a tagged release yet

These parts of the language are on the main branch and arrive with the next release: element scope ([@]), indexes and quoted keys in paths, kind wildcards, categories and derived kinds, label selectors, defaults, transforms, literal values, typed EQUALS and valuesFrom. The latest tagged release at the time of writing is v0.0.6. A pattern that does not use them behaves exactly as before. To use them now, see Try unreleased features. Do not apply such a pattern to an older engine: its linter does not reject them all, and a [@] path or a default it does not know can make the pattern report every target.

Anatomy of a pattern ​

A pattern has three parts. The target is the resource the smell is reported on. The dependencies are the other resources that must be inspected to judge it. The relationships are the relations that the target is expected to have with them.

yaml
apiVersion: kubepattern.dev/v1
kind: Pattern
metadata:
  name: argocd-appproject-empty
spec:
  # Descriptive fields, copied into every finding
  displayName: AppProject Without Applications
  category: Security
  severity: LOW
  message: "AppProject {{target.metadata.name}} grants permissions but no Application uses it."
  reference: "https://argo-cd.readthedocs.io/en/stable/user-guide/projects/"

  # What the smell is about
  target:
    kind: AppProject
    apiVersion: argoproj.io/v1alpha1
    plural: appprojects
    # Narrow the population before any relation is evaluated
    filters:
      matchNone:
        - path: "metadata.name"
          operator: EQUALS
          values: ["default"]

  # What gives it context
  dependencies:
    - id: application
      kind: Application
      apiVersion: argoproj.io/v1alpha1
      plural: applications

  # The relation that makes it a smell
  relationships:
    matchNone:
      - with: application
        type: custom
        criteria:
          - targetPath: "metadata.name"
            dependencyPath: "spec.project"
            operator: EQUALS

Read as a whole: an AppProject other than default is defective when no Application names it in spec.project.

A pattern contains no expressions. Its checks are paths, operators and values placed inside this fixed structure.

API reference v1 ​

Root ​

PropertyTypeRequiredDescription
apiVersionstringYesAlways kubepattern.dev/v1.
kindstringYesAlways Pattern.
metadata.namestringYesUnique name of the pattern, a valid Kubernetes name (for example my-pattern). Patterns are cluster-scoped.
specobjectYesThe specification of the smell.

spec ​

PropertyTypeRequiredDescription
displayNamestringYesHuman-readable name of the pattern.
categorystringYesFree-form category, copied into every finding (for example Security, Reliability, Architecture, Cost).
severityenumYesLOW, MEDIUM, HIGH or CRITICAL.
messagestringYesMessage of every finding. Supports placeholders.
referencestringNoURL of documentation on how to fix the smell.
for next releasedurationNoMinimum time the condition must hold before a finding becomes Active, such as 10m or 24h. See Lifecycle. Empty means 0s.
targetobjectYesThe resource the smell is reported on.
dependenciesarrayNoOther resources that give the target context.
relationshipsobjectNoThe relations the target has with its dependencies.

displayName, category, severity and message are descriptive. They play no part in detection.

target ​

PropertyTypeRequiredDescription
kindstringYesKind of the resource, for example Deployment or ClusterIssuer. "*" stands for every kind of the API version.
apiVersionstringYesAPI version of the kind, for example apps/v1.
pluralstringYesPlural resource name, which the dynamic client needs to list the kind. Omitted when kind is "*".
categorystringNoEvery kind listed under this discovery category. Replaces kind, apiVersion and plural.
kindsarrayNoA list of resource types, each in one of the forms above. Replaces kind, apiVersion and plural.
filtersobjectNoPredicates that narrow the population before any relation is evaluated.
emitOnEmpty next releaseboolNoWhen no resource matches the target, emit a single synthetic Smell. See absence patterns.

A target uses exactly one of kind, category or kinds. Resource types describes the forms that select more than one kind.

dependencies[] ​

A dependency is described like the target, plus an id.

PropertyTypeRequiredDescription
idstringYesUnique identifier, used by relationships[].with.
kind, apiVersion, plural, category, kindsYesAs in target.
fromTargetobjectNoInstead of a fixed kind: the kinds named by fields of the targets. See derived kinds.
filtersobjectNoPredicates on the dependency's own fields.

Dependencies are cluster-wide. When a smell only makes sense within a namespace, make the namespace an explicit criterion, as in the CloudNativePG example below.

Paths ​

Filters and criteria address the fields of a resource with paths. Segments are separated by dots.

SyntaxMeaning
spec.secretStoreRef.namea nested field
spec.containers[*].imageevery element of an array. The values of different elements are not correlated.
spec.data[@].namethe element bound by the evaluation. See element scope.
spec.data[0].nameone element
metadata.labels['app.kubernetes.io/name']a key that contains dots or slashes. ["..."] works too.

A path that reaches nothing yields no value. A criterion with no value on one side does not hold, a filter EXISTS is false, and IS_EMPTY is true.

Filters ​

A filter is a predicate on a single resource: a path, an operator and the values to compare against. Filters restrict the population before any relation is evaluated, so they express the scope of a pattern rather than its detection logic.

PropertyTypeDescription
matchAllarrayEvery condition must hold.
matchAnyarrayAt least one condition must hold.
matchNonearrayNo condition may hold.

Each condition has:

PropertyTypeRequiredDescription
pathstringYesA path into the object, such as metadata.namespace or spec.containers[*].image. [@] is allowed in target filters only.
operatorenumYesOne of the filter operators.
valuesarrayDependsValues to compare with. Required for every operator except EXISTS and IS_EMPTY, which must have none.
valuesFromstringNoInstead of values, with EQUALS only: compare with the values at another path of the same resource, under the same element binding.

Relationships ​

A filter constrains one resource, while a relationship constrains a pair.

PropertyTypeDescription
matchAllarrayEvery relationship must hold.
matchAnyarrayAt least one relationship must hold.
matchNonearrayNo relationship may hold.

Each entry has:

PropertyTypeRequiredDescription
withstringYesThe id of the dependency.
typeenumYescustom, owns, ownedBy, selects or selectedBy. See relationship types.
criteriaarrayFor customThe fields that define the relation. Must be omitted for owns and ownedBy. Optional for selects and selectedBy, where they add conditions to the selection.
selectorPathstringNoFor selects and selectedBy: where the label selector is. Defaults to spec.selector.
emptySelectorenumNoFor selects and selectedBy: what a selector without requirements selects, all or none. See label selectors.

A relationship holds for the target when at least one instance of the dependency satisfies it, so a single entry is an existential statement. Inside matchNone, it is negated: no instance of the dependency relates to the target in the declared way. That is exactly what an orphan or an unused definition is.

When a dependency has no instances at all

matchNone is satisfied, so every target yields a finding. That is the intended reading when nobody has created any instance of the kind. A dependency kind that is not installed or not readable is different: the pattern is skipped instead of reporting everything.

Criteria ​

Each criterion of a relationship compares a value of the target with a value of the dependency.

PropertyTypeRequiredDescription
targetPathstringYesPath into the target, unless targetValue is set.
dependencyPathstringYesPath into the dependency, unless dependencyValue is set. [*] ranges over an array, and may be nested.
operatorenumYesEQUALS.
targetValue, dependencyValuestringNoA literal instead of the path on that side, for example to require that the bound element has resource: pages. At most one side can be a literal.
targetDefault, dependencyDefaultobjectNoThe value to use when the path yields nothing. See defaults.
targetTransform, dependencyTransformobjectNoRewrites each value before the comparison. See transforms.

EQUALS holds when the values extracted from the two sides have a non-empty intersection. All the criteria of one entry must hold for the same dependency instance, and with [@], for the same bound element.

Defaults ​

Many APIs fill in a field that is omitted. A reference without a namespace points to the referrer's own namespace, and a store reference without a kind means a SecretStore. A default supplies that value, so the omitted field is compared like an explicit one.

yaml
criteria:
  - targetPath: "metadata.namespace"
    dependencyPath: "spec.sourceRef.namespace"
    dependencyDefault: {path: "metadata.namespace"}   # another field of the same resource
    operator: EQUALS
  - targetPath: "kind"
    dependencyPath: "spec.secretStoreRef.kind"
    dependencyDefault: {value: SecretStore}           # or a literal
    operator: EQUALS

A default sets exactly one of path or value. It cannot be used with a [*] path, where it would apply only when no element has a value. Use [@] to default each element. The default's own path cannot contain [*] either.

Transforms ​

Some references are encoded in a string: a group inside an API version, a name before an @ revision, a value inside a URL. A transform extracts the part to compare. It is applied to each value, after the default.

TransformKeepsExample
{apiGroup: true}the group of an API versioncert-manager.io/v1 → cert-manager.io, and v1 → empty
{split: {separator: "@", index: 0}}one part of the value. A negative index counts from the end.webservice@v2 → webservice
{regex: "name=([^&]+)"}the first capture group. A value that does not match is dropped.?name=pg&ns=a → pg
{lowercase: true}the value in lower caseClusterIssuer → clusterissuer

A transform sets exactly one of these, and a regex must have exactly one capture group.

Resource types ​

A target or a dependency can stand for several kinds at once, resolved through API discovery at run time.

FormExampleSelects
one kind{kind: Secret, apiVersion: v1, plural: secrets}that kind
every kind of a group version{kind: "*", apiVersion: widgets.templates.krateo.io/v1beta1}the listable kinds that discovery finds in it
a category{category: fluxcd-appliers}every kind whose discovery categories include it
a listkinds: [{kind: Markdown, ...}, {kind: "*", ...}, {category: ...}]the union of its entries
derived from the targets (dependencies only)fromTarget: {apiVersionPath: status.apiVersion, kindPath: status.kind}the kinds named by fields of the targets. See derived kinds.

In this Krateo pattern, one target covers three widget kinds, and one dependency covers two:

yaml
message: "{{target.kind}} {{target.metadata.namespace}}/{{target.metadata.name}} is labelled managed-by a RESTAction or Table that does not exist in its namespace."
target:
  kinds:
    - {kind: Markdown, apiVersion: widgets.templates.krateo.io/v1beta1, plural: markdowns}
    - {kind: YamlViewer, apiVersion: widgets.templates.krateo.io/v1beta1, plural: yamlviewers}
    - {kind: RESTAction, apiVersion: templates.krateo.io/v1, plural: restactions}
  filters:
    matchAll:
      - {path: "metadata.labels['managed-by']", operator: EXISTS}
dependencies:
  - id: manager
    kinds:
      - {kind: RESTAction, apiVersion: templates.krateo.io/v1, plural: restactions}
      - {kind: Table, apiVersion: widgets.templates.krateo.io/v1beta1, plural: tables}
  • {{target.kind}} in the message names the kind of each finding's target.
  • A category resolves to the server's preferred version of each kind.
  • An unknown group version in a kind: "*" makes the pattern be skipped, as a missing CRD does.
  • A category that matches nothing is not an error: it simply has no instances. As a dependency under matchNone, it then reports every target, so check the category name with kubectl api-resources --categories=<name>.

Derived kinds ​

Some resources name the kind they relate to in their own fields, as a definition does for the kind it generates. fromTarget reads that kind from each target instead of listing it in the pattern.

PropertyDescription
apiVersionPath or apiVersionPath to the API version in the target, or a fixed one. Exactly one is set.
kindPathPath to the kind in the target. Required.
pluralPathPath to the plural resource name. When it is omitted, discovery finds it.

The target filters apply, so only the kinds named by actual candidates are fetched. With [@] in the paths, as in spec.generates[@].kind, each element names one kind. [*] is rejected, because it would pair the API version of one element with the kind of another. A kind that the API server does not serve has no instances, and a warning is logged. The CompositionDefinition example uses it.

Element scope ​

With [*], every criterion ranges over the whole array on its own, so a name found in one entry and a namespace found in another count as a match. With [@], all the paths of one resource that share the prefix up to the [@] refer to the same element. The engine enumerates the elements and evaluates one binding at a time.

  • On the dependency side, a dependency matches when one of its elements satisfies all the criteria. spec.data[@].sourceRef.storeRef.name and spec.data[@].sourceRef.storeRef.kind are read from the same entry.
  • On the target side, the target filters and all the relationships share the target's bindings. The target is defective when the relationships hold for at least one element that passes the target filters. Under matchNone, this reads "some reference resolves to nothing".

This RoleBinding pattern examines each ServiceAccount subject on its own. The binding is defective when one of them does not exist, even if the others do. A subject without a namespace defaults to the binding's own.

yaml
target:
  kind: RoleBinding
  apiVersion: rbac.authorization.k8s.io/v1
  plural: rolebindings
  filters:
    matchAll:                                   # examine ServiceAccount subjects only
      - {path: "subjects[@].kind", operator: EQUALS, values: [ServiceAccount]}
dependencies:
  - {id: serviceaccount, kind: ServiceAccount, apiVersion: v1, plural: serviceaccounts}
relationships:
  matchNone:                                    # defective if SOME subject does not exist
    - with: serviceaccount
      type: custom
      criteria:
        - {targetPath: "subjects[@].name", dependencyPath: metadata.name, operator: EQUALS}
        - {targetPath: "subjects[@].namespace", targetDefault: {path: metadata.namespace},
           dependencyPath: metadata.namespace, operator: EQUALS}

The linter enforces these rules:

  • [@] is not allowed in dependency filters, because dependencies are filtered before any binding. It cannot follow a [*] in the same path.
  • The anchors of one side form a single nested chain. spec.rules[@] and spec.rules[@].refs[@] together are fine. spec.data[@] and spec.dataFrom[@] together are rejected, because two independent arrays would bind their cartesian product, which is empty as soon as one of them is. The target side covers the target filters and every relationship. The dependency side is checked per relationship, so two relationships may anchor different arrays of the same dependency.
  • A target whose anchored array is missing or empty has no binding and is not a candidate. A RoleBinding without subjects has no dangling subject.
  • When the target side has anchors, every relationship must read the innermost anchored element, through a target path or a target default. A relationship that ignores it, such as owns, would never be evaluated for a target without the element. Put it in its own pattern.

Relationship types ​

KubePattern captures relations through primitives that map onto the mechanisms Kubernetes resources use to refer to one another.

TypeThe relationship holds when…Mechanism
ownsthe target transitively owns the dependencyownerReferences
ownedBythe target is transitively owned by the dependencyownerReferences
customuser-defined criteria hold between target and dependency fieldsreferences by name
selectsthe target's selector matches the dependency's labelslabel selectors
selectedBythe dependency's selector matches the target's labelslabel selectors

Ownership is transitive, and needs no further specification: a Deployment owns a Pod through the intermediate ReplicaSet without the pattern ever mentioning it. Custom resources usually refer to one another through plain specification fields that KubePattern cannot know in advance, which is why custom lets you name those fields.

Label selectors ​

selects reads the selector of the target at selectorPath and matches it against the labels of the dependency. selectedBy does the reverse. Optional criteria add conditions that must hold for the same pair.

  • A selector can be a LabelSelector (matchLabels, matchExpressions) or a plain label map, as in Service.spec.selector.
  • A missing or invalid selector selects nothing. An invalid one is logged.
  • Namespaces are not implied. Add a namespace criterion when the selection is namespaced.

A selector without requirements selects what emptySelector says, all or none. When it is unset, the API conventions apply. A LabelSelector with empty matchLabels and matchExpressions, or an empty {} in a field whose name ends in Selector (such as podSelector: {} in a NetworkPolicy), selects everything. An empty {} in any other field selects nothing, as for an empty Service selector. Set emptySelector: all where the API differs. A PodDisruptionBudget, for example, selects every Pod of its namespace with spec.selector: {}.

The selector path can be bound to an element. In External Secrets, a PushSecret can choose its stores by label, one entry at a time:

yaml
- with: push
  type: selectedBy
  selectorPath: "spec.secretStoreRefs[@].labelSelector"
  criteria:
    - {targetPath: kind, dependencyPath: "spec.secretStoreRefs[@].kind",
       dependencyDefault: {value: SecretStore}, operator: EQUALS}
    - {targetPath: metadata.namespace, dependencyPath: metadata.namespace, operator: EQUALS}

Operators ​

Filter operators ​

OperatorHolds when…
EQUALSthe field equals any of the values, compared as strings. Booleans and numbers compare by their string form, so values: ["true"] matches spec.suspend: true.
EXISTSthe field exists
IS_EMPTYthe field is missing or empty
GREATER_THAN, GREATER_OR_EQUAL, LESS_THAN, LESS_OR_EQUALthe field, read as a number, compares to the value
ARRAY_SIZE_EQUALS, ARRAY_SIZE_GREATER_THAN, ARRAY_SIZE_GREATER_OR_EQUAL, ARRAY_SIZE_LESS_THAN, ARRAY_SIZE_LESS_OR_EQUALthe size of the array field compares to the value

Criteria operators ​

OperatorHolds when…
EQUALSthe values on the two sides have a non-empty intersection

CONTAINS and LABEL_SELECTOR are declared in the API but not implemented, and the linter rejects them. Use transforms and the label selector relationships instead.

Message placeholders ​

The message of a pattern can contain placeholders, which are filled in for every finding.

PlaceholderReplaced with
{{target.metadata.name}}name of the target
{{target.metadata.namespace}}namespace of the target
{{target.metadata.uid}}UID of the target
{{target.kind}}kind of the target, useful when the target stands for several kinds
{{target.apiVersion}}API version of the target
{{smell.since}} next releasewhen the condition was first observed
{{smell.phase}} next releasePending or Active

Absence patterns ​

next release Sometimes the problem is that nothing of a kind exists at all. Set target.emitOnEmpty: true and, when no resource matches the target and its filters, KubePattern emits a single synthetic Smell named <pattern-name>-empty, with no target resource.

yaml
spec:
  displayName: No Network Policies
  category: Security
  severity: HIGH
  message: "The cluster has no NetworkPolicy at all."
  target:
    kind: NetworkPolicy
    apiVersion: networking.k8s.io/v1
    plural: networkpolicies
    emitOnEmpty: true

Linting ​

Patterns are read from the cluster and checked by a linter before use. A malformed pattern is logged and skipped, and the others still run. The linter checks the structure: required fields, known operators, that every with names a dependency, and that custom relationships have criteria. It also checks the syntax of every path, and the rules of element scope, defaults and transforms.

The linter cannot know your field names

A mistyped dependencyPath yields no values, and inside matchNone that means a finding for every target. Paths are checked for syntax, not against the CRD schemas. Check the paths of a new pattern against the schema (kubectl explain <kind>.spec) before you rely on it, and try it on a cluster you can inspect.

Things to know when writing patterns ​

The language is small on purpose. These are the limits you will meet most often.

LimitWhat it means in practice
A snapshot, not a historyA pattern sees the cluster at one instant. Use for to ignore transient states. To ignore history objects, such as the CertificateRequests that a Certificate owns and that record its past issuances, add a dependency filter like metadata.ownerReferences IS_EMPTY.
Content inside templates and remote referencesA reference that only appears in a CUE or Go template, or inside an OCI bundle, is not seen. KubePattern analyses the state of the cluster, not what a template would render.
Paths are not checked against the schemasA mistyped path yields no value. See Linting.
One API version per kindRead each kind at the same API version in all your patterns. The same object read at two versions has one UID, and the graph keeps it only once.
String-encoded references, partlyTransforms extract most of them. A short host name that would need a conditional default to become a fully qualified one cannot be matched yet.

The roadmap lists what is planned next.

Examples ​

These patterns are written for real platforms, and run without any change to the engine. More are in the Registry.

An unused definition, with several referrers ​

A cert-manager ClusterIssuer is used by a Certificate or a CertificateRequest. The dependencies are filtered because an omitted issuerRef.kind means a namespaced Issuer, so only references that name a ClusterIssuer count.

yaml
apiVersion: kubepattern.dev/v1
kind: Pattern
metadata:
  name: certmanager-clusterissuer-not-used
spec:
  displayName: ClusterIssuer Not Used
  category: Security
  severity: LOW
  message: "ClusterIssuer {{target.metadata.name}} is not referenced by any Certificate or CertificateRequest; it still holds signing material."
  reference: "https://cert-manager.io/docs/usage/certificate/"
  target:
    kind: ClusterIssuer
    apiVersion: cert-manager.io/v1
    plural: clusterissuers
  dependencies:
    - id: certificate
      kind: Certificate
      apiVersion: cert-manager.io/v1
      plural: certificates
      filters:
        matchAll:
          - path: "spec.issuerRef.kind"
            operator: EQUALS
            values: ["ClusterIssuer"]
    - id: request
      kind: CertificateRequest
      apiVersion: cert-manager.io/v1
      plural: certificaterequests
      filters:
        matchAll:
          - path: "spec.issuerRef.kind"
            operator: EQUALS
            values: ["ClusterIssuer"]
  relationships:
    matchNone:
      - with: certificate
        type: custom
        criteria:
          - targetPath: "metadata.name"
            dependencyPath: "spec.issuerRef.name"
            operator: EQUALS
      - with: request
        type: custom
        criteria:
          - targetPath: "metadata.name"
            dependencyPath: "spec.issuerRef.name"
            operator: EQUALS

An ownership relation ​

An Argo CD ApplicationSet whose generators match nothing owns no Application. The owns type needs no criteria, because it follows ownerReferences.

yaml
apiVersion: kubepattern.dev/v1
kind: Pattern
metadata:
  name: argocd-applicationset-generates-nothing
spec:
  displayName: ApplicationSet Generates No Applications
  category: Architecture
  severity: MEDIUM
  message: "ApplicationSet {{target.metadata.namespace}}/{{target.metadata.name}} currently owns no Application: its generators match nothing."
  target:
    kind: ApplicationSet
    apiVersion: argoproj.io/v1alpha1
    plural: applicationsets
  dependencies:
    - id: application
      kind: Application
      apiVersion: argoproj.io/v1alpha1
      plural: applications
  relationships:
    matchNone:
      - with: application
        type: owns

A misconfiguration ​

A CloudNativePG Pooler that shares its name with a Cluster in the same namespace. This is the presence of a relation, so it uses matchAll.

yaml
apiVersion: kubepattern.dev/v1
kind: Pattern
metadata:
  name: cnpg-pooler-name-clashes-with-cluster
spec:
  displayName: Pooler Named Like a Cluster
  category: Misconfiguration
  severity: HIGH
  message: "Pooler {{target.metadata.namespace}}/{{target.metadata.name}} has the same name as a CNPG Cluster in the same namespace."
  target:
    kind: Pooler
    apiVersion: postgresql.cnpg.io/v1
    plural: poolers
  dependencies:
    - id: cluster
      kind: Cluster
      apiVersion: postgresql.cnpg.io/v1
      plural: clusters
  relationships:
    matchAll:
      - with: cluster
        type: custom
        criteria:
          - targetPath: "metadata.name"
            dependencyPath: "metadata.name"
            operator: EQUALS
          - targetPath: "metadata.namespace"
            dependencyPath: "metadata.namespace"
            operator: EQUALS

With kind sets, defaults and transforms ​

These patterns come from KubePattern's evaluation, rewritten with kind sets, defaults and transforms. They were validated on minikube. Each one gives the right verdict on the cases that the original version missed, and removing the construct brings the miss back.

A reference whose namespace may be omitted ​

A Flux OCIRepository is consumed by a HelmRelease or a Kustomization. The referrers are the kinds of the fluxcd-appliers category, so an applier kind added by a later Flux release is covered too. A reference without a namespace points to the referrer's own.

yaml
apiVersion: kubepattern.dev/v1
kind: Pattern
metadata:
  name: flux-ocirepository-not-used
spec:
  displayName: OCIRepository Not Consumed
  category: Cost
  severity: LOW
  message: "OCIRepository {{target.metadata.namespace}}/{{target.metadata.name}} is polled by source-controller but no HelmRelease or Kustomization consumes it."
  reference: "https://fluxcd.io/flux/components/source/ocirepositories/"
  target:
    kind: OCIRepository
    apiVersion: source.toolkit.fluxcd.io/v1
    plural: ocirepositories
  dependencies:
    - id: applier
      category: fluxcd-appliers
  relationships:
    matchNone:
      - with: applier
        type: custom
        criteria:
          - {targetPath: "metadata.name", dependencyPath: "spec.chartRef.name", operator: EQUALS}
          - {targetPath: "kind", dependencyPath: "spec.chartRef.kind", operator: EQUALS}
          - targetPath: "metadata.namespace"
            dependencyPath: "spec.chartRef.namespace"
            dependencyDefault: {path: "metadata.namespace"}
            operator: EQUALS
      - with: applier
        type: custom
        criteria:
          - {targetPath: "metadata.name", dependencyPath: "spec.sourceRef.name", operator: EQUALS}
          - {targetPath: "kind", dependencyPath: "spec.sourceRef.kind", operator: EQUALS}
          - targetPath: "metadata.namespace"
            dependencyPath: "spec.sourceRef.namespace"
            dependencyDefault: {path: "metadata.namespace"}
            operator: EQUALS

A reference encoded in an API version ​

A Crossplane Composition names the composite kind it implements with an API version and a kind. The group inside the API version is compared with the group of each CompositeResourceDefinition, so a kind defined only in another group is detected.

yaml
apiVersion: kubepattern.dev/v1
kind: Pattern
metadata:
  name: crossplane-composition-without-xrd
spec:
  displayName: Composition Targets an Undefined Composite Kind
  category: Architecture
  severity: HIGH
  message: "Composition {{target.metadata.name}} composes a kind that no CompositeResourceDefinition defines: it can never be selected."
  reference: "https://docs.crossplane.io/latest/composition/compositions/"
  target:
    kind: Composition
    apiVersion: apiextensions.crossplane.io/v1
    plural: compositions
  dependencies:
    - id: xrd
      kind: CompositeResourceDefinition
      apiVersion: apiextensions.crossplane.io/v2
      plural: compositeresourcedefinitions
  relationships:
    matchNone:
      - with: xrd
        type: custom
        criteria:
          - {targetPath: "spec.compositeTypeRef.kind", dependencyPath: "spec.names.kind", operator: EQUALS}
          - targetPath: "spec.compositeTypeRef.apiVersion"
            targetTransform: {apiGroup: true}
            dependencyPath: "spec.group"
            operator: EQUALS

A kind named by another resource ​

A Krateo CompositionDefinition generates a kind, which it records in its status. The compositions are listed from that kind, so no generated kind has to be named in the pattern.

yaml
apiVersion: kubepattern.dev/v1
kind: Pattern
metadata:
  name: krateo-compositiondefinition-unused
spec:
  displayName: CompositionDefinition Unused
  category: Architecture
  severity: LOW
  message: "CompositionDefinition {{target.metadata.namespace}}/{{target.metadata.name}} has no composition of the kind it generates, but keeps a CRD and a composition-dynamic-controller running."
  reference: "https://docs.krateo.io/key-concepts/kco/core-provider"
  target:
    kind: CompositionDefinition
    apiVersion: core.krateo.io/v1alpha1
    plural: compositiondefinitions
    filters:
      matchAll:
        - {path: status.kind, operator: EXISTS}
  dependencies:
    - id: composition
      fromTarget: {apiVersionPath: status.apiVersion, kindPath: status.kind, pluralPath: status.resource}
  relationships:
    matchNone:
      - with: composition
        type: custom
        criteria:
          - {targetPath: status.kind, dependencyPath: kind, operator: EQUALS}
          - {targetPath: status.apiVersion, dependencyPath: apiVersion, operator: EQUALS}