iximiuz/client-go-examples

Example for create object from file via dynamic API

yuzhichang opened this issue · 5 comments

I need code to create object from file just like kubectl since I don't want to depend on kubectl executable.

The following code works for creating Deployment, Service etc but not work for ClusterRole, ClusterRoleBind etc. The error is:
client.Resource.Namespace.Create failed: the server could not find the requested resource.

Could you help to fix it?

package main

import (
	"context"
	"flag"
	"fmt"
	"io/fs"
	"os"
	"path/filepath"
	"sort"

	"github.com/thanos-io/thanos/pkg/errors"
	apiv1 "k8s.io/api/core/v1"
	apiErrors "k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/api/meta"
	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"
	"k8s.io/client-go/tools/clientcmd"
	"k8s.io/client-go/util/homedir"
	"sigs.k8s.io/yaml"
)

func kind2Resource(group, version, kind string) (gvr schema.GroupVersionResource, err error) {
	// https://www.cnblogs.com/zhangmingcheng/p/16128224.html
	// https://iximiuz.com/en/posts/kubernetes-api-structure-and-terminology/
	// https://github.com/kubernetes/kubernetes/issues/18622
	// kubectl api-resources
	gvk := schema.GroupVersionKind{Group: group, Version: version, Kind: kind}
	gvr, _ = meta.UnsafeGuessKindToResource(gvk)
	//fmt.Printf("%s %s %s -> %+v\n", group, version, kind, gvr)
	return
}

func createObject(client dynamic.Interface, fp string) (err error) {
	fmt.Printf("Reading %s...\n", fp)
	var b []byte
	if b, err = os.ReadFile(fp); err != nil {
		err = errors.Wrapf(err, "os.ReadFile failed")
		return
	}
	m := make(map[string]interface{})
	if err = yaml.Unmarshal(b, &m); err != nil {
		err = errors.Wrapf(err, "yaml.Unmarshal failed")
		return
	}

	obj := unstructured.Unstructured{Object: m}
	var apiVersion, kind, namespace string
	apiVersion = obj.GetAPIVersion()
	kind = obj.GetKind()
	namespace = obj.GetNamespace()
	if namespace == "" {
		namespace = apiv1.NamespaceDefault
	}
	gv, _ := schema.ParseGroupVersion(apiVersion)
	var gvr schema.GroupVersionResource
	if gvr, err = kind2Resource(gv.Group, gv.Version, kind); err != nil {
		return
	}
	var result *unstructured.Unstructured
	result, err = client.Resource(gvr).Namespace(namespace).Create(context.TODO(), &obj, metav1.CreateOptions{})
	if err != nil {
		err = errors.Wrapf(err, "client.Resource.Namespace.Create failed")
		return
	}
	fmt.Printf("Created resource %q.\n", result.GetName())
	return
}

func main() {
	// Initialize client
	var kubeconfig *string
	if home := homedir.HomeDir(); home != "" {
		kubeconfig = flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "(optional) absolute path to the kubeconfig file")
	} else {
		kubeconfig = flag.String("kubeconfig", "", "absolute path to the kubeconfig file")
	}
	pth := flag.String("f", ".", "path of manifest file or directory")
	flag.Parse()

	config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig)
	if err != nil {
		panic(err)
	}
	client, err := dynamic.NewForConfig(config)
	if err != nil {
		panic(err)
	}

	// Get manifest list
	var fi fs.FileInfo
	if fi, err = os.Stat(*pth); err != nil {
		panic(err)
	}
	var fps []string
	if fi.IsDir() {
		var des []fs.DirEntry
		if des, err = os.ReadDir(*pth); err != nil {
			panic(err)
		}
		for _, de := range des {
			if de.IsDir() {
				continue
			}
			fn := de.Name()
			ext := filepath.Ext(fn)
			if ext != ".yaml" && ext != "yml" {
				continue
			}
			fps = append(fps, filepath.Join(*pth, fn))
		}
		sort.Strings(fps)
	} else {
		fps = append(fps, *pth)
	}

	// Create resource for each manifest
	for _, fp := range fps {
		if err = createObject(client, fp); err != nil {
			if !apiErrors.IsAlreadyExists(err) {
				fmt.Println(err)
			}
		}
	}
}

# blackboxExporter-clusterRoleBinding.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  labels:
    app.kubernetes.io/component: exporter
    app.kubernetes.io/name: blackbox-exporter
    app.kubernetes.io/part-of: kube-prometheus
    app.kubernetes.io/version: 0.22.0
  name: blackbox-exporter
  namespace: monitoring
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: blackbox-exporter
subjects:
- kind: ServiceAccount
  name: blackbox-exporter
  namespace: monitoring

There might be an alternative solution - check out the "Read Kubernetes Manifests Into Go Structs" section of this article of mine. And here is the corresponding mini-program from this repo. Hope this helps!

@iximiuz info.Object has different type with unstructured.Unstructured.Object. I cannot figure out how to use info.Object with dynamic API. Could you provide a complete example?

@yuzhichang info.Object is of the type runtime.Object. It's a special interface type that can front many concrete API types, in particular unstructured.Unstructured.

Here is an example of how to type-cast a runtime.Object into a concrete unstructured object. And you can read more about the Kubernetes API types in another my article.

I've figured out the reason of error client.Resource.Namespace.Create failed: the server could not find the requested resource.
Cluster-wide resources' namespace shall not be specified at creation.

The complete code is the following:

package main

