Files
argo-cd/util/diff/diff_test.go
Alexander Matyushentsev 87b327f52d Issue #1075 - Ability to selectively ignore differences to support fuzzy diff comparisons (#1130)
* Issue #1075 - Ability to selectively ignore differences to support fuzzy diff comparisons
2019-02-22 13:19:10 -08:00

590 lines
17 KiB
Go

package diff
import (
"encoding/json"
"io/ioutil"
"log"
"os"
"strings"
"testing"
"github.com/ghodss/yaml"
"github.com/stretchr/testify/assert"
"github.com/yudai/gojsondiff/formatter"
"golang.org/x/crypto/ssh/terminal"
corev1 "k8s.io/api/core/v1"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/equality"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"github.com/argoproj/argo-cd/errors"
"github.com/argoproj/argo-cd/test"
)
var (
formatOpts = formatter.AsciiFormatterConfig{
Coloring: terminal.IsTerminal(int(os.Stdout.Fd())),
}
)
func toUnstructured(obj interface{}) (*unstructured.Unstructured, error) {
uObj, err := runtime.NewTestUnstructuredConverter(equality.Semantic).ToUnstructured(obj)
if err != nil {
return nil, err
}
return &unstructured.Unstructured{Object: uObj}, nil
}
func mustToUnstructured(obj interface{}) *unstructured.Unstructured {
un, err := toUnstructured(obj)
errors.CheckError(err)
return un
}
func unmarshalFile(path string) *unstructured.Unstructured {
data, err := ioutil.ReadFile(path)
errors.CheckError(err)
var un unstructured.Unstructured
err = json.Unmarshal(data, &un.Object)
errors.CheckError(err)
return &un
}
func TestDiff(t *testing.T) {
leftDep := test.DemoDeployment()
leftUn := mustToUnstructured(leftDep)
diffRes := Diff(leftUn, leftUn, nil)
assert.False(t, diffRes.Diff.Modified())
ascii, err := diffRes.ASCIIFormat(leftUn, formatOpts)
assert.Nil(t, err)
if ascii != "" {
log.Println(ascii)
}
}
func TestDiffWithNils(t *testing.T) {
dep := test.DemoDeployment()
resource := mustToUnstructured(dep)
diffRes := Diff(nil, resource, nil)
// NOTE: if live is non-nil, and config is nil, this is not considered difference
// This "difference" is checked at the comparator.
assert.False(t, diffRes.Diff.Modified())
diffRes = TwoWayDiff(nil, resource)
assert.False(t, diffRes.Diff.Modified())
diffRes = Diff(resource, nil, nil)
assert.True(t, diffRes.Diff.Modified())
diffRes = TwoWayDiff(resource, nil)
assert.True(t, diffRes.Diff.Modified())
}
func TestDiffNilFieldInLive(t *testing.T) {
leftDep := test.DemoDeployment()
rightDep := leftDep.DeepCopy()
leftUn := mustToUnstructured(leftDep)
rightUn := mustToUnstructured(rightDep)
err := unstructured.SetNestedField(rightUn.Object, nil, "spec")
assert.Nil(t, err)
diffRes := Diff(leftUn, rightUn, nil)
assert.True(t, diffRes.Modified)
}
func TestDiffArraySame(t *testing.T) {
leftDep := test.DemoDeployment()
rightDep := leftDep.DeepCopy()
leftUn := mustToUnstructured(leftDep)
rightUn := mustToUnstructured(rightDep)
left := []*unstructured.Unstructured{leftUn}
right := []*unstructured.Unstructured{rightUn}
diffResList, err := DiffArray(left, right, nil)
assert.Nil(t, err)
assert.False(t, diffResList.Modified)
}
func TestDiffArrayAdditions(t *testing.T) {
leftDep := test.DemoDeployment()
rightDep := leftDep.DeepCopy()
rightDep.Status.Replicas = 1
leftUn := mustToUnstructured(leftDep)
rightUn := mustToUnstructured(rightDep)
left := []*unstructured.Unstructured{leftUn}
right := []*unstructured.Unstructured{rightUn}
diffResList, err := DiffArray(left, right, nil)
assert.Nil(t, err)
assert.False(t, diffResList.Modified)
}
func TestDiffArrayModification(t *testing.T) {
leftDep := test.DemoDeployment()
rightDep := leftDep.DeepCopy()
ten := int32(10)
rightDep.Spec.Replicas = &ten
leftUn := mustToUnstructured(leftDep)
rightUn := mustToUnstructured(rightDep)
left := []*unstructured.Unstructured{leftUn}
right := []*unstructured.Unstructured{rightUn}
diffResList, err := DiffArray(left, right, nil)
assert.Nil(t, err)
assert.True(t, diffResList.Modified)
}
// TestThreeWayDiff will perform a diff when there is a kubectl.kubernetes.io/last-applied-configuration
// present in the live object.
func TestThreeWayDiff(t *testing.T) {
// 1. get config and live to be the same. Both have a foo annotation.
configDep := test.DemoDeployment()
configDep.ObjectMeta.Namespace = ""
configDep.Annotations = map[string]string{
"foo": "bar",
}
liveDep := configDep.DeepCopy()
// 2. add a extra field to the live. this simulates kubernetes adding default values in the
// object. We should not consider defaulted values as a difference
liveDep.SetNamespace("default")
configUn := mustToUnstructured(configDep)
liveUn := mustToUnstructured(liveDep)
res := Diff(configUn, liveUn, nil)
if !assert.False(t, res.Modified) {
ascii, err := res.ASCIIFormat(liveUn, formatOpts)
assert.Nil(t, err)
log.Println(ascii)
}
// 3. Add a last-applied-configuration annotation in the live. There should still not be any
// difference
configBytes, err := json.Marshal(configDep)
assert.Nil(t, err)
liveDep.Annotations[v1.LastAppliedConfigAnnotation] = string(configBytes)
configUn = mustToUnstructured(configDep)
liveUn = mustToUnstructured(liveDep)
res = Diff(configUn, liveUn, nil)
if !assert.False(t, res.Modified) {
ascii, err := res.ASCIIFormat(liveUn, formatOpts)
assert.Nil(t, err)
log.Println(ascii)
}
// 4. Remove the foo annotation from config and perform the diff again. We should detect a
// difference since three-way diff detects the removal of a managed field
delete(configDep.Annotations, "foo")
configUn = mustToUnstructured(configDep)
liveUn = mustToUnstructured(liveDep)
res = Diff(configUn, liveUn, nil)
assert.True(t, res.Modified)
// 5. Just to prove three way diff incorporates last-applied-configuration, remove the
// last-applied-configuration annotation from the live object, and redo the diff. This time,
// the diff will report not modified (because we have no way of knowing what was a defaulted
// field without this annotation)
delete(liveDep.Annotations, v1.LastAppliedConfigAnnotation)
configUn = mustToUnstructured(configDep)
liveUn = mustToUnstructured(liveDep)
res = Diff(configUn, liveUn, nil)
ascii, err := res.ASCIIFormat(liveUn, formatOpts)
assert.Nil(t, err)
if ascii != "" {
log.Println(ascii)
}
assert.False(t, res.Modified)
}
var demoConfig = `
{
"apiVersion": "v1",
"kind": "ServiceAccount",
"metadata": {
"labels": {
"app.kubernetes.io/instance": "argocd-demo"
},
"name": "argocd-application-controller"
}
}
`
var demoLive = `
{
"apiVersion": "v1",
"kind": "ServiceAccount",
"metadata": {
"annotations": {
"kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"ServiceAccount\",\"metadata\":{\"annotations\":{},\"labels\":{\"app.kubernetes.io/instance\":\"argocd-demo\"},\"name\":\"argocd-application-controller\",\"namespace\":\"argocd-demo\"}}\n"
},
"creationTimestamp": "2018-04-16T22:08:57Z",
"labels": {
"app.kubernetes.io/instance": "argocd-demo"
},
"name": "argocd-application-controller",
"namespace": "argocd-demo",
"resourceVersion": "7584502",
"selfLink": "/api/v1/namespaces/argocd-demo/serviceaccounts/argocd-application-controller",
"uid": "c22bb2b4-41c2-11e8-978a-028445d52ec8"
},
"secrets": [
{
"name": "argocd-application-controller-token-kfxct"
}
]
}
`
// Tests a real world example
func TestThreeWayDiffExample1(t *testing.T) {
var configUn, liveUn unstructured.Unstructured
// NOTE: it is intentional to unmarshal to Unstructured.Object instead of just Unstructured
// since it catches a case when we comparison fails due to subtle differences in types
// (e.g. float vs. int)
err := json.Unmarshal([]byte(demoConfig), &configUn.Object)
assert.Nil(t, err)
err = json.Unmarshal([]byte(demoLive), &liveUn.Object)
assert.Nil(t, err)
dr := Diff(&configUn, &liveUn, nil)
assert.False(t, dr.Modified)
ascii, err := dr.ASCIIFormat(&liveUn, formatOpts)
assert.Nil(t, err)
if ascii != "" {
log.Println(ascii)
}
}
func TestThreeWayDiffExample2(t *testing.T) {
configUn := unmarshalFile("testdata/elasticsearch-config.json")
liveUn := unmarshalFile("testdata/elasticsearch-live.json")
dr := Diff(configUn, liveUn, nil)
assert.False(t, dr.Modified)
ascii, err := dr.ASCIIFormat(liveUn, formatOpts)
assert.Nil(t, err)
log.Println(ascii)
}
// TestThreeWayDiffExample2WithDifference is same as TestThreeWayDiffExample2 but with differences
func TestThreeWayDiffExample2WithDifference(t *testing.T) {
configUn := unmarshalFile("testdata/elasticsearch-config.json")
liveUn := unmarshalFile("testdata/elasticsearch-live.json")
labels := configUn.GetLabels()
// add a new label
labels["foo"] = "bar"
// modify a label
labels["chart"] = "elasticsearch-1.7.1"
// remove an existing label
delete(labels, "release")
configUn.SetLabels(labels)
dr := Diff(configUn, liveUn, nil)
assert.True(t, dr.Modified)
ascii, err := dr.ASCIIFormat(liveUn, formatOpts)
assert.Nil(t, err)
log.Println(ascii)
// Check that we indicate missing/extra/changed correctly
showsMissing := 0
showsExtra := 0
showsChanged := 0
for _, line := range strings.Split(ascii, "\n") {
if strings.HasPrefix(line, `+ "foo": "bar"`) {
showsMissing++
}
if strings.HasPrefix(line, `- "release": "elasticsearch4"`) {
showsExtra++
}
if strings.HasPrefix(line, `+ "chart": "elasticsearch-1.7.1"`) {
showsChanged++
}
if strings.HasPrefix(line, `- "chart": "elasticsearch-1.7.0"`) {
showsChanged++
}
}
assert.Equal(t, 1, showsMissing)
assert.Equal(t, 1, showsExtra)
assert.Equal(t, 2, showsChanged)
}
func TestThreeWayDiffExplicitNamespace(t *testing.T) {
configUn := unmarshalFile("testdata/spinnaker-sa-config.json")
liveUn := unmarshalFile("testdata/spinnaker-sa-live.json")
dr := Diff(configUn, liveUn, nil)
assert.False(t, dr.Modified)
ascii, err := dr.ASCIIFormat(liveUn, formatOpts)
assert.Nil(t, err)
log.Println(ascii)
}
func TestRemoveNamespaceAnnotation(t *testing.T) {
obj := removeNamespaceAnnotation(&unstructured.Unstructured{Object: map[string]interface{}{
"metadata": map[string]interface{}{
"name": "test",
"namespace": "default",
},
}})
assert.Equal(t, "", obj.GetNamespace())
obj = removeNamespaceAnnotation(&unstructured.Unstructured{Object: map[string]interface{}{
"metadata": map[string]interface{}{
"name": "test",
"namespace": "default",
"annotations": make(map[string]interface{}),
},
}})
assert.Equal(t, "", obj.GetNamespace())
assert.Nil(t, obj.GetAnnotations())
}
const customObjConfig = `
apiVersion: foo.io/v1
kind: Foo
metadata:
name: my-foo
namespace: kube-system
spec:
foo: bar
`
const customObjLive = `
apiVersion: foo.io/v1
kind: Foo
metadata:
creationTimestamp: 2018-07-17 09:17:05 UTC
name: my-foo
resourceVersion: '10308211'
selfLink: "/apis/rbac.authorization.k8s.io/v1/clusterroles/argocd-manager-role"
uid: 2c3d5405-89a2-11e8-aff0-42010a8a0fc6
spec:
foo: bar
`
func TestIgnoreNamespaceForClusterScopedResources(t *testing.T) {
var configUn unstructured.Unstructured
var liveUn unstructured.Unstructured
err := yaml.Unmarshal([]byte(customObjLive), &liveUn)
assert.Nil(t, err)
err = yaml.Unmarshal([]byte(customObjConfig), &configUn)
assert.Nil(t, err)
dr := Diff(&configUn, &liveUn, nil)
assert.False(t, dr.Modified)
}
const secretConfig = `
apiVersion: v1
kind: Secret
metadata:
name: my-secret
type: Opaque
stringData:
foo: bar
bar: "1234"
data:
baz: cXV4
`
const secretLive = `
apiVersion: v1
kind: Secret
metadata:
creationTimestamp: 2018-11-19T11:30:40Z
name: my-secret
namespace: argocd
resourceVersion: "25848035"
selfLink: /api/v1/namespaces/argocd/secrets/my-secret
uid: 8b4a2766-ebee-11e8-93c0-42010a8a0013
type: Opaque
data:
foo: YmFy
bar: MTIzNA==
baz: cXV4
`
func TestSecretStringData(t *testing.T) {
var err error
var configUn unstructured.Unstructured
err = yaml.Unmarshal([]byte(secretConfig), &configUn)
assert.Nil(t, err)
var liveUn unstructured.Unstructured
err = yaml.Unmarshal([]byte(secretLive), &liveUn)
assert.Nil(t, err)
dr := Diff(&configUn, &liveUn, nil)
if !assert.False(t, dr.Modified) {
ascii, err := dr.ASCIIFormat(&liveUn, formatOpts)
assert.Nil(t, err)
log.Println(ascii)
}
}
// This is invalid because foo is a number, not a string
const secretInvalidConfig = `
apiVersion: v1
kind: Secret
metadata:
name: my-secret
type: Opaque
stringData:
foo: 1234
`
const secretInvalidLive = `
apiVersion: v1
kind: Secret
metadata:
creationTimestamp: 2018-11-19T11:30:40Z
name: my-secret
namespace: argocd
resourceVersion: "25848035"
selfLink: /api/v1/namespaces/argocd/secrets/my-secret
uid: 8b4a2766-ebee-11e8-93c0-42010a8a0013
type: Opaque
data:
foo: MTIzNA==
`
func TestInvalidSecretStringData(t *testing.T) {
var err error
var configUn unstructured.Unstructured
err = yaml.Unmarshal([]byte(secretInvalidConfig), &configUn)
assert.Nil(t, err)
var liveUn unstructured.Unstructured
err = yaml.Unmarshal([]byte(secretInvalidLive), &liveUn)
assert.Nil(t, err)
dr := Diff(&configUn, nil, nil)
assert.True(t, dr.Modified)
}
func TestNullSecretData(t *testing.T) {
configUn := unmarshalFile("testdata/wordpress-config.json")
liveUn := unmarshalFile("testdata/wordpress-live.json")
dr := Diff(configUn, liveUn, nil)
if !assert.False(t, dr.Modified) {
ascii, err := dr.ASCIIFormat(liveUn, formatOpts)
assert.Nil(t, err)
log.Println(ascii)
}
}
// TestRedactedSecretData tests we are able to perform diff on redacted secret data, which has
// invalid characters (*) for the the data byte array field.
func TestRedactedSecretData(t *testing.T) {
configUn := unmarshalFile("testdata/wordpress-config.json")
liveUn := unmarshalFile("testdata/wordpress-live.json")
configData := configUn.Object["data"].(map[string]interface{})
liveData := liveUn.Object["data"].(map[string]interface{})
configData["wordpress-password"] = "***"
configData["smtp-password"] = "***"
liveData["wordpress-password"] = "******"
liveData["smtp-password"] = "******"
dr := Diff(configUn, liveUn, nil)
if !assert.True(t, dr.Modified) {
ascii, err := dr.ASCIIFormat(liveUn, formatOpts)
assert.Nil(t, err)
log.Println(ascii)
}
}
func TestNullRoleRule(t *testing.T) {
configUn := unmarshalFile("testdata/grafana-clusterrole-config.json")
liveUn := unmarshalFile("testdata/grafana-clusterrole-live.json")
dr := Diff(configUn, liveUn, nil)
if !assert.False(t, dr.Modified) {
ascii, err := dr.ASCIIFormat(liveUn, formatOpts)
assert.Nil(t, err)
log.Println(ascii)
}
}
func TestNullCreationTimestamp(t *testing.T) {
configUn := unmarshalFile("testdata/sealedsecret-config.json")
liveUn := unmarshalFile("testdata/sealedsecret-live.json")
dr := Diff(configUn, liveUn, nil)
if !assert.False(t, dr.Modified) {
ascii, err := dr.ASCIIFormat(liveUn, formatOpts)
assert.Nil(t, err)
log.Println(ascii)
}
}
func createSecret(data map[string]string) *unstructured.Unstructured {
secret := corev1.Secret{TypeMeta: metav1.TypeMeta{Kind: "Secret"}}
if data != nil {
secret.Data = make(map[string][]byte)
for k, v := range data {
secret.Data[k] = []byte(v)
}
}
return mustToUnstructured(&secret)
}
func secretData(obj *unstructured.Unstructured) map[string]interface{} {
data, _, _ := unstructured.NestedMap(obj.Object, "data")
return data
}
const (
replacement1 = "*********"
replacement2 = "**********"
replacement3 = "***********"
)
func TestHideSecretDataSameKeysDifferentValues(t *testing.T) {
target, live, err := HideSecretData(
createSecret(map[string]string{"key1": "test", "key2": "test"}),
createSecret(map[string]string{"key1": "test-1", "key2": "test-1"}))
assert.Nil(t, err)
assert.Equal(t, map[string]interface{}{"key1": replacement1, "key2": replacement1}, secretData(target))
assert.Equal(t, map[string]interface{}{"key1": replacement2, "key2": replacement2}, secretData(live))
}
func TestHideSecretDataSameKeysSameValues(t *testing.T) {
target, live, err := HideSecretData(
createSecret(map[string]string{"key1": "test", "key2": "test"}),
createSecret(map[string]string{"key1": "test", "key2": "test"}))
assert.Nil(t, err)
assert.Equal(t, map[string]interface{}{"key1": replacement1, "key2": replacement1}, secretData(target))
assert.Equal(t, map[string]interface{}{"key1": replacement1, "key2": replacement1}, secretData(live))
}
func TestHideSecretDataDifferentKeysDifferentValues(t *testing.T) {
target, live, err := HideSecretData(
createSecret(map[string]string{"key1": "test", "key2": "test"}),
createSecret(map[string]string{"key2": "test-1", "key3": "test-1"}))
assert.Nil(t, err)
assert.Equal(t, map[string]interface{}{"key1": replacement1, "key2": replacement1}, secretData(target))
assert.Equal(t, map[string]interface{}{"key2": replacement2, "key3": replacement1}, secretData(live))
}
func TestHideSecretDataLastAppliedConfig(t *testing.T) {
lastAppliedSecret := createSecret(map[string]string{"key1": "test1"})
targetSecret := createSecret(map[string]string{"key1": "test2"})
liveSecret := createSecret(map[string]string{"key1": "test3"})
lastAppliedStr, err := json.Marshal(lastAppliedSecret)
assert.Nil(t, err)
liveSecret.SetAnnotations(map[string]string{corev1.LastAppliedConfigAnnotation: string(lastAppliedStr)})
target, live, err := HideSecretData(targetSecret, liveSecret)
assert.Nil(t, err)
err = json.Unmarshal([]byte(live.GetAnnotations()[corev1.LastAppliedConfigAnnotation]), &lastAppliedSecret)
assert.Nil(t, err)
assert.Equal(t, map[string]interface{}{"key1": replacement1}, secretData(target))
assert.Equal(t, map[string]interface{}{"key1": replacement2}, secretData(live))
assert.Equal(t, map[string]interface{}{"key1": replacement3}, secretData(lastAppliedSecret))
}