skip to content

In Kubernetes, what are the `status.conditions` entries a custom resource's controller reports, and how do you check them with `kubectl`?

level: juniorimportance: should knowfreq 48%

answer

  1. a list, one entry per type
  2. True, False or Unknown
  3. reason for machines, message for humans
  4. transition time moves only on flips
  5. kubectl wait with condition equals

basics

~20 s

Conditions are status entries a custom resource's controller writes, each with a type such as Ready, a status of True, False or Unknown, a reason and a message. Read them with kubectl describe or jsonpath; block on them with kubectl wait --for=condition=.

solid answer

~40 s

`status.conditions` is the standard way a controller reports what it has observed about an object. Each entry follows the `metav1.Condition` shape: `type` (for example `Ready` or `Degraded`), `status` (`True`, `False` or `Unknown`), a CamelCase `reason` a machine can switch on, a human `message`, `lastTransitionTime` (when `status` last flipped) and `observedGeneration` (which spec version the entry describes). Only the controller writes them, and users read them. From the CLI I use `kubectl describe` for a quick look, `kubectl get <kind> <name> -o jsonpath='{.status.conditions}'` for scripts, and `kubectl wait <kind>/<name> --for=condition=Ready --timeout=...` in a pipeline. The value defaults to `True`, and you can wait for another value with `--for=condition=Ready=False`. Before trusting an entry, check that its `observedGeneration` matches the object's `metadata.generation`.

code

bash · 3 lines
bash
kubectl get ledgerrun nightly-2026-09-16 \
  -o jsonpath='{range .status.conditions[*]}{.type}={.status} ({.reason}){"\n"}{end}'
kubectl wait ledgerrun/nightly-2026-09-16 --for=condition=Ready --timeout=45m

go deeper

for a junior

Recall the field shape: type, status with its three values, reason, message, lastTransitionTime and observedGeneration. Show describe, jsonpath and kubectl wait with --for=condition.

for a middle

Explain that one entry exists per type, why lastTransitionTime moves only when status flips, and why reason is the machine-facing contract.

for a senior

Show how you decide whether a condition is stale: observedGeneration against generation, and a dead controller leaving old values behind. Mention designing failure conditions so pipelines fail fast.

for a principal

Frame conditions as the platform-wide health contract: a consistent set of types across operators lets generic tooling judge readiness without per-kind code.

