Background

I am working to build an experimental Kubernetes cluster with Ansible.

Some steps in the Ansible configuration are asynchronous, so I need to use do..until tasks to delay further processing of the configuration until the asynchronous steps are completed.

do..until waits on a condition to become true before considering the task to be complete and applying the remaining tasks. This could, for example, be waiting on provisioned nodes to be in a Ready state.

I looked at a number of examples that used kubectl get nodes ansible_facts['hostname'] and piped the output through awk and/or grep. Others used Jinja2 filters to filter the standard output (stdout). I felt there had to be a better way…

Customising the output of kubectl

The default output of kubectl get nodes look like:

$ kubectl --kubeconfig /etc/kubernetes/admin.conf get nodes k8scp01
NAME     STATUS   ROLES                  AGE   VERSION
k8scp01  Ready    control-plane,master   62m   v1.22.1

Remove Column Headings

The first step is eliminating the column headings. We can do this simply by adding --no-headers to the kubectl command:

$ kubectl --kubeconfig /etc/kubernetes/admin.conf get nodes k8scp01 --no-headers

k8scp01  Ready   control-plane,master   64m   v1.22.1

Inspect the JSON API data

I thought the second step would be returning the single value needed. I thought it would be possible to return just the STATUS column. Before we can reach an elegant solution, we have to grapple with some JSON.

I noted that kubectl accepts a parameter called --output and various values can be appended to that. These include, for example, json to return raw JSON API output.

Here is an excerpt of what gets returned when we append --output=json to the kubectl command:

{
	"apiVersion": "v1",
	"kind": "Node",
	
	<snip>

	"status": {

		<snip>
		
		"conditions": [
			{
				"lastHeartbeatTime": "2021-08-23T18:00:32Z",
				"lastTransitionTime": "2021-08-23T18:00:32Z",
				"message": "Calico is running on this node",
				"reason": "CalicoIsUp",
				"status": "False",
				"type": "NetworkUnavailable"
			},
			{
				"lastHeartbeatTime": "2021-08-23T18:01:34Z",
				"lastTransitionTime": "2021-08-23T17:39:18Z",
				"message": "kubelet has sufficient memory available",
				"reason": "KubeletHasSufficientMemory",
				"status": "False",
				"type": "MemoryPressure"
			},
			{
				"lastHeartbeatTime": "2021-08-23T18:01:34Z",
				"lastTransitionTime": "2021-08-23T17:39:18Z",
				"message": "kubelet has no disk pressure",
				"reason": "KubeletHasNoDiskPressure",
				"status": "False",
				"type": "DiskPressure"
			},
			{
				"lastHeartbeatTime": "2021-08-23T18:01:34Z",
				"lastTransitionTime": "2021-08-23T17:39:18Z",
				"message": "kubelet has sufficient PID available",
				"reason": "KubeletHasSufficientPID",
				"status": "False",
				"type": "PIDPressure"
			},
			{
				"lastHeartbeatTime": "2021-08-23T18:01:34Z",
				"lastTransitionTime": "2021-08-23T18:00:34Z",
				"message": "kubelet is posting ready status. AppArmor enabled",
				"reason": "KubeletReady",
				"status": "True",
				"type": "Ready"
			}
		],
		
		<snip>
		
	}
}

Somewhere here is the status of our node.

The JSON API returns multiple instances of status.condition, each with a type and a boolean status value.

By observation, I see that when our node reaches a condition of “STATUS: Ready” by the conventional kubectl get nodes output, we see a status.condition with a type of Ready and a value of True.

That is good enough for our purposes!

Returning a “custom-column”

The kubectl --output parameter also accepts a parameter called custom-columns which can be specified with a JSONPath. That means we should be able to get a succinct answer with something like $.status.conditions[?(@.type=='Ready')].status:

$ kubectl --kubeconfig /etc/kubernetes/admin.conf get nodes --no-headers -output=custom-columns=STATUS:status.conditions[?(@.type==\'Ready\')].status
True

That gives us a nice easily comparable value for use with Ansible.

Ansible Example

- name: wait until node is in ready state
  environment:
    KUBECONFIG: /etc/kubernetes/admin.conf
  shell: |
    kubectl get nodes {{ ansible_hostname }} --no-headers -o custom-columns=STATUS:status.conditions\[\?\(\@.type==\"Ready\"\)\].status
  register: node_status
  until: node_status.stdout == 'True'
  delay: 10
  retries: 30

In this example, we will wait up to five minutes for the node to get a status type of Ready with a value of True.

Conclusion

This is one small building block in creating a Kubernetes cluster with Ansible. In future I hope to add full example on GitLab with a full write-up here.

References

  1. Kubectl Reference Docs - kubernetes.io/docs
  2. Overview of Kubectl - kubernetes.io/docs
  3. Ansible User Guide: Discvering Variables: Facts and Magic Variables