diff --git a/config/components/evalhub/kustomization.yaml b/config/components/evalhub/kustomization.yaml index 97524250b..8c970dd73 100644 --- a/config/components/evalhub/kustomization.yaml +++ b/config/components/evalhub/kustomization.yaml @@ -6,7 +6,6 @@ resources: - crd/trustyai.opendatahub.io_evalhubs.yaml - rbac/evalhub_auth_reviewer_role.yaml - rbac/evalhub_collections_access_binding.yaml - - rbac/evalhub_events_binding.yaml - rbac/evalhub_events_role.yaml - rbac/evalhub_collections_access_role.yaml - rbac/evalhub_hardware_profiles_reader_role.yaml diff --git a/config/components/evalhub/rbac/evalhub_events_binding.yaml b/config/components/evalhub/rbac/evalhub_events_binding.yaml deleted file mode 100644 index 998d5909c..000000000 --- a/config/components/evalhub/rbac/evalhub_events_binding.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - labels: - app.kubernetes.io/component: evalhub - app.kubernetes.io/name: trustyai-service-operator - name: evalhub-events-binding -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: evalhub-events -subjects: -- kind: ServiceAccount - name: controller-manager - namespace: system diff --git a/controllers/evalhub/service_accounts.go b/controllers/evalhub/service_accounts.go index 1cdf0dd0b..6fb1440b1 100644 --- a/controllers/evalhub/service_accounts.go +++ b/controllers/evalhub/service_accounts.go @@ -202,6 +202,11 @@ func (r *EvalHubReconciler) createServiceAccount(ctx context.Context, instance * return err } + err = r.createEventsRoleBinding(ctx, instance, serviceAccountName) + if err != nil { + return err + } + // Always grant the API SA hardwareprofiles get/list in the applications // namespace (r.Namespace: opendatahub on ODH, redhat-ods-applications on RHOAI), // regardless of where this EvalHub instance is deployed. EvalHub reads profiles @@ -267,6 +272,10 @@ const ( jobsWriterClusterRoleName = "trustyai-service-operator-evalhub-jobs-writer" jobConfigClusterRoleName = "trustyai-service-operator-evalhub-job-config" hardwareProfilesReaderClusterRoleName = "trustyai-service-operator-evalhub-hardware-profiles-reader" + // evalhubEventsClusterRoleName grants events create/patch in tenant namespaces so the + // EvalHub server SA can emit Kubernetes Events on evaluation lifecycle transitions. + // Bound per-tenant-namespace via a RoleBinding created by reconcileTenantNamespaces. + evalhubEventsClusterRoleName = "trustyai-service-operator-evalhub-events" ) // EvalHub API access ClusterRoles for SAR-protected endpoints. @@ -627,6 +636,18 @@ func (r *EvalHubReconciler) createHardwareProfilesReaderRoleBinding(ctx context. }) } +// createEventsRoleBinding creates a RoleBinding for the API SA to the evalhub-events +// ClusterRole (events create,patch) in the instance namespace. This covers single-tenant +// mode and any transitions where resolveNamespace falls back to the server's own namespace. +// Tenant namespaces receive a separate binding via reconcileTenantNamespaces. +func (r *EvalHubReconciler) createEventsRoleBinding(ctx context.Context, instance *evalhubv1.EvalHub, serviceAccountName string) error { + return r.createGenericRoleBinding(ctx, instance, instance.Name+"-events-rb", serviceAccountName, rbacv1.RoleRef{ + Kind: "ClusterRole", + Name: evalhubEventsClusterRoleName, + APIGroup: rbacv1.GroupName, + }) +} + // generateApplicationsHardwareProfilesReaderRBName returns the RoleBinding name used to // grant the API SA hardwareprofiles get/list in the applications namespace (r.Namespace). func generateApplicationsHardwareProfilesReaderRBName(instance *evalhubv1.EvalHub) string { diff --git a/controllers/evalhub/tenant_namespaces.go b/controllers/evalhub/tenant_namespaces.go index 7377001d0..1ebe9c1c6 100644 --- a/controllers/evalhub/tenant_namespaces.go +++ b/controllers/evalhub/tenant_namespaces.go @@ -105,6 +105,18 @@ func (r *EvalHubReconciler) reconcileTenantNamespaces(ctx context.Context, insta return err } + // Create events RoleBinding for the API SA in the tenant namespace so the + // EvalHub server can emit Kubernetes Events on evaluation lifecycle transitions. + eventsRBName := normalizeDNS1123LabelValue(instance.Name + "-" + ns + "-events-rb") + if err := r.createJobRoleBinding(ctx, instance, eventsRBName, serviceAccountName, ns, rbacv1.RoleRef{ + Kind: "ClusterRole", + Name: evalhubEventsClusterRoleName, + APIGroup: rbacv1.GroupName, + }, instance.Namespace); err != nil { + log.Error(err, "Failed to create events RoleBinding in tenant namespace", "namespace", ns) + return err + } + // Create pod-logs Role and RoleBinding so the EvalHub service SA can read job pod logs. if err := r.createServicePodLogsRole(ctx, instance, ns); err != nil { log.Error(err, "Failed to create service pod-logs Role in tenant namespace", "namespace", ns) diff --git a/controllers/evalhub/unit_test.go b/controllers/evalhub/unit_test.go index 4dddb604a..d4217c31f 100644 --- a/controllers/evalhub/unit_test.go +++ b/controllers/evalhub/unit_test.go @@ -1986,6 +1986,22 @@ func TestEvalHubReconciler_reconcileTenantNamespaces(t *testing.T) { }, cm) require.NoError(t, err) assert.Equal(t, "true", cm.Annotations["service.beta.openshift.io/inject-cabundle"]) + + // Verify events RoleBinding was created in the tenant namespace so the + // EvalHub server SA can emit Kubernetes Events on lifecycle transitions. + eventsRBName := normalizeDNS1123LabelValue(evalHubName + "-" + tenantNamespace + "-events-rb") + eventsRB := &rbacv1.RoleBinding{} + err = fakeClient.Get(ctx, types.NamespacedName{ + Name: eventsRBName, + Namespace: tenantNamespace, + }, eventsRB) + require.NoError(t, err, "events RoleBinding should exist in tenant namespace") + assert.Equal(t, "ClusterRole", eventsRB.RoleRef.Kind) + assert.Equal(t, evalhubEventsClusterRoleName, eventsRB.RoleRef.Name) + require.Len(t, eventsRB.Subjects, 1) + assert.Equal(t, "ServiceAccount", eventsRB.Subjects[0].Kind) + assert.Equal(t, evalHubName+"-service", eventsRB.Subjects[0].Name) + assert.Equal(t, instanceNamespace, eventsRB.Subjects[0].Namespace) }) t.Run("should skip terminating tenant namespace", func(t *testing.T) { diff --git a/policy/rbac.rego b/policy/rbac.rego index 1e780c3dd..d40bfff3c 100644 --- a/policy/rbac.rego +++ b/policy/rbac.rego @@ -31,7 +31,6 @@ expected_crbs := { # --- component: evalhub (prefixed overlays: odh, rhoai, dev, testing) --- "trustyai-service-operator-evalhub-manager-rolebinding": "trustyai-service-operator-evalhub-manager-role", - "trustyai-service-operator-evalhub-events-binding": "trustyai-service-operator-evalhub-events", "trustyai-service-operator-evalhub-collections-access-binding": "trustyai-service-operator-evalhub-collections-access", "trustyai-service-operator-evalhub-providers-access-binding": "trustyai-service-operator-evalhub-providers-access", "trustyai-service-operator-evalhub-mlflow-access-binding": "trustyai-service-operator-evalhub-mlflow-access", @@ -41,7 +40,6 @@ expected_crbs := { # --- component: evalhub (un-prefixed overlay: evalhub-only) --- "evalhub-manager-rolebinding": "trustyai-service-operator-evalhub-manager-role", - "evalhub-events-binding": "evalhub-events", "evalhub-collections-access-binding": "evalhub-collections-access", "evalhub-providers-access-binding": "evalhub-providers-access", "evalhub-mlflow-access-binding": "evalhub-mlflow-access",