mirror of
https://github.com/kairos-io/kairos-agent.git
synced 2025-04-28 03:32:27 +00:00
306 lines
8.6 KiB
Go
306 lines
8.6 KiB
Go
// Copyright © 2022 Ettore Di Giacinto <mudler@c3os.io>
|
|
//
|
|
// This program is free software; you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation; either version 2 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License along
|
|
// with this program; if not, see <http://www.gnu.org/licenses/>.
|
|
|
|
package config_test
|
|
|
|
import (
|
|
"fmt"
|
|
sdkTypes "github.com/kairos-io/kairos-sdk/types"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
|
|
pkgConfig "github.com/kairos-io/kairos-agent/v2/pkg/config"
|
|
"github.com/kairos-io/kairos-agent/v2/pkg/constants"
|
|
v1 "github.com/kairos-io/kairos-agent/v2/pkg/types/v1"
|
|
fsutils "github.com/kairos-io/kairos-agent/v2/pkg/utils/fs"
|
|
v1mocks "github.com/kairos-io/kairos-agent/v2/tests/mocks"
|
|
"github.com/twpayne/go-vfs/v4"
|
|
"github.com/twpayne/go-vfs/v4/vfst"
|
|
"gopkg.in/yaml.v3"
|
|
|
|
. "github.com/kairos-io/kairos-agent/v2/pkg/config"
|
|
"github.com/kairos-io/kairos-sdk/collector"
|
|
. "github.com/kairos-io/kairos-sdk/schema"
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
)
|
|
|
|
func getTagName(s string) string {
|
|
if len(s) < 1 {
|
|
return ""
|
|
}
|
|
|
|
if s == "-" {
|
|
return ""
|
|
}
|
|
|
|
f := func(c rune) bool {
|
|
return c == '"' || c == ','
|
|
}
|
|
index := strings.IndexFunc(s, f)
|
|
if index == -1 {
|
|
return s
|
|
}
|
|
|
|
return s[:index]
|
|
}
|
|
|
|
func structContainsField(f, t string, str interface{}) bool {
|
|
values := reflect.ValueOf(str)
|
|
types := values.Type()
|
|
|
|
for j := 0; j < values.NumField(); j++ {
|
|
tagName := getTagName(types.Field(j).Tag.Get("json"))
|
|
if types.Field(j).Name == f || tagName == t {
|
|
return true
|
|
} else {
|
|
if types.Field(j).Type.Kind() == reflect.Struct {
|
|
if types.Field(j).Type.Name() != "" {
|
|
model := reflect.New(types.Field(j).Type)
|
|
if instance, ok := model.Interface().(OneOfModel); ok {
|
|
for _, childSchema := range instance.JSONSchemaOneOf() {
|
|
if structContainsField(f, t, childSchema) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func structFieldsContainedInOtherStruct(left, right interface{}) {
|
|
leftValues := reflect.ValueOf(left)
|
|
leftTypes := leftValues.Type()
|
|
|
|
for i := 0; i < leftValues.NumField(); i++ {
|
|
leftTagName := getTagName(leftTypes.Field(i).Tag.Get("yaml"))
|
|
leftFieldName := leftTypes.Field(i).Name
|
|
if leftTypes.Field(i).IsExported() {
|
|
It(fmt.Sprintf("Checks that the new schema contians the field %s", leftFieldName), func() {
|
|
if leftFieldName == "Source" || leftFieldName == "NoUsers" {
|
|
Skip("Schema not updated yet")
|
|
}
|
|
Expect(
|
|
structContainsField(leftFieldName, leftTagName, right),
|
|
).To(BeTrue())
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
var _ = Describe("Schema", func() {
|
|
Context("NewConfigFromYAML", func() {
|
|
Context("While the new Schema is not the single source of truth", func() {
|
|
structFieldsContainedInOtherStruct(Config{}, RootSchema{})
|
|
})
|
|
Context("While the new InstallSchema is not the single source of truth", func() {
|
|
structFieldsContainedInOtherStruct(Install{}, InstallSchema{})
|
|
})
|
|
Context("While the new BundleSchema is not the single source of truth", func() {
|
|
structFieldsContainedInOtherStruct(Bundle{}, BundleSchema{})
|
|
})
|
|
})
|
|
|
|
Describe("Install unmarshall for payloads", func() {
|
|
It("produces a yaml without empty fields", func() {
|
|
wants := `install:
|
|
poweroff: true
|
|
bind_mounts:
|
|
- /var/lib/ceph
|
|
- /var/lib/osd
|
|
partitions:
|
|
oem:
|
|
size: 5120
|
|
fs: ext4
|
|
system:
|
|
size: 8192
|
|
recovery-system:
|
|
size: 10000
|
|
passive:
|
|
size: 8192
|
|
`
|
|
config := Config{
|
|
Install: &Install{
|
|
Poweroff: true,
|
|
BindMounts: []string{
|
|
"/var/lib/ceph",
|
|
"/var/lib/osd",
|
|
},
|
|
Active: v1.Image{
|
|
Size: 8192,
|
|
},
|
|
Passive: v1.Image{
|
|
Size: 8192,
|
|
},
|
|
Recovery: v1.Image{
|
|
Size: 10000,
|
|
},
|
|
Partitions: v1.ElementalPartitions{
|
|
OEM: &sdkTypes.Partition{
|
|
Size: 5120,
|
|
FS: "ext4",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
got, err := yaml.Marshal(config)
|
|
Expect(Expect(err).NotTo(HaveOccurred()))
|
|
|
|
Expect(string(got)).To(Equal(wants))
|
|
})
|
|
})
|
|
|
|
Describe("Write and load installation state", func() {
|
|
var config *Config
|
|
var runner *v1mocks.FakeRunner
|
|
var fs vfs.FS
|
|
var mounter *v1mocks.ErrorMounter
|
|
var cleanup func()
|
|
var err error
|
|
var dockerState, channelState *v1.ImageState
|
|
var installState *v1.InstallState
|
|
var statePath, recoveryPath string
|
|
|
|
BeforeEach(func() {
|
|
runner = v1mocks.NewFakeRunner()
|
|
mounter = v1mocks.NewErrorMounter()
|
|
fs, cleanup, err = vfst.NewTestFS(map[string]interface{}{})
|
|
Expect(err).Should(BeNil())
|
|
|
|
config = NewConfig(
|
|
WithFs(fs),
|
|
WithRunner(runner),
|
|
WithMounter(mounter),
|
|
)
|
|
dockerState = &v1.ImageState{
|
|
Source: v1.NewDockerSrc("registry.org/my/image:tag"),
|
|
Label: "active_label",
|
|
FS: "ext2",
|
|
SourceMetadata: &v1.DockerImageMeta{
|
|
Digest: "adadgadg",
|
|
Size: 23452345,
|
|
},
|
|
}
|
|
installState = &v1.InstallState{
|
|
Date: "somedate",
|
|
Partitions: map[string]*v1.PartitionState{
|
|
"state": {
|
|
FSLabel: "state_label",
|
|
Images: map[string]*v1.ImageState{
|
|
"active": dockerState,
|
|
},
|
|
},
|
|
"recovery": {
|
|
FSLabel: "state_label",
|
|
Images: map[string]*v1.ImageState{
|
|
"recovery": channelState,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
statePath = filepath.Join(constants.RunningStateDir, constants.InstallStateFile)
|
|
recoveryPath = "/recoverypart/state.yaml"
|
|
err = fsutils.MkdirAll(fs, filepath.Dir(recoveryPath), constants.DirPerm)
|
|
Expect(err).ShouldNot(HaveOccurred())
|
|
err = fsutils.MkdirAll(fs, filepath.Dir(statePath), constants.DirPerm)
|
|
Expect(err).ShouldNot(HaveOccurred())
|
|
})
|
|
AfterEach(func() {
|
|
cleanup()
|
|
})
|
|
It("Scan can override options", func() {
|
|
c, err := Scan(collector.Readers(strings.NewReader(`uki-max-entries: 34`)), collector.NoLogs)
|
|
Expect(err).ShouldNot(HaveOccurred())
|
|
Expect(c.UkiMaxEntries).To(Equal(34))
|
|
})
|
|
It("Writes and loads an installation data", func() {
|
|
err = config.WriteInstallState(installState, statePath, recoveryPath)
|
|
Expect(err).ShouldNot(HaveOccurred())
|
|
loadedInstallState, err := config.LoadInstallState()
|
|
Expect(err).ShouldNot(HaveOccurred())
|
|
stat, err := fs.Stat(statePath)
|
|
Expect(err).To(BeNil())
|
|
Expect(int(stat.Mode().Perm())).To(Equal(constants.ConfigPerm))
|
|
stat, err = fs.Stat(recoveryPath)
|
|
Expect(err).To(BeNil())
|
|
Expect(int(stat.Mode().Perm())).To(Equal(constants.ConfigPerm))
|
|
|
|
Expect(*loadedInstallState).To(Equal(*installState))
|
|
})
|
|
It("Fails writing to state partition", func() {
|
|
err = fs.RemoveAll(filepath.Dir(statePath))
|
|
Expect(err).ShouldNot(HaveOccurred())
|
|
err = config.WriteInstallState(installState, statePath, recoveryPath)
|
|
Expect(err).Should(HaveOccurred())
|
|
})
|
|
It("Fails writing to recovery partition", func() {
|
|
err = fs.RemoveAll(filepath.Dir(statePath))
|
|
Expect(err).ShouldNot(HaveOccurred())
|
|
err = config.WriteInstallState(installState, statePath, recoveryPath)
|
|
Expect(err).Should(HaveOccurred())
|
|
})
|
|
It("Fails loading state file", func() {
|
|
err = config.WriteInstallState(installState, statePath, recoveryPath)
|
|
Expect(err).ShouldNot(HaveOccurred())
|
|
err = fs.RemoveAll(filepath.Dir(statePath))
|
|
_, err = config.LoadInstallState()
|
|
Expect(err).Should(HaveOccurred())
|
|
})
|
|
})
|
|
|
|
Describe("Validate users in config", func() {
|
|
It("Validates a existing user in the system", func() {
|
|
cc := `#cloud-config
|
|
stages:
|
|
initramfs:
|
|
- name: "Set user and password"
|
|
users:
|
|
kairos:
|
|
passwd: "kairos"
|
|
groups:
|
|
- "admin"
|
|
`
|
|
config, err := pkgConfig.Scan(collector.Readers(strings.NewReader(cc)), collector.NoLogs)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(config.CheckForUsers()).ToNot(HaveOccurred())
|
|
})
|
|
It("Fails if there is no user", func() {
|
|
config, err := pkgConfig.Scan()
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(config.CheckForUsers()).To(HaveOccurred())
|
|
})
|
|
It("Fails if there is user but its not admin", func() {
|
|
cc := `#cloud-config
|
|
stages:
|
|
initramfs:
|
|
- name: "Set user and password"
|
|
users:
|
|
kairos:
|
|
passwd: "kairos"
|
|
`
|
|
config, err := pkgConfig.Scan(collector.Readers(strings.NewReader(cc)), collector.NoLogs)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(config.CheckForUsers()).To(HaveOccurred())
|
|
})
|
|
})
|
|
})
|