From 2d3f7795c86c96d52451c3090ab203d1e0ec2284 Mon Sep 17 00:00:00 2001 From: Antoine Pelisse Date: Tue, 11 Sep 2018 10:27:41 -0700 Subject: [PATCH] Use dry-run patch to get the merged version of the object --- pkg/kubectl/cmd/BUILD | 2 - pkg/kubectl/cmd/diff.go | 179 +++++++++++------------------------ pkg/kubectl/cmd/diff_test.go | 14 +-- 3 files changed, 65 insertions(+), 130 deletions(-) diff --git a/pkg/kubectl/cmd/BUILD b/pkg/kubectl/cmd/BUILD index cb15d1f44a..b8ffc63d33 100644 --- a/pkg/kubectl/cmd/BUILD +++ b/pkg/kubectl/cmd/BUILD @@ -60,8 +60,6 @@ go_library( "//pkg/api/legacyscheme:go_default_library", "//pkg/apis/core: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/create:go_default_library", diff --git a/pkg/kubectl/cmd/diff.go b/pkg/kubectl/cmd/diff.go index 3caded567b..fc563ce990 100644 --- a/pkg/kubectl/cmd/diff.go +++ b/pkg/kubectl/cmd/diff.go @@ -17,7 +17,6 @@ limitations under the License. package cmd import ( - "encoding/json" "fmt" "io" "io/ioutil" @@ -25,19 +24,20 @@ import ( "path/filepath" "github.com/ghodss/yaml" + "github.com/jonboulle/clockwork" "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" "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/resource" - "k8s.io/client-go/dynamic" - "k8s.io/kubernetes/pkg/kubectl/apply/parse" - "k8s.io/kubernetes/pkg/kubectl/apply/strategy" + "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/cmd/util/openapi" + "k8s.io/kubernetes/pkg/kubectl/scheme" "k8s.io/kubernetes/pkg/kubectl/util/i18n" "k8s.io/utils/exec" ) @@ -130,7 +130,7 @@ func (d *DiffProgram) Run(from, to string) error { type Printer struct{} // 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 { return nil } @@ -161,10 +161,10 @@ func NewDiffVersion(name string) (*DiffVersion, error) { }, nil } -func (v *DiffVersion) getObject(obj Object) (map[string]interface{}, error) { +func (v *DiffVersion) getObject(obj Object) (runtime.Object, error) { switch v.Name { case "LIVE": - return obj.Live() + return obj.Live(), nil case "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 // it. type Object interface { - Live() (map[string]interface{}, error) - Merged() (map[string]interface{}, error) + Live() runtime.Object + Merged() (runtime.Object, error) Name() string } @@ -225,73 +225,51 @@ type Object interface { // InfoObject is an implementation of the Object interface. It gets all // the information from the Info object. type InfoObject struct { - Remote *unstructured.Unstructured - Info *resource.Info - Encoder runtime.Encoder - Parser *parse.Factory + LocalObj runtime.Object + Info *resource.Info + Encoder runtime.Encoder + OpenAPI openapi.Resources } 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 +// Returns the live version of the object +func (obj InfoObject) Live() runtime.Object { + return obj.Info.Object } -func (obj InfoObject) Live() (map[string]interface{}, error) { - if obj.Remote == nil { - return nil, nil // Object doesn't exist on cluster. +// Returns the "merged" object, as it would look like if applied or +// created. +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) { - data, err := runtime.Encode(obj.Encoder, obj.Info.Object) - if err != nil { - return nil, err - } - local, err := obj.toMap(data) - - live, err := obj.Live() + modified, err := kubectl.GetModifiedConfiguration(obj.LocalObj, false, unstructured.UnstructuredJSONScheme) if err != nil { return nil, err } - last, err := obj.Last() - 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 := &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 { - return local, nil // We probably don't have a live version, 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.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])) + _, 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 { @@ -342,59 +320,14 @@ func (d *Differ) TearDown() { 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 // diff, and find each Info object for each files, and runs against the // differ. func RunDiff(f cmdutil.Factory, diff *DiffProgram, options *DiffOptions) error { - openapi, err := f.OpenAPISchema() + schema, err := f.OpenAPISchema() if err != nil { return err } - parser := &parse.Factory{Resources: openapi} differ, err := NewDiffer("LIVE", "MERGED") if err != nil { @@ -413,29 +346,30 @@ func RunDiff(f cmdutil.Factory, diff *DiffProgram, options *DiffOptions) error { Unstructured(). NamespaceParam(cmdNamespace).DefaultNamespace(). FilenameParam(enforceNamespace, &options.FilenameOptions). - Local(). Flatten(). Do() if err := r.Err(); err != nil { return err } - dl, err := NewDownloader(f) - if err != nil { - return err - } - err = r.Visit(func(info *resource.Info, err error) error { if err != nil { 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{ - Remote: remote, - Info: info, - Parser: parser, - Encoder: cmdutil.InternalVersionJSONEncoder(), + LocalObj: local, + Info: info, + Encoder: scheme.DefaultJSONEncoder(), + OpenAPI: schema, } return differ.Diff(obj, printer) @@ -444,7 +378,8 @@ func RunDiff(f cmdutil.Factory, diff *DiffProgram, options *DiffOptions) error { 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 } diff --git a/pkg/kubectl/cmd/diff_test.go b/pkg/kubectl/cmd/diff_test.go index 9597a1353a..c7e14d53db 100644 --- a/pkg/kubectl/cmd/diff_test.go +++ b/pkg/kubectl/cmd/diff_test.go @@ -25,6 +25,8 @@ import ( "strings" "testing" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/utils/exec" ) @@ -41,12 +43,12 @@ func (f *FakeObject) Name() string { return f.name } -func (f *FakeObject) Merged() (map[string]interface{}, error) { - return f.merged, nil +func (f *FakeObject) Merged() (runtime.Object, error) { + return &unstructured.Unstructured{Object: f.merged}, nil } -func (f *FakeObject) Live() (map[string]interface{}, error) { - return f.live, nil +func (f *FakeObject) Live() runtime.Object { + return &unstructured.Unstructured{Object: f.live} } func TestDiffProgram(t *testing.T) { @@ -68,11 +70,11 @@ func TestDiffProgram(t *testing.T) { func TestPrinter(t *testing.T) { printer := Printer{} - obj := map[string]interface{}{ + obj := &unstructured.Unstructured{Object: map[string]interface{}{ "string": "string", "list": []int{1, 2, 3}, "int": 12, - } + }} buf := bytes.Buffer{} printer.Print(obj, &buf) want := `int: 12