diff --git a/charts/kagenti-operator/values.yaml b/charts/kagenti-operator/values.yaml index dc961436..8ff77cb9 100644 --- a/charts/kagenti-operator/values.yaml +++ b/charts/kagenti-operator/values.yaml @@ -275,3 +275,21 @@ defaults: limits: cpu: 300m memory: 384Mi + + # SPIFFE / spiffe-helper settings. + # Keep helperConfig in sync with DefaultSpiffeHelperConfig in internal/webhook/config/defaults.go. + spiffe: + trustDomain: cluster.local + socketPath: "unix:///spiffe-workload-api/spire-agent.sock" + helperConfig: | + agent_address = "/spiffe-workload-api/spire-agent.sock" + cmd = "" + cmd_args = "" + svid_file_name = "/opt/svid.pem" + svid_key_file_name = "/opt/svid_key.pem" + svid_bundle_file_name = "/opt/svid_bundle.pem" + cert_file_mode = 0644 + key_file_mode = 0640 + jwt_svids = [{jwt_audience="http://keycloak.localtest.me:8080/realms/kagenti", jwt_svid_file_name="/opt/jwt_svid.token"}] + jwt_svid_file_mode = 0644 + include_federated_domains = true diff --git a/kagenti-operator/cmd/main.go b/kagenti-operator/cmd/main.go index 4c3e1f82..31145181 100644 --- a/kagenti-operator/cmd/main.go +++ b/kagenti-operator/cmd/main.go @@ -631,6 +631,7 @@ func main() { EnableCardDiscovery: enableCardDiscovery, SpireTrustDomain: spireTrustDomain, GetFeatureGates: featureGateLoader.Get, + GetPlatformConfig: configLoader.Get, } if enableCardDiscovery { artReconciler.AgentFetcher = agentFetcher diff --git a/kagenti-operator/config/authbridge/spiffe-helper-config.yaml b/kagenti-operator/config/authbridge/spiffe-helper-config.yaml deleted file mode 100644 index ddde6583..00000000 --- a/kagenti-operator/config/authbridge/spiffe-helper-config.yaml +++ /dev/null @@ -1,24 +0,0 @@ -# SPIFFE helper configuration template. -# The operator copies this to agent namespaces via ensureNamespaceConfigMaps. -apiVersion: v1 -kind: ConfigMap -metadata: - name: spiffe-helper-config - namespace: kagenti-operator-system - labels: - app.kubernetes.io/name: kagenti-operator - app.kubernetes.io/component: authbridge - app.kubernetes.io/managed-by: kustomize -data: - helper.conf: | - agent_address = "/spiffe-workload-api/spire-agent.sock" - cmd = "" - cmd_args = "" - svid_file_name = "/opt/svid.pem" - svid_key_file_name = "/opt/svid_key.pem" - svid_bundle_file_name = "/opt/svid_bundle.pem" - cert_file_mode = 0644 - key_file_mode = 0640 - jwt_svids = [{jwt_audience="http://keycloak.localtest.me:8080/realms/kagenti", jwt_svid_file_name="/opt/jwt_svid.token"}] - jwt_svid_file_mode = 0644 - include_federated_domains = true diff --git a/kagenti-operator/config/openshift/kustomization.yaml b/kagenti-operator/config/openshift/kustomization.yaml index 386fa6f7..81e67fe7 100644 --- a/kagenti-operator/config/openshift/kustomization.yaml +++ b/kagenti-operator/config/openshift/kustomization.yaml @@ -12,4 +12,3 @@ resources: patches: - path: patches/authbridge-keycloak.yaml -- path: patches/spiffe-helper-keycloak.yaml diff --git a/kagenti-operator/config/openshift/patches/spiffe-helper-keycloak.yaml b/kagenti-operator/config/openshift/patches/spiffe-helper-keycloak.yaml deleted file mode 100644 index ca816bb2..00000000 --- a/kagenti-operator/config/openshift/patches/spiffe-helper-keycloak.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# Strategic merge patch: override spiffe-helper-config with -# in-cluster Keycloak JWT audience for OpenShift deployments. -apiVersion: v1 -kind: ConfigMap -metadata: - name: spiffe-helper-config - namespace: kagenti-operator-system -data: - helper.conf: | - agent_address = "/spiffe-workload-api/spire-agent.sock" - cmd = "" - cmd_args = "" - svid_file_name = "/opt/svid.pem" - svid_key_file_name = "/opt/svid_key.pem" - svid_bundle_file_name = "/opt/svid_bundle.pem" - cert_file_mode = 0644 - key_file_mode = 0640 - jwt_svids = [{jwt_audience="http://keycloak-service.keycloak.svc:8080/realms/kagenti", jwt_svid_file_name="/opt/jwt_svid.token"}] - jwt_svid_file_mode = 0644 - include_federated_domains = true diff --git a/kagenti-operator/internal/controller/agentruntime_config.go b/kagenti-operator/internal/controller/agentruntime_config.go index 66cd2e0c..ea38385b 100644 --- a/kagenti-operator/internal/controller/agentruntime_config.go +++ b/kagenti-operator/internal/controller/agentruntime_config.go @@ -48,6 +48,11 @@ const ( // ConfigMap are watched by AgentRuntimeReconciler so the resolved-config // hash picks them up and rolls affected workloads. AuthBridgeRuntimeConfigMapName = "authbridge-runtime-config" + + // SpiffeHelperConfigMapName is the namespace-scoped ConfigMap holding + // the spiffe-helper helper.conf. Derived from PlatformConfig by the + // controller; included in the config hash for rolling updates. + SpiffeHelperConfigMapName = "spiffe-helper-config" ) // ClusterDefaultsNamespace is the namespace where cluster-level ConfigMaps @@ -84,6 +89,10 @@ type resolvedConfig struct { // we want any byte change to roll the workload. Empty string when // the ConfigMap doesn't exist in the namespace. AuthBridgeRuntime string `json:"authBridgeRuntime,omitempty"` + + // SpiffeHelperConfig captures the spiffe-helper-config CM content so + // changes to PlatformConfig's spiffe.helperConfig trigger rolling updates. + SpiffeHelperConfig string `json:"spiffeHelperConfig,omitempty"` } // ConfigResult holds the computed hash and any warnings from the config resolution. @@ -130,10 +139,19 @@ func resolveConfig(ctx context.Context, c client.Reader, namespace string) (reso abRuntime = data["config.yaml"] } + // Layer 2c: spiffe-helper-config (helper.conf). + // Derived from PlatformConfig by the controller; included in hash + // so changes to spiffe.helperConfig trigger rolling updates. + spiffeHelper := "" + if data := readConfigMapData(ctx, c, namespace, SpiffeHelperConfigMapName); len(data) > 0 { + spiffeHelper = data["helper.conf"] + } + return resolvedConfig{ - FeatureGates: featureGates, - Defaults: merged, - AuthBridgeRuntime: abRuntime, + FeatureGates: featureGates, + Defaults: merged, + AuthBridgeRuntime: abRuntime, + SpiffeHelperConfig: spiffeHelper, }, warnings } diff --git a/kagenti-operator/internal/controller/agentruntime_config_test.go b/kagenti-operator/internal/controller/agentruntime_config_test.go index b7d77b67..76d4d225 100644 --- a/kagenti-operator/internal/controller/agentruntime_config_test.go +++ b/kagenti-operator/internal/controller/agentruntime_config_test.go @@ -220,6 +220,32 @@ var _ = Describe("AgentRuntime Config", func() { r2, _ := ComputeConfigHash(ctx, k8sClient, namespace) Expect(r1.Hash).NotTo(Equal(r2.Hash)) }) + + It("should change when spiffe-helper-config content changes", func() { + const shHashNS = "sh-hash-ns" + shNS := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: shHashNS}} + _ = k8sClient.Create(ctx, shNS) + + shCM := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "spiffe-helper-config", + Namespace: shHashNS, + }, + Data: map[string]string{ + "helper.conf": "agent_address = \"/old/socket\"", + }, + } + Expect(k8sClient.Create(ctx, shCM)).To(Succeed()) + defer func() { _ = k8sClient.Delete(ctx, shCM) }() + + r1, _ := ComputeConfigHash(ctx, k8sClient, shHashNS) + + shCM.Data["helper.conf"] = "agent_address = \"/new/socket\"" + Expect(k8sClient.Update(ctx, shCM)).To(Succeed()) + + r2, _ := ComputeConfigHash(ctx, k8sClient, shHashNS) + Expect(r1.Hash).NotTo(Equal(r2.Hash)) + }) }) Context("resolveConfig two-layer merge", func() { diff --git a/kagenti-operator/internal/controller/agentruntime_controller.go b/kagenti-operator/internal/controller/agentruntime_controller.go index 7c0e3dd0..b21b3f62 100644 --- a/kagenti-operator/internal/controller/agentruntime_controller.go +++ b/kagenti-operator/internal/controller/agentruntime_controller.go @@ -113,6 +113,7 @@ type AgentRuntimeReconciler struct { EnableCardDiscovery bool SpireTrustDomain string GetFeatureGates func() *webhookconfig.FeatureGates + GetPlatformConfig func() *webhookconfig.PlatformConfig } func (r *AgentRuntimeReconciler) getFeatureGates() *webhookconfig.FeatureGates { @@ -122,6 +123,15 @@ func (r *AgentRuntimeReconciler) getFeatureGates() *webhookconfig.FeatureGates { return webhookconfig.DefaultFeatureGates() } +func (r *AgentRuntimeReconciler) getPlatformConfig() *webhookconfig.PlatformConfig { + if r.GetPlatformConfig != nil { + if cfg := r.GetPlatformConfig(); cfg != nil { + return cfg + } + } + return webhookconfig.CompiledDefaults() +} + // +kubebuilder:rbac:groups=agent.kagenti.dev,resources=agentruntimes,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=agent.kagenti.dev,resources=agentruntimes/status,verbs=get;update;patch // +kubebuilder:rbac:groups=agent.kagenti.dev,resources=agentruntimes/finalizers,verbs=update @@ -194,6 +204,19 @@ func (r *AgentRuntimeReconciler) Reconcile(ctx context.Context, req ctrl.Request } } + // 4.5b. Ensure spiffe-helper-config CM is derived from PlatformConfig. + // Unlike template CMs above, this always overwrites to keep PlatformConfig + // as the single source of truth. + if err := r.ensureSpiffeHelperConfigMap(ctx, rt.Namespace); err != nil { + logger.Error(err, "Failed to ensure spiffe-helper-config") + if r.Recorder != nil { + r.Recorder.Eventf(rt, nil, corev1.EventTypeWarning, "ConfigMapEnsureError", + "EnsureSpiffeHelperConfig", err.Error()) + } + r.updateErrorStatus(ctx, req.NamespacedName, ConditionTypeReady, "SpiffeHelperConfigError", err.Error()) + return ctrl.Result{RequeueAfter: 30 * time.Second}, nil + } + // 4.6. Ensure namespace has Istio ambient mesh labels for ztunnel mTLS. istioLabeled, istioErr := r.ensureIstioMeshLabels(ctx, rt.Namespace) switch { @@ -235,7 +258,7 @@ func (r *AgentRuntimeReconciler) Reconcile(ctx context.Context, req ctrl.Request } // 5. Compute config hash from merged configuration (cluster → namespace) - configResult, err := ComputeConfigHash(ctx, r.Client, rt.Namespace) + configResult, err := ComputeConfigHash(ctx, r.uncachedReader(), rt.Namespace) if err != nil { logger.Error(err, "Failed to compute config hash") r.updateErrorStatus(ctx, req.NamespacedName, ConditionTypeReady, "ConfigHashError", err.Error()) @@ -1045,7 +1068,6 @@ func computeCardContentHash(cardData *agentv1alpha1.AgentCardData) string { var templateConfigMapNames = []string{ "authbridge-config", "authbridge-runtime-config", - "spiffe-helper-config", } // ensureNamespaceConfigMaps copies template ConfigMaps from kagenti-system to the @@ -1097,6 +1119,65 @@ func (r *AgentRuntimeReconciler) ensureNamespaceConfigMaps(ctx context.Context, return nil } +// ensureSpiffeHelperConfigMap creates or updates the spiffe-helper-config ConfigMap +// in the target namespace using content from PlatformConfig. Unlike template CMs +// which are create-if-not-exists, this always overwrites because PlatformConfig is +// the single source of truth. +func (r *AgentRuntimeReconciler) ensureSpiffeHelperConfigMap(ctx context.Context, namespace string) error { + logger := log.FromContext(ctx) + cfg := r.getPlatformConfig() + + desired := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: SpiffeHelperConfigMapName, + Namespace: namespace, + Labels: map[string]string{ + LabelManagedBy: LabelManagedByValue, + }, + }, + Data: map[string]string{ + "helper.conf": cfg.Spiffe.HelperConfig, + }, + } + + existing := &corev1.ConfigMap{} + err := r.uncachedReader().Get(ctx, client.ObjectKey{Namespace: namespace, Name: SpiffeHelperConfigMapName}, existing) + if apierrors.IsNotFound(err) { + if err := r.Create(ctx, desired); err != nil { + if apierrors.IsAlreadyExists(err) { + return nil + } + return fmt.Errorf("failed to create spiffe-helper-config in %s: %w", namespace, err) + } + logger.Info("Created spiffe-helper-config from PlatformConfig", "namespace", namespace) + return nil + } + if err != nil { + return fmt.Errorf("failed to check spiffe-helper-config in %s: %w", namespace, err) + } + + needsUpdate := existing.Data["helper.conf"] != cfg.Spiffe.HelperConfig + + if existing.Labels == nil { + existing.Labels = make(map[string]string) + } + if existing.Labels[LabelManagedBy] != LabelManagedByValue { + existing.Labels[LabelManagedBy] = LabelManagedByValue + needsUpdate = true + } + + if !needsUpdate { + return nil + } + + existing.Data = desired.Data + if err := r.Update(ctx, existing); err != nil { + return fmt.Errorf("failed to update spiffe-helper-config in %s: %w", namespace, err) + } + logger.Info("Updated spiffe-helper-config from PlatformConfig", "namespace", namespace) + return nil +} + const ( sccClusterRoleName = "system:openshift:scc:kagenti-authbridge" sccRoleBindingName = "agent-authbridge-scc" diff --git a/kagenti-operator/internal/controller/agentruntime_controller_test.go b/kagenti-operator/internal/controller/agentruntime_controller_test.go index 8a9fb112..17c74494 100644 --- a/kagenti-operator/internal/controller/agentruntime_controller_test.go +++ b/kagenti-operator/internal/controller/agentruntime_controller_test.go @@ -704,7 +704,7 @@ var _ = Describe("AgentRuntime Controller", func() { }) AfterEach(func() { - for _, name := range templateConfigMapNames { + for _, name := range []string{"authbridge-config", "authbridge-runtime-config", "envoy-config"} { cm := &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: ClusterDefaultsNamespace}} _ = k8sClient.Delete(ctx, cm) cm = &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: cmTestNS}} @@ -809,8 +809,8 @@ var _ = Describe("AgentRuntime Controller", func() { Expect(k8sClient.Get(ctx, types.NamespacedName{Name: "authbridge-config", Namespace: cmTestNS}, abCfg)).To(Succeed()) Expect(abCfg.Data["KEYCLOAK_URL"]).To(Equal("http://existing")) - // The other 3 should be created from templates - for _, name := range []string{"authbridge-runtime-config", "spiffe-helper-config"} { + // authbridge-runtime-config should be created from template + for _, name := range []string{"authbridge-runtime-config"} { cm := &corev1.ConfigMap{} Expect(k8sClient.Get(ctx, types.NamespacedName{Name: name, Namespace: cmTestNS}, cm)).To(Succeed()) Expect(cm.Data["config.yaml"]).To(Equal("template-" + name)) @@ -819,6 +819,128 @@ var _ = Describe("AgentRuntime Controller", func() { }) }) + Context("When ensuring spiffe-helper-config from PlatformConfig", func() { + const shTestNS = "sh-test-ns" + + BeforeEach(func() { + testNS := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: shTestNS}} + _ = k8sClient.Create(ctx, testNS) + }) + + AfterEach(func() { + cm := &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "spiffe-helper-config", Namespace: shTestNS}} + _ = k8sClient.Delete(ctx, cm) + }) + + It("should create spiffe-helper-config when missing", func() { + r := newReconciler() + r.GetPlatformConfig = func() *webhookconfig.PlatformConfig { + cfg := webhookconfig.CompiledDefaults() + cfg.Spiffe.HelperConfig = "test_agent_address = \"/test/socket\"" + return cfg + } + + Expect(r.ensureSpiffeHelperConfigMap(ctx, shTestNS)).To(Succeed()) + + created := &corev1.ConfigMap{} + Expect(k8sClient.Get(ctx, types.NamespacedName{Name: "spiffe-helper-config", Namespace: shTestNS}, created)).To(Succeed()) + Expect(created.Data["helper.conf"]).To(Equal("test_agent_address = \"/test/socket\"")) + Expect(created.Labels[LabelManagedBy]).To(Equal(LabelManagedByValue)) + }) + + It("should overwrite spiffe-helper-config when content differs", func() { + existing := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "spiffe-helper-config", + Namespace: shTestNS, + }, + Data: map[string]string{"helper.conf": "old_content"}, + } + Expect(k8sClient.Create(ctx, existing)).To(Succeed()) + + r := newReconciler() + r.GetPlatformConfig = func() *webhookconfig.PlatformConfig { + cfg := webhookconfig.CompiledDefaults() + cfg.Spiffe.HelperConfig = "new_content" + return cfg + } + + Expect(r.ensureSpiffeHelperConfigMap(ctx, shTestNS)).To(Succeed()) + + updated := &corev1.ConfigMap{} + Expect(k8sClient.Get(ctx, types.NamespacedName{Name: "spiffe-helper-config", Namespace: shTestNS}, updated)).To(Succeed()) + Expect(updated.Data["helper.conf"]).To(Equal("new_content")) + }) + + It("should be idempotent when content matches", func() { + existing := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "spiffe-helper-config", + Namespace: shTestNS, + Labels: map[string]string{LabelManagedBy: LabelManagedByValue}, + }, + Data: map[string]string{"helper.conf": "same_content"}, + } + Expect(k8sClient.Create(ctx, existing)).To(Succeed()) + + r := newReconciler() + r.GetPlatformConfig = func() *webhookconfig.PlatformConfig { + cfg := webhookconfig.CompiledDefaults() + cfg.Spiffe.HelperConfig = "same_content" + return cfg + } + + Expect(r.ensureSpiffeHelperConfigMap(ctx, shTestNS)).To(Succeed()) + + result := &corev1.ConfigMap{} + Expect(k8sClient.Get(ctx, types.NamespacedName{Name: "spiffe-helper-config", Namespace: shTestNS}, result)).To(Succeed()) + Expect(result.Data["helper.conf"]).To(Equal("same_content")) + }) + + It("should adopt label on leftover CM with matching content", func() { + existing := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "spiffe-helper-config", + Namespace: shTestNS, + Labels: map[string]string{"app.kubernetes.io/managed-by": "kustomize"}, + }, + Data: map[string]string{"helper.conf": "same_content"}, + } + Expect(k8sClient.Create(ctx, existing)).To(Succeed()) + + r := newReconciler() + r.GetPlatformConfig = func() *webhookconfig.PlatformConfig { + cfg := webhookconfig.CompiledDefaults() + cfg.Spiffe.HelperConfig = "same_content" + return cfg + } + + Expect(r.ensureSpiffeHelperConfigMap(ctx, shTestNS)).To(Succeed()) + + result := &corev1.ConfigMap{} + Expect(k8sClient.Get(ctx, types.NamespacedName{Name: "spiffe-helper-config", Namespace: shTestNS}, result)).To(Succeed()) + Expect(result.Data["helper.conf"]).To(Equal("same_content")) + Expect(result.Labels[LabelManagedBy]).To(Equal(LabelManagedByValue)) + }) + + It("should use CompiledDefaults when GetPlatformConfig is nil", func() { + r := newReconciler() + Expect(r.ensureSpiffeHelperConfigMap(ctx, shTestNS)).To(Succeed()) + + created := &corev1.ConfigMap{} + Expect(k8sClient.Get(ctx, types.NamespacedName{Name: "spiffe-helper-config", Namespace: shTestNS}, created)).To(Succeed()) + Expect(created.Data["helper.conf"]).To(Equal(webhookconfig.DefaultSpiffeHelperConfig)) + }) + }) + + Context("When templateConfigMapNames excludes spiffe-helper-config", func() { + It("should not contain spiffe-helper-config", func() { + for _, name := range templateConfigMapNames { + Expect(name).NotTo(Equal("spiffe-helper-config")) + } + }) + }) + Context("Service resolution for card discovery", func() { It("should resolve service by name match", func() { dep := newDeployment("svc-name-deploy", namespace) diff --git a/kagenti-operator/internal/webhook/config/defaults.go b/kagenti-operator/internal/webhook/config/defaults.go index 58fb5abf..8a9c36d8 100644 --- a/kagenti-operator/internal/webhook/config/defaults.go +++ b/kagenti-operator/internal/webhook/config/defaults.go @@ -5,6 +5,24 @@ import ( "k8s.io/apimachinery/pkg/api/resource" ) +// DefaultSpiffeHelperConfig is the default helper.conf content for spiffe-helper. +// Keep in sync with charts/kagenti-operator/values.yaml defaults.spiffe.helperConfig. +// The jwt_audience below targets a local dev Keycloak. Production and OpenShift +// deployments MUST override this via Helm values (defaults.spiffe.helperConfig) +// to set the in-cluster Keycloak audience. +const DefaultSpiffeHelperConfig = `agent_address = "/spiffe-workload-api/spire-agent.sock" +cmd = "" +cmd_args = "" +svid_file_name = "/opt/svid.pem" +svid_key_file_name = "/opt/svid_key.pem" +svid_bundle_file_name = "/opt/svid_bundle.pem" +cert_file_mode = 0644 +key_file_mode = 0640 +jwt_svids = [{jwt_audience="http://keycloak.localtest.me:8080/realms/kagenti", jwt_svid_file_name="/opt/jwt_svid.token"}] +jwt_svid_file_mode = 0644 +include_federated_domains = true +` + // CompiledDefaults returns hardcoded defaults used when no config is provided func CompiledDefaults() *PlatformConfig { return &PlatformConfig{ @@ -79,8 +97,9 @@ func CompiledDefaults() *PlatformConfig { DefaultScopes: []string{"openid"}, }, Spiffe: SpiffeConfig{ - TrustDomain: "cluster.local", - SocketPath: "unix:///spiffe-workload-api/spire-agent.sock", + TrustDomain: "cluster.local", + SocketPath: "unix:///spiffe-workload-api/spire-agent.sock", + HelperConfig: DefaultSpiffeHelperConfig, }, Observability: ObservabilityConfig{ LogLevel: "info", diff --git a/kagenti-operator/internal/webhook/config/loader.go b/kagenti-operator/internal/webhook/config/loader.go index 10030168..ea740a47 100644 --- a/kagenti-operator/internal/webhook/config/loader.go +++ b/kagenti-operator/internal/webhook/config/loader.go @@ -227,9 +227,14 @@ func logConfig(cfg *PlatformConfig, source string) { "defaultAudience", cfg.TokenExchange.DefaultAudience, "defaultScopes", cfg.TokenExchange.DefaultScopes, ) + helperSnippet := cfg.Spiffe.HelperConfig + if len(helperSnippet) > 80 { + helperSnippet = helperSnippet[:80] + "..." + } log.Info("[config] spiffe", "trustDomain", cfg.Spiffe.TrustDomain, "socketPath", cfg.Spiffe.SocketPath, + "helperConfig", helperSnippet, ) log.Info("=============================================") } diff --git a/kagenti-operator/internal/webhook/config/types.go b/kagenti-operator/internal/webhook/config/types.go index be035c0c..9735b06b 100644 --- a/kagenti-operator/internal/webhook/config/types.go +++ b/kagenti-operator/internal/webhook/config/types.go @@ -86,8 +86,9 @@ type TokenExchangeDefaults struct { } type SpiffeConfig struct { - TrustDomain string `json:"trustDomain" yaml:"trustDomain"` - SocketPath string `json:"socketPath" yaml:"socketPath"` + TrustDomain string `json:"trustDomain" yaml:"trustDomain"` + SocketPath string `json:"socketPath" yaml:"socketPath"` + HelperConfig string `json:"helperConfig" yaml:"helperConfig"` } type ObservabilityConfig struct { diff --git a/kagenti-operator/internal/webhook/config/types_test.go b/kagenti-operator/internal/webhook/config/types_test.go index 2bb86431..8f7398a2 100644 --- a/kagenti-operator/internal/webhook/config/types_test.go +++ b/kagenti-operator/internal/webhook/config/types_test.go @@ -1,6 +1,11 @@ package config -import "testing" +import ( + "strings" + "testing" + + "sigs.k8s.io/yaml" +) // TransparentPort is the REDIRECT target for the enforce-redirect guard; it must // be a valid, non-privileged port (the proxy binds it as a non-root user). @@ -60,3 +65,47 @@ func TestValidate_IptablesCmd(t *testing.T) { }) } } + +func TestCompiledDefaults_SpiffeHelperConfig(t *testing.T) { + cfg := CompiledDefaults() + if cfg.Spiffe.HelperConfig == "" { + t.Fatal("CompiledDefaults().Spiffe.HelperConfig must not be empty") + } + if !strings.Contains(cfg.Spiffe.HelperConfig, "agent_address") { + t.Error("HelperConfig must contain agent_address") + } + if !strings.Contains(cfg.Spiffe.HelperConfig, "svid_file_name") { + t.Error("HelperConfig must contain svid_file_name") + } +} + +func TestSpiffeHelperConfig_YAMLRoundTrip(t *testing.T) { + input := ` +spiffe: + trustDomain: example.org + socketPath: "unix:///custom/socket.sock" + helperConfig: | + agent_address = "/custom/socket.sock" + cmd = "" +` + var cfg PlatformConfig + if err := yaml.Unmarshal([]byte(input), &cfg); err != nil { + t.Fatalf("YAML unmarshal failed: %v", err) + } + if cfg.Spiffe.TrustDomain != "example.org" { + t.Errorf("TrustDomain = %q, want example.org", cfg.Spiffe.TrustDomain) + } + if !strings.Contains(cfg.Spiffe.HelperConfig, "agent_address") { + t.Error("HelperConfig not loaded from YAML") + } +} + +func TestDeepCopy_PreservesHelperConfig(t *testing.T) { + cfg := CompiledDefaults() + cfg.Spiffe.HelperConfig = "custom_helper_config" + + copied := cfg.DeepCopy() + if copied.Spiffe.HelperConfig != "custom_helper_config" { + t.Errorf("DeepCopy lost HelperConfig: got %q", copied.Spiffe.HelperConfig) + } +} diff --git a/kagenti-operator/internal/webhook/injector/namespace_config.go b/kagenti-operator/internal/webhook/injector/namespace_config.go index d893a174..3d263044 100644 --- a/kagenti-operator/internal/webhook/injector/namespace_config.go +++ b/kagenti-operator/internal/webhook/injector/namespace_config.go @@ -53,9 +53,6 @@ type NamespaceConfig struct { SpiffeIdpAlias string // Keycloak SPIFFE Identity Provider alias (e.g., "spire-spiffe") JWTAudience string // Audience for SPIFFE JWT-SVIDs (token-exchange identity.jwt_audience) - // From "spiffe-helper-config" ConfigMap - SpiffeHelperConf string // raw helper.conf content - // From "authproxy-routes" ConfigMap AuthproxyRoutesYAML string // raw routes.yaml content @@ -91,12 +88,9 @@ func ReadNamespaceConfig(ctx context.Context, c client.Reader, namespace string) // uses SecretKeyRef to reference the secret by name, keeping credentials out of // the NamespaceConfig struct and the webhook's memory. - // Read "spiffe-helper-config" ConfigMap - if cm, err := getConfigMap(ctx, c, namespace, SpiffeHelperConfigMapName); err != nil { - nsConfigLog.V(1).Info("ConfigMap not found", "name", SpiffeHelperConfigMapName, "namespace", namespace, "error", err) - } else { - cfg.SpiffeHelperConf = cm.Data["helper.conf"] - } + // Note: spiffe-helper-config is NOT read here. The controller derives it + // from PlatformConfig via ensureSpiffeHelperConfigMap(). The volume mount + // still references SpiffeHelperConfigMapName directly. // Read "authproxy-routes" ConfigMap if cm, err := getConfigMap(ctx, c, namespace, AuthproxyRoutesConfigMapName); err != nil { diff --git a/kagenti-operator/internal/webhook/injector/namespace_config_test.go b/kagenti-operator/internal/webhook/injector/namespace_config_test.go index b8884421..c0f8126e 100644 --- a/kagenti-operator/internal/webhook/injector/namespace_config_test.go +++ b/kagenti-operator/internal/webhook/injector/namespace_config_test.go @@ -48,16 +48,12 @@ func TestReadNamespaceConfig_AllPresent(t *testing.T) { "DEFAULT_OUTBOUND_POLICY": "passthrough", }, } - spiffeCM := &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{Name: SpiffeHelperConfigMapName, Namespace: "ns1"}, - Data: map[string]string{"helper.conf": "agent_address = \"/spiffe-workload-api/spire-agent.sock\""}, - } routesCM := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{Name: AuthproxyRoutesConfigMapName, Namespace: "ns1"}, Data: map[string]string{"routes.yaml": "routes: []"}, } - reader := newFakeReader(abCM, spiffeCM, routesCM) + reader := newFakeReader(abCM, routesCM) cfg, err := ReadNamespaceConfig(context.Background(), reader, "ns1") if err != nil { t.Fatalf("unexpected error: %v", err) @@ -75,9 +71,6 @@ func TestReadNamespaceConfig_AllPresent(t *testing.T) { if cfg.Issuer != "http://keycloak:8080/realms/demo" { t.Errorf("Issuer = %q", cfg.Issuer) } - if cfg.SpiffeHelperConf == "" { - t.Error("SpiffeHelperConf is empty") - } if cfg.TargetAudience != "auth-target" { t.Errorf("TargetAudience = %q", cfg.TargetAudience) } diff --git a/kagenti-operator/internal/webhook/injector/resolved_config.go b/kagenti-operator/internal/webhook/injector/resolved_config.go index eb856570..053d9016 100644 --- a/kagenti-operator/internal/webhook/injector/resolved_config.go +++ b/kagenti-operator/internal/webhook/injector/resolved_config.go @@ -45,7 +45,6 @@ type ResolvedConfig struct { SpiffeIdpAlias string // Keycloak SPIFFE Identity Provider alias // Sidecar configs — from namespace CMs - SpiffeHelperConf string AuthproxyRoutesYAML string // AuthBridge runtime config — from namespace "authbridge-runtime-config" ConfigMap @@ -86,7 +85,6 @@ func ResolveConfig(platform *config.PlatformConfig, ns *NamespaceConfig) *Resolv DefaultOutboundPolicy: ns.DefaultOutboundPolicy, ClientAuthType: ns.ClientAuthType, SpiffeIdpAlias: ns.SpiffeIdpAlias, - SpiffeHelperConf: ns.SpiffeHelperConf, AuthproxyRoutesYAML: ns.AuthproxyRoutesYAML, AuthBridgeRuntimeYAML: ns.AuthBridgeRuntimeYAML, } diff --git a/kagenti-operator/internal/webhook/injector/resolved_config_test.go b/kagenti-operator/internal/webhook/injector/resolved_config_test.go index 4eddb2f3..e6be76fa 100644 --- a/kagenti-operator/internal/webhook/injector/resolved_config_test.go +++ b/kagenti-operator/internal/webhook/injector/resolved_config_test.go @@ -65,14 +65,10 @@ func TestResolveConfig_SpiffeTrustDomain_FromPlatform(t *testing.T) { func TestResolveConfig_SidecarConfigs_FromNamespace(t *testing.T) { ns := &NamespaceConfig{ - SpiffeHelperConf: "helper.conf content", AuthproxyRoutesYAML: "routes.yaml content", } resolved := ResolveConfig(config.CompiledDefaults(), ns) - if resolved.SpiffeHelperConf != "helper.conf content" { - t.Errorf("SpiffeHelperConf should come from namespace") - } if resolved.AuthproxyRoutesYAML != "routes.yaml content" { t.Errorf("AuthproxyRoutesYAML should come from namespace") } diff --git a/kagenti-operator/test/e2e/e2e_test.go b/kagenti-operator/test/e2e/e2e_test.go index 86beae1f..f9b9c6e3 100644 --- a/kagenti-operator/test/e2e/e2e_test.go +++ b/kagenti-operator/test/e2e/e2e_test.go @@ -1182,9 +1182,11 @@ rules: Expect(err).NotTo(HaveOccurred()) Expect(utils.WaitForDeploymentReady("runtime-agent-target", agentRuntimeTestNamespace, 2*time.Minute)).To(Succeed()) - By("creating AgentRuntime CR") - _, err = utils.KubectlApplyStdin(runtimeAgentCRFixture(), agentRuntimeTestNamespace) - Expect(err).NotTo(HaveOccurred()) + By("creating AgentRuntime CR (with retry for webhook readiness)") + Eventually(func() error { + _, err := utils.KubectlApplyStdin(runtimeAgentCRFixture(), agentRuntimeTestNamespace) + return err + }, 1*time.Minute, 5*time.Second).Should(Succeed()) By("verifying kagenti.io/type=agent on workload metadata") Eventually(func(g Gomega) {