Browse code

Add support to osc new-app from stored template

fabianofranz authored on 2015/04/09 09:42:18
Showing 10 changed files
... ...
@@ -347,6 +347,16 @@ echo "imageRepositoryMappings: ok"
347 347
 # the local image repository takes precedence over the Docker Hub "mysql" image
348 348
 [ "$(osc new-app mysql -o yaml | grep mysql-55-centos7)" ]
349 349
 osc new-app php mysql
350
+# check if we can create from a stored template
351
+osc create -f examples/sample-app/application-template-stibuild.json
352
+osc get template ruby-helloworld-sample
353
+[ "$(osc new-app ruby-helloworld-sample -o yaml | grep MYSQL_USER)" ]
354
+[ "$(osc new-app ruby-helloworld-sample -o yaml | grep MYSQL_PASSWORD)" ]
355
+[ "$(osc new-app ruby-helloworld-sample -o yaml | grep ADMIN_USERNAME)" ]
356
+[ "$(osc new-app ruby-helloworld-sample -o yaml | grep ADMIN_PASSWORD)" ]
357
+# create from template with code explicitly set is not supported
358
+[ ! "$(osc new-app ruby-helloworld-sample~git@github.com/mfojtik/sinatra-app-example)" ]
359
+osc delete template ruby-helloworld-sample
350 360
 echo "new-app: ok"
351 361
 
352 362
 osc get routes
... ...
@@ -41,6 +41,7 @@ type Interface interface {
41 41
 	RootResourceAccessReviews
42 42
 	SubjectAccessReviewsNamespacer
43 43
 	TemplatesNamespacer
44
+	TemplateConfigsNamespacer
44 45
 }
45 46
 
46 47
 // Builds provides a REST client for Builds
... ...
@@ -88,6 +88,10 @@ func (c *Fake) Templates(namespace string) TemplateInterface {
88 88
 	return &FakeTemplates{Fake: c}
89 89
 }
90 90
 
91
+func (c *Fake) TemplateConfigs(namespace string) TemplateConfigInterface {
92
+	return &FakeTemplateConfigs{Fake: c}
93
+}
94
+
91 95
 func (c *Fake) Identities() IdentityInterface {
92 96
 	return &FakeIdentities{Fake: c}
93 97
 }
