Browse code

Implement TemplateConfig endpoint

Adds TemplateProcessor implementation using the default
ExpressionValueGenerator, which is capable of transforming
Template api objects into Config api objects.

Authors:
Michal Fojtik <mfojtik@redhat.com>
Vojtech Vitek <vvitek@redhat.com>

Michal Fojtik authored on 2014/08/12 22:34:16
Showing 28 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,151 @@
0
+{
1
+  "id": "guestbook",
2
+  "kind": "Template",
3
+  "name": "guestbook-example",
4
+  "description": "Example shows how to build a simple multi-tier application using Kubernetes and Docker",
5
+  "parameters": [
6
+    {
7
+      "name": "ADMIN_USERNAME",
8
+      "description": "Guestbook administrator username",
9
+      "type": "string",
10
+      "expression": "admin[A-Z0-9]{3}"
11
+    },
12
+    {
13
+      "name": "ADMIN_PASSWORD",
14
+      "description": "Guestboot administrator password",
15
+      "type": "string",
16
+      "expression": "[a-zA-Z0-9]{8}"
17
+    },
18
+    {
19
+      "name": "REDIS_PASSWORD",
20
+      "description": "The password Redis use for communication",
21
+      "type": "string",
22
+      "expression": "[a-zA-Z0-9]{8}"
23
+    }
24
+  ],
25
+  "items": [
26
+    {
27
+      "id": "frontend",
28
+      "kind": "Service",
29
+      "apiVersion": "v1beta1",
30
+      "port": 5432,
31
+      "selector": {
32
+        "name": "frontend"
33
+      }
34
+    },
35
+    {
36
+      "id": "redismaster",
37
+      "kind": "Service",
38
+      "apiVersion": "v1beta1",
39
+      "port": 10000,
40
+      "selector": {
41
+        "name": "redis-master"
42
+      }
43
+    },
44
+    {
45
+      "id": "redisslave",
46
+      "kind": "Service",
47
+      "apiVersion": "v1beta1",
48
+      "port": 10001,
49
+      "labels": {
50
+        "name": "redisslave"
51
+      },
52
+      "selector": {
53
+        "name": "redisslave"
54
+      }
55
+    },
56
+    {
57
+      "id": "redis-master-2",
58
+      "kind": "Pod",
59
+      "apiVersion": "v1beta1",
60
+      "desiredState": {
61
+        "manifest": {
62
+          "version": "v1beta1",
63
+          "id": "redis-master-2",
64
+          "containers": [{
65
+            "name": "master",
66
+            "image": "dockerfile/redis",
67
+            "env": [
68
+              {
69
+                "name": "REDIS_PASSWORD",
70
+                "value": "${REDIS_PASSWORD}"
71
+              }
72
+            ],
73
+            "ports": [{
74
+              "containerPort": 6379
75
+            }]
76
+          }]
77
+        }
78
+      },
79
+      "labels": {
80
+        "name": "redis-master"
81
+      }
82
+    },
83
+    {
84
+      "id": "frontendController",
85
+      "kind": "ReplicationController",
86
+      "apiVersion": "v1beta1",
87
+      "desiredState": {
88
+        "replicas": 3,
89
+        "replicaSelector": {"name": "frontend"},
90
+        "podTemplate": {
91
+          "desiredState": {
92
+            "manifest": {
93
+              "version": "v1beta1",
94
+              "id": "frontendController",
95
+              "containers": [{
96
+                "name": "php-redis",
97
+                "image": "brendanburns/php-redis",
98
+                "env": [
99
+                  {
100
+                    "name": "ADMIN_USERNAME",
101
+                    "value": "${ADMIN_USERNAME}"
102
+                  },
103
+                  {
104
+                    "name": "ADMIN_PASSWORD",
105
+                    "value": "${ADMIN_PASSWORD}"
106
+                  },
107
+                  {
108
+                    "name": "REDIS_PASSWORD",
109
+                    "value": "${REDIS_PASSWORD}"
110
+                  }
111
+                ],
112
+                "ports": [{"containerPort": 80, "hostPort": 8000}]
113
+              }]
114
+            }
115
+          },
116
+          "labels": {"name": "frontend"}
117
+        }},
118
+        "labels": {"name": "frontend"}
119
+    },
120
+    {
121
+      "id": "redisSlaveController",
122
+      "kind": "ReplicationController",
123
+      "apiVersion": "v1beta1",
124
+      "desiredState": {
125
+        "replicas": 2,
126
+        "replicaSelector": {"name": "redisslave"},
127
+        "podTemplate": {
128
+          "desiredState": {
129
+            "manifest": {
130
+              "version": "v1beta1",
131
+              "id": "redisSlaveController",
132
+              "containers": [{
133
+                "name": "slave",
134
+                "image": "brendanburns/redis-slave",
135
+                "env": [
136
+                  {
137
+                    "name": "REDIS_PASSWORD",
138
+                    "value": "${REDIS_PASSWORD}"
139
+                  }
140
+                ],
141
+                "ports": [{"containerPort": 6379, "hostPort": 6380}]
142
+              }]
143
+            }
144
+          },
145
+          "labels": {"name": "redisslave"}
146
+        }},
147
+        "labels": {"name": "redisslave"}
148
+    }
149
+  ]
150
+}
... ...
@@ -1,3 +1,3 @@
1 1
 package api
2 2
 
