Giter Site home page Giter Site logo

knadh / koanf Goto Github PK

View Code? Open in Web Editor NEW
2.4K 20.0 143.0 478 KB

Simple, extremely lightweight, extensible, configuration management library for Go. Support for JSON, TOML, YAML, env, command line, file, S3 etc. Alternative to viper.

License: MIT License

Go 99.50% HCL 0.50%
configuration configuration-management configuration-file golang go golang-package viper yaml toml s3-bucket

koanf's Introduction

koanf

koanf is a library for reading configuration from different sources in different formats in Go applications. It is a cleaner, lighter alternative to spf13/viper with better abstractions and extensibility and far fewer dependencies.

koanf v2 has modules (Providers) for reading configuration from a variety of sources such as files, command line flags, environment variables, Vault, and S3 and for parsing (Parsers) formats such as JSON, YAML, TOML, Hashicorp HCL. It is easy to plug in custom parsers and providers.

All external dependencies in providers and parsers are detached from the core and can be installed separately as necessary.

Run Tests GoDoc

Installation

# Install the core.
go get -u github.com/knadh/koanf/v2

# Install the necessary Provider(s).
# Available: file, env, posflag, basicflag, confmap, rawbytes,
#            structs, fs, s3, appconfig/v2, consul/v2, etcd/v2, vault/v2, parameterstore/v2
# eg: go get -u github.com/knadh/koanf/providers/s3
# eg: go get -u github.com/knadh/koanf/providers/consul/v2

go get -u github.com/knadh/koanf/providers/file


# Install the necessary Parser(s).
# Available: toml, toml/v2, json, yaml, dotenv, hcl, hjson, nestedtext
# go get -u github.com/knadh/koanf/parsers/$parser

go get -u github.com/knadh/koanf/parsers/toml

See the list of all bundled Providers and Parsers.

Contents

Concepts

  • koanf.Provider is a generic interface that provides configuration, for example, from files, environment variables, HTTP sources, or anywhere. The configuration can either be raw bytes that a parser can parse, or it can be a nested map[string]interface{} that can be directly loaded.
  • koanf.Parser is a generic interface that takes raw bytes, parses, and returns a nested map[string]interface{}. For example, JSON and YAML parsers.
  • Once loaded into koanf, configuration are values queried by a delimited key path syntax. eg: app.server.port. Any delimiter can be chosen.
  • Configuration from multiple sources can be loaded and merged into a koanf instance, for example, load from a file first and override certain values with flags from the command line.

With these two interface implementations, koanf can obtain configuration in any format from any source, parse it, and make it available to an application.

Reading config from files

package main

import (
	"fmt"
	"log"

	"github.com/knadh/koanf/v2"
	"github.com/knadh/koanf/parsers/json"
	"github.com/knadh/koanf/parsers/yaml"
	"github.com/knadh/koanf/providers/file"
)

// Global koanf instance. Use "." as the key path delimiter. This can be "/" or any character.
var k = koanf.New(".")

func main() {
	// Load JSON config.
	if err := k.Load(file.Provider("mock/mock.json"), json.Parser()); err != nil {
		log.Fatalf("error loading config: %v", err)
	}

	// Load YAML config and merge into the previously loaded config (because we can).
	k.Load(file.Provider("mock/mock.yml"), yaml.Parser())

	fmt.Println("parent's name is = ", k.String("parent1.name"))
	fmt.Println("parent's ID is = ", k.Int("parent1.id"))
}

Watching file for changes

Some providers expose a Watch() method that makes the provider watch for changes in configuration and trigger a callback to reload the configuration. This is not goroutine safe if there are concurrent *Get() calls happening on the koanf object while it is doing a Load(). Such scenarios will need mutex locking.

file, appconfig, vault, consul providers have a Watch() method.

package main

import (
	"fmt"
	"log"

	"github.com/knadh/koanf/v2"
	"github.com/knadh/koanf/parsers/json"
	"github.com/knadh/koanf/parsers/yaml"
	"github.com/knadh/koanf/providers/file"
)

// Global koanf instance. Use "." as the key path delimiter. This can be "/" or any character.
var k = koanf.New(".")

func main() {
	// Load JSON config.
	f := file.Provider("mock/mock.json")
	if err := k.Load(f, json.Parser()); err != nil {
		log.Fatalf("error loading config: %v", err)
	}

	// Load YAML config and merge into the previously loaded config (because we can).
	k.Load(file.Provider("mock/mock.yml"), yaml.Parser())

	fmt.Println("parent's name is = ", k.String("parent1.name"))
	fmt.Println("parent's ID is = ", k.Int("parent1.id"))

	// Watch the file and get a callback on change. The callback can do whatever,
	// like re-load the configuration.
	// File provider always returns a nil `event`.
	f.Watch(func(event interface{}, err error) {
		if err != nil {
			log.Printf("watch error: %v", err)
			return
		}

		// Throw away the old config and load a fresh copy.
		log.Println("config changed. Reloading ...")
		k = koanf.New(".")
		k.Load(f, json.Parser())
		k.Print()
	})

	// Block forever (and manually make a change to mock/mock.json) to
	// reload the config.
	log.Println("waiting forever. Try making a change to mock/mock.json to live reload")
	<-make(chan bool)
}

Reading from command line

The following example shows the use of posflag.Provider, a wrapper over the spf13/pflag library, an advanced commandline lib. For Go's built in flag package, use basicflag.Provider.

package main

import (
	"fmt"
	"log"
	"os"

	"github.com/knadh/koanf/v2"
	"github.com/knadh/koanf/parsers/toml"

	// TOML version 2 is available at:
	// "github.com/knadh/koanf/parsers/toml/v2"

	"github.com/knadh/koanf/providers/file"
	"github.com/knadh/koanf/providers/posflag"
	flag "github.com/spf13/pflag"
)

// Global koanf instance. Use "." as the key path delimiter. This can be "/" or any character.
var k = koanf.New(".")

func main() {
	// Use the POSIX compliant pflag lib instead of Go's flag lib.
	f := flag.NewFlagSet("config", flag.ContinueOnError)
	f.Usage = func() {
		fmt.Println(f.FlagUsages())
		os.Exit(0)
	}
	// Path to one or more config files to load into koanf along with some config params.
	f.StringSlice("conf", []string{"mock/mock.toml"}, "path to one or more .toml config files")
	f.String("time", "2020-01-01", "a time string")
	f.String("type", "xxx", "type of the app")
	f.Parse(os.Args[1:])

	// Load the config files provided in the commandline.
	cFiles, _ := f.GetStringSlice("conf")
	for _, c := range cFiles {
		if err := k.Load(file.Provider(c), toml.Parser()); err != nil {
			log.Fatalf("error loading file: %v", err)
		}
	}

	// "time" and "type" may have been loaded from the config file, but
	// they can still be overridden with the values from the command line.
	// The bundled posflag.Provider takes a flagset from the spf13/pflag lib.
	// Passing the Koanf instance to posflag helps it deal with default command
	// line flag values that are not present in conf maps from previously loaded
	// providers.
	if err := k.Load(posflag.Provider(f, ".", k), nil); err != nil {
		log.Fatalf("error loading config: %v", err)
	}

	fmt.Println("time is = ", k.String("time"))
}

Reading environment variables

package main

import (
	"fmt"
	"log"
	"strings"

	"github.com/knadh/koanf/v2"
	"github.com/knadh/koanf/parsers/json"
	"github.com/knadh/koanf/providers/env"
	"github.com/knadh/koanf/providers/file"
)

// Global koanf instance. Use . as the key path delimiter. This can be / or anything.
var k = koanf.New(".")

