github.com/splunk/dan1-qbec@v0.7.3/internal/remote/pristine.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 "bytes" 21 "compress/gzip" 22 "encoding/base64" 23 "encoding/json" 24 25 "github.com/pkg/errors" 26 "github.com/splunk/qbec/internal/model" 27 "github.com/splunk/qbec/internal/sio" 28 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 30 ) 31 32 // this file contains all pristine data processing. Pristine data is the original configuration 33 // written to the remote object as an annotation so that changes are be properly submitted via 34 // a 3-way merge that ensures that data written by the server is not overwritten on updates. 35 // Absence of pristine data is not an error; in this case we generate a pristine object that only 36 // has basic metadata. 37 38 // zipData returns a base64 encoded gzipped version of the JSON serialization of the supplied object. 39 func zipData(data map[string]interface{}) (string, error) { 40 var buf bytes.Buffer 41 gz := gzip.NewWriter(&buf) 42 if err := json.NewEncoder(gz).Encode(data); err != nil { 43 return "", err 44 } 45 if err := gz.Flush(); err != nil { 46 return "", err 47 } 48 if err := gz.Close(); err != nil { 49 return "", err 50 } 51 return base64.StdEncoding.EncodeToString(buf.Bytes()), nil 52 } 53 54 // unzipData is the exact reverse of a zipData operation. 55 func unzipData(s string) (map[string]interface{}, error) { 56 b, err := base64.StdEncoding.DecodeString(s) 57 if err != nil { 58 return nil, err 59 } 60 61 r := bytes.NewReader(b) 62 zr, err := gzip.NewReader(r) 63 if err != nil { 64 return nil, err 65 } 66 defer zr.Close() 67 68 var data map[string]interface{} 69 if err := json.NewDecoder(zr).Decode(&data); err != nil { 70 return nil, err 71 } 72 return data, nil 73 } 74 75 type pristineReader interface { 76 getPristine(annotations map[string]string, obj *unstructured.Unstructured) (pristine *unstructured.Unstructured, source string) 77 } 78 79 type pristineReadWriter interface { 80 pristineReader 81 createFromPristine(obj model.K8sLocalObject) (model.K8sLocalObject, error) 82 } 83 84 type qbecPristine struct{} 85 86 func (k qbecPristine) getPristine(annotations map[string]string, _ *unstructured.Unstructured) (*unstructured.Unstructured, string) { 87 serialized := annotations[model.QbecNames.PristineAnnotation] 88 if serialized == "" { 89 return nil, "" 90 } 91 m, err := unzipData(serialized) 92 if err != nil { 93 sio.Warnln("unable to unzip pristine annotation", err) 94 return nil, "" 95 } 96 return &unstructured.Unstructured{Object: m}, "qbec annotation" 97 } 98 99 func (k qbecPristine) createFromPristine(pristine model.K8sLocalObject) (model.K8sLocalObject, error) { 100 b, err := json.Marshal(pristine) 101 if err != nil { 102 return nil, errors.Wrap(err, "pristine JSON marshal") 103 } 104 var annotated unstructured.Unstructured // duplicate object of pristine to start with 105 if err := json.Unmarshal(b, &annotated); err != nil { 106 return nil, errors.Wrap(err, "pristine JSON unmarshal") 107 } 108 zipped, err := zipData(pristine.ToUnstructured().Object) 109 if err != nil { 110 return nil, errors.Wrap(err, "zip data") 111 } 112 annotations := annotated.GetAnnotations() 113 if annotations == nil { 114 annotations = map[string]string{} 115 } 116 annotations[model.QbecNames.PristineAnnotation] = zipped 117 annotated.SetAnnotations(annotations) 118 return model.NewK8sLocalObject(annotated.Object, pristine.Application(), pristine.Tag(), pristine.Component(), pristine.Environment()), nil 119 } 120 121 type fallbackPristine struct{} 122 123 func (f fallbackPristine) getPristine(annotations map[string]string, orig *unstructured.Unstructured) (*unstructured.Unstructured, string) { 124 delete(annotations, "deployment.kubernetes.io/revision") 125 orig.SetDeletionTimestamp(nil) 126 orig.SetCreationTimestamp(metav1.Time{}) 127 unstructured.RemoveNestedField(orig.Object, "metadata", "resourceVersion") 128 unstructured.RemoveNestedField(orig.Object, "metadata", "selfLink") 129 unstructured.RemoveNestedField(orig.Object, "metadata", "uid") 130 unstructured.RemoveNestedField(orig.Object, "metadata", "generation") 131 unstructured.RemoveNestedField(orig.Object, "status") 132 orig.SetAnnotations(annotations) 133 return orig, "fallback - live object with some attributes removed" 134 } 135 136 func getPristineVersion(obj *unstructured.Unstructured, includeFallback bool) (*unstructured.Unstructured, string) { 137 pristineReaders := []pristineReader{qbecPristine{}, fallbackPristine{}} 138 if includeFallback { 139 pristineReaders = append(pristineReaders) 140 } 141 annotations := obj.GetAnnotations() 142 if annotations == nil { 143 annotations = map[string]string{} 144 } 145 for _, p := range pristineReaders { 146 out, str := p.getPristine(annotations, obj) 147 if out != nil { 148 return out, str 149 } 150 } 151 return nil, "" 152 } 153 154 // GetPristineVersionForDiff interrogates annotations and extracts the pristine version of the supplied 155 // live object. If no annotations are found, it halfheartedly deletes known runtime information that is 156 // set on the server and returns the supplied object with those attributes removed. 157 func GetPristineVersionForDiff(obj *unstructured.Unstructured) (*unstructured.Unstructured, string) { 158 ret, src := getPristineVersion(obj, true) 159 if ret == nil { 160 return obj, "unmodified live" 161 } 162 return ret, src 163 }