154 lines
3.5 KiB
Go
154 lines
3.5 KiB
Go
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.Infof("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
|
|
}
|