Merge pull request #3662 from jlowdermilk/kubectl-stop

Add a kubectl stop command
pull/6/head
Brian Grant 2015-01-22 06:58:29 -08:00
commit a921086507
7 changed files with 393 additions and 10 deletions

View File

@ -394,6 +394,7 @@ Additional help topics:
kubectl rollingupdate Perform a rolling update of the given ReplicationController
kubectl resize Set a new size for a resizable resource (currently only Replication Controllers)
kubectl run-container Run a particular image on the cluster.
kubectl stop Gracefully shutdown a resource
Use "kubectl help [command]" for more information about that command.
```
@ -933,3 +934,46 @@ Usage:
```
#### stop
Gracefully shutdown a resource
Attempts to shutdown and delete a resource that supports graceful termination.
If the resource is resizable it will be resized to 0 before deletion.
Examples:
$ kubectl stop replicationcontroller foo
foo stopped
Usage:
```
kubectl stop <resource> <id> [flags]
Available Flags:
--alsologtostderr=false: log to standard error as well as files
--api-version="": The API version to use when talking to the server
-a, --auth-path="": Path to the auth info file. If missing, prompt the user. Only used if using https.
--certificate-authority="": Path to a cert. file for the certificate authority.
--client-certificate="": Path to a client key file for TLS.
--client-key="": Path to a client key file for TLS.
--cluster="": The name of the kubeconfig cluster to use
--context="": The name of the kubeconfig context to use
--insecure-skip-tls-verify=false: If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure.
--kubeconfig="": Path to the kubeconfig file to use for CLI requests.
--log_backtrace_at=:0: when logging hits line file:N, emit a stack trace
--log_dir=: If non-empty, write log files in this directory
--log_flush_frequency=5s: Maximum number of seconds between log flushes
--logtostderr=true: log to standard error instead of files
--match-server-version=false: Require server version to match client version
-n, --namespace="": If present, the namespace scope for this CLI request.
--ns-path="/home/username/.kubernetes_ns": Path to the namespace info file that holds the namespace context to use for CLI requests.
-s, --server="": The address of the Kubernetes API server
--stderrthreshold=2: logs at or above this threshold go to stderr
--token="": Bearer token for authentication to the API server.
--user="": The name of the kubeconfig user to use
--v=0: log level for V logs
--validate=false: If true, use a schema to validate the input before sending it
--vmodule=: comma-separated list of pattern=N settings for file-filtered logging
```

View File

