cli-utils/pkg/object/graph/depends_test.go

1645 lines
47 KiB
Go

// Copyright 2020 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package graph
import (
"errors"
"testing"
"github.com/fluxcd/cli-utils/pkg/multierror"
"github.com/fluxcd/cli-utils/pkg/object"
"github.com/fluxcd/cli-utils/pkg/object/dependson"
"github.com/fluxcd/cli-utils/pkg/object/mutation"
mutationutil "github.com/fluxcd/cli-utils/pkg/object/mutation/testutil"
"github.com/fluxcd/cli-utils/pkg/object/validation"
"github.com/fluxcd/cli-utils/pkg/testutil"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
)
var (
resources = map[string]string{
"pod": `
kind: Pod
apiVersion: v1
metadata:
name: test-pod
namespace: test-namespace
`,
"default-pod": `
kind: Pod
apiVersion: v1
metadata:
name: pod-in-default-namespace
namespace: default
`,
"deployment": `
kind: Deployment
apiVersion: apps/v1
metadata:
name: foo
namespace: test-namespace
uid: dep-uid
generation: 1
spec:
replicas: 1
`,
"secret": `
kind: Secret
apiVersion: v1
metadata:
name: secret
namespace: test-namespace
uid: secret-uid
generation: 1
type: Opaque
spec:
foo: bar
`,
"namespace": `
kind: Namespace
apiVersion: v1
metadata:
name: test-namespace
`,
"crd": `
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: crontabs.stable.example.com
spec:
group: stable.example.com
versions:
- name: v1
served: true
storage: true
scope: Namespaced
names:
plural: crontabs
singular: crontab
kind: CronTab
`,
"crontab1": `
apiVersion: "stable.example.com/v1"
kind: CronTab
metadata:
name: cron-tab-01
namespace: test-namespace
`,
"crontab2": `
apiVersion: "stable.example.com/v1"
kind: CronTab
metadata:
name: cron-tab-02
namespace: test-namespace
`,
}
)
func TestSortObjs(t *testing.T) {
testCases := map[string]struct {
objs []*unstructured.Unstructured
expected []object.UnstructuredSet
isError bool
}{
"no objects returns no object sets": {
objs: []*unstructured.Unstructured{},
expected: []object.UnstructuredSet{},
isError: false,
},
"one object returns single object set": {
objs: []*unstructured.Unstructured{
testutil.Unstructured(t, resources["deployment"]),
},
expected: []object.UnstructuredSet{
{
testutil.Unstructured(t, resources["deployment"]),
},
},
isError: false,
},
"two unrelated objects returns single object set with two objs": {
objs: []*unstructured.Unstructured{
testutil.Unstructured(t, resources["deployment"]),
testutil.Unstructured(t, resources["secret"]),
},
expected: []object.UnstructuredSet{
{
testutil.Unstructured(t, resources["deployment"]),
testutil.Unstructured(t, resources["secret"]),
},
},
isError: false,
},
"one object depends on the other; two single object sets": {
objs: []*unstructured.Unstructured{
testutil.Unstructured(t, resources["deployment"],
testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))),
testutil.Unstructured(t, resources["secret"]),
},
expected: []object.UnstructuredSet{
{
testutil.Unstructured(t, resources["secret"]),
},
{
testutil.Unstructured(t, resources["deployment"]),
},
},
isError: false,
},
"three objects depend on another; three single object sets": {
objs: []*unstructured.Unstructured{
testutil.Unstructured(t, resources["deployment"],
testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))),
testutil.Unstructured(t, resources["secret"],
testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["pod"]))),
testutil.Unstructured(t, resources["pod"]),
},
expected: []object.UnstructuredSet{
{
testutil.Unstructured(t, resources["pod"]),
},
{
testutil.Unstructured(t, resources["secret"]),
},
{
testutil.Unstructured(t, resources["deployment"]),
},
},
isError: false,
},
"Two objects depend on secret; two object sets": {
objs: []*unstructured.Unstructured{
testutil.Unstructured(t, resources["deployment"],
testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))),
testutil.Unstructured(t, resources["pod"],
testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))),
testutil.Unstructured(t, resources["secret"]),
},
expected: []object.UnstructuredSet{
{
testutil.Unstructured(t, resources["secret"]),
},
{
testutil.Unstructured(t, resources["pod"]),
testutil.Unstructured(t, resources["deployment"]),
},
},
isError: false,
},
"two objects applied with their namespace; two object sets": {
objs: []*unstructured.Unstructured{
testutil.Unstructured(t, resources["deployment"]),
testutil.Unstructured(t, resources["namespace"]),
testutil.Unstructured(t, resources["secret"]),
},
expected: []object.UnstructuredSet{
{
testutil.Unstructured(t, resources["namespace"]),
},
{
testutil.Unstructured(t, resources["secret"]),
testutil.Unstructured(t, resources["deployment"]),
},
},
isError: false,
},
"two custom resources applied with their CRD; two object sets": {
objs: []*unstructured.Unstructured{
testutil.Unstructured(t, resources["crontab1"]),
testutil.Unstructured(t, resources["crontab2"]),
testutil.Unstructured(t, resources["crd"]),
},
expected: []object.UnstructuredSet{
{
testutil.Unstructured(t, resources["crd"]),
},
{
testutil.Unstructured(t, resources["crontab1"]),
testutil.Unstructured(t, resources["crontab2"]),
},
},
isError: false,
},
"two custom resources wit CRD and namespace; two object sets": {
objs: []*unstructured.Unstructured{
testutil.Unstructured(t, resources["crontab1"]),
testutil.Unstructured(t, resources["crontab2"]),
testutil.Unstructured(t, resources["namespace"]),
testutil.Unstructured(t, resources["crd"]),
},
expected: []object.UnstructuredSet{
{
testutil.Unstructured(t, resources["crd"]),
testutil.Unstructured(t, resources["namespace"]),
},
{
testutil.Unstructured(t, resources["crontab1"]),
testutil.Unstructured(t, resources["crontab2"]),
},
},
isError: false,
},
"two objects depends on each other is cyclic dependency": {
objs: []*unstructured.Unstructured{
testutil.Unstructured(t, resources["deployment"],
testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))),
testutil.Unstructured(t, resources["secret"],
testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["deployment"]))),
},
expected: []object.UnstructuredSet{},
isError: true,
},
"three objects in cyclic dependency": {
objs: []*unstructured.Unstructured{
testutil.Unstructured(t, resources["deployment"],
testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))),
testutil.Unstructured(t, resources["secret"],
testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["pod"]))),
testutil.Unstructured(t, resources["pod"],
testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["deployment"]))),
},
expected: []object.UnstructuredSet{},
isError: true,
},
}
for tn, tc := range testCases {
t.Run(tn, func(t *testing.T) {
actual, err := SortObjs(tc.objs)
if tc.isError {
assert.NotNil(t, err, "expected error, but received none")
return
}
assert.Nil(t, err, "unexpected error received")
verifyObjSets(t, tc.expected, actual)
})
}
}
func TestReverseSortObjs(t *testing.T) {
testCases := map[string]struct {
objs []*unstructured.Unstructured
expected []object.UnstructuredSet
isError bool
}{
"no objects returns no object sets": {
objs: []*unstructured.Unstructured{},
expected: []object.UnstructuredSet{},
isError: false,
},
"one object returns single object set": {
objs: []*unstructured.Unstructured{
testutil.Unstructured(t, resources["deployment"]),
},
expected: []object.UnstructuredSet{
{
testutil.Unstructured(t, resources["deployment"]),
},
},
isError: false,
},
"three objects depend on another; three single object sets in opposite order": {
objs: []*unstructured.Unstructured{
testutil.Unstructured(t, resources["deployment"],
testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))),
testutil.Unstructured(t, resources["secret"],
testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["pod"]))),
testutil.Unstructured(t, resources["pod"]),
},
expected: []object.UnstructuredSet{
{
testutil.Unstructured(t, resources["deployment"]),
},
{
testutil.Unstructured(t, resources["secret"]),
},
{
testutil.Unstructured(t, resources["pod"]),
},
},
isError: false,
},
"two objects applied with their namespace; two sets in opposite order": {
objs: []*unstructured.Unstructured{
testutil.Unstructured(t, resources["deployment"]),
testutil.Unstructured(t, resources["namespace"]),
testutil.Unstructured(t, resources["secret"]),
},
expected: []object.UnstructuredSet{
{
testutil.Unstructured(t, resources["secret"]),
testutil.Unstructured(t, resources["deployment"]),
},
{
testutil.Unstructured(t, resources["namespace"]),
},
},
isError: false,
},
"two custom resources wit CRD and namespace; two object sets in opposite order": {
objs: []*unstructured.Unstructured{
testutil.Unstructured(t, resources["crontab1"]),
testutil.Unstructured(t, resources["crontab2"]),
testutil.Unstructured(t, resources["namespace"]),
testutil.Unstructured(t, resources["crd"]),
},
expected: []object.UnstructuredSet{
{
testutil.Unstructured(t, resources["crontab1"]),
testutil.Unstructured(t, resources["crontab2"]),
},
{
testutil.Unstructured(t, resources["crd"]),
testutil.Unstructured(t, resources["namespace"]),
},
},
isError: false,
},
}
for tn, tc := range testCases {
t.Run(tn, func(t *testing.T) {
actual, err := ReverseSortObjs(tc.objs)
if tc.isError {
assert.NotNil(t, err, "expected error, but received none")
return
}
assert.Nil(t, err, "unexpected error received")
verifyObjSets(t, tc.expected, actual)
})
}
}
func TestDependencyGraph(t *testing.T) {
// Use a custom Asserter to customize the graph options
asserter := testutil.NewAsserter(
cmpopts.EquateErrors(),
graphComparer(),
)
testCases := map[string]struct {
objs object.UnstructuredSet
graph *Graph
expectedError error
}{
"no objects": {
objs: object.UnstructuredSet{},
graph: New(),
},
"one object no dependencies": {
objs: object.UnstructuredSet{
testutil.Unstructured(t, resources["deployment"]),
},
graph: &Graph{
edges: map[object.ObjMetadata]object.ObjMetadataSet{
testutil.ToIdentifier(t, resources["deployment"]): {},
},
reverseEdges: map[object.ObjMetadata]object.ObjMetadataSet{
testutil.ToIdentifier(t, resources["deployment"]): {},
},
},
},
"two unrelated objects": {
objs: object.UnstructuredSet{
testutil.Unstructured(t, resources["deployment"]),
testutil.Unstructured(t, resources["secret"]),
},
graph: &Graph{
edges: map[object.ObjMetadata]object.ObjMetadataSet{
testutil.ToIdentifier(t, resources["deployment"]): {},
testutil.ToIdentifier(t, resources["secret"]): {},
},
reverseEdges: map[object.ObjMetadata]object.ObjMetadataSet{
testutil.ToIdentifier(t, resources["deployment"]): {},
testutil.ToIdentifier(t, resources["secret"]): {},
},
},
},
"two objects one dependency": {
objs: object.UnstructuredSet{
testutil.Unstructured(t, resources["deployment"],
testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))),
testutil.Unstructured(t, resources["secret"]),
},
graph: &Graph{
edges: map[object.ObjMetadata]object.ObjMetadataSet{
testutil.ToIdentifier(t, resources["deployment"]): {
testutil.ToIdentifier(t, resources["secret"]),
},
testutil.ToIdentifier(t, resources["secret"]): {},
},
reverseEdges: map[object.ObjMetadata]object.ObjMetadataSet{
testutil.ToIdentifier(t, resources["deployment"]): {},
testutil.ToIdentifier(t, resources["secret"]): {
testutil.ToIdentifier(t, resources["deployment"]),
},
},
},
},
"three objects two dependencies": {
objs: object.UnstructuredSet{
testutil.Unstructured(t, resources["deployment"],
testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))),
testutil.Unstructured(t, resources["secret"],
testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["pod"]))),
testutil.Unstructured(t, resources["pod"]),
},
graph: &Graph{
edges: map[object.ObjMetadata]object.ObjMetadataSet{
testutil.ToIdentifier(t, resources["deployment"]): {
testutil.ToIdentifier(t, resources["secret"]),
},
testutil.ToIdentifier(t, resources["secret"]): {
testutil.ToIdentifier(t, resources["pod"]),
},
testutil.ToIdentifier(t, resources["pod"]): {},
},
reverseEdges: map[object.ObjMetadata]object.ObjMetadataSet{
testutil.ToIdentifier(t, resources["pod"]): {
testutil.ToIdentifier(t, resources["secret"]),
},
testutil.ToIdentifier(t, resources["secret"]): {
testutil.ToIdentifier(t, resources["deployment"]),
},
testutil.ToIdentifier(t, resources["deployment"]): {},
},
},
},
"three objects two dependencies on the same object": {
objs: object.UnstructuredSet{
testutil.Unstructured(t, resources["deployment"],
testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))),
testutil.Unstructured(t, resources["pod"],
testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))),
testutil.Unstructured(t, resources["secret"]),
},
graph: &Graph{
edges: map[object.ObjMetadata]object.ObjMetadataSet{
testutil.ToIdentifier(t, resources["deployment"]): {
testutil.ToIdentifier(t, resources["secret"]),
},
testutil.ToIdentifier(t, resources["pod"]): {
testutil.ToIdentifier(t, resources["secret"]),
},
testutil.ToIdentifier(t, resources["secret"]): {},
},
reverseEdges: map[object.ObjMetadata]object.ObjMetadataSet{
testutil.ToIdentifier(t, resources["secret"]): {
testutil.ToIdentifier(t, resources["deployment"]),
testutil.ToIdentifier(t, resources["pod"]),
},
testutil.ToIdentifier(t, resources["pod"]): {},
testutil.ToIdentifier(t, resources["deployment"]): {},
},
},
},
"two objects and their namespace": {
objs: object.UnstructuredSet{
testutil.Unstructured(t, resources["deployment"]),
testutil.Unstructured(t, resources["namespace"]),
testutil.Unstructured(t, resources["secret"]),
},
graph: &Graph{
edges: map[object.ObjMetadata]object.ObjMetadataSet{
testutil.ToIdentifier(t, resources["deployment"]): {
testutil.ToIdentifier(t, resources["namespace"]),
},
testutil.ToIdentifier(t, resources["secret"]): {
testutil.ToIdentifier(t, resources["namespace"]),
},
testutil.ToIdentifier(t, resources["namespace"]): {},
},
reverseEdges: map[object.ObjMetadata]object.ObjMetadataSet{
testutil.ToIdentifier(t, resources["namespace"]): {
testutil.ToIdentifier(t, resources["secret"]),
testutil.ToIdentifier(t, resources["deployment"]),
},
testutil.ToIdentifier(t, resources["secret"]): {},
testutil.ToIdentifier(t, resources["deployment"]): {},
},
},
},
"two custom resources and their CRD": {
objs: object.UnstructuredSet{
testutil.Unstructured(t, resources["crontab1"]),
testutil.Unstructured(t, resources["crontab2"]),
testutil.Unstructured(t, resources["crd"]),
},
graph: &Graph{
edges: map[object.ObjMetadata]object.ObjMetadataSet{
testutil.ToIdentifier(t, resources["crontab1"]): {
testutil.ToIdentifier(t, resources["crd"]),
},
testutil.ToIdentifier(t, resources["crontab2"]): {
testutil.ToIdentifier(t, resources["crd"]),
},
testutil.ToIdentifier(t, resources["crd"]): {},
},
reverseEdges: map[object.ObjMetadata]object.ObjMetadataSet{
testutil.ToIdentifier(t, resources["crd"]): {
testutil.ToIdentifier(t, resources["crontab1"]),
testutil.ToIdentifier(t, resources["crontab2"]),
},
testutil.ToIdentifier(t, resources["crontab1"]): {},
testutil.ToIdentifier(t, resources["crontab2"]): {},
},
},
},
"two custom resources with their CRD and namespace": {
objs: object.UnstructuredSet{
testutil.Unstructured(t, resources["crontab1"]),
testutil.Unstructured(t, resources["crontab2"]),
testutil.Unstructured(t, resources["namespace"]),
testutil.Unstructured(t, resources["crd"]),
},
graph: &Graph{
edges: map[object.ObjMetadata]object.ObjMetadataSet{
testutil.ToIdentifier(t, resources["crontab1"]): {
testutil.ToIdentifier(t, resources["crd"]),
testutil.ToIdentifier(t, resources["namespace"]),
},
testutil.ToIdentifier(t, resources["crontab2"]): {
testutil.ToIdentifier(t, resources["crd"]),
testutil.ToIdentifier(t, resources["namespace"]),
},
testutil.ToIdentifier(t, resources["crd"]): {},
testutil.ToIdentifier(t, resources["namespace"]): {},
},
reverseEdges: map[object.ObjMetadata]object.ObjMetadataSet{
testutil.ToIdentifier(t, resources["crd"]): {
testutil.ToIdentifier(t, resources["crontab1"]),
testutil.ToIdentifier(t, resources["crontab2"]),
},
testutil.ToIdentifier(t, resources["namespace"]): {
testutil.ToIdentifier(t, resources["crontab1"]),
testutil.ToIdentifier(t, resources["crontab2"]),
},
testutil.ToIdentifier(t, resources["crontab1"]): {},
testutil.ToIdentifier(t, resources["crontab2"]): {},
},
},
},
"two object cyclic dependency": {
objs: object.UnstructuredSet{
testutil.Unstructured(t, resources["deployment"],
testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))),
testutil.Unstructured(t, resources["secret"],
testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["deployment"]))),
},
graph: &Graph{
edges: map[object.ObjMetadata]object.ObjMetadataSet{
testutil.ToIdentifier(t, resources["deployment"]): {
testutil.ToIdentifier(t, resources["secret"]),
},
testutil.ToIdentifier(t, resources["secret"]): {
testutil.ToIdentifier(t, resources["deployment"]),
},
},
reverseEdges: map[object.ObjMetadata]object.ObjMetadataSet{
testutil.ToIdentifier(t, resources["secret"]): {
testutil.ToIdentifier(t, resources["deployment"]),
},
testutil.ToIdentifier(t, resources["deployment"]): {
testutil.ToIdentifier(t, resources["secret"]),
},
},
},
},
"three object cyclic dependency": {
objs: object.UnstructuredSet{
testutil.Unstructured(t, resources["deployment"],
testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))),
testutil.Unstructured(t, resources["secret"],
testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["pod"]))),
testutil.Unstructured(t, resources["pod"],
testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["deployment"]))),
},
graph: &Graph{
edges: map[object.ObjMetadata]object.ObjMetadataSet{
testutil.ToIdentifier(t, resources["deployment"]): {
testutil.ToIdentifier(t, resources["secret"]),
},
testutil.ToIdentifier(t, resources["secret"]): {
testutil.ToIdentifier(t, resources["pod"]),
},
testutil.ToIdentifier(t, resources["pod"]): {
testutil.ToIdentifier(t, resources["deployment"]),
},
},
reverseEdges: map[object.ObjMetadata]object.ObjMetadataSet{
testutil.ToIdentifier(t, resources["deployment"]): {
testutil.ToIdentifier(t, resources["pod"]),
},
testutil.ToIdentifier(t, resources["pod"]): {
testutil.ToIdentifier(t, resources["secret"]),
},
testutil.ToIdentifier(t, resources["secret"]): {
testutil.ToIdentifier(t, resources["deployment"]),
},
},
},
},
}
for tn, tc := range testCases {
t.Run(tn, func(t *testing.T) {
g, err := DependencyGraph(tc.objs)
if tc.expectedError != nil {
require.EqualError(t, err, tc.expectedError.Error())
return
}
assert.NoError(t, err)
asserter.Equal(t, tc.graph, g)
})
}
}
func TestHydrateSetList(t *testing.T) {
testCases := map[string]struct {
idSetList []object.ObjMetadataSet
objs object.UnstructuredSet
expected []object.UnstructuredSet
}{
"no object sets": {
idSetList: []object.ObjMetadataSet{},
expected: nil,
},
"one object set": {
idSetList: []object.ObjMetadataSet{
{
testutil.ToIdentifier(t, resources["deployment"]),
},
},
objs: object.UnstructuredSet{
testutil.Unstructured(t, resources["deployment"]),
},
expected: []object.UnstructuredSet{
{
testutil.Unstructured(t, resources["deployment"]),
},
},
},
"two out of three": {
idSetList: []object.ObjMetadataSet{
{
testutil.ToIdentifier(t, resources["deployment"]),
},
{
testutil.ToIdentifier(t, resources["secret"]),
},
{
testutil.ToIdentifier(t, resources["pod"]),
},
},
objs: object.UnstructuredSet{
testutil.Unstructured(t, resources["deployment"]),
testutil.Unstructured(t, resources["pod"]),
},
expected: []object.UnstructuredSet{
{
testutil.Unstructured(t, resources["deployment"]),
},
{
testutil.Unstructured(t, resources["pod"]),
},
},
},
"two uneven sets": {
idSetList: []object.ObjMetadataSet{
{
testutil.ToIdentifier(t, resources["secret"]),
testutil.ToIdentifier(t, resources["deployment"]),
},
{
testutil.ToIdentifier(t, resources["namespace"]),
},
},
objs: object.UnstructuredSet{
testutil.Unstructured(t, resources["namespace"]),
testutil.Unstructured(t, resources["deployment"]),
testutil.Unstructured(t, resources["secret"]),
testutil.Unstructured(t, resources["pod"]),
},
expected: []object.UnstructuredSet{
{
testutil.Unstructured(t, resources["secret"]),
testutil.Unstructured(t, resources["deployment"]),
},
{
testutil.Unstructured(t, resources["namespace"]),
},
},
},
"one of two sets": {
idSetList: []object.ObjMetadataSet{
{
testutil.ToIdentifier(t, resources["namespace"]),
testutil.ToIdentifier(t, resources["crd"]),
},
{
testutil.ToIdentifier(t, resources["crontab1"]),
testutil.ToIdentifier(t, resources["crontab2"]),
},
},
objs: object.UnstructuredSet{
testutil.Unstructured(t, resources["namespace"]),
testutil.Unstructured(t, resources["crd"]),
},
expected: []object.UnstructuredSet{
{
testutil.Unstructured(t, resources["namespace"]),
testutil.Unstructured(t, resources["crd"]),
},
},
},
}
for tn, tc := range testCases {
t.Run(tn, func(t *testing.T) {
objSetList := HydrateSetList(tc.idSetList, tc.objs)
assert.Equal(t, tc.expected, objSetList)
})
}
}
func TestReverseSetList(t *testing.T) {
testCases := map[string]struct {
setList []object.UnstructuredSet
expected []object.UnstructuredSet
}{
"no object sets": {
setList: []object.UnstructuredSet{},
expected: []object.UnstructuredSet{},
},
"one object set": {
setList: []object.UnstructuredSet{
{
testutil.Unstructured(t, resources["deployment"]),
},
},
expected: []object.UnstructuredSet{
{
testutil.Unstructured(t, resources["deployment"]),
},
},
},
"three object sets": {
setList: []object.UnstructuredSet{
{
testutil.Unstructured(t, resources["deployment"]),
},
{
testutil.Unstructured(t, resources["secret"]),
},
{
testutil.Unstructured(t, resources["pod"]),
},
},
expected: []object.UnstructuredSet{
{
testutil.Unstructured(t, resources["pod"]),
},
{
testutil.Unstructured(t, resources["secret"]),
},
{
testutil.Unstructured(t, resources["deployment"]),
},
},
},
"two uneven sets": {
setList: []object.UnstructuredSet{
{
testutil.Unstructured(t, resources["secret"]),
testutil.Unstructured(t, resources["deployment"]),
},
{
testutil.Unstructured(t, resources["namespace"]),
},
},
expected: []object.UnstructuredSet{
{
testutil.Unstructured(t, resources["namespace"]),
},
{
testutil.Unstructured(t, resources["deployment"]),
testutil.Unstructured(t, resources["secret"]),
},
},
},
"two even sets": {
setList: []object.UnstructuredSet{
{
testutil.Unstructured(t, resources["crontab1"]),
testutil.Unstructured(t, resources["crontab2"]),
},
{
testutil.Unstructured(t, resources["crd"]),
testutil.Unstructured(t, resources["namespace"]),
},
},
expected: []object.UnstructuredSet{
{
testutil.Unstructured(t, resources["namespace"]),
testutil.Unstructured(t, resources["crd"]),
},
{
testutil.Unstructured(t, resources["crontab2"]),
testutil.Unstructured(t, resources["crontab1"]),
},
},
},
}
for tn, tc := range testCases {
t.Run(tn, func(t *testing.T) {
ReverseSetList(tc.setList)
assert.Equal(t, tc.expected, tc.setList)
})
}
}
func TestApplyTimeMutationEdges(t *testing.T) {
testCases := map[string]struct {
objs []*unstructured.Unstructured
expected []Edge
expectedError error
}{
"no objects adds no graph edges": {
objs: []*unstructured.Unstructured{},
expected: []Edge{},
},
"no depends-on annotations adds no graph edges": {
objs: []*unstructured.Unstructured{
testutil.Unstructured(t, resources["deployment"]),
},
expected: []Edge{},
},
"no depends-on annotations, two objects, adds no graph edges": {
objs: []*unstructured.Unstructured{
testutil.Unstructured(t, resources["deployment"]),
testutil.Unstructured(t, resources["secret"]),
},
expected: []Edge{},
},
"two dependent objects, adds one edge": {
objs: []*unstructured.Unstructured{
testutil.Unstructured(
t,
resources["deployment"],
mutationutil.AddApplyTimeMutation(t, &mutation.ApplyTimeMutation{
{
SourceRef: mutation.ResourceReferenceFromObjMetadata(
testutil.ToIdentifier(t, resources["secret"]),
),
SourcePath: "unused",
TargetPath: "unused",
Token: "unused",
},
}),
),
testutil.Unstructured(t, resources["secret"]),
},
expected: []Edge{
{
From: testutil.ToIdentifier(t, resources["deployment"]),
To: testutil.ToIdentifier(t, resources["secret"]),
},
},
},
"three dependent objects, adds two edges": {
objs: []*unstructured.Unstructured{
testutil.Unstructured(
t,
resources["deployment"],
mutationutil.AddApplyTimeMutation(t, &mutation.ApplyTimeMutation{
{
SourceRef: mutation.ResourceReferenceFromObjMetadata(
testutil.ToIdentifier(t, resources["secret"]),
),
SourcePath: "unused",
TargetPath: "unused",
Token: "unused",
},
}),
),
testutil.Unstructured(
t,
resources["pod"],
mutationutil.AddApplyTimeMutation(t, &mutation.ApplyTimeMutation{
{
SourceRef: mutation.ResourceReferenceFromObjMetadata(
testutil.ToIdentifier(t, resources["secret"]),
),
SourcePath: "unused",
TargetPath: "unused",
Token: "unused",
},
}),
),
testutil.Unstructured(t, resources["secret"]),
},
expected: []Edge{
{
From: testutil.ToIdentifier(t, resources["deployment"]),
To: testutil.ToIdentifier(t, resources["secret"]),
},
{
From: testutil.ToIdentifier(t, resources["pod"]),
To: testutil.ToIdentifier(t, resources["secret"]),
},
},
},
"pod has two dependencies, adds two edges": {
objs: []*unstructured.Unstructured{
testutil.Unstructured(
t,
resources["pod"],
mutationutil.AddApplyTimeMutation(t, &mutation.ApplyTimeMutation{
{
SourceRef: mutation.ResourceReferenceFromObjMetadata(
testutil.ToIdentifier(t, resources["secret"]),
),
SourcePath: "unused",
TargetPath: "unused",
Token: "unused",
},
{
SourceRef: mutation.ResourceReferenceFromObjMetadata(
testutil.ToIdentifier(t, resources["deployment"]),
),
SourcePath: "unused",
TargetPath: "unused",
Token: "unused",
},
}),
),
testutil.Unstructured(t, resources["deployment"]),
testutil.Unstructured(t, resources["secret"]),
},
expected: []Edge{
{
From: testutil.ToIdentifier(t, resources["pod"]),
To: testutil.ToIdentifier(t, resources["secret"]),
},
{
From: testutil.ToIdentifier(t, resources["pod"]),
To: testutil.ToIdentifier(t, resources["deployment"]),
},
},
},
"error: invalid annotation": {
objs: []*unstructured.Unstructured{
{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "foo",
"namespace": "default",
"annotations": map[string]interface{}{
mutation.Annotation: "invalid-mutation",
},
},
},
},
},
expected: []Edge{},
expectedError: validation.NewError(
object.InvalidAnnotationError{
Annotation: mutation.Annotation,
Cause: errors.New("error unmarshaling JSON: " +
"while decoding JSON: json: " +
"cannot unmarshal string into Go value of type mutation.ApplyTimeMutation"),
},
object.ObjMetadata{
GroupKind: schema.GroupKind{
Group: "apps",
Kind: "Deployment",
},
Name: "foo",
Namespace: "default",
},
),
},
"error: dependency not in object set": {
objs: []*unstructured.Unstructured{
testutil.Unstructured(t, resources["pod"],
mutationutil.AddApplyTimeMutation(t, &mutation.ApplyTimeMutation{
{
SourceRef: mutation.ResourceReferenceFromObjMetadata(
testutil.ToIdentifier(t, resources["deployment"]),
),
},
}),
),
},
expected: []Edge{},
expectedError: validation.NewError(
object.InvalidAnnotationError{
Annotation: mutation.Annotation,
Cause: ExternalDependencyError{
Edge: Edge{
From: testutil.ToIdentifier(t, resources["pod"]),
To: testutil.ToIdentifier(t, resources["deployment"]),
},
},
},
object.ObjMetadata{
GroupKind: schema.GroupKind{
Group: "",
Kind: "Pod",
},
Name: "test-pod",
Namespace: "test-namespace",
},
),
},
"error: two invalid objects": {
objs: []*unstructured.Unstructured{
{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "foo",
"namespace": "default",
"annotations": map[string]interface{}{
mutation.Annotation: "invalid-mutation",
},
},
},
},
testutil.Unstructured(t, resources["pod"],
mutationutil.AddApplyTimeMutation(t, &mutation.ApplyTimeMutation{
{
SourceRef: mutation.ResourceReferenceFromObjMetadata(
testutil.ToIdentifier(t, resources["secret"]),
),
},
}),
),
},
expected: []Edge{},
expectedError: multierror.New(
validation.NewError(
object.InvalidAnnotationError{
Annotation: mutation.Annotation,
Cause: errors.New("error unmarshaling JSON: " +
"while decoding JSON: json: " +
"cannot unmarshal string into Go value of type mutation.ApplyTimeMutation"),
},
object.ObjMetadata{
GroupKind: schema.GroupKind{
Group: "apps",
Kind: "Deployment",
},
Name: "foo",
Namespace: "default",
},
),
validation.NewError(
object.InvalidAnnotationError{
Annotation: mutation.Annotation,
Cause: ExternalDependencyError{
Edge: Edge{
From: testutil.ToIdentifier(t, resources["pod"]),
To: testutil.ToIdentifier(t, resources["secret"]),
},
},
},
object.ObjMetadata{
GroupKind: schema.GroupKind{
Group: "",
Kind: "Pod",
},
Name: "test-pod",
Namespace: "test-namespace",
},
),
),
},
}
for tn, tc := range testCases {
t.Run(tn, func(t *testing.T) {
g := New()
ids := object.UnstructuredSetToObjMetadataSet(tc.objs)
err := addApplyTimeMutationEdges(g, tc.objs, ids)
if tc.expectedError != nil {
assert.EqualError(t, err, tc.expectedError.Error())
} else {
assert.NoError(t, err)
}
actual := edgeMapToList(g.edges)
verifyEdges(t, tc.expected, actual)
})
}
}
func TestAddDependsOnEdges(t *testing.T) {
testCases := map[string]struct {
objs []*unstructured.Unstructured
expected []Edge
expectedError error
}{
"no objects adds no graph edges": {
objs: []*unstructured.Unstructured{},
expected: []Edge{},
},
"no depends-on annotations adds no graph edges": {
objs: []*unstructured.Unstructured{
testutil.Unstructured(t, resources["deployment"]),
},
expected: []Edge{},
},
"no depends-on annotations, two objects, adds no graph edges": {
objs: []*unstructured.Unstructured{
testutil.Unstructured(t, resources["deployment"]),
testutil.Unstructured(t, resources["secret"]),
},
expected: []Edge{},
},
"two dependent objects, adds one edge": {
objs: []*unstructured.Unstructured{
testutil.Unstructured(t, resources["deployment"],
testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))),
testutil.Unstructured(t, resources["secret"]),
},
expected: []Edge{
{
From: testutil.ToIdentifier(t, resources["deployment"]),
To: testutil.ToIdentifier(t, resources["secret"]),
},
},
},
"three dependent objects, adds two edges": {
objs: []*unstructured.Unstructured{
testutil.Unstructured(t, resources["deployment"],
testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))),
testutil.Unstructured(t, resources["pod"],
testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))),
testutil.Unstructured(t, resources["secret"]),
},
expected: []Edge{
{
From: testutil.ToIdentifier(t, resources["deployment"]),
To: testutil.ToIdentifier(t, resources["secret"]),
},
{
From: testutil.ToIdentifier(t, resources["pod"]),
To: testutil.ToIdentifier(t, resources["secret"]),
},
},
},
"pod has two dependencies, adds two edges": {
objs: []*unstructured.Unstructured{
testutil.Unstructured(t, resources["pod"],
testutil.AddDependsOn(t,
testutil.ToIdentifier(t, resources["secret"]),
testutil.ToIdentifier(t, resources["deployment"]),
),
),
testutil.Unstructured(t, resources["deployment"]),
testutil.Unstructured(t, resources["secret"]),
},
expected: []Edge{
{
From: testutil.ToIdentifier(t, resources["pod"]),
To: testutil.ToIdentifier(t, resources["secret"]),
},
{
From: testutil.ToIdentifier(t, resources["pod"]),
To: testutil.ToIdentifier(t, resources["deployment"]),
},
},
},
"error: invalid annotation": {
objs: []*unstructured.Unstructured{
{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "foo",
"namespace": "default",
"annotations": map[string]interface{}{
dependson.Annotation: "invalid-obj-ref",
},
},
},
},
},
expected: []Edge{},
expectedError: validation.NewError(
object.InvalidAnnotationError{
Annotation: dependson.Annotation,
Cause: errors.New("failed to parse object reference (index: 0): " +
`expected 3 or 5 fields, found 1: "invalid-obj-ref"`),
},
object.ObjMetadata{
GroupKind: schema.GroupKind{
Group: "apps",
Kind: "Deployment",
},
Name: "foo",
Namespace: "default",
},
),
},
"error: duplicate reference": {
objs: []*unstructured.Unstructured{
testutil.Unstructured(t, resources["pod"],
testutil.AddDependsOn(t,
testutil.ToIdentifier(t, resources["deployment"]),
testutil.ToIdentifier(t, resources["deployment"]),
),
),
testutil.Unstructured(t, resources["deployment"]),
},
expected: []Edge{
{
From: testutil.ToIdentifier(t, resources["pod"]),
To: testutil.ToIdentifier(t, resources["deployment"]),
},
},
expectedError: validation.NewError(
object.InvalidAnnotationError{
Annotation: dependson.Annotation,
Cause: DuplicateDependencyError{
Edge: Edge{
From: testutil.ToIdentifier(t, resources["pod"]),
To: testutil.ToIdentifier(t, resources["deployment"]),
},
},
},
object.ObjMetadata{
GroupKind: schema.GroupKind{
Group: "",
Kind: "Pod",
},
Name: "test-pod",
Namespace: "test-namespace",
},
),
},
"error: external dependency": {
objs: []*unstructured.Unstructured{
testutil.Unstructured(t, resources["pod"],
testutil.AddDependsOn(t,
testutil.ToIdentifier(t, resources["deployment"]),
),
),
},
expected: []Edge{},
expectedError: validation.NewError(
object.InvalidAnnotationError{
Annotation: dependson.Annotation,
Cause: ExternalDependencyError{
Edge: Edge{
From: testutil.ToIdentifier(t, resources["pod"]),
To: testutil.ToIdentifier(t, resources["deployment"]),
},
},
},
object.ObjMetadata{
GroupKind: schema.GroupKind{
Group: "",
Kind: "Pod",
},
Name: "test-pod",
Namespace: "test-namespace",
},
),
},
"error: two invalid objects": {
objs: []*unstructured.Unstructured{
{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "foo",
"namespace": "default",
"annotations": map[string]interface{}{
dependson.Annotation: "invalid-obj-ref",
},
},
},
},
testutil.Unstructured(t, resources["pod"],
testutil.AddDependsOn(t,
testutil.ToIdentifier(t, resources["secret"]),
),
),
},
expected: []Edge{},
expectedError: multierror.New(
validation.NewError(
object.InvalidAnnotationError{
Annotation: dependson.Annotation,
Cause: errors.New("failed to parse object reference (index: 0): " +
`expected 3 or 5 fields, found 1: "invalid-obj-ref"`),
},
object.ObjMetadata{
GroupKind: schema.GroupKind{
Group: "apps",
Kind: "Deployment",
},
Name: "foo",
Namespace: "default",
},
),
validation.NewError(
object.InvalidAnnotationError{
Annotation: dependson.Annotation,
Cause: ExternalDependencyError{
Edge: Edge{
From: testutil.ToIdentifier(t, resources["pod"]),
To: testutil.ToIdentifier(t, resources["secret"]),
},
},
},
object.ObjMetadata{
GroupKind: schema.GroupKind{
Group: "",
Kind: "Pod",
},
Name: "test-pod",
Namespace: "test-namespace",
},
),
),
},
"error: one object with two errors": {
objs: []*unstructured.Unstructured{
testutil.Unstructured(t, resources["pod"],
testutil.AddDependsOn(t,
testutil.ToIdentifier(t, resources["deployment"]),
testutil.ToIdentifier(t, resources["deployment"]),
),
),
},
expected: []Edge{},
expectedError: validation.NewError(
multierror.New(
object.InvalidAnnotationError{
Annotation: dependson.Annotation,
Cause: ExternalDependencyError{
Edge: Edge{
From: testutil.ToIdentifier(t, resources["pod"]),
To: testutil.ToIdentifier(t, resources["deployment"]),
},
},
},
object.InvalidAnnotationError{
Annotation: dependson.Annotation,
Cause: DuplicateDependencyError{
Edge: Edge{
From: testutil.ToIdentifier(t, resources["pod"]),
To: testutil.ToIdentifier(t, resources["deployment"]),
},
},
},
),
object.ObjMetadata{
GroupKind: schema.GroupKind{
Group: "",
Kind: "Pod",
},
Name: "test-pod",
Namespace: "test-namespace",
},
),
},
}
for tn, tc := range testCases {
t.Run(tn, func(t *testing.T) {
g := New()
ids := object.UnstructuredSetToObjMetadataSet(tc.objs)
err := addDependsOnEdges(g, tc.objs, ids)
if tc.expectedError != nil {
assert.EqualError(t, err, tc.expectedError.Error())
} else {
assert.NoError(t, err)
}
actual := edgeMapToList(g.edges)
verifyEdges(t, tc.expected, actual)
})
}
}
func TestAddNamespaceEdges(t *testing.T) {
testCases := map[string]struct {
objs []*unstructured.Unstructured
expected []Edge
}{
"no namespace objects adds no graph edges": {
objs: []*unstructured.Unstructured{},
expected: []Edge{},
},
"single namespace adds no graph edges": {
objs: []*unstructured.Unstructured{
testutil.Unstructured(t, resources["namespace"]),
},
expected: []Edge{},
},
"pod within namespace adds one edge": {
objs: []*unstructured.Unstructured{
testutil.Unstructured(t, resources["namespace"]),
testutil.Unstructured(t, resources["pod"]),
},
expected: []Edge{
{
From: testutil.ToIdentifier(t, resources["pod"]),
To: testutil.ToIdentifier(t, resources["namespace"]),
},
},
},
"pod not in namespace does not add edge": {
objs: []*unstructured.Unstructured{
testutil.Unstructured(t, resources["namespace"]),
testutil.Unstructured(t, resources["default-pod"]),
},
expected: []Edge{},
},
"pod, secret, and namespace adds two edges": {
objs: []*unstructured.Unstructured{
testutil.Unstructured(t, resources["namespace"]),
testutil.Unstructured(t, resources["secret"]),
testutil.Unstructured(t, resources["pod"]),
},
expected: []Edge{
{
From: testutil.ToIdentifier(t, resources["pod"]),
To: testutil.ToIdentifier(t, resources["namespace"]),
},
{
From: testutil.ToIdentifier(t, resources["secret"]),
To: testutil.ToIdentifier(t, resources["namespace"]),
},
},
},
"one pod in namespace, one not, adds only one edge": {
objs: []*unstructured.Unstructured{
testutil.Unstructured(t, resources["namespace"]),
testutil.Unstructured(t, resources["default-pod"]),
testutil.Unstructured(t, resources["pod"]),
},
expected: []Edge{
{
From: testutil.ToIdentifier(t, resources["pod"]),
To: testutil.ToIdentifier(t, resources["namespace"]),
},
},
},
}
for tn, tc := range testCases {
t.Run(tn, func(t *testing.T) {
g := New()
ids := object.UnstructuredSetToObjMetadataSet(tc.objs)
addNamespaceEdges(g, tc.objs, ids)
actual := edgeMapToList(g.edges)
verifyEdges(t, tc.expected, actual)
})
}
}
func TestAddCRDEdges(t *testing.T) {
testCases := map[string]struct {
objs []*unstructured.Unstructured
expected []Edge
}{
"no CRD objects adds no graph edges": {
objs: []*unstructured.Unstructured{},
expected: []Edge{},
},
"single namespace adds no graph edges": {
objs: []*unstructured.Unstructured{
testutil.Unstructured(t, resources["crd"]),
},
expected: []Edge{},
},
"two custom resources adds no graph edges": {
objs: []*unstructured.Unstructured{
testutil.Unstructured(t, resources["crontab1"]),
testutil.Unstructured(t, resources["crontab2"]),
},
expected: []Edge{},
},
"two custom resources with crd adds two edges": {
objs: []*unstructured.Unstructured{
testutil.Unstructured(t, resources["crd"]),
testutil.Unstructured(t, resources["crontab1"]),
testutil.Unstructured(t, resources["crontab2"]),
},
expected: []Edge{
{
From: testutil.ToIdentifier(t, resources["crontab1"]),
To: testutil.ToIdentifier(t, resources["crd"]),
},
{
From: testutil.ToIdentifier(t, resources["crontab2"]),
To: testutil.ToIdentifier(t, resources["crd"]),
},
},
},
}
for tn, tc := range testCases {
t.Run(tn, func(t *testing.T) {
g := New()
ids := object.UnstructuredSetToObjMetadataSet(tc.objs)
addCRDEdges(g, tc.objs, ids)
actual := edgeMapToList(g.edges)
verifyEdges(t, tc.expected, actual)
})
}
}
// verifyObjSets ensures the expected and actual slice of object sets are the same,
// and the sets are in order.
func verifyObjSets(t *testing.T, expected []object.UnstructuredSet, actual []object.UnstructuredSet) {
if len(expected) != len(actual) {
t.Fatalf("expected (%d) object sets, got (%d)", len(expected), len(actual))
return
}
// Order matters
for i := range expected {
expectedSet := expected[i]
actualSet := actual[i]
if len(expectedSet) != len(actualSet) {
t.Fatalf("set %d: expected object size (%d), got (%d)", i, len(expectedSet), len(actualSet))
return
}
for _, actualObj := range actualSet {
if !containsObjs(expectedSet, actualObj) {
t.Fatalf("set #%d: actual object (%v) not found in set of expected objects", i, actualObj)
return
}
}
}
}
// containsUnstructured returns true if the passed object is within the passed
// slice of objects; false otherwise. Order is not important.
func containsObjs(objs []*unstructured.Unstructured, obj *unstructured.Unstructured) bool {
ids := object.UnstructuredSetToObjMetadataSet(objs)
id := object.UnstructuredToObjMetadata(obj)
for _, i := range ids {
if i == id {
return true
}
}
return false
}
// verifyEdges ensures the slices of directed Edges contain the same elements.
// Order is not important.
func verifyEdges(t *testing.T, expected []Edge, actual []Edge) {
if len(expected) != len(actual) {
t.Fatalf("expected (%d) edges, got (%d)", len(expected), len(actual))
return
}
for _, actualEdge := range actual {
if !containsEdge(expected, actualEdge) {
t.Errorf("actual Edge (%v) not found in expected Edges", actualEdge)
return
}
}
}
// containsEdge return true if the passed Edge is in the slice of Edges;
// false otherwise.
func containsEdge(edges []Edge, edge Edge) bool {
for _, e := range edges {
if e.To == edge.To && e.From == edge.From {
return true
}
}
return false
}
// waitTaskComparer allows comparion of WaitTasks, ignoring private fields.
func graphComparer() cmp.Option {
return cmp.Comparer(func(x, y *Graph) bool {
if x == nil {
return y == nil
}
if y == nil {
return false
}
return cmp.Equal(x.edges, y.edges) &&
cmp.Equal(x.reverseEdges, y.reverseEdges)
})
}