linkerd2/controller/proxy-injector/webhook.go

289 lines
8.4 KiB
Go

package injector
import (
"encoding/json"
"fmt"
"io/ioutil"
"strings"
yaml "github.com/ghodss/yaml"
"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"
)
const (
defaultNamespace = "default"
envVarKeyProxyTLSPodIdentity = "LINKERD2_PROXY_TLS_POD_IDENTITY"
envVarKeyProxyTLSControllerIdentity = "LINKERD2_PROXY_TLS_CONTROLLER_IDENTITY"
)
// 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 {
deserializer runtime.Decoder
controllerNamespace string
resources *WebhookResources
}
// NewWebhook returns a new instance of Webhook.
func NewWebhook(client kubernetes.Interface, resources *WebhookResources, controllerNamespace string) (*Webhook, error) {
var (
scheme = runtime.NewScheme()
codecs = serializer.NewCodecFactory(scheme)
)
return &Webhook{
deserializer: codecs.UniversalDeserializer(),
controllerNamespace: controllerNamespace,
resources: resources,
}, 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 = defaultNamespace
}
log.Infof("resource namespace: %s", ns)
if w.ignore(&deployment) {
log.Infof("ignoring deployment %s", deployment.ObjectMeta.Name)
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)
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)
patch := NewPatch()
patch.addContainer(proxy)
if len(deployment.Spec.Template.Spec.InitContainers) == 0 {
patch.addInitContainerRoot()
}
patch.addInitContainer(proxyInit, len(deployment.Spec.Template.Spec.InitContainers))
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)
var (
image = strings.Split(proxy.Image, ":")
imageTag = ""
)
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
}
func (w *Webhook) ignore(deployment *appsv1.Deployment) bool {
labels := deployment.Spec.Template.ObjectMeta.GetLabels()
status, defined := labels[k8sPkg.ProxyAutoInjectLabel]
if defined {
switch status {
case k8sPkg.ProxyAutoInjectDisabled, k8sPkg.ProxyAutoInjectCompleted:
return true
}
}
return healthcheck.HasExistingSidecars(&deployment.Spec.Template.Spec)
}
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()
}
}
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
}