mirror of https://github.com/linkerd/linkerd2.git
1462 lines
28 KiB
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())
|
|
}
|