3
-import ()
3
+import ()
4 4
\ No newline at end of file
... ...
@@ -25,6 +25,8 @@ import (
25 25
 	"github.com/fsouza/go-dockerclient"
26 26
 	"github.com/golang/glog"
27 27
 	"github.com/google/cadvisor/client"
28
+	"github.com/spf13/cobra"
29
+
28 30
 	"github.com/openshift/origin/pkg/build"
29 31
 	buildapi "github.com/openshift/origin/pkg/build/api"
30 32
 	buildregistry "github.com/openshift/origin/pkg/build/registry/build"
... ...
@@ -32,8 +34,12 @@ import (
32 32
 	"github.com/openshift/origin/pkg/build/strategy"
33 33
 	osclient "github.com/openshift/origin/pkg/client"
34 34
 	"github.com/openshift/origin/pkg/image"
35
+	"github.com/openshift/origin/pkg/template"
36
+
37
+	// Register versioned api types
38
+	_ "github.com/openshift/origin/pkg/config/api/v1beta1"
35 39
 	_ "github.com/openshift/origin/pkg/image/api/v1beta1"
36
-	"github.com/spf13/cobra"
40
+	_ "github.com/openshift/origin/pkg/template/api/v1beta1"
37 41
 )
38 42
 
39 43
 func NewCommandStartAllInOne(name string) *cobra.Command {
... ...
@@ -140,6 +146,7 @@ func (c *Config) startAllInOne() {
140 140
 		"images":                  image.NewImageStorage(imageRegistry),
141 141
 		"imageRepositories":       image.NewImageRepositoryStorage(imageRegistry),
142 142
 		"imageRepositoryMappings": image.NewImageRepositoryMappingStorage(imageRegistry, imageRegistry),
143
+		"templateConfigs":         template.NewStorage(),
143 144
 	}
144 145
 
145 146
 	osMux := http.NewServeMux()
146 147
new file mode 100644
... ...
@@ -0,0 +1,2 @@
0
+// Package api defines and registers types for Config objects.
1
+package api
0 2
new file mode 100644
... ...
@@ -0,0 +1,7 @@
0
+package api
1
+
2
+import "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
3
+
4
+func init() {
5
+	runtime.AddKnownTypes("", Config{})
6
+}
0 7
new file mode 100644
... ...
@@ -0,0 +1,25 @@
0
+package api
1
+
2
+import (
3
+	kubeapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
4
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
5
+)
6
+
7
+// Config contains a set of Kubernetes resources to be applied.
8
+// TODO: Unify with Kubernetes Config
9
+//       https://github.com/GoogleCloudPlatform/kubernetes/pull/1007
10
+type Config struct {
11
+	kubeapi.JSONBase `json:",inline" yaml:",inline"`
12
+
13
+	// Required: Name identifies the Config.
14
+	Name string `json:"name" yaml:"name"`
15
+
16
+	// Optional: Description describes the Config.
17
+	Description string `json:"description" yaml:"description"`
18
+
19
+	// Required: Items is an array of Kubernetes resources of Service,
20
+	// Pod and/or ReplicationController kind.
21
+	// TODO: Handle unregistered types. Define custom []interface{}
22
+	//       type and its unmarshaller instead of []runtime.Object.
23
+	Items []runtime.Object `json:"items" yaml:"items"`
24
+}
0 25
new file mode 100644
... ...
@@ -0,0 +1,7 @@
0
+package v1beta1
1
+
2
+import "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
3
+
4
+func init() {
5
+	runtime.AddKnownTypes("v1beta1", Config{})
6
+}
0 7
new file mode 100644
... ...
@@ -0,0 +1,25 @@
0
+package v1beta1
1
+
2
+import (
3
+	kubeapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1"
4
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
5
+)
6
+
7
+// Config contains a set of Kubernetes resources to be applied.
8
+// TODO: Unify with Kubernetes Config
9
+//       https://github.com/GoogleCloudPlatform/kubernetes/pull/1007
10
+type Config struct {
11
+	kubeapi.JSONBase `json:",inline" yaml:",inline"`
12
+
13
+	// Required: Name identifies the Config.
14
+	Name string `json:"name" yaml:"name"`
15
+
16
+	// Optional: Description describes the Config.
17
+	Description string `json:"description" yaml:"description"`
18
+
19
+	// Required: Items is an array of Kubernetes resources of Service,
20
+	// Pod and/or ReplicationController kind.
21
+	// TODO: Handle unregistered types. Define custom []interface{}
22
+	//       type and its unmarshaller instead of []runtime.Object.
23
+	Items []runtime.Object `json:"items" yaml:"items"`
24
+}
0 25
new file mode 100644
... ...
@@ -0,0 +1,2 @@
0
+// Package api defines and registers types for Template objects.
1
+package api
0 2
new file mode 100644
... ...
@@ -0,0 +1,7 @@
0
+package api
1
+
2
+import "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
3
+
4
+func init() {
5
+	runtime.AddKnownTypes("", Template{})
6
+}
0 7
new file mode 100644
... ...
@@ -0,0 +1,52 @@
0
+package api
1
+
2
+import (
3
+	kubeapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
4
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
5
+)
6
+
7
+// Template contains the inputs needed to produce a Config.
8
+type Template struct {
9
+	kubeapi.JSONBase `json:",inline" yaml:",inline"`
10
+
11
+	// Required: Name identifies the Template.
12
+	Name string `json:"name" yaml:"name"`
13
+
14
+	// Optional: Description describes the Template.
15
+	Description string `json:"description" yaml:"description"`
16
+
17
+	// Required: Items is an array of Kubernetes resources of Service,
18
+	// Pod and/or ReplicationController kind.
19
+	// TODO: Handle unregistered types. Define custom []interface{}
20
+	//       type and its unmarshaller instead of []runtime.Object.
21
+	Items []runtime.Object `json:"items" yaml:"items"`
22
+
23
+	// Optional: Parameters is an array of Parameters used during the
24
+	// Template to Config transformation.
25
+	Parameters []Parameter `json:"parameters,omitempty" yaml:"parameters,omitempty"`
26
+}
27
+
28
+// Parameter defines a name/value variable that is to be processed during
29
+// the Template to Config transformation.
30
+type Parameter struct {
31
+	// Required: Name uniquely identifies the Parameter. A TemplateProcessor
32
+	// searches given Template for all occurances of the Parameter name, ie.
33
+	// ${PARAM_NAME}, and replaces it with it's corresponding Parameter value.
34
+	Name string `json:"name" yaml:"name"`
35
+
36
+	// Optional: Description describes the Parameter.
37
+	Description string `json:"description" yaml:"description"`
38
+
39
+	// Required: Type defines the type of the Parameter value.
40
+	Type string `json:"type" yaml:"type"`
41
+
42
+	// Optional: Expression generates new Value data using the
43
+	// GeneratorExpressionValue expression.
44
+	// TODO: Support more Generator types.
45
+	Expression string `json:"expression,omitempty" yaml:"expression,omitempty"`
46
+
47
+	// Optional: Value holds the Parameter data. The data replaces all occurances
48
+	// of the Parameter name during the Template to Config transformation.
49
+	// TODO: Change this to interface{} and support more types than just string.
50
+	Value string `json:"value,omitempty" yaml:"value,omitempty"`
51
+}
0 52
new file mode 100644
... ...
@@ -0,0 +1,7 @@
0
+package v1beta1
1
+
2
+import "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
3
+
4
+func init() {
5
+	runtime.AddKnownTypes("v1beta1", Template{})
6
+}
0 7
new file mode 100644
... ...
@@ -0,0 +1,52 @@
0
+package v1beta1
1
+
2
+import (
3
+	kubeapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1"
4
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
5
+)
6
+
7
+// Template contains the inputs needed to produce a Config.
8
+type Template struct {
9
+	kubeapi.JSONBase `json:",inline" yaml:",inline"`
10
+
11
+	// Required: Name identifies the Template.
12
+	Name string `json:"name" yaml:"name"`
13
+
14
+	// Optional: Description describes the Template.
15
+	Description string `json:"description" yaml:"description"`
16
+
17
+	// Required: Items is an array of Kubernetes resources of Service,
18
+	// Pod and/or ReplicationController kind.
19
+	// TODO: Handle unregistered types. Define custom []interface{}
20
+	//       type and its unmarshaller instead of []runtime.Object.
21
+	Items []runtime.Object `json:"items" yaml:"items"`
22
+
23
+	// Optional: Parameters is an array of Parameters used during the
24
+	// Template to Config transformation.
25
+	Parameters []Parameter `json:"parameters,omitempty" yaml:"parameters,omitempty"`
26
+}
27
+
28
+// Parameter defines a name/value variable that is to be processed during
29
+// the Template to Config transformation.
30
+type Parameter struct {
31
+	// Required: Name uniquely identifies the Parameter. A TemplateProcessor
32
+	// searches given Template for all occurances of the Parameter name, ie.
33
+	// ${PARAM_NAME}, and replaces it with it's corresponding Parameter value.
34
+	Name string `json:"name" yaml:"name"`
35
+
36
+	// Optional: Description describes the Parameter.
37
+	Description string `json:"description" yaml:"description"`
38
+
39
+	// Required: Type defines the type of the Parameter value.
40
+	Type string `json:"type" yaml:"type"`
41
+
42
+	// Optional: Expression generates new Value data using the
43
+	// GeneratorExpressionValue expression.
44
+	// TODO: Support more Generator types.
45
+	Expression string `json:"expression,omitempty" yaml:"expression,omitempty"`
46
+
47
+	// Optional: Value holds the Parameter data. The data replaces all occurances
48
+	// of the Parameter name during the Template to Config transformation.
49
+	// TODO: Change this to interface{} and support more types than just string.
50
+	Value string `json:"value,omitempty" yaml:"value,omitempty"`
51
+}
0 52
new file mode 100644
... ...
@@ -0,0 +1,4 @@
0
+// Package validation has functions for validating the correctness of
1
+// Template objects and explaining what is wrong with them when
2
+// they aren't valid.
3
+package validation
0 4
new file mode 100644
... ...
@@ -0,0 +1,52 @@
0
+package validation
1
+
2
+import (
3
+	"fmt"
4
+	"regexp"
5
+
6
+	kubeapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
7
+	. "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors"
8
+	. "github.com/GoogleCloudPlatform/kubernetes/pkg/api/validation"
9
+
10
+	. "github.com/openshift/origin/pkg/template/api"
11
+)
12
+
13
+var parameterNameExp = regexp.MustCompile(`^[a-zA-Z0-9\_]+$`)
14
+
15
+// ValidateParameter tests if required fields in the Parameter are set.
16
+func ValidateParameter(param *Parameter) (errs ErrorList) {
17
+	if len(param.Name) == 0 {
18
+		errs = append(errs, NewFieldRequired("name", ""))
19
+		return
20
+	}
21
+	if !parameterNameExp.MatchString(param.Name) {
22
+		errs = append(errs, NewFieldInvalid("name", param.Name))
23
+	}
24
+	return
25
+}
26
+
27
+// ValidateTemplate tests if required fields in the Template are set.
28
+func ValidateTemplate(template *Template) (errs ErrorList) {
29
+	if len(template.ID) == 0 {
30
+		errs = append(errs, NewFieldRequired("id", template.ID))
31
+	}
32
+	for i, item := range template.Items {
33
+		err := ErrorList{}
34
+		switch obj := item.Object.(type) {
35
+		case *kubeapi.ReplicationController:
36
+			err = ValidateReplicationController(obj)
37
+		case *kubeapi.Pod:
38
+			err = ValidatePod(obj)
39
+		case *kubeapi.Service:
40
+			err = ValidateService(obj)
41
+		default:
42
+			err = append(err, NewFieldInvalid("kind", fmt.Sprintf("%T", item)))
43
+		}
44
+		errs = append(errs, err.PrefixIndex(i).Prefix("items")...)
45
+	}
46
+	for i := range template.Parameters {
47
+		paramErr := ValidateParameter(&template.Parameters[i])
48
+		errs = append(errs, paramErr.PrefixIndex(i).Prefix("parameters")...)
49
+	}
50
+	return
51
+}
0 52
new file mode 100644
... ...
@@ -0,0 +1,73 @@
0
+package validation
1
+
2
+import (
3
+	goruntime "runtime"
4
+	"testing"
5
+
6
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
7
+
8
+	"github.com/openshift/origin/pkg/template/api"
9
+)
10
+
11
+func TestValidateParameter(t *testing.T) {
12
+	var tests = []struct {
13
+		ParameterName   string
14
+		IsValidExpected bool
15
+	}{
16
+		{"VALID_NAME", true},
17
+		{"_valid_name_99", true},
18
+		{"10gen_valid_name", true},
19
+		{"", false},
20
+		{"INVALID NAME", false},
21
+		{"IVALID-NAME", false},
22
+		{">INVALID_NAME", false},
23
+		{"$INVALID_NAME", false},
24
+		{"${INVALID_NAME}", false},
25
+	}
26
+
27
+	for _, test := range tests {
28
+		param := &api.Parameter{Name: test.ParameterName, Value: "1"}
29
+		if test.IsValidExpected && len(ValidateParameter(param)) != 0 {
30
+			t.Errorf("Expected zero validation errors on valid parameter name.")
31
+		}
32
+		if !test.IsValidExpected && len(ValidateParameter(param)) == 0 {
33
+			t.Errorf("Expected some validation errors on invalid parameter name.")
34
+		}
35
+	}
36
+}
37
+
38
+func TestValidateTemplate(t *testing.T) {
39
+	shouldPass := func(template *api.Template) {
40
+		errs := ValidateTemplate(template)
41
+		if len(errs) != 0 {
42
+			_, _, line, _ := goruntime.Caller(1)
43
+			t.Errorf("line %v: Unexpected non-zero error list: %#v", line, errs)
44
+		}
45
+	}
46
+	shouldFail := func(template *api.Template) {
47
+		if len(ValidateTemplate(template)) == 0 {
48
+			_, _, line, _ := goruntime.Caller(1)
49
+			t.Errorf("line %v: Expected non-zero error list", line)
50
+		}
51
+	}
52
+
53
+	// Test empty Template, should fail on empty ID
54
+	template := &api.Template{}
55
+	shouldFail(template)
56
+
57
+	// Set ID, should pass
58
+	template.JSONBase.ID = "templateId"
59
+	shouldPass(template)
60
+
61
+	// Add invalid Parameter, should fail on Parameter name
62
+	template.Parameters = []api.Parameter{{Name: "", Value: "1"}}
63
+	shouldFail(template)
64
+
65
+	// Fix Parameter name, should pass
66
+	template.Parameters[0].Name = "VALID_NAME"
67
+	shouldPass(template)
68
+
69
+	// Add invalid Item, should fail on Object.kind
70
+	template.Items = []runtime.Object{{}}
71
+	shouldFail(template)
72
+}
0 73
new file mode 100644
... ...
@@ -0,0 +1,3 @@
0
+// Package template provides TemplateProcessor, capable of
1
+// transforming Template objects into Config objects.
2
+package template
0 3
new file mode 100644
... ...
@@ -0,0 +1,3 @@
0
+// Package generator defines GeneratorInterface interface and implements
1
+// some random value generators.
2
+package generator
0 3
new file mode 100644
... ...
@@ -0,0 +1,3 @@
0
+// Package examples demonstrates possible implementation of some
1
+// random value generators.
2
+package examples
0 3
new file mode 100644
... ...
@@ -0,0 +1,46 @@
0
+package examples
1
+
2
+import (
3
+	"fmt"
4
+	"io/ioutil"
5
+	"net/http"
6
+	"regexp"
7
+	"strings"
8
+)
9
+
10
+// RemoteValueGenerator implements GeneratorInterface. It fetches random value
11
+// from an external url endpoint based on the "[GET:<url>]" input expression.
12
+//
13
+// Example:
14
+//   - "[GET:http://api.example.com/generateRandomValue]"
15
+type RemoteValueGenerator struct {
16
+}
17
+
18
+var remoteExp = regexp.MustCompile(`\[GET\:(http(s)?:\/\/(.+))\]`)
19
+
20
+// NewRemoteValueGenerator creates new RemoteValueGenerator.
21
+func NewRemoteValueGenerator() RemoteValueGenerator {
22
+	return RemoteValueGenerator{}
23
+}
24
+
25
+// GenerateValue fetches random value from an external url. The input
26
+// expression must be of the form "[GET:<url>]".
27
+func (g RemoteValueGenerator) GenerateValue(expression string) (interface{}, error) {
28
+	matches := remoteExp.FindAllStringIndex(expression, -1)
29
+	if len(matches) < 1 {
30
+		return expression, fmt.Errorf("No matches found.")
31
+	}
32
+	for _, r := range matches {
33
+		response, err := http.Get(expression[5 : len(expression)-1])
34
+		if err != nil {
35
+			return "", err
36
+		}
37
+		defer response.Body.Close()
38
+		body, err := ioutil.ReadAll(response.Body)
39
+		if err != nil {
40
+			return "", err
41
+		}
42
+		expression = strings.Replace(expression, expression[r[0]:r[1]], strings.TrimSpace(string(body)), 1)
43
+	}
44
+	return expression, nil
45
+}
0 46
new file mode 100644
... ...
@@ -0,0 +1,36 @@
0
+package examples
1
+
2
+import (
3
+	"fmt"
4
+	"net"
5
+	"net/http"
6
+	"testing"
7
+)
8
+
9
+func TestRemoteValueGenerator(t *testing.T) {
10
+	generator := NewRemoteValueGenerator()
11
+
12
+	_, err := generator.GenerateValue("[GET:http://api.example.com/new]")
13
+	if err == nil {
14
+		t.Errorf("Expected error while fetching non-existent remote.")
15
+	}
16
+}
17
+
18
+func TestFakeRemoteValueGenerator(t *testing.T) {
19
+	// Run the fake remote server
20
+	http.HandleFunc("/v1/value/generate", func(w http.ResponseWriter, r *http.Request) {
21
+		fmt.Fprintf(w, "NewRandomString")
22
+	})
23
+	listener, _ := net.Listen("tcp", ":12345")
24
+	go http.Serve(listener, nil)
25
+
26
+	generator := NewRemoteValueGenerator()
27
+
28
+	value, err := generator.GenerateValue("[GET:http://127.0.0.1:12345/v1/value/generate]")
29
+	if err != nil {
30
+		t.Errorf(err.Error())
31
+	}
32
+	if value != "NewRandomString" {
33
+		t.Errorf("Failed to fetch remote value using GET.")
34
+	}
35
+}
0 36
new file mode 100644
... ...
@@ -0,0 +1,137 @@
0
+package generator
1
+
2
+import (
3
+	"fmt"
4
+	"math/rand"
5
+	"regexp"
6
+	"strconv"
7
+	"strings"
8
+)
9
+
10
+// ExpressionValueGenerator implements Generator interface. It generates
11
+// random string based on the input expression. The input expression is
12
+// a string, which may contain "[a-zA-Z0-9]{length}" expression constructs,
13
+// defining range and length of the result random characters.
14
+//
15
+// Examples:
16
+//   - "test[0-9]{1}x" => "test7x"
17
+//   - "[0-1]{8}" => "01001100"
18
+//   - "0x[A-F0-9]{4}" => "0xB3AF"
19
+//   - "[a-zA-Z0-9]{8}" => "hW4yQU5i"
20
+//
21
+// TODO: Support more regexp constructs.
22
+type ExpressionValueGenerator struct {
23
+	seed *rand.Rand
24
+}
25
+
26
+const (
27
+	Alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
28
+	Numerals = "0123456789"
29
+	Ascii    = Alphabet + Numerals + "~!@#$%^&*()-_+={}[]\\|<,>.?/\"';:`"
30
+)
31
+
32
+var (
33
+	rangeExp      = regexp.MustCompile(`([\\]?[a-zA-Z0-9]\-?[a-zA-Z0-9]?)`)
34
+	generatorsExp = regexp.MustCompile(`\[([a-zA-Z0-9\-\\]+)\](\{([0-9]+)\})`)
35
+	expressionExp = regexp.MustCompile(`\[(\\w|\\d|\\a)|([a-zA-Z0-9]\-[a-zA-Z0-9])+\]`)
36
+)
37
+
38
+// NewExpressionValueGenerator creates new ExpressionValueGenerator.
39
+func NewExpressionValueGenerator(seed *rand.Rand) ExpressionValueGenerator {
40
+	return ExpressionValueGenerator{seed: seed}
41
+}
42
+
43
+// GenerateValue generates random string based on the input expression.
44
+// The input expression is a pseudo-regex formatted string. See
45
+// ExpressionValueGenerator for more details.
46
+func (g ExpressionValueGenerator) GenerateValue(expression string) (interface{}, error) {
47
+	for {
48
+		r := generatorsExp.FindStringIndex(expression)
49
+		if r == nil {
50
+			break
51
+		}
52
+		ranges, length, err := rangesAndLength(expression[r[0]:r[1]])
53
+		if err != nil {
54
+			return "", err
55
+		}
56
+		err = replaceWithGenerated(
57
+			&expression,
58
+			expression[r[0]:r[1]],
59
+			findExpressionPos(ranges),
60
+			length,
61
+			g.seed,
62
+		)
63
+		if err != nil {
64
+			return "", err
65
+		}
66
+	}
67
+	return expression, nil
68
+}
69
+
70
+// alphabetSlice produces a string slice that contains all characters within
71
+// a specified range.
72
+func alphabetSlice(from, to byte) (string, error) {
73
+	leftPos := strings.Index(Ascii, string(from))
74
+	rightPos := strings.LastIndex(Ascii, string(to))
75
+	if leftPos > rightPos {
76
+		return "", fmt.Errorf("Invalid range specified: %s-%s", string(from), string(to))
77
+	}
78
+	return Ascii[leftPos:rightPos], nil
79
+}
80
+
81
+// replaceWithGenerated replaces all occurences of the given expression
82
+// in the string with random characters of the specified range and length.
83
+func replaceWithGenerated(s *string, expression string, ranges [][]byte, length int, seed *rand.Rand) error {
84
+	var alphabet string
85
+	for _, r := range ranges {
86
+		switch string(r[0]) + string(r[1]) {
87
+		case `\w`:
88
+			alphabet += Ascii
89
+		case `\d`:
90
+			alphabet += Numerals
91
+		case `\a`:
92
+			alphabet += Alphabet + Numerals
93
+		default:
94
+			if slice, err := alphabetSlice(r[0], r[1]); err != nil {
95
+				return err
96
+			} else {
97
+				alphabet += slice
98
+			}
99
+		}
100
+	}
101
+	result := make([]byte, length)
102
+	for i := 0; i < length; i++ {
103
+		result[i] = alphabet[seed.Intn(len(alphabet))]
104
+	}
105
+	*s = strings.Replace(*s, expression, string(result), 1)
106
+	return nil
107
+}
108
+
109
+// findExpressionPos searches the given string for the valid expressions
110
+// and returns their corresponding indexes.
111
+func findExpressionPos(s string) [][]byte {
112
+	matches := rangeExp.FindAllStringIndex(s, -1)
113
+	result := make([][]byte, len(matches))
114
+	for i, r := range matches {
115
+		result[i] = []byte{s[r[0]], s[r[1]-1]}
116
+	}
117
+	return result
118
+}
119
+
120
+// rangesAndLength extracts the expression ranges (eg. [A-Z0-9]) and length
121
+// (eg. {3}). This helper function also validates the expression syntax and
122
+// its length (must be within 1..255).
123
+func rangesAndLength(s string) (string, int, error) {
124
+	expr := s[0:strings.LastIndex(s, "{")]
125
+	if !expressionExp.MatchString(expr) {
126
+		return "", 0, fmt.Errorf("Malformed expresion syntax: %s", expr)
127
+	}
128
+
129
+	length, _ := strconv.Atoi(s[strings.LastIndex(s, "{")+1 : len(s)-1])
130
+	// TODO: We do need to set a better limit for the number of generated characters.
131
+	if length > 0 && length <= 255 {
132
+		return expr, length, nil
133
+	} else {
134
+		return "", 0, fmt.Errorf("Range must be within [1-255] characters (%d)", length)
135
+	}
136
+}
0 137
new file mode 100644
... ...
@@ -0,0 +1,52 @@
0
+package generator
1
+
2
+import (
3
+	"math/rand"
4
+	"testing"
5
+)
6
+
7
+func TestExpressionValueGenerator(t *testing.T) {
8
+	generator := NewExpressionValueGenerator(rand.New(rand.NewSource(1337)))
9
+
10
+	var tests = []struct {
11
+		Expression    string
12
+		ExpectedValue string
13
+	}{
14
+		{"test[A-Z0-9]{4}template", "testQ3HVtemplate"},
15
+		{"[\\d]{4}", "6841"},
16
+		{"[\\w]{4}", "DVgK"},
17
+		{"[\\a]{10}", "nFWmvmjuaZ"},
18
+		{"admin[0-9]{2}[A-Z]{2}", "admin32VU"},
19
+		{"admin[0-9]{2}test[A-Z]{2}", "admin56testGS"},
20
+	}
21
+
22
+	for _, test := range tests {
23
+		value, err := generator.GenerateValue(test.Expression)
24
+		if err != nil {
25
+			t.Errorf("Failed to generate value from %s due to error: %v", test.Expression, err)
26
+		}
27
+		if value != test.ExpectedValue {
28
+			t.Errorf("Failed to generate expected value from %s\n. Generated: %s\n. Expected: %s\n", test.Expression, value, test.ExpectedValue)
29
+		}
30
+	}
31
+}
32
+
33
+func TestExpressionValueGeneratorErrors(t *testing.T) {
34
+	generator := NewExpressionValueGenerator(rand.New(rand.NewSource(1337)))
35
+
36
+	if v, err := generator.GenerateValue("[ABC]{3}"); err == nil {
37
+		t.Errorf("Expected [ABC]{3} to produce malformed syntax error (returned: %s)", v)
38
+	}
39
+
40
+	if v, err := generator.GenerateValue("[Z-A]{3}"); err == nil {
41
+		t.Errorf("Expected Invalid range specified error, got %s", v)
42
+	}
43
+
44
+	if v, err := generator.GenerateValue("[A-Z]{300}"); err == nil {
45
+		t.Errorf("Expected Invalid range specified error, got %s", v)
46
+	}
47
+
48
+	if v, err := generator.GenerateValue("[A-Z]{0}"); err == nil {
49
+		t.Errorf("Expected Invalid range specified error, got %s", v)
50
+	}
51
+}
0 52
new file mode 100644
... ...
@@ -0,0 +1,7 @@
0
+package generator
1
+
2
+// Generator is an interface for generating random values
3
+// from an input expression
4
+type Generator interface {
5
+	GenerateValue(expression string) (interface{}, error)
6
+}
0 7
new file mode 100644
... ...
@@ -0,0 +1,63 @@
0
+package template
1
+
2
+import (
3
+	"errors"
4
+	"fmt"
5
+	"math/rand"
6
+	"time"
7
+
8
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver"
9
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
10
+
11
+	"github.com/openshift/origin/pkg/template/api"
12
+	"github.com/openshift/origin/pkg/template/api/validation"
13
+	. "github.com/openshift/origin/pkg/template/generator"
14
+)
15
+
16
+// Storage implements RESTStorage for the Template objects.
17
+type Storage struct{}
18
+
19
+// NewStorage creates new RESTStorage for the Template objects.
20
+func NewStorage() apiserver.RESTStorage {
21
+	return &Storage{}
22
+}
23
+
24
+func (s *Storage) List(selector labels.Selector) (interface{}, error) {
25
+	return nil, errors.New("template.Storage.List() is not implemented.")
26
+}
27
+
28
+func (s *Storage) Get(id string) (interface{}, error) {
29
+	return nil, errors.New("template.Storage.Get() is not implemented.")
30
+}
31
+
32
+func (s *Storage) New() interface{} {
33
+	return &api.Template{}
34
+}
35
+
36
+func (s *Storage) Delete(id string) (<-chan interface{}, error) {
37
+	return apiserver.MakeAsync(func() (interface{}, error) {
38
+		return nil, errors.New("template.Storage.Delete() is not implemented.")
39
+	}), nil
40
+}
41
+
42
+func (s *Storage) Update(minion interface{}) (<-chan interface{}, error) {
43
+	return nil, errors.New("template.Storage.Update() is not implemented.")
44
+}
45
+
46
+func (s *Storage) Create(obj interface{}) (<-chan interface{}, error) {
47
+	template, ok := obj.(*api.Template)
48
+	if !ok {
49
+		return nil, errors.New("Not a template config.")
50
+	}
51
+	if errs := validation.ValidateTemplate(template); len(errs) > 0 {
52
+		return nil, errors.New(fmt.Sprintf("Invalid template config: %#v", errs))
53
+	}
54
+	return apiserver.MakeAsync(func() (interface{}, error) {
55
+		generators := map[string]Generator{
56
+			"expression": NewExpressionValueGenerator(rand.New(rand.NewSource(time.Now().UnixNano()))),
57
+		}
58
+		processor := NewTemplateProcessor(generators)
59
+		config, err := processor.Process(template)
60
+		return config, err
61
+	}), nil
62
+}
0 63
new file mode 100644
... ...
@@ -0,0 +1,46 @@
0
+package template
1
+
2
+import (
3
+	"testing"
4
+	"time"
5
+
6
+	kubeapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
7
+)
8
+
9
+func TestNewStorageInvalidType(t *testing.T) {
10
+	storage := NewStorage()
11
+	_, err := storage.Create("string")
12
+	if err == nil {
13
+		t.Errorf("Expected type error.")
14
+	}
15
+}
16
+
17
+func TestStorageNotImplementedFunctions(t *testing.T) {
18
+	storage := NewStorage()
19
+
20
+	if _, err := storage.List(nil); err == nil {
21
+		t.Errorf("Expected not implemented error.")
22
+	}
23
+
24
+	if _, err := storage.Get(""); err == nil {
25
+		t.Errorf("Expected not implemented error.")
26
+	}
27
+
28
+	if _, err := storage.Update(nil); err == nil {
29
+		t.Errorf("Expected not implemented error.")
30
+	}
31
+
32
+	channel, err := storage.Delete("")
33
+	if err != nil {
34
+		t.Errorf("Unexpected error when deleting: %v", err)
35
+	}
36
+	select {
37
+	case result := <-channel:
38
+		status, ok := result.(*kubeapi.Status)
39
+		if !ok || status.Status != kubeapi.StatusFailure {
40
+			t.Errorf("Expected not implemented error.")
41
+		}
42
+	case <-time.After(time.Millisecond * 100):
43
+		t.Error("Unexpected timeout from async channel")
44
+	}
45
+}
0 46
new file mode 100644
... ...
@@ -0,0 +1,152 @@
0
+package template
1
+
2
+import (
3
+	"fmt"
4
+	"regexp"
5
+	"strings"
6
+
7
+	kubeapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
8
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
9
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
10
+	"github.com/golang/glog"
11
+
12
+	config "github.com/openshift/origin/pkg/config/api"
13
+	"github.com/openshift/origin/pkg/template/api"
14
+	. "github.com/openshift/origin/pkg/template/generator"
15
+)
16
+
17
+var parameterExp = regexp.MustCompile(`\$\{([a-zA-Z0-9\_]+)\}`)
18
+
19
+// TemplateProcessor transforms Template objects into Config objects.
20
+type TemplateProcessor struct {
21
+	Generators map[string]Generator
22
+}
23
+
24
+// NewTemplateProcessor creates new TemplateProcessor and initializes
25
+// its set of generators.
26
+func NewTemplateProcessor(generators map[string]Generator) *TemplateProcessor {
27
+	return &TemplateProcessor{Generators: generators}
28
+}
29
+
30
+// Process transforms Template object into Config object. It generates
31
+// Parameter values using the defined set of generators first, and then it
32
+// substitutes all Parameter expression occurances with their corresponding
33
+// values (currently in the containers' Environment variables only).
34
+func (p *TemplateProcessor) Process(template *api.Template) (*config.Config, error) {
35
+	if err := p.GenerateParameterValues(template); err != nil {
36
+		return nil, err
37
+	}
38
+	if err := p.SubstituteParameters(template); err != nil {
39
+		return nil, err
40
+	}
41
+
42
+	config := &config.Config{
43
+		Name:        template.Name,
44
+		Description: template.Description,
45
+		Items:       template.Items,
46
+	}
47
+	config.ID = template.ID
48
+	config.Kind = "Config"
49
+	config.CreationTimestamp = util.Now()
50
+	return config, nil
51
+}
52
+
53
+// AddParameter adds new custom parameter to the Template. It overrides
54
+// the existing parameter, if already defined.
55
+func (p *TemplateProcessor) AddParameter(t *api.Template, param api.Parameter) {
56
+	if existing := p.GetParameterByName(t, param.Name); existing != nil {
57
+		*existing = param
58
+	} else {
59
+		t.Parameters = append(t.Parameters, param)
60
+	}
61
+}
62
+
63
+// GetParameterByName searches for a Parameter in the Template
64
+// based on it's name.
65
+func (p *TemplateProcessor) GetParameterByName(t *api.Template, name string) *api.Parameter {
66
+	for i, param := range t.Parameters {
67
+		if param.Name == name {
68
+			return &(t.Parameters[i])
69
+		}
70
+	}
71
+	return nil
72
+}
73
+
74
+// SubstituteParameters loops over all Environment variables defined for
75
+// all ReplicationController and Pod containers and substitutes all
76
+// Parameter expression occurances with their corresponding values.
77
+//
78
+// Example of Parameter expression:
79
+//   - ${PARAMETER_NAME}
80
+func (p *TemplateProcessor) SubstituteParameters(t *api.Template) error {
81
+	// Make searching for given parameter name/value more effective
82
+	paramMap := make(map[string]string, len(t.Parameters))
83
+	for _, param := range t.Parameters {
84
+		paramMap[param.Name] = param.Value
85
+	}
86
+
87
+	for i, item := range t.Items {
88
+		switch obj := item.Object.(type) {
89
+		case *kubeapi.ReplicationController:
90
+			p.substituteParametersInManifest(&obj.DesiredState.PodTemplate.DesiredState.Manifest, paramMap)
91
+			t.Items[i] = runtime.Object{Object: *obj}
92
+		case *kubeapi.Pod:
93
+			p.substituteParametersInManifest(&obj.DesiredState.Manifest, paramMap)
94
+			t.Items[i] = runtime.Object{Object: *obj}
95
+		default:
96
+			glog.V(1).Infof("Unable to process parameters for resource '%T'.", obj)
97
+		}
98
+	}
99
+
100
+	return nil
101
+}
102
+
103
+// substituteParametersInManifest is a helper function that iterates
104
+// over the given manifest and substitutes all Parameter expression
105
+// occurances with their corresponding values.
106
+func (p *TemplateProcessor) substituteParametersInManifest(manifest *kubeapi.ContainerManifest, paramMap map[string]string) {
107
+	for i, _ := range manifest.Containers {
108
+		for e, _ := range manifest.Containers[i].Env {
109
+			envValue := &manifest.Containers[i].Env[e].Value
110
+			// Match all parameter expressions found in the given env var
111
+			for _, match := range parameterExp.FindAllStringSubmatch(*envValue, -1) {
112
+				// Substitute expression with its value, if corresponding parameter found
113
+				if len(match) > 1 {
114
+					if paramValue, found := paramMap[match[1]]; found {
115
+						*envValue = strings.Replace(*envValue, match[0], paramValue, 1)
116
+					}
117
+				}
118
+			}
119
+		}
120
+	}
121
+}
122
+
123
+// GenerateParameterValues generates Value for each Parameter of the given
124
+// Template that has Expression field specified and doesn't have any
125
+// Value yet.
126
+//
127
+// Examples (Expression => Value):
128
+//   - "test[0-9]{1}x" => "test7x"
129
+//   - "[0-1]{8}" => "01001100"
130
+//   - "0x[A-F0-9]{4}" => "0xB3AF"
131
+//   - "[a-zA-Z0-9]{8}" => "hW4yQU5i"
132
+func (p *TemplateProcessor) GenerateParameterValues(t *api.Template) error {
133
+	for i, _ := range t.Parameters {
134
+		param := &t.Parameters[i]
135
+		if param.Expression != "" && param.Value == "" {
136
+			generator, ok := p.Generators["expression"]
137
+			if !ok {
138
+				return fmt.Errorf("Can't find expression generator.")
139
+			}
140
+			value, err := generator.GenerateValue(param.Expression)
141
+			if err != nil {
142
+				return err
143
+			}
144
+			param.Value, ok = value.(string)
145
+			if !ok {
146
+				return fmt.Errorf("Can't convert the generated value %v to string.", value)
147
+			}
148
+		}
149
+	}
150
+	return nil
151
+}
0 152
new file mode 100644
... ...
@@ -0,0 +1,81 @@
0
+package template
1
+
2
+import (
3
+	"encoding/json"
4
+	"fmt"
5
+	"io/ioutil"
6
+	"math/rand"
7
+	"testing"
8
+	"time"
9
+
10
+	_ "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1"
11
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
12
+
13
+	"github.com/openshift/origin/pkg/template/api"
14
+	. "github.com/openshift/origin/pkg/template/generator"
15
+)
16
+
17
+func TestNewTemplate(t *testing.T) {
18
+	var template api.Template
19
+
20
+	jsonData, _ := ioutil.ReadFile("../../examples/guestbook/template.json")
21
+	if err := json.Unmarshal(jsonData, &template); err != nil {
22
+		t.Errorf("Unable to process the JSON template file: %v", err)
23
+	}
24
+}
25
+
26
+func TestAddParameter(t *testing.T) {
27
+	var template api.Template
28
+
29
+	jsonData, _ := ioutil.ReadFile("../../examples/guestbook/template.json")
30
+	json.Unmarshal(jsonData, &template)
31
+
32
+	processor := NewTemplateProcessor(nil)
33
+	processor.AddParameter(&template, api.Parameter{Name: "CUSTOM_PARAM", Value: "1"})
34
+	processor.AddParameter(&template, api.Parameter{Name: "CUSTOM_PARAM", Value: "2"})
35
+
36
+	if p := processor.GetParameterByName(&template, "CUSTOM_PARAM"); p == nil {
37
+		t.Errorf("Unable to add a custom parameter to the template")
38
+	} else {
39
+		if p.Value != "2" {
40
+			t.Errorf("Unable to replace the custom parameter value in template")
41
+		}
42
+	}
43
+}
44
+
45
+func TestEmptyGenerators(t *testing.T) {
46
+	var template api.Template
47
+	jsonData, _ := ioutil.ReadFile("../../examples/guestbook/template.json")
48
+	json.Unmarshal(jsonData, &template)
49
+
50
+	processor := NewTemplateProcessor(map[string]Generator{})
51
+
52
+	_, err := processor.Process(&template)
53
+	if err == nil {
54
+		t.Errorf("Expected error, no generators defined for Expression fields.")
55
+	}
56
+}
57
+
58
+func ExampleProcessTemplateParameters() {
59
+	var template api.Template
60
+	jsonData, _ := ioutil.ReadFile("../../examples/guestbook/template.json")
61
+	json.Unmarshal(jsonData, &template)
62
+
63
+	generators := map[string]Generator{
64
+		"expression": NewExpressionValueGenerator(rand.New(rand.NewSource(1337))),
65
+	}
66
+	processor := NewTemplateProcessor(generators)
67
+
68
+	// Define custom parameter for the transformation:
69
+	processor.AddParameter(&template, api.Parameter{Name: "CUSTOM_PARAM1", Value: "1"})
70
+
71
+	// Transform the template config into the result config
72
+	config, _ := processor.Process(&template)
73
+	// Reset the timestamp for the output comparison
74
+	config.CreationTimestamp = util.Date(1980, 1, 1, 0, 0, 0, 0, time.UTC)
75
+
76
+	result, _ := json.Marshal(config)
77
+	fmt.Println(string(result))
78
+	// Output:
79
+	// {"kind":"Config","id":"guestbook","creationTimestamp":"1980-01-01T00:00:00Z","name":"guestbook-example","description":"Example shows how to build a simple multi-tier application using Kubernetes and Docker","items":[{"kind":"Service","id":"frontend","creationTimestamp":null,"apiVersion":"v1beta1","port":5432,"selector":{"name":"frontend"},"containerPort":0},{"kind":"Service","id":"redismaster","creationTimestamp":null,"apiVersion":"v1beta1","port":10000,"selector":{"name":"redis-master"},"containerPort":0},{"kind":"Service","id":"redisslave","creationTimestamp":null,"apiVersion":"v1beta1","port":10001,"labels":{"name":"redisslave"},"selector":{"name":"redisslave"},"containerPort":0},{"kind":"Pod","id":"redis-master-2","creationTimestamp":null,"apiVersion":"v1beta1","labels":{"name":"redis-master"},"desiredState":{"manifest":{"version":"v1beta1","id":"redis-master-2","volumes":null,"containers":[{"name":"master","image":"dockerfile/redis","ports":[{"containerPort":6379}],"env":[{"name":"REDIS_PASSWORD","key":"REDIS_PASSWORD","value":"P8vxbV4C"}]}]},"restartpolicy":{}},"currentState":{"manifest":{"version":"","id":"","volumes":null,"containers":null},"restartpolicy":{}}},{"kind":"ReplicationController","id":"frontendController","creationTimestamp":null,"apiVersion":"v1beta1","desiredState":{"replicas":3,"replicaSelector":{"name":"frontend"},"podTemplate":{"desiredState":{"manifest":{"version":"v1beta1","id":"frontendController","volumes":null,"containers":[{"name":"php-redis","image":"brendanburns/php-redis","ports":[{"hostPort":8000,"containerPort":80}],"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"}]}]},"restartpolicy":{}},"labels":{"name":"frontend"}}},"labels":{"name":"frontend"}},{"kind":"ReplicationController","id":"redisSlaveController","creationTimestamp":null,"apiVersion":"v1beta1","desiredState":{"replicas":2,"replicaSelector":{"name":"redisslave"},"podTemplate":{"desiredState":{"manifest":{"version":"v1beta1","id":"redisSlaveController","volumes":null,"containers":[{"name":"slave","image":"brendanburns/redis-slave","ports":[{"hostPort":6380,"containerPort":6379}],"env":[{"name":"REDIS_PASSWORD","key":"REDIS_PASSWORD","value":"P8vxbV4C"}]}]},"restartpolicy":{}},"labels":{"name":"redisslave"}}},"labels":{"name":"redisslave"}}]}
80
+}