How Does Kubectl Work: Writing Custom Kubectl Commands
Kubernetes works like magic but it is not magic. It is fundamentally grounded in the simplicity of REST API calls. This straightforward mechanism is key to its powerful capabilities. Today, we’re going to dive into the inner workings of Kubernetes, specifically focusing on what happens behind the scenes when we execute a kubectl command.
At its core, kubectl is more than just a command-line interface; it's the bridge between the user and the Kubernetes api server. We will start with kubectl authentication and proceed with communication with api-server and finish off by forking Kubernetes repository and writing our own kubectl commands. You can access the fork in GitHub.
AUTHENTICATION
While Kubernetes offers a variety of authentication methods to interact with the API server, such as service account tokens for pods or certificate based authentication for Kubelet, our focus here is on the authentication process of kubectl. Here is an step by step explaination the of how kubectl authenticates:
- By default,
kubectllooks for a file namedconfigin the$HOME/.kubedirectory. You can specify other kubeconfig files by setting theKUBECONFIGenvironment variable or by setting the--kubeconfigflag.
2. The user section in a kubeconfig file details the credentials for authentication. These can vary significantly:
- Client Certificates: A common method where
kubectlutilizes a client certificate and key for authentication. - Bearer Tokens:
kubectlcan also authenticate using bearer tokens, included in the Authorization header of each request. - External Authentication Plugins: For more sophisticated authentication needs,
kubectlcan use external plugins, often essential in cloud-based Kubernetes environments.
- name: eksUser@goodjob.eu-west-2.eksctl.io
user:
exec:
apiVersion: client.authentication.k8s.io/v1beta1
args:
- eks
- get-token
- --output
- json
- --cluster-name
- goodjob
- --region
- eu-west-2
command: aws
env:
- name: AWS_STS_REGIONAL_ENDPOINTS
value: regional
interactiveMode: IfAvailable
provideClusterInfo: false
- name: minikube
user:
client-certificate: /Users/hasanbingolbali/.minikube/profiles/minikube/client.crt
client-key: /Users/hasanbingolbali/.minikube/profiles/minikube/client.keyProvided kubeconfig illustrates two distinct user entries in a kubeconfig file, each demonstrating a different method of authentication for kubectl.
** EKS User Authentication via AWS CLI
- User Entry:
eksUser@goodjob.eu-west-2.eksctl.io - Authentication Method: This entry uses an external command for authentication (
aws eks get-token). It's a common pattern for cloud-based Kubernetes services like AWS EKS (Elastic Kubernetes Service).
** Minikube User Authentication via Client Certificates
- User Entry:
minikube - Authentication Method: This entry uses client certificates for authentication, a common method for local or self-managed Kubernetes clusters
REST CALL
Now that we know Kubectl is an utility command line tool that make it easier to send rest-api calls to the apiserver, we should be able to send rest-calls by ourselves. However we’ll also need to CA certificate file-path or certificate-data which is also accessible from kube-config file.
- cluster:
certificate-authority: /Users/hasanbingolbali/.minikube/ca.crt
server: https://127.0.0.1:60749
name: minikubeLet’s send the following request in order to fetch information about the pods in default namespace to our api-server:
curl --cacert /Users/hasanbingolbali/.minikube/ca.crt \
--cert /Users/hasanbingolbali/.minikube/profiles/minikube/client.crt \
--key /Users/hasanbingolbali/.minikube/profiles/minikube/client.key \
https://localhost:60749/api/v1/namespaces/default/podsHowever, the rest-api contains so much information and you probably won’t need that much information for listing pods. And that’s where kubectl is making it easy for us to work with kubernetes.
WRITING CUSTOM KUBECTL COMMAND
Since we know that Kubernetes is an open-source project, we can explore and play with it. First step is to clone GitHub repository.
Kubectl package is under the ./staging/src/k8s.io/kubectl. Isolated kubectl package is also available in a separate repository. But for build purposes, you must clone the main repository. Under the cmd repository the keywords for kubectl are grouped into the folders as follows:
Let’s get into the kubectl/cmd/create folder and observe and talk about create_namespace.go file. For you to follow easily the code is provided below:
package create
import (
"context"
"fmt"
"github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
coreclient "k8s.io/client-go/kubernetes/typed/core/v1"
"k8s.io/kubectl/pkg/scheme"
"k8s.io/kubectl/pkg/util"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/cli-runtime/pkg/genericiooptions"
cmdutil "k8s.io/kubectl/pkg/cmd/util"
"k8s.io/kubectl/pkg/util/i18n"
"k8s.io/kubectl/pkg/util/templates"
)
var (
namespaceLong = templates.LongDesc(i18n.T(`
Create a namespace with the specified name.`))
namespaceExample = templates.Examples(i18n.T(`
# Create a new namespace named my-namespace
kubectl create namespace my-namespace`))
)
// NamespaceOptions is the options for 'create namespace' sub command
type NamespaceOptions struct {
// PrintFlags holds options necessary for obtaining a printer
PrintFlags *genericclioptions.PrintFlags
// Name of resource being created
Name string
DryRunStrategy cmdutil.DryRunStrategy
ValidationDirective string
CreateAnnotation bool
FieldManager string
Client *coreclient.CoreV1Client
PrintObj func(obj runtime.Object) error
genericiooptions.IOStreams
}
// NewNamespaceOptions creates a new *NamespaceOptions with sane defaults
func NewNamespaceOptions(ioStreams genericiooptions.IOStreams) *NamespaceOptions {
return &NamespaceOptions{
PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme),
IOStreams: ioStreams,
}
}
// NewCmdCreateNamespace is a macro command to create a new namespace
func NewCmdCreateNamespace(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobra.Command {
o := NewNamespaceOptions(ioStreams)
cmd := &cobra.Command{
Use: "namespace NAME [--dry-run=server|client|none]",
DisableFlagsInUseLine: true,
Aliases: []string{"ns"},
Short: i18n.T("Create a namespace with the specified name"),
Long: namespaceLong,
Example: namespaceExample,
Run: func(cmd *cobra.Command, args []string) {
cmdutil.CheckErr(o.Complete(f, cmd, args))
cmdutil.CheckErr(o.Validate())
cmdutil.CheckErr(o.Run())
},
}
o.PrintFlags.AddFlags(cmd)
cmdutil.AddApplyAnnotationFlags(cmd)
cmdutil.AddValidateFlags(cmd)
cmdutil.AddDryRunFlag(cmd)
cmdutil.AddFieldManagerFlagVar(cmd, &o.FieldManager, "kubectl-create")
return cmd
}
// Complete completes all the required options
func (o *NamespaceOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
name, err := NameFromCommandArgs(cmd, args)
if err != nil {
return err
}
restConfig, err := f.ToRESTConfig()
if err != nil {
return err
}
o.Client, err = coreclient.NewForConfig(restConfig)
if err != nil {
return err
}
o.Name = name
o.DryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd)
if err != nil {
return err
}
o.CreateAnnotation = cmdutil.GetFlagBool(cmd, cmdutil.ApplyAnnotationsFlag)
cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.DryRunStrategy)
printer, err := o.PrintFlags.ToPrinter()
if err != nil {
return err
}
o.PrintObj = func(obj runtime.Object) error {
return printer.PrintObj(obj, o.Out)
}
o.ValidationDirective, err = cmdutil.GetValidationDirective(cmd)
return err
}
// Run calls the CreateSubcommandOptions.Run in NamespaceOpts instance
func (o *NamespaceOptions) Run() error {
namespace := o.createNamespace()
if err := util.CreateOrUpdateAnnotation(o.CreateAnnotation, namespace, scheme.DefaultJSONEncoder()); err != nil {
return err
}
if o.DryRunStrategy != cmdutil.DryRunClient {
createOptions := metav1.CreateOptions{}
if o.FieldManager != "" {
createOptions.FieldManager = o.FieldManager
}
createOptions.FieldValidation = o.ValidationDirective
if o.DryRunStrategy == cmdutil.DryRunServer {
createOptions.DryRun = []string{metav1.DryRunAll}
}
var err error
namespace, err = o.Client.Namespaces().Create(context.TODO(), namespace, createOptions)
if err != nil {
return err
}
}
return o.PrintObj(namespace)
}
// createNamespace outputs a namespace object using the configured fields
func (o *NamespaceOptions) createNamespace() *corev1.Namespace {
namespace := &corev1.Namespace{
TypeMeta: metav1.TypeMeta{APIVersion: corev1.SchemeGroupVersion.String(), Kind: "Namespace"},
ObjectMeta: metav1.ObjectMeta{Name: o.Name},
}
return namespace
}
// Validate validates required fields are set to support structured generation
func (o *NamespaceOptions) Validate() error {
if len(o.Name) == 0 {
return fmt.Errorf("name must be specified")
}
return nil
}It is not that straightforward, but most of the code is about processing options and validation. I want to take your attention to the following line:
namespace, err = o.Client.Namespaces().Create(context.TODO(), namespace, createOptions)
This is where it sends request to the api-server. Now that we observed how we send request to the api-server. Let’s create file create_lovely_pod.go, that will create a pod with full of love.
type RunOptions struct {
cmdutil.OverrideOptions
PrintFlags *genericclioptions.PrintFlags
PrintObj func(obj runtime.Object) error
Client *coreclient.CoreV1Client
genericiooptions.IOStreams
}
func NewRunOptions(ioStreams genericiooptions.IOStreams) *RunOptions {
return &RunOptions{
PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme),
IOStreams: ioStreams,
}
}
func NewCmdRun(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
o := NewRunOptions(streams)
cmd := &cobra.Command{
Use: "lovely-pod",
DisableFlagsInUseLine: true,
Short: i18n.T("Run a particular image on the cluster"),
Long: "runLong",
Example: "runExample",
Run: func(cmd *cobra.Command, args []string) {
cmdutil.CheckErr(o.Run(f, cmd, args))
},
}
return cmd
}
func (o *RunOptions) Run(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
var err error
restConfig, err := f.ToRESTConfig()
if err != nil {
return err
}
o.Client, err = coreclient.NewForConfig(restConfig)
if err != nil {
return err
}
printer, err := o.PrintFlags.ToPrinter()
if err != nil {
return err
}
o.PrintObj = func(obj runtime.Object) error {
return printer.PrintObj(obj, o.Out)
}
pod, err := o.Client.Pods("default").Create(context.TODO(), &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "iloveyou",
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "nginx",
Image: "nginx",
},
},
},
}, metav1.CreateOptions{})
if err != nil {
return err
}
return o.PrintObj(pod)
}now that we created a custom kubectl command, let’s build kubectl using:
make WHAT=cmd/kubectl KUBE_VERBOSE=5
now that we should have binaries under the _output directory. Let’s test it.
Yes it worked, It is very basic implementation for writing custom kubectl but it is a big step to grasp kubectl fundamentals.
FINAL WORDS
I think, It was a very informative article. We went through the fundamentals and applied it by writing our custom kubectl command. You can access my fork in GitHub. If you liked the article please give it a like. In the upcoming article I will run performance benchmark tests for ORM and plain sql and I’ll share the creation process and result with you. You can follow me for more content like this. Good bye!
