diff --git a/docs/.generated_docs b/docs/.generated_docs index 70d87281940..48b2a6feaf0 100644 --- a/docs/.generated_docs +++ b/docs/.generated_docs @@ -18,6 +18,7 @@ docs/man/man1/kube-apiserver.1 docs/man/man1/kube-controller-manager.1 docs/man/man1/kube-proxy.1 docs/man/man1/kube-scheduler.1 +docs/man/man1/kubectl-alpha-diff.1 docs/man/man1/kubectl-alpha.1 docs/man/man1/kubectl-annotate.1 docs/man/man1/kubectl-api-versions.1 @@ -115,6 +116,8 @@ docs/man/man1/kubectl-version.1 docs/man/man1/kubectl.1 docs/man/man1/kubelet.1 docs/user-guide/kubectl/kubectl.md +docs/user-guide/kubectl/kubectl_alpha.md +docs/user-guide/kubectl/kubectl_alpha_diff.md docs/user-guide/kubectl/kubectl_annotate.md docs/user-guide/kubectl/kubectl_api-versions.md docs/user-guide/kubectl/kubectl_apply.md diff --git a/docs/man/man1/kubectl-alpha-diff.1 b/docs/man/man1/kubectl-alpha-diff.1 new file mode 100644 index 00000000000..b6fd7a0f989 --- /dev/null +++ b/docs/man/man1/kubectl-alpha-diff.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_alpha.md b/docs/user-guide/kubectl/kubectl_alpha.md new file mode 100644 index 00000000000..b6fd7a0f989 --- /dev/null +++ b/docs/user-guide/kubectl/kubectl_alpha.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/docs/user-guide/kubectl/kubectl_alpha_diff.md b/docs/user-guide/kubectl/kubectl_alpha_diff.md new file mode 100644 index 00000000000..b6fd7a0f989 --- /dev/null +++ b/docs/user-guide/kubectl/kubectl_alpha_diff.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 33bd0e46e49..599665331b9 100644 --- a/pkg/kubectl/cmd/BUILD +++ b/pkg/kubectl/cmd/BUILD @@ -38,6 +38,7 @@ go_library( "create_serviceaccount.go", "delete.go", "describe.go", + "diff.go", "drain.go", "edit.go", "exec.go", @@ -78,6 +79,8 @@ go_library( "//pkg/client/clientset_generated/internalclientset/typed/rbac/internalversion:go_default_library", "//pkg/client/unversioned:go_default_library", "//pkg/kubectl:go_default_library", + "//pkg/kubectl/apply/parse:go_default_library", + "//pkg/kubectl/apply/strategy:go_default_library", "//pkg/kubectl/cmd/auth:go_default_library", "//pkg/kubectl/cmd/config:go_default_library", "//pkg/kubectl/cmd/rollout:go_default_library", @@ -169,6 +172,7 @@ go_test( "create_test.go", "delete_test.go", "describe_test.go", + "diff_test.go", "drain_test.go", "edit_test.go", "exec_test.go", @@ -244,6 +248,7 @@ go_test( "//vendor/k8s.io/client-go/tools/remotecommand:go_default_library", "//vendor/k8s.io/kube-openapi/pkg/util/proto:go_default_library", "//vendor/k8s.io/metrics/pkg/apis/metrics/v1alpha1:go_default_library", + "//vendor/k8s.io/utils/exec:go_default_library", ], ) diff --git a/pkg/kubectl/cmd/alpha.go b/pkg/kubectl/cmd/alpha.go index efec2b9b6d7..afbe7347dc0 100644 --- a/pkg/kubectl/cmd/alpha.go +++ b/pkg/kubectl/cmd/alpha.go @@ -37,6 +37,7 @@ func NewCmdAlpha(f cmdutil.Factory, in io.Reader, out, err io.Writer) *cobra.Com // Alpha commands should be added here. As features graduate from alpha they should move // from here to the CommandGroups defined by NewKubeletCommand() in cmd.go. //cmd.AddCommand(NewCmdDebug(f, in, out, err)) + cmd.AddCommand(NewCmdDiff(f, out, err)) // NewKubeletCommand() will hide the alpha command if it has no subcommands. Overriding // the help function ensures a reasonable message if someone types the hidden command anyway. diff --git a/pkg/kubectl/cmd/diff.go b/pkg/kubectl/cmd/diff.go new file mode 100644 index 00000000000..9770164f71a --- /dev/null +++ b/pkg/kubectl/cmd/diff.go @@ -0,0 +1,460 @@ +/* +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 ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + + "github.com/ghodss/yaml" + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/kubectl/apply/parse" + "k8s.io/kubernetes/pkg/kubectl/apply/strategy" + "k8s.io/kubernetes/pkg/kubectl/cmd/templates" + cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" + "k8s.io/kubernetes/pkg/kubectl/resource" + "k8s.io/kubernetes/pkg/kubectl/util/i18n" + "k8s.io/utils/exec" +) + +var ( + diffLong = templates.LongDesc(i18n.T(` + Diff configurations specified by filename or stdin between their local, + last-applied, live and/or "merged" versions. + + LOCAL and LIVE versions are diffed by default. Other availble keywords + are MERGED and LAST. + + Output is always YAML. + + KUBERNETES_EXTERNAL_DIFF environment variable can be used to select your own + diff command. By default, the "diff" command available in your path will be + run with "-u" (unicode) and "-N" (treat new files as empty) options.`)) + diffExample = templates.Examples(i18n.T(` + # Diff resources included in pod.json. By default, it will diff LOCAL and LIVE versions + kubectl alpha diff -f pod.json + + # When one version is specified, diff that version against LIVE + cat service.yaml | kubectl alpha diff -f - MERGED + + # Or specify both versions + kubectl alpha diff -f pod.json -f service.yaml LAST LOCAL`)) +) + +type DiffOptions struct { + FilenameOptions resource.FilenameOptions +} + +func isValidArgument(arg string) error { + switch arg { + case "LOCAL", "LIVE", "LAST", "MERGED": + return nil + default: + return fmt.Errorf(`Invalid parameter %q, must be either "LOCAL", "LIVE", "LAST" or "MERGED"`, arg) + } + +} + +func parseDiffArguments(args []string) (string, string, error) { + if len(args) > 2 { + return "", "", fmt.Errorf("Invalid number of arguments: expected at most 2.") + } + // Default values + from := "LOCAL" + to := "LIVE" + if len(args) > 0 { + from = args[0] + } + if len(args) > 1 { + to = args[1] + } + + if err := isValidArgument(to); err != nil { + return "", "", err + } + if err := isValidArgument(from); err != nil { + return "", "", err + } + + return from, to, nil +} + +func NewCmdDiff(f cmdutil.Factory, stdout, stderr io.Writer) *cobra.Command { + var options DiffOptions + diff := DiffProgram{ + Exec: exec.New(), + Stdout: stdout, + Stderr: stderr, + } + cmd := &cobra.Command{ + Use: "diff -f FILENAME", + Short: i18n.T("Diff different versions of configurations"), + Long: diffLong, + Example: diffExample, + Run: func(cmd *cobra.Command, args []string) { + from, to, err := parseDiffArguments(args) + cmdutil.CheckErr(err) + cmdutil.CheckErr(RunDiff(f, &diff, &options, from, to)) + }, + } + + usage := "contains the configuration to diff" + cmdutil.AddFilenameOptionFlags(cmd, &options.FilenameOptions, usage) + cmd.MarkFlagRequired("filename") + + return cmd +} + +// DiffProgram finds and run the diff program. The value of +// KUBERNETES_EXTERNAL_DIFF environment variable will be used a diff +// program. By default, `diff(1)` will be used. +type DiffProgram struct { + Exec exec.Interface + Stdout io.Writer + Stderr io.Writer +} + +func (d *DiffProgram) getCommand(args ...string) exec.Cmd { + diff := "" + if envDiff := os.Getenv("KUBERNETES_EXTERNAL_DIFF"); envDiff != "" { + diff = envDiff + } else { + diff = "diff" + args = append([]string{"-u", "-N"}, args...) + } + + cmd := d.Exec.Command(diff, args...) + cmd.SetStdout(d.Stdout) + cmd.SetStderr(d.Stderr) + + return cmd +} + +// Run runs the detected diff program. `from` and `to` are the directory to diff. +func (d *DiffProgram) Run(from, to string) error { + d.getCommand(from, to).Run() // Ignore diff return code + return nil +} + +// Printer is used to print an object. +type Printer struct{} + +// Print the object inside the writer w. +func (p *Printer) Print(obj map[string]interface{}, w io.Writer) error { + if obj == nil { + return nil + } + data, err := yaml.Marshal(obj) + if err != nil { + return err + } + _, err = w.Write(data) + return err + +} + +// DiffVersion gets the proper version of objects, and aggregate them into a directory. +type DiffVersion struct { + Dir *Directory + Name string +} + +// NewDiffVersion creates a new DiffVersion with the named version. +func NewDiffVersion(name string) (*DiffVersion, error) { + dir, err := CreateDirectory(name) + if err != nil { + return nil, err + } + return &DiffVersion{ + Dir: dir, + Name: name, + }, nil +} + +func (v *DiffVersion) getObject(obj Object) (map[string]interface{}, error) { + switch v.Name { + case "LIVE": + return obj.Live() + case "MERGED": + return obj.Merged() + case "LOCAL": + return obj.Local() + case "LAST": + return obj.Last() + } + return nil, fmt.Errorf("Unknown version: %v", v.Name) +} + +// Print prints the object using the printer into a new file in the directory. +func (v *DiffVersion) Print(obj Object, printer Printer) error { + vobj, err := v.getObject(obj) + if err != nil { + return err + } + f, err := v.Dir.NewFile(obj.Name()) + if err != nil { + return err + } + defer f.Close() + return printer.Print(vobj, f) +} + +// Directory creates a new temp directory, and allows to easily create new files. +type Directory struct { + Name string +} + +// CreateDirectory does create the actual disk directory, and return a +// new representation of it. +func CreateDirectory(prefix string) (*Directory, error) { + name, err := ioutil.TempDir("", prefix+"-") + if err != nil { + return nil, err + } + + return &Directory{ + Name: name, + }, nil +} + +// NewFile creates a new file in the directory. +func (d *Directory) NewFile(name string) (*os.File, error) { + return os.OpenFile(filepath.Join(d.Name, name), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0700) +} + +// Delete removes the directory recursively. +func (d *Directory) Delete() error { + return os.RemoveAll(d.Name) +} + +// Object is an interface that let's you retrieve multiple version of +// it. +type Object interface { + Local() (map[string]interface{}, error) + Live() (map[string]interface{}, error) + Last() (map[string]interface{}, error) + Merged() (map[string]interface{}, error) + + Name() string +} + +// InfoObject is an implementation of the Object interface. It gets all +// the information from the Info object. +type InfoObject struct { + Info *resource.Info + Encoder runtime.Encoder + Parser *parse.Factory +} + +var _ Object = &InfoObject{} + +func (obj InfoObject) toMap(data []byte) (map[string]interface{}, error) { + m := map[string]interface{}{} + if len(data) == 0 { + return m, nil + } + err := json.Unmarshal(data, &m) + return m, err +} + +func (obj InfoObject) Local() (map[string]interface{}, error) { + data, err := runtime.Encode(obj.Encoder, obj.Info.VersionedObject) + if err != nil { + return nil, err + } + return obj.toMap(data) +} + +func (obj InfoObject) Live() (map[string]interface{}, error) { + if obj.Info.Object == nil { + return nil, nil // Object doesn't exist on cluster. + } + data, err := runtime.Encode(obj.Encoder, obj.Info.Object) + if err != nil { + return nil, err + } + return obj.toMap(data) +} + +func (obj InfoObject) Merged() (map[string]interface{}, error) { + local, err := obj.Local() + if err != nil { + return nil, err + } + + live, err := obj.Live() + if err != nil { + return nil, err + } + + last, err := obj.Last() + if err != nil { + return nil, err + } + + if live == nil || last == nil { + return local, nil // We probably don't have a live verison, merged is local. + } + + elmt, err := obj.Parser.CreateElement(last, local, live) + if err != nil { + return nil, err + } + result, err := elmt.Merge(strategy.Create(strategy.Options{})) + return result.MergedResult.(map[string]interface{}), err +} + +func (obj InfoObject) Last() (map[string]interface{}, error) { + if obj.Info.Object == nil { + return nil, nil // No object is live, return empty + } + accessor, err := meta.Accessor(obj.Info.Object) + if err != nil { + return nil, err + } + annots := accessor.GetAnnotations() + if annots == nil { + return nil, nil // Not an error, just empty. + } + + return obj.toMap([]byte(annots[api.LastAppliedConfigAnnotation])) +} + +func (obj InfoObject) Name() string { + return obj.Info.Name +} + +// Differ creates two DiffVersion and diffs them. +type Differ struct { + From *DiffVersion + To *DiffVersion +} + +func NewDiffer(from, to string) (*Differ, error) { + differ := Differ{} + var err error + differ.From, err = NewDiffVersion(from) + if err != nil { + return nil, err + } + differ.To, err = NewDiffVersion(to) + if err != nil { + differ.From.Dir.Delete() + return nil, err + } + + return &differ, nil +} + +// Diff diffs to versions of a specific object, and print both versions to directories. +func (d *Differ) Diff(obj Object, printer Printer) error { + if err := d.From.Print(obj, printer); err != nil { + return err + } + if err := d.To.Print(obj, printer); err != nil { + return err + } + return nil +} + +// Run runs the diff program against both directories. +func (d *Differ) Run(diff *DiffProgram) error { + return diff.Run(d.From.Dir.Name, d.To.Dir.Name) +} + +// TearDown removes both temporary directories recursively. +func (d *Differ) TearDown() { + d.From.Dir.Delete() // Ignore error + d.To.Dir.Delete() // Ignore error +} + +// RunDiff uses the factory to parse file arguments, find the version to +// diff, and find each Info object for each files, and runs against the +// differ. +func RunDiff(f cmdutil.Factory, diff *DiffProgram, options *DiffOptions, from, to string) error { + openapi, err := f.OpenAPISchema() + if err != nil { + return err + } + parser := &parse.Factory{Resources: openapi} + + differ, err := NewDiffer(from, to) + if err != nil { + return err + } + defer differ.TearDown() + + printer := Printer{} + + mapper, typer, err := f.UnstructuredObject() + if err != nil { + return err + } + + cmdNamespace, enforceNamespace, err := f.DefaultNamespace() + if err != nil { + return err + } + + r := f.NewBuilder(). + Unstructured(f.UnstructuredClientForMapping, mapper, typer). + NamespaceParam(cmdNamespace).DefaultNamespace(). + FilenameParam(enforceNamespace, &options.FilenameOptions). + 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 + } + + if err := info.Get(); err != nil { + if !errors.IsNotFound(err) { + return cmdutil.AddSourceToErr(fmt.Sprintf("retrieving current configuration of:\n%v\nfrom server for:", info), info.Source, err) + } + info.Object = nil + } + + obj := InfoObject{ + Info: info, + Parser: parser, + Encoder: f.JSONEncoder(), + } + differ.Diff(obj, printer) + + return nil + }) + if err != nil { + return err + } + + differ.Run(diff) + + return nil +} diff --git a/pkg/kubectl/cmd/diff_test.go b/pkg/kubectl/cmd/diff_test.go new file mode 100644 index 00000000000..4400023ff3e --- /dev/null +++ b/pkg/kubectl/cmd/diff_test.go @@ -0,0 +1,285 @@ +/* +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" + "io/ioutil" + "os" + "path" + "path/filepath" + "strings" + "testing" + + "k8s.io/utils/exec" +) + +type FakeObject struct { + name string + local map[string]interface{} + merged map[string]interface{} + live map[string]interface{} + last map[string]interface{} +} + +var _ Object = &FakeObject{} + +func (f *FakeObject) Name() string { + return f.name +} + +func (f *FakeObject) Local() (map[string]interface{}, error) { + return f.local, nil +} + +func (f *FakeObject) Merged() (map[string]interface{}, error) { + return f.merged, nil +} + +func (f *FakeObject) Live() (map[string]interface{}, error) { + return f.live, nil +} + +func (f *FakeObject) Last() (map[string]interface{}, error) { + return f.last, nil +} + +func TestArguments(t *testing.T) { + tests := []struct { + // Input + args []string + + // Outputs + from string + to string + err string + }{ + // Defaults + { + args: []string{}, + from: "LOCAL", + to: "LIVE", + err: "", + }, + // One valid argument + { + args: []string{"MERGED"}, + from: "MERGED", + to: "LIVE", + err: "", + }, + // One invalid argument + { + args: []string{"WRONG"}, + from: "", + to: "", + err: `Invalid parameter "WRONG", must be either "LOCAL", "LIVE", "LAST" or "MERGED"`, + }, + // Two valid arguments + { + args: []string{"MERGED", "LAST"}, + from: "MERGED", + to: "LAST", + err: "", + }, + // Two same arguments is fine + { + args: []string{"MERGED", "MERGED"}, + from: "MERGED", + to: "MERGED", + err: "", + }, + // Second argument is invalid + { + args: []string{"MERGED", "WRONG"}, + from: "", + to: "", + err: `Invalid parameter "WRONG", must be either "LOCAL", "LIVE", "LAST" or "MERGED"`, + }, + // Three arguments + { + args: []string{"MERGED", "LIVE", "LAST"}, + from: "", + to: "", + err: `Invalid number of arguments: expected at most 2.`, + }, + } + + for _, test := range tests { + from, to, e := parseDiffArguments(test.args) + err := "" + if e != nil { + err = e.Error() + } + if from != test.from || to != test.to || err != test.err { + t.Errorf("parseDiffArguments(%v) = (%v, %v, %v), expected (%v, %v, %v)", + test.args, + from, to, err, + test.from, test.to, test.err, + ) + } + } +} + +func TestDiffProgram(t *testing.T) { + os.Setenv("KUBERNETES_EXTERNAL_DIFF", "echo") + stdout := bytes.Buffer{} + diff := DiffProgram{ + Stdout: &stdout, + Stderr: &bytes.Buffer{}, + Exec: exec.New(), + } + err := diff.Run("one", "two") + if err != nil { + t.Fatal(err) + } + if output := stdout.String(); output != "one two\n" { + t.Fatalf(`stdout = %q, expected "one two\n"`, output) + } +} + +func TestPrinter(t *testing.T) { + printer := Printer{} + + obj := map[string]interface{}{ + "string": "string", + "list": []int{1, 2, 3}, + "int": 12, + } + buf := bytes.Buffer{} + printer.Print(obj, &buf) + want := `int: 12 +list: +- 1 +- 2 +- 3 +string: string +` + if buf.String() != want { + t.Errorf("Print() = %q, want %q", buf.String(), want) + } +} + +func TestDiffVersion(t *testing.T) { + diff, err := NewDiffVersion("LOCAL") + if err != nil { + t.Fatal(err) + } + defer diff.Dir.Delete() + + obj := FakeObject{ + name: "bla", + local: map[string]interface{}{"local": true}, + last: map[string]interface{}{"last": true}, + live: map[string]interface{}{"live": true}, + merged: map[string]interface{}{"merged": true}, + } + err = diff.Print(&obj, Printer{}) + if err != nil { + t.Fatal(err) + } + fcontent, err := ioutil.ReadFile(path.Join(diff.Dir.Name, obj.Name())) + if err != nil { + t.Fatal(err) + } + econtent := "local: true\n" + if string(fcontent) != econtent { + t.Fatalf("File has %q, expected %q", string(fcontent), econtent) + } +} + +func TestDirectory(t *testing.T) { + dir, err := CreateDirectory("prefix") + defer dir.Delete() + if err != nil { + t.Fatal(err) + } + _, err = os.Stat(dir.Name) + if err != nil { + t.Fatal(err) + } + if !strings.HasPrefix(filepath.Base(dir.Name), "prefix") { + t.Fatalf(`Directory doesn't start with "prefix": %q`, dir.Name) + } + entries, err := ioutil.ReadDir(dir.Name) + if err != nil { + t.Fatal(err) + } + if len(entries) != 0 { + t.Fatalf("Directory should be empty, has %d elements", len(entries)) + } + _, err = dir.NewFile("ONE") + if err != nil { + t.Fatal(err) + } + _, err = dir.NewFile("TWO") + if err != nil { + t.Fatal(err) + } + entries, err = ioutil.ReadDir(dir.Name) + if err != nil { + t.Fatal(err) + } + if len(entries) != 2 { + t.Fatalf("ReadDir should have two elements, has %d elements", len(entries)) + } + err = dir.Delete() + if err != nil { + t.Fatal(err) + } + _, err = os.Stat(dir.Name) + if err == nil { + t.Fatal("Directory should be gone, still present.") + } +} + +func TestDiffer(t *testing.T) { + diff, err := NewDiffer("LOCAL", "LIVE") + if err != nil { + t.Fatal(err) + } + defer diff.TearDown() + + obj := FakeObject{ + name: "bla", + local: map[string]interface{}{"local": true}, + last: map[string]interface{}{"last": true}, + live: map[string]interface{}{"live": true}, + merged: map[string]interface{}{"merged": true}, + } + err = diff.Diff(&obj, Printer{}) + if err != nil { + t.Fatal(err) + } + fcontent, err := ioutil.ReadFile(path.Join(diff.From.Dir.Name, obj.Name())) + if err != nil { + t.Fatal(err) + } + econtent := "local: true\n" + if string(fcontent) != econtent { + t.Fatalf("File has %q, expected %q", string(fcontent), econtent) + } + + fcontent, err = ioutil.ReadFile(path.Join(diff.To.Dir.Name, obj.Name())) + if err != nil { + t.Fatal(err) + } + econtent = "live: true\n" + if string(fcontent) != econtent { + t.Fatalf("File has %q, expected %q", string(fcontent), econtent) + } +}