Merge pull request #41146 from shiywang/apply-view1

Automatic merge from submit-queue (batch tested with PRs 41146, 41486, 41482, 41538, 41784)

 Add apply view-last-applied subcommand

reopen pr https://github.com/kubernetes/kubernetes/pull/40984, implement part of https://github.com/kubernetes/community/pull/287
for now unit test all pass, the output looks like:

```console
shiywang@dhcp-140-33 template $ ./kubectl apply view last-applied deployment nginx-deployment 
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  creationTimestamp: null
  name: nginx-deployment
spec:
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: nginx
    spec:
      containers:
      - image: nginx:1.12.10
        name: nginx
        ports:
        - containerPort: 80
        resources: {}
status: {}
```

```release-note
Support new kubectl apply view-last-applied command for viewing the last configuration file applied
```

not sure if there is any flag I should updated or the some error handling I should changed.
will generate docs when you guys think is ok.
cc @pwittrock @jessfraz @AdoHe @ymqytw
This commit is contained in:
Kubernetes Submit Queue 2017-02-22 21:09:31 -08:00 committed by GitHub
commit afd3db25cf
8 changed files with 327 additions and 3 deletions

View File

@ -12,6 +12,7 @@ docs/man/man1/kube-proxy.1
docs/man/man1/kube-scheduler.1 docs/man/man1/kube-scheduler.1
docs/man/man1/kubectl-annotate.1 docs/man/man1/kubectl-annotate.1
docs/man/man1/kubectl-api-versions.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-apply.1
docs/man/man1/kubectl-attach.1 docs/man/man1/kubectl-attach.1
docs/man/man1/kubectl-autoscale.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_annotate.md
docs/user-guide/kubectl/kubectl_api-versions.md docs/user-guide/kubectl/kubectl_api-versions.md
docs/user-guide/kubectl/kubectl_apply.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_attach.md
docs/user-guide/kubectl/kubectl_autoscale.md docs/user-guide/kubectl/kubectl_autoscale.md
docs/user-guide/kubectl/kubectl_certificate.md docs/user-guide/kubectl/kubectl_certificate.md

View File

@ -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.

View File

@ -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.

View File

@ -14,6 +14,7 @@ go_library(
"annotate.go", "annotate.go",
"apiversions.go", "apiversions.go",
"apply.go", "apply.go",
"apply_view_last_applied.go",
"attach.go", "attach.go",
"autoscale.go", "autoscale.go",
"certificates.go", "certificates.go",
@ -98,6 +99,7 @@ go_library(
"//vendor:github.com/docker/distribution/reference", "//vendor:github.com/docker/distribution/reference",
"//vendor:github.com/docker/docker/pkg/term", "//vendor:github.com/docker/docker/pkg/term",
"//vendor:github.com/evanphx/json-patch", "//vendor:github.com/evanphx/json-patch",
"//vendor:github.com/ghodss/yaml",
"//vendor:github.com/golang/glog", "//vendor:github.com/golang/glog",
"//vendor:github.com/jonboulle/clockwork", "//vendor:github.com/jonboulle/clockwork",
"//vendor:github.com/renstrom/dedent", "//vendor:github.com/renstrom/dedent",

View File

@ -128,6 +128,10 @@ func NewCmdApply(f cmdutil.Factory, out, errOut io.Writer) *cobra.Command {
cmdutil.AddPrinterFlags(cmd) cmdutil.AddPrinterFlags(cmd)
cmdutil.AddRecordFlag(cmd) cmdutil.AddRecordFlag(cmd)
cmdutil.AddInclude3rdPartyFlags(cmd) cmdutil.AddInclude3rdPartyFlags(cmd)
// apply subcommands
cmd.AddCommand(NewCmdApplyViewLastApplied(f, out, errOut))
return cmd return cmd
} }

View File

@ -59,9 +59,10 @@ func validateApplyArgs(cmd *cobra.Command, args []string) error {
} }
const ( const (
filenameRC = "../../../test/fixtures/pkg/kubectl/cmd/apply/rc.yaml" filenameRC = "../../../test/fixtures/pkg/kubectl/cmd/apply/rc.yaml"
filenameSVC = "../../../test/fixtures/pkg/kubectl/cmd/apply/service.yaml" filenameRCLASTAPPLIED = "../../../test/fixtures/pkg/kubectl/cmd/apply/rc-lastapplied.yaml"
filenameRCSVC = "../../../test/fixtures/pkg/kubectl/cmd/apply/rc-service.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 { 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 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) { func TestApplyObjectWithoutAnnotation(t *testing.T) {
initTestErrorHandler(t) initTestErrorHandler(t)
nameRC, rcBytes := readReplicationController(t, filenameRC) nameRC, rcBytes := readReplicationController(t, filenameRC)

View File

@ -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)
}
}

View File

@ -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