© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2023
P. MartinKubernetes Programming with Gohttps://doi.org/10.1007/978-1-4842-9026-2_9

9. Working with Custom Resources

Philippe Martin1  
(1)
Blanquefort, France
 

In the previous chapter, you have seen how to declare a new custom resource to be served by the Kubernetes API using CustomResourceDefinition resources, and how to create new instances of this custom resource using kubectl. But for the moment, you do not have any Go library that allows you to work with instances of custom resources.

This chapter explores the various possibilities to work with custom resources in Go:
  • Generating code for a dedicated Clientset for the custom resource.

  • Using the unstructured package of the API Machinery Library and the dynamic package of the Client-go Library.

Generating a Clientset

The repository https://github.com/kubernetes/code-generator contains the Go code generators. Chapter 3’s “Content of a Package” section contains a very quick overview of these generators, where the content of the API Library was explored.

To use these generators, you will need to first write the Go structures for the kinds defined by the custom resource. In this example, you will write structures for the MyResource and MyResourceList kinds.

To stick with the organization found in the API Library, you will write these types in a types.go file, placed in the directory pkg/apis/<group>/<version>/.

Likewise, to work correctly with the generators, the root directory of your project must be in a directory named after the Go package after the Go package of your project. For example, if the package of the project is github.com/myid/myproject (defined in the first line of the project’s go.mod file), the root directory of the project must be in the directory github.com/myid/myproject/.

As an example, let’s start a new project. You can execute these commands from the directory of your choice, generally a directory containing all your Go projects.
$ mkdir -p github.com/myid/myresource-crd
$ cd github.com/myid/myresource-crd
$ go mod init github.com/myid/myresource-crd
$ mkdir -p pkg/apis/mygroup.example.com/v1alpha1/
$ cd pkg/apis/mygroup.example.com/v1alpha1/
Then, in this directory, you can create the types.go file that contains the definitions of the structures for the kinds. Here are the definitions of the structures that match the schema defined in the CRD in the previous chapter.
package v1alpha1
import (
     metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
     "k8s.io/apimachinery/pkg/util/intstr"
)
type MyResource struct {
     metav1.TypeMeta   `json:",inline"`
     metav1.ObjectMeta `json:"metadata,omitempty"`
     Spec MyResourceSpec `json:"spec"`
}
type MyResourceSpec struct {
     Image  string             `json:"image"`
     Memory resource.Quantity `json:"memory"`
}
type MyResourceList struct {
     metav1.TypeMeta `json:",inline"`
     metav1.ListMeta `json:"metadata,omitempty"`
     Items []MyResource `json:"items"`
}
Now, you need to run two generators:
  • deepcopy-gen – this will generate a DeepCopyObject() method for each Kind structure, which is needed for these types to implement the runtime.Object interface.

  • client-gen – this will generate the clientset for the group/version.

Using deepcopy-gen

Installing deepcopy-gen

To install the deepcopy-gen executable, you can use the go install command:
go install k8s.io/code-generator/cmd/[email protected]

You can use either the @latest tag to use the latest revision of the Kubernetes code or select a specific version.

Adding Annotations

The deepcopy-gen generator needs annotations to work. It first needs the // +k8s:deepcopy-gen=package annotation to be defined at the package level. This annotation asks deepcopy-gen to generate deepcopy methods for all structures of the package.

