mirror of
https://github.com/argoproj/argo-cd
synced 2026-04-21 17:07:16 +00:00
Add diff utility library for comparing unstructured object(s)
This commit is contained in:
parent
be0a7e992a
commit
787ee520b4
6 changed files with 291 additions and 86 deletions
24
Gopkg.lock
generated
24
Gopkg.lock
generated
|
|
@ -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
83
test/testdata.go
Normal 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
93
util/diff/diff.go
Normal 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
83
util/diff/diff_test.go
Normal 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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue