feat(template): added secret template function (#1221)

* feat(tmpl): added fetchSecretValue template function

This adds a tmpl `fetchSecretValue` and `expandSecretRefs` function by:
- Adding:
    - `expandSecretRefs` function in tmpl package that uses vals
    package to fetch secrets
    - `fetchSecretValue` function in tmpl package like below but for
    single string value
    - gomock for tests purpose
- Changing:
    - move init of vals package to function (so the same instance can be used for template values and rendering the whole template)

* doc(secret): added doc how to use new tmpl methods

Added example usage of `fetchSecretValue` and `expandSecretRefs`
This commit is contained in:
Marcin Kaciuba
2020-04-25 21:10:02 +09:00
committed by GitHub
parent 3a19a394ab
commit b1190508b2
10 changed files with 301 additions and 145 deletions
+12 -10
View File
@@ -17,16 +17,18 @@ type Values = map[string]interface{}
func (c *Context) createFuncMap() template.FuncMap {
funcMap := template.FuncMap{
"exec": c.Exec,
"readFile": c.ReadFile,
"toYaml": ToYaml,
"fromYaml": FromYaml,
"setValueAtPath": SetValueAtPath,
"requiredEnv": RequiredEnv,
"get": get,
"getOrNil": getOrNil,
"tpl": c.Tpl,
"required": Required,
"exec": c.Exec,
"readFile": c.ReadFile,
"toYaml": ToYaml,
"fromYaml": FromYaml,
"setValueAtPath": SetValueAtPath,
"requiredEnv": RequiredEnv,
"get": get,
"getOrNil": getOrNil,
"tpl": c.Tpl,
"required": Required,
"fetchSecretValue": fetchSecretValue,
"expandSecretRefs": fetchSecretValues,
}
if c.preRender {
// disable potential side-effect template calls
+63
View File
@@ -0,0 +1,63 @@
package tmpl
import (
"errors"
"fmt"
"github.com/roboll/helmfile/pkg/plugins"
"github.com/variantdev/vals"
"sync"
)
//to generate mock run mockgen -source=expand_secret_ref.go -destination=expand_secrets_mock.go -package=tmpl
type valClient interface {
Eval(template map[string]interface{}) (map[string]interface{}, error)
}
var once sync.Once
var secretsClient valClient
func fetchSecretValue(path string) (string, error) {
tmpMap := make(map[string]interface{})
tmpMap["key"] = path
resultMap, err := fetchSecretValues(tmpMap)
if err != nil {
return "", err
}
rendered, ok := resultMap["key"]
if !ok {
return "", errors.New(fmt.Sprintf("unexpected error occurred, %v doesn't have 'key' key", resultMap))
}
result, ok := rendered.(string)
if !ok {
return "", errors.New(fmt.Sprintf("expected %v to be string", rendered))
}
return result, nil
}
func fetchSecretValues(values map[string]interface{}) (map[string]interface{}, error) {
var err error
// below lines are for tests
once.Do(func() {
var valRuntime *vals.Runtime
if secretsClient == nil {
valRuntime, err = plugins.ValsInstance()
if err != nil {
return
}
secretsClient = valRuntime
}
})
if secretsClient == nil {
return nil, err
}
resultMap, err := secretsClient.Eval(values)
if err != nil {
return nil, err
}
return resultMap, nil
}
+78
View File
@@ -0,0 +1,78 @@
package tmpl
import (
"errors"
"github.com/golang/mock/gomock"
"gotest.tools/assert"
"testing"
)
func Test_fetchSecretValue(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()
c := NewMockvalClient(controller)
secretsClient = c
secretPath := "ref+vault://key/#path"
expectArg := make(map[string]interface{})
expectArg["key"] = secretPath
valsResult := make(map[string]interface{})
valsResult["key"] = "key_value"
c.EXPECT().Eval(expectArg).Return(valsResult, nil)
result, err := fetchSecretValue(secretPath)
assert.NilError(t, err)
assert.Equal(t, result, "key_value")
}
func Test_fetchSecretValue_error(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()
c := NewMockvalClient(controller)
secretsClient = c
secretPath := "ref+vault://key/#path"
expectArg := make(map[string]interface{})
expectArg["key"] = secretPath
expectedErr := errors.New("some error")
c.EXPECT().Eval(expectArg).Return(nil, expectedErr)
result, err := fetchSecretValue(secretPath)
assert.Equal(t, err, expectedErr)
assert.Equal(t, result, "")
}
func Test_fetchSecretValue_no_key(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()
c := NewMockvalClient(controller)
secretsClient = c
secretPath := "ref+vault://key/#path"
expectArg := make(map[string]interface{})
expectArg["key"] = secretPath
valsResult := make(map[string]interface{})
c.EXPECT().Eval(expectArg).Return(valsResult, nil)
result, err := fetchSecretValue(secretPath)
assert.Error(t, err, "unexpected error occurred, map[] doesn't have 'key' key")
assert.Equal(t, result, "")
}
func Test_fetchSecretValue_invalid_type(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()
c := NewMockvalClient(controller)
secretsClient = c
secretPath := "ref+vault://key/#path"
expectArg := make(map[string]interface{})
expectArg["key"] = secretPath
valsResult := make(map[string]interface{})
valsResult["key"] = 10
c.EXPECT().Eval(expectArg).Return(valsResult, nil)
result, err := fetchSecretValue(secretPath)
assert.Error(t, err, "expected 10 to be string")
assert.Equal(t, result, "")
}
+45
View File
@@ -0,0 +1,45 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: expand_secret_ref.go
package tmpl
import (
gomock "github.com/golang/mock/gomock"
reflect "reflect"
)
// MockvalClient is a mock of valClient interface
type MockvalClient struct {
ctrl *gomock.Controller
recorder *MockvalClientMockRecorder
}
// MockvalClientMockRecorder is the mock recorder for MockvalClient
type MockvalClientMockRecorder struct {
mock *MockvalClient
}
// NewMockvalClient creates a new mock instance
func NewMockvalClient(ctrl *gomock.Controller) *MockvalClient {
mock := &MockvalClient{ctrl: ctrl}
mock.recorder = &MockvalClientMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (_m *MockvalClient) EXPECT() *MockvalClientMockRecorder {
return _m.recorder
}
// Eval mocks base method
func (_m *MockvalClient) Eval(template map[string]interface{}) (map[string]interface{}, error) {
ret := _m.ctrl.Call(_m, "Eval", template)
ret0, _ := ret[0].(map[string]interface{})
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Eval indicates an expected call of Eval
func (_mr *MockvalClientMockRecorder) Eval(arg0 interface{}) *gomock.Call {
return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "Eval", reflect.TypeOf((*MockvalClient)(nil).Eval), arg0)
}