94 98
new file mode 100644
... ...
@@ -0,0 +1,18 @@
0
+package client
1
+
2
+import (
3
+	configapi "github.com/openshift/origin/pkg/config/api"
4
+	templateapi "github.com/openshift/origin/pkg/template/api"
5
+)
6
+
7
+// FakeTemplateConfigs implements TemplateConfigsInterface. Meant to be embedded into a struct to get a default
8
+// implementation. This makes faking out just the methods you want to test easier.
9
+type FakeTemplateConfigs struct {
10
+	Fake      *Fake
11
+	Namespace string
12
+}
13
+
14
+func (c *FakeTemplateConfigs) Create(template *templateapi.Template) (*configapi.Config, error) {
15
+	obj, err := c.Fake.Invokes(FakeAction{Action: "create-template-config", Value: template}, &configapi.Config{})
16
+	return obj.(*configapi.Config), err
17
+}
... ...
@@ -30,9 +30,9 @@ var errExit = fmt.Errorf("exit directly")
30 30
 const newAppLongDesc = `
31 31
 Create a new application in OpenShift by specifying source code, templates, and/or images.
32 32
 
33
-This command will try to build up the components of an application using images or code
34
-located on your system. It will lookup the images on the local Docker installation (if
35
-available), a Docker registry, or an OpenShift image stream. If you specify a source
33
+This command will try to build up the components of an application using images, templates, 
34
+or code located on your system. It will lookup the images on the local Docker installation 
35
+(if available), a Docker registry, or an OpenShift image stream. If you specify a source
36 36
 code URL, it will set up a build that takes your source code and converts it into an
37 37
 image that can run inside of a pod. The images will be deployed via a deployment
38 38
 configuration, and a service will be hooked up to the first public port of the app.
... ...
@@ -51,6 +51,9 @@ Examples:
51 51
 	# Create an application from the remote repository using the specified label
52 52
 	$ %[1]s new-app https://github.com/openshift/ruby-hello-world -l name=hello-world
53 53
 
54
+	# Create an application based on a stored template, explicitly setting a parameter value
55
+	$ %[1]s new-app ruby-helloworld-sample --env=MYSQL_USER=admin
56
+
54 57
 If you specify source code, you may need to run a build with 'start-build' after the
55 58
 application is created.
56 59
 
... ...
@@ -80,12 +83,20 @@ func NewCmdNewApplication(fullName string, f *clientcmd.Factory, out io.Writer)
80 80
 	cmd.Flags().Var(&config.SourceRepositories, "code", "Source code to use to build this application.")
81 81
 	cmd.Flags().VarP(&config.ImageStreams, "image", "i", "Name of an OpenShift image stream to use in the app.")
82 82
 	cmd.Flags().Var(&config.DockerImages, "docker-image", "Name of a Docker image to include in the app.")
83
+	cmd.Flags().Var(&config.Templates, "template", "Name of an OpenShift stored template to use in the app.")
84
+	cmd.Flags().VarP(&config.TemplateParameters, "param", "p", "Specify a list of key value pairs (eg. -p FOO=BAR,BAR=FOO) to set/override parameter values in the template.")
83 85
 	cmd.Flags().Var(&config.Groups, "group", "Indicate components that should be grouped together as <comp1>+<comp2>.")
84 86
 	cmd.Flags().VarP(&config.Environment, "env", "e", "Specify key value pairs of environment variables to set into each container.")
85
-	cmd.Flags().StringVar(&config.TypeOfBuild, "build", "", "Specify the type of build to use if you don't want to detect (docker|source)")
86
-	cmd.Flags().StringP("labels", "l", "", "Label to set in all resources for this application")
87
+	cmd.Flags().StringVar(&config.TypeOfBuild, "build", "", "Specify the type of build to use if you don't want to detect (docker|source).")
88
+	cmd.Flags().StringP("labels", "l", "", "Label to set in all resources for this application.")
87 89
 
88
-	cmdutil.AddPrinterFlags(cmd)
90
+	// TODO AddPrinterFlags disabled so that it doesn't conflict with our own "template" flag.
91
+	// Need a better solution.
92
+	// cmdutil.AddPrinterFlags(cmd)
93
+	cmd.Flags().StringP("output", "o", "", "Output format. One of: json|yaml|template|templatefile.")
94
+	cmd.Flags().String("output-version", "", "Output the formatted object with the given version (default api-version).")
95
+	cmd.Flags().Bool("no-headers", false, "When using the default output, don't print headers.")
96
+	cmd.Flags().String("output-template", "", "Template string or path to template file to use when -o=template or -o=templatefile.  The template format is golang templates [http://golang.org/pkg/text/template/#pkg-overview]")
89 97
 
90 98
 	return cmd
91 99
 }
... ...
@@ -126,7 +137,7 @@ func RunNewApplication(f *clientcmd.Factory, out io.Writer, c *cobra.Command, ar
126 126
 		}
127 127
 		if err == newcmd.ErrNoInputs {
128 128
 			// TODO: suggest things to the user
129
-			return cmdutil.UsageError(c, "You must specify one or more images, image streams, or source code locations to create an application.")
129
+			return cmdutil.UsageError(c, "You must specify one or more images, image streams, templates or source code locations to create an application.")
130 130
 		}
131 131
 		return err
132 132
 	}
... ...
@@ -20,27 +20,34 @@ import (
20 20
 	"github.com/openshift/origin/pkg/generate/dockerfile"
21 21
 	"github.com/openshift/origin/pkg/generate/source"
22 22
 	imageapi "github.com/openshift/origin/pkg/image/api"
23
+	"github.com/openshift/origin/pkg/template"
23 24
 )
24 25
 
25 26
 // AppConfig contains all the necessary configuration for an application
26 27
 type AppConfig struct {
27 28
 	SourceRepositories util.StringList
28 29
 
29
-	Components   util.StringList
30
-	ImageStreams util.StringList
31
-	DockerImages util.StringList
32
-	Groups       util.StringList
33
-	Environment  util.StringList
30
+	Components         util.StringList
31
+	ImageStreams       util.StringList
32
+	DockerImages       util.StringList
33
+	Templates          util.StringList
34
+	TemplateParameters util.StringList
35
+	Groups             util.StringList
36
+	Environment        util.StringList
34 37
 
35 38
 	TypeOfBuild string
36 39
 
37 40
 	dockerResolver      app.Resolver
38 41
 	imageStreamResolver app.Resolver
42
+	templateResolver    app.Resolver
39 43
 
40 44
 	searcher app.Searcher
41 45
 	detector app.Detector
42 46
 
43 47
 	typer runtime.ObjectTyper
48
+
49
+	osclient        client.Interface
50
+	originNamespace string
44 51
 }
45 52
 
46 53
 // UsageError is an interface for printing usage errors
... ...
@@ -70,19 +77,25 @@ func NewAppConfig(typer runtime.ObjectTyper) *AppConfig {
70 70
 // SetDockerClient sets the passed Docker client in the application configuration
71 71
 func (c *AppConfig) SetDockerClient(dockerclient *docker.Client) {
72 72
 	c.dockerResolver = app.DockerClientResolver{
73
-		Client: dockerclient,
74
-
73
+		Client:           dockerclient,
75 74
 		RegistryResolver: c.dockerResolver,
76 75
 	}
77 76
 }
78 77
 
79 78
 // SetOpenShiftClient sets the passed OpenShift client in the application configuration
80 79
 func (c *AppConfig) SetOpenShiftClient(osclient client.Interface, originNamespace string) {
80
+	c.osclient = osclient
81
+	c.originNamespace = originNamespace
81 82
 	c.imageStreamResolver = app.ImageStreamResolver{
82 83
 		Client:            osclient,
83 84
 		ImageStreamImages: osclient,
84 85
 		Namespaces:        []string{originNamespace, "default"},
85 86
 	}
87
+	c.templateResolver = app.TemplateResolver{
88
+		Client: osclient,
89
+		TemplateConfigsNamespacer: osclient,
90
+		Namespaces:                []string{originNamespace, "openshift", "default"},
91
+	}
86 92
 }
87 93
 
88 94
 // AddArguments converts command line arguments into the appropriate bucket based on what they look like
... ...
@@ -107,41 +120,54 @@ func (c *AppConfig) AddArguments(args []string) []string {
107 107
 }
108 108
 
109 109
 // validate converts all of the arguments on the config into references to objects, or returns an error
110
-func (c *AppConfig) validate() (app.ComponentReferences, []*app.SourceRepository, cmdutil.Environment, error) {
110
+func (c *AppConfig) validate() (app.ComponentReferences, []*app.SourceRepository, cmdutil.Environment, cmdutil.Environment, error) {
111 111
 	b := &app.ReferenceBuilder{}
112 112
 	for _, s := range c.SourceRepositories {
113 113
 		b.AddSourceRepository(s)
114 114
 	}
115
-	b.AddImages(c.DockerImages, func(input *app.ComponentInput) app.ComponentReference {
115
+	b.AddComponents(c.DockerImages, func(input *app.ComponentInput) app.ComponentReference {
116 116
 		input.Argument = fmt.Sprintf("--docker-image=%q", input.From)
117 117
 		input.Resolver = c.dockerResolver
118 118
 		return input
119 119
 	})
120
-	b.AddImages(c.ImageStreams, func(input *app.ComponentInput) app.ComponentReference {
120
+	b.AddComponents(c.ImageStreams, func(input *app.ComponentInput) app.ComponentReference {
121 121
 		input.Argument = fmt.Sprintf("--image=%q", input.From)
122 122
 		input.Resolver = c.imageStreamResolver
123 123
 		return input
124 124
 	})
125
-	b.AddImages(c.Components, func(input *app.ComponentInput) app.ComponentReference {
125
+	b.AddComponents(c.Templates, func(input *app.ComponentInput) app.ComponentReference {
126
+		input.Argument = fmt.Sprintf("--template=%q", input.From)
127
+		input.Resolver = c.templateResolver
128
+		return input
129
+	})
130
+	b.AddComponents(c.Components, func(input *app.ComponentInput) app.ComponentReference {
126 131
 		input.Resolver = app.PerfectMatchWeightedResolver{
127 132
 			app.WeightedResolver{Resolver: c.imageStreamResolver, Weight: 0.0},
133
+			app.WeightedResolver{Resolver: c.templateResolver, Weight: 0.0},
128 134
 			app.WeightedResolver{Resolver: c.dockerResolver, Weight: 2.0},
129 135
 		}
130 136
 		return input
131 137
 	})
132 138
 	b.AddGroups(c.Groups)
133 139
 	refs, repos, errs := b.Result()
140
+
134 141
 	if len(c.TypeOfBuild) != 0 && len(repos) == 0 {
135 142
 		errs = append(errs, fmt.Errorf("when --build is specified you must provide at least one source code location"))
136 143
 	}
137 144
 
138
-	env, duplicate, envErrs := cmdutil.ParseEnvironmentArguments(c.Environment)
139
-	for _, s := range duplicate {
145
+	env, duplicateEnv, envErrs := cmdutil.ParseEnvironmentArguments(c.Environment)
146
+	for _, s := range duplicateEnv {
140 147
 		glog.V(1).Infof("The environment variable %q was overwritten", s)
141 148
 	}
142 149
 	errs = append(errs, envErrs...)
143 150
 
144
-	return refs, repos, env, errors.NewAggregate(errs)
151
+	parms, duplicateParms, parmsErrs := cmdutil.ParseEnvironmentArguments(c.TemplateParameters)
152
+	for _, s := range duplicateParms {
153
+		glog.V(1).Infof("The template parameter %q was overwritten", s)
154
+	}
155
+	errs = append(errs, parmsErrs...)
156
+
157
+	return refs, repos, env, parms, errors.NewAggregate(errs)
145 158
 }
146 159
 
147 160
 // resolve the references to ensure they are all valid, and identify any images that don't match user input.
... ...
@@ -158,6 +184,10 @@ func (c *AppConfig) resolve(components app.ComponentReferences) error {
158 158
 				glog.Infof("Image %q is a builder, so a repository will be expected unless you also specify --build=docker", input)
159 159
 				input.ExpectToBuild = true
160 160
 			}
161
+		case input.ExpectToBuild && input.Match.IsTemplate():
162
+			// TODO: harder - break the template pieces and check if source code can be attached (look for a build config, build image, etc)
163
+			errs = append(errs, fmt.Errorf("template with source code explicitly attached is not supported - you must either specify the template and source code separately or attach an image to the source code using the '[image]~[code]' form"))
164
+			continue
161 165
 		case input.ExpectToBuild && !input.Match.Builder:
162 166
 			if len(c.TypeOfBuild) == 0 {
163 167
 				errs = append(errs, fmt.Errorf("none of the images that match %q can build source code - check whether this is the image you want to use, then use --build=source to build using source or --build=docker to treat this as a Docker base image and set up a layered Docker build", ref))
... ...
@@ -283,7 +313,9 @@ func (c *AppConfig) buildPipelines(components app.ComponentReferences, environme
283 283
 		glog.V(2).Infof("found group: %#v", group)
284 284
 		common := app.PipelineGroup{}
285 285
 		for _, ref := range group {
286
-
286
+			if !ref.Input().Match.IsImage() {
287
+				continue
288
+			}
287 289
 			var pipeline *app.Pipeline
288 290
 			if ref.Input().ExpectToBuild {
289 291
 				glog.V(2).Infof("will use %q as the base image for a source build of %q", ref, ref.Input().Uses)
... ...
@@ -298,7 +330,6 @@ func (c *AppConfig) buildPipelines(components app.ComponentReferences, environme
298 298
 				if pipeline, err = app.NewBuildPipeline(ref.Input().String(), input, strategy, source); err != nil {
299 299
 					return nil, fmt.Errorf("can't build %q: %v", ref.Input(), err)
300 300
 				}
301
-
302 301
 			} else {
303 302
 				glog.V(2).Infof("will include %q", ref)
304 303
 				input, err := app.InputImageFromMatch(ref.Input().Match)
... ...
@@ -309,7 +340,6 @@ func (c *AppConfig) buildPipelines(components app.ComponentReferences, environme
309 309
 					return nil, fmt.Errorf("can't include %q: %v", ref.Input(), err)
310 310
 				}
311 311
 			}
312
-
313 312
 			if err := pipeline.NeedsDeployment(environment); err != nil {
314 313
 				return nil, fmt.Errorf("can't set up a deployment for %q: %v", ref.Input(), err)
315 314
 			}
... ...
@@ -324,6 +354,39 @@ func (c *AppConfig) buildPipelines(components app.ComponentReferences, environme
324 324
 	return pipelines, nil
325 325
 }
326 326
 
327
+// buildTemplates converts a set of resolved, valid references into references to template objects.
328
+func (c *AppConfig) buildTemplates(components app.ComponentReferences, environment app.Environment) ([]runtime.Object, error) {
329
+	objects := []runtime.Object{}
330
+
331
+	for _, ref := range components {
332
+		if !ref.Input().Match.IsTemplate() {
333
+			continue
334
+		}
335
+
336
+		tpl := ref.Input().Match.Template
337
+
338
+		glog.V(4).Infof("processing template %s/%s", c.originNamespace, tpl.Name)
339
+		for _, env := range environment.List() {
340
+			// only set environment values that match what's expected by the template.
341
+			if v := template.GetParameterByName(tpl, env.Name); v != nil {
342
+				v.Value = env.Value
343
+				v.Generate = ""
344
+				template.AddParameter(tpl, *v)
345
+			} else {
346
+				return nil, fmt.Errorf("unexpected parameter name %q", env.Name)
347
+			}
348
+		}
349
+
350
+		result, err := c.osclient.TemplateConfigs(c.originNamespace).Create(tpl)
351
+		if err != nil {
352
+			return nil, fmt.Errorf("error processing template %s/%s: %v", c.originNamespace, tpl.Name, err)
353
+		}
354
+
355
+		objects = append(objects, result.Items...)
356
+	}
357
+	return objects, nil
358
+}
359
+
327 360
 // ErrNoInputs is returned when no inputs are specified
328 361
 var ErrNoInputs = fmt.Errorf("no inputs provided")
329 362
 
... ...
@@ -337,14 +400,15 @@ type AppResult struct {
337 337
 
338 338
 // Run executes the provided config.
339 339
 func (c *AppConfig) Run(out io.Writer) (*AppResult, error) {
340
-	components, repositories, environment, err := c.validate()
340
+	components, repositories, environment, parameters, err := c.validate()
341 341
 	if err != nil {
342 342
 		return nil, err
343 343
 	}
344 344
 
345 345
 	hasSource := len(repositories) != 0
346
-	hasImages := len(components) != 0
347
-	if !hasSource && !hasImages {
346
+	hasComponents := len(components) != 0
347
+
348
+	if !hasSource && !hasComponents {
348 349
 		return nil, ErrNoInputs
349 350
 	}
350 351
 
... ...
@@ -357,7 +421,7 @@ func (c *AppConfig) Run(out io.Writer) (*AppResult, error) {
357 357
 	}
358 358
 
359 359
 	glog.V(4).Infof("Code %v", repositories)
360
-	glog.V(4).Infof("Images %v", components)
360
+	glog.V(4).Infof("Components %v", components)
361 361
 
362 362
 	// TODO: Source detection needs to happen before components
363 363
 	//       are validated and resolved.
... ...
@@ -386,6 +450,12 @@ func (c *AppConfig) Run(out io.Writer) (*AppResult, error) {
386 386
 
387 387
 	objects = app.AddServices(objects)
388 388
 
389
+	templateObjects, err := c.buildTemplates(components, app.Environment(parameters))
390
+	if err != nil {
391
+		return nil, err
392
+	}
393
+	objects = append(objects, templateObjects...)
394
+
389 395
 	buildNames := []string{}
390 396
 	for _, obj := range objects {
391 397
 		switch t := obj.(type) {
... ...
@@ -1,16 +1,20 @@
1 1
 package cmd
2 2
 
3 3
 import (
4
+	"fmt"
4 5
 	"reflect"
5 6
 	"testing"
6 7
 
7 8
 	"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
9
+	"github.com/openshift/origin/pkg/client"
10
+	"github.com/openshift/origin/pkg/generate/app"
8 11
 )
9 12
 
10 13
 func TestAddArguments(t *testing.T) {
11 14
 	tests := map[string]struct {
12 15
 		args       []string
13 16
 		env        util.StringList
17
+		parms      util.StringList
14 18
 		repos      util.StringList
15 19
 		components util.StringList
16 20
 		unknown    []string
... ...
@@ -31,9 +35,9 @@ func TestAddArguments(t *testing.T) {
31 31
 			unknown: []string{},
32 32
 		},
33 33
 		"mix 1": {
34
-			args:       []string{"git://server/repo.git", "mysql+ruby~git@test.server/repo.git", "env1=test"},
34
+			args:       []string{"git://server/repo.git", "mysql+ruby~git@test.server/repo.git", "env1=test", "ruby-helloworld-sample"},
35 35
 			repos:      util.StringList{"git://server/repo.git"},
36
-			components: util.StringList{"mysql+ruby~git@test.server/repo.git"},
36
+			components: util.StringList{"mysql+ruby~git@test.server/repo.git", "ruby-helloworld-sample"},
37 37
 			env:        util.StringList{"env1=test"},
38 38
 			unknown:    []string{},
39 39
 		},
... ...
@@ -64,6 +68,7 @@ func TestValidate(t *testing.T) {
64 64
 		componentValues     []string
65 65
 		sourceRepoLocations []string
66 66
 		env                 map[string]string
67
+		parms               map[string]string
67 68
 	}{
68 69
 		"components": {
69 70
 			cfg: AppConfig{
... ...
@@ -72,6 +77,7 @@ func TestValidate(t *testing.T) {
72 72
 			componentValues:     []string{"one", "two", "three/four"},
73 73
 			sourceRepoLocations: []string{},
74 74
 			env:                 map[string]string{},
75
+			parms:               map[string]string{},
75 76
 		},
76 77
 		"sourcerepos": {
77 78
 			cfg: AppConfig{
... ...
@@ -80,6 +86,7 @@ func TestValidate(t *testing.T) {
80 80
 			componentValues:     []string{},
81 81
 			sourceRepoLocations: []string{".", "/test/var/src", "https://server/repo.git"},
82 82
 			env:                 map[string]string{},
83
+			parms:               map[string]string{},
83 84
 		},
84 85
 		"envs": {
85 86
 			cfg: AppConfig{
... ...
@@ -88,6 +95,7 @@ func TestValidate(t *testing.T) {
88 88
 			componentValues:     []string{},
89 89
 			sourceRepoLocations: []string{},
90 90
 			env:                 map[string]string{"one": "first", "two": "second", "three": "third"},
91
+			parms:               map[string]string{},
91 92
 		},
92 93
 		"component+source": {
93 94
 			cfg: AppConfig{
... ...
@@ -96,6 +104,7 @@ func TestValidate(t *testing.T) {
96 96
 			componentValues:     []string{"one"},
97 97
 			sourceRepoLocations: []string{"https://server/repo.git"},
98 98
 			env:                 map[string]string{},
99
+			parms:               map[string]string{},
99 100
 		},
100 101
 		"components+source": {
101 102
 			cfg: AppConfig{
... ...
@@ -104,15 +113,17 @@ func TestValidate(t *testing.T) {
104 104
 			componentValues:     []string{"mysql", "ruby"},
105 105
 			sourceRepoLocations: []string{"git://github.com/namespace/repo.git"},
106 106
 			env:                 map[string]string{},
107
+			parms:               map[string]string{},
107 108
 		},
108
-		"components+env": {
109
+		"components+parms": {
109 110
 			cfg: AppConfig{
110
-				Components:  util.StringList{"mysql+php"},
111
-				Environment: util.StringList{"one=first", "two=second"},
111
+				Components:         util.StringList{"ruby-helloworld-sample"},
112
+				TemplateParameters: util.StringList{"one=first", "two=second"},
112 113
 			},
113
-			componentValues:     []string{"mysql", "php"},
114
+			componentValues:     []string{"ruby-helloworld-sample"},
114 115
 			sourceRepoLocations: []string{},
115
-			env: map[string]string{
116
+			env:                 map[string]string{},
117
+			parms: map[string]string{
116 118
 				"one": "first",
117 119
 				"two": "second",
118 120
 			},
... ...
@@ -120,7 +131,7 @@ func TestValidate(t *testing.T) {
120 120
 	}
121 121
 
122 122
 	for n, c := range tests {
123
-		cr, repos, env, err := c.cfg.validate()
123
+		cr, repos, env, parms, err := c.cfg.validate()
124 124
 		if err != nil {
125 125
 			t.Errorf("%s: Unexpected error: %v", n, err)
126 126
 		}
... ...
@@ -147,5 +158,69 @@ func TestValidate(t *testing.T) {
147 147
 				break
148 148
 			}
149 149
 		}
150
+		if len(parms) != len(c.parms) {
151
+			t.Errorf("%s: Template parameters don't match. Expected: %v, Got: %v", n, c.parms, parms)
152
+		}
153
+		for p, v := range parms {
154
+			if c.parms[p] != v {
155
+				t.Errorf("%s: Template parameters don't match. Expected: %v, Got: %v", n, c.parms, parms)
156
+				break
157
+			}
158
+		}
159
+	}
160
+}
161
+
162
+func TestBuildTemplates(t *testing.T) {
163
+	tests := map[string]struct {
164
+		templateName string
165
+		namespace    string
166
+		parms        map[string]string
167
+	}{
168
+		"simple": {
169
+			templateName: "first-stored-template",
170
+			namespace:    "default",
171
+			parms:        map[string]string{},
172
+		},
173
+	}
174
+
175
+	for n, c := range tests {
176
+		appCfg := AppConfig{}
177
+		appCfg.SetOpenShiftClient(&client.Fake{}, c.namespace)
178
+		appCfg.AddArguments([]string{c.templateName})
179
+		appCfg.TemplateParameters = util.StringList{}
180
+		for k, v := range c.parms {
181
+			appCfg.TemplateParameters.Set(fmt.Sprintf("%v=%v", k, v))
182
+		}
183
+
184
+		components, _, _, parms, err := appCfg.validate()
185
+		if err != nil {
186
+			t.Errorf("%s: Unexpected error: %v", n, err)
187
+		}
188
+		err = appCfg.resolve(components)
189
+		if err != nil {
190
+			t.Errorf("%s: Unexpected error: %v", n, err)
191
+		}
192
+		_, err = appCfg.buildTemplates(components, app.Environment(parms))
193
+		if err != nil {
194
+			t.Errorf("%s: Unexpected error: %v", n, err)
195
+		}
196
+		for _, component := range components {
197
+			match := component.Input().Match
198
+			if !match.IsTemplate() {
199
+				t.Errorf("%s: Expected template match, got: %v", n, match)
200
+			}
201
+			if c.templateName != match.Name {
202
+				t.Errorf("%s: Expected template name %q, got: %q", n, c.templateName, match.Name)
203
+			}
204
+			if len(parms) != len(c.parms) {
205
+				t.Errorf("%s: Template parameters don't match. Expected: %v, Got: %v", n, c.parms, parms)
206
+			}
207
+			for p, v := range parms {
208
+				if c.parms[p] != v {
209
+					t.Errorf("%s: Template parameters don't match. Expected: %v, Got: %v", n, c.parms, parms)
210
+					break
211
+				}
212
+			}
213
+		}
150 214
 	}
151 215
 }
... ...
@@ -108,6 +108,14 @@ func (m *ComponentMatch) String() string {
108 108
 	return m.Argument
109 109
 }
110 110
 
111
+func (m *ComponentMatch) IsImage() bool {
112
+	return m.Image != nil || m.ImageStream != nil
113
+}
114
+
115
+func (m *ComponentMatch) IsTemplate() bool {
116
+	return m.Template != nil
117
+}
118
+
111 119
 type Resolver interface {
112 120
 	// resolvers should return ErrMultipleMatches when more than one result could
113 121
 	// be construed as a match. Resolvers should set the score to 0.0 if this is a
... ...
@@ -240,7 +248,7 @@ type ReferenceBuilder struct {
240 240
 	group int
241 241
 }
242 242
 
243
-func (r *ReferenceBuilder) AddImages(inputs []string, fn func(*ComponentInput) ComponentReference) {
243
+func (r *ReferenceBuilder) AddComponents(inputs []string, fn func(*ComponentInput) ComponentReference) {
244 244
 	for _, s := range inputs {
245 245
 		for _, s := range strings.Split(s, "+") {
246 246
 			input, repo, err := NewComponentInput(s)
... ...
@@ -12,9 +12,9 @@ type ErrNoMatch struct {
12 12
 
13 13
 func (e ErrNoMatch) Error() string {
14 14
 	if len(e.qualifier) != 0 {
15
-		return fmt.Sprintf("no image matched %q: %s", e.value, e.qualifier)
15
+		return fmt.Sprintf("no image or template matched %q: %s", e.value, e.qualifier)
16 16
 	}
17
-	return fmt.Sprintf("no image matched %q", e.value)
17
+	return fmt.Sprintf("no image or template matched %q", e.value)
18 18
 }
19 19
 
20 20
 func (e ErrNoMatch) UsageError(commandName string) string {
... ...
@@ -34,14 +34,14 @@ type ErrMultipleMatches struct {
34 34
 }
35 35
 
36 36
 func (e ErrMultipleMatches) Error() string {
37
-	return fmt.Sprintf("multiple images matched %q: %d", e.Image, len(e.Matches))
37
+	return fmt.Sprintf("multiple images or templates matched %q: %d", e.Image, len(e.Matches))
38 38
 }
39 39
 
40 40
 func (e ErrMultipleMatches) UsageError(commandName string) string {
41 41
 	buf := &bytes.Buffer{}
42 42
 	for _, match := range e.Matches {
43 43
 		fmt.Fprintf(buf, "* %s %f\n", match.Description, match.Score)
44
-		fmt.Fprintf(buf, "  Use %[1]s to specify this image\n\n", match.Argument)
44
+		fmt.Fprintf(buf, "  Use %[1]s to specify this image or template\n\n", match.Argument)
45 45
 	}
46 46
 	return fmt.Sprintf(`
