Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

adds support for patching containers, initcontainers and volumes via wildcard in name #4

Merged
merged 1 commit into from
Mar 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 43 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,50 +1,59 @@
# k8s-pod-mutator-webhook
[![Webhook Image Build](https://img.shields.io/docker/cloud/automated/bohlenc/k8s-pod-mutator-webhook?label=webhook%20build)](https://hub.docker.com/r/bohlenc/k8s-pod-mutator-webhook) [![Webhook Image Version](https://img.shields.io/docker/v/bohlenc/k8s-pod-mutator-webhook?label=webhook&sort=semver)](https://hub.docker.com/r/bohlenc/k8s-pod-mutator-webhook) [![Init Image Build](https://img.shields.io/docker/cloud/automated/bohlenc/k8s-pod-mutator-init?label=init%20build)](https://hub.docker.com/r/bohlenc/k8s-pod-mutator-webhook) [![Init Image Version](https://img.shields.io/docker/v/bohlenc/k8s-pod-mutator-init?label=init&sort=semver)](https://hub.docker.com/r/bohlenc/k8s-pod-mutator-init)

This is a Kubernetes Mutating Admission Webhook (see https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/).
This is a Kubernetes Mutating Admission Webhook (see https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/).
It can apply arbitrary changes (a "patch") to a Pod's manifest. A patch can do anything from adding or changing metadata to containers and init-containers with volumes.

The Kubernetes API server only supports communication with webhooks over HTTPS -
an init-container is included that automates cert generation and any necessary configuration (i.e. applying the `caBundle` to the `MutatingWebhookConfiguration`).
The Kubernetes API server only supports communication with webhooks over HTTPS - an init-container is included that automates cert generation
and any necessary configuration (i.e. applying the `caBundle` to the `MutatingWebhookConfiguration`).


## Problem Statement

It is a recurring requirement in Kubernetes deployments to transparently mutate Pod manifests -
either to add new functionality transparently to existing deployments and applications, or to enforce compliance and other policies and requirements.
It is a recurring requirement in Kubernetes deployments to transparently mutate Pod manifests - either to add new functionality transparently to existing deployments
and applications, or to enforce compliance and other policies and requirements.

This webhook provides a flexible and scalable solution to those problems.


## Notable Options

### --patch

Path to the YAML file containing the patch to be applied to eligible Pods (see https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.20/#pod-v1-core for help).

Patches support wildcards ("*") instead of specific names for containers, init-containers and volumes.
If a wildcard is specified, the operation is applied to all existing containers/init-containers/volumes (see [examples](#Examples)).

A patch can contain wildcard and regular operations simultaneously.

A patch can contain only one wildcard per type (container/init-container/volume) currently.

### --log-level

panic | fatal | error | warn | info | debug | trace


## Installation

### Helm

1. adjust the `values.yaml` in `deploy/helm` to your requirements
2. install the chart via `helm install k8s-pod-mutator deploy/helm -f deploy/helm/values.yaml`


By default, the webhook is reachable under "https://<service_name>:8443/mutate"


## Examples

Issue: https://github.com/Azure/azure-sdk-for-net/issues/18312

To apply the workaround proposed [here](https://github.com/Azure/azure-sdk-for-net/issues/18312#issuecomment-771116456)
simply install the Helm chart with the provided example values:
simply install the Helm chart with the provided example values:

`helm upgrade --install k8s-pod-mutator deploy/helm -f values.yaml -f examples/values.example.yaml`.

This example adds an init-container
```
```yaml
spec:
initContainers:
- name: wait-for-imds
Expand All @@ -53,6 +62,31 @@ spec:
```
to all Pods that have a Label `aadpodidbinding`.

---

Feature Request: https://github.com/bohlenc/k8s-pod-mutator-webhook/issues/3

Consider the following patch:
```yaml
spec:
initContainers:
- name: ca
image: alpine
command: ["sh", "-c", "cp -r /etc/ssl/certs /volume"]
volumeMounts:
- mountPath: /volume
name: cacerts
containers:
- name: "*"
volumeMounts:
- mountPath: /etc/ssl
name: cacerts
volumes:
- name: cacerts
emptyDir: {}
```
When this patch is applied, the `volumeMount` "cacerts" is added to all containers currently present in the pod.


## Contributions

Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
module k8s-pod-mutator-webhook

require (
github.com/evanphx/json-patch v4.9.0+incompatible
github.com/sirupsen/logrus v1.7.0
github.com/spf13/cobra v1.1.3
github.com/stretchr/testify v1.7.0
gomodules.xyz/jsonpatch/v3 v3.0.1
k8s.io/api v0.20.2
k8s.io/apimachinery v0.20.2
k8s.io/client-go v0.20.2
sigs.k8s.io/yaml v1.2.0
)

go 1.15
80 changes: 10 additions & 70 deletions pkg/mutator/mutator.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,57 +4,39 @@ import (
"encoding/json"
"fmt"
"github.com/sirupsen/logrus"
"gomodules.xyz/jsonpatch/v3"
"io/ioutil"
"k8s-pod-mutator-webhook/internal/admission_review"
"k8s-pod-mutator-webhook/internal/logger"
admissionv1 "k8s.io/api/admission/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/strategicpatch"
"k8s.io/apimachinery/pkg/util/yaml"
"reflect"
)

const statusAnnotation = "k8s-pod-mutator.io/mutated"

type MutationSettings struct {
PatchFile string
}

type Mutator struct {
patchJsonBytes []byte
patch *Patch
}

func CreateMutator(settings MutationSettings) (*Mutator, error) {
logger.Logger.WithFields(logrus.Fields{
"settings": fmt.Sprintf("%+v", settings),
}).Infoln("creating mutator")

patchJsonBytes, err := readAsJsonBytes(settings.PatchFile)
if err != nil {
return nil, err
}

return &Mutator{patchJsonBytes}, nil
}

func readAsJsonBytes(patchFile string) ([]byte, error) {
logger.Logger.WithFields(logrus.Fields{
"patchFile": patchFile,
}).Tracef("reading patch file...")
patchYamlBytes, err := ioutil.ReadFile(patchFile)
patchYaml, err := ioutil.ReadFile(settings.PatchFile)
if err != nil {
return nil, fmt.Errorf("could not read patch file: %v", err)
}
logger.Logger.Debugf("patch yaml: %v", string(patchYamlBytes))

patchJsonBytes, err := yaml.ToJSON(patchYamlBytes)
patch, err := CreatePatch(patchYaml)
if err != nil {
return nil, fmt.Errorf("could not convert patch from yaml to json: %v", err)
return nil, err
}
logger.Logger.Tracef("patch json: %v", string(patchJsonBytes))
return patchJsonBytes, nil

return &Mutator{patch}, nil
}

func (m *Mutator) Mutate(request *admissionv1.AdmissionRequest) *admissionv1.AdmissionResponse {
Expand All @@ -63,12 +45,12 @@ func (m *Mutator) Mutate(request *admissionv1.AdmissionRequest) *admissionv1.Adm
logger.Logger.WithFields(logrus.Fields{
"error": err,
"type": reflect.TypeOf(pod),
}).Errorln("decode failed")
}).Errorln("unmarshalling failed")
return admission_review.ErrorResponse(err)
}

podName := maybePodName(pod.ObjectMeta)
ensurePodNamespace(request, pod)
ensurePodNamespace(request, &pod)

logger.Logger.WithFields(logrus.Fields{
"namespace": pod.Namespace,
Expand All @@ -87,7 +69,7 @@ func (m *Mutator) Mutate(request *admissionv1.AdmissionRequest) *admissionv1.Adm
}
}

jsonPatch, err := createJsonPatch(&pod, m.patchJsonBytes)
jsonPatch, err := m.patch.Apply(&pod)
if err != nil {
logger.Logger.Errorf("could not create json patch: %v", err)
return admission_review.ErrorResponse(err)
Expand Down Expand Up @@ -124,50 +106,8 @@ func maybePodName(metadata metav1.ObjectMeta) string {
return ""
}

func ensurePodNamespace(request *admissionv1.AdmissionRequest, pod corev1.Pod) {
func ensurePodNamespace(request *admissionv1.AdmissionRequest, pod *corev1.Pod) {
if pod.Namespace == "" {
pod.Namespace = request.Namespace
}
}

func createJsonPatch(pod *corev1.Pod, patchJson []byte) ([]byte, error) {
originalJson, err := json.Marshal(pod)
if err != nil {
return nil, fmt.Errorf("could not encode pod: %v", err)
}
logger.Logger.Tracef("originalJson: %v", string(originalJson))

overlayedJson, err := strategicpatch.StrategicMergePatch(originalJson, patchJson, corev1.Pod{})
if err != nil {
return nil, fmt.Errorf("could not apply strategic merge patch: %v", err)
}

overlayedJson, err = markMutated(overlayedJson)
if err != nil {
return nil, fmt.Errorf("could not set status annotation: %v", err)
}
logger.Logger.Tracef("overlayedJson: %v", string(overlayedJson))

jsonPatch, err := jsonpatch.CreatePatch(originalJson, overlayedJson)
if err != nil {
return nil, fmt.Errorf("could not create two-way merge patch: %v", err)
}
logger.Logger.Tracef("jsonPatch: %v", jsonPatch)

return json.Marshal(jsonPatch)
}

func markMutated(overlayedJson []byte) ([]byte, error) {
overlayedPod := &corev1.Pod{}
if err := json.Unmarshal(overlayedJson, overlayedPod); err != nil {
return nil, err
}

if len(overlayedPod.Annotations) == 0 {
overlayedPod.Annotations = make(map[string]string)
}

overlayedPod.Annotations[statusAnnotation] = "true"

return json.Marshal(overlayedPod)
}
Loading