diff --git a/docs/kubectl.md b/docs/kubectl.md index 68dc9554971..c9ec80669f1 100644 --- a/docs/kubectl.md +++ b/docs/kubectl.md @@ -390,6 +390,7 @@ Additional help topics: kubectl run-container Run a particular image on the cluster. kubectl stop Gracefully shutdown a resource kubectl expose Take a replicated application and expose it as Kubernetes Service + kubectl label Update the labels on a resource Use "kubectl help [command]" for more information about that command. ``` @@ -1037,3 +1038,60 @@ Usage: ``` +#### label +Update the labels on a resource. + +If --overwrite is true, then existing labels can be overwritten, otherwise attempting to overwrite a label will result in an error. +If --resource-version is specified, then updates will use this resource version, otherwise the existing resource-version will be used. + +Examples: + $ kubectl label pods foo unhealthy=true + + + $ kubectl label --overwrite pods foo status=unhealthy + + + $ kubectl label pods foo status=unhealthy --resource-version=1 + + + $ kubectl label pods foo bar- + + +Usage: +``` + kubectl label [--overwrite] = ... = [--resource-version=] [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 + -h, --help=false: help for label + --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 + --namespace="": If present, the namespace scope for this CLI request. + --no-headers=false: When using the default output, don't print headers + -o, --output="": Output format: json|yaml|template|templatefile + --output-version="": Output the formatted object with the given version (default api-version) + --overwrite=false: If true, allow labels to be overwritten, otherwise reject label updates that overwrite existing labels. + --resource-version="": If non-empty, the labels update will only succeed if this is the current resource-version for the object. + -s, --server="": The address of the Kubernetes API server + --stderrthreshold=2: logs at or above this threshold go to stderr + -t, --template="": Template string or path to template file to use when -o=template or -o=templatefile. + --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 2c186684a75..c8dee384c96 100644 --- a/pkg/kubectl/cmd/cmd.go +++ b/pkg/kubectl/cmd/cmd.go @@ -215,6 +215,8 @@ Find more information at https://github.com/GoogleCloudPlatform/kubernetes.`, cmds.AddCommand(f.NewCmdStop(out)) cmds.AddCommand(f.NewCmdExposeService(out)) + cmds.AddCommand(f.NewCmdLabel(out)) + return cmds } diff --git a/pkg/kubectl/cmd/label.go b/pkg/kubectl/cmd/label.go new file mode 100644 index 00000000000..91211314033 --- /dev/null +++ b/pkg/kubectl/cmd/label.go @@ -0,0 +1,170 @@ +/* +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" + "strings" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta" + "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/util" + "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/resource" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + "github.com/spf13/cobra" +) + +func (f *Factory) NewCmdLabel(out io.Writer) *cobra.Command { + cmd := &cobra.Command{ + Use: "label [--overwrite] = ... = [--resource-version=]", + Short: "Update the labels on a resource", + Long: `Update the labels on a resource. + +If --overwrite is true, then existing labels can be overwritten, otherwise attempting to overwrite a label will result in an error. +If --resource-version is specified, then updates will use this resource version, otherwise the existing resource-version will be used. + +Examples: + $ kubectl label pods foo unhealthy=true + + + $ kubectl label --overwrite pods foo status=unhealthy + + + $ kubectl label pods foo status=unhealthy --resource-version=1 + + + $ kubectl label pods foo bar- + `, + Run: func(cmd *cobra.Command, args []string) { + if len(args) < 2 { + usageError(cmd, " is required") + } + if len(args) < 3 { + usageError(cmd, "at least one label update is required.") + } + res := args[:2] + cmdNamespace, err := f.DefaultNamespace(cmd) + checkErr(err) + + mapper, _ := f.Object(cmd) + mapping, namespace, name := util.ResourceFromArgs(cmd, res, mapper, cmdNamespace) + client, err := f.RESTClient(cmd, mapping) + checkErr(err) + + labels, remove, err := parseLabels(args[2:]) + checkErr(err) + overwrite := util.GetFlagBool(cmd, "overwrite") + resourceVersion := util.GetFlagString(cmd, "resource-version") + + obj, err := updateObject(client, mapping, namespace, name, func(obj runtime.Object) runtime.Object { + outObj, err := labelFunc(obj, overwrite, resourceVersion, labels, remove) + checkErr(err) + return outObj + }) + checkErr(err) + + printer, err := f.PrinterForMapping(cmd, mapping) + checkErr(err) + + printer.PrintObj(obj, out) + }, + } + util.AddPrinterFlags(cmd) + cmd.Flags().Bool("overwrite", false, "If true, allow labels to be overwritten, otherwise reject label updates that overwrite existing labels.") + cmd.Flags().String("resource-version", "", "If non-empty, the labels update will only succeed if this is the current resource-version for the object.") + return cmd +} + +func updateObject(client resource.RESTClient, mapping *meta.RESTMapping, namespace, name string, updateFn func(runtime.Object) runtime.Object) (runtime.Object, error) { + helper := resource.NewHelper(client, mapping) + + obj, err := helper.Get(namespace, name) + if err != nil { + return nil, err + } + + obj = updateFn(obj) + + data, err := helper.Codec.Encode(obj) + if err != nil { + return nil, err + } + + _, err = helper.Update(namespace, name, true, data) + if err != nil { + return nil, err + } + return obj, nil +} + +func validateNoOverwrites(meta *api.ObjectMeta, labels map[string]string) error { + for key := range labels { + if value, found := meta.Labels[key]; found { + return fmt.Errorf("'%s' already has a value (%s), and --overwrite is false", key, value) + } + } + return nil +} + +func parseLabels(spec []string) (map[string]string, []string, error) { + labels := map[string]string{} + var remove []string + for _, labelSpec := range spec { + if strings.Index(labelSpec, "=") != -1 { + parts := strings.Split(labelSpec, "=") + if len(parts) != 2 { + return nil, nil, fmt.Errorf("invalid label spec: %v", labelSpec) + } + labels[parts[0]] = parts[1] + } else if strings.HasSuffix(labelSpec, "-") { + remove = append(remove, labelSpec[:len(labelSpec)-1]) + } else { + return nil, nil, fmt.Errorf("unknown label spec: %v") + } + } + for _, removeLabel := range remove { + if _, found := labels[removeLabel]; found { + return nil, nil, fmt.Errorf("can not both modify and remove a label in the same command") + } + } + return labels, remove, nil +} + +func labelFunc(obj runtime.Object, overwrite bool, resourceVersion string, labels map[string]string, remove []string) (runtime.Object, error) { + meta, err := api.ObjectMetaFor(obj) + if err != nil { + return nil, err + } + if !overwrite { + if err := validateNoOverwrites(meta, labels); err != nil { + return nil, err + } + } + + for key, value := range labels { + meta.Labels[key] = value + } + for _, label := range remove { + delete(meta.Labels, label) + } + + if len(resourceVersion) != 0 { + meta.ResourceVersion = resourceVersion + } + return obj, nil +} diff --git a/pkg/kubectl/cmd/label_test.go b/pkg/kubectl/cmd/label_test.go new file mode 100644 index 00000000000..89a20b82cd7 --- /dev/null +++ b/pkg/kubectl/cmd/label_test.go @@ -0,0 +1,249 @@ +/* +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 ( + "reflect" + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" +) + +func TestValidateLabels(t *testing.T) { + tests := []struct { + meta *api.ObjectMeta + labels map[string]string + expectErr bool + test string + }{ + { + meta: &api.ObjectMeta{ + Labels: map[string]string{ + "a": "b", + "c": "d", + }, + }, + labels: map[string]string{ + "a": "c", + "d": "b", + }, + test: "one shared", + expectErr: true, + }, + { + meta: &api.ObjectMeta{ + Labels: map[string]string{ + "a": "b", + "c": "d", + }, + }, + labels: map[string]string{ + "b": "d", + "c": "a", + }, + test: "second shared", + expectErr: true, + }, + { + meta: &api.ObjectMeta{ + Labels: map[string]string{ + "a": "b", + "c": "d", + }, + }, + labels: map[string]string{ + "b": "a", + "d": "c", + }, + test: "no overlap", + }, + { + meta: &api.ObjectMeta{}, + labels: map[string]string{ + "b": "a", + "d": "c", + }, + test: "no labels", + }, + } + for _, test := range tests { + err := validateNoOverwrites(test.meta, test.labels) + if test.expectErr && err == nil { + t.Errorf("%s: unexpected non-error", test.test) + } + if !test.expectErr && err != nil { + t.Errorf("%s: unexpected error: %v", test.test, err) + } + } +} + +func TestParseLabels(t *testing.T) { + tests := []struct { + labels []string + expected map[string]string + expectedRemove []string + expectErr bool + }{ + { + labels: []string{"a=b", "c=d"}, + expected: map[string]string{"a": "b", "c": "d"}, + }, + { + labels: []string{}, + expected: map[string]string{}, + }, + { + labels: []string{"a=b", "c=d", "e-"}, + expected: map[string]string{"a": "b", "c": "d"}, + expectedRemove: []string{"e"}, + }, + { + labels: []string{"ab", "c=d"}, + expectErr: true, + }, + { + labels: []string{"a=b", "c=d", "a-"}, + expectErr: true, + }, + } + for _, test := range tests { + labels, remove, err := parseLabels(test.labels) + if test.expectErr && err == nil { + t.Errorf("unexpected non-error: %v", test) + } + if !test.expectErr && err != nil { + t.Errorf("unexpected error: %v %v", err, test) + } + if !reflect.DeepEqual(labels, test.expected) { + t.Errorf("expected: %v, got %v", test.expected, labels) + } + if !reflect.DeepEqual(remove, test.expectedRemove) { + t.Errorf("expected: %v, got %v", test.expectedRemove, remove) + } + } +} + +func TestLabelFunc(t *testing.T) { + tests := []struct { + obj runtime.Object + overwrite bool + version string + labels map[string]string + remove []string + expected runtime.Object + expectErr bool + }{ + { + obj: &api.Pod{ + ObjectMeta: api.ObjectMeta{ + Labels: map[string]string{"a": "b"}, + }, + }, + labels: map[string]string{"a": "b"}, + expectErr: true, + }, + { + obj: &api.Pod{ + ObjectMeta: api.ObjectMeta{ + Labels: map[string]string{"a": "b"}, + }, + }, + labels: map[string]string{"a": "c"}, + overwrite: true, + expected: &api.Pod{ + ObjectMeta: api.ObjectMeta{ + Labels: map[string]string{"a": "c"}, + }, + }, + }, + { + obj: &api.Pod{ + ObjectMeta: api.ObjectMeta{ + Labels: map[string]string{"a": "b"}, + }, + }, + labels: map[string]string{"c": "d"}, + expected: &api.Pod{ + ObjectMeta: api.ObjectMeta{ + Labels: map[string]string{"a": "b", "c": "d"}, + }, + }, + }, + { + obj: &api.Pod{ + ObjectMeta: api.ObjectMeta{ + Labels: map[string]string{"a": "b"}, + }, + }, + labels: map[string]string{"c": "d"}, + version: "2", + expected: &api.Pod{ + ObjectMeta: api.ObjectMeta{ + Labels: map[string]string{"a": "b", "c": "d"}, + ResourceVersion: "2", + }, + }, + }, + { + obj: &api.Pod{ + ObjectMeta: api.ObjectMeta{ + Labels: map[string]string{"a": "b"}, + }, + }, + labels: map[string]string{}, + remove: []string{"a"}, + expected: &api.Pod{ + ObjectMeta: api.ObjectMeta{ + Labels: map[string]string{}, + }, + }, + }, + { + obj: &api.Pod{ + ObjectMeta: api.ObjectMeta{ + Labels: map[string]string{"a": "b", "c": "d"}, + }, + }, + labels: map[string]string{"e": "f"}, + remove: []string{"a"}, + expected: &api.Pod{ + ObjectMeta: api.ObjectMeta{ + Labels: map[string]string{ + "c": "d", + "e": "f", + }, + }, + }, + }, + } + for _, test := range tests { + out, err := labelFunc(test.obj, test.overwrite, test.version, test.labels, test.remove) + if test.expectErr { + if err == nil { + t.Errorf("unexpected non-error: %v", test) + } + continue + } + if !test.expectErr && err != nil { + t.Errorf("unexpected error: %v %v", err, test) + } + if !reflect.DeepEqual(out, test.expected) { + t.Errorf("expected: %v, got %v", test.expected, out) + } + } +}