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>
| 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 |
+} |
| ... | ... |
@@ -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() |
| 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 | 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 | 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 | 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 | 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 | 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 | 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 |
+} |