A flexible and composable configuration library for Go that doesn't suck
APACHE-2.0 License
We know that there are plenty of other Go configuration and CLI libraries out there already - insert obligatory xkcd... π Unfortunately, most (all?) of them suffer from at least one of these serious issues and limitations:
os.Args()
or os.Environ()
or some other shared global stateencoding.TextUnmarshaler
null
-able or other custom wrapper types for such informationstring
-y:
strconv.ParseInt
orThe impetus for croconf was k6's very complicated configuration. We have a lot of options and most options have at least 5 hierarchical value sources: their default values, JSON config, exported options
in the JS scripts, environment variables, and CLI flag values. Some options have more... π
We currently use several Go config libraries and a lot of glue code to manage this, and it's still a frequent source of bugs and heavy technical debt. As far as we know, no single other existing Go configuration library is sufficient to cover all of our use cases well. And, from what we can see, these issues are only partially explained by Go's weak type system...
So when we tried to find a Go config library that avoids all of these problems and couldn't, croconf was born! π
β οΈ croconf is still in the "proof of concept" stage
The library is not yet ready for production use. It has bugs, not all features are finished, comments and tests are spotty, and the module structure and type names are expected to change a lot in the coming weeks.
In short, croconf shouldn't suffer from any of the issues β¬οΈ, hopefully without introducing any new ones! π€ It should be suitable for any size of a Go project - from the simplest toy project, to the most complicated CLI application and everything in-between!
Some details about croconf's API design
struct
propertiesstring
identifier has to ever be written more than onceencoding.TextUnmarshaler
and slicestypes.go
unsafe
and no magic β¨reflect
and no type assertions needed for user-facing code (both are used very sparingly internally in the library)These nice features and guarantees are achieved because of the type-safe lazy bindings between value destinations and source paths that croconf uses. The configuration definition just defines the source bindings for every value, the actual resolving is done as a subsequent step.
// SimpleConfig is a normal Go struct with plain Go property types.
type SimpleConfig struct {
RPPs int64
DNS struct {
Server net.IP // type that implements encoding.TextUnmarshaler
// ... more nested fields
}
// ... more config fields...
}
// NewScriptConfig defines the sources and metadata for every config field.
func NewScriptConfig(
cm *croconf.Manager, cliSource *croconf.SourceCLI,
envVarsSource *croconf.SourceEnvVars, jsonSource *croconf.SourceJSON,
) *SimpleConfig {
conf := &SimpleConfig{}
cm.AddField(
croconf.NewInt64Field(
&conf.RPPs,
jsonSource.From("rps"),
envVarsSource.From("APP_RPS"),
cliSource.FromNameAndShorthand("rps", "r"),
// ... more bindings - every field can have as many or as few as needed
),
croconf.WithDescription("number of virtual users"),
croconf.IsRequired(),
// ... more field options like validators, meta-information, etc.
)
cm.AddField(
croconf.NewTextBasedField(
&conf.DNS.Server,
croconf.DefaultStringValue("8.8.8.8"),
jsonSource.From("dns").From("server"),
envVarsSource.From("APP_DNS_SERVER"),
),
croconf.WithDescription("server for DNS queries"),
)
// ... more fields
return conf
}
func main() {
configManager := croconf.NewManager()
// Manually create config sources - fully testable, no implicit shared globals!
cliSource := croconf.NewSourceFromCLIFlags(os.Args[1:])
envVarsSource := croconf.NewSourceFromEnv(os.Environ())
jsonSource := croconf.NewJSONSource(getJSONConfigContents())
config := NewScriptConfig(configManager, cliSource, envVarsSource, jsonSource)
if err := configManager.Consolidate(); err != nil {
log.Fatalf("error consolidating the config: %s", err)
}
jsonResult, err := json.MarshalIndent(config, "", " ")
if err != nil {
log.Fatalf("error marshaling JSON: %s", err)
}
fmt.Fprint(os.Stdout, string(jsonResult))
}
This was a relatively simple example taken from here, and it still manages to combine 4 config value sources! For other examples, take a look in the examples
folder in this repo.
croconf comes from croco-dile conf-iguration. So, π not ππ· π And in the tradition set by k6, if we don't like it, we might decide to abbreviate it to c6
later... π
As mentioned above, this library is still in the proof-of-concept stage. It is usable for toy projects and experiments, but it is very far from production-ready. These are some of the remaining tasks:
examples/croconf-complex-example/