## What a condition is A Kubernetes object has two halves. **`spec`** is what a user asks for, and **`status`** is what the controller responsible for the object has observed. Free-form status fields such as `readyShards: 7` are useful but hard for generic tools to read, because every kind names them differently. **Conditions** fix that: `status.conditions` is a list of small records with the same shape on every kind that follows the convention. A tool can then ask any object "are you Ready?" without knowing its schema. For custom resources the recommended shape is the Go type `metav1.Condition` from `k8s.io/apimachinery`. Its fields: | Field | Required | Meaning | |---|---|---| | `type` | yes | What the entry is about, in CamelCase: `Ready`, `Progressing`, `Degraded` | | `status` | yes | One of `True`, `False`, `Unknown` | | `reason` | yes | A CamelCase token a program can switch on, such as `ShardTimeout` | | `message` | yes (may be empty) | Human-readable detail | | `lastTransitionTime` | yes | When `status` last changed value | | `observedGeneration` | no | The `metadata.generation` the controller was looking at when it set the entry | There is **at most one entry per `type`**. A controller updates an entry in place and does not append a new record on every reconcile. Helper functions such as `meta.SetStatusCondition` do exactly that. They also move `lastTransitionTime` only when `status` flips, so the timestamp answers "since when has this been False?" and not "when did the controller last run?". ## Who writes conditions - The **controller** writes them, normally through the object's `/status` subresource, so the write never touches `spec`. - **Users** do not write them. A condition a human set by hand is a lie the controller will overwrite on its next pass. - **Readers** include people, CI pipelines, dashboards and GitOps controllers that decide whether a rollout is healthy. Take a custom resource `LedgerRun` that drives the nightly ledger-reconciliation batch on a 27-worker cluster. Its controller might keep three conditions: 1. `Progressing=True`, reason `ShardsRunning`, while the 7 worker replicas chew through shards. 2. `Ready=True`, reason `LedgerBalanced`, once every shard reconciles. 3. `Degraded=True`, reason `ShardTimeout`, if shard 4 misses its 45-minute deadline. ## Reading them with kubectl ```bash kubectl describe ledgerrun nightly-2026-09-16 kubectl get ledgerrun nightly-2026-09-16 \ -o jsonpath='{range .status.conditions[*]}{.type}={.status} ({.reason}){"\n"}{end}' kubectl wait ledgerrun/nightly-2026-09-16 --for=condition=Ready --timeout=45m ``` - `kubectl describe` prints the conditions in a readable block, which is best for a human. - `-o jsonpath` gives a script exact values without parsing a table. - `kubectl wait --for=condition=Ready` blocks until the named condition has the wanted value. The value defaults to `True`, and `--for=condition=Ready=False` waits for `False`. Values are compared case-insensitively. - Several `--for` flags are **ANDed**. To stop on "succeeded **or** failed", loop over two short waits, because a single `wait` for `Ready` sits until its timeout when the run fails. - `kubectl wait --for=jsonpath='{.status.phase}'=Done` covers a plain status field that is not a condition. ## Trusting what you read A condition is a statement about a point in time and a particular spec. Three checks separate a careful reader from a naive one: - **Is it current?** Compare the condition's `observedGeneration`, or `status.observedGeneration`, with `metadata.generation`. If the object's generation is higher, a user changed the spec after the controller wrote the entry, and `Ready=True` describes the old spec. - **Is it `Unknown`?** `Unknown` means the controller cannot tell. It is not a soft `True`. - **Is the controller alive?** Conditions do not expire. If the operator's Pod is crash-looping, the last value stays on the object indefinitely. `lastTransitionTime` shows only when the value last changed, not whether anyone is still watching. ## Why the convention matters If every custom resource reports health through the same field shape, generic tooling works without per-kind code: `kubectl wait`, printer columns built on a condition, and health checks in delivery tooling. A controller that invents `status.state: "OK-ish"` forces every consumer to write a special case. Keeping the set of types small and stable matters as much as the shape. A consumer that waits on `Ready` breaks if a new operator release renames it to `Available`, so treat condition types and reasons as part of the operator's public API and change them with the same care as a spec field. That is why interviewers treat "do you report Conditions?" as a basic signal of whether someone has written a well-behaved controller.

  • A pipeline runs `kubectl wait --for=condition=Ready` on a LedgerRun that fails. What happens, and how do you fix it?
    The wait sits until `--timeout` expires, because `Ready` never becomes `True`, and the job fails late with a timeout instead of the real reason. Multiple `--for` flags are ANDed, so you cannot say "Ready or Degraded" in one call. Loop over short waits, one for `Ready` and one for `Degraded`, and exit on whichever succeeds first. Then print the condition's `reason` and `message`.
  • Why does a condition carry both `reason` and `message`?
    `reason` is a stable CamelCase token such as `ShardTimeout` that alerting rules and scripts can match exactly. `message` is free text for a human and may include numbers, names or hints that change from run to run. Matching on `message` breaks the first time someone rewords it, while `reason` is part of the controller's contract.

Conditions are like the indicator lights on a car dashboard: a fixed set of labelled lamps, each on, off or blinking, that any driver can read without knowing how the engine works.

saying these in an interview costs you the question

  • Appending a new condition entry on every reconcile instead of updating one per type
  • Treating status Unknown as good enough to proceed
  • Believing lastTransitionTime updates on every reconcile
  • Having users or CI write conditions by hand
  • Parsing the message text in scripts instead of matching reason
  • Trusting Ready=True without checking the generation it describes