crowdsec/pkg/yamlpatch/merge.go
mmetc 98f2ac5e7c
fix #1385: .yaml.local (#1497)
Added support for .yaml.local files to override values in .yaml
2022-05-18 10:08:37 +02:00

169 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"
}