func main() {
	// Load JSON config.
	if err := k.Load(file.Provider("mock/mock.json"), json.Parser()); err != nil {
		log.Fatalf("error loading config: %v", err)
	}

	// Load environment variables and merge into the loaded config.
	// "MYVAR" is the prefix to filter the env vars by.
	// "." is the delimiter used to represent the key hierarchy in env vars.
	// The (optional, or can be nil) function can be used to transform
	// the env var names, for instance, to lowercase them.
	//
	// For example, env vars: MYVAR_TYPE and MYVAR_PARENT1_CHILD1_NAME
	// will be merged into the "type" and the nested "parent1.child1.name"
	// keys in the config file here as we lowercase the key, 
	// replace `_` with `.` and strip the MYVAR_ prefix so that 
	// only "parent1.child1.name" remains.
	k.Load(env.Provider("MYVAR_", ".", func(s string) string {
		return strings.Replace(strings.ToLower(
			strings.TrimPrefix(s, "MYVAR_")), "_", ".", -1)
	}), nil)

	fmt.Println("name is = ", k.String("parent1.child1.name"))
}

You can also use the env.ProviderWithValue with a callback that supports mutating both the key and value to return types other than a string. For example, here, env values separated by spaces are returned as string slices or arrays. eg: MYVAR_slice=a b c becomes slice: [a, b, c].

	k.Load(env.ProviderWithValue("MYVAR_", ".", func(s string, v string) (string, interface{}) {
		// Strip out the MYVAR_ prefix and lowercase and get the key while also replacing
		// the _ character with . in the key (koanf delimeter).
		key := strings.Replace(strings.ToLower(strings.TrimPrefix(s, "MYVAR_")), "_", ".", -1)

		// If there is a space in the value, split the value into a slice by the space.
		if strings.Contains(v, " ") {
			return key, strings.Split(v, " ")
		}

		// Otherwise, return the plain string.
		return key, v
	}), nil)

Reading from an S3 bucket

// Load JSON config from s3.
if err := k.Load(s3.Provider(s3.Config{
	AccessKey: os.Getenv("AWS_S3_ACCESS_KEY"),
	SecretKey: os.Getenv("AWS_S3_SECRET_KEY"),
	Region:    os.Getenv("AWS_S3_REGION"),
	Bucket:    os.Getenv("AWS_S3_BUCKET"),
	ObjectKey: "dir/config.json",
}), json.Parser()); err != nil {
	log.Fatalf("error loading config: %v", err)
}

Reading raw bytes

The bundled rawbytes Provider can be used to read arbitrary bytes from a source, like a database or an HTTP call.

package main

import (
	"fmt"

	"github.com/knadh/koanf/v2"
	"github.com/knadh/koanf/parsers/json"
	"github.com/knadh/koanf/providers/rawbytes"
)

// Global koanf instance. Use . as the key path delimiter. This can be / or anything.
var k = koanf.New(".")

func main() {
	b := []byte(`{"type": "rawbytes", "parent1": {"child1": {"type": "rawbytes"}}}`)
	k.Load(rawbytes.Provider(b), json.Parser())
	fmt.Println("type is = ", k.String("parent1.child1.type"))
}

Unmarshalling and marshalling

Parsers can be used to unmarshal and scan the values in a Koanf instance into a struct based on the field tags, and to marshal a Koanf instance back into serialized bytes, for example, back to JSON or YAML, to write back to files.

package main

import (
	"fmt"
	"log"

	"github.com/knadh/koanf/v2"
	"github.com/knadh/koanf/parsers/json"
	"github.com/knadh/koanf/providers/file"
)

// Global koanf instance. Use . as the key path delimiter. This can be / or anything.
var (
	k      = koanf.New(".")
	parser = json.Parser()
)

func main() {
	// Load JSON config.
	if err := k.Load(file.Provider("mock/mock.json"), parser); err != nil {
		log.Fatalf("error loading config: %v", err)
	}

	// Structure to unmarshal nested conf to.
	type childStruct struct {
		Name       string            `koanf:"name"`
		Type       string            `koanf:"type"`
		Empty      map[string]string `koanf:"empty"`
		GrandChild struct {
			Ids []int `koanf:"ids"`
			On  bool  `koanf:"on"`
		} `koanf:"grandchild1"`
	}

	var out childStruct

	// Quick unmarshal.
	k.Unmarshal("parent1.child1", &out)
	fmt.Println(out)

	// Unmarshal with advanced config.
	out = childStruct{}
	k.UnmarshalWithConf("parent1.child1", &out, koanf.UnmarshalConf{Tag: "koanf"})
	fmt.Println(out)

	// Marshal the instance back to JSON.
	// The paser instance can be anything, eg: json.Paser(), yaml.Parser() etc.
	b, _ := k.Marshal(parser)
	fmt.Println(string(b))
}

Unmarshalling with flat paths

Sometimes it is necessary to unmarshal an assortment of keys from various nested structures into a flat target structure. This is possible with the UnmarshalConf.FlatPaths flag.

package main

import (
	"fmt"
	"log"

	"github.com/knadh/koanf/v2"
	"github.com/knadh/koanf/parsers/json"
	"github.com/knadh/koanf/providers/file"
)

// Global koanf instance. Use . as the key path delimiter. This can be / or anything.
var k = koanf.New(".")

func main() {
	// Load JSON config.
	if err := k.Load(file.Provider("mock/mock.json"), json.Parser()); err != nil {
		log.Fatalf("error loading config: %v", err)
	}

	type rootFlat struct {
		Type                        string            `koanf:"type"`
		Empty                       map[string]string `koanf:"empty"`
		Parent1Name                 string            `koanf:"parent1.name"`
		Parent1ID                   int               `koanf:"parent1.id"`
		Parent1Child1Name           string            `koanf:"parent1.child1.name"`
		Parent1Child1Type           string            `koanf:"parent1.child1.type"`
		Parent1Child1Empty          map[string]string `koanf:"parent1.child1.empty"`
		Parent1Child1Grandchild1IDs []int             `koanf:"parent1.child1.grandchild1.ids"`
		Parent1Child1Grandchild1On  bool              `koanf:"parent1.child1.grandchild1.on"`
	}

	// Unmarshal the whole root with FlatPaths: True.
	var o1 rootFlat
	k.UnmarshalWithConf("", &o1, koanf.UnmarshalConf{Tag: "koanf", FlatPaths: true})
	fmt.Println(o1)

	// Unmarshal a child structure of "parent1".
	type subFlat struct {
		Name                 string            `koanf:"name"`
		ID                   int               `koanf:"id"`
		Child1Name           string            `koanf:"child1.name"`
		Child1Type           string            `koanf:"child1.type"`
		Child1Empty          map[string]string `koanf:"child1.empty"`
		Child1Grandchild1IDs []int             `koanf:"child1.grandchild1.ids"`
		Child1Grandchild1On  bool              `koanf:"child1.grandchild1.on"`
	}

	var o2 subFlat
	k.UnmarshalWithConf("parent1", &o2, koanf.UnmarshalConf{Tag: "koanf", FlatPaths: true})
	fmt.Println(o2)
}

Reading from nested maps

The bundled confmap provider takes a map[string]interface{} that can be loaded into a koanf instance.

package main

import (
	"fmt"
	"log"

	"github.com/knadh/koanf/v2"
	"github.com/knadh/koanf/providers/confmap"
	"github.com/knadh/koanf/providers/file"
	"github.com/knadh/koanf/parsers/json"
	"github.com/knadh/koanf/parsers/yaml"
)

// Global koanf instance. Use "." as the key path delimiter. This can be "/" or any character.
var k = koanf.New(".")

func main() {
	// Load default values using the confmap provider.
	// We provide a flat map with the "." delimiter.
	// A nested map can be loaded by setting the delimiter to an empty string "".
	k.Load(confmap.Provider(map[string]interface{}{
		"parent1.name": "Default Name",
		"parent3.name": "New name here",
	}, "."), nil)

	// Load JSON config on top of the default values.
	if err := k.Load(file.Provider("mock/mock.json"), json.Parser()); err != nil {
		log.Fatalf("error loading config: %v", err)
	}

	// Load YAML config and merge into the previously loaded config (because we can).
	k.Load(file.Provider("mock/mock.yml"), yaml.Parser())

	fmt.Println("parent's name is = ", k.String("parent1.name"))
	fmt.Println("parent's ID is = ", k.Int("parent1.id"))
}

Reading from struct

The bundled structs provider can be used to read data from a struct to load into a koanf instance.

package main

import (
	"fmt"

	"github.com/knadh/koanf/v2"
	"github.com/knadh/koanf/providers/structs"
)

// Global koanf instance. Use "." as the key path delimiter. This can be "/" or any character.
var k = koanf.New(".")

type parentStruct struct {
	Name   string      `koanf:"name"`
	ID     int         `koanf:"id"`
	Child1 childStruct `koanf:"child1"`
}
type childStruct struct {
	Name        string            `koanf:"name"`
	Type        string            `koanf:"type"`
	Empty       map[string]string `koanf:"empty"`
	Grandchild1 grandchildStruct  `koanf:"grandchild1"`
}
type grandchildStruct struct {
	Ids []int `koanf:"ids"`
	On  bool  `koanf:"on"`
}
type sampleStruct struct {
	Type    string            `koanf:"type"`
	Empty   map[string]string `koanf:"empty"`
	Parent1 parentStruct      `koanf:"parent1"`
}

func main() {
	// Load default values using the structs provider.
	// We provide a struct along with the struct tag `koanf` to the
	// provider.
	k.Load(structs.Provider(sampleStruct{
		Type:  "json",
		Empty: make(map[string]string),
		Parent1: parentStruct{
			Name: "parent1",
			ID:   1234,
			Child1: childStruct{
				Name:  "child1",
				Type:  "json",
				Empty: make(map[string]string),
				Grandchild1: grandchildStruct{
					Ids: []int{1, 2, 3},
					On:  true,
				},
			},
		},
	}, "koanf"), nil)

	fmt.Printf("name is = `%s`\n", k.String("parent1.child1.name"))
}

Merge behavior

Default behavior

The default behavior when you create Koanf this way is: koanf.New(delim) that the latest loaded configuration will merge with the previous one.

For example: first.yml

key: [1,2,3]

second.yml

key: 'string'

When second.yml is loaded it will override the type of the first.yml.

If this behavior is not desired, you can merge 'strictly'. In the same scenario, Load will return an error.

package main

import (
	"errors"
	"log"

	"github.com/knadh/koanf/v2"
	"github.com/knadh/koanf/maps"
	"github.com/knadh/koanf/parsers/json"
	"github.com/knadh/koanf/parsers/yaml"
	"github.com/knadh/koanf/providers/file"
)

var conf = koanf.Conf{
	Delim:       ".",
	StrictMerge: true,
}
var k = koanf.NewWithConf(conf)

func main() {
	yamlPath := "mock/mock.yml"
	if err := k.Load(file.Provider(yamlPath), yaml.Parser()); err != nil {
		log.Fatalf("error loading config: %v", err)
	}

	jsonPath := "mock/mock.json"
	if err := k.Load(file.Provider(jsonPath), json.Parser()); err != nil {
		log.Fatalf("error loading config: %v", err)
	}
}

Note: When merging different extensions, each parser can treat his types differently, meaning even though you the load same types there is a probability that it will fail with StrictMerge: true.

For example: merging JSON and YAML will most likely fail because JSON treats integers as float64 and YAML treats them as integers.

Order of merge and key case sensitivity

  • Config keys are case-sensitive in koanf. For example, app.server.port and APP.SERVER.port are not the same.
  • koanf does not impose any ordering on loading config from various providers. Every successive Load() or Merge() merges new config into the existing config. That is, it is possible to load environment variables first, then files on top of it, and then command line variables on top of it, or any such order.

Custom Providers and Parsers

A Provider returns a nested map[string]interface{} config that can be loaded directly into koanf with koanf.Load() or it can return raw bytes that can be parsed with a Parser (again, loaded using koanf.Load(). Writing Providers and Parsers are easy. See the bundled implementations in the providers and parsers directories.

Custom merge strategies

By default, when merging two config sources using Load(), koanf recursively merges keys of nested maps (map[string]interface{}), while static values are overwritten (slices, strings, etc). This behaviour can be changed by providing a custom merge function with the WithMergeFunc option.

package main

import (
	"errors"
	"log"

	"github.com/knadh/koanf/v2"
	"github.com/knadh/koanf/maps"
	"github.com/knadh/koanf/parsers/json"
	"github.com/knadh/koanf/parsers/yaml"
	"github.com/knadh/koanf/providers/file"
)

var conf = koanf.Conf{
	Delim:       ".",
	StrictMerge: true,
}
var k = koanf.NewWithConf(conf)

func main() {
	yamlPath := "mock/mock.yml"
	if err := k.Load(file.Provider(yamlPath), yaml.Parser()); err != nil {
		log.Fatalf("error loading config: %v", err)
	}

	jsonPath := "mock/mock.json"
	if err := k.Load(file.Provider(jsonPath), json.Parser(), koanf.WithMergeFunc(func(src, dest map[string]interface{}) error {
     // Your custom logic, copying values from src into dst
     return nil
    })); err != nil {
		log.Fatalf("error loading config: %v", err)
	}
}

API

See the full API documentation of all available methods at https://pkg.go.dev/github.com/knadh/koanf/v2#section-documentation

Bundled Providers

Install with go get -u github.com/knadh/koanf/providers/$provider

Package Provider Description
file file.Provider(filepath string) Reads a file and returns the raw bytes to be parsed.
fs fs.Provider(f fs.FS, filepath string) (Experimental) Reads a file from fs.FS and returns the raw bytes to be parsed. The provider requires go v1.16 or higher.
basicflag basicflag.Provider(f *flag.FlagSet, delim string) Takes an stdlib flag.FlagSet
posflag posflag.Provider(f *pflag.FlagSet, delim string) Takes an spf13/pflag.FlagSet (advanced POSIX compatible flags with multiple types) and provides a nested config map based on delim.
env env.Provider(prefix, delim string, f func(s string) string) Takes an optional prefix to filter env variables by, an optional function that takes and returns a string to transform env variables, and returns a nested config map based on delim.
confmap confmap.Provider(mp map[string]interface{}, delim string) Takes a premade map[string]interface{} conf map. If delim is provided, the keys are assumed to be flattened, thus unflattened using delim.
structs structs.Provider(s interface{}, tag string) Takes a struct and struct tag.
s3 s3.Provider(s3.S3Config{}) Takes a s3 config struct.
rawbytes rawbytes.Provider(b []byte) Takes a raw []byte slice to be parsed with a koanf.Parser
vault/v2 vault.Provider(vault.Config{}) Hashicorp Vault provider
appconfig/v2 vault.AppConfig(appconfig.Config{}) AWS AppConfig provider
etcd/v2 etcd.Provider(etcd.Config{}) CNCF etcd provider
consul/v2 consul.Provider(consul.Config{}) Hashicorp Consul provider
parameterstore/v2 parameterstore.Provider(parameterstore.Config{}) AWS Systems Manager Parameter Store provider

Bundled Parsers

Install with go get -u github.com/knadh/koanf/parsers/$parser

Package Parser Description
json json.Parser() Parses JSON bytes into a nested map
yaml yaml.Parser() Parses YAML bytes into a nested map
toml toml.Parser() Parses TOML bytes into a nested map
toml/v2 toml.Parser() Parses TOML bytes into a nested map (using go-toml v2)
dotenv dotenv.Parser() Parses DotEnv bytes into a flat map
hcl hcl.Parser(flattenSlices bool) Parses Hashicorp HCL bytes into a nested map. flattenSlices is recommended to be set to true. Read more.
nestedtext nestedtext.Parser() Parses NestedText bytes into a flat map
hjson hjson.Parser() Parses HJSON bytes into a nested map
																						|

Third-party Providers

Package Provider Description
github.com/defensestation/koanf/providers/secretsmanager vault.SecretsMananger(secretsmanager.Config{}, f func(s string) string) AWS Secrets Manager provider, takes map or string as a value from store
github.com/defensestation/koanf/providers/parameterstore vault.ParameterStore(parameterstore.Config{}, f func(s string) string) AWS ParameterStore provider, an optional function that takes and returns a string to transform env variables

Alternative to viper

koanf is a lightweight alternative to the popular spf13/viper. It was written as a result of multiple stumbling blocks encountered with some of viper's fundamental flaws.

  • viper breaks JSON, YAML, TOML, HCL language specs by forcibly lowercasing keys.
  • Significantly bloats build sizes.
  • Tightly couples config parsing with file extensions.
  • Has poor semantics and abstractions. Commandline, env, file etc. and various parses are hardcoded in the core. There are no primitives that can be extended.
  • Pulls a large number of third party dependencies into the core package. For instance, even if you do not use YAML or flags, the dependencies are still pulled as a result of the coupling.
  • Imposes arbitrary ordering conventions (eg: flag -> env -> config etc.)
  • Get() returns references to slices and maps. Mutations made outside change the underlying values inside the conf map.
  • Does non-idiomatic things such as throwing away O(1) on flat maps.
  • Viper treats keys that contain an empty map (eg: my_key: {}) as if they were not set (ie: IsSet("my_key") == false).
  • There are a large number of open issues.

koanf's People

Contributors

0xjac avatar 1995parham avatar aeneasr avatar ahuret avatar amarlearning avatar dependabot[bot] avatar dezren39 avatar elidhu avatar glenn-m avatar gozeloglu avatar grount avatar gsingh-ds avatar infalmo avatar john-behm-bertelsmann avatar knadh avatar misantron avatar missedone avatar mkvolkov avatar mr-karan avatar muxxer avatar mvrahden avatar npillmayer avatar rhnvrm avatar robinbraemer avatar shadrus avatar tchssk avatar testwill avatar thunderbottom avatar tlipoca9 avatar vividvilla avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

koanf's Issues

discussion: `fs.FS` provider, Go v1.16 and package `io/fs`

In PR #87 I tried to implement a primitive provider for the newly (go v1.16) introduced fs.FS from the io/fs package.

However, I wasn't able to find a way to introduce that Provider because it would brake backwards compatibility.
The interface fs.FS depends on fs.File interface, which depends on fs.FileInfo interface which depends on fs.Filemode. So this chain of dependencies is practically impossible to shadow/alias by a local interface definition, if I'm not mistaken.

I believe that file handling is going to change in favor of the introduced package io/fs in the long run, implying that the file provider would likely need some changes in that context as well.

How about branching off a dedicated v1.16 branch and working on that branch towards these changes and later introducing merging back with a new (minor) version, which will do the leap to v1.16.

Any opinions, insights, proposals or suggestions on this matter are highly welcome :)

