package config import ( "fmt" "os" "path/filepath" "reflect" "sort" "strconv" "strings" "gopkg.in/yaml.v2" "knative.dev/func/pkg/builders" fn "knative.dev/func/pkg/functions" "knative.dev/func/pkg/k8s" ) const ( // Filename into which Config is serialized Filename = "config.yaml" // Repositories is the default directory for repositoires. Repositories = "repositories" // DefaultLanguage is intentionaly undefined. DefaultLanguage = "" // DefaultBuilder is statically defined by the builders package. DefaultBuilder = builders.Default ) // Global configuration settings. type Global struct { Builder string `yaml:"builder,omitempty"` Confirm bool `yaml:"confirm,omitempty"` Language string `yaml:"language,omitempty"` Namespace string `yaml:"namespace,omitempty"` Registry string `yaml:"registry,omitempty"` Verbose bool `yaml:"verbose,omitempty"` // NOTE: all members must include their yaml serialized names, even when // this is the default, because these tag values are used for the static // getter/setter accessors to match requests. RegistryInsecure bool `yaml:"registryInsecure,omitempty"` } // New Config struct with all members set to static defaults. See NewDefaults // for one which further takes into account the optional config file. func New() Global { return Global{ Builder: DefaultBuilder, Language: DefaultLanguage, // ... } } // RegistyDefault is a convenience method for deferred calculation of a // default registry taking into account both the global config file and cluster // detection. func (c Global) RegistryDefault() string { // If defined, the user's choice for global registry default value is used if c.Registry != "" { return c.Registry } switch { case k8s.IsOpenShift(): return k8s.GetDefaultOpenShiftRegistry() default: return "" } } // NewDefault returns a config populated by global defaults as defined by the // config file located in .Path() (the global func settings path, which is // // usually ~/.config/func). // // The config path is not required to be present. func NewDefault() (cfg Global, err error) { cfg = New() cp := File() bb, err := os.ReadFile(cp) if err != nil { // config file is not required // permissions warning printed in cmd/root.go as to not spam here // TODO: gauron99 - review the whole process for simplification if os.IsNotExist(err) || os.IsPermission(err) { err = nil } return } err = yaml.Unmarshal(bb, &cfg) // cfg now has applied config.yaml return } // Load the config exactly as it exists at path (no static defaults) func Load(path string) (c Global, err error) { bb, err := os.ReadFile(path) if err != nil { return c, fmt.Errorf("error reading global config: %v", err) } err = yaml.Unmarshal(bb, &c) return } // Write the config to the given path // To use the currently configured path (used by the constructor) use File() // // c := config.NewDefault() // c.Verbose = true // c.Write(config.File()) func (c Global) Write(path string) (err error) { bb, _ := yaml.Marshal(&c) // Marshaling no longer errors; this is back compat return os.WriteFile(path, bb, os.ModePerm) } // Apply populated values from a function to the config. // The resulting config is global settings overridden by a given function. func (c Global) Apply(f fn.Function) Global { // With no way to automate this mapping easily (even with reflection) because // the function now has a complex structure consiting of XSpec sub-structs, // and in some cases has differing member names (language). While this is // yes a bit tedious, manually mapping each member (if defined) is simple, // easy to understand and support; with both mapping direction (Apply and // Configure) in one central place here... with tests. if f.Build.Builder != "" { c.Builder = f.Build.Builder } if f.Runtime != "" { c.Language = f.Runtime } // Namespace resolution is handled manually in the CLI due to needing // to consider current kubernetes context. This may be merged back // in here once the logic is refined. // if f.Deploy.Namespace != "" { // c.Namespace = f.Deploy.Namespace // } if f.Registry != "" { c.Registry = f.Registry } return c } // Configure a function with populated values of the config. // The resulting function is the function overridden by values on config. func (c Global) Configure(f fn.Function) fn.Function { if c.Builder != "" { f.Build.Builder = c.Builder } if c.Language != "" { f.Runtime = c.Language } if c.Namespace != "" { f.Deploy.Namespace = c.Namespace } if c.Registry != "" { f.Registry = c.Registry } return f } // Dir is derived in the following order, from lowest // to highest precedence. // 1. The default path is the zero value, indicating "no config path available", // and users of this package should act accordingly. // 2. ~/.config/func if it exists (can be expanded: user has a home dir) // 3. The value of $XDG_CONFIG_PATH/func if the environment variable exists. // // The path is created if it does not already exist. func Dir() (path string) { // Use home if available if home, err := os.UserHomeDir(); err == nil { path = filepath.Join(home, ".config", "func") } // 'XDG_CONFIG_HOME/func' takes precedence if defined if xdg := os.Getenv("XDG_CONFIG_HOME"); xdg != "" { path = filepath.Join(xdg, "func") } return } // File returns the full path at which to look for a config file. // Use FUNC_CONFIG_FILE to override default. func File() string { path := filepath.Join(Dir(), Filename) if e := os.Getenv("FUNC_CONFIG_FILE"); e != "" { path = e } return path } // RepositoriesPath returns the full path at which to look for repositories. // Use FUNC_REPOSITORIES_PATH to override default. func RepositoriesPath() string { path := filepath.Join(Dir(), Repositories) if e := os.Getenv("FUNC_REPOSITORIES_PATH"); e != "" { path = e } return path } // CreatePaths is a convenience function for creating the on-disk func config // structure. All operations should be tolerant of nonexistant disk // footprint where possible (for example listing repositories should not // require an extant path, but _adding_ a repository does require that the func // config structure exist. // Current structure is: // ~/.config/func // ~/.config/func/repositories func CreatePaths() (err error) { if err = os.MkdirAll(Dir(), os.ModePerm); err != nil { return fmt.Errorf("error creating global config path: %v", err) } if err = os.MkdirAll(RepositoriesPath(), os.ModePerm); err != nil { return fmt.Errorf("error creating global config repositories path: %v", err) } return } // Static Accessors // // Accessors to globally configurable options are implemented as static // package functions to retain the benefits of pass-by-value already in use // on most system structures. // c = config.Set(c, "key", "value") // // This may initially seem confusing to those used to accessors implemented // as object member functions (`c.Set("x","y"`), but it appears to be worth it: // // This method follows the familiar paradigm of other Go builtins (which made // their choice for similar reasons): // args = append(args, "newarg") // ... and thus likewise: // c = config.Set(c, "key", "value") // // We indeed could have implemented these in a more familiar Getter/Setter way // by making this method have a pointer receiver: // func (c *Global) Set(key, value string) // However, this would require the user of the config object to, likely more // confusingly, declare a new local variable to hold their pointer // (or perhaps worse, abandon the benefits of pass-by-value from the config // constructor and always return a pointer from the constructor): // // globalConfig := config.NewDefault() // .... later that day ... // c := &globalConfig // c.Set("builder", "foo") // // This solution, while it would preserve the very narrow familiar usage of // 'Set', fails to be clear due to the setup involved (requiring the allocation // of that pointer). Therefore the accessors are herein implemented more // functionally, as package static methods. // List the globally configurable settings by the key which can be used // in the accessors Get and Set, and in the associated disk serialized. // Sorted. // Implemented as a package-static function because Set is implemented as such. // See the long-winded explanation above. func List() []string { keys := []string{} t := reflect.TypeOf(Global{}) for i := 0; i < t.NumField(); i++ { tt := strings.Split(t.Field(i).Tag.Get("yaml"), ",") keys = append(keys, tt[0]) } sort.Strings(keys) return keys } // Get the named global config value from the given global config struct. // Nonexistent values return nil. // Implemented as a package-static function because Set is implemented as such. // See the long-winded explanation above. func Get(c Global, name string) any { t := reflect.TypeOf(c) for i := 0; i < t.NumField(); i++ { if !strings.HasPrefix(t.Field(i).Tag.Get("yaml"), name) { continue } return reflect.ValueOf(c).FieldByName(t.Field(i).Name).Interface() } return nil } // Set value of a member by name and a stringified value. // Fails if the passed value can not be coerced into the value expected // by the member indicated by name. func Set(c Global, name, value string) (Global, error) { fieldValue, err := getField(&c, name) if err != nil { return c, err } var v reflect.Value switch fieldValue.Kind() { case reflect.String: v = reflect.ValueOf(value) case reflect.Bool: boolValue, err := strconv.ParseBool(value) if err != nil { return c, err } v = reflect.ValueOf(boolValue) default: return c, fmt.Errorf("global config value type not yet implemented: %v", fieldValue.Kind()) } fieldValue.Set(v) return c, nil } // SetString value of a member by name, returning the updated config. func SetString(c Global, name, value string) (Global, error) { return set(c, name, reflect.ValueOf(value)) } // SetBool value of a member by name, returning the updated config. func SetBool(c Global, name string, value bool) (Global, error) { return set(c, name, reflect.ValueOf(value)) } // TODO: add more typesafe setters as needed. // set using a reflect.Value func set(c Global, name string, value reflect.Value) (Global, error) { fieldValue, err := getField(&c, name) if err != nil { return c, err } fieldValue.Set(value) return c, nil } // Get an assignable reflect.Value for the struct field with the given yaml // tag name. func getField(c *Global, name string) (reflect.Value, error) { t := reflect.TypeOf(c).Elem() for i := 0; i < t.NumField(); i++ { if strings.HasPrefix(t.Field(i).Tag.Get("yaml"), name) { fieldValue := reflect.ValueOf(c).Elem().FieldByName(t.Field(i).Name) return fieldValue, nil } } return reflect.Value{}, fmt.Errorf("field not found on global config: %v", name) }