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