Skip to content

The zero configuration configuration package

License

Notifications You must be signed in to change notification settings

brumhard/alligotor

Repository files navigation

Alligotor

golangci-lint Go Report Card Go Reference

The zero configuration configuration package.

Install

go get github.com/brumhard/alligotor

What is Alligotor?

Alligotor is designed to be used as the configuration source for executables (not commands in a command line application) for example for api servers or any other long-running applications that need a startup config.

It takes only a few lines of code to get going, and it supports:

  • setting defaults just like you're used to from for example json unmarshalling (see this example)
  • reading from YAML and JSON files from io.Reader, local file system or fs.FS
  • reading from environment variables
  • reading from command line flags
  • defining custom source to load config from your preferred source (e.g. etcd)
  • extremely simple API
  • support for every type (by implementing TextUnmarshaler) and out of the box support for many common ones
  • autogenerated property names for each child property in the config, but still configurable via struct tags
  • set overwrite order by defining the sources in the preferred order in alligotor.New()

Why Alligotor?

There are a lot of configuration packages for Go that give you the ability to load you configuration from several sources like env vars, command line flags or config files.

Alligotor was designed to have the least configuration effort possible (autogenerating the property names for the source trough reflection) while still keeping it customizable. So for example if a config struct looks like the following:

type cfg struct {
    API struct {
        Port int
    }
}

The port value will be loaded by default from the env variable <PREFIX>_API_PORT and the flag --api-port without the need to set that explicitly.

That's why if you keep the package defaults you only need one function call, and your config struct definition to fill this struct with values from environment variables, several config files and command line flags or your defined custom source.


Known unsupported usecases

Read directly into properties of embedded structs

Generally embedded structs are supported but certain use cases don't work. So for example in the following struct:

type DB struct {
    Host string
}

type Config {
    DB
}

You can set the value for the DB.Host with the env variable <PREFIX>_DB_HOST but not with <PREFIX>_HOST directly.

Arrays

Since there is no nice way of representing arrays in all config sources (for example environment variables) it's currently not supported in these sources.

The ReadersSource on the other hand can easily read arrays.


Minimal example

package main

import (
 "github.com/brumhard/alligotor"
 "go.uber.org/zap/zapcore"
 "time"
)

func main() {
 // define the config struct
 cfg := struct {
  SomeList []string
  SomeMap  map[string]string
  API      struct {
   Enabled  bool
   LogLevel zapcore.Level
  }
  DB struct {
   HostName string
   Timeout  time.Duration
  }
 }{
  // could define defaults here
 }

 // get the values
 _ = alligotor.Get(&cfg)
}

Just like with the json package alligotor only supports setting public properties since it relies on reflection.


Custom setup

As alligotor aims for good customizability, the Collector's constructor supports as many sources as you like. Included in the package are one source for env vars, one for config files (supporting readers, local file system or fs.FS) and one for cli flags (see sources).

It is shown in the following example.

// all predefined sources
_ = alligotor.New(
    alligotor.NewFilesSource("./test_config.*"),
    alligotor.NewEnvSource("TEST"),
    alligotor.NewFlagsSource(),
)

// only from env vars with prefix "TEST" and custom separator
_ = alligotor.New(
    alligotor.NewEnvSource("TEST", alligotor.WithEnvSeparator("::")),
)

As shown in the latter example, the sources support an option to set a custom separator. In case it is not set explicitly, it will be set to the defaults:

  • env vars: _ (underscore)
  • cli flags: . (dash)

Sources

For each of the following sources the following example config struct is used.

// example struct
type Config struct {
    Enabled bool
    Sub struct {
        Port int
    }
}

Environment variables

The source for environment variables can be used as follows:

_ = alligotor.New(
    alligotor.NewEnvSource("TEST", alligotor.WithEnvSeparator("::")),
)

It supports setting a custom prefix as well as a custom separator. The separator is needed for nested config structs.

