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:
commit
afd3db25cf
@ -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
|
||||||
|
3
docs/man/man1/kubectl-apply-view-last-applied.1
Normal file
3
docs/man/man1/kubectl-apply-view-last-applied.1
Normal 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.
|
@ -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.
|
@ -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",
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
165
pkg/kubectl/cmd/apply_view_last_applied.go
Normal file
165
pkg/kubectl/cmd/apply_view_last_applied.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
21
test/fixtures/pkg/kubectl/cmd/apply/rc-lastapplied.yaml
vendored
Normal file
21
test/fixtures/pkg/kubectl/cmd/apply/rc-lastapplied.yaml
vendored
Normal 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
|
Loading…
Reference in New Issue
Block a user