kairos-sdk/collector/collector_test.go

796 lines
21 KiB
Go
Raw Permalink Normal View History

2023-06-14 11:01:06 +00:00
package collector_test
import (
"fmt"
"os"
"path"
"path/filepath"
. "github.com/kairos-io/kairos-sdk/collector"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"gopkg.in/yaml.v1"
)
var _ = Describe("Config Collector", func() {
Describe("Options", func() {
var options *Options
BeforeEach(func() {
options = &Options{
NoLogs: false,
}
})
It("applies a defined option function", func() {
option := func(o *Options) error {
o.NoLogs = true
return nil
}
Expect(options.NoLogs).To(BeFalse())
Expect(options.Apply(option)).NotTo(HaveOccurred())
Expect(options.NoLogs).To(BeTrue())
})
})
Describe("MergeConfig", func() {
var originalConfig, newConfig *Config
BeforeEach(func() {
originalConfig = &Config{}
newConfig = &Config{}
})
Context("different keys", func() {
BeforeEach(func() {
err := yaml.Unmarshal([]byte(`#cloud-config
name: Mario`), originalConfig)
Expect(err).ToNot(HaveOccurred())
err = yaml.Unmarshal([]byte(`#cloud-config
surname: Bros`), newConfig)
Expect(err).ToNot(HaveOccurred())
})
It("gets merged together", func() {
Expect(originalConfig.MergeConfig(newConfig)).ToNot(HaveOccurred())
surname, isString := (*originalConfig)["surname"].(string)
Expect(isString).To(BeTrue())
Expect(surname).To(Equal("Bros"))
})
})
Context("same keys", func() {
Context("when the key is a map", func() {
BeforeEach(func() {
err := yaml.Unmarshal([]byte(`#cloud-config
info:
name: Mario
`), originalConfig)
Expect(err).ToNot(HaveOccurred())
err = yaml.Unmarshal([]byte(`#cloud-config
info:
surname: Bros
`), newConfig)
Expect(err).ToNot(HaveOccurred())
})
It("merges the keys", func() {
Expect(originalConfig.MergeConfig(newConfig)).ToNot(HaveOccurred())
info, isMap := (*originalConfig)["info"].(Config)
Expect(isMap).To(BeTrue())
Expect(info["name"]).To(Equal("Mario"))
Expect(info["surname"]).To(Equal("Bros"))
Expect(*originalConfig).To(HaveLen(1))
Expect(info).To(HaveLen(2))
})
})
Context("when the key is a string", func() {
BeforeEach(func() {
err := yaml.Unmarshal([]byte("#cloud-config\nname: Mario"), originalConfig)
Expect(err).ToNot(HaveOccurred())
err = yaml.Unmarshal([]byte("#cloud-config\nname: Luigi"), newConfig)
Expect(err).ToNot(HaveOccurred())
})
It("overwrites", func() {
Expect(originalConfig.MergeConfig(newConfig)).ToNot(HaveOccurred())
name, isString := (*originalConfig)["name"].(string)
Expect(isString).To(BeTrue())
Expect(name).To(Equal("Luigi"))
Expect(*originalConfig).To(HaveLen(1))
})
})
})
})
Describe("MergeConfigURL", func() {
var originalConfig *Config
BeforeEach(func() {
originalConfig = &Config{}
})
Context("when there is no config_url defined", func() {
BeforeEach(func() {
err := yaml.Unmarshal([]byte("#cloud-config\nname: Mario"), originalConfig)
Expect(err).ToNot(HaveOccurred())
})
It("does nothing", func() {
Expect(originalConfig.MergeConfigURL()).ToNot(HaveOccurred())
Expect(*originalConfig).To(HaveLen(1))
})
})
Context("when there is a chain of config_url defined", func() {
var closeFunc ServerCloseFunc
var port int
var err error
var tmpDir string
var originalConfig *Config
BeforeEach(func() {
tmpDir, err = os.MkdirTemp("", "config_url_chain")
Expect(err).ToNot(HaveOccurred())
closeFunc, port, err = startAssetServer(tmpDir)
Expect(err).ToNot(HaveOccurred())
originalConfig = &Config{}
err = yaml.Unmarshal([]byte(fmt.Sprintf(`#cloud-config
config_url: http://127.0.0.1:%d/config1.yaml
name: Mario
surname: Bros
info:
job: plumber
`, port)), originalConfig)
Expect(err).ToNot(HaveOccurred())
err := os.WriteFile(path.Join(tmpDir, "config1.yaml"), []byte(fmt.Sprintf(`#cloud-config
config_url: http://127.0.0.1:%d/config2.yaml
surname: Bras
`, port)), os.ModePerm)
Expect(err).ToNot(HaveOccurred())
err = os.WriteFile(path.Join(tmpDir, "config2.yaml"), []byte(`#cloud-config
info:
girlfriend: princess
`), os.ModePerm)
Expect(err).ToNot(HaveOccurred())
})
AfterEach(func() {
closeFunc()
err := os.RemoveAll(tmpDir)
Expect(err).ToNot(HaveOccurred())
})
It("merges them all together", func() {
err := originalConfig.MergeConfigURL()
Expect(err).ToNot(HaveOccurred())
name, ok := (*originalConfig)["name"].(string)
Expect(ok).To(BeTrue())
Expect(name).To(Equal("Mario"))
surname, ok := (*originalConfig)["surname"].(string)
Expect(ok).To(BeTrue())
Expect(surname).To(Equal("Bras"))
info, ok := (*originalConfig)["info"].(Config)
Expect(ok).To(BeTrue())
Expect(info["job"]).To(Equal("plumber"))
Expect(info["girlfriend"]).To(Equal("princess"))
Expect(*originalConfig).To(HaveLen(4))
})
})
})
Describe("deepMerge", func() {
Context("different types", func() {
a := map[string]interface{}{}
b := []string{}
It("merges", func() {
_, err := DeepMerge(a, b)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(Equal("cannot merge map[string]interface {} with []string"))
_, err = DeepMerge(b, a)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(Equal("cannot merge []string with map[string]interface {}"))
})
})
Context("simple slices", func() {
a := []interface{}{"one", "three"}
b := []interface{}{"two", 4}
It("merges", func() {
c, err := DeepMerge(a, b)
Expect(err).ToNot(HaveOccurred())
Expect(c).To(Equal([]interface{}{"one", "three", "two", 4}))
})
})
Context("slices containing maps", func() {
a := []interface{}{
map[string]interface{}{
"users": []interface{}{
map[string]interface{}{
"kairos": map[string]interface{}{
"passwd": "kairos",
},
},
},
},
}
b := []interface{}{
map[string]interface{}{
"users": []interface{}{
map[string]interface{}{
"foo": map[string]interface{}{
"passwd": "bar",
},
},
},
},
}
It("merges", func() {
c, err := DeepMerge(a, b)
Expect(err).ToNot(HaveOccurred())
users := c.([]interface{})[0].(map[string]interface{})["users"]
Expect(users).To(HaveLen(1))
Expect(users).To(Equal([]interface{}{
map[string]interface{}{
"kairos": map[string]interface{}{
"passwd": "kairos",
},
"foo": map[string]interface{}{
"passwd": "bar",
},
},
}))
})
})
Context("empty map", func() {
a := map[string]interface{}{}
b := map[string]interface{}{
"foo": "bar",
}
It("merges", func() {
c, err := DeepMerge(a, b)
Expect(err).ToNot(HaveOccurred())
Expect(c).To(Equal(map[string]interface{}{
"foo": "bar",
}))
})
})
Context("simple map", func() {
a := map[string]interface{}{
"es": "uno",
"nl": "een",
"#": 0,
}
b := map[string]interface{}{
"en": "one",
"nl": "één",
"de": "Eins",
"#": 1,
}
It("merges", func() {
c, err := DeepMerge(a, b)
Expect(err).ToNot(HaveOccurred())
Expect(c).To(Equal(map[string]interface{}{
"#": 1,
"de": "Eins",
"en": "one",
"es": "uno",
"nl": "één",
}))
})
})
})
Describe("Scan", func() {
Context("duplicated configs", func() {
var cmdLinePath, tmpDir1 string
var err error
BeforeEach(func() {
tmpDir1, err = os.MkdirTemp("", "config1")
Expect(err).ToNot(HaveOccurred())
err := os.WriteFile(path.Join(tmpDir1, "local_config_1.yaml"), []byte(`#cloud-config
stages:
initramfs:
- name: "Set user and password"
users:
kairos:
passwd: "kairos"
hostname: kairos-{{ trunc 4 .Random }}
install:
auto: true
reboot: true
device: auto
grub_options:
extra_cmdline: foobarzz
bundles:
- rootfs_path: /usr/local/lib/extensions/kubo
targets:
- container://ttl.sh/97d4530c-df80-4eb4-9ae7-39f8f90c26e5:8h
`), os.ModePerm)
Expect(err).ToNot(HaveOccurred())
err = os.WriteFile(path.Join(tmpDir1, "local_config_2.yaml"), []byte(`#cloud-config
stages:
initramfs:
- name: "Set user and password"
users:
kairos:
passwd: "kairos"
hostname: kairos-{{ trunc 4 .Random }}
install:
auto: true
reboot: true
device: auto
grub_options:
extra_cmdline: foobarzz
bundles:
- rootfs_path: /usr/local/lib/extensions/kubo
targets:
- container://ttl.sh/97d4530c-df80-4eb4-9ae7-39f8f90c26e5:8h
`), os.ModePerm)
Expect(err).ToNot(HaveOccurred())
})
AfterEach(func() {
err = os.RemoveAll(tmpDir1)
Expect(err).ToNot(HaveOccurred())
})
It("should be the same as just one of them", func() {
o := &Options{}
err := o.Apply(
MergeBootLine,
WithBootCMDLineFile(cmdLinePath),
Directories(tmpDir1),
)
Expect(err).ToNot(HaveOccurred())
c, err := Scan(o, FilterKeysTestMerge)
Expect(err).ToNot(HaveOccurred())
fmt.Println(c.String())
Expect(c.String()).To(Equal(`#cloud-config
install:
auto: true
bundles:
- rootfs_path: /usr/local/lib/extensions/kubo
targets:
- container://ttl.sh/97d4530c-df80-4eb4-9ae7-39f8f90c26e5:8h
device: auto
grub_options:
extra_cmdline: foobarzz
reboot: true
stages:
initramfs:
- hostname: kairos-{{ trunc 4 .Random }}
name: Set user and password
users:
kairos:
passwd: kairos
`))
})
})
Context("Deep merge maps within arrays", func() {
var cmdLinePath, tmpDir1 string
var err error
BeforeEach(func() {
tmpDir1, err = os.MkdirTemp("", "config1")
Expect(err).ToNot(HaveOccurred())
err := os.WriteFile(path.Join(tmpDir1, "local_config_1.yaml"), []byte(`#cloud-config
install:
auto: true
reboot: false
poweroff: false
grub_options:
extra_cmdline: "console=tty0"
options:
device: /dev/sda
stages:
initramfs:
- users:
kairos:
groups:
- sudo
passwd: kairos
`), os.ModePerm)
Expect(err).ToNot(HaveOccurred())
err = os.WriteFile(path.Join(tmpDir1, "local_config_2.yaml"), []byte(`#cloud-config
stages:
initramfs:
- users:
foo:
groups:
- sudo
passwd: bar
`), os.ModePerm)
Expect(err).ToNot(HaveOccurred())
})
AfterEach(func() {
err = os.RemoveAll(tmpDir1)
Expect(err).ToNot(HaveOccurred())
})
It("merges all the sources accordingly", func() {
o := &Options{}
err := o.Apply(
MergeBootLine,
WithBootCMDLineFile(cmdLinePath),
Directories(tmpDir1),
)
Expect(err).ToNot(HaveOccurred())
c, err := Scan(o, FilterKeysTestMerge)
Expect(err).ToNot(HaveOccurred())
Expect(c.String()).To(Equal(`#cloud-config
install:
auto: true
grub_options:
extra_cmdline: console=tty0
poweroff: false
reboot: false
options:
device: /dev/sda
stages:
initramfs:
- users:
foo:
groups:
- sudo
passwd: bar
kairos:
groups:
- sudo
passwd: kairos
`))
})
})
Context("multiple sources are defined", func() {
var cmdLinePath, serverDir, tmpDir, tmpDir1, tmpDir2 string
var err error
var closeFunc ServerCloseFunc
var port int
BeforeEach(func() {
// Prepare the cmdline config_url chain
serverDir, err = os.MkdirTemp("", "config_url_chain")
Expect(err).ToNot(HaveOccurred())
closeFunc, port, err = startAssetServer(serverDir)
Expect(err).ToNot(HaveOccurred())
cmdLinePath = createRemoteConfigs(serverDir, port)
tmpDir1, err = os.MkdirTemp("", "config1")
Expect(err).ToNot(HaveOccurred())
err := os.WriteFile(path.Join(tmpDir1, "local_config_1.yaml"), []byte(fmt.Sprintf(`#cloud-config
config_url: http://127.0.0.1:%d/remote_config_3.yaml
local_key_1: local_value_1
`, port)), os.ModePerm)
Expect(err).ToNot(HaveOccurred())
err = os.WriteFile(path.Join(serverDir, "remote_config_3.yaml"), []byte(fmt.Sprintf(`#cloud-config
config_url: http://127.0.0.1:%d/remote_config_4.yaml
remote_key_3: remote_value_3
`, port)), os.ModePerm)
Expect(err).ToNot(HaveOccurred())
err = os.WriteFile(path.Join(serverDir, "remote_config_4.yaml"), []byte(`#cloud-config
options:
remote_option_1: remote_option_value_1
`), os.ModePerm)
Expect(err).ToNot(HaveOccurred())
tmpDir2, err = os.MkdirTemp("", "config2")
Expect(err).ToNot(HaveOccurred())
err = os.WriteFile(path.Join(tmpDir2, "local_config_2.yaml"), []byte(fmt.Sprintf(`#cloud-config
config_url: http://127.0.0.1:%d/remote_config_5.yaml
local_key_2: local_value_2
`, port)), os.ModePerm)
Expect(err).ToNot(HaveOccurred())
err = os.WriteFile(path.Join(tmpDir2, "local_config_3.yaml"), []byte(`#cloud-config
local_key_3: local_value_3
`), os.ModePerm)
Expect(err).ToNot(HaveOccurred())
err = os.WriteFile(path.Join(serverDir, "remote_config_5.yaml"), []byte(fmt.Sprintf(`#cloud-config
config_url: http://127.0.0.1:%d/remote_config_6.yaml
remote_key_4: remote_value_4
`, port)), os.ModePerm)
Expect(err).ToNot(HaveOccurred())
err = os.WriteFile(path.Join(serverDir, "remote_config_6.yaml"), []byte(`#cloud-config
options:
remote_option_2: remote_option_value_2
`), os.ModePerm)
Expect(err).ToNot(HaveOccurred())
})
AfterEach(func() {
err = os.RemoveAll(serverDir)
Expect(err).ToNot(HaveOccurred())
err = os.RemoveAll(tmpDir)
Expect(err).ToNot(HaveOccurred())
err = os.RemoveAll(tmpDir1)
Expect(err).ToNot(HaveOccurred())
err = os.RemoveAll(tmpDir2)
Expect(err).ToNot(HaveOccurred())
closeFunc()
})
It("merges all the sources accordingly", func() {
o := &Options{}
err := o.Apply(
MergeBootLine,
WithBootCMDLineFile(cmdLinePath),
Directories(tmpDir1, tmpDir2),
)
Expect(err).ToNot(HaveOccurred())
c, err := Scan(o, FilterKeysTestMerge)
Expect(err).ToNot(HaveOccurred())
configURL, ok := (*c)["config_url"].(string)
Expect(ok).To(BeTrue())
Expect(configURL).To(MatchRegexp("remote_config_2.yaml"))
k := (*c)["local_key_1"].(string)
Expect(k).To(Equal("local_value_1"))
k = (*c)["local_key_2"].(string)
Expect(k).To(Equal("local_value_2"))
k = (*c)["local_key_3"].(string)
Expect(k).To(Equal("local_value_3"))
k = (*c)["remote_key_1"].(string)
Expect(k).To(Equal("remote_value_1"))
k = (*c)["remote_key_2"].(string)
Expect(k).To(Equal("remote_value_2"))
k = (*c)["remote_key_3"].(string)
Expect(k).To(Equal("remote_value_3"))
k = (*c)["remote_key_4"].(string)
Expect(k).To(Equal("remote_value_4"))
options := (*c)["options"].(Config)
Expect(options["foo"]).To(Equal("bar"))
Expect(options["remote_option_1"]).To(Equal("remote_option_value_1"))
Expect(options["remote_option_2"]).To(Equal("remote_option_value_2"))
player := (*c)["player"].(Config)
fmt.Print(player)
Expect(player["name"]).NotTo(Equal("Toad"))
Expect(player["surname"]).To(Equal("Bros"))
})
})
Context("when files have invalid or missing headers", func() {
var serverDir, tmpDir string
var err error
var closeFunc ServerCloseFunc
var port int
BeforeEach(func() {
// Prepare the cmdline config_url chain
serverDir, err = os.MkdirTemp("", "config_url_chain")
Expect(err).ToNot(HaveOccurred())
closeFunc, port, err = startAssetServer(serverDir)
Expect(err).ToNot(HaveOccurred())
tmpDir, err = os.MkdirTemp("", "config")
Expect(err).ToNot(HaveOccurred())
// Local configs
err = os.WriteFile(path.Join(tmpDir, "local_config.yaml"), []byte(fmt.Sprintf(`#cloud-config
config_url: http://127.0.0.1:%d/remote_config_1.yaml
local_key_1: local_value_1
`, port)), os.ModePerm)
Expect(err).ToNot(HaveOccurred())
// missing header
err = os.WriteFile(path.Join(tmpDir, "local_config_2.yaml"),
[]byte("local_key_2: local_value_2"), os.ModePerm)
Expect(err).ToNot(HaveOccurred())
// Remote config with valid header
err := os.WriteFile(path.Join(serverDir, "remote_config_1.yaml"), []byte(fmt.Sprintf(`#cloud-config
config_url: http://127.0.0.1:%d/remote_config_2.yaml
remote_key_1: remote_value_1`, port)), os.ModePerm)
Expect(err).ToNot(HaveOccurred())
// Remote config with invalid header
err = os.WriteFile(path.Join(serverDir, "remote_config_2.yaml"), []byte(`#invalid-header
remote_key_2: remote_value_2`), os.ModePerm)
Expect(err).ToNot(HaveOccurred())
})
AfterEach(func() {
closeFunc()
err = os.RemoveAll(serverDir)
Expect(err).ToNot(HaveOccurred())
err = os.RemoveAll(tmpDir)
})
It("ignores them", func() {
o := &Options{}
err := o.Apply(Directories(tmpDir), NoLogs)
Expect(err).ToNot(HaveOccurred())
c, err := Scan(o, FilterKeysTest)
Expect(err).ToNot(HaveOccurred())
Expect((*c)["local_key_2"]).To(BeNil())
Expect((*c)["remote_key_2"]).To(BeNil())
// sanity check, the rest should be there
v, ok := (*c)["config_url"].(string)
Expect(ok).To(BeTrue())
Expect(v).To(MatchRegexp("remote_config_2.yaml"))
v, ok = (*c)["local_key_1"].(string)
Expect(ok).To(BeTrue())
Expect(v).To(Equal("local_value_1"))
v, ok = (*c)["remote_key_1"].(string)
Expect(ok).To(BeTrue())
Expect(v).To(Equal("remote_value_1"))
})
})
})
Describe("String", func() {
var conf *Config
BeforeEach(func() {
conf = &Config{}
err := yaml.Unmarshal([]byte("name: Mario"), conf)
Expect(err).ToNot(HaveOccurred())
})
It("returns the YAML string representation of the Config", func() {
s, err := conf.String()
Expect(err).ToNot(HaveOccurred())
Expect(s).To(Equal(`#cloud-config
name: Mario
`), s)
})
})
Describe("Query", func() {
var tmpDir string
var err error
BeforeEach(func() {
tmpDir, err = os.MkdirTemp("", "config")
Expect(err).ToNot(HaveOccurred())
err = os.WriteFile(filepath.Join(tmpDir, "b"), []byte(`zz.foo="baa" options.foo=bar`), os.ModePerm)
Expect(err).ToNot(HaveOccurred())
err = os.WriteFile(path.Join(tmpDir, "local_config.yaml"), []byte(`#cloud-config
local_key_1: local_value_1
some:
other:
key: 3
`), os.ModePerm)
Expect(err).ToNot(HaveOccurred())
})
It("can query for keys", func() {
o := &Options{}
err = o.Apply(MergeBootLine, Directories(tmpDir),
WithBootCMDLineFile(filepath.Join(tmpDir, "b")),
)
Expect(err).ToNot(HaveOccurred())
c, err := Scan(o, FilterKeysTest)
Expect(err).ToNot(HaveOccurred())
v, err := c.Query("local_key_1")
Expect(err).ToNot(HaveOccurred())
Expect(v).To(Equal("local_value_1\n"))
v, err = c.Query("some")
Expect(err).ToNot(HaveOccurred())
Expect(v).To(Equal("other:\n key: 3\n"))
v, err = c.Query("some.other")
Expect(err).ToNot(HaveOccurred())
Expect(v).To(Equal("key: 3\n"))
v, err = c.Query("some.other.key")
Expect(err).ToNot(HaveOccurred())
Expect(v).To(Equal("3\n"))
Expect(c.Query("options")).To(Equal("foo: bar\n"))
})
})
})
func createRemoteConfigs(serverDir string, port int) string {
err := os.WriteFile(path.Join(serverDir, "remote_config_1.yaml"), []byte(fmt.Sprintf(`#cloud-config
config_url: http://127.0.0.1:%d/remote_config_2.yaml
player:
remote_key_1: remote_value_1
`, port)), os.ModePerm)
Expect(err).ToNot(HaveOccurred())
err = os.WriteFile(path.Join(serverDir, "remote_config_2.yaml"), []byte(`#cloud-config
player:
surname: Bros
remote_key_2: remote_value_2
`), os.ModePerm)
Expect(err).ToNot(HaveOccurred())
cmdLinePath := filepath.Join(serverDir, "cmdline")
// We put the cmdline in the same dir, it doesn't matter.
cmdLine := fmt.Sprintf(`config_url="http://127.0.0.1:%d/remote_config_1.yaml" player.name="Toad" options.foo=bar`, port)
err = os.WriteFile(cmdLinePath, []byte(cmdLine), os.ModePerm)
Expect(err).ToNot(HaveOccurred())
return cmdLinePath
}
// Generic config type with no fields, accepts everything for filtering
type TestCfgGeneric map[string]interface{}
func FilterKeysTest(d []byte) ([]byte, error) {
cmdLineFilter := TestCfgGeneric{}
err := yaml.Unmarshal(d, &cmdLineFilter)
if err != nil {
return []byte{}, err
}
out, err := yaml.Marshal(cmdLineFilter)
if err != nil {
return []byte{}, err
}
return out, nil
}
// Focused config with explicit fields, anything not here will be dropped by filterkeys
type TestCfgFields struct {
ConfigURL string `yaml:"config_url,omitempty"`
Options map[string]string `yaml:"options,omitempty"`
}
func FilterKeysTestMerge(d []byte) ([]byte, error) {
cmdLineFilter := TestCfgFields{}
err := yaml.Unmarshal(d, &cmdLineFilter)
if err != nil {
return []byte{}, err
}
out, err := yaml.Marshal(cmdLineFilter)
if err != nil {
return []byte{}, err
}
return out, nil
}