diff --git a/examples/examples_test.go b/examples/examples_test.go index a47bc515c391..54038b924b8e 100644 --- a/examples/examples_test.go +++ b/examples/examples_test.go @@ -10,17 +10,16 @@ import ( kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" - //"github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/golang/glog" "github.com/openshift/origin/pkg/api/latest" + "github.com/openshift/origin/pkg/api/validation" configapi "github.com/openshift/origin/pkg/config/api" deployapi "github.com/openshift/origin/pkg/deploy/api" imageapi "github.com/openshift/origin/pkg/image/api" projectapi "github.com/openshift/origin/pkg/project/api" routeapi "github.com/openshift/origin/pkg/route/api" templateapi "github.com/openshift/origin/pkg/template/api" - "github.com/openshift/origin/pkg/util" ) type mockService struct{} @@ -114,7 +113,7 @@ func TestExampleObjectSchemas(t *testing.T) { t.Errorf("%s did not decode correctly: %v\n%s", path, err, string(data)) return } - if errors := util.ValidateObject(expectedType); len(errors) > 0 { + if errors := validation.ValidateObject(expectedType); len(errors) > 0 { t.Errorf("%s did not validate correctly: %v", path, errors) } }) diff --git a/examples/sample-app/application-template-custombuild.json b/examples/sample-app/application-template-custombuild.json index 49b28b2b4a41..dc9643c3ebfe 100644 --- a/examples/sample-app/application-template-custombuild.json +++ b/examples/sample-app/application-template-custombuild.json @@ -1,10 +1,12 @@ { "metadata":{ "name": "ruby-helloworld-sample", + "annotations": { + "description": "This example shows how to create a simple ruby application in openshift origin v3" + } }, "kind": "Template", "apiVersion": "v1beta1", - "description": "This example shows how to create a simple ruby application in openshift origin v3", "parameters": [ { "name": "ADMIN_USERNAME", diff --git a/examples/sample-app/application-template-dockerbuild.json b/examples/sample-app/application-template-dockerbuild.json index cf7f433f2a2a..751e6e5d403a 100644 --- a/examples/sample-app/application-template-dockerbuild.json +++ b/examples/sample-app/application-template-dockerbuild.json @@ -1,10 +1,12 @@ { "metadata":{ "name": "ruby-helloworld-sample", + "annotations": { + "description": "This example shows how to create a simple ruby application in openshift origin v3" + } }, "kind": "Template", "apiVersion": "v1beta1", - "description": "This example shows how to create a simple ruby application in openshift origin v3", "parameters": [ { "name": "ADMIN_USERNAME", diff --git a/examples/sample-app/application-template-stibuild.json b/examples/sample-app/application-template-stibuild.json index 8376f4dfbae6..5d10867ce3dc 100644 --- a/examples/sample-app/application-template-stibuild.json +++ b/examples/sample-app/application-template-stibuild.json @@ -1,10 +1,12 @@ { "metadata":{ "name": "ruby-helloworld-sample", + "annotations": { + "description": "This example shows how to create a simple ruby application in openshift origin v3" + } }, "kind": "Template", "apiVersion": "v1beta1", - "description": "This example shows how to create a simple ruby application in openshift origin v3", "parameters": [ { "name": "ADMIN_USERNAME", diff --git a/hack/test-cmd.sh b/hack/test-cmd.sh index b30a712f6b14..b96a6152df3c 100755 --- a/hack/test-cmd.sh +++ b/hack/test-cmd.sh @@ -41,7 +41,7 @@ API_HOST=${API_HOST:-127.0.0.1} KUBELET_SCHEME=${KUBELET_SCHEME:-http} KUBELET_PORT=${KUBELET_PORT:-10250} -TEMP_DIR=$(mktemp -d /tmp/openshift-cmd.XXXX) +TEMP_DIR=${USE_TEMP:-$(mktemp -d /tmp/openshift-cmd.XXXX)} ETCD_DATA_DIR="${TEMP_DIR}/etcd" VOLUME_DIR="${TEMP_DIR}/volumes" CERT_DIR="${TEMP_DIR}/certs" @@ -80,7 +80,7 @@ if [[ "${API_SCHEME}" == "https" ]]; then fi wait_for_url "http://${API_HOST}:${KUBELET_PORT}/healthz" "kubelet: " 0.25 80 -wait_for_url "${API_SCHEME}://${API_HOST}:${API_PORT}/healthz" "apiserver: " 0.25 80 +wait_for_url "${API_SCHEME}://${API_HOST}:${API_PORT}/healthz" "apiserver: " 0.25 80 wait_for_url "${API_SCHEME}://${API_HOST}:${API_PORT}/api/v1beta1/minions/127.0.0.1" "apiserver(minions): " 0.25 80 # Set KUBERNETES_MASTER for osc @@ -98,6 +98,17 @@ export OPENSHIFT_PROFILE="${CLI_PROFILE-}" # Begin tests # +osc get templates +osc create -f examples/sample-app/application-template-dockerbuild.json +osc get templates +osc get templates ruby-helloworld-sample +[ -n "$(osc get templates ruby-helloworld-sample -o json | osc process -f -)" ] +osc describe templates ruby-helloworld-sample +osc delete templates ruby-helloworld-sample +osc get templates +# TODO: create directly from template +echo "templates: ok" + # verify some default commands [ "$(openshift cli)" ] [ "$(openshift ex)" ] diff --git a/pkg/api/serialization_test.go b/pkg/api/serialization_test.go index 47bcd1e6e28b..e28dddbea2c7 100644 --- a/pkg/api/serialization_test.go +++ b/pkg/api/serialization_test.go @@ -37,7 +37,7 @@ func fuzzInternalObject(t *testing.T, forVersion string, item runtime.Object, se c.Fuzz(&j.ObjectMeta) c.Fuzz(&j.Parameters) // TODO: replace with structured type definition - j.Items = []runtime.Object{} + j.Objects = []runtime.Object{} }, func(j *image.Image, c fuzz.Continue) { c.Fuzz(&j.ObjectMeta) diff --git a/pkg/util/validate.go b/pkg/api/validation/validation.go similarity index 84% rename from pkg/util/validate.go rename to pkg/api/validation/validation.go index aedb8abd38f0..a5ff1556b291 100644 --- a/pkg/util/validate.go +++ b/pkg/api/validation/validation.go @@ -1,4 +1,4 @@ -package util +package validation import ( "fmt" @@ -18,15 +18,11 @@ import ( projectv "github.com/openshift/origin/pkg/project/api/validation" routeapi "github.com/openshift/origin/pkg/route/api" routev "github.com/openshift/origin/pkg/route/api/validation" + templateapi "github.com/openshift/origin/pkg/template/api" + templatev "github.com/openshift/origin/pkg/template/api/validation" ) -type fakeServiceLister struct{} - -func (f *fakeServiceLister) ListServices(kapi.Context) (*kapi.ServiceList, error) { - return &kapi.ServiceList{}, nil -} - -// ValidateObject validates the runtime.Object and returns the validation errors +// ValidateObject runs all known validations and returns the validation errors func ValidateObject(obj runtime.Object) (errors []error) { if m, err := meta.Accessor(obj); err == nil { if len(m.Namespace()) == 0 { @@ -41,6 +37,11 @@ func ValidateObject(obj runtime.Object) (errors []error) { errors = validation.ValidateService(t) case *kapi.Pod: errors = validation.ValidatePod(t) + case *kapi.Namespace: + errors = validation.ValidateNamespace(t) + case *kapi.Node: + errors = validation.ValidateMinion(t) + case *imageapi.Image: errors = imagev.ValidateImage(t) case *imageapi.ImageRepository: @@ -61,6 +62,8 @@ func ValidateObject(obj runtime.Object) (errors []error) { errors = buildv.ValidateBuildConfig(t) case *buildapi.Build: errors = buildv.ValidateBuild(t) + case *templateapi.Template: + errors = templatev.ValidateTemplate(t) default: if list, err := runtime.ExtractList(obj); err == nil { for i := range list { @@ -73,5 +76,4 @@ func ValidateObject(obj runtime.Object) (errors []error) { return []error{fmt.Errorf("no validation defined for %#v", obj)} } return errors - } diff --git a/pkg/client/client.go b/pkg/client/client.go index 71b554d26ccc..2e323ac0be3b 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -33,6 +33,7 @@ type Interface interface { PolicyBindingsNamespacer ResourceAccessReviewsNamespacer SubjectAccessReviewsNamespacer + TemplatesNamespacer } func (c *Client) Builds(namespace string) BuildInterface { @@ -97,6 +98,11 @@ func (c *Client) TemplateConfigs(namespace string) TemplateConfigInterface { return newTemplateConfigs(c, namespace) } +// TemplateConfigs provides a REST client for TemplateConfig +func (c *Client) Templates(namespace string) TemplateInterface { + return newTemplates(c, namespace) +} + func (c *Client) Policies(namespace string) PolicyInterface { return newPolicies(c, namespace) } diff --git a/pkg/client/fake.go b/pkg/client/fake.go index ff06eee4fb87..3939b43e7f5e 100644 --- a/pkg/client/fake.go +++ b/pkg/client/fake.go @@ -48,6 +48,10 @@ func (c *Fake) Routes(namespace string) RouteInterface { return &FakeRoutes{Fake: c, Namespace: namespace} } +func (c *Fake) Templates(namespace string) TemplateInterface { + return &FakeTemplates{Fake: c} +} + func (c *Fake) Users() UserInterface { return &FakeUsers{Fake: c} } diff --git a/pkg/client/fake_templates.go b/pkg/client/fake_templates.go new file mode 100644 index 000000000000..9c0ba780decc --- /dev/null +++ b/pkg/client/fake_templates.go @@ -0,0 +1,45 @@ +package client + +import ( + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" + + templateapi "github.com/openshift/origin/pkg/template/api" +) + +// FakeTemplates implements TemplateInterface. Meant to be embedded into a struct to get a default +// implementation. This makes faking out just the methods you want to test easier. +type FakeTemplates struct { + Fake *Fake + Namespace string +} + +func (c *FakeTemplates) List(label, field labels.Selector) (*templateapi.TemplateList, error) { + c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "list-templates"}) + return &templateapi.TemplateList{}, nil +} + +func (c *FakeTemplates) Get(name string) (*templateapi.Template, error) { + c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "get-template"}) + return &templateapi.Template{}, nil +} + +func (c *FakeTemplates) Create(template *templateapi.Template) (*templateapi.Template, error) { + c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "create-template", Value: template}) + return &templateapi.Template{}, nil +} + +func (c *FakeTemplates) Update(template *templateapi.Template) (*templateapi.Template, error) { + c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "update-template"}) + return &templateapi.Template{}, nil +} + +func (c *FakeTemplates) Delete(name string) error { + c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "delete-template", Value: name}) + return nil +} + +func (c *FakeTemplates) Watch(label, field labels.Selector, resourceVersion string) (watch.Interface, error) { + c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "watch-templates"}) + return nil, nil +} diff --git a/pkg/client/templates.go b/pkg/client/templates.go new file mode 100644 index 000000000000..ba2439c50b54 --- /dev/null +++ b/pkg/client/templates.go @@ -0,0 +1,89 @@ +package client + +import ( + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" + + templateapi "github.com/openshift/origin/pkg/template/api" +) + +// TemplatesNamespacer has methods to work with Template resources in a namespace +type TemplatesNamespacer interface { + Templates(namespace string) TemplateInterface +} + +// TemplateInterface exposes methods on Template resources. +type TemplateInterface interface { + List(label, field labels.Selector) (*templateapi.TemplateList, error) + Get(name string) (*templateapi.Template, error) + Create(template *templateapi.Template) (*templateapi.Template, error) + Update(template *templateapi.Template) (*templateapi.Template, error) + Delete(name string) error + Watch(label, field labels.Selector, resourceVersion string) (watch.Interface, error) +} + +// templates implements TemplatesNamespacer interface +type templates struct { + r *Client + ns string +} + +// newTemplates returns a templates +func newTemplates(c *Client, namespace string) *templates { + return &templates{ + r: c, + ns: namespace, + } +} + +// List returns a list of templates that match the label and field selectors. +func (c *templates) List(label, field labels.Selector) (result *templateapi.TemplateList, err error) { + result = &templateapi.TemplateList{} + err = c.r.Get(). + Namespace(c.ns). + Resource("templates"). + SelectorParam("labels", label). + SelectorParam("fields", field). + Do(). + Into(result) + return +} + +// Get returns information about a particular template and error if one occurs. +func (c *templates) Get(name string) (result *templateapi.Template, err error) { + result = &templateapi.Template{} + err = c.r.Get().Namespace(c.ns).Resource("templates").Name(name).Do().Into(result) + return +} + +// Create creates new template. Returns the server's representation of the template and error if one occurs. +func (c *templates) Create(template *templateapi.Template) (result *templateapi.Template, err error) { + result = &templateapi.Template{} + err = c.r.Post().Namespace(c.ns).Resource("templates").Body(template).Do().Into(result) + return +} + +// Update updates the template on server. Returns the server's representation of the template and error if one occurs. +func (c *templates) Update(template *templateapi.Template) (result *templateapi.Template, err error) { + result = &templateapi.Template{} + err = c.r.Put().Namespace(c.ns).Resource("templates").Name(template.Name).Body(template).Do().Into(result) + return +} + +// Delete deletes a template, returns error if one occurs. +func (c *templates) Delete(name string) (err error) { + err = c.r.Delete().Namespace(c.ns).Resource("templates").Name(name).Do().Error() + return +} + +// Watch returns a watch.Interface that watches the requested templates +func (c *templates) Watch(label, field labels.Selector, resourceVersion string) (watch.Interface, error) { + return c.r.Get(). + Prefix("watch"). + Namespace(c.ns). + Resource("templates"). + Param("resourceVersion", resourceVersion). + SelectorParam("labels", label). + SelectorParam("fields", field). + Watch() +} diff --git a/pkg/cmd/cli/describe/describer.go b/pkg/cmd/cli/describe/describer.go index ce9f51160d18..577ff4ff942d 100644 --- a/pkg/cmd/cli/describe/describer.go +++ b/pkg/cmd/cli/describe/describer.go @@ -6,12 +6,15 @@ import ( "strings" "text/tabwriter" + kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta" kclient "github.com/GoogleCloudPlatform/kubernetes/pkg/client" kctl "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl" - kruntime "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" buildapi "github.com/openshift/origin/pkg/build/api" "github.com/openshift/origin/pkg/client" + templateapi "github.com/openshift/origin/pkg/template/api" ) func DescriberFor(kind string, c *client.Client, kclient kclient.Interface, host string) (kctl.Describer, bool) { @@ -32,6 +35,8 @@ func DescriberFor(kind string, c *client.Client, kclient kclient.Interface, host return &RouteDescriber{c}, true case "Project": return &ProjectDescriber{c}, true + case "Template": + return &TemplateDescriber{c, meta.NewAccessor(), kapi.Scheme, nil}, true case "Policy": return &PolicyDescriber{c}, true case "PolicyBinding": @@ -283,7 +288,7 @@ func (d *PolicyDescriber) Describe(namespace, name string) (string, error) { fmt.Fprint(out, key+"\tVerbs\tResources\tExtension\n") for _, rule := range role.Rules { extensionString := "" - if rule.AttributeRestrictions != (kruntime.EmbeddedObject{}) { + if rule.AttributeRestrictions != (runtime.EmbeddedObject{}) { extensionString = fmt.Sprintf("%v", rule.AttributeRestrictions) } @@ -336,3 +341,76 @@ func (d *PolicyBindingDescriber) Describe(namespace, name string) (string, error return nil }) } + +// TemplateDescriber generates information about a template +type TemplateDescriber struct { + client.Interface + meta.MetadataAccessor + runtime.ObjectTyper + DescribeObject func(obj runtime.Object, out *tabwriter.Writer) (bool, error) +} + +func (d *TemplateDescriber) DescribeParameters(params []templateapi.Parameter, out *tabwriter.Writer) { + formatString(out, "Parameters", " ") + indent := " " + for _, p := range params { + formatString(out, indent+"Name", p.Name) + formatString(out, indent+"Description", p.Description) + if len(p.Generate) == 0 { + formatString(out, indent+"Value", p.Value) + continue + } + if len(p.Value) > 0 { + formatString(out, indent+"Value", p.Value) + formatString(out, indent+"Generated (ignored)", p.Generate) + formatString(out, indent+"From", p.From) + } else { + formatString(out, indent+"Generated", p.Generate) + formatString(out, indent+"From", p.From) + } + out.Write([]byte("\n")) + } +} + +func (d *TemplateDescriber) DescribeObjects(objects []runtime.Object, out *tabwriter.Writer) { + formatString(out, "Objects", " ") + + indent := " " + for _, obj := range objects { + if d.DescribeObject != nil { + if ok, _ := d.DescribeObject(obj, out); ok { + out.Write([]byte("\n")) + continue + } + } + + _, kind, _ := d.ObjectTyper.ObjectVersionAndKind(obj) + meta := kapi.ObjectMeta{} + meta.Name, _ = d.MetadataAccessor.Name(obj) + meta.Annotations, _ = d.MetadataAccessor.Annotations(obj) + meta.Labels, _ = d.MetadataAccessor.Labels(obj) + fmt.Fprintf(out, fmt.Sprintf("%s%s\t%s\n", indent, kind, meta.Name)) + if len(meta.Labels) > 0 { + formatString(out, indent+"Labels", formatLabels(meta.Labels)) + } + formatAnnotations(out, meta, indent) + } +} + +func (d *TemplateDescriber) Describe(namespace, name string) (string, error) { + c := d.Templates(namespace) + template, err := c.Get(name) + if err != nil { + return "", err + } + + return tabbedString(func(out *tabwriter.Writer) error { + formatMeta(out, template.ObjectMeta) + out.Write([]byte("\n")) + out.Flush() + d.DescribeParameters(template.Parameters, out) + out.Write([]byte("\n")) + d.DescribeObjects(template.Objects, out) + return nil + }) +} diff --git a/pkg/cmd/cli/describe/describer_test.go b/pkg/cmd/cli/describe/describer_test.go index 1ddf6ecf0921..aeea0db87d8c 100644 --- a/pkg/cmd/cli/describe/describer_test.go +++ b/pkg/cmd/cli/describe/describer_test.go @@ -50,6 +50,7 @@ func TestDescribers(t *testing.T) { &ProjectDescriber{c}, &PolicyDescriber{c}, &PolicyBindingDescriber{c}, + &TemplateDescriber{c, nil, nil, nil}, } for _, d := range testDescriberList { @@ -57,7 +58,7 @@ func TestDescribers(t *testing.T) { if err != nil { t.Errorf("unexpected error for %v: %v", d, err) } - if !strings.Contains(out, "Name:") || !strings.Contains(out, "Annotations") { + if !strings.Contains(out, "Name:") || !strings.Contains(out, "Labels:") { t.Errorf("unexpected out: %s", out) } } diff --git a/pkg/cmd/cli/describe/helpers.go b/pkg/cmd/cli/describe/helpers.go index ed69eeaa95c9..957035640c87 100644 --- a/pkg/cmd/cli/describe/helpers.go +++ b/pkg/cmd/cli/describe/helpers.go @@ -58,10 +58,36 @@ func formatLabels(labelMap map[string]string) string { return labels.Set(labelMap).String() } +func extractAnnotations(annotations map[string]string, keys ...string) ([]string, map[string]string) { + extracted := make([]string, len(keys)) + remaining := make(map[string]string) + for k, v := range annotations { + remaining[k] = v + } + for i, key := range keys { + extracted[i] = remaining[key] + delete(remaining, key) + } + return extracted, remaining +} + +func formatAnnotations(out *tabwriter.Writer, m api.ObjectMeta, prefix string) { + values, annotations := extractAnnotations(m.Annotations, "description") + if len(values[0]) > 0 { + formatString(out, prefix+"Description", values[0]) + } + if len(annotations) > 0 { + formatString(out, prefix+"Annotations", formatLabels(annotations)) + } +} + func formatMeta(out *tabwriter.Writer, m api.ObjectMeta) { formatString(out, "Name", m.Name) - formatString(out, "Annotations", formatLabels(m.Annotations)) - formatString(out, "Created", m.CreationTimestamp) + if !m.CreationTimestamp.IsZero() { + formatString(out, "Created", m.CreationTimestamp) + } + formatString(out, "Labels", formatLabels(m.Labels)) + formatAnnotations(out, m, "") } // webhookURL assembles map with of webhook type as key and webhook url and value diff --git a/pkg/cmd/cli/describe/printer.go b/pkg/cmd/cli/describe/printer.go index 366fe28a6bac..1790e6263301 100644 --- a/pkg/cmd/cli/describe/printer.go +++ b/pkg/cmd/cli/describe/printer.go @@ -29,6 +29,7 @@ var ( routeColumns = []string{"NAME", "HOST/PORT", "PATH", "SERVICE", "LABELS"} deploymentColumns = []string{"NAME", "STATUS", "CAUSE"} deploymentConfigColumns = []string{"NAME", "TRIGGERS", "LATEST VERSION"} + templateColumns = []string{"NAME", "DESCRIPTION", "PARAMETERS", "OBJECTS"} parameterColumns = []string{"NAME", "DESCRIPTION", "GENERATOR", "VALUE"} policyColumns = []string{"NAME", "ROLES", "LAST MODIFIED"} policyBindingColumns = []string{"NAME", "ROLE BINDINGS", "LAST MODIFIED"} @@ -60,7 +61,8 @@ func NewHumanReadablePrinter(noHeaders bool) *kctl.HumanReadablePrinter { p.Handler(deploymentColumns, printDeploymentList) p.Handler(deploymentConfigColumns, printDeploymentConfig) p.Handler(deploymentConfigColumns, printDeploymentConfigList) - p.Handler(parameterColumns, printParameters) + p.Handler(templateColumns, printTemplate) + p.Handler(templateColumns, printTemplateList) p.Handler(policyColumns, printPolicy) p.Handler(policyColumns, printPolicyList) p.Handler(policyBindingColumns, printPolicyBinding) @@ -80,7 +82,41 @@ func NewHumanReadablePrinter(noHeaders bool) *kctl.HumanReadablePrinter { return p } -func printParameters(t *templateapi.Template, w io.Writer) error { +const templateDescriptionLen = 80 + +func printTemplate(t *templateapi.Template, w io.Writer) error { + description := "" + if t.Annotations != nil { + description = t.Annotations["description"] + } + if len(description) > templateDescriptionLen { + description = strings.TrimSpace(description[:templateDescriptionLen-3]) + "..." + } + empty, generated, total := 0, 0, len(t.Parameters) + for _, p := range t.Parameters { + if len(p.Value) > 0 { + continue + } + if len(p.Generate) > 0 { + generated++ + continue + } + empty++ + } + params := "" + switch { + case empty > 0: + params = fmt.Sprintf("%d (%d blank)", total, empty) + case generated > 0: + params = fmt.Sprintf("%d (%d generated)", total, generated) + default: + params = fmt.Sprintf("%d (all set)", total) + } + _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%d\n", t.Name, description, params, len(t.Objects)) + return err +} + +func printTemplateParameters(t *templateapi.Template, w io.Writer) error { for _, p := range t.Parameters { value := p.Value if len(p.Generate) != 0 { @@ -94,6 +130,15 @@ func printParameters(t *templateapi.Template, w io.Writer) error { return nil } +func printTemplateList(list *templateapi.TemplateList, w io.Writer) error { + for _, t := range list.Items { + if err := printTemplate(&t, w); err != nil { + return err + } + } + return nil +} + func printBuild(build *buildapi.Build, w io.Writer) error { _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", build.Name, build.Parameters.Strategy.Type, build.Status, build.PodName) return err diff --git a/pkg/cmd/server/origin/master.go b/pkg/cmd/server/origin/master.go index 618f02bc60fb..9bbbf3466dfa 100644 --- a/pkg/cmd/server/origin/master.go +++ b/pkg/cmd/server/origin/master.go @@ -67,6 +67,7 @@ import ( routeregistry "github.com/openshift/origin/pkg/route/registry/route" "github.com/openshift/origin/pkg/service" templateregistry "github.com/openshift/origin/pkg/template/registry" + templateetcd "github.com/openshift/origin/pkg/template/registry/etcd" "github.com/openshift/origin/pkg/user" useretcd "github.com/openshift/origin/pkg/user/registry/etcd" userregistry "github.com/openshift/origin/pkg/user/registry/user" @@ -288,6 +289,7 @@ func (c *MasterConfig) InstallProtectedAPI(container *restful.Container) []strin "deploymentConfigRollbacks": deployrollback.NewREST(deployRollbackClient, latest.Codec), "templateConfigs": templateregistry.NewREST(), + "templates": templateetcd.NewREST(c.EtcdHelper), "routes": routeregistry.NewREST(routeEtcd), diff --git a/pkg/template/api/register.go b/pkg/template/api/register.go index 586dace19b74..d46fb39cf1ab 100644 --- a/pkg/template/api/register.go +++ b/pkg/template/api/register.go @@ -7,7 +7,9 @@ import ( func init() { api.Scheme.AddKnownTypes("", &Template{}, + &TemplateList{}, ) } -func (*Template) IsAnAPIObject() {} +func (*Template) IsAnAPIObject() {} +func (*TemplateList) IsAnAPIObject() {} diff --git a/pkg/template/api/types.go b/pkg/template/api/types.go index 23f8c30a4809..95fb719aaa5e 100644 --- a/pkg/template/api/types.go +++ b/pkg/template/api/types.go @@ -7,15 +7,22 @@ import ( // Template contains the inputs needed to produce a Config. type Template struct { - kapi.TypeMeta `json:",inline"` - kapi.ObjectMeta `json:"metadata,omitempty"` - - // Required: A list of resources that might reference parameters - Items []runtime.Object `json:"items"` + kapi.TypeMeta + kapi.ObjectMeta // Optional: Parameters is an array of Parameters used during the // Template to Config transformation. - Parameters []Parameter `json:"parameters,omitempty"` + Parameters []Parameter + + // Required: A list of resources to create + Objects []runtime.Object +} + +// TemplateList is a list of Template objects. +type TemplateList struct { + kapi.TypeMeta + kapi.ListMeta + Items []Template } // Parameter defines a name/value variable that is to be processed during @@ -23,23 +30,22 @@ type Template struct { type Parameter struct { // Required: Parameter name must be set and it can be referenced in Template // Items using ${PARAMETER_NAME} - Name string `json:"name"` + Name string // Optional: Parameter can have description - Description string `json:"description,omitempty"` + Description string + + // Optional: Value holds the Parameter data. If specified, the generator + // will be ignored. The value replaces all occurrences of the Parameter + // ${Name} expression during the Template to Config transformation. + Value string // Optional: Generate specifies the generator to be used to generate // random string from an input value specified by From field. The result // string is stored into Value field. If empty, no generator is being // used, leaving the result Value untouched. - Generate string `json:"generate,omitempty"` + Generate string // Optional: From is an input value for the generator. - From string `json:"from,omitempty"` - - // Optional: Value holds the Parameter data. The Value data can be - // overwritten by the generator. The value replaces all occurrences - // of the Parameter ${Name} expression during the Template to Config - // transformation. - Value string `json:"value,omitempty"` + From string } diff --git a/pkg/template/api/v1beta1/conversion.go b/pkg/template/api/v1beta1/conversion.go new file mode 100644 index 000000000000..2dfdd997a06c --- /dev/null +++ b/pkg/template/api/v1beta1/conversion.go @@ -0,0 +1,29 @@ +package v1beta1 + +import ( + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/conversion" + + newer "github.com/openshift/origin/pkg/template/api" +) + +func init() { + err := api.Scheme.AddConversionFuncs( + // TypeMeta must be split into two objects + func(in *newer.Template, out *Template, s conversion.Scope) error { + if err := s.Convert(&in.Objects, &out.Items, 0); err != nil { + return err + } + return s.DefaultConvert(in, out, conversion.IgnoreMissingFields) + }, + func(in *Template, out *newer.Template, s conversion.Scope) error { + if err := s.Convert(&in.Items, &out.Objects, 0); err != nil { + return err + } + return s.DefaultConvert(in, out, conversion.IgnoreMissingFields) + }, + ) + if err != nil { + panic(err) + } +} diff --git a/pkg/template/api/v1beta1/register.go b/pkg/template/api/v1beta1/register.go index 88fac73eeb9a..dfbc804cc066 100644 --- a/pkg/template/api/v1beta1/register.go +++ b/pkg/template/api/v1beta1/register.go @@ -7,8 +7,10 @@ import ( func init() { api.Scheme.AddKnownTypes("v1beta1", &Template{}, + &TemplateList{}, ) api.Scheme.AddKnownTypeWithName("v1beta1", "TemplateConfig", &Template{}) } -func (*Template) IsAnAPIObject() {} +func (*Template) IsAnAPIObject() {} +func (*TemplateList) IsAnAPIObject() {} diff --git a/pkg/template/api/v1beta1/types.go b/pkg/template/api/v1beta1/types.go index 7ea73b166709..783c0509203b 100644 --- a/pkg/template/api/v1beta1/types.go +++ b/pkg/template/api/v1beta1/types.go @@ -19,6 +19,13 @@ type Template struct { Parameters []Parameter `json:"parameters,omitempty"` } +// TemplateList is a list of Template objects. +type TemplateList struct { + kapi.TypeMeta `json:",inline"` + kapi.ListMeta `json:"metadata,omitempty"` + Items []Template `json:"items"` +} + // Parameter defines a name/value variable that is to be processed during // the Template to Config transformation. type Parameter struct { @@ -29,6 +36,11 @@ type Parameter struct { // Optional: Parameter can have description Description string `json:"description,omitempty"` + // Optional: Value holds the Parameter data. If specified, the generator + // will be ignored. The value replaces all occurrences of the Parameter + // ${Name} expression during the Template to Config transformation. + Value string `json:"value,omitempty"` + // Optional: Generate specifies the generator to be used to generate // random string from an input value specified by From field. The result // string is stored into Value field. If empty, no generator is being @@ -37,10 +49,4 @@ type Parameter struct { // Optional: From is an input value for the generator. From string `json:"from,omitempty"` - - // Optional: Value holds the Parameter data. The Value data can be - // overwritten by the generator. The value replaces all occurrences - // of the Parameter ${Name} expression during the Template to Config - // transformation. - Value string `json:"value,omitempty"` } diff --git a/pkg/template/api/validation/validation.go b/pkg/template/api/validation/validation.go index 3c278eeb1304..24ceaffcdd08 100644 --- a/pkg/template/api/validation/validation.go +++ b/pkg/template/api/validation/validation.go @@ -3,11 +3,11 @@ package validation import ( "fmt" "regexp" - "strings" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/validation" + "github.com/openshift/origin/pkg/template/api" - "github.com/openshift/origin/pkg/util" ) var parameterNameExp = regexp.MustCompile(`^[a-zA-Z0-9\_]+$`) @@ -24,32 +24,29 @@ func ValidateParameter(param *api.Parameter) (errs errors.ValidationErrorList) { return } +// ValidateProcessedTemplate tests if required fields in the Template are set for processing +func ValidateProcessedTemplate(template *api.Template) errors.ValidationErrorList { + return validateTemplateBody(template) +} + // ValidateTemplate tests if required fields in the Template are set. func ValidateTemplate(template *api.Template) (errs errors.ValidationErrorList) { - if len(template.Name) == 0 { - errs = append(errs, errors.NewFieldRequired("name", template.Name)) - } + errs = validation.ValidateObjectMeta(&template.ObjectMeta, true, validation.ValidatePodName).Prefix("metadata") + errs = append(errs, validateTemplateBody(template)...) + return +} + +// ValidateTemplateUpdate tests if required fields in the template are set during an update +func ValidateTemplateUpdate(oldTemplate, template *api.Template) errors.ValidationErrorList { + errs := validation.ValidateObjectMetaUpdate(&oldTemplate.ObjectMeta, &template.ObjectMeta).Prefix("metadata") + return errs +} + +// validateTemplateBody checks the body of a template. +func validateTemplateBody(template *api.Template) (errs errors.ValidationErrorList) { for i := range template.Parameters { paramErr := ValidateParameter(&template.Parameters[i]) errs = append(errs, paramErr.PrefixIndex(i).Prefix("parameters")...) } - for _, obj := range template.Items { - errs = append(errs, util.ValidateObject(obj)...) - } return } - -func filter(errs errors.ValidationErrorList, prefix string) errors.ValidationErrorList { - if errs == nil { - return errs - } - next := errors.ValidationErrorList{} - for _, err := range errs { - ve, ok := err.(*errors.ValidationError) - if ok && strings.HasPrefix(ve.Field, prefix) { - continue - } - next = append(next, err) - } - return next -} diff --git a/pkg/template/api/validation/validation_test.go b/pkg/template/api/validation/validation_test.go index ba68c0271b66..e6563b7a0dd8 100644 --- a/pkg/template/api/validation/validation_test.go +++ b/pkg/template/api/validation/validation_test.go @@ -5,6 +5,7 @@ import ( kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util/errors" "github.com/openshift/origin/pkg/template/api" ) @@ -21,15 +22,15 @@ func TestValidateParameter(t *testing.T) { ParameterName string IsValidExpected bool }{ - {"VALID_NAME", true}, + {"VALname_NAME", true}, {"_valid_name_99", true}, {"10gen_valid_name", true}, {"", false}, - {"INVALID NAME", false}, - {"IVALID-NAME", false}, - {">INVALID_NAME", false}, - {"$INVALID_NAME", false}, - {"${INVALID_NAME}", false}, + {"INVALname NAME", false}, + {"IVALname-NAME", false}, + {">INVALname_NAME", false}, + {"$INVALname_NAME", false}, + {"${INVALname_NAME}", false}, } for _, test := range tests { @@ -43,16 +44,16 @@ func TestValidateParameter(t *testing.T) { } } -func TestValidateTemplate(t *testing.T) { +func TestValidateProcessTemplate(t *testing.T) { var tests = []struct { template *api.Template isValidExpected bool }{ - { // Empty Template, should fail on empty ID + { // Empty Template, should pass &api.Template{}, - false, + true, }, - { // Template with ID, should pass + { // Template with name, should pass &api.Template{ ObjectMeta: kapi.ObjectMeta{Name: "templateId"}, }, @@ -71,7 +72,7 @@ func TestValidateTemplate(t *testing.T) { &api.Template{ ObjectMeta: kapi.ObjectMeta{Name: "templateId"}, Parameters: []api.Parameter{ - *(makeParameter("VALID_NAME", "1")), + *(makeParameter("VALname_NAME", "1")), }, }, true, @@ -80,21 +81,112 @@ func TestValidateTemplate(t *testing.T) { &api.Template{ ObjectMeta: kapi.ObjectMeta{Name: "templateId"}, Parameters: []api.Parameter{ - *(makeParameter("VALID_NAME", "1")), + *(makeParameter("VALname_NAME", "1")), }, - Items: []runtime.Object{}, + Objects: []runtime.Object{}, }, true, }, } - for _, test := range tests { + for i, test := range tests { + errs := ValidateProcessedTemplate(test.template) + if len(errs) != 0 && test.isValidExpected { + t.Errorf("%d: Unexpected non-empty error list: %v", i, errors.NewAggregate(errs)) + } + if len(errs) == 0 && !test.isValidExpected { + t.Errorf("%d: Unexpected empty error list: %#v", i, errs) + } + } +} + +func TestValidateTemplate(t *testing.T) { + var tests = []struct { + template *api.Template + isValidExpected bool + }{ + { // Empty Template, should fail on empty name + &api.Template{}, + false, + }, + { // Template with name, should pass + &api.Template{ + ObjectMeta: kapi.ObjectMeta{ + Name: "template", + Namespace: kapi.NamespaceDefault, + }, + }, + true, + }, + { // Template without namespace, should fail + &api.Template{ + ObjectMeta: kapi.ObjectMeta{ + Name: "template", + }, + }, + false, + }, + { // Template with invalid name characters, should fail + &api.Template{ + ObjectMeta: kapi.ObjectMeta{ + Name: "templateId", + Namespace: kapi.NamespaceDefault, + }, + }, + false, + }, + { // Template with invalid Parameter, should fail on Parameter name + &api.Template{ + ObjectMeta: kapi.ObjectMeta{Name: "template", Namespace: kapi.NamespaceDefault}, + Parameters: []api.Parameter{ + *(makeParameter("", "1")), + }, + }, + false, + }, + { // Template with valid Parameter, should pass + &api.Template{ + ObjectMeta: kapi.ObjectMeta{Name: "template", Namespace: kapi.NamespaceDefault}, + Parameters: []api.Parameter{ + *(makeParameter("VALname_NAME", "1")), + }, + }, + true, + }, + { // Template with empty items, should pass + &api.Template{ + ObjectMeta: kapi.ObjectMeta{Name: "template", Namespace: kapi.NamespaceDefault}, + Parameters: []api.Parameter{}, + Objects: []runtime.Object{}, + }, + true, + }, + { // Template with an item that is invalid, should pass + &api.Template{ + ObjectMeta: kapi.ObjectMeta{Name: "template", Namespace: kapi.NamespaceDefault}, + Parameters: []api.Parameter{}, + Objects: []runtime.Object{ + &kapi.Service{ + ObjectMeta: kapi.ObjectMeta{ + GenerateName: "test", + }, + Spec: kapi.ServiceSpec{ + Port: 8080, + }, + }, + }, + }, + true, + }, + } + + for i, test := range tests { errs := ValidateTemplate(test.template) if len(errs) != 0 && test.isValidExpected { - t.Errorf("Unexpected non-empty error list: %#v", errs) + t.Errorf("%d: Unexpected non-empty error list: %v", i, errors.NewAggregate(errs)) } if len(errs) == 0 && !test.isValidExpected { - t.Errorf("Unexpected empty error list: %#v", errs) + t.Errorf("%d: Unexpected empty error list: %v", i, errs) } } } diff --git a/pkg/template/registry/etcd/etcd.go b/pkg/template/registry/etcd/etcd.go new file mode 100644 index 000000000000..a2764b596320 --- /dev/null +++ b/pkg/template/registry/etcd/etcd.go @@ -0,0 +1,58 @@ +package etcd + +import ( + kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + etcdgeneric "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/generic/etcd" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + "github.com/GoogleCloudPlatform/kubernetes/pkg/tools" + + "github.com/openshift/origin/pkg/template/api" + "github.com/openshift/origin/pkg/template/registry" +) + +// REST implements a RESTStorage for templates against etcd +type REST struct { + *etcdgeneric.Etcd +} + +// NewREST returns a RESTStorage object that will work against templates. +func NewREST(h tools.EtcdHelper) *REST { + store := &etcdgeneric.Etcd{ + NewFunc: func() runtime.Object { return &api.Template{} }, + NewListFunc: func() runtime.Object { return &api.TemplateList{} }, + KeyRootFunc: func(ctx kapi.Context) string { + return etcdgeneric.NamespaceKeyRootFunc(ctx, "/registry/templates") + }, + KeyFunc: func(ctx kapi.Context, name string) (string, error) { + return etcdgeneric.NamespaceKeyFunc(ctx, "/registry/templates", name) + }, + ObjectNameFunc: func(obj runtime.Object) (string, error) { + return obj.(*api.Template).Name, nil + }, + EndpointName: "templates", + + CreateStrategy: registry.Strategy, + UpdateStrategy: registry.Strategy, + + ReturnDeletedObject: true, + + Helper: h, + } + return &REST{store} +} + +// New returns a new object +func (r *REST) New() runtime.Object { + return r.NewFunc() +} + +// NewList returns a new list object +func (r *REST) NewList() runtime.Object { + return r.NewListFunc() +} + +// List obtains a list of templates with labels that match selector. +func (r *REST) List(ctx kapi.Context, label, field labels.Selector) (runtime.Object, error) { + return r.Etcd.List(ctx, registry.MatchTemplate(label, field)) +} diff --git a/pkg/template/registry/etcd/etcd_test.go b/pkg/template/registry/etcd/etcd_test.go new file mode 100644 index 000000000000..37d72b3ebf01 --- /dev/null +++ b/pkg/template/registry/etcd/etcd_test.go @@ -0,0 +1,62 @@ +package etcd + +import ( + "testing" + + kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest/resttest" + "github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver" + "github.com/GoogleCloudPlatform/kubernetes/pkg/tools" + + "github.com/openshift/origin/pkg/api/latest" + "github.com/openshift/origin/pkg/template/api" +) + +func newHelper(t *testing.T) (*tools.FakeEtcdClient, tools.EtcdHelper) { + fakeEtcdClient := tools.NewFakeEtcdClient(t) + fakeEtcdClient.TestIndex = true + helper := tools.EtcdHelper{Client: fakeEtcdClient, Codec: latest.Codec, ResourceVersioner: tools.RuntimeVersionAdapter{latest.ResourceVersioner}} + return fakeEtcdClient, helper +} + +func validNew() *api.Template { + return &api.Template{ + ObjectMeta: kapi.ObjectMeta{ + Name: "foo", + Namespace: kapi.NamespaceDefault, + }, + } +} + +func validChanged() *api.Template { + template := validNew() + template.ResourceVersion = "1" + template.Labels = map[string]string{ + "foo": "bar", + } + return template +} + +func TestStorage(t *testing.T) { + _, helper := newHelper(t) + storage := NewREST(helper) + var _ apiserver.RESTCreater = storage + var _ apiserver.RESTLister = storage + var _ apiserver.RESTDeleter = storage + var _ apiserver.RESTUpdater = storage + var _ apiserver.RESTGetter = storage +} + +func TestCreate(t *testing.T) { + fakeEtcdClient, helper := newHelper(t) + storage := NewREST(helper) + test := resttest.New(t, storage, fakeEtcdClient.SetError) + template := validNew() + template.ObjectMeta = kapi.ObjectMeta{} + test.TestCreate( + // valid + template, + // invalid + &api.Template{}, + ) +} diff --git a/pkg/template/registry/rest.go b/pkg/template/registry/rest.go index 5c09d2e3a301..f976ce626d5b 100644 --- a/pkg/template/registry/rest.go +++ b/pkg/template/registry/rest.go @@ -1,25 +1,74 @@ -package template +package registry import ( + "fmt" "math/rand" "time" kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" - apierr "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/generic" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" utilerr "github.com/GoogleCloudPlatform/kubernetes/pkg/util/errors" "github.com/golang/glog" + "github.com/openshift/origin/pkg/template" "github.com/openshift/origin/pkg/template/api" "github.com/openshift/origin/pkg/template/api/validation" "github.com/openshift/origin/pkg/template/generator" ) -// REST implements RESTStorage interface for Template objects. +// templateStrategy implements behavior for Templates +type templateStrategy struct { + runtime.ObjectTyper + kapi.NameGenerator +} + +// Strategy is the default logic that applies when creating and updating Template +// objects via the REST API. +var Strategy = templateStrategy{kapi.Scheme, kapi.SimpleNameGenerator} + +// NamespaceScoped is true for templates. +func (templateStrategy) NamespaceScoped() bool { + return true +} + +// ResetBeforeCreate clears fields that are not allowed to be set by end users on creation. +func (templateStrategy) ResetBeforeCreate(obj runtime.Object) { +} + +// Validate validates a new template. +func (templateStrategy) Validate(obj runtime.Object) errors.ValidationErrorList { + template := obj.(*api.Template) + return validation.ValidateTemplate(template) +} + +// AllowCreateOnUpdate is false for templates. +func (templateStrategy) AllowCreateOnUpdate() bool { + return false +} + +// ValidateUpdate is the default update validation for an end user. +func (templateStrategy) ValidateUpdate(obj, old runtime.Object) errors.ValidationErrorList { + return validation.ValidateTemplateUpdate(obj.(*api.Template), old.(*api.Template)) +} + +// MatchTemplate returns a generic matcher for a given label and field selector. +func MatchTemplate(label, field labels.Selector) generic.Matcher { + return generic.MatcherFunc(func(obj runtime.Object) (bool, error) { + o, ok := obj.(*api.Template) + if !ok { + return false, fmt.Errorf("not a pod") + } + return label.Matches(labels.Set(o.Labels)), nil + }) +} + +// REST implements RESTStorage interface for processing Template objects. type REST struct{} -// NewREST creates new RESTStorage interface for Template objects. +// NewREST creates new RESTStorage interface for processing Template objects. func NewREST() *REST { return &REST{} } @@ -31,10 +80,10 @@ func (s *REST) New() runtime.Object { func (s *REST) Create(ctx kapi.Context, obj runtime.Object) (runtime.Object, error) { tpl, ok := obj.(*api.Template) if !ok { - return nil, apierr.NewBadRequest("not a template") + return nil, errors.NewBadRequest("not a template") } - if errs := validation.ValidateTemplate(tpl); len(errs) > 0 { - return nil, apierr.NewInvalid("template", tpl.Name, errs) + if errs := validation.ValidateProcessedTemplate(tpl); len(errs) > 0 { + return nil, errors.NewInvalid("template", tpl.Name, errs) } generators := map[string]generator.Generator{ "expression": generator.NewExpressionValueGenerator(rand.New(rand.NewSource(time.Now().UnixNano()))), diff --git a/pkg/template/registry/rest_test.go b/pkg/template/registry/rest_test.go index 862365c71b59..7ae96e814c25 100644 --- a/pkg/template/registry/rest_test.go +++ b/pkg/template/registry/rest_test.go @@ -1,4 +1,4 @@ -package template +package registry import ( "testing" diff --git a/pkg/template/template.go b/pkg/template/template.go index 3f6f073837e2..5856a0ec1802 100644 --- a/pkg/template/template.go +++ b/pkg/template/template.go @@ -45,7 +45,7 @@ func (p *Processor) Process(template *api.Template) (*configapi.Config, errs.Val return nil, append(templateErrors.Prefix("Template"), errs.NewFieldInvalid("parameters", err, "failure to generate parameter value")) } - for i, item := range template.Items { + for i, item := range template.Objects { newItem, err := p.SubstituteParameters(template.Parameters, item) if err != nil { reportError(&templateErrors, i, *errs.NewFieldNotSupported("parameters", err)) @@ -56,10 +56,10 @@ func (p *Processor) Process(template *api.Template) (*configapi.Config, errs.Val reportError(&templateErrors, i, *errs.NewFieldInvalid("namespace", err, "failed to remove the item namespace")) } itemMeta.SetNamespace("") - template.Items[i] = newItem + template.Objects[i] = newItem } - return &configapi.Config{Items: template.Items}, templateErrors.Prefix("Template") + return &configapi.Config{Items: template.Objects}, templateErrors.Prefix("Template") } // AddParameter adds new custom parameter to the Template. It overrides @@ -138,7 +138,8 @@ func (p *Processor) substituteParametersInManifest(containers []kapi.Container, } // GenerateParameterValues generates Value for each Parameter of the given -// Template that has Generate field specified. +// Template that has Generate field specified where Value is not already +// supplied. // // Examples: // @@ -151,6 +152,9 @@ func (p *Processor) substituteParametersInManifest(containers []kapi.Container, func (p *Processor) GenerateParameterValues(t *api.Template) error { for i := range t.Parameters { param := &t.Parameters[i] + if len(param.Value) > 0 { + continue + } if param.Generate != "" { generator, ok := p.Generators[param.Generate] if !ok {