From c0c02c95c4cbd5e4a4f1bef65e813e85035f998e Mon Sep 17 00:00:00 2001 From: Janet Kuo Date: Thu, 12 Nov 2015 17:07:21 -0800 Subject: [PATCH] kubectl run to produce deployment and job --- docs/man/man1/kubectl-run.1 | 15 +- docs/user-guide/kubectl/kubectl_run.md | 15 +- hack/test-cmd.sh | 20 +- pkg/apis/extensions/validation/validation.go | 5 +- pkg/kubectl/cmd/run.go | 21 +- pkg/kubectl/cmd/util/factory.go | 36 +- pkg/kubectl/run.go | 442 +++++++++++++------ pkg/kubectl/run_test.go | 186 ++++++++ test/e2e/kubectl.go | 67 +-- 9 files changed, 618 insertions(+), 189 deletions(-) diff --git a/docs/man/man1/kubectl-run.1 b/docs/man/man1/kubectl-run.1 index 1092b4fedb8..fb5555e7ae7 100644 --- a/docs/man/man1/kubectl-run.1 +++ b/docs/man/man1/kubectl-run.1 @@ -14,7 +14,7 @@ kubectl run \- Run a particular image on the cluster. .SH DESCRIPTION .PP Create and run a particular image, possibly replicated. -Creates a replication controller to manage the created container(s). +Creates a replication controller or job to manage the created container(s). .SH OPTIONS @@ -40,7 +40,7 @@ Creates a replication controller to manage the created container(s). .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'. + The name of the API generator to use. Default is 'run/v1' if \-\-restart=Always, otherwise the default is 'job/v1beta1'. .PP \fB\-\-hostport\fP=\-1 @@ -94,7 +94,7 @@ Creates a replication controller to manage the created container(s). .PP \fB\-\-restart\fP="Always" - The restart policy for this Pod. Legal values [Always, OnFailure, Never]. If set to 'Always' a replication controller is created for this pod, if set to OnFailure or Never, only the Pod is created and \-\-replicas must be 1. Default 'Always' + The restart policy for this Pod. Legal values [Always, OnFailure, Never]. If set to 'Always' a replication controller is created for this pod, if set to OnFailure or Never, a job is created for this pod and \-\-replicas must be 1. Default 'Always' .PP \fB\-\-save\-config\fP=false @@ -247,15 +247,18 @@ $ kubectl run nginx \-\-image=nginx \-\-dry\-run # Start a single instance of nginx, but overload the spec of the replication controller with a partial set of values parsed from JSON. $ kubectl run nginx \-\-image=nginx \-\-overrides='{ "apiVersion": "v1", "spec": { ... } }' -# Start a single instance of nginx and keep it in the foreground, don't restart it if it exits. -$ kubectl run \-i \-\-tty nginx \-\-image=nginx \-\-restart=Never +# Start a single instance of busybox and keep it in the foreground, don't restart it if it exits. +$ kubectl run \-i \-\-tty busybox \-\-image=busybox \-\-restart=Never # Start the nginx container using the default command, but use custom arguments (arg1 .. argN) for that command. $ kubectl run nginx \-\-image=nginx \-\- ... -# Start the nginx container using a different command and custom arguments +# Start the nginx container using a different command and custom arguments. $ kubectl run nginx \-\-image=nginx \-\-command \-\- ... +# Start the perl container to compute π to 2000 places and print it out. +$ kubectl run pi \-\-image=perl \-\-restart=OnFailure \-\- perl \-Mbignum=bpi \-wle 'print bpi(2000)' + .fi .RE diff --git a/docs/user-guide/kubectl/kubectl_run.md b/docs/user-guide/kubectl/kubectl_run.md index 8f26917cc4f..ca244d573b7 100644 --- a/docs/user-guide/kubectl/kubectl_run.md +++ b/docs/user-guide/kubectl/kubectl_run.md @@ -39,7 +39,7 @@ Run a particular image on the cluster. Create and run a particular image, possibly replicated. -Creates a replication controller to manage the created container(s). +Creates a replication controller or job to manage the created container(s). ``` kubectl run NAME --image=image [--env="key=value"] [--port=port] [--replicas=replicas] [--dry-run=bool] [--overrides=inline-json] [--command] -- [COMMAND] [args...] @@ -66,14 +66,17 @@ $ kubectl run nginx --image=nginx --dry-run # Start a single instance of nginx, but overload the spec of the replication controller with a partial set of values parsed from JSON. $ kubectl run nginx --image=nginx --overrides='{ "apiVersion": "v1", "spec": { ... } }' -# Start a single instance of nginx and keep it in the foreground, don't restart it if it exits. -$ kubectl run -i --tty nginx --image=nginx --restart=Never +# Start a single instance of busybox and keep it in the foreground, don't restart it if it exits. +$ kubectl run -i --tty busybox --image=busybox --restart=Never # Start the nginx container using the default command, but use custom arguments (arg1 .. argN) for that command. $ kubectl run nginx --image=nginx -- ... -# Start the nginx container using a different command and custom arguments +# Start the nginx container using a different command and custom arguments. $ kubectl run nginx --image=nginx --command -- ... + +# Start the perl container to compute π to 2000 places and print it out. +$ kubectl run pi --image=perl --restart=OnFailure -- perl -Mbignum=bpi -wle 'print bpi(2000)' ``` ### Options @@ -84,7 +87,7 @@ $ kubectl run nginx --image=nginx --command -- ... --dry-run[=false]: If true, only print the object that would be sent, without sending it. --env=[]: Environment variables to set in the container --expose[=false]: If true, a public, external service is created for the container(s) which are run - --generator="": The name of the API generator to use. Default is 'run/v1' if --restart=Always, otherwise the default is 'run-pod/v1'. + --generator="": The name of the API generator to use. Default is 'run/v1' if --restart=Always, otherwise the default is 'job/v1beta1'. --hostport=-1: The host port mapping for the container port. To demonstrate a single-machine container. --image="": The image for the container to run. -l, --labels="": Labels to apply to the pod(s). @@ -97,7 +100,7 @@ $ kubectl run nginx --image=nginx --command -- ... --port=-1: The port that this container exposes. If --expose is true, this is also the port used by the service that is created. -r, --replicas=1: Number of replicas to create for this container. Default is 1. --requests="": The resource requirement requests for this container. For example, 'cpu=100m,memory=256Mi' - --restart="Always": The restart policy for this Pod. Legal values [Always, OnFailure, Never]. If set to 'Always' a replication controller is created for this pod, if set to OnFailure or Never, only the Pod is created and --replicas must be 1. Default 'Always' + --restart="Always": The restart policy for this Pod. Legal values [Always, OnFailure, Never]. If set to 'Always' a replication controller is created for this pod, if set to OnFailure or Never, a job is created for this pod and --replicas must be 1. Default 'Always' --save-config[=false]: If true, the configuration of current object will be saved in its annotation. This is useful when you want to perform kubectl apply on this object in the future. --service-generator="service/v2": The name of the generator to use for creating a service. Only used if --expose is true --service-overrides="": An inline JSON override for the generated service object. If this is non-empty, it is used to override the generated object. Requires that the object supply a valid apiVersion field. Only used if --expose is true. diff --git a/hack/test-cmd.sh b/hack/test-cmd.sh index 19286ce2d26..05a4151da0b 100755 --- a/hack/test-cmd.sh +++ b/hack/test-cmd.sh @@ -611,7 +611,7 @@ runTests() { # Pre-Condition: no RC is running kube::test::get_object_assert rc "{{range.items}}{{$id_field}}:{{end}}" '' # Command: create the rc "nginx" with image nginx - kubectl run nginx --image=nginx --save-config "${kube_flags[@]}" + kubectl run nginx --image=nginx --save-config --generator=run/v1 "${kube_flags[@]}" # Post-Condition: rc "nginx" has configuration annotation [[ "$(kubectl get rc nginx -o yaml "${kube_flags[@]}" | grep kubectl.kubernetes.io/last-applied-configuration)" ]] ## 5. kubectl expose --save-config should generate configuration annotation @@ -647,6 +647,24 @@ runTests() { # Clean up kubectl delete pods test-pod "${kube_flags[@]}" + ## kubectl run should create deployments or jobs + # Pre-Condition: no Job is running + kube::test::get_object_assert jobs "{{range.items}}{{$id_field}}:{{end}}" '' + # Command + kubectl run pi --image=perl --restart=OnFailure -- perl -Mbignum=bpi -wle 'print bpi(20)' + # Post-Condition: Job "pi" is created + kube::test::get_object_assert jobs "{{range.items}}{{$id_field}}:{{end}}" 'pi:' + # Clean up + kubectl delete jobs pi + # Pre-Condition: no Deployment is running + kube::test::get_object_assert deployment "{{range.items}}{{$id_field}}:{{end}}" '' + # Command + kubectl run nginx --image=nginx --generator=deployment/v1beta1 + # Post-Condition: Deployment "nginx" is created + kube::test::get_object_assert deployment "{{range.items}}{{$id_field}}:{{end}}" 'nginx:' + # Clean up + kubectl delete deployment nginx + ############## # Namespaces # ############## diff --git a/pkg/apis/extensions/validation/validation.go b/pkg/apis/extensions/validation/validation.go index 7971898e0b4..3f82488a1a9 100644 --- a/pkg/apis/extensions/validation/validation.go +++ b/pkg/apis/extensions/validation/validation.go @@ -297,7 +297,10 @@ func ValidateDeploymentSpec(spec *extensions.DeploymentSpec) validation.ErrorLis allErrs = append(allErrs, apivalidation.ValidatePositiveField(int64(spec.Replicas), "replicas")...) allErrs = append(allErrs, apivalidation.ValidatePodTemplateSpecForRC(&spec.Template, spec.Selector, spec.Replicas, "template")...) allErrs = append(allErrs, ValidateDeploymentStrategy(&spec.Strategy, "strategy")...) - allErrs = append(allErrs, apivalidation.ValidateLabelName(spec.UniqueLabelKey, "uniqueLabel")...) + // empty string is a valid UniqueLabelKey + if len(spec.UniqueLabelKey) > 0 { + allErrs = append(allErrs, apivalidation.ValidateLabelName(spec.UniqueLabelKey, "uniqueLabel")...) + } return allErrs } diff --git a/pkg/kubectl/cmd/run.go b/pkg/kubectl/cmd/run.go index 57a195b49d4..bc99f9effd5 100644 --- a/pkg/kubectl/cmd/run.go +++ b/pkg/kubectl/cmd/run.go @@ -34,7 +34,7 @@ import ( const ( run_long = `Create and run a particular image, possibly replicated. -Creates a replication controller to manage the created container(s).` +Creates a replication controller or job to manage the created container(s).` run_example = `# Start a single instance of nginx. $ kubectl run nginx --image=nginx @@ -53,14 +53,17 @@ $ kubectl run nginx --image=nginx --dry-run # Start a single instance of nginx, but overload the spec of the replication controller with a partial set of values parsed from JSON. $ kubectl run nginx --image=nginx --overrides='{ "apiVersion": "v1", "spec": { ... } }' -# Start a single instance of nginx and keep it in the foreground, don't restart it if it exits. -$ kubectl run -i --tty nginx --image=nginx --restart=Never +# Start a single instance of busybox and keep it in the foreground, don't restart it if it exits. +$ kubectl run -i --tty busybox --image=busybox --restart=Never # Start the nginx container using the default command, but use custom arguments (arg1 .. argN) for that command. $ kubectl run nginx --image=nginx -- ... -# Start the nginx container using a different command and custom arguments -$ kubectl run nginx --image=nginx --command -- ... ` +# Start the nginx container using a different command and custom arguments. +$ kubectl run nginx --image=nginx --command -- ... + +# Start the perl container to compute π to 2000 places and print it out. +$ kubectl run pi --image=perl --restart=OnFailure -- perl -Mbignum=bpi -wle 'print bpi(2000)'` ) func NewCmdRun(f *cmdutil.Factory, cmdIn io.Reader, cmdOut, cmdErr io.Writer) *cobra.Command { @@ -84,7 +87,8 @@ func NewCmdRun(f *cmdutil.Factory, cmdIn io.Reader, cmdOut, cmdErr io.Writer) *c } func addRunFlags(cmd *cobra.Command) { - cmd.Flags().String("generator", "", "The name of the API generator to use. Default is 'run/v1' if --restart=Always, otherwise the default is 'run-pod/v1'.") + // TODO: Change the default to "deployment/v1beta1" (which is a valid generator) when deployment reaches beta (#15313) + cmd.Flags().String("generator", "", "The name of the API generator to use. Default is 'run/v1' if --restart=Always, otherwise the default is 'job/v1beta1'.") cmd.Flags().String("image", "", "The image for the container to run.") cmd.MarkFlagRequired("image") cmd.Flags().IntP("replicas", "r", 1, "Number of replicas to create for this container. Default is 1.") @@ -98,7 +102,7 @@ func addRunFlags(cmd *cobra.Command) { cmd.Flags().Bool("tty", false, "Allocated a TTY for each container in the pod. Because -t is currently shorthand for --template, -t is not supported for --tty. This shorthand is deprecated and we expect to adopt -t for --tty soon.") cmd.Flags().Bool("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.") cmd.Flags().Bool("leave-stdin-open", false, "If the pod is started in interactive mode or with stdin, leave stdin open after the first attach completes. By default, stdin will be closed after the first attach completes.") - cmd.Flags().String("restart", "Always", "The restart policy for this Pod. Legal values [Always, OnFailure, Never]. If set to 'Always' a replication controller is created for this pod, if set to OnFailure or Never, only the Pod is created and --replicas must be 1. Default 'Always'") + cmd.Flags().String("restart", "Always", "The restart policy for this Pod. Legal values [Always, OnFailure, Never]. If set to 'Always' a replication controller is created for this pod, if set to OnFailure or Never, a job is created for this pod and --replicas must be 1. Default 'Always'") cmd.Flags().Bool("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.") cmd.Flags().String("requests", "", "The resource requirement requests for this container. For example, 'cpu=100m,memory=256Mi'") cmd.Flags().String("limits", "", "The resource requirement limits for this container. For example, 'cpu=200m,memory=512Mi'") @@ -142,10 +146,11 @@ func Run(f *cmdutil.Factory, cmdIn io.Reader, cmdOut, cmdErr io.Writer, cmd *cob generatorName := cmdutil.GetFlagString(cmd, "generator") if len(generatorName) == 0 { + // TODO: Change the default to "deployment/v1beta1" when deployment reaches beta (#15313) if restartPolicy == api.RestartPolicyAlways { generatorName = "run/v1" } else { - generatorName = "run-pod/v1" + generatorName = "job/v1beta1" } } generator, found := f.Generator(generatorName) diff --git a/pkg/kubectl/cmd/util/factory.go b/pkg/kubectl/cmd/util/factory.go index 260df61ff6e..ca826c9b3e6 100644 --- a/pkg/kubectl/cmd/util/factory.go +++ b/pkg/kubectl/cmd/util/factory.go @@ -36,6 +36,7 @@ import ( "k8s.io/kubernetes/pkg/api/meta" "k8s.io/kubernetes/pkg/api/registered" "k8s.io/kubernetes/pkg/api/validation" + "k8s.io/kubernetes/pkg/apis/extensions" client "k8s.io/kubernetes/pkg/client/unversioned" "k8s.io/kubernetes/pkg/client/unversioned/clientcmd" "k8s.io/kubernetes/pkg/fields" @@ -116,6 +117,8 @@ func NewFactory(optionalClientConfig clientcmd.ClientConfig) *Factory { "service/v1": kubectl.ServiceGeneratorV1{}, "service/v2": kubectl.ServiceGeneratorV2{}, "horizontalpodautoscaler/v1beta1": kubectl.HorizontalPodAutoscalerV1Beta1{}, + "deployment/v1beta1": kubectl.DeploymentV1Beta1{}, + "job/v1beta1": kubectl.JobV1Beta1{}, } clientConfig := optionalClientConfig @@ -312,18 +315,11 @@ func NewFactory(optionalClientConfig clientcmd.ClientConfig) *Factory { } switch t := object.(type) { case *api.ReplicationController: - var pods *api.PodList - for pods == nil || len(pods.Items) == 0 { - var err error - if pods, err = client.Pods(t.Namespace).List(labels.SelectorFromSet(t.Spec.Selector), fields.Everything()); err != nil { - return nil, err - } - if len(pods.Items) == 0 { - time.Sleep(2 * time.Second) - } - } - pod := &pods.Items[0] - return pod, nil + return GetFirstPod(client, t.Namespace, t.Spec.Selector) + case *extensions.Deployment: + return GetFirstPod(client, t.Namespace, t.Spec.Selector) + case *extensions.Job: + return GetFirstPod(client, t.Namespace, t.Spec.Selector.MatchLabels) case *api.Pod: return t, nil default: @@ -337,6 +333,22 @@ func NewFactory(optionalClientConfig clientcmd.ClientConfig) *Factory { } } +// GetFirstPod returns the first pod of an object from its namespace and selector +func GetFirstPod(client *client.Client, namespace string, selector map[string]string) (*api.Pod, error) { + var pods *api.PodList + for pods == nil || len(pods.Items) == 0 { + var err error + if pods, err = client.Pods(namespace).List(labels.SelectorFromSet(selector), fields.Everything()); err != nil { + return nil, err + } + if len(pods.Items) == 0 { + time.Sleep(2 * time.Second) + } + } + pod := &pods.Items[0] + return pod, nil +} + // BindFlags adds any flags that are common to all kubectl sub commands. func (f *Factory) BindFlags(flags *pflag.FlagSet) { // any flags defined by external projects (not part of pflags) diff --git a/pkg/kubectl/run.go b/pkg/kubectl/run.go index 31b82bae339..4c59a578b74 100644 --- a/pkg/kubectl/run.go +++ b/pkg/kubectl/run.go @@ -23,10 +23,264 @@ import ( "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api/resource" + "k8s.io/kubernetes/pkg/apis/extensions" "k8s.io/kubernetes/pkg/runtime" "k8s.io/kubernetes/pkg/util/validation" ) +type DeploymentV1Beta1 struct{} + +func (DeploymentV1Beta1) ParamNames() []GeneratorParam { + return []GeneratorParam{ + {"labels", false}, + {"default-name", false}, + {"name", true}, + {"replicas", true}, + {"image", true}, + {"port", false}, + {"hostport", false}, + {"stdin", false}, + {"tty", false}, + {"command", false}, + {"args", false}, + {"env", false}, + {"requests", false}, + {"limits", false}, + } +} + +func (DeploymentV1Beta1) Generate(genericParams map[string]interface{}) (runtime.Object, error) { + args, err := getArgs(genericParams) + if err != nil { + return nil, err + } + + envs, err := getEnvs(genericParams) + if err != nil { + return nil, err + } + + params, err := getParams(genericParams) + if err != nil { + return nil, err + } + + name, err := getName(params) + if err != nil { + return nil, err + } + + labels, err := getLabels(params, true, name) + if err != nil { + return nil, err + } + + count, err := strconv.Atoi(params["replicas"]) + if err != nil { + return nil, err + } + + podSpec, err := makePodSpec(params, name) + if err != nil { + return nil, err + } + + if err = updatePodContainers(params, args, envs, podSpec); err != nil { + return nil, err + } + + if err := updatePodPorts(params, podSpec); err != nil { + return nil, err + } + + // TODO: use versioned types for generators so that we don't need to + // set default values manually (see issue #17384) + deployment := extensions.Deployment{ + ObjectMeta: api.ObjectMeta{ + Name: name, + Labels: labels, + }, + Spec: extensions.DeploymentSpec{ + Replicas: count, + Selector: labels, + Template: api.PodTemplateSpec{ + ObjectMeta: api.ObjectMeta{ + Labels: labels, + }, + Spec: *podSpec, + }, + UniqueLabelKey: "deployment.kubernetes.io/podTemplateHash", + }, + } + return &deployment, nil +} + +func getLabels(params map[string]string, defaultRunLabel bool, name string) (map[string]string, error) { + labelString, found := params["labels"] + var labels map[string]string + var err error + if found && len(labelString) > 0 { + labels, err = ParseLabels(labelString) + if err != nil { + return nil, err + } + } else if defaultRunLabel { + labels = map[string]string{ + "run": name, + } + } + return labels, nil +} + +func getName(params map[string]string) (string, error) { + name, found := params["name"] + if !found || len(name) == 0 { + name, found = params["default-name"] + if !found || len(name) == 0 { + return "", fmt.Errorf("'name' is a required parameter.") + } + } + return name, nil +} + +func getParams(genericParams map[string]interface{}) (map[string]string, error) { + params := map[string]string{} + for key, value := range genericParams { + strVal, isString := value.(string) + if !isString { + return nil, fmt.Errorf("expected string, saw %v for '%s'", value, key) + } + params[key] = strVal + } + return params, nil +} + +func getArgs(genericParams map[string]interface{}) ([]string, error) { + args := []string{} + val, found := genericParams["args"] + if found { + var isArray bool + args, isArray = val.([]string) + if !isArray { + return nil, fmt.Errorf("expected []string, found: %v", val) + } + delete(genericParams, "args") + } + return args, nil +} + +func getEnvs(genericParams map[string]interface{}) ([]api.EnvVar, error) { + 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) + } + } + return envs, nil +} + +type JobV1Beta1 struct{} + +func (JobV1Beta1) ParamNames() []GeneratorParam { + return []GeneratorParam{ + {"labels", false}, + {"default-name", false}, + {"name", true}, + {"image", true}, + {"port", false}, + {"hostport", false}, + {"stdin", false}, + {"leave-stdin-open", false}, + {"tty", false}, + {"command", false}, + {"args", false}, + {"env", false}, + {"requests", false}, + {"limits", false}, + {"restart", false}, + } +} + +func (JobV1Beta1) Generate(genericParams map[string]interface{}) (runtime.Object, error) { + args, err := getArgs(genericParams) + if err != nil { + return nil, err + } + + envs, err := getEnvs(genericParams) + if err != nil { + return nil, err + } + + params, err := getParams(genericParams) + if err != nil { + return nil, err + } + + name, err := getName(params) + if err != nil { + return nil, err + } + + labels, err := getLabels(params, true, name) + if err != nil { + return nil, err + } + + podSpec, err := makePodSpec(params, name) + if err != nil { + return nil, err + } + + if err = updatePodContainers(params, args, envs, podSpec); err != nil { + return nil, err + } + + leaveStdinOpen, err := GetBool(params, "leave-stdin-open", false) + if err != nil { + return nil, err + } + podSpec.Containers[0].StdinOnce = !leaveStdinOpen && podSpec.Containers[0].Stdin + + if err := updatePodPorts(params, podSpec); err != nil { + return nil, err + } + + restartPolicy := api.RestartPolicy(params["restart"]) + if len(restartPolicy) == 0 { + restartPolicy = api.RestartPolicyAlways + } + podSpec.RestartPolicy = restartPolicy + + job := extensions.Job{ + ObjectMeta: api.ObjectMeta{ + Name: name, + Labels: labels, + }, + Spec: extensions.JobSpec{ + Selector: &extensions.PodSelector{ + MatchLabels: labels, + }, + Template: api.PodTemplateSpec{ + ObjectMeta: api.ObjectMeta{ + Labels: labels, + }, + Spec: *podSpec, + }, + }, + } + + return &job, nil +} + type BasicReplicationController struct{} func (BasicReplicationController) ParamNames() []GeneratorParam { @@ -119,62 +373,31 @@ func makePodSpec(params map[string]string, name string) (*api.PodSpec, error) { } func (BasicReplicationController) Generate(genericParams map[string]interface{}) (runtime.Object, error) { - args := []string{} - val, found := genericParams["args"] - if found { - var isArray bool - args, isArray = val.([]string) - if !isArray { - return nil, fmt.Errorf("expected []string, found: %v", val) - } - delete(genericParams, "args") + args, err := getArgs(genericParams) + if err != nil { + return nil, err } - // 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) - } + envs, err := getEnvs(genericParams) + if err != nil { + return nil, err } - params := map[string]string{} - for key, value := range genericParams { - strVal, isString := value.(string) - if !isString { - return nil, fmt.Errorf("expected string, saw %v for '%s'", value, key) - } - params[key] = strVal + params, err := getParams(genericParams) + if err != nil { + return nil, err } - name, found := params["name"] - if !found || len(name) == 0 { - name, found = params["default-name"] - if !found || len(name) == 0 { - return nil, fmt.Errorf("'name' is a required parameter.") - } + + name, err := getName(params) + if err != nil { + return nil, err } - // TODO: extract this flag to a central location. - labelString, found := params["labels"] - var labels map[string]string - var err error - if found && len(labelString) > 0 { - labels, err = ParseLabels(labelString) - if err != nil { - return nil, err - } - } else { - labels = map[string]string{ - "run": name, - } + + labels, err := getLabels(params, true, name) + if err != nil { + return nil, err } + count, err := strconv.Atoi(params["replicas"]) if err != nil { return nil, err @@ -184,20 +407,13 @@ func (BasicReplicationController) Generate(genericParams map[string]interface{}) if err != nil { return nil, err } - if len(args) > 0 { - command, err := GetBool(params, "command", false) - if err != nil { - return nil, err - } - if command { - podSpec.Containers[0].Command = args - } else { - podSpec.Containers[0].Args = args - } + + if err = updatePodContainers(params, args, envs, podSpec); err != nil { + return nil, err } - if len(envs) > 0 { - podSpec.Containers[0].Env = envs + if err := updatePodPorts(params, podSpec); err != nil { + return nil, err } controller := api.ReplicationController{ @@ -216,12 +432,28 @@ func (BasicReplicationController) Generate(genericParams map[string]interface{}) }, }, } - if err := updatePodPorts(params, &controller.Spec.Template.Spec); err != nil { - return nil, err - } return &controller, nil } +func updatePodContainers(params map[string]string, args []string, envs []api.EnvVar, podSpec *api.PodSpec) error { + if len(args) > 0 { + command, err := GetBool(params, "command", false) + if err != nil { + return err + } + if command { + podSpec.Containers[0].Command = args + } else { + podSpec.Containers[0].Args = args + } + } + + if len(envs) > 0 { + podSpec.Containers[0].Env = envs + } + return nil +} + func updatePodPorts(params map[string]string, podSpec *api.PodSpec) (err error) { port := -1 hostPort := -1 @@ -279,57 +511,31 @@ func (BasicPod) ParamNames() []GeneratorParam { } func (BasicPod) Generate(genericParams map[string]interface{}) (runtime.Object, error) { - args := []string{} - val, found := genericParams["args"] - if found { - var isArray bool - args, isArray = val.([]string) - if !isArray { - return nil, fmt.Errorf("expected []string, found: %v", val) - } - 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) - } + args, err := getArgs(genericParams) + if err != nil { + return nil, err } - params := map[string]string{} - for key, value := range genericParams { - strVal, isString := value.(string) - if !isString { - return nil, fmt.Errorf("expected string, saw %v for '%s'", value, key) - } - params[key] = strVal + envs, err := getEnvs(genericParams) + if err != nil { + return nil, err } - name, found := params["name"] - if !found || len(name) == 0 { - name, found = params["default-name"] - if !found || len(name) == 0 { - return nil, fmt.Errorf("'name' is a required parameter.") - } + + params, err := getParams(genericParams) + if err != nil { + return nil, err } - // TODO: extract this flag to a central location. - labelString, found := params["labels"] - var labels map[string]string - var err error - if found && len(labelString) > 0 { - labels, err = ParseLabels(labelString) - if err != nil { - return nil, err - } + + name, err := getName(params) + if err != nil { + return nil, err } + + labels, err := getLabels(params, false, name) + if err != nil { + return nil, err + } + stdin, err := GetBool(params, "stdin", false) if err != nil { return nil, err @@ -374,20 +580,8 @@ func (BasicPod) Generate(genericParams map[string]interface{}) (runtime.Object, RestartPolicy: restartPolicy, }, } - if len(args) > 0 { - command, err := GetBool(params, "command", false) - if err != nil { - return nil, err - } - if command { - pod.Spec.Containers[0].Command = args - } else { - pod.Spec.Containers[0].Args = args - } - } - - if len(envs) > 0 { - pod.Spec.Containers[0].Env = envs + if err = updatePodContainers(params, args, envs, &pod.Spec); err != nil { + return nil, err } if err := updatePodPorts(params, &pod.Spec); err != nil { diff --git a/pkg/kubectl/run_test.go b/pkg/kubectl/run_test.go index d6eab99d35d..59cf53bade4 100644 --- a/pkg/kubectl/run_test.go +++ b/pkg/kubectl/run_test.go @@ -22,6 +22,7 @@ import ( "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api/resource" + "k8s.io/kubernetes/pkg/apis/extensions" ) func TestGenerate(t *testing.T) { @@ -625,3 +626,188 @@ func TestGeneratePod(t *testing.T) { } } } + +func TestGenerateDeployment(t *testing.T) { + tests := []struct { + params map[string]interface{} + expected *extensions.Deployment + expectErr bool + }{ + { + params: map[string]interface{}{ + "labels": "foo=bar,baz=blah", + "name": "foo", + "replicas": "3", + "image": "someimage", + "port": "80", + "hostport": "80", + "stdin": "true", + "command": "true", + "args": []string{"bar", "baz", "blah"}, + "env": []string{"a=b", "c=d"}, + "requests": "cpu=100m,memory=100Mi", + "limits": "cpu=400m,memory=200Mi", + }, + expected: &extensions.Deployment{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + Labels: map[string]string{"foo": "bar", "baz": "blah"}, + }, + Spec: extensions.DeploymentSpec{ + Replicas: 3, + Selector: map[string]string{"foo": "bar", "baz": "blah"}, + UniqueLabelKey: "deployment.kubernetes.io/podTemplateHash", + Template: api.PodTemplateSpec{ + ObjectMeta: api.ObjectMeta{ + Labels: map[string]string{"foo": "bar", "baz": "blah"}, + }, + Spec: api.PodSpec{ + Containers: []api.Container{ + { + Name: "foo", + Image: "someimage", + Stdin: true, + Ports: []api.ContainerPort{ + { + ContainerPort: 80, + HostPort: 80, + }, + }, + Command: []string{"bar", "baz", "blah"}, + Env: []api.EnvVar{ + { + Name: "a", + Value: "b", + }, + { + Name: "c", + Value: "d", + }, + }, + Resources: api.ResourceRequirements{ + Requests: api.ResourceList{ + api.ResourceCPU: resource.MustParse("100m"), + api.ResourceMemory: resource.MustParse("100Mi"), + }, + Limits: api.ResourceList{ + api.ResourceCPU: resource.MustParse("400m"), + api.ResourceMemory: resource.MustParse("200Mi"), + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + generator := DeploymentV1Beta1{} + for _, test := range tests { + obj, err := generator.Generate(test.params) + if !test.expectErr && err != nil { + t.Errorf("unexpected error: %v", err) + } + if test.expectErr && err != nil { + continue + } + if !reflect.DeepEqual(obj.(*extensions.Deployment), test.expected) { + t.Errorf("\nexpected:\n%#v\nsaw:\n%#v", test.expected, obj.(*extensions.Deployment)) + } + } +} + +func TestGenerateJob(t *testing.T) { + tests := []struct { + params map[string]interface{} + expected *extensions.Job + expectErr bool + }{ + { + params: map[string]interface{}{ + "labels": "foo=bar,baz=blah", + "name": "foo", + "image": "someimage", + "port": "80", + "hostport": "80", + "stdin": "true", + "leave-stdin-open": "true", + "command": "true", + "args": []string{"bar", "baz", "blah"}, + "env": []string{"a=b", "c=d"}, + "requests": "cpu=100m,memory=100Mi", + "limits": "cpu=400m,memory=200Mi", + "restart": "OnFailure", + }, + expected: &extensions.Job{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + Labels: map[string]string{"foo": "bar", "baz": "blah"}, + }, + Spec: extensions.JobSpec{ + Selector: &extensions.PodSelector{ + MatchLabels: map[string]string{"foo": "bar", "baz": "blah"}, + }, + Template: api.PodTemplateSpec{ + ObjectMeta: api.ObjectMeta{ + Labels: map[string]string{"foo": "bar", "baz": "blah"}, + }, + Spec: api.PodSpec{ + RestartPolicy: api.RestartPolicyOnFailure, + Containers: []api.Container{ + { + Name: "foo", + Image: "someimage", + Stdin: true, + StdinOnce: false, + Ports: []api.ContainerPort{ + { + ContainerPort: 80, + HostPort: 80, + }, + }, + Command: []string{"bar", "baz", "blah"}, + Env: []api.EnvVar{ + { + Name: "a", + Value: "b", + }, + { + Name: "c", + Value: "d", + }, + }, + Resources: api.ResourceRequirements{ + Requests: api.ResourceList{ + api.ResourceCPU: resource.MustParse("100m"), + api.ResourceMemory: resource.MustParse("100Mi"), + }, + Limits: api.ResourceList{ + api.ResourceCPU: resource.MustParse("400m"), + api.ResourceMemory: resource.MustParse("200Mi"), + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + generator := JobV1Beta1{} + for _, test := range tests { + obj, err := generator.Generate(test.params) + if !test.expectErr && err != nil { + t.Errorf("unexpected error: %v", err) + } + if test.expectErr && err != nil { + continue + } + if !reflect.DeepEqual(obj.(*extensions.Job), test.expected) { + t.Errorf("\nexpected:\n%#v\nsaw:\n%#v", test.expected, obj.(*extensions.Job)) + } + } +} diff --git a/test/e2e/kubectl.go b/test/e2e/kubectl.go index 3f4ddc79a3c..b8d050f2d70 100644 --- a/test/e2e/kubectl.go +++ b/test/e2e/kubectl.go @@ -44,6 +44,7 @@ import ( client "k8s.io/kubernetes/pkg/client/unversioned" "k8s.io/kubernetes/pkg/fields" "k8s.io/kubernetes/pkg/kubectl" + "k8s.io/kubernetes/pkg/kubectl/cmd/util" "k8s.io/kubernetes/pkg/labels" "k8s.io/kubernetes/pkg/util/wait" @@ -367,7 +368,7 @@ var _ = Describe("Kubectl client", func() { execOrDie() Expect(runOutput).To(ContainSubstring("abcd1234")) Expect(runOutput).To(ContainSubstring("stdin closed")) - Expect(c.Pods(ns).Delete("run-test", api.NewDeleteOptions(0))).To(BeNil()) + Expect(c.Extensions().Jobs(ns).Delete("run-test", api.NewDeleteOptions(0))).To(BeNil()) By("executing a command with run and attach without stdin") runOutput = newKubectlCommand(fmt.Sprintf("--namespace=%v", ns), "run", "run-test-2", "--image=busybox", "--restart=Never", "--attach=true", "--leave-stdin-open=true", "--", "sh", "-c", "cat && echo 'stdin closed'"). @@ -375,24 +376,28 @@ var _ = Describe("Kubectl client", func() { execOrDie() Expect(runOutput).ToNot(ContainSubstring("abcd1234")) Expect(runOutput).To(ContainSubstring("stdin closed")) - Expect(c.Pods(ns).Delete("run-test-2", api.NewDeleteOptions(0))).To(BeNil()) + Expect(c.Extensions().Jobs(ns).Delete("run-test-2", api.NewDeleteOptions(0))).To(BeNil()) By("executing a command with run and attach with stdin with open stdin should remain running") runOutput = newKubectlCommand(nsFlag, "run", "run-test-3", "--image=busybox", "--restart=Never", "--attach=true", "--leave-stdin-open=true", "--stdin", "--", "sh", "-c", "cat && echo 'stdin closed'"). withStdinData("abcd1234\n"). execOrDie() Expect(runOutput).ToNot(ContainSubstring("stdin closed")) - if !checkPodsRunningReady(c, ns, []string{"run-test-3"}, time.Minute) { - Failf("Pod %q should still be running", "run-test-3") + runTestPod, err := util.GetFirstPod(c, ns, map[string]string{"run": "run-test-3"}) + if err != nil { + os.Exit(1) + } + if !checkPodsRunningReady(c, ns, []string{runTestPod.Name}, time.Minute) { + Failf("Pod %q of Job %q should still be running", runTestPod.Name, "run-test-3") } // NOTE: we cannot guarantee our output showed up in the container logs before stdin was closed, so we have // to loop test. - err := wait.PollImmediate(time.Second, time.Minute, func() (bool, error) { - if !checkPodsRunningReady(c, ns, []string{"run-test-3"}, 1*time.Second) { - Failf("Pod %q should still be running", "run-test-3") + err = wait.PollImmediate(time.Second, time.Minute, func() (bool, error) { + if !checkPodsRunningReady(c, ns, []string{runTestPod.Name}, 1*time.Second) { + Failf("Pod %q of Job %q should still be running", runTestPod.Name, "run-test-3") } - logOutput := runKubectlOrDie(nsFlag, "logs", "run-test-3") + logOutput := runKubectlOrDie(nsFlag, "logs", runTestPod.Name) Expect(logOutput).ToNot(ContainSubstring("stdin closed")) return strings.Contains(logOutput, "abcd1234"), nil }) @@ -401,7 +406,7 @@ var _ = Describe("Kubectl client", func() { } Expect(err).To(BeNil()) - Expect(c.Pods(ns).Delete("run-test-3", api.NewDeleteOptions(0))).To(BeNil()) + Expect(c.Extensions().Jobs(ns).Delete("run-test-3", api.NewDeleteOptions(0))).To(BeNil()) }) It("should support port-forward", func() { @@ -804,54 +809,54 @@ var _ = Describe("Kubectl client", func() { }) - Describe("Kubectl run pod", func() { + Describe("Kubectl run job", func() { var nsFlag string - var podName string + var jobName string BeforeEach(func() { nsFlag = fmt.Sprintf("--namespace=%v", ns) - podName = "e2e-test-nginx-pod" + jobName = "e2e-test-nginx-job" }) AfterEach(func() { - runKubectlOrDie("stop", "pods", podName, nsFlag) + runKubectlOrDie("stop", "jobs", jobName, nsFlag) }) - It("should create a pod from an image when restart is OnFailure [Conformance]", func() { + It("should create a job from an image when restart is OnFailure [Conformance]", func() { image := "nginx" By("running the image " + image) - runKubectlOrDie("run", podName, "--restart=OnFailure", "--image="+image, nsFlag) - By("verifying the pod " + podName + " was created") - pod, err := c.Pods(ns).Get(podName) + runKubectlOrDie("run", jobName, "--restart=OnFailure", "--image="+image, nsFlag) + By("verifying the job " + jobName + " was created") + job, err := c.Extensions().Jobs(ns).Get(jobName) if err != nil { - Failf("Failed getting pod %s: %v", podName, err) + Failf("Failed getting job %s: %v", jobName, err) } - containers := pod.Spec.Containers + containers := job.Spec.Template.Spec.Containers if containers == nil || len(containers) != 1 || containers[0].Image != image { - Failf("Failed creating pod %s for 1 pod with expected image %s", podName, image) + Failf("Failed creating job %s for 1 pod with expected image %s", jobName, image) } - if pod.Spec.RestartPolicy != api.RestartPolicyOnFailure { - Failf("Failed creating a pod with correct restart policy for --restart=OnFailure") + if job.Spec.Template.Spec.RestartPolicy != api.RestartPolicyOnFailure { + Failf("Failed creating a job with correct restart policy for --restart=OnFailure") } }) - It("should create a pod from an image when restart is Never [Conformance]", func() { + It("should create a job from an image when restart is Never [Conformance]", func() { image := "nginx" By("running the image " + image) - runKubectlOrDie("run", podName, "--restart=Never", "--image="+image, nsFlag) - By("verifying the pod " + podName + " was created") - pod, err := c.Pods(ns).Get(podName) + runKubectlOrDie("run", jobName, "--restart=Never", "--image="+image, nsFlag) + By("verifying the job " + jobName + " was created") + job, err := c.Extensions().Jobs(ns).Get(jobName) if err != nil { - Failf("Failed getting pod %s: %v", podName, err) + Failf("Failed getting job %s: %v", jobName, err) } - containers := pod.Spec.Containers + containers := job.Spec.Template.Spec.Containers if containers == nil || len(containers) != 1 || containers[0].Image != image { - Failf("Failed creating pod %s for 1 pod with expected image %s", podName, image) + Failf("Failed creating job %s for 1 pod with expected image %s", jobName, image) } - if pod.Spec.RestartPolicy != api.RestartPolicyNever { - Failf("Failed creating a pod with correct restart policy for --restart=OnFailure") + if job.Spec.Template.Spec.RestartPolicy != api.RestartPolicyNever { + Failf("Failed creating a job with correct restart policy for --restart=OnFailure") } })