Skip to content

Instantly share code, notes, and snippets.

@mittachaitu
Last active November 29, 2020 08:38
Show Gist options
  • Save mittachaitu/43684da75994b4cdb094155aac3ff7f2 to your computer and use it in GitHub Desktop.
Save mittachaitu/43684da75994b4cdb094155aac3ff7f2 to your computer and use it in GitHub Desktop.
Following program will help to solve error(unable to find api field in struct Unstructured for the json field "spec"`) faced while patching unstructured object
package main
import (
"context"
"encoding/json"
"flag"
jsonpatch "github.com/evanphx/json-patch"
"github.com/pkg/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/klog/v2"
)
// factory to store clients
// which can be use to perform
// CRUD operations
type factory struct {
ctx context.Context
dynamicClient dynamic.Interface
}
// getClusterConfig return the config for k8s
func getClusterConfig(kubeconfig string) (*rest.Config, error) {
if kubeconfig != "" {
return clientcmd.BuildConfigFromFlags("", kubeconfig)
}
klog.Info("Kubeconfig flag is empty")
return rest.InClusterConfig()
}
func (f *factory) newDynamicClient(config *rest.Config) (*factory, error) {
var err error
f.dynamicClient, err = dynamic.NewForConfig(config)
if err != nil {
return nil, errors.Wrapf(err, "failed to get dynmic client")
}
return f, nil
}
func (f *factory) getObject(namespace, name string, gvr schema.GroupVersionResource) (*unstructured.Unstructured, error) {
return f.dynamicClient.Resource(gvr).Namespace(namespace).Get(f.ctx, name, metav1.GetOptions{})
}
// mututateDeployment will add environment varables to the container
func (f *factory) mututateDeployment(obj *unstructured.Unstructured) error {
copyObj := obj.DeepCopy()
err := injectEnvsInDeployment(copyObj)
if err != nil {
return errors.Wrapf(err, "failed to inject %s container into object %s", obj.GetKind())
}
// Patch deployment
patchBytes, err := getPatchData(obj, copyObj)
if err != nil {
return errors.Wrapf(err, "failed to get patch bytes")
}
_, err = f.dynamicClient.Resource(getDeploymentGVR()).Namespace(obj.GetNamespace()).Patch(f.ctx, obj.GetName(), types.StrategicMergePatchType, patchBytes, metav1.PatchOptions{})
return err
}
// injectEnvsInDeployment will inject environment variables to the
// given object
func injectEnvsInDeployment(obj *unstructured.Unstructured) error {
// Since unstructure understands only map[string]interface{}
// we also need to inject using map[string]interface
// NOTE: All the below lines can be replacable by one line
// by convering unstructured into concrete type but here aim
// is to deal with unstructured
newEnvs := []interface{}{
map[string]interface{}{
"name": "NAMESPACE",
"valueFrom": map[string]interface{}{
"fieldRef": map[string]interface{}{
"fieldPath": "metadata.namespace",
},
},
},
map[string]interface{}{
"name": "POD_UID",
"valueFrom": map[string]interface{}{
"fieldRef": map[string]interface{}{
"fieldPath": "metadata.uid",
},
},
},
}
conInterface, _, err := unstructured.NestedFieldNoCopy(obj.Object, "spec", "template", "spec", "containers")
if err != nil {
return errors.Wrapf(err, "failed to get containers")
}
containers, ok := conInterface.([]interface{})
if !ok {
return errors.Errorf("expected of type %T but got %T", []interface{}{}, conInterface)
}
existingEnvInterface, _, err := unstructured.NestedFieldNoCopy(containers[0].(map[string]interface{}), "env")
if err != nil {
return errors.Wrapf(err, "failed to get envs present in container")
}
var updatedEnvs []interface{}
if existingEnvInterface != nil {
updatedEnvs = append(existingEnvInterface.([]interface{}), newEnvs...)
} else {
updatedEnvs = newEnvs
}
return unstructured.SetNestedField(containers[0].(map[string]interface{}), updatedEnvs, "env")
}
func main() {
var kubeconfig *string
// flag to get custom kubeconfig
kubeconfig = flag.String("kubeconfig", "", "Path for kube config")
flag.Parse()
// Following lines will get a client to talk to kube-apiserver
config, err := getClusterConfig(*kubeconfig)
if err != nil {
klog.Errorf("failed to get config error: %v", err)
return
}
f := &factory{
ctx: context.Background(),
}
f, err = f.newDynamicClient(config)
if err != nil {
klog.Errorf("failed to get dynamic client error: %v", err)
return
}
// Below lines will perform following actions:
// 1. Get deployment
// 2. Patch the deployment using strategic patch -- it will throw an error
unstructObj, err := f.getObject("minio", "minio", getDeploymentGVR())
if err != nil {
klog.Errorf("failed to get minio deployment error: %v", err)
return
}
err = f.mututateDeployment(unstructObj)
if err != nil {
klog.Errorf("failed to mututate deployment error: %v", err)
}
klog.Infof("Successfully applied patch")
}
// getPatchData will return difference between original and modified document
func getPatchData(originalObj, modifiedObj interface{}) ([]byte, error) {
originalData, err := json.Marshal(originalObj)
if err != nil {
return nil, errors.Wrapf(err, "failed marshal original data")
}
modifiedData, err := json.Marshal(modifiedObj)
if err != nil {
return nil, errors.Wrapf(err, "failed marshal original data")
}
// Using strategicpatch package can cause below error
// Error: CreateTwoWayMergePatch failed: unable to find api field in struct Unstructured for the json field "spec"
//patchBytes, err := strategicpatch.CreateTwoWayMergePatch(originalData, modifiedData, originalObj)
// if err != nil {
// return nil, errors.Errorf("CreateTwoWayMergePatch failed: %v", err)
// }
patchBytes, err := jsonpatch.CreateMergePatch(originalData, modifiedData)
if err != nil {
return nil, errors.Errorf("CreateTwoWayMergePatch failed: %v", err)
}
return patchBytes, nil
}
func getDeploymentGVR() schema.GroupVersionResource {
return schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment