linkerd2/test/inject/inject_test.go

371 lines
11 KiB
Go

package inject
import (
"fmt"
"os"
"strings"
"testing"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
jsonpatch "github.com/evanphx/json-patch"
"github.com/linkerd/linkerd2/pkg/k8s"
"github.com/linkerd/linkerd2/pkg/version"
"github.com/linkerd/linkerd2/testutil"
"sigs.k8s.io/yaml"
)
//////////////////////
/// TEST SETUP ///
//////////////////////
var TestHelper *testutil.TestHelper
func TestMain(m *testing.M) {
TestHelper = testutil.NewTestHelper()
os.Exit(m.Run())
}
//////////////////////
/// TEST EXECUTION ///
//////////////////////
func TestInject(t *testing.T) {
cmd := []string{"inject",
"--manual",
"--linkerd-namespace=fake-ns",
"--disable-identity",
"--ignore-cluster",
"--proxy-version=proxy-version",
"--proxy-image=proxy-image",
"--init-image=init-image",
"testdata/inject_test.yaml",
}
out, stderr, err := TestHelper.LinkerdRun(cmd...)
if err != nil {
t.Fatalf("Unexpected error: %v: %s", stderr, err)
}
err = validateInject(out, "injected_default.golden")
if err != nil {
t.Fatalf("Received unexpected output\n%s", err.Error())
}
}
func TestInjectParams(t *testing.T) {
// TODO: test config.linkerd.io/proxy-version
cmd := []string{"inject",
"--manual",
"--linkerd-namespace=fake-ns",
"--disable-identity",
"--disable-tap",
"--ignore-cluster",
"--proxy-version=proxy-version",
"--proxy-image=proxy-image",
"--init-image=init-image",
"--image-pull-policy=Never",
"--control-port=123",
"--skip-inbound-ports=234,345",
"--skip-outbound-ports=456,567",
"--inbound-port=678",
"--admin-port=789",
"--outbound-port=890",
"--proxy-cpu-request=10m",
"--proxy-memory-request=10Mi",
"--proxy-cpu-limit=20m",
"--proxy-memory-limit=20Mi",
"--proxy-uid=1337",
"--proxy-log-level=warn",
"--enable-external-profiles",
"testdata/inject_test.yaml",
}
out, stderr, err := TestHelper.LinkerdRun(cmd...)
if err != nil {
t.Fatalf("Unexpected error: %v: %s", stderr, err)
}
err = validateInject(out, "injected_params.golden")
if err != nil {
t.Fatalf("Received unexpected output\n%s", err.Error())
}
}
func TestNamespaceOverrideAnnotations(t *testing.T) {
// Check for Namespace level override of proxy Configurations
injectYAML, err := testutil.ReadFile("testdata/inject_test.yaml")
if err != nil {
t.Fatalf("failed to read inject test file: %s", err)
}
injectNS := "inject-namespace-override-test"
deployName := "inject-test-terminus"
nsProxyMemReq := "50Mi"
nsProxyCPUReq := "200m"
// Namespace level proxy configuration override
nsAnnotations := map[string]string{
k8s.ProxyInjectAnnotation: k8s.ProxyInjectEnabled,
k8s.ProxyCPURequestAnnotation: nsProxyCPUReq,
k8s.ProxyMemoryRequestAnnotation: nsProxyMemReq,
}
ns := TestHelper.GetTestNamespace(injectNS)
err = TestHelper.CreateNamespaceIfNotExists(ns, nsAnnotations)
if err != nil {
t.Fatalf("failed to create %s namespace: %s", ns, err)
}
// patch injectYAML with unique name and pod annotations
// Pod Level proxy configuration override
podProxyCPUReq := "600m"
podAnnotations := map[string]string{
k8s.ProxyCPURequestAnnotation: podProxyCPUReq,
}
patchedYAML, err := patchDeploy(injectYAML, deployName, podAnnotations)
if err != nil {
t.Fatalf("failed to patch inject test YAML in namespace %s for deploy/%s: %s", ns, deployName, err)
}
o, err := TestHelper.Kubectl(patchedYAML, "--namespace", ns, "create", "-f", "-")
if err != nil {
t.Fatalf("failed to create deploy/%s in namespace %s for %s: %s", deployName, ns, err, o)
}
o, err = TestHelper.Kubectl("", "--namespace", ns, "wait", "--for=condition=available", "--timeout=30s", "deploy/"+deployName)
if err != nil {
t.Fatalf("failed to wait for condition=available for deploy/%s in namespace %s: %s: %s", deployName, ns, err, o)
}
pods, err := TestHelper.GetPodsForDeployment(ns, deployName)
if err != nil {
t.Fatalf("failed to get pods for namespace %s: %s", ns, err)
}
containers := pods[0].Spec.Containers
proxyContainer := getProxyContainer(containers)
// Match the pod configuration with the namespace level overrides
if proxyContainer.Resources.Requests["memory"] != resource.MustParse(nsProxyMemReq) {
t.Fatalf("proxy memory resource request falied to match with namespace level override")
}
// Match with proxy level override
if proxyContainer.Resources.Requests["cpu"] != resource.MustParse(podProxyCPUReq) {
t.Fatalf("proxy cpu resource request falied to match with pod level override")
}
}
func TestAnnotationPermutations(t *testing.T) {
injectYAML, err := testutil.ReadFile("testdata/inject_test.yaml")
if err != nil {
t.Fatalf("failed to read inject test file: %s", err)
}
injectNS := "inject-test"
deployName := "inject-test-terminus"
injectAnnotations := []string{"", k8s.ProxyInjectDisabled, k8s.ProxyInjectEnabled}
// deploy
for _, nsAnnotation := range injectAnnotations {
nsPrefix := injectNS
nsAnnotations := map[string]string{}
if nsAnnotation != "" {
nsAnnotations[k8s.ProxyInjectAnnotation] = nsAnnotation
nsPrefix = fmt.Sprintf("%s-%s", nsPrefix, nsAnnotation)
}
ns := TestHelper.GetTestNamespace(nsPrefix)
err = TestHelper.CreateNamespaceIfNotExists(ns, nsAnnotations)
if err != nil {
t.Fatalf("failed to create %s namespace with annotation %s: %s", ns, nsAnnotation, err)
}
for _, podAnnotation := range injectAnnotations {
// patch injectYAML with unique name and pod annotations
name := deployName
podAnnotations := map[string]string{}
if podAnnotation != "" {
podAnnotations[k8s.ProxyInjectAnnotation] = podAnnotation
name = fmt.Sprintf("%s-%s", name, podAnnotation)
}
patchedYAML, err := patchDeploy(injectYAML, name, podAnnotations)
if err != nil {
t.Fatalf("failed to patch inject test YAML in namespace %s for deploy/%s: %s", ns, name, err)
}
o, err := TestHelper.Kubectl(patchedYAML, "--namespace", ns, "create", "-f", "-")
if err != nil {
t.Fatalf("failed to create deploy/%s in namespace %s for %s: %s", name, ns, err, o)
}
}
}
containerName := "bb-terminus"
// check for successful deploy
for _, nsAnnotation := range injectAnnotations {
nsPrefix := injectNS
if nsAnnotation != "" {
nsPrefix = fmt.Sprintf("%s-%s", nsPrefix, nsAnnotation)
}
ns := TestHelper.GetTestNamespace(nsPrefix)
for _, podAnnotation := range injectAnnotations {
name := deployName
if podAnnotation != "" {
name = fmt.Sprintf("%s-%s", name, podAnnotation)
}
o, err := TestHelper.Kubectl("", "--namespace", ns, "wait", "--for=condition=available", "--timeout=30s", "deploy/"+name)
if err != nil {
t.Fatalf("failed to wait for condition=available for deploy/%s in namespace %s: %s: %s", name, ns, err, o)
}
pods, err := TestHelper.GetPodsForDeployment(ns, name)
if err != nil {
t.Fatalf("failed to get pods for namespace %s: %s", ns, err)
}
if len(pods) != 1 {
t.Fatalf("expected 1 pod for namespace %s, got %d", ns, len(pods))
}
shouldBeInjected := false
switch nsAnnotation {
case "", k8s.ProxyInjectDisabled:
switch podAnnotation {
case k8s.ProxyInjectEnabled:
shouldBeInjected = true
}
case k8s.ProxyInjectEnabled:
switch podAnnotation {
case "", k8s.ProxyInjectEnabled:
shouldBeInjected = true
}
}
containers := pods[0].Spec.Containers
initContainers := pods[0].Spec.InitContainers
if shouldBeInjected {
if len(containers) != 2 {
t.Fatalf("expected 2 containers for pod %s/%s, got %d", ns, pods[0].GetName(), len(containers))
}
if containers[0].Name != containerName && containers[1].Name != containerName {
t.Fatalf("expected bb-terminus container in pod %s/%s, got %+v", ns, pods[0].GetName(), containers[0])
}
if containers[0].Name != k8s.ProxyContainerName && containers[1].Name != k8s.ProxyContainerName {
t.Fatalf("expected %s container in pod %s/%s, got %+v", ns, pods[0].GetName(), k8s.ProxyContainerName, containers[0])
}
if len(initContainers) != 1 {
t.Fatalf("expected 1 init container for pod %s/%s, got %d", ns, pods[0].GetName(), len(initContainers))
}
if initContainers[0].Name != k8s.InitContainerName {
t.Fatalf("expected %s init container in pod %s/%s, got %+v", ns, pods[0].GetName(), k8s.InitContainerName, initContainers[0])
}
} else {
if len(containers) != 1 {
t.Fatalf("expected 1 container for pod %s/%s, got %d", ns, pods[0].GetName(), len(containers))
}
if containers[0].Name != containerName {
t.Fatalf("expected bb-terminus container in pod %s/%s, got %s", ns, pods[0].GetName(), containers[0].Name)
}
if len(initContainers) != 0 {
t.Fatalf("expected 0 init containers for pod %s/%s, got %d", ns, pods[0].GetName(), len(initContainers))
}
}
}
}
}
func applyPatch(in string, patchJSON []byte) (string, error) {
patch, err := jsonpatch.DecodePatch(patchJSON)
if err != nil {
return "", err
}
json, err := yaml.YAMLToJSON([]byte(in))
if err != nil {
return "", err
}
patched, err := patch.Apply(json)
if err != nil {
return "", err
}
return string(patched), nil
}
func patchDeploy(in string, name string, annotations map[string]string) (string, error) {
ops := []string{
fmt.Sprintf(`{"op": "replace", "path": "/metadata/name", "value": "%s"}`, name),
fmt.Sprintf(`{"op": "replace", "path": "/spec/selector/matchLabels/app", "value": "%s"}`, name),
fmt.Sprintf(`{"op": "replace", "path": "/spec/template/metadata/labels/app", "value": "%s"}`, name),
}
if len(annotations) > 0 {
ops = append(ops, `{"op": "add", "path": "/spec/template/metadata/annotations", "value": {}}`)
for k, v := range annotations {
ops = append(ops,
fmt.Sprintf(`{"op": "add", "path": "/spec/template/metadata/annotations/%s", "value": "%s"}`, strings.Replace(k, "/", "~1", -1), v),
)
}
}
patchJSON := []byte(fmt.Sprintf("[%s]", strings.Join(ops, ",")))
return applyPatch(in, patchJSON)
}
func useTestImageTag(in string) (string, error) {
patchOps := []string{
fmt.Sprintf(`{"op": "replace", "path": "/spec/template/metadata/annotations/linkerd.io~1created-by", "value": "linkerd/cli %s"}`, TestHelper.GetVersion()),
fmt.Sprintf(`{"op": "replace", "path": "/spec/template/metadata/annotations/linkerd.io~1proxy-version", "value": "%s"}`, TestHelper.GetVersion()),
fmt.Sprintf(`{"op": "replace", "path": "/spec/template/spec/initContainers/0/image", "value": "init-image:%s"}`, version.ProxyInitVersion),
}
patchJSON := fmt.Sprintf("[%s]", strings.Join(patchOps, ","))
return applyPatch(in, []byte(patchJSON))
}
// validateInject is similar to `TestHelper.ValidateOutput`, but it pins the
// image tag used in some annotations and that of the proxy-init container,
// which vary from build to build.
func validateInject(actual, fixtureFile string) error {
actualPatched, err := useTestImageTag(actual)
if err != nil {
return err
}
fixture, err := testutil.ReadFile("testdata/" + fixtureFile)
if err != nil {
return err
}
fixturePatched, err := useTestImageTag(fixture)
if err != nil {
return err
}
if actualPatched != fixturePatched {
return fmt.Errorf(
"Expected:\n%s\nActual:\n%s", fixturePatched, actualPatched)
}
return nil
}
// Get Proxy Container from Containers
func getProxyContainer(containers []v1.Container) *v1.Container {
for _, c := range containers {
container := c
if container.Name == k8s.ProxyContainerName {
return &container
}
}
return nil
}