Browse code

Add `osc env` command for setting and reading env

Allows osc users to easily add and remove environment variables
from pods, replication controllers, and deployment controllers.

Environment can be read from args, params, or STDIN. Environment
can be listed, modified in place and output, or modified in place
and sent to the server for update.

Adopts the initial "generation" pattern, which allows the input
to be decorated and then passed to a subsequent program for
further decoration.

Clayton Coleman authored on 2015/05/11 04:04:56
Showing 6 changed files
... ...
@@ -399,6 +399,19 @@ osc get deploymentConfigs
399 399
 osc get dc
400 400
 osc create -f test/integration/fixtures/test-deployment-config.json
401 401
 osc describe deploymentConfigs test-deployment-config
402
+[ "$(osc env dc/test-deployment-config --list | grep TEST=value)" ]
403
+[ ! "$(osc env dc/test-deployment-config TEST- --list | grep TEST=value)" ]
404
+[ "$(osc env dc/test-deployment-config TEST=foo --list | grep TEST=foo)" ]
405
+[ "$(osc env dc/test-deployment-config OTHER=foo --list | grep TEST=value)" ]
406
+[ ! "$(osc env dc/test-deployment-config OTHER=foo -c 'ruby' --list | grep OTHER=foo)" ]
407
+[ "$(osc env dc/test-deployment-config OTHER=foo -c 'ruby*'   --list | grep OTHER=foo)" ]
408
+[ "$(osc env dc/test-deployment-config OTHER=foo -c '*hello*' --list | grep OTHER=foo)" ]
409
+[ "$(osc env dc/test-deployment-config OTHER=foo -c '*world'  --list | grep OTHER=foo)" ]
410
+[ "$(osc env dc/test-deployment-config OTHER=foo --list | grep OTHER=foo)" ]
411
+[ "$(osc env dc/test-deployment-config OTHER=foo -o yaml | grep "name: OTHER")" ]
412
+[ "$(echo "OTHER=foo" | osc env dc/test-deployment-config -e - --list | grep OTHER=foo)" ]
413
+[ ! "$(echo "#OTHER=foo" | osc env dc/test-deployment-config -e - --list | grep OTHER=foo)" ]
414
+[ "$(osc env dc/test-deployment-config TEST=bar OTHER=baz BAR-)" ]
402 415
 osc deploy test-deployment-config
403 416
 osc delete deploymentConfigs test-deployment-config
404 417
 echo "deploymentConfigs: ok"
... ...
@@ -71,6 +71,7 @@ func NewCommandCLI(name, fullName string) *cobra.Command {
71 71
 	cmds.AddCommand(cmd.NewCmdBuildLogs(fullName, f, out))
72 72
 	cmds.AddCommand(cmd.NewCmdDeploy(fullName, f, out))
73 73
 	cmds.AddCommand(cmd.NewCmdRollback(fullName, f, out))
74
+	cmds.AddCommand(cmd.NewCmdEnv(fullName, f, os.Stdin, out))
74 75
 	cmds.AddCommand(cmd.NewCmdGet(fullName, f, out))
75 76
 	cmds.AddCommand(cmd.NewCmdDescribe(fullName, f, out))
76 77
 	// Deprecate 'osc apply' with 'osc create' command.
77 78
new file mode 100644
... ...
@@ -0,0 +1,419 @@
0
+/*
1
+Copyright 2014 The Kubernetes Authors All rights reserved.
2
+
3
+Licensed under the Apache License, Version 2.0 (the "License");
4
+you may not use this file except in compliance with the License.
5
+You may obtain a copy of the License at
6
+
7
+    http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+Unless required by applicable law or agreed to in writing, software
10
+distributed under the License is distributed on an "AS IS" BASIS,
11
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+See the License for the specific language governing permissions and
13
+limitations under the License.
14
+*/
15
+
16
+package cmd
17
+
18
+import (
19
+	"bufio"
20
+	"fmt"
21
+	"io"
22
+	"os"
23
+	"strings"
24
+
25
+	kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
26
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl"
27
+	cmdutil "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/util"
28
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/resource"
29
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
30
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
31
+	"github.com/spf13/cobra"
32
+
33
+	//ocutil "github.com/openshift/origin/pkg/cmd/util"
34
+	"github.com/openshift/origin/pkg/cmd/util/clientcmd"
35
+)
36
+
37
+const (
38
+	envLong = `Update the environment on a pod
39
+
40
+Each container in a pod may have its own environment variable definitions. This command can
41
+add, update, or remove environment variables from containers in pods or any object that has
42
+a pod template (replication controllers or deployment configurations). You can specify a
43
+single object or multiple, and alter environment on all containers or just those that match
44
+a wildcard.
45
+
46
+If "--env -" is passed, environment variables can be read from STDIN using the standard env
47
+syntax.`
48
+
49
+	envExample = `  // Update deployment 'registry' with a new environment variable
50
+  $ %[1]s env dc/registry STORAGE_DIR=/local
51
+
52
+  // List the environment variables defined on a deployment 'registry'
53
+  $ %[1]s env dc/registry --list
54
+
55
+  // List the environment variables defined on all pods
56
+  $ %[1]s env pods --all --list
57
+
58
+  // Output a YAML object with updated enviroment for deployment 'registry'
59
+  // Does not alter the object on the server
60
+  $ %[1]s env dc/registry STORAGE_DIR=/local -o yaml
61
+
62
+  // Update all replication controllers in the project to have ENV=prod
63
+  $ %[1]s env replicationControllers --all ENV=prod
64
+
65
+  // Remove the enviroment variable ENV from a pod definition on disk and update the pod on the server
66
+  $ %[1]s env -f pod.json ENV-
67
+
68
+  // Set some of the local shell environment into a deployment on the server
69
+  $ env | grep RAILS_ | %[1]s env -e - dc/registry`
70
+)
71
+
72
+func NewCmdEnv(fullName string, f *clientcmd.Factory, in io.Reader, out io.Writer) *cobra.Command {
73
+	var filenames util.StringList
74
+	var env util.StringList
75
+	cmd := &cobra.Command{
76
+		Use:     "env RESOURCE/NAME KEY_1=VAL_1 ... KEY_N=VAL_N [options]",
77
+		Short:   "Update the environment on a resource with a pod template",
78
+		Long:    envLong,
79
+		Example: fmt.Sprintf(envExample, fullName),
80
+		Run: func(cmd *cobra.Command, args []string) {
81
+			err := RunEnv(f, in, out, cmd, args, env, filenames)
82
+			if err == errExit {
83
+				os.Exit(1)
84
+			}
85
+			cmdutil.CheckErr(err)
86
+		},
87
+	}
88
+	cmd.Flags().StringP("containers", "c", "*", "The names of containers in the selected pod templates to change - may use wildcards")
89
+	cmd.Flags().VarP(&env, "env", "e", "Specify key value pairs of environment variables to set into each container.")
90
+	cmd.Flags().Bool("list", false, "Display the environment and any changes in the standard format")
91
+	cmd.Flags().StringP("selector", "l", "", "Selector (label query) to filter on")
92
+	cmd.Flags().Bool("all", false, "select all resources in the namespace of the specified resource types")
93
+	cmd.Flags().VarP(&filenames, "filename", "f", "Filename, directory, or URL to file to use to edit the resource.")
94
+	cmd.Flags().Bool("overwrite", true, "If true, allow environment to be overwritten, otherwise reject updates that overwrite existing environment.")
95
+	cmd.Flags().String("resource-version", "", "If non-empty, the labels update will only succeed if this is the current resource-version for the object. Only valid when specifying a single resource.")
96
+	cmd.Flags().StringP("output", "o", "", "Display the changed objects instead of updating them. One of: json|yaml.")
97
+	cmd.Flags().String("output-version", "", "Output the changed objects with the given version (default api-version).")
98
+	return cmd
99
+}
100
+
101
+func updateObject(info *resource.Info, updateFn func(runtime.Object) (runtime.Object, error)) (runtime.Object, error) {
102
+	helper := resource.NewHelper(info.Client, info.Mapping)
103
+
104
+	obj, err := updateFn(info.Object)
105
+	if err != nil {
106
+		return nil, err
107
+	}
108
+	data, err := helper.Codec.Encode(obj)
109
+	if err != nil {
110
+		return nil, err
111
+	}
112
+
113
+	_, err = helper.Update(info.Namespace, info.Name, true, data)
114
+	if err != nil {
115
+		return nil, err
116
+	}
117
+	return obj, nil
118
+}
119
+
120
+func validateNoOverwrites(meta *kapi.ObjectMeta, labels map[string]string) error {
121
+	for key := range labels {
122
+		if value, found := meta.Labels[key]; found {
123
+			return fmt.Errorf("'%s' already has a value (%s), and --overwrite is false", key, value)
124
+		}
125
+	}
126
+	return nil
127
+}
128
+
129
+func parseEnv(spec []string, defaultReader io.Reader) ([]kapi.EnvVar, []string, error) {
130
+	env := []kapi.EnvVar{}
131
+	exists := util.NewStringSet()
132
+	var remove []string
133
+	for _, envSpec := range spec {
134
+		switch {
135
+		case envSpec == "-":
136
+			if defaultReader == nil {
137
+				return nil, nil, fmt.Errorf("when '-' is used, STDIN must be open")
138
+			}
139
+			fileEnv, err := readEnv(defaultReader)
140
+			if err != nil {
141
+				return nil, nil, err
142
+			}
143
+			env = append(env, fileEnv...)
144
+		case strings.Index(envSpec, "=") != -1:
145
+			parts := strings.SplitN(envSpec, "=", 2)
146
+			if len(parts) != 2 {
147
+				return nil, nil, fmt.Errorf("invalid environment variable: %v", envSpec)
148
+			}
149
+			exists.Insert(parts[0])
150
+			env = append(env, kapi.EnvVar{
151
+				Name:  parts[0],
152
+				Value: parts[1],
153
+			})
154
+		case strings.HasSuffix(envSpec, "-"):
155
+			remove = append(remove, envSpec[:len(envSpec)-1])
156
+		default:
157
+			return nil, nil, fmt.Errorf("unknown environment variable: %v", envSpec)
158
+		}
159
+	}
160
+	for _, removeLabel := range remove {
161
+		if _, found := exists[removeLabel]; found {
162
+			return nil, nil, fmt.Errorf("can not both modify and remove an environment variable in the same command")
163
+		}
164
+	}
165
+	return env, remove, nil
166
+}
167
+
168
+func readEnv(r io.Reader) ([]kapi.EnvVar, error) {
169
+	env := []kapi.EnvVar{}
170
+	scanner := bufio.NewScanner(r)
171
+	for scanner.Scan() {
172
+		envSpec := scanner.Text()
173
+		if pos := strings.Index(envSpec, "#"); pos != -1 {
174
+			envSpec = envSpec[:pos]
175
+		}
176
+		if strings.Index(envSpec, "=") != -1 {
177
+			parts := strings.SplitN(envSpec, "=", 2)
178
+			if len(parts) != 2 {
179
+				return nil, fmt.Errorf("invalid environment variable: %v", envSpec)
180
+			}
181
+			env = append(env, kapi.EnvVar{
182
+				Name:  parts[0],
183
+				Value: parts[1],
184
+			})
185
+		}
186
+	}
187
+	if err := scanner.Err(); err != nil && err != io.EOF {
188
+		return nil, err
189
+	}
190
+	return env, nil
191
+}
192
+
193
+func RunEnv(f *clientcmd.Factory, in io.Reader, out io.Writer, cmd *cobra.Command, args []string, envParams, filenames util.StringList) error {
194
+	resources, envArgs := []string{}, []string{}
195
+	first := true
196
+	for _, s := range args {
197
+		isEnv := strings.Contains(s, "=") || strings.HasSuffix(s, "-")
198
+		switch {
199
+		case first && isEnv:
200
+			first = false
201
+			fallthrough
202
+		case !first && isEnv:
203
+			envArgs = append(envArgs, s)
204
+		case first && !isEnv:
205
+			resources = append(resources, s)
206
+		case !first && !isEnv:
207
+			return cmdutil.UsageError(cmd, "all resources must be specified before environment changes: %s", s)
208
+		}
209
+	}
210
+	if len(resources) < 1 {
211
+		return cmdutil.UsageError(cmd, "one or more resources must be specified as <resource> <name> or <resource>/<name>")
212
+	}
213
+
214
+	containerMatch := cmdutil.GetFlagString(cmd, "containers")
215
+	list := cmdutil.GetFlagBool(cmd, "list")
216
+	selector := cmdutil.GetFlagString(cmd, "selector")
217
+	all := cmdutil.GetFlagBool(cmd, "all")
218
+	//overwrite := cmdutil.GetFlagBool(cmd, "overwrite")
219
+	resourceVersion := cmdutil.GetFlagString(cmd, "resource-version")
220
+	outputFormat := cmdutil.GetFlagString(cmd, "output")
221
+
222
+	if list && len(outputFormat) > 0 {
223
+		return cmdutil.UsageError(cmd, "--list and --output may not be specified together")
224
+	}
225
+
226
+	clientConfig, err := f.ClientConfig()
227
+	if err != nil {
228
+		return err
229
+	}
230
+	outputVersion := cmdutil.OutputVersion(cmd, clientConfig.Version)
231
+
232
+	cmdNamespace, err := f.DefaultNamespace()
233
+	if err != nil {
234
+		return err
235
+	}
236
+
237
+	env, remove, err := parseEnv(append(envParams, envArgs...), in)
238
+	if err != nil {
239
+		return err
240
+	}
241
+
242
+	mapper, typer := f.Object()
243
+	b := resource.NewBuilder(mapper, typer, f.ClientMapperForCommand()).
244
+		ContinueOnError().
245
+		NamespaceParam(cmdNamespace).DefaultNamespace().
246
+		FilenameParam(filenames...).
247
+		SelectorParam(selector).
248
+		ResourceTypeOrNameArgs(all, resources...).
249
+		Flatten()
250
+
251
+	one := false
252
+	infos, err := b.Do().IntoSingular(&one).Infos()
253
+	if err != nil {
254
+		return err
255
+	}
256
+	// only apply resource version locking on a single resource
257
+	if !one && len(resourceVersion) > 0 {
258
+		return cmdutil.UsageError(cmd, "--resource-version may only be used with a single resource")
259
+	}
260
+
261
+	skipped := 0
262
+	for _, info := range infos {
263
+		ok, err := f.UpdatePodSpecForObject(info.Object, func(spec *kapi.PodSpec) error {
264
+			containers := selectContainers(spec.Containers, containerMatch)
265
+			if len(containers) == 0 {
266
+				fmt.Fprintf(cmd.Out(), "warning: %s/%s does not have any containers matching %q\n", info.Mapping.Resource, info.Name, containerMatch)
267
+				return nil
268
+			}
269
+			for _, c := range containers {
270
+				c.Env = updateEnv(c.Env, env, remove)
271
+
272
+				if list {
273
+					fmt.Fprintf(out, "# %s %s, container %s\n", info.Mapping.Resource, info.Name, c.Name)
274
+					for _, env := range c.Env {
275
+						// if env.ValueFrom != nil && env.ValueFrom.FieldRef != nil {
276
+						// 	fmt.Fprintf(cmd.Out(), "%s= # calculated from pod %s %s\n", env.Name, env.ValueFrom.FieldRef.FieldPath, env.ValueFrom.FieldRef.APIVersion)
277
+						// 	continue
278
+						// }
279
+						fmt.Fprintf(out, "%s=%s\n", env.Name, env.Value)
280
+
281
+					}
282
+				}
283
+			}
284
+			return nil
285
+		})
286
+		if !ok {
287
+			skipped++
288
+			continue
289
+		}
290
+		if err != nil {
291
+			fmt.Fprintf(cmd.Out(), "error: %s/%s %v\n", info.Mapping.Resource, info.Name, err)
292
+			continue
293
+		}
294
+	}
295
+	if one && skipped == len(infos) {
296
+		return fmt.Errorf("the %s %s is not a pod or does not have a pod template", infos[0].Mapping.Resource, infos[0].Name)
297
+	}
298
+
299
+	if list {
300
+		return nil
301
+	}
302
+
303
+	objects, err := resource.AsVersionedObject(infos, false, outputVersion)
304
+	if err != nil {
305
+		return err
306
+	}
307
+
308
+	if len(outputFormat) != 0 {
309
+		p, _, err := kubectl.GetPrinter(outputFormat, "")
310
+		if err != nil {
311
+			return err
312
+		}
313
+		return p.PrintObj(objects, out)
314
+	}
315
+
316
+	failed := false
317
+	for _, info := range infos {
318
+		data, err := info.Mapping.Codec.Encode(info.Object)
319
+		if err != nil {
320
+			fmt.Fprintf(cmd.Out(), "Error: %v\n", err)
321
+			failed = true
322
+			continue
323
+		}
324
+		obj, err := resource.NewHelper(info.Client, info.Mapping).Update(info.Namespace, info.Name, true, data)
325
+		if err != nil {
326
+			fmt.Fprintf(cmd.Out(), "Error: %v\n", err)
327
+			failed = true
328
+			continue
329
+		}
330
+		info.Refresh(obj, true)
331
+		fmt.Fprintf(out, "%s/%s\n", info.Mapping.Resource, info.Name)
332
+	}
333
+	if failed {
334
+		return errExit
335
+	}
336
+	return nil
337
+}
338
+
339
+func updateEnv(existing []kapi.EnvVar, env []kapi.EnvVar, remove []string) []kapi.EnvVar {
340
+	out := []kapi.EnvVar{}
341
+	covered := util.NewStringSet(remove...)
342
+	for _, e := range existing {
343
+		if covered.Has(e.Name) {
344
+			continue
345
+		}
346
+		newer, ok := findEnv(env, e.Name)
347
+		if ok {
348
+			covered.Insert(e.Name)
349
+			out = append(out, newer)
350
+			continue
351
+		}
352
+		out = append(out, e)
353
+	}
354
+	for _, e := range env {
355
+		if covered.Has(e.Name) {
356
+			continue
357
+		}
358
+		covered.Insert(e.Name)
359
+		out = append(out, e)
360
+	}
361
+	return out
362
+}
363
+
364
+func findEnv(env []kapi.EnvVar, name string) (kapi.EnvVar, bool) {
365
+	for _, e := range env {
366
+		if e.Name == name {
367
+			return e, true
368
+		}
369
+	}
370
+	return kapi.EnvVar{}, false
371
+}
372
+
373
+func selectContainers(containers []kapi.Container, spec string) []*kapi.Container {
374
+	out := []*kapi.Container{}
375
+	for i, c := range containers {
376
+		if selectString(c.Name, spec) {
377
+			out = append(out, &containers[i])
378
+		}
379
+	}
380
+	return out
381
+}
382
+
383
+// selectString returns true if the provided string matches spec, where spec is a string with
384
+// a non-greedy '*' wildcard operator.
385
+// TODO: turn into a regex and handle greedy matches and backtracking.
386
+func selectString(s, spec string) bool {
387
+	if spec == "*" {
388
+		return true
389
+	}
390
+	if !strings.Contains(spec, "*") {
391
+		return s == spec
392
+	}
393
+
394
+	pos := 0
395
+	match := true
396
+	parts := strings.Split(spec, "*")
397
+	for i, part := range parts {
398
+		if len(part) == 0 {
399
+			continue
400
+		}
401
+		next := strings.Index(s[pos:], part)
402
+		switch {
403
+		// next part not in string
404
+		case next < pos:
405
+			fallthrough
406
+		// first part does not match start of string
407
+		case i == 0 && pos != 0:
408
+			fallthrough
409
+		// last part does not exactly match remaining part of string
410
+		case i == (len(parts)-1) && len(s) != (len(part)+next):
411
+			match = false
412
+			break
413
+		default:
414
+			pos = next
415
+		}
416
+	}
417
+	return match
418
+}
... ...
@@ -15,6 +15,7 @@ import (
15 15
 	"github.com/openshift/origin/pkg/api/latest"
16 16
 	"github.com/openshift/origin/pkg/client"
17 17
 	"github.com/openshift/origin/pkg/cmd/cli/describe"
18
+	deployapi "github.com/openshift/origin/pkg/deploy/api"
18 19
 
19 20
 	"github.com/spf13/pflag"
20 21
 )
... ...
@@ -108,6 +109,36 @@ func NewFactory(clientConfig kclientcmd.ClientConfig) *Factory {
108 108
 	return w
109 109
 }
110 110
 
111
+// TODO: move to upstream
112
+func (f *Factory) UpdatePodSpecForObject(obj runtime.Object, fn func(*api.PodSpec) error) (bool, error) {
113
+	// TODO: replace with a swagger schema based approach (identify pod template via schema introspection)
114
+	switch t := obj.(type) {
115
+	case *api.Pod:
116
+		return true, fn(&t.Spec)
117
+	case *api.PodTemplate:
118
+		return true, fn(&t.Template.Spec)
119
+	case *api.ReplicationController:
120
+		if t.Spec.TemplateRef != nil {
121
+			return true, fmt.Errorf("references to pod templates (%s/%s) cannot be updated", t.Spec.TemplateRef.Namespace, t.Spec.TemplateRef.Name)
122
+		}
123
+		if t.Spec.Template == nil {
124
+			t.Spec.Template = &api.PodTemplateSpec{}
125
+		}
126
+		return true, fn(&t.Spec.Template.Spec)
127
+	case *deployapi.DeploymentConfig:
128
+		template := t.Template.ControllerTemplate
129
+		if template.TemplateRef != nil {
130
+			return true, fmt.Errorf("deployment configs with references to pod templates (%s/%s) cannot be updated", template.TemplateRef.Namespace, template.TemplateRef.Name)
131
+		}
132
+		if template.Template == nil {
133
+			template.Template = &api.PodTemplateSpec{}
134
+		}
135
+		return true, fn(&template.Template.Spec)
136
+	default:
137
+		return false, fmt.Errorf("the object is not a pod or does not have a pod template")
138
+	}
139
+}
140
+
111 141
 // Clients returns an OpenShift and Kubernetes client.
112 142
 func (f *Factory) Clients() (*client.Client, *kclient.Client, error) {
113 143
 	kClient, err := f.Client()
... ...
@@ -35,7 +35,7 @@ func GetEnv(key string) (string, bool) {
35 35
 
36 36
 type Environment map[string]string
37 37
 
38
-var argumentEnvironment = regexp.MustCompile("^([\\w\\-]+)\\=(.*)$")
38
+var argumentEnvironment = regexp.MustCompile("^([\\w\\-_]+)\\=(.*)$")
39 39
 
40 40
 func IsEnvironmentArgument(s string) bool {
41 41
 	return argumentEnvironment.MatchString(s)
... ...
@@ -29,6 +29,9 @@
29 29
                   {
30 30
                     "containerPort": 8080
31 31
                   }
32
+                ],
33
+                "env": [
34
+                  {"name":"TEST","value":"value"}
32 35
                 ]
33 36
               }
34 37
             ]