Implement kubectl annotation update command. Refactor kubectl annotate to decouple command framework from business logic.

This commit is contained in:
Janet Kuo 2015-07-23 15:43:48 -07:00
parent 6df4d6703b
commit 7e63213478
13 changed files with 1182 additions and 31 deletions

View File

@ -735,6 +735,32 @@ _kubectl_label()
must_have_one_noun=()
}
_kubectl_annotate()
{
last_command="kubectl_annotate"
commands=()
flags=()
two_word_flags=()
flags_with_completion=()
flags_completion=()
flags+=("--all")
flags+=("--help")
flags+=("-h")
flags+=("--no-headers")
flags+=("--output=")
two_word_flags+=("-o")
flags+=("--output-version=")
flags+=("--overwrite")
flags+=("--resource-version=")
flags+=("--template=")
two_word_flags+=("-t")
must_have_one_flag=()
must_have_one_noun=()
}
_kubectl_config_view()
{
last_command="kubectl_config_view"
@ -978,6 +1004,7 @@ _kubectl()
commands+=("stop")
commands+=("expose")
commands+=("label")
commands+=("annotate")
commands+=("config")
commands+=("cluster-info")
commands+=("api-versions")

View File

@ -1,3 +1,4 @@
kubectl-annotate.1
kubectl-api-versions.1
kubectl-attach.1
kubectl-cluster-info.1

View File

@ -0,0 +1,197 @@
.TH "KUBERNETES" "1" " kubernetes User Manuals" "Eric Paris" "Jan 2015" ""
.SH NAME
.PP
kubectl annotate \- Update the annotations on a resource
.SH SYNOPSIS
.PP
\fBkubectl annotate\fP [OPTIONS]
.SH DESCRIPTION
.PP
Update the annotations on one or more resources.
.PP
An annotation is a key/value pair that can hold larger (compared to a label), and possibly not human\-readable, data.
It is intended to store non\-identifying auxiliary data, especially data manipulated by tools and system extensions.
If \-\-overwrite is true, then existing annotations can be overwritten, otherwise attempting to overwrite an annotation 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.
.PP
Possible resources include (case insensitive): pods (po), services (svc),
replicationcontrollers (rc), nodes (no), events (ev), componentstatuses (cs),
limitranges (limits), persistentvolumes (pv), persistentvolumeclaims (pvc),
resourcequotas (quota) or secrets.
.SH OPTIONS
.PP
\fB\-\-all\fP=false
select all resources in the namespace of the specified resource types
.PP
\fB\-h\fP, \fB\-\-help\fP=false
help for annotate
.PP
\fB\-\-no\-headers\fP=false
When using the default output, don't print headers.
.PP
\fB\-o\fP, \fB\-\-output\fP=""
Output format. One of: json|yaml|template|templatefile|wide.
.PP
\fB\-\-output\-version\fP=""
Output the formatted object with the given version (default api\-version).
.PP
\fB\-\-overwrite\fP=false
If true, allow annotations to be overwritten, otherwise reject annotation updates that overwrite existing annotations.
.PP
\fB\-\-resource\-version\fP=""
If non\-empty, the annotation update will only succeed if this is the current resource\-version for the object. Only valid when specifying a single resource.
.PP
\fB\-t\fP, \fB\-\-template\fP=""
Template string or path to template file to use when \-o=template or \-o=templatefile. The template format is golang templates [
\[la]http://golang.org/pkg/text/template/#pkg-overview\[ra]]
.SH OPTIONS INHERITED FROM PARENT COMMANDS
.PP
\fB\-\-alsologtostderr\fP=false
log to standard error as well as files
.PP
\fB\-\-api\-version\fP=""
The API version to use when talking to the server
.PP
\fB\-\-certificate\-authority\fP=""
Path to a cert. file for the certificate authority.
.PP
\fB\-\-client\-certificate\fP=""
Path to a client key file for TLS.
.PP
\fB\-\-client\-key\fP=""
Path to a client key file for TLS.
.PP
\fB\-\-cluster\fP=""
The name of the kubeconfig cluster to use
.PP
\fB\-\-context\fP=""
The name of the kubeconfig context to use
.PP
\fB\-\-insecure\-skip\-tls\-verify\fP=false
If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure.
.PP
\fB\-\-kubeconfig\fP=""
Path to the kubeconfig file to use for CLI requests.
.PP
\fB\-\-log\-backtrace\-at\fP=:0
when logging hits line file:N, emit a stack trace
.PP
\fB\-\-log\-dir\fP=""
If non\-empty, write log files in this directory
.PP
\fB\-\-log\-flush\-frequency\fP=5s
Maximum number of seconds between log flushes
.PP
\fB\-\-logtostderr\fP=true
log to standard error instead of files
.PP
\fB\-\-match\-server\-version\fP=false
Require server version to match client version
.PP
\fB\-\-namespace\fP=""
If present, the namespace scope for this CLI request.
.PP
\fB\-\-password\fP=""
Password for basic authentication to the API server.
.PP
\fB\-s\fP, \fB\-\-server\fP=""
The address and port of the Kubernetes API server
.PP
\fB\-\-stderrthreshold\fP=2
logs at or above this threshold go to stderr
.PP
\fB\-\-token\fP=""
Bearer token for authentication to the API server.
.PP
\fB\-\-user\fP=""
The name of the kubeconfig user to use
.PP
\fB\-\-username\fP=""
Username for basic authentication to the API server.
.PP
\fB\-\-v\fP=0
log level for V logs
.PP
\fB\-\-validate\fP=false
If true, use a schema to validate the input before sending it
.PP
\fB\-\-vmodule\fP=
comma\-separated list of pattern=N settings for file\-filtered logging
.SH EXAMPLE
.PP
.RS
.nf
# Update pod 'foo' with the annotation 'description' and the value 'my frontend'.
# If the same annotation is set multiple times, only the last value will be applied
$ kubectl annotate pods foo description='my frontend'
# Update pod 'foo' with the annotation 'description' and the value 'my frontend running nginx', overwriting any existing value.
$ kubectl annotate \-\-overwrite pods foo description='my frontend running nginx'
# Update all pods in the namespace
$ kubectl annotate pods \-\-all description='my frontend running nginx'
# Update pod 'foo' only if the resource is unchanged from version 1.
$ kubectl annotate pods foo description='my frontend running nginx' \-\-resource\-version=1
# Update pod 'foo' by removing an annotation named 'description' if it exists.
# Does not require the \-\-overwrite flag.
$ kubectl annotate pods foo description\-
.fi
.RE
.SH SEE ALSO
.PP
\fBkubectl(1)\fP,
.SH HISTORY
.PP
January 2015, Originally compiled by Eric Paris (eparis at redhat dot com) based on the kubernetes source material, but hopefully they have been automatically generated since!

View File

@ -124,7 +124,7 @@ Find more information at
.SH SEE ALSO
.PP
\fBkubectl\-get(1)\fP, \fBkubectl\-describe(1)\fP, \fBkubectl\-create(1)\fP, \fBkubectl\-replace(1)\fP, \fBkubectl\-patch(1)\fP, \fBkubectl\-delete(1)\fP, \fBkubectl\-namespace(1)\fP, \fBkubectl\-logs(1)\fP, \fBkubectl\-rolling\-update(1)\fP, \fBkubectl\-scale(1)\fP, \fBkubectl\-attach(1)\fP, \fBkubectl\-exec(1)\fP, \fBkubectl\-port\-forward(1)\fP, \fBkubectl\-proxy(1)\fP, \fBkubectl\-run(1)\fP, \fBkubectl\-stop(1)\fP, \fBkubectl\-expose(1)\fP, \fBkubectl\-label(1)\fP, \fBkubectl\-config(1)\fP, \fBkubectl\-cluster\-info(1)\fP, \fBkubectl\-api\-versions(1)\fP, \fBkubectl\-version(1)\fP,
\fBkubectl\-get(1)\fP, \fBkubectl\-describe(1)\fP, \fBkubectl\-create(1)\fP, \fBkubectl\-replace(1)\fP, \fBkubectl\-patch(1)\fP, \fBkubectl\-delete(1)\fP, \fBkubectl\-namespace(1)\fP, \fBkubectl\-logs(1)\fP, \fBkubectl\-rolling\-update(1)\fP, \fBkubectl\-scale(1)\fP, \fBkubectl\-attach(1)\fP, \fBkubectl\-exec(1)\fP, \fBkubectl\-port\-forward(1)\fP, \fBkubectl\-proxy(1)\fP, \fBkubectl\-run(1)\fP, \fBkubectl\-stop(1)\fP, \fBkubectl\-expose(1)\fP, \fBkubectl\-label(1)\fP, \fBkubectl\-annotate(1)\fP, \fBkubectl\-config(1)\fP, \fBkubectl\-cluster\-info(1)\fP, \fBkubectl\-api\-versions(1)\fP, \fBkubectl\-version(1)\fP,
.SH HISTORY

View File

@ -1,4 +1,5 @@
kubectl.md
kubectl_annotate.md
kubectl_api-versions.md
kubectl_attach.md
kubectl_cluster-info.md

View File

@ -78,6 +78,7 @@ kubectl
### SEE ALSO
* [kubectl annotate](kubectl_annotate.md) - Update the annotations on a resource
* [kubectl api-versions](kubectl_api-versions.md) - Print available API versions.
* [kubectl attach](kubectl_attach.md) - Attach to a running container.
* [kubectl cluster-info](kubectl_cluster-info.md) - Display cluster info

View File

@ -0,0 +1,128 @@
<!-- BEGIN MUNGE: UNVERSIONED_WARNING -->
<!-- BEGIN STRIP_FOR_RELEASE -->
<img src="http://kubernetes.io/img/warning.png" alt="WARNING"
width="25" height="25">
<img src="http://kubernetes.io/img/warning.png" alt="WARNING"
width="25" height="25">
<img src="http://kubernetes.io/img/warning.png" alt="WARNING"
width="25" height="25">
<img src="http://kubernetes.io/img/warning.png" alt="WARNING"
width="25" height="25">
<img src="http://kubernetes.io/img/warning.png" alt="WARNING"
width="25" height="25">
<h2>PLEASE NOTE: This document applies to the HEAD of the source tree</h2>
If you are using a released version of Kubernetes, you should
refer to the docs that go with that version.
<strong>
The latest 1.0.x release of this document can be found
[here](http://releases.k8s.io/release-1.0/docs/user-guide/kubectl/kubectl_annotate.md).
Documentation for other releases can be found at
[releases.k8s.io](http://releases.k8s.io).
</strong>
--
<!-- END STRIP_FOR_RELEASE -->
<!-- END MUNGE: UNVERSIONED_WARNING -->
## kubectl annotate
Update the annotations on a resource
### Synopsis
Update the annotations on one or more resources.
An annotation is a key/value pair that can hold larger (compared to a label), and possibly not human-readable, data.
It is intended to store non-identifying auxiliary data, especially data manipulated by tools and system extensions.
If --overwrite is true, then existing annotations can be overwritten, otherwise attempting to overwrite an annotation 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.
Possible resources include (case insensitive): pods (po), services (svc),
replicationcontrollers (rc), nodes (no), events (ev), componentstatuses (cs),
limitranges (limits), persistentvolumes (pv), persistentvolumeclaims (pvc),
resourcequotas (quota) or secrets.
```
kubectl annotate [--overwrite] RESOURCE NAME KEY_1=VAL_1 ... KEY_N=VAL_N [--resource-version=version]
```
### Examples
```
# Update pod 'foo' with the annotation 'description' and the value 'my frontend'.
# If the same annotation is set multiple times, only the last value will be applied
$ kubectl annotate pods foo description='my frontend'
# Update pod 'foo' with the annotation 'description' and the value 'my frontend running nginx', overwriting any existing value.
$ kubectl annotate --overwrite pods foo description='my frontend running nginx'
# Update all pods in the namespace
$ kubectl annotate pods --all description='my frontend running nginx'
# Update pod 'foo' only if the resource is unchanged from version 1.
$ kubectl annotate pods foo description='my frontend running nginx' --resource-version=1
# Update pod 'foo' by removing an annotation named 'description' if it exists.
# Does not require the --overwrite flag.
$ kubectl annotate pods foo description-
```
### Options
```
--all=false: select all resources in the namespace of the specified resource types
-h, --help=false: help for annotate
--no-headers=false: When using the default output, don't print headers.
-o, --output="": Output format. One of: json|yaml|template|templatefile|wide.
--output-version="": Output the formatted object with the given version (default api-version).
--overwrite=false: If true, allow annotations to be overwritten, otherwise reject annotation updates that overwrite existing annotations.
--resource-version="": If non-empty, the annotation update will only succeed if this is the current resource-version for the object. Only valid when specifying a single resource.
-t, --template="": Template string or path to template file to use when -o=template or -o=templatefile. The template format is golang templates [http://golang.org/pkg/text/template/#pkg-overview]
```
### Options inherited from parent commands
```
--alsologtostderr=false: log to standard error as well as files
--api-version="": The API version to use when talking to the server
--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
--namespace="": If present, the namespace scope for this CLI request.
--password="": Password for basic authentication to the API server.
-s, --server="": The address and port 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
--username="": Username for basic authentication to the API server.
--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
```
### SEE ALSO
* [kubectl](kubectl.md) - kubectl controls the Kubernetes cluster manager
###### Auto generated by spf13/cobra at 2015-08-03 21:33:00.41118358 +0000 UTC
<!-- BEGIN MUNGE: GENERATED_ANALYTICS -->
[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/docs/user-guide/kubectl/kubectl_annotate.md?pixel)]()
<!-- END MUNGE: GENERATED_ANALYTICS -->

View File

@ -0,0 +1,278 @@
/*
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 (
"bytes"
"fmt"
"io"
"strings"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
cmdutil "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"
)
// AnnotateOptions have the data required to perform the annotate operation
type AnnotateOptions struct {
out io.Writer
resources []string
newAnnotations map[string]string
removeAnnotations []string
builder *resource.Builder
overwrite bool
all bool
resourceVersion string
}
const (
annotate_long = `Update the annotations on one or more resources.
An annotation is a key/value pair that can hold larger (compared to a label), and possibly not human-readable, data.
It is intended to store non-identifying auxiliary data, especially data manipulated by tools and system extensions.
If --overwrite is true, then existing annotations can be overwritten, otherwise attempting to overwrite an annotation 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.
Possible resources include (case insensitive): pods (po), services (svc),
replicationcontrollers (rc), nodes (no), events (ev), componentstatuses (cs),
limitranges (limits), persistentvolumes (pv), persistentvolumeclaims (pvc),
resourcequotas (quota) or secrets.`
annotate_example = `# Update pod 'foo' with the annotation 'description' and the value 'my frontend'.
# If the same annotation is set multiple times, only the last value will be applied
$ kubectl annotate pods foo description='my frontend'
# Update pod 'foo' with the annotation 'description' and the value 'my frontend running nginx', overwriting any existing value.
$ kubectl annotate --overwrite pods foo description='my frontend running nginx'
# Update all pods in the namespace
$ kubectl annotate pods --all description='my frontend running nginx'
# Update pod 'foo' only if the resource is unchanged from version 1.
$ kubectl annotate pods foo description='my frontend running nginx' --resource-version=1
# Update pod 'foo' by removing an annotation named 'description' if it exists.
# Does not require the --overwrite flag.
$ kubectl annotate pods foo description-`
)
func NewCmdAnnotate(f *cmdutil.Factory, out io.Writer) *cobra.Command {
options := &AnnotateOptions{}
cmd := &cobra.Command{
Use: "annotate [--overwrite] RESOURCE NAME KEY_1=VAL_1 ... KEY_N=VAL_N [--resource-version=version]",
Short: "Update the annotations on a resource",
Long: annotate_long,
Example: annotate_example,
Run: func(cmd *cobra.Command, args []string) {
if err := options.Complete(f, args, out); err != nil {
cmdutil.CheckErr(err)
}
if err := options.Validate(args); err != nil {
cmdutil.CheckErr(cmdutil.UsageError(cmd, err.Error()))
}
if err := options.RunAnnotate(); err != nil {
cmdutil.CheckErr(err)
}
},
}
cmdutil.AddPrinterFlags(cmd)
cmd.Flags().BoolVar(&options.overwrite, "overwrite", false, "If true, allow annotations to be overwritten, otherwise reject annotation updates that overwrite existing annotations.")
cmd.Flags().BoolVar(&options.all, "all", false, "select all resources in the namespace of the specified resource types")
cmd.Flags().StringVar(&options.resourceVersion, "resource-version", "", "If non-empty, the annotation update will only succeed if this is the current resource-version for the object. Only valid when specifying a single resource.")
return cmd
}
// Complete adapts from the command line args and factory to the data required.
func (o *AnnotateOptions) Complete(f *cmdutil.Factory, args []string, out io.Writer) (err error) {
namespace, _, err := f.DefaultNamespace()
if err != nil {
return err
}
// retrieves resource and annotation args from args
// also checks args to verify that all resources are specified before annotations
annotationArgs := []string{}
metAnnotaionArg := false
for _, s := range args {
isAnnotation := strings.Contains(s, "=") || strings.HasSuffix(s, "-")
switch {
case !metAnnotaionArg && isAnnotation:
metAnnotaionArg = true
fallthrough
case metAnnotaionArg && isAnnotation:
annotationArgs = append(annotationArgs, s)
case !metAnnotaionArg && !isAnnotation:
o.resources = append(o.resources, s)
case metAnnotaionArg && !isAnnotation:
return fmt.Errorf("all resources must be specified before annotation changes: %s", s)
}
}
if len(o.resources) < 1 {
return fmt.Errorf("one or more resources must be specified as <resource> <name> or <resource>/<name>")
}
if len(annotationArgs) < 1 {
return fmt.Errorf("at least one annotation update is required")
}
if o.newAnnotations, o.removeAnnotations, err = parseAnnotations(annotationArgs); err != nil {
return err
}
mapper, typer := f.Object()
o.builder = resource.NewBuilder(mapper, typer, f.ClientMapperForCommand()).
ContinueOnError().
NamespaceParam(namespace).DefaultNamespace().
ResourceTypeOrNameArgs(o.all, o.resources...).
Flatten().
Latest()
return nil
}
// Validate checks to the AnnotateOptions to see if there is sufficient information run the command.
func (o AnnotateOptions) Validate(args []string) error {
if err := validateAnnotations(o.removeAnnotations, o.newAnnotations); err != nil {
return err
}
// only apply resource version locking on a single resource
if len(o.resources) > 1 && len(o.resourceVersion) > 0 {
return fmt.Errorf("--resource-version may only be used with a single resource")
}
return nil
}
// RunAnnotate does the work
func (o AnnotateOptions) RunAnnotate() error {
r := o.builder.Do()
if err := r.Err(); err != nil {
return err
}
return r.Visit(func(info *resource.Info) error {
_, err := cmdutil.UpdateObject(info, func(obj runtime.Object) error {
err := o.updateAnnotations(obj)
if err != nil {
return err
}
return nil
})
if err != nil {
return err
}
return nil
})
}
// parseAnnotations retrieves new and remove annotations from annotation args
func parseAnnotations(annotationArgs []string) (map[string]string, []string, error) {
var invalidBuf bytes.Buffer
newAnnotations := map[string]string{}
removeAnnotations := []string{}
for _, annotationArg := range annotationArgs {
if strings.Index(annotationArg, "=") != -1 {
parts := strings.SplitN(annotationArg, "=", 2)
if len(parts) != 2 || len(parts[1]) == 0 {
if invalidBuf.Len() > 0 {
invalidBuf.WriteString(", ")
}
invalidBuf.WriteString(fmt.Sprintf(annotationArg))
} else {
newAnnotations[parts[0]] = parts[1]
}
} else if strings.HasSuffix(annotationArg, "-") {
removeAnnotations = append(removeAnnotations, annotationArg[:len(annotationArg)-1])
} else {
if invalidBuf.Len() > 0 {
invalidBuf.WriteString(", ")
}
invalidBuf.WriteString(fmt.Sprintf(annotationArg))
}
}
if invalidBuf.Len() > 0 {
return newAnnotations, removeAnnotations, fmt.Errorf("invalid annotation format: %s", invalidBuf.String())
}
return newAnnotations, removeAnnotations, nil
}
// validateAnnotations checks the format of annotation args and checks removed annotations aren't in the new annotations map
func validateAnnotations(removeAnnotations []string, newAnnotations map[string]string) error {
var modifyRemoveBuf bytes.Buffer
for _, removeAnnotation := range removeAnnotations {
if _, found := newAnnotations[removeAnnotation]; found {
if modifyRemoveBuf.Len() > 0 {
modifyRemoveBuf.WriteString(", ")
}
modifyRemoveBuf.WriteString(fmt.Sprintf(removeAnnotation))
}
}
if modifyRemoveBuf.Len() > 0 {
return fmt.Errorf("can not both modify and remove the following annotation(s) in the same command: %s", modifyRemoveBuf.String())
}
return nil
}
// validateNoAnnotationOverwrites validates that when overwrite is false, to-be-updated annotations don't exist in the object annotation map (yet)
func validateNoAnnotationOverwrites(meta *api.ObjectMeta, annotations map[string]string) error {
var buf bytes.Buffer
for key := range annotations {
if value, found := meta.Annotations[key]; found {
if buf.Len() > 0 {
buf.WriteString("; ")
}
buf.WriteString(fmt.Sprintf("'%s' already has a value (%s)", key, value))
}
}
if buf.Len() > 0 {
return fmt.Errorf("--overwrite is false but found the following declared annotation(s): %s", buf.String())
}
return nil
}
// updateAnnotations updates annotations of obj
func (o AnnotateOptions) updateAnnotations(obj runtime.Object) error {
meta, err := api.ObjectMetaFor(obj)
if err != nil {
return err
}
if !o.overwrite {
if err := validateNoAnnotationOverwrites(meta, o.newAnnotations); err != nil {
return err
}
}
if meta.Annotations == nil {
meta.Annotations = make(map[string]string)
}
for key, value := range o.newAnnotations {
meta.Annotations[key] = value
}
for _, annotation := range o.removeAnnotations {
delete(meta.Annotations, annotation)
}
if len(o.resourceVersion) != 0 {
meta.ResourceVersion = o.resourceVersion
}
return nil
}

View File

@ -0,0 +1,515 @@
/*
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 (
"bytes"
"net/http"
"reflect"
"strings"
"testing"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/testapi"
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
)
func TestValidateAnnotationOverwrites(t *testing.T) {
tests := []struct {
meta *api.ObjectMeta
annotations map[string]string
expectErr bool
scenario string
}{
{
meta: &api.ObjectMeta{
Annotations: map[string]string{
"a": "A",
"b": "B",
},
},
annotations: map[string]string{
"a": "a",
"c": "C",
},
scenario: "share first annotation",
expectErr: true,
},
{
meta: &api.ObjectMeta{
Annotations: map[string]string{
"a": "A",
"c": "C",
},
},
annotations: map[string]string{
"b": "B",
"c": "c",
},
scenario: "share second annotation",
expectErr: true,
},
{
meta: &api.ObjectMeta{
Annotations: map[string]string{
"a": "A",
"c": "C",
},
},
annotations: map[string]string{
"b": "B",
"d": "D",
},
scenario: "no overlap",
},
{
meta: &api.ObjectMeta{},
annotations: map[string]string{
"a": "A",
"b": "B",
},
scenario: "no annotations",
},
}
for _, test := range tests {
err := validateNoAnnotationOverwrites(test.meta, test.annotations)
if test.expectErr && err == nil {
t.Errorf("%s: unexpected non-error", test.scenario)
} else if !test.expectErr && err != nil {
t.Errorf("%s: unexpected error: %v", test.scenario, err)
}
}
}
func TestParseAnnotations(t *testing.T) {
testURL := "https://test.com/index.htm?id=123#u=user-name"
testJSON := `'{"kind":"SerializedReference","apiVersion":"v1","reference":{"kind":"ReplicationController","namespace":"default","name":"my-nginx","uid":"c544ee78-2665-11e5-8051-42010af0c213","apiVersion":"v1","resourceVersion":"61368"}}'`
tests := []struct {
annotations []string
expected map[string]string
expectedRemove []string
scenario string
expectedErr string
expectErr bool
}{
{
annotations: []string{"a=b", "c=d"},
expected: map[string]string{"a": "b", "c": "d"},
expectedRemove: []string{},
scenario: "add two annotations",
expectErr: false,
},
{
annotations: []string{"url=" + testURL, "kubernetes.io/created-by=" + testJSON},
expected: map[string]string{"url": testURL, "kubernetes.io/created-by": testJSON},
expectedRemove: []string{},
scenario: "add annotations with special characters",
expectErr: false,
},
{
annotations: []string{},
expected: map[string]string{},
expectedRemove: []string{},
scenario: "add no annotations",
expectErr: false,
},
{
annotations: []string{"a=b", "c=d", "e-"},
expected: map[string]string{"a": "b", "c": "d"},
expectedRemove: []string{"e"},
scenario: "add two annotations, remove one",
expectErr: false,
},
{
annotations: []string{"ab", "c=d"},
expectedErr: "invalid annotation format: ab",
scenario: "incorrect annotation input (missing =value)",
expectErr: true,
},
{
annotations: []string{"a="},
expectedErr: "invalid annotation format: a=",
scenario: "incorrect annotation input (missing value)",
expectErr: true,
},
{
annotations: []string{"ab", "a="},
expectedErr: "invalid annotation format: ab, a=",
scenario: "incorrect multiple annotation input (missing value)",
expectErr: true,
},
}
for _, test := range tests {
annotations, remove, err := parseAnnotations(test.annotations)
switch {
case test.expectErr && err == nil:
t.Errorf("%s: unexpected non-error, should return %v", test.scenario, test.expectedErr)
case test.expectErr && err.Error() != test.expectedErr:
t.Errorf("%s: unexpected error %v, expected %v", test.scenario, err, test.expectedErr)
case !test.expectErr && err != nil:
t.Errorf("%s: unexpected error %v", test.scenario, err)
case !test.expectErr && !reflect.DeepEqual(annotations, test.expected):
t.Errorf("%s: expected %v, got %v", test.scenario, test.expected, annotations)
case !test.expectErr && !reflect.DeepEqual(remove, test.expectedRemove):
t.Errorf("%s: expected %v, got %v", test.scenario, test.expectedRemove, remove)
}
}
}
func TestValidateAnnotations(t *testing.T) {
tests := []struct {
removeAnnotations []string
newAnnotations map[string]string
expectedErr string
scenario string
}{
{
expectedErr: "can not both modify and remove the following annotation(s) in the same command: a",
removeAnnotations: []string{"a"},
newAnnotations: map[string]string{"a": "b", "c": "d"},
scenario: "remove an added annotation",
},
{
expectedErr: "can not both modify and remove the following annotation(s) in the same command: a, c",
removeAnnotations: []string{"a", "c"},
newAnnotations: map[string]string{"a": "b", "c": "d"},
scenario: "remove added annotations",
},
}
for _, test := range tests {
if err := validateAnnotations(test.removeAnnotations, test.newAnnotations); err == nil {
t.Errorf("%s: unexpected non-error", test.scenario)
} else if err.Error() != test.expectedErr {
t.Errorf("%s: expected error %s, got %s", test.scenario, test.expectedErr, err.Error())
}
}
}
func TestUpdateAnnotations(t *testing.T) {
tests := []struct {
obj runtime.Object
overwrite bool
version string
annotations map[string]string
remove []string
expected runtime.Object
expectErr bool
}{
{
obj: &api.Pod{
ObjectMeta: api.ObjectMeta{
Annotations: map[string]string{"a": "b"},
},
},
annotations: map[string]string{"a": "b"},
expectErr: true,
},
{
obj: &api.Pod{
ObjectMeta: api.ObjectMeta{
Annotations: map[string]string{"a": "b"},
},
},
annotations: map[string]string{"a": "c"},
overwrite: true,
expected: &api.Pod{
ObjectMeta: api.ObjectMeta{
Annotations: map[string]string{"a": "c"},
},
},
},
{
obj: &api.Pod{
ObjectMeta: api.ObjectMeta{
Annotations: map[string]string{"a": "b"},
},
},
annotations: map[string]string{"c": "d"},
expected: &api.Pod{
ObjectMeta: api.ObjectMeta{
Annotations: map[string]string{"a": "b", "c": "d"},
},
},
},
{
obj: &api.Pod{
ObjectMeta: api.ObjectMeta{
Annotations: map[string]string{"a": "b"},
},
},
annotations: map[string]string{"c": "d"},
version: "2",
expected: &api.Pod{
ObjectMeta: api.ObjectMeta{
Annotations: map[string]string{"a": "b", "c": "d"},
ResourceVersion: "2",
},
},
},
{
obj: &api.Pod{
ObjectMeta: api.ObjectMeta{
Annotations: map[string]string{"a": "b"},
},
},
annotations: map[string]string{},
remove: []string{"a"},
expected: &api.Pod{
ObjectMeta: api.ObjectMeta{
Annotations: map[string]string{},
},
},
},
{
obj: &api.Pod{
ObjectMeta: api.ObjectMeta{
Annotations: map[string]string{"a": "b", "c": "d"},
},
},
annotations: map[string]string{"e": "f"},
remove: []string{"a"},
expected: &api.Pod{
ObjectMeta: api.ObjectMeta{
Annotations: map[string]string{
"c": "d",
"e": "f",
},
},
},
},
{
obj: &api.Pod{
ObjectMeta: api.ObjectMeta{
Annotations: map[string]string{"a": "b", "c": "d"},
},
},
annotations: map[string]string{"e": "f"},
remove: []string{"g"},
expected: &api.Pod{
ObjectMeta: api.ObjectMeta{
Annotations: map[string]string{
"a": "b",
"c": "d",
"e": "f",
},
},
},
},
{
obj: &api.Pod{
ObjectMeta: api.ObjectMeta{
Annotations: map[string]string{"a": "b", "c": "d"},
},
},
remove: []string{"e"},
expected: &api.Pod{
ObjectMeta: api.ObjectMeta{
Annotations: map[string]string{
"a": "b",
"c": "d",
},
},
},
},
{
obj: &api.Pod{
ObjectMeta: api.ObjectMeta{},
},
annotations: map[string]string{"a": "b"},
expected: &api.Pod{
ObjectMeta: api.ObjectMeta{
Annotations: map[string]string{"a": "b"},
},
},
},
}
for _, test := range tests {
options := &AnnotateOptions{
overwrite: test.overwrite,
newAnnotations: test.annotations,
removeAnnotations: test.remove,
resourceVersion: test.version,
}
err := options.updateAnnotations(test.obj)
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(test.obj, test.expected) {
t.Errorf("expected: %v, got %v", test.expected, test.obj)
}
}
}
func TestAnnotateErrors(t *testing.T) {
testCases := map[string]struct {
args []string
flags map[string]string
errFn func(error) bool
}{
"no args": {
args: []string{},
errFn: func(err error) bool { return strings.Contains(err.Error(), "one or more resources must be specified") },
},
"not enough annotations": {
args: []string{"pods"},
errFn: func(err error) bool {
return strings.Contains(err.Error(), "at least one annotation update is required")
},
},
"no resources remove annotations": {
args: []string{"pods-"},
errFn: func(err error) bool { return strings.Contains(err.Error(), "one or more resources must be specified") },
},
"no resources add annotations": {
args: []string{"pods=bar"},
errFn: func(err error) bool { return strings.Contains(err.Error(), "one or more resources must be specified") },
},
}
for k, testCase := range testCases {
f, tf, _ := NewAPIFactory()
tf.Printer = &testPrinter{}
tf.Namespace = "test"
tf.ClientConfig = &client.Config{Version: testapi.Version()}
buf := bytes.NewBuffer([]byte{})
cmd := NewCmdAnnotate(f, buf)
cmd.SetOutput(buf)
for k, v := range testCase.flags {
cmd.Flags().Set(k, v)
}
options := &AnnotateOptions{}
err := options.Complete(f, testCase.args, buf)
if !testCase.errFn(err) {
t.Errorf("%s: unexpected error: %v", k, err)
continue
}
if tf.Printer.(*testPrinter).Objects != nil {
t.Errorf("unexpected print to default printer")
}
if buf.Len() > 0 {
t.Errorf("buffer should be empty: %s", string(buf.Bytes()))
}
}
}
func TestAnnotateObject(t *testing.T) {
pods, _, _ := testData()
f, tf, codec := NewAPIFactory()
tf.Printer = &testPrinter{}
tf.Client = &client.FakeRESTClient{
Codec: codec,
Client: client.HTTPClientFunc(func(req *http.Request) (*http.Response, error) {
switch req.Method {
case "GET":
switch req.URL.Path {
case "/namespaces/test/pods/foo":
return &http.Response{StatusCode: 200, Body: objBody(codec, &pods.Items[0])}, nil
default:
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
return nil, nil
}
case "PUT":
switch req.URL.Path {
case "/namespaces/test/pods/foo":
return &http.Response{StatusCode: 200, Body: objBody(codec, &pods.Items[0])}, nil
default:
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
return nil, nil
}
default:
t.Fatalf("unexpected request: %s %#v\n%#v", req.Method, req.URL, req)
return nil, nil
}
}),
}
tf.Namespace = "test"
tf.ClientConfig = &client.Config{Version: testapi.Version()}
buf := bytes.NewBuffer([]byte{})
options := &AnnotateOptions{}
args := []string{"pods/foo", "a=b", "c-"}
if err := options.Complete(f, args, buf); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if err := options.Validate(args); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if err := options.RunAnnotate(); err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
func TestAnnotateMultipleObjects(t *testing.T) {
pods, _, _ := testData()
f, tf, codec := NewAPIFactory()
tf.Printer = &testPrinter{}
tf.Client = &client.FakeRESTClient{
Codec: codec,
Client: client.HTTPClientFunc(func(req *http.Request) (*http.Response, error) {
switch req.Method {
case "GET":
switch req.URL.Path {
case "/namespaces/test/pods":
return &http.Response{StatusCode: 200, Body: objBody(codec, pods)}, nil
default:
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
return nil, nil
}
case "PUT":
switch req.URL.Path {
case "/namespaces/test/pods/foo":
return &http.Response{StatusCode: 200, Body: objBody(codec, &pods.Items[0])}, nil
case "/namespaces/test/pods/bar":
return &http.Response{StatusCode: 200, Body: objBody(codec, &pods.Items[1])}, nil
default:
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
return nil, nil
}
default:
t.Fatalf("unexpected request: %s %#v\n%#v", req.Method, req.URL, req)
return nil, nil
}
}),
}
tf.Namespace = "test"
tf.ClientConfig = &client.Config{Version: testapi.Version()}
buf := bytes.NewBuffer([]byte{})
options := &AnnotateOptions{}
options.all = true
args := []string{"pods", "a=b", "c-"}
if err := options.Complete(f, args, buf); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if err := options.Validate(args); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if err := options.RunAnnotate(); err != nil {
t.Fatalf("unexpected error: %v", err)
}
}

View File

@ -146,6 +146,7 @@ Find more information at https://github.com/GoogleCloudPlatform/kubernetes.`,
cmds.AddCommand(NewCmdExposeService(f, out))
cmds.AddCommand(NewCmdLabel(f, out))
cmds.AddCommand(NewCmdAnnotate(f, out))
cmds.AddCommand(cmdconfig.NewCmdConfig(cmdconfig.NewDefaultPathOptions(), out))
cmds.AddCommand(NewCmdClusterInfo(f, out))

View File

@ -71,25 +71,6 @@ func NewCmdLabel(f *cmdutil.Factory, out io.Writer) *cobra.Command {
return cmd
}
func updateObject(info *resource.Info, updateFn func(runtime.Object) (runtime.Object, error)) (runtime.Object, error) {
helper := resource.NewHelper(info.Client, info.Mapping)
obj, err := updateFn(info.Object)
if err != nil {
return nil, err
}
data, err := helper.Codec.Encode(obj)
if err != nil {
return nil, err
}
_, err = helper.Replace(info.Namespace, info.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 {
@ -123,14 +104,14 @@ func parseLabels(spec []string) (map[string]string, []string, error) {
return labels, remove, nil
}
func labelFunc(obj runtime.Object, overwrite bool, resourceVersion string, labels map[string]string, remove []string) (runtime.Object, error) {
func labelFunc(obj runtime.Object, overwrite bool, resourceVersion string, labels map[string]string, remove []string) error {
meta, err := api.ObjectMetaFor(obj)
if err != nil {
return nil, err
return err
}
if !overwrite {
if err := validateNoOverwrites(meta, labels); err != nil {
return nil, err
return err
}
}
@ -148,7 +129,7 @@ func labelFunc(obj runtime.Object, overwrite bool, resourceVersion string, label
if len(resourceVersion) != 0 {
meta.ResourceVersion = resourceVersion
}
return obj, nil
return nil
}
func RunLabel(f *cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []string) error {
@ -211,12 +192,12 @@ func RunLabel(f *cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []stri
// TODO: support bulk generic output a la Get
return r.Visit(func(info *resource.Info) error {
obj, err := updateObject(info, func(obj runtime.Object) (runtime.Object, error) {
outObj, err := labelFunc(obj, overwrite, resourceVersion, labels, remove)
obj, err := cmdutil.UpdateObject(info, func(obj runtime.Object) error {
err := labelFunc(obj, overwrite, resourceVersion, labels, remove)
if err != nil {
return nil, err
return err
}
return outObj, nil
return nil
})
if err != nil {
return err

View File

@ -256,7 +256,7 @@ func TestLabelFunc(t *testing.T) {
},
}
for _, test := range tests {
out, err := labelFunc(test.obj, test.overwrite, test.version, test.labels, test.remove)
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)
@ -266,8 +266,8 @@ func TestLabelFunc(t *testing.T) {
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)
if !reflect.DeepEqual(test.obj, test.expected) {
t.Errorf("expected: %v, got %v", test.expected, test.obj)
}
}
}

View File

@ -34,6 +34,7 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest"
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
"github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd"
"github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/resource"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
utilerrors "github.com/GoogleCloudPlatform/kubernetes/pkg/util/errors"
@ -431,3 +432,23 @@ func DumpReaderToFile(reader io.Reader, filename string) error {
}
return nil
}
// UpdateObject updates resource object with updateFn
func UpdateObject(info *resource.Info, updateFn func(runtime.Object) error) (runtime.Object, error) {
helper := resource.NewHelper(info.Client, info.Mapping)
err := updateFn(info.Object)
if err != nil {
return nil, err
}
data, err := helper.Codec.Encode(info.Object)
if err != nil {
return nil, err
}
_, err = helper.Replace(info.Namespace, info.Name, true, data)
if err != nil {
return nil, err
}
return info.Object, nil
}