import (
	"context"
	"flag"
	"fmt"
	"io/fs"
	"os"
	"path/filepath"
	"sort"
	"strings"

	"github.com/thanos-io/thanos/pkg/errors"
	apiv1 "k8s.io/api/core/v1"
	apiErrors "k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/api/meta"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/util/yaml"
	"k8s.io/client-go/dynamic"
	"k8s.io/client-go/tools/clientcmd"
	"k8s.io/client-go/util/homedir"
)

func kind2Resource(group, version, kind string) (gvr schema.GroupVersionResource, err error) {
	// https://www.cnblogs.com/zhangmingcheng/p/16128224.html
	// https://iximiuz.com/en/posts/kubernetes-api-structure-and-terminology/
	// https://github.com/kubernetes/kubernetes/issues/18622
	// kubectl api-resources
	gvk := schema.GroupVersionKind{Group: group, Version: version, Kind: kind}
	gvr, _ = meta.UnsafeGuessKindToResource(gvk)
	//fmt.Printf("%s %s %s -> %+v\n", group, version, kind, gvr)
	return
}

func isClusterWideResource(resource string) bool {
	if strings.HasPrefix(resource, "cluster") || resource == "customresourcedefinitions" || resource == "namespaces" {
		return true
	}
	return false
}

func createObject(client dynamic.Interface, fp string) (err error) {
	fmt.Printf("Reading %s\n", fp)
	// YAML -> Unstructured (through JSON)
	var yConfigMap, jConfigMap []byte
	if yConfigMap, err = os.ReadFile(fp); err != nil {
		err = errors.Wrapf(err, "os.ReadFile failed")
		return
	}
	jConfigMap, err = yaml.ToJSON(yConfigMap)
	if err != nil {
		err = errors.Wrapf(err, "yaml.ToJSON failed")
		return
	}

	var object runtime.Object
	object, err = runtime.Decode(unstructured.UnstructuredJSONScheme, jConfigMap)
	if err != nil {
		err = errors.Wrapf(err, "runtime.Decode failed")
		return
	}

	var uConfigMaps []unstructured.Unstructured
	switch t := object.(type) {
	case *unstructured.Unstructured:
		uConfigMaps = append(uConfigMaps, *t)
	case *unstructured.UnstructuredList:
		uConfigMaps = t.Items
	default:
		err = errors.Wrapf(err, "unstructured.Unstructured or unstructured.UnstructuredList expected")
		return
	}
	for _, uConfigMap := range uConfigMaps {
		var apiVersion, kind, namespace string
		apiVersion = uConfigMap.GetAPIVersion()
		kind = uConfigMap.GetKind()
		gv, _ := schema.ParseGroupVersion(apiVersion)
		var gvr schema.GroupVersionResource
		if gvr, err = kind2Resource(gv.Group, gv.Version, kind); err != nil {
			return
		}
		if isClusterWideResource(gvr.Resource) {
			_, err = client.Resource(gvr).Create(context.TODO(), &uConfigMap, metav1.CreateOptions{})
		} else {
			namespace = uConfigMap.GetNamespace()
			if namespace == "" {
				namespace = apiv1.NamespaceDefault
			}
			_, err = client.Resource(gvr).Namespace(namespace).Create(context.TODO(), &uConfigMap, metav1.CreateOptions{})
		}
		if err != nil {
			if apiErrors.IsAlreadyExists(err) {
				err = nil
				fmt.Printf("Object already exist: namespace %s, name %s, resource %s\n", namespace, uConfigMap.GetName(), gvr.Resource)
				return
			} else {
				err = errors.Wrapf(err, "client.Resource.Namespace.Create %+v failed", gvr)
				return
			}
		}
		fmt.Printf("Created object: namespace %s, name %s, resource %s\n", namespace, uConfigMap.GetName(), gvr.Resource)
	}
	return
}

func main() {
	// Initialize client
	var kubeconfig *string
	if home := homedir.HomeDir(); home != "" {
		kubeconfig = flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "(optional) absolute path to the kubeconfig file")
	} else {
		kubeconfig = flag.String("kubeconfig", "", "absolute path to the kubeconfig file")
	}
	pth := flag.String("f", ".", "path of manifest file or directory")
	flag.Parse()

	config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig)
	if err != nil {
		panic(err)
	}
	client, err := dynamic.NewForConfig(config)
	if err != nil {
		panic(err)
	}

	// Get manifest list
	var fi fs.FileInfo
	if fi, err = os.Stat(*pth); err != nil {
		panic(err)
	}
	var fps []string
	if fi.IsDir() {
		var des []fs.DirEntry
		if des, err = os.ReadDir(*pth); err != nil {
			panic(err)
		}
		for _, de := range des {
			if de.IsDir() {
				continue
			}
			fn := de.Name()
			ext := filepath.Ext(fn)
			if ext != ".yaml" && ext != "yml" {
				continue
			}
			fps = append(fps, filepath.Join(*pth, fn))
		}
		sort.Strings(fps)
	} else {
		fps = append(fps, *pth)
	}

	// Create resource for each manifest
	for _, fp := range fps {
		if err = createObject(client, fp); err != nil {
			fmt.Println(err)
		}
	}
}

It's good that you figured it out. At the same time, it seems like you're reinventing the wheel. The module k8s.io/cli-runtime does pretty much the same for you out of the box.

Also, beware that isClusterWideResource() is not the most reliable way of telling if a resource is namespaced or cluster-wide. Every APIResource object has a dedicated Namespaced bool flag for that.