Browse code

Import app.json to OpenShift applications

Clayton Coleman authored on 2016/05/02 10:32:11
Showing 10 changed files
... ...
@@ -22499,7 +22499,7 @@
22499 22499
      },
22500 22500
      "generate": {
22501 22501
       "type": "string",
22502
-      "description": "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. Optional."
22502
+      "description": "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. Optional.\n\nThe only supported generator is \"expression\", which accepts a \"from\" value in the form of a simple regular expression containing the range expression \"[a-zA-Z0-9]\", and the length expression \"a{length}\".\n\nExamples:\n\nfrom             | value"
22503 22503
      },
22504 22504
      "from": {
22505 22505
       "type": "string",
... ...
@@ -8035,11 +8035,67 @@ _oc_import_docker-compose()
8035 8035
     must_have_one_noun=()
8036 8036
 }
8037 8037
 
8038
+_oc_import_app.json()
8039
+{
8040
+    last_command="oc_import_app.json"
8041
+    commands=()
8042
+
8043
+    flags=()
8044
+    two_word_flags=()
8045
+    flags_with_completion=()
8046
+    flags_completion=()
8047
+
8048
+    flags+=("--as-template=")
8049
+    flags+=("--dry-run")
8050
+    flags+=("--filename=")
8051
+    flags_with_completion+=("--filename")
8052
+    flags_completion+=("__handle_filename_extension_flag json|yaml|yml")
8053
+    two_word_flags+=("-f")
8054
+    flags_with_completion+=("-f")
8055
+    flags_completion+=("__handle_filename_extension_flag json|yaml|yml")
8056
+    flags+=("--generator=")
8057
+    flags+=("--image=")
8058
+    flags+=("--output=")
8059
+    two_word_flags+=("-o")
8060
+    flags+=("--output-version=")
8061
+    flags+=("--api-version=")
8062
+    flags+=("--as=")
8063
+    flags+=("--certificate-authority=")
8064
+    flags_with_completion+=("--certificate-authority")
8065
+    flags_completion+=("_filedir")
8066
+    flags+=("--client-certificate=")
8067
+    flags_with_completion+=("--client-certificate")
8068
+    flags_completion+=("_filedir")
8069
+    flags+=("--client-key=")
8070
+    flags_with_completion+=("--client-key")
8071
+    flags_completion+=("_filedir")
8072
+    flags+=("--cluster=")
8073
+    flags+=("--config=")
8074
+    flags_with_completion+=("--config")
8075
+    flags_completion+=("_filedir")
8076
+    flags+=("--context=")
8077
+    flags+=("--google-json-key=")
8078
+    flags+=("--insecure-skip-tls-verify")
8079
+    flags+=("--log-flush-frequency=")
8080
+    flags+=("--match-server-version")
8081
+    flags+=("--namespace=")
8082
+    two_word_flags+=("-n")
8083
+    flags+=("--server=")
8084
+    flags+=("--token=")
8085
+    flags+=("--user=")
8086
+
8087
+    must_have_one_flag=()
8088
+    must_have_one_flag+=("--filename=")
8089
+    must_have_one_flag+=("-f")
8090
+    must_have_one_noun=()
8091
+}
8092
+
8038 8093
 _oc_import()
8039 8094
 {
8040 8095
     last_command="oc_import"
8041 8096
     commands=()
8042 8097
     commands+=("docker-compose")
8098
+    commands+=("app.json")
8043 8099
 
8044 8100
     flags=()
8045 8101
     two_word_flags=()
... ...
@@ -11622,11 +11622,67 @@ _openshift_cli_import_docker-compose()
11622 11622
     must_have_one_noun=()
11623 11623
 }
11624 11624
 
