What is client-go?
client-go
is the official Golang client for Kubernetes, responsible for interacting with the Kubernetes API server using REST API. In fact, client-go
can do almost anything, not just for writing operators. Even the internal implementation of kubectl
is based on client-go
. As for more specialized frameworks used to write operators, including
controller-runtime
,
kubebuilder
, and
operator-sdk
, they will be introduced later in this series.
Introduction to Sample Controller Mechanism
sample-controller is an official Kubernetes example operator implemented using client-go.
To understand the code, we need to first understand how the operator we write interacts with client-go
. The explanation here is a simplified version of the
official documentation
.
The above image comes from the official documentation and can also be found in many tutorials online.
The upper part of the image shows the internal components of client-go
. It looks complicated, with terms like Reflector, Informer, and Indexer, but actually, you only need to understand Informer. The main function of Informer is to notify us when the status of resources changes. Why don’t we just hit the API directly to the Kubernetes API server? This is because calling the API is an expensive operation, and so the Informer maintains an Informer Cache to reduce the number of requests to the API server.
The lower part shows what we need to write ourselves:
- Resource Event Handlers: When Informer notifies us of a change in a resource’s status, we decide what to do, which usually means putting its key (namespace + name) into the workqueue.
- Workqueue: This stores the keys of all objects waiting to be processed. Our operator constantly retrieves items from the workqueue and tries to bring the cluster to the desired state. If it fails, the object key may need to be added back to the workqueue for further processing.
Sample Controller Codebase Walkthrough
Defining the CRD
In
register.go
, the GroupName is defined, and in
v1alpha1/types.go
, the type for the CRD is defined. You can see that it defines a Foo
resource as follows:
|
|
Apart from the basic TypeMeta
and ObjectMeta
, it defines Spec
and Status
. Spec
is where users can input data, defining the “desired state of the resource.” Status
is where our Operator writes values, representing the “current state of the resource.”
The Sample Controller uses
Kubernetes’ code-generator
to generate typed clients, informers, listers, and deep-copy functions for the CRD. So whenever you modify types.go
, you need to run ./hack/update-codegen.sh
to regenerate the code.
Program Entrypoint
Next, look at main.go , which is the entry point of the program. It’s actually very simple, just pay attention to these lines:
|
|
Basically, it creates clients and informers for both Kubernetes built-in resources and our custom Foo
resource, then passes them to NewController
, and finally calls controller.Run
.
Main Logic
Now, let’s examine the main part: controller.go .
|
|
This part shows the event handler we talked about earlier, where you can register AddFunc
, UpdateFunc
, and DeleteFunc
. When the informer detects a change in the resource, it will call the corresponding function. You can see that for fooInformer
, it simply calls enqueueFoo
, while for deploymentInformer
, it calls handleObject
.
|
|
enqueueFoo
is just adding the key of the Foo
object to the workqueue. You can see here:
- cache.ObjectToName
: Takes an object and converts it to
ObjectName
. - ObjectName : This is just namespace + name.
|
|
This is part of the handleObject
function. It checks whether the owner of the deployment is Foo
. If it’s not, we ignore it. If it is, we add the corresponding Foo
key to the workqueue. This relates to a concept called
OwnerReference
, where certain objects in Kubernetes are owned by others. The default behavior is that when the owner is deleted, the owned objects are also deleted. For example, a ReplicaSet is the owner of Pods, so when the ReplicaSet is deleted, the Pods it manages are also deleted. This is also why there is no DeleteFunc
handler for fooInformer
— when Foo
is deleted, we want to delete all corresponding deployments, but since the owner of the deployment is already set to Foo
, they will be deleted automatically without further handling.
|
|
Run
is the entry point called by the controller in main.go
. It starts multiple goroutines to run runWorker
. runWorker
is simply an infinite loop calling processNextWorkItem
.
|
|
This is a portion of processNextWorkItem
. First, it retrieves an object key from the workqueue, then calls syncHandler
to handle it. If successful, it removes it from the workqueue. Otherwise, it performs error handling and puts the key back into the workqueue for later processing.
|
|
Finally, this is a portion of syncHandler
. Here is where we write the actual logic, adjusting the cluster to match the desired state declared by the user in the Spec
. The desired state in this case is that the deployment specified in Spec
has been created and that the replica count matches what is declared in Spec
.
Conclusion
After going through this, you may feel that I’ve only covered a small part of the Sample Controller code. That’s because client-go
is a rather low-level library, and there are some downsides to using it for writing operators:
- We don’t need to write much custom logic, but still have to write some boilerplate, which can feel redundant.
- When watching to different resources, we need to declare informers, listers, and other repetitive things for each resource. For example, in the Sample Controller, both
fooInformer
anddeploymentInformer
are declared, and managing multiple resources becomes cumbersome.
These drawbacks have led to the development of other frameworks that are more specialized for writing operators, such as controller-runtime , kubebuilder , and operator-sdk . Stay tuned for future articles in this series to learn about these frameworks.