igconfig

package module
v0.2.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 19, 2024 License: MIT Imports: 14 Imported by: 3

README

igconfig

Codecov GitHub Workflow Status Go Reference

This package can be used to load configuration values from a configuration file, environment variables, Consul, Vault and/or command-line parameters.

Install

go get github.com/worldline-go/igconfig

Example

cfg tag value are case insensitive and weakly dash/underscore so Network-Name, network_name, NeTWoK-NaMe and NeTWoKNaMe are same in both tag and configs.

NOTE if secret tag not found it will check cfg tag after that it will check variable's name.

type Config struct {
	NetworkName string `cfg:"networkName" env:"NETWORK_NAME"`
	// application specific vault
	DBSchema     string `cfg:"dbSchema"     env:"SCHEMA"       default:"transaction"`
	DBDataSource string `cfg:"dbDataSource" env:"DBDATASOURCE" log:"false"`
	DBType       string `cfg:"dbType"       env:"DBTYPE"       default:"pgx"`

	CustomConfig map[string]interface{} `cfg:"customConfig"`
}

// ---

var cfg Config

if err := igconfig.LoadConfig("myappname", &cfg); err != nil {
    log.Fatal().Err(err).Msg("unable to load configuration settings.")
}

Also check example:
Examples section
Example usage _example/readFromAll/main.go

Description

There is only a single exported function:

func LoadConfig(appName string, config interface{}) error

or if specific loaders needed:

func LoadWithLoaders(appName string, configStruct interface{}, loaders ...loader.Loader) error

There are also context accepted functions LoadConfigWithContext, LoadWithLoadersWithContext.

  • appName is name of application. It is used in Consul and Vault to find proper path for variables also file name for file loader.
  • config must be a pointer to struct. Otherwise, the function will fail with an error.
  • loaders is list of Loaders to use.
Config struct

All exported fields of this structure will be checked and filled based on their tags or field names.

The field type is taken into consideration when processing parameters.

If the given value cannot be converted to the field's type, an error will be returned. Failing to provide proper value for a type is human-made error, which means that something is not right.

Fields can be given a tag with identifier "default", where the value of the tag will then be used as the default value for that field. If the default value cannot be converted to the field's type, an error will be returned.

Config struct can have inner structs as a fields, but not all Loaders might support them. For example Consul and Vault support inner structs, while Env and Flags don't.

Tags

Config structs can have special tags for fine-grained field name configuration.

There are no required tags, but setting them can improve readability and understanding.

cfg

cfg tag is fallback tag when no Loader-specific tag can be found. As such defining only this tag can be enough for most situations.

env

env tag specifies a name of environmental variable to get value from.

cmd

cmd tag is used to set flag names for fields.

secret

secret tag specifies name of field in Vault that should be used to fill the field.
If not exist it use as struct's field name.

default

default is special tag.

Unlike other tags it does not point to a place from which value should be taken, but instead it itself holds value.

default:"data" will mean that value of string field that has this tag will be data.

This tag is optional

Loaders

Loaders are actual specification on how fields should be filled.

igconfig provides a simple interface for creating new loaders.

Below is a sorted list of currently provided loaders that are included by default(if not stated otherwise)

Change order of loaders and configurations:

// this is the default loaders; change configurations and order or eliminate some loaders
loaders := []loader.Loader{
	&loader.Default{},
	&loader.Consul{},
	&loader.Vault{},
	&loader.File{},
	&loader.Env{},
	&loader.Flags{},
}

// read configurations with custom loaders
if err := igconfig.LoadWithLoaders("test", &conf, loaders...); err != nil {
    log.Fatal().Err(err).Msg("unable to load configuration settings.")
}
Default

This loader uses default tag to get value for fields.

Consul

Loads configuration from Consul and uses map decoder with cfg tag to decode data from Consul to a struct.

If not give CONSUL_HTTP_ADDR as environment variable, this config will skip!

For connection to Consul server you need to set some of environment variables.

