diff --git a/examples/jenkins/application-template.json b/examples/jenkins/application-template.json index 26a75ec0dce2..53f6448f603e 100644 --- a/examples/jenkins/application-template.json +++ b/examples/jenkins/application-template.json @@ -112,10 +112,10 @@ } ], "resources": { - "limits": { - "memory": "${MEMORY_LIMIT}" - } - }, + "limits": { + "memory": "${MEMORY_LIMIT}" + } + }, "terminationMessagePath": "/dev/termination-log", "imagePullPolicy": "IfNotPresent", "securityContext": { @@ -131,7 +131,6 @@ }, "status": {} }, - { "kind": "Service", "apiVersion": "v1", @@ -294,10 +293,10 @@ } ], "resources": { - "limits": { - "memory": "${MEMORY_LIMIT}" - } - }, + "limits": { + "memory": "${MEMORY_LIMIT}" + } + }, "terminationMessagePath": "/dev/termination-log", "imagePullPolicy": "IfNotPresent", "securityContext": { diff --git a/pkg/template/template.go b/pkg/template/template.go index 68b081354416..163614cb7bc4 100644 --- a/pkg/template/template.go +++ b/pkg/template/template.go @@ -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 { @@ -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 { @@ -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 @@ -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 diff --git a/pkg/template/template_test.go b/pkg/template/template_test.go index 0e6475e1e58f..d694d7db1214 100644 --- a/pkg/template/template_test.go +++ b/pkg/template/template_test.go @@ -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(`{ diff --git a/pkg/util/stringreplace/object.go b/pkg/util/stringreplace/object.go index f5557b4f8d77..bc4ee6b033bd 100644 --- a/pkg/util/stringreplace/object.go +++ b/pkg/util/stringreplace/object.go @@ -1,6 +1,8 @@ package stringreplace import ( + "encoding/json" + "fmt" "reflect" "github.com/golang/glog" @@ -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. @@ -73,8 +95,28 @@ 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) @@ -82,5 +124,5 @@ func visitUnsettableValues(typeOf reflect.Type, original reflect.Value, visitor visitValue(val, visitor) } - return val + return val, nil } diff --git a/pkg/util/stringreplace/object_test.go b/pkg/util/stringreplace/object_test.go index b6e5c8a4bff6..593448e51e86 100644 --- a/pkg/util/stringreplace/object_test.go +++ b/pkg/util/stringreplace/object_test.go @@ -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]) } } } @@ -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]) @@ -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])