diff --git a/go.mod b/go.mod index ae222981..92a511e1 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/spf13/cobra v1.8.0 go.uber.org/multierr v1.11.0 google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de - google.golang.org/grpc v1.63.0 + google.golang.org/grpc v1.63.2 k8s.io/apimachinery v0.29.3 ) diff --git a/go.sum b/go.sum index 1a393723..3089ac0b 100644 --- a/go.sum +++ b/go.sum @@ -324,10 +324,10 @@ google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de/go. google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de h1:cZGRis4/ot9uVm639a+rHCUaG0JJHEsdyzSQTMX+suY= google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY= google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= -google.golang.org/grpc v1.63.0 h1:WjKe+dnvABXyPJMD7KDNLxtoGk5tgk+YFWN6cBWjZE8= -google.golang.org/grpc v1.63.0/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= +google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/sidecar-injector/Dockerfile b/sidecar-injector/Dockerfile new file mode 100644 index 00000000..0518a556 --- /dev/null +++ b/sidecar-injector/Dockerfile @@ -0,0 +1,11 @@ +FROM golang:1.22 as build +RUN go install golang.org/x/lint/golint@latest +WORKDIR /build +COPY . ./ +RUN CGO_ENABLED=0 GOOS=linux go build -o sidecar-injector + +FROM scratch +WORKDIR / +COPY --from=build /build/sidecar-injector / + +ENTRYPOINT ["/sidecar-injector"] diff --git a/sidecar-injector/README.md b/sidecar-injector/README.md new file mode 100644 index 00000000..a73a3ad2 --- /dev/null +++ b/sidecar-injector/README.md @@ -0,0 +1,202 @@ +# Kyverno Envoy Sidecar Injector + +Uses [MutatingAdmissionWebhook Controller](https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#mutatingadmissionwebhook) in Kubernetes to inject kyverno-envoy-plugin sidecar into newly created pods. This injection occurs at pod creation time, targeting pods that have the label `kyverno-envoy-sidecar/injection=enabled`. By introducing this sidecar, we can enforce policies on all incoming HTTP requests and make external authorization decisions to the targeted pod without modifying the primary application code/containers. + + +## Prerequisites + +Kubernetes 1.16.0 or above with the `admissionregistration.k8s.io/v1` API enabled. +Verify that by the following command: +```bash + ~$ kubectl api-versions | grep admissionregistration.k8s.io/v1 +``` +The result should be: +```bash +admissionregistration.k8s.io/v1 +``` +## Installation + +#### Dedicated Namespace + +Create a namespace `kyverno-envoy-sidecar-injector`, where you will deploy the Kyverno Envoy Sidecar Injector Webhook components. + +```bash + ~$ kubectl create namespace kyverno-envoy-sidecar-injector +``` + +#### Deploy Sidecar Injector + +1. Create a signed cert/key pair and store it in a Kubernetes `secret` that will be consumed by sidecar injector deployment + + Generate cert/key pair with openssl + ```bash + ~$ openssl req -new -x509 \ + -subj "/CN=kyverno-envoy-sidecar.kyverno-envoy-sidecar-injector.svc" \ + -addext "subjectAltName = DNS:kyverno-envoy-sidecar.kyverno-envoy-sidecar-injector.svc" \ + -nodes -newkey rsa:4096 -keyout tls.key -out tls.crt + ``` + Now apply below command to create `secret` + ```bash + ~$ kubectl create secret generic kyverno-envoy-sidecar-certs \ + --from-file tls.crt=tls.crt \ + --from-file tls.key=tls.key \ + --dry-run=client -n kyverno-envoy-sidecar-injector -oyaml > secret.yaml + ``` + Apply the secret + ```bash + ~$ kubectl apply -f secret.yaml + ``` + +2. Run the script to Patch the `Mutating Webhook Configuration` with the CA bundle extracted from the `secret` created in the previous step and apply the MutatingWebhookConfiguration changes: + +```bash + ~$ ./manifests/create-mutating-webhook.sh +``` +3. To Inject the Kyverno Envoy Sidecar, Create this configmap of name `kyverno-envoy-sidecar` in `kyverno-envoy--sidecar-injector` namespace. If their is requirement of multiple policy files, you can add more `--policy` flags and then add them in the `policy-files` configmap. + +```bash +kubectl apply -f - < 443/TCP 3m11s + +NAME READY UP-TO-DATE AVAILABLE AGE +deployment.apps/kyverno-envoy-sidecar 1/1 1 1 46s + +NAME DESIRED CURRENT READY AGE +replicaset.apps/kyverno-envoy-sidecar-976c94445 1 1 1 46s + +``` +2. Now create a pod with the label `kyverno-envoy-sidecar/injection=enabled` in any namespace other than `kyverno-envoy-sidecar-injector`. But before we have to apply configmap `policy-files` in the same namespace where we will create the pod. +```bash +kubectl apply -f - < mutatingwebhook.yaml +apiVersion: admissionregistration.k8s.io/v1 +kind: MutatingWebhookConfiguration +metadata: + name: kyverno-envoy-sidecar + labels: + app.kubernetes.io/name: sidecar-injector + app.kubernetes.io/instance: sidecar-injector +webhooks: + - name: kyverno-envoy-sidecar.kyverno-envoy-sidecar-injector.svc + clientConfig: + service: + name: kyverno-envoy-sidecar + namespace: kyverno-envoy-sidecar-injector + path: "/mutate" + caBundle: $CA_BUNDLE + failurePolicy: Fail + sideEffects: None + admissionReviewVersions: ["v1"] + rules: + - apiGroups: + - "" + resources: + - pods + apiVersions: + - "v1" + operations: + - CREATE + scope: '*' + objectSelector: + matchExpressions: + - key: kyverno-envoy-sidecar/injection + operator: In + values: + - enabled +EOF + +# Apply the mutatingwebhook.yaml file +kubectl apply -f mutatingwebhook.yaml \ No newline at end of file diff --git a/sidecar-injector/manifests/deployment.yaml b/sidecar-injector/manifests/deployment.yaml new file mode 100644 index 00000000..5536c0f4 --- /dev/null +++ b/sidecar-injector/manifests/deployment.yaml @@ -0,0 +1,63 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: kyverno-envoy-sidecar + namespace: kyverno-envoy-sidecar-injector + labels: + app.kubernetes.io/name: sidecar-injector + app.kubernetes.io/instance: sidecar-injector +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: sidecar-injector + app.kubernetes.io/instance: sidecar-injector + template: + metadata: + labels: + app.kubernetes.io/name: sidecar-injector + app.kubernetes.io/instance: sidecar-injector + spec: + serviceAccountName: kyverno-envoy-sidecar + containers: + - name: kyverno-envoy-sidecar + image: "sanskardevops/sidecar-injector:0.0.6" + imagePullPolicy: IfNotPresent + args: + - --port=8443 + - --certFile=/opt/kubernetes-sidecar-injector/certs/tls.crt + - --keyFile=/opt/kubernetes-sidecar-injector/certs/tls.key + - --sidecarDataKey=sidecars.yaml + volumeMounts: + - name: kyverno-envoy-sidecar-certs + mountPath: /opt/kubernetes-sidecar-injector/certs + readOnly: true + ports: + - name: https + containerPort: 8443 + protocol: TCP + livenessProbe: + httpGet: + path: /healthz + port: https + scheme: HTTPS + initialDelaySeconds: 5 + periodSeconds: 10 + successThreshold: 1 + failureThreshold: 5 + timeoutSeconds: 4 + readinessProbe: + httpGet: + path: /healthz + port: https + scheme: HTTPS + initialDelaySeconds: 30 + periodSeconds: 10 + successThreshold: 1 + failureThreshold: 5 + timeoutSeconds: 4 + volumes: + - name: kyverno-envoy-sidecar-certs + secret: + secretName: kyverno-envoy-sidecar-certs + \ No newline at end of file diff --git a/sidecar-injector/manifests/mutatingwebhook.yaml b/sidecar-injector/manifests/mutatingwebhook.yaml new file mode 100644 index 00000000..13db0956 --- /dev/null +++ b/sidecar-injector/manifests/mutatingwebhook.yaml @@ -0,0 +1,37 @@ + +apiVersion: admissionregistration.k8s.io/v1 +kind: MutatingWebhookConfiguration +metadata: + name: kyverno-envoy-sidecar + labels: + app.kubernetes.io/name: sidecar-injector + app.kubernetes.io/instance: sidecar-injector +webhooks: + - name: kyverno-envoy-sidecar.kyverno-envoy-sidecar-injector.svc + clientConfig: + service: + name: kyverno-envoy-sidecar + namespace: kyverno-envoy-sidecar-injector + path: "/mutate" + caBundle: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZsakNDQTM2Z0F3SUJBZ0lVVFNGS2k0NDlzYWNmUXF4UTVnWFhBanU5SjA0d0RRWUpLb1pJaHZjTkFRRUwKQlFBd096RTVNRGNHQTFVRUF3d3dhM1ZpWlhKdVpYUmxjeTF6YVdSbFkyRnlMV2x1YW1WamRHOXlMbk5wWkdWagpZWEl0YVc1cVpXTjBiM0l1YzNaak1CNFhEVEkwTURReE5qRXhNVFl3TlZvWERUSTFNRFF4TmpFeE1UWXdOVm93Ck96RTVNRGNHQTFVRUF3d3dhM1ZpWlhKdVpYUmxjeTF6YVdSbFkyRnlMV2x1YW1WamRHOXlMbk5wWkdWallYSXQKYVc1cVpXTjBiM0l1YzNaak1JSUNJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBZzhBTUlJQ0NnS0NBZ0VBdFNSeApXMWpGbWJrTW1GOWZzK05yREY3NUFBQzY2aDg3UGRnOGpKaDEwWjRGRDlVallDVUhqL1RuVVg4d3V3cVVoWTVmCjUyTU1sV1AxMi9FWkNQc2lKL3VGMjA1aWRsa3k1UWkvNzBtVzZUUEpMTkFhcHpxa2pSZDBGbklXUllKOXJ1cS8KZDdEOHYwaHJtaUJZUHhaR0VEVEhWWDBGMWFwRTBncEZtRE1vU200ZjRYZEdTazNuc3N3YWptSTBJYk1aaURpRwpKcmVQOXFSMDIwU0VxUEpNQng0ZUdQSVFEVmtiMlgwanV3ZXljbFhHUWduWWFhRFhZd0czWUNqcmRXWUtYcEF5CkJmNUpLNlZaNkNVWVluWmpSd1U5c2xSS0xGRDhiNlpiUXIzQmR0UmpRRDBheG5aZ0wwa0xKamsvSVBsQk55eEwKclZwRG83VWpCY1FrVVgvZ1k3UjBreFRIcG9GUVZzOXlHY3M2Wi9NeE4wNUIrSGlEYzVGcjBhUEU0Vm9XR3E5TwptaEFSYmZuNDRqcm92b0tVTThXOGR4NjFyeFR3cklLejdKVW1BOGdKRFFIL2hVTjFJMlEyQ0ZXNXpHYUNHdVBBCjBuSHJzMzF2Q0N4dTBybTM2N3Bnd3RIbDhLWEE1aW9hN1Zia1NpZkZ3MDVCaitrOHFPSkNDSFd4ZG5IVVNhSUoKU2RWaW5LaXBUOS9INmdNRHg4dUxNRnhTM2txVm9Oa1hxMlVTOTFjN3B2Rk9Qdi9HQzBrTWNmSkN3SVdoK3JJdAp2VDhjc1BOaXVWWGdEOXg2QjhKSUErSi9XOUV1L2VuWU8xdzhoVnc5cXVhUVdEMi9MYkdNd01LQWlyUUlBSkRaCjk4Y1pFaVFUcnpTTGdxTDhzQWR2eTRsNG5MUUdQN2lEWUVmZGxLa0NBd0VBQWFPQmtUQ0JqakFkQmdOVkhRNEUKRmdRVUN3NUdQOVlVMC9xZ3pEejFmcDh0alhFdXhBb3dId1lEVlIwakJCZ3dGb0FVQ3c1R1A5WVUwL3FnekR6MQpmcDh0alhFdXhBb3dEd1lEVlIwVEFRSC9CQVV3QXdFQi96QTdCZ05WSFJFRU5EQXlnakJyZFdKbGNtNWxkR1Z6CkxYTnBaR1ZqWVhJdGFXNXFaV04wYjNJdWMybGtaV05oY2kxcGJtcGxZM1J2Y2k1emRtTXdEUVlKS29aSWh2Y04KQVFFTEJRQURnZ0lCQUowTDFmc2tGVHRWenBXZWlBaXNxWVFzTmxMWUhkczNyNGVkRzhiS0w3bWZtYm5pZllmOQpjblphYWxVaWRrazU3VFpVMFYrdlc5M2RONGZ3cXZ3a1IzL240V2dpbEZRR3RmekI2WURoZnRBNlRlZUI3Y3lRCmZnSmVCakRPYndIMzNHVDlmWDlVZFNaSTJ1dVJRRGZ5RW1mWEd3aHVSajJpdS9XQ3MxT2cvb01WUy9XM1VCb2EKSis4YXVQWUViSTRrcmtBUkJvOEJ2SGxKeGxjdWRnTjg1bWFMMWc4OWU5U0s1WVM2Ymswa3lKa1J1ZFNNZ2ZvbgpoRkJHcjR1emJrSmpWYmZzVUxiWGZEOTB3ZXpIekQ4NG5OTVhWTExrSU9YMm9SSFZaKzI4V0I5TzVKeWxOUWllCjk0UWJVWTFnMkpPYzZodUNYS0h1KzdqQlRRTzdHYWZTbFBHcTBreUl4eDh6bXd5T1h2M3M2OVZJQkNqbVcwSnQKTlZqYTNDVzViS1FmbU90ODRoOFJuVmJtVmN6b3RZcWVJOGNBVlJCL0V1NzM2UkZzeGNSL3JFeEdyZ3BIZG9IeApQNUF4bTBJTlVBQnVrZjlJbStLSXFOOUxKRzF5M3hocGxWNzAySmxKWElFcDFOeGoyMGlWaDZlZ3FsNksvTE5pCjc4VkdxdFBwM1FqVUtIMkRicExsRnluTGdoNm8zM1NHWjRlNWtXbXB4djdyUncrblJhZEo0ekRoa0hkWjZ1ZjIKMFoyTG9ZVmpMdExRTEljclRxcENVODRSUDg5WjN4cFF1N3lHQlptVVc2OURIUFVmcm9LZ25UUTZyODJYY21IUQpKTkV2TS93VXlpdUhsY1Erb2pCZ2ZSd2MzRjNkblZ6VHZ4N1drKzVaMkdFbVk4NEp6SE9TQmIvdAotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==" + failurePolicy: Fail + sideEffects: None + admissionReviewVersions: ["v1"] + rules: + - apiGroups: + - "" + resources: + - pods + apiVersions: + - "v1" + operations: + - CREATE + scope: '*' + objectSelector: + matchExpressions: + - key: kyverno-envoy-sidecar/injection + operator: In + values: + - enabled + +--- \ No newline at end of file diff --git a/sidecar-injector/manifests/rbac.yaml b/sidecar-injector/manifests/rbac.yaml new file mode 100644 index 00000000..d5a3e6b5 --- /dev/null +++ b/sidecar-injector/manifests/rbac.yaml @@ -0,0 +1,43 @@ +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: kyverno-envoy-sidecar + namespace: kyverno-envoy-sidecar-injector + labels: + app.kubernetes.io/name: sidecar-injector + app.kubernetes.io/instance: sidecar-injector + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: kyverno-envoy-sidecar + labels: + app.kubernetes.io/name: sidecar-injector + app.kubernetes.io/instance: sidecar-injector +rules: + - apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: kyverno-envoy-sidecar + labels: + app.kubernetes.io/name: sidecar-injector + app.kubernetes.io/instance: sidecar-injector +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: kyverno-envoy-sidecar +subjects: + - kind: ServiceAccount + name: kyverno-envoy-sidecar + namespace: kyverno-envoy-sidecar-injector +--- diff --git a/sidecar-injector/manifests/secret.yaml b/sidecar-injector/manifests/secret.yaml new file mode 100644 index 00000000..5b461234 --- /dev/null +++ b/sidecar-injector/manifests/secret.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +data: + tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZsakNDQTM2Z0F3SUJBZ0lVVFNGS2k0NDlzYWNmUXF4UTVnWFhBanU5SjA0d0RRWUpLb1pJaHZjTkFRRUwKQlFBd096RTVNRGNHQTFVRUF3d3dhM1ZpWlhKdVpYUmxjeTF6YVdSbFkyRnlMV2x1YW1WamRHOXlMbk5wWkdWagpZWEl0YVc1cVpXTjBiM0l1YzNaak1CNFhEVEkwTURReE5qRXhNVFl3TlZvWERUSTFNRFF4TmpFeE1UWXdOVm93Ck96RTVNRGNHQTFVRUF3d3dhM1ZpWlhKdVpYUmxjeTF6YVdSbFkyRnlMV2x1YW1WamRHOXlMbk5wWkdWallYSXQKYVc1cVpXTjBiM0l1YzNaak1JSUNJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBZzhBTUlJQ0NnS0NBZ0VBdFNSeApXMWpGbWJrTW1GOWZzK05yREY3NUFBQzY2aDg3UGRnOGpKaDEwWjRGRDlVallDVUhqL1RuVVg4d3V3cVVoWTVmCjUyTU1sV1AxMi9FWkNQc2lKL3VGMjA1aWRsa3k1UWkvNzBtVzZUUEpMTkFhcHpxa2pSZDBGbklXUllKOXJ1cS8KZDdEOHYwaHJtaUJZUHhaR0VEVEhWWDBGMWFwRTBncEZtRE1vU200ZjRYZEdTazNuc3N3YWptSTBJYk1aaURpRwpKcmVQOXFSMDIwU0VxUEpNQng0ZUdQSVFEVmtiMlgwanV3ZXljbFhHUWduWWFhRFhZd0czWUNqcmRXWUtYcEF5CkJmNUpLNlZaNkNVWVluWmpSd1U5c2xSS0xGRDhiNlpiUXIzQmR0UmpRRDBheG5aZ0wwa0xKamsvSVBsQk55eEwKclZwRG83VWpCY1FrVVgvZ1k3UjBreFRIcG9GUVZzOXlHY3M2Wi9NeE4wNUIrSGlEYzVGcjBhUEU0Vm9XR3E5TwptaEFSYmZuNDRqcm92b0tVTThXOGR4NjFyeFR3cklLejdKVW1BOGdKRFFIL2hVTjFJMlEyQ0ZXNXpHYUNHdVBBCjBuSHJzMzF2Q0N4dTBybTM2N3Bnd3RIbDhLWEE1aW9hN1Zia1NpZkZ3MDVCaitrOHFPSkNDSFd4ZG5IVVNhSUoKU2RWaW5LaXBUOS9INmdNRHg4dUxNRnhTM2txVm9Oa1hxMlVTOTFjN3B2Rk9Qdi9HQzBrTWNmSkN3SVdoK3JJdAp2VDhjc1BOaXVWWGdEOXg2QjhKSUErSi9XOUV1L2VuWU8xdzhoVnc5cXVhUVdEMi9MYkdNd01LQWlyUUlBSkRaCjk4Y1pFaVFUcnpTTGdxTDhzQWR2eTRsNG5MUUdQN2lEWUVmZGxLa0NBd0VBQWFPQmtUQ0JqakFkQmdOVkhRNEUKRmdRVUN3NUdQOVlVMC9xZ3pEejFmcDh0alhFdXhBb3dId1lEVlIwakJCZ3dGb0FVQ3c1R1A5WVUwL3FnekR6MQpmcDh0alhFdXhBb3dEd1lEVlIwVEFRSC9CQVV3QXdFQi96QTdCZ05WSFJFRU5EQXlnakJyZFdKbGNtNWxkR1Z6CkxYTnBaR1ZqWVhJdGFXNXFaV04wYjNJdWMybGtaV05oY2kxcGJtcGxZM1J2Y2k1emRtTXdEUVlKS29aSWh2Y04KQVFFTEJRQURnZ0lCQUowTDFmc2tGVHRWenBXZWlBaXNxWVFzTmxMWUhkczNyNGVkRzhiS0w3bWZtYm5pZllmOQpjblphYWxVaWRrazU3VFpVMFYrdlc5M2RONGZ3cXZ3a1IzL240V2dpbEZRR3RmekI2WURoZnRBNlRlZUI3Y3lRCmZnSmVCakRPYndIMzNHVDlmWDlVZFNaSTJ1dVJRRGZ5RW1mWEd3aHVSajJpdS9XQ3MxT2cvb01WUy9XM1VCb2EKSis4YXVQWUViSTRrcmtBUkJvOEJ2SGxKeGxjdWRnTjg1bWFMMWc4OWU5U0s1WVM2Ymswa3lKa1J1ZFNNZ2ZvbgpoRkJHcjR1emJrSmpWYmZzVUxiWGZEOTB3ZXpIekQ4NG5OTVhWTExrSU9YMm9SSFZaKzI4V0I5TzVKeWxOUWllCjk0UWJVWTFnMkpPYzZodUNYS0h1KzdqQlRRTzdHYWZTbFBHcTBreUl4eDh6bXd5T1h2M3M2OVZJQkNqbVcwSnQKTlZqYTNDVzViS1FmbU90ODRoOFJuVmJtVmN6b3RZcWVJOGNBVlJCL0V1NzM2UkZzeGNSL3JFeEdyZ3BIZG9IeApQNUF4bTBJTlVBQnVrZjlJbStLSXFOOUxKRzF5M3hocGxWNzAySmxKWElFcDFOeGoyMGlWaDZlZ3FsNksvTE5pCjc4VkdxdFBwM1FqVUtIMkRicExsRnluTGdoNm8zM1NHWjRlNWtXbXB4djdyUncrblJhZEo0ekRoa0hkWjZ1ZjIKMFoyTG9ZVmpMdExRTEljclRxcENVODRSUDg5WjN4cFF1N3lHQlptVVc2OURIUFVmcm9LZ25UUTZyODJYY21IUQpKTkV2TS93VXlpdUhsY1Erb2pCZ2ZSd2MzRjNkblZ6VHZ4N1drKzVaMkdFbVk4NEp6SE9TQmIvdAotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== + tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUpRZ0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQ1N3d2dna29BZ0VBQW9JQ0FRQzFKSEZiV01XWnVReVkKWDErejQyc01YdmtBQUxycUh6czkyRHlNbUhYUm5nVVAxU05nSlFlUDlPZFJmekM3Q3BTRmpsL25Zd3lWWS9YYgo4UmtJK3lJbis0WGJUbUoyV1RMbENML3ZTWmJwTThrczBCcW5PcVNORjNRV2NoWkZnbjJ1NnI5M3NQeS9TR3VhCklGZy9Ga1lRTk1kVmZRWFZxa1RTQ2tXWU15aEtiaC9oZDBaS1RlZXl6QnFPWWpRaHN4bUlPSVltdDQvMnBIVGIKUklTbzhrd0hIaDRZOGhBTldSdlpmU083QjdKeVZjWkNDZGhwb05kakFiZGdLT3QxWmdwZWtESUYva2tycFZubwpKUmhpZG1OSEJUMnlWRW9zVVB4dnBsdEN2Y0YyMUdOQVBSckdkbUF2U1FzbU9UOGcrVUUzTEV1dFdrT2p0U01GCnhDUlJmK0JqdEhTVEZNZW1nVkJXejNJWnl6cG44ekUzVGtINGVJTnprV3ZSbzhUaFdoWWFyMDZhRUJGdCtmamkKT3VpK2dwUXp4YngzSHJXdkZQQ3NnclBzbFNZRHlBa05BZitGUTNValpEWUlWYm5NWm9JYTQ4RFNjZXV6Zlc4SQpMRzdTdWJmcnVtREMwZVh3cGNEbUtocnRWdVJLSjhYRFRrR1A2VHlvNGtJSWRiRjJjZFJKb2dsSjFXS2NxS2xQCjM4ZnFBd1BIeTRzd1hGTGVTcFdnMlJlclpSTDNWenVtOFU0Ky84WUxTUXh4OGtMQWhhSDZzaTI5UHh5dzgySzUKVmVBUDNIb0h3a2dENG45YjBTNzk2ZGc3WER5RlhEMnE1cEJZUGI4dHNZekF3b0NLdEFnQWtObjN4eGtTSkJPdgpOSXVDb3Z5d0IyL0xpWGljdEFZL3VJTmdSOTJVcVFJREFRQUJBb0lDQUFoYWNxaWxZV3lLSS8zbzJKbWVBVDZ2CmNhallscUYyMzZyY2cwZ0VQTGFzLytibXB4ZkNsb2t3Y3FzK1gyQnJCN05ZdHRGV25LRzZ0eVFqVU1wemN1SGEKdENYbXpoemhNZ1Fra1JUQWVjdmtrSW1nYmt0ZUUweGs3dng0a1dMSnBEbjQrWDRkVC9USllheFlneEp0SWIyUwozaFBOZzh5NUlRaFZMRGUyZ29NM0NDWU5HRU1UWGpTOU80YlpQbHFnV0QxdXRDWXFOaXUxcE1rTk5ud00zOUkrCnh4Z2NIWkUrM3ZIV0lBQjZvUWdaTHFna2lJUDVWc1ZxSUZqdThRREZUbDcza2NPTi9YNktSTHRYeWdPNkdjS2kKdnJ5djQvZ01OWGJLREtMbkVJeUdPZExwZWVObngxNHBKVjc2UEd1SVdlaFdncXFjeDhvcTczWXFycitBMHplaAozcXdEUEZrYUpkYTBnazFaaXRxOTBzVWtKcmFwMUY4VWhmU01lcjJGeVptaWF6cy9ZdisxRDZxWi9sOTcyZXRZCktISmJhTmREb1FSVUthSk1JNEN5dmVhVEJlSWkvU3RmUmdlUFhGMnF0U0huNHJjL1kzdkkrVFBvYUVYeGtBMysKS0JXWUw4Z1hzbUxBdjkyclZPSTdQa1VyL1pZZVZLNE05RmlLWVErdlhaaHh5RHNwenVrbkI1aXRGeUJuOW54YQozaU44R0hWL3c3bWEwTFJBL1p1UFAwclRmRDNITVhJSFBtckhYL3MwUmZnbHpkek9xVzZGYTdaWnRVS1ZsalI1CnRxNDI3d0dNNVF6Q2NnU3JMai8rVnNIMG5DK0k3bUk4OGpDUUlvSmxSem9Ea1NNZDMvaTFXemF6cW9EY3VrYmoKMlV3ZjBVc3k5SnFpaDRZdVJIS0RBb0lCQVFEQTV6MmtTejVPNmhHR1g2N1lsR3Y4SDl0a1A3VFNFV2MvNEEydwpNZVRGSDczWnJBZ0tDRERRdFMyQ2QxZDZGR1JOdzJnMllxSGRyRUl4dE4wMGRPN0hXeWxPa2d1cVo2V0VPNEcwCng4NmtieUVndFF0NytsaHFvU29iZHg5Z1FndEJsL0ZHM1N3WGNSY0I3VTVoMEtEdWpBdzVNZjNoZmNxQmdGSGwKM3BzMFlxdngrL25RQm1RTDdIODVwamxBRTU1WXg4NWVFR3orUU0vRXZoMHk5NkFqWVVudy8wNXBlOGZRWk5LbwpLdjlmalA1L0F5Q1NWV2lnRVRDUWpDbVQ4b2lqYWVzTEIxMGpjRUVaRlFIdE1hZ0liZ1pCaEVjaWd6NTI1WGVpCjZnUytpbWlZTmVKMlFjTGFUK2hiUjRlUGt3REJOV3Y2dXRIZFNuRS9ET0RKMUE2TEFvSUJBUUR3WkdhQTNzaGUKY0loVXhFNTJwVUlpbG0zbGlCOGs3VXFCL3E1cTNVRnRiS2xNWGprQmIxNU01bS9SRUhuMVIzaG9qbEN1ai9KWApwOWJ5Nkw1UEc2SEdqT1pnSnl4dDU4TEdyQUxwOTkwbThpTG96WEpJTllxanV6WS9oRExDNXN3bm12SnlWYk1iCnQrU2Y0YzlPeTFLZWN2aTV2bkZvUXRVcDh0R2krSUM1d3pkVXEyUVJtWnk0eGJqVXFnQkV5N01DNWJINDVEcXgKTkE4Q0l0UlhELzkwQ1ZVcEdEMWNUVXRTSXVMR2hDRVdUTUk3VHpuU0lib2ljbzhMdWpBSi9GRzA5QjFXN3NkQwpVUy81VFJxb0VBWGVzSSt5ZkR4Vk9PdE1kRE4zTVRHdEhHazJlMVNDOWlFdnNhZ2xtVDlHWWN3S0lCRjVHQVBaClhzRmFFL0ZjYmFRYkFvSUJBSEVPbGhnV2FWeEMzeWFNS2FPUnlZQXBBNkpMbkNTS1FxTXpJNUtpaTF2azhKWUUKdDJsNXgzSnEzVk5ic285QUtGRlROMTY0aS9tcG5kb1lFSlZQK3lvb0NadWRDTzFFZGNOOFJOYTVUQ2tmWUtFVQp1cmhjenprZlg5aGRCcXlaeUpNWEJEZnVKSXRRb3BWa2ljM1dRcHZNeE5VNHNYMVpCampFQmp2ZExjV1VGd1pxCkVjMlVFVXJUdnZVQXNRa1c5blUrRlhzWDBXbHFmdHJtT2FMSGNybUpxWlp2YTN0ektuYSt3S0FESTB6VEM4MVEKL2VRRjNwNEJ0UjdpcHZPbzcrQW1rYlVUQ2NsZFh5bmVJQlR1UjNjNVZMMU5VNHVzdEExbkM2a1YwdFlCdEsrUQoxVHRONjIrYjZhaWwwWk9hS3BVU1JFamMrV2JpM0dDQm9iVm9iV1VDZ2dFQkFJNFd1aU80Q3ZVUFRQWFZwbzhvCmRTUGVpSXlnWGRCRTFjSnFtQXVnUmZqNHZrVGVlSkZwazNLZXpqN2puMEtrZ1A1RUNGcDF5UWVZdEV1VjJFOEkKQlNKSHpDL1BWOHFLcjYwZ3BRUklOcGE3am5qT1hwdGgwbFdlNVp5N2RnbVB3K0l4Q3RjYjRxY2lsZWNPNEtzeApNTjlwRTYwdWJQZjBjT3kva3J2aWFLdmtRSU15WHc2c0hsOTB0eUEwYjc0NkxOQXNsbnFINUUwemVSK0pHTHR4ClFFd0U3Q3BESXBtNU1pa1ZaN2R4QitHWGMwTDlQQzhCTW5VRUE1c3A3UlVwNTkydVlOMHVlK2F0K0U1Q0RkeUMKeEFWeGxTNHBrcnZJemdPOXQySGZXUDU2aVpIamFmdVNvZUVBQUdSZzVXNmpoYWdDZG5GK0NXQmxTcUlFb2FoQgpRanNDZ2dFQVhOazh6QVV5dTVYZzVRZW5jVXlCVjVzeC9jUERJZ1p0YWZXTWdtdmI1Uk9OcmZ6TlBGL1MyN1VWCk5tWmU1bmxTMXhCc0RuR0ZFa2NnOWlDTnVnYVhadFVwMXc5eFFhamEwWmpYUjJOZzBPSDZRK1FxWnpqdnNMNHUKdVRIZHgwUG45Mm12ZGVSUmlnODVJRUx3RDVnU0RINm1LMlJ5UUpSZ0lMcVVIc3BtKzdya1hnNmo4b2JhNVlOQQpFR2IrSlZudXRtUGgyc1cwUzNYQW9rc2twemFmazhJMWJVY3k4c1drTWJZSjF5RUxLUWh5RDIzTmgwYzRDLytXCjUzaWVZT2ZnY2RGVEJvSlVEdHk2c3NwSWg5b1BBR3VONXROQlFBK2VlRHFQbEtUQmdaOTU0OEpvMSt5cVFaMFcKb1lYeG0rUEJObTZxK3FmVzJzQ2VyQjRaY0pxWkFnPT0KLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLQo= +kind: Secret +metadata: + name: kyverno-envoy-sidecar-certs + namespace: kyverno-envoy-sidecar-injector + labels: + app.kubernetes.io/name: sidecar-injector + app.kubernetes.io/instance: sidecar-injector diff --git a/sidecar-injector/manifests/service.yaml b/sidecar-injector/manifests/service.yaml new file mode 100644 index 00000000..d867da41 --- /dev/null +++ b/sidecar-injector/manifests/service.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Service +metadata: + name: kyverno-envoy-sidecar + namespace: kyverno-envoy-sidecar-injector + labels: + app.kubernetes.io/name: sidecar-injector + app.kubernetes.io/instance: sidecar-injector +spec: + type: ClusterIP + ports: + - name: https + protocol: TCP + port: 443 + targetPort: 8443 + selector: + app.kubernetes.io/name: sidecar-injector + app.kubernetes.io/instance: sidecar-injector \ No newline at end of file diff --git a/sidecar-injector/manifests/sidecar-configmap.yaml b/sidecar-injector/manifests/sidecar-configmap.yaml new file mode 100644 index 00000000..4b4f767b --- /dev/null +++ b/sidecar-injector/manifests/sidecar-configmap.yaml @@ -0,0 +1,26 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: kyverno-envoy-sidecar + namespace: kyverno-envoy-sidecar-injector +data: + sidecars.yaml: | + - name: kyverno-envoy-sidecar + containers: + - image: sanskardevops/plugin:0.0.25 + imagePullPolicy: IfNotPresent + name: ext-authz + ports: + - containerPort: 8000 + - containerPort: 9000 + args: + - "serve" + - "--policy=/policies/policy.yaml" + volumeMounts: + - name: policy-files + mountPath: /policies + volumes: + - name: policy-files + configMap: + name: policy-files + \ No newline at end of file diff --git a/sidecar-injector/pkg/admission/admission.go b/sidecar-injector/pkg/admission/admission.go new file mode 100644 index 00000000..1f3daab4 --- /dev/null +++ b/sidecar-injector/pkg/admission/admission.go @@ -0,0 +1,167 @@ +package admission + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + + log "github.com/sirupsen/logrus" + admissionv1 "k8s.io/api/admission/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// PatchOperation JsonPatch struct http://jsonpatch.com/ +type PatchOperation struct { + Op string `json:"op"` + Path string `json:"path"` + Value interface{} `json:"value,omitempty"` +} + +// RequestHandler AdmissionRequest handler +type RequestHandler interface { + handleAdmissionCreate(ctx context.Context, request *admissionv1.AdmissionRequest) ([]PatchOperation, error) +} + +// Handler Generic handler for Admission +type Handler struct { + Handler RequestHandler +} + +// HandleAdmission HttpServer function to handle Admissions +func (handler *Handler) HandleAdmission(writer http.ResponseWriter, request *http.Request) { + if err := validateRequest(request); err != nil { + log.Error(err.Error()) + handler.writeErrorAdmissionReview(http.StatusBadRequest, err.Error(), writer) + return + } + + body, err := readRequestBody(request) + if err != nil { + log.Error(err.Error()) + handler.writeErrorAdmissionReview(http.StatusInternalServerError, err.Error(), writer) + return + } + + admReview := admissionv1.AdmissionReview{} + + err = json.Unmarshal(body, &admReview) + if err != nil { + message := fmt.Sprintf("Could not decode body: %v", err) + log.Error(message) + handler.writeErrorAdmissionReview(http.StatusInternalServerError, message, writer) + return + } + + ctx := context.Background() + + req := admReview.Request + log.Infof("AdmissionReview for Kind=%v, Namespace=%v Name=%v UID=%v patchOperation=%v UserInfo=%v", req.Kind, req.Namespace, req.Name, req.UID, req.Operation, req.UserInfo) + if patchOperations, err := handler.Process(ctx, req); err != nil { + message := fmt.Sprintf("request for object '%s' with name '%s' in namespace '%s' denied: %v", req.Kind.String(), req.Name, req.Namespace, err) + log.Error(message) + handler.writeDeniedAdmissionResponse(&admReview, message, writer) + } else if patchBytes, err := json.Marshal(patchOperations); err != nil { + message := fmt.Sprintf("request for object '%s' with name '%s' in namespace '%s' denied: %v", req.Kind.String(), req.Name, req.Namespace, err) + log.Error(message) + handler.writeDeniedAdmissionResponse(&admReview, message, writer) + } else { + handler.writeAllowedAdmissionReview(&admReview, patchBytes, writer) + } +} + +// Process Handles the AdmissionRequest via the handler +func (handler *Handler) Process(ctx context.Context, request *admissionv1.AdmissionRequest) ([]PatchOperation, error) { + + if request.Operation == admissionv1.Create { + return handler.Handler.handleAdmissionCreate(ctx, request) + } + return nil, fmt.Errorf("unhandled request operation %s", request.Operation) +} + +func validateRequest(req *http.Request) error { + if req.Method != http.MethodPost { + return fmt.Errorf("wrong http verb. got %s", req.Method) + } + if req.Body == nil { + return errors.New("empty body") + } + contentType := req.Header.Get("Content-Type") + if contentType != "application/json" { + return fmt.Errorf("wrong content type. expected 'application/json', got: '%s'", contentType) + } + return nil +} + +func readRequestBody(req *http.Request) ([]byte, error) { + body, err := ioutil.ReadAll(req.Body) + if err != nil { + return nil, fmt.Errorf("unable to read Request Body: %v", err) + } + return body, nil +} + +func (handler *Handler) writeAllowedAdmissionReview(ar *admissionv1.AdmissionReview, patch []byte, res http.ResponseWriter) { + ar.Response = handler.admissionResponse(http.StatusOK, "") + ar.Response.Allowed = true + ar.Response.UID = ar.Request.UID + if patch != nil { + pt := admissionv1.PatchTypeJSONPatch + ar.Response.Patch = patch + ar.Response.PatchType = &pt + } + handler.write(ar, res) +} + +func (handler *Handler) writeDeniedAdmissionResponse(ar *admissionv1.AdmissionReview, message string, res http.ResponseWriter) { + ar.Response = handler.admissionResponse(http.StatusForbidden, message) + ar.Response.UID = ar.Request.UID + handler.write(ar, res) +} + +func (handler *Handler) writeErrorAdmissionReview(status int, message string, res http.ResponseWriter) { + admResp := handler.errorAdmissionReview(status, message) + handler.write(admResp, res) +} + +func (handler *Handler) errorAdmissionReview(httpErrorCode int, message string) *admissionv1.AdmissionReview { + r := baseAdmissionReview() + r.Response = handler.admissionResponse(httpErrorCode, message) + return r +} + +func (handler *Handler) admissionResponse(httpErrorCode int, message string) *admissionv1.AdmissionResponse { + return &admissionv1.AdmissionResponse{ + Result: &metav1.Status{ + Code: int32(httpErrorCode), + Message: message, + }, + } +} + +func baseAdmissionReview() *admissionv1.AdmissionReview { + gvk := admissionv1.SchemeGroupVersion.WithKind("AdmissionReview") + return &admissionv1.AdmissionReview{ + TypeMeta: metav1.TypeMeta{ + Kind: gvk.Kind, + APIVersion: gvk.GroupVersion().String(), + }, + } +} + +func (handler *Handler) write(r *admissionv1.AdmissionReview, res http.ResponseWriter) { + resp, err := json.Marshal(r) + if err != nil { + log.Errorf("Error marshalling decision: %v", err) + res.WriteHeader(http.StatusInternalServerError) + return + } + _, err = res.Write(resp) + if err != nil { + log.Errorf("Error writing response: %v", err) + res.WriteHeader(http.StatusInternalServerError) + return + } +} diff --git a/sidecar-injector/pkg/admission/podpatcher.go b/sidecar-injector/pkg/admission/podpatcher.go new file mode 100644 index 00000000..01136884 --- /dev/null +++ b/sidecar-injector/pkg/admission/podpatcher.go @@ -0,0 +1,11 @@ +package admission + +import ( + "context" + + corev1 "k8s.io/api/core/v1" +) + +type PodPatcher interface { + PatchPodCreate(ctx context.Context, pod corev1.Pod) ([]PatchOperation, error) +} diff --git a/sidecar-injector/pkg/admission/podrequesthandler.go b/sidecar-injector/pkg/admission/podrequesthandler.go new file mode 100644 index 00000000..21144baa --- /dev/null +++ b/sidecar-injector/pkg/admission/podrequesthandler.go @@ -0,0 +1,29 @@ +package admission + +import ( + "context" + "encoding/json" + + "github.com/pkg/errors" + admissionv1 "k8s.io/api/admission/v1" + corev1 "k8s.io/api/core/v1" +) + +// PodAdmissionRequestHandler PodAdmissionRequest handler +type PodAdmissionRequestHandler struct { + PodHandler PodPatcher +} + +func (handler *PodAdmissionRequestHandler) handleAdmissionCreate(ctx context.Context, request *admissionv1.AdmissionRequest) ([]PatchOperation, error) { + pod, err := unmarshalPod(request.Object.Raw) + if err != nil { + return nil, err + } + return handler.PodHandler.PatchPodCreate(ctx, pod) +} + +func unmarshalPod(rawObject []byte) (corev1.Pod, error) { + var pod corev1.Pod + err := json.Unmarshal(rawObject, &pod) + return pod, errors.Wrapf(err, "error unmarshalling object") +} diff --git a/sidecar-injector/pkg/httpd/simpleserver.go b/sidecar-injector/pkg/httpd/simpleserver.go new file mode 100644 index 00000000..32f7952b --- /dev/null +++ b/sidecar-injector/pkg/httpd/simpleserver.go @@ -0,0 +1,76 @@ +package httpd + +import ( + "fmt" + "net/http" + "os" + "path/filepath" + + "github.com/kyverno/kyverno-envoy-plugin/sidecar-injector/pkg/admission" + "github.com/kyverno/kyverno-envoy-plugin/sidecar-injector/pkg/webhook" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" +) + +/*SimpleServer is the required config to create httpd server*/ +type SimpleServer struct { + Local bool + Port int + CertFile string + KeyFile string + Patcher webhook.SidecarInjectorPatcher + Debug bool +} + +/*Start the simple http server supporting TLS*/ +func (simpleServer *SimpleServer) Start() error { + k8sClient, err := simpleServer.CreateClient() + if err != nil { + return err + } + + simpleServer.Patcher.K8sClient = k8sClient + server := &http.Server{ + Addr: fmt.Sprintf(":%d", simpleServer.Port), + } + + mux := http.NewServeMux() + server.Handler = mux + + admissionHandler := &admission.Handler{ + Handler: &admission.PodAdmissionRequestHandler{ + PodHandler: &simpleServer.Patcher, + }, + } + mux.HandleFunc("/healthz", webhook.HealthCheckHandler) + mux.HandleFunc("/mutate", admissionHandler.HandleAdmission) + + if simpleServer.Local { + return server.ListenAndServe() + } + return server.ListenAndServeTLS(simpleServer.CertFile, simpleServer.KeyFile) +} + +// CreateClient Create the server +func (simpleServer *SimpleServer) CreateClient() (*kubernetes.Clientset, error) { + config, err := simpleServer.buildConfig() + + if err != nil { + return nil, errors.Wrapf(err, "error setting up cluster config") + } + + return kubernetes.NewForConfig(config) +} + +func (simpleServer *SimpleServer) buildConfig() (*rest.Config, error) { + if simpleServer.Local { + log.Debug("Using local kubeconfig.") + kubeconfig := filepath.Join(os.Getenv("HOME"), ".kube", "config") + return clientcmd.BuildConfigFromFlags("", kubeconfig) + } + log.Debug("Using in cluster kubeconfig.") + return rest.InClusterConfig() +} diff --git a/sidecar-injector/pkg/webhook/health.go b/sidecar-injector/pkg/webhook/health.go new file mode 100644 index 00000000..4e8243fb --- /dev/null +++ b/sidecar-injector/pkg/webhook/health.go @@ -0,0 +1,8 @@ +package webhook + +import "net/http" + +// HealthCheckHandler HttpServer function to handle Health check +func HealthCheckHandler(writer http.ResponseWriter, _ *http.Request) { + writer.WriteHeader(http.StatusOK) +} diff --git a/sidecar-injector/pkg/webhook/sidecarhandler.go b/sidecar-injector/pkg/webhook/sidecarhandler.go new file mode 100644 index 00000000..63058558 --- /dev/null +++ b/sidecar-injector/pkg/webhook/sidecarhandler.go @@ -0,0 +1,124 @@ +package webhook + +import ( + "context" + "strings" + + "github.com/ghodss/yaml" + "github.com/kyverno/kyverno-envoy-plugin/sidecar-injector/pkg/admission" + log "github.com/sirupsen/logrus" + corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" +) + +// Sidecar Kubernetes Sidecar Injector schema +type Sidecar struct { + Name string `yaml:"name"` + InitContainers []corev1.Container `yaml:"initContainers"` + Containers []corev1.Container `yaml:"containers"` + Volumes []corev1.Volume `yaml:"volumes"` + ImagePullSecrets []corev1.LocalObjectReference `yaml:"imagePullSecrets"` + Annotations map[string]string `yaml:"annotations"` + Labels map[string]string `yaml:"labels"` +} + +// SidecarInjectorPatcher Sidecar Injector patcher +type SidecarInjectorPatcher struct { + K8sClient kubernetes.Interface + SidecarDataKey string + AllowAnnotationOverrides bool + AllowLabelOverrides bool +} + +func createArrayPatches[T any](newCollection []T, existingCollection []T, path string) []admission.PatchOperation { + var patches []admission.PatchOperation + for index, item := range newCollection { + indexPath := path + var value interface{} + first := index == 0 && len(existingCollection) == 0 + if !first { + indexPath = indexPath + "/-" + value = item + } else { + value = []T{item} + } + patches = append(patches, admission.PatchOperation{ + Op: "add", + Path: indexPath, + Value: value, + }) + } + return patches +} + +func createObjectPatches(newMap map[string]string, existingMap map[string]string, path string, override bool) []admission.PatchOperation { + var patches []admission.PatchOperation + if existingMap == nil { + patches = append(patches, admission.PatchOperation{ + Op: "add", + Path: path, + Value: newMap, + }) + } else { + for key, value := range newMap { + if _, ok := existingMap[key]; !ok || (ok && override) { + key = escapeJSONPath(key) + op := "add" + if ok { + op = "replace" + } + patches = append(patches, admission.PatchOperation{ + Op: op, + Path: path + "/" + key, + Value: value, + }) + } + } + } + return patches +} + +// Escape keys that may contain `/`s or `~`s to have a valid patch +// Order matters here, otherwise `/` --> ~01, instead of ~1 +func escapeJSONPath(k string) string { + k = strings.ReplaceAll(k, "~", "~0") + return strings.ReplaceAll(k, "/", "~1") +} + +// PatchPodCreate Handle Pod Create Patch +func (patcher *SidecarInjectorPatcher) PatchPodCreate(ctx context.Context, pod corev1.Pod) ([]admission.PatchOperation, error) { + namespace := "kyverno-envoy-sidecar-injector" + podName := pod.GetName() + if podName == "" { + podName = pod.GetGenerateName() + } + var patches []admission.PatchOperation + configmapSidecarName := "kyverno-envoy-sidecar" + log.Infof("sideCar injection for %v/%v: sidecars: %v", namespace, podName, configmapSidecarName) + configmapSidecar, err := patcher.K8sClient.CoreV1().ConfigMaps(namespace).Get(ctx, configmapSidecarName, metav1.GetOptions{}) + if k8serrors.IsNotFound(err) { + log.Warnf("sidecar configmap %s/%s was not found", namespace, configmapSidecarName) + } else if err != nil { + log.Errorf("error fetching sidecar configmap %s/%s - %v", namespace, configmapSidecarName, err) + } else if sidecarsStr, ok := configmapSidecar.Data[patcher.SidecarDataKey]; ok { + var sidecars []Sidecar + if err := yaml.Unmarshal([]byte(sidecarsStr), &sidecars); err != nil { + log.Errorf("error unmarshalling %s from configmap %s/%s", patcher.SidecarDataKey, pod.GetNamespace(), configmapSidecarName) + } + if sidecars != nil { + for _, sidecar := range sidecars { + patches = append(patches, createArrayPatches(sidecar.InitContainers, pod.Spec.InitContainers, "/spec/initContainers")...) + patches = append(patches, createArrayPatches(sidecar.Containers, pod.Spec.Containers, "/spec/containers")...) + patches = append(patches, createArrayPatches(sidecar.Volumes, pod.Spec.Volumes, "/spec/volumes")...) + patches = append(patches, createArrayPatches(sidecar.ImagePullSecrets, pod.Spec.ImagePullSecrets, "/spec/imagePullSecrets")...) + patches = append(patches, createObjectPatches(sidecar.Annotations, pod.Annotations, "/metadata/annotations", patcher.AllowAnnotationOverrides)...) + patches = append(patches, createObjectPatches(sidecar.Labels, pod.Labels, "/metadata/labels", patcher.AllowLabelOverrides)...) + } + log.Debugf("sidecar patches being applied for %v/%v: patches: %v", namespace, podName, patches) + } + } + + return patches, nil +}