Unmarshaling environment variables

Hey,
as far as I got from the README it should be possible to load YAML and environment variables and ultimately unmarshal the results of both into my own Go struct. If that's the case I'm struggling with loading env variables, while YAML unmarshaling works fine:

	type Foo struct {
		Servers []string `koanf:"servers"`
		SSHPassword string `koanf:"sshPassword"`
		SSHPass2 string `koanf:"ssh_password"`
		Pass string `koanf:"pass"`
	}
	os.Setenv("SSH_PASSWORD", "secret")
	os.Setenv("SSHPASSWORD", "secret2")
	os.Setenv("PASS", "secret3")

	k := koanf.New(".")
	err := k.Load(file.Provider("./config/test.yaml"), yaml.Parser())
	if err != nil {
		panic(err)
	}

	err = k.Load(env.Provider("", "_", func(s string) string {
		return ""
	}), nil)
	if err != nil {
		panic(err)
	}

	var cfg Foo
	err = k.Unmarshal("", &cfg)
	if err != nil {
		panic(err)
	}

The yaml file just sets servers: ["bla1:9093"] and is actually unmarshaled successfully, the rest isn't:
image

I wasn't sure how koanf behaves with lower camel cased names hence I tried three different versions ( sshPassword, ssh_password and just pass). None of that seem to have worked / ended up in the unmarshaled cfg instance. What am I doing wrong here?

feature request: register unmarshaler

First of all thanks for this awesome library. I know we can register map structure configuration for unmarshaling but it would be great if could register just some decoder functions besides the default one which works great. for example:

UnmarshalWithHooks(path string, o interface{}, hooks ...DecodeHookFunc)

Then merges these hooks with the default one. if you agree with it, I can create a PR for this.

Also koanf can have a method for returning its default configuration for a map structure decoder for users who wants to change it.

Support for watching file changes

Feature Request, I need this before I can consider leaving viper.

In particular Kubernetes mounted files, which use crazy symlink stuff

Custom error handler

When validating type of values passed via flags or config file I have the choice of either k.Int() or k.MustInt(), first one will default to 0 if the value is incorrect, second will panic on parse error.
What I'm missing is a way of validating passed values and reporting an error to the user without the need to panic, there is no variant of a getter that returns an error from what I see.
Is it possible to setup custom error handler?
Perhaps one could register error handling function that's called on MustInt() to avoid polluting API with yet another version of a Int() getter?

Tests on master don't pass - MacOS

I made a pull request and tried to run the tests - which failed on the file reloading.

I proceeded to then run the tests from master just incase I had somehow broken something unrelated, but they still failed.

I'm guessing something platform specific?

➜  koanf git:(feature/add-dotenv-support) git checkout master
Switched to branch 'master'
Your branch is up to date with 'origin/master'.
➜  koanf git:(master) go test
--- FAIL: TestWatchFile (2.02s)
    koanf_test.go:305:
                Error Trace:    koanf_test.go:305
                Error:          Not equal:
                                expected: "name2"
                                actual  : ""

                                Diff:
                                --- Expected
                                +++ Actual
                                @@ -1 +1 @@
                                -name2
                                +
                Test:           TestWatchFile
                Messages:       file watch reload didn't change config
panic: Fail in goroutine after TestWatchFile has completed

goroutine 10 [running]:
testing.(*common).Fail(0xc0001cec60)
   /usr/local/go/src/testing/testing.go:613 +0x11a
testing.(*common).Errorf(0xc0001cec60, 0x13ffe83, 0x3, 0xc000412050, 0x1, 0x1)
   /usr/local/go/src/testing/testing.go:712 +0x90
github.com/stretchr/testify/assert.Fail(0x1489fa0, 0xc0001cec60, 0xc000408070, 0x65, 0xc00004ae90, 0x1, 0x1, 0x13ef3e0)
   /Users/kevinglasson/go/pkg/mod/github.com/stretchr/[email protected]/assert/assertions.go:260 +0x2cd
github.com/stretchr/testify/assert.NoError(0x1489fa0, 0xc0001cec60, 0x1489e40, 0xc00008e360, 0xc00004ae90, 0x1, 0x1, 0xc00004af78)
   /Users/kevinglasson/go/pkg/mod/github.com/stretchr/[email protected]/assert/assertions.go:1165 +0x10d
github.com/stretchr/testify/assert.(*Assertions).NoError(0xc0001df790, 0x1489e40, 0xc00008e360, 0xc00004ae90, 0x1, 0x1, 0x0)
   /Users/kevinglasson/go/pkg/mod/github.com/stretchr/[email protected]/assert/assertion_forward.go:622 +0xa4
github.com/knadh/koanf_test.TestWatchFile.func1(0x0, 0x0, 0x1489e40, 0xc00008e360)
   /Users/kevinglasson/Development/git/koanf/koanf_test.go:290 +0x9f
github.com/knadh/koanf/providers/file.(*File).Watch.func1(0xc0001bb980, 0xc0001c3ef0, 0xc0001df820, 0xc0001e28c0, 0xc0001df810, 0xc0001df7a0)
   /Users/kevinglasson/Development/git/koanf/providers/file/file.go:118 +0x55f
created by github.com/knadh/koanf/providers/file.(*File).Watch
   /Users/kevinglasson/Development/git/koanf/providers/file/file.go:61 +0x1a2
exit status 2
FAIL    github.com/knadh/koanf  2.043s