Envrionment variable Meaning
CONSUL_HTTP_ADDR Ex: consul:8500, sets the HTTP address
CONSUL_HTTP_TOKEN_FILE sets the HTTP token file
CONSUL_HTTP_TOKEN sets the HTTP token
CONSUL_HTTP_AUTH Ex: username:password, sets the HTTP authentication header
CONSUL_HTTP_SSL Ex: true, sets whether or not to use HTTPS
CONSUL_TLS_SERVER_NAME sets the server name to use as the SNI host when connecting via TLS
CONSUL_CACERT sets the CA file to use for talking to Consul over TLS
CONSUL_CAPATH sets the path to a directory of CA certs to use for talking to Consul over TLS
CONSUL_CLIENT_CERT sets the client cert file to use for talking to Consul over TLS
CONSUL_CLIENT_KEY sets the client key file to use for talking to Consul over TLS.
CONSUL_HTTP_SSL_VERIFY Ex: false, sets whether or not to disable certificate checking
CONSUL_NAMESPACE sets the HTTP Namespace to be used by default. This can still be overridden
CONSUL_CONFIG_PATH_PREFIX sets the path prefix to be used by default.

While it is possible to change decoder from YAML to JSON for example it is not recommended if there are no objective reasons to do so. YAML is superior to JSON in terms of readability while providing as much ability to write configurations.

For better configurability configuration struct might include cfg tag for fields to specify a proper name to bind from Consul, if this tag is skipper - lowercase field name will be used to bind.

For example:

type Config struct {
    Field1 int `cfg:"field"`
    Str struct {
        Inner string
    }
}

will match this YAML

field: 50
str:
  inner: "test string"
Vault

Loads configuration from Vault and uses MapDecoder to decode data from Vault to a struct.

First Vault loads in finops/data/generic path and after that process application's configuration in finops/data/<appname> path.

generic path can have inner path, vault loader combine them.

To use additional path to load with appname set AdditionalPaths value in the loader.
Default value is [{Map: "", Name: "generic"}], Map is a wrapper for read value in key-value format, generic doesn't have map value so it will apply what read and append directly in our config.

To read more than one path, just append your path to the loader.VaultSecretAdditionalPaths slice.

Use Map value to wrap readed data with a key and Name is a path of configuration.

loader.VaultSecretAdditionalPaths = append(
    loader.VaultSecretAdditionalPaths,
    loader.AdditionalPath{Map: "loadtest", Name: "loadtest"},
)

If not given any of VAULT_ADDR, VAULT_AGENT_ADDR or CONSUL_HTTP_ADDR as environment variable, this config will skip!

If CONSUL_HTTP_ADDR exists, it uses Consul to get vault address.

Envrionment Variable Meaning
CONSUL_HTTP_ADDR get VAULT_ADDR from this consul server with vault service tag name.
VAULT_CONSUL_ADDR_DISABLE disable to get VAULT_ADDR from this consul server.
VAULT_ADDR the address of the Vault server. This should be a complete URL such as "http://vault.example.com". If need a custom SSL cert or enable insecure mode, you need to specify a custom HttpClient.
VAULT_AGENT_ADDR the address of the local Vault agent. This should be a complete URL such as "http://vault.example.com".
VAULT_MAX_RETRIES controls the maximum number of times to retry when a 5xx error occurs. Set to 0 to disable retrying. Defaults to 2 (for a total of three tries).
VAULT_RATE_LIMIT EX: rateFloat:brustInt
VAULT_CLIENT_TIMEOUT seconds
VAULT_SRV_LOOKUP enables the client to lookup the host through DNS SRV lookup
VAULT_CACERT TLS
VAULT_CAPATH TLS
VAULT_CLIENT_CERT TLS
VAULT_CAPATH TLS
VAULT_CLIENT_CERT TLS
VAULT_CLIENT_KEY TLS
VAULT_TLS_SERVER_NAME TLS
VAULT_SKIP_VERIFY TLS
VAULT_APPROLE_BASE_PATH set login path to be used by default.
VAULT_SECRET_BASE_PATH set secret base path to be used by default.

