KubernetesKubernetes

no matches for kind "X" in version "Y"

kubectl asked the API server for a resource type it does not serve. How to tell a missing CRD from a removed API version, and fix each.

medium fix6 min read

the kubernetes error
error: unable to recognize "app.yaml": no matches for kind "Ingress" in version "extensions/v1beta1"

error: unable to recognize "crd.yaml": no matches for kind "Certificate" in version "cert-manager.io/v1"

error: resource mapping not found for name: "web" namespace: "" from "app.yaml": no matches for kind "CronJob" in version "batch/v1beta1"

Do this first3 steps

Run these in order. Each one tells you what its output means before you change anything.

  1. 1

    Ask the cluster which versions it serves for that kind

    kubectl api-resources | grep -i ingress

    The APIVERSION column is the answer. If the kind is listed under a different version than your manifest uses, it is a deprecation. If the kind is absent entirely, the type is not installed.

  2. 2

    If the kind is missing, check whether its CRD exists

    kubectl get crd | grep -i certificate

    No CRD means the controller that defines the type was never installed, or is still starting. Install it before applying anything that uses it.

  3. 3

    Update the manifest to the version the cluster serves

    kubectl apply -f app.yaml --dry-run=server

    A server-side dry run validates against the live API without changing anything, so you can confirm the fix before applying it for real.

All 7 sections

no matches for kind "X" in version "Y" means kubectl asked the API server for a resource type and the server said it does not serve that type at that version. There are only two reasons:

  1. The version is wrong. The kind exists, at a different apiVersion. This is a deprecation.
  2. The kind does not exist. A CustomResourceDefinition was never installed, so nothing defines it.

The message is the same for both. One command tells them apart.

Ask the cluster what it serves

kubectl api-resources | grep -i ingress
NAME        SHORTNAMES   APIVERSION             NAMESPACED   KIND
ingresses   ing          networking.k8s.io/v1   true         Ingress

The kind exists at networking.k8s.io/v1, and your manifest says extensions/v1beta1. That is case 1: a removed API version.

If grep finds nothing, the type is not installed at all. That is case 2.

To see every version of a single kind, including ones being served alongside each other:

kubectl api-versions | sort
kubectl explain ingress --recursive | head -5

Case 1: A removed API version

Kubernetes promotes APIs from v1beta1 to v1 and then removes the beta after a deprecation window. Manifests written against the old version stop applying the moment you upgrade the cluster. These are the removals that catch the most people:

KindRemoved versionUse insteadRemoved in
Ingressextensions/v1beta1, networking.k8s.io/v1beta1networking.k8s.io/v11.22
CustomResourceDefinitionapiextensions.k8s.io/v1beta1apiextensions.k8s.io/v11.22
ClusterRole, RoleBindingrbac.authorization.k8s.io/v1beta1rbac.authorization.k8s.io/v11.22
CronJobbatch/v1beta1batch/v11.25
PodDisruptionBudgetpolicy/v1beta1policy/v11.25
HorizontalPodAutoscalerautoscaling/v2beta2autoscaling/v21.26
PodSecurityPolicypolicy/v1beta1Pod Security Admission1.25

Changing apiVersion is often not enough, because the schema usually changed too. Ingress is the clearest example:

# extensions/v1beta1, no longer served
spec:
  rules:
    - http:
        paths:
          - path: /
            backend:
              serviceName: web
              servicePort: 80
# networking.k8s.io/v1
apiVersion: networking.k8s.io/v1
kind: Ingress
spec:
  ingressClassName: nginx
  rules:
    - http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: web
                port:
                  number: 80

pathType is required in v1, and the backend is now a nested object. Editing only the apiVersion line gets you a validation error instead, which at least tells you what is missing.

PodSecurityPolicy is the hard one: it was removed with no replacement resource. You migrate to Pod Security Admission, which is namespace labels rather than an object.

Finding these before an upgrade

Do not discover them by upgrading. Check the live cluster for objects on doomed versions:

kubectl get --raw /metrics 2>/dev/null \
  | grep apiserver_requested_deprecated_apis
apiserver_requested_deprecated_apis{group="batch",removed_release="1.25",resource="cronjobs",version="v1beta1"} 1

The API server tracks every deprecated API anyone requests, including the removed_release. That metric is the most reliable pre-upgrade checklist you have, because it reports what is genuinely being used rather than what is in your repository.

