By default `kubectl get` on a custom resource shows only NAME and AGE. How do you make it display fields from the object itself, and what is the mechanism behind it?
answer
- additionalPrinterColumns per version, not per CRD
- name + type + jsonPath (+ priority, description)
- type: date renders as an age
- priority > 0 → only with -o wide
- server-side Table printing, so all clients benefit
basics
~20 sAdd additionalPrinterColumns to the CRD version: each entry has a name, a type, and a jsonPath into the object. The API server renders the table server-side, so every client that asks for a table sees the same columns.
solid answer
~40 s`additionalPrinterColumns` is declared **per version** inside `spec.versions[]` of the CRD. Each column specifies: - `name` — the column header; - `type` — `string`, `integer`, `number`, `boolean` or `date` (`date` renders as an age); - `jsonPath` — where to read the value, e.g. `.spec.engine` or `.status.conditions[?(@.type=="Ready")].status`; - optional `description`, and `priority` — columns with priority greater than 0 are hidden unless the user passes `-o wide`. The mechanism is **server-side printing**: kubectl requests the resource with an `Accept` header asking for a `Table` object, and the API server builds the rows from these definitions. That is why the columns appear for every user and every tool, without anyone configuring kubectl. For a one-off view you do not want to bake into the CRD, `kubectl get db -o custom-columns=NAME:.metadata.name,ENGINE:.spec.engine` does the same thing client-side.
code
yaml · 23 linesversions:
- name: v1
served: true
storage: true
subresources:
status: {}
additionalPrinterColumns:
- name: Engine
type: string
jsonPath: .spec.engine
- name: Ready
type: string
jsonPath: .status.conditions[?(@.type=="Ready")].status
- name: Size
type: integer
jsonPath: .spec.sizeGb
priority: 1 # only with -o wide
- name: Age
type: date
jsonPath: .metadata.creationTimestamp
schema:
openAPIV3Schema:
type: objectgo deeper
Know the field name, that each column is a name plus a type plus a jsonPath, and that it goes inside a version entry.
Add priority for -o wide, the date type rendering as an age, and JSONPath filters for surfacing a specific condition.
Explain server-side Table printing and why it benefits every client, and treat column choice as an operability decision for on-call triage.
Consider the default kubectl view part of the platform API's user experience: what an operator must see without reaching for -o yaml, and consistency of column naming across the organisation's custom kinds.
## The default is deliberately minimal Without extra configuration, `kubectl get databases` prints just NAME and AGE, because those are the only fields the API server knows are meaningful for an arbitrary kind. Built-in resources look richer (READY, STATUS, RESTARTS for Pods) because the API server has printer definitions for them. `additionalPrinterColumns` is how you supply the same thing for your kind. ## Declaring columns Columns live inside a version entry, so different versions of the same CRD can present differently — useful when a rename means the interesting field moved. Each column has: - **`name`** — the header text, conventionally upper-case. - **`type`** — one of `string`, `integer`, `number`, `boolean`, `date`. The `date` type is rendered as a relative age, which is what makes an `Age` column read as `5d` rather than a timestamp. - **`format`** (optional) — e.g. `int32`, `byte`; mostly cosmetic. - **`jsonPath`** — a restricted JSONPath expression into the object. Filters work, which is how people surface a specific condition: `.status.conditions[?(@.type=="Ready")].status`. - **`priority`** — `0` (default) means always shown; anything greater than 0 means shown only with `kubectl get -o wide`. Use it to keep the default view to about four or five columns while still exposing detail. - **`description`** — appears in `kubectl explain`-adjacent output and documents the column. A missing path renders as `<none>` rather than erroring, so columns are safe to add for fields that are only populated later by a controller. ## Server-side printing The reason this works everywhere is that table rendering happens **in the API server**, not in kubectl. When kubectl performs a `get` without `-o json|yaml`, it sends an `Accept: application/json;as=Table;g=meta.k8s.io;v=v1` header, and the API server returns a `Table` object containing column definitions and pre-rendered rows. kubectl just prints what it is given. Consequences worth knowing: - Every user and every tool that requests a Table — dashboards, `k9s`, IDE plugins — gets the same columns automatically, with no client configuration. - Adding a column takes effect immediately on the next `kubectl get`; no client upgrade, no restart. - It costs nothing at storage time; the paths are evaluated at read time. ## When to use the alternative For an ad-hoc view you do not want to standardise, `-o custom-columns` takes the same kind of paths on the command line and is evaluated client-side: ``` kubectl get databases -o custom-columns=NAME:.metadata.name,ENGINE:.spec.engine,READY:.status.conditions[?(@.type=="Ready")].status ``` The rule of thumb: bake into the CRD whatever an operator needs to triage the resource at a glance — phase or readiness, the one or two spec fields that identify the instance, and age. Leave everything else to `-o wide` via `priority`, or to `custom-columns` for one-off investigations. A CRD whose default `kubectl get` output already answers "is this healthy and what is it" is a meaningfully better operational experience, and it is about six lines of YAML.
- How do you add a column that is useful but too detailed for the default view?Give it a priority greater than 0. Columns with priority 0 always appear, and anything higher is rendered only when the user asks for kubectl get -o wide. That keeps the default table to a handful of triage-relevant columns while still making the detail available without switching to -o yaml.
- Why do these columns appear in tools other than kubectl without any configuration?Because printing is server-side: clients request the resource as a meta.k8s.io Table, and the API server builds the column definitions and rows from additionalPrinterColumns. Any client that asks for a Table — dashboards, k9s, IDE plugins — receives the same columns, so adding one to the CRD improves every tool at once.
saying these in an interview costs you the question
- Thinking the columns are a kubectl-side setting rather than server-side table printing
- Declaring additionalPrinterColumns once for the whole CRD instead of per version
- Expecting a column with a path into an unpopulated field to error rather than render as <none>
- Cramming ten columns into the default view instead of using priority for the detail ones