From 9de094f078b9905633f0cfea92bdd8e3a1363435 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 12 Apr 2020 12:22:37 -0700 Subject: [PATCH] Fix support for configuration files with more complex cases --- parser/helpers.go | 85 +++++++++++++++++++++++++++-------------------- parser/parser.go | 39 ++++++++++++---------- parser/value.go | 22 ++++++++++++ 3 files changed, 93 insertions(+), 53 deletions(-) create mode 100644 parser/value.go diff --git a/parser/helpers.go b/parser/helpers.go index aa8c366..15a74f8 100644 --- a/parser/helpers.go +++ b/parser/helpers.go @@ -1,12 +1,15 @@ package parser import ( + "bytes" "github.com/Jeffail/gabs/v2" "github.com/buger/jsonparser" "github.com/iancoleman/strcase" "github.com/pkg/errors" + "go.uber.org/zap" "io/ioutil" "os" + "reflect" "regexp" "strconv" "strings" @@ -44,32 +47,19 @@ func readFileBytes(path string) ([]byte, error) { return ioutil.ReadAll(file) } -// Helper function to set the value of the JSON key item based on the jsonparser value -// type returned. -func setPathway(c *gabs.Container, path string, value []byte, vt jsonparser.ValueType) error { - v := getKeyValue(value, vt) - - _, err := c.SetP(v, path) - - return err -} - // Gets the value of a key based on the value type defined. -func getKeyValue(value []byte, vt jsonparser.ValueType) interface{} { - switch vt { - case jsonparser.Number: - { - v, _ := strconv.Atoi(string(value)) - return v - } - case jsonparser.Boolean: - { - v, _ := strconv.ParseBool(string(value)) - return v - } - default: - return string(value) +func getKeyValue(value []byte) interface{} { + if reflect.ValueOf(value).Kind() == reflect.Bool { + v, _ := strconv.ParseBool(string(value)) + return v } + + // Try to parse into an int, if this fails just ignore the error and + if v, err := strconv.Atoi(string(value)); err == nil { + return v + } + + return string(value) } // Iterate over an unstructured JSON/YAML/etc. interface and set all of the required @@ -88,7 +78,7 @@ func (f *ConfigurationFile) IterateOverJson(data []byte) (*gabs.Container, error } for _, v := range f.Replace { - value, dt, err := f.LookupConfigurationValue(v) + value, err := f.LookupConfigurationValue(v) if err != nil { return nil, err } @@ -104,12 +94,12 @@ func (f *ConfigurationFile) IterateOverJson(data []byte) (*gabs.Container, error // If the child is a null value, nothing will happen. Seems reasonable as of the // time this code is being written. for _, child := range parsed.Path(strings.Trim(parts[0], ".")).Children() { - if err := setPathway(child, strings.Trim(parts[1], "."), value, dt); err != nil { + if err := v.SetAtPathway(child, strings.Trim(parts[1], "."), value); err != nil { return nil, err } } } else { - if err = setPathway(parsed, v.Match, value, dt); err != nil { + if err = v.SetAtPathway(parsed, v.Match, value); err != nil { return nil, err } } @@ -118,17 +108,34 @@ func (f *ConfigurationFile) IterateOverJson(data []byte) (*gabs.Container, error return parsed, nil } +// Sets the value at a specific pathway, but checks if we were looking for a specific +// value or not before doing it. +func (cfr *ConfigurationFileReplacement) SetAtPathway(c *gabs.Container, path string, value []byte) error { + if cfr.IfValue != nil { + if !c.Exists(path) || (c.Exists(path) && !bytes.Equal(c.Bytes(), []byte(*cfr.IfValue))) { + return nil + } + } + + _, err := c.SetP(getKeyValue(value), path) + + return err +} + // Looks up a configuration value on the Daemon given a dot-notated syntax. -func (f *ConfigurationFile) LookupConfigurationValue(cfr ConfigurationFileReplacement) ([]byte, jsonparser.ValueType, error) { - if !configMatchRegex.Match([]byte(cfr.Value)) { - return []byte(cfr.Value), cfr.ValueType, nil +func (f *ConfigurationFile) LookupConfigurationValue(cfr ConfigurationFileReplacement) ([]byte, error) { + // If this is not something that we can do a regex lookup on then just continue + // on our merry way. If the value isn't a string, we're not going to be doing anything + // with it anyways. + if cfr.ReplaceWith.Type() != jsonparser.String || !configMatchRegex.Match(cfr.ReplaceWith.Value()) { + return cfr.ReplaceWith.Value(), nil } // If there is a match, lookup the value in the configuration for the Daemon. If no key // is found, just return the string representation, otherwise use the value from the // daemon configuration here. huntPath := configMatchRegex.ReplaceAllString( - configMatchRegex.FindString(cfr.Value), "$1", + configMatchRegex.FindString(cfr.ReplaceWith.String()), "$1", ) var path []string @@ -141,18 +148,24 @@ func (f *ConfigurationFile) LookupConfigurationValue(cfr ConfigurationFileReplac // Look for the key in the configuration file, and if found return that value to the // calling function. - match, dt, _, err := jsonparser.Get(f.configuration, path...) + match, _, _, err := jsonparser.Get(f.configuration, path...) if err != nil { if err != jsonparser.KeyPathNotFoundError { - return match, dt, errors.WithStack(err) + return match, errors.WithStack(err) } + zap.S().Warnw( + "attempted to load a configuration value that does not exist", + zap.Strings("path", path), + zap.String("filename", f.FileName), + ) + // If there is no key, keep the original value intact, that way it is obvious there // is a replace issue at play. - return []byte(cfr.Value), cfr.ValueType, nil + return cfr.ReplaceWith.Value(), nil } else { - replaced := []byte(configMatchRegex.ReplaceAllString(cfr.Value, string(match))) + replaced := []byte(configMatchRegex.ReplaceAllString(cfr.ReplaceWith.String(), string(match))) - return replaced, cfr.ValueType, nil + return replaced, nil } } diff --git a/parser/parser.go b/parser/parser.go index d22bb45..d03c343 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -3,7 +3,6 @@ package parser import ( "bufio" "encoding/json" - "fmt" "github.com/beevik/etree" "github.com/buger/jsonparser" "github.com/ghodss/yaml" @@ -43,11 +42,13 @@ type ConfigurationFile struct { // Defines a single find/replace instance for a given server configuration file. type ConfigurationFileReplacement struct { - Match string `json:"match"` - Value string `json:"value"` - ValueType jsonparser.ValueType `json:"-"` + Match string `json:"match"` + IfValue *string `json:"if_value"` + ReplaceWith ReplaceValue `json:"replace_with"` } +// Handles unmarshaling the JSON representation into a struct that provides more useful +// data to this functionality. func (cfr *ConfigurationFileReplacement) UnmarshalJSON(data []byte) error { if m, err := jsonparser.GetString(data, "match"); err != nil { return err @@ -55,17 +56,13 @@ func (cfr *ConfigurationFileReplacement) UnmarshalJSON(data []byte) error { cfr.Match = m } - if v, dt, _, err := jsonparser.Get(data, "value"); err != nil { + if v, dt, _, err := jsonparser.Get(data, "replace_with"); err != nil { return err } else { - if dt != jsonparser.String && dt != jsonparser.Number && dt != jsonparser.Boolean { - return errors.New( - fmt.Sprintf("cannot parse JSON: received unexpected replacement value type: %s", dt.String()), - ) + cfr.ReplaceWith = ReplaceValue{ + value: v, + valueType: dt, } - - cfr.Value = string(v) - cfr.ValueType = dt } return nil @@ -139,7 +136,7 @@ func (f *ConfigurationFile) parseXmlFile(path string) error { } for i, replacement := range f.Replace { - value, _, err := f.LookupConfigurationValue(replacement) + value, err := f.LookupConfigurationValue(replacement) if err != nil { return err } @@ -209,7 +206,7 @@ func (f *ConfigurationFile) parseXmlFile(path string) error { func (f *ConfigurationFile) parseIniFile(path string) error { // Ini package can't handle a non-existent file, so handle that automatically here // by creating it if not exists. - file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0644); + file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0644) if err != nil { return err } @@ -223,7 +220,7 @@ func (f *ConfigurationFile) parseIniFile(path string) error { for _, replacement := range f.Replace { path := strings.SplitN(replacement.Match, ".", 2) - value, _, err := f.LookupConfigurationValue(replacement) + value, err := f.LookupConfigurationValue(replacement) if err != nil { return err } @@ -336,7 +333,7 @@ func (f *ConfigurationFile) parseTextFile(path string) error { } hasReplaced = true - t = strings.Replace(t, replace.Match, replace.Value, 1) + t = strings.Replace(t, replace.Match, replace.ReplaceWith.String(), 1) } // If there was a replacement that occurred on this specific line, do a write to the file @@ -364,11 +361,19 @@ func (f *ConfigurationFile) parsePropertiesFile(path string) error { } for _, replace := range f.Replace { - data, _, err := f.LookupConfigurationValue(replace) + data, err := f.LookupConfigurationValue(replace) if err != nil { return err } + v, ok := p.Get(replace.Match) + // Don't attempt to replace the value if we're looking for a specific value and + // it does not match. If there was no match at all in the file for this key but + // we're doing an IfValue match, do nothing. + if replace.IfValue != nil && (!ok || (ok && v != *replace.IfValue)) { + continue + } + if _, _, err := p.Set(replace.Match, string(data)); err != nil { return err } diff --git a/parser/value.go b/parser/value.go new file mode 100644 index 0000000..5fd307e --- /dev/null +++ b/parser/value.go @@ -0,0 +1,22 @@ +package parser + +import ( + "github.com/buger/jsonparser" +) + +type ReplaceValue struct { + value []byte + valueType jsonparser.ValueType `json:"-"` +} + +func (cv *ReplaceValue) Value() []byte { + return cv.value +} + +func (cv *ReplaceValue) String() string { + return string(cv.value) +} + +func (cv *ReplaceValue) Type() jsonparser.ValueType { + return cv.valueType +} \ No newline at end of file