Add optional arguments to kubectl run ...

This commit is contained in:
Brendan Burns 2015-08-11 22:48:00 -07:00
parent a6148e79c3
commit 586931fe16
12 changed files with 305 additions and 57 deletions

View File

@ -630,6 +630,7 @@ _kubectl_run()
flags_completion=()
flags+=("--attach")
flags+=("--command")
flags+=("--dry-run")
flags+=("--generator=")
flags+=("--help")

View File

@ -22,6 +22,10 @@ Creates a replication controller to manage the created container(s).
\fB\-\-attach\fP=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.
.PP
\fB\-\-command\fP=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.
.PP
\fB\-\-dry\-run\fP=false
If true, only print the object that would be sent, without sending it.
@ -207,6 +211,15 @@ $ 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 the nginx container using the default command, but use custom arguments (arg1 .. argN) for that command.
$ kubectl run nginx \-\-image=nginx \-\- <arg1> <arg2> ... <argN>
# Start the nginx container using a different command and custom arguments
$ kubectl run nginx \-\-image=nginx \-\-command \-\- <cmd> <arg1> ... <argN>
.fi
.RE

View File

@ -59,12 +59,22 @@ $ 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 the nginx container using the default command, but use custom arguments (arg1 .. argN) for that command.
$ kubectl run nginx --image=nginx -- <arg1> <arg2> ... <argN>
# Start the nginx container using a different command and custom arguments
$ kubectl run nginx --image=nginx --command -- <cmd> <arg1> ... <argN>
```
### Options
```
--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.
--generator="": The name of the API generator to use. Default is 'run/v1' if --restart=Always, otherwise the default is 'run-pod/v1'.
-h, --help[=false]: help for run
@ -117,7 +127,7 @@ $ kubectl run nginx --image=nginx --overrides='{ "apiVersion": "v1", "spec": { .
* [kubectl](kubectl.md) - kubectl controls the Kubernetes cluster manager
###### Auto generated by spf13/cobra at 2015-08-12 23:41:01.307766241 +0000 UTC
###### Auto generated by spf13/cobra at 2015-08-13 16:41:44.465440991 +0000 UTC
<!-- BEGIN MUNGE: GENERATED_ANALYTICS -->
[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/docs/user-guide/kubectl/kubectl_run.md?pixel)]()

View File

