...

Source file src/github.com/linkerd/linkerd2/controller/k8s/api_test.go

Documentation: github.com/linkerd/linkerd2/controller/k8s

     1  package k8s
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"reflect"
     8  	"strings"
     9  	"testing"
    10  
    11  	"google.golang.org/grpc/codes"
    12  	"google.golang.org/grpc/status"
    13  	"k8s.io/apimachinery/pkg/labels"
    14  
    15  	"github.com/go-test/deep"
    16  	"github.com/linkerd/linkerd2/pkg/k8s"
    17  
    18  	corev1 "k8s.io/api/core/v1"
    19  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    20  	"k8s.io/apimachinery/pkg/runtime"
    21  )
    22  
    23  type resources struct {
    24  	results []string
    25  	misc    []string
    26  }
    27  
    28  // newMockAPI constructs a mock controller/k8s.API object for testing If
    29  // useInformer is true, it forces informer indexing, enabling informer lookups
    30  func newMockAPI(useInformer bool, res resources) (
    31  	*API,
    32  	*MetadataAPI,
    33  	[]runtime.Object,
    34  	error,
    35  ) {
    36  	k8sConfigs := []string{}
    37  	k8sResults := []runtime.Object{}
    38  
    39  	for _, config := range res.results {
    40  		obj, err := k8s.ToRuntimeObject(config)
    41  		if err != nil {
    42  			return nil, nil, nil, err
    43  		}
    44  		k8sConfigs = append(k8sConfigs, config)
    45  		k8sResults = append(k8sResults, obj)
    46  	}
    47  
    48  	k8sConfigs = append(k8sConfigs, res.misc...)
    49  
    50  	api, err := NewFakeAPI(k8sConfigs...)
    51  	if err != nil {
    52  		return nil, nil, nil, fmt.Errorf("NewFakeAPI returned an error: %w", err)
    53  	}
    54  
    55  	metadataAPI, err := NewFakeMetadataAPI(k8sConfigs)
    56  	if err != nil {
    57  		return nil, nil, nil, fmt.Errorf("NewFakeMetadataAPI returned an error: %w", err)
    58  	}
    59  
    60  	if useInformer {
    61  		api.Sync(nil)
    62  		metadataAPI.Sync(nil)
    63  	}
    64  
    65  	return api, metadataAPI, k8sResults, nil
    66  }
    67  
    68  // TestGetObjects tests both api.GetObjects() and
    69  // metadataAPI.GetByNamespaceFiltered()
    70  func TestGetObjects(t *testing.T) {
    71  
    72  	type getObjectsExpected struct {
    73  		resources
    74  
    75  		err       error
    76  		namespace string
    77  		resType   string
    78  		name      string
    79  	}
    80  
    81  	t.Run("Returns expected objects based on input", func(t *testing.T) {
    82  		expectations := []getObjectsExpected{
    83  			{
    84  				err:       status.Errorf(codes.Unimplemented, "unimplemented resource type: bar"),
    85  				namespace: "foo",
    86  				resType:   "bar",
    87  				name:      "baz",
    88  				resources: resources{
    89  					results: []string{},
    90  					misc:    []string{},
    91  				},
    92  			},
    93  			{
    94  				err:       nil,
    95  				namespace: "my-ns",
    96  				resType:   k8s.Pod,
    97  				name:      "my-pod",
    98  				resources: resources{
    99  					results: []string{`
   100  apiVersion: v1
   101  kind: Pod
   102  metadata:
   103    name: my-pod
   104    namespace: my-ns
   105  spec:
   106    containers:
   107    - name: my-pod
   108  status:
   109    phase: Running`,
   110  					},
   111  					misc: []string{},
   112  				},
   113  			},
   114  			{
   115  				err:       errors.New("\"my-pod\" not found"),
   116  				namespace: "not-my-ns",
   117  				resType:   k8s.Pod,
   118  				name:      "my-pod",
   119  				resources: resources{
   120  					results: []string{},
   121  					misc: []string{`
   122  apiVersion: v1
   123  kind: Pod
   124  metadata:
   125    name: my-pod
   126    namespace: my-ns`,
   127  					},
   128  				},
   129  			},
   130  			{
   131  				err:       nil,
   132  				namespace: "",
   133  				resType:   k8s.ReplicationController,
   134  				name:      "",
   135  				resources: resources{
   136  					results: []string{`
   137  apiVersion: v1
   138  kind: ReplicationController
   139  metadata:
   140    name: my-rc
   141    namespace: my-ns`,
   142  					},
   143  					misc: []string{},
   144  				},
   145  			},
   146  			{
   147  				err:       nil,
   148  				namespace: "my-ns",
   149  				resType:   k8s.Deployment,
   150  				name:      "",
   151  				resources: resources{
   152  					results: []string{`
   153  apiVersion: apps/v1
   154  kind: Deployment
   155  metadata:
   156    name: my-deploy
   157    namespace: my-ns`,
   158  					},
   159  					misc: []string{`
   160  apiVersion: apps/v1
   161  kind: Deployment
   162  metadata:
   163    name: my-deploy
   164    namespace: not-my-ns`,
   165  					},
   166  				},
   167  			},
   168  			{
   169  				err:       nil,
   170  				namespace: "",
   171  				resType:   k8s.DaemonSet,
   172  				name:      "",
   173  				resources: resources{
   174  					results: []string{`
   175  apiVersion: apps/v1
   176  kind: DaemonSet
   177  metadata:
   178    name: my-ds
   179    namespace: my-ns`,
   180  					},
   181  				},
   182  			},
   183  			{
   184  				err:       nil,
   185  				namespace: "my-ns",
   186  				resType:   k8s.DaemonSet,
   187  				name:      "my-ds",
   188  				resources: resources{
   189  					results: []string{`
   190  apiVersion: apps/v1
   191  kind: DaemonSet
   192  metadata:
   193    name: my-ds
   194    namespace: my-ns`,
   195  					},
   196  					misc: []string{`
   197  apiVersion: apps/v1
   198  kind: DaemonSet
   199  metadata:
   200    name: my-ds
   201    namespace: not-my-ns`,
   202  					},
   203  				},
   204  			},
   205  			{
   206  				err:       nil,
   207  				namespace: "my-ns",
   208  				resType:   k8s.Job,
   209  				name:      "my-job",
   210  				resources: resources{
   211  					results: []string{`
   212  apiVersion: batch/v1
   213  kind: Job
   214  metadata:
   215    name: my-job
   216    namespace: my-ns`,
   217  					},
   218  					misc: []string{`
   219  apiVersion: batch/v1
   220  kind: Job
   221  metadata:
   222    name: my-job
   223    namespace: not-my-ns`,
   224  					},
   225  				},
   226  			},
   227  			{
   228  				err:       nil,
   229  				namespace: "my-ns",
   230  				resType:   k8s.CronJob,
   231  				name:      "my-cronjob",
   232  				resources: resources{
   233  					results: []string{`
   234  apiVersion: batch/v1
   235  kind: CronJob
   236  metadata:
   237    name: my-cronjob
   238    namespace: my-ns`,
   239  					},
   240  					misc: []string{`
   241  apiVersion: batch/v1
   242  kind: CronJob
   243  metadata:
   244    name: my-cronjob
   245    namespace: not-my-ns`,
   246  					},
   247  				},
   248  			},
   249  			{
   250  				err:       nil,
   251  				namespace: "",
   252  				resType:   k8s.StatefulSet,
   253  				name:      "",
   254  				resources: resources{
   255  					results: []string{`
   256  apiVersion: apps/v1
   257  kind: StatefulSet
   258  metadata:
   259    name: my-ss
   260    namespace: my-ns`,
   261  					},
   262  				},
   263  			},
   264  			{
   265  				err:       nil,
   266  				namespace: "my-ns",
   267  				resType:   k8s.StatefulSet,
   268  				name:      "my-ss",
   269  				resources: resources{
   270  					results: []string{`
   271  apiVersion: apps/v1
   272  kind: StatefulSet
   273  metadata:
   274    name: my-ss
   275    namespace: my-ns`,
   276  					},
   277  					misc: []string{`
   278  apiVersion: apps/v1
   279  kind: StatefulSet
   280  metadata:
   281    name: my-ss
   282    namespace: not-my-ns`,
   283  					},
   284  				},
   285  			},
   286  			{
   287  				err:       nil,
   288  				namespace: "",
   289  				resType:   k8s.Namespace,
   290  				name:      "",
   291  				resources: resources{
   292  					results: []string{`
   293  apiVersion: v1
   294  kind: Namespace
   295  metadata:
   296    name: my-ns`,
   297  					},
   298  					misc: []string{},
   299  				},
   300  			},
   301  		}
   302  
   303  		for _, exp := range expectations {
   304  			api, metadataAPI, k8sResults, err := newMockAPI(true, exp.resources)
   305  			if err != nil {
   306  				t.Fatalf("newMockAPI error: %s", err)
   307  			}
   308  
   309  			pods, err := api.GetObjects(exp.namespace, exp.resType, exp.name, labels.Everything())
   310  			if err != nil || exp.err != nil {
   311  				if unexpectedErrors(err, exp.err) {
   312  					t.Fatalf("api.GetObjects() unexpected error, expected [%s] got: [%s]", exp.err, err)
   313  				}
   314  			} else {
   315  				if diff := deep.Equal(pods, k8sResults); diff != nil {
   316  					t.Fatalf("Expected: %+v", diff)
   317  				}
   318  			}
   319  
   320  			var objMetas []*metav1.PartialObjectMetadata
   321  			res, err := GetAPIResource(exp.resType)
   322  			if err == nil {
   323  				objMetas, err = metadataAPI.GetByNamespaceFiltered(res, exp.namespace, exp.name, labels.Everything())
   324  			}
   325  			if err != nil || exp.err != nil {
   326  				if unexpectedErrors(err, exp.err) {
   327  					fmt.Printf("objMetas: %#v\n", objMetas)
   328  					t.Fatalf("metadataAPI.GetNamespaceFilteredCache() unexpected error, expected [%s] got: [%s]", exp.err, err)
   329  				}
   330  			} else {
   331  				expMetas := []*metav1.PartialObjectMetadata{}
   332  				for _, obj := range k8sResults {
   333  					objMeta, err := toPartialObjectMetadata(obj)
   334  					if err != nil {
   335  						t.Fatalf("error converting Object to PartialObjectMetadata: %s", err)
   336  					}
   337  					expMetas = append(expMetas, objMeta)
   338  				}
   339  				if diff := deep.Equal(objMetas, expMetas); diff != nil {
   340  					t.Fatalf("Expected: %+v", diff)
   341  				}
   342  			}
   343  		}
   344  	})
   345  
   346  	t.Run("If objects are pods", func(t *testing.T) {
   347  		t.Run("Return running or pending pods", func(t *testing.T) {
   348  			expectations := []getObjectsExpected{
   349  				{
   350  					err:       nil,
   351  					namespace: "my-ns",
   352  					resType:   k8s.Pod,
   353  					name:      "my-pod",
   354  					resources: resources{
   355  						results: []string{`
   356  apiVersion: v1
   357  kind: Pod
   358  metadata:
   359    name: my-pod
   360    namespace: my-ns
   361  spec:
   362    containers:
   363    - name: my-pod
   364  status:
   365    phase: Running`,
   366  						},
   367  					},
   368  				},
   369  				{
   370  					err:       nil,
   371  					namespace: "my-ns",
   372  					resType:   k8s.Pod,
   373  					name:      "my-pod",
   374  					resources: resources{
   375  						results: []string{`
   376  apiVersion: v1
   377  kind: Pod
   378  metadata:
   379    name: my-pod
   380    namespace: my-ns
   381  spec:
   382    containers:
   383    - name: my-pod
   384  status:
   385    phase: Pending`,
   386  						},
   387  					},
   388  				},
   389  			}
   390  
   391  			for _, exp := range expectations {
   392  				api, _, k8sResults, err := newMockAPI(true, exp.resources)
   393  				if err != nil {
   394  					t.Fatalf("newMockAPI error: %s", err)
   395  				}
   396  
   397  				pods, err := api.GetObjects(exp.namespace, exp.resType, exp.name, labels.Everything())
   398  				if err != nil {
   399  					t.Fatalf("api.GetObjects() unexpected error %s", err)
   400  				}
   401  
   402  				if diff := deep.Equal(pods, k8sResults); diff != nil {
   403  					t.Fatalf("%+v", diff)
   404  				}
   405  			}
   406  		})
   407  
   408  		t.Run("Don't return failed or succeeded pods", func(t *testing.T) {
   409  			expectations := []getObjectsExpected{
   410  				{
   411  					err:       nil,
   412  					namespace: "my-ns",
   413  					resType:   k8s.Pod,
   414  					name:      "my-pod",
   415  					resources: resources{
   416  						results: []string{`
   417  apiVersion: v1
   418  kind: Pod
   419  metadata:
   420    name: my-pod
   421    namespace: my-ns
   422  spec:
   423    containers:
   424    - name: my-pod
   425  status:
   426    phase: Succeeded`,
   427  						},
   428  					},
   429  				},
   430  				{
   431  					err:       nil,
   432  					namespace: "my-ns",
   433  					resType:   k8s.Pod,
   434  					name:      "my-pod",
   435  					resources: resources{
   436  						results: []string{`
   437  apiVersion: v1
   438  kind: Pod
   439  metadata:
   440    name: my-pod
   441    namespace: my-ns
   442  spec:
   443    containers:
   444    - name: my-pod
   445  status:
   446    phase: Failed`,
   447  						},
   448  					},
   449  				},
   450  			}
   451  
   452  			for _, exp := range expectations {
   453  				api, _, _, err := newMockAPI(true, exp.resources)
   454  				if err != nil {
   455  					t.Fatalf("newMockAPI error: %s", err)
   456  				}
   457  
   458  				pods, err := api.GetObjects(exp.namespace, exp.resType, exp.name, labels.Everything())
   459  				if err != nil {
   460  					t.Fatalf("api.GetObjects() unexpected error %s", err)
   461  				}
   462  
   463  				if len(pods) != 0 {
   464  					t.Errorf("Expected no terminating or failed pods to be returned but got %d pods", len(pods))
   465  				}
   466  			}
   467  
   468  		})
   469  	})
   470  }
   471  
   472  func TestGetPodsFor(t *testing.T) {
   473  
   474  	type getPodsForExpected struct {
   475  		resources
   476  
   477  		err         error
   478  		k8sResInput string // object used as input to GetPodFor()
   479  	}
   480  
   481  	t.Run("Returns expected pods based on input", func(t *testing.T) {
   482  		expectations := []getPodsForExpected{
   483  			{
   484  				err: nil,
   485  				k8sResInput: `
   486  apiVersion: apps/v1
   487  kind: Deployment
   488  metadata:
   489    name: emoji
   490    namespace: emojivoto
   491  spec:
   492    selector:
   493      matchLabels:
   494        app: emoji-svc`,
   495  				resources: resources{
   496  					results: []string{},
   497  					misc: []string{`
   498  apiVersion: v1
   499  kind: Pod
   500  metadata:
   501    name: emojivoto-meshed-finished
   502    namespace: emojivoto
   503    labels:
   504      app: emoji-svc
   505    ownerReferences:
   506    - apiVersion: apps/v1
   507  status:
   508    phase: Finished`,
   509  					},
   510  				},
   511  			},
   512  			// Retrieve pods associated to a ClusterIP service
   513  			{
   514  				err: nil,
   515  				k8sResInput: `
   516  apiVersion: v1
   517  kind: Service
   518  metadata:
   519    name: emoji-svc
   520    namespace: emojivoto
   521    uid: serviceUIDDoesNotMatter
   522  spec:
   523    type: ClusterIP
   524    selector:
   525      app: emoji-svc`,
   526  				resources: resources{
   527  					results: []string{`
   528  apiVersion: v1
   529  kind: Pod
   530  metadata:
   531    name: emojivoto-meshed-finished
   532    namespace: emojivoto
   533    labels:
   534      app: emoji-svc
   535    ownerReferences:
   536    - apiVersion: apps/v1
   537  status:
   538    phase: Running`,
   539  					},
   540  					misc: []string{},
   541  				},
   542  			},
   543  			// ExternalName services shouldn't return any pods
   544  			{
   545  				err: nil,
   546  				k8sResInput: `
   547  apiVersion: v1
   548  kind: Service
   549  metadata:
   550    name: emoji-svc
   551    namespace: emojivoto
   552  spec:
   553    type: ExternalName
   554    externalName: someapi.example.com`,
   555  				resources: resources{
   556  					results: []string{},
   557  					misc: []string{`
   558  apiVersion: v1
   559  kind: Pod
   560  metadata:
   561    name: emojivoto-meshed-finished
   562    namespace: emojivoto
   563    labels:
   564      app: emoji-svc
   565  status:
   566    phase: Running`,
   567  					},
   568  				},
   569  			},
   570  			// Cronjob
   571  			{
   572  				err: nil,
   573  				k8sResInput: `
   574  apiVersion: batch/v1
   575  kind: CronJob
   576  metadata:
   577    name: emoji
   578    namespace: emojivoto
   579    uid: cronjob`,
   580  				resources: resources{
   581  					results: []string{`
   582  apiVersion: v1
   583  kind: Pod
   584  metadata:
   585    name: emojivoto-meshed
   586    namespace: emojivoto
   587    labels:
   588      app: emoji-svc
   589    ownerReferences:
   590    - apiVersion: batch/v1
   591      uid: job
   592  status:
   593    phase: Running`,
   594  					},
   595  					misc: []string{`
   596  apiVersion: batch/v1
   597  kind: Job
   598  metadata:
   599    name: emoji
   600    namespace: emojivoto
   601    uid: job
   602    ownerReferences:
   603    - apiVersion: batch/v1
   604      uid: cronjob
   605  spec:
   606    selector:
   607      matchLabels:
   608        app: emoji-svc`,
   609  					},
   610  				},
   611  			},
   612  			// Daemonset
   613  			{
   614  				err: nil,
   615  				k8sResInput: `
   616  apiVersion: apps/v1
   617  kind: DaemonSet
   618  metadata:
   619    name: emoji
   620    namespace: emojivoto
   621    uid: daemonset
   622  spec:
   623    selector:
   624      matchLabels:
   625        app: emoji-svc`,
   626  				resources: resources{
   627  					results: []string{`
   628  apiVersion: v1
   629  kind: Pod
   630  metadata:
   631    name: emojivoto-meshed
   632    namespace: emojivoto
   633    labels:
   634      app: emoji-svc
   635    ownerReferences:
   636    - apiVersion: apps/v1
   637      uid: daemonset
   638  status:
   639    phase: Running`,
   640  					},
   641  					misc: []string{},
   642  				},
   643  			},
   644  			// replicaset
   645  			{
   646  				err: nil,
   647  				k8sResInput: `
   648  apiVersion: apps/v1
   649  kind: ReplicaSet
   650  metadata:
   651    name: emoji
   652    namespace: emojivoto
   653    uid: replicaset
   654  spec:
   655    selector:
   656      matchLabels:
   657        app: emoji-svc`,
   658  				resources: resources{
   659  					results: []string{`
   660  apiVersion: v1
   661  kind: Pod
   662  metadata:
   663    name: emojivoto-meshed
   664    namespace: emojivoto
   665    labels:
   666      app: emoji-svc
   667    ownerReferences:
   668    - apiVersion: apps/v1
   669      uid: replicaset
   670  status:
   671    phase: Running`,
   672  					},
   673  					misc: []string{`
   674  apiVersion: v1
   675  kind: Pod
   676  metadata:
   677    name: emojivoto-meshed-finished
   678    namespace: emojivoto
   679    labels:
   680      app: emoji-svc
   681    ownerReferences:
   682    - apiVersion: apps/v1
   683      uid: replicaset
   684  status:
   685    phase: Finished`,
   686  					},
   687  				},
   688  			},
   689  			// single pod
   690  			{
   691  				err: nil,
   692  				k8sResInput: `
   693  apiVersion: v1
   694  kind: Pod
   695  metadata:
   696    name: emojivoto-meshed
   697    namespace: emojivoto
   698    labels:
   699      app: emoji-svc
   700    ownerReferences:
   701    - apiVersion: apps/v1
   702      uid: singlePod
   703  status:
   704    phase: Running`,
   705  				resources: resources{
   706  					results: []string{`
   707  apiVersion: v1
   708  kind: Pod
   709  metadata:
   710    name: emojivoto-meshed
   711    namespace: emojivoto
   712    labels:
   713      app: emoji-svc
   714    ownerReferences:
   715    - apiVersion: apps/v1
   716      uid: singlePod
   717  status:
   718    phase: Running`,
   719  					},
   720  					misc: []string{`
   721  apiVersion: v1
   722  kind: Pod
   723  metadata:
   724    name: emojivoto-meshed_2
   725    namespace: emojivoto
   726    labels:
   727      app: emoji-svc
   728  status:
   729    phase: Running`,
   730  					},
   731  				},
   732  			},
   733  			// deployment
   734  			{
   735  				err: nil,
   736  				k8sResInput: `
   737  apiVersion: apps/v1
   738  kind: Deployment
   739  metadata:
   740    annotations:
   741      deployment.kubernetes.io/revision: "2"
   742    name: emojivoto-meshed
   743    namespace: emojivoto
   744    uid: deployment
   745    labels:
   746      app: emoji-svc
   747  spec:
   748    selector:
   749      matchLabels:
   750        app: emoji-svc`,
   751  				resources: resources{
   752  					results: []string{`
   753  apiVersion: v1
   754  kind: Pod
   755  metadata:
   756    name: emojivoto-meshed
   757    namespace: emojivoto
   758    ownerReferences:
   759    - apiVersion: apps/v1
   760      uid: deploymentRS
   761    labels:
   762      app: emoji-svc
   763      pod-template-hash: deploymentPod
   764  status:
   765    phase: Running`,
   766  					},
   767  					misc: []string{`
   768  apiVersion: apps/v1
   769  kind: ReplicaSet
   770  metadata:
   771    uid: deploymentRS
   772    annotations:
   773      deployment.kubernetes.io/revision: "2"
   774    name: emojivoto-meshed_2
   775    namespace: emojivoto
   776    labels:
   777      app: emoji-svc
   778      pod-template-hash: deploymentPod
   779    ownerReferences:
   780    - apiVersion: apps/v1
   781      uid: deployment
   782  spec:
   783    selector:
   784      matchLabels:
   785        app: emoji-svc
   786        pod-template-hash: deploymentPod`,
   787  						`apiVersion: apps/v1
   788  kind: ReplicaSet
   789  metadata:
   790    uid: deploymentRSOld
   791    annotations:
   792      deployment.kubernetes.io/revision: "1"
   793    name: emojivoto-meshed_1
   794    namespace: emojivoto
   795    labels:
   796      app: emoji-svc
   797      pod-template-hash: deploymentPodOld
   798    ownerReferences:
   799    - apiVersion: apps/v1
   800      uid: deployment
   801  spec:
   802    selector:
   803      matchLabels:
   804        app: emoji-svc
   805        pod-template-hash: deploymentPodOld`,
   806  					},
   807  				},
   808  			},
   809  			// deployment without RS
   810  			{
   811  				err: nil,
   812  				k8sResInput: `
   813  apiVersion: apps/v1
   814  kind: Deployment
   815  metadata:
   816    annotations:
   817      deployment.kubernetes.io/revision: "2"
   818    name: emojivoto-meshed
   819    namespace: emojivoto
   820    uid: deploymentWithoutRS
   821    labels:
   822      app: emoji-svc
   823  spec:
   824    selector:
   825      matchLabels:
   826        app: emoji-svc`,
   827  				resources: resources{
   828  					results: []string{},
   829  					misc: []string{`
   830  apiVersion: apps/v1
   831  kind: ReplicaSet
   832  metadata:
   833    uid: AnotherRS
   834    annotations:
   835      deployment.kubernetes.io/revision: "2"
   836    name: emojivoto-meshed_2
   837    namespace: emojivoto
   838    labels:
   839      app: emoji-svc
   840      pod-template-hash: doesntMatter
   841    ownerReferences:
   842    - apiVersion: apps/v1
   843      uid: doesntMatch
   844  spec:
   845    selector:
   846      matchLabels:
   847        app: emoji-svc
   848        pod-template-hash: doesntMatter`,
   849  					},
   850  				},
   851  			},
   852  			// Deployment with 2 replicasets
   853  			{
   854  				err: nil,
   855  				k8sResInput: `
   856  apiVersion: apps/v1
   857  kind: Deployment
   858  metadata:
   859    annotations:
   860      deployment.kubernetes.io/revision: "2"
   861    name: emojivoto-meshed
   862    namespace: emojivoto
   863    uid: deployment2RS
   864    labels:
   865      app: emoji-svc
   866  spec:
   867    selector:
   868      matchLabels:
   869        app: emoji-svc`,
   870  				resources: resources{
   871  					results: []string{`
   872  apiVersion: v1
   873  kind: Pod
   874  metadata:
   875    name: emojivoto-meshed-pod1
   876    namespace: emojivoto
   877    ownerReferences:
   878    - apiVersion: apps/v1
   879      uid: RS1
   880    labels:
   881      app: emoji-svc
   882      pod-template-hash: pod1
   883  status:
   884    phase: Running`,
   885  						`apiVersion: v1
   886  kind: Pod
   887  metadata:
   888    name: emojivoto-meshed-pod2
   889    namespace: emojivoto
   890    ownerReferences:
   891    - apiVersion: apps/v1
   892      uid: RS2
   893    labels:
   894      app: emoji-svc
   895      pod-template-hash: pod2
   896  status:
   897    phase: Running`,
   898  					},
   899  					misc: []string{`
   900  apiVersion: apps/v1
   901  kind: ReplicaSet
   902  metadata:
   903    uid: RS1
   904    annotations:
   905      deployment.kubernetes.io/revision: "2"
   906    name: emojivoto-meshed_2
   907    namespace: emojivoto
   908    labels:
   909      app: emoji-svc
   910      pod-template-hash: pod1
   911    ownerReferences:
   912    - apiVersion: apps/v1
   913      uid: deployment2RS
   914  spec:
   915    selector:
   916      matchLabels:
   917        app: emoji-svc
   918        pod-template-hash: pod1`,
   919  						`apiVersion: apps/v1
   920  kind: ReplicaSet
   921  metadata:
   922    uid: RS2
   923    annotations:
   924      deployment.kubernetes.io/revision: "1"
   925    name: emojivoto-meshed_1
   926    namespace: emojivoto
   927    labels:
   928      app: emoji-svc
   929      pod-template-hash: pod2
   930    ownerReferences:
   931    - apiVersion: apps/v1
   932      uid: deployment2RS
   933  spec:
   934    selector:
   935      matchLabels:
   936        app: emoji-svc
   937        pod-template-hash: pod2`,
   938  					},
   939  				},
   940  			},
   941  			// Deployment 2 Pods just one valid
   942  			{
   943  				err: nil,
   944  				k8sResInput: `
   945  apiVersion: apps/v1
   946  kind: Deployment
   947  metadata:
   948    annotations:
   949      deployment.kubernetes.io/revision: "2"
   950    name: emojivoto-meshed
   951    namespace: emojivoto
   952    uid: deployment2Pods
   953    labels:
   954      app: emoji-svc
   955  spec:
   956    selector:
   957      matchLabels:
   958        app: emoji-svc`,
   959  				resources: resources{
   960  					results: []string{`apiVersion: v1
   961  kind: Pod
   962  metadata:
   963    name: emojivoto-meshed-with-RS
   964    namespace: emojivoto
   965    ownerReferences:
   966    - apiVersion: apps/v1
   967      uid: validRS
   968    labels:
   969      app: emoji-svc
   970      pod-template-hash: podWithRS
   971  status:
   972    phase: Running`,
   973  					},
   974  					misc: []string{`
   975  apiVersion: apps/v1
   976  kind: ReplicaSet
   977  metadata:
   978    uid: validRS
   979    annotations:
   980      deployment.kubernetes.io/revision: "2"
   981    name: emojivoto-meshed_2
   982    namespace: emojivoto
   983    labels:
   984      app: emoji-svc
   985      pod-template-hash: podWithRS
   986    ownerReferences:
   987    - apiVersion: apps/v1
   988      uid: deployment2Pods
   989  spec:
   990    selector:
   991      matchLabels:
   992        app: emoji-svc
   993        pod-template-hash: podWithRS`,
   994  						`apiVersion: v1
   995  kind: Pod
   996  metadata:
   997    name: emojivoto-meshed-without-RS
   998    namespace: emojivoto
   999    ownerReferences:
  1000    - apiVersion: apps/v1
  1001      uid: notHere
  1002    labels:
  1003      app: emoji-svc
  1004      pod-template-hash: invalidPod
  1005  status:
  1006    phase: Running`,
  1007  					},
  1008  				},
  1009  			},
  1010  		}
  1011  
  1012  		for _, exp := range expectations {
  1013  			k8sInputObj, err := k8s.ToRuntimeObject(exp.k8sResInput)
  1014  			if err != nil {
  1015  				t.Fatalf("could not decode yml: %s", err)
  1016  			}
  1017  
  1018  			api, _, k8sResults, err := newMockAPI(true, exp.resources)
  1019  			if err != nil {
  1020  				t.Fatalf("newMockAPI error: %s", err)
  1021  			}
  1022  
  1023  			k8sResultPods := []*corev1.Pod{}
  1024  			for _, obj := range k8sResults {
  1025  				k8sResultPods = append(k8sResultPods, obj.(*corev1.Pod))
  1026  			}
  1027  
  1028  			pods, err := api.GetPodsFor(k8sInputObj, false)
  1029  			if !errors.Is(err, exp.err) {
  1030  				t.Fatalf("api.GetPodsFor() unexpected error, expected [%s] got: [%s]", exp.err, err)
  1031  			}
  1032  
  1033  			if len(pods) != len(k8sResultPods) {
  1034  				t.Fatalf("Expected: %+v, Got: %+v", k8sResultPods, pods)
  1035  			}
  1036  
  1037  			for _, pod := range pods {
  1038  				found := false
  1039  				for _, resultPod := range k8sResultPods {
  1040  					if reflect.DeepEqual(pod, resultPod) {
  1041  						found = true
  1042  						break
  1043  					}
  1044  				}
  1045  				if !found {
  1046  					t.Fatalf("Expected: %+v, Got: %+v", k8sResultPods, pods)
  1047  				}
  1048  			}
  1049  		}
  1050  	})
  1051  }
  1052  
  1053  // TestGetOwnerKindAndName tests GetOwnerKindAndName for both api and
  1054  // metadataAPI. Both return strings, so unlike TestGetObjects above, there's no
  1055  // need to create []*metav1.PartialObjectMetadata fixtures
  1056  func TestGetOwnerKindAndName(t *testing.T) {
  1057  	for i, tt := range []struct {
  1058  		resources
  1059  
  1060  		expectedOwnerKind string
  1061  		expectedOwnerName string
  1062  	}{
  1063  		{
  1064  			expectedOwnerKind: "deployment",
  1065  			expectedOwnerName: "t2",
  1066  			resources: resources{
  1067  				results: []string{`
  1068  apiVersion: v1
  1069  kind: Pod
  1070  metadata:
  1071    name: t2-5f79f964bc-d5jvf
  1072    namespace: default
  1073    ownerReferences:
  1074    - apiVersion: apps/v1
  1075      kind: ReplicaSet
  1076      name: t2-5f79f964bc`,
  1077  				},
  1078  				misc: []string{`
  1079  apiVersion: apps/v1
  1080  kind: ReplicaSet
  1081  metadata:
  1082    name: t2-5f79f964bc
  1083    namespace: default
  1084    ownerReferences:
  1085    - apiVersion: apps/v1
  1086      kind: Deployment
  1087      name: t2`,
  1088  				},
  1089  			},
  1090  		},
  1091  		{
  1092  			expectedOwnerKind: "replicaset",
  1093  			expectedOwnerName: "t1-b4f55d87f",
  1094  			resources: resources{
  1095  				results: []string{`
  1096  apiVersion: v1
  1097  kind: Pod
  1098  metadata:
  1099    name: t1-b4f55d87f-98dbz
  1100    namespace: default
  1101    ownerReferences:
  1102    - apiVersion: apps/v1
  1103      kind: ReplicaSet
  1104      name: t1-b4f55d87f`,
  1105  				},
  1106  			},
  1107  		},
  1108  		{
  1109  			expectedOwnerKind: "job",
  1110  			expectedOwnerName: "slow-cooker",
  1111  			resources: resources{
  1112  				results: []string{`
  1113  apiVersion: v1
  1114  kind: Pod
  1115  metadata:
  1116    name: slow-cooker-bxtnq
  1117    namespace: default
  1118    ownerReferences:
  1119    - apiVersion: batch/v1
  1120      kind: Job
  1121      name: slow-cooker`,
  1122  				},
  1123  			},
  1124  		},
  1125  		{
  1126  			expectedOwnerKind: "replicationcontroller",
  1127  			expectedOwnerName: "web",
  1128  			resources: resources{
  1129  				results: []string{`
  1130  apiVersion: v1
  1131  kind: Pod
  1132  metadata:
  1133    name: web-dcfq4
  1134    namespace: default
  1135    ownerReferences:
  1136    - apiVersion: v1
  1137      kind: ReplicationController
  1138      name: web`,
  1139  				},
  1140  			},
  1141  		},
  1142  		{
  1143  			expectedOwnerKind: "pod",
  1144  			expectedOwnerName: "vote-bot",
  1145  			resources: resources{
  1146  				results: []string{`
  1147  apiVersion: v1
  1148  kind: Pod
  1149  metadata:
  1150    name: vote-bot
  1151    namespace: default`,
  1152  				},
  1153  			},
  1154  		},
  1155  		{
  1156  			expectedOwnerKind: "cronjob",
  1157  			expectedOwnerName: "my-cronjob",
  1158  			resources: resources{
  1159  				results: []string{`
  1160  apiVersion: v1
  1161  kind: Pod
  1162  metadata:
  1163    name: my-pod
  1164    namespace: my-ns
  1165    ownerReferences:
  1166    - apiVersion: batch/v1
  1167      kind: Job
  1168      name: my-job`,
  1169  				},
  1170  				misc: []string{`
  1171  apiVersion: batch/v1
  1172  kind: Job
  1173  metadata:
  1174    name: my-job
  1175    namespace: my-ns
  1176    ownerReferences:
  1177    - apiVersion: batch/v1
  1178      kind: CronJob
  1179      name: my-cronjob`,
  1180  				},
  1181  			},
  1182  		},
  1183  		{
  1184  			expectedOwnerKind: "replicaset",
  1185  			expectedOwnerName: "invalid-rs-parent-2abdffa",
  1186  			resources: resources{
  1187  				results: []string{`
  1188  apiVersion: v1
  1189  kind: Pod
  1190  metadata:
  1191    name: invalid-rs-parent-dcfq4
  1192    namespace: default
  1193    ownerReferences:
  1194    - apiVersion: v1
  1195      kind: ReplicaSet
  1196      name: invalid-rs-parent-2abdffa`,
  1197  				},
  1198  				misc: []string{`
  1199  apiVersion: apps/v1
  1200  kind: ReplicaSet
  1201  metadata:
  1202    name: invalid-rs-parent-2abdffa
  1203    namespace: default
  1204    ownerReferences:
  1205    - apiVersion: invalidParent/v1
  1206      kind: InvalidParentKind
  1207      name: invalid-parent`,
  1208  				},
  1209  			},
  1210  		},
  1211  	} {
  1212  		tt := tt // pin
  1213  		for _, retry := range []bool{
  1214  			false,
  1215  			true,
  1216  		} {
  1217  			retry := retry // pin
  1218  			t.Run(fmt.Sprintf("%d/retry:%t", i, retry), func(t *testing.T) {
  1219  				api, metadataAPI, objs, err := newMockAPI(!retry, tt.resources)
  1220  				if err != nil {
  1221  					t.Fatalf("newMockAPI error: %s", err)
  1222  				}
  1223  
  1224  				pod := objs[0].(*corev1.Pod)
  1225  				ownerKind, ownerName := api.GetOwnerKindAndName(context.Background(), pod, retry)
  1226  
  1227  				if ownerKind != tt.expectedOwnerKind {
  1228  					t.Fatalf("Expected kind to be [%s], got [%s]", tt.expectedOwnerKind, ownerKind)
  1229  				}
  1230  
  1231  				if ownerName != tt.expectedOwnerName {
  1232  					t.Fatalf("Expected name to be [%s], got [%s]", tt.expectedOwnerName, ownerName)
  1233  				}
  1234  
  1235  				ownerKind, ownerName, err = metadataAPI.GetOwnerKindAndName(context.Background(), pod, retry)
  1236  				if err != nil {
  1237  					t.Fatalf("Unexpected error: %s", err)
  1238  				}
  1239  
  1240  				if ownerKind != tt.expectedOwnerKind {
  1241  					t.Fatalf("Expected kind to be [%s], got [%s]", tt.expectedOwnerKind, ownerKind)
  1242  				}
  1243  
  1244  				if ownerName != tt.expectedOwnerName {
  1245  					t.Fatalf("Expected name to be [%s], got [%s]", tt.expectedOwnerName, ownerName)
  1246  				}
  1247  			})
  1248  		}
  1249  	}
  1250  }
  1251  
  1252  func TestGetServiceProfileFor(t *testing.T) {
  1253  	for _, tt := range []struct {
  1254  		resources
  1255  
  1256  		expectedRouteNames []string
  1257  	}{
  1258  		// No service profiles -> default service profile
  1259  		{
  1260  			expectedRouteNames: []string{},
  1261  			resources:          resources{},
  1262  		},
  1263  		// Service profile in unrelated namespace -> default service profile
  1264  		{
  1265  			expectedRouteNames: []string{},
  1266  			resources: resources{
  1267  				results: []string{`
  1268  apiVersion: linkerd.io/v1alpha2
  1269  kind: ServiceProfile
  1270  metadata:
  1271    name: books.server.svc.cluster.local
  1272    namespace: linkerd
  1273  spec:
  1274    routes:
  1275    - condition:
  1276        pathRegex: /server
  1277      name: server`,
  1278  				},
  1279  			},
  1280  		},
  1281  		// Uses service profile in server namespace
  1282  		{
  1283  			expectedRouteNames: []string{"server"},
  1284  			resources: resources{
  1285  				results: []string{`
  1286  apiVersion: linkerd.io/v1alpha2
  1287  kind: ServiceProfile
  1288  metadata:
  1289    name: books.server.svc.cluster.local
  1290    namespace: server
  1291  spec:
  1292    routes:
  1293    - condition:
  1294        pathRegex: /server
  1295      name: server`,
  1296  				},
  1297  			},
  1298  		},
  1299  		// Uses service profile in client namespace
  1300  		{
  1301  			expectedRouteNames: []string{"client"},
  1302  			resources: resources{
  1303  				results: []string{`
  1304  apiVersion: linkerd.io/v1alpha2
  1305  kind: ServiceProfile
  1306  metadata:
  1307    name: books.server.svc.cluster.local
  1308    namespace: client
  1309  spec:
  1310    routes:
  1311    - condition:
  1312        pathRegex: /client
  1313      name: client`,
  1314  				},
  1315  			},
  1316  		},
  1317  		// Service profile in client namespace takes priority
  1318  		{
  1319  			expectedRouteNames: []string{"client"},
  1320  			resources: resources{
  1321  				results: []string{`
  1322  apiVersion: linkerd.io/v1alpha2
  1323  kind: ServiceProfile
  1324  metadata:
  1325    name: books.server.svc.cluster.local
  1326    namespace: server
  1327  spec:
  1328    routes:
  1329    - condition:
  1330        pathRegex: /server
  1331      name: server`,
  1332  					`
  1333  apiVersion: linkerd.io/v1alpha2
  1334  kind: ServiceProfile
  1335  metadata:
  1336    name: books.server.svc.cluster.local
  1337    namespace: client
  1338  spec:
  1339    routes:
  1340    - condition:
  1341        pathRegex: /client
  1342      name: client`,
  1343  				},
  1344  			},
  1345  		},
  1346  	} {
  1347  		api, _, _, err := newMockAPI(true, tt.resources)
  1348  		if err != nil {
  1349  			t.Fatalf("newMockAPI error: %s", err)
  1350  		}
  1351  
  1352  		svc := corev1.Service{
  1353  			ObjectMeta: metav1.ObjectMeta{
  1354  				Name:      "books",
  1355  				Namespace: "server",
  1356  			},
  1357  		}
  1358  
  1359  		sp := api.GetServiceProfileFor(&svc, "client", "cluster.local")
  1360  
  1361  		if len(sp.Spec.Routes) != len(tt.expectedRouteNames) {
  1362  			t.Fatalf("Expected %d routes, got %d", len(tt.expectedRouteNames), len(sp.Spec.Routes))
  1363  		}
  1364  
  1365  		for i, route := range sp.Spec.Routes {
  1366  			if tt.expectedRouteNames[i] != route.Name {
  1367  				t.Fatalf("Expected route [%s], got [%s]", tt.expectedRouteNames[i], route.Name)
  1368  			}
  1369  		}
  1370  	}
  1371  }
  1372  
  1373  func TestGetServicesFor(t *testing.T) {
  1374  
  1375  	type getServicesForExpected struct {
  1376  		resources
  1377  
  1378  		err         error
  1379  		k8sResInput string // object used as input to GetServicesFor()
  1380  	}
  1381  
  1382  	t.Run("GetServicesFor", func(t *testing.T) {
  1383  		expectations := []getServicesForExpected{
  1384  			// If a service contains a pod, GetPodsFor should return the service.
  1385  			{
  1386  				err: nil,
  1387  				k8sResInput: `
  1388  apiVersion: v1
  1389  kind: Pod
  1390  metadata:
  1391    name: my-pod
  1392    namespace: emojivoto
  1393    labels:
  1394      app: my-pod
  1395  status:
  1396    phase: Running`,
  1397  				resources: resources{
  1398  					results: []string{`
  1399  apiVersion: v1
  1400  kind: Service
  1401  metadata:
  1402    name: my-svc
  1403    namespace: emojivoto
  1404  spec:
  1405    type: ClusterIP
  1406    selector:
  1407      app: my-pod`,
  1408  					},
  1409  					misc: []string{},
  1410  				},
  1411  			},
  1412  		}
  1413  
  1414  		for _, exp := range expectations {
  1415  			k8sInputObj, err := k8s.ToRuntimeObject(exp.k8sResInput)
  1416  			if err != nil {
  1417  				t.Fatalf("could not decode yml: %s", err)
  1418  			}
  1419  
  1420  			exp.misc = append(exp.misc, exp.k8sResInput)
  1421  			api, _, k8sResults, err := newMockAPI(true, exp.resources)
  1422  			if err != nil {
  1423  				t.Fatalf("newMockAPI error: %s", err)
  1424  			}
  1425  
  1426  			k8sResultServices := []*corev1.Service{}
  1427  			for _, obj := range k8sResults {
  1428  				k8sResultServices = append(k8sResultServices, obj.(*corev1.Service))
  1429  			}
  1430  
  1431  			services, err := api.GetServicesFor(k8sInputObj, false)
  1432  			if !errors.Is(err, exp.err) {
  1433  				t.Fatalf("api.GetServicesFor() unexpected error, expected [%s] got: [%s]", exp.err, err)
  1434  			}
  1435  
  1436  			if len(services) != len(k8sResultServices) {
  1437  				t.Fatalf("Expected: %+v, Got: %+v", k8sResultServices, services)
  1438  			}
  1439  
  1440  			for _, service := range services {
  1441  				found := false
  1442  				for _, resultService := range k8sResultServices {
  1443  					if reflect.DeepEqual(service, resultService) {
  1444  						found = true
  1445  						break
  1446  					}
  1447  				}
  1448  				if !found {
  1449  					t.Fatalf("Expected: %+v, Got: %+v", k8sResultServices, services)
  1450  				}
  1451  			}
  1452  		}
  1453  
  1454  	})
  1455  }
  1456  
  1457  func unexpectedErrors(err, expErr error) bool {
  1458  	return (err == nil && expErr != nil) ||
  1459  		(err != nil && expErr == nil) ||
  1460  		!strings.Contains(err.Error(), expErr.Error())
  1461  }
  1462  

View as plain text