Question: Correct way of reading int args from flag

I've a flag defined like:

f.Int("ndots", 0, "Specify the ndots parameter. Default value blah...")

When I use Unmarshal:

err = k.Unmarshal("", &hub.QueryFlags)

where QueryFlags struct has:

type QueryFlags struct {
	Ndots            int           `koanf:"ndots"`

}

hub.QueryFlags.Ndots is set to 0 only even after passing --ndots=5 from CLI.

I changed the flag defintion from f.Int to f.IntP and now hub.QueryFlags.Ndots is correctly set to 5.

f.IntP("ndots", "", 0, "Specify the ndots parameter. Default value blah...")

Is this the correct behaviour? Because for bool, the unmarshal works normally with f.Bool, I don't have to use f.BoolP.

Thanks!

defect: Get does not respect the parser

When a user uses yaml.Parser to parse the file, all the marshaling and unmarshaling of the data should use that parser.

Explanation:
file.yml

int: 1
ints: [1,2,3]

When doing k.Get("int"), it returns the type that was loaded by the provider. It returns: 1 with type int.
When doing k.Get("ints"), it is using JSON.Marshal and JSON.Unmarshal and its erases its type. It returns [1.0, 2.0, 3.0] with type []float64.

Using yaml.Marshal and Yaml.Unmarshal solves this issue. It returns [1, 2, 3] with type []int.

I think the given provider should be used in the entire lifecycle.

WDYT?

Edit:
The provider cannot be used since it expectss map[string]interface{}

Edit 2:
As a workaround, I can wrap the result with map[string]interface[} and use the load parser.
An implication of this would mean that maps.Copy will need to receive a parser.

Register flags from provided config struct

It would be nice if Koanf would be able to register flags automatically, so that I parse my config properties from YAML, env variables and also flags without touching a lot of the code. With YAML and env variables this is already possible, but as far as I understood I'd need to register flags manually in order to parse them with Koanf.

`TestWatchFile` and `TestWatchFileSymlink` are non-deterministic

Executing go test ./... on commit 5234867 leads to the following output:

--- FAIL: TestWatchFile (2.04s)
    koanf_test.go:431: 
                Error Trace:    koanf_test.go:431
                Error:          Not equal: 
                                expected: "name2"
                                actual  : ""
                            
                                Diff:
                                --- Expected
                                +++ Actual
                                @@ -1 +1 @@
                                -name2
                                +
                Test:           TestWatchFile
                Messages:       file watch reload didn't change config
--- FAIL: TestWatchFileSymlink (2.52s)
    koanf_test.go:482: 
                Error Trace:    koanf_test.go:482
                Error:          Not equal: 
                                expected: "yml"
                                actual  : ""
                            
                                Diff:
                                --- Expected
                                +++ Actual
                                @@ -1 +1 @@
                                -yml
                                +
                Test:           TestWatchFileSymlink
                Messages:       symlink watch reload didn't change config
FAIL
FAIL    github.com/knadh/koanf  4.939s
?       github.com/knadh/koanf/examples/default-values  [no test files]
?       github.com/knadh/koanf/examples/read-commandline        [no test files]
?       github.com/knadh/koanf/examples/read-environment        [no test files]
?       github.com/knadh/koanf/examples/read-file       [no test files]
?       github.com/knadh/koanf/examples/read-raw-bytes  [no test files]
?       github.com/knadh/koanf/examples/read-s3 [no test files]
?       github.com/knadh/koanf/examples/read-struct     [no test files]
?       github.com/knadh/koanf/examples/read-vault      [no test files]
?       github.com/knadh/koanf/examples/unmarshal       [no test files]
?       github.com/knadh/koanf/examples/unmarshal-flat  [no test files]
ok      github.com/knadh/koanf/maps     0.245s
?       github.com/knadh/koanf/parsers/dotenv   [no test files]
?       github.com/knadh/koanf/parsers/hcl      [no test files]
?       github.com/knadh/koanf/parsers/json     [no test files]
?       github.com/knadh/koanf/parsers/toml     [no test files]
?       github.com/knadh/koanf/parsers/yaml     [no test files]
?       github.com/knadh/koanf/providers/basicflag      [no test files]
?       github.com/knadh/koanf/providers/confmap        [no test files]
?       github.com/knadh/koanf/providers/env    [no test files]
?       github.com/knadh/koanf/providers/file   [no test files]
?       github.com/knadh/koanf/providers/posflag        [no test files]
?       github.com/knadh/koanf/providers/rawbytes       [no test files]
?       github.com/knadh/koanf/providers/s3     [no test files]
ok      github.com/knadh/koanf/providers/structs        0.283s
?       github.com/knadh/koanf/providers/vault  [no test files]
FAIL

go version: go1.16.4 darwin/amd64
OS: macOS Big Sur 11.4

file watch handler is called twice

Hello,

I've registered a handler for the file watcher like this:

var (
	k = koanf.New(".")
	f = file.Provider("./config.json")
)

f.Watch(loadConfig)

And the handler like this:

func loadConfig(event interface{}, err error) {

	if err != nil {
		log.Error(err)
		return
	}

	if err = k.Load(f, json.Parser()); err != nil {
		log.Error(err)
		return
	}

	log.Infof("... update configuration ...")
	config = Config{}
	k.Unmarshal("", &config)
}

Unfortunately the info message is always printed twice, when I save the file. Do you have an idea what might be the problem here?

Thanks in advance,
Marko

Values not being loaded

I'm working with koanf and it looks like maps and some custom values are not being loaded properly. They appear to be empty even if the toml config file I'm using has values specified.

Is there a delay or similar on reading values from a config file?

Implement ristretto caching for even faster lookups

First of all, I have spent so much time debugging and optimizing spf13/viper. We've been using it everywhere at github.com/ory and it's just such a pain. So I wanted to say thank you, this library works like a charm! The interfaces are nice and clean. Spent a whole day siphoning through all the different libraries yesterday and started implementing a backwards compatible interface with our current config solution.

To speed up spf13/viper, we added ristretto cache with pretty good results:

BenchmarkFind/cache=true-16          	  231657	      5056 ns/op	    1650 B/op	      41 allocs/op # viper with cache
BenchmarkKoanf/check_keys-16         	 1194748	      1010 ns/op	     317 B/op	       4 allocs/op # viper without cache

For the benchmark we used a big config from various sources and did a key look ups on all the keys in the config. I implemented the same benchmark for koanf without caching

		for i := 0; i < b.N; i++ {
			key = keys[i%numKeys]

			if k.Get(key) == nil {
				b.Fatalf("cachedFind returned a nil value for key: %s", key)
			}
		}

and with ristretto caching

		for i := 0; i < b.N; i++ {
			key = keys[i%numKeys]

			if val, found = cache.Get(key); !found {
				val = k.Get(key)
				_ = cache.Set(key, val, 0)
			}

			if val == nil {
				b.Fatalf("cachedFind returned a nil value for key: %s", key)
			}
		}

The results show that we can more than half allocs, b/op and ns/op and double throughput if ristretto is used:

BenchmarkKoanf/cache=false-16         	        34237884	       344 ns/op	     100 B/op	       2 allocs/op
BenchmarkKoanf/cache=true
BenchmarkKoanf/cache=true/config=2
BenchmarkKoanf/cache=true/config=2-16 	78600840	       157 ns/op	      43 B/op	       1 allocs/op

Because of the way String and other functions work (they use Get as an underlying fetch mechanism), it is not possible to add the cache "on top" without much overhead, which is why I am opening this issue.

Reconsider internal use of `keyMap`

We are using koanf in production for a multi-tenant system and are having serious issues with memory consumption when a number (~5000) of configs is loaded that have a medium amount of config values (~85 flattened key/value pairs) with memory spikes of up to 7 GB.

Benchmarking and memory profiling shows that a lot of time is spent in maps.Flatten and koanf.populateKeyParts hence my two recent PRs:

Bildschirmfoto 2021-06-07 um 13 34 44

I think some of this boils down to KeyMap which contains the flattened keys in string and slice form. I looked through the code and it appears that the feature is not really used, or where it is used, strings.Split could be used instead which would probably reduce the memory count quite a bit because we avoid keeping these in memory:

koanf.keyMap = map[string][]string{
  "foo": []string{"foo"},
  "foo.bar": []string{"foo", "bar"},
  "foo.bar.baz1": []string{"foo", "bar", "baz1"},
  "foo.bar.baz2": []string{"foo", "bar", "baz2"},
  "foo.bar.baz3": []string{"foo", "bar", "baz3"},
}

As you can see, the amount of memory explodes the more nested keys are used because the same keyparts are copied over and over. As Koanf strictly uses slice copying to avoid modification (I assume?) this leads to a lot of unnecessary memory use.

The performance impact of using strings.Split("foo.bar.baz3", delimiter) should be negligible compared to the amount of data kept in memory.

The only place where we would need to generate this keymap as it exists right now would be the KeyMap() function if we want to keep compatibility.

Feature request: MergeUnder (or path argument for all providers)

I have an instance of struct I want to load in a Koanf under a specific path. For example some ftp settings defined as:

type FTPConf struct {
    User string     `koanf:"user"`
    Password string `koanf:"password"`
    Addr string     `koanf:"addr"`
}

The only solution I have found so far is to convert it to a map[string]interface{} and load it using the confmap.Provider which handles parent keys:

ftp := FTPConf{}
ftpMap := map[string]interface{}{
    "ftp.user": ftp.User,
    "ftp.password": ftp.Password,
    "ftp.addr": ftp.Addr,
}

mainKoanf := koanf.New(".")
mainKoanf.Load(confmap.Provider(ftpMap, "koanf"), nil)

One option would be to have all providers accept a path settings. For example, the structs.Provider would be defined as structs.Provider(s interface{}, tag, path string). This would allow to do:

ftp := FTPConf{}

mainKoanf := koanf.New(".")
mainKoanf.Load(structs.Provider(ftp, "koanf", "ftp"), nil)

However that will require changes in all providers and break their API).

It might be easier and cleaner to have a MergeUnder(in *Koanf, path string) method, similar to the Merge but with an extra path parameter. (Or the somewhat opposite of Cut(path string)).

Then the child config can be loaded as a separate Koanf and then merged in the parent Koanf.
Such as:

parentKoanf := koanf.New(".")

ftp := FTPConf{}
ftpKoanf := koanf.New(".")
ftpKoanf.Load(structs.Provider(ftp, "koanf"), nil)

ftpKoanf.MergeUnder(parentKoanf, "ftp")

I think this would be a useful features when loading config values from multiple sources.

Consider exposing the delimiter for a koanf instance

I am writing a custom provider that has a different delimiter than another one.
I cannot access the

// Koanf is the configuration apparatus.
type Koanf struct {
...
	conf        Conf
}

the conf property is private but there's no func to get it back.

Let's consider the following snippets:

func Provider(ctx *cli.Context, flagDelim string, ko *koanf.Koanf, koDelim string) *Cli {
	return &Cli{
		delim:   flagDelim,
		ko:      ko,
		koDelim: koDelim,
		ctx:     ctx,
	}
}

Here, I have to explicitly pass the koDelim for the ko instance.
This is because I'm using this in here:

