feat: implement pod-specific secret injection for DaemonSets with automated lifecycle management
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user