For manifests and charts in git, kubectl-convert rewrites them:

kubectl convert -f old-ingress.yaml --output-version networking.k8s.io/v1

It ships separately from kubectl now, so you may have to install it.

Case 2: The CRD is not installed

kubectl get crd | grep cert-manager

Nothing returned means the type genuinely does not exist. Certificate, ServiceMonitor, VirtualService, IngressRoute and similar are all defined by controllers you install:

# cert-manager, which defines Certificate and Issuer
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.16.2/cert-manager.yaml

kubectl get crd | grep cert-manager.io

Install the controller first, then your resources.

The ordering problem in CI and Helm

A single kubectl apply -f ./manifests/ that contains both a CRD and a resource using it can fail even when everything is present, because kubectl builds its type mappings once at the start. The CRD is created during the apply, and the custom resource in the same batch is rejected against the mapping from before it existed.

Apply in two passes:

kubectl apply -f manifests/crds/
kubectl wait --for=condition=Established crd/certificates.cert-manager.io --timeout=60s
kubectl apply -f manifests/

The kubectl wait matters. A CRD exists before it is Established, and applying against it in that window fails intermittently, which is the sort of flake that only shows up in CI.

Helm has the same problem and solves it with a crds/ directory, which is installed before the templates are rendered. Note that Helm deliberately never upgrades or deletes anything in crds/, so CRD upgrades stay a manual step.

A checklist

  1. kubectl api-resources | grep -i <kind>.
  2. Listed at a different APIVERSION → deprecation. Update the manifest, and check the schema too.
  3. Not listed → kubectl get crd | grep <name>. Missing → install the controller.
  4. kubectl apply --dry-run=server to verify before applying for real.
  5. Before a cluster upgrade, read apiserver_requested_deprecated_apis from /metrics.
  6. In CI, apply CRDs first and kubectl wait --for=condition=Established.
  7. In Helm, put CRDs in crds/, and remember Helm will not upgrade them for you.

Frequently Asked Questions

What does "no matches for kind" mean in Kubernetes?

The API server does not serve the resource type at the apiVersion your manifest specifies. Either the kind exists at a different version, which means you are using an API that has been deprecated and removed, or the kind is not installed at all because the CustomResourceDefinition that defines it is missing. kubectl api-resources | grep -i <kind> distinguishes them: if the kind appears with a different APIVERSION it is the first case, and if it does not appear at all it is the second.

Why did my Ingress stop working after upgrading Kubernetes?

extensions/v1beta1 and networking.k8s.io/v1beta1 Ingress were removed in Kubernetes 1.22. The replacement is networking.k8s.io/v1, and the schema changed as well as the version: pathType is now required on every path, and the backend moved from flat serviceName and servicePort fields to a nested service.name and service.port.number. Changing only the apiVersion line produces a validation error rather than a working Ingress, which is at least a clearer message than the original one.

How do I find deprecated APIs before upgrading a cluster?

Read apiserver_requested_deprecated_apis from the API server's metrics endpoint with kubectl get --raw /metrics. Each series names the group, resource, version and the release in which it will be removed, and it reflects what is genuinely being requested against the live cluster rather than what happens to be in your repository. That makes it far more reliable than grepping manifests, because it also catches controllers, operators and Helm charts making calls you never wrote.

Why does kubectl apply fail on a custom resource right after I created its CRD?

kubectl builds its mapping of kinds to API endpoints once, at the start of the command. If the CRD is created during the same apply, the custom resource is validated against the mapping from before that type existed. There is also a window where the CRD object exists but is not yet Established. Apply CRDs as a separate step, then kubectl wait --for=condition=Established crd/<name> before applying the resources that use them. Intermittent failures of this kind are a classic CI flake.

Does Helm handle CRDs differently?

Yes. Files in a chart's crds/ directory are installed before any template is rendered, which solves the ordering problem for a fresh install. The important caveat is that Helm deliberately never upgrades or deletes those CRDs on helm upgrade or helm uninstall, because doing so would delete every custom resource in the cluster along with them. That means CRD upgrades are a manual step you have to plan for, and a chart upgrade that needs a newer CRD schema will fail until you apply it yourself.

Reference and practice

Learn the underlying concept

Other Kubernetes errors