Skip to content

Commit

Permalink
Add x directory and mutating webhook (#524)
Browse files Browse the repository at this point in the history
* Add mutating webhook to x

* fix build

* fix

* update

* fix cfg

* linter

* fix

* add info to readme

* linter

* linter

* fix lint
  • Loading branch information
alexmasi authored Apr 3, 2024
1 parent 6161c08 commit f562169
Show file tree
Hide file tree
Showing 25 changed files with 2,383 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
**/super-linter.log
kne_cli/kne_cli
controller/server/server
x/webhook/webhook
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ require (
github.com/spf13/viper v1.17.0
github.com/srl-labs/srl-controller v0.6.1
github.com/srl-labs/srlinux-scrapli v0.6.0
github.com/wI2L/jsondiff v0.5.1
go.universe.tf/metallb v0.13.5
golang.org/x/oauth2 v0.13.0
google.golang.org/api v0.149.0
Expand Down Expand Up @@ -121,6 +122,10 @@ require (
github.com/spf13/afero v1.10.0 // indirect
github.com/spf13/cast v1.5.1 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tidwall/gjson v1.17.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/otel v1.19.0 // indirect
go.opentelemetry.io/otel/metric v1.19.0 // indirect
Expand Down
11 changes: 11 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1318,6 +1318,17 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM=
github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/wI2L/jsondiff v0.5.1 h1:xS4zYUspH4U3IB0Lwo9+jv+MSRJSWMF87Y4BpDbFMHo=
github.com/wI2L/jsondiff v0.5.1/go.mod h1:qqG6hnK0Lsrz2BpIVCxWiK9ItsBCpIZQiv0izJjOZ9s=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
Expand Down
15 changes: 15 additions & 0 deletions x/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# `x` directory

This directory contains code that is compatible with KNE but is outside of the
main tree. Code is separated here to indicate that it is less thoroughly tested
than the rest of the code in the repository. There is no SLO for `x` code. Code
under `x` may contain backwards incompatible changes (although this will try to
be avoided).

We will try to maintain this tree and continue to contribute to and enhance it.
However this will largely be "best effort" and held to a lesser standard than
the main tree.

This is similar to the concept of core Golang X-repositories (with even looser
requirements). See [Golang docs](https://go.dev/wiki/X-Repositories) for more
info.
9 changes: 9 additions & 0 deletions x/webhook/Dockerfile.webhook
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
FROM golang:latest

RUN mkdir /launcher
COPY webhook /launcher
WORKDIR /launcher

EXPOSE 8080

CMD ["./webhook"]
203 changes: 203 additions & 0 deletions x/webhook/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
# KNE Mutating Webhook

This directory contains the code and configurations (in the form of manifests)
for the mutating webhook. The webhook should be deployed onto a KNE
cluster.

This webhook can be used to mutate any K8 resources. This directory contains
the generic webhook along with an example mutator that simply adds an alpine
linux container to created pods.

To develop custom a custom mutation simply change the mutate function in the
examples subdirectory.

The following guide assumes your working directory is `kne/x/webhook`.

## Build

Run:

```bash
./containerize.sh
```

to build the webhook container from the binary `main.go`.

## Deployment

For the webhook to be effective it must be deployed when the k8s cluster is up
but before the KNE topology is created.

Start by deploying the kubernetes cluster

```bash
kne deploy ../../deploy/kne/kind-bridge.yaml
```

You should be in this state:

```bash
$ kubectl get pods -A
NAMESPACE NAME READY STATUS RESTARTS AGE
arista-ceoslab-operator-system arista-ceoslab-operator-controller-manager-5cb5fb9db4-7jqp9 2/2 Running 0 45h
ixiatg-op-system ixiatg-op-controller-manager-5947cd6f59-jq5pw 2/2 Running 0 45h
kube-system coredns-787d4945fb-8x8wf 1/1 Running 0 45h
kube-system coredns-787d4945fb-ng7hf 1/1 Running 0 45h
kube-system etcd-kne-control-plane 1/1 Running 0 45h
kube-system kindnet-zlwzz 1/1 Running 0 45h
kube-system kube-apiserver-kne-control-plane 1/1 Running 0 45h
kube-system kube-controller-manager-kne-control-plane 1/1 Running 0 45h
kube-system kube-proxy-kwsqm 1/1 Running 0 45h
kube-system kube-scheduler-kne-control-plane 1/1 Running 0 45h
lemming-operator lemming-controller-manager-6fc9d47f7d-vnshj 2/2 Running 0 45h
local-path-storage local-path-provisioner-c8855d4bb-8m9bp 1/1 Running 0 45h
meshnet meshnet-ddm8q 1/1 Running 0 45h
metallb-system controller-8bb68977b-vx99n 1/1 Running 0 45h
metallb-system speaker-hj8jf 1/1 Running 0 45h
srlinux-controller srlinux-controller-controller-manager-57f8c48bf-6kqlg 2/2 Running 0 45h
```

At this point the k8s cluster is up and operational. We can now load the webhook
manifests.

```bash
kind load docker-image webhook:latest --name kne
```

```bash
kubectl apply -f manifests/
```

This should result in the webhook pod to be present.

```bash
$ kubectl get pods -A
...
default kne-assembly-webhook-f5b8cf987-lpxjt 1/1 Running 0 5s
...
```

We can now create the KNE topology.

*Note* The KNE topology must have the label `webhook:enabled` for each node, as in
[this example](examples/topology.textproto),
otherwise the webhook will ignore the pod upon create.

```bash
labels {
key: "webhook"
value: "enabled"
}
```

Use the normal KNE command to create the topology.

```bash
kne create examples/topology.textproto
```

You should now see r1 with 2 containers instead of the one, this is
because the webhook has injected the alpine linux container.

```bash
$ kubectl get pods -n webhook-example
r1 3/3 Running 0 24s
r2 2/2 Running 0 22s
```

```bash
$ kubectl describe pod r1 -n webhook-example
...
Containers:
r1:
Container ID: containerd://0dd84381ac5970d796c866adf73022c1ed5610ceb40546e443a35b7eff6a3f39
Image: alpine:latest
Image ID: docker.io/library/alpine@sha256:c5b1261d6d3e43071626931fc004f70149baeba2c8ec672bd4f27761f8e1ad6b
Port: <none>
Host Port: <none>
Command:
/bin/sh
-c
sleep 2000000000000
State: Running
Started: Tue, 02 Apr 2024 23:24:40 +0000
Ready: True
Restart Count: 0
Environment: <none>
Mounts:
/var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-kgrn6 (ro)
alpine:
Container ID: containerd://be7db4c4704b415d0dda1d5ed64b70c7f271086670aa803f105399dc95e35ad8
Image: alpine:latest
Image ID: docker.io/library/alpine@sha256:c5b1261d6d3e43071626931fc004f70149baeba2c8ec672bd4f27761f8e1ad6b
Port: <none>
Host Port: <none>
Command:
/bin/sh
-c
sleep 2000000000000
State: Running
Started: Tue, 02 Apr 2024 23:24:40 +0000
Ready: True
Restart Count: 0
Requests:
cpu: 500m
memory: 1Gi
Environment: <none>
Mounts:
/var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-kgrn6 (ro)
...
```

## Removing the webhook

Removing the webhook can be achieved by deleting the loaded manifests from the
k8s cluster.

```bash
kubectl delete -f manifests/
```

## Debugging

In order to obtain the logs of the webhook you can use the following command.

```bash
kubectl logs -l app=kne-assembly-webhook -f
```

In the logs you should see output similar to this:

```bash
I1215 11:54:46.729680 1 main.go:25] Listening on port 443...
I0402 23:24:36.383536 1 mutate.go:45] Mutating &TypeMeta{Kind:Pod,APIVersion:v1,}
I0402 23:24:36.394188 1 mutate.go:45] Mutating &TypeMeta{Kind:Pod,APIVersion:v1,}
I0402 23:24:36.394227 1 addcontainer.go:34] Ignoring pod "r2", mutation not requested
```

This output shows that it mutated the pod r1 but not r2 since
the label was not added to that KNE node.

### TLS

Run:

```bash
./secure/genCerts.sh
```

to optionally update the TLS certs in the manifest files. It handles updating
`manifests/tls.secret.yaml` however the `caBundle` in
`manifests/mutating.config.yaml` will need to be updated manually based on the
output of the script. This is not required but may be useful.

## Customize the webhook

Edit `main.go` to specify any mutation functions as desired. The example uses
the mutation function found in `examples/addcontainer/addcontainer.go` but any
mutation function is supported. This includes mutating services and other
resources besides just pods. However you may also have to change
`manifests/mutating.config.yaml` to select other resources types than just
pods.

After customization is done, rebuild the container and reapply the manifests.
101 changes: 101 additions & 0 deletions x/webhook/admission/admission.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package admission handles kubernetes admissions.
package admission

import (
"net/http"

"github.com/openconfig/kne/x/webhook/mutate"
admissionv1 "k8s.io/api/admission/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes/scheme"
)

// mutator is an entity capable of mutating pods. A mutation is any addition or removal from a
// Pod configuration. This could be adding containers or simply added envVars.
type mutator interface {
MutateObject(runtime.Object) ([]byte, error)
}

// Admitter admits a pod into the review process.
type Admitter struct {
request *admissionv1.AdmissionRequest
mutator mutator
}

// New builds a new Admitter.
func New(request *admissionv1.AdmissionRequest, mutations []mutate.MutationFunc) *Admitter {
return &Admitter{
request: request,
mutator: mutate.New(mutations),
}
}

// Review filters for resources that should be mutated by this mutating webhook. Specifically, any resource who
// has the label `"webhook":"enabled"`, will be mutated by this webhook.
func (a Admitter) Review() (*admissionv1.AdmissionReview, error) {
obj, err := runtime.Decode(scheme.Codecs.UniversalDeserializer(), a.request.Object.Raw)
if err != nil {
return reviewResponse(a.request.UID, false, http.StatusBadRequest, err.Error()), err
}
patch, err := a.mutator.MutateObject(obj)
if err != nil {
return reviewResponse(a.request.UID, false, http.StatusBadRequest, err.Error()), err
}
return patchReviewResponse(a.request.UID, patch)
}

// reviewResponse constructs a valid response for the k8s server.
func reviewResponse(uid types.UID, allowed bool, httpCode int32, reason string) *admissionv1.AdmissionReview {
return &admissionv1.AdmissionReview{
TypeMeta: metav1.TypeMeta{
Kind: "AdmissionReview",
APIVersion: "admission.k8s.io/v1",
},
Response: &admissionv1.AdmissionResponse{
UID: uid,
Allowed: allowed,
Result: &metav1.Status{
Code: httpCode,
Message: reason,
},
},
}
}

// patchReviewResponse builds an admission review with given json patch
func patchReviewResponse(uid types.UID, patch []byte) (*admissionv1.AdmissionReview, error) {
patchType := admissionv1.PatchTypeJSONPatch
resp := &admissionv1.AdmissionResponse{
UID: uid,
Allowed: true,
}

if patch != nil {
resp.PatchType = &patchType
resp.Patch = patch
}

return &admissionv1.AdmissionReview{
TypeMeta: metav1.TypeMeta{
Kind: "AdmissionReview",
APIVersion: "admission.k8s.io/v1",
},
Response: resp,
}, nil
}
Loading

0 comments on commit f562169

Please sign in to comment.