1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154
|
package yamlpatch
import (
"bytes"
"io"
"os"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"gopkg.in/yaml.v2"
)
type Patcher struct {
BaseFilePath string
PatchFilePath string
}
func NewPatcher(filePath string, suffix string) *Patcher {
return &Patcher{
BaseFilePath: filePath,
PatchFilePath: filePath + suffix,
}
}
// read a single YAML file, check for errors (the merge package doesn't) then return the content as bytes.
func readYAML(filePath string) ([]byte, error) {
var content []byte
var err error
if content, err = os.ReadFile(filePath); err != nil {
return nil, errors.Wrap(err, "while reading yaml file")
}
var yamlMap map[interface{}]interface{}
if err = yaml.Unmarshal(content, &yamlMap); err != nil {
return nil, errors.Wrap(err, filePath)
}
return content, nil
}
// MergedPatchContent reads a YAML file and, if it exists, its patch file,
// then merges them and returns it serialized.
func (p *Patcher) MergedPatchContent() ([]byte, error) {
var err error
var base []byte
base, err = readYAML(p.BaseFilePath)
if err != nil {
return nil, err
}
var over []byte
over, err = readYAML(p.PatchFilePath)
// optional file, ignore if it does not exist
if err != nil && !errors.Is(err, os.ErrNotExist) {
return nil, err
}
if err == nil {
log.Debugf("Patching yaml: '%s' with '%s'", p.BaseFilePath, p.PatchFilePath)
}
var patched *bytes.Buffer
// strict mode true, will raise errors for duplicate map keys and
// overriding with a different type
patched, err = YAML([][]byte{base, over}, true)
if err != nil {
return nil, err
}
return patched.Bytes(), nil
}
// read multiple YAML documents inside a file, and writes them to a buffer
// separated by the appropriate '---' terminators.
func decodeDocuments(file *os.File, buf *bytes.Buffer, finalDashes bool) error {
var (
err error
docBytes []byte
)
dec := yaml.NewDecoder(file)
dec.SetStrict(true)
dashTerminator := false
for {
yml := make(map[interface{}]interface{})
err = dec.Decode(&yml)
if err != nil {
if errors.Is(err, io.EOF) {
break
}
return errors.Wrapf(err, "while decoding %s", file.Name())
}
docBytes, err = yaml.Marshal(&yml)
if err != nil {
return errors.Wrapf(err, "while marshaling %s", file.Name())
}
if dashTerminator {
buf.Write([]byte("---\n"))
}
buf.Write(docBytes)
dashTerminator = true
}
if dashTerminator && finalDashes {
buf.Write([]byte("---\n"))
}
return nil
}
// PrependedPatchContent collates the base .yaml file with the .yaml.patch, by putting
// the content of the patch BEFORE the base document. The result is a multi-document
// YAML in all cases, even if the base and patch files are single documents.
func (p *Patcher) PrependedPatchContent() ([]byte, error) {
var (
result bytes.Buffer
patchFile *os.File
baseFile *os.File
err error
)
patchFile, err = os.Open(p.PatchFilePath)
// optional file, ignore if it does not exist
if err != nil && !errors.Is(err, os.ErrNotExist) {
return nil, errors.Wrapf(err, "while opening %s", p.PatchFilePath)
}
if patchFile != nil {
if err = decodeDocuments(patchFile, &result, true); err != nil {
return nil, err
}
log.Infof("Prepending yaml: '%s' with '%s'", p.BaseFilePath, p.PatchFilePath)
}
baseFile, err = os.Open(p.BaseFilePath)
if err != nil {
return nil, errors.Wrapf(err, "while opening %s", p.BaseFilePath)
}
if err = decodeDocuments(baseFile, &result, false); err != nil {
return nil, err
}
return result.Bytes(), nil
}
|