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  }