argo-cd/pkg/diff/diff_test.go
Matthieu MOREL ccee58366a
chore: enable unused-parameter and var-declaration from revive (#668)
Signed-off-by: Matthieu MOREL <matthieu.morel35@gmail.com>
2025-02-07 09:55:39 -05:00

1546 lines
45 KiB
Go

package diff
import (
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"github.com/argoproj/gitops-engine/pkg/diff/mocks"
"github.com/argoproj/gitops-engine/pkg/diff/testdata"
openapi_v2 "github.com/google/gnostic-models/openapiv2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/proto"
appsv1 "k8s.io/api/apps/v1"
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"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/apimachinery/pkg/util/managedfields"
"k8s.io/klog/v2/textlogger"
openapiproto "k8s.io/kube-openapi/pkg/util/proto"
"sigs.k8s.io/yaml"
)
func printDiff(result *DiffResult) (string, error) {
var live unstructured.Unstructured
if err := json.Unmarshal(result.NormalizedLive, &live); err != nil {
return "", err
}
var target unstructured.Unstructured
if err := json.Unmarshal(result.PredictedLive, &target); err != nil {
return "", err
}
out, _ := printDiffInternal("diff", &live, &target)
return string(out), nil
}
// printDiffInternal prints a diff between two unstructured objects using an external diff utility and returns the output.
func printDiffInternal(name string, live *unstructured.Unstructured, target *unstructured.Unstructured) ([]byte, error) {
tempDir, err := os.MkdirTemp("", "argocd-diff")
if err != nil {
return nil, err
}
targetFile := filepath.Join(tempDir, name)
var targetData []byte
if target != nil {
targetData, err = yaml.Marshal(target)
if err != nil {
return nil, err
}
}
err = os.WriteFile(targetFile, targetData, 0o644)
if err != nil {
return nil, err
}
liveFile := filepath.Join(tempDir, fmt.Sprintf("%s-live.yaml", name))
liveData := []byte("")
if live != nil {
liveData, err = yaml.Marshal(live)
if err != nil {
return nil, err
}
}
err = os.WriteFile(liveFile, liveData, 0o644)
if err != nil {
return nil, err
}
cmd := exec.Command("diff", liveFile, targetFile)
return cmd.Output()
}
func toUnstructured(obj any) (*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 any) *unstructured.Unstructured {
un, err := toUnstructured(obj)
if err != nil {
panic(err)
}
return un
}
func unmarshalFile(path string) *unstructured.Unstructured {
data, err := os.ReadFile(path)
if err != nil {
panic(err)
}
var un unstructured.Unstructured
err = json.Unmarshal(data, &un.Object)
if err != nil {
panic(err)
}
return &un
}
func newDeployment() *appsv1.Deployment {
var two int32 = 2
return &appsv1.Deployment{
TypeMeta: metav1.TypeMeta{
APIVersion: "apps/v1beta1",
Kind: "Deployment",
},
ObjectMeta: metav1.ObjectMeta{
Name: "demo",
Namespace: "test",
},
Spec: appsv1.DeploymentSpec{
Replicas: &two,
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"app": "demo",
},
},
Template: v1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"app": "demo",
},
},
Spec: v1.PodSpec{
Containers: []v1.Container{
{
Name: "demo",
Image: "gcr.io/kuar-demo/kuard-amd64:1",
Ports: []v1.ContainerPort{
{
ContainerPort: 80,
},
},
},
},
},
},
},
}
}
func diff(t *testing.T, config, live *unstructured.Unstructured, options ...Option) *DiffResult {
res, err := Diff(config, live, options...)
assert.NoError(t, err)
return res
}
func TestDiff(t *testing.T) {
leftDep := newDeployment()
leftUn := mustToUnstructured(leftDep)
diffRes := diff(t, leftUn, leftUn, diffOptionsForTest()...)
assert.False(t, diffRes.Modified)
ascii, err := printDiff(diffRes)
require.NoError(t, err)
if ascii != "" {
t.Log(ascii)
}
}
func TestDiff_KnownTypeInvalidValue(t *testing.T) {
leftDep := newDeployment()
leftUn := mustToUnstructured(leftDep)
if !assert.NoError(t, unstructured.SetNestedField(leftUn.Object, "badValue", "spec", "revisionHistoryLimit")) {
return
}
t.Run("NoDifference", func(t *testing.T) {
diffRes := diff(t, leftUn, leftUn, diffOptionsForTest()...)
assert.False(t, diffRes.Modified)
ascii, err := printDiff(diffRes)
require.NoError(t, err)
if ascii != "" {
t.Log(ascii)
}
})
t.Run("HasDifference", func(t *testing.T) {
rightUn := leftUn.DeepCopy()
if !assert.NoError(t, unstructured.SetNestedField(rightUn.Object, "3", "spec", "revisionHistoryLimit")) {
return
}
diffRes := diff(t, leftUn, rightUn, diffOptionsForTest()...)
assert.True(t, diffRes.Modified)
})
}
func TestDiffWithNils(t *testing.T) {
dep := newDeployment()
resource := mustToUnstructured(dep)
diffRes := diff(t, nil, resource, diffOptionsForTest()...)
// 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.Modified)
diffRes, err := TwoWayDiff(nil, resource)
assert.NoError(t, err)
assert.False(t, diffRes.Modified)
diffRes = diff(t, resource, nil, diffOptionsForTest()...)
assert.True(t, diffRes.Modified)
diffRes, err = TwoWayDiff(resource, nil)
assert.NoError(t, err)
assert.True(t, diffRes.Modified)
}
func TestDiffNilFieldInLive(t *testing.T) {
leftDep := newDeployment()
rightDep := leftDep.DeepCopy()
leftUn := mustToUnstructured(leftDep)
rightUn := mustToUnstructured(rightDep)
err := unstructured.SetNestedField(rightUn.Object, nil, "spec")
require.NoError(t, err)
diffRes := diff(t, leftUn, rightUn, diffOptionsForTest()...)
assert.True(t, diffRes.Modified)
}
func TestDiffArraySame(t *testing.T) {
leftDep := newDeployment()
rightDep := leftDep.DeepCopy()
leftUn := mustToUnstructured(leftDep)
rightUn := mustToUnstructured(rightDep)
left := []*unstructured.Unstructured{leftUn}
right := []*unstructured.Unstructured{rightUn}
diffResList, err := DiffArray(left, right, diffOptionsForTest()...)
require.NoError(t, err)
assert.False(t, diffResList.Modified)
}
func TestDiffArrayAdditions(t *testing.T) {
leftDep := newDeployment()
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, diffOptionsForTest()...)
require.NoError(t, err)
assert.False(t, diffResList.Modified)
}
func TestDiffArrayModification(t *testing.T) {
leftDep := newDeployment()
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, diffOptionsForTest()...)
require.NoError(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 := newDeployment()
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(t, configUn, liveUn, diffOptionsForTest()...)
if !assert.False(t, res.Modified) {
ascii, err := printDiff(res)
require.NoError(t, err)
t.Log(ascii)
}
// 3. Add a last-applied-configuration annotation in the live. There should still not be any
// difference
configBytes, err := json.Marshal(configDep)
require.NoError(t, err)
liveDep.Annotations[v1.LastAppliedConfigAnnotation] = string(configBytes)
configUn = mustToUnstructured(configDep)
liveUn = mustToUnstructured(liveDep)
res = diff(t, configUn, liveUn, diffOptionsForTest()...)
if !assert.False(t, res.Modified) {
ascii, err := printDiff(res)
require.NoError(t, err)
t.Log(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(t, configUn, liveUn, diffOptionsForTest()...)
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(t, configUn, liveUn, diffOptionsForTest()...)
ascii, err := printDiff(res)
require.NoError(t, err)
if ascii != "" {
t.Log(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)
require.NoError(t, err)
err = json.Unmarshal([]byte(demoLive), &liveUn.Object)
require.NoError(t, err)
dr := diff(t, &configUn, &liveUn, diffOptionsForTest()...)
assert.False(t, dr.Modified)
ascii, err := printDiff(dr)
require.NoError(t, err)
if ascii != "" {
t.Log(ascii)
}
}
// Test for ignoring aggregated cluster roles
func TestDiffOptionIgnoreAggregateRoles(t *testing.T) {
// Test case 1: Ignore option is true, the rules in the role should be ignored
{
configUn := unmarshalFile("testdata/aggr-clusterrole-config.json")
liveUn := unmarshalFile("testdata/aggr-clusterrole-live.json")
dr := diff(t, configUn, liveUn, IgnoreAggregatedRoles(true))
assert.False(t, dr.Modified)
ascii, err := printDiff(dr)
require.NoError(t, err)
t.Log(ascii)
}
// Test case 2: Ignore option is false, the aggregation should produce a diff
{
configUn := unmarshalFile("testdata/aggr-clusterrole-config.json")
liveUn := unmarshalFile("testdata/aggr-clusterrole-live.json")
dr := diff(t, configUn, liveUn, IgnoreAggregatedRoles(false))
assert.True(t, dr.Modified)
ascii, err := printDiff(dr)
require.NoError(t, err)
t.Log(ascii)
}
}
func TestThreeWayDiffExample2(t *testing.T) {
configUn := unmarshalFile("testdata/elasticsearch-config.json")
liveUn := unmarshalFile("testdata/elasticsearch-live.json")
dr := diff(t, configUn, liveUn, diffOptionsForTest()...)
assert.False(t, dr.Modified)
ascii, err := printDiff(dr)
require.NoError(t, err)
t.Log(ascii)
}
// Tests a real world example
func TestThreeWayDiffExample3(t *testing.T) {
configUn := unmarshalFile("testdata/deployment-config.json")
liveUn := unmarshalFile("testdata/deployment-live.json")
dr := diff(t, configUn, liveUn, diffOptionsForTest()...)
assert.False(t, dr.Modified)
ascii, err := printDiff(dr)
require.NoError(t, err)
if ascii != "" {
t.Log(ascii)
}
}
func TestThreeWayDiffExample4(t *testing.T) {
configUn := unmarshalFile("testdata/mutatingwebhookconfig-config.json")
liveUn := unmarshalFile("testdata/mutatingwebhookconfig-live.json")
dr := diff(t, configUn, liveUn, diffOptionsForTest()...)
assert.False(t, dr.Modified)
ascii, err := printDiff(dr)
require.NoError(t, err)
if ascii != "" {
t.Log(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(t, configUn, liveUn, diffOptionsForTest()...)
assert.True(t, dr.Modified)
ascii, err := printDiff(dr)
require.NoError(t, err, ascii)
t.Log(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(t, configUn, liveUn, diffOptionsForTest()...)
assert.False(t, dr.Modified)
ascii, err := printDiff(dr)
require.NoError(t, err)
t.Log(ascii)
}
func TestDiffResourceWithInvalidField(t *testing.T) {
// Diff(...) should not silently discard invalid fields (fields that are not present in the underlying k8s resource).
leftDep := `{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": {
"name": "invalid-cm"
},
"invalidKey": "asdf"
}`
var leftUn unstructured.Unstructured
err := json.Unmarshal([]byte(leftDep), &leftUn.Object)
if err != nil {
panic(err)
}
rightUn := leftUn.DeepCopy()
unstructured.RemoveNestedField(rightUn.Object, "invalidKey")
diffRes := diff(t, &leftUn, rightUn, diffOptionsForTest()...)
assert.True(t, diffRes.Modified)
ascii, err := printDiff(diffRes)
assert.NoError(t, err)
assert.Contains(t, ascii, "invalidKey")
if ascii != "" {
t.Log(ascii)
}
}
func TestRemoveNamespaceAnnotation(t *testing.T) {
obj := removeNamespaceAnnotation(&unstructured.Unstructured{Object: map[string]any{
"metadata": map[string]any{
"name": "test",
"namespace": "default",
},
}})
assert.Equal(t, "", obj.GetNamespace())
obj = removeNamespaceAnnotation(&unstructured.Unstructured{Object: map[string]any{
"metadata": map[string]any{
"name": "test",
"namespace": "default",
"annotations": make(map[string]any),
},
}})
assert.Equal(t, "", obj.GetNamespace())
assert.Nil(t, obj.GetAnnotations())
obj = removeNamespaceAnnotation(&unstructured.Unstructured{Object: map[string]any{
"metadata": map[string]any{
"name": "test",
"namespace": "default",
"annotations": "wrong value",
},
}})
assert.Equal(t, "", obj.GetNamespace())
val, _, _ := unstructured.NestedString(obj.Object, "metadata", "annotations")
assert.Equal(t, "wrong value", val)
}
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)
require.NoError(t, err)
err = yaml.Unmarshal([]byte(customObjConfig), &configUn)
require.NoError(t, err)
dr := diff(t, &configUn, &liveUn, diffOptionsForTest()...)
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)
require.NoError(t, err)
var liveUn unstructured.Unstructured
err = yaml.Unmarshal([]byte(secretLive), &liveUn)
require.NoError(t, err)
dr := diff(t, &configUn, &liveUn, diffOptionsForTest()...)
if !assert.False(t, dr.Modified) {
ascii, err := printDiff(dr)
require.NoError(t, err)
t.Log(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)
require.NoError(t, err)
var liveUn unstructured.Unstructured
err = yaml.Unmarshal([]byte(secretInvalidLive), &liveUn)
require.NoError(t, err)
dr := diff(t, &configUn, nil, diffOptionsForTest()...)
assert.True(t, dr.Modified)
}
func TestNullSecretData(t *testing.T) {
configUn := unmarshalFile("testdata/wordpress-config.json")
liveUn := unmarshalFile("testdata/wordpress-live.json")
dr := diff(t, configUn, liveUn, diffOptionsForTest()...)
if !assert.False(t, dr.Modified) {
ascii, err := printDiff(dr)
require.NoError(t, err)
t.Log(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]any)
liveData := liveUn.Object["data"].(map[string]any)
configData["wordpress-password"] = "++++++++"
configData["smtp-password"] = "++++++++"
liveData["wordpress-password"] = "++++++++++++"
liveData["smtp-password"] = "++++++++++++"
dr := diff(t, configUn, liveUn, diffOptionsForTest()...)
if !assert.True(t, dr.Modified) {
ascii, err := printDiff(dr)
require.NoError(t, err)
t.Log(ascii)
}
}
func TestNullRoleRule(t *testing.T) {
configUn := unmarshalFile("testdata/grafana-clusterrole-config.json")
liveUn := unmarshalFile("testdata/grafana-clusterrole-live.json")
dr := diff(t, configUn, liveUn, diffOptionsForTest()...)
if !assert.False(t, dr.Modified) {
ascii, err := printDiff(dr)
require.NoError(t, err)
t.Log(ascii)
}
}
func TestNullCreationTimestamp(t *testing.T) {
configUn := unmarshalFile("testdata/sealedsecret-config.json")
liveUn := unmarshalFile("testdata/sealedsecret-live.json")
dr := diff(t, configUn, liveUn, diffOptionsForTest()...)
if !assert.False(t, dr.Modified) {
ascii, err := printDiff(dr)
require.NoError(t, err)
t.Log(ascii)
}
}
func TestUnsortedEndpoints(t *testing.T) {
configUn := unmarshalFile("testdata/endpoints-config.json")
liveUn := unmarshalFile("testdata/endpoints-live.json")
dr := diff(t, configUn, liveUn, diffOptionsForTest()...)
if !assert.False(t, dr.Modified) {
ascii, err := printDiff(dr)
require.NoError(t, err)
t.Log(ascii)
}
}
func buildGVKParser(t *testing.T) *managedfields.GvkParser {
document := &openapi_v2.Document{}
err := proto.Unmarshal(testdata.OpenAPIV2Doc, document)
if err != nil {
t.Fatalf("error unmarshaling openapi doc: %s", err)
}
models, err := openapiproto.NewOpenAPIData(document)
if err != nil {
t.Fatalf("error building openapi data: %s", err)
}
gvkParser, err := managedfields.NewGVKParser(models, false)
if err != nil {
t.Fatalf("error building gvkParser: %s", err)
}
return gvkParser
}
func TestStructuredMergeDiff(t *testing.T) {
buildParams := func(live, config *unstructured.Unstructured) *SMDParams {
gvkParser := buildGVKParser(t)
manager := "argocd-controller"
return &SMDParams{
config: config,
live: live,
gvkParser: gvkParser,
manager: manager,
}
}
t.Run("will apply default values", func(t *testing.T) {
// given
t.Parallel()
liveState := StrToUnstructured(testdata.ServiceLiveYAML)
desiredState := StrToUnstructured(testdata.ServiceConfigYAML)
params := buildParams(liveState, desiredState)
// when
result, err := structuredMergeDiff(params)
// then
require.NoError(t, err)
assert.NotNil(t, result)
assert.True(t, result.Modified)
predictedSVC := YamlToSvc(t, result.PredictedLive)
liveSVC := YamlToSvc(t, result.NormalizedLive)
require.NotNil(t, predictedSVC.Spec.InternalTrafficPolicy)
require.NotNil(t, liveSVC.Spec.InternalTrafficPolicy)
assert.Equal(t, "Cluster", string(*predictedSVC.Spec.InternalTrafficPolicy))
assert.Equal(t, "Cluster", string(*liveSVC.Spec.InternalTrafficPolicy))
assert.Empty(t, predictedSVC.Annotations[AnnotationLastAppliedConfig])
assert.Empty(t, liveSVC.Annotations[AnnotationLastAppliedConfig])
})
t.Run("will remove entries in list", func(t *testing.T) {
// given
t.Parallel()
liveState := StrToUnstructured(testdata.ServiceLiveYAML)
desiredState := StrToUnstructured(testdata.ServiceConfigWith2Ports)
params := buildParams(liveState, desiredState)
// when
result, err := structuredMergeDiff(params)
// then
require.NoError(t, err)
assert.NotNil(t, result)
assert.True(t, result.Modified)
svc := YamlToSvc(t, result.PredictedLive)
assert.Len(t, svc.Spec.Ports, 2)
})
t.Run("will remove previously added fields not present in desired state", func(t *testing.T) {
// given
t.Parallel()
liveState := StrToUnstructured(testdata.LiveServiceWithTypeYAML)
desiredState := StrToUnstructured(testdata.ServiceConfigYAML)
params := buildParams(liveState, desiredState)
// when
result, err := structuredMergeDiff(params)
// then
require.NoError(t, err)
assert.NotNil(t, result)
assert.True(t, result.Modified)
svc := YamlToSvc(t, result.PredictedLive)
assert.Equal(t, corev1.ServiceTypeClusterIP, svc.Spec.Type)
})
t.Run("will apply service with multiple ports", func(t *testing.T) {
// given
t.Parallel()
liveState := StrToUnstructured(testdata.ServiceLiveYAML)
desiredState := StrToUnstructured(testdata.ServiceConfigWithSamePortsYAML)
params := buildParams(liveState, desiredState)
// when
result, err := structuredMergeDiff(params)
// then
require.NoError(t, err)
assert.NotNil(t, result)
assert.True(t, result.Modified)
svc := YamlToSvc(t, result.PredictedLive)
assert.Len(t, svc.Spec.Ports, 5)
})
t.Run("will apply deployment defaults correctly", func(t *testing.T) {
// given
t.Parallel()
liveState := StrToUnstructured(testdata.DeploymentLiveYAML)
desiredState := StrToUnstructured(testdata.DeploymentConfigYAML)
params := buildParams(liveState, desiredState)
// when
result, err := structuredMergeDiff(params)
// then
require.NoError(t, err)
assert.NotNil(t, result)
assert.False(t, result.Modified)
deploy := YamlToDeploy(t, result.PredictedLive)
assert.Len(t, deploy.Spec.Template.Spec.Containers, 1)
assert.Equal(t, "0", deploy.Spec.Template.Spec.Containers[0].Resources.Requests.Cpu().String())
assert.Equal(t, "0", deploy.Spec.Template.Spec.Containers[0].Resources.Requests.Memory().String())
assert.Equal(t, "0", deploy.Spec.Template.Spec.Containers[0].Resources.Requests.Storage().String())
assert.Equal(t, "0", deploy.Spec.Template.Spec.Containers[0].Resources.Limits.Cpu().String())
assert.Equal(t, "0", deploy.Spec.Template.Spec.Containers[0].Resources.Limits.Memory().String())
assert.Equal(t, "0", deploy.Spec.Template.Spec.Containers[0].Resources.Limits.Storage().String())
require.NotNil(t, deploy.Spec.Strategy.RollingUpdate)
expectedMaxSurge := &intstr.IntOrString{
Type: intstr.String,
StrVal: "25%",
}
assert.Equal(t, expectedMaxSurge, deploy.Spec.Strategy.RollingUpdate.MaxSurge)
assert.Equal(t, "ClusterFirst", string(deploy.Spec.Template.Spec.DNSPolicy))
})
}
func TestServerSideDiff(t *testing.T) {
buildOpts := func(predictedLive string) []Option {
gvkParser := buildGVKParser(t)
manager := "argocd-controller"
dryRunner := mocks.NewServerSideDryRunner(t)
dryRunner.On("Run", mock.Anything, mock.AnythingOfType("*unstructured.Unstructured"), manager).
Return(func(_ context.Context, _ *unstructured.Unstructured, _ string) (string, error) {
return predictedLive, nil
})
opts := []Option{
WithGVKParser(gvkParser),
WithManager(manager),
WithServerSideDryRunner(dryRunner),
}
return opts
}
t.Run("will ignore modifications done by mutation webhook by default", func(t *testing.T) {
// given
t.Parallel()
liveState := StrToUnstructured(testdata.ServiceLiveYAMLSSD)
desiredState := StrToUnstructured(testdata.ServiceConfigYAMLSSD)
opts := buildOpts(testdata.ServicePredictedLiveJSONSSD)
// when
result, err := serverSideDiff(desiredState, liveState, opts...)
// then
require.NoError(t, err)
assert.NotNil(t, result)
assert.True(t, result.Modified)
predictedSVC := YamlToSvc(t, result.PredictedLive)
liveSVC := YamlToSvc(t, result.NormalizedLive)
require.NotNil(t, predictedSVC.Spec.InternalTrafficPolicy)
require.NotNil(t, liveSVC.Spec.InternalTrafficPolicy)
assert.Equal(t, "Cluster", string(*predictedSVC.Spec.InternalTrafficPolicy))
assert.Equal(t, "Cluster", string(*liveSVC.Spec.InternalTrafficPolicy))
assert.Empty(t, predictedSVC.Annotations[AnnotationLastAppliedConfig])
assert.Empty(t, liveSVC.Annotations[AnnotationLastAppliedConfig])
assert.Empty(t, predictedSVC.Labels["event"])
})
t.Run("will test removing some field with undoing changes done by webhook", func(t *testing.T) {
// given
t.Parallel()
liveState := StrToUnstructured(testdata.Deployment2LiveYAML)
desiredState := StrToUnstructured(testdata.Deployment2ConfigYAML)
opts := buildOpts(testdata.Deployment2PredictedLiveJSONSSD)
// when
result, err := serverSideDiff(desiredState, liveState, opts...)
// then
require.NoError(t, err)
assert.NotNil(t, result)
assert.True(t, result.Modified)
predictedDeploy := YamlToDeploy(t, result.PredictedLive)
liveDeploy := YamlToDeploy(t, result.NormalizedLive)
assert.Len(t, predictedDeploy.Spec.Template.Spec.Containers, 1)
assert.Len(t, liveDeploy.Spec.Template.Spec.Containers, 1)
assert.Equal(t, "500m", predictedDeploy.Spec.Template.Spec.Containers[0].Resources.Requests.Cpu().String())
assert.Equal(t, "512Mi", predictedDeploy.Spec.Template.Spec.Containers[0].Resources.Requests.Memory().String())
assert.Equal(t, "500m", liveDeploy.Spec.Template.Spec.Containers[0].Resources.Requests.Cpu().String())
assert.Equal(t, "512Mi", liveDeploy.Spec.Template.Spec.Containers[0].Resources.Requests.Memory().String())
})
t.Run("will include mutation webhook modifications", func(t *testing.T) {
// given
t.Parallel()
liveState := StrToUnstructured(testdata.ServiceLiveYAMLSSD)
desiredState := StrToUnstructured(testdata.ServiceConfigYAMLSSD)
opts := buildOpts(testdata.ServicePredictedLiveJSONSSD)
opts = append(opts, WithIgnoreMutationWebhook(false))
// when
result, err := serverSideDiff(desiredState, liveState, opts...)
// then
require.NoError(t, err)
assert.NotNil(t, result)
assert.True(t, result.Modified)
predictedSVC := YamlToSvc(t, result.PredictedLive)
liveSVC := YamlToSvc(t, result.NormalizedLive)
require.NotNil(t, predictedSVC.Spec.InternalTrafficPolicy)
require.NotNil(t, liveSVC.Spec.InternalTrafficPolicy)
assert.Equal(t, "Cluster", string(*predictedSVC.Spec.InternalTrafficPolicy))
assert.Equal(t, "Cluster", string(*liveSVC.Spec.InternalTrafficPolicy))
assert.Empty(t, predictedSVC.Annotations[AnnotationLastAppliedConfig])
assert.Empty(t, liveSVC.Annotations[AnnotationLastAppliedConfig])
assert.NotEmpty(t, predictedSVC.Labels["event"])
})
}
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]any {
data, _, _ := unstructured.NestedMap(obj.Object, "data")
return data
}
var (
replacement1 = strings.Repeat("+", 8)
replacement2 = strings.Repeat("+", 12)
replacement3 = strings.Repeat("+", 16)
)
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"}),
nil,
)
require.NoError(t, err)
assert.Equal(t, map[string]any{"key1": replacement1, "key2": replacement1}, secretData(target))
assert.Equal(t, map[string]any{"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"}),
nil,
)
require.NoError(t, err)
assert.Equal(t, map[string]any{"key1": replacement1, "key2": replacement1}, secretData(target))
assert.Equal(t, map[string]any{"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"}),
nil,
)
require.NoError(t, err)
assert.Equal(t, map[string]any{"key1": replacement1, "key2": replacement1}, secretData(target))
assert.Equal(t, map[string]any{"key2": replacement2, "key3": replacement1}, secretData(live))
}
func TestHideStringDataInInvalidSecret(t *testing.T) {
liveUn := &unstructured.Unstructured{
Object: map[string]any{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]any{
"name": "test-secret",
},
"type": "Opaque",
"data": map[string]any{
"key1": "a2V5MQ==",
"key2": "a2V5MQ==",
},
},
}
targetUn := &unstructured.Unstructured{
Object: map[string]any{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]any{
"name": "test-secret",
},
"type": "Opaque",
"data": map[string]any{
"key1": "a2V5MQ==",
"key2": "a2V5Mg==",
"key3": false,
},
"stringData": map[string]any{
"key4": "key4",
"key5": 5,
},
},
}
liveUn = remarshal(liveUn, applyOptions(diffOptionsForTest()))
targetUn = remarshal(targetUn, applyOptions(diffOptionsForTest()))
target, live, err := HideSecretData(targetUn, liveUn, nil)
require.NoError(t, err)
assert.Equal(t, map[string]any{"key1": replacement1, "key2": replacement2}, secretData(live))
assert.Equal(t, map[string]any{"key1": replacement1, "key2": replacement1, "key3": replacement1, "key4": replacement1, "key5": replacement1}, secretData(target))
}
// stringData in secrets should be normalized even if it is invalid
func TestNormalizeSecret(t *testing.T) {
tests := []struct {
testname string
data map[string]any
stringData map[string]any
}{
{
testname: "Valid secret",
data: map[string]any{
"key1": "key1",
},
stringData: map[string]any{
"key2": "a2V5Mg==",
},
},
{
testname: "Invalid secret",
data: map[string]any{
"key1": "key1",
"key2": 2,
},
stringData: map[string]any{
"key3": "key3",
"key4": nil,
},
},
{
testname: "Invalid secret with stringData only",
data: nil,
stringData: map[string]any{
"key3": "key3",
"key4": true,
},
},
}
for _, tt := range tests {
t.Run(tt.testname, func(t *testing.T) {
un := &unstructured.Unstructured{
Object: map[string]any{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]any{
"name": "test-secret",
},
"type": "Opaque",
"data": tt.data,
"stringData": tt.stringData,
},
}
un = remarshal(un, applyOptions(diffOptionsForTest()))
NormalizeSecret(un)
_, found, _ := unstructured.NestedMap(un.Object, "stringData")
assert.False(t, found)
data, found, _ := unstructured.NestedMap(un.Object, "data")
assert.True(t, found)
// check all secret keys are found under data in normalized secret
for _, obj := range []map[string]any{tt.data, tt.stringData} {
if obj == nil {
continue
}
for k := range obj {
_, ok := data[k]
assert.True(t, ok)
}
}
})
}
}
func TestHideSecretAnnotations(t *testing.T) {
tests := []struct {
name string
hideAnnots map[string]bool
annots map[string]any
expectedAnnots map[string]any
targetNil bool
}{
{
name: "no hidden annotations",
hideAnnots: nil,
annots: map[string]any{"token/value": "secret", "key": "secret-key", "app": "test"},
expectedAnnots: map[string]any{"token/value": "secret", "key": "secret-key", "app": "test"},
},
{
name: "hide annotations",
hideAnnots: map[string]bool{"token/value": true, "key": true},
annots: map[string]any{"token/value": "secret", "key": "secret-key", "app": "test"},
expectedAnnots: map[string]any{"token/value": replacement1, "key": replacement1, "app": "test"},
},
{
name: "hide annotations in last-applied-config",
hideAnnots: map[string]bool{"token/value": true, "key": true},
annots: map[string]any{
"token/value": "secret",
"app": "test",
"kubectl.kubernetes.io/last-applied-configuration": `{"apiVersion":"v1","kind":"Secret","metadata":{"annotations":{"app":"test","token/value":"secret","key":"secret-key"},"labels":{"app.kubernetes.io/instance":"test"},"name":"my-secret","namespace":"default"},"type":"Opaque"}`,
},
expectedAnnots: map[string]any{
"token/value": replacement1,
"app": "test",
"kubectl.kubernetes.io/last-applied-configuration": `{"apiVersion":"v1","kind":"Secret","metadata":{"annotations":{"app":"test","key":"++++++++","token/value":"++++++++"},"labels":{"app.kubernetes.io/instance":"test"},"name":"my-secret","namespace":"default"},"type":"Opaque"}`,
},
targetNil: true,
},
{
name: "special case: hide last-applied-config annotation",
hideAnnots: map[string]bool{"kubectl.kubernetes.io/last-applied-configuration": true},
annots: map[string]any{
"token/value": replacement1,
"app": "test",
"kubectl.kubernetes.io/last-applied-configuration": `{"apiVersion":"v1","kind":"Secret","metadata":{"annotations":{"app":"test","token/value":"secret","key":"secret-key"},"labels":{"app.kubernetes.io/instance":"test"},"name":"my-secret","namespace":"default"},"type":"Opaque"}`,
},
expectedAnnots: map[string]any{
"app": "test",
"kubectl.kubernetes.io/last-applied-configuration": replacement1,
},
targetNil: true,
},
{
name: "hide annotations for malformed annotations",
hideAnnots: map[string]bool{"token/value": true, "key": true},
annots: map[string]any{"token/value": 0, "key": "secret", "app": true},
expectedAnnots: map[string]any{"token/value": replacement1, "key": replacement1, "app": true},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
unSecret := &unstructured.Unstructured{
Object: map[string]any{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]any{
"name": "test-secret",
"annotations": tt.annots,
},
"type": "Opaque",
},
}
liveUn := remarshal(unSecret, applyOptions(diffOptionsForTest()))
targetUn := remarshal(unSecret, applyOptions(diffOptionsForTest()))
if tt.targetNil {
targetUn = nil
}
target, live, err := HideSecretData(targetUn, liveUn, tt.hideAnnots)
require.NoError(t, err)
// verify configured annotations are hidden
for _, obj := range []*unstructured.Unstructured{target, live} {
if obj != nil {
annots, _, _ := unstructured.NestedMap(obj.Object, "metadata", "annotations")
for ek, ev := range tt.expectedAnnots {
v, found := annots[ek]
assert.True(t, found)
assert.Equal(t, ev, v)
}
}
}
})
}
}
func TestHideSecretAnnotationsPreserveDifference(t *testing.T) {
hideAnnots := map[string]bool{"token/value": true}
liveUn := &unstructured.Unstructured{
Object: map[string]any{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]any{
"name": "test-secret",
"annotations": map[string]any{"token/value": "secret", "app": "test"},
},
"type": "Opaque",
},
}
targetUn := &unstructured.Unstructured{
Object: map[string]any{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]any{
"name": "test-secret",
"annotations": map[string]any{"token/value": "new-secret", "app": "test"},
},
"type": "Opaque",
},
}
liveUn = remarshal(liveUn, applyOptions(diffOptionsForTest()))
targetUn = remarshal(targetUn, applyOptions(diffOptionsForTest()))
target, live, err := HideSecretData(targetUn, liveUn, hideAnnots)
require.NoError(t, err)
liveAnnots := live.GetAnnotations()
v, found := liveAnnots["token/value"]
assert.True(t, found)
assert.Equal(t, replacement2, v)
targetAnnots := target.GetAnnotations()
v, found = targetAnnots["token/value"]
assert.True(t, found)
assert.Equal(t, replacement1, v)
}
func getTargetSecretJsonBytes() []byte {
return []byte(`
{
"apiVersion": "v1",
"kind": "Secret",
"type": "kubernetes.io/service-account-token",
"metadata": {
"annotations": {
"kubernetes.io/service-account.name": "default"
},
"labels": {
"app.kubernetes.io/instance": "empty-secret"
},
"name": "an-empty-secret",
"namespace": "default"
},
"data": {}
}`)
}
func getLiveSecretJsonBytes() []byte {
return []byte(`
{
"kind": "Secret",
"apiVersion": "v1",
"type": "kubernetes.io/service-account-token",
"metadata": {
"annotations": {
"kubernetes.io/service-account.name": "default",
"kubernetes.io/service-account.uid": "78688180-d432-4ee8-939d-382b015a6b13"
},
"creationTimestamp": "2021-10-27T19:09:22Z",
"labels": {
"app.kubernetes.io/instance": "empty-secret"
},
"name": "an-empty-secret",
"namespace": "default",
"resourceVersion": "2329692",
"uid": "2e98590d-a699-4281-89d5-aa94dfc1d7d7"
},
"data": {
"namespace": "ZGVmYXVsdA==",
"token": "ZGVmYXVsdAcb=="
}
}`)
}
func bytesToUnstructured(t *testing.T, jsonBytes []byte) *unstructured.Unstructured {
t.Helper()
var jsonMap map[string]any
err := json.Unmarshal(jsonBytes, &jsonMap)
if err != nil {
t.Fatal(err)
}
return &unstructured.Unstructured{
Object: jsonMap,
}
}
func TestHideSecretDataHandleEmptySecret(t *testing.T) {
// given
targetSecret := bytesToUnstructured(t, getTargetSecretJsonBytes())
liveSecret := bytesToUnstructured(t, getLiveSecretJsonBytes())
// when
target, live, err := HideSecretData(targetSecret, liveSecret, nil)
// then
assert.NoError(t, err)
assert.NotNil(t, target)
assert.NotNil(t, live)
assert.Nil(t, target.Object["data"])
assert.Equal(t, map[string]any{"namespace": "++++++++", "token": "++++++++"}, 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)
require.NoError(t, err)
liveSecret.SetAnnotations(map[string]string{corev1.LastAppliedConfigAnnotation: string(lastAppliedStr)})
target, live, err := HideSecretData(targetSecret, liveSecret, nil)
require.NoError(t, err)
err = json.Unmarshal([]byte(live.GetAnnotations()[corev1.LastAppliedConfigAnnotation]), &lastAppliedSecret)
require.NoError(t, err)
assert.Equal(t, map[string]any{"key1": replacement1}, secretData(target))
assert.Equal(t, map[string]any{"key1": replacement2}, secretData(live))
assert.Equal(t, map[string]any{"key1": replacement3}, secretData(lastAppliedSecret))
}
func TestRemarshal(t *testing.T) {
manifest := []byte(`
apiVersion: v1
kind: ServiceAccount
imagePullSecrets: []
metadata:
name: my-sa
`)
var un unstructured.Unstructured
err := yaml.Unmarshal(manifest, &un)
assert.NoError(t, err)
newUn := remarshal(&un, applyOptions(diffOptionsForTest()))
_, ok := newUn.Object["imagePullSecrets"]
assert.False(t, ok)
metadata := newUn.Object["metadata"].(map[string]any)
_, ok = metadata["creationTimestamp"]
assert.False(t, ok)
}
func TestRemarshalResources(t *testing.T) {
getRequests := func(un *unstructured.Unstructured) map[string]any {
return un.Object["spec"].(map[string]any)["containers"].([]any)[0].(map[string]any)["resources"].(map[string]any)["requests"].(map[string]any)
}
setRequests := func(un *unstructured.Unstructured, requests map[string]any) {
un.Object["spec"].(map[string]any)["containers"].([]any)[0].(map[string]any)["resources"].(map[string]any)["requests"] = requests
}
manifest := []byte(`
apiVersion: v1
kind: Pod
metadata:
name: my-pod
spec:
containers:
- image: nginx:1.7.9
name: nginx
resources:
requests:
cpu: 0.2
`)
un := unstructured.Unstructured{}
err := yaml.Unmarshal(manifest, &un)
require.NoError(t, err)
testCases := []struct {
name string
cpu any
expectedCPU any
}{
{"from float", 0.2, "200m"},
{"from float64", float64(0.2), "200m"},
{"from string", "0.2", "200m"},
{"from invalid", "invalid", "invalid"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
setRequests(&un, map[string]any{"cpu": tc.cpu})
newUn := remarshal(&un, applyOptions(diffOptionsForTest()))
requestsAfter := getRequests(newUn)
assert.Equal(t, tc.expectedCPU, requestsAfter["cpu"])
})
}
}
func ExampleDiff() {
expectedResource := unstructured.Unstructured{}
if err := yaml.Unmarshal([]byte(`
apiVersion: v1
kind: Pod
metadata:
name: my-pod
spec:
containers:
- image: nginx:1.7.9
name: nginx
resources:
requests:
cpu: 0.2
`), &expectedResource); err != nil {
panic(err)
}
liveResource := unstructured.Unstructured{}
if err := yaml.Unmarshal([]byte(`
apiVersion: v1
kind: Pod
metadata:
name: my-pod-123
creationTimestamp: "2020-03-30T21:34:59Z"
labels:
pod-template-hash: 84bf9649fd
name: argo-cd-cli-84bf9649fd-tm59q
resourceVersion: "233081332"
uid: 9a5ae31a-eed2-4f82-81fe-833799c54f99
spec:
containers:
- image: nginx:1.7.9
name: nginx
resources:
requests:
cpu: 0.1
`), &liveResource); err != nil {
panic(err)
}
diff, err := Diff(&expectedResource, &liveResource, diffOptionsForTest()...)
if err != nil {
panic(err)
}
if diff.Modified {
fmt.Println("Resources are different")
}
}
func diffOptionsForTest() []Option {
return []Option{
WithLogr(textlogger.NewLogger(textlogger.NewConfig())),
IgnoreAggregatedRoles(false),
}
}
func YamlToSvc(t *testing.T, y []byte) *corev1.Service {
t.Helper()
svc := corev1.Service{}
err := yaml.Unmarshal(y, &svc)
if err != nil {
t.Fatalf("error unmarshaling service bytes: %s", err)
}
return &svc
}
func YamlToDeploy(t *testing.T, y []byte) *appsv1.Deployment {
t.Helper()
deploy := appsv1.Deployment{}
err := yaml.Unmarshal(y, &deploy)
if err != nil {
t.Fatalf("error unmarshaling deployment bytes: %s", err)
}
return &deploy
}
func StrToUnstructured(yamlStr string) *unstructured.Unstructured {
obj := make(map[string]any)
err := yaml.Unmarshal([]byte(yamlStr), &obj)
if err != nil {
panic(err)
}
return &unstructured.Unstructured{Object: obj}
}