TL;DR

While building a simple install script for Tigera’s CNX product, I received equally emphatic advice to use bash as well as golang.

I decided to build the same (subset of) functionality twice: once in bash and once again in golang.

Outcome

  • The bash function took less than half the time to write and it appears equally robust and not particularly hard to read.
  • The golang executable is better for scale: unit tests, strongly typed variables, scoped functionality, and more make it easier to maintain and extend safely.

Wait till pod is running - golang edition

My package builds upon the kubernetes e2e utils framework. This robust framework monitors the state of a kubernetes cluster using client-go.


package k8sutils

import (
	"fmt"
	"k8s.io/api/core/v1"
	meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/util/wait"
	"k8s.io/client-go/kubernetes"
	"k8s.io/kubernetes/pkg/client/conditions"
	"time"
)

// return a condition function that indicates whether the given pod is
// currently running
func isPodRunning(c kubernetes.Interface, podName, namespace string) wait.ConditionFunc {
	return func() (bool, error) {
		fmt.Printf(".") // progress bar!

		pod, err := c.CoreV1().Pods(namespace).Get(podName, meta_v1.GetOptions{IncludeUninitialized: true})
		if err != nil {
			return false, err
		}

		switch pod.Status.Phase {
		case v1.PodRunning:
			return true, nil
		case v1.PodFailed, v1.PodSucceeded:
			return false, conditions.ErrPodCompleted
		}
		return false, nil
	}
}

// Poll up to timeout seconds for pod to enter running state.
// Returns an error if the pod never enters the running state.
func waitForPodRunning(c kubernetes.Interface, namespace, podName string, timeout time.Duration) error {
	return wait.PollImmediate(time.Second, timeout, isPodRunning(c, podName, namespace))
}

// Returns the list of currently scheduled or running pods in `namespace` with the given selector
func ListPods(c kubernetes.Interface, namespace, selector string) (*v1.PodList, error) {
	listOptions := meta_v1.ListOptions{IncludeUninitialized: true, LabelSelector: selector}
	podList, err := c.CoreV1().Pods(namespace).List(listOptions)

	if err != nil {
		return nil, err
	}
	return podList, nil
}

// Wait up to timeout seconds for all pods in 'namespace' with given 'selector' to enter running state.
// Returns an error if no pods are found or not all discovered pods enter running state.
func WaitForPodBySelectorRunning(c kubernetes.Interface, namespace, selector string, timeout int) error {
	podList, err := ListPods(c, namespace, selector)
	if err != nil {
		return err
	}
	if len(podList.Items) == 0 {
		return fmt.Errorf("no pods in %s with selector %s", namespace, selector)
	}

	for _, pod := range podList.Items {
		if err := waitForPodRunning(c, namespace, pod.Name, time.Duration(timeout)*time.Second); err != nil {
			return err
		}
	}
	return nil
}

In addition to the k8sutils package above, I built a simple main executable package:

package main

import (
	"flag"
	"fmt"
	"github.com/bcreane/k8sutils"
	log "github.com/sirupsen/logrus"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/tools/clientcmd"
	"k8s.io/client-go/util/homedir"
	"os"
	"path/filepath"
)

func main() {
	var kubeConfig *string
	if home := homedir.HomeDir(); home != "" {
		kubeConfig = flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "(optional) absolute path to the kubeconfig file")
	} else {
		kubeConfig = flag.String("kubeconfig", "", "absolute path to the kubeconfig file")
	}
	var namespace = flag.String("namespace", "default", "namespace")
	var selector = flag.String("selector", "", "pod selector")
	var timeout = flag.Int("timeout", 30, "timeout in seconds")
	flag.Parse()

	config, err := clientcmd.BuildConfigFromFlags("", *kubeConfig)
	if err != nil {
		panic(err)
	}
	clientSet, err := kubernetes.NewForConfig(config)
	if err != nil {
		panic(err)
	}

	// Block up to timeout seconds for listed pods in namespace/selector to enter running state
	err = k8sutils.WaitForPodBySelectorRunning(clientSet, *namespace, *selector, *timeout)
	if err != nil {
		log.Errorf("\nThe pod never entered running phase\n")
		os.Exit(1)
	}
	fmt.Printf("\nAll pods in namespace=\"%s\" with selector=\"%s\" are running!\n", *namespace, *selector)
}

Invoking the program is simple:

    # Wait up to 30 seconds for pods w/ selector and namespace to enter running phase
    ./watch --selector=k8s-app=kube-dns --namespace=kube-system --timeout=30`

This works about the same as the bash script I wrote a few weeks ago.

Take away

  • Bash is quicker to write and often more condensed than golang since it builds on powerful utilities such as kubectl.
  • golang is more maintainable and robust, and frankly cooler.

If you need a simple script that’s less than a few hundred lines, consider bash. Otherwise golang’s higher upfront cost but much more robust extensibility and verifiability are a better choice.

The real install script has grown to about 1,000 lines of bash. Don’t forget to allow head room for feature creep - your 50 line bash script may grow into 500 lines before you know it!