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 }