diff --git a/Gopkg.lock b/Gopkg.lock index 98fa90c925..65582b2c50 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -295,6 +295,12 @@ revision = "792786c7400a136282c1664665ae0a8db921c6c2" version = "v1.0.0" +[[projects]] + name = "github.com/sergi/go-diff" + packages = ["diffmatchpatch"] + revision = "1744e2970ca51c86172c8190fadad617561ed6e7" + version = "v1.0.0" + [[projects]] name = "github.com/sirupsen/logrus" packages = ["."] @@ -341,6 +347,21 @@ revision = "12b6f73e6084dad08a7c6e575284b177ecafbc71" version = "v1.2.1" +[[projects]] + name = "github.com/yudai/gojsondiff" + packages = [ + ".", + "formatter" + ] + revision = "7b1b7adf999dab73a6eb02669c3d82dbb27a3dd6" + version = "1.0.0" + +[[projects]] + branch = "master" + name = "github.com/yudai/golcs" + packages = ["."] + revision = "ecda9a501e8220fae3b4b600c3db4b0ba22cfc68" + [[projects]] branch = "master" name = "golang.org/x/crypto" @@ -532,6 +553,7 @@ branch = "release-1.9" name = "k8s.io/apimachinery" packages = [ + "pkg/api/equality", "pkg/api/errors", "pkg/api/meta", "pkg/api/resource", @@ -698,6 +720,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "9f0307c913e8dc2b20bc7d25b9dabee66db88830c6d7d488ab944521c32170fd" + inputs-digest = "5be74ebbb2ce35072bfaad0fee8f947fe029c7b45fb8451bf8741ef1737ba4ef" solver-name = "gps-cdcl" solver-version = 1 diff --git a/test/testdata.go b/test/testdata.go new file mode 100644 index 0000000000..a27783a3ce --- /dev/null +++ b/test/testdata.go @@ -0,0 +1,83 @@ +package test + +import ( + "github.com/argoproj/argo-cd/common" + appsv1 "k8s.io/api/apps/v1" + apiv1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" +) + +const ( + TestNamespace = "test-namespace" + TestAppInstanceName = "test-app-instance" +) + +func DemoService() *apiv1.Service { + return &apiv1.Service{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Service", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "demo", + Namespace: TestNamespace, + Labels: map[string]string{ + common.LabelKeyAppInstance: TestAppInstanceName, + }, + }, + Spec: apiv1.ServiceSpec{ + Ports: []apiv1.ServicePort{ + { + Port: 80, + TargetPort: intstr.FromInt(80), + }, + }, + Selector: map[string]string{ + "app": "demo", + }, + Type: "ClusterIP", + }, + } + +} + +func DemoDeployment() *appsv1.Deployment { + var two int32 = 2 + return &appsv1.Deployment{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "apps/v1beta1", + Kind: "Deployment", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "demo", + Namespace: TestNamespace, + Labels: map[string]string{ + common.LabelKeyAppInstance: TestAppInstanceName, + }, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: &two, + Template: apiv1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "app": "demo", + }, + }, + Spec: apiv1.PodSpec{ + Containers: []apiv1.Container{ + { + Name: "demo", + Image: "gcr.io/kuar-demo/kuard-amd64:1", + Ports: []apiv1.ContainerPort{ + { + ContainerPort: 80, + }, + }, + }, + }, + }, + }, + }, + } +} diff --git a/util/diff/diff.go b/util/diff/diff.go new file mode 100644 index 0000000000..8facf161c7 --- /dev/null +++ b/util/diff/diff.go @@ -0,0 +1,93 @@ +package diff + +import ( + "fmt" + "log" + "strings" + + "github.com/yudai/gojsondiff" + "github.com/yudai/gojsondiff/formatter" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +type DiffResult struct { + Diff gojsondiff.Diff `json:"-"` + Modified bool `json:"modified"` + AdditionsOnly *bool `json:"additionsOnly,omitempty"` + Output string `json:"output,omitempty"` +} + +// Diff performs a diff on two unstructured objects +func Diff(left, right *unstructured.Unstructured) *DiffResult { + gjDiff := gojsondiff.New().CompareObjects(left.Object, right.Object) + out, additions := renderOutput(left.Object, gjDiff) + return &DiffResult{ + Diff: gjDiff, + Output: out, + AdditionsOnly: additions, + Modified: gjDiff.Modified(), + } +} + +type DiffResultList struct { + Diffs []DiffResult `json:"diffs,omitempty"` + Modified bool `json:"modified"` + AdditionsOnly *bool `json:"additionsOnly,omitempty"` +} + +// DiffArray performs a diff on a list of unstructured objects. Objects are expected to match +// environments +func DiffArray(leftArray, rightArray []*unstructured.Unstructured) (*DiffResultList, error) { + numItems := len(leftArray) + if len(rightArray) != numItems { + return nil, fmt.Errorf("left and right arrays have mismatched lengths") + } + + diffResultList := DiffResultList{ + Diffs: make([]DiffResult, numItems), + } + for i := 0; i < numItems; i++ { + left := leftArray[i] + right := rightArray[i] + diffRes := Diff(left, right) + diffResultList.Diffs[i] = *diffRes + log.Println(diffRes.Output) + if diffRes.Modified { + diffResultList.Modified = true + if !*diffRes.AdditionsOnly { + diffResultList.AdditionsOnly = diffRes.AdditionsOnly + } + } + } + if diffResultList.Modified && diffResultList.AdditionsOnly == nil { + t := true + diffResultList.AdditionsOnly = &t + } + return &diffResultList, nil +} + +// renderOutput is a helper to render the output and check if the modifications are only additions +func renderOutput(left interface{}, diff gojsondiff.Diff) (string, *bool) { + if !diff.Modified() { + return "", nil + } + diffFmt := formatter.NewAsciiFormatter(left, formatter.AsciiFormatterConfig{}) + out, err := diffFmt.Format(diff) + if err != nil { + panic(err) + } + for _, line := range strings.Split(out, "\n") { + if len(line) == 0 { + continue + } + log.Println(string(line[0])) + switch string(line[0]) { + case formatter.AsciiAdded, formatter.AsciiSame: + default: + f := false + return out, &f + } + } + t := true + return out, &t +} diff --git a/util/diff/diff_test.go b/util/diff/diff_test.go new file mode 100644 index 0000000000..a342666f7b --- /dev/null +++ b/util/diff/diff_test.go @@ -0,0 +1,83 @@ +package diff + +import ( + "testing" + + "github.com/argoproj/argo-cd/test" + "github.com/stretchr/testify/assert" + apiequality "k8s.io/apimachinery/pkg/api/equality" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" +) + +func TestDiff(t *testing.T) { + leftDep := test.DemoDeployment() + leftMap, err := runtime.NewTestUnstructuredConverter(apiequality.Semantic).ToUnstructured(leftDep) + assert.Nil(t, err) + leftUn := unstructured.Unstructured{Object: leftMap} + left := &leftUn + + diffRes := Diff(left, left) + assert.False(t, diffRes.Diff.Modified()) + assert.Nil(t, diffRes.AdditionsOnly) +} + +func TestDiffArraySame(t *testing.T) { + leftDep := test.DemoDeployment() + rightDep := leftDep.DeepCopy() + + leftMap, err := runtime.NewTestUnstructuredConverter(apiequality.Semantic).ToUnstructured(leftDep) + assert.Nil(t, err) + leftUn := unstructured.Unstructured{Object: leftMap} + rightMap, err := runtime.NewTestUnstructuredConverter(apiequality.Semantic).ToUnstructured(rightDep) + assert.Nil(t, err) + rightUn := unstructured.Unstructured{Object: rightMap} + + left := []*unstructured.Unstructured{&leftUn} + right := []*unstructured.Unstructured{&rightUn} + diffResList, err := DiffArray(left, right) + assert.Nil(t, err) + assert.False(t, diffResList.Modified) + assert.Nil(t, diffResList.AdditionsOnly) +} + +func TestDiffArrayAdditions(t *testing.T) { + leftDep := test.DemoDeployment() + rightDep := leftDep.DeepCopy() + rightDep.Status.Replicas = 1 + + leftMap, err := runtime.NewTestUnstructuredConverter(apiequality.Semantic).ToUnstructured(leftDep) + assert.Nil(t, err) + leftUn := unstructured.Unstructured{Object: leftMap} + rightMap, err := runtime.NewTestUnstructuredConverter(apiequality.Semantic).ToUnstructured(rightDep) + assert.Nil(t, err) + rightUn := unstructured.Unstructured{Object: rightMap} + + left := []*unstructured.Unstructured{&leftUn} + right := []*unstructured.Unstructured{&rightUn} + diffResList, err := DiffArray(left, right) + assert.Nil(t, err) + assert.True(t, diffResList.Modified) + assert.True(t, *diffResList.AdditionsOnly) +} + +func TestDiffArrayModification(t *testing.T) { + leftDep := test.DemoDeployment() + rightDep := leftDep.DeepCopy() + ten := int32(10) + rightDep.Spec.Replicas = &ten + + leftMap, err := runtime.NewTestUnstructuredConverter(apiequality.Semantic).ToUnstructured(leftDep) + assert.Nil(t, err) + leftUn := unstructured.Unstructured{Object: leftMap} + rightMap, err := runtime.NewTestUnstructuredConverter(apiequality.Semantic).ToUnstructured(rightDep) + assert.Nil(t, err) + rightUn := unstructured.Unstructured{Object: rightMap} + + left := []*unstructured.Unstructured{&leftUn} + right := []*unstructured.Unstructured{&rightUn} + diffResList, err := DiffArray(left, right) + assert.Nil(t, err) + assert.True(t, diffResList.Modified) + assert.False(t, *diffResList.AdditionsOnly) +} diff --git a/util/ksonnet/ksonnet.go b/util/ksonnet/ksonnet.go index 7e4db25bf8..fa891b9a47 100644 --- a/util/ksonnet/ksonnet.go +++ b/util/ksonnet/ksonnet.go @@ -27,7 +27,7 @@ type KsonnetApp interface { AppSpec() app.Spec // Show returns a list of unstructured objects that would be applied to an environment - Show(environment string) ([]unstructured.Unstructured, error) + Show(environment string) ([]*unstructured.Unstructured, error) } type ksonnetApp struct { @@ -75,13 +75,13 @@ func (k *ksonnetApp) AppSpec() app.Spec { return k.spec } -func (k *ksonnetApp) Show(environment string) ([]unstructured.Unstructured, error) { +func (k *ksonnetApp) Show(environment string) ([]*unstructured.Unstructured, error) { out, err := k.ksCmd("show", environment) if err != nil { return nil, err } parts := diffSeparator.Split(out, -1) - objs := make([]unstructured.Unstructured, 0) + objs := make([]*unstructured.Unstructured, 0) for _, part := range parts { if strings.TrimSpace(part) == "" { continue @@ -91,7 +91,7 @@ func (k *ksonnetApp) Show(environment string) ([]unstructured.Unstructured, erro if err != nil { return nil, fmt.Errorf("Failed to unmarshal manifest from `ks show`") } - objs = append(objs, obj) + objs = append(objs, &obj) } return objs, nil } diff --git a/util/kube/kube_test.go b/util/kube/kube_test.go index 5c95fcfa0c..2b6b113292 100644 --- a/util/kube/kube_test.go +++ b/util/kube/kube_test.go @@ -5,10 +5,9 @@ import ( "log" "testing" - "github.com/argoproj/argo-cd/common" argoappv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1" + "github.com/argoproj/argo-cd/test" "github.com/stretchr/testify/assert" - appsv1 "k8s.io/api/apps/v1" appsv1beta1 "k8s.io/api/apps/v1beta1" appsv1beta2 "k8s.io/api/apps/v1beta2" apiv1 "k8s.io/api/core/v1" @@ -16,87 +15,12 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/util/intstr" fakediscovery "k8s.io/client-go/discovery/fake" fakedynamic "k8s.io/client-go/dynamic/fake" "k8s.io/client-go/kubernetes/fake" kubetesting "k8s.io/client-go/testing" ) -const ( - testNamespace = "test-namespace" - testAppInstanceName = "test-app-instance" -) - -func demoService() *apiv1.Service { - return &apiv1.Service{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "v1", - Kind: "Service", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "demo", - Namespace: testNamespace, - Labels: map[string]string{ - common.LabelKeyAppInstance: testAppInstanceName, - }, - }, - Spec: apiv1.ServiceSpec{ - Ports: []apiv1.ServicePort{ - { - Port: 80, - TargetPort: intstr.FromInt(80), - }, - }, - Selector: map[string]string{ - "app": "demo", - }, - Type: "ClusterIP", - }, - } - -} - -func demoDeployment() *appsv1.Deployment { - var two int32 = 2 - return &appsv1.Deployment{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "apps/v1beta1", - Kind: "Deployment", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "demo", - Namespace: testNamespace, - Labels: map[string]string{ - common.LabelKeyAppInstance: testAppInstanceName, - }, - }, - Spec: appsv1.DeploymentSpec{ - Replicas: &two, - Template: apiv1.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{ - Labels: map[string]string{ - "app": "demo", - }, - }, - Spec: apiv1.PodSpec{ - Containers: []apiv1.Container{ - { - Name: "demo", - Image: "gcr.io/kuar-demo/kuard-amd64:1", - Ports: []apiv1.ContainerPort{ - { - ContainerPort: 80, - }, - }, - }, - }, - }, - }, - }, - } -} - func resourceList() []*metav1.APIResourceList { return []*metav1.APIResourceList{ { @@ -139,7 +63,7 @@ func resourceList() []*metav1.APIResourceList { } func TestListAPIResources(t *testing.T) { - kubeclientset := fake.NewSimpleClientset(demoService(), demoDeployment()) + kubeclientset := fake.NewSimpleClientset(test.DemoService(), test.DemoDeployment()) fakeDiscovery, ok := kubeclientset.Discovery().(*fakediscovery.FakeDiscovery) assert.True(t, ok) fakeDiscovery.Fake.Resources = resourceList() @@ -149,12 +73,12 @@ func TestListAPIResources(t *testing.T) { } func TestListResources(t *testing.T) { - kubeclientset := fake.NewSimpleClientset(demoService(), demoDeployment()) + kubeclientset := fake.NewSimpleClientset(test.DemoService(), test.DemoDeployment()) fakeDynClient := fakedynamic.FakeClient{ Fake: &kubetesting.Fake{}, } fakeDynClient.Fake.AddReactor("list", "services", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) { - svcList, err := kubeclientset.CoreV1().Services(testNamespace).List(metav1.ListOptions{}) + svcList, err := kubeclientset.CoreV1().Services(test.TestNamespace).List(metav1.ListOptions{}) assert.Nil(t, err) svcList.Kind = "ServiceList" svcListBytes, err := json.Marshal(svcList) @@ -172,7 +96,7 @@ func TestListResources(t *testing.T) { Version: "v1", Kind: "Service", } - resList, err := ListResources(&fakeDynClient, apiResource, testNamespace, metav1.ListOptions{}) + resList, err := ListResources(&fakeDynClient, apiResource, test.TestNamespace, metav1.ListOptions{}) assert.Nil(t, err) assert.Equal(t, 1, len(resList)) }