func/pkg/config/config.go

348 lines
11 KiB
Go

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)
}