11625
+_openshift_cli_import_app.json()
11626
+{
11627
+    last_command="openshift_cli_import_app.json"
11628
+    commands=()
11629
+
11630
+    flags=()
11631
+    two_word_flags=()
11632
+    flags_with_completion=()
11633
+    flags_completion=()
11634
+
11635
+    flags+=("--as-template=")
11636
+    flags+=("--dry-run")
11637
+    flags+=("--filename=")
11638
+    flags_with_completion+=("--filename")
11639
+    flags_completion+=("__handle_filename_extension_flag json|yaml|yml")
11640
+    two_word_flags+=("-f")
11641
+    flags_with_completion+=("-f")
11642
+    flags_completion+=("__handle_filename_extension_flag json|yaml|yml")
11643
+    flags+=("--generator=")
11644
+    flags+=("--image=")
11645
+    flags+=("--output=")
11646
+    two_word_flags+=("-o")
11647
+    flags+=("--output-version=")
11648
+    flags+=("--api-version=")
11649
+    flags+=("--as=")
11650
+    flags+=("--certificate-authority=")
11651
+    flags_with_completion+=("--certificate-authority")
11652
+    flags_completion+=("_filedir")
11653
+    flags+=("--client-certificate=")
11654
+    flags_with_completion+=("--client-certificate")
11655
+    flags_completion+=("_filedir")
11656
+    flags+=("--client-key=")
11657
+    flags_with_completion+=("--client-key")
11658
+    flags_completion+=("_filedir")
11659
+    flags+=("--cluster=")
11660
+    flags+=("--config=")
11661
+    flags_with_completion+=("--config")
11662
+    flags_completion+=("_filedir")
11663
+    flags+=("--context=")
11664
+    flags+=("--google-json-key=")
11665
+    flags+=("--insecure-skip-tls-verify")
11666
+    flags+=("--log-flush-frequency=")
11667
+    flags+=("--match-server-version")
11668
+    flags+=("--namespace=")
11669
+    two_word_flags+=("-n")
11670
+    flags+=("--server=")
11671
+    flags+=("--token=")
11672
+    flags+=("--user=")
11673
+
11674
+    must_have_one_flag=()
11675
+    must_have_one_flag+=("--filename=")
11676
+    must_have_one_flag+=("-f")
11677
+    must_have_one_noun=()
11678
+}
11679
+
11625 11680
 _openshift_cli_import()
