linkerd2/controller/proxy-injector/webhook.go

320 lines
9.5 KiB
Go

package injector
import (
"encoding/json"
"fmt"
"io/ioutil"
"strings"
"github.com/linkerd/linkerd2/pkg/healthcheck"
k8sPkg "github.com/linkerd/linkerd2/pkg/k8s"
log "github.com/sirupsen/logrus"
admissionv1beta1 "k8s.io/api/admission/v1beta1"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/client-go/kubernetes"
"sigs.k8s.io/yaml"
)
const (
envVarKeyProxyTLSPodIdentity = "LINKERD2_PROXY_TLS_POD_IDENTITY"
envVarKeyProxyTLSControllerIdentity = "LINKERD2_PROXY_TLS_CONTROLLER_IDENTITY"
envVarKeyProxyID = "LINKERD2_PROXY_ID"
)
// Webhook is a Kubernetes mutating admission webhook that mutates pods admission
// requests by injecting sidecar container spec into the pod spec during pod
// creation.
type Webhook struct {
client kubernetes.Interface
deserializer runtime.Decoder
controllerNamespace string
resources *WebhookResources
noInitContainer bool
tlsEnabled bool
}
// NewWebhook returns a new instance of Webhook.
func NewWebhook(client kubernetes.Interface, resources *WebhookResources, controllerNamespace string, noInitContainer, tlsEnabled bool) (*Webhook, error) {
var (
scheme = runtime.NewScheme()
codecs = serializer.NewCodecFactory(scheme)
)
return &Webhook{
client: client,
deserializer: codecs.UniversalDeserializer(),
controllerNamespace: controllerNamespace,
resources: resources,
noInitContainer: noInitContainer,
tlsEnabled: tlsEnabled,
}, nil
}
// Mutate changes the given pod spec by injecting the proxy sidecar container
// into the spec. The admission review object returns contains the original
// request and the response with the mutated pod spec.
func (w *Webhook) Mutate(data []byte) *admissionv1beta1.AdmissionReview {
admissionReview, err := w.decode(data)
if err != nil {
log.Error("failed to decode data. Reason: ", err)
admissionReview.Response = &admissionv1beta1.AdmissionResponse{
UID: admissionReview.Request.UID,
Allowed: false,
Result: &metav1.Status{
Message: err.Error(),
},
}
return admissionReview
}
log.Infof("received admission review request %s", admissionReview.Request.UID)
log.Debugf("admission request: %+v", admissionReview.Request)
admissionResponse, err := w.inject(admissionReview.Request)
if err != nil {
log.Error("failed to inject sidecar. Reason: ", err)
admissionReview.Response = &admissionv1beta1.AdmissionResponse{
UID: admissionReview.Request.UID,
Allowed: false,
Result: &metav1.Status{
Message: err.Error(),
},
}
return admissionReview
}
admissionReview.Response = admissionResponse
if len(admissionResponse.Patch) > 0 {
log.Infof("patch generated: %s", admissionResponse.Patch)
}
log.Info("done")
return admissionReview
}
func (w *Webhook) decode(data []byte) (*admissionv1beta1.AdmissionReview, error) {
var admissionReview admissionv1beta1.AdmissionReview
err := yaml.Unmarshal(data, &admissionReview)
return &admissionReview, err
}
func (w *Webhook) inject(request *admissionv1beta1.AdmissionRequest) (*admissionv1beta1.AdmissionResponse, error) {
var deployment appsv1.Deployment
if err := yaml.Unmarshal(request.Object.Raw, &deployment); err != nil {
return nil, err
}
log.Infof("working on %s/%s %s..", request.Kind.Version, strings.ToLower(request.Kind.Kind), deployment.ObjectMeta.Name)
ns := request.Namespace
if ns == "" {
ns = corev1.NamespaceDefault
}
log.Infof("resource namespace: %s", ns)
inject, err := w.shouldInject(ns, &deployment)
if err != nil {
return nil, err
}
if !inject {
log.Infof("skipping deployment %s", deployment.GetName())
return &admissionv1beta1.AdmissionResponse{
UID: request.UID,
Allowed: true,
}, nil
}
identity := &k8sPkg.TLSIdentity{
Name: deployment.ObjectMeta.Name,
Kind: strings.ToLower(request.Kind.Kind),
Namespace: ns,
ControllerNamespace: w.controllerNamespace,
}
proxy, proxyInit, err := w.containersSpec(identity)
if err != nil {
return nil, err
}
log.Infof("proxy image: %s", proxy.Image)
log.Infof("proxy-init image: %s", proxyInit.Image)
log.Debugf("proxy container: %+v", proxy)
log.Debugf("init container: %+v", proxyInit)
patch := NewPatch()
patch.addContainer(proxy)
if !w.noInitContainer {
if len(deployment.Spec.Template.Spec.InitContainers) == 0 {
patch.addInitContainerRoot()
}
patch.addInitContainer(proxyInit)
}
if w.tlsEnabled {
caBundle, tlsSecrets, err := w.volumesSpec(identity)
if err != nil {
return nil, err
}
log.Debugf("ca bundle volume: %+v", caBundle)
log.Debugf("tls secrets volume: %+v", tlsSecrets)
if len(deployment.Spec.Template.Spec.Volumes) == 0 {
patch.addVolumeRoot()
}
patch.addVolume(caBundle)
patch.addVolume(tlsSecrets)
}
if deployment.Spec.Template.Labels == nil {
deployment.Spec.Template.Labels = map[string]string{}
}
deployment.Spec.Template.Labels[k8sPkg.ControllerNSLabel] = w.controllerNamespace
deployment.Spec.Template.Labels[k8sPkg.ProxyDeploymentLabel] = deployment.ObjectMeta.Name
patch.addPodLabels(deployment.Spec.Template.Labels)
if deployment.Labels == nil {
deployment.Labels = map[string]string{}
}
deployment.Labels[k8sPkg.ControllerNSLabel] = w.controllerNamespace
deployment.Labels[k8sPkg.ProxyDeploymentLabel] = deployment.ObjectMeta.Name
patch.addDeploymentLabels(deployment.Labels)
image := strings.Split(proxy.Image, ":")
var imageTag string
if len(image) < 2 {
imageTag = "latest"
} else {
imageTag = image[1]
}
if deployment.Spec.Template.Annotations == nil {
deployment.Spec.Template.Annotations = map[string]string{}
}
deployment.Spec.Template.Annotations[k8sPkg.CreatedByAnnotation] = fmt.Sprintf("linkerd/proxy-injector %s", imageTag)
deployment.Spec.Template.Annotations[k8sPkg.ProxyVersionAnnotation] = imageTag
patch.addPodAnnotations(deployment.Spec.Template.Annotations)
patchJSON, err := json.Marshal(patch.patchOps)
if err != nil {
return nil, err
}
patchType := admissionv1beta1.PatchTypeJSONPatch
admissionResponse := &admissionv1beta1.AdmissionResponse{
UID: request.UID,
Allowed: true,
Patch: patchJSON,
PatchType: &patchType,
}
return admissionResponse, nil
}
// shouldInject determines whether or not the given deployment should be
// injected. A deployment should be injected if it does not already contain
// any known sidecars, and:
// - the deployment's namespace has the linkerd.io/inject annotation set to
// "enabled", and the deployment's pod spec does not have the
// linkerd.io/inject annotation set to "disabled"; or
// - the deployment's pod spec has the linkerd.io/inject annotation set to
// "enabled"
func (w *Webhook) shouldInject(ns string, deployment *appsv1.Deployment) (bool, error) {
if healthcheck.HasExistingSidecars(&deployment.Spec.Template.Spec) {
return false, nil
}
namespace, err := w.client.CoreV1().Namespaces().Get(ns, metav1.GetOptions{})
if err != nil {
return false, err
}
nsAnnotation := namespace.GetAnnotations()[k8sPkg.ProxyInjectAnnotation]
podAnnotation := deployment.Spec.Template.GetAnnotations()[k8sPkg.ProxyInjectAnnotation]
if nsAnnotation == k8sPkg.ProxyInjectEnabled && podAnnotation != k8sPkg.ProxyInjectDisabled {
return true, nil
}
return podAnnotation == k8sPkg.ProxyInjectEnabled, nil
}
func (w *Webhook) containersSpec(identity *k8sPkg.TLSIdentity) (*corev1.Container, *corev1.Container, error) {
proxySpec, err := ioutil.ReadFile(w.resources.FileProxySpec)
if err != nil {
return nil, nil, err
}
var proxy corev1.Container
if err := yaml.Unmarshal(proxySpec, &proxy); err != nil {
return nil, nil, err
}
for index, env := range proxy.Env {
if env.Name == envVarKeyProxyTLSPodIdentity {
proxy.Env[index].Value = identity.ToDNSName()
} else if env.Name == envVarKeyProxyTLSControllerIdentity {
proxy.Env[index].Value = identity.ToControllerIdentity().ToDNSName()
} else if env.Name == envVarKeyProxyID {
proxy.Env[index].Value = identity.ToDNSName()
}
}
proxyInitSpec, err := ioutil.ReadFile(w.resources.FileProxyInitSpec)
if err != nil {
return nil, nil, err
}
var proxyInit corev1.Container
if err := yaml.Unmarshal(proxyInitSpec, &proxyInit); err != nil {
return nil, nil, err
}
return &proxy, &proxyInit, nil
}
func (w *Webhook) volumesSpec(identity *k8sPkg.TLSIdentity) (*corev1.Volume, *corev1.Volume, error) {
trustAnchorVolumeSpec, err := ioutil.ReadFile(w.resources.FileTLSTrustAnchorVolumeSpec)
if err != nil {
return nil, nil, err
}
var trustAnchors corev1.Volume
if err := yaml.Unmarshal(trustAnchorVolumeSpec, &trustAnchors); err != nil {
return nil, nil, err
}
tlsVolumeSpec, err := ioutil.ReadFile(w.resources.FileTLSIdentityVolumeSpec)
if err != nil {
return nil, nil, err
}
var linkerdSecrets corev1.Volume
if err := yaml.Unmarshal(tlsVolumeSpec, &linkerdSecrets); err != nil {
return nil, nil, err
}
linkerdSecrets.VolumeSource.Secret.SecretName = identity.ToSecretName()
return &trustAnchors, &linkerdSecrets, nil
}
// WebhookResources contain paths to all the needed file resources.
type WebhookResources struct {
// FileProxySpec is the path to the proxy spec.
FileProxySpec string
// FileProxyInitSpec is the path to the proxy-init spec.
FileProxyInitSpec string
// FileTLSTrustAnchorVolumeSpec is the path to the trust anchor volume spec.
FileTLSTrustAnchorVolumeSpec string
// FileTLSIdentityVolumeSpec is the path to the TLS identity volume spec.
FileTLSIdentityVolumeSpec string
}