For authentication, you should set VAULT_ROLE_ID and VAULT_ROLE_SECRET environment variables.

File

TOML, YAML and JSON files supported, and file path should be located on CONFIG_FILE env variable.
If that environment variable not found, file loader check working directory and /etc path with this formation <appName>.[toml|yml|yaml|json] (if there is more than appName with different suffixes, order is toml > yml > yaml > json).
The appName used as the file name is not the full name, only the part after the last slash. So if your app name is transactions/consumers/internal/apm/, the loader will try to load a file with the name apm.

The key is checked against the exported field names of the config struct and the field tag identified by cfg.
If a key is matched, the corresponding field in the struct will be filled with the value from the configuration file.

NOTE: if cfg tag not exists, it is still read values in file and match struct's field name!
Don't want to read a value just delete it in your config file or add cfg:"-".

FileLoader editable, you can add your own decoder or new file format or order of file suffixes.

Environment variables

For all exported fields from the config struct the name and the field tag identified by "env" will be checked if a corresponding environment variable is present. The tag may contain a list of names separated by comma's. The comparison is upper-case, even if tag specifies lower- or mixed-case. Lower-case environment variables are ignored.

Once a match is found the value from the corresponding environment variable is placed in the struct field, and no further comparisons will be done for that field.

Set value in inner struct:

type Config struct {
    Inner Inner
}

type Inner struct {
	GetENV       string `env:"TEST_ENV"` // env:"test_EnV" same as TEST_ENV
	// GetENV       string `cfg:"TEST_ENV"` // cfg tag also usable
}

To set GetENV value use INNER_TEST_ENV environment value. Or you can change INNER name with env tag.

type Config struct {
    Inner Inner `env:"IN"`
}

Now value use IN_TEST_ENV

NOTE: if env tag not exists, it will check cfg tag and if both not exists, it will check struct's field name as uppercase.

Flags (command-line parameters)

For all exported fields from the config struct the tag of the field identified by "cmd" will be checked if a corresponding command-line parameter is present. The tag may contain a list of names separated by comma's. The comparison is always done in a case-sensitive manner. Once a match is found the value from the corresponding command-line parameter is placed in the struct field, and no further comparisons are done for that field. For boolean struct fields the command-line parameter is checked as a flag. For all other field types the command-line parameter should have a compatible value. Parameters can be supplied on the command-line as described in the standard Go package "flag".

Log with context

Set a new zerolog logger and attach to the context, igconfig will use that context's logger.

// set new schemas for log
logConfig := log.With().Str("component", "config").Logger()
// replace context.Background() with own context
logCtx := logConfig.WithContext(context.Background())

// call igconfig with context
if err := igconfig.LoadConfigWithContext(logCtx, "test", &conf); err != nil {
    log.Ctx(logCtx).Fatal().Err(err).Msg("unable to load configuration settings.")
}

Print configuration

secret tag is disabled to print but if you want to print it add aditional option to secret called loggable or log.

Info string     `cfg:"info" secret:"info,log"` // Set secret's log option

Or you can set general log to manage it all tags in releated field.

User string     `cfg:"user" secret:"user" log:"true"` // Set general log
conf := config.AppConfig{} // set up config value somehow
// log is zerolog/log package
log.Info().
    EmbedObject(Printer{Value: conf}).
    Msg("loaded config")

Examples

Example usage of File
(
export CONFIG_FILE=_example/readFromAll/dataFile/test.yml
go run _example/readFromAll/main.go
)
Example usage of Vault server

Set Vault or Consul server address with releated environment variables.

Run a development vault

docker run -it --rm --cap-add=IPC_LOCK --name=dev-vault -p 8200:8200 vault

After that connect this vault with vault CLI app.

# export address for http
export VAULT_ADDR="http://127.0.0.1:8200"
# login with root token (appears in docker output)
vault login <token>
# unseal it
vault operator unseal <unsealkey>
# create kv secret engine
vault secrets enable -path=finops -version=2 kv
# create policy to read
{
cat <<EOF
path "finops/*" {
  capabilities = ["read", "list"]
}
path "finops/data/generic/super-secret" {
  capabilities = ["deny"]
}
EOF
} | vault policy write finops-read -

# create a approle with policy and enable connection without secret_id
vault auth enable approle
vault write auth/approle/role/my-role bind_secret_id=false secret_id_bound_cidrs="127.0.0.0/8,172.17.0.0/16" policies="default","finops-read"
# learn role-id
ROLE_ID=$(vault read -field=role_id auth/approle/role/my-role/role-id)

# fill some data
vault kv put finops/generic/keycloack @_example/readFromAll/dataVault/generic_keycloack.json
vault kv put finops/generic/super-secret @_example/readFromAll/dataVault/generic_supersecret.json
vault kv put finops/test @_example/readFromAll/dataVault/test.json
vault kv put finops/loadtest @_example/readFromAll/dataVault/loadtest.json

After that add our data in your finops kv section. Under usually should be a generic section and you should add keycloack and migration in there. also add your application name data in finops.

(
export VAULT_ADDR="http://localhost:8200"
export VAULT_ROLE_ID=${ROLE_ID}
# export CONSUL_HTTP_ADDR="consul:8500"
export MIGRATIONS_TEST_ENV="testing_testing_1234"
go run _example/readFromAll/main.go
)
Example usage of Consul server

Start consul agent with dev mode

docker run -it --rm --name=dev-consul --net=host consul:1.10.4

Go to localhost:8500 webui and add key values but for our tool folder should be finops.

It could be yaml or json format or you can handle by codec.Decoder interface.

Test it

export CONSUL_HTTP_ADDR="localhost:8500"
go run _example/readFromAll/main.go
Example usage of Consul dynamic listen

To listen a key, our function is easily usable.
While listening a key, you can restart consul server or close or not even started yet or delete key, it is totally safe.

Check example to get information:

export CONSUL_HTTP_ADDR="localhost:8500"
go run _example/dynamicConsul/main.go

Development

Package Test

Unit tests

go test --race -cover ./...

Code coverage report (Browser)

mkdir _out
go test -cover -coverprofile cover.out -outputdir ./_out/ ./...
# Auto open html result
go tool cover -html=./_out/cover.out
# Export HTML
# go tool cover -html=./_out/cover.out -o ./_out/coverage.html
JSON|YAML

Use yq tool for translate json to yaml or yaml to json.

# yaml to json
cat _example/readFromAll/dataFile/test.yml | yq
# json to yaml
cat _example/readFromAll/dataVault/generic_keycloack.json | yq -y

Documentation

Overview

Package igconfig allows to set struct values based on different input places.

For example struct can be filled from default values defined in tags, environmental variables or Vault secrets.

All values in tags(except value in 'default' tag) should be lower-cased. Failing to make values lower-case will result in inability to properly set those fields. This is a choice to make lower number of mixed-cased values and generally be more in-line with best practices and proper usages.

Library is basically a set of "loaders".

Loader is anything that satisfies loader.Loader interface.

Such loaders will take application name and structure that should be filled and will do specific steps do try and set fields in structure.

LoadConfig function contains all the loaders that are enabled by default. This function should be used in most of the cases as only functionality that should be used by the caller.

In rare cases caller might want to use specific set of loaders to load configuration.

Some loaders also provide ability to periodically update values. This can be useful if application has logging and caller might want to change logging level at a runtime.

Example (FileLoader)

Example_fileLoader for getting values from file(YAML or JSON). In this example, used to change etc path and get the file name from the appname.

package main

import (
	"fmt"
	"log"
	"time"

	"github.com/worldline-go/igconfig"
	"github.com/worldline-go/igconfig/loader"
)

