Using Kubernetes objects in Go

Posted by boon4376 on Fri, 10 Dec 2021 04:06:35 +0100

By Jason Snouffer Translator | Luga Lee Planning | Luga Lee

In general, in some cases, we need a common way to use Kubernetes resource objects instead of writing code to deal with specific types. For example, some simple use case reference scenarios are as follows:

1. Use K8s API objects from plug-ins that do not have an associated Golang structure.

2. Perform general CRUD (create / read / update / delete) operations on K8s objects using JsonPath, JMESPath, jq, etc. A common approach is needed to avoid having to write explicit code to handle every possible resource type.

API Machinery SIG

Many projects in the Kubernetes community are managed by special interest groups (sigs). API machine sig is used to maintain the client library of K8s API server interface, including the package for general API CRUD semantics. Client go, a famous sub project of API machine, is an official Go API for interacting with K8s API server.

The most common entry point for client go is kubernetes Clientset, a group of typed clients, provides pre generated local API objects for each core resource type (Pod, deployment, service, etc.). Based on its ease of use, we recommend that you use this entry point as much as possible. However, using typed clients can be very limited because the code is often tightly coupled to the specific type and version used.

Client go / dynamic and unstructured objects

The universal machine subproject of API machine maintains a shared dependency library for servers and clients to use Kubernetes API infrastructure without direct type dependency. Unstructured packages are part of this shared dependency library, allowing common operations on K8s resource objects.

struct unstructured.Unstructured is a simple type that uses a set of nested map[string]interface {} values to create an internal structure that is very similar to the REST load from the K8s API server.

The client go / dynamic package provides a dynamic client that can perform RESTful operations on any API resource. struct dynamic.Interface uses unstructured Unstructured to represent all object values from the API server. Dynamic packages defer all data binding until runtime.

Basic example

The following code example requires a dependency k8s IO / client go / Kubernetes and sigs k8s. io/controller-runtime. The controller runtime project is a set of libraries for building Kubernetes operators. You can use client go without a controller runtime, but it simplifies configuring the client go client for K8s API server access.

When configuring client go for API access, there are two common configuration methods. When running within the Pod, use the in cluster configuration and use the service account token mounted to the Pod. When running outside the cluster, use the out of cluster configuration and use the provided kubeconfig file or the default kubeconfig file of the current user. The controller runtime library provides a convenient all-in-one function GetConfig(). It first attempts to configure outside the cluster. If it fails, it attempts to configure inside the cluster.

To add the required dependencies to the Go project, execute the following command:

go get k8s.io/client-go/kubernetes
go get sigs.k8s.io/controller-runtime

The following example is functionally equivalent, but demonstrates the semantic differences when using typed and dynamic clients.

Use kubernetes Clientset get K8s object

The following code snippet defines a function to use data from kubernetes The typed deployment client of clientset retrieves K8s deployment objects.

package main

import (
  "context"
  "fmt"

  v1 "k8s.io/api/apps/v1"
  metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  "k8s.io/client-go/kubernetes"
  ctrl "sigs.k8s.io/controller-runtime"
)

func main() {
  ctx := context.Background()
  config := ctrl.GetConfigOrDie()
  clientset := kubernetes.NewForConfigOrDie(config)

  namespace := "default"
  items, err := GetDeployments(clientset, ctx, namespace)
  if err != nil {
    fmt.Println(err)
  } else {
    for _, item := range items {
      fmt.Printf("%+v\n", item)
    }
  }
}

func GetDeployments(clientset *kubernetes.Clientset, ctx context.Context,
      namespace string) ([]v1.Deployment, error) {

  list, err := clientset.AppsV1().Deployments(namespace).
    List(ctx, metav1.ListOptions{})
  if err != nil {
    return nil, err
  }
  return list.Items, nil
}

Use dynamic Interface get K8s object

The following code snippet defines a function that uses a dynamic client to retrieve K8s objects. Call this function to retrieve the deployment list in the default namespace.

import (
  "context"
  "fmt"

  metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
  "k8s.io/apimachinery/pkg/runtime/schema"
  "k8s.io/client-go/dynamic"
  ctrl "sigs.k8s.io/controller-runtime"
)

func main() {
  ctx := context.Background()
  config := ctrl.GetConfigOrDie()
  dynamic := dynamic.NewForConfigOrDie(config)

  namespace := "default"
  items, err := GetResourcesDynamically(dynamic, ctx,
        "apps", "v1", "deployments", namespace)
  if err != nil {
    fmt.Println(err)
  } else {
    for _, item := range items {
      fmt.Printf("%+v\n", item)
    }
  }
}