@ -127,8 +127,8 @@ func RunExpose(f *cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []str
names := generator.ParamNames()
params := kubectl.MakeParams(cmd, names)
params["default-name"] = info.Name
if s, found := params["selector"]; !found || len(s) == 0 || cmdutil.GetFlagInt(cmd, "port") < 1 {
if len(s) == 0 {
if s, found := params["selector"]; !found || kubectl.IsZero(s) || cmdutil.GetFlagInt(cmd, "port") < 1 {
if kubectl.IsZero(s) {
s, err := f.PodSelectorForObject(inputObject)
if err != nil {
return cmdutil.UsageError(cmd, fmt.Sprintf("couldn't find selectors via --selector flag or introspection: %s", err))
@ -160,7 +160,7 @@ func RunExpose(f *cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []str
if cmdutil.GetFlagBool(cmd, "create-external-load-balancer") {
params["create-external-load-balancer"] = "true"
}
if len(params["labels"]) == 0 {
if kubectl.IsZero(params["labels"]) {
labels, err := f.LabelsForObject(inputObject)
if err != nil {
return err

View File

@ -39,7 +39,41 @@ func TestRunExposeService(t *testing.T) {
output runtime.Object
expected string
status int
podSelector string
}{
{
name: "expose-service-from-service-no-selector",
args: []string{"service", "baz"},
ns: "test",
calls: map[string]string{
"GET": "/namespaces/test/services/baz",
"POST": "/namespaces/test/services",
},
input: &api.Service{
ObjectMeta: api.ObjectMeta{Name: "baz", Namespace: "test", ResourceVersion: "12"},
TypeMeta: api.TypeMeta{Kind: "Service", APIVersion: "v1"},
Spec: api.ServiceSpec{
Selector: map[string]string{"app": "go"},
},
},
podSelector: "app=go",
flags: map[string]string{"protocol": "UDP", "port": "14", "name": "foo", "labels": "svc=test"},
output: &api.Service{
ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: "test", ResourceVersion: "12", Labels: map[string]string{"svc": "test"}},
TypeMeta: api.TypeMeta{Kind: "Service", APIVersion: "v1"},
Spec: api.ServiceSpec{
Ports: []api.ServicePort{
{
Name: "default",
Protocol: api.Protocol("UDP"),
Port: 14,
},
},
Selector: map[string]string{"app": "go"},
},
},
status: 200,
},
{
name: "expose-service-from-service",
args: []string{"service", "baz"},
@ -194,6 +228,8 @@ func TestRunExposeService(t *testing.T) {
}),
}
tf.Namespace = test.ns
f.PodSelectorForObject = func(obj runtime.Object) (string, error) { return test.podSelector, nil }
buf := bytes.NewBuffer([]byte{})
cmd := NewCmdExposeService(f, buf)

View File

@ -44,7 +44,16 @@ $ kubectl run nginx --image=nginx --replicas=5
$ 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": { ... } }'`
$ 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 the nginx container using the default command, but use custom arguments (arg1 .. argN) for that command.
$ kubectl run nginx --image=nginx -- <arg1> <arg2> ... <argN>
# Start the nginx container using a different command and custom arguments
$ kubectl run nginx --image=nginx --command -- <cmd> <arg1> ... <argN>`
)
func NewCmdRun(f *cmdutil.Factory, cmdIn io.Reader, cmdOut, cmdErr io.Writer) *cobra.Command {
@ -74,6 +83,7 @@ func NewCmdRun(f *cmdutil.Factory, cmdIn io.Reader, cmdOut, cmdErr io.Writer) *c
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().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().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.")
return cmd
}
@ -82,7 +92,7 @@ func Run(f *cmdutil.Factory, cmdIn io.Reader, cmdOut, cmdErr io.Writer, cmd *cob
printDeprecationWarning("run", "run-container")
}
if len(args) != 1 {
if len(args) == 0 {
return cmdutil.UsageError(cmd, "NAME is required for run")
}
@ -128,7 +138,9 @@ func Run(f *cmdutil.Factory, cmdIn io.Reader, cmdOut, cmdErr io.Writer, cmd *cob
names := generator.ParamNames()
params := kubectl.MakeParams(cmd, names)
params["name"] = args[0]
if len(args) > 1 {
params["args"] = args[1:]
}
err = kubectl.ValidateParams(names, params)
if err != nil {
return err

View File

@ -18,6 +18,7 @@ package kubectl
import (
"fmt"
"reflect"
"strconv"
"strings"
@ -35,17 +36,24 @@ type GeneratorParam struct {
// Generator is an interface for things that can generate API objects from input parameters.
type Generator interface {
// Generate creates an API object given a set of parameters
Generate(params map[string]string) (runtime.Object, error)
Generate(params map[string]interface{}) (runtime.Object, error)
// ParamNames returns the list of parameters that this generator uses
ParamNames() []GeneratorParam
}
func IsZero(i interface{}) bool {
if i == nil {
return true
}
return reflect.DeepEqual(i, reflect.Zero(reflect.TypeOf(i)).Interface())
}
// ValidateParams ensures that all required params are present in the params map
func ValidateParams(paramSpec []GeneratorParam, params map[string]string) error {
func ValidateParams(paramSpec []GeneratorParam, params map[string]interface{}) error {
for ix := range paramSpec {
if paramSpec[ix].Required {
value, found := params[paramSpec[ix].Name]
if !found || len(value) == 0 {
if !found || IsZero(value) {
return fmt.Errorf("Parameter: %s is required", paramSpec[ix].Name)
}
}
@ -54,8 +62,8 @@ func ValidateParams(paramSpec []GeneratorParam, params map[string]string) error
}
// MakeParams is a utility that creates generator parameters from a command line
func MakeParams(cmd *cobra.Command, params []GeneratorParam) map[string]string {
result := map[string]string{}
func MakeParams(cmd *cobra.Command, params []GeneratorParam) map[string]interface{} {
result := map[string]interface{}{}
for ix := range params {
f := cmd.Flags().Lookup(params[ix].Name)
if f != nil {
@ -74,7 +82,11 @@ func MakeLabels(labels map[string]string) string {
}
// ParseLabels turns a string representation of a label set into a map[string]string
func ParseLabels(labelString string) (map[string]string, error) {
func ParseLabels(labelSpec interface{}) (map[string]string, error) {
labelString, isString := labelSpec.(string)
if !isString {
return nil, fmt.Errorf("expected string, found %v", labelSpec)
}
if len(labelString) == 0 {
return nil, fmt.Errorf("no label spec passed")
}

View File

@ -23,29 +23,55 @@ import (
"github.com/spf13/cobra"
)
type TestStruct struct {
val int
}
func TestIsZero(t *testing.T) {
tests := []struct {
val interface{}
expectZero bool
}{
{"", true},
{nil, true},
{0, true},
{TestStruct{}, true},
{"foo", false},
{1, false},
{TestStruct{val: 2}, false},
}
for _, test := range tests {
output := IsZero(test.val)
if output != test.expectZero {
t.Errorf("expected: %v, saw %v", test.expectZero, output)
}
}
}
func TestValidateParams(t *testing.T) {
tests := []struct {
paramSpec []GeneratorParam
params map[string]string
params map[string]interface{}
valid bool
}{
{
paramSpec: []GeneratorParam{},
params: map[string]string{},
params: map[string]interface{}{},
valid: true,
},
{
paramSpec: []GeneratorParam{
{Name: "foo"},
},
params: map[string]string{},
params: map[string]interface{}{},
valid: true,
},
{
paramSpec: []GeneratorParam{
{Name: "foo", Required: true},
},
params: map[string]string{
params: map[string]interface{}{
"foo": "bar",
},
valid: true,
@ -54,7 +80,7 @@ func TestValidateParams(t *testing.T) {
paramSpec: []GeneratorParam{
{Name: "foo", Required: true},
},
params: map[string]string{
params: map[string]interface{}{
"baz": "blah",
"foo": "bar",
},
@ -65,7 +91,7 @@ func TestValidateParams(t *testing.T) {
{Name: "foo", Required: true},
{Name: "baz", Required: true},
},
params: map[string]string{
params: map[string]interface{}{
"baz": "blah",
"foo": "bar",
},
@ -76,7 +102,7 @@ func TestValidateParams(t *testing.T) {
{Name: "foo", Required: true},
{Name: "baz", Required: true},
},
params: map[string]string{
params: map[string]interface{}{
"foo": "bar",
},
valid: false,
@ -103,7 +129,7 @@ func TestMakeParams(t *testing.T) {
{Name: "foo", Required: true},
{Name: "baz", Required: true},
}
expected := map[string]string{
expected := map[string]interface{}{
"foo": "bar",
"baz": "blah",
}

View File

@ -37,6 +37,8 @@ func (BasicReplicationController) ParamNames() []GeneratorParam {
{"hostport", false},
{"stdin", false},
{"tty", false},
{"command", false},
{"args", false},
}
}
@ -64,7 +66,25 @@ func makePodSpec(params map[string]string, name string) (*api.PodSpec, error) {
return &spec, nil
}
func (BasicReplicationController) Generate(params map[string]string) (runtime.Object, 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")
}
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
}
name, found := params["name"]
if !found || len(name) == 0 {
name, found = params["default-name"]
@ -95,6 +115,18 @@ func (BasicReplicationController) Generate(params map[string]string) (runtime.Ob
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
}
}
controller := api.ReplicationController{
ObjectMeta: api.ObjectMeta{
Name: name,
@ -164,10 +196,30 @@ func (BasicPod) ParamNames() []GeneratorParam {
{"stdin", false},
{"tty", false},
{"restart", false},
{"command", false},
{"args", false},
}
}
func (BasicPod) Generate(params map[string]string) (runtime.Object, error) {
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")
}
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
}
name, found := params["name"]
if !found || len(name) == 0 {
name, found = params["default-name"]
@ -218,6 +270,17 @@ func (BasicPod) Generate(params map[string]string) (runtime.Object, error) {
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 err := updatePodPorts(params, &pod.Spec); err != nil {
return nil, err
}

View File

@ -25,12 +25,12 @@ import (
func TestGenerate(t *testing.T) {
tests := []struct {
params map[string]string
params map[string]interface{}
expected *api.ReplicationController
expectErr bool
}{
{
params: map[string]string{
params: map[string]interface{}{
"name": "foo",
"image": "someimage",
"replicas": "1",
@ -61,7 +61,74 @@ func TestGenerate(t *testing.T) {
},
},
{
params: map[string]string{
params: map[string]interface{}{
"name": "foo",
"image": "someimage",
"replicas": "1",
"port": "-1",
"args": []string{"bar", "baz", "blah"},
},
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",
Args: []string{"bar", "baz", "blah"},
},
},
},
},
},
},
},
{
params: map[string]interface{}{
"name": "foo",
"image": "someimage",
"replicas": "1",
"port": "-1",
"args": []string{"bar", "baz", "blah"},
"command": "true",
},
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",
Command: []string{"bar", "baz", "blah"},
},
},
},
},
},
},
},
{
params: map[string]interface{}{
"name": "foo",
"image": "someimage",
"replicas": "1",
@ -97,7 +164,7 @@ func TestGenerate(t *testing.T) {
},
},
{
params: map[string]string{
params: map[string]interface{}{
"name": "foo",
"image": "someimage",
"replicas": "1",
@ -135,7 +202,7 @@ func TestGenerate(t *testing.T) {
},
},
{
params: map[string]string{
params: map[string]interface{}{
"name": "foo",
"image": "someimage",
"replicas": "1",
@ -145,7 +212,7 @@ func TestGenerate(t *testing.T) {
expectErr: true,
},
{
params: map[string]string{
params: map[string]interface{}{
"name": "foo",
"image": "someimage",
"replicas": "1",
@ -193,12 +260,12 @@ func TestGenerate(t *testing.T) {
func TestGeneratePod(t *testing.T) {
tests := []struct {
params map[string]string
params map[string]interface{}
expected *api.Pod
expectErr bool
}{
{
params: map[string]string{
params: map[string]interface{}{
"name": "foo",
"image": "someimage",
"port": "-1",
@ -221,7 +288,7 @@ func TestGeneratePod(t *testing.T) {
},
},
{
params: map[string]string{
params: map[string]interface{}{
"name": "foo",
"image": "someimage",
"port": "80",
@ -249,7 +316,7 @@ func TestGeneratePod(t *testing.T) {
},
},
{
params: map[string]string{
params: map[string]interface{}{
"name": "foo",
"image": "someimage",
"port": "80",
@ -279,7 +346,7 @@ func TestGeneratePod(t *testing.T) {
},
},
{
params: map[string]string{
params: map[string]interface{}{
"name": "foo",
"image": "someimage",
"hostport": "80",
@ -288,7 +355,7 @@ func TestGeneratePod(t *testing.T) {
expectErr: true,
},
{
params: map[string]string{
params: map[string]interface{}{
"name": "foo",
"image": "someimage",
"replicas": "1",

View File

@ -32,7 +32,7 @@ func (ServiceGeneratorV1) ParamNames() []GeneratorParam {
return paramNames()
}
func (ServiceGeneratorV1) Generate(params map[string]string) (runtime.Object, error) {
func (ServiceGeneratorV1) Generate(params map[string]interface{}) (runtime.Object, error) {
params["port-name"] = "default"
return generate(params)
}
@ -43,7 +43,7 @@ func (ServiceGeneratorV2) ParamNames() []GeneratorParam {
return paramNames()
}
func (ServiceGeneratorV2) Generate(params map[string]string) (runtime.Object, error) {
func (ServiceGeneratorV2) Generate(params map[string]interface{}) (runtime.Object, error) {
return generate(params)
}
@ -65,7 +65,15 @@ func paramNames() []GeneratorParam {
}
}
func generate(params map[string]string) (runtime.Object, error) {
func generate(genericParams map[string]interface{}) (runtime.Object, 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
}
selectorString, found := params["selector"]
if !found || len(selectorString) == 0 {
return nil, fmt.Errorf("'selector' is a required parameter.")

View File

@ -27,12 +27,12 @@ import (
func TestGenerateService(t *testing.T) {
tests := []struct {
generator Generator
params map[string]string
params map[string]interface{}
expected api.Service
}{
{
generator: ServiceGeneratorV2{},
params: map[string]string{
params: map[string]interface{}{
"selector": "foo=bar,baz=blah",
"name": "test",
"port": "80",
@ -61,7 +61,7 @@ func TestGenerateService(t *testing.T) {
{
generator: ServiceGeneratorV2{},
params: map[string]string{
params: map[string]interface{}{
"selector": "foo=bar,baz=blah",
"name": "test",
"port": "80",
@ -89,7 +89,7 @@ func TestGenerateService(t *testing.T) {
},
{
generator: ServiceGeneratorV2{},
params: map[string]string{
params: map[string]interface{}{
"selector": "foo=bar,baz=blah",
"labels": "key1=value1,key2=value2",
"name": "test",
@ -122,7 +122,7 @@ func TestGenerateService(t *testing.T) {
},
{
generator: ServiceGeneratorV2{},
params: map[string]string{
params: map[string]interface{}{
"selector": "foo=bar,baz=blah",
"name": "test",
"port": "80",
@ -152,7 +152,7 @@ func TestGenerateService(t *testing.T) {
},
{
generator: ServiceGeneratorV2{},
params: map[string]string{
params: map[string]interface{}{
"selector": "foo=bar,baz=blah",
"name": "test",
"port": "80",
@ -184,7 +184,7 @@ func TestGenerateService(t *testing.T) {
},
{
generator: ServiceGeneratorV2{},
params: map[string]string{
params: map[string]interface{}{
"selector": "foo=bar,baz=blah",
"name": "test",
"port": "80",
@ -214,7 +214,7 @@ func TestGenerateService(t *testing.T) {
},
{
generator: ServiceGeneratorV2{},
params: map[string]string{
params: map[string]interface{}{
"selector": "foo=bar,baz=blah",
"name": "test",
"port": "80",
@ -245,7 +245,7 @@ func TestGenerateService(t *testing.T) {
},
{
generator: ServiceGeneratorV1{},
params: map[string]string{
params: map[string]interface{}{
"selector": "foo=bar,baz=blah",
"name": "test",
"port": "80",
@ -274,7 +274,7 @@ func TestGenerateService(t *testing.T) {
},
{
generator: ServiceGeneratorV1{},
params: map[string]string{
params: map[string]interface{}{
"selector": "foo=bar,baz=blah",
"name": "test",
"port": "80",