Banking

Building a Consistent Kubernetes API Server Access Layer with clientcmd

2026-01-19 10:00
603 views

Building a command line client for Kubernetes APIs often raises the question of how to replicate the familiar feel of kubectl. Running kubectl options reveals a daunting array of flags that might seem overwhelming to implement from scratch.

Fortunately, the Kubernetes project offers Go libraries that handle much of this complexity for you. Two key packages simplify kubectl-style argument parsing: clientcmd and cli-runtime (which builds on clientcmd). This guide focuses on implementing the former.

General philosophy

As part of client-go, clientcmd delivers a restclient.Config instance configured to communicate with a Kubernetes API server.

The library mirrors kubectl behavior:

  • Default settings load from ~/.kube or the platform equivalent
  • The KUBECONFIG environment variable specifies alternative configuration files
  • Command line arguments override both environment and file-based settings

Note that clientcmd doesn't automatically create a --kubeconfig flag. You'll need to add this manually to match kubectl's interface, as demonstrated in the "Bind the flags" section below.

Available features

The clientcmd package provides support for:

  • Configuration file selection via KUBECONFIG
  • Context switching
  • Namespace targeting
  • Client certificate and private key authentication
  • User impersonation
  • HTTP Basic authentication credentials

Configuration merging

Configuration merging in clientcmd handles scenarios where multiple sources define the same settings. When KUBECONFIG lists multiple files, their contents combine according to specific precedence rules. Map-based settings follow a first-wins strategy where initial definitions take precedence. Non-map settings use last-wins behavior where later definitions override earlier ones.

Files referenced through KUBECONFIG generate warnings if missing, but don't halt execution. However, explicitly specified paths (like --kubeconfig) must point to existing files.

When KUBECONFIG remains unset, the library falls back to ~/.kube/config if available.

Overall process

The clientcmd package documentation outlines the standard implementation pattern:

loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
// if you want to change the loading rules (which files in which order), you can do so here

configOverrides := &clientcmd.ConfigOverrides{}
// if you want to change override values or bind them to flags, there are methods to help you

kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides)
config, err := kubeConfig.ClientConfig()
if err != nil {
 // Do something
}
client, err := metav1.New(config)
// ...

Implementation breaks down into six steps:

  1. Configure the loading rules
  2. Configure the overrides
  3. Build a set of flags
  4. Bind the flags
  5. Build the merged configuration
  6. Obtain an API client

Configure the loading rules

The clientcmd.NewDefaultClientConfigLoadingRules() function creates loading rules that read from either the KUBECONFIG environment variable or the default ~/.kube/config file. It also handles migration from the legacy ~/.kube/.kubeconfig location when using the default path.

While you can construct custom ClientConfigLoadingRules, the default configuration suits most use cases.

Configure the overrides

The clientcmd.ConfigOverrides struct stores values that supersede settings from configuration files. Its main role here is capturing command line argument values. The implementation uses the pflag library, which extends Go's standard flag package with POSIX-style long option names.

Typically, you'll initialize an empty overrides struct and populate it through flag binding.

Build a set of flags

Flags represent command line arguments with their long names (like --namespace), optional short names (like -n), default values, and help text. The FlagInfo struct stores these definitions.

Three predefined flag groups cover:

  • Authentication: certificates, tokens, impersonation, and basic auth credentials
  • Cluster: API server address, certificate authority, TLS settings, proxy, and compression
  • Context: cluster name, user name, and namespace

The recommended approach combines all three groups with context selection and timeout options.

Access these through Recommended…Flags functions, which accept a prefix parameter. This prefix prepends to all long option names.

Calling clientcmd.RecommendedConfigOverrideFlags("") with an empty prefix generates standard arguments like --context and --namespace (with -n shorthand). The --timeout flag defaults to zero. Using a prefix like "from-" produces --from-context, --from-namespace, and similar variants—useful for tools that interact with multiple clusters simultaneously.

Watch out for short name conflicts: prefixes don't affect short names, so multiple --namespace variants all try to claim -n. You'll need to clear the short name for all but one prefix:

kflags := clientcmd.RecommendedConfigOverrideFlags(prefix)
kflags.ContextOverrideFlags.Namespace.ShortName = ""

Disable flags entirely by clearing their long name:

kflags.ContextOverrideFlags.Namespace.LongName = ""

Bind the flags

After defining your flags, connect them to the overrides struct using clientcmd.BindOverrideFlags. This function requires a pflag FlagSet rather than Go's standard flag package.

To add --kubeconfig support, bind the loading rules' ExplicitPath field:

flags.StringVarP(&loadingRules.ExplicitPath, "kubeconfig", "", "", "absolute path(s) to the kubeconfig file(s)")

Build the merged configuration

Two functions create merged configurations:

The key distinction between these two functions lies in their authentication handling. The interactive variant can prompt users for credentials through a provided reader, while the non-interactive version relies solely on pre-supplied configuration data.

Both functions use "deferred" loading, meaning the final configuration isn't resolved until it's actually needed. This design allows you to initialize the configuration before parsing command-line arguments—the resulting config will automatically incorporate whatever values are available when it's eventually constructed.

Obtain an API client

These functions return a ClientConfig instance representing the merged configuration. To get a working API client, call its ClientConfig() method.

When no configuration exists—meaning KUBECONFIG is empty or references missing files, ~/.kube/config doesn't exist, and no command-line config is provided—you'll encounter a cryptic error mentioning KUBERNETES_MASTER. This legacy behavior persists to support kubectl's --local and --dry-run flags. To handle this gracefully, use clientcmd.IsEmptyConfig() to detect empty configuration scenarios and present users with a clearer error message.

The Namespace() method is equally valuable: it returns the appropriate namespace to use and indicates whether the user explicitly overrode it via --namespace.

Full example

Here's a working implementation that ties everything together:

package main

import (
 "context"
 "fmt"
 "os"

 "github.com/spf13/pflag"
 v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 "k8s.io/client-go/kubernetes"
 "k8s.io/client-go/tools/clientcmd"
)

func main() {
 // Loading rules, no configuration
 loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()

 // Overrides and flag (command line argument) setup
 configOverrides := &clientcmd.ConfigOverrides{}
 flags := pflag.NewFlagSet("clientcmddemo", pflag.ExitOnError)
 clientcmd.BindOverrideFlags(configOverrides, flags,
 clientcmd.RecommendedConfigOverrideFlags(""))
 flags.StringVarP(&loadingRules.ExplicitPath, "kubeconfig", "", "", "absolute path(s) to the kubeconfig file(s)")
 flags.Parse(os.Args)

 // Client construction
 kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides)
 config, err := kubeConfig.ClientConfig()
 if err != nil {
 if clientcmd.IsEmptyConfig(err) {
 panic("Please provide a configuration pointing to the Kubernetes API server")
 }
 panic(err)
 }
 client, err := kubernetes.NewForConfig(config)
 if err != nil {
 panic(err)
 }

 // How to find out what namespace to use
 namespace, overridden, err := kubeConfig.Namespace()
 if err != nil {
 panic(err)
 }
 fmt.Printf("Chosen namespace: %s; overridden: %t\n", namespace, overridden)

 // Let's use the client
 nodeList, err := client.CoreV1().Nodes().List(context.TODO(), v1.ListOptions{})
 if err != nil {
 panic(err)
 }
 for _, node := range nodeList.Items {
 fmt.Println(node.Name)
 }
}

This approach gives your tools the same familiar configuration patterns that Kubernetes users already know, making adoption seamless.