From b1aa037990e8dea748f0a219c0f07508ad93cd8b Mon Sep 17 00:00:00 2001 From: Jeff Lowdermilk Date: Tue, 20 Jan 2015 18:58:04 -0800 Subject: [PATCH] Add a kubectl stop command --- docs/kubectl.md | 44 ++++++++ pkg/kubectl/cmd/cmd.go | 14 ++- pkg/kubectl/cmd/stop.go | 55 +++++++++ pkg/kubectl/resize.go | 6 +- pkg/kubectl/rolling_updater_test.go | 6 +- pkg/kubectl/stop.go | 110 ++++++++++++++++++ pkg/kubectl/stop_test.go | 168 ++++++++++++++++++++++++++++ 7 files changed, 393 insertions(+), 10 deletions(-) create mode 100644 pkg/kubectl/cmd/stop.go create mode 100644 pkg/kubectl/stop.go create mode 100644 pkg/kubectl/stop_test.go diff --git a/docs/kubectl.md b/docs/kubectl.md index ba21583102..6fd46f2d5a 100644 --- a/docs/kubectl.md +++ b/docs/kubectl.md @@ -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 [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 + +``` + diff --git a/pkg/kubectl/cmd/cmd.go b/pkg/kubectl/cmd/cmd.go index 30a5c8b404..e6363d5cf9 100644 --- a/pkg/kubectl/cmd/cmd.go +++ b/pkg/kubectl/cmd/cmd.go @@ -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) } @@ -119,11 +121,14 @@ func NewFactory() *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") { @@ -193,6 +198,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 } diff --git a/pkg/kubectl/cmd/stop.go b/pkg/kubectl/cmd/stop.go new file mode 100644 index 0000000000..75431b0129 --- /dev/null +++ b/pkg/kubectl/cmd/stop.go @@ -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 ", + 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, " ") + } + 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 +} diff --git a/pkg/kubectl/resize.go b/pkg/kubectl/resize.go index b92d45d3d0..ff3f866a63 100644 --- a/pkg/kubectl/resize.go +++ b/pkg/kubectl/resize.go @@ -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 { diff --git a/pkg/kubectl/rolling_updater_test.go b/pkg/kubectl/rolling_updater_test.go index d1b56cb955..aad03195f1 100644 --- a/pkg/kubectl/rolling_updater_test.go +++ b/pkg/kubectl/rolling_updater_test.go @@ -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{ diff --git a/pkg/kubectl/stop.go b/pkg/kubectl/stop.go new file mode 100644 index 0000000000..9f3537c917 --- /dev/null +++ b/pkg/kubectl/stop.go @@ -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 +} diff --git a/pkg/kubectl/stop_test.go b/pkg/kubectl/stop_test.go new file mode 100644 index 0000000000..0d7c3f3a22 --- /dev/null +++ b/pkg/kubectl/stop_test.go @@ -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) + } + } + } +}