Skip to content

Commit

Permalink
Support ENV format (#1)
Browse files Browse the repository at this point in the history
* Support env format

* Update test

* Fix lint

* Update test case

* Update test cases

* Remove convert array to flat value

* Add missing test case
  • Loading branch information
vietanhduong authored Oct 23, 2021
1 parent 3c764b6 commit 34d8117
Show file tree
Hide file tree
Showing 11 changed files with 332 additions and 4 deletions.
2 changes: 0 additions & 2 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ name: CI
on:
pull_request:
push:
branches:
- master

jobs:
ci:
Expand Down
102 changes: 102 additions & 0 deletions pkg/env/env.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package env

import (
"fmt"
"github.com/pkg/errors"
"regexp"
"strconv"
"strings"
)

type Env struct{}

func NewEnv() *Env {
return &Env{}
}

// ToENV convert a map (JSON structure) to string
// (concatenated by \n)
// This function only support convert simple format
// i.e: string, number
// another types will be ignored
// it will be converted to empty string.
// The key and value will be concatenated by equal sign.
func (e *Env) ToENV(src map[string]interface{}) []byte {
var lines []string

for key, value := range src {
var raw string

switch t := value.(type) {
case int:
raw = addQuote(fmt.Sprintf("%v", value))
case float64:
raw = addQuote(fmt.Sprintf("%v", value))
case string:
raw = addQuote(fmt.Sprintf("%v", value))
case bool:
raw = addQuote(fmt.Sprintf("%v", value))
default:
raw = ""
fmt.Println("WARN: This type", t, "will be ignored")
}
lines = append(lines, fmt.Sprintf("export %s=%s", key, raw))
}

content := strings.Join(lines, "\n")
return []byte(content)
}

// addQuote to input string if it contains special characters
func addQuote(s string) string {
var isValid = regexp.MustCompile(`^[a-zA-Z0-9.,_-]+$`).MatchString
if !isValid(s) {
s = strconv.Quote(s)
}
return s
}

// ToJSON convert string lines to a map (JSON structure)
// Each line should be formatted like export key=value or key=value
// The value can be empty. The key should contain only
// alphabet, number and underscore.
// Comment lines will be ignored.
func (e *Env) ToJSON(src []string) (map[string]interface{}, error) {
content := make(map[string]interface{})
var isValid = regexp.MustCompile(`^[a-zA-Z0-9_]+$`).MatchString
for _, line := range src {
line := strings.TrimSpace(line)
// Skip with line start with # or empty
if len(line) == 0 || strings.HasPrefix(line, "#") {
continue
}

// Remove export
line = strings.TrimSpace(strings.TrimPrefix(line, "export"))

v := strings.Split(line, "=")
// Validate key
if !isValid(v[0]) {
return nil, errors.New(fmt.Sprintf("Env: Key %s is invalid format", v[0]))
}

value := ""
if len(v) > 1 {
value = strings.Join(v[1:], "=")
}

content[v[0]] = trimQuotes(value)
}

return content, nil
}

// trimQuotes remove quote from string
func trimQuotes(s string) string {
if len(s) >= 2 {
if c := s[len(s)-1]; s[0] == c && (c == '"' || c == '\'') {
return s[1 : len(s)-1]
}
}
return s
}
82 changes: 82 additions & 0 deletions pkg/env/env_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package env

import (
"github.com/stretchr/testify/assert"
"strings"
"testing"
)

func TestEnv_ToENV(t *testing.T) {
t.Run("With success case: return byte array", func(tc *testing.T) {
e := NewEnv()
input := map[string]interface{}{
"TEST": "TEST",
"DATA": []int{1, 2, 3},
"STR": []string{"a", "b", "c"},
"EMPTY": map[string]interface{}{"test": true},
"FLOAT": 0.23,
"STR_WITH_SPACE": "string with space",
"BOOL": true,
}

expected := map[string]bool{
"export TEST=TEST": true,
"export BOOL=true": true,
"export DATA=": true,
"export STR=": true,
"export EMPTY=": true,
"export FLOAT=0.23": true,
"export STR_WITH_SPACE=\"string with space\"": true,
}

actual := strings.Split(string(e.ToENV(input)), "\n")

assert.Equal(tc, len(expected), len(actual))
for _, a := range actual {
assert.Contains(tc, expected, a)
}
})
}

func TestEnv_ToJSON(t *testing.T) {
t.Run("With success case: return a map", func(tc *testing.T) {
e := NewEnv()
input := []string{
"export TEST=true",
"export DATA=1,2,3",
"#export PASS=no",
"CI=true=test",
"T=",
"export T1='test data'",
"export T2=\"test data\"",
}

expected := map[string]interface{}{
"TEST": "true",
"DATA": "1,2,3",
"CI": "true=test",
"T": "",
"T1": "test data",
"T2": "test data",
}

content, err := e.ToJSON(input)
assert.NoError(tc, err)
assert.Equal(tc, len(expected), len(content))

for k, v := range expected {
assert.Contains(tc, content, k)
assert.Equal(tc, v, content[k])
}
})

t.Run("With error case: return an error key invalid", func(tc *testing.T) {
e := NewEnv()
input := []string{
"export test = true",
}
_, err := e.ToJSON(input)
assert.Error(tc, err)
assert.Contains(tc, err.Error(), "Env: Key test is invalid format")
})
}
2 changes: 2 additions & 0 deletions pkg/pull/converter.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ func NewConverter(format string) (Converter, error) {
switch strings.ToLower(format) {
case "tfvars":
return NewTfvars(), nil
case "env":
return NewEnv(), nil
default:
return nil, errors.New(fmt.Sprintf("`%s` is not yet supported.", format))
}
Expand Down
36 changes: 36 additions & 0 deletions pkg/pull/env.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package pull

import (
"fmt"
"github.com/pkg/errors"
envlib "github.com/vietanhduong/vault-converter/pkg/env"
osext "github.com/vietanhduong/vault-converter/pkg/util/os"
)

type env struct {
envLib *envlib.Env
}

func NewEnv() Converter {
return &env{
envLib: envlib.NewEnv(),
}
}

// Convert a source to ENV file
// src input is a map. It should be a JSON format
// output should be an absolute path
func (e *env) Convert(src map[string]interface{}, output string) error {

content := e.envLib.ToENV(src)

if err := osext.MkdirP(output); err != nil {
return errors.Wrap(err, "Pull: Create folder failed")
}

if err := osext.Write(content, output); err != nil {
return errors.Wrap(err, fmt.Sprintf("Pull: Write to %s failed", output))
}

return nil
}
41 changes: 41 additions & 0 deletions pkg/pull/env_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package pull

import (
"github.com/stretchr/testify/assert"
osext "github.com/vietanhduong/vault-converter/pkg/util/os"
"os"
"testing"
)

func TestEnv_Convert(t *testing.T) {
dir := os.TempDir()
defer os.Remove(dir)

t.Run("With success case: return no error", func(tc *testing.T) {
e := NewEnv()
raw := map[string]interface{}{
"test": "this is a str",
"arr": []interface{}{
map[string]interface{}{
"node": 1,
"ready": true,
},
map[string]interface{}{
"node": 2,
"ready": false,
},
},
"bool_val": false,
}
outputPath := dir + "/.env"
defer os.Remove(outputPath)
err := e.Convert(raw, outputPath)
assert.NoError(tc, err)

content, _ := osext.Cat(outputPath)
assert.Contains(tc, string(content), `export test="this is a str"`)
assert.Contains(tc, string(content), `export bool_val=false`)
assert.Contains(tc, string(content), `export arr=`)
})

}
2 changes: 1 addition & 1 deletion pkg/pull/tfvars_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"testing"
)

func TestConvert(t *testing.T) {
func TestTfvars_Convert(t *testing.T) {
dir := os.TempDir()
defer os.Remove(dir)

Expand Down
2 changes: 2 additions & 0 deletions pkg/push/converter.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ func NewConverter(format string) (Converter, error) {
switch strings.ToLower(format) {
case ".tfvars":
return NewTfvars(), nil
case ".env":
return NewEnv(), nil
default:
return nil, errors.New(fmt.Sprintf("`%s` is not yet supported.", format))
}
Expand Down
30 changes: 30 additions & 0 deletions pkg/push/env.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package push

import (
"github.com/pkg/errors"
envlib "github.com/vietanhduong/vault-converter/pkg/env"
"strings"
)

type env struct {
envLib *envlib.Env
}

func NewEnv() Converter {
return &env{
envLib: envlib.NewEnv(),
}
}

// Convert an ENV file to JSON
func (e *env) Convert(src []byte) (map[string]interface{}, error){

lines := strings.Split(string(src), "\n")

content, err := e.envLib.ToJSON(lines)
if err != nil {
return nil, errors.Wrap(err, "Push: Convert bytes to json failed")
}

return content, nil
}
35 changes: 35 additions & 0 deletions pkg/push/env_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package push

import (
"github.com/stretchr/testify/assert"
"testing"
)

func TestEnv_Convert(t *testing.T) {
t.Run("With success case: return a map", func(tc *testing.T) {
e := NewEnv()
content, err := e.Convert([]byte(`export test=true
data=1,2,3
export str='this is test string'
`))
assert.NoError(tc, err)
expected := map[string]interface{}{
"test": "true",
"data": "1,2,3",
"str": "this is test string",
}
assert.Equal(tc, len(expected), len(content))
for k, v := range expected {
assert.Contains(tc, content, k)
assert.Equal(tc, v, content[k])
}
})

t.Run("With error case: src invalid", func(tc *testing.T) {
e := NewTfvars()
_, err := e.Convert([]byte(`export name = "test"`))
assert.Error(tc, err)
assert.Contains(tc, err.Error(), "Push: Parse content to JSON failed")

})
}
2 changes: 1 addition & 1 deletion pkg/push/tfvars_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"testing"
)

func TestConvert(t *testing.T) {
func TestTfvars_Convert(t *testing.T) {
t.Run("With success case: return a map", func(tc *testing.T) {
tf := NewTfvars()
content, err := tf.Convert([]byte(`name = "test"
Expand Down

0 comments on commit 34d8117

Please sign in to comment.