123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510 |
- /*
- 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 diff
- import (
- "fmt"
- "io"
- "io/ioutil"
- "os"
- "path/filepath"
- "github.com/jonboulle/clockwork"
- "github.com/spf13/cobra"
- "k8s.io/apimachinery/pkg/api/errors"
- "k8s.io/apimachinery/pkg/api/meta"
- metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
- "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
- "k8s.io/apimachinery/pkg/runtime"
- "k8s.io/apimachinery/pkg/types"
- "k8s.io/cli-runtime/pkg/genericclioptions"
- "k8s.io/cli-runtime/pkg/resource"
- "k8s.io/client-go/discovery"
- "k8s.io/client-go/dynamic"
- "k8s.io/klog"
- "k8s.io/kubernetes/pkg/kubectl"
- "k8s.io/kubernetes/pkg/kubectl/cmd/apply"
- cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
- "k8s.io/kubernetes/pkg/kubectl/cmd/util/openapi"
- "k8s.io/kubernetes/pkg/kubectl/scheme"
- "k8s.io/kubernetes/pkg/kubectl/util/i18n"
- "k8s.io/kubernetes/pkg/kubectl/util/templates"
- "k8s.io/utils/exec"
- "sigs.k8s.io/yaml"
- )
- var (
- diffLong = templates.LongDesc(i18n.T(`
- Diff configurations specified by filename or stdin between the current online
- configuration, and the configuration as it would be if applied.
- Output is always YAML.
- KUBECTL_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.
- kubectl diff -f pod.json
- # Diff file read from stdin
- cat service.yaml | kubectl diff -f -`))
- )
- // Number of times we try to diff before giving-up
- const maxRetries = 4
- type DiffOptions struct {
- FilenameOptions resource.FilenameOptions
- ServerSideApply bool
- ForceConflicts bool
- OpenAPISchema openapi.Resources
- DiscoveryClient discovery.DiscoveryInterface
- DynamicClient dynamic.Interface
- DryRunVerifier *apply.DryRunVerifier
- CmdNamespace string
- EnforceNamespace bool
- Builder *resource.Builder
- Diff *DiffProgram
- }
- func validateArgs(cmd *cobra.Command, args []string) error {
- if len(args) != 0 {
- return cmdutil.UsageErrorf(cmd, "Unexpected args: %v", args)
- }
- return nil
- }
- func NewDiffOptions(ioStreams genericclioptions.IOStreams) *DiffOptions {
- return &DiffOptions{
- Diff: &DiffProgram{
- Exec: exec.New(),
- IOStreams: ioStreams,
- },
- }
- }
- func NewCmdDiff(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command {
- options := NewDiffOptions(streams)
- cmd := &cobra.Command{
- Use: "diff -f FILENAME",
- DisableFlagsInUseLine: true,
- Short: i18n.T("Diff live version against would-be applied version"),
- Long: diffLong,
- Example: diffExample,
- Run: func(cmd *cobra.Command, args []string) {
- cmdutil.CheckErr(options.Complete(f, cmd))
- cmdutil.CheckErr(validateArgs(cmd, args))
- cmdutil.CheckErr(options.Run())
- },
- }
- usage := "contains the configuration to diff"
- cmdutil.AddFilenameOptionFlags(cmd, &options.FilenameOptions, usage)
- cmdutil.AddServerSideApplyFlags(cmd)
- return cmd
- }
- // DiffProgram finds and run the diff program. The value of
- // KUBECTL_EXTERNAL_DIFF environment variable will be used a diff
- // program. By default, `diff(1)` will be used.
- type DiffProgram struct {
- Exec exec.Interface
- genericclioptions.IOStreams
- }
- func (d *DiffProgram) getCommand(args ...string) exec.Cmd {
- diff := ""
- if envDiff := os.Getenv("KUBECTL_EXTERNAL_DIFF"); envDiff != "" {
- diff = envDiff
- } else {
- diff = "diff"
- args = append([]string{"-u", "-N"}, args...)
- }
- cmd := d.Exec.Command(diff, args...)
- cmd.SetStdout(d.Out)
- cmd.SetStderr(d.ErrOut)
- return cmd
- }
- // Run runs the detected diff program. `from` and `to` are the directory to diff.
- func (d *DiffProgram) Run(from, to string) error {
- return d.getCommand(from, to).Run()
- }
- // Printer is used to print an object.
- type Printer struct{}
- // Print the object inside the writer w.
- func (p *Printer) Print(obj runtime.Object, 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) (runtime.Object, error) {
- switch v.Name {
- case "LIVE":
- return obj.Live(), nil
- case "MERGED":
- return obj.Merged()
- }
- 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 {
- Live() runtime.Object
- Merged() (runtime.Object, error)
- Name() string
- }
- // InfoObject is an implementation of the Object interface. It gets all
- // the information from the Info object.
- type InfoObject struct {
- LocalObj runtime.Object
- Info *resource.Info
- Encoder runtime.Encoder
- OpenAPI openapi.Resources
- Force bool
- ServerSideApply bool
- ForceConflicts bool
- }
- var _ Object = &InfoObject{}
- // Returns the live version of the object
- func (obj InfoObject) Live() runtime.Object {
- return obj.Info.Object
- }
- // Returns the "merged" object, as it would look like if applied or
- // created.
- func (obj InfoObject) Merged() (runtime.Object, error) {
- if obj.ServerSideApply {
- data, err := runtime.Encode(unstructured.UnstructuredJSONScheme, obj.LocalObj)
- if err != nil {
- return nil, err
- }
- options := metav1.PatchOptions{
- Force: &obj.ForceConflicts,
- DryRun: []string{metav1.DryRunAll},
- }
- return resource.NewHelper(obj.Info.Client, obj.Info.Mapping).Patch(
- obj.Info.Namespace,
- obj.Info.Name,
- types.ApplyPatchType,
- data,
- &options,
- )
- }
- // Build the patcher, and then apply the patch with dry-run, unless the object doesn't exist, in which case we need to create it.
- if obj.Live() == nil {
- // Dry-run create if the object doesn't exist.
- return resource.NewHelper(obj.Info.Client, obj.Info.Mapping).Create(
- obj.Info.Namespace,
- true,
- obj.LocalObj,
- &metav1.CreateOptions{DryRun: []string{metav1.DryRunAll}},
- )
- }
- var resourceVersion *string
- if !obj.Force {
- accessor, err := meta.Accessor(obj.Info.Object)
- if err != nil {
- return nil, err
- }
- str := accessor.GetResourceVersion()
- resourceVersion = &str
- }
- modified, err := kubectl.GetModifiedConfiguration(obj.LocalObj, false, unstructured.UnstructuredJSONScheme)
- if err != nil {
- return nil, err
- }
- // This is using the patcher from apply, to keep the same behavior.
- // We plan on replacing this with server-side apply when it becomes available.
- patcher := &apply.Patcher{
- Mapping: obj.Info.Mapping,
- Helper: resource.NewHelper(obj.Info.Client, obj.Info.Mapping),
- Overwrite: true,
- BackOff: clockwork.NewRealClock(),
- ServerDryRun: true,
- OpenapiSchema: obj.OpenAPI,
- ResourceVersion: resourceVersion,
- }
- _, result, err := patcher.Patch(obj.Info.Object, modified, obj.Info.Source, obj.Info.Namespace, obj.Info.Name, nil)
- return result, err
- }
- func (obj InfoObject) Name() string {
- group := ""
- if obj.Info.Mapping.GroupVersionKind.Group != "" {
- group = fmt.Sprintf("%v.", obj.Info.Mapping.GroupVersionKind.Group)
- }
- return group + fmt.Sprintf(
- "%v.%v.%v.%v",
- obj.Info.Mapping.GroupVersionKind.Version,
- obj.Info.Mapping.GroupVersionKind.Kind,
- obj.Info.Namespace,
- 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
- }
- func isConflict(err error) bool {
- return err != nil && errors.IsConflict(err)
- }
- func (o *DiffOptions) Complete(f cmdutil.Factory, cmd *cobra.Command) error {
- var err error
- err = o.FilenameOptions.RequireFilenameOrKustomize()
- if err != nil {
- return err
- }
- o.ServerSideApply = cmdutil.GetServerSideApplyFlag(cmd)
- o.ForceConflicts = cmdutil.GetForceConflictsFlag(cmd)
- if o.ForceConflicts && !o.ServerSideApply {
- return fmt.Errorf("--experimental-force-conflicts only works with --experimental-server-side")
- }
- if !o.ServerSideApply {
- o.OpenAPISchema, err = f.OpenAPISchema()
- if err != nil {
- return err
- }
- }
- o.DiscoveryClient, err = f.ToDiscoveryClient()
- if err != nil {
- return err
- }
- o.DynamicClient, err = f.DynamicClient()
- if err != nil {
- return err
- }
- o.DryRunVerifier = &apply.DryRunVerifier{
- Finder: cmdutil.NewCRDFinder(cmdutil.CRDFromDynamic(o.DynamicClient)),
- OpenAPIGetter: o.DiscoveryClient,
- }
- o.CmdNamespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace()
- if err != nil {
- return err
- }
- o.Builder = f.NewBuilder()
- return nil
- }
- // 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 (o *DiffOptions) Run() error {
- differ, err := NewDiffer("LIVE", "MERGED")
- if err != nil {
- return err
- }
- defer differ.TearDown()
- printer := Printer{}
- r := o.Builder.
- Unstructured().
- NamespaceParam(o.CmdNamespace).DefaultNamespace().
- FilenameParam(o.EnforceNamespace, &o.FilenameOptions).
- Flatten().
- Do()
- if err := r.Err(); err != nil {
- return err
- }
- err = r.Visit(func(info *resource.Info, err error) error {
- if err != nil {
- return err
- }
- if err := o.DryRunVerifier.HasSupport(info.Mapping.GroupVersionKind); err != nil {
- return err
- }
- local := info.Object.DeepCopyObject()
- for i := 1; i <= maxRetries; i++ {
- if err = info.Get(); err != nil {
- if !errors.IsNotFound(err) {
- return err
- }
- info.Object = nil
- }
- force := i == maxRetries
- if force {
- klog.Warningf(
- "Object (%v: %v) keeps changing, diffing without lock",
- info.Object.GetObjectKind().GroupVersionKind(),
- info.Name,
- )
- }
- obj := InfoObject{
- LocalObj: local,
- Info: info,
- Encoder: scheme.DefaultJSONEncoder(),
- OpenAPI: o.OpenAPISchema,
- Force: force,
- ServerSideApply: o.ServerSideApply,
- ForceConflicts: o.ForceConflicts,
- }
- err = differ.Diff(obj, printer)
- if !isConflict(err) {
- break
- }
- }
- return err
- })
- if err != nil {
- return err
- }
- return differ.Run(o.Diff)
- }
|