From 288e6635f7260ba318009e9139548ec5be63beeb Mon Sep 17 00:00:00 2001 From: cwiklik Date: Mon, 9 Mar 2026 16:32:28 -0400 Subject: [PATCH 1/3] feat: Target Pods at CREATE time instead of workload objects Change the mutating webhook from intercepting Deployments, StatefulSets, DaemonSets, Jobs, and CronJobs to intercepting Pods at CREATE time. This follows the pattern used by Istio, Linkerd, and Vault Agent Injector, and eliminates GitOps drift caused by mutating workload objects in etcd. Key changes: - Webhook handler decodes corev1.Pod directly (no workload switch/case) - deriveWorkloadName() extracts stable name from GenerateName - MutatingWebhookConfiguration targets pods in core API group - objectSelector pre-filters on kagenti.io/type + kagenti.io/inject - namespaceSelector requires kagenti-enabled=true label - reinvocationPolicy set to IfNeeded for multi-webhook compatibility - ClusterRole updated: removed apps/batch perms, added replicasets - Rollout script applies webhook config before image rollout (race fix) - 5 Ginkgo webhook handler tests added Closes #176 (Phase 1) Assisted-By: Claude (Anthropic AI) Signed-off-by: cwiklik --- .../templates/authbridge-mutatingwebhook.yaml | 30 ++-- .../templates/clusterrole.yaml | 5 +- kagenti-webhook/ARCHITECTURE.md | 19 +- kagenti-webhook/CLAUDE.md | 19 +- kagenti-webhook/README.md | 23 +-- kagenti-webhook/config/webhook/manifests.yaml | 31 +--- .../webhook/v1alpha1/authbridge_webhook.go | 119 ++++--------- .../v1alpha1/authbridge_webhook_test.go | 166 ++++++++++++++++++ .../webhook/v1alpha1/webhook_suite_test.go | 4 + kagenti-webhook/scripts/webhook-rollout.sh | 128 +++++++------- 10 files changed, 304 insertions(+), 240 deletions(-) create mode 100644 kagenti-webhook/internal/webhook/v1alpha1/authbridge_webhook_test.go diff --git a/charts/kagenti-webhook/templates/authbridge-mutatingwebhook.yaml b/charts/kagenti-webhook/templates/authbridge-mutatingwebhook.yaml index 2f1be44b6..51d42320c 100644 --- a/charts/kagenti-webhook/templates/authbridge-mutatingwebhook.yaml +++ b/charts/kagenti-webhook/templates/authbridge-mutatingwebhook.yaml @@ -17,9 +17,9 @@ webhooks: namespace: {{ include "kagenti-webhook.namespace" . }} path: /mutate-workloads-authbridge failurePolicy: Fail + reinvocationPolicy: IfNeeded timeoutSeconds: 10 sideEffects: None - # Exclude system namespaces and the webhook's own namespace from mutation. namespaceSelector: matchExpressions: - key: kubernetes.io/metadata.name @@ -29,9 +29,10 @@ webhooks: - kube-public - kube-node-lease - {{ include "kagenti-webhook.namespace" . }} - # Only intercept workloads explicitly labelled as agent or tool. - # Per-sidecar eligibility is then decided inside the handler via the - # two-stage precedence chain (feature gates + workload opt-out labels). + matchLabels: + # Only trigger webhook for namespaces that have opted-in + # This aligns with the precedence evaluator Layer 3 which requires kagenti-enabled: true + kagenti-enabled: "true" objectSelector: matchExpressions: - key: kagenti.io/type @@ -39,26 +40,17 @@ webhooks: values: - agent - tool + - key: kagenti.io/inject + operator: NotIn + values: + - disabled rules: - operations: - CREATE - - UPDATE - apiGroups: - - apps - apiVersions: - - v1 - resources: - - deployments - - statefulsets - - daemonsets - - operations: - - CREATE - - UPDATE apiGroups: - - batch + - "" apiVersions: - v1 resources: - - jobs - - cronjobs + - pods {{- end }} diff --git a/charts/kagenti-webhook/templates/clusterrole.yaml b/charts/kagenti-webhook/templates/clusterrole.yaml index 6056c0401..fe33201c7 100644 --- a/charts/kagenti-webhook/templates/clusterrole.yaml +++ b/charts/kagenti-webhook/templates/clusterrole.yaml @@ -22,9 +22,6 @@ rules: resources: ["serviceaccounts"] verbs: ["get", "create", "update"] - apiGroups: ["apps"] - resources: ["deployments", "statefulsets", "daemonsets"] - verbs: ["get", "list", "watch"] -- apiGroups: ["batch"] - resources: ["jobs", "cronjobs"] + resources: ["replicasets"] verbs: ["get", "list", "watch"] {{- end }} diff --git a/kagenti-webhook/ARCHITECTURE.md b/kagenti-webhook/ARCHITECTURE.md index abac4d1db..ba74e1c44 100644 --- a/kagenti-webhook/ARCHITECTURE.md +++ b/kagenti-webhook/ARCHITECTURE.md @@ -25,15 +25,10 @@ graph TB end subgraph "Kubernetes Resources" - subgraph "Standard Workloads" - DEPLOY[Deployments] - STS[StatefulSets] - DS[DaemonSets] - JOB[Jobs/CronJobs] - end + POD[Pods at CREATE] end - API -->|mutate workloads| AUTH + API -->|mutate pods| AUTH MAIN -->|creates & shares| MUTATOR MAIN -->|registers| AUTH @@ -43,19 +38,13 @@ graph TB MUTATOR -->|builds containers| CONT MUTATOR -->|builds volumes| VOL - AUTH -.->|modifies| DEPLOY - AUTH -.->|modifies| STS - AUTH -.->|modifies| DS - AUTH -.->|modifies| JOB + AUTH -.->|modifies| POD style MUTATOR fill:#90EE90 style AUTH fill:#32CD32,stroke:#006400,stroke-width:3px style CONT fill:#FFB6C1 style VOL fill:#FFB6C1 - style DEPLOY fill:#87CEEB - style STS fill:#87CEEB - style DS fill:#87CEEB - style JOB fill:#87CEEB + style POD fill:#87CEEB ``` ## Container Injection Flow diff --git a/kagenti-webhook/CLAUDE.md b/kagenti-webhook/CLAUDE.md index 9d7d7a128..63d784b25 100644 --- a/kagenti-webhook/CLAUDE.md +++ b/kagenti-webhook/CLAUDE.md @@ -15,7 +15,7 @@ There is one registered webhook: | Webhook | Path | Handles | |---------|------|---------| -| **AuthBridge** | `/mutate-workloads-authbridge` | Deployments, StatefulSets, DaemonSets, Jobs, CronJobs | +| **AuthBridge** | `/mutate-workloads-authbridge` | Pods at CREATE time (works with any workload controller) | The `PodMutator` instance is created in `cmd/main.go` and passed to the webhook setup function. @@ -68,7 +68,8 @@ kagenti-webhook/ │ │ ├── container_builder.go # Build* functions for each injected container │ │ └── volume_builder.go # BuildRequiredVolumes / BuildRequiredVolumesNoSpire │ └── v1alpha1/ # Webhook handlers -│ ├── authbridge_webhook.go # AuthBridge: raw admission.Handler +│ ├── authbridge_webhook.go # AuthBridge: raw admission.Handler (Pod-level) +│ ├── authbridge_webhook_test.go # Webhook handler tests (Ginkgo) │ └── webhook_suite_test.go # ENVTEST-based test setup (Ginkgo) ├── config/ # Kustomize manifests (CRDs, RBAC, webhook configs, etc.) ├── test/ @@ -199,20 +200,16 @@ Secrets: 5. Update `isAlreadyInjected()` in `authbridge_webhook.go` to check for the new container name. 6. Update `internal/webhook/config/types.go` and `defaults.go` with image/resource defaults. -### Adding a New Supported Workload Type -1. Add a new `case` in `AuthBridgeWebhook.Handle()` in `authbridge_webhook.go`. -2. Update the kubebuilder webhook marker to include the new resource in the `resources` list. -3. Run `make manifests` to regenerate webhook configuration YAML. -4. Update `scripts/webhook-rollout.sh` to include the new resource in the webhook rules. -5. Update the Helm chart template `charts/kagenti-webhook/templates/authbridge-mutatingwebhook.yaml`. +### Webhook Targeting Model +The webhook targets **Pods at CREATE time** (not Deployments/StatefulSets/etc.). This follows the same pattern used by Istio, Linkerd, and Vault Agent Injector. The handler decodes `corev1.Pod` directly — no switch on workload Kind. The `deriveWorkloadName()` helper extracts the workload name from `GenerateName` (trims trailing `-`). The `reinvocationPolicy` is set to `IfNeeded` so our webhook re-runs if other webhooks modify the Pod after our first pass. ### Modifying Injection Logic - Injection decision logic lives in `pod_mutator.go` in `InjectAuthBridge()`. - Changes to label/annotation keys require updating the constants at the top of `pod_mutator.go`. ### Updating Container Images -- Default images are defined as constants in `injector/container_builder.go` (`DefaultEnvoyImage`, `DefaultProxyInitImage`) and inline in `BuildSpiffeHelperContainer()` and `BuildClientRegistrationContainerWithSpireOption()`. -- The `internal/webhook/config/defaults.go` file has a parallel set of defaults in `CompiledDefaults()` -- keep them in sync (or wire the config system into the injector, which is a TODO). +- Default images are defined in `internal/webhook/config/defaults.go` via `CompiledDefaults()`. The `ContainerBuilder` reads from `*config.PlatformConfig` at build time — never hardcode images/ports/resources in the builder. +- The config system is fully wired in: `PodMutator` uses getter functions `func() *config.PlatformConfig` and creates a new `ContainerBuilder` per request with the current config snapshot, enabling hot-reload. - The GitHub Actions CI builds images defined in `../.github/workflows/build.yaml`. ### Helm Chart @@ -226,7 +223,7 @@ Secrets: 2. **Kubebuilder markers**: The `+kubebuilder:webhook` comments generate webhook manifests. If you change the path, resources, or groups, you must run `make manifests` to regenerate. -3. **AuthBridge uses raw admission.Handler**: Unlike webhooks that use `CustomDefaulter`/`CustomValidator`, the AuthBridge webhook registers directly via `mgr.GetWebhookServer().Register()`. This is because it handles multiple resource types in a single handler. +3. **AuthBridge uses raw admission.Handler**: Unlike webhooks that use `CustomDefaulter`/`CustomValidator`, the AuthBridge webhook registers directly via `mgr.GetWebhookServer().Register()`. It decodes `corev1.Pod` directly and includes a Kind guard for defense-in-depth against stale webhook configs. 4. **Idempotency check**: `isAlreadyInjected()` checks for all four injected components (`envoy-proxy`, `spiffe-helper`, `kagenti-client-registration` in sidecar containers, `proxy-init` in init containers). If any one is found, re-admission is short-circuited. diff --git a/kagenti-webhook/README.md b/kagenti-webhook/README.md index 5cbb3ae30..a2badedab 100644 --- a/kagenti-webhook/README.md +++ b/kagenti-webhook/README.md @@ -4,7 +4,7 @@ A Kubernetes admission webhook that automatically injects sidecar containers to ## Overview -This webhook provides security by automatically injecting sidecar containers that handle identity and authentication. It supports both standard Kubernetes workloads (Deployments, StatefulSets, etc.) and custom resources. +This webhook provides security by automatically injecting sidecar containers that handle identity and authentication. It intercepts **Pod CREATE** requests (not Deployments or other workload objects), which eliminates GitOps drift — the workload object in etcd remains exactly as the developer defined it, and sidecars are only visible at the Pod level. The webhook injects: @@ -27,15 +27,9 @@ Previous versions required `kagenti.io/inject: enabled` to trigger sidecar injec ## Supported Resources -The webhook supports sidecar injection for: +The **AuthBridge webhook** intercepts **Pod CREATE** requests in the core API group (`v1`). It injects sidecars into Pods created by any workload controller — Deployments, StatefulSets, DaemonSets, Jobs, and CronJobs all produce Pods that the webhook can mutate. -The **AuthBridge webhook** supports standard Kubernetes workload resources: - -- **Deployments** (apps/v1) -- **StatefulSets** (apps/v1) -- **DaemonSets** (apps/v1) -- **Jobs** (batch/v1) -- **CronJobs** (batch/v1) +This Pod-level targeting follows the same pattern used by Istio, Linkerd, and Vault Agent Injector. Because the webhook mutates Pods (not the parent Deployment), GitOps tools like Argo CD and Flux see no drift on the workload object stored in etcd. ## Injection Control @@ -206,7 +200,6 @@ The AuthBridge webhook supports two modes of operation: │ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ Your Application Container │ │ -│ │ (Deployment/StatefulSet/Job/etc.) │ │ │ └──────────────────────────────────────────────────────────┘ │ └────────────────────────────────────────────────────────────────┘ │ │ │ @@ -232,7 +225,6 @@ The AuthBridge webhook supports two modes of operation: │ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ Your Application Container │ │ -│ │ (Deployment/StatefulSet/Job/etc.) │ │ │ └──────────────────────────────────────────────────────────┘ │ └────────────────────────────────────────────────────────────────┘ │ @@ -347,8 +339,9 @@ The script handles the full build-and-deploy cycle: 2. Loads the image into the Kind cluster 3. Deploys the platform defaults ConfigMap (`kagenti-webhook-defaults`) 4. Deploys the feature gates ConfigMap (`kagenti-webhook-feature-gates`) -5. Updates the deployment image and patches in config volume mounts -6. Applies the AuthBridge `MutatingWebhookConfiguration` (always, so updates take effect on re-runs) +5. Applies the AuthBridge `MutatingWebhookConfiguration` (before image rollout to avoid race conditions) +6. Updates the deployment image and patches in config volume mounts +7. Waits for rollout to complete ```bash cd kagenti-webhook @@ -435,8 +428,8 @@ The `InjectAuthBridge()` method supports: - Init container injection (proxy-init) - Sidecar container injection (envoy-proxy, spiffe-helper, client-registration) -- Optional SPIRE integration via pod labels -- Support for standard Kubernetes workloads (Deployments, StatefulSets, DaemonSets, Jobs, CronJobs) +- Optional SPIRE integration via pod labels and feature gates +- Pod-level mutation at CREATE time (works with any workload controller) ## Uninstallation diff --git a/kagenti-webhook/config/webhook/manifests.yaml b/kagenti-webhook/config/webhook/manifests.yaml index b60153743..487a64b9c 100644 --- a/kagenti-webhook/config/webhook/manifests.yaml +++ b/kagenti-webhook/config/webhook/manifests.yaml @@ -13,39 +13,14 @@ webhooks: path: /mutate-workloads-authbridge failurePolicy: Fail name: inject.kagenti.io - namespaceSelector: - matchExpressions: - # Exclude system namespaces. - - key: kubernetes.io/metadata.name - operator: NotIn - values: - - kube-system - - kube-public - - kube-node-lease - # Only intercept workloads explicitly labelled as agent or tool. - # Per-sidecar eligibility is decided inside the handler via the two-stage - # precedence chain (feature gates + workload opt-out labels). - objectSelector: - matchExpressions: - - key: kagenti.io/type - operator: In - values: - - agent - - tool - timeoutSeconds: 10 + reinvocationPolicy: IfNeeded rules: - apiGroups: - - apps - - batch + - "" apiVersions: - v1 operations: - CREATE - - UPDATE resources: - - deployments - - statefulsets - - daemonsets - - jobs - - cronjobs + - pods sideEffects: None diff --git a/kagenti-webhook/internal/webhook/v1alpha1/authbridge_webhook.go b/kagenti-webhook/internal/webhook/v1alpha1/authbridge_webhook.go index f11b8401f..6d96347ae 100644 --- a/kagenti-webhook/internal/webhook/v1alpha1/authbridge_webhook.go +++ b/kagenti-webhook/internal/webhook/v1alpha1/authbridge_webhook.go @@ -20,10 +20,9 @@ import ( "context" "encoding/json" "net/http" + "strings" "github.com/kagenti/kagenti-extensions/kagenti-webhook/internal/webhook/injector" - appsv1 "k8s.io/api/apps/v1" - batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" ctrl "sigs.k8s.io/controller-runtime" logf "sigs.k8s.io/controller-runtime/pkg/log" @@ -33,7 +32,7 @@ import ( // authbridgelog is for logging in this package. var authbridgelog = logf.Log.WithName("authbridge-webhook") -// AuthBridgeWebhook handles mutation of workload resources for AuthBridge injection +// AuthBridgeWebhook handles mutation of Pod resources for AuthBridge injection type AuthBridgeWebhook struct { Mutator *injector.PodMutator decoder admission.Decoder @@ -53,7 +52,7 @@ func SetupAuthBridgeWebhookWithManager(mgr ctrl.Manager, mutator *injector.PodMu return nil } -// Handle processes admission requests for workload resources +// Handle processes admission requests for Pod resources // //nolint:gocritic // hugeParam: admission.Handler interface requires value receiver for admission.Request func (w *AuthBridgeWebhook) Handle(ctx context.Context, req admission.Request) admission.Response { @@ -63,111 +62,71 @@ func (w *AuthBridgeWebhook) Handle(ctx context.Context, req admission.Request) a "name", req.Name, "operation", req.Operation) - var podSpec *corev1.PodSpec - var resourceName string - var mutatedObj interface{} - var labels map[string]string - - // Extract PodSpec based on resource type - switch req.Kind.Kind { - case "Deployment": - var deployment appsv1.Deployment - if err := w.decoder.Decode(req, &deployment); err != nil { - authbridgelog.Error(err, "Failed to decode Deployment") - return admission.Errored(http.StatusBadRequest, err) - } - podSpec = &deployment.Spec.Template.Spec - resourceName = deployment.Name - mutatedObj = &deployment - labels = deployment.Spec.Template.Labels - - case "StatefulSet": - var statefulset appsv1.StatefulSet - if err := w.decoder.Decode(req, &statefulset); err != nil { - authbridgelog.Error(err, "Failed to decode StatefulSet") - return admission.Errored(http.StatusBadRequest, err) - } - podSpec = &statefulset.Spec.Template.Spec - resourceName = statefulset.Name - mutatedObj = &statefulset - labels = statefulset.Spec.Template.Labels - - case "DaemonSet": - var daemonset appsv1.DaemonSet - if err := w.decoder.Decode(req, &daemonset); err != nil { - authbridgelog.Error(err, "Failed to decode DaemonSet") - return admission.Errored(http.StatusBadRequest, err) - } - podSpec = &daemonset.Spec.Template.Spec - resourceName = daemonset.Name - mutatedObj = &daemonset - labels = daemonset.Spec.Template.Labels - - case "Job": - var job batchv1.Job - if err := w.decoder.Decode(req, &job); err != nil { - authbridgelog.Error(err, "Failed to decode Job") - return admission.Errored(http.StatusBadRequest, err) - } - podSpec = &job.Spec.Template.Spec - resourceName = job.Name - mutatedObj = &job - labels = job.Spec.Template.Labels - - case "CronJob": - var cronjob batchv1.CronJob - if err := w.decoder.Decode(req, &cronjob); err != nil { - authbridgelog.Error(err, "Failed to decode CronJob") - return admission.Errored(http.StatusBadRequest, err) - } - podSpec = &cronjob.Spec.JobTemplate.Spec.Template.Spec - resourceName = cronjob.Name - mutatedObj = &cronjob - labels = cronjob.Spec.JobTemplate.Spec.Template.Labels - - default: - authbridgelog.Info("Unsupported resource kind", "kind", req.Kind.Kind) - return admission.Allowed("unsupported kind") + // Only handle Pod resources; allow anything else through unchanged. + // This guards against transient misconfigurations where the + // MutatingWebhookConfiguration still targets non-Pod resources. + if req.Kind.Kind != "Pod" || req.Kind.Group != "" { + authbridgelog.Info("Skipping non-Pod resource", + "kind", req.Kind.Kind, + "group", req.Kind.Group) + return admission.Allowed("not a Pod") + } + + var pod corev1.Pod + if err := w.decoder.Decode(req, &pod); err != nil { + authbridgelog.Error(err, "Failed to decode Pod") + return admission.Errored(http.StatusBadRequest, err) } + // Derive a workload name for ServiceAccount and client-registration naming. + // At Pod CREATE time the Name may be empty (generated by the API server), + // but GenerateName is set by the owning controller (e.g. "myapp-7d4f8b9c5-"). + resourceName := deriveWorkloadName(&pod) + // Check if already injected (idempotency) - if w.isAlreadyInjected(podSpec) { + if w.isAlreadyInjected(&pod.Spec) { authbridgelog.Info("Skipping - sidecars already injected", - "kind", req.Kind.Kind, "namespace", req.Namespace, "name", resourceName) return admission.Allowed("already injected") } - if mutated, err := w.Mutator.InjectAuthBridge(ctx, podSpec, req.Namespace, resourceName, labels); err != nil { + if mutated, err := w.Mutator.InjectAuthBridge(ctx, &pod.Spec, req.Namespace, resourceName, pod.Labels); err != nil { authbridgelog.Error(err, "Failed to mutate pod spec", - "kind", req.Kind.Kind, "namespace", req.Namespace, "name", resourceName) return admission.Errored(http.StatusInternalServerError, err) } else if !mutated { authbridgelog.Info("Skipping mutation (injection not enabled)", - "kind", req.Kind.Kind, "namespace", req.Namespace, "name", resourceName) return admission.Allowed("injection not enabled") } - // Marshal the mutated object - marshaledMutated, err := json.Marshal(mutatedObj) + // Marshal the mutated Pod + marshaledMutated, err := json.Marshal(&pod) if err != nil { - authbridgelog.Error(err, "Failed to marshal mutated resource") + authbridgelog.Error(err, "Failed to marshal mutated Pod") return admission.Errored(http.StatusInternalServerError, err) } - authbridgelog.Info("Successfully mutated resource", - "kind", req.Kind.Kind, + authbridgelog.Info("Successfully mutated Pod", "namespace", req.Namespace, "name", resourceName) return admission.PatchResponseFromRaw(req.Object.Raw, marshaledMutated) } +// deriveWorkloadName extracts a stable workload name from the Pod. +// For controller-managed Pods, GenerateName is set (e.g. "myapp-7d4f8b9c5-") +// and we trim the trailing hyphen. For bare Pods, we use the Pod name directly. +func deriveWorkloadName(pod *corev1.Pod) string { + if pod.GenerateName != "" { + return strings.TrimRight(pod.GenerateName, "-") + } + return pod.Name +} + func (w *AuthBridgeWebhook) isAlreadyInjected(podSpec *corev1.PodSpec) bool { // Check sidecar containers. Any one of these being present means the full // injection cycle already ran for this pod (each Build* call is guarded by @@ -188,4 +147,4 @@ func (w *AuthBridgeWebhook) isAlreadyInjected(podSpec *corev1.PodSpec) bool { return false } -// +kubebuilder:webhook:path=/mutate-workloads-authbridge,mutating=true,failurePolicy=fail,sideEffects=None,groups=apps;batch,resources=deployments;statefulsets;daemonsets;jobs;cronjobs,verbs=create;update,versions=v1,name=inject.kagenti.io,admissionReviewVersions=v1 +// +kubebuilder:webhook:path=/mutate-workloads-authbridge,mutating=true,failurePolicy=fail,sideEffects=None,groups="",resources=pods,verbs=create,versions=v1,name=inject.kagenti.io,admissionReviewVersions=v1,reinvocationPolicy=IfNeeded diff --git a/kagenti-webhook/internal/webhook/v1alpha1/authbridge_webhook_test.go b/kagenti-webhook/internal/webhook/v1alpha1/authbridge_webhook_test.go new file mode 100644 index 000000000..f64f457a3 --- /dev/null +++ b/kagenti-webhook/internal/webhook/v1alpha1/authbridge_webhook_test.go @@ -0,0 +1,166 @@ +/* +Copyright 2025. + +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 v1alpha1 + +import ( + "fmt" + + "github.com/kagenti/kagenti-extensions/kagenti-webhook/internal/webhook/injector" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var testNsCounter int + +var _ = Describe("AuthBridge Pod Webhook", func() { + var testNamespace string + + BeforeEach(func() { + // Create a unique namespace with kagenti-enabled=true for each test + testNsCounter++ + testNamespace = fmt.Sprintf("test-webhook-%d", testNsCounter) + + ns := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: testNamespace, + Labels: map[string]string{ + "kagenti-enabled": "true", + }, + }, + } + err := k8sClient.Create(ctx, ns) + Expect(err).NotTo(HaveOccurred()) + }) + + newTestPod := func(name string, labels map[string]string) *corev1.Pod { + return &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: name + "-", + Namespace: testNamespace, + Labels: labels, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "app", + Image: "busybox:latest", + }, + }, + }, + } + } + + Context("when a Pod has kagenti.io/type=agent and kagenti.io/inject=enabled", func() { + It("should inject sidecars", func() { + pod := newTestPod("agent-pod", map[string]string{ + "kagenti.io/type": "agent", + "kagenti.io/inject": "enabled", + }) + + err := k8sClient.Create(ctx, pod) + Expect(err).NotTo(HaveOccurred()) + + // Verify sidecars were injected + Expect(containerNames(pod.Spec.Containers)).To(ContainElement(injector.EnvoyProxyContainerName)) + Expect(initContainerNames(pod.Spec.InitContainers)).To(ContainElement(injector.ProxyInitContainerName)) + }) + }) + + Context("when a Pod has kagenti.io/type=tool and kagenti.io/inject=enabled", func() { + It("should not inject sidecars (injectTools feature gate is disabled by default)", func() { + pod := newTestPod("tool-pod", map[string]string{ + "kagenti.io/type": "tool", + "kagenti.io/inject": "enabled", + }) + + err := k8sClient.Create(ctx, pod) + Expect(err).NotTo(HaveOccurred()) + + Expect(containerNames(pod.Spec.Containers)).NotTo(ContainElement(injector.EnvoyProxyContainerName)) + Expect(initContainerNames(pod.Spec.InitContainers)).NotTo(ContainElement(injector.ProxyInitContainerName)) + }) + }) + + Context("when a Pod does not have kagenti.io/type label", func() { + It("should not inject sidecars", func() { + pod := newTestPod("no-type-pod", map[string]string{ + "kagenti.io/inject": "enabled", + }) + + err := k8sClient.Create(ctx, pod) + Expect(err).NotTo(HaveOccurred()) + + Expect(containerNames(pod.Spec.Containers)).NotTo(ContainElement(injector.EnvoyProxyContainerName)) + Expect(initContainerNames(pod.Spec.InitContainers)).NotTo(ContainElement(injector.ProxyInitContainerName)) + }) + }) + + Context("when a Pod has kagenti.io/inject=disabled", func() { + It("should not inject sidecars", func() { + pod := newTestPod("disabled-pod", map[string]string{ + "kagenti.io/type": "agent", + "kagenti.io/inject": "disabled", + }) + + err := k8sClient.Create(ctx, pod) + Expect(err).NotTo(HaveOccurred()) + + Expect(containerNames(pod.Spec.Containers)).NotTo(ContainElement(injector.EnvoyProxyContainerName)) + Expect(initContainerNames(pod.Spec.InitContainers)).NotTo(ContainElement(injector.ProxyInitContainerName)) + }) + }) + + Context("when a Pod already has injected containers (idempotency)", func() { + It("should not double-inject", func() { + pod := newTestPod("already-injected-pod", map[string]string{ + "kagenti.io/type": "agent", + "kagenti.io/inject": "enabled", + }) + // Pre-add the envoy-proxy container to simulate prior injection + pod.Spec.Containers = append(pod.Spec.Containers, corev1.Container{ + Name: injector.EnvoyProxyContainerName, + Image: "envoy:test", + }) + + err := k8sClient.Create(ctx, pod) + Expect(err).NotTo(HaveOccurred()) + + // Count envoy-proxy containers — should be exactly 1 (the pre-existing one) + count := 0 + for _, c := range pod.Spec.Containers { + if c.Name == injector.EnvoyProxyContainerName { + count++ + } + } + Expect(count).To(Equal(1)) + }) + }) +}) + +func containerNames(containers []corev1.Container) []string { + names := make([]string, len(containers)) + for i, c := range containers { + names[i] = c.Name + } + return names +} + +func initContainerNames(containers []corev1.Container) []string { + return containerNames(containers) +} diff --git a/kagenti-webhook/internal/webhook/v1alpha1/webhook_suite_test.go b/kagenti-webhook/internal/webhook/v1alpha1/webhook_suite_test.go index e3daf499f..0912501a6 100644 --- a/kagenti-webhook/internal/webhook/v1alpha1/webhook_suite_test.go +++ b/kagenti-webhook/internal/webhook/v1alpha1/webhook_suite_test.go @@ -32,6 +32,7 @@ import ( "github.com/kagenti/kagenti-extensions/kagenti-webhook/internal/webhook/config" "github.com/kagenti/kagenti-extensions/kagenti-webhook/internal/webhook/injector" admissionv1 "k8s.io/api/admission/v1" + corev1 "k8s.io/api/core/v1" apimachineryruntime "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/rest" ctrl "sigs.k8s.io/controller-runtime" @@ -72,6 +73,9 @@ var _ = BeforeSuite(func() { err = admissionv1.AddToScheme(scheme) Expect(err).NotTo(HaveOccurred()) + err = corev1.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + // +kubebuilder:scaffold:scheme By("bootstrapping test environment") diff --git a/kagenti-webhook/scripts/webhook-rollout.sh b/kagenti-webhook/scripts/webhook-rollout.sh index 272a485f3..1c72d8327 100755 --- a/kagenti-webhook/scripts/webhook-rollout.sh +++ b/kagenti-webhook/scripts/webhook-rollout.sh @@ -118,75 +118,35 @@ fi echo "" # Step 1: Build and load image -echo "[1/6] Building image..." +echo "[1/7] Building image..." "${DETECTED}" build -f Dockerfile . --tag "${IMAGE_NAME}" --load echo "" -echo "[2/6] Loading image into kind cluster..." +echo "[2/7] Loading image into kind cluster..." if ! kind load docker-image --name "${CLUSTER}" "${IMAGE_NAME}" 2>/dev/null; then echo "kind load failed, using save workaround..." "${DETECTED}" save "${IMAGE_NAME}" | "${DETECTED}" exec -i "${CLUSTER}-control-plane" ctr --namespace k8s.io images import - fi -# Steps 3-5: Deploy ConfigMaps and update deployment +# Steps 3-4: Deploy ConfigMaps CHART_DIR="${SCRIPT_DIR}/../../charts/kagenti-webhook" echo "" -echo "[3/6] Deploying platform defaults ConfigMap..." +echo "[3/7] Deploying platform defaults ConfigMap..." helm template kagenti-webhook "${CHART_DIR}" \ --set namespaceOverride="${NAMESPACE}" \ --show-only templates/configmap-platform-defaults.yaml | kubectl apply -f - echo "" -echo "[4/6] Deploying feature gates ConfigMap..." +echo "[4/7] Deploying feature gates ConfigMap..." helm template kagenti-webhook "${CHART_DIR}" \ --set namespaceOverride="${NAMESPACE}" \ --show-only templates/configmap-feature-gates.yaml | kubectl apply -f - +# Step 5: Apply webhook configuration BEFORE image rollout to avoid a race +# where the old config sends non-Pod resources to the new Pod-only handler. echo "" -echo "[5/6] Updating deployment (image + config volumes)..." -# Update the container image -kubectl -n "${NAMESPACE}" set image deployment/kagenti-webhook-controller-manager "manager=${IMAGE_NAME}" - -# Idempotently ensure a volume and corresponding volumeMount exist on the deployment. -# JSON-patch op:add to /volumes/- and /volumeMounts/- appends duplicates on re-run, -# so we pre-check by name before patching. -ensure_volume_and_mount() { - local volume_name="$1" - local configmap_name="$2" - local mount_path="$3" - - local existing_volumes - existing_volumes="$(kubectl -n "${NAMESPACE}" get deployment kagenti-webhook-controller-manager \ - -o jsonpath='{.spec.template.spec.volumes[*].name}' 2>/dev/null || true)" - - local existing_mounts - existing_mounts="$(kubectl -n "${NAMESPACE}" get deployment kagenti-webhook-controller-manager \ - -o jsonpath='{.spec.template.spec.containers[0].volumeMounts[*].name}' 2>/dev/null || true)" - - if echo "${existing_volumes}" | tr ' ' '\n' | grep -qx "${volume_name}" || \ - echo "${existing_mounts}" | tr ' ' '\n' | grep -qx "${volume_name}"; then - echo " ${volume_name} volume/volumeMount already present, skipping patch" - return 0 - fi - - kubectl -n "${NAMESPACE}" patch deployment kagenti-webhook-controller-manager --type=json -p="[ - {\"op\": \"add\", \"path\": \"/spec/template/spec/volumes/-\", \"value\": {\"name\": \"${volume_name}\", \"configMap\": {\"name\": \"${configmap_name}\"}}}, - {\"op\": \"add\", \"path\": \"/spec/template/spec/containers/0/volumeMounts/-\", \"value\": {\"name\": \"${volume_name}\", \"mountPath\": \"${mount_path}\", \"readOnly\": true}} - ]" - echo " ${volume_name} volume/volumeMount added" -} - -ensure_volume_and_mount "platform-config" "kagenti-webhook-defaults" "/etc/kagenti" -ensure_volume_and_mount "feature-gates" "kagenti-webhook-feature-gates" "/etc/kagenti/feature-gates" - -echo "" -echo "Waiting for rollout to complete..." -kubectl rollout status -n "${NAMESPACE}" deployment/kagenti-webhook-controller-manager --timeout=120s - -# Step 6: Apply authbridge webhook configuration (always, so updates take effect) -echo "" -echo "[6/6] Applying authbridge webhook configuration..." +echo "[5/7] Applying authbridge webhook configuration..." kubectl apply -f - </dev/null || true)" + + local existing_mounts + existing_mounts="$(kubectl -n "${NAMESPACE}" get deployment kagenti-webhook-controller-manager \ + -o jsonpath='{.spec.template.spec.containers[0].volumeMounts[*].name}' 2>/dev/null || true)" + + if echo "${existing_volumes}" | tr ' ' '\n' | grep -qx "${volume_name}" || \ + echo "${existing_mounts}" | tr ' ' '\n' | grep -qx "${volume_name}"; then + echo " ${volume_name} volume/volumeMount already present, skipping patch" + return 0 + fi + + kubectl -n "${NAMESPACE}" patch deployment kagenti-webhook-controller-manager --type=json -p="[ + {\"op\": \"add\", \"path\": \"/spec/template/spec/volumes/-\", \"value\": {\"name\": \"${volume_name}\", \"configMap\": {\"name\": \"${configmap_name}\"}}}, + {\"op\": \"add\", \"path\": \"/spec/template/spec/containers/0/volumeMounts/-\", \"value\": {\"name\": \"${volume_name}\", \"mountPath\": \"${mount_path}\", \"readOnly\": true}} + ]" + echo " ${volume_name} volume/volumeMount added" +} + +ensure_volume_and_mount "platform-config" "kagenti-webhook-defaults" "/etc/kagenti" +ensure_volume_and_mount "feature-gates" "kagenti-webhook-feature-gates" "/etc/kagenti/feature-gates" + +echo "" +echo "[7/7] Waiting for rollout to complete..." +kubectl rollout status -n "${NAMESPACE}" deployment/kagenti-webhook-controller-manager --timeout=120s echo "" echo "==========================================" From d4578c2dd279002bf051d4833326768f5603a1ed Mon Sep 17 00:00:00 2001 From: cwiklik Date: Mon, 9 Mar 2026 16:40:15 -0400 Subject: [PATCH 2/3] fix: Address review feedback on pod-level webhook - Add comment to deriveWorkloadName clarifying it returns the ReplicaSet name (not Deployment); Deployment-level resolution is Phase 2 (#177) - Handle edge case where both GenerateName and Name are empty (fall back to UID with warning log) - Add table-driven tests for deriveWorkloadName (5 edge cases) - Fix copyright year (2025 -> 2025-2026) - Add nolint directives for standard Ginkgo/Gomega dot imports Assisted-By: Claude (Anthropic AI) Signed-off-by: cwiklik --- .../webhook/v1alpha1/authbridge_webhook.go | 19 +++++++++--- .../v1alpha1/authbridge_webhook_test.go | 30 +++++++++++++++++-- 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/kagenti-webhook/internal/webhook/v1alpha1/authbridge_webhook.go b/kagenti-webhook/internal/webhook/v1alpha1/authbridge_webhook.go index 6d96347ae..06e2d643a 100644 --- a/kagenti-webhook/internal/webhook/v1alpha1/authbridge_webhook.go +++ b/kagenti-webhook/internal/webhook/v1alpha1/authbridge_webhook.go @@ -117,14 +117,25 @@ func (w *AuthBridgeWebhook) Handle(ctx context.Context, req admission.Request) a return admission.PatchResponseFromRaw(req.Object.Raw, marshaledMutated) } -// deriveWorkloadName extracts a stable workload name from the Pod. -// For controller-managed Pods, GenerateName is set (e.g. "myapp-7d4f8b9c5-") -// and we trim the trailing hyphen. For bare Pods, we use the Pod name directly. +// deriveWorkloadName extracts a workload name from the Pod for ServiceAccount +// and client-registration naming. For controller-managed Pods, GenerateName +// is set (e.g. "myapp-7d4f8b9c5-") and we trim the trailing hyphen — this +// yields the ReplicaSet name (e.g. "myapp-7d4f8b9c5"), not the Deployment +// name. Resolving the Deployment name requires a ReplicaSet owner-ref lookup, +// which is planned for Phase 2 (AgentRuntime CR integration, issue #177). +// For bare Pods, we use the Pod name directly. func deriveWorkloadName(pod *corev1.Pod) string { if pod.GenerateName != "" { return strings.TrimRight(pod.GenerateName, "-") } - return pod.Name + if pod.Name != "" { + return pod.Name + } + // Both GenerateName and Name empty — should not happen for valid + // admission requests, but fall back to UID to avoid empty names. + authbridgelog.Info("Warning: Pod has neither Name nor GenerateName, falling back to UID", + "uid", pod.UID) + return string(pod.UID) } func (w *AuthBridgeWebhook) isAlreadyInjected(podSpec *corev1.PodSpec) bool { diff --git a/kagenti-webhook/internal/webhook/v1alpha1/authbridge_webhook_test.go b/kagenti-webhook/internal/webhook/v1alpha1/authbridge_webhook_test.go index f64f457a3..5973db87d 100644 --- a/kagenti-webhook/internal/webhook/v1alpha1/authbridge_webhook_test.go +++ b/kagenti-webhook/internal/webhook/v1alpha1/authbridge_webhook_test.go @@ -1,5 +1,5 @@ /* -Copyright 2025. +Copyright 2025-2026. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -20,8 +20,8 @@ import ( "fmt" "github.com/kagenti/kagenti-extensions/kagenti-webhook/internal/webhook/injector" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" + . "github.com/onsi/ginkgo/v2" //nolint:revive // dot import is standard Ginkgo usage + . "github.com/onsi/gomega" //nolint:revive // dot import is standard Gomega usage corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -153,6 +153,30 @@ var _ = Describe("AuthBridge Pod Webhook", func() { }) }) +var _ = Describe("deriveWorkloadName", func() { + DescribeTable("should extract the correct workload name", + func(generateName, name, expected string) { + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: generateName, + Name: name, + }, + } + Expect(deriveWorkloadName(pod)).To(Equal(expected)) + }, + Entry("Deployment Pod (GenerateName with ReplicaSet hash)", + "myapp-7d4f8b9c5-", "", "myapp-7d4f8b9c5"), + Entry("StatefulSet Pod (GenerateName without hash)", + "myapp-", "", "myapp"), + Entry("Bare Pod with Name only", + "", "my-bare-pod", "my-bare-pod"), + Entry("Pod with both GenerateName and Name (GenerateName wins)", + "myapp-abc12-", "myapp-abc12-xyz", "myapp-abc12"), + Entry("GenerateName with no trailing hyphen", + "myapp", "", "myapp"), + ) +}) + func containerNames(containers []corev1.Container) []string { names := make([]string, len(containers)) for i, c := range containers { From 1f2869ffc88d531dc05efd95514998faa4e75479 Mon Sep 17 00:00:00 2001 From: cwiklik Date: Tue, 10 Mar 2026 09:52:40 -0400 Subject: [PATCH 3/3] fix: Address pdettori review feedback on pod-level webhook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Re-fetch Pods after Create in all tests to assert on server-side mutations rather than in-memory state (false-pass prevention) - Add UID fallback test entry for deriveWorkloadName - Use structured logging (V(0).Info) instead of embedding severity in message string - Document BREAKING CHANGE: namespaces now require kagenti-enabled=true label for injection — add migration step to README - Rollout script labels AuthBridge demo namespace with kagenti-enabled=true Assisted-By: Claude (Anthropic AI) Signed-off-by: cwiklik --- kagenti-webhook/README.md | 12 +++++++ .../webhook/v1alpha1/authbridge_webhook.go | 2 +- .../v1alpha1/authbridge_webhook_test.go | 33 +++++++++++++++---- kagenti-webhook/scripts/webhook-rollout.sh | 1 + 4 files changed, 41 insertions(+), 7 deletions(-) diff --git a/kagenti-webhook/README.md b/kagenti-webhook/README.md index a2badedab..e762546d2 100644 --- a/kagenti-webhook/README.md +++ b/kagenti-webhook/README.md @@ -25,6 +25,18 @@ Previous versions required `kagenti.io/inject: enabled` to trigger sidecar injec **Impact on upgrade:** Existing agent workloads that relied on the *absence* of `kagenti.io/inject: enabled` to avoid injection will now receive sidecars. To preserve the previous behavior, add `kagenti.io/inject: disabled` to those workloads before upgrading. +### BREAKING CHANGE: Namespace opt-in required + +The webhook now requires namespaces to have the `kagenti-enabled: "true"` label to receive sidecar injection. Existing namespaces that previously received injection will **silently stop** unless labeled. + +**Migration step:** Label each namespace where agent workloads run: + +```bash +kubectl label ns kagenti-enabled=true +``` + +The `webhook-rollout.sh` script automatically labels the AuthBridge demo namespace when `AUTHBRIDGE_DEMO=true` is set. + ## Supported Resources The **AuthBridge webhook** intercepts **Pod CREATE** requests in the core API group (`v1`). It injects sidecars into Pods created by any workload controller — Deployments, StatefulSets, DaemonSets, Jobs, and CronJobs all produce Pods that the webhook can mutate. diff --git a/kagenti-webhook/internal/webhook/v1alpha1/authbridge_webhook.go b/kagenti-webhook/internal/webhook/v1alpha1/authbridge_webhook.go index 06e2d643a..69ee8eeea 100644 --- a/kagenti-webhook/internal/webhook/v1alpha1/authbridge_webhook.go +++ b/kagenti-webhook/internal/webhook/v1alpha1/authbridge_webhook.go @@ -133,7 +133,7 @@ func deriveWorkloadName(pod *corev1.Pod) string { } // Both GenerateName and Name empty — should not happen for valid // admission requests, but fall back to UID to avoid empty names. - authbridgelog.Info("Warning: Pod has neither Name nor GenerateName, falling back to UID", + authbridgelog.V(0).Info("Pod has neither Name nor GenerateName, falling back to UID", "uid", pod.UID) return string(pod.UID) } diff --git a/kagenti-webhook/internal/webhook/v1alpha1/authbridge_webhook_test.go b/kagenti-webhook/internal/webhook/v1alpha1/authbridge_webhook_test.go index 5973db87d..230d0b065 100644 --- a/kagenti-webhook/internal/webhook/v1alpha1/authbridge_webhook_test.go +++ b/kagenti-webhook/internal/webhook/v1alpha1/authbridge_webhook_test.go @@ -24,6 +24,8 @@ import ( . "github.com/onsi/gomega" //nolint:revive // dot import is standard Gomega usage corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" ) var testNsCounter int @@ -76,6 +78,10 @@ var _ = Describe("AuthBridge Pod Webhook", func() { err := k8sClient.Create(ctx, pod) Expect(err).NotTo(HaveOccurred()) + // Re-fetch from the API server to get server-side mutations + err = k8sClient.Get(ctx, client.ObjectKeyFromObject(pod), pod) + Expect(err).NotTo(HaveOccurred()) + // Verify sidecars were injected Expect(containerNames(pod.Spec.Containers)).To(ContainElement(injector.EnvoyProxyContainerName)) Expect(initContainerNames(pod.Spec.InitContainers)).To(ContainElement(injector.ProxyInitContainerName)) @@ -92,6 +98,9 @@ var _ = Describe("AuthBridge Pod Webhook", func() { err := k8sClient.Create(ctx, pod) Expect(err).NotTo(HaveOccurred()) + err = k8sClient.Get(ctx, client.ObjectKeyFromObject(pod), pod) + Expect(err).NotTo(HaveOccurred()) + Expect(containerNames(pod.Spec.Containers)).NotTo(ContainElement(injector.EnvoyProxyContainerName)) Expect(initContainerNames(pod.Spec.InitContainers)).NotTo(ContainElement(injector.ProxyInitContainerName)) }) @@ -106,6 +115,9 @@ var _ = Describe("AuthBridge Pod Webhook", func() { err := k8sClient.Create(ctx, pod) Expect(err).NotTo(HaveOccurred()) + err = k8sClient.Get(ctx, client.ObjectKeyFromObject(pod), pod) + Expect(err).NotTo(HaveOccurred()) + Expect(containerNames(pod.Spec.Containers)).NotTo(ContainElement(injector.EnvoyProxyContainerName)) Expect(initContainerNames(pod.Spec.InitContainers)).NotTo(ContainElement(injector.ProxyInitContainerName)) }) @@ -121,6 +133,9 @@ var _ = Describe("AuthBridge Pod Webhook", func() { err := k8sClient.Create(ctx, pod) Expect(err).NotTo(HaveOccurred()) + err = k8sClient.Get(ctx, client.ObjectKeyFromObject(pod), pod) + Expect(err).NotTo(HaveOccurred()) + Expect(containerNames(pod.Spec.Containers)).NotTo(ContainElement(injector.EnvoyProxyContainerName)) Expect(initContainerNames(pod.Spec.InitContainers)).NotTo(ContainElement(injector.ProxyInitContainerName)) }) @@ -141,6 +156,9 @@ var _ = Describe("AuthBridge Pod Webhook", func() { err := k8sClient.Create(ctx, pod) Expect(err).NotTo(HaveOccurred()) + err = k8sClient.Get(ctx, client.ObjectKeyFromObject(pod), pod) + Expect(err).NotTo(HaveOccurred()) + // Count envoy-proxy containers — should be exactly 1 (the pre-existing one) count := 0 for _, c := range pod.Spec.Containers { @@ -155,25 +173,28 @@ var _ = Describe("AuthBridge Pod Webhook", func() { var _ = Describe("deriveWorkloadName", func() { DescribeTable("should extract the correct workload name", - func(generateName, name, expected string) { + func(generateName, name, uid, expected string) { pod := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ GenerateName: generateName, Name: name, + UID: types.UID(uid), }, } Expect(deriveWorkloadName(pod)).To(Equal(expected)) }, Entry("Deployment Pod (GenerateName with ReplicaSet hash)", - "myapp-7d4f8b9c5-", "", "myapp-7d4f8b9c5"), + "myapp-7d4f8b9c5-", "", "", "myapp-7d4f8b9c5"), Entry("StatefulSet Pod (GenerateName without hash)", - "myapp-", "", "myapp"), + "myapp-", "", "", "myapp"), Entry("Bare Pod with Name only", - "", "my-bare-pod", "my-bare-pod"), + "", "my-bare-pod", "", "my-bare-pod"), Entry("Pod with both GenerateName and Name (GenerateName wins)", - "myapp-abc12-", "myapp-abc12-xyz", "myapp-abc12"), + "myapp-abc12-", "myapp-abc12-xyz", "", "myapp-abc12"), Entry("GenerateName with no trailing hyphen", - "myapp", "", "myapp"), + "myapp", "", "", "myapp"), + Entry("Both empty — falls back to UID", + "", "", "abc-123-uid", "abc-123-uid"), ) }) diff --git a/kagenti-webhook/scripts/webhook-rollout.sh b/kagenti-webhook/scripts/webhook-rollout.sh index 1c72d8327..222d14a70 100755 --- a/kagenti-webhook/scripts/webhook-rollout.sh +++ b/kagenti-webhook/scripts/webhook-rollout.sh @@ -268,6 +268,7 @@ if [ "${AUTHBRIDGE_DEMO}" = "true" ]; then echo "[AuthBridge 1/2] Creating namespace ${AUTHBRIDGE_NAMESPACE}..." kubectl create namespace "${AUTHBRIDGE_NAMESPACE}" --dry-run=client -o yaml | kubectl apply -f - kubectl label namespace "${AUTHBRIDGE_NAMESPACE}" istio.io/dataplane-mode=ambient --overwrite + kubectl label namespace "${AUTHBRIDGE_NAMESPACE}" kagenti-enabled=true --overwrite # Apply ConfigMaps (update namespace in-place) # Note: AUTHBRIDGE_NAMESPACE is validated above to be a safe DNS-1123 label,