Pretty Printing CRDs with Kubebuilder

Adding extra columns to kubectl get

Posted by Brendan Powers on Saturday, November 2, 2019

If you’ve developed a CRD with Kubebuilder, you know how nice the code generation features are. It lets you define your CRD using standard Go object, and then add additional information like documentation. Did you know, that you can also control how kubectl prints your CRD as well?

Let’s look at an example from my Codebook project. Codebook is designed to bring up a set VS Code Server instances for use in training classes. In this project, I have a resource called Group. The Group resource is where I store collective information about a class. For example, what image should be used, how long any instance is allowed to run, resource limits, etc… When I list Groups with kubectl, I get something like this:

$ kubectl get groups
NAME        AGE
default     2d15h
testgroup   61s

This is useful, but not all that informative. I know that I have two groups, and how old they are. To get more information, I’d need to do a kubectl describe for each instance. This will quickly get tedious. Since groups contain sets of VS Code instances, I’d really like to know how many instances there are, and how many are running. Let’s look at the status object for a Group.

// GroupStatus defines the observed state of Group
type GroupStatus struct {
  // +optional
  Instances []GroupInstanceStatus `json:"instances,omitempty"`

  NumInstances        *int64 `json:"numInstances"`
  NumRunningInstances *int64 `json:"numRunningInstances"`
}

We have to properties (NumInstances, and NumRunningInstances) that have what we need. We can use the +kubebuilder:printcolumn annotation to tell kubebuilder to add the needed information to our CRD. I place them right after the +kubebuilder:object:root=true annotation. For example, let’s print the number of instances.

// +kubebuilder:object:root=true
// +kubebuilder:printcolumn:name="Instances",type="integer",JSONPath=".status.numInstances",description="Number of instances in group"

// Group is the Schema for the groups API
type Group struct {
...

The printcolumn annotation has a few different options, we only used a few in this example:

  • name: This is the title of the new column, printed in the header by kubectl.
  • type: The data type of the value being printed. Valid types are integer, number, string, boolean and date.
  • JSONPath: This is a dot notated path to the date you want to print. In our case, the numInstances property in the Status object, so we use .status.numInstances. Notice that we use numInstances, not NumInstances. The JSONPath property refers to the generated JSON CRD, not the native Go classes.
  • Description: A human-readable string describing the column. I don’t actually know if these are printed anywhere, but I leave them in just in case.

Now that we have our annotation created, we can run make install, and try listing our CRD again.

$ kubectl get groups
NAME        INSTANCES
default     2
testgroup   1

Cool! We can now see the number of running instances in each group. Where did the Age column go though? It seems that when you start adding custom columns, any default columns are no longer shown (aside from Name, I guess). So let’s add the Age column back, and we can add the number of running instances while we are at it.

// +kubebuilder:object:root=true
// +kubebuilder:printcolumn:name="Instances",type="integer",JSONPath=".status.numInstances",description="Number of instances in group"
// +kubebuilder:printcolumn:name="Running",type="integer",JSONPath=".status.numRunningInstances",description="Number of running instances in group"
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"

// Group is the Schema for the groups API
type Group struct {
...

Run make install, and then list the groups again.

$ kubectl get groups
NAME        INSTANCES   RUNNING   AGE
default     2           2         2d16h
testgroup   1           1         31m

Awesome, we now see the number of running instances, and our age column is back. This is a lot more useful than what we started out with. It could be more useful though. What if we wanted to know more than just the current status of the Group? It would be nice to tell them apart in some way. One of the ways we could do that is to display the image for each Group. The group object is not in the status, it’s in the spec. This is not a problem for column annotations we simply use .spec instead of .status in the JSONPath property. The new annotation looks like this:

// +kubebuilder:printcolumn:name="Image",type="string",JSONPath=".spec.image"

When we list our Groups again, we now see the image!

$ kubectl get groups
NAME        INSTANCES   RUNNING   AGE     IMAGE
default     2           2         2d16h   codercom/code-server:v2
testgroup   1           1         36m     codercom/code-server:v2

This is useful to know, but perhaps it’s a bit much for the default output. What if we’d like to hide the field, and only show it when requested? This is where the priority field comes in. We’ve left it out so far, and each of our columns has the default value of 0. If you set priority to any number above 1, it will only show up when you specify -o wide. Let’s try it:

// +kubebuilder:printcolumn:name="Image",type="string",priority=1,JSONPath=".spec.image"

When we re-run kubectl, we no longer see the image column.

$ kubectl get groups
NAME        INSTANCES   RUNNING   AGE
default     2           2         2d16h
testgroup   1           1         31m

When we add -o wide, the field shows up again:

$ kubectl get group -o wide
NAME        INSTANCES   RUNNING   AGE     IMAGE
default     2           2         3d16h   codercom/code-server:v2
testgroup   1           1         24h     codercom/code-server:v2

That covers most of the features of the printcolumn annotation. If you’d like a little more detail, check out the CRD docs on the additionalPrinterColumns field.