common/config/config_parser.go (278 lines of code) (raw):
/*
_____ _____ _____ ____ ______ _____ ------
| | | | | | | | | | | | |
| | | | | | | | | | | | |
| --- | | | | |-----| |---- | | |-----| |----- ------
| | | | | | | | | | | | |
| ____| |_____ | ____| | ____| | |_____| _____| |_____ |_____
Licensed under the MIT License <http://opensource.org/licenses/MIT>.
Copyright © 2020-2025 Microsoft Corporation. All rights reserved.
Author : <blobfusedev@microsoft.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE
*/
package config
import (
"fmt"
"io"
"os"
"strings"
"time"
"github.com/Azure/azure-storage-fuse/v2/common"
"github.com/Azure/azure-storage-fuse/v2/common/log"
"github.com/spf13/cobra"
"github.com/fsnotify/fsnotify"
"github.com/mitchellh/mapstructure"
"github.com/spf13/pflag"
"github.com/spf13/viper"
)
//config is the common package to handle all configuration related functions of the entire tool
//Precedence order for retrieving config values is as follows:
//1. Flags
//2. Environment Variables
//3. Config file
//
//Any of the bind functions can be put even in init function. Calling of ReadFromConfigFile is not necessary for binding.
//Any reads must happen only after calling ReadFromConfigFile.
// ConfigChangeEventHandler is the interface that must implemented by any object that wants to be notified of changes in the config file
type ConfigChangeEventHandler interface {
OnConfigChange()
}
type ConfigChangeEventHandlerFunc func()
func (handler ConfigChangeEventHandlerFunc) OnConfigChange() {
handler()
}
type KeysTree map[string]interface{}
type options struct {
path string
listeners []ConfigChangeEventHandler
flags *pflag.FlagSet
flagTree *Tree
envTree *Tree
completionFuncMap map[string]func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective)
secureConfig bool
passphrase string
}
var userOptions options
func SetSecureConfigOptions(passphrase string) {
userOptions.secureConfig = true
userOptions.passphrase = passphrase
}
// SetConfigFile : set config file name to be watched by viper
func SetConfigFile(configFilePath string) {
userOptions.path = configFilePath
userOptions.secureConfig = false
viper.SetConfigType("yaml")
viper.SetConfigFile(userOptions.path)
}
// ReadFromConfigFile is used to the configFilePath and initialize viper object
func ReadFromConfigFile(configFilePath string) error {
userOptions.path = configFilePath
viper.SetConfigFile(userOptions.path)
err := viper.ReadInConfig()
if err != nil {
return err
}
WatchConfig()
return nil
}
func loadConfigFromBufferToViper(configData []byte) error {
err := viper.ReadConfig(strings.NewReader(string(configData)))
if err != nil {
return err
}
return nil
}
// ReadFromConfigBuffer is used to the configFilePath and initialize viper object
func ReadFromConfigBuffer(configData []byte) error {
err := loadConfigFromBufferToViper(configData)
if err != nil {
return err
}
WatchConfig()
return nil
}
func DecryptConfigFile(fileName string, passphrase string) error {
cipherText, err := os.ReadFile(fileName)
if err != nil {
return fmt.Errorf("Failed to read encrypted config file [%s]", err.Error())
}
if len(cipherText) == 0 {
return fmt.Errorf("Encrypted config file is empty")
}
plainText, err := common.DecryptData(cipherText, []byte(passphrase))
if err != nil {
return fmt.Errorf("Failed to decrypt config file [%s]", err.Error())
}
err = loadConfigFromBufferToViper(plainText)
if err != nil {
return fmt.Errorf("Failed to load decrypted config file [%s]", err.Error())
}
return nil
}
func WatchConfig() {
viper.WatchConfig()
viper.OnConfigChange(func(_ fsnotify.Event) {
log.Crit("WatchConfig : Config change detected")
if userOptions.secureConfig {
err := DecryptConfigFile(userOptions.path, userOptions.passphrase)
if err != nil {
log.Err("WatchConfig : %s", err.Error())
return
}
}
OnConfigChange()
})
}
func ReadConfigFromReader(reader io.Reader) error {
viper.SetConfigType("yaml")
err := viper.ReadConfig(reader)
if err != nil {
return err
}
return nil
}
// AddConfigChangeEventListener function is used to register any ConfigChangeEventHandler
func AddConfigChangeEventListener(listener ConfigChangeEventHandler) {
userOptions.listeners = append(userOptions.listeners, listener)
}
func OnConfigChange() {
for _, listener := range userOptions.listeners {
listener.OnConfigChange()
}
}
// BindEnv binds the key parameter to a particular environment variable
// For a hierarchical structure pass the keys separated by a .
// For examples to access "name" field in the following structure:
//
// auth:
// name: value
//
// the key parameter should take on the value "auth.key"
func BindEnv(key string, envVarName string) {
userOptions.envTree.Insert(key, envVarName)
}
// BindPFlag binds the key parameter to a particular flag
// For a hierarchical structure pass the keys separated by a .
// For examples to access "name" field in the following structure:
//
// auth:
// name: value
//
// the key parameter should take on the value "auth.key"
func BindPFlag(key string, flag *pflag.Flag) {
userOptions.flagTree.Insert(key, flag)
}
//func BindPFlagWithName(key string, name string) error {
// return viper.BindPFlag(key, userOptions.flags.Lookup(name))
//}
// UnmarshalKey is used to obtain a subtree starting from the key parameter
// For a hierarchical structure pass the keys separated by a .
// For examples to access "name" field in the following structure:
//
// auth:
// name: value
//
// the key parameter should take on the value "auth.key"
func UnmarshalKey(key string, obj interface{}) error {
err := viper.UnmarshalKey(key, obj, func(decodeConfig *mapstructure.DecoderConfig) { decodeConfig.TagName = STRUCT_TAG })
if err != nil {
return fmt.Errorf("config error: unmarshalling [%v]", err)
}
userOptions.envTree.MergeWithKey(key, obj, func(val interface{}) (interface{}, bool) {
envVar := val.(string)
res, ok := os.LookupEnv(envVar)
if ok {
return res, true
} else {
return "", false
}
})
userOptions.flagTree.MergeWithKey(key, obj, func(val interface{}) (interface{}, bool) {
flag := val.(*pflag.Flag)
if flag.Changed {
return flag.Value.String(), true
} else {
return "", false
}
})
return nil
}
// Unmarshal populates the passed object and all the exported fields.
// use lower case attribute names to ignore a particular field
func Unmarshal(obj interface{}) error {
err := viper.Unmarshal(obj, func(decodeConfig *mapstructure.DecoderConfig) { decodeConfig.TagName = STRUCT_TAG })
if err != nil {
return fmt.Errorf("config error: unmarshalling [%v]", err)
}
userOptions.envTree.Merge(obj, func(val interface{}) (interface{}, bool) {
envVar := val.(string)
res, ok := os.LookupEnv(envVar)
if ok {
return res, true
} else {
return "", false
}
})
userOptions.flagTree.Merge(obj, func(val interface{}) (interface{}, bool) {
flag := val.(*pflag.Flag)
if flag.Changed {
return flag.Value.String(), true
} else {
return "", false
}
})
return nil
}
func Set(key string, val string) {
viper.Set(key, val)
}
func SetBool(key string, val bool) {
viper.Set(key, val)
}
func IsSet(key string) bool {
if viper.IsSet(key) {
return true
}
pieces := strings.Split(key, ".")
node := userOptions.flagTree.head
for _, s := range pieces {
node = node.children[s]
if node == nil {
return false
}
}
return node.value.(*pflag.Flag).Changed
}
// AttachToFlagSet is used to attach the flags in config to the cmd flags
func AttachToFlagSet(flagset *pflag.FlagSet) {
flagset.AddFlagSet(userOptions.flags)
}
func AttachFlagCompletions(cmd *cobra.Command) {
for key, fn := range userOptions.completionFuncMap {
_ = cmd.RegisterFlagCompletionFunc(key, fn)
}
}
// ----------------------------------------------------------
// Functions to add flags from a component
func AddStringFlag(name string, value string, usage string) *pflag.Flag {
userOptions.flags.String(name, value, usage)
return userOptions.flags.Lookup(name)
}
func AddIntFlag(name string, value int, usage string) *pflag.Flag {
userOptions.flags.Int(name, value, usage)
return userOptions.flags.Lookup(name)
}
func AddInt8Flag(name string, value int8, usage string) *pflag.Flag {
userOptions.flags.Int8(name, value, usage)
return userOptions.flags.Lookup(name)
}
func AddInt16Flag(name string, value int16, usage string) *pflag.Flag {
userOptions.flags.Int16(name, value, usage)
return userOptions.flags.Lookup(name)
}
func AddInt32Flag(name string, value int32, usage string) *pflag.Flag {
userOptions.flags.Int32(name, value, usage)
return userOptions.flags.Lookup(name)
}
func AddInt64Flag(name string, value int64, usage string) *pflag.Flag {
userOptions.flags.Int64(name, value, usage)
return userOptions.flags.Lookup(name)
}
func AddBoolFlag(name string, value bool, usage string) *pflag.Flag {
userOptions.flags.Bool(name, value, usage)
return userOptions.flags.Lookup(name)
}
func AddBoolPFlag(name string, value bool, usage string) *pflag.Flag {
userOptions.flags.BoolP(name, name, value, usage)
return userOptions.flags.Lookup(name)
}
func AddFloat64Flag(name string, value float64, usage string) *pflag.Flag {
userOptions.flags.Float64(name, value, usage)
return userOptions.flags.Lookup(name)
}
func AddUintFlag(name string, value uint, usage string) *pflag.Flag {
userOptions.flags.Uint(name, value, usage)
return userOptions.flags.Lookup(name)
}
func AddUint8Flag(name string, value uint8, usage string) *pflag.Flag {
userOptions.flags.Uint8(name, value, usage)
return userOptions.flags.Lookup(name)
}
func AddUint16Flag(name string, value uint16, usage string) *pflag.Flag {
userOptions.flags.Uint16(name, value, usage)
return userOptions.flags.Lookup(name)
}
func AddUint32Flag(name string, value uint32, usage string) *pflag.Flag {
userOptions.flags.Uint32(name, value, usage)
return userOptions.flags.Lookup(name)
}
func AddUint64Flag(name string, value uint64, usage string) *pflag.Flag {
userOptions.flags.Uint64(name, value, usage)
return userOptions.flags.Lookup(name)
}
func AddDurationFlag(name string, value time.Duration, usage string) *pflag.Flag {
userOptions.flags.Duration(name, value, usage)
return userOptions.flags.Lookup(name)
}
func RegisterFlagCompletionFunc(flagName string, completionFunc func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective)) {
userOptions.completionFuncMap[flagName] = completionFunc
}
func ResetConfig() {
viper.Reset()
userOptions = options{
path: "",
listeners: make([]ConfigChangeEventHandler, 0),
flags: pflag.NewFlagSet("config-options", pflag.ContinueOnError),
flagTree: NewTree(),
envTree: NewTree(),
}
}
func init() {
userOptions.flags = pflag.NewFlagSet("config-options", pflag.ContinueOnError)
userOptions.flagTree = NewTree()
userOptions.envTree = NewTree()
userOptions.completionFuncMap = make(map[string]func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective))
}