func main() {

	/* ===== YAML file of testdata/config/train.yml =====
	      speed: 10
	      ADDRESS: "Hoofddrop"
	      bay: 4
	      secure: false
	      info:
	        train_name: "IN-NS1234"
	        age: 10
	        destination: "Eindhoven"
	      InfoStruct:
	        train_name: "Embedded-NS1234"
	        # WARNING RANDOM GETS IF SAME KEY EXISTS
	   	 # testing | TesTing | tesTing are same for Testing | TesTing fields in struct
	        testing: "testX"
	        # TesTing: "testY"
	        # tesTing: "testZ"
	        # DoubleName: "textD"
	        # doubleName: "textD"
	        doublename: "textD"
	      extra:
	        new: "Extra data ignore"
	*/

	type InfoStruct struct {
		Name        string `cfg:"train_name"`
		Age         uint   `cfg:"age"`
		Destination string
		Testing     string
		TesTing     string
		DoubleName  string
		Time        time.Time `cfg:"time" default:"2000-01-01T10:00:00Z"`
	}

	type AwesomeTrain struct {
		Name    string   `cfg:"train_name"    env:"name"           cmd:"name,n"           default:"NS1"`
		Age     uint     `cfg:"age"            env:"age"            cmd:"age,a"            default:"18"`
		Speed   float64  `cfg:"speed"         env:"speed"         cmd:"speed,s"         default:"200.50"  loggable:"false"`
		Address string   `cfg:"ADDRESS"        env:"ADDRESS"        default:"localhost"`
		Bay     int      `cmd:"bay,p"           default:"10"`
		Secure  bool     `cfg:"secure" env:"secure" cmd:"secure" default:"false"    loggable:"false"`
		Slice   []string `cfg:"slice" env:"slice" cmd:"slice" default:"1,2,5,6"`
		InfoStruct
		Info           InfoStruct
		InfoStructSkip InfoStruct `cfg:"-" default:"-"`
	}

	mytrain := AwesomeTrain{}

	// If not found, it return an error in igconfig loader.
	// os.Setenv(loader.EnvConfigFile, "testdata/config/train.yml")
	// defer os.Unsetenv(loader.EnvConfigFile)

	// err := igconfig.LoadConfig("train", &mytrain)
	// For this example just used file loader, most cases just use LoadConfig function.
	err := igconfig.LoadWithLoaders("train", &mytrain, loader.File{EtcPath: "testdata/config"})
	if err != nil {
		log.Fatal("unable to load configuration settings", err)
	}

	fmt.Printf("%+v", mytrain)

}
Output:

{Name: Age:0 Speed:10 Address:Hoofddrop Bay:4 Secure:false Slice:[] InfoStruct:{Name:Embedded-NS1234 Age:0 Destination: Testing:testX TesTing:testX DoubleName:textD Time:0001-01-01 00:00:00 +0000 UTC} Info:{Name:IN-NS1234 Age:10 Destination:Eindhoven Testing: TesTing: DoubleName: Time:0001-01-01 00:00:00 +0000 UTC} InfoStructSkip:{Name: Age:0 Destination: Testing: TesTing: DoubleName: Time:0001-01-01 00:00:00 +0000 UTC}}

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// LogTagOptionNames is a tag name for loggable boolean check.
	LogTagOptionNames = []string{"loggable", "log"}
	// SecretTagName is a tag name for secret loaders to prevent print it.
	SecretTagName = "secret"
)
View Source
var DefaultLoaders = []loader.Loader{
	&loader.Default{},
	&loader.Consul{},
	&loader.Vault{},
	&loader.File{},
	&loader.Env{},
	&loader.Flags{},
}

DefaultLoaders is a list of default loaders to use.

Functions

func ConfigureZerolog

func ConfigureZerolog()

ConfigureZerolog can be used to "reset" default global logging configuration for zerolog.

This will update Time format to RFC3339Nano and will set up new global logger.

func DefaultNameGetter

func DefaultNameGetter(t reflect.StructField) string

DefaultNameGetter returns lowercase field name as json field name.

func LoadConfig

