Add --restart support to kubectl run

This commit is contained in:
Brendan Burns
2015-08-04 12:54:17 -07:00
parent fbb5ce6636
commit e42d6aa255
15 changed files with 461 additions and 42 deletions

View File

@@ -61,7 +61,7 @@ func NewCmdRun(f *cmdutil.Factory, cmdIn io.Reader, cmdOut, cmdErr io.Writer) *c
},
}
cmdutil.AddPrinterFlags(cmd)
cmd.Flags().String("generator", "run/v1", "The name of the API generator to use. Default is 'run-controller/v1'.")
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'.")
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.")
@@ -73,6 +73,7 @@ func NewCmdRun(f *cmdutil.Factory, cmdIn io.Reader, cmdOut, cmdErr io.Writer) *c
cmd.Flags().BoolP("stdin", "i", false, "Keep stdin open on the container(s) in the pod, even if nothing is attached.")
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'")
return cmd
}
@@ -105,10 +106,24 @@ func Run(f *cmdutil.Factory, cmdIn io.Reader, cmdOut, cmdErr io.Writer, cmd *cob
return err
}
restartPolicy, err := getRestartPolicy(cmd, interactive)
if err != nil {
return err
}
if restartPolicy != api.RestartPolicyAlways && replicas != 1 {
return cmdutil.UsageError(cmd, fmt.Sprintf("--restart=%s requires that --repliacs=1, found %d", restartPolicy, replicas))
}
generatorName := cmdutil.GetFlagString(cmd, "generator")
if len(generatorName) == 0 {
if restartPolicy == api.RestartPolicyAlways {
generatorName = "run/v1"
} else {
generatorName = "run-pod/v1"
}
}
generator, found := f.Generator(generatorName)
if !found {
return cmdutil.UsageError(cmd, fmt.Sprintf("Generator: %s not found.", generator))
return cmdutil.UsageError(cmd, fmt.Sprintf("Generator: %s not found.", generatorName))
}
names := generator.ParamNames()
params := kubectl.MakeParams(cmd, names)
@@ -119,14 +134,20 @@ func Run(f *cmdutil.Factory, cmdIn io.Reader, cmdOut, cmdErr io.Writer, cmd *cob
return err
}
controller, err := generator.Generate(params)
obj, err := generator.Generate(params)
if err != nil {
return err
}
inline := cmdutil.GetFlagString(cmd, "overrides")
if len(inline) > 0 {
controller, err = cmdutil.Merge(controller, inline, "ReplicationController")
var objType string
if restartPolicy == api.RestartPolicyAlways {
objType = "ReplicationController"
} else {
objType = "Pod"
}
obj, err = cmdutil.Merge(obj, inline, objType)
if err != nil {
return err
}
@@ -134,7 +155,11 @@ func Run(f *cmdutil.Factory, cmdIn io.Reader, cmdOut, cmdErr io.Writer, cmd *cob
// TODO: extract this flag to a central location, when such a location exists.
if !cmdutil.GetFlagBool(cmd, "dry-run") {
controller, err = client.ReplicationControllers(namespace).Create(controller.(*api.ReplicationController))
if restartPolicy == api.RestartPolicyAlways {
obj, err = client.ReplicationControllers(namespace).Create(obj.(*api.ReplicationController))
} else {
obj, err = client.Pods(namespace).Create(obj.(*api.Pod))
}
if err != nil {
return err
}
@@ -168,9 +193,13 @@ func Run(f *cmdutil.Factory, cmdIn io.Reader, cmdOut, cmdErr io.Writer, cmd *cob
return err
}
opts.Client = client
return handleAttach(client, controller.(*api.ReplicationController), opts)
if restartPolicy == api.RestartPolicyAlways {
return handleAttachReplicationController(client, obj.(*api.ReplicationController), opts)
} else {
return handleAttachPod(client, obj.(*api.Pod), opts)
}
}
return f.PrintObject(cmd, controller, cmdOut)
return f.PrintObject(cmd, obj, cmdOut)
}
func waitForPodRunning(c *client.Client, pod *api.Pod, out io.Writer) error {
@@ -197,7 +226,7 @@ func waitForPodRunning(c *client.Client, pod *api.Pod, out io.Writer) error {
}
}
func handleAttach(c *client.Client, controller *api.ReplicationController, opts *AttachOptions) error {
func handleAttachReplicationController(c *client.Client, controller *api.ReplicationController, opts *AttachOptions) error {
var pods *api.PodList
for pods == nil || len(pods.Items) == 0 {
var err error
@@ -210,7 +239,10 @@ func handleAttach(c *client.Client, controller *api.ReplicationController, opts
}
}
pod := &pods.Items[0]
return handleAttachPod(c, pod, opts)
}
func handleAttachPod(c *client.Client, pod *api.Pod, opts *AttachOptions) error {
if err := waitForPodRunning(c, pod, opts.Out); err != nil {
return err
}
@@ -219,3 +251,24 @@ func handleAttach(c *client.Client, controller *api.ReplicationController, opts
opts.Namespace = pod.Namespace
return opts.Run()
}
func getRestartPolicy(cmd *cobra.Command, interactive bool) (api.RestartPolicy, error) {
restart := cmdutil.GetFlagString(cmd, "restart")
if len(restart) == 0 {
if interactive {
return api.RestartPolicyOnFailure, nil
} else {
return api.RestartPolicyAlways, nil
}
}
switch api.RestartPolicy(restart) {
case api.RestartPolicyAlways:
return api.RestartPolicyAlways, nil
case api.RestartPolicyOnFailure:
return api.RestartPolicyOnFailure, nil
case api.RestartPolicyNever:
return api.RestartPolicyNever, nil
default:
return "", cmdutil.UsageError(cmd, fmt.Sprintf("invalid restart policy: %s", restart))
}
}

View File

@@ -0,0 +1,80 @@
/*
Copyright 2014 The Kubernetes Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd
import (
"testing"
"github.com/spf13/cobra"
"k8s.io/kubernetes/pkg/api"
)
func TestGetRestartPolicy(t *testing.T) {
tests := []struct {
input string
interactive bool
expected api.RestartPolicy
expectErr bool
}{
{
input: "",
expected: api.RestartPolicyAlways,
},
{
input: "",
interactive: true,
expected: api.RestartPolicyOnFailure,
},
{
input: string(api.RestartPolicyAlways),
interactive: true,
expected: api.RestartPolicyAlways,
},
{
input: string(api.RestartPolicyNever),
interactive: true,
expected: api.RestartPolicyNever,
},
{
input: string(api.RestartPolicyAlways),
expected: api.RestartPolicyAlways,
},
{
input: string(api.RestartPolicyNever),
expected: api.RestartPolicyNever,
},
{
input: "foo",
expectErr: true,
},
}
for _, test := range tests {
cmd := &cobra.Command{}
cmd.Flags().String("restart", "", "dummy restart flag")
cmd.Flags().Lookup("restart").Value.Set(test.input)
policy, err := getRestartPolicy(cmd, test.interactive)
if test.expectErr && err == nil {
t.Error("unexpected non-error")
}
if !test.expectErr && err != nil {
t.Errorf("unexpected error: %v", err)
}
if !test.expectErr && policy != test.expected {
t.Errorf("expected: %s, saw: %s (%s:%v)", test.expected, policy, test.input, test.interactive)
}
}
}

View File

@@ -97,6 +97,7 @@ func NewFactory(optionalClientConfig clientcmd.ClientConfig) *Factory {
generators := map[string]kubectl.Generator{
"run/v1": kubectl.BasicReplicationController{},
"run-pod/v1": kubectl.BasicPod{},
"service/v1": kubectl.ServiceGeneratorV1{},
"service/v2": kubectl.ServiceGeneratorV2{},
}

View File

@@ -40,6 +40,30 @@ func (BasicReplicationController) ParamNames() []GeneratorParam {
}
}
func makePodSpec(params map[string]string, name string) (*api.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
}
spec := api.PodSpec{
Containers: []api.Container{
{
Name: name,
Image: params["image"],
Stdin: stdin,
TTY: tty,
},
},
}
return &spec, nil
}
func (BasicReplicationController) Generate(params map[string]string) (runtime.Object, error) {
name, found := params["name"]
if !found || len(name) == 0 {
@@ -66,16 +90,11 @@ func (BasicReplicationController) Generate(params map[string]string) (runtime.Ob
if err != nil {
return nil, err
}
stdin, err := GetBool(params, "stdin", false)
podSpec, err := makePodSpec(params, name)
if err != nil {
return nil, err
}
tty, err := GetBool(params, "tty", false)
if err != nil {
return nil, err
}
controller := api.ReplicationController{
ObjectMeta: api.ObjectMeta{
Name: name,
@@ -88,49 +107,119 @@ func (BasicReplicationController) Generate(params map[string]string) (runtime.Ob
ObjectMeta: api.ObjectMeta{
Labels: labels,
},
Spec: api.PodSpec{
Containers: []api.Container{
{
Name: name,
Image: params["image"],
Stdin: stdin,
TTY: tty,
},
},
},
Spec: *podSpec,
},
},
}
if err := updatePodPorts(params, &controller.Spec.Template.Spec); err != nil {
return nil, err
}
return &controller, nil
}
func updatePodPorts(params map[string]string, podSpec *api.PodSpec) (err error) {
port := -1
hostPort := -1
if len(params["port"]) > 0 {
port, err = strconv.Atoi(params["port"])
if err != nil {
return nil, err
return err
}
}
if len(params["hostport"]) > 0 {
hostPort, err = strconv.Atoi(params["hostport"])
if err != nil {
return nil, err
return err
}
if hostPort > 0 && port < 0 {
return nil, fmt.Errorf("--hostport requires --port to be specified")
return fmt.Errorf("--hostport requires --port to be specified")
}
}
// Don't include the port if it was not specified.
if port > 0 {
controller.Spec.Template.Spec.Containers[0].Ports = []api.ContainerPort{
podSpec.Containers[0].Ports = []api.ContainerPort{
{
ContainerPort: port,
},
}
if hostPort > 0 {
controller.Spec.Template.Spec.Containers[0].Ports[0].HostPort = hostPort
podSpec.Containers[0].Ports[0].HostPort = hostPort
}
}
return &controller, nil
return nil
}
type BasicPod struct{}
func (BasicPod) ParamNames() []GeneratorParam {
return []GeneratorParam{
{"labels", false},
{"default-name", false},
{"name", true},
{"image", true},
{"port", false},
{"hostport", false},
{"stdin", false},
{"tty", false},
{"restart", false},
}
}
func (BasicPod) Generate(params map[string]string) (runtime.Object, error) {
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.")
}
}
// 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
}
}
stdin, err := GetBool(params, "stdin", false)
if err != nil {
return nil, err
}
tty, err := GetBool(params, "tty", false)
if err != nil {
return nil, err
}
restartPolicy := api.RestartPolicy(params["restart"])
if len(restartPolicy) == 0 {
restartPolicy = api.RestartPolicyAlways
}
pod := api.Pod{
ObjectMeta: api.ObjectMeta{
Name: name,
Labels: labels,
},
Spec: api.PodSpec{
Containers: []api.Container{
{
Name: name,
Image: params["image"],
ImagePullPolicy: api.PullIfNotPresent,
Stdin: stdin,
TTY: tty,
},
},
DNSPolicy: api.DNSClusterFirst,
RestartPolicy: restartPolicy,
},
}
if err := updatePodPorts(params, &pod.Spec); err != nil {
return nil, err
}
return &pod, nil
}

View File

@@ -190,3 +190,140 @@ func TestGenerate(t *testing.T) {
}
}
}
func TestGeneratePod(t *testing.T) {
tests := []struct {
params map[string]string
expected *api.Pod
expectErr bool
}{
{
params: map[string]string{
"name": "foo",
"image": "someimage",
"port": "-1",
},
expected: &api.Pod{
ObjectMeta: api.ObjectMeta{
Name: "foo",
},
Spec: api.PodSpec{
Containers: []api.Container{
{
Name: "foo",
Image: "someimage",
ImagePullPolicy: api.PullIfNotPresent,
},
},
DNSPolicy: api.DNSClusterFirst,
RestartPolicy: api.RestartPolicyAlways,
},
},
},
{
params: map[string]string{
"name": "foo",
"image": "someimage",
"port": "80",
},
expected: &api.Pod{
ObjectMeta: api.ObjectMeta{
Name: "foo",
},
Spec: api.PodSpec{
Containers: []api.Container{
{
Name: "foo",
Image: "someimage",
ImagePullPolicy: api.PullIfNotPresent,
Ports: []api.ContainerPort{
{
ContainerPort: 80,
},
},
},
},
DNSPolicy: api.DNSClusterFirst,
RestartPolicy: api.RestartPolicyAlways,
},
},
},
{
params: map[string]string{
"name": "foo",
"image": "someimage",
"port": "80",
"hostport": "80",
},
expected: &api.Pod{
ObjectMeta: api.ObjectMeta{
Name: "foo",
},
Spec: api.PodSpec{
Containers: []api.Container{
{
Name: "foo",
Image: "someimage",
ImagePullPolicy: api.PullIfNotPresent,
Ports: []api.ContainerPort{
{
ContainerPort: 80,
HostPort: 80,
},
},
},
},
DNSPolicy: api.DNSClusterFirst,
RestartPolicy: api.RestartPolicyAlways,
},
},
},
{
params: map[string]string{
"name": "foo",
"image": "someimage",
"hostport": "80",
},
expected: nil,
expectErr: true,
},
{
params: map[string]string{
"name": "foo",
"image": "someimage",
"replicas": "1",
"labels": "foo=bar,baz=blah",
},
expected: &api.Pod{
ObjectMeta: api.ObjectMeta{
Name: "foo",
Labels: map[string]string{"foo": "bar", "baz": "blah"},
},
Spec: api.PodSpec{
Containers: []api.Container{
{
Name: "foo",
Image: "someimage",
ImagePullPolicy: api.PullIfNotPresent,
},
},
DNSPolicy: api.DNSClusterFirst,
RestartPolicy: api.RestartPolicyAlways,
},
},
},
}
generator := BasicPod{}
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.(*api.Pod), test.expected) {
t.Errorf("\nexpected:\n%#v\nsaw:\n%#v", test.expected, obj.(*api.Pod))
}
}
}