So for example for the example struct from above and the defined source configuration the value for the Port field will be read from TEST::SUB::PORT.

Like with the other sources the properties name to look up can be changed by adding a struct tag. In that case add config:"env=something" as a struct tag for the Port field and it will be read from TEST::SUB::SOMETHING.

Commandline flags

The source for command line flags can be used as follows:

_ = alligotor.New(
    alligotor.NewFlagsSource(alligotor.WithFlagSeparator(".")),
)

It supports setting a custom separator, that is needed for nested structs.

So for example for the example struct from above and the defined source configuration the value for the Port field will be read from --sub.port.

Like with the other sources the properties name to look up can be changed by adding a struct tag. In that case add config:"flag=something" as a struct tag for the Port field and it will be read from --sub.something.

In addition the struct tag can be defined as config:"flag=p" to set the short name for the flag (-p) or any of config:"flag=p some" or config:"flag=some p" to overwrite the name and the short name.

To set a flags usage string in addition to the config struct tag also the description struct tag is read and set as the flags usage that is returned when the user requests help with --help or -h.

Files

The source for files can be used in one of the following ways:

_ = alligotor.New(
    // any io.Reader is supported
    alligotor.NewReadersSource(strings.NewReader(`{"key":"value"}`))
)

_ = alligotor.New(
    // reads from local fs in this case
    alligotor.NewFilesSource("dir/example_config.*", "test2/config.yml"),
)

_ = alligotor.New(
    // fsys has a type implementing fs.FS in this case
    alligotor.NewFSFilesSource(fsys, "dir/example_config.*", "test2/config.yml")
)

NewReadersSource reads the config file from any io.Reader so for example a file or an http endpoint. NewFilesSource and NewFSFilesSource are simple wrappers around the ReadersSource to find the files using glob patterns on any filesystem (either local FS or fs.FS). This differentiation is used since os.DirFS does not support propper relative and absolute paths for the local filesystem.

Reading from files works as expected (just like json or yaml unmarshaling). The only difference is that it looks for fields in a case-insensitive manner.

Of course also here the name can be defined by setting the struct tag to for example config="file=something" which works just like the json or yaml struct tag.

Currently, only yaml and json files are supported but others will be added if needed.

Struct tags

Struct tags are used to overwrite the name for the env source that is generated by default. They are defined in the following format:

type Config struct {
    Enabled bool `config:"key=value,key2=value2"`
}

where key could for example be file or env. The struct tag can also be consumed from custom sources from the Field property Field.Configs(), which contains a map from struct tag key to value.

Custom

Custom sources can be added by implementing the following interfaces. For an example on how to implement a config source take a look at the env source, which implements reading from environment variables in less than 100 lines of code.

ConfigSource

Each config source needs to implement at least the following interface.

type ConfigSource interface {
    Read(field *Field) (interface{}, error)
}

As shown it contains only one method that receives a Field instance and returns the value that was found for the field. For sources that only support setting values as strings (like for example environment variables) just return a byte slice containing the string and it will automatically be converted to the target type if possible. Any other type is used directly leading to an error on type mismatch.

You should not return structs directly since this could lead to errors if some struct properties are set and others are not. This would then overwrite the target with the zero value, which is not intended.

The received fields match directly to the fields in the config. So for example for a config struct like the following:

type Config struct {
    Sub struct {
        Field string
    }
}

two fields will be send to the Read function, one containing the whole sub struct and one referencing only the Field property. The structs are included to enable structs that implement the TextUnmarshaler interface. If no value is found for a specific field nil should be returned in order to not override any existing value for that field with an empty one.

ConfigSourceInitializer

If the custom config source depends on some initialization before reading the fields the ConfigSourceInitializer interface can be implemented as well. The method is invoked right before calling the Read function. In the existing sources this is used for example to read the the files to not do it for every field or read in the environment variables.

type ConfigSourceInitializer interface {
    Init(fields []Field) error
}