11626 11681
 {
11627 11682
     last_command="openshift_cli_import"
11628 11683
     commands=()
11629 11684
     commands+=("docker-compose")
11685
+    commands+=("app.json")
11630 11686
 
11631 11687
     flags=()
11632 11688
     two_word_flags=()
... ...
@@ -1290,6 +1290,23 @@ Display one or many resources
1290 1290
 ====
1291 1291
 
1292 1292
 
1293
+== oc import app.json
1294
+Import an app.json definition into OpenShift
1295
+
1296
+====
1297
+
1298
+[options="nowrap"]
1299
+----
1300
+  # Import a directory containing an app.json file
1301
+  $ oc import app.json -f .
1302
+
1303
+  # Turn an app.json file into a template
1304
+  $ oc import app.json -f ./app.json -o yaml --as-template
1305
+
1306
+----
1307
+====
1308
+
1309
+
1293 1310
 == oc import docker-compose
1294 1311
 Import a docker-compose.yml project into OpenShift
1295 1312
 
1296 1313
new file mode 100644
... ...
@@ -0,0 +1,255 @@
0
+package importer
1
+
2
+import (
3
+	"fmt"
4
+	"io"
5
+	"io/ioutil"
6
+	"net/http"
7
+	"net/url"
8
+	"os"
9
+	"path"
10
+	"path/filepath"
11
+	"strings"
12
+
13
+	"github.com/spf13/cobra"
14
+
15
+	kapi "k8s.io/kubernetes/pkg/api"
16
+	"k8s.io/kubernetes/pkg/api/unversioned"
17
+	"k8s.io/kubernetes/pkg/apimachinery/registered"
18
+	"k8s.io/kubernetes/pkg/kubectl"
19
+	kcmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
20
+	"k8s.io/kubernetes/pkg/runtime"
21
+
22
+	"github.com/openshift/origin/pkg/client"
23
+	cmdutil "github.com/openshift/origin/pkg/cmd/util"
24
+	"github.com/openshift/origin/pkg/cmd/util/clientcmd"
25
+	configcmd "github.com/openshift/origin/pkg/config/cmd"
26
+	"github.com/openshift/origin/pkg/generate/app"
27
+	appcmd "github.com/openshift/origin/pkg/generate/app/cmd"
28
+	"github.com/openshift/origin/pkg/generate/appjson"
29
+)
30
+
31
+const (
32
+	appJSONLong = `
33
+Import app.json files as OpenShift objects
34
+
35
+app.json defines the pattern of a simple, stateless web application that can be horizontally scaled.
36
+This command will transform a provided app.json object into its OpenShift equivalent.
37
+During transformation fields in the app.json syntax that are not relevant when running on top of
38
+a containerized platform will be ignored and a warning printed.
39
+
40
+The command will create objects unless you pass the -o yaml or --as-template flags to generate a
41
+configuration file for later use.`
42
+
43
+	appJSONExample = `  # Import a directory containing an app.json file
44
+  $ %[1]s app.json -f .
45
+
46
+  # Turn an app.json file into a template
47
+  $ %[1]s app.json -f ./app.json -o yaml --as-template
48
+`
49
+
50
+	AppJSONV1GeneratorName = "app-json/v1"
51
+)
52
+
53
+type AppJSONOptions struct {
54
+	Action configcmd.BulkAction
55
+
56
+	In        io.Reader
57
+	Filenames []string
58
+
59
+	BaseImage  string
60
+	Generator  string
61
+	AsTemplate string
62
+
63
+	PrintObject    func(runtime.Object) error
64
+	OutputVersions []unversioned.GroupVersion
65
+
66
+	Namespace string
67
+	Client    client.TemplateConfigsNamespacer
68
+}
69
+
70
+// NewCmdAppJSON imports an app.json file (schema described here: https://devcenter.heroku.com/articles/app-json-schema)
71
+// as a template.
72
+func NewCmdAppJSON(fullName string, f *clientcmd.Factory, in io.Reader, out, errout io.Writer) *cobra.Command {
73
+	options := &AppJSONOptions{
74
+		Action: configcmd.BulkAction{
75
+			Out:    out,
76
+			ErrOut: errout,
77
+		},
78
+		In:        in,
79
+		Generator: AppJSONV1GeneratorName,
80
+	}
81
+	cmd := &cobra.Command{
82
+		Use:     "app.json -f APPJSON",
83
+		Short:   "Import an app.json definition into OpenShift",
84
+		Long:    appJSONLong,
85
+		Example: fmt.Sprintf(appJSONExample, fullName),
86
+		Run: func(cmd *cobra.Command, args []string) {
87
+			kcmdutil.CheckErr(options.Complete(f, cmd, args))
88
+			kcmdutil.CheckErr(options.Validate())
89
+			if err := options.Run(); err != nil {
90
+				// TODO: move met to kcmdutil
91
+				if err == cmdutil.ErrExit {
92
+					os.Exit(1)
93
+				}
94
+				kcmdutil.CheckErr(err)
95
+			}
96
+		},
97
+	}
98
+	usage := "Filename, directory, or URL to app.json file to use"
99
+	kubectl.AddJsonFilenameFlag(cmd, &options.Filenames, usage)
100
+	cmd.MarkFlagRequired("filename")
101
+
102
+	cmd.Flags().StringVar(&options.BaseImage, "image", options.BaseImage, "An optional image to use as your base Docker build (must have ONBUILD directives)")
103
+	cmd.Flags().String("generator", options.Generator, "The name of the generator strategy to use - specify this value to for backwards compatibility.")
104
+	cmd.Flags().StringVar(&options.AsTemplate, "as-template", "", "If set, generate a template with the provided name")
105
+
106
+	options.Action.BindForOutput(cmd.Flags())
107
+	cmd.Flags().String("output-version", "", "The preferred API versions of the output objects")
108
+
109
+	return cmd
110
+}
111
+
112
+func (o *AppJSONOptions) Complete(f *clientcmd.Factory, cmd *cobra.Command, args []string) error {
113
+	version, _ := cmd.Flags().GetString("output-version")
114
+	for _, v := range strings.Split(version, ",") {
115
+		gv, err := unversioned.ParseGroupVersion(v)
116
+		if err != nil {
117
+			return fmt.Errorf("provided output-version %q is not valid: %v", v, err)
118
+		}
119
+		o.OutputVersions = append(o.OutputVersions, gv)
120
+	}
121
+	o.OutputVersions = append(o.OutputVersions, registered.EnabledVersions()...)
122
+
123
+	o.Action.Bulk.Mapper = clientcmd.ResourceMapper(f)
124
+	o.Action.Bulk.Op = configcmd.Create
125
+	mapper, _ := f.Object(false)
126
+	o.PrintObject = cmdutil.VersionedPrintObject(f.PrintObject, cmd, mapper, o.Action.Out)
127
+
128
+	o.Generator, _ = cmd.Flags().GetString("generator")
129
+
130
+	ns, _, err := f.DefaultNamespace()
131
+	if err != nil {
132
+		return err
133
+	}
134
+	o.Namespace = ns
135
+
136
+	o.Client, _, err = f.Clients()
137
+	return err
138
+}
139
+
140
+func (o *AppJSONOptions) Validate() error {
141
+	if len(o.Filenames) != 1 {
142
+		return fmt.Errorf("you must provide the path to an app.json file or directory containing app.json")
143
+	}
144
+	switch o.Generator {
145
+	case AppJSONV1GeneratorName:
146
+	default:
147
+		return fmt.Errorf("the generator %q is not supported, use: %s", o.Generator, AppJSONV1GeneratorName)
148
+	}
149
+	return nil
150
+}
151
+
152
+func (o *AppJSONOptions) Run() error {
153
+	localPath, contents, err := contentsForPathOrURL(o.Filenames[0], o.In, "app.json")
154
+	if err != nil {
155
+		return err
156
+	}
157
+
158
+	g := &appjson.Generator{
159
+		LocalPath: localPath,
160
+		BaseImage: o.BaseImage,
161
+	}
162
+	switch {
163
+	case len(o.AsTemplate) > 0:
164
+		g.Name = o.AsTemplate
165
+	case len(localPath) > 0:
166
+		g.Name = filepath.Base(localPath)
167
+	default:
168
+		g.Name = path.Base(path.Dir(o.Filenames[0]))
169
+	}
170
+	if len(g.Name) == 0 {
171
+		g.Name = "app"
172
+	}
173
+
174
+	template, err := g.Generate(contents)
175
+	if err != nil {
176
+		return err
177
+	}
178
+
179
+	template.ObjectLabels = map[string]string{"app.json": template.Name}
180
+
181
+	// all the types generated into the template should be known
182
+	if errs := app.AsVersionedObjects(template.Objects, kapi.Scheme, kapi.Scheme, o.OutputVersions...); len(errs) > 0 {
183
+		for _, err := range errs {
184
+			fmt.Fprintf(o.Action.ErrOut, "error: %v\n", err)
185
+		}
186
+	}
187
+
188
+	if o.Action.ShouldPrint() || (o.Action.Output == "name" && len(o.AsTemplate) > 0) {
189
+		var out runtime.Object
190
+		if len(o.AsTemplate) > 0 {
191
+			template.Name = o.AsTemplate
192
+			out = template
193
+		} else {
194
+			out = &kapi.List{Items: template.Objects}
195
+		}
196
+		return o.PrintObject(out)
197
+	}
198
+
199
+	result, err := appcmd.TransformTemplate(template, o.Client, o.Namespace, nil)
200
+	if err != nil {
201
+		return err
202
+	}
203
+
204
+	if o.Action.Verbose() {
205
+		appcmd.DescribeGeneratedTemplate(o.Action.Out, "", result, o.Namespace)
206
+	}
207
+
208
+	if errs := o.Action.WithMessage("Importing app.json", "creating").Run(&kapi.List{Items: result.Objects}, o.Namespace); len(errs) > 0 {
209
+		return cmdutil.ErrExit
210
+	}
211
+	return nil
212
+}
213
+
214
+func contentsForPathOrURL(s string, in io.Reader, subpaths ...string) (string, []byte, error) {
215
+	switch {
216
+	case s == "-":
217
+		contents, err := ioutil.ReadAll(in)
218
+		return "", contents, err
219
+	case strings.Index(s, "http://") == 0 || strings.Index(s, "https://") == 0:
220
+		_, err := url.Parse(s)
221
+		if err != nil {
222
+			return "", nil, fmt.Errorf("the URL passed to filename %q is not valid: %v", s, err)
223
+		}
224
+		res, err := http.Get(s)
225
+		if err != nil {
226
+			return "", nil, err
227
+		}
228
+		defer res.Body.Close()
229
+		contents, err := ioutil.ReadAll(res.Body)
230
+		return "", contents, err
231
+	default:
232
+		stat, err := os.Stat(s)
233
+		if err != nil {
234
+			return s, nil, err
235
+		}
236
+		if !stat.IsDir() {
237
+			contents, err := ioutil.ReadFile(s)
238
+			return s, contents, err
239
+		}
240
+		for _, sub := range subpaths {
241
+			path := filepath.Join(s, sub)
242
+			stat, err := os.Stat(path)
243
+			if err != nil {
244
+				continue
245
+			}
246
+			if stat.IsDir() {
247
+				continue
248
+			}
249
+			contents, err := ioutil.ReadFile(s)
250
+			return path, contents, err
251
+		}
252
+		return s, nil, os.ErrNotExist
253
+	}
254
+}
... ...
@@ -29,5 +29,6 @@ func NewCmdImport(fullName string, f *clientcmd.Factory, in io.Reader, out, erro
29 29
 	name := fmt.Sprintf("%s import", fullName)
30 30
 
31 31
 	cmd.AddCommand(NewCmdDockerCompose(name, f, in, out, errout))
32
+	cmd.AddCommand(NewCmdAppJSON(name, f, in, out, errout))
32 33
 	return cmd
33 34
 }
... ...
@@ -276,13 +276,18 @@ func (r *BuildRef) BuildConfig() (*buildapi.BuildConfig, error) {
276 276
 	}, nil
277 277
 }
