diff --git a/docs/man/man1/kubectl-run.1 b/docs/man/man1/kubectl-run.1 index 52779c6e9d6..e0abc9e2a17 100644 --- a/docs/man/man1/kubectl-run.1 +++ b/docs/man/man1/kubectl-run.1 @@ -40,7 +40,7 @@ Creates a deployment or job to manage the created container(s). .PP \fB\-\-generator\fP="" - The name of the API generator to use. Default is 'deployment/v1beta1' if \-\-restart=Always, otherwise the default is 'job/v1beta1'. + The name of the API generator to use. Default is 'deployment/v1beta1' if \-\-restart=Always, otherwise the default is 'job/v1'. .PP \fB\-\-hostport\fP=\-1 diff --git a/docs/user-guide/kubectl/kubectl_run.md b/docs/user-guide/kubectl/kubectl_run.md index 6eb4ca43ff2..84906ebcea3 100644 --- a/docs/user-guide/kubectl/kubectl_run.md +++ b/docs/user-guide/kubectl/kubectl_run.md @@ -88,7 +88,7 @@ kubectl run pi --image=perl --restart=OnFailure -- perl -Mbignum=bpi -wle 'print --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 'deployment/v1beta1' if --restart=Always, otherwise the default is 'job/v1beta1'. + --generator="": The name of the API generator to use. Default is 'deployment/v1beta1' if --restart=Always, otherwise the default is 'job/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. -l, --labels="": Labels to apply to the pod(s). diff --git a/hack/test-cmd.sh b/hack/test-cmd.sh index 0ae6a16c6af..c7ed6b5eb60 100755 --- a/hack/test-cmd.sh +++ b/hack/test-cmd.sh @@ -485,11 +485,11 @@ runTests() { kube::test::get_object_assert pods "{{range.items}}{{$image_field}}:{{end}}" 'nginx:' # Post-condition: valid-pod has the record annotation kube::test::get_object_assert pods "{{range.items}}{{$annotations_field}}:{{end}}" "${change_cause_annotation}" - # prove that patch can use different types + # prove that patch can use different types kubectl patch "${kube_flags[@]}" pod valid-pod --type="json" -p='[{"op": "replace", "path": "/spec/containers/0/image", "value":"nginx2"}]' # Post-condition: valid-pod POD has image nginx kube::test::get_object_assert pods "{{range.items}}{{$image_field}}:{{end}}" 'nginx2:' - # prove that patch can use different types + # prove that patch can use different types kubectl patch "${kube_flags[@]}" pod valid-pod --type="json" -p='[{"op": "replace", "path": "/spec/containers/0/image", "value":"nginx"}]' # Post-condition: valid-pod POD has image nginx kube::test::get_object_assert pods "{{range.items}}{{$image_field}}:{{end}}" 'nginx:' @@ -545,7 +545,7 @@ runTests() { kube::test::get_object_assert 'pod valid-pod' "{{(index .spec.containers 0).name}}" 'replaced-k8s-serve-hostname' #cleaning rm /tmp/tmp-valid-pod.json - + ## replace of a cluster scoped resource can succeed # Pre-condition: a node exists kubectl create -f - "${kube_flags[@]}" << __EOF__ @@ -755,6 +755,12 @@ __EOF__ kube::test::get_object_assert jobs "{{range.items}}{{$id_field}}:{{end}}" 'pi:' # Clean up kubectl delete jobs pi "${kube_flags[@]}" + # Command + kubectl run pi --generator=job/v1 --image=perl --restart=OnFailure -- perl -Mbignum=bpi -wle 'print bpi(20)' "${kube_flags[@]}" + # Post-Condition: Job "pi" is created + kube::test::get_object_assert jobs "{{range.items}}{{$id_field}}:{{end}}" 'pi:' + # Clean up + kubectl delete jobs pi "${kube_flags[@]}" # Post-condition: no pods exist. kube::test::get_object_assert pods "{{range.items}}{{$id_field}}:{{end}}" '' # Pre-Condition: no Deployment exists @@ -853,7 +859,7 @@ __EOF__ # Command [[ "$(kubectl create secret generic test-secret --namespace=test-secrets --from-literal=key1=value1 --output=go-template --template=\"{{.metadata.name}}:\" | grep 'test-secret:')" ]] ## Clean-up - kubectl delete secret test-secret --namespace=test-secrets + kubectl delete secret test-secret --namespace=test-secrets # Clean up kubectl delete namespace test-secrets @@ -884,7 +890,7 @@ __EOF__ # Clean-up kubectl delete configmap test-configmap --namespace=test-configmaps kubectl delete namespace test-configmaps - + #################### # Service Accounts # #################### diff --git a/pkg/kubectl/cmd/run.go b/pkg/kubectl/cmd/run.go index 2fb88b63933..2cf823c5992 100644 --- a/pkg/kubectl/cmd/run.go +++ b/pkg/kubectl/cmd/run.go @@ -88,7 +88,7 @@ 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 'deployment/v1beta1' if --restart=Always, otherwise the default is 'job/v1beta1'.") + cmd.Flags().String("generator", "", "The name of the API generator to use. Default is 'deployment/v1beta1' if --restart=Always, otherwise the default is 'job/v1'.") 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.") @@ -149,7 +149,7 @@ func Run(f *cmdutil.Factory, cmdIn io.Reader, cmdOut, cmdErr io.Writer, cmd *cob if restartPolicy == api.RestartPolicyAlways { generatorName = "deployment/v1beta1" } else { - generatorName = "job/v1beta1" + generatorName = "job/v1" } } generators := f.Generators("run") diff --git a/pkg/kubectl/cmd/util/factory.go b/pkg/kubectl/cmd/util/factory.go index 9964160fdbf..326697ce0fa 100644 --- a/pkg/kubectl/cmd/util/factory.go +++ b/pkg/kubectl/cmd/util/factory.go @@ -143,6 +143,7 @@ const ( HorizontalPodAutoscalerV1Beta1GeneratorName = "horizontalpodautoscaler/v1beta1" DeploymentV1Beta1GeneratorName = "deployment/v1beta1" JobV1Beta1GeneratorName = "job/v1beta1" + JobV1GeneratorName = "job/v1" NamespaceV1GeneratorName = "namespace/v1" SecretV1GeneratorName = "secret/v1" SecretForDockerRegistryV1GeneratorName = "secret-for-docker-registry/v1" @@ -161,6 +162,7 @@ func DefaultGenerators(cmdName string) map[string]kubectl.Generator { RunPodV1GeneratorName: kubectl.BasicPod{}, DeploymentV1Beta1GeneratorName: kubectl.DeploymentV1Beta1{}, JobV1Beta1GeneratorName: kubectl.JobV1Beta1{}, + JobV1GeneratorName: kubectl.JobV1{}, } generators["autoscale"] = map[string]kubectl.Generator{ HorizontalPodAutoscalerV1Beta1GeneratorName: kubectl.HorizontalPodAutoscalerV1Beta1{}, diff --git a/pkg/kubectl/run.go b/pkg/kubectl/run.go index 8cf4325b9c2..688b570e7d1 100644 --- a/pkg/kubectl/run.go +++ b/pkg/kubectl/run.go @@ -24,6 +24,8 @@ import ( "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api/resource" "k8s.io/kubernetes/pkg/api/unversioned" + "k8s.io/kubernetes/pkg/api/v1" + batchv1 "k8s.io/kubernetes/pkg/apis/batch/v1" "k8s.io/kubernetes/pkg/apis/extensions" "k8s.io/kubernetes/pkg/runtime" "k8s.io/kubernetes/pkg/util/validation" @@ -187,6 +189,24 @@ func getEnvs(genericParams map[string]interface{}) ([]api.EnvVar, error) { return envs, nil } +func getV1Envs(genericParams map[string]interface{}) ([]v1.EnvVar, error) { + var envs []v1.EnvVar + envStrings, found := genericParams["env"] + if found { + if envStringArray, isArray := envStrings.([]string); isArray { + var err error + envs, err = parseV1Envs(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 { @@ -256,7 +276,7 @@ func (JobV1Beta1) Generate(genericParams map[string]interface{}) (runtime.Object restartPolicy := api.RestartPolicy(params["restart"]) if len(restartPolicy) == 0 { - restartPolicy = api.RestartPolicyAlways + restartPolicy = api.RestartPolicyNever } podSpec.RestartPolicy = restartPolicy @@ -282,6 +302,97 @@ func (JobV1Beta1) Generate(genericParams map[string]interface{}) (runtime.Object return &job, nil } +type JobV1 struct{} + +func (JobV1) 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 (JobV1) Generate(genericParams map[string]interface{}) (runtime.Object, error) { + args, err := getArgs(genericParams) + if err != nil { + return nil, err + } + + envs, err := getV1Envs(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 := makeV1PodSpec(params, name) + if err != nil { + return nil, err + } + + if err = updateV1PodContainers(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 := updateV1PodPorts(params, podSpec); err != nil { + return nil, err + } + + restartPolicy := v1.RestartPolicy(params["restart"]) + if len(restartPolicy) == 0 { + restartPolicy = v1.RestartPolicyNever + } + podSpec.RestartPolicy = restartPolicy + + job := batchv1.Job{ + ObjectMeta: v1.ObjectMeta{ + Name: name, + Labels: labels, + }, + Spec: batchv1.JobSpec{ + Template: v1.PodTemplateSpec{ + ObjectMeta: v1.ObjectMeta{ + Labels: labels, + }, + Spec: *podSpec, + }, + }, + } + + return &job, nil +} + type BasicReplicationController struct{} func (BasicReplicationController) ParamNames() []GeneratorParam { @@ -327,6 +438,30 @@ func populateResourceList(spec string) (api.ResourceList, error) { return result, nil } +// populateResourceList takes strings of form =,= +func populateV1ResourceList(spec string) (v1.ResourceList, error) { + // empty input gets a nil response to preserve generator test expected behaviors + if spec == "" { + return nil, nil + } + + result := v1.ResourceList{} + resourceStatements := strings.Split(spec, ",") + for _, resourceStatement := range resourceStatements { + parts := strings.Split(resourceStatement, "=") + if len(parts) != 2 { + return nil, fmt.Errorf("Invalid argument syntax %v, expected =", resourceStatement) + } + resourceName := v1.ResourceName(parts[0]) + resourceQuantity, err := resource.ParseQuantity(parts[1]) + if err != nil { + return nil, err + } + result[resourceName] = *resourceQuantity + } + return result, nil +} + // HandleResourceRequirements parses the limits and requests parameters if specified func HandleResourceRequirements(params map[string]string) (api.ResourceRequirements, error) { result := api.ResourceRequirements{} @@ -343,6 +478,22 @@ func HandleResourceRequirements(params map[string]string) (api.ResourceRequireme return result, nil } +// HandleResourceRequirements parses the limits and requests parameters if specified +func handleV1ResourceRequirements(params map[string]string) (v1.ResourceRequirements, error) { + result := v1.ResourceRequirements{} + limits, err := populateV1ResourceList(params["limits"]) + if err != nil { + return result, err + } + result.Limits = limits + requests, err := populateV1ResourceList(params["requests"]) + if err != nil { + return result, err + } + result.Requests = requests + return result, nil +} + func makePodSpec(params map[string]string, name string) (*api.PodSpec, error) { stdin, err := GetBool(params, "stdin", false) if err != nil { @@ -373,6 +524,36 @@ func makePodSpec(params map[string]string, name string) (*api.PodSpec, error) { return &spec, nil } +func makeV1PodSpec(params map[string]string, name string) (*v1.PodSpec, error) { + stdin, err := GetBool(params, "stdin", false) + if err != nil { + return nil, err + } + + tty, err := GetBool(params, "tty", false) + if err != nil { + return nil, err + } + + resourceRequirements, err := handleV1ResourceRequirements(params) + if err != nil { + return nil, err + } + + spec := v1.PodSpec{ + Containers: []v1.Container{ + { + Name: name, + Image: params["image"], + Stdin: stdin, + TTY: tty, + Resources: resourceRequirements, + }, + }, + } + return &spec, nil +} + func (BasicReplicationController) Generate(genericParams map[string]interface{}) (runtime.Object, error) { args, err := getArgs(genericParams) if err != nil { @@ -455,6 +636,25 @@ func updatePodContainers(params map[string]string, args []string, envs []api.Env return nil } +func updateV1PodContainers(params map[string]string, args []string, envs []v1.EnvVar, podSpec *v1.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 @@ -489,6 +689,40 @@ func updatePodPorts(params map[string]string, podSpec *api.PodSpec) (err error) return nil } +func updateV1PodPorts(params map[string]string, podSpec *v1.PodSpec) (err error) { + port := -1 + hostPort := -1 + if len(params["port"]) > 0 { + port, err = strconv.Atoi(params["port"]) + if err != nil { + return err + } + } + + if len(params["hostport"]) > 0 { + hostPort, err = strconv.Atoi(params["hostport"]) + if err != nil { + return err + } + if hostPort > 0 && port < 0 { + return fmt.Errorf("--hostport requires --port to be specified") + } + } + + // Don't include the port if it was not specified. + if port > 0 { + podSpec.Containers[0].Ports = []v1.ContainerPort{ + { + ContainerPort: int32(port), + }, + } + if hostPort > 0 { + podSpec.Containers[0].Ports[0].HostPort = int32(hostPort) + } + } + return nil +} + type BasicPod struct{} func (BasicPod) ParamNames() []GeneratorParam { @@ -609,6 +843,24 @@ func parseEnvs(envArray []string) ([]api.EnvVar, error) { return envs, nil } +func parseV1Envs(envArray []string) ([]v1.EnvVar, error) { + envs := []v1.EnvVar{} + for _, env := range envArray { + pos := strings.Index(env, "=") + if pos == -1 { + return nil, fmt.Errorf("invalid env: %v", env) + } + name := env[:pos] + value := env[pos+1:] + if len(name) == 0 || !validation.IsCIdentifier(name) || len(value) == 0 { + return nil, fmt.Errorf("invalid env: %v", env) + } + envVar := v1.EnvVar{Name: name, Value: value} + envs = append(envs, envVar) + } + return envs, nil +} + func newBool(val bool) *bool { p := new(bool) *p = val