Skip to content

Commit

Permalink
Use true generic objects in templates
Browse files Browse the repository at this point in the history
Allow templates to handle map[string]interface{}
  • Loading branch information
smarterclayton committed Apr 29, 2015
1 parent 26a1c5e commit 3de701d
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 43 deletions.
44 changes: 37 additions & 7 deletions pkg/template/template.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package template

import (
"encoding/json"
"fmt"
"regexp"
"strings"
Expand Down Expand Up @@ -40,20 +41,49 @@ func (p *Processor) Process(template *api.Template) (*configapi.Config, fielderr
}

for i, item := range template.Objects {
if obj, ok := item.(*runtime.Unknown); ok {
contents := make(map[string]interface{})
if err := json.Unmarshal(obj.RawJSON, &contents); err != nil {
util.ReportError(&templateErrors, i, *fielderrors.NewFieldInvalid("objects", err, "unable to handle object"))
continue
}
item = &runtime.Unstructured{
TypeMeta: obj.TypeMeta,
Object: contents,
}
}

newItem, err := p.SubstituteParameters(template.Parameters, item)
if err != nil {
util.ReportError(&templateErrors, i, *fielderrors.NewFieldNotSupported("parameters", err))
}
// Remove namespace from the item
itemMeta, err := meta.Accessor(newItem)
if err != nil {
util.ReportError(&templateErrors, i, *fielderrors.NewFieldInvalid("namespace", err, "failed to remove the item namespace"))
}
itemMeta.SetNamespace("")
stripNamespace(newItem)
template.Objects[i] = newItem
}

return &configapi.Config{Items: template.Objects}, templateErrors.Prefix("Template")
return &configapi.Config{Items: template.Objects}, templateErrors
}

func stripNamespace(obj runtime.Object) {
// Remove namespace from the item
if itemMeta, err := meta.Accessor(obj); err == nil {
itemMeta.SetNamespace("")
return
}
if unstruct, ok := obj.(*runtime.Unstructured); ok && unstruct.Object != nil {
if obj, ok := unstruct.Object["metadata"]; ok {
if m, ok := obj.(map[string]interface{}); ok {
if _, ok := m["namespace"]; ok {
m["namespace"] = ""
}
}
return
}
if _, ok := unstruct.Object["namespace"]; ok {
unstruct.Object["namespace"] = ""
return
}
}
}

// AddParameter adds new custom parameter to the Template. It overrides
Expand Down
55 changes: 46 additions & 9 deletions pkg/template/template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,54 @@ func TestParameterGenerators(t *testing.T) {
}
}

