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