Add opaque ports namespace inheritance to pods (#5941)

### What

When a namespace has the opaque ports annotation, pods and services should
inherit it if they do not have one themselves. Currently, services do this but
pods do not. This can lead to surprising behavior where services are correctly
marked as opaque, but pods are not.

This changes the proxy-injector so that it now passes down the opaque ports
annotation to pods from their namespace if they do not have their own annotation
set. Closes #5736.

### How

The proxy-injector webhook receives admission requests for pods and services.
Regardless of the resource kind, it now checks if the resource should inherit
the opaque ports annotation from its namespace. It should inherit it if the
namespace has the annotation but the resource does not.

If the resource should inherit the annotation, the webhook creates an annotation
patch which is only responsible for adding the opaque ports annotation.

After generating the annotation patch, it checks if the resource is injectable.
From here there are a few scenarios:

1. If no annotation patch was created and the resource is not injectable, then
   admit the request with no changes. Examples of this are services with no OP
   annotation and inject-disabled pods with no OP annotation.
2. If the resource is a pod and it is injectable, create a patch that includes
   the proxy and proxy-init containers—as well as any other annotations and
   labels.
3. The above two scenarios lead to a patch being generated at this point, so no
   matter the resource the patch is returned.

### UI changes

Resources are now reported to either be "injected", "skipped", or "annotated".

The first pass at this PR worked around the fact that injection reports consider
services and namespaces injectable. This is not accurate because they don't have
pod templates that could be injected; they can however be annotated.

To fix this, an injection report now considers resources "annotatable" and uses
this to clean up some logic in the `inject` command, as well as avoid a more
complex proxy-injector webhook.

What's cool about this is it fixes some `inject` command output that would label
resources as "injected" when they were not even mutated. For example, namespaces
were always reported as being injected even if annotations were not added. Now,
it will properly report that a namespace has been "annotated" or "skipped".

### Tests

For testing, unit tests and integration tests have been added. Manual testing
can be done by installing linkerd with `debug` controller log levels, and
tailing the proxy-injector's app container when creating pods or services.

Signed-off-by: Kevin Leimkuhler <kevin@kleimkuhler.com>
This commit is contained in:
Kevin Leimkuhler 2021-03-29 19:41:15 -04:00 committed by GitHub
parent 3e54bf9af9
commit a11012819c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 502 additions and 169 deletions

View File

@ -29,7 +29,7 @@ const (
hostNetworkDesc = "pods do not use host networking"
sidecarDesc = "pods do not have a 3rd party proxy or initContainer already injected"
injectDisabledDesc = "pods are not annotated to disable injection"
unsupportedDesc = "at least one resource injected"
unsupportedDesc = "at least one resource can be injected or annotated"
udpDesc = "pod specs do not include UDP ports"
automountServiceAccountTokenDesc = "pods do not have automountServiceAccountToken set to \"false\""
slash = "/"
@ -171,20 +171,25 @@ func (rt resourceTransformerInject) transform(bytes []byte) ([]byte, []inject.Re
reports := []inject.Report{*report}
if conf.IsService() {
opaquePortsAnnotations := map[string]string{}
if opaquePorts, ok := rt.overrideAnnotations[k8s.ProxyOpaquePortsAnnotation]; ok {
opaquePortsAnnotations[k8s.ProxyOpaquePortsAnnotation] = opaquePorts
b, err := conf.AnnotateService(opaquePortsAnnotations)
return b, reports, err
opaquePorts, ok := rt.overrideAnnotations[k8s.ProxyOpaquePortsAnnotation]
if ok {
annotations := map[string]string{k8s.ProxyOpaquePortsAnnotation: opaquePorts}
bytes, err = conf.AnnotateService(annotations)
report.Annotated = true
}
return bytes, reports, nil
return bytes, reports, err
}
if rt.allowNsInject && conf.IsNamespace() {
bytes, err = conf.AnnotateNamespace(rt.overrideAnnotations)
report.Annotated = true
return bytes, reports, err
}
if conf.HasPodTemplate() {
conf.AppendPodAnnotations(rt.overrideAnnotations)
report.Annotated = true
}
if rt.allowNsInject && conf.IsNamespace() {
b, err := conf.InjectNamespace(rt.overrideAnnotations)
return b, reports, err
}
if b, _ := report.Injectable(); !b {
if ok, _ := report.Injectable(); !ok {
if errs := report.ThrowInjectError(); len(errs) > 0 {
return bytes, reports, fmt.Errorf("failed to inject %s%s%s: %v", report.Kind, slash, report.Name, concatErrors(errs, ", "))
}
@ -201,10 +206,6 @@ func (rt resourceTransformerInject) transform(bytes []byte) ([]byte, []inject.Re
conf.AppendPodAnnotation(k8s.ProxyInjectAnnotation, k8s.ProxyInjectEnabled)
}
if len(rt.overrideAnnotations) > 0 {
conf.AppendPodAnnotations(rt.overrideAnnotations)
}
patchJSON, err := conf.GetPodPatch(rt.injectProxy)
if err != nil {
return nil, nil, err
@ -236,6 +237,7 @@ func (rt resourceTransformerInject) transform(bytes []byte) ([]byte, []inject.Re
func (resourceTransformerInject) generateReport(reports []inject.Report, output io.Writer) {
injected := []inject.Report{}
annotatable := false
hostNetwork := []string{}
sidecar := []string{}
udp := []string{}
@ -248,6 +250,10 @@ func (resourceTransformerInject) generateReport(reports []inject.Report, output
injected = append(injected, r)
}
if r.IsAnnotatable() {
annotatable = true
}
if r.HostNetwork {
hostNetwork = append(hostNetwork, r.ResName())
warningsPrinted = true
@ -300,7 +306,7 @@ func (resourceTransformerInject) generateReport(reports []inject.Report, output
output.Write([]byte(fmt.Sprintf("%s %s\n", okStatus, injectDisabledDesc)))
}
if len(injected) == 0 {
if len(injected) == 0 && !annotatable {
output.Write([]byte(fmt.Sprintf("%s no supported objects found\n", warnStatus)))
warningsPrinted = true
} else if verbose {
@ -329,9 +335,14 @@ func (resourceTransformerInject) generateReport(reports []inject.Report, output
}
for _, r := range reports {
if b, _ := r.Injectable(); b {
if r.Annotated {
output.Write([]byte(fmt.Sprintf("%s \"%s\" annotated\n", r.Kind, r.Name)))
}
ok, _ := r.Injectable()
if ok {
output.Write([]byte(fmt.Sprintf("%s \"%s\" injected\n", r.Kind, r.Name)))
} else {
}
if !r.Annotated && !ok {
if r.Kind != "" {
output.Write([]byte(fmt.Sprintf("%s \"%s\" skipped\n", r.Kind, r.Name)))
} else {

View File

@ -2,7 +2,7 @@
√ pods do not use host networking
√ pods do not have a 3rd party proxy or initContainer already injected
√ pods are not annotated to disable injection
√ at least one resource injected
√ at least one resource can be injected or annotated
√ pod specs do not include UDP ports
√ pods do not have automountServiceAccountToken set to "false"

View File

@ -2,7 +2,7 @@
√ pods do not use host networking
√ pods do not have a 3rd party proxy or initContainer already injected
√ pods are not annotated to disable injection
√ at least one resource injected
√ at least one resource can be injected or annotated
√ pod specs do not include UDP ports
√ pods do not have automountServiceAccountToken set to "false"
@ -12,7 +12,7 @@ deployment "redis" injected
√ pods do not use host networking
√ pods do not have a 3rd party proxy or initContainer already injected
√ pods are not annotated to disable injection
√ at least one resource injected
√ at least one resource can be injected or annotated
√ pod specs do not include UDP ports
√ pods do not have automountServiceAccountToken set to "false"

View File

@ -2,7 +2,7 @@
√ pods do not use host networking
√ pods do not have a 3rd party proxy or initContainer already injected
√ pods are not annotated to disable injection
√ at least one resource injected
√ at least one resource can be injected or annotated
√ pod specs do not include UDP ports
√ pods do not have automountServiceAccountToken set to "false"

View File

@ -2,7 +2,7 @@
√ pods do not use host networking
√ pods do not have a 3rd party proxy or initContainer already injected
√ pods are not annotated to disable injection
√ at least one resource injected
√ at least one resource can be injected or annotated
√ pod specs do not include UDP ports
√ pods do not have automountServiceAccountToken set to "false"

View File

@ -2,7 +2,7 @@
√ pods do not use host networking
√ pods do not have a 3rd party proxy or initContainer already injected
√ pods are not annotated to disable injection
√ at least one resource injected
√ at least one resource can be injected or annotated
√ pod specs do not include UDP ports
√ pods do not have automountServiceAccountToken set to "false"

View File

@ -2,7 +2,7 @@
√ pods do not use host networking
√ pods do not have a 3rd party proxy or initContainer already injected
√ pods are not annotated to disable injection
√ at least one resource injected
√ at least one resource can be injected or annotated
√ pod specs do not include UDP ports
√ pods do not have automountServiceAccountToken set to "false"

View File

@ -2,7 +2,7 @@
√ pods do not use host networking
√ pods do not have a 3rd party proxy or initContainer already injected
√ pods are not annotated to disable injection
√ at least one resource injected
√ at least one resource can be injected or annotated
√ pod specs do not include UDP ports
√ pods do not have automountServiceAccountToken set to "false"

View File

@ -2,7 +2,7 @@
√ pods do not use host networking
√ pods do not have a 3rd party proxy or initContainer already injected
√ pods are not annotated to disable injection
√ at least one resource injected
√ at least one resource can be injected or annotated
√ pod specs do not include UDP ports
√ pods do not have automountServiceAccountToken set to "false"

View File

@ -2,7 +2,7 @@
√ pods do not use host networking
√ pods do not have a 3rd party proxy or initContainer already injected
√ pods are not annotated to disable injection
√ at least one resource injected
√ at least one resource can be injected or annotated
√ pod specs do not include UDP ports
√ pods do not have automountServiceAccountToken set to "false"

View File

@ -2,7 +2,7 @@
√ pods do not use host networking
√ pods do not have a 3rd party proxy or initContainer already injected
√ pods are not annotated to disable injection
√ at least one resource injected
√ at least one resource can be injected or annotated
√ pod specs do not include UDP ports
√ pods do not have automountServiceAccountToken set to "false"

View File

@ -1,6 +1,5 @@
‼ "linkerd.io/inject: disabled" annotation set on deployment/web
‼ no supported objects found
deployment "web" skipped

View File

@ -2,7 +2,7 @@
√ pods do not use host networking
√ pods do not have a 3rd party proxy or initContainer already injected
‼ "linkerd.io/inject: disabled" annotation set on deployment/web
‼ no supported objects found
√ at least one resource can be injected or annotated
√ pod specs do not include UDP ports
√ pods do not have automountServiceAccountToken set to "false"

View File

@ -2,7 +2,7 @@
√ pods do not use host networking
√ pods do not have a 3rd party proxy or initContainer already injected
√ pods are not annotated to disable injection
√ at least one resource injected
√ at least one resource can be injected or annotated
√ pod specs do not include UDP ports
√ pods do not have automountServiceAccountToken set to "false"

View File

@ -2,7 +2,7 @@
√ pods do not use host networking
√ pods do not have a 3rd party proxy or initContainer already injected
√ pods are not annotated to disable injection
√ at least one resource injected
√ at least one resource can be injected or annotated
‼ deployment/web uses "protocol: UDP"
√ pods do not have automountServiceAccountToken set to "false"

View File

@ -2,7 +2,7 @@
√ pods do not use host networking
√ pods do not have a 3rd party proxy or initContainer already injected
√ pods are not annotated to disable injection
√ at least one resource injected
√ at least one resource can be injected or annotated
√ pod specs do not include UDP ports
√ pods do not have automountServiceAccountToken set to "false"

View File

@ -2,7 +2,7 @@
√ pods do not use host networking
√ pods do not have a 3rd party proxy or initContainer already injected
√ pods are not annotated to disable injection
√ at least one resource injected
√ at least one resource can be injected or annotated
√ pod specs do not include UDP ports
√ pods do not have automountServiceAccountToken set to "false"

View File

@ -1,3 +1,3 @@
namespace "emojivoto" injected
namespace "emojivoto" skipped

View File

@ -2,9 +2,9 @@
√ pods do not use host networking
√ pods do not have a 3rd party proxy or initContainer already injected
√ pods are not annotated to disable injection
√ at least one resource injected
√ at least one resource can be injected or annotated
√ pod specs do not include UDP ports
√ pods do not have automountServiceAccountToken set to "false"
namespace "emojivoto" injected
namespace "emojivoto" skipped

View File

@ -2,7 +2,7 @@
√ pods do not use host networking
√ pods do not have a 3rd party proxy or initContainer already injected
√ pods are not annotated to disable injection
√ at least one resource injected
√ at least one resource can be injected or annotated
√ pod specs do not include UDP ports
√ pods do not have automountServiceAccountToken set to "false"

View File

@ -2,7 +2,7 @@
√ pods do not use host networking
√ pods do not have a 3rd party proxy or initContainer already injected
√ pods are not annotated to disable injection
√ at least one resource injected
√ at least one resource can be injected or annotated
√ pod specs do not include UDP ports
√ pods do not have automountServiceAccountToken set to "false"

View File

@ -2,7 +2,7 @@
√ pods do not use host networking
√ pods do not have a 3rd party proxy or initContainer already injected
√ pods are not annotated to disable injection
√ at least one resource injected
√ at least one resource can be injected or annotated
√ pod specs do not include UDP ports
√ pods do not have automountServiceAccountToken set to "false"

View File

@ -2,7 +2,7 @@
√ pods do not use host networking
√ pods do not have a 3rd party proxy or initContainer already injected
√ pods are not annotated to disable injection
√ at least one resource injected
√ at least one resource can be injected or annotated
√ pod specs do not include UDP ports
√ pods do not have automountServiceAccountToken set to "false"

View File

@ -2,7 +2,7 @@
√ pods do not use host networking
√ pods do not have a 3rd party proxy or initContainer already injected
√ pods are not annotated to disable injection
√ at least one resource injected
√ at least one resource can be injected or annotated
√ pod specs do not include UDP ports
√ pods do not have automountServiceAccountToken set to "false"

View File

@ -1,4 +1,9 @@
[
{
"op": "add",
"path": "/metadata/annotations",
"value": {}
},
{
"op": "add",
"path": "/metadata/annotations/config.linkerd.io~1opaque-ports",

View File

@ -0,0 +1,15 @@
---
apiVersion: v1
kind: Pod
metadata:
name: pod
namespace: kube-public
annotations:
config.linkerd.io/opaque-ports: "8080"
spec:
containers:
- name: nginx
image: nginx
ports:
- name: http
containerPort: 80

View File

@ -0,0 +1,13 @@
---
apiVersion: v1
kind: Pod
metadata:
name: pod
namespace: kube-public
spec:
containers:
- name: nginx
image: nginx
ports:
- name: http
containerPort: 80

View File

@ -5,8 +5,6 @@ import (
"fmt"
"strings"
"k8s.io/apimachinery/pkg/labels"
"github.com/linkerd/linkerd2/controller/k8s"
"github.com/linkerd/linkerd2/pkg/config"
"github.com/linkerd/linkerd2/pkg/inject"
@ -15,6 +13,7 @@ import (
log "github.com/sirupsen/logrus"
admissionv1beta1 "k8s.io/api/admission/v1beta1"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/tools/record"
)
@ -34,35 +33,34 @@ func Inject(
) (*admissionv1beta1.AdmissionResponse, error) {
log.Debugf("request object bytes: %s", request.Object.Raw)
// Build the resource config based off the request metadata and kind of
// object. This is later used to build the injection report and generated
// patch.
valuesConfig, err := config.Values(pkgK8s.MountPathValuesConfig)
if err != nil {
return nil, err
}
namespace, err := api.NS().Lister().Get(request.Namespace)
if err != nil {
return nil, err
}
nsAnnotations := namespace.GetAnnotations()
resourceConfig := inject.NewResourceConfig(valuesConfig, inject.OriginWebhook).
WithOwnerRetriever(ownerRetriever(ctx, api, request.Namespace)).
WithNsAnnotations(nsAnnotations).
WithKind(request.Kind.Kind)
// Build the injection report.
report, err := resourceConfig.ParseMetaAndYAML(request.Object.Raw)
if err != nil {
return nil, err
}
log.Infof("received %s", report.ResName())
admissionResponse := &admissionv1beta1.AdmissionResponse{
UID: request.UID,
Allowed: true,
}
configLabels := configToPrometheusLabels(resourceConfig)
// If the resource has an owner, then it should be retrieved for recording
// events.
var parent *runtime.Object
ownerKind := ""
var ownerKind string
if ownerRef := resourceConfig.GetOwnerRef(); ownerRef != nil {
objs, err := api.GetObjects(request.Namespace, ownerRef.Kind, ownerRef.Name, labels.Everything())
if err != nil {
@ -74,9 +72,64 @@ func Inject(
}
ownerKind = strings.ToLower(ownerRef.Kind)
}
configLabels := configToPrometheusLabels(resourceConfig)
proxyInjectionAdmissionRequests.With(admissionRequestLabels(ownerKind, request.Namespace, report.InjectAnnotationAt, configLabels)).Inc()
if injectable, reasons := report.Injectable(); !injectable {
// If the pod's namespace has the opaque ports annotation but the pod does
// not, then it should be added to the pod template metadata.
opaquePorts, opaquePortsOk := resourceConfig.GetOpaquePorts()
if resourceConfig.IsPod() && opaquePortsOk {
resourceConfig.AppendPodAnnotation(pkgK8s.ProxyOpaquePortsAnnotation, opaquePorts)
}
// If the resource is injectable then admit it after creating a patch that
// adds the proxy-init and proxy containers.
injectable, reasons := report.Injectable()
if injectable {
resourceConfig.AppendPodAnnotation(pkgK8s.CreatedByAnnotation, fmt.Sprintf("linkerd/proxy-injector %s", version.Version))
patchJSON, err := resourceConfig.GetPodPatch(true)
if err != nil {
return nil, err
}
if parent != nil {
recorder.Event(*parent, v1.EventTypeNormal, eventTypeInjected, "Linkerd sidecar proxy injected")
}
log.Infof("injection patch generated for: %s", report.ResName())
log.Debugf("injection patch: %s", patchJSON)
proxyInjectionAdmissionResponses.With(admissionResponseLabels(ownerKind, request.Namespace, "false", "", report.InjectAnnotationAt, configLabels)).Inc()
patchType := admissionv1beta1.PatchTypeJSONPatch
return &admissionv1beta1.AdmissionResponse{
UID: request.UID,
Allowed: true,
PatchType: &patchType,
Patch: patchJSON,
}, nil
}
// If the resource is not injectable but does need the opaque ports
// annotation added, then admit it after creating a patch that adds the
// annotation.
if opaquePortsOk {
patchJSON, err := resourceConfig.CreateAnnotationPatch(opaquePorts)
if err != nil {
return nil, err
}
log.Infof("annotation patch generated for: %s", report.ResName())
log.Debugf("annotation patch: %s", patchJSON)
proxyInjectionAdmissionResponses.With(admissionResponseLabels(ownerKind, request.Namespace, "false", "", report.InjectAnnotationAt, configLabels)).Inc()
patchType := admissionv1beta1.PatchTypeJSONPatch
return &admissionv1beta1.AdmissionResponse{
UID: request.UID,
Allowed: true,
PatchType: &patchType,
Patch: patchJSON,
}, nil
}
// The resource should be admitted without a patch. If it is a pod, create
// an event to record that injection was skipped.
if resourceConfig.IsPod() {
var readableReasons, metricReasons string
metricReasons = strings.Join(reasons, ",")
for _, reason := range reasons {
@ -89,37 +142,15 @@ func Inject(
}
log.Infof("skipped %s: %s", report.ResName(), readableReasons)
proxyInjectionAdmissionResponses.With(admissionResponseLabels(ownerKind, request.Namespace, "true", metricReasons, report.InjectAnnotationAt, configLabels)).Inc()
return admissionResponse, nil
return &admissionv1beta1.AdmissionResponse{
UID: request.UID,
Allowed: true,
}, nil
}
resourceConfig.AppendPodAnnotations(map[string]string{
pkgK8s.CreatedByAnnotation: fmt.Sprintf("linkerd/proxy-injector %s", version.Version),
})
var patchJSON []byte
if resourceConfig.IsService() {
patchJSON, err = resourceConfig.GetServicePatch()
} else {
patchJSON, err = resourceConfig.GetPodPatch(true)
}
if err != nil {
return nil, err
}
if len(patchJSON) == 0 {
return admissionResponse, nil
}
if parent != nil {
recorder.Event(*parent, v1.EventTypeNormal, eventTypeInjected, "Linkerd sidecar proxy injected")
}
log.Infof("patch generated for: %s", report.ResName())
log.Debugf("patch: %s", patchJSON)
proxyInjectionAdmissionResponses.With(admissionResponseLabels(ownerKind, request.Namespace, "false", "", report.InjectAnnotationAt, configLabels)).Inc()
patchType := admissionv1beta1.PatchTypeJSONPatch
admissionResponse.Patch = patchJSON
admissionResponse.PatchType = &patchType
return admissionResponse, nil
return &admissionv1beta1.AdmissionResponse{
UID: request.UID,
Allowed: true,
}, nil
}
func ownerRetriever(ctx context.Context, api *k8s.API, ns string) inject.OwnerRetrieverFunc {

View File

@ -190,7 +190,7 @@ func TestGetPodPatch(t *testing.T) {
})
}
func TestGetServicePatch(t *testing.T) {
func TestGetAnnotationPatch(t *testing.T) {
factory := fake.NewFactory(filepath.Join("fake", "data"))
nsWithOpaquePorts, err := factory.Namespace("namespace-with-opaque-ports.yaml")
if err != nil {
@ -201,11 +201,19 @@ func TestGetServicePatch(t *testing.T) {
t.Fatalf("Unexpected error: %s", err)
}
t.Run("by checking patch annotations", func(t *testing.T) {
patchBytes, err := factory.FileContents("service.patch.json")
servicePatchBytes, err := factory.FileContents("annotation.patch.json")
if err != nil {
t.Fatalf("Unexpected error: %s", err)
}
patch, err := unmarshalPatch(patchBytes)
servicePatch, err := unmarshalPatch(servicePatchBytes)
if err != nil {
t.Fatalf("Unexpected error: %s", err)
}
podPatchBytes, err := factory.FileContents("annotation.patch.json")
if err != nil {
t.Fatalf("Unexpected error: %s", err)
}
podPatch, err := unmarshalPatch(podPatchBytes)
if err != nil {
t.Fatalf("Unexpected error: %s", err)
}
@ -222,8 +230,8 @@ func TestGetServicePatch(t *testing.T) {
filename: "service-without-opaque-ports.yaml",
ns: nsWithOpaquePorts,
conf: confNsWithOpaquePorts(),
expectedPatchBytes: patchBytes,
expectedPatch: patch,
expectedPatchBytes: servicePatchBytes,
expectedPatch: servicePatch,
},
{
name: "service with opaque ports and namespace with",
@ -243,6 +251,26 @@ func TestGetServicePatch(t *testing.T) {
ns: nsWithoutOpaquePorts,
conf: confNsWithoutOpaquePorts(),
},
{
name: "pod without opaque ports and namespace with",
filename: "pod-without-opaque-ports.yaml",
ns: nsWithOpaquePorts,
conf: confNsWithOpaquePorts(),
expectedPatchBytes: podPatchBytes,
expectedPatch: podPatch,
},
{
name: "pod with opaque ports and namespace with",
filename: "pod-with-opaque-ports.yaml",
ns: nsWithOpaquePorts,
conf: confNsWithOpaquePorts(),
},
{
name: "pod with opaque ports and namespace without",
filename: "pod-with-opaque-ports.yaml",
ns: nsWithoutOpaquePorts,
conf: confNsWithoutOpaquePorts(),
},
}
for _, testCase := range testCases {
testCase := testCase // pin
@ -259,9 +287,14 @@ func TestGetServicePatch(t *testing.T) {
if err != nil {
t.Fatal(err)
}
patchJSON, err := fullConf.GetServicePatch()
if err != nil {
t.Fatalf("Unexpected PatchForAdmissionRequest error: %s", err)
var patchJSON []byte
opaquePorts, ok := fullConf.GetOpaquePorts()
if ok {
fullConf.AppendPodAnnotation(pkgK8s.ProxyOpaquePortsAnnotation, opaquePorts)
patchJSON, err = fullConf.CreateAnnotationPatch(opaquePorts)
if err != nil {
t.Fatalf("Unexpected PatchForAdmissionRequest error: %s", err)
}
}
if len(testCase.expectedPatchBytes) != 0 && len(patchJSON) == 0 {
t.Fatalf("There was no patch, but one was expected: %s", testCase.expectedPatchBytes)

View File

@ -1,6 +1,13 @@
package inject
var tpl = `[
{{- if .AddRootAnnotations }}
{
"op": "add",
"path": "/metadata/annotations",
"value": {}
},
{{- end }}
{
"op": "add",
"path": "/metadata/annotations/config.linkerd.io~1opaque-ports",

View File

@ -90,12 +90,13 @@ type OwnerRetrieverFunc func(*corev1.Pod) (string, string)
// ResourceConfig contains the parsed information for a given workload
type ResourceConfig struct {
// These values used for the rendering of the patch may be further overridden
// by the annotations on the resource or the resource's namespace.
// These values used for the rendering of the patch may be further
// overridden by the annotations on the resource or the resource's
// namespace.
values *l5dcharts.Values
// These annotations from the resources's namespace are used as a base.
// The resources's annotations will be applied on top of these, which allows
// the nsAnnotations to act as a default.
// The resources's annotations will be applied on top of these, which
// allows the nsAnnotations to act as a default.
nsAnnotations map[string]string
ownerRetriever OwnerRetrieverFunc
origin Origin
@ -103,11 +104,9 @@ type ResourceConfig struct {
workload struct {
obj runtime.Object
metaType metav1.TypeMeta
// Meta is the workload's metadata. It's exported so that metadata of
// non-workload resources can be unmarshalled by the YAML parser
Meta *metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
Meta *metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
ownerRef *metav1.OwnerReference
}
@ -136,8 +135,9 @@ type podPatch struct {
DebugContainer *l5dcharts.DebugContainer `json:"debugContainer"`
}
type servicePatch struct {
OpaquePorts string
type annotationPatch struct {
AddRootAnnotations bool
OpaquePorts string
}
// NewResourceConfig creates and initializes a ResourceConfig
@ -148,6 +148,7 @@ func NewResourceConfig(values *l5dcharts.Values, origin Origin) *ResourceConfig
origin: origin,
}
config.workload.Meta = &metav1.ObjectMeta{}
config.pod.meta = &metav1.ObjectMeta{}
// Values can be nil for commands like Uninject
@ -305,24 +306,36 @@ func (conf *ResourceConfig) GetPodPatch(injectProxy bool) ([]byte, error) {
return res, nil
}
// GetServicePatch returns the JSON patch containing the service opaque ports
// annotation if the annotation is present on the namespace, but absent on the
// service.
func (conf *ResourceConfig) GetServicePatch() ([]byte, error) {
_, ok := conf.workload.Meta.Annotations[k8s.ProxyOpaquePortsAnnotation]
// There does not need to be a patch if the service already has the
// annotation.
// GetOpaquePorts returns two values. The first value is the the opaque ports
// annotation value. The second is used to decide whether or not the caller
// should add the annotation. The caller should not add the annotation if the
// resource already has its own.
func (conf *ResourceConfig) GetOpaquePorts() (string, bool) {
_, ok := conf.pod.meta.Annotations[k8s.ProxyOpaquePortsAnnotation]
if ok {
return nil, nil
log.Debugf("using pod %s %s annotation value", conf.pod.meta.Name, k8s.ProxyOpaquePortsAnnotation)
return "", false
}
opaquePorts, ok := conf.nsAnnotations[k8s.ProxyOpaquePortsAnnotation]
// There does not need to be a patch if the namespace does not have the
// annotation.
if !ok {
return nil, nil
_, ok = conf.workload.Meta.Annotations[k8s.ProxyOpaquePortsAnnotation]
if ok {
log.Debugf("using service %s %s annotation value", conf.workload.Meta.Name, k8s.ProxyOpaquePortsAnnotation)
return "", false
}
patch := &servicePatch{
OpaquePorts: opaquePorts,
annotation, ok := conf.nsAnnotations[k8s.ProxyOpaquePortsAnnotation]
if ok {
log.Debugf("using namespace %s %s annotation value", conf.workload.Meta.Namespace, k8s.ProxyOpaquePortsAnnotation)
return annotation, true
}
return "", false
}
// CreateAnnotationPatch returns a json patch which adds the opaque ports
// annotation with the `opaquePorts` value.
func (conf *ResourceConfig) CreateAnnotationPatch(opaquePorts string) ([]byte, error) {
addRootAnnotations := len(conf.pod.meta.Annotations) == 0
patch := &annotationPatch{
AddRootAnnotations: addRootAnnotations,
OpaquePorts: opaquePorts,
}
t, err := template.New("tpl").Parse(tpl)
if err != nil {
@ -484,7 +497,6 @@ func (conf *ResourceConfig) parse(bytes []byte) error {
}
conf.workload.obj = v
conf.workload.Meta = &v.ObjectMeta
// If annotations not present previously
if conf.workload.Meta.Annotations == nil {
conf.workload.Meta.Annotations = map[string]string{}
}
@ -528,6 +540,9 @@ func (conf *ResourceConfig) parse(bytes []byte) error {
}
}
conf.pod.labels[k8s.WorkloadNamespaceLabel] = v.Namespace
if conf.pod.meta.Annotations == nil {
conf.pod.meta.Annotations = map[string]string{}
}
case *corev1.Service:
if err := yaml.Unmarshal(bytes, v); err != nil {
@ -547,16 +562,15 @@ func (conf *ResourceConfig) parse(bytes []byte) error {
}
}
if conf.pod.meta.Annotations == nil {
conf.pod.meta.Annotations = map[string]string{}
}
return nil
}
func (conf *ResourceConfig) complete(template *corev1.PodTemplateSpec) {
conf.pod.spec = &template.Spec
conf.pod.meta = &template.ObjectMeta
if conf.pod.meta.Annotations == nil {
conf.pod.meta.Annotations = map[string]string{}
}
}
// injectPodSpec adds linkerd sidecars to the provided PodSpec.
@ -921,20 +935,28 @@ func (conf *ResourceConfig) IsService() bool {
return strings.ToLower(conf.workload.metaType.Kind) == k8s.Service
}
//InjectNamespace annotates any given Namespace config
func (conf *ResourceConfig) InjectNamespace(annotations map[string]string) ([]byte, error) {
// IsPod checks if a given config is a workload of Kind pod.
func (conf *ResourceConfig) IsPod() bool {
return strings.ToLower(conf.workload.metaType.Kind) == k8s.Pod
}
// HasPodTemplate checks if a given config has a pod template spec.
func (conf *ResourceConfig) HasPodTemplate() bool {
return conf.pod.meta != nil && conf.pod.spec != nil
}
// AnnotateNamespace annotates a namespace resource config with `annotations`.
func (conf *ResourceConfig) AnnotateNamespace(annotations map[string]string) ([]byte, error) {
ns, ok := conf.workload.obj.(*corev1.Namespace)
if !ok {
return nil, errors.New("can't inject namespace. Type assertion failed")
}
ns.Annotations[k8s.ProxyInjectAnnotation] = k8s.ProxyInjectEnabled
//For overriding annotations
if len(annotations) > 0 {
for annotation, value := range annotations {
ns.Annotations[annotation] = value
}
}
j, err := getFilteredJSON(ns)
if err != nil {
return nil, err
@ -942,23 +964,18 @@ func (conf *ResourceConfig) InjectNamespace(annotations map[string]string) ([]by
return yaml.JSONToYAML(j)
}
// AnnotateService returns a Service with the appropriate annotations
// Currently, a `Service` may only need the `config.linkerd.io/opaque-ports` annotation via `inject`
// See - https://github.com/linkerd/linkerd2/pull/5721
// AnnotateService annotates a service resource config with `annotations`.
func (conf *ResourceConfig) AnnotateService(annotations map[string]string) ([]byte, error) {
ns, ok := conf.workload.obj.(*corev1.Service)
service, ok := conf.workload.obj.(*corev1.Service)
if !ok {
return nil, errors.New("can't inject service. Type assertion failed")
}
//For overriding annotations
if len(annotations) > 0 {
for annotation, value := range annotations {
ns.Annotations[annotation] = value
service.Annotations[annotation] = value
}
}
j, err := getFilteredJSON(ns)
j, err := getFilteredJSON(service)
if err != nil {
return nil, err
}

View File

@ -51,6 +51,8 @@ type Report struct {
InjectDisabled bool
InjectDisabledReason string
InjectAnnotationAt string
Annotatable bool
Annotated bool
AutomountServiceAccountToken bool
// Uninjected consists of two boolean flags to indicate if a proxy and
@ -68,13 +70,13 @@ type Report struct {
// from conf
func newReport(conf *ResourceConfig) *Report {
var name string
if m := conf.workload.Meta; m != nil {
name = m.Name
} else if m := conf.pod.meta; m != nil {
name = m.Name
if conf.IsPod() {
name = conf.pod.meta.Name
if name == "" {
name = m.GenerateName
name = conf.pod.meta.GenerateName
}
} else if m := conf.workload.Meta; m != nil {
name = m.Name
}
report := &Report{
@ -83,8 +85,8 @@ func newReport(conf *ResourceConfig) *Report {
AutomountServiceAccountToken: true,
}
if conf.pod.meta != nil && conf.pod.spec != nil {
report.InjectDisabled, report.InjectDisabledReason, report.InjectAnnotationAt = report.disableByAnnotation(conf)
if conf.HasPodTemplate() {
report.InjectDisabled, report.InjectDisabledReason, report.InjectAnnotationAt = report.disabledByAnnotation(conf)
report.HostNetwork = conf.pod.spec.HostNetwork
report.Sidecar = healthcheck.HasExistingSidecars(conf.pod.spec)
report.UDP = checkUDPPorts(conf.pod.spec)
@ -96,10 +98,14 @@ func newReport(conf *ResourceConfig) *Report {
report.AutomountServiceAccountToken = false
}
}
} else if report.Kind != k8s.Service && report.Kind != k8s.Namespace {
} else {
report.UnsupportedResource = true
}
if conf.HasPodTemplate() || conf.IsService() || conf.IsNamespace() {
report.Annotatable = true
}
return report
}
@ -136,6 +142,11 @@ func (r *Report) Injectable() (bool, []string) {
return true, nil
}
// IsAnnotatable returns true if the resource for a report can be annotated.
func (r *Report) IsAnnotatable() bool {
return r.Annotatable
}
func checkUDPPorts(t *v1.PodSpec) bool {
// Check for ports with `protocol: UDP`, which will not be routed by Linkerd
for _, container := range t.Containers {
@ -148,9 +159,10 @@ func checkUDPPorts(t *v1.PodSpec) bool {
return false
}
// disabledByAnnotation checks annotations for both workload, namespace and returns
// if disabled, Inject Disabled reason and the resource where that annotation was present
func (r *Report) disableByAnnotation(conf *ResourceConfig) (bool, string, string) {
// disabledByAnnotation checks the workload and namespace for the annotation
// that disables injection. It returns if it is disabled, why it is disabled,
// and the location where the annotation was present.
func (r *Report) disabledByAnnotation(conf *ResourceConfig) (bool, string, string) {
// truth table of the effects of the inject annotation:
//
// origin | namespace | pod | inject? | return

View File

@ -384,7 +384,7 @@ func TestDisableByAnnotation(t *testing.T) {
resourceConfig.pod.spec = &corev1.PodSpec{} //initialize empty spec to prevent test from failing
report := newReport(resourceConfig)
if actual, _, _ := report.disableByAnnotation(resourceConfig); testCase.expected != actual {
if actual, _, _ := report.disabledByAnnotation(resourceConfig); testCase.expected != actual {
t.Errorf("Expected %t. Actual %t", testCase.expected, actual)
}
})
@ -426,7 +426,7 @@ func TestDisableByAnnotation(t *testing.T) {
resourceConfig.pod.spec = &corev1.PodSpec{} //initialize empty spec to prevent test from failing
report := newReport(resourceConfig)
if actual, _, _ := report.disableByAnnotation(resourceConfig); testCase.expected != actual {
if actual, _, _ := report.disabledByAnnotation(resourceConfig); testCase.expected != actual {
t.Errorf("Expected %t. Actual %t", testCase.expected, actual)
}
})

View File

@ -19,6 +19,11 @@ import (
"k8s.io/apimachinery/pkg/runtime/serializer/json"
)
const (
opaquePorts = "11211"
manualOpaquePorts = "22122"
)
//////////////////////
/// TEST SETUP ///
//////////////////////
@ -358,7 +363,7 @@ func TestInjectAutoAnnotationPermutations(t *testing.T) {
}
func TestInjectAutoPod(t *testing.T) {
podYAML, err := testutil.ReadFile("testdata/pod.yaml")
podsYAML, err := testutil.ReadFile("testdata/pods.yaml")
if err != nil {
testutil.AnnotatedFatalf(t, "failed to read inject test file",
"failed to read inject test file: %s", err)
@ -366,8 +371,10 @@ func TestInjectAutoPod(t *testing.T) {
injectNS := "inject-pod-test"
podName := "inject-pod-test-terminus"
opaquePodName := "inject-opaque-pod-test-terminus"
nsAnnotations := map[string]string{
k8s.ProxyInjectAnnotation: k8s.ProxyInjectEnabled,
k8s.ProxyInjectAnnotation: k8s.ProxyInjectEnabled,
k8s.ProxyOpaquePortsAnnotation: opaquePorts,
}
truthy := true
@ -422,10 +429,10 @@ func TestInjectAutoPod(t *testing.T) {
ctx := context.Background()
TestHelper.WithDataPlaneNamespace(ctx, injectNS, nsAnnotations, t, func(t *testing.T, ns string) {
o, err := TestHelper.Kubectl(podYAML, "--namespace", ns, "create", "-f", "-")
o, err := TestHelper.Kubectl(podsYAML, "--namespace", ns, "create", "-f", "-")
if err != nil {
testutil.AnnotatedFatalf(t, "failed to create pod",
"failed to create pod/%s in namespace %s for %s: %s", podName, ns, err, o)
testutil.AnnotatedFatalf(t, "failed to create pods",
"failed to create pods in namespace %s for %s: %s", ns, err, o)
}
o, err = TestHelper.Kubectl("", "--namespace", ns, "wait", "--for=condition=initialized", "--timeout=120s", "pod/"+podName)
@ -434,6 +441,7 @@ func TestInjectAutoPod(t *testing.T) {
"failed to wait for condition=initialized for pod/%s in namespace %s: %s: %s", podName, ns, err, o)
}
// Check that pods with no annotation inherit from the namespace.
pods, err := TestHelper.GetPods(ctx, ns, map[string]string{"app": podName})
if err != nil {
testutil.AnnotatedFatalf(t, "failed to get pods", "failed to get pods for namespace %s: %s", ns, err)
@ -441,6 +449,27 @@ func TestInjectAutoPod(t *testing.T) {
if len(pods) != 1 {
testutil.Fatalf(t, "wrong number of pods returned for namespace %s: %d", ns, len(pods))
}
annotation, ok := pods[0].Annotations[k8s.ProxyOpaquePortsAnnotation]
if !ok {
testutil.Fatalf(t, "pod in namespace %s did not inherit opaque ports annotation", ns)
}
if annotation != opaquePorts {
testutil.Fatalf(t, "expected pod in namespace %s to have %s opaque ports, but it had %s", ns, opaquePorts, annotation)
}
// Check that pods with an annotation do not inherit from the
// namespace.
opaquePods, err := TestHelper.GetPods(ctx, ns, map[string]string{"app": opaquePodName})
if err != nil {
testutil.AnnotatedFatalf(t, "failed to get pods", "failed to get pods for namespace %s: %s", ns, err)
}
if len(opaquePods) != 1 {
testutil.Fatalf(t, "wrong number of pods returned for namespace %s: %d", ns, len(opaquePods))
}
annotation = opaquePods[0].Annotations[k8s.ProxyOpaquePortsAnnotation]
if annotation != manualOpaquePorts {
testutil.Fatalf(t, "expected pod in namespace %s to have %s opaque ports, but it had %s", ns, manualOpaquePorts, annotation)
}
containers := pods[0].Spec.Containers
if proxyContainer := testutil.GetProxyContainer(containers); proxyContainer == nil {
@ -464,3 +493,115 @@ func TestInjectAutoPod(t *testing.T) {
}
})
}
func TestInjectDisabledAutoPod(t *testing.T) {
podsYAML, err := testutil.ReadFile("testdata/pods.yaml")
if err != nil {
testutil.AnnotatedFatalf(t, "failed to read inject test file",
"failed to read inject test file: %s", err)
}
ns := "inject-disabled-pod-test"
podName := "inject-pod-test-terminus"
opaquePodName := "inject-opaque-pod-test-terminus"
nsAnnotations := map[string]string{
k8s.ProxyInjectAnnotation: k8s.ProxyInjectDisabled,
k8s.ProxyOpaquePortsAnnotation: opaquePorts,
}
ctx := context.Background()
TestHelper.WithDataPlaneNamespace(ctx, ns, nsAnnotations, t, func(t *testing.T, ns string) {
o, err := TestHelper.Kubectl(podsYAML, "--namespace", ns, "create", "-f", "-")
if err != nil {
testutil.AnnotatedFatalf(t, "failed to create pods",
"failed to create pods in namespace %s for %s: %s", ns, err, o)
}
o, err = TestHelper.Kubectl("", "--namespace", ns, "wait", "--for=condition=initialized", "--timeout=120s", "pod/"+podName)
if err != nil {
testutil.AnnotatedFatalf(t, "failed to wait for condition=initialized",
"failed to wait for condition=initialized for pod/%s in namespace %s: %s: %s", podName, ns, err, o)
}
// Check that pods with no annotation inherit from the namespace.
pods, err := TestHelper.GetPods(ctx, ns, map[string]string{"app": podName})
if err != nil {
testutil.AnnotatedFatalf(t, "failed to get pods", "failed to get pods for namespace %s: %s", ns, err)
}
if len(pods) != 1 {
testutil.Fatalf(t, "wrong number of pods returned for namespace %s: %d", ns, len(pods))
}
annotation, ok := pods[0].Annotations[k8s.ProxyOpaquePortsAnnotation]
if !ok {
testutil.Fatalf(t, "pod in namespace %s did not inherit opaque ports annotation", ns)
}
if annotation != opaquePorts {
testutil.Fatalf(t, "expected pod in namespace %s to have %s opaque ports, but it had %s", ns, opaquePorts, annotation)
}
// Check that pods with an annotation do not inherit from the
// namespace.
opaquePods, err := TestHelper.GetPods(ctx, ns, map[string]string{"app": opaquePodName})
if err != nil {
testutil.AnnotatedFatalf(t, "failed to get pods", "failed to get pods for namespace %s: %s", ns, err)
}
if len(opaquePods) != 1 {
testutil.Fatalf(t, "wrong number of pods returned for namespace %s: %d", ns, len(opaquePods))
}
annotation = opaquePods[0].Annotations[k8s.ProxyOpaquePortsAnnotation]
if annotation != manualOpaquePorts {
testutil.Fatalf(t, "expected pod in namespace %s to have %s opaque ports, but it had %s", ns, manualOpaquePorts, annotation)
}
containers := pods[0].Spec.Containers
if proxyContainer := testutil.GetProxyContainer(containers); proxyContainer != nil {
testutil.Fatalf(t, "pod in namespace %s should not have been injected", ns)
}
})
}
func TestInjectService(t *testing.T) {
servicesYAML, err := testutil.ReadFile("testdata/services.yaml")
if err != nil {
testutil.AnnotatedFatalf(t, "failed to read inject test file",
"failed to read inject test file: %s", err)
}
ns := "inject-service-test"
serviceName := "service-test"
opaqueServiceName := "opaque-service-test"
nsAnnotations := map[string]string{
k8s.ProxyInjectAnnotation: k8s.ProxyInjectEnabled,
k8s.ProxyOpaquePortsAnnotation: opaquePorts,
}
ctx := context.Background()
TestHelper.WithDataPlaneNamespace(ctx, ns, nsAnnotations, t, func(t *testing.T, ns string) {
o, err := TestHelper.Kubectl(servicesYAML, "--namespace", ns, "create", "-f", "-")
if err != nil {
testutil.AnnotatedFatalf(t, "failed to create services",
"failed to create services in namespace %s for %s: %s", ns, err, o)
}
// Check that the service with no annotation inherits from the namespace.
service, err := TestHelper.GetService(ctx, ns, serviceName)
if err != nil {
testutil.AnnotatedFatalf(t, "failed to get service", "failed to get service for namespace %s: %s", ns, err)
}
annotation, ok := service.Annotations[k8s.ProxyOpaquePortsAnnotation]
if !ok {
testutil.Fatalf(t, "pod in namespace %s did not inherit opaque ports annotation", ns)
}
if annotation != opaquePorts {
testutil.Fatalf(t, "expected pod in namespace %s to have %s opaque ports, but it had %s", ns, opaquePorts, annotation)
}
// Check that the service with no annotation did not inherit from the namespace.
service, err = TestHelper.GetService(ctx, ns, opaqueServiceName)
if err != nil {
testutil.AnnotatedFatalf(t, "failed to get service", "failed to get service for namespace %s: %s", ns, err)
}
annotation = service.Annotations[k8s.ProxyOpaquePortsAnnotation]
if annotation != manualOpaquePorts {
testutil.Fatalf(t, "expected service in namespace %s to have %s opaque ports, but it had %s", ns, manualOpaquePorts, annotation)
}
})
}

View File

@ -1,13 +0,0 @@
apiVersion: v1
kind: Pod
metadata:
name: inject-pod-test-terminus
labels:
app: inject-pod-test-terminus
spec:
containers:
- name: bb-terminus
image: buoyantio/bb:v0.0.6
args: ["terminus", "--grpc-server-port", "9090", "--response-text", "BANANA"]
ports:
- containerPort: 9090

View File

@ -0,0 +1,29 @@
apiVersion: v1
kind: Pod
metadata:
name: inject-pod-test-terminus
labels:
app: inject-pod-test-terminus
spec:
containers:
- name: bb-terminus
image: buoyantio/bb:v0.0.6
args: ["terminus", "--grpc-server-port", "9090", "--response-text", "BANANA"]
ports:
- containerPort: 9090
---
apiVersion: v1
kind: Pod
metadata:
name: inject-opaque-pod-test-terminus
annotations:
config.linkerd.io/opaque-ports: "22122"
labels:
app: inject-opaque-pod-test-terminus
spec:
containers:
- name: bb-terminus
image: buoyantio/bb:v0.0.6
args: ["terminus", "--grpc-server-port", "9090", "--response-text", "BANANA"]
ports:
- containerPort: 9090

View File

@ -0,0 +1,24 @@
apiVersion: v1
kind: Service
metadata:
name: service-test
spec:
selector:
app: svc
ports:
- name: http
port: 8080
targetPort: 8080
---
apiVersion: v1
kind: Service
metadata:
annotations:
config.linkerd.io/opaque-ports: "22122"
name: opaque-service-test
spec:
selector:
app: svc
ports:
- port: 22122
targetPort: 22122

View File

@ -1,6 +1,6 @@
deployment "smoke-test-terminus" injected
service "smoke-test-terminus-svc" injected
service "smoke-test-terminus-svc" skipped
deployment "smoke-test-gateway" injected
service "smoke-test-gateway-svc" injected
service "smoke-test-gateway-svc" skipped

View File

@ -242,6 +242,15 @@ func (h *KubernetesHelper) CheckService(ctx context.Context, namespace string, s
})
}
// GetService gets a service that exists in a namespace.
func (h *KubernetesHelper) GetService(ctx context.Context, namespace string, serviceName string) (*corev1.Service, error) {
service, err := h.clientset.CoreV1().Services(namespace).Get(ctx, serviceName, metav1.GetOptions{})
if err != nil {
return nil, err
}
return service, nil
}
// GetPods returns all pods with the given labels
func (h *KubernetesHelper) GetPods(ctx context.Context, namespace string, podLabels map[string]string) ([]corev1.Pod, error) {
podList, err := h.clientset.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{