304 lines
7.6 KiB
Go
304 lines
7.6 KiB
Go
package config
|
|
|
|
import (
|
|
"github.com/kairos-io/enki/internal/version"
|
|
"github.com/kairos-io/enki/pkg/constants"
|
|
"github.com/kairos-io/enki/pkg/utils"
|
|
"github.com/kairos-io/kairos-agent/v2/pkg/cloudinit"
|
|
"github.com/kairos-io/kairos-agent/v2/pkg/http"
|
|
v1 "github.com/kairos-io/kairos-agent/v2/pkg/types/v1"
|
|
"github.com/mitchellh/mapstructure"
|
|
"github.com/sanity-io/litter"
|
|
"github.com/sirupsen/logrus"
|
|
"github.com/spf13/pflag"
|
|
"github.com/spf13/viper"
|
|
"github.com/twpayne/go-vfs"
|
|
"io"
|
|
"io/fs"
|
|
"k8s.io/mount-utils"
|
|
"os"
|
|
"reflect"
|
|
"runtime"
|
|
)
|
|
|
|
var decodeHook = viper.DecodeHook(
|
|
mapstructure.ComposeDecodeHookFunc(
|
|
UnmarshalerHook(),
|
|
mapstructure.StringToTimeDurationHookFunc(),
|
|
mapstructure.StringToSliceHookFunc(","),
|
|
),
|
|
)
|
|
|
|
func WithFs(fs v1.FS) func(r *v1.Config) error {
|
|
return func(r *v1.Config) error {
|
|
r.Fs = fs
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func WithLogger(logger v1.Logger) func(r *v1.Config) error {
|
|
return func(r *v1.Config) error {
|
|
r.Logger = logger
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func WithSyscall(syscall v1.SyscallInterface) func(r *v1.Config) error {
|
|
return func(r *v1.Config) error {
|
|
r.Syscall = syscall
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func WithMounter(mounter mount.Interface) func(r *v1.Config) error {
|
|
return func(r *v1.Config) error {
|
|
r.Mounter = mounter
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func WithRunner(runner v1.Runner) func(r *v1.Config) error {
|
|
return func(r *v1.Config) error {
|
|
r.Runner = runner
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func WithClient(client v1.HTTPClient) func(r *v1.Config) error {
|
|
return func(r *v1.Config) error {
|
|
r.Client = client
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func WithCloudInitRunner(ci v1.CloudInitRunner) func(r *v1.Config) error {
|
|
return func(r *v1.Config) error {
|
|
r.CloudInitRunner = ci
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func WithArch(arch string) func(r *v1.Config) error {
|
|
return func(r *v1.Config) error {
|
|
r.Arch = arch
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func WithImageExtractor(extractor v1.ImageExtractor) func(r *v1.Config) error {
|
|
return func(r *v1.Config) error {
|
|
r.ImageExtractor = extractor
|
|
return nil
|
|
}
|
|
}
|
|
|
|
type GenericOptions func(a *v1.Config) error
|
|
|
|
func ReadConfigBuild(configDir string, flags *pflag.FlagSet, mounter mount.Interface) (*v1.BuildConfig, error) {
|
|
logger := v1.NewLogger()
|
|
if configDir == "" {
|
|
configDir = "."
|
|
}
|
|
|
|
cfg := NewBuildConfig(
|
|
WithLogger(logger),
|
|
WithMounter(mounter),
|
|
)
|
|
|
|
configLogger(cfg.Logger, cfg.Fs)
|
|
|
|
viper.AddConfigPath(configDir)
|
|
viper.SetConfigType("yaml")
|
|
viper.SetConfigName("manifest.yaml")
|
|
// If a config file is found, read it in.
|
|
_ = viper.MergeInConfig()
|
|
|
|
// Bind buildconfig flags
|
|
bindGivenFlags(viper.GetViper(), flags)
|
|
|
|
// unmarshal all the vars into the config object
|
|
err := viper.Unmarshal(cfg, setDecoder, decodeHook)
|
|
if err != nil {
|
|
cfg.Logger.Warnf("error unmarshalling config: %s", err)
|
|
}
|
|
|
|
err = cfg.Sanitize()
|
|
cfg.Logger.Debugf("Full config loaded: %s", litter.Sdump(cfg))
|
|
return cfg, err
|
|
}
|
|
|
|
func ReadBuildISO(b *v1.BuildConfig, flags *pflag.FlagSet) (*v1.LiveISO, error) {
|
|
iso := NewISO()
|
|
vp := viper.Sub("iso")
|
|
if vp == nil {
|
|
vp = viper.New()
|
|
}
|
|
// Bind build-iso cmd flags
|
|
bindGivenFlags(vp, flags)
|
|
|
|
err := vp.Unmarshal(iso, setDecoder, decodeHook)
|
|
if err != nil {
|
|
b.Logger.Warnf("error unmarshalling LiveISO: %s", err)
|
|
}
|
|
err = iso.Sanitize()
|
|
b.Logger.Debugf("Loaded LiveISO: %s", litter.Sdump(iso))
|
|
return iso, err
|
|
}
|
|
|
|
func NewISO() *v1.LiveISO {
|
|
return &v1.LiveISO{
|
|
Label: constants.ISOLabel,
|
|
GrubEntry: constants.GrubDefEntry,
|
|
UEFI: []*v1.ImageSource{},
|
|
Image: []*v1.ImageSource{},
|
|
}
|
|
}
|
|
|
|
func NewBuildConfig(opts ...GenericOptions) *v1.BuildConfig {
|
|
b := &v1.BuildConfig{
|
|
Config: *NewConfig(opts...),
|
|
Name: constants.BuildImgName,
|
|
}
|
|
return b
|
|
}
|
|
|
|
func NewConfig(opts ...GenericOptions) *v1.Config {
|
|
log := v1.NewLogger()
|
|
arch, err := utils.GolangArchToArch(runtime.GOARCH)
|
|
if err != nil {
|
|
log.Errorf("invalid arch: %s", err.Error())
|
|
return nil
|
|
}
|
|
|
|
c := &v1.Config{
|
|
Fs: vfs.OSFS,
|
|
Logger: log,
|
|
Syscall: &v1.RealSyscall{},
|
|
Client: http.NewClient(),
|
|
Repos: []v1.Repository{},
|
|
Arch: arch,
|
|
SquashFsNoCompression: true,
|
|
}
|
|
for _, o := range opts {
|
|
err := o(c)
|
|
if err != nil {
|
|
log.Errorf("error applying config option: %s", err.Error())
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// delay runner creation after we have run over the options in case we use WithRunner
|
|
if c.Runner == nil {
|
|
c.Runner = &v1.RealRunner{Logger: c.Logger}
|
|
}
|
|
|
|
// Now check if the runner has a logger inside, otherwise point our logger into it
|
|
// This can happen if we set the WithRunner option as that doesn't set a logger
|
|
if c.Runner.GetLogger() == nil {
|
|
c.Runner.SetLogger(c.Logger)
|
|
}
|
|
|
|
// Delay the yip runner creation, so we set the proper logger instead of blindly setting it to the logger we create
|
|
// at the start of NewRunConfig, as WithLogger can be passed on init, and that would result in 2 different logger
|
|
// instances, on the config.Logger and the other on config.CloudInitRunner
|
|
if c.CloudInitRunner == nil {
|
|
c.CloudInitRunner = cloudinit.NewYipCloudInitRunner(c.Logger, c.Runner, vfs.OSFS)
|
|
}
|
|
|
|
if c.Mounter == nil {
|
|
c.Mounter = mount.New(constants.MountBinary)
|
|
}
|
|
|
|
return c
|
|
}
|
|
|
|
func configLogger(log v1.Logger, vfs v1.FS) {
|
|
// Set debug level
|
|
if viper.GetBool("debug") {
|
|
log.SetLevel(v1.DebugLevel())
|
|
}
|
|
|
|
// Set formatter so both file and stdout format are equal
|
|
log.SetFormatter(&logrus.TextFormatter{
|
|
ForceColors: true,
|
|
DisableColors: false,
|
|
DisableTimestamp: false,
|
|
FullTimestamp: true,
|
|
})
|
|
|
|
// Logfile
|
|
logfile := viper.GetString("logfile")
|
|
if logfile != "" {
|
|
o, err := vfs.OpenFile(logfile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, fs.ModePerm)
|
|
|
|
if err != nil {
|
|
log.Errorf("Could not open %s for logging to file: %s", logfile, err.Error())
|
|
}
|
|
|
|
// else set it to both stdout and the file
|
|
mw := io.MultiWriter(os.Stdout, o)
|
|
log.SetOutput(mw)
|
|
} else { // no logfile
|
|
if viper.GetBool("quiet") { // quiet is enabled so discard all logging
|
|
log.SetOutput(io.Discard)
|
|
} else { // default to stdout
|
|
log.SetOutput(os.Stdout)
|
|
}
|
|
}
|
|
|
|
log.Infof("Starting enki version %s", version.GetVersion())
|
|
if log.GetLevel() == logrus.DebugLevel {
|
|
log.Debugf("%+v\n", version.Get())
|
|
}
|
|
}
|
|
|
|
// BindGivenFlags binds to viper only passed flags, ignoring any non provided flag
|
|
func bindGivenFlags(vp *viper.Viper, flagSet *pflag.FlagSet) {
|
|
if flagSet != nil {
|
|
flagSet.VisitAll(func(f *pflag.Flag) {
|
|
if f.Changed {
|
|
_ = vp.BindPFlag(f.Name, f)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// setDecoder sets ZeroFields mastructure attribute to true
|
|
func setDecoder(config *mapstructure.DecoderConfig) {
|
|
// Make sure we zero fields before applying them, this is relevant for slices
|
|
// so we do not merge with any already present value and directly apply whatever
|
|
// we got form configs.
|
|
config.ZeroFields = true
|
|
}
|
|
|
|
type Unmarshaler interface {
|
|
CustomUnmarshal(interface{}) (bool, error)
|
|
}
|
|
|
|
func UnmarshalerHook() mapstructure.DecodeHookFunc {
|
|
return func(from reflect.Value, to reflect.Value) (interface{}, error) {
|
|
// get the destination object address if it is not passed by reference
|
|
if to.CanAddr() {
|
|
to = to.Addr()
|
|
}
|
|
// If the destination implements the unmarshaling interface
|
|
u, ok := to.Interface().(Unmarshaler)
|
|
if !ok {
|
|
return from.Interface(), nil
|
|
}
|
|
// If it is nil and a pointer, create and assign the target value first
|
|
if to.IsNil() && to.Type().Kind() == reflect.Ptr {
|
|
to.Set(reflect.New(to.Type().Elem()))
|
|
u = to.Interface().(Unmarshaler)
|
|
}
|
|
// Call the custom unmarshaling method
|
|
cont, err := u.CustomUnmarshal(from.Interface())
|
|
if cont {
|
|
// Continue with the decoding stack
|
|
return from.Interface(), err
|
|
}
|
|
// Decoding finalized
|
|
return to.Interface(), err
|
|
}
|
|
}
|