diff --git a/docs/kubectl.md b/docs/kubectl.md index fef88760de..dc46cc9898 100644 --- a/docs/kubectl.md +++ b/docs/kubectl.md @@ -388,6 +388,7 @@ Additional help topics: kubectl namespace Set and view the current Kubernetes namespace kubectl log Print the logs for a container in a pod. 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. Use "kubectl help [command]" for more information about that command. @@ -815,6 +816,59 @@ Usage: ``` +#### resize +Set a new size for a resizable resource (currently only Replication Controllers) + +Resize also allows users to specify one or more preconditions for the resize action. +The new size is specified by --replicas=. You can also specify an optional precondition. +The two currently supported options are --current-replicas or --resource-version. +If a precondition is specified, it is validated before the resize is attempted, and it is +guaranteed that the precondition holds true when the resize is sent to the server. + +Examples: + $ kubectl resize replicationcontrollers foo 3 + resized + + # will only execute if the current size is 3 + $ kubectl resize --current-replicas=2 replicationcontrollers foo 3 + + +Usage: +``` + kubectl resize [---resource-version=] [--current-replicas=] --replicas= [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 + --current-replicas=-1: Precondition for current size. Requires that the current size of the replication controller match this value in order to resize + -h, --help=false: help for resize + --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. + --replicas=-1: The new number desired number of replicas. Required. + --resource-version="": Precondition for resource version. Requires that the current resource version match this value in order to resize + -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 + +``` + #### run-container Create and run a particular image, possibly replicated. Creates a replication controller to manage the created container(s) diff --git a/pkg/kubectl/cmd/cmd.go b/pkg/kubectl/cmd/cmd.go index c825598036..7ba3011fb4 100644 --- a/pkg/kubectl/cmd/cmd.go +++ b/pkg/kubectl/cmd/cmd.go @@ -62,6 +62,8 @@ type Factory struct { Describer func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.Describer, error) // Returns a Printer for formatting objects of the given type or an error. 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 schema that can validate objects stored on disk. Validator func(*cobra.Command) (validation.Schema, error) } @@ -112,6 +114,17 @@ func NewFactory() *Factory { Printer: func(cmd *cobra.Command, mapping *meta.RESTMapping, noHeaders bool) (kubectl.ResourcePrinter, error) { return kubectl.NewHumanReadablePrinter(noHeaders), nil }, + Resizer: func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.Resizer, error) { + client, err := clients.ClientForVersion(mapping.APIVersion) + 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 resizer, nil + }, Validator: func(cmd *cobra.Command) (validation.Schema, error) { if GetFlagBool(cmd, "validate") { client, err := clients.ClientForVersion("") @@ -173,6 +186,7 @@ Find more information at https://github.com/GoogleCloudPlatform/kubernetes.`, cmds.AddCommand(NewCmdNamespace(out)) cmds.AddCommand(f.NewCmdLog(out)) cmds.AddCommand(f.NewCmdRollingUpdate(out)) + cmds.AddCommand(f.NewCmdResize(out)) cmds.AddCommand(f.NewCmdRunContainer(out)) diff --git a/pkg/kubectl/cmd/resize.go b/pkg/kubectl/cmd/resize.go new file mode 100644 index 0000000000..c68a617935 --- /dev/null +++ b/pkg/kubectl/cmd/resize.go @@ -0,0 +1,68 @@ +/* +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/GoogleCloudPlatform/kubernetes/pkg/kubectl" + "github.com/spf13/cobra" +) + +func (f *Factory) NewCmdResize(out io.Writer) *cobra.Command { + cmd := &cobra.Command{ + Use: "resize [---resource-version=] [--current-replicas=] --replicas= ", + Short: "Set a new size for a resizable resource (currently only Replication Controllers)", + Long: `Set a new size for a resizable resource (currently only Replication Controllers) + +Resize also allows users to specify one or more preconditions for the resize action. +The new size is specified by --replicas=. You can also specify an optional precondition. +The two currently supported options are --current-replicas or --resource-version. +If a precondition is specified, it is validated before the resize is attempted, and it is +guaranteed that the precondition holds true when the resize is sent to the server. + +Examples: + $ kubectl resize replicationcontrollers foo 3 + resized + + # will only execute if the current size is 3 + $ kubectl resize --current-replicas=2 replicationcontrollers foo 3 +`, + Run: func(cmd *cobra.Command, args []string) { + count := GetFlagInt(cmd, "replicas") + if len(args) != 2 || count < 0 { + usageError(cmd, "--replicas= ") + } + mapper, _ := f.Object(cmd) + mapping, namespace, name := ResourceFromArgs(cmd, args, mapper) + + resizer, err := f.Resizer(cmd, mapping) + checkErr(err) + + resourceVersion := GetFlagString(cmd, "resource-version") + currentSize := GetFlagInt(cmd, "current-replicas") + s, err := resizer.Resize(namespace, name, &kubectl.ResizePrecondition{currentSize, resourceVersion}, uint(count)) + checkErr(err) + fmt.Fprintf(out, "%s\n", s) + }, + } + cmd.Flags().String("resource-version", "", "Precondition for resource version. Requires that the current resource version match this value in order to resize") + cmd.Flags().Int("current-replicas", -1, "Precondition for current size. Requires that the current size of the replication controller match this value in order to resize") + cmd.Flags().Int("replicas", -1, "The new number desired number of replicas. Required.") + return cmd +} diff --git a/pkg/kubectl/resize.go b/pkg/kubectl/resize.go new file mode 100644 index 0000000000..b92d45d3d0 --- /dev/null +++ b/pkg/kubectl/resize.go @@ -0,0 +1,93 @@ +/* +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" + "strconv" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" +) + +// ResizePrecondition describes a condition that must be true for the resize to take place +// If CurrentSize == -1, it is ignored. +// If CurrentResourceVersion is the empty string, it is ignored. +// Otherwise they must equal the values in the replication controller for it to be valid. +type ResizePrecondition struct { + Size int + ResourceVersion string +} + +type PreconditionError struct { + Precondition string + ExpectedValue string + ActualValue string +} + +func (pe *PreconditionError) Error() string { + return fmt.Sprintf("Expected %s to be %s, was %s", pe.Precondition, pe.ExpectedValue, pe.ActualValue) +} + +// Validate ensures that the preconditions match. Returns nil if they are valid, an error otherwise +func (precondition *ResizePrecondition) Validate(controller *api.ReplicationController) error { + if precondition.Size != -1 && controller.Spec.Replicas != precondition.Size { + return &PreconditionError{"replicas", strconv.Itoa(precondition.Size), strconv.Itoa(controller.Spec.Replicas)} + } + if precondition.ResourceVersion != "" && controller.ResourceVersion != precondition.ResourceVersion { + return &PreconditionError{"resource version", precondition.ResourceVersion, controller.ResourceVersion} + } + return nil +} + +type Resizer interface { + Resize(namespace, name string, preconditions *ResizePrecondition, newSize uint) (string, error) +} + +func ResizerFor(kind string, c *client.Client) (Resizer, bool) { + switch kind { + case "ReplicationController": + return &ReplicationControllerResizer{c}, true + } + return nil, false +} + +type ReplicationControllerResizer struct { + client.Interface +} + +func (resize *ReplicationControllerResizer) Resize(namespace, name string, preconditions *ResizePrecondition, newSize uint) (string, error) { + rc := resize.ReplicationControllers(namespace) + controller, err := rc.Get(name) + if err != nil { + return "", err + } + + if preconditions != nil { + if err := preconditions.Validate(controller); err != nil { + return "", err + } + } + + controller.Spec.Replicas = int(newSize) + // TODO: do retry on 409 errors here? + if _, err := rc.Update(controller); err != nil { + return "", err + } + // TODO: do a better job of printing objects here. + return "resized", nil +} diff --git a/pkg/kubectl/resize_test.go b/pkg/kubectl/resize_test.go new file mode 100644 index 0000000000..4e1da6c59a --- /dev/null +++ b/pkg/kubectl/resize_test.go @@ -0,0 +1,182 @@ +/* +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 ( + // "strings" + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + // "github.com/GoogleCloudPlatform/kubernetes/pkg/util" +) + +func TestReplicationControllerResize(t *testing.T) { + fake := &client.Fake{} + resizer := ReplicationControllerResizer{fake} + preconditions := ResizePrecondition{-1, ""} + count := uint(3) + name := "foo" + resizer.Resize("default", name, &preconditions, count) + + if len(fake.Actions) != 2 { + t.Errorf("unexpected actions: %v, expected 2 actions (get, update)", fake.Actions) + } + if fake.Actions[0].Action != "get-controller" || fake.Actions[0].Value != name { + t.Errorf("unexpected action: %v, expected get-controller %s", fake.Actions[0], name) + } + if fake.Actions[1].Action != "update-controller" || fake.Actions[1].Value.(*api.ReplicationController).Spec.Replicas != int(count) { + t.Errorf("unexpected action %v, expected update-controller with replicas = %d", count) + } +} + +func TestReplicationControllerResizeFailsPreconditions(t *testing.T) { + fake := &client.Fake{ + Ctrl: api.ReplicationController{ + Spec: api.ReplicationControllerSpec{ + Replicas: 10, + }, + }, + } + resizer := ReplicationControllerResizer{fake} + preconditions := ResizePrecondition{2, ""} + count := uint(3) + name := "foo" + resizer.Resize("default", name, &preconditions, count) + + if len(fake.Actions) != 1 { + t.Errorf("unexpected actions: %v, expected 2 actions (get, update)", fake.Actions) + } + if fake.Actions[0].Action != "get-controller" || fake.Actions[0].Value != name { + t.Errorf("unexpected action: %v, expected get-controller %s", fake.Actions[0], name) + } +} + +func TestPreconditionValidate(t *testing.T) { + tests := []struct { + preconditions ResizePrecondition + controller api.ReplicationController + expectError bool + test string + }{ + { + preconditions: ResizePrecondition{-1, ""}, + expectError: false, + test: "defaults", + }, + { + preconditions: ResizePrecondition{-1, ""}, + controller: api.ReplicationController{ + ObjectMeta: api.ObjectMeta{ + ResourceVersion: "foo", + }, + Spec: api.ReplicationControllerSpec{ + Replicas: 10, + }, + }, + expectError: false, + test: "defaults 2", + }, + { + preconditions: ResizePrecondition{0, ""}, + controller: api.ReplicationController{ + ObjectMeta: api.ObjectMeta{ + ResourceVersion: "foo", + }, + Spec: api.ReplicationControllerSpec{ + Replicas: 0, + }, + }, + expectError: false, + test: "size matches", + }, + { + preconditions: ResizePrecondition{-1, "foo"}, + controller: api.ReplicationController{ + ObjectMeta: api.ObjectMeta{ + ResourceVersion: "foo", + }, + Spec: api.ReplicationControllerSpec{ + Replicas: 10, + }, + }, + expectError: false, + test: "resource version matches", + }, + { + preconditions: ResizePrecondition{10, "foo"}, + controller: api.ReplicationController{ + ObjectMeta: api.ObjectMeta{ + ResourceVersion: "foo", + }, + Spec: api.ReplicationControllerSpec{ + Replicas: 10, + }, + }, + expectError: false, + test: "both match", + }, + { + preconditions: ResizePrecondition{10, "foo"}, + controller: api.ReplicationController{ + ObjectMeta: api.ObjectMeta{ + ResourceVersion: "foo", + }, + Spec: api.ReplicationControllerSpec{ + Replicas: 20, + }, + }, + expectError: true, + test: "size different", + }, + { + preconditions: ResizePrecondition{10, "foo"}, + controller: api.ReplicationController{ + ObjectMeta: api.ObjectMeta{ + ResourceVersion: "bar", + }, + Spec: api.ReplicationControllerSpec{ + Replicas: 10, + }, + }, + expectError: true, + test: "version different", + }, + { + preconditions: ResizePrecondition{10, "foo"}, + controller: api.ReplicationController{ + ObjectMeta: api.ObjectMeta{ + ResourceVersion: "bar", + }, + Spec: api.ReplicationControllerSpec{ + Replicas: 20, + }, + }, + expectError: true, + test: "both different", + }, + } + for _, test := range tests { + err := test.preconditions.Validate(&test.controller) + if err != nil && !test.expectError { + t.Errorf("unexpected error: %v (%s)", err, test.test) + } + if err == nil && test.expectError { + t.Errorf("unexpected non-error: %v (%s)", err, test.test) + } + } +}