github.com/replicatedhq/ship@v0.55.0/pkg/lifecycle/render/helm/values.go (about)

     1  package helm
     2  
     3  import (
     4  	"crypto/sha256"
     5  	"fmt"
     6  
     7  	"github.com/emosbaugh/yaml"
     8  	"github.com/pkg/errors"
     9  )
    10  
    11  // MergeHelmValues merges user edited values from state file and vendor values from upstream Helm repo.
    12  // base is the original config from state
    13  // user is the modified config from state
    14  // vendor is the new config from current chart
    15  // Value priorities: user, vendor, base
    16  func MergeHelmValues(baseValues, userValues, vendorValues string, preserveComments bool) (string, error) {
    17  	// First time merge is performed, there are no user values.  We are shortcutting this
    18  	// in order to preserve original file formatting and comments
    19  	if userValues == "" {
    20  		return vendorValues, nil
    21  	}
    22  	if vendorValues == "" {
    23  		vendorValues = baseValues
    24  	}
    25  
    26  	var base, user, vendor yaml.MapSlice
    27  
    28  	// we can drop comments in base
    29  	if err := yaml.Unmarshal([]byte(baseValues), &base); err != nil {
    30  		return "", errors.Wrapf(err, "unmarshal base values")
    31  	}
    32  	// TODO: preserve user comments
    33  	if err := yaml.Unmarshal([]byte(userValues), &user); err != nil {
    34  		return "", errors.Wrapf(err, "unmarshal user values")
    35  	}
    36  	if preserveComments {
    37  		var unmarshaler yaml.CommentUnmarshaler
    38  		if err := unmarshaler.Unmarshal([]byte(vendorValues), &vendor); err != nil {
    39  			return "", errors.Wrapf(err, "unmarshal vendor values")
    40  		}
    41  	} else {
    42  		if err := yaml.Unmarshal([]byte(vendorValues), &vendor); err != nil {
    43  			return "", errors.Wrapf(err, "unmarshal vendor values")
    44  		}
    45  	}
    46  
    47  	merged, err := deepMerge(base, user, vendor)
    48  	if err != nil {
    49  		return "", errors.Wrap(err, "deep merge values")
    50  	}
    51  
    52  	vals, err := yaml.Marshal(merged)
    53  	if err != nil {
    54  		return "", errors.Wrapf(err, "marshal merged values")
    55  	}
    56  	return string(vals), nil
    57  }
    58  
    59  // Value priorities: user, vendor, base
    60  func deepMerge(base, user, vendor yaml.MapSlice) (yaml.MapSlice, error) {
    61  	merged := yaml.MapSlice{}
    62  
    63  	allKeys := getAllKeys(vendor, user) // we can drop keys that have been dropped by the vendor
    64  
    65  	for _, k := range allKeys {
    66  		// don't merge comments
    67  		if _, ok := k.(yaml.Comment); ok {
    68  			merged = append(merged, yaml.MapItem{Key: k})
    69  			continue
    70  		}
    71  
    72  		baseVal, baseOk := getValueFromKey(base, k)
    73  		userVal, userOk := getValueFromKey(user, k)
    74  		vendorVal, vendorOk := getValueFromKey(vendor, k)
    75  
    76  		numExistingMaps := 0
    77  		preprocessValue := func(exists bool, value interface{}) yaml.MapSlice {
    78  			if !exists {
    79  				return yaml.MapSlice{}
    80  			}
    81  			m, ok := value.(yaml.MapSlice)
    82  			if ok {
    83  				numExistingMaps++
    84  			}
    85  			return m
    86  		}
    87  
    88  		baseSubmap := preprocessValue(baseOk, baseVal)
    89  		userSubmap := preprocessValue(userOk, userVal)
    90  		vendorSubmap := preprocessValue(vendorOk, vendorVal)
    91  
    92  		if numExistingMaps > 1 {
    93  			mergedSubmap, err := deepMerge(baseSubmap, userSubmap, vendorSubmap)
    94  			if err != nil {
    95  				return merged, errors.Wrapf(err, "merge submap at key %s", k)
    96  			}
    97  			merged = setValueAtKey(merged, k, mergedSubmap)
    98  			continue
    99  		}
   100  
   101  		if userOk && baseOk && vendorOk {
   102  			if eq, err := valuesEqual(userVal, baseVal); err != nil {
   103  				return merged, errors.Wrapf(err, "compare values at key %s", k)
   104  			} else if eq {
   105  				// user didn't change the value shipped in the previous version
   106  				// so we continue propagating vendor shipped values
   107  				merged = setValueAtKey(merged, k, vendorVal)
   108  			} else {
   109  				merged = setValueAtKey(merged, k, userVal)
   110  			}
   111  		} else if userOk {
   112  			merged = setValueAtKey(merged, k, userVal)
   113  		} else {
   114  			merged = setValueAtKey(merged, k, vendorVal)
   115  		}
   116  	}
   117  	return merged, nil
   118  }
   119  
   120  func getAllKeys(maps ...yaml.MapSlice) (allKeys []interface{}) {
   121  	seenKeys := map[interface{}]bool{}
   122  	for _, m := range maps {
   123  		for _, item := range m {
   124  			// comments are unique
   125  			if _, ok := item.Key.(yaml.Comment); ok {
   126  				allKeys = append(allKeys, item.Key)
   127  			} else if _, ok := seenKeys[item.Key]; !ok {
   128  				seenKeys[item.Key] = true
   129  				allKeys = append(allKeys, item.Key)
   130  			}
   131  		}
   132  	}
   133  	return
   134  }
   135  
   136  func getValueFromKey(m yaml.MapSlice, key interface{}) (interface{}, bool) {
   137  	for _, item := range m {
   138  		if item.Key == key {
   139  			return item.Value, true
   140  		}
   141  	}
   142  	return nil, false
   143  }
   144  
   145  func setValueAtKey(m yaml.MapSlice, key, value interface{}) (next yaml.MapSlice) {
   146  	var found bool
   147  	for _, item := range m {
   148  		if item.Key == key {
   149  			item.Value = value
   150  			found = true
   151  		}
   152  		next = append(next, item)
   153  	}
   154  	if !found {
   155  		next = append(next, yaml.MapItem{Key: key, Value: value})
   156  	}
   157  	return
   158  }
   159  
   160  func valuesEqual(val1, val2 interface{}) (bool, error) {
   161  	val1Array, val1OK := val1.([]interface{})
   162  	val2Array, val2OK := val2.([]interface{})
   163  	if !val1OK && !val2OK {
   164  		// arrays are the only types expected here that won't compare with `==`
   165  		return val1 == val2, nil
   166  	}
   167  	if !val1OK || !val2OK {
   168  		return false, nil
   169  	}
   170  
   171  	// compare two arrays by checksumming their elements
   172  	arr1Checksums, err := arrayToChecksums(val1Array)
   173  	if err != nil {
   174  		return false, errors.Wrap(err, "array1 to checksums")
   175  	}
   176  	arr2Checksums, err := arrayToChecksums(val2Array)
   177  	if err != nil {
   178  		return false, errors.Wrap(err, "array2 to checksums")
   179  	}
   180  
   181  	for k := range arr1Checksums {
   182  		_, ok := arr2Checksums[k]
   183  		if !ok {
   184  			return false, nil
   185  		}
   186  		delete(arr2Checksums, k)
   187  	}
   188  
   189  	return len(arr2Checksums) == 0, nil
   190  }
   191  
   192  func arrayToChecksums(arr []interface{}) (map[string]bool, error) {
   193  	result := map[string]bool{}
   194  	for _, v := range arr {
   195  		str, err := yaml.Marshal(v)
   196  		if err != nil {
   197  			return nil, errors.Wrap(err, "marshal value into yaml")
   198  		}
   199  		sum := sha256.Sum256(str)
   200  		result[fmt.Sprintf("%x", sum)] = true
   201  	}
   202  	return result, nil
   203  }