mirror of https://github.com/k3s-io/k3s
Use dry-run patch to get the merged version of the object
parent
0787715978
commit
2d3f7795c8
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue