Skip to content
This repository has been archived by the owner on Apr 27, 2022. It is now read-only.

Commit

Permalink
Add Source : KvSource (#13)
Browse files Browse the repository at this point in the history
* add generateMapstructure

* add func decodeHookSlice
+ refactor generateMapstructure

* add source KV

* add kv.StoreConfig() + test

* fix Anonymous squashed fields + add and test LoadConfig

* update readme

* fix prefix without / + test StoreConfig

* fixes typo
  • Loading branch information
Martin Piegay committed Jun 20, 2016
1 parent e2aa88e commit b432e31
Show file tree
Hide file tree
Showing 5 changed files with 1,401 additions and 5 deletions.
87 changes: 83 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ We developed [`flaeg`](https://github.com/containous/flaeg) and `staert` in orde
## Features
- Load your Configuration structure from many sources
- Keep your Configuration structure values unchanged if no overwriting (support defaults values)
- Two native sources :
- [Flæg](https://github.com/containous/flaeg)
- [Toml](http://github.com/BurntSushi/toml)
- Three native sources :
- Command line arguments using [flæg](https://github.com/containous/flaeg) package
- TOML config file using [toml](http://github.com/BurntSushi/toml) package
- [Key-Value Store](#kvstore) using [libkv](https://github.com/docker/libkv) and [mapstructure](https://github.com/mitchellh/mapstructure) packages
- An Interface to add your own sources
- Handle pointers field :
- You can give a structure of default values for pointers
Expand Down Expand Up @@ -114,7 +115,7 @@ Just call LoadConfig function :
if err != nil {
//OOPS
}
//DO WATH YOU WANT WITH loadedConfig
//DO WHAT YOU WANT WITH loadedConfig
//OR CALL RUN FUNC
```
Expand Down Expand Up @@ -143,6 +144,84 @@ PointerField contains:&{BoolField:true FloatField:55.55}
```

## Full example :
[Tagoæl](https://github.com/debovema/tagoael) is a trivial example which shows how Stært can be use.
This funny golang progam takes its configuration from both TOML and Flaeg sources to display messages.
```shell
$ ./tagoael -h
tagoæl is an enhanced Hello World program to display messages with
an advanced configuration mechanism provided by flæg & stært.

flæg: https://github.com/containous/flaeg
stært: https://github.com/containous/staert
tagoæl: https://github.com/debovema/tagoael


Usage: tagoael [--flag=flag_argument] [-f[flag_argument]] ... set flag_argument to flag(s)
or: tagoael [--flag[=true|false| ]] [-f[true|false| ]] ... set true/false to boolean flag(s)

Flags:
-c, --commandlineoverridesconfigfile Whether configuration from command line overrides configuration from configuration file or not. (default "true")
--configfile Configuration file to use (TOML). (default "tagoael")
-i, --displayindex Whether to display index of each message (default "false")
-m, --messagetodisplay Message to display (default "HELLO WOLRD")
-n, --numbertodisplay Number of messages to display (default "1000")
-h, --help Print Help (this message) and exit
```
Thank you [@debovema](https://github.com/debovema) for this work :)
## KvStore
As with Flæg and Toml sources, the configuration structure can be loaded from a Key-Value Store.
The package [libkv](https://github.com/docker/libkv) provides connection to many KV Store like `Consul`, `Etcd` or `Zookeeper`.
The whole configuration structure is stored, using architecture like this pattern :
- Key : `<prefix1>/<prefix2>/.../<fieldNameLevel1>/<fieldNameLevel2>/.../<fieldName>`
- Value : `<value>`
It handles :
- All [mapstructure](https://github.com/mitchellh/mapstructure) features(`bool`, `int`, ... , Squashed Embedded Sub `struct`, Pointer).
- Maps with pattern : `.../<MapFieldName>/<mapKey>` -> `<mapValue>` (Struct as key not supported)
- Slices (and Arrays) with pattern : `.../<SliceFieldName>/<SliceIndex>` -> `<value>`
Note : Hopefully, we provide the function `StoreConfig` to store your configuration structure ;)
### KvSource
KvSource impement Source:
```go
type KvSource struct {
store.Store
Prefix string // like this "prefix" (witout the /)
}
```
### Initialize
It can be initialized like this :
```go
kv, err := staert.KvSource(backend store.Backend, addrs []string, options *store.Config, prefix string)
```
### LoadConfig
You can directly load data from the KV Store into the config structure (given by reference)
```go
config := &ConfigStruct{} // Here your configuration structure by reference
err := kv.Parse(config)
//DO WHAT YOU WANT WITH config
```
### Add to Stært sources
Or you can add this source to Stært, as with other sources
```go
s.AddSource(kv)
```
### StoreConfig
You can also store your whole configuration structure into the KV Store :
```go
// We assume that config is Initialazed
err := s.StoreConfig(config)
```
## Contributing
1. Fork it!
Expand Down
224 changes: 224 additions & 0 deletions kv.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
package staert

import (
"errors"
"fmt"
"github.com/containous/flaeg"
"github.com/docker/libkv"
"github.com/docker/libkv/store"
"github.com/mitchellh/mapstructure"
"reflect"
"sort"
"strconv"
"strings"
)

// KvSource implements Source
// It handles all mapstructure features(Squashed Embeded Sub-Structures, Maps, Pointers)
// It supports Slices (and maybe Arraies). They must be sorted in the KvStore like this :
// Key : ".../[sliceIndex]" -> Value
type KvSource struct {
store.Store
Prefix string // like this "prefix" (without the /)
}

// NewKvSource creates a new KvSource
func NewKvSource(backend store.Backend, addrs []string, options *store.Config, prefix string) (*KvSource, error) {
store, err := libkv.NewStore(backend, addrs, options)
return &KvSource{Store: store, Prefix: prefix}, err
}

// Parse uses libkv and mapstructure to fill the structure
func (kv *KvSource) Parse(cmd *flaeg.Command) (*flaeg.Command, error) {
err := kv.LoadConfig(cmd.Config)
if err != nil {
return nil, err
}
return cmd, nil
}

// LoadConfig loads data from the KV Store into the config structure (given by reference)
func (kv *KvSource) LoadConfig(config interface{}) error {
pairs, err := kv.List(kv.Prefix)
if err != nil {
return err
}
mapstruct, err := generateMapstructure(pairs, kv.Prefix)
if err != nil {
return err
}
configDecoder := &mapstructure.DecoderConfig{
Metadata: nil,
Result: config,
WeaklyTypedInput: true,
DecodeHook: decodeHook,
}
decoder, err := mapstructure.NewDecoder(configDecoder)
if err != nil {
return err
}
if err := decoder.Decode(mapstruct); err != nil {
return err
}
return nil
}

func generateMapstructure(pairs []*store.KVPair, prefix string) (map[string]interface{}, error) {
raw := make(map[string]interface{})
for _, p := range pairs {
// Trim the prefix off our key first
key := strings.TrimPrefix(p.Key, prefix+"/")
raw, err := processKV(key, string(p.Value), raw)
if err != nil {
return raw, err
}

}
return raw, nil
}

func processKV(key string, v string, raw map[string]interface{}) (map[string]interface{}, error) {
// Determine which map we're writing the value to. We split by '/'
// to determine any sub-maps that need to be created.
m := raw
children := strings.Split(key, "/")
if len(children) > 0 {
key = children[len(children)-1]
children = children[:len(children)-1]
for _, child := range children {
if m[child] == nil {
m[child] = make(map[string]interface{})
}
subm, ok := m[child].(map[string]interface{})
if !ok {
return nil, fmt.Errorf("child is both a data item and dir: %s", child)
}
m = subm
}
}
m[key] = string(v)
return raw, nil
}

func decodeHook(fromType reflect.Type, toType reflect.Type, data interface{}) (interface{}, error) {
// TODO : Array support
switch toType.Kind() {
case reflect.Slice:
if fromType.Kind() == reflect.Map {
// Type assertion
dataMap, ok := data.(map[string]interface{})
if !ok {
return data, fmt.Errorf("input data is not a map : %#v", data)
}
// Sorting map
indexes := make([]int, len(dataMap))
i := 0
for k := range dataMap {
ind, err := strconv.Atoi(k)
if err != nil {
return dataMap, err
}
indexes[i] = ind
i++
}
sort.Ints(indexes)
// Building slice
dataOutput := make([]interface{}, i)
i = 0
for _, k := range indexes {
dataOutput[i] = dataMap[strconv.Itoa(k)]
i++
}

return dataOutput, nil
}
}
return data, nil
}

// StoreConfig stores the config into the KV Store
func (kv *KvSource) StoreConfig(config interface{}) error {
kvMap := map[string]string{}
if err := collateKvRecursive(reflect.ValueOf(config), kvMap, kv.Prefix); err != nil {
return err
}
for k, v := range kvMap {
if err := kv.Put(k, []byte(v), nil); err != nil {
return err
}
}
return nil
}

func collateKvRecursive(objValue reflect.Value, kv map[string]string, key string) error {
name := key
kind := objValue.Kind()
switch kind {
case reflect.Struct:
for i := 0; i < objValue.NumField(); i++ {
objType := objValue.Type()
squashed := false
if objType.Field(i).Anonymous {
if objValue.Field(i).Kind() == reflect.Struct {
tags := objType.Field(i).Tag
if strings.Contains(string(tags), "squash") {
squashed = true
}
}
}
if squashed {
if err := collateKvRecursive(objValue.Field(i), kv, name); err != nil {
return err
}
} else {
fieldName := objType.Field(i).Name
//useless if not empty Prefix is required ?
if len(key) == 0 {
name = strings.ToLower(fieldName)
} else {
name = key + "/" + strings.ToLower(fieldName)
}

if err := collateKvRecursive(objValue.Field(i), kv, name); err != nil {
return err
}
}
}

case reflect.Ptr:
if !objValue.IsNil() {
if err := collateKvRecursive(objValue.Elem(), kv, name); err != nil {
return err
}
}

case reflect.Map:
for _, k := range objValue.MapKeys() {
if k.Kind() == reflect.Struct {
return errors.New("Struct as key not supported")
}
name = key + "/" + fmt.Sprint(k)
if err := collateKvRecursive(objValue.MapIndex(k), kv, name); err != nil {
return err
}
}
case reflect.Array, reflect.Slice:
for i := 0; i < objValue.Len(); i++ {
name = key + "/" + strconv.Itoa(i)
if err := collateKvRecursive(objValue.Index(i), kv, name); err != nil {
return err
}
}
case reflect.Interface, reflect.String, reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16,
reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16,
reflect.Uint32, reflect.Uint64, reflect.Uintptr, reflect.Float32, reflect.Float64:
if _, ok := kv[name]; ok {
return errors.New("key already exists: " + name)
}
kv[name] = fmt.Sprint(objValue)

default:
return fmt.Errorf("Kind %s not supported", kind.String())
}
return nil
}
Loading

0 comments on commit b432e31

Please sign in to comment.