From 84e94e39cd958b9ae9925e75170b370e4ec88e9b Mon Sep 17 00:00:00 2001 From: feihujiang Date: Tue, 1 Sep 2015 11:03:29 +0800 Subject: [PATCH] Support setting env vars in kubectl run --- contrib/completions/bash/kubectl | 1 + docs/man/man1/kubectl-run.1 | 10 +++ docs/user-guide/docker-cli-to-kubectl.md | 4 +- docs/user-guide/kubectl/kubectl_run.md | 11 ++- pkg/kubectl/cmd/run.go | 12 +++- pkg/kubectl/cmd/run_test.go | 19 ++++++ pkg/kubectl/run.go | 59 ++++++++++++++++ pkg/kubectl/run_test.go | 87 ++++++++++++++++++++++++ 8 files changed, 198 insertions(+), 5 deletions(-) diff --git a/contrib/completions/bash/kubectl b/contrib/completions/bash/kubectl index 34bd8d3c7a..0413c2a526 100644 --- a/contrib/completions/bash/kubectl +++ b/contrib/completions/bash/kubectl @@ -681,6 +681,7 @@ _kubectl_run() flags+=("--attach") flags+=("--command") flags+=("--dry-run") + flags+=("--env=") flags+=("--generator=") flags+=("--hostport=") flags+=("--image=") diff --git a/docs/man/man1/kubectl-run.1 b/docs/man/man1/kubectl-run.1 index c332c0adfd..1ba98e3898 100644 --- a/docs/man/man1/kubectl-run.1 +++ b/docs/man/man1/kubectl-run.1 @@ -30,6 +30,10 @@ Creates a replication controller to manage the created container(s). \fB\-\-dry\-run\fP=false If true, only print the object that would be sent, without sending it. +.PP +\fB\-\-env\fP=[] + Environment variables to set in the container + .PP \fB\-\-generator\fP="" The name of the API generator to use. Default is 'run/v1' if \-\-restart=Always, otherwise the default is 'run\-pod/v1'. @@ -200,6 +204,12 @@ Creates a replication controller to manage the created container(s). # Start a single instance of nginx. $ kubectl run nginx \-\-image=nginx +# Start a single instance of hazelcast and let the container expose port 5701 . +$ kubectl run hazelcast \-\-image=hazelcast \-\-port=5701 + +# Start a single instance of hazelcast and set environment variables "DNS\_DOMAIN=cluster" and "POD\_NAMESPACE=default" in the container. +$ kubectl run hazelcast \-\-image=hazelcast \-\-env="DNS\_DOMAIN=local" \-\-env="POD\_NAMESPACE=default" + # Start a replicated instance of nginx. $ kubectl run nginx \-\-image=nginx \-\-replicas=5 diff --git a/docs/user-guide/docker-cli-to-kubectl.md b/docs/user-guide/docker-cli-to-kubectl.md index ad9feb29e1..77152a9b1f 100644 --- a/docs/user-guide/docker-cli-to-kubectl.md +++ b/docs/user-guide/docker-cli-to-kubectl.md @@ -58,7 +58,7 @@ How do I run an nginx container and expose it to the world? Checkout [kubectl ru With docker: ```console -$ docker run -d --restart=always --name nginx-app -p 80:80 nginx +$ docker run -d --restart=always -e DOMAIN=cluster --name nginx-app -p 80:80 nginx a9ec34d9878748d2f33dc20cb25c714ff21da8d40558b45bfaec9955859075d0 $ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES @@ -69,7 +69,7 @@ With kubectl: ```console # start the pod running nginx -$ kubectl run --image=nginx nginx-app +$ kubectl run --image=nginx nginx-app --port=80 --env="DOMAIN=local" CONTROLLER CONTAINER(S) IMAGE(S) SELECTOR REPLICAS nginx-app nginx-app nginx run=nginx-app 1 # expose a port through with a service diff --git a/docs/user-guide/kubectl/kubectl_run.md b/docs/user-guide/kubectl/kubectl_run.md index b9a664dd75..c664afd417 100644 --- a/docs/user-guide/kubectl/kubectl_run.md +++ b/docs/user-guide/kubectl/kubectl_run.md @@ -42,7 +42,7 @@ Create and run a particular image, possibly replicated. Creates a replication controller to manage the created container(s). ``` -kubectl run NAME --image=image [--port=port] [--replicas=replicas] [--dry-run=bool] [--overrides=inline-json] +kubectl run NAME --image=image [--env="key=value"] [--port=port] [--replicas=replicas] [--dry-run=bool] [--overrides=inline-json] ``` ### Examples @@ -51,6 +51,12 @@ kubectl run NAME --image=image [--port=port] [--replicas=replicas] [--dry-run=bo # Start a single instance of nginx. $ kubectl run nginx --image=nginx +# Start a single instance of hazelcast and let the container expose port 5701 . +$ kubectl run hazelcast --image=hazelcast --port=5701 + +# Start a single instance of hazelcast and set environment variables "DNS_DOMAIN=cluster" and "POD_NAMESPACE=default" in the container. +$ kubectl run hazelcast --image=hazelcast --env="DNS_DOMAIN=local" --env="POD_NAMESPACE=default" + # Start a replicated instance of nginx. $ kubectl run nginx --image=nginx --replicas=5 @@ -76,6 +82,7 @@ $ kubectl run nginx --image=nginx --command -- ... --attach[=false]: If true, wait for the Pod to start running, and then attach to the Pod as if 'kubectl attach ...' were called. Default false, unless '-i/--interactive' is set, in which case the default is true. --command[=false]: If true and extra arguments are present, use them as the 'command' field in the container, rather than the 'args' field which is the default. --dry-run[=false]: If true, only print the object that would be sent, without sending it. + --env=[]: Environment variables to set in the container --generator="": The name of the API generator to use. Default is 'run/v1' if --restart=Always, otherwise the default is 'run-pod/v1'. --hostport=-1: The host port mapping for the container port. To demonstrate a single-machine container. --image="": The image for the container to run. @@ -126,7 +133,7 @@ $ kubectl run nginx --image=nginx --command -- ... * [kubectl](kubectl.md) - kubectl controls the Kubernetes cluster manager -###### Auto generated by spf13/cobra at 2015-08-29 13:01:26.772003236 +0000 UTC +###### Auto generated by spf13/cobra at 2015-09-07 06:40:12.142439604 +0000 UTC [![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/docs/user-guide/kubectl/kubectl_run.md?pixel)]() diff --git a/pkg/kubectl/cmd/run.go b/pkg/kubectl/cmd/run.go index b02931a22e..5d2730967e 100644 --- a/pkg/kubectl/cmd/run.go +++ b/pkg/kubectl/cmd/run.go @@ -38,6 +38,12 @@ Creates a replication controller to manage the created container(s).` run_example = `# Start a single instance of nginx. $ kubectl run nginx --image=nginx +# Start a single instance of hazelcast and let the container expose port 5701 . +$ kubectl run hazelcast --image=hazelcast --port=5701 + +# Start a single instance of hazelcast and set environment variables "DNS_DOMAIN=cluster" and "POD_NAMESPACE=default" in the container. +$ kubectl run hazelcast --image=hazelcast --env="DNS_DOMAIN=local" --env="POD_NAMESPACE=default" + # Start a replicated instance of nginx. $ kubectl run nginx --image=nginx --replicas=5 @@ -59,7 +65,7 @@ $ kubectl run nginx --image=nginx --command -- ... ` func NewCmdRun(f *cmdutil.Factory, cmdIn io.Reader, cmdOut, cmdErr io.Writer) *cobra.Command { cmd := &cobra.Command{ - Use: "run NAME --image=image [--port=port] [--replicas=replicas] [--dry-run=bool] [--overrides=inline-json]", + Use: "run NAME --image=image [--env=\"key=value\"] [--port=port] [--replicas=replicas] [--dry-run=bool] [--overrides=inline-json]", // run-container is deprecated Aliases: []string{"run-container"}, Short: "Run a particular image on the cluster.", @@ -77,6 +83,7 @@ func NewCmdRun(f *cmdutil.Factory, cmdIn io.Reader, cmdOut, cmdErr io.Writer) *c cmd.Flags().IntP("replicas", "r", 1, "Number of replicas to create for this container. Default is 1.") cmd.Flags().Bool("dry-run", false, "If true, only print the object that would be sent, without sending it.") cmd.Flags().String("overrides", "", "An inline JSON override for the generated object. If this is non-empty, it is used to override the generated object. Requires that the object supply a valid apiVersion field.") + cmd.Flags().StringSlice("env", []string{}, "Environment variables to set in the container") cmd.Flags().Int("port", -1, "The port that this container exposes.") cmd.Flags().Int("hostport", -1, "The host port mapping for the container port. To demonstrate a single-machine container.") cmd.Flags().StringP("labels", "l", "", "Labels to apply to the pod(s).") @@ -137,6 +144,9 @@ func Run(f *cmdutil.Factory, cmdIn io.Reader, cmdOut, cmdErr io.Writer, cmd *cob if len(args) > 1 { params["args"] = args[1:] } + + params["env"] = cmdutil.GetFlagStringSlice(cmd, "env") + err = kubectl.ValidateParams(names, params) if err != nil { return err diff --git a/pkg/kubectl/cmd/run_test.go b/pkg/kubectl/cmd/run_test.go index 56185a5f1a..c40ab85ea1 100644 --- a/pkg/kubectl/cmd/run_test.go +++ b/pkg/kubectl/cmd/run_test.go @@ -17,10 +17,12 @@ limitations under the License. package cmd import ( + "reflect" "testing" "github.com/spf13/cobra" "k8s.io/kubernetes/pkg/api" + cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" ) func TestGetRestartPolicy(t *testing.T) { @@ -78,3 +80,20 @@ func TestGetRestartPolicy(t *testing.T) { } } } + +func TestGetEnv(t *testing.T) { + test := struct { + input []string + expected []string + }{ + input: []string{"a=b", "c=d"}, + expected: []string{"a=b", "c=d"}, + } + cmd := &cobra.Command{} + cmd.Flags().StringSlice("env", test.input, "") + + envStrings := cmdutil.GetFlagStringSlice(cmd, "env") + if len(envStrings) != 2 || !reflect.DeepEqual(envStrings, test.expected) { + t.Errorf("expected: %s, saw: %s", test.expected, envStrings) + } +} diff --git a/pkg/kubectl/run.go b/pkg/kubectl/run.go index 6c2c5dd7a4..233cbbe9ba 100644 --- a/pkg/kubectl/run.go +++ b/pkg/kubectl/run.go @@ -19,9 +19,11 @@ package kubectl import ( "fmt" "strconv" + "strings" "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/runtime" + "k8s.io/kubernetes/pkg/util" ) type BasicReplicationController struct{} @@ -39,6 +41,7 @@ func (BasicReplicationController) ParamNames() []GeneratorParam { {"tty", false}, {"command", false}, {"args", false}, + {"env", false}, } } @@ -77,6 +80,23 @@ func (BasicReplicationController) Generate(genericParams map[string]interface{}) } delete(genericParams, "args") } + + // TODO: abstract this logic so that multiple generators can handle env in the same way. Same for parse envs. + var envs []api.EnvVar + envStrings, found := genericParams["env"] + if found { + if envStringArray, isArray := envStrings.([]string); isArray { + var err error + envs, err = parseEnvs(envStringArray) + if err != nil { + return nil, err + } + delete(genericParams, "env") + } else { + return nil, fmt.Errorf("expected []string, found: %v", envStrings) + } + } + params := map[string]string{} for key, value := range genericParams { strVal, isString := value.(string) @@ -127,6 +147,10 @@ func (BasicReplicationController) Generate(genericParams map[string]interface{}) } } + if len(envs) > 0 { + podSpec.Containers[0].Env = envs + } + controller := api.ReplicationController{ ObjectMeta: api.ObjectMeta{ Name: name, @@ -198,6 +222,7 @@ func (BasicPod) ParamNames() []GeneratorParam { {"restart", false}, {"command", false}, {"args", false}, + {"env", false}, } } @@ -212,6 +237,22 @@ func (BasicPod) Generate(genericParams map[string]interface{}) (runtime.Object, } delete(genericParams, "args") } + // TODO: abstract this logic so that multiple generators can handle env in the same way. Same for parse envs. + var envs []api.EnvVar + envStrings, found := genericParams["env"] + if found { + if envStringArray, isArray := envStrings.([]string); isArray { + var err error + envs, err = parseEnvs(envStringArray) + if err != nil { + return nil, err + } + delete(genericParams, "env") + } else { + return nil, fmt.Errorf("expected []string, found: %v", envStrings) + } + } + params := map[string]string{} for key, value := range genericParams { strVal, isString := value.(string) @@ -281,8 +322,26 @@ func (BasicPod) Generate(genericParams map[string]interface{}) (runtime.Object, pod.Spec.Containers[0].Args = args } } + + if len(envs) > 0 { + pod.Spec.Containers[0].Env = envs + } + if err := updatePodPorts(params, &pod.Spec); err != nil { return nil, err } return &pod, nil } + +func parseEnvs(envArray []string) ([]api.EnvVar, error) { + envs := []api.EnvVar{} + for _, env := range envArray { + parts := strings.Split(env, "=") + if len(parts) != 2 || !util.IsCIdentifier(parts[0]) || len(parts[1]) == 0 { + return nil, fmt.Errorf("invalid env: %v", env) + } + envVar := api.EnvVar{Name: parts[0], Value: parts[1]} + envs = append(envs, envVar) + } + return envs, nil +} diff --git a/pkg/kubectl/run_test.go b/pkg/kubectl/run_test.go index 66138f25f7..3b187eba80 100644 --- a/pkg/kubectl/run_test.go +++ b/pkg/kubectl/run_test.go @@ -60,6 +60,50 @@ func TestGenerate(t *testing.T) { }, }, }, + + { + params: map[string]interface{}{ + "name": "foo", + "image": "someimage", + "replicas": "1", + "port": "-1", + "env": []string{"a=b", "c=d"}, + }, + expected: &api.ReplicationController{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + Labels: map[string]string{"run": "foo"}, + }, + Spec: api.ReplicationControllerSpec{ + Replicas: 1, + Selector: map[string]string{"run": "foo"}, + Template: &api.PodTemplateSpec{ + ObjectMeta: api.ObjectMeta{ + Labels: map[string]string{"run": "foo"}, + }, + Spec: api.PodSpec{ + Containers: []api.Container{ + { + Name: "foo", + Image: "someimage", + Env: []api.EnvVar{ + { + Name: "a", + Value: "b", + }, + { + Name: "c", + Value: "d", + }, + }, + }, + }, + }, + }, + }, + }, + }, + { params: map[string]interface{}{ "name": "foo", @@ -287,6 +331,49 @@ func TestGeneratePod(t *testing.T) { }, }, }, + { + params: map[string]interface{}{ + "name": "foo", + "image": "someimage", + "env": []string{"a", "c"}, + }, + + expected: nil, + expectErr: true, + }, + { + params: map[string]interface{}{ + "name": "foo", + "image": "someimage", + "env": []string{"a=b", "c=d"}, + }, + expected: &api.Pod{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + }, + Spec: api.PodSpec{ + Containers: []api.Container{ + { + Name: "foo", + Image: "someimage", + ImagePullPolicy: api.PullIfNotPresent, + Env: []api.EnvVar{ + { + Name: "a", + Value: "b", + }, + { + Name: "c", + Value: "d", + }, + }, + }, + }, + DNSPolicy: api.DNSClusterFirst, + RestartPolicy: api.RestartPolicyAlways, + }, + }, + }, { params: map[string]interface{}{ "name": "foo",