From d3cb30e674a0338b05e5b461b10fa049902c9a57 Mon Sep 17 00:00:00 2001 From: Varsha Prasad Narsing Date: Mon, 22 Jun 2026 12:29:25 -0700 Subject: [PATCH] feat: add MTLSReady condition logic to AgentRuntime reconciler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement T006 from the mTLS transport security spec. The controller now evaluates SPIRE availability on each reconcile and sets the MTLSReady condition on AgentRuntime status: - SPIREAvailable (True): spire-agent-socket or svid-output volumes detected in the workload's pod template - SPIREUnavailable (False): mTLS mode is permissive/strict but no SPIRE volumes found; emits a Warning Event with actionable guidance - MTLSDisabled (True): mTLSMode explicitly set to disabled - SPIREAssumed (True): Sandbox workloads where PodSpec is not available; SPIRE injection handled by webhook at pod CREATE MTLSReady=False does NOT block Ready=True — it is informational so operators can track mTLS rollout progress across the fleet. Jira: RHAIENG-4928 Signed-off-by: Varsha Prasad Narsing Assisted-By: Claude (Anthropic AI) --- kagenti-operator/config/rbac/role.yaml | 31 +++----- .../controller/agentruntime_controller.go | 75 ++++++++++++++++++- 2 files changed, 85 insertions(+), 21 deletions(-) diff --git a/kagenti-operator/config/rbac/role.yaml b/kagenti-operator/config/rbac/role.yaml index 0ff59955..0c9ac34e 100644 --- a/kagenti-operator/config/rbac/role.yaml +++ b/kagenti-operator/config/rbac/role.yaml @@ -41,15 +41,6 @@ rules: - "" resources: - serviceaccounts - verbs: - - create - - get - - list - - update - - watch -- apiGroups: - - "" - resources: - services verbs: - create @@ -168,17 +159,6 @@ rules: - get - list - watch -- apiGroups: - - k8s.keycloak.org - resources: - - keycloakrealmimports - - keycloaks - verbs: - - create - - get - - list - - patch - - update - apiGroups: - datasciencecluster.opendatahub.io resources: @@ -195,6 +175,17 @@ rules: - get - list - watch +- apiGroups: + - k8s.keycloak.org + resources: + - keycloakrealmimports + - keycloaks + verbs: + - create + - get + - list + - patch + - update - apiGroups: - kuadrant.io resources: diff --git a/kagenti-operator/internal/controller/agentruntime_controller.go b/kagenti-operator/internal/controller/agentruntime_controller.go index 79f14b35..7c0e3dd0 100644 --- a/kagenti-operator/internal/controller/agentruntime_controller.go +++ b/kagenti-operator/internal/controller/agentruntime_controller.go @@ -88,6 +88,10 @@ const ( // AnnotationRestartPendingValue is the value set on AnnotationRestartPending. AnnotationRestartPendingValue = "true" + + // SPIRE volume names injected by the webhook when mTLS is enabled. + VolumeSpireAgentSocket = "spire-agent-socket" + VolumeSVIDOutput = "svid-output" ) var sandboxGVK = schema.GroupVersionKind{ @@ -212,7 +216,10 @@ func (r *AgentRuntimeReconciler) Reconcile(ctx context.Context, req ctrl.Request fmt.Sprintf("Namespace %s opted out of Istio mesh enrollment", rt.Namespace)) } - // 4.7. Ensure SCC RoleBinding exists in the namespace. + // 4.7. Evaluate MTLSReady condition based on resolved mTLSMode and SPIRE availability. + r.evaluateMTLSReady(ctx, rt) + + // 4.8. Ensure SCC RoleBinding exists in the namespace. // Creates a RoleBinding granting all ServiceAccounts in the namespace // access to the kagenti-authbridge SCC. No-op on non-OpenShift clusters. // On OpenShift, a transient failure is retried via requeue to prevent @@ -763,6 +770,72 @@ func (r *AgentRuntimeReconciler) handleDeletion(ctx context.Context, rt *agentv1 return ctrl.Result{}, nil } +// evaluateMTLSReady sets the MTLSReady condition based on the resolved +// mTLSMode and whether SPIRE infrastructure is available on the workload. +// This is informational — MTLSReady=False does NOT block Ready=True. +func (r *AgentRuntimeReconciler) evaluateMTLSReady(ctx context.Context, rt *agentv1alpha1.AgentRuntime) { + logger := log.FromContext(ctx) + + mtlsMode := rt.Spec.MTLSMode + if mtlsMode == "" { + mtlsMode = "permissive" + } + + if mtlsMode == "disabled" { + r.setCondition(rt, ConditionTypeMTLSReady, metav1.ConditionTrue, "MTLSDisabled", + "mTLS explicitly disabled on this AgentRuntime") + return + } + + // Check if workload has SPIRE infrastructure by looking for + // spire-agent-socket or svid-output volumes in the pod template. + acc, ok := newRuntimePodTemplateAccessor(rt.Spec.TargetRef.Kind) + if !ok { + r.setCondition(rt, ConditionTypeMTLSReady, metav1.ConditionFalse, "UnsupportedKind", + fmt.Sprintf("Cannot check SPIRE availability for workload kind %s", rt.Spec.TargetRef.Kind)) + return + } + + key := types.NamespacedName{Name: rt.Spec.TargetRef.Name, Namespace: rt.Namespace} + if err := r.Get(ctx, key, acc.obj); err != nil { + logger.V(1).Info("Cannot read workload for SPIRE check", "error", err) + r.setCondition(rt, ConditionTypeMTLSReady, metav1.ConditionFalse, "WorkloadNotFound", + fmt.Sprintf("Cannot verify SPIRE availability: %v", err)) + return + } + + podSpec := acc.getPodSpec(acc.obj) + if podSpec == nil { + // Sandbox workloads don't expose a typed PodSpec; assume SPIRE + // is available since the webhook handles injection at pod CREATE. + r.setCondition(rt, ConditionTypeMTLSReady, metav1.ConditionTrue, "SPIREAssumed", + "Sandbox workload; SPIRE availability assumed (webhook handles injection)") + return + } + + spireDetected := false + for _, v := range podSpec.Volumes { + if v.Name == VolumeSpireAgentSocket || v.Name == VolumeSVIDOutput { + spireDetected = true + break + } + } + + if spireDetected { + r.setCondition(rt, ConditionTypeMTLSReady, metav1.ConditionTrue, "SPIREAvailable", + fmt.Sprintf("SPIRE volumes detected on workload %s; mTLS mode: %s", rt.Spec.TargetRef.Name, mtlsMode)) + } else { + msg := "mTLS requires SPIRE; either deploy SPIRE or set mTLSMode: disabled" + r.setCondition(rt, ConditionTypeMTLSReady, metav1.ConditionFalse, "SPIREUnavailable", msg) + if r.Recorder != nil { + r.Recorder.Eventf(rt, nil, corev1.EventTypeWarning, "SPIREUnavailable", + "MTLSReadyCheck", msg) + } + logger.Info("SPIRE not detected for mTLS-enabled workload", + "workload", rt.Spec.TargetRef.Name, "mtlsMode", mtlsMode) + } +} + func (r *AgentRuntimeReconciler) setCondition(rt *agentv1alpha1.AgentRuntime, condType string, status metav1.ConditionStatus, reason, message string) { meta.SetStatusCondition(&rt.Status.Conditions, metav1.Condition{ Type: condType,