func TestProcessValueEscape(t *testing.T) {
var template api.Template
if err := latest.Codec.DecodeInto([]byte(`{
"kind":"Template", "apiVersion":"v1beta1",
"items": [
{
"kind": "Service", "apiVersion": "v1beta3${VALUE}",
"metadata": {
"labels": {
"key1": "${VALUE}",
"key2": "$${VALUE}"
}
}
}
]
}`), &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", ""))

// Transform the template config into the result config
config, errs := processor.Process(&template)
if len(errs) > 0 {
t.Fatalf("unexpected error: %v", errs)
}
result, err := latest.Codec.Encode(config)
if err != nil {
t.Fatalf("unexpected error during encoding Config: %#v", err)
}
expect := `{"kind":"Config","apiVersion":"v1beta1","metadata":{"creationTimestamp":null},"items":[{"apiVersion":"v1beta31","kind":"Service","metadata":{"labels":{"key1":"1","key2":"$1"}}}]}`
if expect != string(result) {
t.Errorf("unexpected output: %s", util.StringDiff(expect, string(result)))
}
}

func TestProcessTemplateParameters(t *testing.T) {
var template api.Template
jsonData, _ := ioutil.ReadFile("../../test/templates/fixtures/guestbook.json")
latest.Codec.DecodeInto(jsonData, &template)
if err := latest.Codec.DecodeInto(jsonData, &template); err != nil {
t.Fatalf("unexpected error: %v", err)
}

generators := map[string]generator.Generator{
"expression": generator.NewExpressionValueGenerator(rand.New(rand.NewSource(1337))),
Expand All @@ -128,14 +172,7 @@ func TestProcessTemplateParameters(t *testing.T) {
if err != nil {
t.Fatalf("unexpected error during encoding Config: %#v", err)
}
expect := `
{"kind":"Config","apiVersion":"v1beta1","metadata":{"creationTimestamp":null},"items":[{"kind":"Route","apiVersion":"v1beta1","metadata":{"name":"frontend-route","creationTimestamp":null},"host":"guestbook.example.com","serviceName":"frontend-service"},
{"kind":"Service","id":"frontend-service","creationTimestamp":null,"apiVersion":"v1beta1","port":5432,"protocol":"TCP","containerPort":0,"selector":{"name":"frontend-service"},"sessionAffinity":"None","ports":[{"name":"","protocol":"TCP","port":5432,"containerPort":0}]},
{"kind":"Service","id":"redis-master","creationTimestamp":null,"apiVersion":"v1beta1","port":10000,"protocol":"TCP","containerPort":0,"selector":{"name":"redis-master"},"sessionAffinity":"None","ports":[{"name":"","protocol":"TCP","port":10000,"containerPort":0}]},
{"kind":"Service","id":"redis-slave","creationTimestamp":null,"apiVersion":"v1beta1","port":10001,"protocol":"TCP","containerPort":0,"selector":{"name":"redis-slave"},"sessionAffinity":"None","ports":[{"name":"","protocol":"TCP","port":10001,"containerPort":0}]},
{"kind":"Pod","id":"redis-master","creationTimestamp":null,"apiVersion":"v1beta1","labels":{"name":"redis-master"},"desiredState":{"manifest":{"version":"v1beta2","id":"","volumes":null,"containers":[{"name":"master","image":"dockerfile/redis","ports":[{"containerPort":6379,"protocol":"TCP"}],"env":[{"name":"REDIS_PASSWORD","key":"REDIS_PASSWORD","value":"P8vxbV4C"}],"resources":{},"terminationMessagePath":"/dev/termination-log","imagePullPolicy":"PullIfNotPresent","capabilities":{}}],"restartPolicy":{"always":{}},"dnsPolicy":"ClusterFirst"}},"currentState":{"manifest":{"version":"","id":"","volumes":null,"containers":null,"restartPolicy":{}}}},
{"kind":"ReplicationController","id":"guestbook","creationTimestamp":null,"apiVersion":"v1beta1","desiredState":{"replicas":3,"replicaSelector":{"name":"frontend-service"},"podTemplate":{"desiredState":{"manifest":{"version":"v1beta2","id":"","volumes":null,"containers":[{"name":"php-redis","image":"brendanburns/php-redis","ports":[{"hostPort":8000,"containerPort":80,"protocol":"TCP"}],"env":[{"name":"ADMIN_USERNAME","key":"ADMIN_USERNAME","value":"adminQ3H"},{"name":"ADMIN_PASSWORD","key":"ADMIN_PASSWORD","value":"dwNJiJwW"},{"name":"REDIS_PASSWORD","key":"REDIS_PASSWORD","value":"P8vxbV4C"}],"resources":{},"terminationMessagePath":"/dev/termination-log","imagePullPolicy":"PullIfNotPresent","capabilities":{}}],"restartPolicy":{"always":{}},"dnsPolicy":"ClusterFirst"}},"labels":{"name":"frontend-service"}}},"currentState":{"replicas":0,"podTemplate":{"desiredState":{"manifest":{"version":"","id":"","volumes":null,"containers":null,"restartPolicy":{}}}}}},
{"kind":"ReplicationController","id":"redis-slave","creationTimestamp":null,"apiVersion":"v1beta1","desiredState":{"replicas":2,"replicaSelector":{"name":"redis-slave"},"podTemplate":{"desiredState":{"manifest":{"version":"v1beta2","id":"","volumes":null,"containers":[{"name":"slave","image":"brendanburns/redis-slave","ports":[{"hostPort":6380,"containerPort":6379,"protocol":"TCP"}],"env":[{"name":"REDIS_PASSWORD","key":"REDIS_PASSWORD","value":"P8vxbV4C"}],"resources":{},"terminationMessagePath":"/dev/termination-log","imagePullPolicy":"PullIfNotPresent","capabilities":{}}],"restartPolicy":{"always":{}},"dnsPolicy":"ClusterFirst"}},"labels":{"name":"redis-slave"}}},"currentState":{"replicas":0,"podTemplate":{"desiredState":{"manifest":{"version":"","id":"","volumes":null,"containers":null,"restartPolicy":{}}}}}}]}`
expect := `{"kind":"Config","apiVersion":"v1beta1","metadata":{"creationTimestamp":null},"items":[{"apiVersion":"v1beta1","host":"guestbook.example.com","id":"frontend-route","kind":"Route","metadata":{"name":"frontend-route"},"serviceName":"frontend-service"},{"apiVersion":"v1beta1","id":"frontend-service","kind":"Service","port":5432,"selector":{"name":"frontend-service"}},{"apiVersion":"v1beta1","id":"redis-master","kind":"Service","port":10000,"selector":{"name":"redis-master"}},{"apiVersion":"v1beta1","id":"redis-slave","kind":"Service","port":10001,"selector":{"name":"redis-slave"}},{"apiVersion":"v1beta1","desiredState":{"manifest":{"containers":[{"env":[{"name":"REDIS_PASSWORD","value":"P8vxbV4C"}],"image":"dockerfile/redis","name":"master","ports":[{"containerPort":6379}]}],"name":"redis-master","version":"v1beta1"}},"id":"redis-master","kind":"Pod","labels":{"name":"redis-master"}},{"apiVersion":"v1beta1","desiredState":{"podTemplate":{"desiredState":{"manifest":{"containers":[{"env":[{"name":"ADMIN_USERNAME","value":"adminQ3H"},{"name":"ADMIN_PASSWORD","value":"dwNJiJwW"},{"name":"REDIS_PASSWORD","value":"P8vxbV4C"}],"image":"brendanburns/php-redis","name":"php-redis","ports":[{"containerPort":80,"hostPort":8000}]}],"name":"guestbook","version":"v1beta1"}},"labels":{"name":"frontend-service"}},"replicaSelector":{"name":"frontend-service"},"replicas":3},"id":"guestbook","kind":"ReplicationController"},{"apiVersion":"v1beta1","desiredState":{"podTemplate":{"desiredState":{"manifest":{"containers":[{"env":[{"name":"REDIS_PASSWORD","value":"P8vxbV4C"}],"image":"brendanburns/redis-slave","name":"slave","ports":[{"containerPort":6379,"hostPort":6380}]}],"id":"redis-slave","version":"v1beta1"}},"labels":{"name":"redis-slave"}},"replicaSelector":{"name":"redis-slave"},"replicas":2},"id":"redis-slave","kind":"ReplicationController"}]}`
expect = strings.Replace(expect, "\n", "", -1)
if string(result) != expect {
t.Errorf("unexpected output: %s", util.StringDiff(expect, string(result)))
Expand Down
65 changes: 38 additions & 27 deletions pkg/util/stringreplace/object.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,44 +10,55 @@ import (
// 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) {
v := reflect.ValueOf(obj).Elem()

visitField := func(val reflect.Value) {
switch val.Kind() {
case reflect.Ptr, reflect.Interface:
if val.CanInterface() {
VisitObjectStrings(val.Interface(), visitor)
}
case reflect.Struct, reflect.Map, reflect.Slice, reflect.Array:
if val.CanInterface() {
VisitObjectStrings(val.Addr().Interface(), visitor)
}
case reflect.String:
if !val.CanSet() {
glog.V(5).Infof("Unable to set String value '%v'", v)
}
val.SetString(visitor(val.String()))
}
}
visitValue(reflect.ValueOf(obj), visitor)
}

func visitValue(v reflect.Value, visitor func(string) string) {
switch v.Kind() {

case reflect.Ptr:
visitValue(v.Elem(), visitor)
case reflect.Interface:
visitValue(reflect.ValueOf(v.Interface()), visitor)

case reflect.Slice, reflect.Array:
for c := 0; c < v.Len(); c++ {
visitField(v.Index(c))
for i := 0; i < v.Len(); i++ {
visitValue(v.Index(i), visitor)
}
case reflect.Struct:
for i := 0; i < v.NumField(); i++ {
visitField(v.Field(i))
visitValue(v.Field(i), visitor)
}

case reflect.Map:
vt := v.Type().Elem()
for _, k := range v.MapKeys() {
c := reflect.New(v.MapIndex(k).Type()).Elem()
c.Set(v.MapIndex(k))
visitField(c)
v.SetMapIndex(k, c)
val := reflect.New(vt).Elem()
existing := v.MapIndex(k)
// if the map value type is interface, we must resolve it to a concrete
// value prior to setting it back.
if existing.CanInterface() {
existing = reflect.ValueOf(existing.Interface())
}
switch existing.Kind() {
case reflect.String:
s := visitor(existing.String())
val.Set(reflect.ValueOf(s))
default:
val.Set(existing)
visitValue(val, visitor)
}
v.SetMapIndex(k, val)
}

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

default:
glog.V(5).Infof("Unknown field type '%s': %v", v.Kind(), v)
visitField(v)
}
}

0 comments on commit 3de701d

Please sign in to comment.