k3s/vendor/k8s.io/kubectl/pkg/cmd/diff/diff.go
Brad Davidson e204d863a5 Update Kubernetes to v1.22.1
* Update Kubernetes to v1.22.1
* Update dependent modules to track with upstream

Signed-off-by: Brad Davidson <brad.davidson@rancher.com>
2021-08-20 18:47:16 -07:00

718 lines
18 KiB
Go

/*
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"
"regexp"
"strings"
"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/v2"
"k8s.io/kubectl/pkg/cmd/apply"
cmdutil "k8s.io/kubectl/pkg/cmd/util"
"k8s.io/kubectl/pkg/scheme"
"k8s.io/kubectl/pkg/util"
"k8s.io/kubectl/pkg/util/i18n"
"k8s.io/kubectl/pkg/util/openapi"
"k8s.io/kubectl/pkg/util/templates"
"k8s.io/utils/exec"
"sigs.k8s.io/yaml"
)
var (
diffLong = templates.LongDesc(i18n.T(`
Diff configurations specified by file name or stdin between the current online
configuration, and the configuration as it would be if applied.
The output is always YAML.
KUBECTL_EXTERNAL_DIFF environment variable can be used to select your own
diff command. Users can use external commands with params too, example:
KUBECTL_EXTERNAL_DIFF="colordiff -N -u"
By default, the "diff" command available in your path will be
run with the "-u" (unified diff) and "-N" (treat absent files as empty) options.
Exit status:
0
No differences were found.
1
Differences were found.
>1
Kubectl or diff failed with an error.
Note: KUBECTL_EXTERNAL_DIFF, if used, is expected to follow that convention.`))
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
// Constants for masking sensitive values
const (
sensitiveMaskDefault = "***"
sensitiveMaskBefore = "*** (before)"
sensitiveMaskAfter = "*** (after)"
)
// diffError returns the ExitError if the status code is less than 1,
// nil otherwise.
func diffError(err error) exec.ExitError {
if err, ok := err.(exec.ExitError); ok && err.ExitStatus() <= 1 {
return err
}
return nil
}
type DiffOptions struct {
FilenameOptions resource.FilenameOptions
ServerSideApply bool
FieldManager string
ForceConflicts bool
Selector string
OpenAPISchema openapi.Resources
DiscoveryClient discovery.DiscoveryInterface
DynamicClient dynamic.Interface
DryRunVerifier *resource.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 the live version against a would-be applied version"),
Long: diffLong,
Example: diffExample,
Run: func(cmd *cobra.Command, args []string) {
cmdutil.CheckDiffErr(options.Complete(f, cmd))
cmdutil.CheckDiffErr(validateArgs(cmd, args))
// `kubectl diff` propagates the error code from
// diff or `KUBECTL_EXTERNAL_DIFF`. Also, we
// don't want to print an error if diff returns
// error code 1, which simply means that changes
// were found. We also don't want kubectl to
// return 1 if there was a problem.
if err := options.Run(); err != nil {
if exitErr := diffError(err); exitErr != nil {
os.Exit(exitErr.ExitStatus())
}
cmdutil.CheckDiffErr(err)
}
},
}
usage := "contains the configuration to diff"
cmd.Flags().StringVarP(&options.Selector, "selector", "l", options.Selector, "Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2)")
cmdutil.AddFilenameOptionFlags(cmd, &options.FilenameOptions, usage)
cmdutil.AddServerSideApplyFlags(cmd)
cmdutil.AddFieldManagerFlagVar(cmd, &options.FieldManager, apply.FieldManagerClientSideApply)
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) (string, exec.Cmd) {
diff := ""
if envDiff := os.Getenv("KUBECTL_EXTERNAL_DIFF"); envDiff != "" {
diffCommand := strings.Split(envDiff, " ")
diff = diffCommand[0]
if len(diffCommand) > 1 {
// Regex accepts: Alphanumeric (case-insensitive), dash and equal
isValidChar := regexp.MustCompile(`^[a-zA-Z0-9-=]+$`).MatchString
for i := 1; i < len(diffCommand); i++ {
if isValidChar(diffCommand[i]) {
args = append(args, diffCommand[i])
}
}
}
} else {
diff = "diff"
args = append([]string{"-u", "-N"}, args...)
}
cmd := d.Exec.Command(diff, args...)
cmd.SetStdout(d.Out)
cmd.SetStderr(d.ErrOut)
return diff, cmd
}
// Run runs the detected diff program. `from` and `to` are the directory to diff.
func (d *DiffProgram) Run(from, to string) error {
diff, cmd := d.getCommand(from, to)
if err := cmd.Run(); err != nil {
// Let's not wrap diff errors, or we won't be able to
// differentiate them later.
if diffErr := diffError(err); diffErr != nil {
return diffErr
}
return fmt.Errorf("failed to run %q: %v", diff, err)
}
return nil
}
// 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(name string, obj runtime.Object, printer Printer) error {
f, err := v.Dir.NewFile(name)
if err != nil {
return err
}
defer f.Close()
return printer.Print(obj, 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
FieldManager string
ForceConflicts bool
genericclioptions.IOStreams
}
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) {
helper := resource.NewHelper(obj.Info.Client, obj.Info.Mapping).
DryRun(true).
WithFieldManager(obj.FieldManager)
if obj.ServerSideApply {
data, err := runtime.Encode(unstructured.UnstructuredJSONScheme, obj.LocalObj)
if err != nil {
return nil, err
}
options := metav1.PatchOptions{
Force: &obj.ForceConflicts,
FieldManager: obj.FieldManager,
}
return helper.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 helper.CreateWithOptions(
obj.Info.Namespace,
true,
obj.LocalObj,
&metav1.CreateOptions{},
)
}
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 := util.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: helper,
Overwrite: true,
BackOff: clockwork.NewRealClock(),
OpenapiSchema: obj.OpenAPI,
ResourceVersion: resourceVersion,
}
_, result, err := patcher.Patch(obj.Info.Object, modified, obj.Info.Source, obj.Info.Namespace, obj.Info.Name, obj.ErrOut)
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,
)
}
// toUnstructured converts a runtime.Object into an unstructured.Unstructured object.
func toUnstructured(obj runtime.Object) (*unstructured.Unstructured, error) {
if obj == nil {
return nil, nil
}
c, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj.DeepCopyObject())
if err != nil {
return nil, fmt.Errorf("convert to unstructured: %w", err)
}
u := &unstructured.Unstructured{}
u.SetUnstructuredContent(c)
return u, nil
}
// Masker masks sensitive values in an object while preserving diff-able
// changes.
//
// All sensitive values in the object will be masked with a fixed-length
// asterisk mask. If two values are different, an additional suffix will
// be added so they can be diff-ed.
type Masker struct {
from *unstructured.Unstructured
to *unstructured.Unstructured
}
func NewMasker(from, to runtime.Object) (*Masker, error) {
// Convert objects to unstructured
f, err := toUnstructured(from)
if err != nil {
return nil, fmt.Errorf("convert to unstructured: %w", err)
}
t, err := toUnstructured(to)
if err != nil {
return nil, fmt.Errorf("convert to unstructured: %w", err)
}
// Run masker
m := &Masker{
from: f,
to: t,
}
if err := m.run(); err != nil {
return nil, fmt.Errorf("run masker: %w", err)
}
return m, nil
}
// dataFromUnstructured returns the underlying nested map in the data key.
func (m Masker) dataFromUnstructured(u *unstructured.Unstructured) (map[string]interface{}, error) {
if u == nil {
return nil, nil
}
data, found, err := unstructured.NestedMap(u.UnstructuredContent(), "data")
if err != nil {
return nil, fmt.Errorf("get nested map: %w", err)
}
if !found {
return nil, nil
}
return data, nil
}
// run compares and patches sensitive values.
func (m *Masker) run() error {
// Extract nested map object
from, err := m.dataFromUnstructured(m.from)
if err != nil {
return fmt.Errorf("extract 'data' field: %w", err)
}
to, err := m.dataFromUnstructured(m.to)
if err != nil {
return fmt.Errorf("extract 'data' field: %w", err)
}
for k := range from {
// Add before/after suffix when key exists on both
// objects and are not equal, so that it will be
// visible in diffs.
if _, ok := to[k]; ok {
if from[k] != to[k] {
from[k] = sensitiveMaskBefore
to[k] = sensitiveMaskAfter
continue
}
to[k] = sensitiveMaskDefault
}
from[k] = sensitiveMaskDefault
}
for k := range to {
// Mask remaining keys that were not in 'from'
if _, ok := from[k]; !ok {
to[k] = sensitiveMaskDefault
}
}
// Patch objects with masked data
if m.from != nil && from != nil {
if err := unstructured.SetNestedMap(m.from.UnstructuredContent(), from, "data"); err != nil {
return fmt.Errorf("patch masked data: %w", err)
}
}
if m.to != nil && to != nil {
if err := unstructured.SetNestedMap(m.to.UnstructuredContent(), to, "data"); err != nil {
return fmt.Errorf("patch masked data: %w", err)
}
}
return nil
}
// From returns the masked version of the 'from' object.
func (m *Masker) From() runtime.Object {
return m.from
}
// To returns the masked version of the 'to' object.
func (m *Masker) To() runtime.Object {
return m.to
}
// 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 {
from, err := d.From.getObject(obj)
if err != nil {
return err
}
to, err := d.To.getObject(obj)
if err != nil {
return err
}
// Mask secret values if object is V1Secret
if gvk := to.GetObjectKind().GroupVersionKind(); gvk.Version == "v1" && gvk.Kind == "Secret" {
m, err := NewMasker(from, to)
if err != nil {
return err
}
from, to = m.From(), m.To()
}
if err := d.From.Print(obj.Name(), from, printer); err != nil {
return err
}
if err := d.To.Print(obj.Name(), to, 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.FieldManager = apply.GetApplyFieldManagerFlag(cmd, o.ServerSideApply)
o.ForceConflicts = cmdutil.GetForceConflictsFlag(cmd)
if o.ForceConflicts && !o.ServerSideApply {
return fmt.Errorf("--force-conflicts only works with --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 = resource.NewDryRunVerifier(o.DynamicClient, f.OpenAPIGetter())
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).
LabelSelectorParam(o.Selector).
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,
FieldManager: o.FieldManager,
ForceConflicts: o.ForceConflicts,
IOStreams: o.Diff.IOStreams,
}
err = differ.Diff(obj, printer)
if !isConflict(err) {
break
}
}
apply.WarnIfDeleting(info.Object, o.Diff.ErrOut)
return err
})
if err != nil {
return err
}
return differ.Run(o.Diff)
}