func LoadConfig(appName string, c interface{}) error

LoadConfig loads a configuration struct from loaders.

Example
package main

import (
	"fmt"
	"log"
	"os"

	"github.com/rs/zerolog"
	"github.com/worldline-go/igconfig"
	"github.com/worldline-go/igconfig/testdata"
)

func main() {
	var config testdata.TestConfig

	// Disable logging for unreachable local services.
	// In non-local environments this should not be done.
	zerolog.SetGlobalLevel(zerolog.ErrorLevel)

	// Below are just an examples of how values can be provided. You don't need to do this in your code.
	// In real-world - this will be provided from env, flags or Consul/Vault
	os.Args = []string{"executable", "-name", "FromFlags"}
	_ = os.Setenv("PORT", "5647")

	if err := igconfig.LoadConfig("adm0001s", &config); err != nil {
		log.Fatalf("load configuration: %s", err.Error())
	}

	fmt.Println(config.Host) // This value is set from default
	fmt.Println(config.Name) // This value is set from application flags
	fmt.Println(config.Port) // This value is set from environmental variable

}
Output:

localhost
FromFlags
5647

func LoadConfigWithContext

func LoadConfigWithContext(ctx context.Context, appName string, c interface{}) error

LoadConfigWithContext loads a configuration struct from a fileName, the environment and finally from command-line parameters (the latter override the former) into a config struct. This is a convenience function encapsulating all individual loaders specified in DefaultLoaders.

func LoadWithLoaders

func LoadWithLoaders(appName string, configStruct interface{}, loaders ...loader.Loader) error

LoadWithLoaders loads a configuration struct from a loaders.

Example
package main

import (
	"fmt"
	"os"

	"github.com/worldline-go/igconfig"
	"github.com/worldline-go/igconfig/loader"
	"github.com/worldline-go/igconfig/testdata"
)

func main() {
	// If only particular loaders are needed or new loader should be added - it is possible to do.
	//
	// igconfig.DefaultLoaders is an array of loaders provided by default.
	//
	// This example uses only Flags loader.
	// This means that no default or environmental variables will be loaded.
	//
	// Some loaders may accept additional configuration when used like this
	flagsLoader := loader.Flags{
		NoUsage: true,
	}

	// Prepare pre-defined list of flags for this example
	os.Args = []string{"executable", "-salary", "12345.66"}

	var c testdata.TestConfig

	// igconfig.LoadWithLoaders provides ability to use specific loaders.
	//
	// P.S.: Please check errors in your code.
	_ = igconfig.LoadWithLoaders("adm0001s", &c, flagsLoader)

	fmt.Println(c.Name)
	fmt.Println(c.Salary)

}
Output:


12345.66

func LoadWithLoadersWithContext

func LoadWithLoadersWithContext(ctx context.Context, appName string, configStruct interface{}, loaders ...loader.Loader) error

LoadWithLoadersWithContext uses provided Loader's to fill 'configStruct'.

Types

type NameGetter

type NameGetter func(t reflect.StructField) string

type Printer

type Printer struct {
	// NameGetter will be called for each field to get name of it.
	NameGetter
	// Value is actual struct that should be printed.
	// It is possible for the value to be pointer to a struct.
	//
	// Value can implement some zerolog interfaces, but it has to be of proper type:
	// if interface is defined on pointer receiver - passed value should also be pointer.
	Value interface{}
}

Printer is an implementation of zerolog.LogObjectMarshaler for marshaling struct to zerolog event.

Primary use case is to add configuration struct to the log.

Usage example:

conf := config.AppConfig{} // set up config value somehow
// log is zerolog/log package
log.Info().
	Object("config", Printer{Value: conf}).
	Msg("loaded config")

func (Printer) MarshalZerologObject

func (p Printer) MarshalZerologObject(ev *zerolog.Event)

MarshalZerologObject marshals config to zerolog event.

Value should not be specifically config, but could be any struct.

Directories

Path Synopsis
_example
credential

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL