From 557c18694a2ccd6b70e26e9be6deb1785a68d36a Mon Sep 17 00:00:00 2001 From: shiywang Date: Thu, 9 Feb 2017 01:42:31 +0800 Subject: [PATCH] Add apply view last-applied subcommand change to GetOriginalConfiguration add bazel refactor apply view-last-applied command update some changes minor change add unit tests, update update some codes and genreate docs update LongDesc --- docs/.generated_docs | 2 + .../man1/kubectl-apply-view-last-applied.1 | 3 + .../kubectl_apply_view-last-applied.md | 3 + pkg/kubectl/cmd/BUILD | 2 + pkg/kubectl/cmd/apply.go | 4 + pkg/kubectl/cmd/apply_test.go | 130 +++++++++++++- pkg/kubectl/cmd/apply_view_last_applied.go | 165 ++++++++++++++++++ .../pkg/kubectl/cmd/apply/rc-lastapplied.yaml | 21 +++ 8 files changed, 327 insertions(+), 3 deletions(-) create mode 100644 docs/man/man1/kubectl-apply-view-last-applied.1 create mode 100644 docs/user-guide/kubectl/kubectl_apply_view-last-applied.md create mode 100644 pkg/kubectl/cmd/apply_view_last_applied.go create mode 100644 test/fixtures/pkg/kubectl/cmd/apply/rc-lastapplied.yaml diff --git a/docs/.generated_docs b/docs/.generated_docs index 7be6b168c6f..5d0ada9c2a5 100644 --- a/docs/.generated_docs +++ b/docs/.generated_docs @@ -12,6 +12,7 @@ docs/man/man1/kube-proxy.1 docs/man/man1/kube-scheduler.1 docs/man/man1/kubectl-annotate.1 docs/man/man1/kubectl-api-versions.1 +docs/man/man1/kubectl-apply-view-last-applied.1 docs/man/man1/kubectl-apply.1 docs/man/man1/kubectl-attach.1 docs/man/man1/kubectl-autoscale.1 @@ -97,6 +98,7 @@ docs/user-guide/kubectl/kubectl.md docs/user-guide/kubectl/kubectl_annotate.md docs/user-guide/kubectl/kubectl_api-versions.md docs/user-guide/kubectl/kubectl_apply.md +docs/user-guide/kubectl/kubectl_apply_view-last-applied.md docs/user-guide/kubectl/kubectl_attach.md docs/user-guide/kubectl/kubectl_autoscale.md docs/user-guide/kubectl/kubectl_certificate.md diff --git a/docs/man/man1/kubectl-apply-view-last-applied.1 b/docs/man/man1/kubectl-apply-view-last-applied.1 new file mode 100644 index 00000000000..b6fd7a0f989 --- /dev/null +++ b/docs/man/man1/kubectl-apply-view-last-applied.1 @@ -0,0 +1,3 @@ +This file is autogenerated, but we've stopped checking such files into the +repository to reduce the need for rebases. Please run hack/generate-docs.sh to +populate this file. diff --git a/docs/user-guide/kubectl/kubectl_apply_view-last-applied.md b/docs/user-guide/kubectl/kubectl_apply_view-last-applied.md new file mode 100644 index 00000000000..b6fd7a0f989 --- /dev/null +++ b/docs/user-guide/kubectl/kubectl_apply_view-last-applied.md @@ -0,0 +1,3 @@ +This file is autogenerated, but we've stopped checking such files into the +repository to reduce the need for rebases. Please run hack/generate-docs.sh to +populate this file. diff --git a/pkg/kubectl/cmd/BUILD b/pkg/kubectl/cmd/BUILD index d38b7cf9b0c..1ddff19e5aa 100644 --- a/pkg/kubectl/cmd/BUILD +++ b/pkg/kubectl/cmd/BUILD @@ -14,6 +14,7 @@ go_library( "annotate.go", "apiversions.go", "apply.go", + "apply_view_last_applied.go", "attach.go", "autoscale.go", "certificates.go", @@ -98,6 +99,7 @@ go_library( "//vendor:github.com/docker/distribution/reference", "//vendor:github.com/docker/docker/pkg/term", "//vendor:github.com/evanphx/json-patch", + "//vendor:github.com/ghodss/yaml", "//vendor:github.com/golang/glog", "//vendor:github.com/jonboulle/clockwork", "//vendor:github.com/renstrom/dedent", diff --git a/pkg/kubectl/cmd/apply.go b/pkg/kubectl/cmd/apply.go index 7d5d2e00070..98085275df6 100644 --- a/pkg/kubectl/cmd/apply.go +++ b/pkg/kubectl/cmd/apply.go @@ -128,6 +128,10 @@ func NewCmdApply(f cmdutil.Factory, out, errOut io.Writer) *cobra.Command { cmdutil.AddPrinterFlags(cmd) cmdutil.AddRecordFlag(cmd) cmdutil.AddInclude3rdPartyFlags(cmd) + + // apply subcommands + cmd.AddCommand(NewCmdApplyViewLastApplied(f, out, errOut)) + return cmd } diff --git a/pkg/kubectl/cmd/apply_test.go b/pkg/kubectl/cmd/apply_test.go index 509d06f2413..c233dd64f19 100644 --- a/pkg/kubectl/cmd/apply_test.go +++ b/pkg/kubectl/cmd/apply_test.go @@ -59,9 +59,10 @@ func validateApplyArgs(cmd *cobra.Command, args []string) error { } const ( - filenameRC = "../../../test/fixtures/pkg/kubectl/cmd/apply/rc.yaml" - filenameSVC = "../../../test/fixtures/pkg/kubectl/cmd/apply/service.yaml" - filenameRCSVC = "../../../test/fixtures/pkg/kubectl/cmd/apply/rc-service.yaml" + filenameRC = "../../../test/fixtures/pkg/kubectl/cmd/apply/rc.yaml" + filenameRCLASTAPPLIED = "../../../test/fixtures/pkg/kubectl/cmd/apply/rc-lastapplied.yaml" + filenameSVC = "../../../test/fixtures/pkg/kubectl/cmd/apply/service.yaml" + filenameRCSVC = "../../../test/fixtures/pkg/kubectl/cmd/apply/rc-service.yaml" ) func readBytesFromFile(t *testing.T, filename string) []byte { @@ -193,6 +194,129 @@ func walkMapPath(t *testing.T, start map[string]interface{}, path []string) map[ return finish } +func TestRunApplyViewLastApplied(t *testing.T) { + _, rcBytesWithConfig := readReplicationController(t, filenameRCLASTAPPLIED) + nameRC, rcBytes := readReplicationController(t, filenameRC) + pathRC := "/namespaces/test/replicationcontrollers/" + nameRC + + tests := []struct { + name, nameRC, pathRC, filePath, outputFormat, expectedErr, expectedOut, selector string + args []string + respBytes []byte + }{ + { + name: "view with file", + filePath: filenameRC, + outputFormat: "", + expectedErr: "", + expectedOut: "test: 1234\n", + selector: "", + args: []string{}, + respBytes: rcBytesWithConfig, + }, + { + name: "view with file json format", + filePath: filenameRC, + outputFormat: "json", + expectedErr: "", + expectedOut: "{\n \"test\": 1234\n}\n", + selector: "", + args: []string{}, + respBytes: rcBytesWithConfig, + }, + { + name: "view resource/name invalid format", + filePath: "", + outputFormat: "wide", + expectedErr: "error: Unexpected -o output mode: wide, the flag 'output' must be one of yaml|json\nSee 'view-last-applied -h' for help and examples.", + expectedOut: "", + selector: "", + args: []string{"rc", "test-rc"}, + respBytes: rcBytesWithConfig, + }, + { + name: "view resource with label", + filePath: "", + outputFormat: "", + expectedErr: "", + expectedOut: "test: 1234\n", + selector: "name=test-rc", + args: []string{"rc"}, + respBytes: rcBytesWithConfig, + }, + { + name: "view resource without annotations", + filePath: "", + outputFormat: "", + expectedErr: "error: no last-applied-configuration annotation found on resource: test-rc", + expectedOut: "", + selector: "", + args: []string{"rc", "test-rc"}, + respBytes: rcBytes, + }, + { + name: "view resource no match", + filePath: "", + outputFormat: "", + expectedErr: "Error from server (NotFound): the server could not find the requested resource (get replicationcontrollers no-match)", + expectedOut: "", + selector: "", + args: []string{"rc", "no-match"}, + respBytes: nil, + }, + } + for _, test := range tests { + f, tf, codec, _ := cmdtesting.NewAPIFactory() + tf.Printer = &testPrinter{} + tf.UnstructuredClient = &fake.RESTClient{ + APIRegistry: api.Registry, + NegotiatedSerializer: unstructuredSerializer, + Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { + switch p, m := req.URL.Path, req.Method; { + case p == pathRC && m == "GET": + bodyRC := ioutil.NopCloser(bytes.NewReader(test.respBytes)) + return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: bodyRC}, nil + case p == "/namespaces/test/replicationcontrollers" && m == "GET": + bodyRC := ioutil.NopCloser(bytes.NewReader(test.respBytes)) + return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: bodyRC}, nil + case p == "/namespaces/test/replicationcontrollers/no-match" && m == "GET": + return &http.Response{StatusCode: 404, Header: defaultHeader(), Body: objBody(codec, &api.Pod{})}, nil + case p == "/api/v1/namespaces/test" && m == "GET": + return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: objBody(codec, &api.Namespace{})}, nil + default: + t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) + return nil, nil + } + }), + } + tf.Namespace = "test" + tf.ClientConfig = defaultClientConfig() + buf, errBuf := bytes.NewBuffer([]byte{}), bytes.NewBuffer([]byte{}) + + cmdutil.BehaviorOnFatal(func(str string, code int) { + if str != test.expectedErr { + t.Errorf("%s: unexpected error: %s\nexpected: %s", test.name, str, test.expectedErr) + } + }) + + cmd := NewCmdApplyViewLastApplied(f, buf, errBuf) + if test.filePath != "" { + cmd.Flags().Set("filename", test.filePath) + } + if test.outputFormat != "" { + cmd.Flags().Set("output", test.outputFormat) + } + if test.selector != "" { + cmd.Flags().Set("selector", test.selector) + } + + cmd.Run(cmd, test.args) + if buf.String() != test.expectedOut { + t.Fatalf("%s: unexpected output: %s\nexpected: %s", test.name, buf.String(), test.expectedOut) + } + } +} + func TestApplyObjectWithoutAnnotation(t *testing.T) { initTestErrorHandler(t) nameRC, rcBytes := readReplicationController(t, filenameRC) diff --git a/pkg/kubectl/cmd/apply_view_last_applied.go b/pkg/kubectl/cmd/apply_view_last_applied.go new file mode 100644 index 00000000000..f763d0034c8 --- /dev/null +++ b/pkg/kubectl/cmd/apply_view_last_applied.go @@ -0,0 +1,165 @@ +/* +Copyright 2017 The Kubernetes Authors. + +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" + "encoding/json" + "fmt" + "io" + + "github.com/ghodss/yaml" + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/kubernetes/pkg/kubectl" + "k8s.io/kubernetes/pkg/kubectl/cmd/templates" + cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" + "k8s.io/kubernetes/pkg/kubectl/resource" +) + +type ViewLastAppliedOptions struct { + FilenameOptions resource.FilenameOptions + Selector string + LastAppliedConfigurationList []string + OutputFormat string + Factory cmdutil.Factory + Out io.Writer + ErrOut io.Writer +} + +var ( + applyViewLastAppliedLong = templates.LongDesc(` + View the latest last-applied-configuration annotations by type/name or file. + + The default output will be printed to stdout in YAML format. One can use -o option + to change output format.`) + + applyViewLastAppliedExample = templates.Examples(` + # View the last-applied-configuration annotations by type/name in YAML. + kubectl apply view-last-applied deployment/nginx + + # View the last-applied-configuration annotations by file in JSON + kubectl apply view-last-applied -f deploy.yaml -o json`) +) + +func NewCmdApplyViewLastApplied(f cmdutil.Factory, out, err io.Writer) *cobra.Command { + options := &ViewLastAppliedOptions{Out: out, ErrOut: err} + cmd := &cobra.Command{ + Use: "view-last-applied (TYPE [NAME | -l label] | TYPE/NAME | -f FILENAME)", + Short: "View latest last-applied-configuration annotations of a resource/object", + Long: applyViewLastAppliedLong, + Example: applyViewLastAppliedExample, + Run: func(cmd *cobra.Command, args []string) { + cmdutil.CheckErr(options.ValidateOutputArgs(cmd)) + cmdutil.CheckErr(options.Complete(f, args)) + cmdutil.CheckErr(options.Validate(cmd)) + cmdutil.CheckErr(options.RunApplyViewLastApplied()) + }, + } + + cmd.Flags().StringP("output", "o", "", "Output format. Must be one of yaml|json") + cmd.Flags().StringVarP(&options.Selector, "selector", "l", "", "Selector (label query) to filter on, supports '=', '==', and '!='.") + usage := "that contains the last-applied-configuration annotations" + cmdutil.AddFilenameOptionFlags(cmd, &options.FilenameOptions, usage) + + return cmd +} + +func (o *ViewLastAppliedOptions) Complete(f cmdutil.Factory, args []string) error { + mapper, typer, err := f.UnstructuredObject() + if err != nil { + return err + } + + cmdNamespace, enforceNamespace, err := f.DefaultNamespace() + if err != nil { + return err + } + + r := resource.NewBuilder(mapper, typer, resource.ClientMapperFunc(f.UnstructuredClientForMapping), unstructured.UnstructuredJSONScheme). + NamespaceParam(cmdNamespace).DefaultNamespace(). + FilenameParam(enforceNamespace, &o.FilenameOptions). + ResourceTypeOrNameArgs(enforceNamespace, args...). + SelectorParam(o.Selector). + Latest(). + Flatten(). + Do() + err = r.Err() + if err != nil { + return err + } + + err = r.Visit(func(info *resource.Info, err error) error { + if err != nil { + return err + } + + configString, err := kubectl.GetOriginalConfiguration(info.Mapping, info.Object) + if err != nil { + return err + } + if configString == nil { + return cmdutil.AddSourceToErr(fmt.Sprintf("no last-applied-configuration annotation found on resource: %s\n", info.Name), info.Source, err) + } + o.LastAppliedConfigurationList = append(o.LastAppliedConfigurationList, string(configString)) + return nil + }) + + if err != nil { + return err + } + + return nil +} + +func (o *ViewLastAppliedOptions) Validate(cmd *cobra.Command) error { + return nil +} + +func (o *ViewLastAppliedOptions) RunApplyViewLastApplied() error { + for _, str := range o.LastAppliedConfigurationList { + yamlOutput, err := yaml.JSONToYAML([]byte(str)) + switch o.OutputFormat { + case "json": + jsonBuffer := &bytes.Buffer{} + err = json.Indent(jsonBuffer, []byte(str), "", " ") + if err != nil { + return err + } + fmt.Fprintf(o.Out, string(jsonBuffer.Bytes())) + case "yaml": + fmt.Fprintf(o.Out, string(yamlOutput)) + } + } + + return nil +} + +func (o *ViewLastAppliedOptions) ValidateOutputArgs(cmd *cobra.Command) error { + format := cmdutil.GetFlagString(cmd, "output") + switch format { + case "json": + o.OutputFormat = "json" + return nil + // If flag -o is not specified, use yaml as default + case "yaml", "": + o.OutputFormat = "yaml" + return nil + default: + return cmdutil.UsageError(cmd, "Unexpected -o output mode: %s, the flag 'output' must be one of yaml|json", format) + } +} diff --git a/test/fixtures/pkg/kubectl/cmd/apply/rc-lastapplied.yaml b/test/fixtures/pkg/kubectl/cmd/apply/rc-lastapplied.yaml new file mode 100644 index 00000000000..6db0a5c47a1 --- /dev/null +++ b/test/fixtures/pkg/kubectl/cmd/apply/rc-lastapplied.yaml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: ReplicationController +metadata: + annotations: + kubectl.kubernetes.io/last-applied-configuration: | + {"test":1234} + name: test-rc + labels: + name: test-rc +spec: + replicas: 1 + template: + metadata: + labels: + name: test-rc + spec: + containers: + - name: test-rc + image: nginx + ports: + - containerPort: 80