func GetResourcesDynamically(dynamic dynamic.Interface, ctx context.Context,
  group string, version string, resource string, namespace string) (
  []unstructured.Unstructured, error) {

  resourceId := schema.GroupVersionResource{
    Group:    group,
    Version:  version,
    Resource: resource,
  }
  list, err := dynamic.Resource(resourceId).Namespace(namespace).
    List(ctx, metav1.ListOptions{})

  if err != nil {
    return nil, err
  }

  return list.Items, nil
}

In these two examples, it is obvious that it is simpler and less code to use a typed client to process K8s objects. However, the dynamic method is more powerful and flexible, especially when the resource type is unknown in advance or needs to use a custom resource definition that lacks an associated Golang structure.

Advanced examples

The use case that really benefits from the flexibility provided by the dynamic client is to use Jq to evaluate or change K8s objects. For JSON data, Jq is like sed, awk, and grep. It is a useful companion for Kubectl and simplifies the reading, parsing and mutation of K8s objects.

In this case, writing explicit type processing for each resource type encountered can be tedious. In addition, you may not know all the resource types you may encounter in advance.

This code example uses GitHub COM / itchyny / gojq, which is a pure Go implementation of jq. To add the required dependencies to the Go project, execute the following command:

go get github.com/itchyny/gojq

Check the Kubernetes object for a specific label

The following code snippet reuses the GetResourcesDynamically function in the previous example to get the deployment list in the default namespace. Then check whether each deployment uses the jq tag app kubernetes. IO / managed by is set to Helm value.

In order to be jq evaluated, the object returned from the API server must be converted to JSON. K8s. The IO / apimachinery / PKG / runtime package passes through the runtime The defaultunstructuredconverter provides an unstructured to JSON converter to simplify this process. Once converted to JSON, the jq evaluation is performed, and if it returns a boolean result and the result is "true", the K8s object is added to the slice returned by the function.

package main

import (
  "context"
  "fmt"

  "github.com/itchyny/gojq"
  "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
  "k8s.io/apimachinery/pkg/runtime"
  "k8s.io/client-go/dynamic"
  ctrl "sigs.k8s.io/controller-runtime"
)

func main() {
  ctx := context.Background()
  config := ctrl.GetConfigOrDie()
  dynamic := dynamic.NewForConfigOrDie(config)

  namespace := "default"
  query := ".metadata.labels[\"app.kubernetes.io/managed-by\"] == \"Helm\""
  items, err := GetResourcesByJq(dynamic, ctx, "apps", "v1", "deployments", namespace, query)
  if err != nil {
    fmt.Println(err)
  } else {
    for _, item := range items {
      fmt.Printf("%+v\n", item)
    }
  }
}

func GetResourcesByJq(dynamic dynamic.Interface, ctx context.Context, group string,
  version string, resource string, namespace string, jq string) (
  []unstructured.Unstructured, error) {

  resources := make([]unstructured.Unstructured, 0)

  query, err := gojq.Parse(jq)
  if err != nil {
    return nil, err
  }

  items, err := GetResourcesDynamically(dynamic, ctx, group, version, resource, namespace)
  if err != nil {
    return nil, err
  }

  for _, item := range items {
    // Convert object to raw JSON
    var rawJson interface{}
    err = runtime.DefaultUnstructuredConverter.FromUnstructured(item.Object, &rawJson)
    if err != nil {
      return nil, err
    }

    // Evaluate jq against JSON
    iter := query.Run(rawJson)
    for {
      result, ok := iter.Next()
      if !ok {
        break
      }
      if err, ok := result.(error); ok {
        if err != nil {
          return nil, err
        }
      } else {
        boolResult, ok := result.(bool)
        if !ok {
          fmt.Println("Query returned non-boolean value")
        } else if boolResult {
          resources = append(resources, item)
        }
      }
    }
  }
  return resources, nil
}

Once again, the above example can be completed more easily and with less code using typed clients. However, this is because we know that we are dealing with deployment and looking at Kubernetes metadata, which is common in all object types. However, imagine how much code we would need if we were writing a function that could evaluate any field in any object type. Without the ability of dynamic client, access to the underlying JSON content and jq, it will be an impossible task.

generalization

In this article, we evaluated the use of real-time Kubernetes objects in Go using the typed and dynamic clients provided by the API machine subproject client Go. For basic use cases, typed clients provide simple and elegant access to K8s objects. However, if there are many object types, or a specific object type is not known before the type, or the object type comes from a third-party resource that lacks an associated Golang structure, the dynamic client provides the required flexibility.

reference resources

  • https://github.com/kubernetes/client-go
  • https://github.com/kubernetes/apimachinery
  • https://jqplay.org