...

Source file src/sigs.k8s.io/cli-utils/pkg/object/graph/depends_test.go

Documentation: sigs.k8s.io/cli-utils/pkg/object/graph

     1  // Copyright 2020 The Kubernetes Authors.
     2  // SPDX-License-Identifier: Apache-2.0
     3  
     4  package graph
     5  
     6  import (
     7  	"errors"
     8  	"testing"
     9  
    10  	"github.com/google/go-cmp/cmp"
    11  	"github.com/google/go-cmp/cmp/cmpopts"
    12  	"github.com/stretchr/testify/assert"
    13  	"github.com/stretchr/testify/require"
    14  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    15  	"k8s.io/apimachinery/pkg/runtime/schema"
    16  	"sigs.k8s.io/cli-utils/pkg/multierror"
    17  	"sigs.k8s.io/cli-utils/pkg/object"
    18  	"sigs.k8s.io/cli-utils/pkg/object/dependson"
    19  	"sigs.k8s.io/cli-utils/pkg/object/mutation"
    20  	mutationutil "sigs.k8s.io/cli-utils/pkg/object/mutation/testutil"
    21  	"sigs.k8s.io/cli-utils/pkg/object/validation"
    22  	"sigs.k8s.io/cli-utils/pkg/testutil"
    23  )
    24  
    25  var (
    26  	resources = map[string]string{
    27  		"pod": `
    28  kind: Pod
    29  apiVersion: v1
    30  metadata:
    31    name: test-pod
    32    namespace: test-namespace
    33  `,
    34  		"default-pod": `
    35  kind: Pod
    36  apiVersion: v1
    37  metadata:
    38    name: pod-in-default-namespace
    39    namespace: default
    40  `,
    41  		"deployment": `
    42  kind: Deployment
    43  apiVersion: apps/v1
    44  metadata:
    45    name: foo
    46    namespace: test-namespace
    47    uid: dep-uid
    48    generation: 1
    49  spec:
    50    replicas: 1
    51  `,
    52  		"secret": `
    53  kind: Secret
    54  apiVersion: v1
    55  metadata:
    56    name: secret
    57    namespace: test-namespace
    58    uid: secret-uid
    59    generation: 1
    60  type: Opaque
    61  spec:
    62    foo: bar
    63  `,
    64  		"namespace": `
    65  kind: Namespace
    66  apiVersion: v1
    67  metadata:
    68    name: test-namespace
    69  `,
    70  
    71  		"crd": `
    72  apiVersion: apiextensions.k8s.io/v1
    73  kind: CustomResourceDefinition
    74  metadata:
    75    name: crontabs.stable.example.com
    76  spec:
    77    group: stable.example.com
    78    versions:
    79      - name: v1
    80        served: true
    81        storage: true
    82    scope: Namespaced
    83    names:
    84      plural: crontabs
    85      singular: crontab
    86      kind: CronTab
    87  `,
    88  		"crontab1": `
    89  apiVersion: "stable.example.com/v1"
    90  kind: CronTab
    91  metadata:
    92    name: cron-tab-01
    93    namespace: test-namespace
    94  `,
    95  		"crontab2": `
    96  apiVersion: "stable.example.com/v1"
    97  kind: CronTab
    98  metadata:
    99    name: cron-tab-02
   100    namespace: test-namespace
   101  `,
   102  	}
   103  )
   104  
   105  func TestSortObjs(t *testing.T) {
   106  	testCases := map[string]struct {
   107  		objs     []*unstructured.Unstructured
   108  		expected []object.UnstructuredSet
   109  		isError  bool
   110  	}{
   111  		"no objects returns no object sets": {
   112  			objs:     []*unstructured.Unstructured{},
   113  			expected: []object.UnstructuredSet{},
   114  			isError:  false,
   115  		},
   116  		"one object returns single object set": {
   117  			objs: []*unstructured.Unstructured{
   118  				testutil.Unstructured(t, resources["deployment"]),
   119  			},
   120  			expected: []object.UnstructuredSet{
   121  				{
   122  					testutil.Unstructured(t, resources["deployment"]),
   123  				},
   124  			},
   125  			isError: false,
   126  		},
   127  		"two unrelated objects returns single object set with two objs": {
   128  			objs: []*unstructured.Unstructured{
   129  				testutil.Unstructured(t, resources["deployment"]),
   130  				testutil.Unstructured(t, resources["secret"]),
   131  			},
   132  			expected: []object.UnstructuredSet{
   133  				{
   134  					testutil.Unstructured(t, resources["deployment"]),
   135  					testutil.Unstructured(t, resources["secret"]),
   136  				},
   137  			},
   138  			isError: false,
   139  		},
   140  		"one object depends on the other; two single object sets": {
   141  			objs: []*unstructured.Unstructured{
   142  				testutil.Unstructured(t, resources["deployment"],
   143  					testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))),
   144  				testutil.Unstructured(t, resources["secret"]),
   145  			},
   146  			expected: []object.UnstructuredSet{
   147  				{
   148  					testutil.Unstructured(t, resources["secret"]),
   149  				},
   150  				{
   151  					testutil.Unstructured(t, resources["deployment"]),
   152  				},
   153  			},
   154  			isError: false,
   155  		},
   156  		"three objects depend on another; three single object sets": {
   157  			objs: []*unstructured.Unstructured{
   158  				testutil.Unstructured(t, resources["deployment"],
   159  					testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))),
   160  				testutil.Unstructured(t, resources["secret"],
   161  					testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["pod"]))),
   162  				testutil.Unstructured(t, resources["pod"]),
   163  			},
   164  			expected: []object.UnstructuredSet{
   165  				{
   166  					testutil.Unstructured(t, resources["pod"]),
   167  				},
   168  				{
   169  					testutil.Unstructured(t, resources["secret"]),
   170  				},
   171  				{
   172  					testutil.Unstructured(t, resources["deployment"]),
   173  				},
   174  			},
   175  			isError: false,
   176  		},
   177  		"Two objects depend on secret; two object sets": {
   178  			objs: []*unstructured.Unstructured{
   179  				testutil.Unstructured(t, resources["deployment"],
   180  					testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))),
   181  				testutil.Unstructured(t, resources["pod"],
   182  					testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))),
   183  				testutil.Unstructured(t, resources["secret"]),
   184  			},
   185  			expected: []object.UnstructuredSet{
   186  				{
   187  					testutil.Unstructured(t, resources["secret"]),
   188  				},
   189  				{
   190  					testutil.Unstructured(t, resources["pod"]),
   191  					testutil.Unstructured(t, resources["deployment"]),
   192  				},
   193  			},
   194  			isError: false,
   195  		},
   196  		"two objects applied with their namespace; two object sets": {
   197  			objs: []*unstructured.Unstructured{
   198  				testutil.Unstructured(t, resources["deployment"]),
   199  				testutil.Unstructured(t, resources["namespace"]),
   200  				testutil.Unstructured(t, resources["secret"]),
   201  			},
   202  			expected: []object.UnstructuredSet{
   203  				{
   204  					testutil.Unstructured(t, resources["namespace"]),
   205  				},
   206  				{
   207  					testutil.Unstructured(t, resources["secret"]),
   208  					testutil.Unstructured(t, resources["deployment"]),
   209  				},
   210  			},
   211  			isError: false,
   212  		},
   213  		"two custom resources applied with their CRD; two object sets": {
   214  			objs: []*unstructured.Unstructured{
   215  				testutil.Unstructured(t, resources["crontab1"]),
   216  				testutil.Unstructured(t, resources["crontab2"]),
   217  				testutil.Unstructured(t, resources["crd"]),
   218  			},
   219  			expected: []object.UnstructuredSet{
   220  				{
   221  					testutil.Unstructured(t, resources["crd"]),
   222  				},
   223  				{
   224  					testutil.Unstructured(t, resources["crontab1"]),
   225  					testutil.Unstructured(t, resources["crontab2"]),
   226  				},
   227  			},
   228  			isError: false,
   229  		},
   230  		"two custom resources wit CRD and namespace; two object sets": {
   231  			objs: []*unstructured.Unstructured{
   232  				testutil.Unstructured(t, resources["crontab1"]),
   233  				testutil.Unstructured(t, resources["crontab2"]),
   234  				testutil.Unstructured(t, resources["namespace"]),
   235  				testutil.Unstructured(t, resources["crd"]),
   236  			},
   237  			expected: []object.UnstructuredSet{
   238  				{
   239  					testutil.Unstructured(t, resources["crd"]),
   240  					testutil.Unstructured(t, resources["namespace"]),
   241  				},
   242  				{
   243  					testutil.Unstructured(t, resources["crontab1"]),
   244  					testutil.Unstructured(t, resources["crontab2"]),
   245  				},
   246  			},
   247  			isError: false,
   248  		},
   249  		"two objects depends on each other is cyclic dependency": {
   250  			objs: []*unstructured.Unstructured{
   251  				testutil.Unstructured(t, resources["deployment"],
   252  					testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))),
   253  				testutil.Unstructured(t, resources["secret"],
   254  					testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["deployment"]))),
   255  			},
   256  			expected: []object.UnstructuredSet{},
   257  			isError:  true,
   258  		},
   259  		"three objects in cyclic dependency": {
   260  			objs: []*unstructured.Unstructured{
   261  				testutil.Unstructured(t, resources["deployment"],
   262  					testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))),
   263  				testutil.Unstructured(t, resources["secret"],
   264  					testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["pod"]))),
   265  				testutil.Unstructured(t, resources["pod"],
   266  					testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["deployment"]))),
   267  			},
   268  			expected: []object.UnstructuredSet{},
   269  			isError:  true,
   270  		},
   271  	}
   272  
   273  	for tn, tc := range testCases {
   274  		t.Run(tn, func(t *testing.T) {
   275  			actual, err := SortObjs(tc.objs)
   276  			if tc.isError {
   277  				assert.NotNil(t, err, "expected error, but received none")
   278  				return
   279  			}
   280  			assert.Nil(t, err, "unexpected error received")
   281  			verifyObjSets(t, tc.expected, actual)
   282  		})
   283  	}
   284  }
   285  
   286  func TestReverseSortObjs(t *testing.T) {
   287  	testCases := map[string]struct {
   288  		objs     []*unstructured.Unstructured
   289  		expected []object.UnstructuredSet
   290  		isError  bool
   291  	}{
   292  		"no objects returns no object sets": {
   293  			objs:     []*unstructured.Unstructured{},
   294  			expected: []object.UnstructuredSet{},
   295  			isError:  false,
   296  		},
   297  		"one object returns single object set": {
   298  			objs: []*unstructured.Unstructured{
   299  				testutil.Unstructured(t, resources["deployment"]),
   300  			},
   301  			expected: []object.UnstructuredSet{
   302  				{
   303  					testutil.Unstructured(t, resources["deployment"]),
   304  				},
   305  			},
   306  			isError: false,
   307  		},
   308  		"three objects depend on another; three single object sets in opposite order": {
   309  			objs: []*unstructured.Unstructured{
   310  				testutil.Unstructured(t, resources["deployment"],
   311  					testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))),
   312  				testutil.Unstructured(t, resources["secret"],
   313  					testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["pod"]))),
   314  				testutil.Unstructured(t, resources["pod"]),
   315  			},
   316  			expected: []object.UnstructuredSet{
   317  				{
   318  					testutil.Unstructured(t, resources["deployment"]),
   319  				},
   320  				{
   321  					testutil.Unstructured(t, resources["secret"]),
   322  				},
   323  				{
   324  					testutil.Unstructured(t, resources["pod"]),
   325  				},
   326  			},
   327  			isError: false,
   328  		},
   329  		"two objects applied with their namespace; two sets in opposite order": {
   330  			objs: []*unstructured.Unstructured{
   331  				testutil.Unstructured(t, resources["deployment"]),
   332  				testutil.Unstructured(t, resources["namespace"]),
   333  				testutil.Unstructured(t, resources["secret"]),
   334  			},
   335  			expected: []object.UnstructuredSet{
   336  				{
   337  					testutil.Unstructured(t, resources["secret"]),
   338  					testutil.Unstructured(t, resources["deployment"]),
   339  				},
   340  				{
   341  					testutil.Unstructured(t, resources["namespace"]),
   342  				},
   343  			},
   344  			isError: false,
   345  		},
   346  		"two custom resources wit CRD and namespace; two object sets in opposite order": {
   347  			objs: []*unstructured.Unstructured{
   348  				testutil.Unstructured(t, resources["crontab1"]),
   349  				testutil.Unstructured(t, resources["crontab2"]),
   350  				testutil.Unstructured(t, resources["namespace"]),
   351  				testutil.Unstructured(t, resources["crd"]),
   352  			},
   353  			expected: []object.UnstructuredSet{
   354  				{
   355  					testutil.Unstructured(t, resources["crontab1"]),
   356  					testutil.Unstructured(t, resources["crontab2"]),
   357  				},
   358  				{
   359  					testutil.Unstructured(t, resources["crd"]),
   360  					testutil.Unstructured(t, resources["namespace"]),
   361  				},
   362  			},
   363  			isError: false,
   364  		},
   365  	}
   366  
   367  	for tn, tc := range testCases {
   368  		t.Run(tn, func(t *testing.T) {
   369  			actual, err := ReverseSortObjs(tc.objs)
   370  			if tc.isError {
   371  				assert.NotNil(t, err, "expected error, but received none")
   372  				return
   373  			}
   374  			assert.Nil(t, err, "unexpected error received")
   375  			verifyObjSets(t, tc.expected, actual)
   376  		})
   377  	}
   378  }
   379  
   380  func TestDependencyGraph(t *testing.T) {
   381  	// Use a custom Asserter to customize the graph options
   382  	asserter := testutil.NewAsserter(
   383  		cmpopts.EquateErrors(),
   384  		graphComparer(),
   385  	)
   386  
   387  	testCases := map[string]struct {
   388  		objs          object.UnstructuredSet
   389  		graph         *Graph
   390  		expectedError error
   391  	}{
   392  		"no objects": {
   393  			objs:  object.UnstructuredSet{},
   394  			graph: New(),
   395  		},
   396  		"one object no dependencies": {
   397  			objs: object.UnstructuredSet{
   398  				testutil.Unstructured(t, resources["deployment"]),
   399  			},
   400  			graph: &Graph{
   401  				edges: map[object.ObjMetadata]object.ObjMetadataSet{
   402  					testutil.ToIdentifier(t, resources["deployment"]): {},
   403  				},
   404  				reverseEdges: map[object.ObjMetadata]object.ObjMetadataSet{
   405  					testutil.ToIdentifier(t, resources["deployment"]): {},
   406  				},
   407  			},
   408  		},
   409  		"two unrelated objects": {
   410  			objs: object.UnstructuredSet{
   411  				testutil.Unstructured(t, resources["deployment"]),
   412  				testutil.Unstructured(t, resources["secret"]),
   413  			},
   414  			graph: &Graph{
   415  				edges: map[object.ObjMetadata]object.ObjMetadataSet{
   416  					testutil.ToIdentifier(t, resources["deployment"]): {},
   417  					testutil.ToIdentifier(t, resources["secret"]):     {},
   418  				},
   419  				reverseEdges: map[object.ObjMetadata]object.ObjMetadataSet{
   420  					testutil.ToIdentifier(t, resources["deployment"]): {},
   421  					testutil.ToIdentifier(t, resources["secret"]):     {},
   422  				},
   423  			},
   424  		},
   425  		"two objects one dependency": {
   426  			objs: object.UnstructuredSet{
   427  				testutil.Unstructured(t, resources["deployment"],
   428  					testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))),
   429  				testutil.Unstructured(t, resources["secret"]),
   430  			},
   431  			graph: &Graph{
   432  				edges: map[object.ObjMetadata]object.ObjMetadataSet{
   433  					testutil.ToIdentifier(t, resources["deployment"]): {
   434  						testutil.ToIdentifier(t, resources["secret"]),
   435  					},
   436  					testutil.ToIdentifier(t, resources["secret"]): {},
   437  				},
   438  				reverseEdges: map[object.ObjMetadata]object.ObjMetadataSet{
   439  					testutil.ToIdentifier(t, resources["deployment"]): {},
   440  					testutil.ToIdentifier(t, resources["secret"]): {
   441  						testutil.ToIdentifier(t, resources["deployment"]),
   442  					},
   443  				},
   444  			},
   445  		},
   446  		"three objects two dependencies": {
   447  			objs: object.UnstructuredSet{
   448  				testutil.Unstructured(t, resources["deployment"],
   449  					testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))),
   450  				testutil.Unstructured(t, resources["secret"],
   451  					testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["pod"]))),
   452  				testutil.Unstructured(t, resources["pod"]),
   453  			},
   454  			graph: &Graph{
   455  				edges: map[object.ObjMetadata]object.ObjMetadataSet{
   456  					testutil.ToIdentifier(t, resources["deployment"]): {
   457  						testutil.ToIdentifier(t, resources["secret"]),
   458  					},
   459  					testutil.ToIdentifier(t, resources["secret"]): {
   460  						testutil.ToIdentifier(t, resources["pod"]),
   461  					},
   462  					testutil.ToIdentifier(t, resources["pod"]): {},
   463  				},
   464  				reverseEdges: map[object.ObjMetadata]object.ObjMetadataSet{
   465  					testutil.ToIdentifier(t, resources["pod"]): {
   466  						testutil.ToIdentifier(t, resources["secret"]),
   467  					},
   468  					testutil.ToIdentifier(t, resources["secret"]): {
   469  						testutil.ToIdentifier(t, resources["deployment"]),
   470  					},
   471  					testutil.ToIdentifier(t, resources["deployment"]): {},
   472  				},
   473  			},
   474  		},
   475  		"three objects two dependencies on the same object": {
   476  			objs: object.UnstructuredSet{
   477  				testutil.Unstructured(t, resources["deployment"],
   478  					testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))),
   479  				testutil.Unstructured(t, resources["pod"],
   480  					testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))),
   481  				testutil.Unstructured(t, resources["secret"]),
   482  			},
   483  			graph: &Graph{
   484  				edges: map[object.ObjMetadata]object.ObjMetadataSet{
   485  					testutil.ToIdentifier(t, resources["deployment"]): {
   486  						testutil.ToIdentifier(t, resources["secret"]),
   487  					},
   488  					testutil.ToIdentifier(t, resources["pod"]): {
   489  						testutil.ToIdentifier(t, resources["secret"]),
   490  					},
   491  					testutil.ToIdentifier(t, resources["secret"]): {},
   492  				},
   493  				reverseEdges: map[object.ObjMetadata]object.ObjMetadataSet{
   494  					testutil.ToIdentifier(t, resources["secret"]): {
   495  						testutil.ToIdentifier(t, resources["deployment"]),
   496  						testutil.ToIdentifier(t, resources["pod"]),
   497  					},
   498  					testutil.ToIdentifier(t, resources["pod"]):        {},
   499  					testutil.ToIdentifier(t, resources["deployment"]): {},
   500  				},
   501  			},
   502  		},
   503  		"two objects and their namespace": {
   504  			objs: object.UnstructuredSet{
   505  				testutil.Unstructured(t, resources["deployment"]),
   506  				testutil.Unstructured(t, resources["namespace"]),
   507  				testutil.Unstructured(t, resources["secret"]),
   508  			},
   509  			graph: &Graph{
   510  				edges: map[object.ObjMetadata]object.ObjMetadataSet{
   511  					testutil.ToIdentifier(t, resources["deployment"]): {
   512  						testutil.ToIdentifier(t, resources["namespace"]),
   513  					},
   514  					testutil.ToIdentifier(t, resources["secret"]): {
   515  						testutil.ToIdentifier(t, resources["namespace"]),
   516  					},
   517  					testutil.ToIdentifier(t, resources["namespace"]): {},
   518  				},
   519  				reverseEdges: map[object.ObjMetadata]object.ObjMetadataSet{
   520  					testutil.ToIdentifier(t, resources["namespace"]): {
   521  						testutil.ToIdentifier(t, resources["secret"]),
   522  						testutil.ToIdentifier(t, resources["deployment"]),
   523  					},
   524  					testutil.ToIdentifier(t, resources["secret"]):     {},
   525  					testutil.ToIdentifier(t, resources["deployment"]): {},
   526  				},
   527  			},
   528  		},
   529  		"two custom resources and their CRD": {
   530  			objs: object.UnstructuredSet{
   531  				testutil.Unstructured(t, resources["crontab1"]),
   532  				testutil.Unstructured(t, resources["crontab2"]),
   533  				testutil.Unstructured(t, resources["crd"]),
   534  			},
   535  			graph: &Graph{
   536  				edges: map[object.ObjMetadata]object.ObjMetadataSet{
   537  					testutil.ToIdentifier(t, resources["crontab1"]): {
   538  						testutil.ToIdentifier(t, resources["crd"]),
   539  					},
   540  					testutil.ToIdentifier(t, resources["crontab2"]): {
   541  						testutil.ToIdentifier(t, resources["crd"]),
   542  					},
   543  					testutil.ToIdentifier(t, resources["crd"]): {},
   544  				},
   545  				reverseEdges: map[object.ObjMetadata]object.ObjMetadataSet{
   546  					testutil.ToIdentifier(t, resources["crd"]): {
   547  						testutil.ToIdentifier(t, resources["crontab1"]),
   548  						testutil.ToIdentifier(t, resources["crontab2"]),
   549  					},
   550  					testutil.ToIdentifier(t, resources["crontab1"]): {},
   551  					testutil.ToIdentifier(t, resources["crontab2"]): {},
   552  				},
   553  			},
   554  		},
   555  		"two custom resources with their CRD and namespace": {
   556  			objs: object.UnstructuredSet{
   557  				testutil.Unstructured(t, resources["crontab1"]),
   558  				testutil.Unstructured(t, resources["crontab2"]),
   559  				testutil.Unstructured(t, resources["namespace"]),
   560  				testutil.Unstructured(t, resources["crd"]),
   561  			},
   562  			graph: &Graph{
   563  				edges: map[object.ObjMetadata]object.ObjMetadataSet{
   564  					testutil.ToIdentifier(t, resources["crontab1"]): {
   565  						testutil.ToIdentifier(t, resources["crd"]),
   566  						testutil.ToIdentifier(t, resources["namespace"]),
   567  					},
   568  					testutil.ToIdentifier(t, resources["crontab2"]): {
   569  						testutil.ToIdentifier(t, resources["crd"]),
   570  						testutil.ToIdentifier(t, resources["namespace"]),
   571  					},
   572  					testutil.ToIdentifier(t, resources["crd"]):       {},
   573  					testutil.ToIdentifier(t, resources["namespace"]): {},
   574  				},
   575  				reverseEdges: map[object.ObjMetadata]object.ObjMetadataSet{
   576  					testutil.ToIdentifier(t, resources["crd"]): {
   577  						testutil.ToIdentifier(t, resources["crontab1"]),
   578  						testutil.ToIdentifier(t, resources["crontab2"]),
   579  					},
   580  					testutil.ToIdentifier(t, resources["namespace"]): {
   581  						testutil.ToIdentifier(t, resources["crontab1"]),
   582  						testutil.ToIdentifier(t, resources["crontab2"]),
   583  					},
   584  					testutil.ToIdentifier(t, resources["crontab1"]): {},
   585  					testutil.ToIdentifier(t, resources["crontab2"]): {},
   586  				},
   587  			},
   588  		},
   589  		"two object cyclic dependency": {
   590  			objs: object.UnstructuredSet{
   591  				testutil.Unstructured(t, resources["deployment"],
   592  					testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))),
   593  				testutil.Unstructured(t, resources["secret"],
   594  					testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["deployment"]))),
   595  			},
   596  			graph: &Graph{
   597  				edges: map[object.ObjMetadata]object.ObjMetadataSet{
   598  					testutil.ToIdentifier(t, resources["deployment"]): {
   599  						testutil.ToIdentifier(t, resources["secret"]),
   600  					},
   601  					testutil.ToIdentifier(t, resources["secret"]): {
   602  						testutil.ToIdentifier(t, resources["deployment"]),
   603  					},
   604  				},
   605  				reverseEdges: map[object.ObjMetadata]object.ObjMetadataSet{
   606  					testutil.ToIdentifier(t, resources["secret"]): {
   607  						testutil.ToIdentifier(t, resources["deployment"]),
   608  					},
   609  					testutil.ToIdentifier(t, resources["deployment"]): {
   610  						testutil.ToIdentifier(t, resources["secret"]),
   611  					},
   612  				},
   613  			},
   614  		},
   615  		"three object cyclic dependency": {
   616  			objs: object.UnstructuredSet{
   617  				testutil.Unstructured(t, resources["deployment"],
   618  					testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))),
   619  				testutil.Unstructured(t, resources["secret"],
   620  					testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["pod"]))),
   621  				testutil.Unstructured(t, resources["pod"],
   622  					testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["deployment"]))),
   623  			},
   624  			graph: &Graph{
   625  				edges: map[object.ObjMetadata]object.ObjMetadataSet{
   626  					testutil.ToIdentifier(t, resources["deployment"]): {
   627  						testutil.ToIdentifier(t, resources["secret"]),
   628  					},
   629  					testutil.ToIdentifier(t, resources["secret"]): {
   630  						testutil.ToIdentifier(t, resources["pod"]),
   631  					},
   632  					testutil.ToIdentifier(t, resources["pod"]): {
   633  						testutil.ToIdentifier(t, resources["deployment"]),
   634  					},
   635  				},
   636  				reverseEdges: map[object.ObjMetadata]object.ObjMetadataSet{
   637  					testutil.ToIdentifier(t, resources["deployment"]): {
   638  						testutil.ToIdentifier(t, resources["pod"]),
   639  					},
   640  					testutil.ToIdentifier(t, resources["pod"]): {
   641  						testutil.ToIdentifier(t, resources["secret"]),
   642  					},
   643  					testutil.ToIdentifier(t, resources["secret"]): {
   644  						testutil.ToIdentifier(t, resources["deployment"]),
   645  					},
   646  				},
   647  			},
   648  		},
   649  	}
   650  
   651  	for tn, tc := range testCases {
   652  		t.Run(tn, func(t *testing.T) {
   653  			g, err := DependencyGraph(tc.objs)
   654  			if tc.expectedError != nil {
   655  				require.EqualError(t, err, tc.expectedError.Error())
   656  				return
   657  			}
   658  			assert.NoError(t, err)
   659  			asserter.Equal(t, tc.graph, g)
   660  		})
   661  	}
   662  }
   663  
   664  func TestHydrateSetList(t *testing.T) {
   665  	testCases := map[string]struct {
   666  		idSetList []object.ObjMetadataSet
   667  		objs      object.UnstructuredSet
   668  		expected  []object.UnstructuredSet
   669  	}{
   670  		"no object sets": {
   671  			idSetList: []object.ObjMetadataSet{},
   672  			expected:  nil,
   673  		},
   674  		"one object set": {
   675  			idSetList: []object.ObjMetadataSet{
   676  				{
   677  					testutil.ToIdentifier(t, resources["deployment"]),
   678  				},
   679  			},
   680  			objs: object.UnstructuredSet{
   681  				testutil.Unstructured(t, resources["deployment"]),
   682  			},
   683  			expected: []object.UnstructuredSet{
   684  				{
   685  					testutil.Unstructured(t, resources["deployment"]),
   686  				},
   687  			},
   688  		},
   689  		"two out of three": {
   690  			idSetList: []object.ObjMetadataSet{
   691  				{
   692  					testutil.ToIdentifier(t, resources["deployment"]),
   693  				},
   694  				{
   695  					testutil.ToIdentifier(t, resources["secret"]),
   696  				},
   697  				{
   698  					testutil.ToIdentifier(t, resources["pod"]),
   699  				},
   700  			},
   701  			objs: object.UnstructuredSet{
   702  				testutil.Unstructured(t, resources["deployment"]),
   703  				testutil.Unstructured(t, resources["pod"]),
   704  			},
   705  			expected: []object.UnstructuredSet{
   706  				{
   707  					testutil.Unstructured(t, resources["deployment"]),
   708  				},
   709  				{
   710  					testutil.Unstructured(t, resources["pod"]),
   711  				},
   712  			},
   713  		},
   714  		"two uneven sets": {
   715  			idSetList: []object.ObjMetadataSet{
   716  				{
   717  					testutil.ToIdentifier(t, resources["secret"]),
   718  					testutil.ToIdentifier(t, resources["deployment"]),
   719  				},
   720  				{
   721  					testutil.ToIdentifier(t, resources["namespace"]),
   722  				},
   723  			},
   724  			objs: object.UnstructuredSet{
   725  				testutil.Unstructured(t, resources["namespace"]),
   726  				testutil.Unstructured(t, resources["deployment"]),
   727  				testutil.Unstructured(t, resources["secret"]),
   728  				testutil.Unstructured(t, resources["pod"]),
   729  			},
   730  			expected: []object.UnstructuredSet{
   731  				{
   732  					testutil.Unstructured(t, resources["secret"]),
   733  					testutil.Unstructured(t, resources["deployment"]),
   734  				},
   735  				{
   736  					testutil.Unstructured(t, resources["namespace"]),
   737  				},
   738  			},
   739  		},
   740  		"one of two sets": {
   741  			idSetList: []object.ObjMetadataSet{
   742  				{
   743  					testutil.ToIdentifier(t, resources["namespace"]),
   744  					testutil.ToIdentifier(t, resources["crd"]),
   745  				},
   746  				{
   747  					testutil.ToIdentifier(t, resources["crontab1"]),
   748  					testutil.ToIdentifier(t, resources["crontab2"]),
   749  				},
   750  			},
   751  			objs: object.UnstructuredSet{
   752  				testutil.Unstructured(t, resources["namespace"]),
   753  				testutil.Unstructured(t, resources["crd"]),
   754  			},
   755  			expected: []object.UnstructuredSet{
   756  				{
   757  					testutil.Unstructured(t, resources["namespace"]),
   758  					testutil.Unstructured(t, resources["crd"]),
   759  				},
   760  			},
   761  		},
   762  	}
   763  
   764  	for tn, tc := range testCases {
   765  		t.Run(tn, func(t *testing.T) {
   766  			objSetList := HydrateSetList(tc.idSetList, tc.objs)
   767  			assert.Equal(t, tc.expected, objSetList)
   768  		})
   769  	}
   770  }
   771  
   772  func TestReverseSetList(t *testing.T) {
   773  	testCases := map[string]struct {
   774  		setList  []object.UnstructuredSet
   775  		expected []object.UnstructuredSet
   776  	}{
   777  		"no object sets": {
   778  			setList:  []object.UnstructuredSet{},
   779  			expected: []object.UnstructuredSet{},
   780  		},
   781  		"one object set": {
   782  			setList: []object.UnstructuredSet{
   783  				{
   784  					testutil.Unstructured(t, resources["deployment"]),
   785  				},
   786  			},
   787  			expected: []object.UnstructuredSet{
   788  				{
   789  					testutil.Unstructured(t, resources["deployment"]),
   790  				},
   791  			},
   792  		},
   793  		"three object sets": {
   794  			setList: []object.UnstructuredSet{
   795  				{
   796  					testutil.Unstructured(t, resources["deployment"]),
   797  				},
   798  				{
   799  					testutil.Unstructured(t, resources["secret"]),
   800  				},
   801  				{
   802  					testutil.Unstructured(t, resources["pod"]),
   803  				},
   804  			},
   805  			expected: []object.UnstructuredSet{
   806  				{
   807  					testutil.Unstructured(t, resources["pod"]),
   808  				},
   809  				{
   810  					testutil.Unstructured(t, resources["secret"]),
   811  				},
   812  				{
   813  					testutil.Unstructured(t, resources["deployment"]),
   814  				},
   815  			},
   816  		},
   817  		"two uneven sets": {
   818  			setList: []object.UnstructuredSet{
   819  				{
   820  					testutil.Unstructured(t, resources["secret"]),
   821  					testutil.Unstructured(t, resources["deployment"]),
   822  				},
   823  				{
   824  					testutil.Unstructured(t, resources["namespace"]),
   825  				},
   826  			},
   827  			expected: []object.UnstructuredSet{
   828  				{
   829  					testutil.Unstructured(t, resources["namespace"]),
   830  				},
   831  				{
   832  					testutil.Unstructured(t, resources["deployment"]),
   833  					testutil.Unstructured(t, resources["secret"]),
   834  				},
   835  			},
   836  		},
   837  		"two even sets": {
   838  			setList: []object.UnstructuredSet{
   839  				{
   840  					testutil.Unstructured(t, resources["crontab1"]),
   841  					testutil.Unstructured(t, resources["crontab2"]),
   842  				},
   843  				{
   844  					testutil.Unstructured(t, resources["crd"]),
   845  					testutil.Unstructured(t, resources["namespace"]),
   846  				},
   847  			},
   848  			expected: []object.UnstructuredSet{
   849  				{
   850  					testutil.Unstructured(t, resources["namespace"]),
   851  					testutil.Unstructured(t, resources["crd"]),
   852  				},
   853  				{
   854  					testutil.Unstructured(t, resources["crontab2"]),
   855  					testutil.Unstructured(t, resources["crontab1"]),
   856  				},
   857  			},
   858  		},
   859  	}
   860  
   861  	for tn, tc := range testCases {
   862  		t.Run(tn, func(t *testing.T) {
   863  			ReverseSetList(tc.setList)
   864  			assert.Equal(t, tc.expected, tc.setList)
   865  		})
   866  	}
   867  }
   868  
   869  func TestApplyTimeMutationEdges(t *testing.T) {
   870  	testCases := map[string]struct {
   871  		objs          []*unstructured.Unstructured
   872  		expected      []Edge
   873  		expectedError error
   874  	}{
   875  		"no objects adds no graph edges": {
   876  			objs:     []*unstructured.Unstructured{},
   877  			expected: []Edge{},
   878  		},
   879  		"no depends-on annotations adds no graph edges": {
   880  			objs: []*unstructured.Unstructured{
   881  				testutil.Unstructured(t, resources["deployment"]),
   882  			},
   883  			expected: []Edge{},
   884  		},
   885  		"no depends-on annotations, two objects, adds no graph edges": {
   886  			objs: []*unstructured.Unstructured{
   887  				testutil.Unstructured(t, resources["deployment"]),
   888  				testutil.Unstructured(t, resources["secret"]),
   889  			},
   890  			expected: []Edge{},
   891  		},
   892  		"two dependent objects, adds one edge": {
   893  			objs: []*unstructured.Unstructured{
   894  				testutil.Unstructured(
   895  					t,
   896  					resources["deployment"],
   897  					mutationutil.AddApplyTimeMutation(t, &mutation.ApplyTimeMutation{
   898  						{
   899  							SourceRef: mutation.ResourceReferenceFromObjMetadata(
   900  								testutil.ToIdentifier(t, resources["secret"]),
   901  							),
   902  							SourcePath: "unused",
   903  							TargetPath: "unused",
   904  							Token:      "unused",
   905  						},
   906  					}),
   907  				),
   908  				testutil.Unstructured(t, resources["secret"]),
   909  			},
   910  			expected: []Edge{
   911  				{
   912  					From: testutil.ToIdentifier(t, resources["deployment"]),
   913  					To:   testutil.ToIdentifier(t, resources["secret"]),
   914  				},
   915  			},
   916  		},
   917  		"three dependent objects, adds two edges": {
   918  			objs: []*unstructured.Unstructured{
   919  				testutil.Unstructured(
   920  					t,
   921  					resources["deployment"],
   922  					mutationutil.AddApplyTimeMutation(t, &mutation.ApplyTimeMutation{
   923  						{
   924  							SourceRef: mutation.ResourceReferenceFromObjMetadata(
   925  								testutil.ToIdentifier(t, resources["secret"]),
   926  							),
   927  							SourcePath: "unused",
   928  							TargetPath: "unused",
   929  							Token:      "unused",
   930  						},
   931  					}),
   932  				),
   933  				testutil.Unstructured(
   934  					t,
   935  					resources["pod"],
   936  					mutationutil.AddApplyTimeMutation(t, &mutation.ApplyTimeMutation{
   937  						{
   938  							SourceRef: mutation.ResourceReferenceFromObjMetadata(
   939  								testutil.ToIdentifier(t, resources["secret"]),
   940  							),
   941  							SourcePath: "unused",
   942  							TargetPath: "unused",
   943  							Token:      "unused",
   944  						},
   945  					}),
   946  				),
   947  				testutil.Unstructured(t, resources["secret"]),
   948  			},
   949  			expected: []Edge{
   950  				{
   951  					From: testutil.ToIdentifier(t, resources["deployment"]),
   952  					To:   testutil.ToIdentifier(t, resources["secret"]),
   953  				},
   954  				{
   955  					From: testutil.ToIdentifier(t, resources["pod"]),
   956  					To:   testutil.ToIdentifier(t, resources["secret"]),
   957  				},
   958  			},
   959  		},
   960  		"pod has two dependencies, adds two edges": {
   961  			objs: []*unstructured.Unstructured{
   962  				testutil.Unstructured(
   963  					t,
   964  					resources["pod"],
   965  					mutationutil.AddApplyTimeMutation(t, &mutation.ApplyTimeMutation{
   966  						{
   967  							SourceRef: mutation.ResourceReferenceFromObjMetadata(
   968  								testutil.ToIdentifier(t, resources["secret"]),
   969  							),
   970  							SourcePath: "unused",
   971  							TargetPath: "unused",
   972  							Token:      "unused",
   973  						},
   974  						{
   975  							SourceRef: mutation.ResourceReferenceFromObjMetadata(
   976  								testutil.ToIdentifier(t, resources["deployment"]),
   977  							),
   978  							SourcePath: "unused",
   979  							TargetPath: "unused",
   980  							Token:      "unused",
   981  						},
   982  					}),
   983  				),
   984  				testutil.Unstructured(t, resources["deployment"]),
   985  				testutil.Unstructured(t, resources["secret"]),
   986  			},
   987  			expected: []Edge{
   988  				{
   989  					From: testutil.ToIdentifier(t, resources["pod"]),
   990  					To:   testutil.ToIdentifier(t, resources["secret"]),
   991  				},
   992  				{
   993  					From: testutil.ToIdentifier(t, resources["pod"]),
   994  					To:   testutil.ToIdentifier(t, resources["deployment"]),
   995  				},
   996  			},
   997  		},
   998  		"error: invalid annotation": {
   999  			objs: []*unstructured.Unstructured{
  1000  				{
  1001  					Object: map[string]interface{}{
  1002  						"apiVersion": "apps/v1",
  1003  						"kind":       "Deployment",
  1004  						"metadata": map[string]interface{}{
  1005  							"name":      "foo",
  1006  							"namespace": "default",
  1007  							"annotations": map[string]interface{}{
  1008  								mutation.Annotation: "invalid-mutation",
  1009  							},
  1010  						},
  1011  					},
  1012  				},
  1013  			},
  1014  			expected: []Edge{},
  1015  			expectedError: validation.NewError(
  1016  				object.InvalidAnnotationError{
  1017  					Annotation: mutation.Annotation,
  1018  					Cause: errors.New("error unmarshaling JSON: " +
  1019  						"while decoding JSON: json: " +
  1020  						"cannot unmarshal string into Go value of type mutation.ApplyTimeMutation"),
  1021  				},
  1022  				object.ObjMetadata{
  1023  					GroupKind: schema.GroupKind{
  1024  						Group: "apps",
  1025  						Kind:  "Deployment",
  1026  					},
  1027  					Name:      "foo",
  1028  					Namespace: "default",
  1029  				},
  1030  			),
  1031  		},
  1032  		"error: dependency not in object set": {
  1033  			objs: []*unstructured.Unstructured{
  1034  				testutil.Unstructured(t, resources["pod"],
  1035  					mutationutil.AddApplyTimeMutation(t, &mutation.ApplyTimeMutation{
  1036  						{
  1037  							SourceRef: mutation.ResourceReferenceFromObjMetadata(
  1038  								testutil.ToIdentifier(t, resources["deployment"]),
  1039  							),
  1040  						},
  1041  					}),
  1042  				),
  1043  			},
  1044  			expected: []Edge{},
  1045  			expectedError: validation.NewError(
  1046  				object.InvalidAnnotationError{
  1047  					Annotation: mutation.Annotation,
  1048  					Cause: ExternalDependencyError{
  1049  						Edge: Edge{
  1050  							From: testutil.ToIdentifier(t, resources["pod"]),
  1051  							To:   testutil.ToIdentifier(t, resources["deployment"]),
  1052  						},
  1053  					},
  1054  				},
  1055  				object.ObjMetadata{
  1056  					GroupKind: schema.GroupKind{
  1057  						Group: "",
  1058  						Kind:  "Pod",
  1059  					},
  1060  					Name:      "test-pod",
  1061  					Namespace: "test-namespace",
  1062  				},
  1063  			),
  1064  		},
  1065  		"error: two invalid objects": {
  1066  			objs: []*unstructured.Unstructured{
  1067  				{
  1068  					Object: map[string]interface{}{
  1069  						"apiVersion": "apps/v1",
  1070  						"kind":       "Deployment",
  1071  						"metadata": map[string]interface{}{
  1072  							"name":      "foo",
  1073  							"namespace": "default",
  1074  							"annotations": map[string]interface{}{
  1075  								mutation.Annotation: "invalid-mutation",
  1076  							},
  1077  						},
  1078  					},
  1079  				},
  1080  				testutil.Unstructured(t, resources["pod"],
  1081  					mutationutil.AddApplyTimeMutation(t, &mutation.ApplyTimeMutation{
  1082  						{
  1083  							SourceRef: mutation.ResourceReferenceFromObjMetadata(
  1084  								testutil.ToIdentifier(t, resources["secret"]),
  1085  							),
  1086  						},
  1087  					}),
  1088  				),
  1089  			},
  1090  			expected: []Edge{},
  1091  			expectedError: multierror.New(
  1092  				validation.NewError(
  1093  					object.InvalidAnnotationError{
  1094  						Annotation: mutation.Annotation,
  1095  						Cause: errors.New("error unmarshaling JSON: " +
  1096  							"while decoding JSON: json: " +
  1097  							"cannot unmarshal string into Go value of type mutation.ApplyTimeMutation"),
  1098  					},
  1099  					object.ObjMetadata{
  1100  						GroupKind: schema.GroupKind{
  1101  							Group: "apps",
  1102  							Kind:  "Deployment",
  1103  						},
  1104  						Name:      "foo",
  1105  						Namespace: "default",
  1106  					},
  1107  				),
  1108  				validation.NewError(
  1109  					object.InvalidAnnotationError{
  1110  						Annotation: mutation.Annotation,
  1111  						Cause: ExternalDependencyError{
  1112  							Edge: Edge{
  1113  								From: testutil.ToIdentifier(t, resources["pod"]),
  1114  								To:   testutil.ToIdentifier(t, resources["secret"]),
  1115  							},
  1116  						},
  1117  					},
  1118  					object.ObjMetadata{
  1119  						GroupKind: schema.GroupKind{
  1120  							Group: "",
  1121  							Kind:  "Pod",
  1122  						},
  1123  						Name:      "test-pod",
  1124  						Namespace: "test-namespace",
  1125  					},
  1126  				),
  1127  			),
  1128  		},
  1129  	}
  1130  
  1131  	for tn, tc := range testCases {
  1132  		t.Run(tn, func(t *testing.T) {
  1133  			g := New()
  1134  			ids := object.UnstructuredSetToObjMetadataSet(tc.objs)
  1135  			err := addApplyTimeMutationEdges(g, tc.objs, ids)
  1136  			if tc.expectedError != nil {
  1137  				assert.EqualError(t, err, tc.expectedError.Error())
  1138  			} else {
  1139  				assert.NoError(t, err)
  1140  			}
  1141  			actual := edgeMapToList(g.edges)
  1142  			verifyEdges(t, tc.expected, actual)
  1143  		})
  1144  	}
  1145  }
  1146  
  1147  func TestAddDependsOnEdges(t *testing.T) {
  1148  	testCases := map[string]struct {
  1149  		objs          []*unstructured.Unstructured
  1150  		expected      []Edge
  1151  		expectedError error
  1152  	}{
  1153  		"no objects adds no graph edges": {
  1154  			objs:     []*unstructured.Unstructured{},
  1155  			expected: []Edge{},
  1156  		},
  1157  		"no depends-on annotations adds no graph edges": {
  1158  			objs: []*unstructured.Unstructured{
  1159  				testutil.Unstructured(t, resources["deployment"]),
  1160  			},
  1161  			expected: []Edge{},
  1162  		},
  1163  		"no depends-on annotations, two objects, adds no graph edges": {
  1164  			objs: []*unstructured.Unstructured{
  1165  				testutil.Unstructured(t, resources["deployment"]),
  1166  				testutil.Unstructured(t, resources["secret"]),
  1167  			},
  1168  			expected: []Edge{},
  1169  		},
  1170  		"two dependent objects, adds one edge": {
  1171  			objs: []*unstructured.Unstructured{
  1172  				testutil.Unstructured(t, resources["deployment"],
  1173  					testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))),
  1174  				testutil.Unstructured(t, resources["secret"]),
  1175  			},
  1176  			expected: []Edge{
  1177  				{
  1178  					From: testutil.ToIdentifier(t, resources["deployment"]),
  1179  					To:   testutil.ToIdentifier(t, resources["secret"]),
  1180  				},
  1181  			},
  1182  		},
  1183  		"three dependent objects, adds two edges": {
  1184  			objs: []*unstructured.Unstructured{
  1185  				testutil.Unstructured(t, resources["deployment"],
  1186  					testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))),
  1187  				testutil.Unstructured(t, resources["pod"],
  1188  					testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))),
  1189  				testutil.Unstructured(t, resources["secret"]),
  1190  			},
  1191  			expected: []Edge{
  1192  				{
  1193  					From: testutil.ToIdentifier(t, resources["deployment"]),
  1194  					To:   testutil.ToIdentifier(t, resources["secret"]),
  1195  				},
  1196  				{
  1197  					From: testutil.ToIdentifier(t, resources["pod"]),
  1198  					To:   testutil.ToIdentifier(t, resources["secret"]),
  1199  				},
  1200  			},
  1201  		},
  1202  		"pod has two dependencies, adds two edges": {
  1203  			objs: []*unstructured.Unstructured{
  1204  				testutil.Unstructured(t, resources["pod"],
  1205  					testutil.AddDependsOn(t,
  1206  						testutil.ToIdentifier(t, resources["secret"]),
  1207  						testutil.ToIdentifier(t, resources["deployment"]),
  1208  					),
  1209  				),
  1210  				testutil.Unstructured(t, resources["deployment"]),
  1211  				testutil.Unstructured(t, resources["secret"]),
  1212  			},
  1213  			expected: []Edge{
  1214  				{
  1215  					From: testutil.ToIdentifier(t, resources["pod"]),
  1216  					To:   testutil.ToIdentifier(t, resources["secret"]),
  1217  				},
  1218  				{
  1219  					From: testutil.ToIdentifier(t, resources["pod"]),
  1220  					To:   testutil.ToIdentifier(t, resources["deployment"]),
  1221  				},
  1222  			},
  1223  		},
  1224  		"error: invalid annotation": {
  1225  			objs: []*unstructured.Unstructured{
  1226  				{
  1227  					Object: map[string]interface{}{
  1228  						"apiVersion": "apps/v1",
  1229  						"kind":       "Deployment",
  1230  						"metadata": map[string]interface{}{
  1231  							"name":      "foo",
  1232  							"namespace": "default",
  1233  							"annotations": map[string]interface{}{
  1234  								dependson.Annotation: "invalid-obj-ref",
  1235  							},
  1236  						},
  1237  					},
  1238  				},
  1239  			},
  1240  			expected: []Edge{},
  1241  			expectedError: validation.NewError(
  1242  				object.InvalidAnnotationError{
  1243  					Annotation: dependson.Annotation,
  1244  					Cause: errors.New("failed to parse object reference (index: 0): " +
  1245  						`expected 3 or 5 fields, found 1: "invalid-obj-ref"`),
  1246  				},
  1247  				object.ObjMetadata{
  1248  					GroupKind: schema.GroupKind{
  1249  						Group: "apps",
  1250  						Kind:  "Deployment",
  1251  					},
  1252  					Name:      "foo",
  1253  					Namespace: "default",
  1254  				},
  1255  			),
  1256  		},
  1257  		"error: duplicate reference": {
  1258  			objs: []*unstructured.Unstructured{
  1259  				testutil.Unstructured(t, resources["pod"],
  1260  					testutil.AddDependsOn(t,
  1261  						testutil.ToIdentifier(t, resources["deployment"]),
  1262  						testutil.ToIdentifier(t, resources["deployment"]),
  1263  					),
  1264  				),
  1265  				testutil.Unstructured(t, resources["deployment"]),
  1266  			},
  1267  			expected: []Edge{
  1268  				{
  1269  					From: testutil.ToIdentifier(t, resources["pod"]),
  1270  					To:   testutil.ToIdentifier(t, resources["deployment"]),
  1271  				},
  1272  			},
  1273  			expectedError: validation.NewError(
  1274  				object.InvalidAnnotationError{
  1275  					Annotation: dependson.Annotation,
  1276  					Cause: DuplicateDependencyError{
  1277  						Edge: Edge{
  1278  							From: testutil.ToIdentifier(t, resources["pod"]),
  1279  							To:   testutil.ToIdentifier(t, resources["deployment"]),
  1280  						},
  1281  					},
  1282  				},
  1283  				object.ObjMetadata{
  1284  					GroupKind: schema.GroupKind{
  1285  						Group: "",
  1286  						Kind:  "Pod",
  1287  					},
  1288  					Name:      "test-pod",
  1289  					Namespace: "test-namespace",
  1290  				},
  1291  			),
  1292  		},
  1293  		"error: external dependency": {
  1294  			objs: []*unstructured.Unstructured{
  1295  				testutil.Unstructured(t, resources["pod"],
  1296  					testutil.AddDependsOn(t,
  1297  						testutil.ToIdentifier(t, resources["deployment"]),
  1298  					),
  1299  				),
  1300  			},
  1301  			expected: []Edge{},
  1302  			expectedError: validation.NewError(
  1303  				object.InvalidAnnotationError{
  1304  					Annotation: dependson.Annotation,
  1305  					Cause: ExternalDependencyError{
  1306  						Edge: Edge{
  1307  							From: testutil.ToIdentifier(t, resources["pod"]),
  1308  							To:   testutil.ToIdentifier(t, resources["deployment"]),
  1309  						},
  1310  					},
  1311  				},
  1312  				object.ObjMetadata{
  1313  					GroupKind: schema.GroupKind{
  1314  						Group: "",
  1315  						Kind:  "Pod",
  1316  					},
  1317  					Name:      "test-pod",
  1318  					Namespace: "test-namespace",
  1319  				},
  1320  			),
  1321  		},
  1322  		"error: two invalid objects": {
  1323  			objs: []*unstructured.Unstructured{
  1324  				{
  1325  					Object: map[string]interface{}{
  1326  						"apiVersion": "apps/v1",
  1327  						"kind":       "Deployment",
  1328  						"metadata": map[string]interface{}{
  1329  							"name":      "foo",
  1330  							"namespace": "default",
  1331  							"annotations": map[string]interface{}{
  1332  								dependson.Annotation: "invalid-obj-ref",
  1333  							},
  1334  						},
  1335  					},
  1336  				},
  1337  				testutil.Unstructured(t, resources["pod"],
  1338  					testutil.AddDependsOn(t,
  1339  						testutil.ToIdentifier(t, resources["secret"]),
  1340  					),
  1341  				),
  1342  			},
  1343  			expected: []Edge{},
  1344  			expectedError: multierror.New(
  1345  				validation.NewError(
  1346  					object.InvalidAnnotationError{
  1347  						Annotation: dependson.Annotation,
  1348  						Cause: errors.New("failed to parse object reference (index: 0): " +
  1349  							`expected 3 or 5 fields, found 1: "invalid-obj-ref"`),
  1350  					},
  1351  					object.ObjMetadata{
  1352  						GroupKind: schema.GroupKind{
  1353  							Group: "apps",
  1354  							Kind:  "Deployment",
  1355  						},
  1356  						Name:      "foo",
  1357  						Namespace: "default",
  1358  					},
  1359  				),
  1360  				validation.NewError(
  1361  					object.InvalidAnnotationError{
  1362  						Annotation: dependson.Annotation,
  1363  						Cause: ExternalDependencyError{
  1364  							Edge: Edge{
  1365  								From: testutil.ToIdentifier(t, resources["pod"]),
  1366  								To:   testutil.ToIdentifier(t, resources["secret"]),
  1367  							},
  1368  						},
  1369  					},
  1370  					object.ObjMetadata{
  1371  						GroupKind: schema.GroupKind{
  1372  							Group: "",
  1373  							Kind:  "Pod",
  1374  						},
  1375  						Name:      "test-pod",
  1376  						Namespace: "test-namespace",
  1377  					},
  1378  				),
  1379  			),
  1380  		},
  1381  		"error: one object with two errors": {
  1382  			objs: []*unstructured.Unstructured{
  1383  				testutil.Unstructured(t, resources["pod"],
  1384  					testutil.AddDependsOn(t,
  1385  						testutil.ToIdentifier(t, resources["deployment"]),
  1386  						testutil.ToIdentifier(t, resources["deployment"]),
  1387  					),
  1388  				),
  1389  			},
  1390  			expected: []Edge{},
  1391  			expectedError: validation.NewError(
  1392  				multierror.New(
  1393  					object.InvalidAnnotationError{
  1394  						Annotation: dependson.Annotation,
  1395  						Cause: ExternalDependencyError{
  1396  							Edge: Edge{
  1397  								From: testutil.ToIdentifier(t, resources["pod"]),
  1398  								To:   testutil.ToIdentifier(t, resources["deployment"]),
  1399  							},
  1400  						},
  1401  					},
  1402  					object.InvalidAnnotationError{
  1403  						Annotation: dependson.Annotation,
  1404  						Cause: DuplicateDependencyError{
  1405  							Edge: Edge{
  1406  								From: testutil.ToIdentifier(t, resources["pod"]),
  1407  								To:   testutil.ToIdentifier(t, resources["deployment"]),
  1408  							},
  1409  						},
  1410  					},
  1411  				),
  1412  				object.ObjMetadata{
  1413  					GroupKind: schema.GroupKind{
  1414  						Group: "",
  1415  						Kind:  "Pod",
  1416  					},
  1417  					Name:      "test-pod",
  1418  					Namespace: "test-namespace",
  1419  				},
  1420  			),
  1421  		},
  1422  	}
  1423  
  1424  	for tn, tc := range testCases {
  1425  		t.Run(tn, func(t *testing.T) {
  1426  			g := New()
  1427  			ids := object.UnstructuredSetToObjMetadataSet(tc.objs)
  1428  			err := addDependsOnEdges(g, tc.objs, ids)
  1429  			if tc.expectedError != nil {
  1430  				assert.EqualError(t, err, tc.expectedError.Error())
  1431  			} else {
  1432  				assert.NoError(t, err)
  1433  			}
  1434  			actual := edgeMapToList(g.edges)
  1435  			verifyEdges(t, tc.expected, actual)
  1436  		})
  1437  	}
  1438  }
  1439  
  1440  func TestAddNamespaceEdges(t *testing.T) {
  1441  	testCases := map[string]struct {
  1442  		objs     []*unstructured.Unstructured
  1443  		expected []Edge
  1444  	}{
  1445  		"no namespace objects adds no graph edges": {
  1446  			objs:     []*unstructured.Unstructured{},
  1447  			expected: []Edge{},
  1448  		},
  1449  		"single namespace adds no graph edges": {
  1450  			objs: []*unstructured.Unstructured{
  1451  				testutil.Unstructured(t, resources["namespace"]),
  1452  			},
  1453  			expected: []Edge{},
  1454  		},
  1455  		"pod within namespace adds one edge": {
  1456  			objs: []*unstructured.Unstructured{
  1457  				testutil.Unstructured(t, resources["namespace"]),
  1458  				testutil.Unstructured(t, resources["pod"]),
  1459  			},
  1460  			expected: []Edge{
  1461  				{
  1462  					From: testutil.ToIdentifier(t, resources["pod"]),
  1463  					To:   testutil.ToIdentifier(t, resources["namespace"]),
  1464  				},
  1465  			},
  1466  		},
  1467  		"pod not in namespace does not add edge": {
  1468  			objs: []*unstructured.Unstructured{
  1469  				testutil.Unstructured(t, resources["namespace"]),
  1470  				testutil.Unstructured(t, resources["default-pod"]),
  1471  			},
  1472  			expected: []Edge{},
  1473  		},
  1474  		"pod, secret, and namespace adds two edges": {
  1475  			objs: []*unstructured.Unstructured{
  1476  				testutil.Unstructured(t, resources["namespace"]),
  1477  				testutil.Unstructured(t, resources["secret"]),
  1478  				testutil.Unstructured(t, resources["pod"]),
  1479  			},
  1480  			expected: []Edge{
  1481  				{
  1482  					From: testutil.ToIdentifier(t, resources["pod"]),
  1483  					To:   testutil.ToIdentifier(t, resources["namespace"]),
  1484  				},
  1485  				{
  1486  					From: testutil.ToIdentifier(t, resources["secret"]),
  1487  					To:   testutil.ToIdentifier(t, resources["namespace"]),
  1488  				},
  1489  			},
  1490  		},
  1491  		"one pod in namespace, one not, adds only one edge": {
  1492  			objs: []*unstructured.Unstructured{
  1493  				testutil.Unstructured(t, resources["namespace"]),
  1494  				testutil.Unstructured(t, resources["default-pod"]),
  1495  				testutil.Unstructured(t, resources["pod"]),
  1496  			},
  1497  			expected: []Edge{
  1498  				{
  1499  					From: testutil.ToIdentifier(t, resources["pod"]),
  1500  					To:   testutil.ToIdentifier(t, resources["namespace"]),
  1501  				},
  1502  			},
  1503  		},
  1504  	}
  1505  
  1506  	for tn, tc := range testCases {
  1507  		t.Run(tn, func(t *testing.T) {
  1508  			g := New()
  1509  			ids := object.UnstructuredSetToObjMetadataSet(tc.objs)
  1510  			addNamespaceEdges(g, tc.objs, ids)
  1511  			actual := edgeMapToList(g.edges)
  1512  			verifyEdges(t, tc.expected, actual)
  1513  		})
  1514  	}
  1515  }
  1516  
  1517  func TestAddCRDEdges(t *testing.T) {
  1518  	testCases := map[string]struct {
  1519  		objs     []*unstructured.Unstructured
  1520  		expected []Edge
  1521  	}{
  1522  		"no CRD objects adds no graph edges": {
  1523  			objs:     []*unstructured.Unstructured{},
  1524  			expected: []Edge{},
  1525  		},
  1526  		"single namespace adds no graph edges": {
  1527  			objs: []*unstructured.Unstructured{
  1528  				testutil.Unstructured(t, resources["crd"]),
  1529  			},
  1530  			expected: []Edge{},
  1531  		},
  1532  		"two custom resources adds no graph edges": {
  1533  			objs: []*unstructured.Unstructured{
  1534  				testutil.Unstructured(t, resources["crontab1"]),
  1535  				testutil.Unstructured(t, resources["crontab2"]),
  1536  			},
  1537  			expected: []Edge{},
  1538  		},
  1539  		"two custom resources with crd adds two edges": {
  1540  			objs: []*unstructured.Unstructured{
  1541  				testutil.Unstructured(t, resources["crd"]),
  1542  				testutil.Unstructured(t, resources["crontab1"]),
  1543  				testutil.Unstructured(t, resources["crontab2"]),
  1544  			},
  1545  			expected: []Edge{
  1546  				{
  1547  					From: testutil.ToIdentifier(t, resources["crontab1"]),
  1548  					To:   testutil.ToIdentifier(t, resources["crd"]),
  1549  				},
  1550  				{
  1551  					From: testutil.ToIdentifier(t, resources["crontab2"]),
  1552  					To:   testutil.ToIdentifier(t, resources["crd"]),
  1553  				},
  1554  			},
  1555  		},
  1556  	}
  1557  
  1558  	for tn, tc := range testCases {
  1559  		t.Run(tn, func(t *testing.T) {
  1560  			g := New()
  1561  			ids := object.UnstructuredSetToObjMetadataSet(tc.objs)
  1562  			addCRDEdges(g, tc.objs, ids)
  1563  			actual := edgeMapToList(g.edges)
  1564  			verifyEdges(t, tc.expected, actual)
  1565  		})
  1566  	}
  1567  }
  1568  
  1569  // verifyObjSets ensures the expected and actual slice of object sets are the same,
  1570  // and the sets are in order.
  1571  func verifyObjSets(t *testing.T, expected []object.UnstructuredSet, actual []object.UnstructuredSet) {
  1572  	if len(expected) != len(actual) {
  1573  		t.Fatalf("expected (%d) object sets, got (%d)", len(expected), len(actual))
  1574  		return
  1575  	}
  1576  	// Order matters
  1577  	for i := range expected {
  1578  		expectedSet := expected[i]
  1579  		actualSet := actual[i]
  1580  		if len(expectedSet) != len(actualSet) {
  1581  			t.Fatalf("set %d: expected object size (%d), got (%d)", i, len(expectedSet), len(actualSet))
  1582  			return
  1583  		}
  1584  		for _, actualObj := range actualSet {
  1585  			if !containsObjs(expectedSet, actualObj) {
  1586  				t.Fatalf("set #%d: actual object (%v) not found in set of expected objects", i, actualObj)
  1587  				return
  1588  			}
  1589  		}
  1590  	}
  1591  }
  1592  
  1593  // containsUnstructured returns true if the passed object is within the passed
  1594  // slice of objects; false otherwise. Order is not important.
  1595  func containsObjs(objs []*unstructured.Unstructured, obj *unstructured.Unstructured) bool {
  1596  	ids := object.UnstructuredSetToObjMetadataSet(objs)
  1597  	id := object.UnstructuredToObjMetadata(obj)
  1598  	for _, i := range ids {
  1599  		if i == id {
  1600  			return true
  1601  		}
  1602  	}
  1603  	return false
  1604  }
  1605  
  1606  // verifyEdges ensures the slices of directed Edges contain the same elements.
  1607  // Order is not important.
  1608  func verifyEdges(t *testing.T, expected []Edge, actual []Edge) {
  1609  	if len(expected) != len(actual) {
  1610  		t.Fatalf("expected (%d) edges, got (%d)", len(expected), len(actual))
  1611  		return
  1612  	}
  1613  	for _, actualEdge := range actual {
  1614  		if !containsEdge(expected, actualEdge) {
  1615  			t.Errorf("actual Edge (%v) not found in expected Edges", actualEdge)
  1616  			return
  1617  		}
  1618  	}
  1619  }
  1620  
  1621  // containsEdge return true if the passed Edge is in the slice of Edges;
  1622  // false otherwise.
  1623  func containsEdge(edges []Edge, edge Edge) bool {
  1624  	for _, e := range edges {
  1625  		if e.To == edge.To && e.From == edge.From {
  1626  			return true
  1627  		}
  1628  	}
  1629  	return false
  1630  }
  1631  
  1632  // waitTaskComparer allows comparion of WaitTasks, ignoring private fields.
  1633  func graphComparer() cmp.Option {
  1634  	return cmp.Comparer(func(x, y *Graph) bool {
  1635  		if x == nil {
  1636  			return y == nil
  1637  		}
  1638  		if y == nil {
  1639  			return false
  1640  		}
  1641  		return cmp.Equal(x.edges, y.edges) &&
  1642  			cmp.Equal(x.reverseEdges, y.reverseEdges)
  1643  	})
  1644  }
  1645  

View as plain text