165 lines
4.2 KiB
Go
165 lines
4.2 KiB
Go
package webhook
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"math/rand"
|
|
"net/http"
|
|
"time"
|
|
|
|
corev1 "k8s.io/api/core/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
ctrl "sigs.k8s.io/controller-runtime"
|
|
)
|
|
|
|
func init() {
|
|
rand.Seed(time.Now().UnixNano())
|
|
}
|
|
|
|
func randomString(n int) string {
|
|
var letters = []rune("abcdefghijklmnopqrstuvwxyz0123456789")
|
|
s := make([]rune, n)
|
|
for i := range s {
|
|
s[i] = letters[rand.Intn(len(letters))]
|
|
}
|
|
return string(s)
|
|
}
|
|
|
|
var log = ctrl.Log.WithName("pod-mutator")
|
|
|
|
// PodMutator mutates Pods
|
|
type PodMutator struct {
|
|
Client client.Client
|
|
decoder *admission.Decoder
|
|
TargetNS string
|
|
TargetDSList []string
|
|
}
|
|
|
|
// PodMutator implements admission.DecoderInjector.
|
|
// A decoder will be automatically injected.
|
|
|
|
// InjectDecoder injects the decoder.
|
|
func (a *PodMutator) InjectDecoder(d *admission.Decoder) error {
|
|
a.decoder = d
|
|
return nil
|
|
}
|
|
|
|
// Handle handles admission requests.
|
|
func (a *PodMutator) Handle(ctx context.Context, req admission.Request) admission.Response {
|
|
pod := &corev1.Pod{}
|
|
|
|
err := a.decoder.Decode(req, pod)
|
|
if err != nil {
|
|
return admission.Errored(http.StatusBadRequest, err)
|
|
}
|
|
|
|
// Check namespace
|
|
if pod.Namespace == "" {
|
|
pod.Namespace = req.Namespace
|
|
}
|
|
|
|
if pod.Namespace != a.TargetNS {
|
|
return admission.Allowed("not in target namespace")
|
|
}
|
|
|
|
// Check if it belongs to target DaemonSet
|
|
isTarget := false
|
|
dsName := ""
|
|
for _, targetDS := range a.TargetDSList {
|
|
for _, owner := range pod.OwnerReferences {
|
|
if owner.Kind == "DaemonSet" && owner.Name == targetDS {
|
|
isTarget = true
|
|
dsName = targetDS
|
|
break
|
|
}
|
|
}
|
|
if isTarget {
|
|
break
|
|
}
|
|
}
|
|
|
|
if !isTarget {
|
|
return admission.Allowed("not a target daemonset pod")
|
|
}
|
|
|
|
// Extract NodeName from affinity if not set (common for DaemonSets)
|
|
nodeName := pod.Spec.NodeName
|
|
if nodeName == "" && pod.Spec.Affinity != nil &&
|
|
pod.Spec.Affinity.NodeAffinity != nil &&
|
|
pod.Spec.Affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution != nil {
|
|
for _, term := range pod.Spec.Affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms {
|
|
for _, expr := range term.MatchFields {
|
|
if expr.Key == "metadata.name" && expr.Operator == corev1.NodeSelectorOpIn && len(expr.Values) > 0 {
|
|
nodeName = expr.Values[0]
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Requirement: Secret name is the same as pod name.
|
|
// Since pod.Name is empty during CREATE, we must assign it.
|
|
if pod.Name == "" {
|
|
pod.Name = pod.GenerateName + randomString(5)
|
|
}
|
|
secretName := pod.Name
|
|
|
|
// Add finalizer to ensure our controller can cleanup the secret before pod is gone
|
|
pod.Finalizers = append(pod.Finalizers, "inject-ds-webhook.example.com/cleanup")
|
|
|
|
log.Info("Mutating pod", "node", nodeName, "podName", pod.Name, "secretName", secretName, "ds", dsName)
|
|
|
|
// Create Secret
|
|
secret := &corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: secretName,
|
|
Namespace: pod.Namespace,
|
|
Labels: map[string]string{
|
|
"injected-by": "inject-ds-webhook",
|
|
},
|
|
},
|
|
StringData: map[string]string{
|
|
"node-name": nodeName,
|
|
"pod-name": pod.Name,
|
|
"secret-data": fmt.Sprintf("unique-secret-for-%s-on-%s", pod.Name, nodeName),
|
|
},
|
|
}
|
|
|
|
err = a.Client.Create(ctx, secret)
|
|
if err != nil && !apierrors.IsAlreadyExists(err) {
|
|
return admission.Errored(http.StatusInternalServerError, err)
|
|
}
|
|
|
|
// Mutate Pod to add volume
|
|
volumeName := "node-secret-volume"
|
|
|
|
// Add volume
|
|
pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{
|
|
Name: volumeName,
|
|
VolumeSource: corev1.VolumeSource{
|
|
Secret: &corev1.SecretVolumeSource{
|
|
SecretName: secretName,
|
|
},
|
|
},
|
|
})
|
|
|
|
// Add volume mount to all containers
|
|
for i := range pod.Spec.Containers {
|
|
pod.Spec.Containers[i].VolumeMounts = append(pod.Spec.Containers[i].VolumeMounts, corev1.VolumeMount{
|
|
Name: volumeName,
|
|
MountPath: "/etc/node-secret",
|
|
ReadOnly: true,
|
|
})
|
|
}
|
|
|
|
marshaledPod, err := json.Marshal(pod)
|
|
if err != nil {
|
|
return admission.Errored(http.StatusInternalServerError, err)
|
|
}
|
|
|
|
return admission.PatchResponseFromRaw(req.Object.Raw, marshaledPod)
|
|
} |