func (p *Cli) Read() (map[string]interface{}, error) {
...
			if p.ko != nil {
				newFlag := strings.ReplaceAll(flagName, p.delim, p.koDelim)
				if p.ko.Exists(newFlag) {
					continue
				}
			}

Let's say the provider was instantiated with flagDelim="-" but ko was passed with delim=".".
In order to check for an existing key, I have to replace the delimiter in flagName first.

It would be cool to have some sort of p.ko.Delim() method or similar for direct access to remove an "unnecessary" parameter.
Or is there a better way?

Feature request: path groups

Hi, thank you for great project!

Sometimes, instead of writing

k.String("parent1.name")
k.Int("parent1.id")

One want to use following, because it is pretty easy to make a typo while repeating prefixes:

k = k.Group("parent1")
k.String("name")
k.Int("id")

Probably it can be implemented by some kind of wrapper, but this feature seems to be pretty lightweight and land to koanf package directly.

What do you think about this? I can eventually make a PR if you are OK with this feature.

Support validation

The library currently does not seem to support input sanitation as "first-class citizens".

The entire library centres around accepting user input from different sources. One of the golden rules of defensive programming (or just sane coding practices, as some would say...) is to never trust user input.

Currently, if I want to validate the my users input (other than validating by type), I'll have to create some unholy, ham-fisted hack by combining koanf with go-validator (I'll be honest, I haven't looked at actually doing that yet as I just recently stumbled upon koanf).

Would you guys be open to extending koanf to support something like go-validator? I'd be happy to help create the code, I'd just need some guidance as to how you guys would like it integrated.

Provide a way to load a structure

Hi, thanks for this cool library !

I think it would be a nice feature to be able to load a structure by providing path and structure pointer.

I made a quick POC, I bet with bugs, but idea is to marshal map[string]interface{} to json then unmarshal inside structure:

func (k *Koanf) LoadStruct(path string, ptr interface{}) error {
	sv, ok := k.Get(path).([]interface{})
	if !ok || len(sv) != 1 {
		return errors.New("cannot load struct")
	}
	sm, err := json.Marshal(sv[0])
	if err != nil {
		return err
	}
	if err = json.Unmarshal(sm, ptr); err != nil {
		return err
	}
	return nil
}

IMO it's not a good implementation because of encoding/decoding.
I wonder if we could "easily" implement such feature on an efficient way ?

Thanks

Failure to Unmarshal, without err

In certain situations, koanf will fail to Unmarshal, but will not report any errors. Here is a minimal example:

package main

import (
	"fmt"

	"github.com/knadh/koanf"
	"github.com/knadh/koanf/parsers/yaml"
	"github.com/knadh/koanf/providers/rawbytes"
)

var data = `
foo: bar
things:
  - name: John
    baz: abc
  - name: Jane
    baz: xyz
`

type Cfg struct {
	Foo    string
	Things []struct {
		Name string
		Baz  string
	}
}

func main() {
	var cfg Cfg

	k := koanf.New(".")
	if err := k.Load(rawbytes.Provider([]byte(data)), yaml.Parser()); err != nil {
		panic(err)
	}

	if err := k.Unmarshal("", &cfg); err != nil {
		panic(err)
	}

	if cfg.Foo == "" {
		fmt.Println("failed to unmarshal")
	}
	fmt.Printf("Cfg: %+v\n", cfg)
}

Neither of the panics trip. But rather, the cfg struct is returned completely blank, empty. So the output is:

failed to unmarshal
Cfg: {Foo: Things:[]}

I've traced this a little bit and found it to go wrong during maps.Copy. The json error is ignored here.

b, _ := json.Marshal(mp)

When it's checked, the error is returning:

json: unsupported type: map[interface {}]interface {}

Watch example in README does not remove keys / values

The current README example around file watching could be improved by making the limitations of that approach clear. Removing a value from the config file (e.g. parent1.id) will not remove the value from the config but keep the old value around. It would probably be expected for the removed value to be no longer available in the config as it was removed from the file.

Example

Run program and have mock.json with:

{
  "parent1": {
    "name": "foo",
    "id": 34
  }
}
% go run .
parent's name is =  bar
parent's ID is =  34

Remove all keys from mock.json

{}

Config reloads, but values are still present:

2021/06/05 17:43:19 config changed. Reloading ...
parent1.id -> 34
parent1.name -> bar

Setting Property in Array via Environment

Hi There!

I have trouble with setting a specific property of an array via its environment variable.
Here's an example, let's say I have a configuration file like this:

providers:
    - id: google
      provider: google
      client_id: ''
      client_secret: ''
      ...
    - id: github
      ...

One could add many providers and each provider has its own client_id and client_secret.

So now let's say I want to add the client_secret to the google provider.
How would the environment variable look? Also would it override the rest of the providers or the other provider properties?
I want it to look something like this SELFSERVICE_METHODS_OIDC_CONFIG_PROVIDERS_GOOGLE_CLIENT_SECRET?

The issue is tracked from another project here:
ory/kratos#1186 (reply in thread)

Unmarshal error with int slice using pflag

follow the code.

package main

import (
	"fmt"
	"os"

	"github.com/knadh/koanf/providers/posflag"
	flag "github.com/spf13/pflag"

	"github.com/knadh/koanf"
)

var instance = koanf.New(".")

type c struct {
	Name string
	Numbers []int
	Texts []string
}

func main() {

	f := flag.NewFlagSet("config", flag.ContinueOnError)

	f.Usage = func() {
		fmt.Println(f.FlagUsages())
		os.Exit(0)
	}

	f.String("name", "my name", "name example")
	f.IntSlice("numbers", []int{0,9,10}, "numbers example")
	f.StringSlice("texts", []string{"text1", "text2", "text3"}, "texts example")

	if err := f.Parse(os.Args[1:]); err != nil {
		panic(err)
	}

	flap := posflag.Provider(f, ".", instance)

	if err := instance.Load(flap, nil); err != nil {
		panic(err)
	}

	out := c{}
	if err := instance.Unmarshal("", &out); err != nil {
		panic(err)
	}

	fmt.Println(out)
}

below is the error.

panic: 1 error(s) decoding:

* cannot parse 'Numbers[0]' as int: strconv.ParseInt: parsing "[0,9,10]": invalid syntax

goroutine 1 [running]:
main.main()
        ...main.go:46 +0x576

Process finished with exit code 2

json.RawMessage not taken into account when loading json file

Hello,

I have the following use-case :
In a json configuration file, I have a list of items for which I cannot know in advance the types of. I will unmarshal the Koanf object in a structure where these items are stored in a json.RawMessage so that they can be decoded later on.

Below an example and the unexpected behaviour I encountered (link to go playground) :

package main

import (
	"encoding/json"
	"fmt"

	"github.com/knadh/koanf"
	kson "github.com/knadh/koanf/parsers/json"
	"github.com/knadh/koanf/providers/rawbytes"
	"github.com/knadh/koanf/providers/structs"
)

type Parent struct {
	A int             `json:"A"`
	B json.RawMessage `json:"B"`
}

type Child struct {
	C string `json:"C"`
}

func main() {

	var k *koanf.Koanf
	var p Parent
	var err error

	k = koanf.New(".")

	err = json.Unmarshal([]byte(`{"A":3, "B":{"C":"string"}}`), &p)
	if err != nil {
		fmt.Println(err)
	}

	err = k.Load(structs.Provider(p, "json"), nil)
	if err != nil {
		fmt.Println(err)
	}

	k.Print()

	k = koanf.New(".")
	err = k.Load(rawbytes.Provider([]byte(`{"A":3, "B":{"C":"string"}}`)), kson.Parser())
	if err != nil {
		fmt.Println(err)
	}
	k.Print()
}

I would expect the same results with both methods, meaning a slice of bytes stored in B paths. However, the resulting paths for the Koanf object show a B.C path with value "string" when using directly the json parser. Also, trying to unmarhsal in p causes the following error : expected type 'uint8', got unconvertible type 'map[string]interface {}', value: 'map[C:string]'.

I found a workaround using a specific DecodeHook passed in the UnmarshalConf, but I'm not fully satisfied as this may lead to unintended side effects. I would expect it to be handled natively when using a json parser.

Below the workaround I used :

k.UnmarshalWithConf("", c, koanf.UnmarshalConf{Tag: "json", DecoderConfig: &mapstructure.DecoderConfig{
	Metadata: nil,
	Result:   c,
	// koanf relies on mapstructure to unmarshal data. In our case we have to
	// parse some parts as json.RawMessage, which is not supported natively by mapstructure.
	// This hook marshals the map[string]interface{} read if the target is a []byte (json.RawMessage is an underlying []byte).
	DecodeHook: func(f reflect.Type, t reflect.Type, data interface{}) (interface{}, error) {
		if t.Kind() == reflect.Slice {
			if t.Elem().Kind() == reflect.Uint8 {
				fmt.Println(data)
					return json.Marshal(data)
			}
		}
		return data, nil
	},
}})

Is this behaviour intended ? If yes, how about adding support for json.RawMessage when loading with a json parser ?

posflag: defaults override when using callback to modify key names

So currently it seems if you use a callback function to modify the key name when using the posflag provider, if the key name is modified and the original key name was not in the existing key map, it overrides the value with the default even if the posflag was not changed.

If you need an example I can show you.

Loading a string slice from environment variables

Hey,
I'm trying to load a string slice (comma separated) from an environment variable and it seemed like the example in the readme handles exactly that. I wasn't able to get it work using the example:

The example (return is by the way wrong, it should return key , v I think?

	k.Load(env.ProviderWithValue("MYVAR_", ".", func(s string, v string) (string, interface{}) {
		// Convert the environment variable key to the koanf key format
		key := strings.Replace(strings.ToLower(strings.TrimPrefix(s, "MYVAR_")), "_", ".", -1)
		// Check to exist if we have a configuration option already and see if it's a slice
		switch k.Get(key).(type) {
		case []interface{}, []string:
			// Convert our environment variable to a slice by splitting on space
			return key, strings.Split(v, " ")
		}
		return key, s // Otherwise return the new key with the unaltered value
	}), nil)

I adapted the example by removing the prefix and using , as separator instead of the used whitespace. So for my env variable KAFKA_BROKERS the key is then kafka.brokers. But when it looks up it's type I noticed that Get() returns nil because the keymap of the koanf instance doesn't contain kafka.brokers at that point. What am I missing?

More code context (if needed): https://github.com/cloudhut/kminion/blob/51ef31dea4db69a662ce4af36185330dde2dd487/config.go#L54-L116

How to provide string slice from environment variables

Any thoughts on how to provide a string slice from an environment variable? Viper looks at the existing value and if the default is a slice it interprets the environment variable as a slice. It almost would have been better to return (string, interface{}) from the callback and allow the callback to manipulate the value as well.

Bug report 1

Describe the bug
A clear and concise description of what the bug is.

To Reproduce
Steps / breaking unit test / example to reproduce the behavior

Expected behavior
A clear and concise description of what you expected to happen.

Please provide the following information):

  • OS: [e.g. linux/osx/windows]
  • Koanf Version [e.g. v1.0.0]

Additional context
Add any other context about the problem here.

Default empty []string unmarshalls as a nil

When I have a string slice that defaults (via flags) to empty slice ([]string) then it gets unmarshalled as nil.

This isn't really a bug in koanf but in mitchellh/mapstructure#146
Reporting here since koanf uses mapstructure directly.
Is there any way to workaround this in koanf? mitchellh/mapstructure didn't get any commit in a while so looks like the issue might not get fixed, in that case is there an alternative koanf could use?

mapstructure: reflect: reflect.Value.Set using unaddressable value

Problem

Error: "reflect: reflect.Value.Set using unaddressable value"

I'm getting the error above with the function "Koanf.UnmarshalWithConf":
instance.UnmarshalWithConf(path, &o, koanf.UnmarshalConf{Tag: "config"})
path is pointing to the bellow yaml structure, and &o is a pointer to a variable of type map[string][]string.

mystructure:
  TESTE1:
    - "ABC"
    - "DDD"
    - "CDA"
  TESTE2:
    - "CDA"
    - "CDA+DA"

Solution:

I updated the mapstructure lib to the latest version and it worked accordingly.

Disable tag support

Hello,

Is it possible to disable tag support with UnmarshalWithConf so we could unmarshal a konf instance to a struct without having to provides tags?

Reference: https://github.com/knadh/koanf/blob/master/koanf.go#L199

Like adding a new field called DisableTag as a boolean (in order to avoid compatibility break).

Let me know what you think.
Thank you in advance.

Int64s returning empty slice when math.MaxInt64 used

testdata/config.yaml:


config:
  int64s: [9223372036854775807,9223372036854775807,9223372036854775807]

config_test.go


import (
	"fmt"
	"github.com/knadh/koanf"
	"github.com/knadh/koanf/parsers/yaml"
	"github.com/knadh/koanf/providers/file"
	"log"
	"math"
	"testing"
)

var k = koanf.New(".")

func TestConfig_Int64s(t *testing.T) {
	if err := k.Load(file.Provider("testdata/config.yaml"), yaml.Parser()); err != nil {
		log.Fatalf("error loading config: %v", err)
	}
	vs := k.Int64s("config.int64s")
	fmt.Printf("%#v", vs)
	expected := []int64{math.MaxInt64,math.MaxInt64,math.MaxInt64}
	for i, e := range expected {
		v := vs[i]
		if v != e {
			t.Fatalf("'%v' != '%v'", v, expected)
		}
	}
}

Result


=== RUN   TestConfig_Int64s
[]int64{}--- FAIL: TestConfig_Int64s (0.00s)
panic: runtime error: index out of range [0] with length 0 [recovered]
	panic: runtime error: index out of range [0] with length 0

Reason

Json Marshal and Unmarshal in func(ko *koanf) Get(path string) interface{} converts int to float64, see attached screenshot

koanf

feature request: control merging behavior

When a user loads multiple YAML files that have the same key with different types the last type loaded is deciding the loaded type.
For example:
first.yml

key: value

second.yml

key: [1,2,3]

The loaded value into the key field will be [1,2,3] if second.yml will be loaded last.
I would like to control the behavior that it could be "loss" with the current behavior and "strict" with type enforcing in the merge.

If it makes sense, I could try to work on it with some guidance 👍
Or if it already exists, I'd appreciate the 'how-to'.

Support for unmarshalling CLI Flags with dashes

Hi
In the case where we want to have CLI flags with dashes (e.g. --commit-limit), how could an implementation look like so that Environment variables could also be used in the Unmarshalling process?

The order of precedence should be
CLI flags override -> ENV var override -> internal hardcoded defaults

Example:

type (
Configuration struct {
		Git       GitConfig      `koanf:",squash"`
        }
GitConfig struct {
		CommitLimit  int    `koanf:"commit-limit"`
	}
)
...
cmd.PersistentFlags().IntP("commit-limit", "l", defaults.Git.CommitLimit,
		"Only look at the first ...")
...
	err := k.Load(env.Provider("", ".", func(s string) string {
		return strings.Replace(strings.ToLower(s), "_", ".", -1)
	}), nil)
	if err != nil {
		log.WithError(err).Fatal("Could not load environment variables")
	}
        err = k.Load(posflag.Provider(flagSet, ".", k), nil)
	if err != nil {
		log.WithError(err).Fatal("Could not bind flags")
	}

	if err := k.Unmarshal("", &config); err != nil {
		log.WithError(err).Fatal("Could not read config")
	}

I tried several combinations to unmarshal into the Configuration struct:

COMMIT_LIMIT
COMMITLIMIT
GIT_COMMIT_LIMIT
GIT_COMMITLIMIT

It's interesting that Viper can do that (using COMMIT_LIMIT), but it seems not so easy with Koanf. The problem is basically recognizing whether an underscore is used as dash or as delimiter. Any ideas?

The exact code used is in this PR: appuio/seiso#31

Remove `Provider.Watch`

Method Watch(func(event interface{}, err error)) error is actually not used by Koanf (unlike Load and ReadBytes) itself and serves no purpose except for adding bloat to providers who do not support Watching (e.g. env or flags). As an implementer of my own Provider I want to use a different watch mechanism with channels (instead of a callback) which means I have to implement two watchers, or not use channels.

Env var example doesn't work

I tried to use your example for overriding a config key with an environment variable, but it doesn't work out of the box.

To give you some context I am building an application which will read its default config from a yaml file, then override that with environment variables set by either docker or helm depending on which environment the app is running in.

I created my default config with a key called environment expecting to be able to override that with an env var called KOANF_ENVIRONMENT as per your example.

What actually happened was that the existing key was not overridden, instead a new key was created called koanf_environment

Please update your example code to be something like, to save someone else falling down the same rabbit hole:

k.Load(env.Provider("KOANF_", ".", func(s string) string {
		return strings.ToLower(strings.TrimPrefix(s, "KOANF_"))
	}), nil)

Bug report 2

Describe the bug
A clear and concise description of what the bug is.

To Reproduce
Steps / breaking unit test / example to reproduce the behavior

Expected behavior
A clear and concise description of what you expected to happen.

Please provide the following information):

  • OS: [e.g. linux/osx/windows]
  • Koanf Version [e.g. v1.0.0]

