linkerd2/controller/k8s/api_test.go

1462 lines
28 KiB
Go

package k8s
import (
"context"
"errors"
"fmt"
"reflect"
"strings"
"testing"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"k8s.io/apimachinery/pkg/labels"
"github.com/go-test/deep"
"github.com/linkerd/linkerd2/pkg/k8s"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
type resources struct {
results []string
misc []string
}
// newMockAPI constructs a mock controller/k8s.API object for testing If
// useInformer is true, it forces informer indexing, enabling informer lookups
func newMockAPI(useInformer bool, res resources) (
*API,
*MetadataAPI,
[]runtime.Object,
error,
) {
k8sConfigs := []string{}
k8sResults := []runtime.Object{}
for _, config := range res.results {
obj, err := k8s.ToRuntimeObject(config)
if err != nil {
return nil, nil, nil, err
}
k8sConfigs = append(k8sConfigs, config)
k8sResults = append(k8sResults, obj)
}
k8sConfigs = append(k8sConfigs, res.misc...)
api, err := NewFakeAPI(k8sConfigs...)
if err != nil {
return nil, nil, nil, fmt.Errorf("NewFakeAPI returned an error: %w", err)
}
metadataAPI, err := NewFakeMetadataAPI(k8sConfigs)
if err != nil {
return nil, nil, nil, fmt.Errorf("NewFakeMetadataAPI returned an error: %w", err)
}
if useInformer {
api.Sync(nil)
metadataAPI.Sync(nil)
}
return api, metadataAPI, k8sResults, nil
}
// TestGetObjects tests both api.GetObjects() and
// metadataAPI.GetByNamespaceFiltered()
func TestGetObjects(t *testing.T) {
type getObjectsExpected struct {
resources
err error
namespace string
resType string
name string
}
t.Run("Returns expected objects based on input", func(t *testing.T) {
expectations := []getObjectsExpected{
{
err: status.Errorf(codes.Unimplemented, "unimplemented resource type: bar"),
namespace: "foo",
resType: "bar",
name: "baz",
resources: resources{
results: []string{},
misc: []string{},
},
},
{
err: nil,
namespace: "my-ns",
resType: k8s.Pod,
name: "my-pod",
resources: resources{
results: []string{`
apiVersion: v1
kind: Pod
metadata:
name: my-pod
namespace: my-ns
spec:
containers:
- name: my-pod
status:
phase: Running`,
},
misc: []string{},
},
},
{
err: errors.New("\"my-pod\" not found"),
namespace: "not-my-ns",
resType: k8s.Pod,
name: "my-pod",
resources: resources{
results: []string{},
misc: []string{`
apiVersion: v1
kind: Pod
metadata:
name: my-pod
namespace: my-ns`,
},
},
},
{
err: nil,
namespace: "",
resType: k8s.ReplicationController,
name: "",
resources: resources{
results: []string{`
apiVersion: v1
kind: ReplicationController
metadata:
name: my-rc
namespace: my-ns`,
},
misc: []string{},
},
},
{
err: nil,
namespace: "my-ns",
resType: k8s.Deployment,
name: "",
resources: resources{
results: []string{`
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-deploy
namespace: my-ns`,
},
misc: []string{`
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-deploy
namespace: not-my-ns`,
},
},
},
{
err: nil,
namespace: "",
resType: k8s.DaemonSet,
name: "",
resources: resources{
results: []string{`
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: my-ds
namespace: my-ns`,
},
},
},
{
err: nil,
namespace: "my-ns",
resType: k8s.DaemonSet,
name: "my-ds",
resources: resources{
results: []string{`
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: my-ds
namespace: my-ns`,
},
misc: []string{`
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: my-ds
namespace: not-my-ns`,
},
},
},
{
err: nil,
namespace: "my-ns",
resType: k8s.Job,
name: "my-job",
resources: resources{
results: []string{`
apiVersion: batch/v1
kind: Job
metadata:
name: my-job
namespace: my-ns`,
},
misc: []string{`
apiVersion: batch/v1
kind: Job
metadata:
name: my-job
namespace: not-my-ns`,
},
},
},
{
err: nil,
namespace: "my-ns",
resType: k8s.CronJob,
name: "my-cronjob",
resources: resources{
results: []string{`
apiVersion: batch/v1
kind: CronJob
metadata:
name: my-cronjob
namespace: my-ns`,
},
misc: []string{`
apiVersion: batch/v1
kind: CronJob
metadata:
name: my-cronjob
namespace: not-my-ns`,
},
},
},
{
err: nil,
namespace: "",
resType: k8s.StatefulSet,
name: "",
resources: resources{
results: []string{`
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: my-ss
namespace: my-ns`,
},
},
},
{
err: nil,
namespace: "my-ns",
resType: k8s.StatefulSet,
name: "my-ss",
resources: resources{
results: []string{`
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: my-ss
namespace: my-ns`,
},
misc: []string{`
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: my-ss
namespace: not-my-ns`,
},
},
},
{
err: nil,
namespace: "",
resType: k8s.Namespace,
name: "",
resources: resources{
results: []string{`
apiVersion: v1
kind: Namespace
metadata:
name: my-ns`,
},
misc: []string{},
},
},
}
for _, exp := range expectations {
api, metadataAPI, k8sResults, err := newMockAPI(true, exp.resources)
if err != nil {
t.Fatalf("newMockAPI error: %s", err)
}
pods, err := api.GetObjects(exp.namespace, exp.resType, exp.name, labels.Everything())
if err != nil || exp.err != nil {
if unexpectedErrors(err, exp.err) {
t.Fatalf("api.GetObjects() unexpected error, expected [%s] got: [%s]", exp.err, err)
}
} else {
if diff := deep.Equal(pods, k8sResults); diff != nil {
t.Fatalf("Expected: %+v", diff)
}
}
var objMetas []*metav1.PartialObjectMetadata
res, err := GetAPIResource(exp.resType)
if err == nil {
objMetas, err = metadataAPI.GetByNamespaceFiltered(res, exp.namespace, exp.name, labels.Everything())
}
if err != nil || exp.err != nil {
if unexpectedErrors(err, exp.err) {
fmt.Printf("objMetas: %#v\n", objMetas)
t.Fatalf("metadataAPI.GetNamespaceFilteredCache() unexpected error, expected [%s] got: [%s]", exp.err, err)
}
} else {
expMetas := []*metav1.PartialObjectMetadata{}
for _, obj := range k8sResults {
objMeta, err := toPartialObjectMetadata(obj)
if err != nil {
t.Fatalf("error converting Object to PartialObjectMetadata: %s", err)
}
expMetas = append(expMetas, objMeta)
}
if diff := deep.Equal(objMetas, expMetas); diff != nil {
t.Fatalf("Expected: %+v", diff)
}
}
}
})
t.Run("If objects are pods", func(t *testing.T) {
t.Run("Return running or pending pods", func(t *testing.T) {
expectations := []getObjectsExpected{
{
err: nil,
namespace: "my-ns",
resType: k8s.Pod,
name: "my-pod",
resources: resources{
results: []string{`
apiVersion: v1
kind: Pod
metadata:
name: my-pod
namespace: my-ns
spec:
containers:
- name: my-pod
status:
phase: Running`,
},
},
},
{
err: nil,
namespace: "my-ns",
resType: k8s.Pod,
name: "my-pod",
resources: resources{
results: []string{`
apiVersion: v1
kind: Pod
metadata:
name: my-pod
namespace: my-ns
spec:
containers:
- name: my-pod
status:
phase: Pending`,
},
},
},
}
for _, exp := range expectations {
api, _, k8sResults, err := newMockAPI(true, exp.resources)
if err != nil {
t.Fatalf("newMockAPI error: %s", err)
}
pods, err := api.GetObjects(exp.namespace, exp.resType, exp.name, labels.Everything())
if err != nil {
t.Fatalf("api.GetObjects() unexpected error %s", err)
}
if diff := deep.Equal(pods, k8sResults); diff != nil {
t.Fatalf("%+v", diff)
}
}
})
t.Run("Don't return failed or succeeded pods", func(t *testing.T) {
expectations := []getObjectsExpected{
{
err: nil,
namespace: "my-ns",
resType: k8s.Pod,
name: "my-pod",
resources: resources{
results: []string{`
apiVersion: v1
kind: Pod
metadata:
name: my-pod
namespace: my-ns
spec:
containers:
- name: my-pod
status:
phase: Succeeded`,
},
},
},
{
err: nil,
namespace: "my-ns",
resType: k8s.Pod,
name: "my-pod",
resources: resources{
results: []string{`
apiVersion: v1
kind: Pod
metadata:
name: my-pod
namespace: my-ns
spec:
containers:
- name: my-pod
status:
phase: Failed`,
},
},
},
}
for _, exp := range expectations {
api, _, _, err := newMockAPI(true, exp.resources)
if err != nil {
t.Fatalf("newMockAPI error: %s", err)
}
pods, err := api.GetObjects(exp.namespace, exp.resType, exp.name, labels.Everything())
if err != nil {
t.Fatalf("api.GetObjects() unexpected error %s", err)
}
if len(pods) != 0 {
t.Errorf("Expected no terminating or failed pods to be returned but got %d pods", len(pods))
}
}
})
})
}
func TestGetPodsFor(t *testing.T) {
type getPodsForExpected struct {
resources
err error
k8sResInput string // object used as input to GetPodFor()
}
t.Run("Returns expected pods based on input", func(t *testing.T) {
expectations := []getPodsForExpected{
{
err: nil,
k8sResInput: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: emoji
namespace: emojivoto
spec:
selector:
matchLabels:
app: emoji-svc`,
resources: resources{
results: []string{},
misc: []string{`
apiVersion: v1
kind: Pod
metadata:
name: emojivoto-meshed-finished
namespace: emojivoto
labels:
app: emoji-svc
ownerReferences:
- apiVersion: apps/v1
status:
phase: Finished`,
},
},
},
// Retrieve pods associated to a ClusterIP service
{
err: nil,
k8sResInput: `
apiVersion: v1
kind: Service
metadata:
name: emoji-svc
namespace: emojivoto
uid: serviceUIDDoesNotMatter
spec:
type: ClusterIP
selector:
app: emoji-svc`,
resources: resources{
results: []string{`
apiVersion: v1
kind: Pod
metadata:
name: emojivoto-meshed-finished
namespace: emojivoto
labels:
app: emoji-svc
ownerReferences:
- apiVersion: apps/v1
status:
phase: Running`,
},
misc: []string{},
},
},
// ExternalName services shouldn't return any pods
{
err: nil,
k8sResInput: `
apiVersion: v1
kind: Service
metadata:
name: emoji-svc
namespace: emojivoto
spec:
type: ExternalName
externalName: someapi.example.com`,
resources: resources{
results: []string{},
misc: []string{`
apiVersion: v1
kind: Pod
metadata:
name: emojivoto-meshed-finished
namespace: emojivoto
labels:
app: emoji-svc
status:
phase: Running`,
},
},
},
// Cronjob
{
err: nil,
k8sResInput: `
apiVersion: batch/v1
kind: CronJob
metadata:
name: emoji
namespace: emojivoto
uid: cronjob`,
resources: resources{
results: []string{`
apiVersion: v1
kind: Pod
metadata:
name: emojivoto-meshed
namespace: emojivoto
labels:
app: emoji-svc
ownerReferences:
- apiVersion: batch/v1
uid: job
status:
phase: Running`,
},
misc: []string{`
apiVersion: batch/v1
kind: Job
metadata:
name: emoji
namespace: emojivoto
uid: job
ownerReferences:
- apiVersion: batch/v1
uid: cronjob
spec:
selector:
matchLabels:
app: emoji-svc`,
},
},
},
// Daemonset
{
err: nil,
k8sResInput: `
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: emoji
namespace: emojivoto
uid: daemonset
spec:
selector:
matchLabels:
app: emoji-svc`,
resources: resources{
results: []string{`
apiVersion: v1
kind: Pod
metadata:
name: emojivoto-meshed
namespace: emojivoto
labels:
app: emoji-svc
ownerReferences:
- apiVersion: apps/v1
uid: daemonset
status:
phase: Running`,
},
misc: []string{},
},
},
// replicaset
{
err: nil,
k8sResInput: `
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: emoji
namespace: emojivoto
uid: replicaset
spec:
selector:
matchLabels:
app: emoji-svc`,
resources: resources{
results: []string{`
apiVersion: v1
kind: Pod
metadata:
name: emojivoto-meshed
namespace: emojivoto
labels:
app: emoji-svc
ownerReferences:
- apiVersion: apps/v1
uid: replicaset
status:
phase: Running`,
},
misc: []string{`
apiVersion: v1
kind: Pod
metadata:
name: emojivoto-meshed-finished
namespace: emojivoto
labels:
app: emoji-svc
ownerReferences:
- apiVersion: apps/v1
uid: replicaset
status:
phase: Finished`,
},
},
},
// single pod
{
err: nil,
k8sResInput: `
apiVersion: v1
kind: Pod
metadata:
name: emojivoto-meshed
namespace: emojivoto
labels:
app: emoji-svc
ownerReferences:
- apiVersion: apps/v1
uid: singlePod
status:
phase: Running`,
resources: resources{
results: []string{`
apiVersion: v1
kind: Pod
metadata:
name: emojivoto-meshed
namespace: emojivoto
labels:
app: emoji-svc
ownerReferences:
- apiVersion: apps/v1
uid: singlePod
status:
phase: Running`,
},
misc: []string{`
apiVersion: v1
kind: Pod
metadata:
name: emojivoto-meshed_2
namespace: emojivoto
labels:
app: emoji-svc
status:
phase: Running`,
},
},
},
// deployment
{
err: nil,
k8sResInput: `
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
deployment.kubernetes.io/revision: "2"
name: emojivoto-meshed
namespace: emojivoto
uid: deployment
labels:
app: emoji-svc
spec:
selector:
matchLabels:
app: emoji-svc`,
resources: resources{
results: []string{`
apiVersion: v1
kind: Pod
metadata:
name: emojivoto-meshed
namespace: emojivoto
ownerReferences:
- apiVersion: apps/v1
uid: deploymentRS
labels:
app: emoji-svc
pod-template-hash: deploymentPod
status:
phase: Running`,
},
misc: []string{`
apiVersion: apps/v1
kind: ReplicaSet
metadata:
uid: deploymentRS
annotations:
deployment.kubernetes.io/revision: "2"
name: emojivoto-meshed_2
namespace: emojivoto
labels:
app: emoji-svc
pod-template-hash: deploymentPod
ownerReferences:
- apiVersion: apps/v1
uid: deployment
spec:
selector:
matchLabels:
app: emoji-svc
pod-template-hash: deploymentPod`,
`apiVersion: apps/v1
kind: ReplicaSet
metadata:
uid: deploymentRSOld
annotations:
deployment.kubernetes.io/revision: "1"
name: emojivoto-meshed_1
namespace: emojivoto
labels:
app: emoji-svc
pod-template-hash: deploymentPodOld
ownerReferences:
- apiVersion: apps/v1
uid: deployment
spec:
selector:
matchLabels:
app: emoji-svc
pod-template-hash: deploymentPodOld`,
},
},
},
// deployment without RS
{
err: nil,
k8sResInput: `
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
deployment.kubernetes.io/revision: "2"
name: emojivoto-meshed
namespace: emojivoto
uid: deploymentWithoutRS
labels:
app: emoji-svc
spec:
selector:
matchLabels:
app: emoji-svc`,
resources: resources{
results: []string{},
misc: []string{`
apiVersion: apps/v1
kind: ReplicaSet
metadata:
uid: AnotherRS
annotations:
deployment.kubernetes.io/revision: "2"
name: emojivoto-meshed_2
namespace: emojivoto
labels:
app: emoji-svc
pod-template-hash: doesntMatter
ownerReferences:
- apiVersion: apps/v1
uid: doesntMatch
spec:
selector:
matchLabels:
app: emoji-svc
pod-template-hash: doesntMatter`,
},
},
},
// Deployment with 2 replicasets
{
err: nil,
k8sResInput: `
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
deployment.kubernetes.io/revision: "2"
name: emojivoto-meshed
namespace: emojivoto
uid: deployment2RS
labels:
app: emoji-svc
spec:
selector:
matchLabels:
app: emoji-svc`,
resources: resources{
results: []string{`
apiVersion: v1
kind: Pod
metadata:
name: emojivoto-meshed-pod1
namespace: emojivoto
ownerReferences:
- apiVersion: apps/v1
uid: RS1
labels:
app: emoji-svc
pod-template-hash: pod1
status:
phase: Running`,
`apiVersion: v1
kind: Pod
metadata:
name: emojivoto-meshed-pod2
namespace: emojivoto
ownerReferences:
- apiVersion: apps/v1
uid: RS2
labels:
app: emoji-svc
pod-template-hash: pod2
status:
phase: Running`,
},
misc: []string{`
apiVersion: apps/v1
kind: ReplicaSet
metadata:
uid: RS1
annotations:
deployment.kubernetes.io/revision: "2"
name: emojivoto-meshed_2
namespace: emojivoto
labels:
app: emoji-svc
pod-template-hash: pod1
ownerReferences:
- apiVersion: apps/v1
uid: deployment2RS
spec:
selector:
matchLabels:
app: emoji-svc
pod-template-hash: pod1`,
`apiVersion: apps/v1
kind: ReplicaSet
metadata:
uid: RS2
annotations:
deployment.kubernetes.io/revision: "1"
name: emojivoto-meshed_1
namespace: emojivoto
labels:
app: emoji-svc
pod-template-hash: pod2
ownerReferences:
- apiVersion: apps/v1
uid: deployment2RS
spec:
selector:
matchLabels:
app: emoji-svc
pod-template-hash: pod2`,
},
},
},
// Deployment 2 Pods just one valid
{
err: nil,
k8sResInput: `
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
deployment.kubernetes.io/revision: "2"
name: emojivoto-meshed
namespace: emojivoto
uid: deployment2Pods
labels:
app: emoji-svc
spec:
selector:
matchLabels:
app: emoji-svc`,
resources: resources{
results: []string{`apiVersion: v1
kind: Pod
metadata:
name: emojivoto-meshed-with-RS
namespace: emojivoto
ownerReferences:
- apiVersion: apps/v1
uid: validRS
labels:
app: emoji-svc
pod-template-hash: podWithRS
status:
phase: Running`,
},
misc: []string{`
apiVersion: apps/v1
kind: ReplicaSet
metadata:
uid: validRS
annotations:
deployment.kubernetes.io/revision: "2"
name: emojivoto-meshed_2
namespace: emojivoto
labels:
app: emoji-svc
pod-template-hash: podWithRS
ownerReferences:
- apiVersion: apps/v1
uid: deployment2Pods
spec:
selector:
matchLabels:
app: emoji-svc
pod-template-hash: podWithRS`,
`apiVersion: v1
kind: Pod
metadata:
name: emojivoto-meshed-without-RS
namespace: emojivoto
ownerReferences:
- apiVersion: apps/v1
uid: notHere
labels:
app: emoji-svc
pod-template-hash: invalidPod
status:
phase: Running`,
},
},
},
}
for _, exp := range expectations {
k8sInputObj, err := k8s.ToRuntimeObject(exp.k8sResInput)
if err != nil {
t.Fatalf("could not decode yml: %s", err)
}
api, _, k8sResults, err := newMockAPI(true, exp.resources)
if err != nil {
t.Fatalf("newMockAPI error: %s", err)
}
k8sResultPods := []*corev1.Pod{}
for _, obj := range k8sResults {
k8sResultPods = append(k8sResultPods, obj.(*corev1.Pod))
}
pods, err := api.GetPodsFor(k8sInputObj, false)
if !errors.Is(err, exp.err) {
t.Fatalf("api.GetPodsFor() unexpected error, expected [%s] got: [%s]", exp.err, err)
}
if len(pods) != len(k8sResultPods) {
t.Fatalf("Expected: %+v, Got: %+v", k8sResultPods, pods)
}
for _, pod := range pods {
found := false
for _, resultPod := range k8sResultPods {
if reflect.DeepEqual(pod, resultPod) {
found = true
break
}
}
if !found {
t.Fatalf("Expected: %+v, Got: %+v", k8sResultPods, pods)
}
}
}
})
}
// TestGetOwnerKindAndName tests GetOwnerKindAndName for both api and
// metadataAPI. Both return strings, so unlike TestGetObjects above, there's no
// need to create []*metav1.PartialObjectMetadata fixtures
func TestGetOwnerKindAndName(t *testing.T) {
for i, tt := range []struct {
resources
expectedOwnerKind string
expectedOwnerName string
}{
{
expectedOwnerKind: "deployment",
expectedOwnerName: "t2",
resources: resources{
results: []string{`
apiVersion: v1
kind: Pod
metadata:
name: t2-5f79f964bc-d5jvf
namespace: default
ownerReferences:
- apiVersion: apps/v1
kind: ReplicaSet
name: t2-5f79f964bc`,
},
misc: []string{`
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: t2-5f79f964bc
namespace: default
ownerReferences:
- apiVersion: apps/v1
kind: Deployment
name: t2`,
},
},
},
{
expectedOwnerKind: "replicaset",
expectedOwnerName: "t1-b4f55d87f",
resources: resources{
results: []string{`
apiVersion: v1
kind: Pod
metadata:
name: t1-b4f55d87f-98dbz
namespace: default
ownerReferences:
- apiVersion: apps/v1
kind: ReplicaSet
name: t1-b4f55d87f`,
},
},
},
{
expectedOwnerKind: "job",
expectedOwnerName: "slow-cooker",
resources: resources{
results: []string{`
apiVersion: v1
kind: Pod
metadata:
name: slow-cooker-bxtnq
namespace: default
ownerReferences:
- apiVersion: batch/v1
kind: Job
name: slow-cooker`,
},
},
},
{
expectedOwnerKind: "replicationcontroller",
expectedOwnerName: "web",
resources: resources{
results: []string{`
apiVersion: v1
kind: Pod
metadata:
name: web-dcfq4
namespace: default
ownerReferences:
- apiVersion: v1
kind: ReplicationController
name: web`,
},
},
},
{
expectedOwnerKind: "pod",
expectedOwnerName: "vote-bot",
resources: resources{
results: []string{`
apiVersion: v1
kind: Pod
metadata:
name: vote-bot
namespace: default`,
},
},
},
{
expectedOwnerKind: "cronjob",
expectedOwnerName: "my-cronjob",
resources: resources{
results: []string{`
apiVersion: v1
kind: Pod
metadata:
name: my-pod
namespace: my-ns
ownerReferences:
- apiVersion: batch/v1
kind: Job
name: my-job`,
},
misc: []string{`
apiVersion: batch/v1
kind: Job
metadata:
name: my-job
namespace: my-ns
ownerReferences:
- apiVersion: batch/v1
kind: CronJob
name: my-cronjob`,
},
},
},
{
expectedOwnerKind: "replicaset",
expectedOwnerName: "invalid-rs-parent-2abdffa",
resources: resources{
results: []string{`
apiVersion: v1
kind: Pod
metadata:
name: invalid-rs-parent-dcfq4
namespace: default
ownerReferences:
- apiVersion: v1
kind: ReplicaSet
name: invalid-rs-parent-2abdffa`,
},
misc: []string{`
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: invalid-rs-parent-2abdffa
namespace: default
ownerReferences:
- apiVersion: invalidParent/v1
kind: InvalidParentKind
name: invalid-parent`,
},
},
},
} {
tt := tt // pin
for _, retry := range []bool{
false,
true,
} {
retry := retry // pin
t.Run(fmt.Sprintf("%d/retry:%t", i, retry), func(t *testing.T) {
api, metadataAPI, objs, err := newMockAPI(!retry, tt.resources)
if err != nil {
t.Fatalf("newMockAPI error: %s", err)
}
pod := objs[0].(*corev1.Pod)
ownerKind, ownerName := api.GetOwnerKindAndName(context.Background(), pod, retry)
if ownerKind != tt.expectedOwnerKind {
t.Fatalf("Expected kind to be [%s], got [%s]", tt.expectedOwnerKind, ownerKind)
}
if ownerName != tt.expectedOwnerName {
t.Fatalf("Expected name to be [%s], got [%s]", tt.expectedOwnerName, ownerName)
}
ownerKind, ownerName, err = metadataAPI.GetOwnerKindAndName(context.Background(), pod, retry)
if err != nil {
t.Fatalf("Unexpected error: %s", err)
}
if ownerKind != tt.expectedOwnerKind {
t.Fatalf("Expected kind to be [%s], got [%s]", tt.expectedOwnerKind, ownerKind)
}
if ownerName != tt.expectedOwnerName {
t.Fatalf("Expected name to be [%s], got [%s]", tt.expectedOwnerName, ownerName)
}
})
}
}
}
func TestGetServiceProfileFor(t *testing.T) {
for _, tt := range []struct {
resources
expectedRouteNames []string
}{
// No service profiles -> default service profile
{
expectedRouteNames: []string{},
resources: resources{},
},
// Service profile in unrelated namespace -> default service profile
{
expectedRouteNames: []string{},
resources: resources{
results: []string{`
apiVersion: linkerd.io/v1alpha2
kind: ServiceProfile
metadata:
name: books.server.svc.cluster.local
namespace: linkerd
spec:
routes:
- condition:
pathRegex: /server
name: server`,
},
},
},
// Uses service profile in server namespace
{
expectedRouteNames: []string{"server"},
resources: resources{
results: []string{`
apiVersion: linkerd.io/v1alpha2
kind: ServiceProfile
metadata:
name: books.server.svc.cluster.local
namespace: server
spec:
routes:
- condition:
pathRegex: /server
name: server`,
},
},
},
// Uses service profile in client namespace
{
expectedRouteNames: []string{"client"},
resources: resources{
results: []string{`
apiVersion: linkerd.io/v1alpha2
kind: ServiceProfile
metadata:
name: books.server.svc.cluster.local
namespace: client
spec:
routes:
- condition:
pathRegex: /client
name: client`,
},
},
},
// Service profile in client namespace takes priority
{
expectedRouteNames: []string{"client"},
resources: resources{
results: []string{`
apiVersion: linkerd.io/v1alpha2
kind: ServiceProfile
metadata:
name: books.server.svc.cluster.local
namespace: server
spec:
routes:
- condition:
pathRegex: /server
name: server`,
`
apiVersion: linkerd.io/v1alpha2
kind: ServiceProfile
metadata:
name: books.server.svc.cluster.local
namespace: client
spec:
routes:
- condition:
pathRegex: /client
name: client`,
},
},
},
} {
api, _, _, err := newMockAPI(true, tt.resources)
if err != nil {
t.Fatalf("newMockAPI error: %s", err)
}
svc := corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "books",
Namespace: "server",
},
}
sp := api.GetServiceProfileFor(&svc, "client", "cluster.local")
if len(sp.Spec.Routes) != len(tt.expectedRouteNames) {
t.Fatalf("Expected %d routes, got %d", len(tt.expectedRouteNames), len(sp.Spec.Routes))
}
for i, route := range sp.Spec.Routes {
if tt.expectedRouteNames[i] != route.Name {
t.Fatalf("Expected route [%s], got [%s]", tt.expectedRouteNames[i], route.Name)
}
}
}
}
func TestGetServicesFor(t *testing.T) {
type getServicesForExpected struct {
resources
err error
k8sResInput string // object used as input to GetServicesFor()
}
t.Run("GetServicesFor", func(t *testing.T) {
expectations := []getServicesForExpected{
// If a service contains a pod, GetPodsFor should return the service.
{
err: nil,
k8sResInput: `
apiVersion: v1
kind: Pod
metadata:
name: my-pod
namespace: emojivoto
labels:
app: my-pod
status:
phase: Running`,
resources: resources{
results: []string{`
apiVersion: v1
kind: Service
metadata:
name: my-svc
namespace: emojivoto
spec:
type: ClusterIP
selector:
app: my-pod`,
},
misc: []string{},
},
},
}
for _, exp := range expectations {
k8sInputObj, err := k8s.ToRuntimeObject(exp.k8sResInput)
if err != nil {
t.Fatalf("could not decode yml: %s", err)
}
exp.misc = append(exp.misc, exp.k8sResInput)
api, _, k8sResults, err := newMockAPI(true, exp.resources)
if err != nil {
t.Fatalf("newMockAPI error: %s", err)
}
k8sResultServices := []*corev1.Service{}
for _, obj := range k8sResults {
k8sResultServices = append(k8sResultServices, obj.(*corev1.Service))
}
services, err := api.GetServicesFor(k8sInputObj, false)
if !errors.Is(err, exp.err) {
t.Fatalf("api.GetServicesFor() unexpected error, expected [%s] got: [%s]", exp.err, err)
}
if len(services) != len(k8sResultServices) {
t.Fatalf("Expected: %+v, Got: %+v", k8sResultServices, services)
}
for _, service := range services {
found := false
for _, resultService := range k8sResultServices {
if reflect.DeepEqual(service, resultService) {
found = true
break
}
}
if !found {
t.Fatalf("Expected: %+v, Got: %+v", k8sResultServices, services)
}
}
}
})
}
func unexpectedErrors(err, expErr error) bool {
return (err == nil && expErr != nil) ||
(err != nil && expErr == nil) ||
!strings.Contains(err.Error(), expErr.Error())
}