github.com/splunk/dan1-qbec@v0.7.3/internal/remote/patch.go (about)

     1  /*
     2     Copyright 2019 Splunk Inc.
     3  
     4     Licensed under the Apache License, Version 2.0 (the "License");
     5     you may not use this file except in compliance with the License.
     6     You may obtain a copy of the License at
     7  
     8         http://www.apache.org/licenses/LICENSE-2.0
     9  
    10     Unless required by applicable law or agreed to in writing, software
    11     distributed under the License is distributed on an "AS IS" BASIS,
    12     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13     See the License for the specific language governing permissions and
    14     limitations under the License.
    15  */
    16  
    17  package remote
    18  
    19  import (
    20  	"encoding/json"
    21  	"fmt"
    22  	"time"
    23  
    24  	"github.com/jonboulle/clockwork"
    25  	"github.com/pkg/errors"
    26  	"github.com/splunk/qbec/internal/model"
    27  	"github.com/splunk/qbec/internal/sio"
    28  	apiErrors "k8s.io/apimachinery/pkg/api/errors"
    29  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    30  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    31  	"k8s.io/apimachinery/pkg/runtime"
    32  	"k8s.io/apimachinery/pkg/runtime/schema"
    33  	"k8s.io/apimachinery/pkg/types"
    34  	"k8s.io/apimachinery/pkg/util/jsonmergepatch"
    35  	"k8s.io/apimachinery/pkg/util/mergepatch"
    36  	"k8s.io/apimachinery/pkg/util/strategicpatch"
    37  	"k8s.io/client-go/dynamic"
    38  	"k8s.io/client-go/kubernetes/scheme"
    39  	"k8s.io/kube-openapi/pkg/util/proto"
    40  	oapi "k8s.io/kube-openapi/pkg/util/proto"
    41  )
    42  
    43  // this file contains the patch code from kubectl, modified such that it does not pull in the whole world from
    44  // those libraries with parts re-written for maintainer clarity and a new algorithm for detecting empty patches
    45  // to reduce apply --dry-run noise.
    46  
    47  const (
    48  	// maxPatchRetry is the maximum number of conflicts retry for during a patch operation before returning failure
    49  	maxPatchRetry = 5
    50  	// backOffPeriod is the period to back off when apply patch resutls in error.
    51  	backOffPeriod = 1 * time.Second
    52  	// how many times we can retry before back off
    53  	triesBeforeBackOff = 1
    54  )
    55  
    56  type resourceInterfaceProvider func(gvk schema.GroupVersionKind, namespace string) (dynamic.ResourceInterface, error)
    57  type originalConfigurationProvider func(obj *unstructured.Unstructured) ([]byte, error)
    58  type openAPILookup func(gvk schema.GroupVersionKind) proto.Schema
    59  
    60  type patcher struct {
    61  	provider      resourceInterfaceProvider
    62  	cfgProvider   originalConfigurationProvider
    63  	overwrite     bool
    64  	backOff       clockwork.Clock
    65  	openAPILookup openAPILookup
    66  }
    67  
    68  type serialized struct {
    69  	server   []byte // the document as it exists on the server
    70  	pristine []byte // the last applied document if known
    71  	desired  []byte // the current document we want
    72  }
    73  
    74  func (p *patcher) getSerialized(serverObj *unstructured.Unstructured, desired model.K8sObject) (*serialized, error) {
    75  	// serialize the current configuration of the object from the server.
    76  	serverBytes, err := runtime.Encode(unstructured.UnstructuredJSONScheme, serverObj)
    77  	if err != nil {
    78  		return nil, errors.Wrap(err, "serialize server config")
    79  	}
    80  
    81  	// retrieve the original configuration of the object.
    82  	desiredBytes, err := runtime.Encode(unstructured.UnstructuredJSONScheme, desired.ToUnstructured())
    83  	if err != nil {
    84  		return nil, errors.Wrap(err, fmt.Sprintf("serialize desired config"))
    85  	}
    86  
    87  	// retrieve the original configuration of the object. nil is ok if no original config was found.
    88  	pristineBytes, err := p.cfgProvider(serverObj)
    89  	if err != nil {
    90  		return nil, errors.Wrap(err, fmt.Sprintf("retrieve original config"))
    91  	}
    92  	return &serialized{
    93  		server:   serverBytes,
    94  		desired:  desiredBytes,
    95  		pristine: pristineBytes,
    96  	}, nil
    97  }
    98  
    99  // deleteEmpty deletes the supplied key for the parent if the value for the
   100  // key is an empty object after deleteEmpty has been called on _it_.
   101  func deleteEmpty(parent map[string]interface{}, key string) {
   102  	entry := parent[key]
   103  	switch value := entry.(type) {
   104  	case map[string]interface{}:
   105  		for k := range value {
   106  			deleteEmpty(value, k)
   107  		}
   108  		if len(value) == 0 {
   109  			delete(parent, key)
   110  		}
   111  	}
   112  }
   113  
   114  // isEmptyPatch returns true if the unmarshaled version of the JSON patch is an empty object or only
   115  // contains empty objects. It makes an assumption that there is actually no reason an empty object
   116  // needs to be updated for a Kubernetes resource considering that the server would already have an object
   117  // there on initial create if needed. Things considered empty will be of the form:
   118  //  {}
   119  //  { metadata: { labels: {}, annotations: {} }
   120  //  { metadata: { labels: {}, annotations: {} }, spec: { foo: { bar: {} } } }
   121  //
   122  func isEmptyPatch(patch []byte) bool {
   123  	var root map[string]interface{}
   124  	err := json.Unmarshal(patch, &root)
   125  	if err != nil {
   126  		sio.Warnf("could not unmarshal patch %s", patch)
   127  		return false // assume the worst
   128  	}
   129  	for k := range root {
   130  		deleteEmpty(root, k)
   131  	}
   132  	return len(root) == 0
   133  }
   134  
   135  func newPatchResult(src string, kind types.PatchType, patch []byte) *updateResult {
   136  	if isEmptyPatch(patch) {
   137  		return &updateResult{SkipReason: identicalObjects}
   138  	}
   139  	pr := &updateResult{
   140  		Operation: opUpdate,
   141  		Source:    src,
   142  		Kind:      kind,
   143  		patch:     patch,
   144  	}
   145  	return pr
   146  }
   147  
   148  // getPatchContents returns the contents of the patch to take the supplied object to its modified version considering
   149  // any previous configuration applied. The result has a SkipReason set when nothing needs to be done. This is the only
   150  // way to correctly determine if a patch needs to be applied.
   151  func (p *patcher) getPatchContents(serverObj *unstructured.Unstructured, desired model.K8sObject) (*updateResult, error) {
   152  	// get the serialized versions of server, desired and pristine
   153  	ser, err := p.getSerialized(serverObj, desired)
   154  	if err != nil {
   155  		return nil, err
   156  	}
   157  	var lookupPatchMeta strategicpatch.LookupPatchMeta
   158  	var sch oapi.Schema
   159  	patchContext := fmt.Sprintf("creating patch with:\npristine:\n%s\ndesired:\n%s\nserver:\n%s\nfor:", ser.pristine, ser.desired, ser.server)
   160  	gvk := serverObj.GroupVersionKind()
   161  
   162  	// prefer open API if available to create a strategic merge patch
   163  	if p.openAPILookup != nil {
   164  		if sch = p.openAPILookup(gvk); sch != nil {
   165  			lookupPatchMeta = strategicpatch.PatchMetaFromOpenAPI{Schema: sch}
   166  			if openapiPatch, err := strategicpatch.CreateThreeWayMergePatch(ser.pristine, ser.desired, ser.server, lookupPatchMeta, p.overwrite); err == nil {
   167  				return newPatchResult("open API", types.StrategicMergePatchType, openapiPatch), nil
   168  			}
   169  			sio.Warnf("warning: error calculating patch from openapi spec: %v\n", err)
   170  		}
   171  	}
   172  
   173  	// next try a versioned struct if available in scheme
   174  	versionedObject, err := scheme.Scheme.New(gvk)
   175  	if err != nil && !runtime.IsNotRegisteredError(err) {
   176  		return nil, errors.Wrap(err, fmt.Sprintf("getting instance of versioned object for %v:", gvk))
   177  	}
   178  
   179  	if runtime.IsNotRegisteredError(err) { // fallback to generic JSON merge patch
   180  		preconditions := []mergepatch.PreconditionFunc{
   181  			mergepatch.RequireKeyUnchanged("apiVersion"),
   182  			mergepatch.RequireKeyUnchanged("kind"),
   183  			mergepatch.RequireMetadataKeyUnchanged("name"),
   184  		}
   185  		patch, err := jsonmergepatch.CreateThreeWayJSONMergePatch(ser.pristine, ser.desired, ser.server, preconditions...)
   186  		if err != nil {
   187  			if mergepatch.IsPreconditionFailed(err) {
   188  				return nil, fmt.Errorf("%s%s", patchContext, "At least one of apiVersion, kind and name was changed")
   189  			}
   190  			return nil, errors.Wrap(err, patchContext)
   191  		}
   192  		return newPatchResult("unregistered", types.MergePatchType, patch), nil
   193  	}
   194  
   195  	// strategic merge patch with struct metadata as source
   196  	lookupPatchMeta, err = strategicpatch.NewPatchMetaFromStruct(versionedObject)
   197  	if err != nil {
   198  		return nil, errors.Wrap(err, patchContext)
   199  	}
   200  	patch, err := strategicpatch.CreateThreeWayMergePatch(ser.pristine, ser.desired, ser.server, lookupPatchMeta, p.overwrite)
   201  	if err != nil {
   202  		return nil, errors.Wrap(err, patchContext)
   203  	}
   204  	return newPatchResult("struct definition", types.StrategicMergePatchType, patch), nil
   205  }
   206  
   207  func (p *patcher) patchSimple(serverObj *unstructured.Unstructured, desired model.K8sObject) (result *updateResult, err error) {
   208  	result, err = p.getPatchContents(serverObj, desired)
   209  	if err != nil {
   210  		return
   211  	}
   212  	if result.SkipReason != "" {
   213  		return
   214  	}
   215  	gvk := serverObj.GroupVersionKind()
   216  	ri, err := p.provider(gvk, serverObj.GetNamespace())
   217  	if err != nil {
   218  		return nil, errors.Wrap(err, fmt.Sprintf("error getting update interface for %v", gvk))
   219  	}
   220  	_, err = ri.Patch(serverObj.GetName(), result.Kind, result.patch)
   221  	return result, err
   222  }
   223  
   224  func (p *patcher) patch(serverObj *unstructured.Unstructured, desired model.K8sObject) (*updateResult, error) {
   225  	gvk := serverObj.GroupVersionKind()
   226  	namespace := serverObj.GetNamespace()
   227  	name := serverObj.GetName()
   228  	var getErr error
   229  	result, err := p.patchSimple(serverObj, desired)
   230  	for i := 1; i <= maxPatchRetry && apiErrors.IsConflict(err); i++ {
   231  		if i > triesBeforeBackOff {
   232  			p.backOff.Sleep(backOffPeriod)
   233  		}
   234  		var ri dynamic.ResourceInterface
   235  		ri, err = p.provider(gvk, namespace)
   236  		if err != nil {
   237  			return nil, err
   238  		}
   239  		serverObj, getErr = ri.Get(name, metav1.GetOptions{})
   240  		if getErr != nil {
   241  			return nil, getErr
   242  		}
   243  		result, err = p.patchSimple(serverObj, desired)
   244  	}
   245  	return result, err
   246  }