Additional context
Add any other context about the problem here.

Probable performance issue as compared to pelletier

Hi,
Trying to migrate from viper (toml)

Using koanf: Time taken :718.4µs (Multiple runs do reduce the time though)
Using pelletier: Time taken :384.125µs

go version go1.13.6 darwin/amd64

Code using koanf:
`package main

import (
"fmt"
"time"

"github.com/knadh/koanf"
"github.com/knadh/koanf/parsers/toml"
"github.com/knadh/koanf/providers/file"

)

type Configuration struct {
}

var (
Config *Configuration = nil
)

// Global koanf instance. Use "." as the key path delimiter. This can be "/" or any character.
var k = koanf.New(".")

func main() {
start := time.Now()

f := file.Provider("config.toml")

// Load toml config and merge into the previously loaded config (because we can).
err := k.Load(f, toml.Parser())
if err != nil {
	fmt.Printf("%s", err.Error())
	return
}
var readConfig Configuration

k.Unmarshal("", &readConfig)
Config = &readConfig

fmt.Println("Time taken :%s", time.Since(start))

}

`

Code with pelletier:

`package main

import (
"fmt"
"time"

"github.com/pelletier/go-toml"

)

type Configuration struct {
}

var (
Config *Configuration = nil
)

func main() {
start := time.Now()

config, err := toml.LoadFile("config.toml")
if err != nil {
	fmt.Println("Error ", err.Error())
	return
}

var readConfig Configuration
err = config.Unmarshal(&readConfig)
if err != nil {
	fmt.Println("Unmarshal error ", err.Error())
	return
}
Config = &readConfig

fmt.Println("Time taken :", time.Since(start))

}`

Decryption of config

I was wondering if integration decryption is worth integrating

Sops now has age support. Sops is an alternative to vault from hashicorp.

The use case is that I need to give software to users and some of the config needs to include api keys that we don’t want to be leaked.
A but like how devs accidentally save an api key into their git repo for GC loud credentials - I did it a few days ago and google rang me via telephone.

Here is a good summary.

https://oteemo.com/2019/06/20/hashicorp-vault-is-overhyped-and-mozilla-sops-with-kms-and-git-is-massively-underrated/

How to read a list of tuples (or 3-tuples)?

Hi,

I'm actually having the same problem with koanf that I was having with Viper, supporting a complex type across config, flags, and environment values.

In the two use cases I've run into with my code, the user needs to be able to provide me with either a tuple or a 3-tuple, repeated.

My program is run in a container, (often kubernetes) so I need to give flexibility to the user, providing configuration via their choice of file, flag, or environment variables. Using koanf, that's the easy part, I have a prototype that first tries to load from the config file, then overrides successively with flag, then environment variables.

However, I can't figure out how to specify/code to support a complex variable across the three types. A simple string is easy, but not lists of tuples/structs.

As a concrete example, imagine the user is passing me named environments, each of which takes two strings that are uri's:

type Environment struct {
  Name string
  Frontend string
  Database string
}

type Config struct {
  Environments []Environment
}

This could also be:

type Environment struct {
  Frontend string
  Database string
}

type Config struct {
  Environments map[string]Environment
}

I'm flexible with forcing the user to do different things for how the values are passed via each method, but I can't figure out how to do this in koanf.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.