Skip to content

Instantly share code, notes, and snippets.

@erdii
Last active February 4, 2025 15:55
Show Gist options
  • Save erdii/439fe05b4c29f7087fa6543db0687be2 to your computer and use it in GitHub Desktop.
Save erdii/439fe05b4c29f7087fa6543db0687be2 to your computer and use it in GitHub Desktop.
Example: using dynamic client to parse and apply generic object types
package main
import (
"context"
"fmt"
"os"
"k8s.io/apimachinery/pkg/api/meta"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/restmapper"
"sigs.k8s.io/controller-runtime/pkg/client/config"
"sigs.k8s.io/yaml"
)
const FieldManager = "parse-and-apply-example"
func newClients() (kubernetes.Interface, dynamic.Interface) {
restConfig := config.GetConfigOrDie()
client := kubernetes.NewForConfigOrDie(restConfig)
dynamicClient := dynamic.NewForConfigOrDie(restConfig)
return client, dynamicClient
}
func discoverAPIsAndConstructRESTMapper(client kubernetes.Interface) (meta.RESTMapper, error) {
groupResources, err := restmapper.GetAPIGroupResources(client.Discovery())
if err != nil {
return nil, fmt.Errorf("discovering api group resources: %w", err)
}
return restmapper.NewDiscoveryRESTMapper(groupResources), nil
}
func applyObject(
dynamicClient dynamic.Interface,
mapper meta.RESTMapper,
obj *unstructured.Unstructured,
) (*unstructured.Unstructured, error) {
// Find (API) mapping for the parsed resource's GVK.
gvk := obj.GetObjectKind().GroupVersionKind()
mapping, err := mapper.RESTMapping(gvk.GroupKind(), gvk.Version)
must(err)
// Bail if the given object kind is not namespace scoped.
if mapping.Scope.Name() != meta.RESTScopeNameNamespace {
panic("this code only supports applying namespaced resources")
}
// Construct dynamic resource interface (aka the actual client we'll use.)
dri := dynamicClient.Resource(mapping.Resource).Namespace(obj.GetNamespace())
// Apply the object.
appliedObj, err := dri.Apply(context.TODO(), obj.GetName(), obj, v1.ApplyOptions{
FieldManager: FieldManager,
Force: false, // set to true to force ownership on contested fields
})
if err != nil {
return nil, fmt.Errorf("applying object: %w", err)
}
return appliedObj, nil
}
func parseFile(name string) (*unstructured.Unstructured, error) {
bytes, err := os.ReadFile(name)
if err != nil {
return nil, fmt.Errorf("reading file: %w", err)
}
return parseBytes(bytes)
}
func parseBytes(bytes []byte) (*unstructured.Unstructured, error) {
obj := &unstructured.Unstructured{}
if err := yaml.Unmarshal(bytes, obj); err != nil {
return nil, fmt.Errorf("unmarshalling yaml: %w", err)
}
return obj, nil
}
func must(err error) {
if err != nil {
panic(err)
}
}
func main() {
// Set up clients - can be done once globally.
client, dynamicClient := newClients()
// Look up available APIs and construct rest mapper.
mapper, err := discoverAPIsAndConstructRESTMapper(client)
must(err)
// Parse object manifest.
obj, err := parseFile(os.Args[1])
must(err)
// Default "default" namespace.
if obj.GetNamespace() == "" {
obj.SetNamespace("default")
}
// Apply object.
appliedObj, err := applyObject(dynamicClient, mapper, obj)
must(err)
fmt.Println(appliedObj)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment