GithubHelp home page GithubHelp logo

segmentio / conf Goto Github PK

View Code? Open in Web Editor NEW
83.0 7.0 12.0 121 KB

Go package for loading program configuration from multiple sources.

License: MIT License

Go 99.78% Makefile 0.22%
command-line command-line-parser configuration configuration-file configuration-loader environment environment-variables environment-vars golang go

conf's Introduction

conf CircleCI Go Report Card GoDoc

Go package for loading program configuration from multiple sources.

Motivations

Loading program configurations is usually done by parsing the arguments passed to the command line, and in this case the standard library offers a good support with the flag package.
However, there are times where the standard is just too limiting, for example when the program needs to load configuration from other sources (like a file, or the environment variables).
The conf package was built to address these issues, here were the goals:

  • Loading the configuration has to be type-safe, there were other packages available that were covering the same use-cases but they often required doing type assertions on the configuration values which is always an opportunity to get the program to panic.

  • Keeping the API minimal, while the flag package offered the type safety we needed it is also very verbose to setup. With conf, only a single function call is needed to setup and load the entire program configuration.

  • Supporting richer syntaxes, because program configurations are often generated dynamically, the conf package accepts YAML values as input to all configuration values. It also has support for sub-commands on the command line, which is a common approach used by CLI tools.

  • Supporting multiple sources, because passing values through the command line is not always the best appraoch, programs may need to receive their configuration from files, environment variables, secret stores, or other network locations.

Basic Usage

A program using the conf package needs to declare a struct which is passed to conf.Load to populate the fields with the configuration that was made available at runtime through a configuration file, environment variables or the program arguments.

Each field of the structure may declare a conf tag which sets the name of the property, and a help tag to provide a help message for the configuration.

The conf package will automatically understand the structure of the program configuration based on the struct it receives, as well as generating the program usage and help messages if the -h or -help options are passed (or an error is detected).

The conf.Load function adds support for a -config-file option on the program arguments which accepts the path to a file that the configuration may be loaded from as well.

Here's an example of how a program would typically use the package:

package main

import (
    "fmt"

    "github.com/segmentio/conf"
)

func main() {
    var config struct {
        Message string `conf:"m" help:"A message to print."`
    }

    // Load the configuration, either from a config file, the environment or the program arguments.
    conf.Load(&config)

    fmt.Println(config.Message)
}
$ go run ./example.go -m 'Hello World!'
Hello World!

Environment Variables

By default, conf will look for environment variables before loading command-line configuration flags with one important caveat: environment variables are prefixed with the program name. For example, given a program named "foobar":

func main() {
	config := struct {
		Name string `conf:"name"`
	}{
		Name: "default",
	}
	conf.Load(&config)
	fmt.Println("Hello", config.Name)
}

The following will be output:

$ ./foobar                                    // "Hello default"
$ FOOBAR_NAME=world ./foobar                  // "Hello world"
$ FOOBAR_NAME=world ./foobar --name neighbor  // "Hello neighbor"
$ MAIN_NAME=world go run main.go              // "Hello world"

If you want to hard-code the prefix to guarantee immutability or just to customize it, you can supply a custom loader config:

loader := conf.Loader{
	Name: "my-service",
	Args: os.Args[1:],
	Sources: []conf.Source{
		conf.NewEnvSource("MY_SVC", os.Environ()...),
	},
}
conf.LoadWith(&config, loader)

Advanced Usage

While the conf.Load function is good enough for common use cases, programs sometimes need to customize the default behavior.
A program may then use the conf.LoadWith function, which accepts a conf.Loader as second argument to gain more control over how the configuration is loaded.

Here's the conf.Loader definition:

package conf

type Loader struct {
     Name     string    // program name
     Usage    string    // program usage
     Args     []string  // list of arguments
     Commands []Command // list of commands
     Sources  []Source  // list of sources to load configuration from.
}

The conf.Load function is actually just a wrapper around conf.LoadWith that passes a default loader. The default loader gets the program name from the first program argument, supports no sub-commands, and has two custom sources setup to potentially load its configuration from a configuration file or the environment variables.

Here's an example showing how to configure a CLI tool that supports a couple of sub-commands:

package main

import (
    "fmt"

    "github.com/segmentio/conf"
)

func main() {
    // If nil is passed instead of a configuration struct no arguments are
    // parsed, only the command is extracted.
    cmd, args := conf.LoadWith(nil, conf.Loader{
        Name:     "example",
        Args:     os.Args[1:],
        Commands: []conf.Command{
            {"print", "Print the message passed to -m"},
            {"version", "Show the program version"},
        },
    })

    switch cmd {
    case "print":
        var config struct{
            Message string `conf:"m" help:"A message to print."`
        }

        conf.LoadWith(&config, conf.Loader{
            Name: "example print",
            Args: args,
        })

        fmt.Println(config.Message)

    case "version":
        fmt.Println("1.2.3")
    }
}
$ go run ./example.go version
1.2.3
$ go run ./example.go print -m 'Hello World!'
Hello World!

Custom Sources

We mentionned the conf.Loader type supported setting custom sources that the program configuration can be loaded from. Here's the the conf.Source interface definition:

package conf

type Source interface {
    Load(dst Map)
}

The source has a single method which receives a conf.Map value which is an intermediate representation of the configuration struct that was received by the loader.
The package uses this type internally as well for loading configuration values from the program arguments, it can be seen as a reflective representation of the original value which exposes an API that is more convenient to use that having a raw reflect.Value.

