98f2ac5e7c
Added support for .yaml.local files to override values in .yaml
168 lines
5 KiB
Go
168 lines
5 KiB
Go
//
|
|
// from https://github.com/uber-go/config/tree/master/internal/merge
|
|
//
|
|
// Copyright (c) 2019 Uber Technologies, Inc.
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
// of this software and associated documentation files (the "Software"), to deal
|
|
// in the Software without restriction, including without limitation the rights
|
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
// copies of the Software, and to permit persons to whom the Software is
|
|
// furnished to do so, subject to the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included in
|
|
// all copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
// THE SOFTWARE.
|
|
|
|
package yamlpatch
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
yaml "gopkg.in/yaml.v2"
|
|
)
|
|
|
|
type (
|
|
// YAML has three fundamental types. When unmarshaled into interface{},
|
|
// they're represented like this.
|
|
mapping = map[interface{}]interface{}
|
|
sequence = []interface{}
|
|
)
|
|
|
|
// YAML deep-merges any number of YAML sources, with later sources taking
|
|
// priority over earlier ones.
|
|
//
|
|
// Maps are deep-merged. For example,
|
|
// {"one": 1, "two": 2} + {"one": 42, "three": 3}
|
|
// == {"one": 42, "two": 2, "three": 3}
|
|
// Sequences are replaced. For example,
|
|
// {"foo": [1, 2, 3]} + {"foo": [4, 5, 6]}
|
|
// == {"foo": [4, 5, 6]}
|
|
//
|
|
// In non-strict mode, duplicate map keys are allowed within a single source,
|
|
// with later values overwriting previous ones. Attempting to merge
|
|
// mismatched types (e.g., merging a sequence into a map) replaces the old
|
|
// value with the new.
|
|
//
|
|
// Enabling strict mode returns errors in both of the above cases.
|
|
func YAML(sources [][]byte, strict bool) (*bytes.Buffer, error) {
|
|
var merged interface{}
|
|
var hasContent bool
|
|
for _, r := range sources {
|
|
d := yaml.NewDecoder(bytes.NewReader(r))
|
|
d.SetStrict(strict)
|
|
|
|
var contents interface{}
|
|
if err := d.Decode(&contents); err == io.EOF {
|
|
// Skip empty and comment-only sources, which we should handle
|
|
// differently from explicit nils.
|
|
continue
|
|
} else if err != nil {
|
|
return nil, fmt.Errorf("couldn't decode source: %v", err)
|
|
}
|
|
|
|
hasContent = true
|
|
pair, err := merge(merged, contents, strict)
|
|
if err != nil {
|
|
return nil, err // error is already descriptive enough
|
|
}
|
|
merged = pair
|
|
}
|
|
|
|
buf := &bytes.Buffer{}
|
|
if !hasContent {
|
|
// No sources had any content. To distinguish this from a source with just
|
|
// an explicit top-level null, return an empty buffer.
|
|
return buf, nil
|
|
}
|
|
enc := yaml.NewEncoder(buf)
|
|
if err := enc.Encode(merged); err != nil {
|
|
return nil, errors.Wrap(err, "couldn't re-serialize merged YAML")
|
|
}
|
|
return buf, nil
|
|
}
|
|
|
|
func merge(into, from interface{}, strict bool) (interface{}, error) {
|
|
// It's possible to handle this with a mass of reflection, but we only need
|
|
// to merge whole YAML files. Since we're always unmarshaling into
|
|
// interface{}, we only need to handle a few types. This ends up being
|
|
// cleaner if we just handle each case explicitly.
|
|
if into == nil {
|
|
return from, nil
|
|
}
|
|
if from == nil {
|
|
// Allow higher-priority YAML to explicitly nil out lower-priority entries.
|
|
return nil, nil
|
|
}
|
|
if IsScalar(into) && IsScalar(from) {
|
|
return from, nil
|
|
}
|
|
if IsSequence(into) && IsSequence(from) {
|
|
return from, nil
|
|
}
|
|
if IsMapping(into) && IsMapping(from) {
|
|
return mergeMapping(into.(mapping), from.(mapping), strict)
|
|
}
|
|
// YAML types don't match, so no merge is possible. For backward
|
|
// compatibility, ignore mismatches unless we're in strict mode and return
|
|
// the higher-priority value.
|
|
if !strict {
|
|
return from, nil
|
|
}
|
|
return nil, fmt.Errorf("can't merge a %s into a %s", describe(from), describe(into))
|
|
}
|
|
|
|
func mergeMapping(into, from mapping, strict bool) (mapping, error) {
|
|
merged := make(mapping, len(into))
|
|
for k, v := range into {
|
|
merged[k] = v
|
|
}
|
|
for k := range from {
|
|
m, err := merge(merged[k], from[k], strict)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
merged[k] = m
|
|
}
|
|
return merged, nil
|
|
}
|
|
|
|
// IsMapping reports whether a type is a mapping in YAML, represented as a
|
|
// map[interface{}]interface{}.
|
|
func IsMapping(i interface{}) bool {
|
|
_, is := i.(mapping)
|
|
return is
|
|
}
|
|
|
|
// IsSequence reports whether a type is a sequence in YAML, represented as an
|
|
// []interface{}.
|
|
func IsSequence(i interface{}) bool {
|
|
_, is := i.(sequence)
|
|
return is
|
|
}
|
|
|
|
// IsScalar reports whether a type is a scalar value in YAML.
|
|
func IsScalar(i interface{}) bool {
|
|
return !IsMapping(i) && !IsSequence(i)
|
|
}
|
|
|
|
func describe(i interface{}) string {
|
|
if IsMapping(i) {
|
|
return "mapping"
|
|
}
|
|
if IsSequence(i) {
|
|
return "sequence"
|
|
}
|
|
return "scalar"
|
|
}
|