@ -64,6 +64,8 @@ type Factory struct {
Printer func(cmd *cobra.Command, mapping *meta.RESTMapping, noHeaders bool) (kubectl.ResourcePrinter, error)
// Returns a Resizer for changing the size of the specified RESTMapping type or an error
Resizer func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.Resizer, error)
// Returns a Reaper for gracefully shutting down resources.
Reaper func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.Reaper, error)
// Returns a schema that can validate objects stored on disk.
Validator func(*cobra.Command) (validation.Schema, error)
// Returns the default namespace to use in cases where no other namespace is specified
@ -131,11 +133,14 @@ func NewFactory(optionalClientConfig clientcmd.ClientConfig) *Factory {
if err != nil {
return nil, err
}
resizer, ok := kubectl.ResizerFor(mapping.Kind, client)
if !ok {
return nil, fmt.Errorf("no resizer has been implemented for %q", mapping.Kind)
return kubectl.ResizerFor(mapping.Kind, client)
},
Reaper: func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.Reaper, error) {
client, err := clients.ClientForVersion(mapping.APIVersion)
if err != nil {
return nil, err
}
return resizer, nil
return kubectl.ReaperFor(mapping.Kind, client)
},
Validator: func(cmd *cobra.Command) (validation.Schema, error) {
if GetFlagBool(cmd, "validate") {
@ -206,6 +211,7 @@ Find more information at https://github.com/GoogleCloudPlatform/kubernetes.`,
cmds.AddCommand(f.NewCmdResize(out))
cmds.AddCommand(f.NewCmdRunContainer(out))
cmds.AddCommand(f.NewCmdStop(out))
return cmds
}

55
pkg/kubectl/cmd/stop.go Normal file
View File

@ -0,0 +1,55 @@
/*
Copyright 2014 Google Inc. 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 (
"fmt"
"io"
"github.com/spf13/cobra"
)
func (f *Factory) NewCmdStop(out io.Writer) *cobra.Command {
cmd := &cobra.Command{
Use: "stop <resource> <id>",
Short: "Gracefully shutdown a resource",
Long: `Gracefully shutdown a resource
Attempts to shutdown and delete a resource that supports graceful termination.
If the resource is resizable it will be resized to 0 before deletion.
Examples:
$ kubectl stop replicationcontroller foo
foo stopped
`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
usageError(cmd, "<resource> <id>")
}
mapper, _ := f.Object(cmd)
mapping, namespace, name := ResourceFromArgs(cmd, args, mapper)
reaper, err := f.Reaper(cmd, mapping)
checkErr(err)
s, err := reaper.Stop(namespace, name)
checkErr(err)
fmt.Fprintf(out, "%s\n", s)
},
}
return cmd
}

View File

@ -58,12 +58,12 @@ type Resizer interface {
Resize(namespace, name string, preconditions *ResizePrecondition, newSize uint) (string, error)
}
func ResizerFor(kind string, c *client.Client) (Resizer, bool) {
func ResizerFor(kind string, c client.Interface) (Resizer, error) {
switch kind {
case "ReplicationController":
return &ReplicationControllerResizer{c}, true
return &ReplicationControllerResizer{c}, nil
}
return nil, false
return nil, fmt.Errorf("no resizer has been implemented for %q", kind)
}
type ReplicationControllerResizer struct {

View File

@ -26,18 +26,18 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
)
type customFake struct {
type updaterFake struct {
*client.Fake
ctrl client.ReplicationControllerInterface
}
func (c *customFake) ReplicationControllers(namespace string) client.ReplicationControllerInterface {
func (c *updaterFake) ReplicationControllers(namespace string) client.ReplicationControllerInterface {
return c.ctrl
}
func fakeClientFor(namespace string, responses []fakeResponse) client.Interface {
fake := client.Fake{}
return &customFake{
return &updaterFake{
&fake,
&fakeRc{
&client.FakeReplicationControllers{

110
pkg/kubectl/stop.go Normal file
View File

@ -0,0 +1,110 @@
/*
Copyright 2014 Google Inc. 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 kubectl
import (
"fmt"
"time"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta"
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util/wait"
)
const (
interval = time.Second * 3
timeout = time.Minute * 5
)
// A Reaper handles terminating an object as gracefully as possible.
type Reaper interface {
Stop(namespace, name string) (string, error)
}
func ReaperFor(kind string, c client.Interface) (Reaper, error) {
switch kind {
case "ReplicationController":
return &ReplicationControllerReaper{c, interval, timeout}, nil
case "Pod":
return &PodReaper{c}, nil
case "Service":
return &ServiceReaper{c}, nil
}
return nil, fmt.Errorf("no reaper has been implemented for %q", kind)
}
type ReplicationControllerReaper struct {
client.Interface
pollInterval, timeout time.Duration
}
type PodReaper struct {
client.Interface
}
type ServiceReaper struct {
client.Interface
}
type objInterface interface {
Delete(name string) error
Get(name string) (meta.Interface, error)
}
func (reaper *ReplicationControllerReaper) Stop(namespace, name string) (string, error) {
rc := reaper.ReplicationControllers(namespace)
controller, err := rc.Get(name)
if err != nil {
return "", err
}
controller.Spec.Replicas = 0
// TODO: do retry on 409 errors here?
if _, err := rc.Update(controller); err != nil {
return "", err
}
if err := wait.Poll(reaper.pollInterval, reaper.timeout,
client.ControllerHasDesiredReplicas(reaper, controller)); err != nil {
return "", err
}
if err := rc.Delete(name); err != nil {
return "", err
}
return fmt.Sprintf("%s stopped", name), nil
}
func (reaper *PodReaper) Stop(namespace, name string) (string, error) {
pods := reaper.Pods(namespace)
_, err := pods.Get(name)
if err != nil {
return "", err
}
if err := pods.Delete(name); err != nil {
return "", err
}
return fmt.Sprintf("%s stopped", name), nil
}
func (reaper *ServiceReaper) Stop(namespace, name string) (string, error) {
services := reaper.Services(namespace)
_, err := services.Get(name)
if err != nil {
return "", err
}
if err := services.Delete(name); err != nil {
return "", err
}
return fmt.Sprintf("%s stopped", name), nil
}

168
pkg/kubectl/stop_test.go Normal file
View File

@ -0,0 +1,168 @@
/*
Copyright 2014 Google Inc. 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 kubectl
import (
"fmt"
"testing"
"time"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
)
func TestReplicationControllerStop(t *testing.T) {
fake := &client.Fake{
Ctrl: api.ReplicationController{
Spec: api.ReplicationControllerSpec{
Replicas: 0,
},
},
}
reaper := ReplicationControllerReaper{fake, time.Millisecond, time.Millisecond}
name := "foo"
s, err := reaper.Stop("default", name)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
expected := "foo stopped"
if s != expected {
t.Errorf("expected %s, got %s", expected, s)
}
if len(fake.Actions) != 4 {
t.Errorf("unexpected actions: %v, expected 4 actions (get, update, get, delete)", fake.Actions)
}
for i, action := range []string{"get", "update", "get", "delete"} {
if fake.Actions[i].Action != action+"-controller" {
t.Errorf("unexpected action: %v, expected %s-controller", fake.Actions[i], action)
}
}
}
type noSuchPod struct {
*client.FakePods
}
func (c *noSuchPod) Get(name string) (*api.Pod, error) {
return nil, fmt.Errorf("%s does not exist", name)
}
type noDeleteService struct {
*client.FakeServices
}
func (c *noDeleteService) Delete(service string) error {
return fmt.Errorf("I'm afraid I can't do that, Dave")
}
type reaperFake struct {
*client.Fake
noSuchPod, noDeleteService bool
}
func (c *reaperFake) Pods(namespace string) client.PodInterface {
pods := &client.FakePods{c.Fake, namespace}
if c.noSuchPod {
return &noSuchPod{pods}
}
return pods
}
func (c *reaperFake) Services(namespace string) client.ServiceInterface {
services := &client.FakeServices{c.Fake, namespace}
if c.noDeleteService {
return &noDeleteService{services}
}
return services
}
func TestSimpleStop(t *testing.T) {
tests := []struct {
fake *reaperFake
kind string
actions []string
expectError bool
test string
}{
{
fake: &reaperFake{
Fake: &client.Fake{},
},
kind: "Pod",
actions: []string{"get-pod", "delete-pod"},
expectError: false,
test: "stop pod succeeds",
},
{
fake: &reaperFake{
Fake: &client.Fake{},
},
kind: "Service",
actions: []string{"get-service", "delete-service"},
expectError: false,
test: "stop service succeeds",
},
{
fake: &reaperFake{
Fake: &client.Fake{},
noSuchPod: true,
},
kind: "Pod",
actions: []string{},
expectError: true,
test: "stop pod fails, no pod",
},
{
fake: &reaperFake{
Fake: &client.Fake{},
noDeleteService: true,
},
kind: "Service",
actions: []string{"get-service"},
expectError: true,
test: "stop service fails, can't delete",
},
}
for _, test := range tests {
fake := test.fake
reaper, err := ReaperFor(test.kind, fake)
if err != nil {
t.Errorf("unexpected error: %v (%s)", err, test.test)
}
s, err := reaper.Stop("default", "foo")
if err != nil && !test.expectError {
t.Errorf("unexpected error: %v (%s)", err, test.test)
}
if err == nil {
if test.expectError {
t.Errorf("unexpected non-error: %v (%s)", err, test.test)
}
if s != "foo stopped" {
t.Errorf("unexpected return: %s (%s)", s, test.test)
}
}
if len(test.actions) != len(fake.Actions) {
t.Errorf("unexpected actions: %v; expected %v (%s)", fake.Actions, test.actions, test.test)
}
for i, action := range fake.Actions {
testAction := test.actions[i]
if action.Action != testAction {
t.Errorf("unexpected action: %v; expected %v (%s)", action, testAction, test.test)
}
}
}
}