Use dry-run patch to get the merged version of the object

pull/58/head
Antoine Pelisse 2018-09-11 10:27:41 -07:00
parent 0787715978
commit 2d3f7795c8
3 changed files with 65 additions and 130 deletions

View File

@ -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",

View File

@ -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
}

View File

@ -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