278 278
 
279
+type DeploymentHook struct {
280
+	Shell string
281
+}
282
+
279 283
 // DeploymentConfigRef is a reference to a deployment configuration
280 284
 type DeploymentConfigRef struct {
281
-	Name   string
282
-	Images []*ImageRef
283
-	Env    Environment
284
-	Labels map[string]string
285
-	AsTest bool
285
+	Name     string
286
+	Images   []*ImageRef
287
+	Env      Environment
288
+	Labels   map[string]string
289
+	AsTest   bool
290
+	PostHook *DeploymentHook
286 291
 }
287 292
 
288 293
 // DeploymentConfig creates a deploymentConfig resource from the deployment configuration reference
... ...
@@ -348,7 +353,7 @@ func (r *DeploymentConfigRef) DeploymentConfig() (*deployapi.DeploymentConfig, e
348 348
 		template.Containers[i].Env = append(template.Containers[i].Env, r.Env.List()...)
349 349
 	}
350 350
 
351
-	return &deployapi.DeploymentConfig{
351
+	dc := &deployapi.DeploymentConfig{
352 352
 		ObjectMeta: kapi.ObjectMeta{
353 353
 			Name: r.Name,
354 354
 		},
... ...
@@ -365,7 +370,21 @@ func (r *DeploymentConfigRef) DeploymentConfig() (*deployapi.DeploymentConfig, e
365 365
 			},
366 366
 			Triggers: triggers,
367 367
 		},
368
-	}, nil
368
+	}
369
+	if r.PostHook != nil {
370
+		//dc.Spec.Strategy.Type = "Rolling"
371
+		if len(r.PostHook.Shell) > 0 {
372
+			dc.Spec.Strategy.RecreateParams = &deployapi.RecreateDeploymentStrategyParams{
373
+				Post: &deployapi.LifecycleHook{
374
+					ExecNewPod: &deployapi.ExecNewPodHook{
375
+						Command: []string{"/bin/sh", "-c", r.PostHook.Shell},
376
+					},
377
+				},
378
+			}
379
+		}
380
+	}
381
+
382
+	return dc, nil
369 383
 }
