Add Sources field to Config (#501)

* Add Sources field to Config

and keep track of merged files there. Also print the Sources as a
comment in the String() method.

Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>

* Fix tests

Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>

* Fix linter

Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>

* Fix TODO

by renaming the toMap function and making it operate on ConfigValues
instead of full Config objects (because after all, it wasn't copying the
Sources field)

Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>

* [minor] Return ConfigValues interface when erroring out

although nobody should consume it since we errored

Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>

* Add check for "Sources" comment

to check that these all generate a line:
- cmdline
- remote config (config_url)
- local files

Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>

---------

Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
This commit is contained in:
Dimitris Karakasilis
2024-09-26 12:04:21 +03:00
committed by GitHub
parent 5c38240a46
commit a56cb0bb38
5 changed files with 186 additions and 127 deletions

View File

@@ -31,9 +31,14 @@ var ValidFileHeaders = []string{
type Configs []*Config type Configs []*Config
type ConfigValues map[string]interface{}
// We don't allow yamls that are plain arrays because is has no use in Kairos // We don't allow yamls that are plain arrays because is has no use in Kairos
// and there is no way to merge an array yaml with a "map" yaml. // and there is no way to merge an array yaml with a "map" yaml.
type Config map[string]interface{} type Config struct {
Sources []string
Values ConfigValues
}
// MergeConfigURL looks for the "config_url" key and if it's found // MergeConfigURL looks for the "config_url" key and if it's found
// it downloads the remote config and merges it with the current one. // it downloads the remote config and merges it with the current one.
@@ -63,49 +68,53 @@ func (c *Config) MergeConfigURL() error {
return c.MergeConfig(remoteConfig) return c.MergeConfig(remoteConfig)
} }
func (c *Config) toMap() (map[string]interface{}, error) { func (c *Config) valuesCopy() (ConfigValues, error) {
var result map[string]interface{} var result ConfigValues
data, err := yaml.Marshal(c) data, err := yaml.Marshal(c.Values)
if err != nil { if err != nil {
return result, err return result, err
} }
err = yaml.Unmarshal(data, &result) err = yaml.Unmarshal(data, &result)
return result, err return result, err
} }
func (c *Config) applyMap(i interface{}) error {
data, err := yaml.Marshal(i)
if err != nil {
return err
}
err = yaml.Unmarshal(data, c)
return err
}
// MergeConfig merges the config passed as parameter back to the receiver Config. // MergeConfig merges the config passed as parameter back to the receiver Config.
func (c *Config) MergeConfig(newConfig *Config) error { func (c *Config) MergeConfig(newConfig *Config) error {
var err error var err error
// convert the two configs into maps aMap, err := c.valuesCopy()
aMap, err := c.toMap()
if err != nil { if err != nil {
return err return err
} }
bMap, err := newConfig.toMap() bMap, err := newConfig.valuesCopy()
if err != nil { if err != nil {
return err return err
} }
// TODO: Consider removing the `name:` key because in the end we end up with the
// value from the last config merged. Ideally we should display the name in the "sources"
// comment next to the file but doing it here is not possible because the configs
// passed, could already be results of various merged thus we don't know which of
// the "sources" should take the "name" next to it.
//
// if _, exists := bMap.Values["name"]; exists {
// delete(bMap.Values, "name")
// }
// deep merge the two maps // deep merge the two maps
cMap, err := DeepMerge(aMap, bMap) mergedValues, err := DeepMerge(aMap, bMap)
if err != nil { if err != nil {
return err return err
} }
finalConfig := Config{}
finalConfig.Sources = append(c.Sources, newConfig.Sources...)
finalConfig.Values = mergedValues.(ConfigValues)
// apply the result of the deepmerge into the base config *c = finalConfig
return c.applyMap(cMap)
return nil
} }
func mergeSlices(sliceA, sliceB []interface{}) ([]interface{}, error) { func mergeSlices(sliceA, sliceB []interface{}) ([]interface{}, error) {
@@ -148,7 +157,7 @@ func mergeSlices(sliceA, sliceB []interface{}) ([]interface{}, error) {
return sliceA, nil return sliceA, nil
} }
func deepMergeMaps(a, b map[string]interface{}) (map[string]interface{}, error) { func deepMergeMaps(a, b ConfigValues) (ConfigValues, error) {
// go through all items in b and merge them to a // go through all items in b and merge them to a
for k, v := range b { for k, v := range b {
current, ok := a[k] current, ok := a[k]
@@ -190,7 +199,7 @@ func DeepMerge(a, b interface{}) (interface{}, error) {
// We don't support merging different data structures // We don't support merging different data structures
if typeA.Kind() != typeB.Kind() { if typeA.Kind() != typeB.Kind() {
return map[string]interface{}{}, fmt.Errorf("cannot merge %s with %s", typeA.String(), typeB.String()) return ConfigValues{}, fmt.Errorf("cannot merge %s with %s", typeA.String(), typeB.String())
} }
if typeA.Kind() == reflect.Slice { if typeA.Kind() == reflect.Slice {
@@ -198,7 +207,7 @@ func DeepMerge(a, b interface{}) (interface{}, error) {
} }
if typeA.Kind() == reflect.Map { if typeA.Kind() == reflect.Map {
return deepMergeMaps(a.(map[string]interface{}), b.(map[string]interface{})) return deepMergeMaps(a.(ConfigValues), b.(ConfigValues))
} }
// for any other type, b should take precedence // for any other type, b should take precedence
@@ -207,12 +216,22 @@ func DeepMerge(a, b interface{}) (interface{}, error) {
// String returns a string which is a Yaml representation of the Config. // String returns a string which is a Yaml representation of the Config.
func (c *Config) String() (string, error) { func (c *Config) String() (string, error) {
data, err := yaml.Marshal(c) sourcesComment := ""
if err != nil { config := *c
return "", err if len(config.Sources) > 0 {
sourcesComment = "# Sources:\n"
for _, s := range config.Sources {
sourcesComment += fmt.Sprintf("# - %s\n", s)
}
sourcesComment += "\n"
} }
return fmt.Sprintf("%s\n\n%s", DefaultHeader, string(data)), nil data, err := yaml.Marshal(config.Values)
if err != nil {
return "", fmt.Errorf("marshalling the config to a string: %s", err)
}
return fmt.Sprintf("%s\n\n%s%s", DefaultHeader, sourcesComment, string(data)), nil
} }
func (cs Configs) Merge() (*Config, error) { func (cs Configs) Merge() (*Config, error) {
@@ -251,7 +270,7 @@ func Scan(o *Options, filter func(d []byte) ([]byte, error)) (*Config, error) {
} }
if o.Overwrites != "" { if o.Overwrites != "" {
yaml.Unmarshal([]byte(o.Overwrites), &mergedConfig) //nolint:errcheck yaml.Unmarshal([]byte(o.Overwrites), &mergedConfig.Values) //nolint:errcheck
} }
return mergedConfig, nil return mergedConfig, nil
@@ -295,10 +314,12 @@ func parseFiles(dir []string, nologs bool) Configs {
} }
var newConfig Config var newConfig Config
err = yaml.Unmarshal(b, &newConfig) err = yaml.Unmarshal(b, &newConfig.Values)
if err != nil && !nologs { if err != nil && !nologs {
fmt.Printf("warning: failed to parse config:\n%s\n", err.Error()) fmt.Printf("warning: failed to parse config:\n%s\n", err.Error())
} }
newConfig.Sources = []string{f}
result = append(result, &newConfig) result = append(result, &newConfig)
} else { } else {
if !nologs { if !nologs {
@@ -324,9 +345,9 @@ func parseReaders(readers []io.Reader, nologs bool) Configs {
} }
continue continue
} }
err = yaml.Unmarshal(read, &newConfig) err = yaml.Unmarshal(read, &newConfig.Values)
if err != nil { if err != nil {
err = json.Unmarshal(read, &newConfig) err = json.Unmarshal(read, &newConfig.Values)
if err != nil { if err != nil {
if !nologs { if !nologs {
fmt.Printf("Error unmarshalling config(error: %s): %s", err.Error(), string(read)) fmt.Printf("Error unmarshalling config(error: %s): %s", err.Error(), string(read))
@@ -334,6 +355,7 @@ func parseReaders(readers []io.Reader, nologs bool) Configs {
continue continue
} }
} }
newConfig.Sources = []string{"reader"}
result = append(result, &newConfig) result = append(result, &newConfig)
} }
@@ -380,7 +402,7 @@ func listFiles(dir string) ([]string, error) {
// ParseCmdLine reads options from the kernel cmdline and returns the equivalent // ParseCmdLine reads options from the kernel cmdline and returns the equivalent
// Config. // Config.
func ParseCmdLine(file string, filter func(d []byte) ([]byte, error)) (*Config, error) { func ParseCmdLine(file string, filter func(d []byte) ([]byte, error)) (*Config, error) {
result := Config{} result := Config{Sources: []string{"cmdline"}}
dotToYAML, err := machine.DotToYAML(file) dotToYAML, err := machine.DotToYAML(file)
if err != nil { if err != nil {
return &result, err return &result, err
@@ -391,7 +413,7 @@ func ParseCmdLine(file string, filter func(d []byte) ([]byte, error)) (*Config,
return &result, err return &result, err
} }
err = yaml.Unmarshal(filteredYAML, &result) err = yaml.Unmarshal(filteredYAML, &result.Values)
if err != nil { if err != nil {
return &result, err return &result, err
} }
@@ -401,7 +423,7 @@ func ParseCmdLine(file string, filter func(d []byte) ([]byte, error)) (*Config,
// ConfigURL returns the value of config_url if set or empty string otherwise. // ConfigURL returns the value of config_url if set or empty string otherwise.
func (c Config) ConfigURL() string { func (c Config) ConfigURL() string {
if val, hasKey := c["config_url"]; hasKey { if val, hasKey := c.Values["config_url"]; hasKey {
if s, isString := val.(string); isString { if s, isString := val.(string); isString {
return s return s
} }
@@ -446,10 +468,12 @@ func fetchRemoteConfig(url string) (*Config, error) {
return result, nil return result, nil
} }
if err := yaml.Unmarshal(body, result); err != nil { if err := yaml.Unmarshal(body, &result.Values); err != nil {
return result, fmt.Errorf("could not unmarshal remote config to an object: %w", err) return result, fmt.Errorf("could not unmarshal remote config to an object: %w", err)
} }
result.Sources = []string{url}
return result, nil return result, nil
} }

View File

@@ -46,16 +46,16 @@ var _ = Describe("Config Collector", func() {
Context("different keys", func() { Context("different keys", func() {
BeforeEach(func() { BeforeEach(func() {
err := yaml.Unmarshal([]byte(`#cloud-config err := yaml.Unmarshal([]byte(`#cloud-config
name: Mario`), originalConfig) name: Mario`), &originalConfig.Values)
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
err = yaml.Unmarshal([]byte(`#cloud-config err = yaml.Unmarshal([]byte(`#cloud-config
surname: Bros`), newConfig) surname: Bros`), &newConfig.Values)
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
}) })
It("gets merged together", func() { It("gets merged together", func() {
Expect(originalConfig.MergeConfig(newConfig)).ToNot(HaveOccurred()) Expect(originalConfig.MergeConfig(newConfig)).ToNot(HaveOccurred())
surname, isString := (*originalConfig)["surname"].(string) surname, isString := originalConfig.Values["surname"].(string)
Expect(isString).To(BeTrue()) Expect(isString).To(BeTrue())
Expect(surname).To(Equal("Bros")) Expect(surname).To(Equal("Bros"))
}) })
@@ -67,58 +67,58 @@ surname: Bros`), newConfig)
err := yaml.Unmarshal([]byte(`#cloud-config err := yaml.Unmarshal([]byte(`#cloud-config
info: info:
name: Mario name: Mario
`), originalConfig) `), &originalConfig.Values)
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
err = yaml.Unmarshal([]byte(`#cloud-config err = yaml.Unmarshal([]byte(`#cloud-config
info: info:
surname: Bros surname: Bros
`), newConfig) `), &newConfig.Values)
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
}) })
It("merges the keys", func() { It("merges the keys", func() {
Expect(originalConfig.MergeConfig(newConfig)).ToNot(HaveOccurred()) Expect(originalConfig.MergeConfig(newConfig)).ToNot(HaveOccurred())
info, isMap := (*originalConfig)["info"].(Config) info, isMap := originalConfig.Values["info"].(ConfigValues)
Expect(isMap).To(BeTrue()) Expect(isMap).To(BeTrue())
Expect(info["name"]).To(Equal("Mario")) Expect(info["name"]).To(Equal("Mario"))
Expect(info["surname"]).To(Equal("Bros")) Expect(info["surname"]).To(Equal("Bros"))
Expect(*originalConfig).To(HaveLen(1)) Expect(originalConfig.Values).To(HaveLen(1))
Expect(info).To(HaveLen(2)) Expect(info).To(HaveLen(2))
}) })
}) })
Context("when the key is a string", func() { Context("when the key is a string", func() {
BeforeEach(func() { BeforeEach(func() {
err := yaml.Unmarshal([]byte("#cloud-config\nname: Mario"), originalConfig) err := yaml.Unmarshal([]byte("#cloud-config\nname: Mario"), &originalConfig.Values)
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
err = yaml.Unmarshal([]byte("#cloud-config\nname: Luigi"), newConfig) err = yaml.Unmarshal([]byte("#cloud-config\nname: Luigi"), &newConfig.Values)
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
}) })
It("overwrites", func() { It("overwrites", func() {
Expect(originalConfig.MergeConfig(newConfig)).ToNot(HaveOccurred()) Expect(originalConfig.MergeConfig(newConfig)).ToNot(HaveOccurred())
name, isString := (*originalConfig)["name"].(string) name, isString := originalConfig.Values["name"].(string)
Expect(isString).To(BeTrue()) Expect(isString).To(BeTrue())
Expect(name).To(Equal("Luigi")) Expect(name).To(Equal("Luigi"))
Expect(*originalConfig).To(HaveLen(1)) Expect(originalConfig.Values).To(HaveLen(1))
}) })
}) })
}) })
Context("reset keys", func() { Context("reset keys", func() {
Context("remove keys", func() { Context("remove keys", func() {
BeforeEach(func() { BeforeEach(func() {
err := yaml.Unmarshal([]byte("#cloud-config\nlist:\n - 1\n - 2\nname: Mario"), originalConfig) err := yaml.Unmarshal([]byte("#cloud-config\nlist:\n - 1\n - 2\nname: Mario"), &originalConfig.Values)
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
err = yaml.Unmarshal([]byte("#cloud-config\nlist: null\nname: null"), newConfig) err = yaml.Unmarshal([]byte("#cloud-config\nlist: null\nname: null"), &newConfig.Values)
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
}) })
It("overwrites", func() { It("overwrites", func() {
Expect(originalConfig.MergeConfig(newConfig)).ToNot(HaveOccurred()) Expect(originalConfig.MergeConfig(newConfig)).ToNot(HaveOccurred())
Expect((*originalConfig)["list"]).To(BeEmpty()) Expect(originalConfig.Values["list"]).To(BeEmpty())
name, isString := (*originalConfig)["name"].(string) name, isString := originalConfig.Values["name"].(string)
Expect(isString).To(BeTrue()) Expect(isString).To(BeTrue())
Expect(name).To(Equal("")) Expect(name).To(Equal(""))
Expect(*originalConfig).To(HaveLen(2)) Expect(originalConfig.Values).To(HaveLen(2))
}) })
}) })
}) })
@@ -132,13 +132,13 @@ info:
Context("when there is no config_url defined", func() { Context("when there is no config_url defined", func() {
BeforeEach(func() { BeforeEach(func() {
err := yaml.Unmarshal([]byte("#cloud-config\nname: Mario"), originalConfig) err := yaml.Unmarshal([]byte("#cloud-config\nname: Mario"), &originalConfig.Values)
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
}) })
It("does nothing", func() { It("does nothing", func() {
Expect(originalConfig.MergeConfigURL()).ToNot(HaveOccurred()) Expect(originalConfig.MergeConfigURL()).ToNot(HaveOccurred())
Expect(*originalConfig).To(HaveLen(1)) Expect(originalConfig.Values).To(HaveLen(1))
}) })
}) })
@@ -163,7 +163,7 @@ name: Mario
surname: Bros surname: Bros
info: info:
job: plumber job: plumber
`, port)), originalConfig) `, port)), &originalConfig.Values)
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
err := os.WriteFile(path.Join(tmpDir, "config1.yaml"), []byte(fmt.Sprintf(`#cloud-config err := os.WriteFile(path.Join(tmpDir, "config1.yaml"), []byte(fmt.Sprintf(`#cloud-config
@@ -191,20 +191,20 @@ info:
err := originalConfig.MergeConfigURL() err := originalConfig.MergeConfigURL()
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
name, ok := (*originalConfig)["name"].(string) name, ok := originalConfig.Values["name"].(string)
Expect(ok).To(BeTrue()) Expect(ok).To(BeTrue())
Expect(name).To(Equal("Mario")) Expect(name).To(Equal("Mario"))
surname, ok := (*originalConfig)["surname"].(string) surname, ok := originalConfig.Values["surname"].(string)
Expect(ok).To(BeTrue()) Expect(ok).To(BeTrue())
Expect(surname).To(Equal("Bras")) Expect(surname).To(Equal("Bras"))
info, ok := (*originalConfig)["info"].(Config) info, ok := originalConfig.Values["info"].(ConfigValues)
Expect(ok).To(BeTrue()) Expect(ok).To(BeTrue())
Expect(info["job"]).To(Equal("plumber")) Expect(info["job"]).To(Equal("plumber"))
Expect(info["girlfriend"]).To(Equal("princess")) Expect(info["girlfriend"]).To(Equal("princess"))
Expect(*originalConfig).To(HaveLen(4)) Expect(originalConfig.Values).To(HaveLen(4))
}) })
}) })
}) })
@@ -224,12 +224,12 @@ info:
return d, nil return d, nil
}) })
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
Expect(*c).To(HaveKey("mario")) Expect(c.Values).To(HaveKey("mario"))
Expect(*c).To(HaveKey("luigi")) Expect(c.Values).To(HaveKey("luigi"))
Expect(*c).To(HaveKey("princess")) Expect(c.Values).To(HaveKey("princess"))
Expect((*c)["mario"]).To(Equal("bros")) Expect(c.Values["mario"]).To(Equal("bros"))
Expect((*c)["luigi"]).To(Equal("bros")) Expect(c.Values["luigi"]).To(Equal("bros"))
Expect((*c)["princess"]).To(Equal("peach")) Expect(c.Values["princess"]).To(Equal("peach"))
}) })
It("Reads from several reader objects and merges them (json)", func() { It("Reads from several reader objects and merges them (json)", func() {
obj1 := bytes.NewReader([]byte(`{"mario":"bros"}`)) obj1 := bytes.NewReader([]byte(`{"mario":"bros"}`))
@@ -245,12 +245,12 @@ info:
return d, nil return d, nil
}) })
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
Expect(*c).To(HaveKey("mario")) Expect(c.Values).To(HaveKey("mario"))
Expect(*c).To(HaveKey("luigi")) Expect(c.Values).To(HaveKey("luigi"))
Expect(*c).To(HaveKey("princess")) Expect(c.Values).To(HaveKey("princess"))
Expect((*c)["mario"]).To(Equal("bros")) Expect(c.Values["mario"]).To(Equal("bros"))
Expect((*c)["luigi"]).To(Equal("bros")) Expect(c.Values["luigi"]).To(Equal("bros"))
Expect((*c)["princess"]).To(Equal("peach")) Expect(c.Values["princess"]).To(Equal("peach"))
}) })
It("Reads from several reader objects and merges them (json+yaml)", func() { It("Reads from several reader objects and merges them (json+yaml)", func() {
obj1 := bytes.NewReader([]byte(`{"mario":"bros"}`)) obj1 := bytes.NewReader([]byte(`{"mario":"bros"}`))
@@ -266,12 +266,12 @@ info:
return d, nil return d, nil
}) })
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
Expect(*c).To(HaveKey("mario")) Expect(c.Values).To(HaveKey("mario"))
Expect(*c).To(HaveKey("luigi")) Expect(c.Values).To(HaveKey("luigi"))
Expect(*c).To(HaveKey("princess")) Expect(c.Values).To(HaveKey("princess"))
Expect((*c)["mario"]).To(Equal("bros")) Expect(c.Values["mario"]).To(Equal("bros"))
Expect((*c)["luigi"]).To(Equal("bros")) Expect(c.Values["luigi"]).To(Equal("bros"))
Expect((*c)["princess"]).To(Equal("peach")) Expect(c.Values["princess"]).To(Equal("peach"))
}) })
It("Fails to read from a reader which is neither json or yaml", func() { It("Fails to read from a reader which is neither json or yaml", func() {
obj1 := bytes.NewReader([]byte(`blip`)) obj1 := bytes.NewReader([]byte(`blip`))
@@ -288,9 +288,9 @@ info:
return d, nil return d, nil
}) })
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
Expect(*c).ToNot(HaveKey("mario")) Expect(c.Values).ToNot(HaveKey("mario"))
Expect(*c).ToNot(HaveKey("luigi")) Expect(c.Values).ToNot(HaveKey("luigi"))
Expect(*c).ToNot(HaveKey("princess")) Expect(c.Values).ToNot(HaveKey("princess"))
}) })
}) })
@@ -384,27 +384,27 @@ info:
}) })
Context("empty map", func() { Context("empty map", func() {
a := map[string]interface{}{} a := ConfigValues{}
b := map[string]interface{}{ b := ConfigValues{
"foo": "bar", "foo": "bar",
} }
It("merges", func() { It("merges", func() {
c, err := DeepMerge(a, b) c, err := DeepMerge(a, b)
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
Expect(c).To(Equal(map[string]interface{}{ Expect(c).To(Equal(ConfigValues{
"foo": "bar", "foo": "bar",
})) }))
}) })
}) })
Context("simple map", func() { Context("simple map", func() {
a := map[string]interface{}{ a := ConfigValues{
"es": "uno", "es": "uno",
"nl": "een", "nl": "een",
"#": 0, "#": 0,
} }
b := map[string]interface{}{ b := ConfigValues{
"en": "one", "en": "one",
"nl": "één", "nl": "één",
"de": "Eins", "de": "Eins",
@@ -414,7 +414,7 @@ info:
It("merges", func() { It("merges", func() {
c, err := DeepMerge(a, b) c, err := DeepMerge(a, b)
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
Expect(c).To(Equal(map[string]interface{}{ Expect(c).To(Equal(ConfigValues{
"#": 1, "#": 1,
"de": "Eins", "de": "Eins",
"en": "one", "en": "one",
@@ -425,7 +425,7 @@ info:
}) })
Context("reset key", func() { Context("reset key", func() {
a := map[string]interface{}{ a := ConfigValues{
"string": "val", "string": "val",
"slice": []interface{}{"valA", "valB"}, "slice": []interface{}{"valA", "valB"},
"map": map[string]interface{}{ "map": map[string]interface{}{
@@ -433,7 +433,7 @@ info:
"valB": "", "valB": "",
}, },
} }
b := map[string]interface{}{ b := ConfigValues{
"string": nil, "string": nil,
"slice": nil, "slice": nil,
"map": nil, "map": nil,
@@ -442,7 +442,7 @@ info:
It("merges", func() { It("merges", func() {
c, err := DeepMerge(a, b) c, err := DeepMerge(a, b)
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
Expect(c).To(Equal(map[string]interface{}{ Expect(c).To(Equal(ConfigValues{
"string": "", "string": "",
"slice": []interface{}{}, "slice": []interface{}{},
"map": map[string]interface{}{}, "map": map[string]interface{}{},
@@ -506,8 +506,12 @@ stages:
c, err := Scan(o, FilterKeysTestMerge) c, err := Scan(o, FilterKeysTestMerge)
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
fmt.Println(c.String()) Expect(c.String()).To(MatchRegexp(`#cloud-config
Expect(c.String()).To(Equal(`#cloud-config
# Sources:
# - .*/local_config_1.yaml
# - .*/local_config_2.yaml
# - cmdline
install: install:
auto: true auto: true
@@ -582,15 +586,19 @@ stages:
c, err := Scan(o, FilterKeysTestMerge) c, err := Scan(o, FilterKeysTestMerge)
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
fmt.Println(c.String()) Expect(c.String()).To(MatchRegexp(`#cloud-config
Expect(c.String()).To(Equal(`#cloud-config
# Sources:
# - .*/local_config_1.yaml
# - .*/local_config_2.yaml
# - cmdline
stages: stages:
initramfs: initramfs:
- users: - users:
kairos: kairos:
passwd: kairos passwd: kairos
- if: '[ ! -f /oem/80_stylus.yaml ]' - if: '\[ ! -f /oem/80_stylus.yaml \]'
name: set_inotify_max_values name: set_inotify_max_values
sysctl: sysctl:
fs.inotify.max_user_instances: "8192" fs.inotify.max_user_instances: "8192"
@@ -674,8 +682,12 @@ install:
c, err := Scan(o, FilterKeysTestMerge) c, err := Scan(o, FilterKeysTestMerge)
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
fmt.Println(c.String()) Expect(c.String()).To(MatchRegexp(`#cloud-config
Expect(c.String()).To(Equal(`#cloud-config
# Sources:
# - .*/local_config_1.yaml
# - .*/local_config_2.yaml
# - cmdline
install: install:
auto: true auto: true
@@ -761,7 +773,10 @@ stages:
c, err := Scan(o, FilterKeysTestMerge) c, err := Scan(o, FilterKeysTestMerge)
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
Expect(c.String()).To(Equal(`#cloud-config Expect(c.String()).To(MatchRegexp(`#cloud-config
# Sources:
# - .*/local_config_1.yaml
foo: bar foo: bar
install: install:
@@ -837,7 +852,12 @@ stages:
c, err := Scan(o, FilterKeysTestMerge) c, err := Scan(o, FilterKeysTestMerge)
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
Expect(c.String()).To(Equal(`#cloud-config Expect(c.String()).To(MatchRegexp(`#cloud-config
# Sources:
# .*/local_config_1\.yaml
# .*/local_config_2\.yaml
# - cmdline
install: install:
auto: true auto: true
@@ -950,34 +970,51 @@ options:
c, err := Scan(o, FilterKeysTestMerge) c, err := Scan(o, FilterKeysTestMerge)
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
configURL, ok := (*c)["config_url"].(string) configURL, ok := c.Values["config_url"].(string)
Expect(ok).To(BeTrue()) Expect(ok).To(BeTrue())
Expect(configURL).To(MatchRegexp("remote_config_2.yaml")) Expect(configURL).To(MatchRegexp("remote_config_2.yaml"))
k := (*c)["local_key_1"].(string) k := c.Values["local_key_1"].(string)
Expect(k).To(Equal("local_value_1")) Expect(k).To(Equal("local_value_1"))
k = (*c)["local_key_2"].(string) k = c.Values["local_key_2"].(string)
Expect(k).To(Equal("local_value_2")) Expect(k).To(Equal("local_value_2"))
k = (*c)["local_key_3"].(string) k = c.Values["local_key_3"].(string)
Expect(k).To(Equal("local_value_3")) Expect(k).To(Equal("local_value_3"))
k = (*c)["remote_key_1"].(string) k = c.Values["remote_key_1"].(string)
Expect(k).To(Equal("remote_value_1")) Expect(k).To(Equal("remote_value_1"))
k = (*c)["remote_key_2"].(string) k = c.Values["remote_key_2"].(string)
Expect(k).To(Equal("remote_value_2")) Expect(k).To(Equal("remote_value_2"))
k = (*c)["remote_key_3"].(string) k = c.Values["remote_key_3"].(string)
Expect(k).To(Equal("remote_value_3")) Expect(k).To(Equal("remote_value_3"))
k = (*c)["remote_key_4"].(string) k = c.Values["remote_key_4"].(string)
Expect(k).To(Equal("remote_value_4")) Expect(k).To(Equal("remote_value_4"))
options := (*c)["options"].(Config) options := c.Values["options"].(ConfigValues)
Expect(options["foo"]).To(Equal("bar")) Expect(options["foo"]).To(Equal("bar"))
Expect(options["remote_option_1"]).To(Equal("remote_option_value_1")) Expect(options["remote_option_1"]).To(Equal("remote_option_value_1"))
Expect(options["remote_option_2"]).To(Equal("remote_option_value_2")) Expect(options["remote_option_2"]).To(Equal("remote_option_value_2"))
player := (*c)["player"].(Config) player := c.Values["player"].(ConfigValues)
fmt.Print(player) fmt.Print(player)
Expect(player["name"]).NotTo(Equal("Toad")) Expect(player["name"]).NotTo(Equal("Toad"))
Expect(player["surname"]).To(Equal("Bros")) Expect(player["surname"]).To(Equal("Bros"))
cs, _ := c.String()
// Check "Sources" comment
Expect(cs).To(MatchRegexp(`.*
# Sources:
# - /tmp/.*/local_config_1.yaml
# - http://127.0.0.1:.*/remote_config_3.yaml
# - http://127.0.0.1:.*/remote_config_4.yaml
# - /tmp/.*/local_config_2.yaml
# - http://127.0.0.1:.*/remote_config_5.yaml
# - http://127.0.0.1:.*/remote_config_6.yaml
# - /tmp/.*/local_config_3.yaml
# - cmdline
# - http://127.0.0.1:.*/remote_config_1.yaml
# - http://127.0.0.1:.*/remote_config_2.yaml
.*
`))
}) })
}) })
@@ -1036,19 +1073,19 @@ remote_key_2: remote_value_2`), os.ModePerm)
c, err := Scan(o, FilterKeysTest) c, err := Scan(o, FilterKeysTest)
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
Expect((*c)["local_key_2"]).To(BeNil()) Expect(c.Values["local_key_2"]).To(BeNil())
Expect((*c)["remote_key_2"]).To(BeNil()) Expect(c.Values["remote_key_2"]).To(BeNil())
// sanity check, the rest should be there // sanity check, the rest should be there
v, ok := (*c)["config_url"].(string) v, ok := c.Values["config_url"].(string)
Expect(ok).To(BeTrue()) Expect(ok).To(BeTrue())
Expect(v).To(MatchRegexp("remote_config_2.yaml")) Expect(v).To(MatchRegexp("remote_config_2.yaml"))
v, ok = (*c)["local_key_1"].(string) v, ok = c.Values["local_key_1"].(string)
Expect(ok).To(BeTrue()) Expect(ok).To(BeTrue())
Expect(v).To(Equal("local_value_1")) Expect(v).To(Equal("local_value_1"))
v, ok = (*c)["remote_key_1"].(string) v, ok = c.Values["remote_key_1"].(string)
Expect(ok).To(BeTrue()) Expect(ok).To(BeTrue())
Expect(v).To(Equal("remote_value_1")) Expect(v).To(Equal("remote_value_1"))
}) })
@@ -1095,14 +1132,14 @@ local_key_2: local_value_2
c, err := Scan(o, FilterKeysTest) c, err := Scan(o, FilterKeysTest)
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
Expect((*c)["local_key_1"]).ToNot(BeNil()) Expect(c.Values["local_key_1"]).ToNot(BeNil())
Expect((*c)["local_key_2"]).ToNot(BeNil()) Expect(c.Values["local_key_2"]).ToNot(BeNil())
v, ok := (*c)["local_key_1"].(string) v, ok := c.Values["local_key_1"].(string)
Expect(ok).To(BeTrue()) Expect(ok).To(BeTrue())
Expect(v).To(Equal("local_value_1")) Expect(v).To(Equal("local_value_1"))
v, ok = (*c)["local_key_2"].(string) v, ok = c.Values["local_key_2"].(string)
Expect(ok).To(BeTrue()) Expect(ok).To(BeTrue())
Expect(v).To(Equal("local_value_2")) Expect(v).To(Equal("local_value_2"))
}) })
@@ -1113,7 +1150,7 @@ local_key_2: local_value_2
var conf *Config var conf *Config
BeforeEach(func() { BeforeEach(func() {
conf = &Config{} conf = &Config{}
err := yaml.Unmarshal([]byte("name: Mario"), conf) err := yaml.Unmarshal([]byte("name: Mario"), &conf.Values)
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
}) })

View File

@@ -11,6 +11,7 @@ import (
) )
func TestConfig(t *testing.T) { func TestConfig(t *testing.T) {
// format.TruncatedDiff = false
RegisterFailHandler(Fail) RegisterFailHandler(Fail)
RunSpecs(t, "Config Collector Suite") RunSpecs(t, "Config Collector Suite")
} }

3
go.mod
View File

@@ -23,7 +23,7 @@ require (
github.com/qeesung/image2ascii v1.0.1 github.com/qeesung/image2ascii v1.0.1
github.com/rs/zerolog v1.33.0 github.com/rs/zerolog v1.33.0
github.com/saferwall/pe v1.5.4 github.com/saferwall/pe v1.5.4
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 github.com/santhosh-tekuri/jsonschema/v5 v5.3.1
github.com/swaggest/jsonschema-go v0.3.62 github.com/swaggest/jsonschema-go v0.3.62
github.com/twpayne/go-vfs/v4 v4.3.0 github.com/twpayne/go-vfs/v4 v4.3.0
github.com/urfave/cli/v2 v2.27.4 github.com/urfave/cli/v2 v2.27.4
@@ -110,7 +110,6 @@ require (
google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0 // indirect
google.golang.org/grpc v1.59.0 // indirect google.golang.org/grpc v1.59.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
gotest.tools/v3 v3.4.0 // indirect gotest.tools/v3 v3.4.0 // indirect
howett.net/plist v1.0.0 // indirect howett.net/plist v1.0.0 // indirect

14
go.sum
View File

@@ -119,8 +119,8 @@ github.com/docker/cli v27.1.1+incompatible h1:goaZxOqs4QKxznZjjBWKONQci/MywhtRv2
github.com/docker/cli v27.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli v27.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8=
github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v27.2.1+incompatible h1:fQdiLfW7VLscyoeYEBz7/J8soYFDZV1u6VW6gJEjNMI= github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI=
github.com/docker/docker v27.2.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A= github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A=
github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0= github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
@@ -306,8 +306,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/mudler/go-pluggable v0.0.0-20230126220627-7710299a0ae5 h1:FaZD86+A9mVt7lh9glAryzQblMsbJYU2VnrdZ8yHlTs= github.com/mudler/go-pluggable v0.0.0-20230126220627-7710299a0ae5 h1:FaZD86+A9mVt7lh9glAryzQblMsbJYU2VnrdZ8yHlTs=
github.com/mudler/go-pluggable v0.0.0-20230126220627-7710299a0ae5/go.mod h1:WmKcT8ONmhDQIqQ+HxU+tkGWjzBEyY/KFO8LTGCu4AI= github.com/mudler/go-pluggable v0.0.0-20230126220627-7710299a0ae5/go.mod h1:WmKcT8ONmhDQIqQ+HxU+tkGWjzBEyY/KFO8LTGCu4AI=
github.com/mudler/yip v1.9.4 h1:yaiPKWG5kt/DTNCf7ZGfyWdb1j5c06zYqWF3F+SVKsE= github.com/mudler/yip v1.10.0 h1:MwEIySEfSRRwTUz2BmQQpRn6+M7jqVGf/OldsepBvz0=
github.com/mudler/yip v1.9.4/go.mod h1:nqf8JFCq7a7rIkm7cSs+SOc8QbiyvVJ/xLbUw4GgzFs= github.com/mudler/yip v1.10.0/go.mod h1:gwH7iGcr1Jimox2xKtN2AprEO00GzY7smvuycqCL7+Y=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
@@ -359,7 +359,6 @@ github.com/saferwall/pe v1.5.4 h1:tLmMggEMUfeqrpJ25zS/okUQmyFdD5xWKL2+z9njCqg=
github.com/saferwall/pe v1.5.4/go.mod h1:mJx+PuptmNpoPFBNhWs/uDMFL/kTHVZIkg0d4OUJFbQ= github.com/saferwall/pe v1.5.4/go.mod h1:mJx+PuptmNpoPFBNhWs/uDMFL/kTHVZIkg0d4OUJFbQ=
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4=
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
github.com/secDre4mer/pkcs7 v0.0.0-20240322103146-665324a4461d h1:RQqyEogx5J6wPdoxqL132b100j8KjcVHO1c0KLRoIhc= github.com/secDre4mer/pkcs7 v0.0.0-20240322103146-665324a4461d h1:RQqyEogx5J6wPdoxqL132b100j8KjcVHO1c0KLRoIhc=
github.com/secDre4mer/pkcs7 v0.0.0-20240322103146-665324a4461d/go.mod h1:PegD7EVqlN88z7TpCqH92hHP+GBpfomGCCnw1PFtNOA= github.com/secDre4mer/pkcs7 v0.0.0-20240322103146-665324a4461d/go.mod h1:PegD7EVqlN88z7TpCqH92hHP+GBpfomGCCnw1PFtNOA=
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
@@ -412,8 +411,8 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zcalusic/sysinfo v1.1.0 h1:79Hqn8h4poVz6T57/4ezXbT5ZkZbZm7u1YU1C4paMyk= github.com/zcalusic/sysinfo v1.1.2 h1:38KUgZQmCxlN9vUTt4miis4rU5ISJXGXOJ2rY7bMC8g=
github.com/zcalusic/sysinfo v1.1.0/go.mod h1:NX+qYnWGtJVPV0yWldff9uppNKU4h40hJIRPf/pGLv4= github.com/zcalusic/sysinfo v1.1.2/go.mod h1:NX+qYnWGtJVPV0yWldff9uppNKU4h40hJIRPf/pGLv4=
go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1 h1:A/5uWzF44DlIgdm/PQFwfMkW0JX+cIcQi/SwLAmZP5M= go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1 h1:A/5uWzF44DlIgdm/PQFwfMkW0JX+cIcQi/SwLAmZP5M=
go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
@@ -793,7 +792,6 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0 h1:POO/ycCATvegFmVuPpQzZFJ+pGZeX22Ufu6fibxDVjU=
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=