Use dry-run patch to get the merged version of the object
This commit is contained in:
		| @@ -60,8 +60,6 @@ go_library( | |||||||
|         "//pkg/api/legacyscheme:go_default_library", |         "//pkg/api/legacyscheme:go_default_library", | ||||||
|         "//pkg/apis/core:go_default_library", |         "//pkg/apis/core:go_default_library", | ||||||
|         "//pkg/kubectl: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/auth:go_default_library", | ||||||
|         "//pkg/kubectl/cmd/config:go_default_library", |         "//pkg/kubectl/cmd/config:go_default_library", | ||||||
|         "//pkg/kubectl/cmd/create:go_default_library", |         "//pkg/kubectl/cmd/create:go_default_library", | ||||||
|   | |||||||
| @@ -17,7 +17,6 @@ limitations under the License. | |||||||
| package cmd | package cmd | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"encoding/json" |  | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"io" | 	"io" | ||||||
| 	"io/ioutil" | 	"io/ioutil" | ||||||
| @@ -25,19 +24,20 @@ import ( | |||||||
| 	"path/filepath" | 	"path/filepath" | ||||||
|  |  | ||||||
| 	"github.com/ghodss/yaml" | 	"github.com/ghodss/yaml" | ||||||
|  | 	"github.com/jonboulle/clockwork" | ||||||
| 	"github.com/spf13/cobra" | 	"github.com/spf13/cobra" | ||||||
| 	corev1 "k8s.io/api/core/v1" |  | ||||||
| 	"k8s.io/apimachinery/pkg/api/meta" | 	"k8s.io/apimachinery/pkg/api/errors" | ||||||
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||||
| 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" | 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" | ||||||
| 	"k8s.io/apimachinery/pkg/runtime" | 	"k8s.io/apimachinery/pkg/runtime" | ||||||
| 	"k8s.io/cli-runtime/pkg/genericclioptions" | 	"k8s.io/cli-runtime/pkg/genericclioptions" | ||||||
| 	"k8s.io/cli-runtime/pkg/genericclioptions/resource" | 	"k8s.io/cli-runtime/pkg/genericclioptions/resource" | ||||||
| 	"k8s.io/client-go/dynamic" | 	"k8s.io/kubernetes/pkg/kubectl" | ||||||
| 	"k8s.io/kubernetes/pkg/kubectl/apply/parse" |  | ||||||
| 	"k8s.io/kubernetes/pkg/kubectl/apply/strategy" |  | ||||||
| 	"k8s.io/kubernetes/pkg/kubectl/cmd/templates" | 	"k8s.io/kubernetes/pkg/kubectl/cmd/templates" | ||||||
| 	cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" | 	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/i18n" | ||||||
| 	"k8s.io/utils/exec" | 	"k8s.io/utils/exec" | ||||||
| ) | ) | ||||||
| @@ -130,7 +130,7 @@ func (d *DiffProgram) Run(from, to string) error { | |||||||
| type Printer struct{} | type Printer struct{} | ||||||
|  |  | ||||||
| // Print the object inside the writer w. | // Print the object inside the writer w. | ||||||
| func (p *Printer) Print(obj map[string]interface{}, w io.Writer) error { | func (p *Printer) Print(obj runtime.Object, w io.Writer) error { | ||||||
| 	if obj == nil { | 	if obj == nil { | ||||||
| 		return nil | 		return nil | ||||||
| 	} | 	} | ||||||
| @@ -161,10 +161,10 @@ func NewDiffVersion(name string) (*DiffVersion, error) { | |||||||
| 	}, nil | 	}, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (v *DiffVersion) getObject(obj Object) (map[string]interface{}, error) { | func (v *DiffVersion) getObject(obj Object) (runtime.Object, error) { | ||||||
| 	switch v.Name { | 	switch v.Name { | ||||||
| 	case "LIVE": | 	case "LIVE": | ||||||
| 		return obj.Live() | 		return obj.Live(), nil | ||||||
| 	case "MERGED": | 	case "MERGED": | ||||||
| 		return obj.Merged() | 		return obj.Merged() | ||||||
| 	} | 	} | ||||||
| @@ -216,8 +216,8 @@ func (d *Directory) Delete() error { | |||||||
| // Object is an interface that let's you retrieve multiple version of | // Object is an interface that let's you retrieve multiple version of | ||||||
| // it. | // it. | ||||||
| type Object interface { | type Object interface { | ||||||
| 	Live() (map[string]interface{}, error) | 	Live() runtime.Object | ||||||
| 	Merged() (map[string]interface{}, error) | 	Merged() (runtime.Object, error) | ||||||
|  |  | ||||||
| 	Name() string | 	Name() string | ||||||
| } | } | ||||||
| @@ -225,73 +225,51 @@ type Object interface { | |||||||
| // InfoObject is an implementation of the Object interface. It gets all | // InfoObject is an implementation of the Object interface. It gets all | ||||||
| // the information from the Info object. | // the information from the Info object. | ||||||
| type InfoObject struct { | type InfoObject struct { | ||||||
| 	Remote  *unstructured.Unstructured | 	LocalObj runtime.Object | ||||||
| 	Info     *resource.Info | 	Info     *resource.Info | ||||||
| 	Encoder  runtime.Encoder | 	Encoder  runtime.Encoder | ||||||
| 	Parser  *parse.Factory | 	OpenAPI  openapi.Resources | ||||||
| } | } | ||||||
|  |  | ||||||
| var _ Object = &InfoObject{} | var _ Object = &InfoObject{} | ||||||
|  |  | ||||||
| func (obj InfoObject) toMap(data []byte) (map[string]interface{}, error) { | // Returns the live version of the object | ||||||
| 	m := map[string]interface{}{} | func (obj InfoObject) Live() runtime.Object { | ||||||
| 	if len(data) == 0 { | 	return obj.Info.Object | ||||||
| 		return m, nil |  | ||||||
| 	} |  | ||||||
| 	err := json.Unmarshal(data, &m) |  | ||||||
| 	return m, err |  | ||||||
| } | } | ||||||
|  |  | ||||||
| func (obj InfoObject) Live() (map[string]interface{}, error) { | // Returns the "merged" object, as it would look like if applied or | ||||||
| 	if obj.Remote == nil { | // created. | ||||||
| 		return nil, nil // Object doesn't exist on cluster. | func (obj InfoObject) Merged() (runtime.Object, error) { | ||||||
|  | 	// 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}}, | ||||||
|  | 		) | ||||||
| 	} | 	} | ||||||
| 	return obj.Remote.UnstructuredContent(), nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (obj InfoObject) Merged() (map[string]interface{}, error) { | 	modified, err := kubectl.GetModifiedConfiguration(obj.LocalObj, false, unstructured.UnstructuredJSONScheme) | ||||||
| 	data, err := runtime.Encode(obj.Encoder, obj.Info.Object) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 	local, err := obj.toMap(data) |  | ||||||
|  |  | ||||||
| 	live, err := obj.Live() |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	last, err := obj.Last() | 	// This is using the patcher from apply, to keep the same behavior. | ||||||
| 	if err != nil { | 	// We plan on replacing this with server-side apply when it becomes available. | ||||||
| 		return nil, err | 	patcher := &patcher{ | ||||||
|  | 		mapping:       obj.Info.Mapping, | ||||||
|  | 		helper:        resource.NewHelper(obj.Info.Client, obj.Info.Mapping), | ||||||
|  | 		overwrite:     true, | ||||||
|  | 		backOff:       clockwork.NewRealClock(), | ||||||
|  | 		serverDryRun:  true, | ||||||
|  | 		openapiSchema: obj.OpenAPI, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if live == nil || last == nil { | 	_, result, err := patcher.patch(obj.Info.Object, modified, obj.Info.Source, obj.Info.Namespace, obj.Info.Name, nil) | ||||||
| 		return local, nil // We probably don't have a live version, merged is local. | 	return result, err | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	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.Remote == nil { |  | ||||||
| 		return nil, nil // No object is live, return empty |  | ||||||
| 	} |  | ||||||
| 	accessor, err := meta.Accessor(obj.Remote) |  | ||||||
| 	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[corev1.LastAppliedConfigAnnotation])) |  | ||||||
| } | } | ||||||
|  |  | ||||||
| func (obj InfoObject) Name() string { | func (obj InfoObject) Name() string { | ||||||
| @@ -342,59 +320,14 @@ func (d *Differ) TearDown() { | |||||||
| 	d.To.Dir.Delete()   // Ignore error | 	d.To.Dir.Delete()   // Ignore error | ||||||
| } | } | ||||||
|  |  | ||||||
| type Downloader struct { |  | ||||||
| 	mapper  meta.RESTMapper |  | ||||||
| 	dclient dynamic.Interface |  | ||||||
| 	ns      string |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func NewDownloader(f cmdutil.Factory) (*Downloader, error) { |  | ||||||
| 	var err error |  | ||||||
| 	var d Downloader |  | ||||||
|  |  | ||||||
| 	d.mapper, err = f.ToRESTMapper() |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 	d.dclient, err = f.DynamicClient() |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 	d.ns, _, _ = f.ToRawKubeConfigLoader().Namespace() |  | ||||||
|  |  | ||||||
| 	return &d, nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (d *Downloader) Download(info *resource.Info) (*unstructured.Unstructured, error) { |  | ||||||
| 	gvk := info.Object.GetObjectKind().GroupVersionKind() |  | ||||||
| 	mapping, err := d.mapper.RESTMapping(gvk.GroupKind(), gvk.Version) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	var resource dynamic.ResourceInterface |  | ||||||
| 	switch mapping.Scope.Name() { |  | ||||||
| 	case meta.RESTScopeNameNamespace: |  | ||||||
| 		if info.Namespace == "" { |  | ||||||
| 			info.Namespace = d.ns |  | ||||||
| 		} |  | ||||||
| 		resource = d.dclient.Resource(mapping.Resource).Namespace(info.Namespace) |  | ||||||
| 	case meta.RESTScopeNameRoot: |  | ||||||
| 		resource = d.dclient.Resource(mapping.Resource) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return resource.Get(info.Name, metav1.GetOptions{}) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // RunDiff uses the factory to parse file arguments, find the version to | // 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 | // diff, and find each Info object for each files, and runs against the | ||||||
| // differ. | // differ. | ||||||
| func RunDiff(f cmdutil.Factory, diff *DiffProgram, options *DiffOptions) error { | func RunDiff(f cmdutil.Factory, diff *DiffProgram, options *DiffOptions) error { | ||||||
| 	openapi, err := f.OpenAPISchema() | 	schema, err := f.OpenAPISchema() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 	parser := &parse.Factory{Resources: openapi} |  | ||||||
|  |  | ||||||
| 	differ, err := NewDiffer("LIVE", "MERGED") | 	differ, err := NewDiffer("LIVE", "MERGED") | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @@ -413,29 +346,30 @@ func RunDiff(f cmdutil.Factory, diff *DiffProgram, options *DiffOptions) error { | |||||||
| 		Unstructured(). | 		Unstructured(). | ||||||
| 		NamespaceParam(cmdNamespace).DefaultNamespace(). | 		NamespaceParam(cmdNamespace).DefaultNamespace(). | ||||||
| 		FilenameParam(enforceNamespace, &options.FilenameOptions). | 		FilenameParam(enforceNamespace, &options.FilenameOptions). | ||||||
| 		Local(). |  | ||||||
| 		Flatten(). | 		Flatten(). | ||||||
| 		Do() | 		Do() | ||||||
| 	if err := r.Err(); err != nil { | 	if err := r.Err(); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	dl, err := NewDownloader(f) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	err = r.Visit(func(info *resource.Info, err error) error { | 	err = r.Visit(func(info *resource.Info, err error) error { | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		remote, _ := dl.Download(info) | 		local := info.Object.DeepCopyObject() | ||||||
|  | 		if err := info.Get(); err != nil { | ||||||
|  | 			if !errors.IsNotFound(err) { | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
|  | 			info.Object = nil | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		obj := InfoObject{ | 		obj := InfoObject{ | ||||||
| 			Remote:  remote, | 			LocalObj: local, | ||||||
| 			Info:     info, | 			Info:     info, | ||||||
| 			Parser:  parser, | 			Encoder:  scheme.DefaultJSONEncoder(), | ||||||
| 			Encoder: cmdutil.InternalVersionJSONEncoder(), | 			OpenAPI:  schema, | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		return differ.Diff(obj, printer) | 		return differ.Diff(obj, printer) | ||||||
| @@ -444,7 +378,8 @@ func RunDiff(f cmdutil.Factory, diff *DiffProgram, options *DiffOptions) error { | |||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	differ.Run(diff) | 	// Error ignore on purpose. diff(1) for example, returns an error if there is any diff. | ||||||
|  | 	_ = differ.Run(diff) | ||||||
|  |  | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|   | |||||||
| @@ -25,6 +25,8 @@ import ( | |||||||
| 	"strings" | 	"strings" | ||||||
| 	"testing" | 	"testing" | ||||||
|  |  | ||||||
|  | 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" | ||||||
|  | 	"k8s.io/apimachinery/pkg/runtime" | ||||||
| 	"k8s.io/cli-runtime/pkg/genericclioptions" | 	"k8s.io/cli-runtime/pkg/genericclioptions" | ||||||
| 	"k8s.io/utils/exec" | 	"k8s.io/utils/exec" | ||||||
| ) | ) | ||||||
| @@ -41,12 +43,12 @@ func (f *FakeObject) Name() string { | |||||||
| 	return f.name | 	return f.name | ||||||
| } | } | ||||||
|  |  | ||||||
| func (f *FakeObject) Merged() (map[string]interface{}, error) { | func (f *FakeObject) Merged() (runtime.Object, error) { | ||||||
| 	return f.merged, nil | 	return &unstructured.Unstructured{Object: f.merged}, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (f *FakeObject) Live() (map[string]interface{}, error) { | func (f *FakeObject) Live() runtime.Object { | ||||||
| 	return f.live, nil | 	return &unstructured.Unstructured{Object: f.live} | ||||||
| } | } | ||||||
|  |  | ||||||
| func TestDiffProgram(t *testing.T) { | func TestDiffProgram(t *testing.T) { | ||||||
| @@ -68,11 +70,11 @@ func TestDiffProgram(t *testing.T) { | |||||||
| func TestPrinter(t *testing.T) { | func TestPrinter(t *testing.T) { | ||||||
| 	printer := Printer{} | 	printer := Printer{} | ||||||
|  |  | ||||||
| 	obj := map[string]interface{}{ | 	obj := &unstructured.Unstructured{Object: map[string]interface{}{ | ||||||
| 		"string": "string", | 		"string": "string", | ||||||
| 		"list":   []int{1, 2, 3}, | 		"list":   []int{1, 2, 3}, | ||||||
| 		"int":    12, | 		"int":    12, | ||||||
| 	} | 	}} | ||||||
| 	buf := bytes.Buffer{} | 	buf := bytes.Buffer{} | ||||||
| 	printer.Print(obj, &buf) | 	printer.Print(obj, &buf) | ||||||
| 	want := `int: 12 | 	want := `int: 12 | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Antoine Pelisse
					Antoine Pelisse