diff --git a/kagenti-operator/internal/webhook/injector/agentruntime_config.go b/kagenti-operator/internal/webhook/injector/agentruntime_config.go deleted file mode 100644 index 82280b5d..00000000 --- a/kagenti-operator/internal/webhook/injector/agentruntime_config.go +++ /dev/null @@ -1,135 +0,0 @@ -/* -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 injector - -import ( - "context" - "fmt" - "slices" - - agentv1alpha1 "github.com/kagenti/operator/api/v1alpha1" - "k8s.io/apimachinery/pkg/api/meta" - "k8s.io/apimachinery/pkg/runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - logf "sigs.k8s.io/controller-runtime/pkg/log" -) - -var arConfigLog = logf.Log.WithName("agentruntime-config") - -// AgentRuntimeOverrides holds the per-workload overrides extracted from an -// AgentRuntime CR (agent.kagenti.dev/v1alpha1). Nil pointer fields mean -// "no override". -type AgentRuntimeOverrides struct { - // Identity — from .spec.identity.spiffe - SpiffeTrustDomain *string - - // Identity — from .spec.identity.clientRegistration - // Note: These fields are not yet in the typed CRD. They are retained for - // forward compatibility and will always be nil until the CRD is extended. - ClientRegistrationProvider *string - ClientRegistrationRealm *string - AdminCredentialsSecretName *string - AdminCredentialsSecretNamespace *string - - // Identity — from .spec.identity.allowedAudiences - AllowedAudiences []string - - // AuthBridge deployment shape — from .spec.authBridgeMode - // Nil = no per-workload override; the namespace's - // authbridge-runtime-config mode (if set) or the cluster fallback - // applies. - AuthBridgeMode *string - - // mTLS posture — from .spec.mtlsMode - // Nil = no per-workload override; the namespace's - // authbridge-runtime-config mtls.mode (if set) or "disabled" - // applies. - MTLSMode *string -} - -// ReadAgentRuntimeOverrides reads the AgentRuntime CR for a given workload -// using typed access. It lists AgentRuntimes in the namespace and finds the -// one whose spec.targetRef.name matches workloadName. -// Returns (nil, nil) if no matching AgentRuntime CR is found. -func ReadAgentRuntimeOverrides(ctx context.Context, c client.Reader, namespace, workloadName string) (*AgentRuntimeOverrides, error) { - list := &agentv1alpha1.AgentRuntimeList{} - if err := c.List(ctx, list, client.InNamespace(namespace)); err != nil { - // If the AgentRuntime CRD is not installed, there are no CRs to find. - // Treat this the same as "no matching CR" so the webhook skips injection - // gracefully instead of blocking pod creation. - // meta.IsNoMatchError catches real API server responses; - // runtime.IsNotRegisteredError catches scheme-level errors (e.g. fake client). - if meta.IsNoMatchError(err) || runtime.IsNotRegisteredError(err) { - arConfigLog.V(1).Info("AgentRuntime CRD not installed, skipping", - "namespace", namespace) - return nil, nil - } - return nil, fmt.Errorf("listing AgentRuntime CRs in %s: %w", namespace, err) - } - - // Find the AgentRuntime whose spec.targetRef.name matches the workload - for i := range list.Items { - rt := &list.Items[i] - if rt.Spec.TargetRef.Name != workloadName { - continue - } - - arConfigLog.Info("Found matching AgentRuntime CR", - "namespace", namespace, "crName", rt.Name, "targetRef.name", workloadName) - return extractOverrides(rt), nil - } - - arConfigLog.V(1).Info("No AgentRuntime CR targets this workload", - "namespace", namespace, "workloadName", workloadName) - return nil, nil -} - -// extractOverrides reads the overridable fields from a typed AgentRuntime CR. -func extractOverrides(rt *agentv1alpha1.AgentRuntime) *AgentRuntimeOverrides { - overrides := &AgentRuntimeOverrides{} - - // .spec.identity.spiffe.trustDomain - if rt.Spec.Identity != nil && rt.Spec.Identity.SPIFFE != nil && rt.Spec.Identity.SPIFFE.TrustDomain != "" { - td := rt.Spec.Identity.SPIFFE.TrustDomain - overrides.SpiffeTrustDomain = &td - } - - // .spec.identity.allowedAudiences — clone to decouple from CR memory - if rt.Spec.Identity != nil && len(rt.Spec.Identity.AllowedAudiences) > 0 { - overrides.AllowedAudiences = slices.Clone(rt.Spec.Identity.AllowedAudiences) - } - - // .spec.authBridgeMode - if rt.Spec.AuthBridgeMode != "" { - mode := rt.Spec.AuthBridgeMode - overrides.AuthBridgeMode = &mode - } - - // .spec.mtlsMode - if rt.Spec.MTLSMode != "" { - mode := rt.Spec.MTLSMode - overrides.MTLSMode = &mode - } - - arConfigLog.Info("AgentRuntime overrides extracted", - "hasSpiffeTrustDomain", overrides.SpiffeTrustDomain != nil, - "hasClientRegistration", overrides.ClientRegistrationProvider != nil, - "hasAuthBridgeMode", overrides.AuthBridgeMode != nil, - "hasMTLSMode", overrides.MTLSMode != nil) - - return overrides -} diff --git a/kagenti-operator/internal/webhook/injector/agentruntime_config_test.go b/kagenti-operator/internal/webhook/injector/agentruntime_config_test.go deleted file mode 100644 index cfeef1ec..00000000 --- a/kagenti-operator/internal/webhook/injector/agentruntime_config_test.go +++ /dev/null @@ -1,185 +0,0 @@ -/* -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 injector - -import ( - "context" - "testing" - - agentv1alpha1 "github.com/kagenti/operator/api/v1alpha1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "sigs.k8s.io/controller-runtime/pkg/client/fake" -) - -func newAgentRuntimeScheme() *runtime.Scheme { - scheme := runtime.NewScheme() - _ = agentv1alpha1.AddToScheme(scheme) - return scheme -} - -func TestReadAgentRuntimeOverrides_NotFound(t *testing.T) { - scheme := newAgentRuntimeScheme() - fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build() - - overrides, err := ReadAgentRuntimeOverrides(context.Background(), fakeClient, "ns1", "my-agent") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if overrides != nil { - t.Fatalf("expected nil overrides, got %+v", overrides) - } -} - -func TestReadAgentRuntimeOverrides_MatchesByTargetRef(t *testing.T) { - scheme := newAgentRuntimeScheme() - - cr := &agentv1alpha1.AgentRuntime{ - ObjectMeta: metav1.ObjectMeta{ - Name: "my-agent-runtime", - Namespace: "ns1", - }, - Spec: agentv1alpha1.AgentRuntimeSpec{ - Type: agentv1alpha1.RuntimeTypeAgent, - TargetRef: agentv1alpha1.TargetRef{ - APIVersion: "apps/v1", - Kind: "Deployment", - Name: "my-agent", - }, - Identity: &agentv1alpha1.IdentitySpec{ - SPIFFE: &agentv1alpha1.SPIFFEIdentity{ - TrustDomain: "override.local", - }, - }, - }, - } - - fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(cr).Build() - - overrides, err := ReadAgentRuntimeOverrides(context.Background(), fakeClient, "ns1", "my-agent") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if overrides == nil { - t.Fatal("expected non-nil overrides") - } - - // Identity — SPIFFE - if overrides.SpiffeTrustDomain == nil || *overrides.SpiffeTrustDomain != "override.local" { - t.Errorf("SpiffeTrustDomain = %v", overrides.SpiffeTrustDomain) - } - - // ClientRegistration fields are not in the typed CRD yet — should be nil - if overrides.ClientRegistrationProvider != nil { - t.Errorf("expected nil ClientRegistrationProvider, got %v", overrides.ClientRegistrationProvider) - } - if overrides.ClientRegistrationRealm != nil { - t.Errorf("expected nil ClientRegistrationRealm, got %v", overrides.ClientRegistrationRealm) - } -} - -func TestReadAgentRuntimeOverrides_PartialOverrides(t *testing.T) { - scheme := newAgentRuntimeScheme() - - cr := &agentv1alpha1.AgentRuntime{ - ObjectMeta: metav1.ObjectMeta{ - Name: "my-agent-rt", - Namespace: "ns1", - }, - Spec: agentv1alpha1.AgentRuntimeSpec{ - Type: agentv1alpha1.RuntimeTypeAgent, - TargetRef: agentv1alpha1.TargetRef{ - APIVersion: "apps/v1", - Kind: "Deployment", - Name: "my-agent", - }, - Identity: &agentv1alpha1.IdentitySpec{ - SPIFFE: &agentv1alpha1.SPIFFEIdentity{ - TrustDomain: "custom.domain", - }, - }, - }, - } - - fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(cr).Build() - - overrides, err := ReadAgentRuntimeOverrides(context.Background(), fakeClient, "ns1", "my-agent") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if overrides == nil { - t.Fatal("expected non-nil overrides") - } - if overrides.SpiffeTrustDomain == nil || *overrides.SpiffeTrustDomain != "custom.domain" { - t.Errorf("SpiffeTrustDomain = %v", overrides.SpiffeTrustDomain) - } - // Other fields should be nil - if overrides.ClientRegistrationProvider != nil { - t.Errorf("expected nil ClientRegistrationProvider, got %v", overrides.ClientRegistrationProvider) - } -} - -func TestReadAgentRuntimeOverrides_NoTargetRefMatch(t *testing.T) { - scheme := newAgentRuntimeScheme() - - cr := &agentv1alpha1.AgentRuntime{ - ObjectMeta: metav1.ObjectMeta{ - Name: "other-runtime", - Namespace: "ns1", - }, - Spec: agentv1alpha1.AgentRuntimeSpec{ - Type: agentv1alpha1.RuntimeTypeAgent, - TargetRef: agentv1alpha1.TargetRef{ - APIVersion: "apps/v1", - Kind: "Deployment", - Name: "other-agent", - }, - Identity: &agentv1alpha1.IdentitySpec{ - SPIFFE: &agentv1alpha1.SPIFFEIdentity{ - TrustDomain: "should-not-match", - }, - }, - }, - } - - fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(cr).Build() - - overrides, err := ReadAgentRuntimeOverrides(context.Background(), fakeClient, "ns1", "my-agent") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if overrides != nil { - t.Fatalf("expected nil overrides for non-matching targetRef, got %+v", overrides) - } -} - -func TestReadAgentRuntimeOverrides_CRDNotInstalled(t *testing.T) { - // Empty scheme — no AgentRuntime types registered. - // The List call returns a NoKindMatch error, which should be treated as - // "no matching CR" (nil, nil) so the webhook skips injection gracefully - // instead of blocking pod creation. - scheme := runtime.NewScheme() - fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build() - - overrides, err := ReadAgentRuntimeOverrides(context.Background(), fakeClient, "ns1", "my-agent") - if err != nil { - t.Fatalf("expected nil error for missing CRD (graceful skip), got: %v", err) - } - if overrides != nil { - t.Fatalf("expected nil overrides, got %+v", overrides) - } -} diff --git a/kagenti-operator/internal/webhook/injector/constants.go b/kagenti-operator/internal/webhook/injector/constants.go index b740a091..f779f8c0 100644 --- a/kagenti-operator/internal/webhook/injector/constants.go +++ b/kagenti-operator/internal/webhook/injector/constants.go @@ -11,8 +11,7 @@ const ( LabelClientRegistrationInject = "kagenti.io/client-registration-inject" ) -// AuthBridge deployment modes. Selected per workload via AgentRuntime -// CR `Spec.AuthBridgeMode`, falling back to the namespace +// AuthBridge deployment modes. Selected via the namespace // `authbridge-runtime-config` ConfigMap's `mode` field, the deprecated // per-pod annotation, then ModeProxySidecar as the cluster-wide default. const ( @@ -22,12 +21,12 @@ const ( ModeWaypoint = "waypoint" // standalone deployment (not injected) // AnnotationAuthBridgeMode is the legacy per-pod mode selector. The - // canonical surface is now AgentRuntime.Spec.AuthBridgeMode and the - // namespace authbridge-runtime-config ConfigMap; this annotation is - // only honored as a deprecated fallback so existing deployments do - // not silently shape-shift to a different mode on first redeploy. + // canonical surface is the namespace authbridge-runtime-config + // ConfigMap; this annotation is only honored as a deprecated fallback + // so existing deployments do not silently shape-shift to a different + // mode on first redeploy. // - // Deprecated: set Spec.AuthBridgeMode on the AgentRuntime CR. + // Deprecated: set mode in the namespace authbridge-runtime-config ConfigMap. AnnotationAuthBridgeMode = "kagenti.io/authbridge-mode" // Container name for proxy-sidecar mode @@ -57,12 +56,11 @@ const ( ProxyInitModeEnforceRedirect ProxyInitMode = "enforce-redirect" ) -// mTLS modes for the proxy-sidecar / lite paths. Selected per workload -// via AgentRuntime CR `Spec.MTLSMode`, falling back to the namespace -// `authbridge-runtime-config` ConfigMap's `mtls.mode` field, then -// MTLSModeDisabled. envoy-sidecar mode is incompatible with mTLS today -// (Envoy SDS not configured by the kagenti envoy-config) — admission -// rejects mtlsMode != disabled in that combination. +// mTLS modes for the proxy-sidecar / lite paths. Selected via the +// namespace `authbridge-runtime-config` ConfigMap's `mtls.mode` field, +// then MTLSModeDisabled. envoy-sidecar mode is incompatible with mTLS +// today (Envoy SDS not configured by the kagenti envoy-config) — +// admission rejects mtlsMode != disabled in that combination. const ( MTLSModeDisabled = "disabled" MTLSModePermissive = "permissive" diff --git a/kagenti-operator/internal/webhook/injector/container_builder.go b/kagenti-operator/internal/webhook/injector/container_builder.go index 850c26fa..f4fb646a 100644 --- a/kagenti-operator/internal/webhook/injector/container_builder.go +++ b/kagenti-operator/internal/webhook/injector/container_builder.go @@ -62,7 +62,7 @@ func NewContainerBuilder(cfg *config.PlatformConfig) *ContainerBuilder { // env var values from the resolved config (admission-time resolution). func NewResolvedContainerBuilder(resolved *ResolvedConfig) *ContainerBuilder { if resolved == nil { - resolved = ResolveConfig(nil, nil, nil) + resolved = ResolveConfig(nil, nil) } return &ContainerBuilder{ cfg: resolved.Platform, diff --git a/kagenti-operator/internal/webhook/injector/pod_mutator.go b/kagenti-operator/internal/webhook/injector/pod_mutator.go index 53ff0bab..69e5192e 100644 --- a/kagenti-operator/internal/webhook/injector/pod_mutator.go +++ b/kagenti-operator/internal/webhook/injector/pod_mutator.go @@ -19,7 +19,6 @@ package injector import ( "context" "fmt" - "slices" "github.com/kagenti/operator/internal/webhook/config" appsv1 "k8s.io/api/apps/v1" @@ -163,21 +162,6 @@ func (m *PodMutator) InjectAuthBridge(ctx context.Context, podSpec *corev1.PodSp return false, nil } - // Read AgentRuntime CR overrides. If no CR exists the webhook still - // injects sidecars using defaults-only config (platform + namespace - // defaults, no per-workload overrides). ResolveConfig handles nil - // overrides transparently. - arOverrides, err := ReadAgentRuntimeOverrides(ctx, m.Client, namespace, crName) - if err != nil { - mutatorLog.Error(err, "failed to read AgentRuntime", - "namespace", namespace, "crName", crName) - return false, err - } - if arOverrides == nil { - mutatorLog.Info("No AgentRuntime CR found, injecting with defaults-only config", - "namespace", namespace, "crName", crName) - } - // Derive SPIRE mode from the injection decision: if spiffe-helper is being // injected then SPIRE volumes and a dedicated ServiceAccount are needed. spireEnabled := decision.SpiffeHelper.Inject @@ -230,31 +214,19 @@ func (m *PodMutator) InjectAuthBridge(ctx context.Context, podSpec *corev1.PodSp } // ======================================== - // Resolve mTLS posture (CR > namespace > "disabled") + // Resolve mTLS posture (namespace > "disabled") // ======================================== // // Done BEFORE the volume-building / per-workload resolution so that // "mtlsMode != disabled implies SPIRE" can flip spireEnabled and // fall through to the existing SPIRE-aware code paths (volumes, - // ServiceAccount, container env). Mode-compat validation - // (mtlsMode incompatible with envoy-sidecar) runs in the - // AgentRuntime validating webhook upstream of pod admission. - // Resolution chain: CR > namespace > "disabled". An explicit - // CR value (including "disabled") pins; the namespace fallback - // only fires when the CR doesn't set the field at all. - // arOverrides.MTLSMode is the sentinel — extractOverrides only - // populates it when Spec.MTLSMode is non-empty. + // ServiceAccount, container env). + // Resolution chain: namespace ConfigMap > "disabled". mtlsMode := "" mtlsSource := "" - if arOverrides != nil && arOverrides.MTLSMode != nil { - mtlsMode = *arOverrides.MTLSMode - mtlsSource = "agentruntime-cr" - } - if mtlsMode == "" { - if m := ExtractMTLSMode(nsConfig.AuthBridgeRuntimeYAML); m != "" { - mtlsMode = m - mtlsSource = "namespace-configmap" - } + if m := ExtractMTLSMode(nsConfig.AuthBridgeRuntimeYAML); m != "" { + mtlsMode = m + mtlsSource = "namespace-configmap" } if mtlsMode == "" { mtlsMode = MTLSModeDisabled @@ -301,24 +273,11 @@ func (m *PodMutator) InjectAuthBridge(ctx context.Context, podSpec *corev1.PodSp } } - // ======================================== - // Resolve AllowedAudiences (from AgentRuntime CR) - // ======================================== - var allowedAudiences []string - if arOverrides != nil { - allowedAudiences = slices.Clone(arOverrides.AllowedAudiences) - } - if currentGates.PerWorkloadConfigResolution { // Resolved path: build literal env vars from namespace config - // arOverrides was already read above as a gate check. - resolved := ResolveConfig(currentConfig, nsConfig, arOverrides) + resolved := ResolveConfig(currentConfig, nsConfig) builder = NewResolvedContainerBuilder(resolved) requiredVolumes = BuildResolvedVolumes(spireEnabled, "") - - mutatorLog.Info("Using resolved config path", - "namespace", namespace, "crName", crName, - "hasAgentRuntimeOverrides", arOverrides != nil) } else { // Legacy path: ValueFrom refs, kubelet resolves at runtime builder = NewContainerBuilder(currentConfig) @@ -341,27 +300,20 @@ func (m *PodMutator) InjectAuthBridge(ctx context.Context, podSpec *corev1.PodSp // waypoint — standalone deployment, not injected as sidecar // // Resolution chain (first non-empty wins): - // 1. AgentRuntime CR `Spec.AuthBridgeMode` (per-workload override) - // 2. namespace authbridge-runtime-config `mode:` field (namespace default) - // 3. kagenti.io/authbridge-mode annotation (deprecated) - // 4. ModeProxySidecar (cluster-wide fallback) + // 1. namespace authbridge-runtime-config `mode:` field (namespace default) + // 2. kagenti.io/authbridge-mode annotation (deprecated) + // 3. ModeProxySidecar (cluster-wide fallback) authBridgeMode := "" modeSource := "" - if arOverrides != nil && arOverrides.AuthBridgeMode != nil { - authBridgeMode = *arOverrides.AuthBridgeMode - modeSource = "agentruntime-cr" - } - if authBridgeMode == "" { - if m := ExtractMode(nsConfig.AuthBridgeRuntimeYAML); m != "" { - authBridgeMode = m - modeSource = "namespace-configmap" - } + if m := ExtractMode(nsConfig.AuthBridgeRuntimeYAML); m != "" { + authBridgeMode = m + modeSource = "namespace-configmap" } if authBridgeMode == "" { if m := annotations[AnnotationAuthBridgeMode]; m != "" { authBridgeMode = m modeSource = "annotation-deprecated" - mutatorLog.Info("DEPRECATED: kagenti.io/authbridge-mode annotation used; set AgentRuntime.Spec.AuthBridgeMode instead", + mutatorLog.Info("DEPRECATED: kagenti.io/authbridge-mode annotation used; set mode in namespace authbridge-runtime-config ConfigMap instead", "namespace", namespace, "crName", crName, "mode", authBridgeMode) } } @@ -499,7 +451,7 @@ func (m *PodMutator) InjectAuthBridge(ctx context.Context, podSpec *corev1.PodSp "reverse_proxy_backend": fmt.Sprintf("http://127.0.0.1:%d", newAgentPort), "forward_proxy_addr": fmt.Sprintf(":%d", forwardProxyPort), }, - mtlsMode, allowedAudiences) + mtlsMode) if err != nil { return false, fmt.Errorf("proxy-sidecar per-agent ConfigMap: %w", err) } @@ -593,7 +545,7 @@ func (m *PodMutator) InjectAuthBridge(ctx context.Context, podSpec *corev1.PodSp // inbound listener (gated on MTLSEnabled) and UpstreamTlsContext on // original_destination_tls (strict only). perAgentCMName, err := m.ensurePerAgentConfigMap(ctx, namespace, crName, - ModeEnvoySidecar, nsConfig.AuthBridgeRuntimeYAML, nsConfig, nil, mtlsMode, allowedAudiences) + ModeEnvoySidecar, nsConfig.AuthBridgeRuntimeYAML, nsConfig, nil, mtlsMode) if err != nil { return false, fmt.Errorf("envoy-sidecar per-agent ConfigMap: %w", err) } @@ -607,7 +559,7 @@ func (m *PodMutator) InjectAuthBridge(ctx context.Context, podSpec *corev1.PodSp // ResolveConfig is cheap/idempotent; we clear EnvoyYAML so // RenderEnvoyConfig uses the template path (with mtls TLS // blocks) instead of short-circuiting on the namespace CM. - resolvedForEnvoy := ResolveConfig(currentConfig, nsConfig, arOverrides) + resolvedForEnvoy := ResolveConfig(currentConfig, nsConfig) resolvedForEnvoy.EnvoyYAML = "" envoyCMName, err := m.ensurePerAgentEnvoyConfigMap(ctx, namespace, crName, resolvedForEnvoy) if err != nil { @@ -796,38 +748,6 @@ func synthesizePipeline(nsConfig *NamespaceConfig) map[string]interface{} { } } -// injectAllowedAudiences walks into cfg["pipeline"]["inbound"]["plugins"] and sets -// allowed_audiences on the jwt-validation plugin's config block. If the pipeline -// structure does not contain a jwt-validation plugin, a warning is logged and the -// setting is silently dropped. -func injectAllowedAudiences(cfg map[string]interface{}, audiences []string) { - pipeline, _ := cfg["pipeline"].(map[string]interface{}) - if pipeline == nil { - mutatorLog.Info("WARN: allowedAudiences set on AgentRuntime CR but no pipeline section in config; setting has no effect") - return - } - inbound, _ := pipeline["inbound"].(map[string]interface{}) - if inbound == nil { - mutatorLog.Info("WARN: allowedAudiences set on AgentRuntime CR but no inbound pipeline; setting has no effect") - return - } - plugins, _ := inbound["plugins"].([]interface{}) - for _, p := range plugins { - pm, _ := p.(map[string]interface{}) - if pm == nil || pm["name"] != "jwt-validation" { - continue - } - pluginCfg, _ := pm["config"].(map[string]interface{}) - if pluginCfg == nil { - pluginCfg = map[string]interface{}{} - pm["config"] = pluginCfg - } - pluginCfg["allowed_audiences"] = audiences - return - } - mutatorLog.Info("WARN: allowedAudiences set on AgentRuntime CR but jwt-validation plugin not in inbound pipeline; setting has no effect") -} - // ensurePerAgentConfigMap creates or updates a per-agent ConfigMap that merges the // namespace-level authbridge-runtime-config with per-agent overrides (mode, listener // addresses, mtls). The authbridge sidecar mounts this instead of the shared ConfigMap. @@ -848,7 +768,6 @@ func (m *PodMutator) ensurePerAgentConfigMap( nsConfig *NamespaceConfig, listenerOverrides map[string]string, mtlsMode string, - allowedAudiences []string, ) (string, error) { cmName := perAgentConfigMapName(crName) @@ -883,13 +802,6 @@ func (m *PodMutator) ensurePerAgentConfigMap( cfg["pipeline"] = synthesizePipeline(nsConfig) } - // Inject per-agent AllowedAudiences into the jwt-validation plugin config. - // This overrides any namespace-level audience setting with the per-agent value - // from the AgentRuntime CR's .spec.identity.allowedAudiences. - if len(allowedAudiences) > 0 { - injectAllowedAudiences(cfg, allowedAudiences) - } - // Override mode cfg["mode"] = mode diff --git a/kagenti-operator/internal/webhook/injector/pod_mutator_test.go b/kagenti-operator/internal/webhook/injector/pod_mutator_test.go index 8abd0c4d..789f0a4d 100644 --- a/kagenti-operator/internal/webhook/injector/pod_mutator_test.go +++ b/kagenti-operator/internal/webhook/injector/pod_mutator_test.go @@ -20,7 +20,6 @@ import ( "context" "testing" - agentv1alpha1 "github.com/kagenti/operator/api/v1alpha1" "github.com/kagenti/operator/internal/webhook/config" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" @@ -32,39 +31,10 @@ import ( sigsyaml "sigs.k8s.io/yaml" ) -// newAgentRuntime creates a minimal AgentRuntime CR targeting the given workload name. -func newAgentRuntime(namespace, targetName string) *agentv1alpha1.AgentRuntime { - return &agentv1alpha1.AgentRuntime{ - ObjectMeta: metav1.ObjectMeta{ - Name: targetName + "-runtime", - Namespace: namespace, - }, - Spec: agentv1alpha1.AgentRuntimeSpec{ - Type: agentv1alpha1.RuntimeTypeAgent, - TargetRef: agentv1alpha1.TargetRef{ - APIVersion: "apps/v1", - Kind: "Deployment", - Name: targetName, - }, - }, - } -} - -// newAgentRuntimeWithMode is the same as newAgentRuntime but pins the -// per-workload AuthBridgeMode (proxy-sidecar / envoy-sidecar / waypoint). -// Used by tests that exercise mode-specific code paths now that mode -// selection comes from the CR rather than a pod annotation. -func newAgentRuntimeWithMode(namespace, targetName, mode string) *agentv1alpha1.AgentRuntime { - rt := newAgentRuntime(namespace, targetName) - rt.Spec.AuthBridgeMode = mode - return rt -} - func newTestMutator(objs ...client.Object) *PodMutator { scheme := runtime.NewScheme() _ = corev1.AddToScheme(scheme) _ = appsv1.AddToScheme(scheme) - _ = agentv1alpha1.AddToScheme(scheme) fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(objs...).Build() return &PodMutator{ Client: fakeClient, @@ -182,7 +152,7 @@ func TestInjectAuthBridge_NoAgentRuntime_InjectsWithDefaults(t *testing.T) { func TestInjectAuthBridge_SetsServiceAccountName(t *testing.T) { // Opt-out model: agent workloads are injected by default (no inject label needed). - m := newTestMutator(newAgentRuntime("test-ns", "my-agent")) + m := newTestMutator() ctx := context.Background() podSpec := &corev1.PodSpec{} @@ -208,7 +178,7 @@ func TestInjectAuthBridge_SetsServiceAccountName(t *testing.T) { } func TestInjectAuthBridge_RespectsExistingServiceAccountName(t *testing.T) { - m := newTestMutator(newAgentRuntime("test-ns", "my-agent")) + m := newTestMutator() ctx := context.Background() podSpec := &corev1.PodSpec{ @@ -233,7 +203,7 @@ func TestInjectAuthBridge_RespectsExistingServiceAccountName(t *testing.T) { func TestInjectAuthBridge_NoSACreationWhenSpiffeHelperDisabled(t *testing.T) { // Spiffe-helper is injected by default for agents. SA creation is skipped // when spiffe-helper is explicitly opted out via its per-sidecar label. - m := newTestMutator(newAgentRuntime("test-ns", "my-agent")) + m := newTestMutator() ctx := context.Background() podSpec := &corev1.PodSpec{} @@ -334,7 +304,7 @@ func TestInjectAuthBridge_Tool_SkippedByGateRegardlessOfOptOut(t *testing.T) { } func TestInjectAuthBridge_DefaultSAOverridden(t *testing.T) { - m := newTestMutator(newAgentRuntime("test-ns", "my-agent")) + m := newTestMutator() ctx := context.Background() podSpec := &corev1.PodSpec{ @@ -358,7 +328,7 @@ func TestInjectAuthBridge_DefaultSAOverridden(t *testing.T) { func TestInjectAuthBridge_OutboundPortsExcludeAnnotation(t *testing.T) { // proxy-init is only injected in envoy-sidecar mode. - m := newTestMutator(newAgentRuntimeWithMode("test-ns", "my-agent", ModeEnvoySidecar)) + m := newTestMutator(authbridgeRuntimeConfigMap("test-ns", ModeEnvoySidecar)) ctx := context.Background() podSpec := &corev1.PodSpec{} @@ -396,7 +366,7 @@ func TestInjectAuthBridge_OutboundPortsExcludeAnnotation(t *testing.T) { func TestInjectAuthBridge_InboundPortsExcludeAnnotation(t *testing.T) { // proxy-init is only injected in envoy-sidecar mode. - m := newTestMutator(newAgentRuntimeWithMode("test-ns", "my-agent", ModeEnvoySidecar)) + m := newTestMutator(authbridgeRuntimeConfigMap("test-ns", ModeEnvoySidecar)) ctx := context.Background() podSpec := &corev1.PodSpec{} @@ -448,7 +418,7 @@ func TestInjectAuthBridge_InboundPortsExcludeAnnotation(t *testing.T) { func TestInjectAuthBridge_NilAnnotations(t *testing.T) { // proxy-init is only injected in envoy-sidecar mode. - m := newTestMutator(newAgentRuntimeWithMode("test-ns", "my-agent", ModeEnvoySidecar)) + m := newTestMutator(authbridgeRuntimeConfigMap("test-ns", ModeEnvoySidecar)) ctx := context.Background() podSpec := &corev1.PodSpec{} @@ -501,18 +471,13 @@ func authbridgeRuntimeConfigMap(namespace, mode string) *corev1.ConfigMap { } // Mode resolution chain (first non-empty wins): -// 1. AgentRuntime CR Spec.AuthBridgeMode -// 2. namespace authbridge-runtime-config mode field -// 3. kagenti.io/authbridge-mode annotation (deprecated) -// 4. ModeProxySidecar (cluster default) -// -// Layer 1 is exercised by the existing WaypointMode / ProxySidecarMode -// tests via newAgentRuntimeWithMode. The tests below cover layers 2-4. +// 1. namespace authbridge-runtime-config mode field +// 2. kagenti.io/authbridge-mode annotation (deprecated) +// 3. ModeProxySidecar (cluster default) func TestInjectAuthBridge_ModeResolution_NamespaceConfigMap(t *testing.T) { - // AgentRuntime CR has no mode set; namespace ConfigMap pins envoy-sidecar. + // Namespace ConfigMap pins envoy-sidecar. m := newTestMutator( - newAgentRuntime("team1", "my-agent"), authbridgeRuntimeConfigMap("team1", ModeEnvoySidecar), ) ctx := context.Background() @@ -542,10 +507,10 @@ func TestInjectAuthBridge_ModeResolution_NamespaceConfigMap(t *testing.T) { } } -func TestInjectAuthBridge_ModeResolution_CRBeatsNamespaceConfigMap(t *testing.T) { - // CR pins proxy-sidecar; namespace ConfigMap says envoy-sidecar. CR wins. +func TestInjectAuthBridge_ModeResolution_NamespaceConfigMapWinsOverCR(t *testing.T) { + // With AgentRuntime overrides removed, the namespace ConfigMap is + // the highest-priority mode source. Verify envoy-sidecar is selected. m := newTestMutator( - newAgentRuntimeWithMode("team1", "my-agent", ModeProxySidecar), authbridgeRuntimeConfigMap("team1", ModeEnvoySidecar), ) ctx := context.Background() @@ -564,17 +529,17 @@ func TestInjectAuthBridge_ModeResolution_CRBeatsNamespaceConfigMap(t *testing.T) t.Fatal("expected mutation") } - if !containerExists(podSpec.Containers, AuthBridgeProxyContainerName) { - t.Errorf("expected %s container (CR field beats namespace ConfigMap)", AuthBridgeProxyContainerName) + if !containerExists(podSpec.Containers, EnvoyProxyContainerName) { + t.Errorf("expected %s container (namespace ConfigMap wins)", EnvoyProxyContainerName) } - if containerExists(podSpec.Containers, EnvoyProxyContainerName) { - t.Error("unexpected envoy-proxy container — CR pin should override namespace ConfigMap") + if containerExists(podSpec.Containers, AuthBridgeProxyContainerName) { + t.Error("unexpected authbridge-proxy container — namespace ConfigMap selected envoy-sidecar") } } func TestInjectAuthBridge_ModeResolution_DeprecatedAnnotation(t *testing.T) { - // Neither CR nor namespace ConfigMap set; deprecated annotation pins envoy-sidecar. - m := newTestMutator(newAgentRuntime("team1", "my-agent")) + // No namespace ConfigMap; deprecated annotation pins envoy-sidecar. + m := newTestMutator() ctx := context.Background() podSpec := &corev1.PodSpec{ @@ -596,9 +561,10 @@ func TestInjectAuthBridge_ModeResolution_DeprecatedAnnotation(t *testing.T) { } } -func TestInjectAuthBridge_ModeResolution_CRBeatsAnnotation(t *testing.T) { - // CR pins proxy-sidecar; annotation says envoy-sidecar. CR wins. - m := newTestMutator(newAgentRuntimeWithMode("team1", "my-agent", ModeProxySidecar)) +func TestInjectAuthBridge_ModeResolution_AnnotationWinsOverCR(t *testing.T) { + // With AgentRuntime overrides removed, the annotation is a valid + // mode source. Verify envoy-sidecar is selected from the annotation. + m := newTestMutator() ctx := context.Background() podSpec := &corev1.PodSpec{ @@ -616,17 +582,17 @@ func TestInjectAuthBridge_ModeResolution_CRBeatsAnnotation(t *testing.T) { t.Fatal("expected mutation") } - if !containerExists(podSpec.Containers, AuthBridgeProxyContainerName) { - t.Errorf("expected %s container (CR field beats annotation)", AuthBridgeProxyContainerName) + if !containerExists(podSpec.Containers, EnvoyProxyContainerName) { + t.Errorf("expected %s container (annotation wins)", EnvoyProxyContainerName) } - if containerExists(podSpec.Containers, EnvoyProxyContainerName) { - t.Error("unexpected envoy-proxy container — CR pin should override annotation") + if containerExists(podSpec.Containers, AuthBridgeProxyContainerName) { + t.Error("unexpected authbridge-proxy container — annotation selected envoy-sidecar") } } func TestInjectAuthBridge_ModeResolution_ClusterDefault(t *testing.T) { - // No CR, no namespace ConfigMap, no annotation — expect proxy-sidecar default. - m := newTestMutator(newAgentRuntime("team1", "my-agent")) + // No namespace ConfigMap, no annotation — expect proxy-sidecar default. + m := newTestMutator() ctx := context.Background() podSpec := &corev1.PodSpec{ @@ -653,7 +619,7 @@ func TestInjectAuthBridge_ModeResolution_ClusterDefault(t *testing.T) { func TestInjectAuthBridge_LiteMode_UsesAuthBridgeLiteImage(t *testing.T) { // Lite mode is structurally proxy-sidecar but uses Images.AuthBridgeLite. - m := newTestMutator(newAgentRuntimeWithMode("team1", "my-agent", ModeLite)) + m := newTestMutator(authbridgeRuntimeConfigMap("team1", ModeLite)) ctx := context.Background() podSpec := &corev1.PodSpec{ @@ -693,9 +659,8 @@ func TestInjectAuthBridge_LiteMode_UsesAuthBridgeLiteImage(t *testing.T) { } func TestInjectAuthBridge_LiteMode_FromNamespaceConfigMap(t *testing.T) { - // Namespace ConfigMap pins lite; CR has no override. + // Namespace ConfigMap pins lite. m := newTestMutator( - newAgentRuntime("team1", "my-agent"), authbridgeRuntimeConfigMap("team1", ModeLite), ) ctx := context.Background() @@ -728,7 +693,6 @@ func TestInjectAuthBridge_ModeResolution_UnrecognizedFallsBackToProxySidecar(t * // resolution chain validates the resolved value and falls back to // proxy-sidecar with a WARN log. m := newTestMutator( - newAgentRuntime("team1", "my-agent"), authbridgeRuntimeConfigMap("team1", "proxy-sidecart"), ) ctx := context.Background() @@ -757,7 +721,7 @@ func TestInjectAuthBridge_ModeResolution_UnrecognizedFallsBackToProxySidecar(t * } func TestInjectAuthBridge_WaypointMode_SkipsInjection(t *testing.T) { - m := newTestMutator(newAgentRuntimeWithMode("team1", "my-agent", ModeWaypoint)) + m := newTestMutator(authbridgeRuntimeConfigMap("team1", ModeWaypoint)) ctx := context.Background() podSpec := &corev1.PodSpec{ @@ -803,7 +767,7 @@ func TestInjectAuthBridge_ProxySidecar_EgressEnforcement(t *testing.T) { } t.Run("always injects proxy-init in enforce-redirect mode", func(t *testing.T) { - m := newTestMutator(newAgentRuntimeWithMode("team1", "my-agent", ModeProxySidecar)) + m := newTestMutator() spec := makePod() if _, err := m.InjectAuthBridge(ctx, spec, "team1", "my-agent", labels, nil); err != nil { t.Fatalf("unexpected error: %v", err) @@ -830,7 +794,7 @@ func TestInjectAuthBridge_ProxySidecar_EgressEnforcement(t *testing.T) { }) t.Run("does not duplicate an existing proxy-init", func(t *testing.T) { - m := newTestMutator(newAgentRuntimeWithMode("team1", "my-agent", ModeProxySidecar)) + m := newTestMutator() spec := makePod() spec.InitContainers = []corev1.Container{{Name: ProxyInitContainerName, Image: "preexisting"}} if _, err := m.InjectAuthBridge(ctx, spec, "team1", "my-agent", labels, nil); err != nil { @@ -849,7 +813,7 @@ func TestInjectAuthBridge_ProxySidecar_EgressEnforcement(t *testing.T) { } func TestInjectAuthBridge_ProxySidecarMode_InjectsCorrectly(t *testing.T) { - m := newTestMutator(newAgentRuntimeWithMode("team1", "my-agent", ModeProxySidecar)) + m := newTestMutator() ctx := context.Background() podSpec := &corev1.PodSpec{ @@ -935,7 +899,7 @@ func TestInjectAuthBridge_ProxySidecarMode_MountsKeycloakCredentials(t *testing. // Regression: the proxy-sidecar branch used to return before reaching // ApplyKeycloakClientCredentialsSecretVolumes. That left authbridge-proxy polling // /shared/client-id.txt forever and returning 503 "identity not yet configured". - m := newTestMutator(newAgentRuntimeWithMode("team1", "my-agent", ModeProxySidecar)) + m := newTestMutator() ctx := context.Background() podSpec := &corev1.PodSpec{ @@ -1038,7 +1002,7 @@ func TestInjectHTTPProxyEnv_DoesNotDuplicate(t *testing.T) { } func TestInjectAuthBridge_ProxySidecarMode_PortCollision(t *testing.T) { - m := newTestMutator(newAgentRuntimeWithMode("team1", "my-agent", ModeProxySidecar)) + m := newTestMutator() ctx := context.Background() // Agent uses ports 8000 and 8001 — agent should move to 8002, not 8001 @@ -1096,7 +1060,7 @@ func TestInjectAuthBridge_ProxySidecarMode_PortCollision(t *testing.T) { } func TestInjectAuthBridge_ProxySidecarMode_ForwardProxyCollision(t *testing.T) { - m := newTestMutator(newAgentRuntimeWithMode("team1", "my-agent", ModeProxySidecar)) + m := newTestMutator() ctx := context.Background() // Agent uses port 8081 — forward proxy should use 8082 instead of default 8081 @@ -1214,7 +1178,7 @@ func TestSetOrAddEnv_AddsNew(t *testing.T) { } func TestInjectAuthBridge_ProxySidecarMode_NoPorts_UsesDefault(t *testing.T) { - m := newTestMutator(newAgentRuntimeWithMode("team1", "my-agent", ModeProxySidecar)) + m := newTestMutator() ctx := context.Background() // Agent container with no ports — should use default 8000 @@ -1313,7 +1277,7 @@ func TestEnsurePerAgentConfigMap_EmptyBaseYAML_FallbackFromNsConfig(t *testing.T } cmName, err := m.ensurePerAgentConfigMap(ctx, "team1", "weather-service", - ModeProxySidecar, "", nsConfig, nil, "", nil) + ModeProxySidecar, "", nsConfig, nil, "") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -1430,7 +1394,7 @@ pipeline: ` cmName, err := m.ensurePerAgentConfigMap(ctx, "team1", "my-agent", - ModeEnvoySidecar, baseYAML, &NamespaceConfig{}, nil, "", nil) + ModeEnvoySidecar, baseYAML, &NamespaceConfig{}, nil, "") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -1489,7 +1453,7 @@ pipeline: } cmName, err := m.ensurePerAgentConfigMap(ctx, "team1", "my-agent", - ModeProxySidecar, baseYAML, &NamespaceConfig{}, overrides, "", nil) + ModeProxySidecar, baseYAML, &NamespaceConfig{}, overrides, "") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -1525,7 +1489,7 @@ func TestEnsurePerAgentConfigMap_ExistingCM_OwnedByWebhook_Updated(t *testing.T) ctx := context.Background() _, err := m.ensurePerAgentConfigMap(ctx, "team1", "my-agent", - ModeEnvoySidecar, "", &NamespaceConfig{ClientAuthType: "client-secret"}, nil, "", nil) + ModeEnvoySidecar, "", &NamespaceConfig{ClientAuthType: "client-secret"}, nil, "") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -1553,7 +1517,7 @@ func TestEnsurePerAgentConfigMap_ExistingCM_OverwrittenBySSA(t *testing.T) { ctx := context.Background() cmName, err := m.ensurePerAgentConfigMap(ctx, "team1", "my-agent", - ModeEnvoySidecar, "", &NamespaceConfig{ClientAuthType: "client-secret"}, nil, "", nil) + ModeEnvoySidecar, "", &NamespaceConfig{ClientAuthType: "client-secret"}, nil, "") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -1581,7 +1545,7 @@ func TestEnsurePerAgentConfigMap_OwnerReference_SetFromDeployment(t *testing.T) ctx := context.Background() cmName, err := m.ensurePerAgentConfigMap(ctx, "team1", "weather-service", - ModeEnvoySidecar, "", &NamespaceConfig{ClientAuthType: "client-secret"}, nil, "", nil) + ModeEnvoySidecar, "", &NamespaceConfig{ClientAuthType: "client-secret"}, nil, "") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -1608,7 +1572,7 @@ func TestEnsurePerAgentConfigMap_OwnerReference_SetFromStatefulSet(t *testing.T) ctx := context.Background() cmName, err := m.ensurePerAgentConfigMap(ctx, "team1", "my-stateful-agent", - ModeEnvoySidecar, "", &NamespaceConfig{ClientAuthType: "client-secret"}, nil, "", nil) + ModeEnvoySidecar, "", &NamespaceConfig{ClientAuthType: "client-secret"}, nil, "") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -1629,7 +1593,7 @@ func TestEnsurePerAgentConfigMap_OwnerReference_NoWorkload_Skipped(t *testing.T) ctx := context.Background() cmName, err := m.ensurePerAgentConfigMap(ctx, "team1", "bare-pod-agent", - ModeEnvoySidecar, "", &NamespaceConfig{ClientAuthType: "client-secret"}, nil, "", nil) + ModeEnvoySidecar, "", &NamespaceConfig{ClientAuthType: "client-secret"}, nil, "") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -1652,7 +1616,7 @@ func TestEnsurePerAgentConfigMap_FederatedJWT_MapsToSpiffe(t *testing.T) { } cmName, err := m.ensurePerAgentConfigMap(ctx, "team1", "spiffe-agent", - ModeEnvoySidecar, "", nsConfig, nil, "", nil) + ModeEnvoySidecar, "", nsConfig, nil, "") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -1694,7 +1658,7 @@ func TestEnsurePerAgentConfigMap_MTLSStrict_RendersBlock(t *testing.T) { ctx := context.Background() cmName, err := m.ensurePerAgentConfigMap(ctx, "team1", "mtls-agent", - ModeProxySidecar, "", &NamespaceConfig{ClientAuthType: "client-secret"}, nil, MTLSModeStrict, nil) + ModeProxySidecar, "", &NamespaceConfig{ClientAuthType: "client-secret"}, nil, MTLSModeStrict) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -1725,7 +1689,7 @@ func TestEnsurePerAgentConfigMap_MTLSPermissive_RendersBlock(t *testing.T) { ctx := context.Background() cmName, err := m.ensurePerAgentConfigMap(ctx, "team1", "mtls-agent", - ModeProxySidecar, "", &NamespaceConfig{ClientAuthType: "client-secret"}, nil, MTLSModePermissive, nil) + ModeProxySidecar, "", &NamespaceConfig{ClientAuthType: "client-secret"}, nil, MTLSModePermissive) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -1760,7 +1724,7 @@ func TestEnsurePerAgentConfigMap_MTLSDisabled_OmitsBlock(t *testing.T) { ctx := context.Background() cmName, err := m.ensurePerAgentConfigMap(ctx, "team1", "no-mtls-"+tt.name, - ModeProxySidecar, "", &NamespaceConfig{ClientAuthType: "client-secret"}, nil, tt.mtlsMode, nil) + ModeProxySidecar, "", &NamespaceConfig{ClientAuthType: "client-secret"}, nil, tt.mtlsMode) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -1788,7 +1752,7 @@ func TestEnsurePerAgentConfigMap_MTLSScrubsStaleBlock(t *testing.T) { baseYAML := "mode: proxy-sidecar\nmtls:\n mode: strict\n" cmName, err := m.ensurePerAgentConfigMap(ctx, "team1", "scrub-agent", - ModeProxySidecar, baseYAML, &NamespaceConfig{ClientAuthType: "client-secret"}, nil, MTLSModeDisabled, nil) + ModeProxySidecar, baseYAML, &NamespaceConfig{ClientAuthType: "client-secret"}, nil, MTLSModeDisabled) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -1801,113 +1765,3 @@ func TestEnsurePerAgentConfigMap_MTLSScrubsStaleBlock(t *testing.T) { } } -func TestEnsurePerAgentConfigMap_AllowedAudiences_InjectedIntoJWTValidation(t *testing.T) { - m := newTestMutator() - ctx := context.Background() - - nsConfig := &NamespaceConfig{ - Issuer: "http://keycloak:8080/realms/kagenti", - KeycloakURL: "http://keycloak:8080", - KeycloakRealm: "kagenti", - ClientAuthType: "client-secret", - } - - audiences := []string{"playground", "kagenti-agents"} - cmName, err := m.ensurePerAgentConfigMap(ctx, "team1", "my-agent", - ModeProxySidecar, "", nsConfig, nil, "", audiences) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - - cm := fetchConfigMap(t, m, "team1", cmName) - cfg := parseConfigYAML(t, cm) - - jwtCfg := pluginConfigAt(t, cfg, "inbound", "jwt-validation") - rawAud, ok := jwtCfg["allowed_audiences"] - if !ok { - t.Fatal("expected allowed_audiences in jwt-validation config") - } - audList, ok := rawAud.([]interface{}) - if !ok { - t.Fatalf("allowed_audiences is %T, want []interface{}", rawAud) - } - if len(audList) != 2 || audList[0] != "playground" || audList[1] != "kagenti-agents" { - t.Errorf("allowed_audiences = %v, want [playground kagenti-agents]", audList) - } -} - -func TestEnsurePerAgentConfigMap_AllowedAudiences_NilDoesNotInject(t *testing.T) { - m := newTestMutator() - ctx := context.Background() - - nsConfig := &NamespaceConfig{ - Issuer: "http://keycloak:8080/realms/kagenti", - KeycloakURL: "http://keycloak:8080", - KeycloakRealm: "kagenti", - ClientAuthType: "client-secret", - } - - cmName, err := m.ensurePerAgentConfigMap(ctx, "team1", "my-agent", - ModeProxySidecar, "", nsConfig, nil, "", nil) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - - cm := fetchConfigMap(t, m, "team1", cmName) - cfg := parseConfigYAML(t, cm) - - jwtCfg := pluginConfigAt(t, cfg, "inbound", "jwt-validation") - if _, ok := jwtCfg["allowed_audiences"]; ok { - t.Error("allowed_audiences should not be present when nil") - } -} - -func TestEnsurePerAgentConfigMap_AllowedAudiences_OverridesBaseYAML(t *testing.T) { - m := newTestMutator() - ctx := context.Background() - - // Base YAML has namespace-level audiences - baseYAML := ` -mode: proxy-sidecar -pipeline: - inbound: - plugins: - - name: jwt-validation - config: - issuer: "http://issuer" - allowed_audiences: - - namespace-aud - outbound: - plugins: - - name: token-exchange - config: - keycloak_url: "http://keycloak:8080" - keycloak_realm: "kagenti" - identity: - type: client-secret -` - - // AgentRuntime CR sets different audiences — AR must win - audiences := []string{"agent-aud"} - cmName, err := m.ensurePerAgentConfigMap(ctx, "team1", "my-agent", - ModeProxySidecar, baseYAML, &NamespaceConfig{}, nil, "", audiences) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - - cm := fetchConfigMap(t, m, "team1", cmName) - cfg := parseConfigYAML(t, cm) - - jwtCfg := pluginConfigAt(t, cfg, "inbound", "jwt-validation") - rawAud, ok := jwtCfg["allowed_audiences"] - if !ok { - t.Fatal("expected allowed_audiences in jwt-validation config") - } - audList, ok := rawAud.([]interface{}) - if !ok { - t.Fatalf("allowed_audiences is %T, want []interface{}", rawAud) - } - if len(audList) != 1 || audList[0] != "agent-aud" { - t.Errorf("allowed_audiences = %v, want [agent-aud] (AgentRuntime CR must override base YAML)", audList) - } -} diff --git a/kagenti-operator/internal/webhook/injector/resolved_config.go b/kagenti-operator/internal/webhook/injector/resolved_config.go index 75648cb5..1ca07cc0 100644 --- a/kagenti-operator/internal/webhook/injector/resolved_config.go +++ b/kagenti-operator/internal/webhook/injector/resolved_config.go @@ -21,13 +21,12 @@ import ( ) // ResolvedConfig is the fully-merged configuration for a single workload injection. -// It combines PlatformConfig (images, ports, resources) with namespace ConfigMap -// values and optional AgentRuntime CR overrides. +// It combines PlatformConfig (images, ports, resources) with namespace ConfigMap values. type ResolvedConfig struct { // Platform config (images, ports, resources) — from PlatformConfig Platform *config.PlatformConfig - // Identity — merged from namespace CMs + AgentRuntime overrides + // Identity — from namespace CMs KeycloakURL string KeycloakRealm string AdminCredentialsSecretName string // Secret name for KEYCLOAK_ADMIN_USERNAME/PASSWORD (default: "keycloak-admin-secret") @@ -35,18 +34,17 @@ type ResolvedConfig struct { SpiffeTrustDomain string PlatformClientIDs string - // Token exchange — from namespace CMs (not overridable by AgentRuntime v1alpha1) + // Token exchange — from namespace CMs TokenURL string Issuer string ExpectedAudience string - AllowedAudiences []string // from AgentRuntime .spec.identity.allowedAudiences or namespace CM TargetAudience string TargetScopes string DefaultOutboundPolicy string ClientAuthType string // "client-secret" or "federated-jwt" SpiffeIdpAlias string // Keycloak SPIFFE Identity Provider alias - // Sidecar configs — from namespace CMs (not overridable by AgentRuntime v1alpha1) + // Sidecar configs — from namespace CMs SpiffeHelperConf string EnvoyYAML string // empty = use template AuthproxyRoutesYAML string @@ -54,19 +52,14 @@ type ResolvedConfig struct { // AuthBridge runtime config — from namespace "authbridge-runtime-config" ConfigMap AuthBridgeRuntimeYAML string // raw config.yaml (base for per-agent ConfigMap) - // AuthBridgeMode and MTLSMode are the resolved values from the chain - // CR > namespace ConfigMap > default. They're populated alongside the - // raw AuthBridgeRuntimeYAML so callers (e.g. RenderEnvoyConfig) can - // branch on the resolved values without re-parsing the YAML. - // AuthBridgeMode is "" when no source set it (caller picks the default). - // MTLSMode is "" when no source set it (caller treats as "disabled"). + // AuthBridgeMode and MTLSMode are resolved from the namespace + // ConfigMap (or left empty for caller-side defaults). AuthBridgeMode string MTLSMode string } -// ResolveConfig merges all three configuration layers into a single ResolvedConfig. -// Merge precedence (highest wins): AgentRuntime > namespace CMs > platform defaults. -func ResolveConfig(platform *config.PlatformConfig, ns *NamespaceConfig, ar *AgentRuntimeOverrides) *ResolvedConfig { +// ResolveConfig merges platform defaults with namespace ConfigMap values. +func ResolveConfig(platform *config.PlatformConfig, ns *NamespaceConfig) *ResolvedConfig { if platform == nil { platform = config.CompiledDefaults() } @@ -98,32 +91,10 @@ func ResolveConfig(platform *config.PlatformConfig, ns *NamespaceConfig, ar *Age AuthBridgeRuntimeYAML: ns.AuthBridgeRuntimeYAML, } - // Apply AgentRuntime identity overrides (highest precedence) - if ar != nil { - if len(ar.AllowedAudiences) > 0 { - resolved.AllowedAudiences = ar.AllowedAudiences - } - if ar.SpiffeTrustDomain != nil { - resolved.SpiffeTrustDomain = *ar.SpiffeTrustDomain - } - if ar.ClientRegistrationRealm != nil { - resolved.KeycloakRealm = *ar.ClientRegistrationRealm - } - } - - // Resolve AuthBridgeMode + MTLSMode along the same CR > namespace > "" - // chain that pod_mutator uses. Keep this resolution local to - // ResolveConfig so consumers (e.g. RenderEnvoyConfig) can read the - // already-merged values straight off ResolvedConfig instead of - // re-implementing the chain. - if ar != nil && ar.AuthBridgeMode != nil { - resolved.AuthBridgeMode = *ar.AuthBridgeMode - } else if m := ExtractMode(resolved.AuthBridgeRuntimeYAML); m != "" { + if m := ExtractMode(resolved.AuthBridgeRuntimeYAML); m != "" { resolved.AuthBridgeMode = m } - if ar != nil && ar.MTLSMode != nil { - resolved.MTLSMode = *ar.MTLSMode - } else if m := ExtractMTLSMode(resolved.AuthBridgeRuntimeYAML); m != "" { + if m := ExtractMTLSMode(resolved.AuthBridgeRuntimeYAML); m != "" { resolved.MTLSMode = m } diff --git a/kagenti-operator/internal/webhook/injector/resolved_config_test.go b/kagenti-operator/internal/webhook/injector/resolved_config_test.go index 6ec6fb81..601dba44 100644 --- a/kagenti-operator/internal/webhook/injector/resolved_config_test.go +++ b/kagenti-operator/internal/webhook/injector/resolved_config_test.go @@ -20,11 +20,10 @@ import ( "testing" "github.com/kagenti/operator/internal/webhook/config" - "k8s.io/utils/ptr" ) func TestResolveConfig_NilInputs(t *testing.T) { - resolved := ResolveConfig(nil, nil, nil) + resolved := ResolveConfig(nil, nil) if resolved.Platform == nil { t.Fatal("expected Platform to be set to compiled defaults") } @@ -43,7 +42,7 @@ func TestResolveConfig_NamespaceOnly(t *testing.T) { TargetScopes: "openid", } - resolved := ResolveConfig(config.CompiledDefaults(), ns, nil) + resolved := ResolveConfig(config.CompiledDefaults(), ns) if resolved.KeycloakURL != "http://keycloak:8080" { t.Errorf("KeycloakURL = %q", resolved.KeycloakURL) } @@ -52,60 +51,26 @@ func TestResolveConfig_NamespaceOnly(t *testing.T) { } } -func TestResolveConfig_AgentRuntimeOverrides_Realm(t *testing.T) { - ns := &NamespaceConfig{ - KeycloakRealm: "ns-realm", - } - ar := &AgentRuntimeOverrides{ - ClientRegistrationRealm: ptr.To("ar-realm"), - } - - resolved := ResolveConfig(config.CompiledDefaults(), ns, ar) - - // AgentRuntime realm override should win - if resolved.KeycloakRealm != "ar-realm" { - t.Errorf("KeycloakRealm = %q, want AR override", resolved.KeycloakRealm) - } -} - func TestResolveConfig_SpiffeTrustDomain_FromPlatform(t *testing.T) { platform := config.CompiledDefaults() platform.Spiffe.TrustDomain = "custom.domain" ns := &NamespaceConfig{} - resolved := ResolveConfig(platform, ns, nil) + resolved := ResolveConfig(platform, ns) if resolved.SpiffeTrustDomain != "custom.domain" { t.Errorf("SpiffeTrustDomain = %q, want %q", resolved.SpiffeTrustDomain, "custom.domain") } } -func TestResolveConfig_SpiffeTrustDomain_AROverride(t *testing.T) { - platform := config.CompiledDefaults() - platform.Spiffe.TrustDomain = "platform.domain" - - ar := &AgentRuntimeOverrides{ - SpiffeTrustDomain: ptr.To("ar.domain"), - } - - resolved := ResolveConfig(platform, &NamespaceConfig{}, ar) - if resolved.SpiffeTrustDomain != "ar.domain" { - t.Errorf("SpiffeTrustDomain = %q, want %q", resolved.SpiffeTrustDomain, "ar.domain") - } -} - -func TestResolveConfig_SidecarConfigs_NotOverridable(t *testing.T) { +func TestResolveConfig_SidecarConfigs(t *testing.T) { ns := &NamespaceConfig{ SpiffeHelperConf: "helper.conf content", EnvoyYAML: "envoy.yaml content", AuthproxyRoutesYAML: "routes.yaml content", } - // AR overrides don't have fields for these — they flow through from namespace - ar := &AgentRuntimeOverrides{ - SpiffeTrustDomain: ptr.To("override"), - } - resolved := ResolveConfig(config.CompiledDefaults(), ns, ar) + resolved := ResolveConfig(config.CompiledDefaults(), ns) if resolved.SpiffeHelperConf != "helper.conf content" { t.Errorf("SpiffeHelperConf should come from namespace") } @@ -117,16 +82,14 @@ func TestResolveConfig_SidecarConfigs_NotOverridable(t *testing.T) { } } -func TestResolveConfig_TokenExchange_NotOverridable(t *testing.T) { +func TestResolveConfig_TokenExchange(t *testing.T) { ns := &NamespaceConfig{ TokenURL: "http://keycloak:8080/token", TargetAudience: "my-audience", TargetScopes: "openid", } - // AR has no token exchange fields — they come from namespace only - ar := &AgentRuntimeOverrides{} - resolved := ResolveConfig(config.CompiledDefaults(), ns, ar) + resolved := ResolveConfig(config.CompiledDefaults(), ns) if resolved.TokenURL != "http://keycloak:8080/token" { t.Errorf("TokenURL = %q, want namespace value", resolved.TokenURL) } diff --git a/kagenti-operator/internal/webhook/v1alpha1/authbridge_webhook_test.go b/kagenti-operator/internal/webhook/v1alpha1/authbridge_webhook_test.go index 4538de75..a84ae887 100644 --- a/kagenti-operator/internal/webhook/v1alpha1/authbridge_webhook_test.go +++ b/kagenti-operator/internal/webhook/v1alpha1/authbridge_webhook_test.go @@ -100,9 +100,18 @@ var _ = Describe("AuthBridge Pod Webhook", func() { Context("when a Pod has kagenti.io/type=agent and kagenti.io/inject=enabled", func() { It("should inject sidecars", func() { - // AgentRuntime CR pins envoy-sidecar mode so this test continues to - // exercise the envoy-proxy + proxy-init injection path. - createAgentRuntimeWithMode(testNamespace, "agent-pod", injector.ModeEnvoySidecar) + // Namespace ConfigMap pins envoy-sidecar mode so this test exercises + // the envoy-proxy + proxy-init injection path. + cm := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: injector.AuthBridgeRuntimeConfigMapName, + Namespace: testNamespace, + }, + Data: map[string]string{ + "config.yaml": "mode: " + injector.ModeEnvoySidecar + "\n", + }, + } + Expect(k8sClient.Create(ctx, cm)).To(Succeed()) pod := newTestPod("agent-pod", map[string]string{ "kagenti.io/type": "agent",