mirror of https://github.com/knative/client.git
345 lines
10 KiB
Go
345 lines
10 KiB
Go
// 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"
|
|
}
|