istio.io/istio@v0.0.0-20240520182934-d79c90f27776/operator/pkg/object/objects_test.go (about)

     1  // Copyright Istio Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package object
    16  
    17  import (
    18  	"fmt"
    19  	"io"
    20  	"os"
    21  	"reflect"
    22  	"strings"
    23  	"testing"
    24  
    25  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    26  	k8syaml "k8s.io/apimachinery/pkg/util/yaml"
    27  
    28  	"istio.io/istio/operator/pkg/util"
    29  	"istio.io/istio/pkg/test/util/assert"
    30  )
    31  
    32  func TestHash(t *testing.T) {
    33  	hashTests := []struct {
    34  		desc      string
    35  		kind      string
    36  		namespace string
    37  		name      string
    38  		want      string
    39  	}{
    40  		{"CalculateHashForObjectWithNormalCharacter", "Service", "default", "ingressgateway", "Service:default:ingressgateway"},
    41  		{"CalculateHashForObjectWithDash", "Deployment", "istio-system", "istio-pilot", "Deployment:istio-system:istio-pilot"},
    42  		{"CalculateHashForObjectWithDot", "ConfigMap", "istio-system", "my.config", "ConfigMap:istio-system:my.config"},
    43  	}
    44  
    45  	for _, tt := range hashTests {
    46  		t.Run(tt.desc, func(t *testing.T) {
    47  			got := Hash(tt.kind, tt.namespace, tt.name)
    48  			if got != tt.want {
    49  				t.Errorf("Hash(%s): got %s for kind %s, namespace %s, name %s, want %s", tt.desc, got, tt.kind, tt.namespace, tt.name, tt.want)
    50  			}
    51  		})
    52  	}
    53  }
    54  
    55  func TestFromHash(t *testing.T) {
    56  	hashTests := []struct {
    57  		desc      string
    58  		hash      string
    59  		kind      string
    60  		namespace string
    61  		name      string
    62  	}{
    63  		{"ParseHashWithNormalCharacter", "Service:default:ingressgateway", "Service", "default", "ingressgateway"},
    64  		{"ParseHashForObjectWithDash", "Deployment:istio-system:istio-pilot", "Deployment", "istio-system", "istio-pilot"},
    65  		{"ParseHashForObjectWithDot", "ConfigMap:istio-system:my.config", "ConfigMap", "istio-system", "my.config"},
    66  		{"InvalidHash", "test", "Bad hash string: test", "", ""},
    67  	}
    68  
    69  	for _, tt := range hashTests {
    70  		t.Run(tt.desc, func(t *testing.T) {
    71  			k, ns, name := FromHash(tt.hash)
    72  			if k != tt.kind || ns != tt.namespace || name != tt.name {
    73  				t.Errorf("FromHash(%s): got kind %s, namespace %s, name %s, want kind %s, namespace %s, name %s", tt.desc, k, ns, name, tt.kind, tt.namespace, tt.name)
    74  			}
    75  		})
    76  	}
    77  }
    78  
    79  func TestHashNameKind(t *testing.T) {
    80  	hashNameKindTests := []struct {
    81  		desc string
    82  		kind string
    83  		name string
    84  		want string
    85  	}{
    86  		{"CalculateHashNameKindForObjectWithNormalCharacter", "Service", "ingressgateway", "Service:ingressgateway"},
    87  		{"CalculateHashNameKindForObjectWithDash", "Deployment", "istio-pilot", "Deployment:istio-pilot"},
    88  		{"CalculateHashNameKindForObjectWithDot", "ConfigMap", "my.config", "ConfigMap:my.config"},
    89  	}
    90  
    91  	for _, tt := range hashNameKindTests {
    92  		t.Run(tt.desc, func(t *testing.T) {
    93  			got := HashNameKind(tt.kind, tt.name)
    94  			if got != tt.want {
    95  				t.Errorf("HashNameKind(%s): got %s for kind %s, name %s, want %s", tt.desc, got, tt.kind, tt.name, tt.want)
    96  			}
    97  		})
    98  	}
    99  }
   100  
   101  func TestParseJSONToK8sObject(t *testing.T) {
   102  	testDeploymentJSON := `{
   103  	"apiVersion": "apps/v1",
   104  	"kind": "Deployment",
   105  	"metadata": {
   106  		"name": "istio-citadel",
   107  		"namespace": "istio-system",
   108  		"labels": {
   109  			"istio": "citadel"
   110  		}
   111  	},
   112  	"spec": {
   113  		"replicas": 1,
   114  		"selector": {
   115  			"matchLabels": {
   116  				"istio": "citadel"
   117  			}
   118  		},
   119  		"template": {
   120  			"metadata": {
   121  				"labels": {
   122  					"istio": "citadel"
   123  				}
   124  			},
   125  			"spec": {
   126  				"containers": [
   127  					{
   128  						"name": "citadel",
   129  						"image": "docker.io/istio/citadel:1.1.8",
   130  						"args": [
   131  							"--append-dns-names=true",
   132  							"--grpc-port=8060",
   133  							"--grpc-hostname=citadel",
   134  							"--citadel-storage-namespace=istio-system",
   135  							"--custom-dns-names=istio-pilot-service-account.istio-system:istio-pilot.istio-system",
   136  							"--monitoring-port=15014",
   137  							"--self-signed-ca=true"
   138  					  ]
   139  					}
   140  				]
   141  			}
   142  		}
   143  	}
   144  }`
   145  	testPodJSON := `{
   146  	"apiVersion": "v1",
   147  	"kind": "Pod",
   148  	"metadata": {
   149  		"name": "istio-galley-75bcd59768-hpt5t",
   150  		"namespace": "istio-system",
   151  		"labels": {
   152  			"istio": "galley"
   153  		}
   154  	},
   155  	"spec": {
   156  		"containers": [
   157  			{
   158  				"name": "galley",
   159  				"image": "docker.io/istio/galley:1.1.8",
   160  				"command": [
   161  					"/usr/local/bin/galley",
   162  					"server",
   163  					"--meshConfigFile=/etc/mesh-config/mesh",
   164  					"--livenessProbeInterval=1s",
   165  					"--livenessProbePath=/healthliveness",
   166  					"--readinessProbePath=/healthready",
   167  					"--readinessProbeInterval=1s",
   168  					"--deployment-namespace=istio-system",
   169  					"--insecure=true",
   170  					"--validation-webhook-config-file",
   171  					"/etc/config/validatingwebhookconfiguration.yaml",
   172  					"--monitoringPort=15014",
   173  					"--log_output_level=default:info"
   174  				],
   175  				"ports": [
   176  					{
   177  						"containerPort": 443,
   178  						"protocol": "TCP"
   179  					},
   180  					{
   181  						"containerPort": 15014,
   182  						"protocol": "TCP"
   183  					},
   184  					{
   185  						"containerPort": 9901,
   186  						"protocol": "TCP"
   187  					}
   188  				]
   189  			}
   190  		]
   191  	}
   192  }`
   193  	testServiceJSON := `{
   194  	"apiVersion": "v1",
   195  	"kind": "Service",
   196  	"metadata": {
   197  			"labels": {
   198  					"app": "pilot"
   199  			},
   200  			"name": "istio-pilot",
   201  			"namespace": "istio-system"
   202  	},
   203  	"spec": {
   204  			"clusterIP": "10.102.230.31",
   205  			"ports": [
   206  					{
   207  							"name": "grpc-xds",
   208  							"port": 15010,
   209  							"protocol": "TCP",
   210  							"targetPort": 15010
   211  					},
   212  					{
   213  							"name": "https-xds",
   214  							"port": 15011,
   215  							"protocol": "TCP",
   216  							"targetPort": 15011
   217  					},
   218  					{
   219  							"name": "http-legacy-discovery",
   220  							"port": 8080,
   221  							"protocol": "TCP",
   222  							"targetPort": 8080
   223  					},
   224  					{
   225  							"name": "http-monitoring",
   226  							"port": 15014,
   227  							"protocol": "TCP",
   228  							"targetPort": 15014
   229  					}
   230  			],
   231  			"selector": {
   232  					"istio": "pilot"
   233  			},
   234  			"sessionAffinity": "None",
   235  			"type": "ClusterIP"
   236  	}
   237  }`
   238  
   239  	testInvalidJSON := `invalid json`
   240  
   241  	parseJSONToK8sObjectTests := []struct {
   242  		desc          string
   243  		objString     string
   244  		wantGroup     string
   245  		wantKind      string
   246  		wantName      string
   247  		wantNamespace string
   248  		wantErr       bool
   249  	}{
   250  		{"ParseJsonToK8sDeployment", testDeploymentJSON, "apps", "Deployment", "istio-citadel", "istio-system", false},
   251  		{"ParseJsonToK8sPod", testPodJSON, "", "Pod", "istio-galley-75bcd59768-hpt5t", "istio-system", false},
   252  		{"ParseJsonToK8sService", testServiceJSON, "", "Service", "istio-pilot", "istio-system", false},
   253  		{"ParseJsonError", testInvalidJSON, "", "", "", "", true},
   254  	}
   255  
   256  	for _, tt := range parseJSONToK8sObjectTests {
   257  		t.Run(tt.desc, func(t *testing.T) {
   258  			k8sObj, err := ParseJSONToK8sObject([]byte(tt.objString))
   259  			if err == nil {
   260  				if tt.wantErr {
   261  					t.Errorf("ParseJsonToK8sObject(%s): should be error", tt.desc)
   262  				}
   263  				k8sObjStr := k8sObj.YAMLDebugString()
   264  				if k8sObj.Group != tt.wantGroup {
   265  					t.Errorf("ParseJsonToK8sObject(%s): got group %s for k8s object %s, want %s", tt.desc, k8sObj.Group, k8sObjStr, tt.wantGroup)
   266  				}
   267  				if k8sObj.Kind != tt.wantKind {
   268  					t.Errorf("ParseJsonToK8sObject(%s): got kind %s for k8s object %s, want %s", tt.desc, k8sObj.Kind, k8sObjStr, tt.wantKind)
   269  				}
   270  				if k8sObj.Name != tt.wantName {
   271  					t.Errorf("ParseJsonToK8sObject(%s): got name %s for k8s object %s, want %s", tt.desc, k8sObj.Name, k8sObjStr, tt.wantName)
   272  				}
   273  				if k8sObj.Namespace != tt.wantNamespace {
   274  					t.Errorf("ParseJsonToK8sObject(%s): got group %s for k8s object %s, want %s", tt.desc, k8sObj.Namespace, k8sObjStr, tt.wantNamespace)
   275  				}
   276  			} else if !tt.wantErr {
   277  				t.Errorf("ParseJsonToK8sObject(%s): got unexpected error: %v", tt.desc, err)
   278  			}
   279  		})
   280  	}
   281  }
   282  
   283  func TestParseK8sObjectsFromYAMLManifest(t *testing.T) {
   284  	testDeploymentYaml := `apiVersion: apps/v1
   285  kind: Deployment
   286  metadata:
   287    name: istio-citadel
   288    namespace: istio-system
   289    labels:
   290      istio: citadel
   291  spec:
   292    replicas: 1
   293    selector:
   294      matchLabels:
   295        istio: citadel
   296    template:
   297      metadata:
   298        labels:
   299          istio: citadel
   300      spec:
   301        containers:
   302        - name: citadel
   303          image: docker.io/istio/citadel:1.1.8
   304          args:
   305          - "--append-dns-names=true"
   306          - "--grpc-port=8060"
   307          - "--grpc-hostname=citadel"
   308          - "--citadel-storage-namespace=istio-system"
   309          - "--custom-dns-names=istio-pilot-service-account.istio-system:istio-pilot.istio-system"
   310          - "--monitoring-port=15014"
   311          - "--self-signed-ca=true"`
   312  
   313  	testPodYaml := `apiVersion: v1
   314  kind: Pod
   315  metadata:
   316    name: istio-galley-75bcd59768-hpt5t
   317    namespace: istio-system
   318    labels:
   319      istio: galley
   320  spec:
   321    containers:
   322    - name: galley
   323      image: docker.io/istio/galley:1.1.8
   324      command:
   325      - "/usr/local/bin/galley"
   326      - server
   327      - "--meshConfigFile=/etc/mesh-config/mesh"
   328      - "--livenessProbeInterval=1s"
   329      - "--livenessProbePath=/healthliveness"
   330      - "--readinessProbePath=/healthready"
   331      - "--readinessProbeInterval=1s"
   332      - "--deployment-namespace=istio-system"
   333      - "--insecure=true"
   334      - "--validation-webhook-config-file"
   335      - "/etc/config/validatingwebhookconfiguration.yaml"
   336      - "--monitoringPort=15014"
   337      - "--log_output_level=default:info"
   338      ports:
   339      - containerPort: 443
   340        protocol: TCP
   341      - containerPort: 15014
   342        protocol: TCP
   343      - containerPort: 9901
   344        protocol: TCP`
   345  
   346  	testServiceYaml := `apiVersion: v1
   347  kind: Service
   348  metadata:
   349    labels:
   350      app: pilot
   351    name: istio-pilot
   352    namespace: istio-system
   353  spec:
   354    clusterIP: 10.102.230.31
   355    ports:
   356    - name: grpc-xds
   357      port: 15010
   358      protocol: TCP
   359      targetPort: 15010
   360    - name: https-xds
   361      port: 15011
   362      protocol: TCP
   363      targetPort: 15011
   364    - name: http-legacy-discovery
   365      port: 8080
   366      protocol: TCP
   367      targetPort: 8080
   368    - name: http-monitoring
   369      port: 15014
   370      protocol: TCP
   371      targetPort: 15014
   372    selector:
   373      istio: pilot
   374    sessionAffinity: None
   375    type: ClusterIP`
   376  
   377  	parseK8sObjectsFromYAMLManifestTests := []struct {
   378  		desc    string
   379  		objsMap map[string]string
   380  	}{
   381  		{
   382  			"FromHybridYAMLManifest",
   383  			map[string]string{
   384  				"Deployment:istio-system:istio-citadel":          testDeploymentYaml,
   385  				"Pod:istio-system:istio-galley-75bcd59768-hpt5t": testPodYaml,
   386  				"Service:istio-system:istio-pilot":               testServiceYaml,
   387  			},
   388  		},
   389  	}
   390  
   391  	for _, tt := range parseK8sObjectsFromYAMLManifestTests {
   392  		t.Run(tt.desc, func(t *testing.T) {
   393  			testManifestYaml := strings.Join([]string{testDeploymentYaml, testPodYaml, testServiceYaml}, YAMLSeparator)
   394  			gotK8sObjs, err := ParseK8sObjectsFromYAMLManifest(testManifestYaml)
   395  			if err != nil {
   396  				gotK8sObjsMap := gotK8sObjs.ToMap()
   397  				for objHash, want := range tt.objsMap {
   398  					if gotObj, ok := gotK8sObjsMap[objHash]; ok {
   399  						gotObjYaml := gotObj.YAMLDebugString()
   400  						if !util.IsYAMLEqual(gotObjYaml, want) {
   401  							t.Errorf("ParseK8sObjectsFromYAMLManifest(%s): got:\n%s\n\nwant:\n%s\nDiff:\n%s\n", tt.desc, gotObjYaml, want, util.YAMLDiff(gotObjYaml, want))
   402  						}
   403  					}
   404  				}
   405  			}
   406  		})
   407  	}
   408  }
   409  
   410  func TestK8sObject_Equal(t *testing.T) {
   411  	obj1 := K8sObject{
   412  		object: &unstructured.Unstructured{Object: map[string]any{
   413  			"key": "value1",
   414  		}},
   415  	}
   416  	obj2 := K8sObject{
   417  		object: &unstructured.Unstructured{Object: map[string]any{
   418  			"key": "value2",
   419  		}},
   420  	}
   421  	cases := []struct {
   422  		desc string
   423  		o1   *K8sObject
   424  		o2   *K8sObject
   425  		want bool
   426  	}{
   427  		{
   428  			desc: "Equals",
   429  			o1:   &obj1,
   430  			o2:   &obj1,
   431  			want: true,
   432  		},
   433  		{
   434  			desc: "NotEquals",
   435  			o1:   &obj1,
   436  			o2:   &obj2,
   437  			want: false,
   438  		},
   439  		{
   440  			desc: "NilSource",
   441  			o1:   nil,
   442  			o2:   &obj2,
   443  			want: false,
   444  		},
   445  		{
   446  			desc: "NilDest",
   447  			o1:   &obj1,
   448  			o2:   nil,
   449  			want: false,
   450  		},
   451  		{
   452  			desc: "TwoNils",
   453  			o1:   nil,
   454  			o2:   nil,
   455  			want: true,
   456  		},
   457  	}
   458  	for _, tt := range cases {
   459  		t.Run(tt.desc, func(t *testing.T) {
   460  			res := tt.o1.Equal(tt.o2)
   461  			if res != tt.want {
   462  				t.Errorf("got %v, want: %v", res, tt.want)
   463  			}
   464  		})
   465  	}
   466  }
   467  
   468  func TestK8sObject_ResolveK8sConflict(t *testing.T) {
   469  	getK8sObject := func(ystr string) *K8sObject {
   470  		o, err := ParseYAMLToK8sObject([]byte(ystr))
   471  		if err != nil {
   472  			panic(err)
   473  		}
   474  		// Ensure that json data is in sync.
   475  		// Since the object was created using yaml, json is empty.
   476  		// make sure the object json is set correctly.
   477  		o.json, _ = o.JSON()
   478  		return o
   479  	}
   480  
   481  	cases := []struct {
   482  		desc string
   483  		o1   *K8sObject
   484  		o2   *K8sObject
   485  	}{
   486  		{
   487  			desc: "not applicable kind",
   488  			o1: getK8sObject(`
   489                    apiVersion: v1
   490                    kind: Service
   491                    metadata:
   492                      labels:
   493                        app: pilot
   494                      name: istio-pilot
   495                      namespace: istio-system
   496                    spec:
   497                      clusterIP: 10.102.230.31`),
   498  			o2: getK8sObject(`
   499                    apiVersion: v1
   500                    kind: Service
   501                    metadata:
   502                      labels:
   503                        app: pilot
   504                      name: istio-pilot
   505                      namespace: istio-system
   506                    spec:
   507                      clusterIP: 10.102.230.31`),
   508  		},
   509  		{
   510  			desc: "only minAvailable is set",
   511  			o1: getK8sObject(`
   512                    apiVersion: policy/v1
   513                    kind: PodDisruptionBudget
   514                    metadata:
   515                      name: zk-pdb
   516                    spec:
   517                      minAvailable: 2`),
   518  			o2: getK8sObject(`
   519                    apiVersion: policy/v1
   520                    kind: PodDisruptionBudget
   521                    metadata:
   522                      name: zk-pdb
   523                    spec:
   524                      minAvailable: 2`),
   525  		},
   526  		{
   527  			desc: "only maxUnavailable is set",
   528  			o1: getK8sObject(`
   529                    apiVersion: policy/v1
   530                    kind: PodDisruptionBudget
   531                    metadata:
   532                      name: istio
   533                    spec: 
   534                      maxUnavailable: 3`),
   535  			o2: getK8sObject(`
   536                    apiVersion: policy/v1
   537                    kind: PodDisruptionBudget
   538                    metadata:
   539                      name: istio
   540                    spec: 
   541                      maxUnavailable: 3`),
   542  		},
   543  		{
   544  			desc: "minAvailable and maxUnavailable are set to none zero values",
   545  			o1: getK8sObject(`
   546                    apiVersion: policy/v1
   547                    kind: PodDisruptionBudget
   548                    metadata:
   549                      name: istio
   550                    spec: 
   551                      maxUnavailable: 50%
   552                      minAvailable: 3`),
   553  			o2: getK8sObject(`
   554                    apiVersion: policy/v1
   555                    kind: PodDisruptionBudget
   556                    metadata:
   557                      name: istio
   558                    spec: 
   559                      maxUnavailable: 50%`),
   560  		},
   561  		{
   562  			desc: "both minAvailable and maxUnavailable are set default",
   563  			o1: getK8sObject(`
   564                    apiVersion: policy/v1
   565                    kind: PodDisruptionBudget
   566                    metadata:
   567                      name: istio
   568                    spec: 
   569                      minAvailable: 0
   570                      maxUnavailable: 0`),
   571  			o2: getK8sObject(`
   572                    apiVersion: policy/v1
   573                    kind: PodDisruptionBudget
   574                    metadata:
   575                      name: istio
   576                    spec:
   577                      maxUnavailable: 0
   578                      minAvailable: 0`),
   579  		},
   580  	}
   581  	for _, tt := range cases {
   582  		t.Run(tt.desc, func(t *testing.T) {
   583  			newObj := tt.o1.ResolveK8sConflict()
   584  			if !newObj.Equal(tt.o2) {
   585  				newObjjson, _ := newObj.JSON()
   586  				wantedObjjson, _ := tt.o2.JSON()
   587  				t.Errorf("Got: %s, want: %s", string(newObjjson), string(wantedObjjson))
   588  			}
   589  		})
   590  	}
   591  }
   592  
   593  func TestParseK8sObjectsFromYAMLManifestFailOption(t *testing.T) {
   594  	cases := []struct {
   595  		name        string
   596  		input       string
   597  		failOnError bool
   598  		expectErr   bool
   599  		expectCount int
   600  		expectOut   bool
   601  	}{
   602  		{
   603  			name:        "well formed yaml, no errors",
   604  			input:       "well-formed",
   605  			failOnError: false,
   606  			expectErr:   false,
   607  			expectCount: 2,
   608  		},
   609  		{
   610  			name:        "malformed yaml, fail on error",
   611  			input:       "malformed",
   612  			failOnError: true,
   613  			expectErr:   true,
   614  			expectCount: 0,
   615  		},
   616  		{
   617  			name:        "malformed yaml, continue on error",
   618  			input:       "malformed",
   619  			failOnError: false,
   620  			expectErr:   false,
   621  			expectCount: 1,
   622  		},
   623  		{
   624  			name:        "space in the end of the manifest",
   625  			input:       "well-formed-with-space",
   626  			expectCount: 1,
   627  			expectOut:   true,
   628  		},
   629  		{
   630  			name:        "some random comments",
   631  			input:       "well-formed-with-comments",
   632  			expectCount: 1,
   633  			expectOut:   true,
   634  		},
   635  		{
   636  			name:        "invalid k8s object - missing kind",
   637  			input:       "invalid",
   638  			failOnError: true,
   639  			expectErr:   true,
   640  			expectCount: 0,
   641  		},
   642  		{
   643  			name:        "invalid k8s object - missing kind - skip error",
   644  			input:       "invalid",
   645  			failOnError: false,
   646  			expectErr:   false,
   647  			expectCount: 0,
   648  		},
   649  		{
   650  			name:        "empty object - do not have errors",
   651  			input:       "empty",
   652  			failOnError: true,
   653  			expectErr:   false,
   654  			expectCount: 0,
   655  		},
   656  	}
   657  
   658  	for _, tc := range cases {
   659  		t.Run(tc.name, func(t *testing.T) {
   660  			inputFileName := fmt.Sprintf("testdata/%s.yaml", tc.input)
   661  			inputFile, err := os.Open(inputFileName)
   662  			if err != nil {
   663  				t.Errorf("error opening test data file: %v", err)
   664  			}
   665  			defer inputFile.Close()
   666  			manifest, err := io.ReadAll(inputFile)
   667  			if err != nil {
   668  				t.Errorf("error reading test data file: %v", err)
   669  			}
   670  			objects, err := ParseK8sObjectsFromYAMLManifestFailOption(string(manifest), tc.failOnError)
   671  			if tc.expectErr {
   672  				assert.Error(t, err)
   673  			} else {
   674  				assert.NoError(t, err)
   675  			}
   676  			assert.Equal(t, tc.expectCount, len(objects))
   677  			if tc.expectOut {
   678  				outputFileName := fmt.Sprintf("testdata/%s.out.yaml", tc.input)
   679  				outputFile, err := os.Open(outputFileName)
   680  				if err != nil {
   681  					t.Errorf("error opening test data file: %v", err)
   682  				}
   683  				defer outputFile.Close()
   684  				expectedYAML, err := io.ReadAll(outputFile)
   685  				if err != nil {
   686  					t.Errorf("error reading test data file: %v", err)
   687  				}
   688  				expectedYAMLs := strings.Split(string(expectedYAML), "---")
   689  				if len(expectedYAMLs) != len(objects) {
   690  					t.Errorf("expected %d objects, got %d", len(expectedYAMLs), len(objects))
   691  				}
   692  				for i, obj := range objects {
   693  					assert.Equal(t, true, compareYAMLContent(string(obj.yaml), expectedYAMLs[i]))
   694  				}
   695  			}
   696  		})
   697  	}
   698  }
   699  
   700  // compareYAMLContent compares two yaml resources and returns true if they are equal. If they have same content but different
   701  // order of fields, it will return true as well.
   702  func compareYAMLContent(yaml1, yaml2 string) bool {
   703  	var obj1, obj2 interface{}
   704  	err := k8syaml.Unmarshal([]byte(yaml1), &obj1)
   705  	if err != nil {
   706  		return false
   707  	}
   708  	err = k8syaml.Unmarshal([]byte(yaml2), &obj2)
   709  	if err != nil {
   710  		return false
   711  	}
   712  	return reflect.DeepEqual(obj1, obj2)
   713  }