mirror of https://github.com/k3s-io/k3s
kubectl-diff: Simplify interface
The current interface is kind of clunky and not super easy to use, since you have to specify parameters to specify which versions to diff. Also the default isn't the most useful setting. Change the interface by removing all the parameters and force only one useful use-case, that is: diffing what's currently live against what would be live if applied.pull/58/head
parent
7bfd0d358c
commit
0db6249740
|
@ -0,0 +1,10 @@
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Pod
|
||||||
|
metadata:
|
||||||
|
name: test-pod
|
||||||
|
labels:
|
||||||
|
name: test-pod-label
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: kubernetes-pause
|
||||||
|
image: k8s.gcr.io/pause:3.0
|
|
@ -44,11 +44,8 @@ import (
|
||||||
|
|
||||||
var (
|
var (
|
||||||
diffLong = templates.LongDesc(i18n.T(`
|
diffLong = templates.LongDesc(i18n.T(`
|
||||||
Diff configurations specified by filename or stdin between their local,
|
Diff configurations specified by filename or stdin between the current online
|
||||||
last-applied, live and/or "merged" versions.
|
configuration, and the configuration as it would be if applied.
|
||||||
|
|
||||||
LOCAL and LIVE versions are diffed by default. Other available keywords
|
|
||||||
are MERGED and LAST.
|
|
||||||
|
|
||||||
Output is always YAML.
|
Output is always YAML.
|
||||||
|
|
||||||
|
@ -56,52 +53,22 @@ var (
|
||||||
diff command. By default, the "diff" command available in your path will be
|
diff command. By default, the "diff" command available in your path will be
|
||||||
run with "-u" (unicode) and "-N" (treat new files as empty) options.`))
|
run with "-u" (unicode) and "-N" (treat new files as empty) options.`))
|
||||||
diffExample = templates.Examples(i18n.T(`
|
diffExample = templates.Examples(i18n.T(`
|
||||||
# Diff resources included in pod.json. By default, it will diff LOCAL and LIVE versions
|
# Diff resources included in pod.json.
|
||||||
kubectl alpha diff -f pod.json
|
kubectl alpha diff -f pod.json
|
||||||
|
|
||||||
# When one version is specified, diff that version against LIVE
|
# Diff file read from stdin
|
||||||
cat service.yaml | kubectl alpha diff -f - MERGED
|
cat service.yaml | kubectl alpha diff -f -`))
|
||||||
|
|
||||||
# Or specify both versions
|
|
||||||
kubectl alpha diff -f pod.json -f service.yaml LAST LOCAL`))
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type DiffOptions struct {
|
type DiffOptions struct {
|
||||||
FilenameOptions resource.FilenameOptions
|
FilenameOptions resource.FilenameOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
func isValidArgument(arg string) error {
|
func checkDiffArgs(cmd *cobra.Command, args []string) error {
|
||||||
switch arg {
|
if len(args) != 0 {
|
||||||
case "LOCAL", "LIVE", "LAST", "MERGED":
|
return cmdutil.UsageErrorf(cmd, "Unexpected args: %v", args)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
default:
|
|
||||||
return fmt.Errorf(`Invalid parameter %q, must be either "LOCAL", "LIVE", "LAST" or "MERGED"`, arg)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseDiffArguments(args []string) (string, string, error) {
|
|
||||||
if len(args) > 2 {
|
|
||||||
return "", "", fmt.Errorf("Invalid number of arguments: expected at most 2.")
|
|
||||||
}
|
|
||||||
// Default values
|
|
||||||
from := "LOCAL"
|
|
||||||
to := "LIVE"
|
|
||||||
if len(args) > 0 {
|
|
||||||
from = args[0]
|
|
||||||
}
|
|
||||||
if len(args) > 1 {
|
|
||||||
to = args[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := isValidArgument(to); err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
if err := isValidArgument(from); err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return from, to, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCmdDiff(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command {
|
func NewCmdDiff(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command {
|
||||||
|
@ -113,13 +80,12 @@ func NewCmdDiff(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.C
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "diff -f FILENAME",
|
Use: "diff -f FILENAME",
|
||||||
DisableFlagsInUseLine: true,
|
DisableFlagsInUseLine: true,
|
||||||
Short: i18n.T("Diff different versions of configurations"),
|
Short: i18n.T("Diff live version against would-be applied version"),
|
||||||
Long: diffLong,
|
Long: diffLong,
|
||||||
Example: diffExample,
|
Example: diffExample,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
from, to, err := parseDiffArguments(args)
|
cmdutil.CheckErr(checkDiffArgs(cmd, args))
|
||||||
cmdutil.CheckErr(err)
|
cmdutil.CheckErr(RunDiff(f, &diff, &options))
|
||||||
cmdutil.CheckErr(RunDiff(f, &diff, &options, from, to))
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -201,10 +167,6 @@ func (v *DiffVersion) getObject(obj Object) (map[string]interface{}, error) {
|
||||||
return obj.Live()
|
return obj.Live()
|
||||||
case "MERGED":
|
case "MERGED":
|
||||||
return obj.Merged()
|
return obj.Merged()
|
||||||
case "LOCAL":
|
|
||||||
return obj.Local()
|
|
||||||
case "LAST":
|
|
||||||
return obj.Last()
|
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("Unknown version: %v", v.Name)
|
return nil, fmt.Errorf("Unknown version: %v", v.Name)
|
||||||
}
|
}
|
||||||
|
@ -254,9 +216,7 @@ 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 {
|
||||||
Local() (map[string]interface{}, error)
|
|
||||||
Live() (map[string]interface{}, error)
|
Live() (map[string]interface{}, error)
|
||||||
Last() (map[string]interface{}, error)
|
|
||||||
Merged() (map[string]interface{}, error)
|
Merged() (map[string]interface{}, error)
|
||||||
|
|
||||||
Name() string
|
Name() string
|
||||||
|
@ -282,14 +242,6 @@ func (obj InfoObject) toMap(data []byte) (map[string]interface{}, error) {
|
||||||
return m, err
|
return m, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (obj InfoObject) Local() (map[string]interface{}, error) {
|
|
||||||
data, err := runtime.Encode(obj.Encoder, obj.Info.Object)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return obj.toMap(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (obj InfoObject) Live() (map[string]interface{}, error) {
|
func (obj InfoObject) Live() (map[string]interface{}, error) {
|
||||||
if obj.Remote == nil {
|
if obj.Remote == nil {
|
||||||
return nil, nil // Object doesn't exist on cluster.
|
return nil, nil // Object doesn't exist on cluster.
|
||||||
|
@ -298,10 +250,11 @@ func (obj InfoObject) Live() (map[string]interface{}, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (obj InfoObject) Merged() (map[string]interface{}, error) {
|
func (obj InfoObject) Merged() (map[string]interface{}, error) {
|
||||||
local, err := obj.Local()
|
data, err := runtime.Encode(obj.Encoder, obj.Info.Object)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
local, err := obj.toMap(data)
|
||||||
|
|
||||||
live, err := obj.Live()
|
live, err := obj.Live()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -436,14 +389,14 @@ func (d *Downloader) Download(info *resource.Info) (*unstructured.Unstructured,
|
||||||
// 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, from, to string) error {
|
func RunDiff(f cmdutil.Factory, diff *DiffProgram, options *DiffOptions) error {
|
||||||
openapi, err := f.OpenAPISchema()
|
openapi, err := f.OpenAPISchema()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
parser := &parse.Factory{Resources: openapi}
|
parser := &parse.Factory{Resources: openapi}
|
||||||
|
|
||||||
differ, err := NewDiffer(from, to)
|
differ, err := NewDiffer("LIVE", "MERGED")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,10 +31,8 @@ import (
|
||||||
|
|
||||||
type FakeObject struct {
|
type FakeObject struct {
|
||||||
name string
|
name string
|
||||||
local map[string]interface{}
|
|
||||||
merged map[string]interface{}
|
merged map[string]interface{}
|
||||||
live map[string]interface{}
|
live map[string]interface{}
|
||||||
last map[string]interface{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ Object = &FakeObject{}
|
var _ Object = &FakeObject{}
|
||||||
|
@ -43,10 +41,6 @@ func (f *FakeObject) Name() string {
|
||||||
return f.name
|
return f.name
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FakeObject) Local() (map[string]interface{}, error) {
|
|
||||||
return f.local, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *FakeObject) Merged() (map[string]interface{}, error) {
|
func (f *FakeObject) Merged() (map[string]interface{}, error) {
|
||||||
return f.merged, nil
|
return f.merged, nil
|
||||||
}
|
}
|
||||||
|
@ -55,87 +49,6 @@ func (f *FakeObject) Live() (map[string]interface{}, error) {
|
||||||
return f.live, nil
|
return f.live, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FakeObject) Last() (map[string]interface{}, error) {
|
|
||||||
return f.last, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestArguments(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
// Input
|
|
||||||
args []string
|
|
||||||
|
|
||||||
// Outputs
|
|
||||||
from string
|
|
||||||
to string
|
|
||||||
err string
|
|
||||||
}{
|
|
||||||
// Defaults
|
|
||||||
{
|
|
||||||
args: []string{},
|
|
||||||
from: "LOCAL",
|
|
||||||
to: "LIVE",
|
|
||||||
err: "",
|
|
||||||
},
|
|
||||||
// One valid argument
|
|
||||||
{
|
|
||||||
args: []string{"MERGED"},
|
|
||||||
from: "MERGED",
|
|
||||||
to: "LIVE",
|
|
||||||
err: "",
|
|
||||||
},
|
|
||||||
// One invalid argument
|
|
||||||
{
|
|
||||||
args: []string{"WRONG"},
|
|
||||||
from: "",
|
|
||||||
to: "",
|
|
||||||
err: `Invalid parameter "WRONG", must be either "LOCAL", "LIVE", "LAST" or "MERGED"`,
|
|
||||||
},
|
|
||||||
// Two valid arguments
|
|
||||||
{
|
|
||||||
args: []string{"MERGED", "LAST"},
|
|
||||||
from: "MERGED",
|
|
||||||
to: "LAST",
|
|
||||||
err: "",
|
|
||||||
},
|
|
||||||
// Two same arguments is fine
|
|
||||||
{
|
|
||||||
args: []string{"MERGED", "MERGED"},
|
|
||||||
from: "MERGED",
|
|
||||||
to: "MERGED",
|
|
||||||
err: "",
|
|
||||||
},
|
|
||||||
// Second argument is invalid
|
|
||||||
{
|
|
||||||
args: []string{"MERGED", "WRONG"},
|
|
||||||
from: "",
|
|
||||||
to: "",
|
|
||||||
err: `Invalid parameter "WRONG", must be either "LOCAL", "LIVE", "LAST" or "MERGED"`,
|
|
||||||
},
|
|
||||||
// Three arguments
|
|
||||||
{
|
|
||||||
args: []string{"MERGED", "LIVE", "LAST"},
|
|
||||||
from: "",
|
|
||||||
to: "",
|
|
||||||
err: `Invalid number of arguments: expected at most 2.`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
from, to, e := parseDiffArguments(test.args)
|
|
||||||
err := ""
|
|
||||||
if e != nil {
|
|
||||||
err = e.Error()
|
|
||||||
}
|
|
||||||
if from != test.from || to != test.to || err != test.err {
|
|
||||||
t.Errorf("parseDiffArguments(%v) = (%v, %v, %v), expected (%v, %v, %v)",
|
|
||||||
test.args,
|
|
||||||
from, to, err,
|
|
||||||
test.from, test.to, test.err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDiffProgram(t *testing.T) {
|
func TestDiffProgram(t *testing.T) {
|
||||||
os.Setenv("KUBERNETES_EXTERNAL_DIFF", "echo")
|
os.Setenv("KUBERNETES_EXTERNAL_DIFF", "echo")
|
||||||
streams, _, stdout, _ := genericclioptions.NewTestIOStreams()
|
streams, _, stdout, _ := genericclioptions.NewTestIOStreams()
|
||||||
|
@ -175,7 +88,7 @@ string: string
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDiffVersion(t *testing.T) {
|
func TestDiffVersion(t *testing.T) {
|
||||||
diff, err := NewDiffVersion("LOCAL")
|
diff, err := NewDiffVersion("MERGED")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -183,8 +96,6 @@ func TestDiffVersion(t *testing.T) {
|
||||||
|
|
||||||
obj := FakeObject{
|
obj := FakeObject{
|
||||||
name: "bla",
|
name: "bla",
|
||||||
local: map[string]interface{}{"local": true},
|
|
||||||
last: map[string]interface{}{"last": true},
|
|
||||||
live: map[string]interface{}{"live": true},
|
live: map[string]interface{}{"live": true},
|
||||||
merged: map[string]interface{}{"merged": true},
|
merged: map[string]interface{}{"merged": true},
|
||||||
}
|
}
|
||||||
|
@ -196,7 +107,7 @@ func TestDiffVersion(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
econtent := "local: true\n"
|
econtent := "merged: true\n"
|
||||||
if string(fcontent) != econtent {
|
if string(fcontent) != econtent {
|
||||||
t.Fatalf("File has %q, expected %q", string(fcontent), econtent)
|
t.Fatalf("File has %q, expected %q", string(fcontent), econtent)
|
||||||
}
|
}
|
||||||
|
@ -248,7 +159,7 @@ func TestDirectory(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDiffer(t *testing.T) {
|
func TestDiffer(t *testing.T) {
|
||||||
diff, err := NewDiffer("LOCAL", "LIVE")
|
diff, err := NewDiffer("LIVE", "MERGED")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -256,8 +167,6 @@ func TestDiffer(t *testing.T) {
|
||||||
|
|
||||||
obj := FakeObject{
|
obj := FakeObject{
|
||||||
name: "bla",
|
name: "bla",
|
||||||
local: map[string]interface{}{"local": true},
|
|
||||||
last: map[string]interface{}{"last": true},
|
|
||||||
live: map[string]interface{}{"live": true},
|
live: map[string]interface{}{"live": true},
|
||||||
merged: map[string]interface{}{"merged": true},
|
merged: map[string]interface{}{"merged": true},
|
||||||
}
|
}
|
||||||
|
@ -269,7 +178,7 @@ func TestDiffer(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
econtent := "local: true\n"
|
econtent := "live: true\n"
|
||||||
if string(fcontent) != econtent {
|
if string(fcontent) != econtent {
|
||||||
t.Fatalf("File has %q, expected %q", string(fcontent), econtent)
|
t.Fatalf("File has %q, expected %q", string(fcontent), econtent)
|
||||||
}
|
}
|
||||||
|
@ -278,7 +187,7 @@ func TestDiffer(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
econtent = "live: true\n"
|
econtent = "merged: true\n"
|
||||||
if string(fcontent) != econtent {
|
if string(fcontent) != econtent {
|
||||||
t.Fatalf("File has %q, expected %q", string(fcontent), econtent)
|
t.Fatalf("File has %q, expected %q", string(fcontent), econtent)
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,27 +27,13 @@ run_kubectl_diff_tests() {
|
||||||
kube::log::status "Testing kubectl alpha diff"
|
kube::log::status "Testing kubectl alpha diff"
|
||||||
|
|
||||||
# Test that it works when the live object doesn't exist
|
# Test that it works when the live object doesn't exist
|
||||||
output_message=$(kubectl alpha diff LOCAL LIVE -f hack/testdata/pod.yaml)
|
output_message=$(kubectl alpha diff -f hack/testdata/pod.yaml)
|
||||||
kube::test::if_has_string "${output_message}" 'test-pod'
|
kube::test::if_has_string "${output_message}" 'test-pod'
|
||||||
|
|
||||||
kubectl apply -f hack/testdata/pod.yaml
|
kubectl apply -f hack/testdata/pod.yaml
|
||||||
|
|
||||||
# Ensure that selfLink has been added, and shown in the diff
|
output_message=$(kubectl alpha diff -f hack/testdata/pod-changed.yaml)
|
||||||
output_message=$(kubectl alpha diff -f hack/testdata/pod.yaml)
|
kube::test::if_has_string "${output_message}" 'k8s.gcr.io/pause:3.0'
|
||||||
kube::test::if_has_string "${output_message}" 'selfLink'
|
|
||||||
output_message=$(kubectl alpha diff LOCAL LIVE -f hack/testdata/pod.yaml)
|
|
||||||
kube::test::if_has_string "${output_message}" 'selfLink'
|
|
||||||
output_message=$(kubectl alpha diff LOCAL MERGED -f hack/testdata/pod.yaml)
|
|
||||||
kube::test::if_has_string "${output_message}" 'selfLink'
|
|
||||||
|
|
||||||
output_message=$(kubectl alpha diff MERGED MERGED -f hack/testdata/pod.yaml)
|
|
||||||
kube::test::if_empty_string "${output_message}"
|
|
||||||
output_message=$(kubectl alpha diff LIVE LIVE -f hack/testdata/pod.yaml)
|
|
||||||
kube::test::if_empty_string "${output_message}"
|
|
||||||
output_message=$(kubectl alpha diff LAST LAST -f hack/testdata/pod.yaml)
|
|
||||||
kube::test::if_empty_string "${output_message}"
|
|
||||||
output_message=$(kubectl alpha diff LOCAL LOCAL -f hack/testdata/pod.yaml)
|
|
||||||
kube::test::if_empty_string "${output_message}"
|
|
||||||
|
|
||||||
kubectl delete -f hack/testdata/pod.yaml
|
kubectl delete -f hack/testdata/pod.yaml
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue