// Copyright © 2020 The Knative Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package config import ( "errors" "fmt" "os" "path/filepath" "runtime" "github.com/mitchellh/go-homedir" flag "github.com/spf13/pflag" "github.com/spf13/viper" ) // bootstrapDefaults are the defaults values to use type defaultConfig struct { configFile string pluginsDir string lookupPluginsInPath bool } // Initialize defaults var bootstrapDefaults = initDefaults() const configContentDefaults = `# Taken from https://github.com/knative/client/blob/main/docs/README.md#options # #plugins: # directory: ~/.config/kn/plugins #eventing: # sink-mappings: # - prefix: svc # group: core # version: v1 # resource: services # channel-type-mappings: # - alias: Kafka # group: messaging.knative.dev # version: v1alpha1 # kind: KafkaChannel ` // config contains the variables for the Kn config type config struct { // configFile is the config file location configFile string // sinkMappings is a list of sink mapping sinkMappings []SinkMapping // channelTypeMappings is a list of channel type mapping channelTypeMappings []ChannelTypeMapping // profiles is a map of profiles from the config file and built-in profiles profiles map[string]Profile } func (c *config) ContextSharing() bool { return viper.GetBool(keyFeaturesContextSharing) } // ConfigFile returns the config file which is either the default XDG conform // config file location or the one set with --config func (c *config) ConfigFile() string { if c.configFile != "" { return c.configFile } return bootstrapDefaults.configFile } // PluginsDir returns the plugins' directory func (c *config) PluginsDir() string { if viper.IsSet(keyPluginsDirectory) { return viper.GetString(keyPluginsDirectory) } else if viper.IsSet(legacyKeyPluginsDirectory) { // Remove that branch if legacy option is switched off return viper.GetString(legacyKeyPluginsDirectory) } else { return bootstrapDefaults.pluginsDir } } // LookupPluginsInPath returns true if plugins should be also checked in the pat func (c *config) LookupPluginsInPath() bool { return bootstrapDefaults.lookupPluginsInPath } func (c *config) SinkMappings() []SinkMapping { return c.sinkMappings } func (c *config) Profile(profile string) Profile { return c.profiles[profile] } func (c *config) ChannelTypeMappings() []ChannelTypeMapping { return c.channelTypeMappings } // Config used for flag binding var globalConfig = config{} // GlobalConfig is the global configuration available for every sub-command var GlobalConfig Config = &globalConfig // BootstrapConfig reads in config file and bootstrap options if set. func BootstrapConfig() error { // Create a new FlagSet for the bootstrap flags and parse those. This will // initialize the config file to use (obtained via GlobalConfig.ConfigFile()) bootstrapFlagSet := flag.NewFlagSet("kn", flag.ContinueOnError) AddBootstrapFlags(bootstrapFlagSet) bootstrapFlagSet.ParseErrorsWhitelist = flag.ParseErrorsWhitelist{UnknownFlags: true} // wokeignore:rule=whitelist // TODO(#1031) bootstrapFlagSet.Usage = func() {} err := bootstrapFlagSet.Parse(os.Args) if err != nil && !errors.Is(err, flag.ErrHelp) { return err } // Bind flags so that options that have been provided have priority. // Important: Always read options via GlobalConfig methods err = viper.BindPFlag(keyPluginsDirectory, bootstrapFlagSet.Lookup(flagPluginsDir)) if err != nil { return err } viper.SetConfigFile(GlobalConfig.ConfigFile()) configFile := GlobalConfig.ConfigFile() _, err = os.Lstat(configFile) if err != nil { if !os.IsNotExist(err) { return fmt.Errorf("cannot stat configfile %s: %w", configFile, err) } if err := os.MkdirAll(filepath.Dir(viper.ConfigFileUsed()), 0775); err != nil { // Can't create config directory, proceed silently without reading the config return nil } if err := os.WriteFile(viper.ConfigFileUsed(), []byte(configContentDefaults), 0600); err != nil { // Can't create config file, proceed silently without reading the config return nil } } viper.AutomaticEnv() // read in environment variables that match // Defaults are taken from the parsed flags, which in turn have bootstrap defaults // TODO: Re-enable when legacy handling for plugin config has been removed // For now default handling is happening directly in the getter of GlobalConfig // viper.SetDefault(keyPluginsDirectory, bootstrapDefaults.pluginsDir) // If a config file is found, read it in. err = viper.ReadInConfig() if err != nil { return err } // Deserialize sink mappings if configured err = parseSinkMappings() if err != nil { return err } // Deserialize profiles if configured err = parseProfiles() if err != nil { return err } // Deserialize channel type mappings if configured err = parseChannelTypeMappings() return err } // Add bootstrap flags use in a separate bootstrap proceeds func AddBootstrapFlags(flags *flag.FlagSet) { flags.StringVar(&globalConfig.configFile, "config", "", fmt.Sprintf("kn configuration file (default: %s)", defaultConfigFileForUsageMessage())) flags.String(flagPluginsDir, "", "Directory holding kn plugins") // Let's try that and mark the flags as hidden: (as those configuration is a permanent choice of operation) flags.MarkHidden(flagPluginsDir) } // =========================================================================================================== // Initialize defaults. This happens lazily go allow to change the // home directory for e.g. tests func initDefaults() *defaultConfig { return &defaultConfig{ configFile: defaultConfigLocation("config.yaml"), pluginsDir: defaultConfigLocation("plugins"), lookupPluginsInPath: true, } } func defaultConfigLocation(subpath string) string { var base string if runtime.GOOS == "windows" { base = defaultConfigDirWindows() } else { base = defaultConfigDirUnix() } return filepath.Join(base, subpath) } func defaultConfigDirUnix() string { home, err := homedir.Dir() if err != nil { home = "~" } // Check the deprecated path first and fallback to it, add warning to error message if configHome := filepath.Join(home, ".kn"); dirExists(configHome) { migrationPath := filepath.Join(home, ".config", "kn") fmt.Fprintf(os.Stderr, "WARNING: deprecated kn config directory '%s' detected. Please move your configuration to '%s'\n", configHome, migrationPath) return configHome } // Respect XDG_CONFIG_HOME if set if xdgHome := os.Getenv("XDG_CONFIG_HOME"); xdgHome != "" { return filepath.Join(xdgHome, "kn") } // Fallback to XDG default for both Linux and macOS // ~/.config/kn return filepath.Join(home, ".config", "kn") } func defaultConfigDirWindows() string { home, err := homedir.Dir() if err != nil { // Check the deprecated path first and fallback to it, add warning to error message if configHome := filepath.Join(home, ".kn"); dirExists(configHome) { migrationPath := filepath.Join(os.Getenv("APPDATA"), "kn") fmt.Fprintf(os.Stderr, "WARNING: deprecated kn config directory '%s' detected. Please move your configuration to '%s'\n", configHome, migrationPath) return configHome } } return filepath.Join(os.Getenv("APPDATA"), "kn") } func dirExists(path string) bool { if _, err := os.Stat(path); !os.IsNotExist(err) { return true } return false } // parse sink mappings and store them in the global configuration func parseSinkMappings() error { // Parse sink configuration key := "" if viper.IsSet(keySinkMappings) { key = keySinkMappings } if key == "" && viper.IsSet(legacyKeySinkMappings) { key = legacyKeySinkMappings } if key != "" { err := viper.UnmarshalKey(key, &globalConfig.sinkMappings) if err != nil { return fmt.Errorf("error while parsing sink mappings in configuration file %s: %w", viper.ConfigFileUsed(), err) } } return nil } // parse profiles and store them in the global configuration func parseProfiles() error { if viper.IsSet(profiles) { err := viper.UnmarshalKey(profiles, &globalConfig.profiles) if err != nil { return fmt.Errorf("error while parsing profiles in configuration file %s: %w", viper.ConfigFileUsed(), err) } } globalConfig.profiles = mergeProfilesWithBuiltInProfiles(globalConfig.profiles) return nil } // defaultProfiles returns the built-in profiles func builtInProfiles() map[string]Profile { return map[string]Profile{ istio: { Annotations: []NamedValue{ {Name: "sidecar.istio.io/inject", Value: "true"}, {Name: "sidecar.istio.io/rewriteAppHTTPProbers", Value: "true"}, {Name: "serving.knative.openshift.io/enablePassthrough", Value: "true"}, }, }, } } // mergeProfilesWithDefaultProfiles merges the given profiles with the built-in profiles func mergeProfilesWithBuiltInProfiles(profiles map[string]Profile) map[string]Profile { builtInProfiles := builtInProfiles() mergedProfiles := make(map[string]Profile, len(builtInProfiles)+len(profiles)) for key, value := range builtInProfiles { mergedProfiles[key] = value } for key, value := range profiles { mergedProfiles[key] = value } return mergedProfiles } // parse channel type mappings and store them in the global configuration func parseChannelTypeMappings() error { if viper.IsSet(keyChannelTypeMappings) { err := viper.UnmarshalKey(keyChannelTypeMappings, &globalConfig.channelTypeMappings) if err != nil { return fmt.Errorf("error while parsing channel type mappings in configuration file %s: %w", viper.ConfigFileUsed(), err) } } return nil } // Prepare the default config file for the usage message func defaultConfigFileForUsageMessage() string { if runtime.GOOS == "windows" { return "%APPDATA%\\kn\\config.yaml" } return "~/.config/kn/config.yaml" }