Skip to content

Commit

Permalink
support non-string template parameter substitution
Browse files Browse the repository at this point in the history
  • Loading branch information
bparees committed Oct 21, 2016
1 parent 92a71d4 commit b4be712
Show file tree
Hide file tree
Showing 5 changed files with 162 additions and 46 deletions.
17 changes: 8 additions & 9 deletions examples/jenkins/application-template.json
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,10 @@
}
],
"resources": {
"limits": {
"memory": "${MEMORY_LIMIT}"
}
},
"limits": {
"memory": "${MEMORY_LIMIT}"
}
},
"terminationMessagePath": "/dev/termination-log",
"imagePullPolicy": "IfNotPresent",
"securityContext": {
Expand All @@ -131,7 +131,6 @@
},
"status": {}
},

{
"kind": "Service",
"apiVersion": "v1",
Expand Down Expand Up @@ -294,10 +293,10 @@
}
],
"resources": {
"limits": {
"memory": "${MEMORY_LIMIT}"
}
},
"limits": {
"memory": "${MEMORY_LIMIT}"
}
},
"terminationMessagePath": "/dev/termination-log",
"imagePullPolicy": "IfNotPresent",
"securityContext": {
Expand Down
40 changes: 32 additions & 8 deletions pkg/template/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ import (
"github.com/openshift/origin/pkg/util/stringreplace"
)

var parameterExp = regexp.MustCompile(`\$\{([a-zA-Z0-9\_]+)\}`)
// match ${KEY}, KEY will be grouped
var stringParameterExp = regexp.MustCompile(`\$\{([a-zA-Z0-9\_]+?)\}`)

// match ${{KEY}} exact match only, KEY will be grouped
var nonStringParameterExp = regexp.MustCompile(`^\$\{\{([a-zA-Z0-9\_]+)\}\}$`)

// Processor process the Template into the List with substituted parameters
type Processor struct {
Expand Down Expand Up @@ -46,7 +50,7 @@ func (p *Processor) Process(template *api.Template) field.ErrorList {

// Perform parameter substitution on the template's user message. This can be used to
// instruct a user on next steps for the template.
template.Message = p.EvaluateParameterSubstitution(paramMap, template.Message)
template.Message, _ = p.EvaluateParameterSubstitution(paramMap, template.Message)

itemPath := field.NewPath("item")
for i, item := range template.Objects {
Expand Down Expand Up @@ -125,16 +129,36 @@ func GetParameterByName(t *api.Template, name string) *api.Parameter {
}

// EvaluateParameterSubstitution replaces escaped parameters in a string with values from the
// provided map.
func (p *Processor) EvaluateParameterSubstitution(params map[string]api.Parameter, in string) string {
for _, match := range parameterExp.FindAllStringSubmatch(in, -1) {
// provided map. Returns the substituted value (if any substitution applied) and a boolean
// indicating if the resulting value should be treated as a string(true) or a non-string
// value(false) for purposes of json encoding.
func (p *Processor) EvaluateParameterSubstitution(params map[string]api.Parameter, in string) (string, bool) {
out := in
// First check if the value matches the "${{KEY}}" substitution syntax, which
// means replace and drop the quotes because the parameter value is to be used
// as a non-string value. If we hit a match here, we're done because the
// "${{KEY}}" syntax is exact match only, it cannot be used in a value like
// "FOO_${{KEY}}_BAR", no substitution will be performed if it is used in that way.
for _, match := range nonStringParameterExp.FindAllStringSubmatch(in, -1) {
if len(match) > 1 {
if paramValue, found := params[match[1]]; found {
out = strings.Replace(out, match[0], paramValue.Value, 1)
return out, false
}
}
}

// If we didn't do a non-string substitution above, do normal string substitution
// on the value here if it contains a "${KEY}" reference. This substitution does
// allow multiple matches and prefix/postfix, eg "FOO_${KEY1}_${KEY2}_BAR"
for _, match := range stringParameterExp.FindAllStringSubmatch(in, -1) {
if len(match) > 1 {
if paramValue, found := params[match[1]]; found {
in = strings.Replace(in, match[0], paramValue.Value, 1)
out = strings.Replace(out, match[0], paramValue.Value, 1)
}
}
}
return in
return out, true
}

// SubstituteParameters loops over all values defined in structured
Expand All @@ -144,7 +168,7 @@ func (p *Processor) EvaluateParameterSubstitution(params map[string]api.Paramete
// - ${PARAMETER_NAME}
//
func (p *Processor) SubstituteParameters(params map[string]api.Parameter, item runtime.Object) (runtime.Object, error) {
stringreplace.VisitObjectStrings(item, func(in string) string {
stringreplace.VisitObjectStrings(item, func(in string) (string, bool) {
return p.EvaluateParameterSubstitution(params, in)
})
return item, nil
Expand Down
51 changes: 51 additions & 0 deletions pkg/template/template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,57 @@ func TestParameterGenerators(t *testing.T) {
}
}

func TestProcessValue(t *testing.T) {
var template api.Template
if err := runtime.DecodeInto(kapi.Codecs.UniversalDecoder(), []byte(`{
"kind":"Template", "apiVersion":"v1",
"objects": [
{
"kind": "Service", "apiVersion": "v${VALUE}",
"metadata": {
"labels": {
"key1": "${VALUE}",
"key2": "$${VALUE}",
"s1_s1": "${STRING_1}_${STRING_1}",
"s1_s2": "${STRING_1}_${STRING_2}",
"i1": "${{INT_1}}",
"untouched": "a${{INT_1}}",
"untouched2": "${{INT_1}}a"
}
}
}
]
}`), &template); err != nil {
t.Fatalf("unexpected error: %v", err)
}
generators := map[string]generator.Generator{
"expression": generator.NewExpressionValueGenerator(rand.New(rand.NewSource(1337))),
}
processor := NewProcessor(generators)

// Define custom parameter for the transformation:
AddParameter(&template, makeParameter("VALUE", "1", "", false))
AddParameter(&template, makeParameter("STRING_1", "string1", "", false))
AddParameter(&template, makeParameter("STRING_2", "string2", "", false))
AddParameter(&template, makeParameter("INT_1", "1", "", false))

// Transform the template config into the result config
errs := processor.Process(&template)
if len(errs) > 0 {
t.Fatalf("unexpected error: %v", errs)
}
result, err := runtime.Encode(kapi.Codecs.LegacyCodec(v1.SchemeGroupVersion), &template)
if err != nil {
t.Fatalf("unexpected error during encoding Config: %#v", err)
}
expect := `{"kind":"Template","apiVersion":"v1","metadata":{"creationTimestamp":null},"objects":[{"apiVersion":"v1","kind":"Service","metadata":{"labels":{"i1":1,"key1":"1","key2":"$1","s1_s1":"string1_string1","s1_s2":"string1_string2","untouched":"a${{INT_1}}","untouched2":"${{INT_1}}a"}}}],"parameters":[{"name":"VALUE","value":"1"},{"name":"STRING_1","value":"string1"},{"name":"STRING_2","value":"string2"},{"name":"INT_1","value":"1"}]}`
stringResult := strings.TrimSpace(string(result))
if expect != stringResult {
//t.Errorf("unexpected output, expected: \n%s\nGot:\n%s\n", expect, stringResult)
t.Errorf("unexpected output: %s", diff.StringDiff(expect, stringResult))
}
}

func TestProcessValueEscape(t *testing.T) {
var template api.Template
if err := runtime.DecodeInto(kapi.Codecs.UniversalDecoder(), []byte(`{
Expand Down
84 changes: 63 additions & 21 deletions pkg/util/stringreplace/object.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package stringreplace

import (
"encoding/json"
"fmt"
"reflect"

"github.com/golang/glog"
Expand All @@ -9,62 +11,82 @@ import (
// VisitObjectStrings visits recursively all string fields in the object and call the
// visitor function on them. The visitor function can be used to modify the
// value of the string fields.
func VisitObjectStrings(obj interface{}, visitor func(string) string) {
visitValue(reflect.ValueOf(obj), visitor)
func VisitObjectStrings(obj interface{}, visitor func(string) (string, bool)) error {
return visitValue(reflect.ValueOf(obj), visitor)
}

func visitValue(v reflect.Value, visitor func(string) string) {
func visitValue(v reflect.Value, visitor func(string) (string, bool)) error {
// you'll never be able to substitute on a nil. Check the kind first or you'll accidentally
// end up panic-ing
switch v.Kind() {
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
if v.IsNil() {
return
return nil
}
}

switch v.Kind() {

case reflect.Ptr:
visitValue(v.Elem(), visitor)
err := visitValue(v.Elem(), visitor)
if err != nil {
return err
}
case reflect.Interface:
visitValue(reflect.ValueOf(v.Interface()), visitor)

err := visitValue(reflect.ValueOf(v.Interface()), visitor)
if err != nil {
return err
}
case reflect.Slice, reflect.Array:
vt := v.Type().Elem()
for i := 0; i < v.Len(); i++ {
val := visitUnsettableValues(vt, v.Index(i), visitor)
val, err := visitUnsettableValues(vt, v.Index(i), visitor)
if err != nil {
return err
}
v.Index(i).Set(val)
}
case reflect.Struct:
for i := 0; i < v.NumField(); i++ {
visitValue(v.Field(i), visitor)
err := visitValue(v.Field(i), visitor)
if err != nil {
return err
}
}

case reflect.Map:
vt := v.Type().Elem()
for _, oldKey := range v.MapKeys() {
newKey := visitUnsettableValues(oldKey.Type(), oldKey, visitor)
newKey, err := visitUnsettableValues(oldKey.Type(), oldKey, visitor)
if err != nil {
return err
}

oldValue := v.MapIndex(oldKey)
newValue := visitUnsettableValues(vt, oldValue, visitor)
newValue, err := visitUnsettableValues(vt, oldValue, visitor)
if err != nil {
return err
}
v.SetMapIndex(oldKey, reflect.Value{})
v.SetMapIndex(newKey, newValue)
}

case reflect.String:
if !v.CanSet() {
glog.Infof("Unable to set String value '%v'", v)
return
return fmt.Errorf("Unable to set String value '%v'", v)
}
v.SetString(visitor(v.String()))

s, asString := visitor(v.String())
if !asString {
return fmt.Errorf("Attempted to set String field to non-string value '%v'", s)
}
v.SetString(s)
default:
glog.V(5).Infof("Unknown field type '%s': %v", v.Kind(), v)
return nil
}
return nil
}

// visitUnsettableValues creates a copy of the object you want to modify and returns the modified result
func visitUnsettableValues(typeOf reflect.Type, original reflect.Value, visitor func(string) string) reflect.Value {
func visitUnsettableValues(typeOf reflect.Type, original reflect.Value, visitor func(string) (string, bool)) (reflect.Value, error) {
val := reflect.New(typeOf).Elem()
existing := original
// if the value type is interface, we must resolve it to a concrete value prior to setting it back.
Expand All @@ -73,14 +95,34 @@ func visitUnsettableValues(typeOf reflect.Type, original reflect.Value, visitor
}
switch existing.Kind() {
case reflect.String:
s := visitor(existing.String())
val.Set(reflect.ValueOf(s))
s, asString := visitor(existing.String())

if asString {
val = reflect.ValueOf(s)
} else {
b := []byte(s)
var data interface{}
err := json.Unmarshal(b, &data)
if err != nil {
// the result of substitution may have been an unquoted string value,
// which is an error when decoding in json(only "true", "false", and numeric
// values can be unquoted), so try wrapping the value in quotes so it will be
// properly converted to a string type during decoding.
b = []byte(fmt.Sprintf("\"%s\"", s))
err := json.Unmarshal(b, &data)
if err != nil {
return reflect.Value{}, err
}
}
val = reflect.ValueOf(data)
}

default:
if existing.IsValid() && existing.Kind() != reflect.Invalid {
val.Set(existing)
}
visitValue(val, visitor)
}

return val
return val, nil
}
16 changes: 8 additions & 8 deletions pkg/util/stringreplace/object_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,14 @@ func TestVisitObjectStringsOnStruct(t *testing.T) {
},
}
for i := range samples {
VisitObjectStrings(&samples[i][0], func(in string) string {
VisitObjectStrings(&samples[i][0], func(in string) (string, bool) {
if len(in) == 0 {
return in
return in, true
}
return fmt.Sprintf("sample-%s", in)
return fmt.Sprintf("sample-%s", in), true
})
if !reflect.DeepEqual(samples[i][0], samples[i][1]) {
t.Errorf("Got %#v, expected %#v", samples[i][0], samples[i][1])
t.Errorf("[%d] Got:\n%#v\nExpected:\n%#v", i, samples[i][0], samples[i][1])
}
}
}
Expand All @@ -82,8 +82,8 @@ func TestVisitObjectStringsOnMap(t *testing.T) {
}

for i := range samples {
VisitObjectStrings(&samples[i][0], func(in string) string {
return fmt.Sprintf("sample-%s", in)
VisitObjectStrings(&samples[i][0], func(in string) (string, bool) {
return fmt.Sprintf("sample-%s", in), true
})
if !reflect.DeepEqual(samples[i][0], samples[i][1]) {
t.Errorf("Got %#v, expected %#v", samples[i][0], samples[i][1])
Expand All @@ -100,8 +100,8 @@ func TestVisitObjectStringsOnArray(t *testing.T) {
}

for i := range samples {
VisitObjectStrings(&samples[i][0], func(in string) string {
return fmt.Sprintf("sample-%s", in)
VisitObjectStrings(&samples[i][0], func(in string) (string, bool) {
return fmt.Sprintf("sample-%s", in), true
})
if !reflect.DeepEqual(samples[i][0], samples[i][1]) {
t.Errorf("Got %#v, expected %#v", samples[i][0], samples[i][1])
Expand Down

0 comments on commit b4be712

Please sign in to comment.