For this, you can create a doc.go file in the directory in which types.go resides, to add this annotation:
// pkg/apis/mygroup.example.com/v1alpha1/doc.go
// +k8s:deepcopy-gen=package
package v1alpha1
By default, deepcopy-gen will generate the DeepCopy() and DeepCopyInto() methods, but no DeepCopyObject(). For this, you need to add another annotation (// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object) before each kind structure.
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type MyResource struct {
[...]
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type MyResourceList struct {
[...]

Running deepcopy-gen

The generator needs a file that contains the text (generally the license) added at the beginning of the generated files. For this, you can create an empty file (or with the content you prefer, do not forget that the text should be Go comments) named hack/boilerplate.go.txt.

You need to run go mod tidy for the generator to work (or go mod vendor if you prefer to vendor Go dependencies). Finally, you can run the deepcopy-gen command, which will generate a file pkg/apis/mygroup.example.com/v1alpha1/zz_generated.deepcopy.go:
$ go mod tidy
$ deepcopy-gen --input-dirs github.com/myid/myresource-crd/pkg/apis/mygroup.example.com/v1alpha1
     -O zz_generated.deepcopy
     --output-base ../../..
     --go-header-file ./hack/boilerplate.go.txt

Note the “../../..” as output-base. It will place the output base in the directory from which you created the directory for the project:

$ mkdir -p github.com/myid/myresource-crd

You will need to adapt this to the number of subdirectories you created if different from the three.

At this point, you should have the following file structure for your project:
├── go.mod
├── hack
│   └── boilerplate.go.txt
├── pkg
│   └── apis
│       └── mygroup.example.com
│           └── v1alpha1
│               ├── doc.go
│               ├── types.go
│               └── zz_generated.deepcopy.go

Using client-gen

Installing client-go

To install the client-gen executable, you can use the go install command:
go install k8s.io/code-generator/cmd/[email protected]

You can use either the @latest tag to use the latest revision of the Kubernetes code or select a specific version if you want to run the command in a reproducible way.

Adding Annotations

You need to add annotations to the structures, defined in the types.go file, to indicate for which types you want to define a Clientset. The annotation to use is // +genclient.
  • // +genclient (with no option) will ask client-gen to generate a Clientset for a namespaced resource.

  • // +genclient:nonNamespaced will generate a Clientset for a non-namespaced resource.

  • +genclient:onlyVerbs=create,get will generate these verbs only, instead of generating all verbs by default.

  • +genclient:skipVerbs=watch will generate all verbs except these ones, instead of all verbs by default.

  • +genclient:noStatus – if a Status field is present in the annotated structure, an updateStatus function will be generated. With this option, you can disable the generation of the updateStatus function (note that it is not necessary if the Status field does not exist).

The custom resource you are creating is namespaced so you can use the annotation without an option:
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// +genclient
type MyResource struct {
[...]

Adding AddToScheme Function

The generated code relies on an AddToScheme function defined in the package of the resource. To stick with the convention found in the API Library, you will write this function in a register.go file, placed in the directory pkg/apis/<group>/<version>/.

By getting as boilerplate, the register.go file from a native Kubernetes resource from the Kubernetes API Library, you will obtain the following file. The only changes are to the group name (❶),version name (❷), and the list of resources (❸) to register to the scheme.
package v1alpha1
import (
     metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
     "k8s.io/apimachinery/pkg/runtime"
     "k8s.io/apimachinery/pkg/runtime/schema"
)
const GroupName = "mygroup.example.com"          ❶
var SchemeGroupVersion = schema.GroupVersion{
     Group: GroupName,
     Version: "v1alpha1",                         ❷
}
var (
     SchemeBuilder      = runtime.NewSchemeBuilder(addKnownTypes)
     localSchemeBuilder = &SchemeBuilder
     AddToScheme        = localSchemeBuilder.AddToScheme
)
func addKnownTypes(scheme *runtime.Scheme) error {
     scheme.AddKnownTypes(SchemeGroupVersion,
          &MyResource{},                         ❸
          &MyResourceList{},
     )
     metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
     return nil
}

Running client-go

The client-gen needs a file containing the text (generally the license) added at the beginning of generated files. You will use the same file as with deepcopy-gen: hack/boilerplate.go.txt.

You can run the client-gen command, which will generate files in the directory pkg/clientset/clientset:
client-gen
     --clientset-name clientset
     --input-base ""
     --input github.com/myid/myresource-crd/pkg/apis/mygroup.example.com/v1alpha1
     --output-package github.com/myid/myresource-crd/pkg/clientset
     --output-base ../../..
     --go-header-file hack/boilerplate.go.txt

Note the “../../..” as output-base. It will place the output base in the directory from which you created the directory for the project:

$ mkdir -p github.com/myid/myresource-crd

You will need to adapt this to the number of subdirectories you created if different from the three.

Note that you will need to run this command again when you update the definition of your custom resource. It is recommended to place this command in a Makefile to automatically run it every time the files defining the custom resource are modified.

Using the Generated Clientset

Now that the Clientset is generated and the types implement the runtime.Object interface, you can work with the custom resource the same way you work with the native Kubernetes resources. For example, this code will use the dedicated Clientset to list the custom resources on the default namespace:
import (
     "context"
     "fmt"
     "github.com/myid/myresource-crd/pkg/clientset/clientset"
     metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
     "k8s.io/client-go/tools/clientcmd"
)
config, err :=
     clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
          clientcmd.NewDefaultClientConfigLoadingRules(),
          nil,
     ).ClientConfig()
if err != nil {
     return err
}
clientset, err := clientset.NewForConfig(config)
if err != nil {
     return err
}
list, err := clientset.MygroupV1alpha1().
     MyResources("default").
     List(context.Background(), metav1.ListOptions{})
if err != nil {
     return err
}
for _, res := range list.Items {
     fmt.Printf("%s ", res.GetName())
}

Using the Generated fake Clientset

The client-gen tool also generates a fake Clientset that you can use the same way you use the fake Clientset from the Client-go Library. For more information, see Chapter 7’s “Fake Clientset” section.

Using the Unstructured Package and Dynamic Client

The Unstructured and UnstructuredList types are defined in the unstructured package of the API Machinery Library. The import to use is the following:
import (
     "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)

These types can be used to represent any Kubernetes Kind, either a list or a nonlist.

The Unstructured Type

The Unstructured type is defined as a structure containing a unique Object field:
type Unstructured struct {
     // Object is a JSON compatible map with
     // string, float, int, bool, []interface{}, or
     // map[string]interface{}
     // children.
     Object map[string]interface{}
}

Using this type, it is possible to define any Kubernetes resource without having to use the typed structures (e.g., the Pod structure, found in the API Library).

Getters and Setters methods are defined for this type to access generic fields from the TypeMeta and ObjectMeta fields, common to all structures representing Kubernetes Kinds.

Getters and Setters to Access TypeMeta Fields

The APIVersion and Kind Getters/Setters can be used to directly get and set the apiVersion and Kind fields of the TypeMeta.

The GroupVersionKind Getters/Setters can be used to convert the apiVersion and kind specified in the object to and from a GroupVersionKind value.
GetAPIVersion() string
GetKind() string
GroupVersionKind() schema.GroupVersionKind
SetAPIVersion(version string)
SetKind(kind string)
SetGroupVersionKind(gvk schema.GroupVersionKind)

Getters and Setters to Access ObjectMeta Fields

Getters and Setters are defined for all fields of the ObjectMeta structure. The details of the structure can be found in Chapter 3’s “The ObjectMeta Fields” section.

As an example, the getter and setter to access the Name field are GetName() string and SetName(name string).

Methods for Creating and Converting

  • NewEmptyInstance() runtime.Unstructured – this returns a new instance with only the apiVersion and kind fields copied from the receiver.

  • MarshalJSON() ([]byte, error) – this returns the JSON representation of the receiver.

  • UnmarshalJSON(b []byte) error – this populates the receiver with the passed JSON representation.

  • UnstructuredContent() map[string]interface{} – this returns the value of the Object field of the receiver.

  • SetUnstructuredContent(

    content map[string]interface{},

    )

    – this sets the value of the Object field of the receiver,].

  • IsList() bool – this returns true if the receiver describes a list, by checking if an items field exists, and is an array.

  • ToList() (*UnstructuredList, error) – this converts the receiver to an UnstructuredList.

Helpers to Access Non-meta Fields

The following helpers can be used to get and set the value of specific fields in the Object field of an Unstructured instance.

Note that these helpers are functions, and not methods on the Unstructured type.

They all accept:
  • A first parameter obj of type map[string]interface{} used to pass the Object field of the Unstructured instance,

  • A last parameter fields of type …string used to pass the keys to navigate into the object. Note that no array/slice syntax is supported.

The Setters accept a second parameter giving the value to the set for the specific field in the given object. The Getters return three values:
  • The value of the requested field, if possible

  • A boolean indicating if the requested field has been found

  • An error if the field has been found but is not of the requested type

The names of the helper functions are:
  • RemoveNestedField – this removes the requested field

  • NestedFieldCopy, NestedFieldNoCopy – this returns a copy or the original value of the requested field

  • NestedBool, NestedFloat64, NestedInt64, NestedString, SetNestedField - this gets and sets bool / float64 / int64 / string field

  • NestedMap, SetNestedMap - this gets and sets fields of type map[string]interface{}

  • NestedSlice, SetNestedSlice - this gets and sets fields of type []interface{}

  • NestedStringMap, SetNestedStringMap - this gets and sets fields of type map[string]string

  • NestedStringSlice, SetNestedStringSlice - this gets and sets fields of type []string

Example

As an example, here is some code that defines a MyResource instance:
import (
     myresourcev1alpha1 "github.com/myid/myresource-crd/pkg/apis/mygroup.example.com/v1alpha1"
     "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
func getResource() (*unstructured.Unstructured, error) {
     myres := unstructured.Unstructured{}
     myres.SetGroupVersionKind(
          myresourcev1alpha1.SchemeGroupVersion.
               WithKind("MyResource"))
     myres.SetName("myres1")
     myres.SetNamespace("default")
     err := unstructured.SetNestedField(
          myres.Object,
          "nginx",
          "spec", "image",
     )
     if err != nil {
          return err
     }
     // Use int64
     err = unstructured.SetNestedField(
          myres.Object,
          int64(1024*1024*1024),
          "spec", "memory",
     )
     if err != nil {
          return err
     }
     // or use string
     err = unstructured.SetNestedField(
          myres.Object,
          "1024Mo",
          "spec", "memory",
     )
     if err != nil {
          return err
     }
     return &myres, nil
}

The UnstructuredList Type

The UnstructuredList type is defined as a structure containing an Object field and a slice of Unstructured instances as items:
type UnstructuredList struct {
     Object map[string]interface{}
     // Items is a list of unstructured objects.
     Items []Unstructured
}

Getters and Setters to Access TypeMeta Fields

The APIVersion and Kind Getters/Setters can be used to directly get and set the apiVersion and kind fields of the TypeMeta.

The GroupVersionKind Getters/Setters can be used to convert the apiVersion and kind specified in the object to and from a GroupVersionKind value.
GetAPIVersion() string
GetKind() string
GroupVersionKind() schema.GroupVersionKind
SetAPIVersion(version string)
SetKind(kind string)
SetGroupVersionKind(gvk schema.GroupVersionKind)

Getters and Setters to Access ListMeta Fields

These getters and setters are used to get and set values related to the result of List operations, in the ListMeta field of the List.
GetResourceVersion() string
GetContinue() string
GetRemainingItemCount() *int64
SetResourceVersion(version string)
SetContinue(c string)
SetRemainingItemCount(c *int64)

Methods for Creating and Converting

  • NewEmptyInstance() runtime.Unstructured – this creates a new instance of an Unstructured object, using the apiVersion and kind copied from the List receiver.

  • MarshalJSON() ([]byte, error) – this returns the JSON representation of the receiver.

  • UnmarshalJSON(b []byte) error – this populates the receiver with the passed JSON representation.

  • UnstructuredContent() map[string]interface{} SetUnstructuredContent(content map[string]interface{})– this gets the value of the Object field of the receiver.

  • EachListItem(fn func(runtime.Object) error) error – this executes the fn function for each item of the list.

Converting Between Typed and Unstructured Objects

The runtime package of the API Machinery Library provides utilities to convert between typed objects and unstructured objects, objects being either a resource or a list.
import (
     "k8s.io/apimachinery/pkg/runtime"
)
converter := runtime.DefaultUnstructuredConverter          ❶
var pod corev1.Pod
converter.FromUnstructured(                                   ❷
     u.UnstructuredContent(), &pod,
)
var u unstructured.Unstructured
u.Object = converter.ToUnstructured(&pod)                    ❸
  • ❶ Get the converter

  • ❷ Convert an unstructured object (defining a Pod) to a typed Pod

  • ❸ Convert a typed Pod to an unstructured object

The Dynamic Client

As you have seen in Chapter 6, the Client-go provides clients with the ability to work with the Kubernetes API: the Clientset to access typed resources, the REST client to make low-level REST calls to the API, and the Discovery client to get information about the resources served by the API.

It provides another client, the dynamic client, to work with untyped resources, described with the Unstructured type.

Getting the dynamic Client

The dynamic package provides functions to create a dynamic client of the type dynamic.Interface.
  • func NewForConfig(c *rest.Config) (Interface, error) – this function returns a dynamic client, using the provided rest.Config built with one of the methods seen in chapter 6, section “Connecting to the cluster”.

  • func NewForConfigOrDie(c *rest.Config) Interface – this function is like the previous one, but panics in case of error, instead of returning the error. This function can be used with a hard-coded configuration, for which you want to assert the validity.

  • NewForConfigAndClient(–

    c *rest.Config,

    httpClient *http.Client,

    ) (Interface, error)

    - this function returns a dynamic client, using the provided rest.Config, and the provided httpClient.

    The previous function NewForConfig uses a default HTTP Client built with the function rest.HTTPClientFor. If you want to personalize the HTTP Client before building the dynamic client, you can use this function instead.

Working with the dynamic Client

The dynamic client implements the dynamic.Interface, defined as follows:
type Interface interface {
     Resource(resource schema.GroupVersionResource)
          NamespaceableResourceInterface
}
The only direct method for the dynamic client is Resource(gvr), returning an object implementing NamespaceableResourceInterface.
type NamespaceableResourceInterface interface {
     Namespace(string) ResourceInterface
     ResourceInterface
}

It is possible to chain the call to Resource(gvr) with a call to the method Namespace(ns), returning an object implementing the ResourceInterface. Or, if Resource(gvr) is not chained with the Namespace(ns) method, it also implements the ResourceInterface.

Thanks to this, after calling Resource(gvr), you can either chain with Namespace(ns), if the resource described by gvr is a namespaced resource, and you want to define on which namespace to work, or else you can omit this call for non-namespaced resources if you want to make a cluster-wide operation.

The ResourceInterface is defined as (the complete signature of functions is omitted for conciseness) follows:
type ResourceInterface interface {
     Create(...)
     Update(...)
     UpdateStatus(...)
     Delete(...)
     DeleteCollection(...)
     Get(...)
     List(...)
     Watch(...)
     Patch(...)
     Apply(...)          # starting at v1.25
     ApplyStatus(...)     # starting at v1.25
}
These methods work with Unstructured and UnstructuredList types. For example, the Create method accepts an Unstructured object as input, and returns the created object as Unstructured, and the List method returns the list of objects as an UnstructuredList:
Create(
     ctx context.Context,
     obj *unstructured.Unstructured,
     options metav1.CreateOptions,
     subresources ...string,
) (*unstructured.Unstructured, error)
List(
     ctx context.Context,
     opts metav1.ListOptions,
) (*unstructured.UnstructuredList, error)
If we compare the signatures of these methods with the methods provided by the Client-go Clientset, you can see that they are very similar. The following are the changes:
  • Typed objects (e.g., corev1.Pod) for Clientset are replaced by Unstructured for the dynamic client.

  • A subresource… parameter is present for dynamic methods, whereas Clientset provides specific methods for subresources.

You can refer to Chapter 6 for more about the behavior of the various operations.

Example

As an example, here is some code that creates a MyResource instance in the cluster:
import (
     "context"
     myresourcev1alpha1 "github.com/feloy/myresource-crd/pkg/apis/mygroup.example.com/v1alpha1"
     metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
     "k8s.io/client-go/dynamic"
)
func CreateMyResource(
     dynamicClient dynamic.Interface,
     u *unstructured.Unstructured,
) (*unstructured.Unstructured, error) {
     gvr := myresourcev1alpha1.
          SchemeGroupVersion.
          WithResource("myresources")
     return dynamicClient.
          Resource(gvr).
          Namespace("default").
          Create(
               context.Background(),
               u,
               metav1.CreateOptions{},
          )
}

The fake dynamic Client

As you have seen in Chapter 7, the Client-go Library provides fake implementations for the Clientset, the discovery client, and the REST client. The library also provides a fake implementation for the dynamic client.

Like other fake implementations, you can register reactors using PrependReactor and similar methods on the dynamicClient.Fake object, and you can inspect Actions of the dynamicClient.Fake object after the test is executed.

The function fake.NewSimpleDynamicClient is used to create a new fake dynamic client.
func NewSimpleDynamicClient(
     scheme *runtime.Scheme,
     objects ...runtime.Object,
) *FakeDynamicClient)

The objects parameter indicates the resources to create in the fake cluster when creating the fake client. As an example, here is a test for a function using the dynamic client.

The NewSimpleDynamicClient function is called with the resource to create an initial resource. Note that the expected type for initial resources is runtime.Object, which is an interface implemented by Unstructured.
func TestCreateMyResourceWhenResourceExists(t *testing.T) {
     myres, err := getResource()
     if err != nil {
          t.Error(err)
     }
     dynamicClient := fake.NewSimpleDynamicClient(
          runtime.NewScheme(),
          myres,
     )
     // Not really used, just to show how to use it
     dynamicClient.Fake.PrependReactor(
          "create",
          "myresources",
          func(
               action ktesting.Action,
          ) (handled bool, ret runtime.Object, err error) {
               return false, nil, nil
     })
     _, err = CreateMyResource(dynamicClient, myres)
     if err == nil {
          t.Error("Error should happen")
     }
     actions := dynamicClient.Fake.Actions()
     If len(actions) != 1 {
          t.Errorf(“# of actions should be %d but is %d”, 1, len(actions)
     }
}

Conclusion

This chapter has explored various solutions to work with custom resources in Go.

A first solution is to generate Go code based on the definition of a custom resource, to generate a Clientset dedicated to this specific custom resource definition, so you can work with custom resource instances the same as you have been working with for native Kubernetes resources.

Another solution is to work with the dynamic client from the Client-go Library and to rely on the Unstructured type to define the custom resources.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.63.136