Add diff utility library for comparing unstructured object(s)

This commit is contained in:
Jesse Suen 2018-02-25 18:36:19 -08:00
parent be0a7e992a
commit 787ee520b4
No known key found for this signature in database
GPG key ID: 90C911E8A6106562
6 changed files with 291 additions and 86 deletions

24
Gopkg.lock generated
View file

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

83
test/testdata.go Normal file
View file

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

93
util/diff/diff.go Normal file
View file

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

83
util/diff/diff_test.go Normal file
View file

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

View file

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

View file

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