429 lines
10 KiB
Go
429 lines
10 KiB
Go
/*
|
|
Copyright IBM Corp. All Rights Reserved.
|
|
|
|
SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
package viperutil
|
|
|
|
import (
|
|
"encoding/pem"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"math"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/hyperledger/fabric/bccsp/factory"
|
|
"github.com/hyperledger/fabric/common/flogging"
|
|
"github.com/mitchellh/mapstructure"
|
|
"github.com/pkg/errors"
|
|
"gopkg.in/yaml.v2"
|
|
)
|
|
|
|
var logger = flogging.MustGetLogger("viperutil")
|
|
|
|
// ConfigPaths returns the paths from environment and
|
|
// defaults which are CWD and /etc/hyperledger/fabric.
|
|
func ConfigPaths() []string {
|
|
var paths []string
|
|
if p := os.Getenv("FABRIC_CFG_PATH"); p != "" {
|
|
paths = append(paths, p)
|
|
}
|
|
return append(paths, ".", "/etc/hyperledger/fabric")
|
|
}
|
|
|
|
// ConfigParser holds the configuration file locations.
|
|
// It keeps the config file directory locations and env variables.
|
|
// From the file the config is unmarshalled and stored.
|
|
// Currently "yaml" is supported.
|
|
type ConfigParser struct {
|
|
// configuration file to process
|
|
configPaths []string
|
|
configName string
|
|
configFile string
|
|
|
|
// parsed config
|
|
config map[string]interface{}
|
|
}
|
|
|
|
// New creates a ConfigParser instance
|
|
func New() *ConfigParser {
|
|
return &ConfigParser{
|
|
config: map[string]interface{}{},
|
|
}
|
|
}
|
|
|
|
// AddConfigPaths keeps a list of path to search the relevant
|
|
// config file. Multiple paths can be provided.
|
|
func (c *ConfigParser) AddConfigPaths(cfgPaths ...string) {
|
|
c.configPaths = append(c.configPaths, cfgPaths...)
|
|
}
|
|
|
|
// SetConfigName provides the configuration file name stem. The upper-cased
|
|
// version of this value also serves as the environment variable override
|
|
// prefix.
|
|
func (c *ConfigParser) SetConfigName(in string) {
|
|
c.configName = in
|
|
}
|
|
|
|
// ConfigFileUsed returns the used configFile.
|
|
func (c *ConfigParser) ConfigFileUsed() string {
|
|
return c.configFile
|
|
}
|
|
|
|
// Search for the existence of filename for all supported extensions
|
|
func (c *ConfigParser) searchInPath(in string) (filename string) {
|
|
supportedExts := []string{"yaml", "yml"}
|
|
for _, ext := range supportedExts {
|
|
fullPath := filepath.Join(in, c.configName+"."+ext)
|
|
_, err := os.Stat(fullPath)
|
|
if err == nil {
|
|
return fullPath
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// Search for the configName in all configPaths
|
|
func (c *ConfigParser) findConfigFile() string {
|
|
paths := c.configPaths
|
|
if len(paths) == 0 {
|
|
paths = ConfigPaths()
|
|
}
|
|
for _, cp := range paths {
|
|
file := c.searchInPath(cp)
|
|
if file != "" {
|
|
return file
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// Get the valid and present config file
|
|
func (c *ConfigParser) getConfigFile() string {
|
|
// if explicitly set, then use it
|
|
if c.configFile != "" {
|
|
return c.configFile
|
|
}
|
|
|
|
c.configFile = c.findConfigFile()
|
|
return c.configFile
|
|
}
|
|
|
|
// ReadInConfig reads and unmarshals the config file.
|
|
func (c *ConfigParser) ReadInConfig() error {
|
|
cf := c.getConfigFile()
|
|
logger.Debugf("Attempting to open the config file: %s", cf)
|
|
file, err := os.Open(cf)
|
|
if err != nil {
|
|
logger.Errorf("Unable to open the config file: %s", cf)
|
|
return err
|
|
}
|
|
defer file.Close()
|
|
|
|
return c.ReadConfig(file)
|
|
}
|
|
|
|
// ReadConfig parses the buffer and initializes the config.
|
|
func (c *ConfigParser) ReadConfig(in io.Reader) error {
|
|
return yaml.NewDecoder(in).Decode(c.config)
|
|
}
|
|
|
|
// Get value for the key by searching environment variables.
|
|
func (c *ConfigParser) getFromEnv(key string) string {
|
|
envKey := key
|
|
if c.configName != "" {
|
|
envKey = c.configName + "_" + envKey
|
|
}
|
|
envKey = strings.ToUpper(envKey)
|
|
envKey = strings.ReplaceAll(envKey, ".", "_")
|
|
return os.Getenv(envKey)
|
|
}
|
|
|
|
// Prototype declaration for getFromEnv function.
|
|
type envGetter func(key string) string
|
|
|
|
func getKeysRecursively(base string, getenv envGetter, nodeKeys map[string]interface{}, oType reflect.Type) map[string]interface{} {
|
|
subTypes := map[string]reflect.Type{}
|
|
|
|
if oType != nil && oType.Kind() == reflect.Struct {
|
|
outer:
|
|
for i := 0; i < oType.NumField(); i++ {
|
|
fieldName := oType.Field(i).Name
|
|
fieldType := oType.Field(i).Type
|
|
|
|
for key := range nodeKeys {
|
|
if strings.EqualFold(fieldName, key) {
|
|
subTypes[key] = fieldType
|
|
continue outer
|
|
}
|
|
}
|
|
|
|
subTypes[fieldName] = fieldType
|
|
nodeKeys[fieldName] = nil
|
|
}
|
|
}
|
|
|
|
result := make(map[string]interface{})
|
|
for key, val := range nodeKeys {
|
|
fqKey := base + key
|
|
|
|
// overwrite val, if an environment is available
|
|
if override := getenv(fqKey); override != "" {
|
|
val = override
|
|
}
|
|
|
|
switch val := val.(type) {
|
|
case map[string]interface{}:
|
|
logger.Debugf("Found map[string]interface{} value for %s", fqKey)
|
|
result[key] = getKeysRecursively(fqKey+".", getenv, val, subTypes[key])
|
|
|
|
case map[interface{}]interface{}:
|
|
logger.Debugf("Found map[interface{}]interface{} value for %s", fqKey)
|
|
result[key] = getKeysRecursively(fqKey+".", getenv, toMapStringInterface(val), subTypes[key])
|
|
|
|
case nil:
|
|
if override := getenv(fqKey + ".File"); override != "" {
|
|
result[key] = map[string]interface{}{"File": override}
|
|
}
|
|
|
|
default:
|
|
result[key] = val
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func toMapStringInterface(m map[interface{}]interface{}) map[string]interface{} {
|
|
result := map[string]interface{}{}
|
|
for k, v := range m {
|
|
k, ok := k.(string)
|
|
if !ok {
|
|
panic(fmt.Sprintf("Non string %v, %v: key-entry: %v", k, v, k))
|
|
}
|
|
result[k] = v
|
|
}
|
|
return result
|
|
}
|
|
|
|
// customDecodeHook parses strings of the format "[thing1, thing2, thing3]"
|
|
// into string slices. Note that whitespace around slice elements is removed.
|
|
func customDecodeHook(f reflect.Type, t reflect.Type, data interface{}) (interface{}, error) {
|
|
if f.Kind() != reflect.String {
|
|
return data, nil
|
|
}
|
|
|
|
raw := data.(string)
|
|
l := len(raw)
|
|
if l > 1 && raw[0] == '[' && raw[l-1] == ']' {
|
|
slice := strings.Split(raw[1:l-1], ",")
|
|
for i, v := range slice {
|
|
slice[i] = strings.TrimSpace(v)
|
|
}
|
|
return slice, nil
|
|
}
|
|
|
|
return data, nil
|
|
}
|
|
|
|
func byteSizeDecodeHook(f reflect.Kind, t reflect.Kind, data interface{}) (interface{}, error) {
|
|
if f != reflect.String || t != reflect.Uint32 {
|
|
return data, nil
|
|
}
|
|
raw := data.(string)
|
|
if raw == "" {
|
|
return data, nil
|
|
}
|
|
re := regexp.MustCompile(`^(?P<size>[0-9]+)\s*(?i)(?P<unit>(k|m|g))b?$`)
|
|
if re.MatchString(raw) {
|
|
size, err := strconv.ParseUint(re.ReplaceAllString(raw, "${size}"), 0, 64)
|
|
if err != nil {
|
|
return data, nil
|
|
}
|
|
unit := re.ReplaceAllString(raw, "${unit}")
|
|
switch strings.ToLower(unit) {
|
|
case "g":
|
|
size = size << 10
|
|
fallthrough
|
|
case "m":
|
|
size = size << 10
|
|
fallthrough
|
|
case "k":
|
|
size = size << 10
|
|
}
|
|
if size > math.MaxUint32 {
|
|
return size, fmt.Errorf("value '%s' overflows uint32", raw)
|
|
}
|
|
return size, nil
|
|
}
|
|
return data, nil
|
|
}
|
|
|
|
func stringFromFileDecodeHook(f reflect.Kind, t reflect.Kind, data interface{}) (interface{}, error) {
|
|
// "to" type should be string
|
|
if t != reflect.String {
|
|
return data, nil
|
|
}
|
|
// "from" type should be map
|
|
if f != reflect.Map {
|
|
return data, nil
|
|
}
|
|
v := reflect.ValueOf(data)
|
|
switch v.Kind() {
|
|
case reflect.String:
|
|
return data, nil
|
|
case reflect.Map:
|
|
d := data.(map[string]interface{})
|
|
fileName, ok := d["File"]
|
|
if !ok {
|
|
fileName, ok = d["file"]
|
|
}
|
|
switch {
|
|
case ok && fileName != nil:
|
|
bytes, err := ioutil.ReadFile(fileName.(string))
|
|
if err != nil {
|
|
return data, err
|
|
}
|
|
return string(bytes), nil
|
|
case ok:
|
|
// fileName was nil
|
|
return nil, fmt.Errorf("Value of File: was nil")
|
|
}
|
|
}
|
|
return data, nil
|
|
}
|
|
|
|
func pemBlocksFromFileDecodeHook(f reflect.Kind, t reflect.Kind, data interface{}) (interface{}, error) {
|
|
// "to" type should be string
|
|
if t != reflect.Slice {
|
|
return data, nil
|
|
}
|
|
// "from" type should be map
|
|
if f != reflect.Map {
|
|
return data, nil
|
|
}
|
|
v := reflect.ValueOf(data)
|
|
switch v.Kind() {
|
|
case reflect.String:
|
|
return data, nil
|
|
case reflect.Map:
|
|
var fileName string
|
|
var ok bool
|
|
switch d := data.(type) {
|
|
case map[string]string:
|
|
fileName, ok = d["File"]
|
|
if !ok {
|
|
fileName, ok = d["file"]
|
|
}
|
|
case map[string]interface{}:
|
|
var fileI interface{}
|
|
fileI, ok = d["File"]
|
|
if !ok {
|
|
fileI = d["file"]
|
|
}
|
|
fileName, ok = fileI.(string)
|
|
}
|
|
|
|
switch {
|
|
case ok && fileName != "":
|
|
var result []string
|
|
bytes, err := ioutil.ReadFile(fileName)
|
|
if err != nil {
|
|
return data, err
|
|
}
|
|
for len(bytes) > 0 {
|
|
var block *pem.Block
|
|
block, bytes = pem.Decode(bytes)
|
|
if block == nil {
|
|
break
|
|
}
|
|
if block.Type != "CERTIFICATE" || len(block.Headers) != 0 {
|
|
continue
|
|
}
|
|
result = append(result, string(pem.EncodeToMemory(block)))
|
|
}
|
|
return result, nil
|
|
case ok:
|
|
// fileName was nil
|
|
return nil, fmt.Errorf("Value of File: was nil")
|
|
}
|
|
}
|
|
return data, nil
|
|
}
|
|
|
|
func bccspHook(f reflect.Type, t reflect.Type, data interface{}) (interface{}, error) {
|
|
if t != reflect.TypeOf(&factory.FactoryOpts{}) {
|
|
return data, nil
|
|
}
|
|
|
|
config := factory.GetDefaultOpts()
|
|
|
|
err := mapstructure.WeakDecode(data, config)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "could not decode bccsp type")
|
|
}
|
|
|
|
return config, nil
|
|
}
|
|
|
|
// EnhancedExactUnmarshal is intended to unmarshal a config file into a structure
|
|
// producing error when extraneous variables are introduced and supporting
|
|
// the time.Duration type
|
|
func (c *ConfigParser) EnhancedExactUnmarshal(output interface{}) error {
|
|
oType := reflect.TypeOf(output)
|
|
if oType.Kind() != reflect.Ptr {
|
|
return errors.Errorf("supplied output argument must be a pointer to a struct but is not pointer")
|
|
}
|
|
eType := oType.Elem()
|
|
if eType.Kind() != reflect.Struct {
|
|
return errors.Errorf("supplied output argument must be a pointer to a struct, but it is pointer to something else")
|
|
}
|
|
|
|
baseKeys := c.config
|
|
leafKeys := getKeysRecursively("", c.getFromEnv, baseKeys, eType)
|
|
|
|
logger.Debugf("%+v", leafKeys)
|
|
config := &mapstructure.DecoderConfig{
|
|
ErrorUnused: true,
|
|
Metadata: nil,
|
|
Result: output,
|
|
WeaklyTypedInput: true,
|
|
DecodeHook: mapstructure.ComposeDecodeHookFunc(
|
|
bccspHook,
|
|
mapstructure.StringToTimeDurationHookFunc(),
|
|
customDecodeHook,
|
|
byteSizeDecodeHook,
|
|
stringFromFileDecodeHook,
|
|
pemBlocksFromFileDecodeHook,
|
|
),
|
|
}
|
|
|
|
decoder, err := mapstructure.NewDecoder(config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return decoder.Decode(leafKeys)
|
|
}
|
|
|
|
// YamlStringToStructHook is a hook for viper(viper.Unmarshal(*,*, here)), it is able to parse a string of minified yaml into a slice of structs
|
|
func YamlStringToStructHook(m interface{}) func(rf reflect.Kind, rt reflect.Kind, data interface{}) (interface{}, error) {
|
|
return func(rf reflect.Kind, rt reflect.Kind, data interface{}) (interface{}, error) {
|
|
if rf != reflect.String || rt != reflect.Slice {
|
|
return data, nil
|
|
}
|
|
|
|
raw := data.(string)
|
|
if raw == "" {
|
|
return m, nil
|
|
}
|
|
|
|
return m, yaml.UnmarshalStrict([]byte(raw), &m)
|
|
}
|
|
}
|