Skip to content

Commit

Permalink
feat: command to convert config to consul template
Browse files Browse the repository at this point in the history
  • Loading branch information
vividvilla committed Nov 9, 2018
1 parent 5a7c4a7 commit 122434d
Show file tree
Hide file tree
Showing 6 changed files with 390 additions and 128 deletions.
19 changes: 18 additions & 1 deletion README.md
Expand Up @@ -6,6 +6,9 @@

## Get started

### Config to KV Pairs
Export config file to Consul KV Pairs format which can be used for [Consul bulk import](https://www.consul.io/docs/commands/kv/import.html).

```bash
# Read config from multiple files
consul-cfg kv --type toml config1.toml config2.toml
Expand All @@ -17,6 +20,20 @@ cat config.toml | consul-cfg kv --type toml
cat config.toml | consul-cfg kv --type toml --prefix myconfig/app
```

### Config to consul-template
Convert config file to consul-template (go template). Config values replaced with consul-template [`keyOrUpdate`](https://github.com/hashicorp/consul-template#keyordefault) syntax with default value as current value.

```bash
# Read config from multiple files
consul-cfg tmpl --type toml config1.toml config2.toml

# Pipe stdin from other commands
cat config.toml | consul-cfg tmpl --type toml

# Specify prefix for all keys
cat config.toml | consul-cfg tmpl --type toml --prefix myconfig/app
```

## Supported formats
Currently following config formats are supported - `toml`, `yaml`, `hcl`, `json` and `props` (JAVA properties).

Expand All @@ -28,7 +45,7 @@ go install github.com/vividvilla/consul-cfg
```

### Install binary
You can download latest binary from [release page](/releases) based on your OS.
You can download latest binary from [releases](https://github.com/vividvilla/consul-cfg/releases/latest) based on your OS.

## Caveat
Array of maps are not supported by consul KV so it will be encoded as JSON string.
37 changes: 37 additions & 0 deletions common.go
@@ -0,0 +1,37 @@
// Common methods uses by template and KV commands

package main

import (
"io"
"strings"

"github.com/spf13/viper"
)

// Parse config file to a map
func configToMap(cType string, r io.Reader) (map[string]interface{}, error) {
viper.SetConfigType(cType)
err := viper.ReadConfig(r)
if err != nil {
return nil, err
}

return viper.AllSettings(), nil
}

// Check if given input format is supported.
func isValidInputFormat(format string, availFormats []string) bool {
for _, f := range availFormats {
if f == format {
return true
}
}

return false
}

// Convert array of string to string
func formatsToString(formats []string) string {
return strings.Join(formats, ", ")
}
4 changes: 4 additions & 0 deletions go.mod
Expand Up @@ -2,9 +2,13 @@ module github.com/vividvilla/consul-cfg

require (
github.com/BurntSushi/toml v0.3.1 // indirect
github.com/hashicorp/hcl v1.0.0
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/magiconair/properties v1.8.0
github.com/pelletier/go-toml v1.2.0
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/spf13/cobra v0.0.3
github.com/spf13/viper v1.2.1
github.com/stretchr/testify v1.2.2 // indirect
gopkg.in/yaml.v2 v2.2.1
)
111 changes: 111 additions & 0 deletions kv.go
@@ -0,0 +1,111 @@
package main

import (
"bytes"
"encoding/base64"
"encoding/json"
"io"
"os"
"reflect"
"strings"

"github.com/spf13/cobra"
)

// Run KV commands.
func runKVCmd(cmd *cobra.Command, args []string) {
// Check if input format is supported.
if !isValidInputFormat(kvInputType, kvAvailableFormats) {
errLog.Fatalf("Invalid input file format - %s. Available options are: %s", kvInputType, formatsToString(kvAvailableFormats))
}

var inputs []io.Reader
var output []consulKVPair

// Add stdin as default input if files are not provided else add given input files.
if len(args) == 0 {
inputs = append(inputs, os.Stdin)
} else {
// Add all files as inputs
for _, fname := range args {
f, err := os.Open(fname)
if err != nil {
errLog.Fatalf("Error: error opening input file - %v", err)
}

inputs = append(inputs, f)
}
}

for _, i := range inputs {
// Process toml inputs
m, err := configToMap(kvInputType, i)
if err != nil {
errLog.Fatalf("Error: error parsing input - %v", err)
}

// Recursively parse map and add KV pairs
mapToKVPairs(&output, kvKeyPrefix, m)
}

// Print JSON output
bytes, err := json.MarshalIndent(output, "", " ")
if err != nil {
errLog.Fatalf("error marshelling output: %v", err)
}

sysLog.Println(string(bytes[:]))
}

// Recursively traverse map and insert KV Pair to output if it can't be further traversed.
func mapToKVPairs(ckv *[]consulKVPair, prefix string, inp map[string]interface{}) {
for k, v := range inp {
var newPrefix string

// If prefix is empty then don't append "/" else form a new prefix with current key.
if prefix == "" {
newPrefix = k
} else {
newPrefix = prefix + "/" + k
}

// Check if value is a map. If map then traverse further else write to output as a KVPair.
vKind := reflect.TypeOf(v).Kind()
if vKind == reflect.Map {
// Check if map value is of interface type and keys are string.
m, ok := v.(map[string]interface{})
if !ok {
errLog.Fatalf("not ok: %v - %v\n", k, v)
}

// Recursion.
mapToKVPairs(ckv, newPrefix, m)
} else {
// If its not string then encode it using JSON
// CAVEAT: TOML supports array of maps but consul KV doesn't support this so it will be JSON marshalled.

// Custom JSON marshaller with safe escaped html is disabled. Since default JSON marshaller escapes &, > and <.
val := bytes.NewBufferString("")
jEncoder := json.NewEncoder(val)
jEncoder.SetEscapeHTML(false)

// JSON encode value to preserve the type.
if err := jEncoder.Encode(v); err != nil {
errLog.Fatalf("error while marshalling value: %v err: %v", v, err)
}

// Trim new lines if any at the end
trimmed := strings.TrimSuffix(val.String(), "\n")

// Base64 encode JSON encoded values since Consul reads only base64 encoded values.
b64Encoded := base64.StdEncoding.EncodeToString([]byte(trimmed))

// Append KV pair to results.
*ckv = append(*ckv, consulKVPair{
Flags: 0,
Key: newPrefix,
Value: b64Encoded,
})
}
}
}

0 comments on commit 122434d

Please sign in to comment.