One of the advantages of the conf.Map type is that it implements the objconv.ValueDecoder interface and therefore can be used directly to load configurations from a serialized format (like JSON for example).

Validation

Last but not least, the conf package also supports automatic validation of the fields in the configuration struct. This happens after the values were loaded and is based on gopkg.in/validator.v2.

This step could have been done outside the package however it is both convenient and useful to have all configuration errors treated the same way (getting the usage and help message shown when something is wrong).

conf's People

Contributors

abraithwaite avatar achille-roussel avatar deankarn avatar kevinburkesegment avatar manaswinidas avatar maxence-charriere avatar stevevls avatar tysonmote avatar yields 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

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

conf's Issues

string fields are not parsed when numbers are set in cmd line.

conf example:

type Config struct {
	Name             string `conf:"name" help:"Package name"`
	ID               string `conf:"id"   help:"UTI representing the app."`
}

if I launch my program like that:

myprog -id 42
myprog -id "42"

The usage description is displayed like if -h is used and instructions after conf.load are not called.

String configs that look like numbers can get corrupted on parse

Sample case:

package main

import (
	"fmt"

	"github.com/segmentio/conf"
)

func main() {
	var config struct {
		StringValue string `conf:"string-value"`
	}

	conf.LoadWith(&config, conf.Loader{
		Args: []string{"-string-value", "0123456789"},
	})

	fmt.Println(config.StringValue)
}

Outputs:

1.23456789e+08

Env Vars get interpreted incorrectly for default source

Using the default loader, it includes the file source with a templated yaml file.

It populates the template with the environment variables, then parses the yaml file. This causes strings like a: b to get interpreted as yaml kv pairs.

Not sure what the best course of action is, but probably needs to be addressed by escaping/cleaning the input somehow.

Common example scenario is addr:port URIs.

https://github.com/segmentio/conf/blob/master/source.go#L127-L132

Personalize help output

It would be awesome if we can personalize the -h output.
I m in the case where I want to specify a command as parameter.
e.g.

gomac build -name MyProg
gomac run -v

I can retrieve the command with the result of conf.Load
but if i don't find the command i want, I would like to print the help with a customized header like:

unknown command.

Usage:
   gomac [-h] [-help] [options...] [build | run]

Options:
... // The rest of the option output.

Have a way to set the usage would be nice.
Also is there a way to trigger manually the help output?

flags with string literal "null" get skipped

This was unexpected behavior for me. I imagine it has to do with JSON/yaml parsing support.

Perhaps documentation is all that's needed.

package main

import (
        "fmt"

        "github.com/segmentio/conf"
)

type config struct {
        Name string `conf:"name" help:"set me please"`
}

func main() {
        c := config{}
        conf.Load(&c)
        fmt.Println(c)
}
abraithwaite at alan-mbpr in ~GOPATH/src/github.com/segmentio
10:57:05 $ go run play.go -name null
{}

configure next source based on parsed conf

i have useful case - after parsing env or cmdline i need to configure consul and vault source and load conf from it
how to do that? before first source loaded (env and/or cmdline) i can't configure consul source

behavior with maps

I guess I expected based on the Map abstraction that you could deserialize config into either a map or a struct. One of the first tests I wrote did this:

       source := NewKubernetesConfigMapSource("./testdata/configmap")
       base := make(map[string]string)
       mp := makeNodeMap(reflect.ValueOf(base), reflect.TypeOf(base))
       if err := source.Load(mp); err != nil {
               t.Fatal(err)
       }

Which "worked" but produces a panic if you try Loader.Load.

We should maybe try to make more clear that you should only be passing structs, maybe by also panicking further down.

Special case the "version" string

We have a config that looks like e.g.

type config struct {
	Version         bool   `conf:"version" help:"Print the program version and exit."`
	S3Bucket        string `conf:"s3-bucket" help:"The S3 bucket to deliver logs batches to." validate:"nonzero"`
}

If you run ./myprogram --version you get the following error:

Error:
  invalid value passed to s3-bucket: zero value

Instead I expect to see the version number printed out. I'm not sure what the right approach would be.

Embedded structs don't have their config merged up

When processing command line flags, you must specify the embedded structs name or tag when loading configuration. This is unlike the behavior of other deserializers (json, primarily).

An example is much easier to understand than this is to explain:

abraithwaite at alan-mbpr in ~GOPATH/src/github.com/segmentio
15:58:58 $ go run play.go -name "blah" -D.location "SF"
Hello, blah
{"name": "asdf", "location": "NY"}
{"location":"SF","name":"blah"}
abraithwaite at alan-mbpr in ~GOPATH/src/github.com/segmentio
15:59:14 $ go run play.go -name "blah" -location "SF"
Usage:
  play [-h] [-help] [options...]

Options:
  -D object

  -D.location string

  -config-file source
        Location to load the configuration file from.

  -name string
        (default blah)

Error:
  flag provided but not defined: -location

exit status 1

For the file: https://gist.github.com/abraithwaite/3f74d9fbd06dbcba0c2ac14d61cc2a09#file-play-go

It would be preferable (although it would be a breaking change, maybe that can be avoided) for the properties on D to be merged up to C without needing a prefix. Perhaps we can get away with a special tag like, but not equal to -.

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.