...

Source file src/github.com/linkerd/linkerd2/controller/proxy-injector/webhook_test.go

Documentation: github.com/linkerd/linkerd2/controller/proxy-injector

     1  package injector
     2  
     3  import (
     4  	"encoding/json"
     5  	"path/filepath"
     6  	"testing"
     7  
     8  	"github.com/go-test/deep"
     9  	"github.com/linkerd/linkerd2/controller/proxy-injector/fake"
    10  	"github.com/linkerd/linkerd2/pkg/charts/linkerd2"
    11  	"github.com/linkerd/linkerd2/pkg/inject"
    12  	pkgK8s "github.com/linkerd/linkerd2/pkg/k8s"
    13  	admissionv1beta1 "k8s.io/api/admission/v1beta1"
    14  	corev1 "k8s.io/api/core/v1"
    15  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    16  	"k8s.io/apimachinery/pkg/runtime"
    17  )
    18  
    19  type unmarshalledPatch []map[string]interface{}
    20  
    21  var (
    22  	values, _ = linkerd2.NewValues()
    23  )
    24  
    25  func confNsEnabled() *inject.ResourceConfig {
    26  	return inject.
    27  		NewResourceConfig(values, inject.OriginWebhook, "linkerd").
    28  		WithNsAnnotations(map[string]string{
    29  			pkgK8s.ProxyInjectAnnotation: pkgK8s.ProxyInjectEnabled,
    30  		})
    31  }
    32  
    33  func confNsDisabled() *inject.ResourceConfig {
    34  	return inject.NewResourceConfig(values, inject.OriginWebhook, "linkerd").
    35  		WithNsAnnotations(map[string]string{})
    36  }
    37  
    38  func confNsWithOpaquePorts() *inject.ResourceConfig {
    39  	return inject.
    40  		NewResourceConfig(values, inject.OriginWebhook, "linkerd").
    41  		WithNsAnnotations(map[string]string{
    42  			pkgK8s.ProxyInjectAnnotation:      pkgK8s.ProxyInjectEnabled,
    43  			pkgK8s.ProxyOpaquePortsAnnotation: "3306",
    44  		})
    45  }
    46  
    47  func confNsWithoutOpaquePorts() *inject.ResourceConfig {
    48  	return inject.
    49  		NewResourceConfig(values, inject.OriginWebhook, "linkerd").
    50  		WithNsAnnotations(map[string]string{
    51  			pkgK8s.ProxyInjectAnnotation: pkgK8s.ProxyInjectEnabled,
    52  		})
    53  }
    54  
    55  func confNsWithConfigAnnotations() *inject.ResourceConfig {
    56  	return inject.
    57  		NewResourceConfig(values, inject.OriginWebhook, "linkerd").
    58  		WithNsAnnotations(map[string]string{
    59  			pkgK8s.ProxyInjectAnnotation:                pkgK8s.ProxyInjectEnabled,
    60  			pkgK8s.ProxyIgnoreOutboundPortsAnnotation:   "34567",
    61  			pkgK8s.ProxyWaitBeforeExitSecondsAnnotation: "300",
    62  			"config.linkerd.io/invalid-key":             "invalid-value",
    63  		})
    64  }
    65  func TestGetPodPatch(t *testing.T) {
    66  
    67  	values.IdentityTrustAnchorsPEM = "IdentityTrustAnchorsPEM"
    68  
    69  	factory := fake.NewFactory(filepath.Join("fake", "data"))
    70  	nsEnabled, err := factory.Namespace("namespace-inject-enabled.yaml")
    71  	if err != nil {
    72  		t.Fatalf("Unexpected error: %s", err)
    73  	}
    74  
    75  	nsDisabled, err := factory.Namespace("namespace-inject-disabled.yaml")
    76  	if err != nil {
    77  		t.Fatalf("Unexpected error: %s", err)
    78  	}
    79  
    80  	t.Run("by checking annotations", func(t *testing.T) {
    81  		var testCases = []struct {
    82  			filename string
    83  			ns       *corev1.Namespace
    84  			conf     *inject.ResourceConfig
    85  		}{
    86  			{
    87  				filename: "pod-inject-empty.yaml",
    88  				ns:       nsEnabled,
    89  				conf:     confNsEnabled(),
    90  			},
    91  			{
    92  				filename: "pod-inject-enabled.yaml",
    93  				ns:       nsEnabled,
    94  				conf:     confNsEnabled(),
    95  			},
    96  			{
    97  				filename: "pod-inject-enabled.yaml",
    98  				ns:       nsDisabled,
    99  				conf:     confNsDisabled(),
   100  			},
   101  			{
   102  				filename: "pod-with-debug-disabled.yaml",
   103  				ns:       nsDisabled,
   104  				conf:     confNsDisabled(),
   105  			},
   106  		}
   107  
   108  		expectedPatchBytes, err := factory.FileContents("pod.patch.json")
   109  		if err != nil {
   110  			t.Fatalf("Unexpected error: %s", err)
   111  		}
   112  		expectedPatch, err := unmarshalPatch(expectedPatchBytes)
   113  		if err != nil {
   114  			t.Fatalf("Unexpected error: %s", err)
   115  		}
   116  
   117  		for _, testCase := range testCases {
   118  			testCase := testCase // pin
   119  			t.Run(testCase.filename, func(t *testing.T) {
   120  				pod, err := factory.FileContents(testCase.filename)
   121  				if err != nil {
   122  					t.Fatalf("Unexpected error: %s", err)
   123  				}
   124  
   125  				fakeReq := getFakePodReq(pod)
   126  				fullConf := testCase.conf.
   127  					WithKind(fakeReq.Kind.Kind).
   128  					WithOwnerRetriever(ownerRetrieverFake)
   129  				_, err = fullConf.ParseMetaAndYAML(fakeReq.Object.Raw)
   130  				if err != nil {
   131  					t.Fatal(err)
   132  				}
   133  
   134  				patchJSON, err := fullConf.GetPodPatch(true)
   135  				if err != nil {
   136  					t.Fatalf("Unexpected PatchForAdmissionRequest error: %s", err)
   137  				}
   138  				actualPatch, err := unmarshalPatch(patchJSON)
   139  				if err != nil {
   140  					t.Fatalf("Unexpected error: %s", err)
   141  				}
   142  				if diff := deep.Equal(expectedPatch, actualPatch); diff != nil {
   143  					t.Fatalf("The actual patch didn't match what was expected.\n%+v", diff)
   144  				}
   145  			})
   146  		}
   147  	})
   148  
   149  	t.Run("by checking annotations with debug", func(t *testing.T) {
   150  		expectedPatchBytes, err := factory.FileContents("pod-with-debug.patch.json")
   151  		if err != nil {
   152  			t.Fatalf("Unexpected error: %s", err)
   153  		}
   154  
   155  		expectedPatch, err := unmarshalPatch(expectedPatchBytes)
   156  		if err != nil {
   157  			t.Fatalf("Unexpected error: %s", err)
   158  		}
   159  
   160  		pod, err := factory.FileContents("pod-with-debug-enabled.yaml")
   161  		if err != nil {
   162  			t.Fatalf("Unexpected error: %s", err)
   163  		}
   164  		fakeReq := getFakePodReq(pod)
   165  		conf := confNsEnabled().WithKind(fakeReq.Kind.Kind).WithOwnerRetriever(ownerRetrieverFake)
   166  		_, err = conf.ParseMetaAndYAML(fakeReq.Object.Raw)
   167  		if err != nil {
   168  			t.Fatal(err)
   169  		}
   170  
   171  		patchJSON, err := conf.GetPodPatch(true)
   172  		if err != nil {
   173  			t.Fatalf("Unexpected PatchForAdmissionRequest error: %s", err)
   174  		}
   175  		actualPatch, err := unmarshalPatch(patchJSON)
   176  		if err != nil {
   177  			t.Fatalf("Unexpected error: %s", err)
   178  		}
   179  		if diff := deep.Equal(expectedPatch, actualPatch); diff != nil {
   180  			t.Fatalf("The actual patch didn't match what was expected.\n%+v", diff)
   181  		}
   182  	})
   183  
   184  	t.Run("by checking pod inherits config annotations from namespace", func(t *testing.T) {
   185  		expectedPatchBytes, err := factory.FileContents("pod-with-ns-annotations.patch.json")
   186  		if err != nil {
   187  			t.Fatalf("Unexpected error: %s", err)
   188  		}
   189  		expectedPatch, err := unmarshalPatch(expectedPatchBytes)
   190  		if err != nil {
   191  			t.Fatalf("Unexpected error: %s", err)
   192  		}
   193  
   194  		pod, err := factory.FileContents("pod-inject-enabled.yaml")
   195  		if err != nil {
   196  			t.Fatalf("Unexpected error: %s", err)
   197  		}
   198  		fakeReq := getFakePodReq(pod)
   199  		conf := confNsWithConfigAnnotations().
   200  			WithKind(fakeReq.Kind.Kind).
   201  			WithOwnerRetriever(ownerRetrieverFake)
   202  		_, err = conf.ParseMetaAndYAML(fakeReq.Object.Raw)
   203  		if err != nil {
   204  			t.Fatal(err)
   205  		}
   206  
   207  		// The namespace has two config annotations: one valid and one invalid
   208  		// the pod patch should only contain the valid annotation.
   209  		inject.AppendNamespaceAnnotations(conf.GetOverrideAnnotations(), conf.GetNsAnnotations(), conf.GetWorkloadAnnotations())
   210  		patchJSON, err := conf.GetPodPatch(true)
   211  		if err != nil {
   212  			t.Fatalf("Unexpected PatchForAdmissionRequest error: %s", err)
   213  		}
   214  		actualPatch, err := unmarshalPatch(patchJSON)
   215  		if err != nil {
   216  			t.Fatalf("Unexpected error: %s", err)
   217  		}
   218  		if diff := deep.Equal(expectedPatch, actualPatch); diff != nil {
   219  			t.Fatalf("The actual patch didn't match what was expected.\n+%v", diff)
   220  		}
   221  	})
   222  
   223  	t.Run("by checking container spec", func(t *testing.T) {
   224  		deployment, err := factory.FileContents("deployment-with-injected-proxy.yaml")
   225  		if err != nil {
   226  			t.Fatalf("Unexpected error: %s", err)
   227  		}
   228  
   229  		fakeReq := getFakePodReq(deployment)
   230  		conf := confNsDisabled().WithKind(fakeReq.Kind.Kind)
   231  		patchJSON, err := conf.GetPodPatch(true)
   232  		if err != nil {
   233  			t.Fatalf("Unexpected PatchForAdmissionRequest error: %s", err)
   234  		}
   235  
   236  		if len(patchJSON) == 0 {
   237  			t.Errorf("Expected empty patch")
   238  		}
   239  	})
   240  }
   241  
   242  func TestGetAnnotationPatch(t *testing.T) {
   243  	factory := fake.NewFactory(filepath.Join("fake", "data"))
   244  	nsWithOpaquePorts, err := factory.Namespace("namespace-with-opaque-ports.yaml")
   245  	if err != nil {
   246  		t.Fatalf("Unexpected error: %s", err)
   247  	}
   248  	nsWithoutOpaquePorts, err := factory.Namespace("namespace-inject-enabled.yaml")
   249  	if err != nil {
   250  		t.Fatalf("Unexpected error: %s", err)
   251  	}
   252  	t.Run("by checking patch annotations", func(t *testing.T) {
   253  		servicePatchBytes, err := factory.FileContents("annotation.patch.json")
   254  		if err != nil {
   255  			t.Fatalf("Unexpected error: %s", err)
   256  		}
   257  		servicePatch, err := unmarshalPatch(servicePatchBytes)
   258  		if err != nil {
   259  			t.Fatalf("Unexpected error: %s", err)
   260  		}
   261  		podPatchBytes, err := factory.FileContents("annotation.patch.json")
   262  		if err != nil {
   263  			t.Fatalf("Unexpected error: %s", err)
   264  		}
   265  		podPatch, err := unmarshalPatch(podPatchBytes)
   266  		if err != nil {
   267  			t.Fatalf("Unexpected error: %s", err)
   268  		}
   269  		filteredServiceBytes, err := factory.FileContents("filtered-service-opaque-ports.json")
   270  		if err != nil {
   271  			t.Fatalf("Unexpected error: %s", err)
   272  		}
   273  		filteredServicePatch, err := unmarshalPatch(filteredServiceBytes)
   274  		if err != nil {
   275  			t.Fatalf("Unexpected error: %s", err)
   276  		}
   277  		filteredPodBytes, err := factory.FileContents("filtered-pod-opaque-ports.json")
   278  		if err != nil {
   279  			t.Fatalf("Unexpected error: %s", err)
   280  		}
   281  		filteredPodPatch, err := unmarshalPatch(filteredPodBytes)
   282  		if err != nil {
   283  			t.Fatalf("Unexpected error: %s", err)
   284  		}
   285  		var testCases = []struct {
   286  			name               string
   287  			filename           string
   288  			ns                 *corev1.Namespace
   289  			conf               *inject.ResourceConfig
   290  			expectedPatchBytes []byte
   291  			expectedPatch      unmarshalledPatch
   292  		}{
   293  			{
   294  				name:               "service without opaque ports and namespace with",
   295  				filename:           "service-without-opaque-ports.yaml",
   296  				ns:                 nsWithOpaquePorts,
   297  				conf:               confNsWithOpaquePorts(),
   298  				expectedPatchBytes: servicePatchBytes,
   299  				expectedPatch:      servicePatch,
   300  			},
   301  			{
   302  				name:     "service with opaque ports and namespace with",
   303  				filename: "service-with-opaque-ports.yaml",
   304  				ns:       nsWithOpaquePorts,
   305  				conf:     confNsWithOpaquePorts(),
   306  			},
   307  			{
   308  				name:     "service with opaque ports and namespace without",
   309  				filename: "service-with-opaque-ports.yaml",
   310  				ns:       nsWithoutOpaquePorts,
   311  				conf:     confNsWithoutOpaquePorts(),
   312  			},
   313  			{
   314  				name:     "service without opaque ports and namespace without",
   315  				filename: "service-without-opaque-ports.yaml",
   316  				ns:       nsWithoutOpaquePorts,
   317  				conf:     confNsWithoutOpaquePorts(),
   318  			},
   319  			{
   320  				name:               "pod without opaque ports and namespace with",
   321  				filename:           "pod-without-opaque-ports.yaml",
   322  				ns:                 nsWithOpaquePorts,
   323  				conf:               confNsWithOpaquePorts(),
   324  				expectedPatchBytes: podPatchBytes,
   325  				expectedPatch:      podPatch,
   326  			},
   327  			{
   328  				name:     "pod with opaque ports and namespace with",
   329  				filename: "pod-with-opaque-ports.yaml",
   330  				ns:       nsWithOpaquePorts,
   331  				conf:     confNsWithOpaquePorts(),
   332  			},
   333  			{
   334  				name:     "pod with opaque ports and namespace without",
   335  				filename: "pod-with-opaque-ports.yaml",
   336  				ns:       nsWithoutOpaquePorts,
   337  				conf:     confNsWithoutOpaquePorts(),
   338  			},
   339  			{
   340  				name:     "pod without opaque ports and namespace without",
   341  				filename: "pod-without-opaque-ports.yaml",
   342  				ns:       nsWithoutOpaquePorts,
   343  				conf:     confNsWithoutOpaquePorts(),
   344  			},
   345  			{
   346  				name:               "service opaque ports are filtered",
   347  				filename:           "filter-service-opaque-ports.yaml",
   348  				ns:                 nsWithoutOpaquePorts,
   349  				conf:               confNsWithoutOpaquePorts(),
   350  				expectedPatchBytes: filteredServiceBytes,
   351  				expectedPatch:      filteredServicePatch,
   352  			},
   353  			{
   354  				name:               "pod opaque ports are filtered",
   355  				filename:           "filter-pod-opaque-ports.yaml",
   356  				ns:                 nsWithoutOpaquePorts,
   357  				conf:               confNsWithoutOpaquePorts(),
   358  				expectedPatchBytes: filteredPodBytes,
   359  				expectedPatch:      filteredPodPatch,
   360  			},
   361  		}
   362  		for _, testCase := range testCases {
   363  			testCase := testCase // pin
   364  			t.Run(testCase.name, func(t *testing.T) {
   365  				service, err := factory.FileContents(testCase.filename)
   366  				if err != nil {
   367  					t.Fatalf("Unexpected error: %s", err)
   368  				}
   369  				fakeReq := getFakeServiceReq(service)
   370  				fullConf := testCase.conf.
   371  					WithKind(fakeReq.Kind.Kind).
   372  					WithOwnerRetriever(ownerRetrieverFake)
   373  				_, err = fullConf.ParseMetaAndYAML(fakeReq.Object.Raw)
   374  				if err != nil {
   375  					t.Fatal(err)
   376  				}
   377  				patchJSON, err := fullConf.CreateOpaquePortsPatch()
   378  				if err != nil {
   379  					t.Fatalf("Unexpected error creating default opaque ports patch: %s", err)
   380  				}
   381  				if len(testCase.expectedPatchBytes) != 0 && len(patchJSON) == 0 {
   382  					t.Fatalf("There was no patch, but one was expected: %s", testCase.expectedPatchBytes)
   383  				} else if len(testCase.expectedPatchBytes) == 0 && len(patchJSON) != 0 {
   384  					t.Fatalf("No patch was expected, but one was returned: %s", patchJSON)
   385  				}
   386  				if len(testCase.expectedPatchBytes) == 0 {
   387  					return
   388  				}
   389  				actualPatch, err := unmarshalPatch(patchJSON)
   390  				if err != nil {
   391  					t.Fatalf("Unexpected error: %s", err)
   392  				}
   393  				if diff := deep.Equal(testCase.expectedPatch, actualPatch); diff != nil {
   394  					t.Fatalf("The actual patch didn't match what was expected.\n%+v", diff)
   395  				}
   396  			})
   397  		}
   398  	})
   399  }
   400  
   401  func getFakePodReq(b []byte) *admissionv1beta1.AdmissionRequest {
   402  	return &admissionv1beta1.AdmissionRequest{
   403  		Kind:      metav1.GroupVersionKind{Kind: "Pod"},
   404  		Name:      "foobar",
   405  		Namespace: "linkerd",
   406  		Object:    runtime.RawExtension{Raw: b},
   407  	}
   408  }
   409  
   410  func getFakeServiceReq(b []byte) *admissionv1beta1.AdmissionRequest {
   411  	return &admissionv1beta1.AdmissionRequest{
   412  		Kind:      metav1.GroupVersionKind{Kind: "Service"},
   413  		Name:      "foobar",
   414  		Namespace: "linkerd",
   415  		Object:    runtime.RawExtension{Raw: b},
   416  	}
   417  }
   418  
   419  func ownerRetrieverFake(p *corev1.Pod) (string, string, error) {
   420  	return pkgK8s.Deployment, "owner-deployment", nil
   421  }
   422  
   423  func unmarshalPatch(patchJSON []byte) (unmarshalledPatch, error) {
   424  	var actualPatch unmarshalledPatch
   425  	err := json.Unmarshal(patchJSON, &actualPatch)
   426  	if err != nil {
   427  		return nil, err
   428  	}
   429  
   430  	return actualPatch, nil
   431  }
   432  

View as plain text