Skip to content

Commit

Permalink
feat: Add configuration data source (#36)
Browse files Browse the repository at this point in the history
* feat(config): Add valuable type to allow data source reference

* feat(storage): Use valuable configuration type for redis credentials

* feat(storage): Use valuable configuration type for postgres credentials

* fix(config): Valuable needed to be recovered once to work

* feat(security): Factory compareWithStaticValue use Valuable now

* docs(example): Add Valuable to the example

* docs(readme): Update roadmap for 0.3
  • Loading branch information
42atomys authored Mar 5, 2022
1 parent 7d56e97 commit 0a4f06b
Show file tree
Hide file tree
Showing 10 changed files with 484 additions and 33 deletions.
Binary file modified .github/profile/roadmap.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ specs:
- getHeader:
name: X-Hook-Secret
- compareWithStaticValue:
value: 'test'
values: ['foo', 'bar']
valueFrom:
envRef: SECRET_TOKEN
# Storage allows you to list where you want to store the raw payloads
# received by webhooked. You can add an unlimited number of storages, webhooked
# will store in **ALL** the listed storages
Expand Down
7 changes: 5 additions & 2 deletions config/webhooks.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ specs:
- getHeader:
name: X-Hook-Secret
- compareWithStaticValue:
value: 'test'
values: ['foo', 'bar']
valueFrom:
envRef: SECRET_TOKEN
storage:
- type: redis
specs:
host: redis
port: 6379
database: 0
password:
valueFrom:
envRef: REDIS_PASSWORD
key: example-webhook
37 changes: 37 additions & 0 deletions internal/valuable/mapstructure_decode.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package valuable

import (
"reflect"

"github.com/mitchellh/mapstructure"
)

// Decode decodes the given data into the given result.
// In case of the target Type if a Valuable, we serialize it with
// `SerializeValuable` func.
// @param input is the data to decode
// @param output is the result of the decoding
// @return an error if the decoding failed
func Decode(input, output interface{}) (err error) {
var decoder *mapstructure.Decoder

decoder, err = mapstructure.NewDecoder(&mapstructure.DecoderConfig{
Result: output,
DecodeHook: valuableDecodeHook,
})
if err != nil {
return err
}

return decoder.Decode(input)
}

// valuableDecodeHook is a mapstructure.DecodeHook that serializes
// the given data into a Valuable.
func valuableDecodeHook(f reflect.Type, t reflect.Type, data interface{}) (interface{}, error) {
if t != reflect.TypeOf(Valuable{}) {
return data, nil
}

return SerializeValuable(data)
}
111 changes: 111 additions & 0 deletions internal/valuable/mapstructure_decode_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package valuable

import (
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)

type TestSuiteValuableDecode struct {
suite.Suite

testValue, testValueCommaSeparated string
testValues []string
}

func (suite *TestSuiteValuableDecode) BeforeTest(suiteName, testName string) {
suite.testValue = "testValue"
suite.testValues = []string{"testValue1", "testValue2"}
suite.testValueCommaSeparated = "testValue3,testValue4"
}

func (suite *TestSuiteValuableDecode) TestDecodeInvalidOutput() {
assert := assert.New(suite.T())

err := Decode(map[string]interface{}{"value": suite.testValue}, nil)
assert.Error(err)
}

func (suite *TestSuiteValuableDecode) TestDecodeString() {
assert := assert.New(suite.T())

type strukt struct {
Value string `mapstructure:"value"`
}

output := strukt{}
err := Decode(map[string]interface{}{"value": suite.testValue}, &output)
assert.NoError(err)
assert.Equal(suite.testValue, output.Value)
}

func (suite *TestSuiteValuableDecode) TestDecodeValuableRootString() {
assert := assert.New(suite.T())

type strukt struct {
Value Valuable `mapstructure:"value"`
}

output := strukt{}
err := Decode(map[string]interface{}{"value": suite.testValue}, &output)
assert.NoError(err)
assert.Equal(suite.testValue, output.Value.First())
}

func (suite *TestSuiteValuableDecode) TestDecodeValuableRootBool() {
assert := assert.New(suite.T())

type strukt struct {
Value Valuable `mapstructure:"value"`
}

output := strukt{}
err := Decode(map[string]interface{}{"value": true}, &output)
assert.NoError(err)
assert.Equal("true", output.Value.First())
}

func (suite *TestSuiteValuableDecode) TestDecodeValuableValue() {
assert := assert.New(suite.T())

type strukt struct {
Value Valuable `mapstructure:"value"`
}

output := strukt{}
err := Decode(map[string]interface{}{"value": map[string]interface{}{"value": suite.testValue}}, &output)
assert.NoError(err)
assert.Equal(suite.testValue, output.Value.First())
}

func (suite *TestSuiteValuableDecode) TestDecodeValuableValues() {
assert := assert.New(suite.T())

type strukt struct {
Value Valuable `mapstructure:"value"`
}

output := strukt{}
err := Decode(map[string]interface{}{"value": map[string]interface{}{"values": suite.testValues}}, &output)
assert.NoError(err)
assert.Equal(suite.testValues, output.Value.Get())
}

func (suite *TestSuiteValuableDecode) TestDecodeValuableStaticValuesWithComma() {
assert := assert.New(suite.T())

type strukt struct {
Value Valuable `mapstructure:"value"`
}

output := strukt{}
err := Decode(map[string]interface{}{"value": map[string]interface{}{"valueFrom": map[string]interface{}{"staticRef": suite.testValueCommaSeparated}}}, &output)
assert.NoError(err)
assert.Equal(strings.Split(suite.testValueCommaSeparated, ","), output.Value.Get())
}

func TestRunSuiteValuableDecode(t *testing.T) {
suite.Run(t, new(TestSuiteValuableDecode))
}
137 changes: 137 additions & 0 deletions internal/valuable/valuable.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package valuable

import (
"errors"
"fmt"
"os"
"strings"

"github.com/mitchellh/mapstructure"
)

// Valuable represent value who it is possible to retrieve the data
// in multiple ways. From a simple value without nesting,
// or from a deep data source.
type Valuable struct {
// Value represents the `value` field of a configuration entry that
// contains only one value
Value *string `json:"value,omitempty"`
// Values represents the `value` field of a configuration entry that
// contains multiple values stored in a list
Values []string `json:"values,omitempty"`
// ValueFrom represents the `valueFrom` field of a configuration entry
// that contains a reference to a data source
ValueFrom *ValueFromSource `json:"valueFrom,omitempty"`
}

type ValueFromSource struct {
// StaticRef represents the `staticRef` field of a configuration entry
// that contains a static value. Can contain a comma separated list
StaticRef *string `json:"staticRef,omitempty"`
// EnvRef represents the `envRef` field of a configuration entry
// that contains a reference to an environment variable
EnvRef *string `json:"envRef,omitempty"`
}

// SerializeValuable serialize anything to a Valuable
// @param data is the data to serialize
// @return the serialized Valuable
func SerializeValuable(data interface{}) (*Valuable, error) {
switch t := data.(type) {
case string:
return &Valuable{Value: &t}, nil
case int, float32, float64, bool:
str := fmt.Sprint(t)
return &Valuable{Value: &str}, nil
case nil:
return &Valuable{}, nil
default:
valuable := Valuable{}
if err := mapstructure.Decode(data, &valuable); err != nil {
return nil, errors.New("unimplemented valuable type")
}
return &valuable, nil
}
}

// Get returns all values of the Valuable as a slice
// @return the slice of values
func (v *Valuable) Get() []string {
var computedValues []string

computedValues = append(computedValues, v.Values...)

if v.Value != nil && !contains(computedValues, *v.Value) {
computedValues = append(computedValues, *v.Value)
}

if v.ValueFrom == nil {
return computedValues
}

if v.ValueFrom.StaticRef != nil && !contains(computedValues, *v.ValueFrom.StaticRef) {
computedValues = appendCommaListIfAbsent(computedValues, *v.ValueFrom.StaticRef)
}

if v.ValueFrom.EnvRef != nil {
computedValues = appendCommaListIfAbsent(computedValues, os.Getenv(*v.ValueFrom.EnvRef))
}

return computedValues
}

// First returns the first value of the Valuable possible values
// as a string. The order of preference is:
// - Values
// - Value
// - ValueFrom.StaticRef
// - ValueFrom.EnvRef
// @return the first value
func (v *Valuable) First() string {
if len(v.Get()) == 0 {
return ""
}

return v.Get()[0]
}

// Contains returns true if the Valuable contains the given value
// @param value is the value to check
// @return true if the Valuable contains the given value
func (v *Valuable) Contains(element string) bool {
for _, s := range v.Get() {
if s == element {
return true
}
}
return false
}

// contains returns true if the Valuable contains the given value.
// This function is private to prevent stack overflow during the initialization
// of the Valuable object.
// @param
// @param value is the value to check
// @return true if the Valuable contains the given value
func contains(slice []string, element string) bool {
for _, s := range slice {
if s == element {
return true
}
}
return false
}

// appendCommaListIfAbsent accept a string list separated with commas to append
// to the Values all elements of this list only if element is absent
// of the Values
func appendCommaListIfAbsent(slice []string, commaList string) []string {
for _, s := range strings.Split(commaList, ",") {
if contains(slice, s) {
continue
}

slice = append(slice, s)
}
return slice
}
Loading

0 comments on commit 0a4f06b

Please sign in to comment.