47 47
 The argument %[1]q could apply to the following Docker images or OpenShift image repositories:
48 48
new file mode 100644
... ...
@@ -0,0 +1,47 @@
0
+package app
1
+
2
+import (
3
+	"fmt"
4
+
5
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors"
6
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
7
+	"github.com/golang/glog"
8
+	"github.com/openshift/origin/pkg/client"
9
+)
10
+
11
+type TemplateResolver struct {
12
+	Client                    client.TemplatesNamespacer
13
+	TemplateConfigsNamespacer client.TemplateConfigsNamespacer
14
+	Namespaces                []string
15
+}
16
+
17
+func (r TemplateResolver) Resolve(value string) (*ComponentMatch, error) {
18
+	checked := util.NewStringSet()
19
+
20
+	for _, namespace := range r.Namespaces {
21
+		if checked.Has(namespace) {
22
+			continue
23
+		}
24
+
25
+		checked.Insert(namespace)
26
+
27
+		glog.V(4).Infof("checking template %s/%s", namespace, value)
28
+		repo, err := r.Client.Templates(namespace).Get(value)
29
+		if err != nil {
30
+			if errors.IsNotFound(err) {
31
+				continue
32
+			}
33
+			return nil, err
34
+		}
35
+
36
+		return &ComponentMatch{
37
+			Value:       value,
38
+			Argument:    fmt.Sprintf("--template=%q", value),
39
+			Name:        value,
40
+			Description: fmt.Sprintf("Template %s in project %s", repo.Name, repo.Namespace),
41
+			Score:       0,
42
+			Template:    repo,
43
+		}, nil
44
+	}
45
+	return nil, ErrNoMatch{value: value}
46
+}