370 384
 
371 385
 // GenerateSecret generates a random secret string
372 386
new file mode 100644
... ...
@@ -0,0 +1,412 @@
0
+package appjson
1
+
2
+import (
3
+	"encoding/json"
4
+	"fmt"
5
+	"path/filepath"
6
+	"strconv"
7
+	"strings"
8
+
9
+	"github.com/MakeNowJust/heredoc"
10
+	"github.com/golang/glog"
11
+
12
+	kapi "k8s.io/kubernetes/pkg/api"
13
+	"k8s.io/kubernetes/pkg/api/resource"
14
+	utilerrs "k8s.io/kubernetes/pkg/util/errors"
15
+	"k8s.io/kubernetes/pkg/util/sets"
16
+
17
+	deployapi "github.com/openshift/origin/pkg/deploy/api"
18
+	"github.com/openshift/origin/pkg/generate/app"
19
+	templateapi "github.com/openshift/origin/pkg/template/api"
20
+	"github.com/openshift/origin/pkg/util/docker/dockerfile"
21
+)
22
+
23
+type EnvVarOrString struct {
24
+	Value  string
25
+	EnvVar *EnvVar
26
+}
27
+
28
+type EnvVar struct {
29
+	Description string
30
+	Generator   string
31
+	Value       string
32
+	Required    bool
33
+	Default     interface{}
34
+}
35
+
36
+func (e *EnvVarOrString) UnmarshalJSON(data []byte) error {
37
+	if len(data) < 2 {
38
+		return nil
39
+	}
40
+	if data[0] == '"' {
41
+		e.Value = string(data[1 : len(data)-1])
42
+		return nil
43
+	}
44
+	e.EnvVar = &EnvVar{}
45
+	return json.Unmarshal(data, e.EnvVar)
46
+}
47
+
48
+type Formation struct {
49
+	Quantity int32
50
+	Size     string
51
+	Command  string
52
+}
53
+
54
+type Buildpack struct {
55
+	URL string `json:"url"`
56
+}
57
+
58
+type AppJSON struct {
59
+	Name        string
60
+	Description string
61
+	Keywords    []string
62
+	Repository  string
63
+	Website     string
64
+	Logo        string
65
+	SuccessURL  string `json:"success_url"`
66
+	Scripts     map[string]string
67
+	Env         map[string]EnvVarOrString
68
+	Formation   map[string]Formation
69
+	Image       string
70
+	Addons      []string
71
+	Buildpacks  []Buildpack
72
+}
73
+
74
+type Generator struct {
75
+	LocalPath string
76
+	Name      string
77
+	BaseImage string
78
+}
79
+
80
+// Generate accepts a path to an app.json file and generates a template from it
81
+func (g *Generator) Generate(body []byte) (*templateapi.Template, error) {
82
+	appJSON := &AppJSON{}
83
+	if err := json.Unmarshal(body, appJSON); err != nil {
84
+		return nil, err
85
+	}
86
+
87
+	glog.V(4).Infof("app.json: %#v", appJSON)
88
+
89
+	name := g.Name
90
+	if len(name) == 0 && len(g.LocalPath) > 0 {
91
+		name = filepath.Base(g.LocalPath)
92
+	}
93
+
94
+	template := &templateapi.Template{}
95
+	template.Name = name
96
+	template.Annotations = make(map[string]string)
97
+	template.Annotations["openshift.io/website"] = appJSON.Website
98
+	template.Annotations["k8s.io/display-name"] = appJSON.Name
99
+	template.Annotations["k8s.io/description"] = appJSON.Description
100
+	template.Annotations["tags"] = strings.Join(appJSON.Keywords, ",")
101
+	template.Annotations["iconURL"] = appJSON.Logo
102
+
103
+	// create parameters and environment for containers
104
+	allEnv := make(app.Environment)
105
+	for k, v := range appJSON.Env {
106
+		if v.EnvVar != nil {
107
+			allEnv[k] = fmt.Sprintf("${%s}", k)
108
+		}
109
+	}
110
+	envVars := allEnv.List()
111
+	for _, v := range envVars {
112
+		env := appJSON.Env[v.Name]
113
+		if env.EnvVar == nil {
114
+			continue
115
+		}
116
+		e := env.EnvVar
117
+		displayName := v.Name
118
+		displayName = strings.Join(strings.Split(strings.ToLower(displayName), "_"), " ")
119
+		displayName = strings.ToUpper(displayName[:1]) + displayName[1:]
120
+		param := templateapi.Parameter{
121
+			Name:        v.Name,
122
+			DisplayName: displayName,
123
+			Description: e.Description,
124
+			Value:       e.Value,
125
+		}
126
+		switch e.Generator {
127
+		case "secret":
128
+			param.Generate = "expression"
129
+			param.From = "[a-zA-Z0-9]{14}"
130
+		}
131
+		if len(param.Value) == 0 && e.Default != nil {
132
+			switch t := e.Default.(type) {
133
+			case string:
134
+				param.Value = t
135
+			case float64, float32:
136
+				out, _ := json.Marshal(t)
137
+				param.Value = string(out)
138
+			}
139
+		}
140
+		template.Parameters = append(template.Parameters, param)
141
+	}
142
+
143
+	warnings := make(map[string][]string)
144
+
145
+	if len(appJSON.Formation) == 0 {
146
+		glog.V(4).Infof("No formation in app.json, adding a default web")
147
+		// TODO: read Procfile for command?
148
+		appJSON.Formation = map[string]Formation{
149
+			"web": {
150
+				Quantity: 1,
151
+			},
152
+		}
153
+		msg := "adding a default formation 'web' with scale 1"
154
+		warnings[msg] = append(warnings[msg], "app.json")
155
+	}
156
+
157
+	formations := sets.NewString()
158
+	for k := range appJSON.Formation {
159
+		formations.Insert(k)
160
+	}
161
+
162
+	var primaryFormation = "web"
163
+	if _, ok := appJSON.Formation["web"]; !ok || len(appJSON.Formation) == 1 {
164
+		for k := range appJSON.Formation {
165
+			primaryFormation = k
166
+			break
167
+		}
168
+	}
169
+
170
+	imageGen := app.NewImageRefGenerator()
171
+
172
+	buildPath := appJSON.Repository
173
+	if len(buildPath) == 0 && len(g.LocalPath) > 0 {
174
+		buildPath = g.LocalPath
175
+	}
176
+	if len(buildPath) == 0 {
177
+		return nil, fmt.Errorf("app.json did not contain a repository URL and no local path was specified")
178
+	}
179
+
180
+	repo, err := app.NewSourceRepository(buildPath)
181
+	if err != nil {
182
+		return nil, err
183
+	}
184
+
185
+	var ports []string
186
+
187
+	var pipelines app.PipelineGroup
188
+	baseImage := g.BaseImage
189
+	if len(baseImage) == 0 {
190
+		baseImage = appJSON.Image
191
+	}
192
+	if len(baseImage) == 0 {
193
+		return nil, fmt.Errorf("Docker image required: provide an --image flag or 'image' key in app.json")
194
+	}
195
+
196
+	fakeDockerfile := heredoc.Docf(`
197
+      # Generated from app.json
198
+      FROM %s
199
+    `, baseImage)
200
+
201
+	dockerfilePath := filepath.Join(buildPath, "Dockerfile")
202
+	if df, err := app.NewDockerfileFromFile(dockerfilePath); err == nil {
203
+		repo.Info().Dockerfile = df
204
+		repo.Info().Path = dockerfilePath
205
+		ports = dockerfile.LastExposedPorts(df.AST())
206
+	}
207
+	// TODO: look for procfile for more info?
208
+
209
+	repo.BuildWithDocker()
210
+
211
+	image, err := imageGen.FromNameAndPorts(baseImage, ports)
212
+	if err != nil {
213
+		return nil, err
214
+	}
215
+	image.AsImageStream = true
216
+	image.TagDirectly = true
217
+	image.ObjectName = name
218
+	image.Tag = "from"
219
+
220
+	pipeline, err := app.NewPipelineBuilder(name, nil, false).To(name).NewBuildPipeline(name, image, repo)
221
+	if err != nil {
222
+		return nil, err
223
+	}
224
+
225
+	// TODO: this should not be necessary
226
+	pipeline.Build.Source.Name = name
227
+	pipeline.Build.Source.DockerfileContents = fakeDockerfile
228
+	pipeline.Name = name
229
+	pipeline.Image.ObjectName = name
230
+	glog.V(4).Infof("created pipeline %+v", pipeline)
231
+
232
+	pipelines = append(pipelines, pipeline)
233
+
234
+	var errs []error
235
+
236
+	// create deployments for each formation
237
+	var group app.PipelineGroup
238
+	for _, component := range formations.List() {
239
+		componentName := fmt.Sprintf("%s-%s", name, component)
240
+		if formations.Len() == 1 {
241
+			componentName = name
242
+		}
243
+		formationName := component
244
+		formation := appJSON.Formation[component]
245
+
246
+		inputImage := pipelines[0].Image
247
+
248
+		inputImage.ContainerFn = func(c *kapi.Container) {
249
+			for _, s := range ports {
250
+				if port, err := strconv.Atoi(s); err == nil {
251
+					c.Ports = append(c.Ports, kapi.ContainerPort{ContainerPort: port})
252
+				}
253
+			}
254
+			if len(formation.Command) > 0 {
255
+				c.Args = []string{formation.Command}
256
+			} else {
257
+				msg := "no command defined, defaulting to command in the Procfile"
258
+				warnings[msg] = append(warnings[msg], formationName)
259
+				c.Args = []string{"/bin/sh", "-c", fmt.Sprintf("$(grep %s Procfile | cut -f 2 -d :)", formationName)}
260
+			}
261
+			c.Env = append(c.Env, envVars...)
262
+
263
+			c.Resources = resourcesForProfile(formation.Size)
264
+		}
265
+
266
+		pipeline, err := app.NewPipelineBuilder(componentName, nil, true).To(componentName).NewImagePipeline(componentName, inputImage)
267
+		if err != nil {
268
+			errs = append(errs, err)
269
+			break
270
+		}
271
+
272
+		if err := pipeline.NeedsDeployment(nil, nil, false); err != nil {
273
+			return nil, err
274
+		}
275
+
276
+		if cmd, ok := appJSON.Scripts["postdeploy"]; ok && primaryFormation == component {
277
+			pipeline.Deployment.PostHook = &app.DeploymentHook{Shell: cmd}
278
+			delete(appJSON.Scripts, "postdeploy")
279
+		}
280
+
281
+		group = append(group, pipeline)
282
+	}
283
+	if err := group.Reduce(); err != nil {
284
+		return nil, err
285
+	}
286
+	pipelines = append(pipelines, group...)
287
+
288
+	if len(errs) > 0 {
289
+		return nil, utilerrs.NewAggregate(errs)
290
+	}
291
+
292
+	acceptors := app.Acceptors{app.NewAcceptUnique(kapi.Scheme), app.AcceptNew}
293
+	objects := app.Objects{}
294
+	accept := app.NewAcceptFirst()
295
+	for _, p := range pipelines {
296
+		accepted, err := p.Objects(accept, acceptors)
297
+		if err != nil {
298
+			return nil, fmt.Errorf("can't setup %q: %v", p.From, err)
299
+		}
300
+		objects = append(objects, accepted...)
301
+	}
302
+
303
+	// create services for each object with a name based on alias.
304
+	var services []*kapi.Service
305
+	for _, obj := range objects {
306
+		switch t := obj.(type) {
307
+		case *deployapi.DeploymentConfig:
308
+			ports := app.UniqueContainerToServicePorts(app.AllContainerPorts(t.Spec.Template.Spec.Containers...))
309
+			if len(ports) == 0 {
310
+				continue
311
+			}
312
+			svc := app.GenerateService(t.ObjectMeta, t.Spec.Selector)
313
+			svc.Spec.Ports = ports
314
+			services = append(services, svc)
315
+		}
316
+	}
317
+	for _, svc := range services {
318
+		objects = append(objects, svc)
319
+	}
320
+
321
+	template.Objects = objects
322
+
323
+	// generate warnings
324
+	warnUnusableAppJSONElements("app.json", appJSON, warnings)
325
+	if len(warnings) > 0 {
326
+		allWarnings := sets.NewString()
327
+		for msg, services := range warnings {
328
+			allWarnings.Insert(fmt.Sprintf("%s: %s", strings.Join(services, ","), msg))
329
+		}
330
+		if template.Annotations == nil {
331
+			template.Annotations = make(map[string]string)
332
+		}
333
+		template.Annotations[app.GenerationWarningAnnotation] = fmt.Sprintf("not all app.json fields were honored:\n* %s", strings.Join(allWarnings.List(), "\n* "))
334
+	}
335
+
336
+	return template, nil
337
+}
338
+
339
+// warnUnusableAppJSONElements add warnings for unsupported elements in the provided service config
340
+func warnUnusableAppJSONElements(k string, v *AppJSON, warnings map[string][]string) {
341
+	fn := func(msg string) {
342
+		warnings[msg] = append(warnings[msg], k)
343
+	}
344
+	if len(v.Buildpacks) > 0 {
345
+		fn("buildpacks are not handled")
346
+	}
347
+	for _, s := range v.Addons {
348
+		fn(fmt.Sprintf("addon %q is not supported and must be added separately", s))
349
+	}
350
+	if len(v.SuccessURL) > 0 {
351
+		fn("success_url is not handled")
352
+	}
353
+	for k, v := range v.Scripts {
354
+		fn(fmt.Sprintf("script directive %q for %q is not handled", v, k))
355
+	}
356
+}
357
+
358
+func checkForPorts(repo *app.SourceRepository) []string {
359
+	info := repo.Info()
360
+	if info == nil || info.Dockerfile == nil {
361
+		return nil
362
+	}
363
+	node := info.Dockerfile.AST()
364
+	return dockerfile.LastExposedPorts(node)
365
+}
366
+
367
+// resourcesForProfile takes standard Heroku sizes described here:
368
+// https://devcenter.heroku.com/articles/dyno-types#available-dyno-types and turns them into
369
+// Kubernetes resource requests.
370
+func resourcesForProfile(profile string) kapi.ResourceRequirements {
371
+	profile = strings.ToLower(profile)
372
+	switch profile {
373
+	case "standard-2x":
374
+		return kapi.ResourceRequirements{
375
+			Limits: kapi.ResourceList{
376
+				kapi.ResourceCPU:    resource.MustParse("200m"),
377
+				kapi.ResourceMemory: resource.MustParse("1Gi"),
378
+			},
379
+		}
380
+	case "performance-m":
381
+		return kapi.ResourceRequirements{
382
+			Requests: kapi.ResourceList{
383
+				kapi.ResourceCPU: resource.MustParse("500m"),
384
+			},
385
+			Limits: kapi.ResourceList{
386
+				kapi.ResourceCPU:    resource.MustParse("500m"),
387
+				kapi.ResourceMemory: resource.MustParse("2.5Gi"),
388
+			},
389
+		}
390
+	case "performance-l":
391
+		return kapi.ResourceRequirements{
392
+			Requests: kapi.ResourceList{
393
+				kapi.ResourceCPU:    resource.MustParse("1"),
394
+				kapi.ResourceMemory: resource.MustParse("2G"),
395
+			},
396
+			Limits: kapi.ResourceList{
397
+				kapi.ResourceCPU:    resource.MustParse("2"),
398
+				kapi.ResourceMemory: resource.MustParse("14Gi"),
399
+			},
400
+		}
401
+	case "free", "hobby", "standard":
402
+		fallthrough
403
+	default:
404
+		return kapi.ResourceRequirements{
405
+			Limits: kapi.ResourceList{
406
+				kapi.ResourceCPU:    resource.MustParse("100m"),
407
+				kapi.ResourceMemory: resource.MustParse("512Mi"),
408
+			},
409
+		}
410
+	}
411
+}
... ...
@@ -11,7 +11,7 @@ var map_Parameter = map[string]string{
11 11
 	"displayName": "Optional: The name that will show in UI instead of parameter 'Name'",
12 12
 	"description": "Description of a parameter. Optional.",
13 13
 	"value":       "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. Optional.",
14
-	"generate":    "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. Optional.",
14
+	"generate":    "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. Optional.\n\nThe only supported generator is \"expression\", which accepts a \"from\" value in the form of a simple regular expression containing the range expression \"[a-zA-Z0-9]\", and the length expression \"a{length}\".\n\nExamples:\n\nfrom             | value",
15 15
 	"from":        "From is an input value for the generator. Optional.",
16 16
 	"required":    "Optional: Indicates the parameter must have a value.  Defaults to false.",
17 17
 }
... ...
@@ -52,10 +52,24 @@ type Parameter struct {
52 52
 	// expression during the Template to Config transformation. Optional.
53 53
 	Value string `json:"value,omitempty"`
54 54
 
55
-	// Generate specifies the generator to be used to generate random string
55
+	// generate specifies the generator to be used to generate random string
56 56
 	// from an input value specified by From field. The result string is
57 57
 	// stored into Value field. If empty, no generator is being used, leaving
58 58
 	// the result Value untouched. Optional.
59
+	//
60
+	// The only supported generator is "expression", which accepts a "from"
61
+	// value in the form of a simple regular expression containing the
62
+	// range expression "[a-zA-Z0-9]", and the length expression "a{length}".
63
+	//
64
+	// Examples:
65
+	//
66
+	// from             | value
67
+	// -----------------------------
68
+	// "test[0-9]{1}x"  | "test7x"
69
+	// "[0-1]{8}"       | "01001100"
70
+	// "0x[A-F0-9]{4}"  | "0xB3AF"
71
+	// "[a-zA-Z0-9]{8}" | "hW4yQU5i"
72
+	//
59 73
 	Generate string `json:"generate,omitempty"`
60 74
 
61 75
 	// From is an input value for the generator. Optional.