knext

Upgrading knext

Upgrade the operator and CRD before the CLI, and what the deploy-time schema check does when you don't.

knext has two pieces that version independently: the operator (with its NextApp CRD) running in your cluster, and the CLI (@getknext/core) running on your machine or in CI.

They are deliberately not lockstep — but when you upgrade them, the order matters.

Upgrade the operator and its CRD first, then the CLI.

Why the order matters

kn-next deploy describes your app as a NextApp custom resource and applies it to the cluster. The CRD installed by the operator defines which fields that resource may contain.

A newer CLI can emit a field a older CRD has never heard of. knext applies with strict validation on purpose, so the cluster rejects that resource rather than silently accepting it and dropping the field. Silently dropping it would be worse: your deploy would report success while the setting you asked for simply vanished — a TLS setting, a database credential reference, a scaling bound.

So the failure you get from upgrading in the wrong order is loud and safe. It is still a failure.

The safe direction

An older CLI against a newer CRD is always fine. A newer CRD understands every field an older CLI knows how to emit. You do not need to upgrade both at once, and you do not need to keep them on matching versions — you only need to avoid the CLI running ahead of the cluster.

That is what makes "operator first" a rule you can follow incrementally: upgrade the cluster when it suits you, and upgrade CLIs afterwards at whatever pace your team and CI move.

What it looks like when the order is wrong

kn-next deploy stops before it changes anything and names the field:

PREFLIGHT FAILED: the NextApp CRD installed on this cluster does not know field(s) this
CLI emits, so the CR would be rejected (or, under a client that does not assert strict
validation, SILENTLY PRUNED):
  - spec.database.roSecretRef
Upgrade order is load-bearing: upgrade the OPERATOR/CRD first, THEN the CLI.
Nothing was built, uploaded or applied — this ran before any side effect.
  kubectl get crd nextapps.apps.kn-next.dev -o jsonpath='{.spec.versions[*].name}'
  kn-next doctor

Three things to note.

Nothing was uploaded. The check runs as the very first step that touches your cluster — before the build is pushed and before any assets are uploaded to object storage. A failed deploy leaves no half-written state behind and no orphaned files to clean up.

It names the field. You get the specific setting your cluster cannot store, not a generic validation error, so you can tell immediately whether the gap matters for this deploy.

For some fields it also tells you what the gap would have cost. If the missing field is one where silently dropping it would weaken your setup rather than merely change it, the message says so. spec.database.roSecretRef is the clearest example: without it the operator never injects a read-only database URL, so reads that were meant for a replica fall back to the read-write credential — and the app would still report healthy.

Checking before you deploy

kn-next doctor reports whether the cluster's CRD covers the fields your CLI emits, so you can find a version gap without running a deploy at all:

npx kn-next doctor

If doctor cannot read your cluster's full schema — some restricted credentials cannot — it will say so rather than guess. The deploy-time check does not depend on that permission: it works with exactly the access a deploy already needs, so a restricted credential can still deploy safely.

Upgrading the operator

Apply the operator release for the version you want, then confirm the CRD is in place:

kubectl get crd nextapps.apps.kn-next.dev

Once that reports the version you expect, upgrade the CLI:

npm install -D @getknext/core@latest

Version-specific step: the metrics port moved from 9091 to 9464

Older releases served app metrics on :9091 — a port Knative's queue-proxy binds inside every pod on a stock Serving install, which could crash-loop apps with EADDRINUSE. Current releases serve metrics on :9464 everywhere: the runtime's default, the prometheus.io/port annotation, the default NetworkPolicy's scrape grants, the shipped PodMonitor and the bundled Grafana dashboards all move together when you upgrade the operator and redeploy your apps.

What does not move automatically is anything you wrote yourself against the old port:

  1. Hand-rolled Prometheus scrape_configs, or ServiceMonitors/PodMonitors you authored — repoint them from 9091 to 9464.
  2. Grafana panels or alert rules of your own pinned to the old target.
  3. Any NetworkPolicy you added alongside knext's default one that grants 9091 for scraping.
  4. Redeploy each app after upgrading: a pod still running an image built before the change binds :9091 until its next deploy, and during that window the upgraded operator's scrape configuration points at :9464 — so that app's metrics are dark until it is rebuilt.

Verify an app after redeploying (from a pod or node that can reach it):

kubectl exec deploy/<your-scraper> -- wget -qO- http://<pod-ip>:9464/metrics | head -3

kn-next doctor (the metrics-port check) flags any app still pinned onto a queue-proxy-owned port and tells you which serving ConfigMap key governs :9091 on your cluster.

The scale-to-zero database: roll the failover controller before its new config

The scale-to-zero database ships its own failover controller — a small component that watches the primary pageserver and promotes the warm standby if it dies. It reads its behaviour from environment configuration set on its manifests, and it follows the same ordering rule as the operator and CLI, for the same reason: the piece that reads a contract upgrades before the manifest that writes it.

When an upgrade adds or changes a failover-controller setting — the tenant set it promotes, the routed pageserver URL it reads generations from, the maintenance-freeze ConfigMap name or the freeze's hard time bound — roll the failover-controller image to the new version first, then apply the manifests that wire the new configuration. Apply the new setting against the old image and the setting is inert: the running controller does not know the variable exists, so nothing reads it and the change silently does nothing — the same failure mode as running the CLI ahead of the CRD.

If you deploy the scale-to-zero database from its bundled manifests (kubectl apply -f packages/scale-zero-pg/deploy/), bump the controller image digest and roll it before applying the rest of the directory in the same upgrade.

What this does not cover

Deployment tools that apply manifests directly to your cluster — GitOps controllers, for example — do not go through kn-next deploy, so they do not get the preflight check described above. If you apply NextApp resources through such a tool, keep the same upgrade order yourself: the cluster is still the thing that decides which fields it can store.

On this page