From ef0fd1cc442bb9644bfb970439a274dacc4251d3 Mon Sep 17 00:00:00 2001 From: blublinsky Date: Thu, 9 Jul 2026 10:20:59 +0100 Subject: [PATCH] OLS-3520: Grant operator RBAC to create alerts adapter proposals ClusterRole --- .ai/spec/how/project-structure.md | 2 +- .ai/spec/how/reconciliation.md | 2 +- .ai/spec/what/bundle-composition.md | 2 +- .ai/spec/what/reconciliation.md | 6 +- ARCHITECTURE.md | 8 +- ...tspeed-operator.clusterserviceversion.yaml | 9 +- config/rbac/role.yaml | 8 ++ internal/controller/alertsadapter/assets.go | 16 +-- .../controller/alertsadapter/assets_test.go | 8 +- .../controller/alertsadapter/reconciler.go | 103 ++++++++++++------ .../alertsadapter/reconciler_test.go | 12 +- internal/controller/olsconfig_controller.go | 2 + internal/controller/utils/constants.go | 10 +- internal/controller/utils/errors.go | 52 ++++----- 14 files changed, 146 insertions(+), 94 deletions(-) diff --git a/.ai/spec/how/project-structure.md b/.ai/spec/how/project-structure.md index 7c70b2c45..73cab82b3 100644 --- a/.ai/spec/how/project-structure.md +++ b/.ai/spec/how/project-structure.md @@ -146,7 +146,7 @@ Embeds `client.Client` and adds getter methods for: The OLSConfig CR uses finalizer `ols.openshift.io/finalizer` (defined in `utils.OLSConfigFinalizer`). On deletion: 1. Remove chat console UI (deactivate plugin, delete ConsolePlugin CR) 2. Remove agentic console UI (deactivate plugin, delete ConsolePlugin CR) -3. Remove alerts adapter operand resources (`alertsadapter.RemoveAlertsAdapter()`: deployment, namespaced RBAC, SA, NetworkPolicy, monitoring RoleBinding; proposals ClusterRole/ClusterRoleBinding when the platform permits delete) +3. Remove alerts adapter operand resources (`alertsadapter.RemoveAlertsAdapter()`: deployment, namespaced RBAC, SA, NetworkPolicy, monitoring RoleBinding; AgenticRun ClusterRole/ClusterRoleBinding when the platform permits delete) 3. List all owned resources via owner references 4. Explicitly delete owned resources 5. Wait up to 3 minutes for deletion (poll every 5 seconds) diff --git a/.ai/spec/how/reconciliation.md b/.ai/spec/how/reconciliation.md index b7ff7e560..e730f98d2 100644 --- a/.ai/spec/how/reconciliation.md +++ b/.ai/spec/how/reconciliation.md @@ -50,7 +50,7 @@ Two ownership models: 2. **External resources**: Watches() with custom predicates. Annotation-based filtering. Secret/ConfigMap handlers compare data and trigger deployment restarts. ### Finalizer Cleanup -The `finalizeOLSConfig()` method removes Console UI, deletes alerts adapter operand resources via `alertsadapter.RemoveAlertsAdapter()` (deployment, namespaced RBAC, SA, NetworkPolicy, cross-namespace monitoring RoleBinding; proposals ClusterRole/ClusterRoleBinding when permitted—may remain on managed OpenShift if admission webhook blocks delete), then uses `listOwnedResources()` which queries every resource type by owner reference UID (not labels). This is more reliable than label-based cleanup. The wait loop polls with a fixed interval and timeout, using `wait.PollUntilContextTimeout`. +The `finalizeOLSConfig()` method removes Console UI, deletes alerts adapter operand resources via `alertsadapter.RemoveAlertsAdapter()` (deployment, namespaced RBAC, SA, NetworkPolicy, cross-namespace monitoring RoleBinding; AgenticRun ClusterRole/ClusterRoleBinding when permitted—may remain on managed OpenShift if admission webhook blocks delete), then uses `listOwnedResources()` which queries every resource type by owner reference UID (not labels). This is more reliable than label-based cleanup. The wait loop polls with a fixed interval and timeout, using `wait.PollUntilContextTimeout`. ### Status Update Mechanics `UpdateStatusCondition()` uses `retry.RetryOnConflict` with `client.MergeFrom` patch. It preserves `LastTransitionTime` for conditions whose status hasn't changed. It re-fetches the CR before each update attempt to get the latest ResourceVersion. diff --git a/.ai/spec/what/bundle-composition.md b/.ai/spec/what/bundle-composition.md index 79d407dec..e743c8712 100644 --- a/.ai/spec/what/bundle-composition.md +++ b/.ai/spec/what/bundle-composition.md @@ -27,7 +27,7 @@ The lightspeed-operator OLM bundle installs both the lightspeed-operator control 11. The two controllers share no runtime state. They reconcile different CRDs (`ols.openshift.io` vs `agentic.openshift.io`). 12. Feature gates on `OLSConfig` (`MCPServer`, `ToolFiltering`) have no effect on the agentic controller. -13. The agentic controller is effectively inert until its CRs are created — it watches for `AgenticOLSConfig`, `Proposal`, and related CRs, but takes no action without them. +13. The agentic controller is effectively inert until its CRs are created — it watches for `AgenticOLSConfig`, `AgenticRun`, and related CRs, but takes no action without them. ### Console Plugins diff --git a/.ai/spec/what/reconciliation.md b/.ai/spec/what/reconciliation.md index 92d6c30ae..bdcf737cd 100644 --- a/.ai/spec/what/reconciliation.md +++ b/.ai/spec/what/reconciliation.md @@ -20,8 +20,8 @@ The operator reconciles the OLSConfig CR into Kubernetes resources through a two 9. Five component groups are reconciled in Phase 1: chat Console UI, agentic console plugin, PostgreSQL, the application server, and (when enabled) the agentic alerts adapter. 10. All Phase 1 resource groups are independent and can be reconciled in any order. 11. If any Phase 1 resource fails, the operator continues reconciling the remaining resources, then reports all failures in the CR status with ResourceReconciliation conditions. -11a. Alerts adapter (OLS-3348) is **opt-in** via `spec.ols.deployment.alertsAdapter.configMapRef`. When unset, `ReconcileAlertsAdapterResources()` calls `RemoveAlertsAdapter()` to delete operator-managed operand resources (deployment, SA, namespaced RBAC, NetworkPolicy, monitoring RoleBinding; proposals ClusterRole/ClusterRoleBinding when the platform allows delete) and Phase 2 is skipped with `AlertsAdapterReady=True`, `Reason=NotConfigured`. -11b. When `configMapRef` is set, Phase 1 reconciles: ServiceAccount, ClusterRole (`agentic.openshift.io/proposals`: create, list, get), ClusterRoleBinding, legacy config Role/RoleBinding cleanup, RoleBinding in `openshift-monitoring` (binds SA to `monitoring-alertmanager-view`), NetworkPolicy. The operator does not create, update, or validate ConfigMap data. When the referenced ConfigMap exists, Phase 2 mounts it at `/etc/alerts-adapter`; when absent, no config volume is mounted. The adapter reads `config.yaml` and uses built-in defaults when the file is missing or invalid. +11a. Alerts adapter (OLS-3348) is **opt-in** via `spec.ols.deployment.alertsAdapter.configMapRef`. When unset, `ReconcileAlertsAdapterResources()` calls `RemoveAlertsAdapter()` to delete operator-managed operand resources (deployment, SA, namespaced RBAC, NetworkPolicy, monitoring RoleBinding; AgenticRun ClusterRole/ClusterRoleBinding when the platform allows delete) and Phase 2 is skipped with `AlertsAdapterReady=True`, `Reason=NotConfigured`. +11b. When `configMapRef` is set, Phase 1 reconciles: ServiceAccount, ClusterRole (`agentic.openshift.io/agenticruns`: create, list, get), ClusterRoleBinding, legacy config Role/RoleBinding cleanup, RoleBinding in `openshift-monitoring` (binds SA to `monitoring-alertmanager-view`), NetworkPolicy. The operator does not create, update, or validate ConfigMap data. When the referenced ConfigMap exists, Phase 2 mounts it at `/etc/alerts-adapter`; when absent, no config volume is mounted. The adapter reads `config.yaml` and uses built-in defaults when the file is missing or invalid. 11c. Agentic console Phase 1 resources: ServiceAccount, ConfigMap (nginx.conf), NetworkPolicy. ### Phase 2: Deployments and Status @@ -38,7 +38,7 @@ The operator reconciles the OLSConfig CR into Kubernetes resources through a two ### Finalizer Lifecycle 19. On CR creation: add finalizer, return immediately (controller-runtime auto-requeues). 20. On CR deletion: run finalizer cleanup before removing finalizer. -21. Finalizer cleanup sequence: remove chat Console UI from Console CR, delete chat ConsolePlugin CR, remove agentic console plugin from Console CR, delete agentic ConsolePlugin CR (`agenticconsole.RemoveAgenticConsole()`), delete alerts adapter operand resources via `alertsadapter.RemoveAlertsAdapter()` (deployment, namespaced RBAC, SA, NetworkPolicy, monitoring RoleBinding; proposals ClusterRole/ClusterRoleBinding when permitted), list all owned resources by owner reference, explicitly delete them, wait for deletion (polling with timeout). +21. Finalizer cleanup sequence: remove chat Console UI from Console CR, delete chat ConsolePlugin CR, remove agentic console plugin from Console CR, delete agentic ConsolePlugin CR (`agenticconsole.RemoveAgenticConsole()`), delete alerts adapter operand resources via `alertsadapter.RemoveAlertsAdapter()` (deployment, namespaced RBAC, SA, NetworkPolicy, monitoring RoleBinding; AgenticRun ClusterRole/ClusterRoleBinding when permitted), list all owned resources by owner reference, explicitly delete them, wait for deletion (polling with timeout). 22. If cleanup times out, the finalizer is removed anyway to prevent the CR from being stuck in Terminating state. 23. Console UI and agentic component removal errors during finalization are logged but do not block finalization. diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index f95abb44d..4e1a7a4fb 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -98,13 +98,13 @@ Provides clean contract between main controller and component packages: ### Alerts Adapter Package (`internal/controller/alertsadapter`) -**Purpose:** Manages the agentic alerts adapter that polls Alertmanager and creates `Proposal` CRs for firing alerts. +**Purpose:** Manages the agentic alerts adapter that polls Alertmanager and creates `AgenticRun` CRs for firing alerts. **Opt-in:** Enabled only when `spec.ols.deployment.alertsAdapter.configMapRef` is set (non-empty name). When unset, Phase 1 calls `RemoveAlertsAdapter()` to tear down operand resources and Phase 2 sets `AlertsAdapterReady=True` with `Reason=NotConfigured`. **Entry Points:** `ReconcileAlertsAdapterResources()` (Phase 1), `ReconcileAlertsAdapterDeployment()` (Phase 2), `RemoveAlertsAdapter()` (operand teardown on disable and during finalization), `RestartAlertsAdapter()` (rolling restart on deployment spec or runtime ConfigMap changes). -**Phase 1 resources (when enabled):** ServiceAccount, ClusterRole/ClusterRoleBinding for `agentic.openshift.io/proposals`, legacy config Role/RoleBinding cleanup (removed from reconcile; deleted if still present), RoleBinding in `openshift-monitoring` to `monitoring-alertmanager-view`, NetworkPolicy. The operator does not create, update, or validate user ConfigMap data. +**Phase 1 resources (when enabled):** ServiceAccount, ClusterRole/ClusterRoleBinding for `agentic.openshift.io/agenticruns`, legacy config Role/RoleBinding cleanup (removed from reconcile; deleted if still present), RoleBinding in `openshift-monitoring` to `monitoring-alertmanager-view`, NetworkPolicy. The operator does not create, update, or validate user ConfigMap data. **Runtime config:** User creates the ConfigMap (see [adapter manifests](https://github.com/openshift/lightspeed-agentic-alerts-adapter/tree/main/manifests)). When the referenced ConfigMap exists, it is mounted read-only at `/etc/alerts-adapter`; when absent, no config volume is mounted. The adapter reads `config.yaml` from that path and uses built-in defaults when the file is missing or invalid. ConfigMap data changes trigger a deployment restart via the external ConfigMap watcher (`RestartAlertsAdapter`). @@ -167,7 +167,7 @@ The operator uses a finalizer (`ols.openshift.io/finalizer`) to ensure proper cl **Why Needed:** - **Console plugin cleanup**: `ConsolePlugin` is cluster-scoped and not cascade-deleted by owner references; chat and agentic plugins must be deactivated in the Console CR -- **Alerts adapter cleanup**: RoleBinding in `openshift-monitoring` is outside the operator namespace; `RemoveAlertsAdapter()` also deletes deployment, namespaced RBAC, SA, and NetworkPolicy when the operand is disabled or during finalization. Proposals ClusterRole/ClusterRoleBinding deletion may be blocked on managed OpenShift clusters (admission webhook); the operator logs and continues. +- **Alerts adapter cleanup**: RoleBinding in `openshift-monitoring` is outside the operator namespace; `RemoveAlertsAdapter()` also deletes deployment, namespaced RBAC, SA, and NetworkPolicy when the operand is disabled or during finalization. AgenticRun ClusterRole/ClusterRoleBinding deletion may be blocked on managed OpenShift clusters (admission webhook); the operator logs and continues. - **PVC cleanup**: PersistentVolumeClaims can block deletion if not properly released - **Race condition prevention**: Ensures complete cleanup before CR can be recreated (important for tests and sequential deployments) @@ -195,7 +195,7 @@ if !olsconfig.DeletionTimestamp.IsZero() { **Cleanup Sequence** (`finalizeOLSConfig`): 1. **Remove chat console UI**: Deactivate plugin from Console CR, delete ConsolePlugin CR 2. **Remove agentic console UI**: Deactivate plugin from Console CR, delete ConsolePlugin CR -3. **Remove alerts adapter operand**: Delete deployment, namespaced RBAC, SA, NetworkPolicy, monitoring RoleBinding, and attempt proposals ClusterRoleBinding/ClusterRole deletion (`alertsadapter.RemoveAlertsAdapter()`). ClusterRoleBinding deletion may be blocked on managed OpenShift; remaining cluster RBAC is harmless when the operand is disabled. +3. **Remove alerts adapter operand**: Delete deployment, namespaced RBAC, SA, NetworkPolicy, monitoring RoleBinding, and attempt AgenticRun ClusterRoleBinding/ClusterRole deletion (`alertsadapter.RemoveAlertsAdapter()`). ClusterRoleBinding deletion may be blocked on managed OpenShift; remaining cluster RBAC is harmless when the operand is disabled. 4. **Wait for owned resources**: Poll for up to 3 minutes until deployments, services, PVCs are deleted (cascade deletion) 5. **Remove finalizer**: Allows Kubernetes to remove CR from etcd diff --git a/bundle/manifests/lightspeed-operator.clusterserviceversion.yaml b/bundle/manifests/lightspeed-operator.clusterserviceversion.yaml index aeda1c8b2..a65c7f261 100644 --- a/bundle/manifests/lightspeed-operator.clusterserviceversion.yaml +++ b/bundle/manifests/lightspeed-operator.clusterserviceversion.yaml @@ -423,7 +423,6 @@ spec: - tls.key: Private key (PEM format) - REQUIRED - ca.crt: CA certificate for console proxy trust (PEM format) - OPTIONAL - If ca.crt is not provided, the OpenShift Console proxy will use the default system trust store. displayName: TLS Certificate Secret Reference path: ols.tlsConfig.keyCertSecretRef @@ -743,6 +742,14 @@ spec: - get - list - watch + - apiGroups: + - agentic.openshift.io + resources: + - agenticruns + verbs: + - create + - get + - list - apiGroups: - apps resources: diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 80441ddbb..332d9cf52 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -55,6 +55,14 @@ rules: - get - list - watch +- apiGroups: + - agentic.openshift.io + resources: + - agenticruns + verbs: + - create + - get + - list - apiGroups: - apps resources: diff --git a/internal/controller/alertsadapter/assets.go b/internal/controller/alertsadapter/assets.go index da35a2d9c..df9cd07df 100644 --- a/internal/controller/alertsadapter/assets.go +++ b/internal/controller/alertsadapter/assets.go @@ -22,17 +22,17 @@ func GenerateServiceAccount(r reconciler.Reconciler, cr *olsv1alpha1.OLSConfig) return utils.GenerateServiceAccount(r, cr, utils.AlertsAdapterServiceAccountName) } -// GenerateProposalsClusterRole generates the ClusterRole granting proposal create/list/get access. -func GenerateProposalsClusterRole(r reconciler.Reconciler, cr *olsv1alpha1.OLSConfig) (*rbacv1.ClusterRole, error) { +// GenerateAgenticRunsClusterRole generates the ClusterRole granting AgenticRun create/list/get access. +func GenerateAgenticRunsClusterRole(r reconciler.Reconciler, cr *olsv1alpha1.OLSConfig) (*rbacv1.ClusterRole, error) { role := rbacv1.ClusterRole{ ObjectMeta: metav1.ObjectMeta{ - Name: utils.AlertsAdapterProposalsClusterRoleName, + Name: utils.AlertsAdapterAgenticRunsClusterRoleName, Labels: utils.GenerateAlertsAdapterSelectorLabels(), }, Rules: []rbacv1.PolicyRule{ { APIGroups: []string{"agentic.openshift.io"}, - Resources: []string{"proposals"}, + Resources: []string{"agenticruns"}, Verbs: []string{"create", "list", "get"}, }, }, @@ -45,11 +45,11 @@ func GenerateProposalsClusterRole(r reconciler.Reconciler, cr *olsv1alpha1.OLSCo return &role, nil } -// GenerateProposalsClusterRoleBinding binds the alerts adapter ServiceAccount to the proposals ClusterRole. -func GenerateProposalsClusterRoleBinding(r reconciler.Reconciler, cr *olsv1alpha1.OLSConfig) (*rbacv1.ClusterRoleBinding, error) { +// GenerateAgenticRunsClusterRoleBinding binds the alerts adapter ServiceAccount to the AgenticRun ClusterRole. +func GenerateAgenticRunsClusterRoleBinding(r reconciler.Reconciler, cr *olsv1alpha1.OLSConfig) (*rbacv1.ClusterRoleBinding, error) { rb := rbacv1.ClusterRoleBinding{ ObjectMeta: metav1.ObjectMeta{ - Name: utils.AlertsAdapterProposalsClusterRoleBindingName, + Name: utils.AlertsAdapterAgenticRunsClusterRoleBindingName, Labels: utils.GenerateAlertsAdapterSelectorLabels(), }, Subjects: []rbacv1.Subject{ @@ -62,7 +62,7 @@ func GenerateProposalsClusterRoleBinding(r reconciler.Reconciler, cr *olsv1alpha RoleRef: rbacv1.RoleRef{ APIGroup: "rbac.authorization.k8s.io", Kind: "ClusterRole", - Name: utils.AlertsAdapterProposalsClusterRoleName, + Name: utils.AlertsAdapterAgenticRunsClusterRoleName, }, } diff --git a/internal/controller/alertsadapter/assets_test.go b/internal/controller/alertsadapter/assets_test.go index 08f910390..52932a198 100644 --- a/internal/controller/alertsadapter/assets_test.go +++ b/internal/controller/alertsadapter/assets_test.go @@ -27,13 +27,13 @@ var _ = Describe("Alerts adapter assets", func() { Expect(sa.Namespace).To(Equal(utils.OLSNamespaceDefault)) }) - It("should generate the proposals ClusterRole", func() { - role, err := GenerateProposalsClusterRole(testReconcilerInstance, cr) + It("should generate the agenticruns ClusterRole", func() { + role, err := GenerateAgenticRunsClusterRole(testReconcilerInstance, cr) Expect(err).NotTo(HaveOccurred()) - Expect(role.Name).To(Equal(utils.AlertsAdapterProposalsClusterRoleName)) + Expect(role.Name).To(Equal(utils.AlertsAdapterAgenticRunsClusterRoleName)) Expect(role.Rules).To(ContainElement(rbacv1.PolicyRule{ APIGroups: []string{"agentic.openshift.io"}, - Resources: []string{"proposals"}, + Resources: []string{"agenticruns"}, Verbs: []string{"create", "list", "get"}, })) }) diff --git a/internal/controller/alertsadapter/reconciler.go b/internal/controller/alertsadapter/reconciler.go index 940f76e80..1e9467430 100644 --- a/internal/controller/alertsadapter/reconciler.go +++ b/internal/controller/alertsadapter/reconciler.go @@ -1,5 +1,5 @@ // Package alertsadapter reconciles the agentic alerts adapter operand that polls -// Alertmanager and creates Proposal CRs for firing alerts. +// Alertmanager and creates AgenticRun CRs for firing alerts. package alertsadapter import ( @@ -33,8 +33,9 @@ func ReconcileAlertsAdapterResources(r reconciler.Reconciler, ctx context.Contex tasks := []utils.ReconcileTask{ {Name: "reconcile alerts adapter ServiceAccount", Task: reconcileServiceAccount}, - {Name: "reconcile alerts adapter proposals ClusterRole", Task: reconcileProposalsClusterRole}, - {Name: "reconcile alerts adapter proposals ClusterRoleBinding", Task: reconcileProposalsClusterRoleBinding}, + {Name: "reconcile alerts adapter agenticruns ClusterRole", Task: reconcileAgenticRunsClusterRole}, + {Name: "reconcile alerts adapter agenticruns ClusterRoleBinding", Task: reconcileAgenticRunsClusterRoleBinding}, + {Name: "remove legacy alerts adapter proposals cluster RBAC", Task: removeLegacyProposalsClusterRBAC}, {Name: "remove legacy alerts adapter config RoleBinding", Task: removeLegacyConfigRoleBinding}, {Name: "remove legacy alerts adapter config Role", Task: removeLegacyConfigRole}, {Name: "reconcile alerts adapter Alertmanager RoleBinding", Task: reconcileAlertmanagerRoleBinding}, @@ -84,7 +85,7 @@ func RemoveAlertsAdapter(r reconciler.Reconciler, ctx context.Context) error { {Name: "delete alerts adapter config Role", Task: deleteConfigRole}, {Name: "delete alerts adapter service account", Task: deleteServiceAccount}, {Name: "delete alerts adapter Alertmanager RoleBinding", Task: deleteAlertmanagerRoleBinding}, - {Name: "delete alerts adapter proposals cluster RBAC", Task: deleteProposalsClusterRBAC}, + {Name: "delete alerts adapter agenticruns cluster RBAC", Task: deleteAgenticRunsClusterRBAC}, } var errs []error @@ -124,47 +125,47 @@ func reconcileServiceAccount(r reconciler.Reconciler, ctx context.Context, cr *o return nil } -func reconcileProposalsClusterRole(r reconciler.Reconciler, ctx context.Context, cr *olsv1alpha1.OLSConfig) error { - role, err := GenerateProposalsClusterRole(r, cr) +func reconcileAgenticRunsClusterRole(r reconciler.Reconciler, ctx context.Context, cr *olsv1alpha1.OLSConfig) error { + role, err := GenerateAgenticRunsClusterRole(r, cr) if err != nil { - return fmt.Errorf("%s: %w", utils.ErrGenerateAlertsAdapterProposalsClusterRole, err) + return fmt.Errorf("%s: %w", utils.ErrGenerateAlertsAdapterAgenticRunsClusterRole, err) } foundRole := &rbacv1.ClusterRole{} err = r.Get(ctx, client.ObjectKey{Name: role.Name}, foundRole) if err != nil && apierrors.IsNotFound(err) { - r.GetLogger().Info("creating alerts adapter proposals cluster role", "ClusterRole", role.Name) + r.GetLogger().Info("creating alerts adapter agenticruns cluster role", "ClusterRole", role.Name) if err := r.Create(ctx, role); err != nil { - return fmt.Errorf("%s: %w", utils.ErrCreateAlertsAdapterProposalsClusterRole, err) + return fmt.Errorf("%s: %w", utils.ErrCreateAlertsAdapterAgenticRunsClusterRole, err) } return nil } else if err != nil { - return fmt.Errorf("%s: %w", utils.ErrGetAlertsAdapterProposalsClusterRole, err) + return fmt.Errorf("%s: %w", utils.ErrGetAlertsAdapterAgenticRunsClusterRole, err) } - r.GetLogger().Info("alerts adapter proposals cluster role reconciled", "ClusterRole", role.Name) + r.GetLogger().Info("alerts adapter agenticruns cluster role reconciled", "ClusterRole", role.Name) return nil } -func reconcileProposalsClusterRoleBinding(r reconciler.Reconciler, ctx context.Context, cr *olsv1alpha1.OLSConfig) error { - rb, err := GenerateProposalsClusterRoleBinding(r, cr) +func reconcileAgenticRunsClusterRoleBinding(r reconciler.Reconciler, ctx context.Context, cr *olsv1alpha1.OLSConfig) error { + rb, err := GenerateAgenticRunsClusterRoleBinding(r, cr) if err != nil { - return fmt.Errorf("%s: %w", utils.ErrGenerateAlertsAdapterProposalsClusterRoleBinding, err) + return fmt.Errorf("%s: %w", utils.ErrGenerateAlertsAdapterAgenticRunsClusterRoleBinding, err) } foundRB := &rbacv1.ClusterRoleBinding{} err = r.Get(ctx, client.ObjectKey{Name: rb.Name}, foundRB) if err != nil && apierrors.IsNotFound(err) { - r.GetLogger().Info("creating alerts adapter proposals cluster role binding", "ClusterRoleBinding", rb.Name) + r.GetLogger().Info("creating alerts adapter agenticruns cluster role binding", "ClusterRoleBinding", rb.Name) if err := r.Create(ctx, rb); err != nil { - return fmt.Errorf("%s: %w", utils.ErrCreateAlertsAdapterProposalsClusterRoleBinding, err) + return fmt.Errorf("%s: %w", utils.ErrCreateAlertsAdapterAgenticRunsClusterRoleBinding, err) } return nil } else if err != nil { - return fmt.Errorf("%s: %w", utils.ErrGetAlertsAdapterProposalsClusterRoleBinding, err) + return fmt.Errorf("%s: %w", utils.ErrGetAlertsAdapterAgenticRunsClusterRoleBinding, err) } - r.GetLogger().Info("alerts adapter proposals cluster role binding reconciled", "ClusterRoleBinding", rb.Name) + r.GetLogger().Info("alerts adapter agenticruns cluster role binding reconciled", "ClusterRoleBinding", rb.Name) return nil } @@ -411,54 +412,86 @@ func isOpenShiftManagedCRBDeleteDenied(err error) bool { (strings.Contains(msg, "Deleting ClusterRoleBinding") && strings.Contains(msg, "is not allowed")) } -func deleteProposalsClusterRBAC(r reconciler.Reconciler, ctx context.Context) error { +func deleteAgenticRunsClusterRBAC(r reconciler.Reconciler, ctx context.Context) error { rb := &rbacv1.ClusterRoleBinding{} - err := r.Get(ctx, client.ObjectKey{Name: utils.AlertsAdapterProposalsClusterRoleBindingName}, rb) + err := r.Get(ctx, client.ObjectKey{Name: utils.AlertsAdapterAgenticRunsClusterRoleBindingName}, rb) if err != nil { if apierrors.IsNotFound(err) { - r.GetLogger().Info("alerts adapter proposals ClusterRoleBinding not found, skip deletion") - return deleteProposalsClusterRole(r, ctx) + r.GetLogger().Info("alerts adapter agenticruns ClusterRoleBinding not found, skip deletion") + return deleteAgenticRunsClusterRole(r, ctx) } - return fmt.Errorf("%s: %w", utils.ErrGetAlertsAdapterProposalsClusterRoleBinding, err) + return fmt.Errorf("%s: %w", utils.ErrGetAlertsAdapterAgenticRunsClusterRoleBinding, err) } if err := r.Delete(ctx, rb); err != nil { if apierrors.IsNotFound(err) { - return deleteProposalsClusterRole(r, ctx) + return deleteAgenticRunsClusterRole(r, ctx) } if isOpenShiftManagedCRBDeleteDenied(err) { r.GetLogger().Info( - "alerts adapter proposals ClusterRoleBinding deletion blocked by OpenShift; deleting ClusterRole to remove effective permissions", + "alerts adapter agenticruns ClusterRoleBinding deletion blocked by OpenShift; deleting ClusterRole to remove effective permissions", "ClusterRoleBinding", rb.Name, ) - return deleteProposalsClusterRole(r, ctx) + return deleteAgenticRunsClusterRole(r, ctx) } - return fmt.Errorf("failed to delete alerts adapter proposals ClusterRoleBinding: %w", err) + return fmt.Errorf("failed to delete alerts adapter agenticruns ClusterRoleBinding: %w", err) } - r.GetLogger().Info("alerts adapter proposals ClusterRoleBinding deleted") - return deleteProposalsClusterRole(r, ctx) + r.GetLogger().Info("alerts adapter agenticruns ClusterRoleBinding deleted") + return deleteAgenticRunsClusterRole(r, ctx) } -func deleteProposalsClusterRole(r reconciler.Reconciler, ctx context.Context) error { +func deleteAgenticRunsClusterRole(r reconciler.Reconciler, ctx context.Context) error { role := &rbacv1.ClusterRole{} - err := r.Get(ctx, client.ObjectKey{Name: utils.AlertsAdapterProposalsClusterRoleName}, role) + err := r.Get(ctx, client.ObjectKey{Name: utils.AlertsAdapterAgenticRunsClusterRoleName}, role) if err != nil { if apierrors.IsNotFound(err) { - r.GetLogger().Info("alerts adapter proposals ClusterRole not found, skip deletion") + r.GetLogger().Info("alerts adapter agenticruns ClusterRole not found, skip deletion") return nil } - return fmt.Errorf("%s: %w", utils.ErrGetAlertsAdapterProposalsClusterRole, err) + return fmt.Errorf("%s: %w", utils.ErrGetAlertsAdapterAgenticRunsClusterRole, err) } if err := r.Delete(ctx, role); err != nil { if apierrors.IsNotFound(err) { return nil } - return fmt.Errorf("failed to delete alerts adapter proposals ClusterRole: %w", err) + return fmt.Errorf("failed to delete alerts adapter agenticruns ClusterRole: %w", err) } - r.GetLogger().Info("alerts adapter proposals ClusterRole deleted") + r.GetLogger().Info("alerts adapter agenticruns ClusterRole deleted") + return nil +} + +// removeLegacyProposalsClusterRBAC deletes pre-OLS-3475 ClusterRole/Binding names after Proposal→AgenticRun rename. +func removeLegacyProposalsClusterRBAC(r reconciler.Reconciler, ctx context.Context, _ *olsv1alpha1.OLSConfig) error { + rb := &rbacv1.ClusterRoleBinding{} + err := r.Get(ctx, client.ObjectKey{Name: utils.AlertsAdapterLegacyProposalsClusterRoleName}, rb) + if err == nil { + delErr := r.Delete(ctx, rb) + if delErr != nil && !apierrors.IsNotFound(delErr) && !isOpenShiftManagedCRBDeleteDenied(delErr) { + return fmt.Errorf("failed to delete legacy alerts adapter proposals ClusterRoleBinding: %w", delErr) + } + if delErr == nil { + r.GetLogger().Info("deleted legacy alerts adapter proposals ClusterRoleBinding", "ClusterRoleBinding", rb.Name) + } + } else if !apierrors.IsNotFound(err) { + return fmt.Errorf("failed to get legacy alerts adapter proposals ClusterRoleBinding: %w", err) + } + + role := &rbacv1.ClusterRole{} + err = r.Get(ctx, client.ObjectKey{Name: utils.AlertsAdapterLegacyProposalsClusterRoleName}, role) + if err != nil { + if apierrors.IsNotFound(err) { + return nil + } + return fmt.Errorf("failed to get legacy alerts adapter proposals ClusterRole: %w", err) + } + + if delErr := r.Delete(ctx, role); delErr != nil && !apierrors.IsNotFound(delErr) { + return fmt.Errorf("failed to delete legacy alerts adapter proposals ClusterRole: %w", delErr) + } + r.GetLogger().Info("deleted legacy alerts adapter proposals ClusterRole", "ClusterRole", role.Name) return nil } diff --git a/internal/controller/alertsadapter/reconciler_test.go b/internal/controller/alertsadapter/reconciler_test.go index 1a85c7c2a..e1dbbfe28 100644 --- a/internal/controller/alertsadapter/reconciler_test.go +++ b/internal/controller/alertsadapter/reconciler_test.go @@ -41,8 +41,8 @@ var _ = Describe("Alerts adapter reconciler", Ordered, func() { It("detects OpenShift managed ClusterRoleBinding delete denial", func() { webhookErr := apierrors.NewForbidden( schema.GroupResource{Group: "rbac.authorization.k8s.io", Resource: "clusterrolebindings"}, - "lightspeed-agentic-alerts-adapter-proposals", - fmt.Errorf(`admission webhook "clusterrolebindings-validation.managed.openshift.io" denied the request: Deleting ClusterRoleBinding lightspeed-agentic-alerts-adapter-proposals is not allowed`), + "lightspeed-agentic-alerts-adapter-agenticruns", + fmt.Errorf(`admission webhook "clusterrolebindings-validation.managed.openshift.io" denied the request: Deleting ClusterRoleBinding lightspeed-agentic-alerts-adapter-agenticruns is not allowed`), ) Expect(isOpenShiftManagedCRBDeleteDenied(webhookErr)).To(BeTrue()) Expect(isOpenShiftManagedCRBDeleteDenied(fmt.Errorf("some other error"))).To(BeFalse()) @@ -165,14 +165,14 @@ var _ = Describe("Alerts adapter reconciler", Ordered, func() { expectOwnedByOLSConfig(sa) }) - It("should create the proposals ClusterRole and ClusterRoleBinding", func() { + It("should create the agenticruns ClusterRole and ClusterRoleBinding", func() { role := &rbacv1.ClusterRole{} - err := k8sClient.Get(ctx, types.NamespacedName{Name: utils.AlertsAdapterProposalsClusterRoleName}, role) + err := k8sClient.Get(ctx, types.NamespacedName{Name: utils.AlertsAdapterAgenticRunsClusterRoleName}, role) Expect(err).NotTo(HaveOccurred()) expectOwnedByOLSConfig(role) rb := &rbacv1.ClusterRoleBinding{} - err = k8sClient.Get(ctx, types.NamespacedName{Name: utils.AlertsAdapterProposalsClusterRoleBindingName}, rb) + err = k8sClient.Get(ctx, types.NamespacedName{Name: utils.AlertsAdapterAgenticRunsClusterRoleBindingName}, rb) Expect(err).NotTo(HaveOccurred()) expectOwnedByOLSConfig(rb) }) @@ -277,7 +277,7 @@ var _ = Describe("Alerts adapter reconciler", Ordered, func() { Expect(err).To(HaveOccurred()) role := &rbacv1.ClusterRole{} - err = k8sClient.Get(ctx, types.NamespacedName{Name: utils.AlertsAdapterProposalsClusterRoleName}, role) + err = k8sClient.Get(ctx, types.NamespacedName{Name: utils.AlertsAdapterAgenticRunsClusterRoleName}, role) Expect(err).To(HaveOccurred()) }) }) diff --git a/internal/controller/olsconfig_controller.go b/internal/controller/olsconfig_controller.go index 2acae569b..3b640d91f 100644 --- a/internal/controller/olsconfig_controller.go +++ b/internal/controller/olsconfig_controller.go @@ -129,6 +129,8 @@ type OLSConfigReconciler struct { // Modify console CR to activate console plugin // +kubebuilder:rbac:groups=operator.openshift.io,resources=consoles,verbs=watch;list;get;update // +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=clusterroles;clusterrolebindings;rolebindings,verbs=get;list;create;update;patch;delete;watch +// AgenticRun API for alerts adapter ClusterRole (operator must hold permissions it grants to operands) +// +kubebuilder:rbac:groups=agentic.openshift.io,resources=agenticruns,verbs=get;list;create // +kubebuilder:rbac:groups=rbac.authorization.k8s.io,namespace=openshift-lightspeed,resources=roles;rolebindings,verbs=get;list;watch;create;update;patch;delete // NonResourceURLs for Lightspeed access control and metrics // +kubebuilder:rbac:urls=/ls-access,verbs=get diff --git a/internal/controller/utils/constants.go b/internal/controller/utils/constants.go index c0bb470a6..e2a5605a6 100644 --- a/internal/controller/utils/constants.go +++ b/internal/controller/utils/constants.go @@ -298,10 +298,12 @@ ssl_ca_file = '/etc/certs/cm-olspostgresca/service-ca.crt' AlertsAdapterContainerName = "adapter" // AlertsAdapterNetworkPolicyName is the name of the network policy for the alerts adapter AlertsAdapterNetworkPolicyName = "lightspeed-agentic-alerts-adapter" - // AlertsAdapterProposalsClusterRoleName is the cluster role granting Proposal create/list/get - AlertsAdapterProposalsClusterRoleName = "lightspeed-agentic-alerts-adapter-proposals" - // AlertsAdapterProposalsClusterRoleBindingName binds the proposals ClusterRole to the alerts adapter SA - AlertsAdapterProposalsClusterRoleBindingName = "lightspeed-agentic-alerts-adapter-proposals" + // AlertsAdapterAgenticRunsClusterRoleName is the cluster role granting AgenticRun create/list/get + AlertsAdapterAgenticRunsClusterRoleName = "lightspeed-agentic-alerts-adapter-agenticruns" + // AlertsAdapterAgenticRunsClusterRoleBindingName binds the AgenticRun ClusterRole to the alerts adapter SA + AlertsAdapterAgenticRunsClusterRoleBindingName = "lightspeed-agentic-alerts-adapter-agenticruns" + // AlertsAdapterLegacyProposalsClusterRoleName is the pre-OLS-3475 ClusterRole name removed on reconcile + AlertsAdapterLegacyProposalsClusterRoleName = "lightspeed-agentic-alerts-adapter-proposals" // AlertsAdapterAlertmanagerRoleBindingName is the RoleBinding in openshift-monitoring for Alertmanager read access AlertsAdapterAlertmanagerRoleBindingName = "lightspeed-agentic-alerts-adapter-alertmanager" // AlertsAdapterConfigMapName is the ConfigMap holding runtime adapter settings (poll interval, cooldown, tools) diff --git a/internal/controller/utils/errors.go b/internal/controller/utils/errors.go index 3db0e91ed..078fcfefc 100644 --- a/internal/controller/utils/errors.go +++ b/internal/controller/utils/errors.go @@ -114,32 +114,32 @@ const ( ErrDeleteOldPostgresSecrets = "failed to delete old OLS Postgres secret" // Alerts adapter errors - ErrCreateAlertsAdapterDeployment = "failed to create alerts adapter deployment" - ErrCreateAlertsAdapterNetworkPolicy = "failed to create alerts adapter network policy" - ErrCreateAlertsAdapterServiceAccount = "failed to create alerts adapter service account" - ErrCreateAlertsAdapterProposalsClusterRole = "failed to create alerts adapter proposals cluster role" - ErrCreateAlertsAdapterProposalsClusterRoleBinding = "failed to create alerts adapter proposals cluster role binding" - ErrCreateAlertsAdapterAlertmanagerRoleBinding = "failed to create alerts adapter alertmanager role binding" - ErrCreateAlertsAdapterConfigMap = "failed to create alerts adapter configmap" - ErrGenerateAlertsAdapterDeployment = "failed to generate alerts adapter deployment" - ErrSetAlertsAdapterDeploymentOwnerReference = "failed to set alerts adapter deployment owner reference" - ErrGenerateAlertsAdapterNetworkPolicy = "failed to generate alerts adapter network policy" - ErrGenerateAlertsAdapterServiceAccount = "failed to generate alerts adapter service account" - ErrGenerateAlertsAdapterConfigMap = "failed to generate alerts adapter configmap" - ErrGenerateAlertsAdapterProposalsClusterRole = "failed to generate alerts adapter proposals cluster role" - ErrGenerateAlertsAdapterProposalsClusterRoleBinding = "failed to generate alerts adapter proposals cluster role binding" - ErrGenerateAlertsAdapterAlertmanagerRoleBinding = "failed to generate alerts adapter alertmanager role binding" - ErrGetAlertsAdapterDeployment = "failed to get alerts adapter deployment" - ErrGetAlertsAdapterNetworkPolicy = "failed to get alerts adapter network policy" - ErrGetAlertsAdapterServiceAccount = "failed to get alerts adapter service account" - ErrGetAlertsAdapterProposalsClusterRole = "failed to get alerts adapter proposals cluster role" - ErrGetAlertsAdapterProposalsClusterRoleBinding = "failed to get alerts adapter proposals cluster role binding" - ErrGetAlertsAdapterAlertmanagerRoleBinding = "failed to get alerts adapter alertmanager role binding" - ErrGetAlertsAdapterConfigMap = "failed to get alerts adapter configmap" - ErrGetAlertsAdapterConfigRole = "failed to get alerts adapter config role" - ErrGetAlertsAdapterConfigRoleBinding = "failed to get alerts adapter config role binding" - ErrUpdateAlertsAdapterDeployment = "failed to update alerts adapter deployment" - ErrUpdateAlertsAdapterNetworkPolicy = "failed to update alerts adapter network policy" + ErrCreateAlertsAdapterDeployment = "failed to create alerts adapter deployment" + ErrCreateAlertsAdapterNetworkPolicy = "failed to create alerts adapter network policy" + ErrCreateAlertsAdapterServiceAccount = "failed to create alerts adapter service account" + ErrCreateAlertsAdapterAgenticRunsClusterRole = "failed to create alerts adapter agenticruns cluster role" + ErrCreateAlertsAdapterAgenticRunsClusterRoleBinding = "failed to create alerts adapter agenticruns cluster role binding" + ErrCreateAlertsAdapterAlertmanagerRoleBinding = "failed to create alerts adapter alertmanager role binding" + ErrCreateAlertsAdapterConfigMap = "failed to create alerts adapter configmap" + ErrGenerateAlertsAdapterDeployment = "failed to generate alerts adapter deployment" + ErrSetAlertsAdapterDeploymentOwnerReference = "failed to set alerts adapter deployment owner reference" + ErrGenerateAlertsAdapterNetworkPolicy = "failed to generate alerts adapter network policy" + ErrGenerateAlertsAdapterServiceAccount = "failed to generate alerts adapter service account" + ErrGenerateAlertsAdapterConfigMap = "failed to generate alerts adapter configmap" + ErrGenerateAlertsAdapterAgenticRunsClusterRole = "failed to generate alerts adapter agenticruns cluster role" + ErrGenerateAlertsAdapterAgenticRunsClusterRoleBinding = "failed to generate alerts adapter agenticruns cluster role binding" + ErrGenerateAlertsAdapterAlertmanagerRoleBinding = "failed to generate alerts adapter alertmanager role binding" + ErrGetAlertsAdapterDeployment = "failed to get alerts adapter deployment" + ErrGetAlertsAdapterNetworkPolicy = "failed to get alerts adapter network policy" + ErrGetAlertsAdapterServiceAccount = "failed to get alerts adapter service account" + ErrGetAlertsAdapterAgenticRunsClusterRole = "failed to get alerts adapter agenticruns cluster role" + ErrGetAlertsAdapterAgenticRunsClusterRoleBinding = "failed to get alerts adapter agenticruns cluster role binding" + ErrGetAlertsAdapterAlertmanagerRoleBinding = "failed to get alerts adapter alertmanager role binding" + ErrGetAlertsAdapterConfigMap = "failed to get alerts adapter configmap" + ErrGetAlertsAdapterConfigRole = "failed to get alerts adapter config role" + ErrGetAlertsAdapterConfigRoleBinding = "failed to get alerts adapter config role binding" + ErrUpdateAlertsAdapterDeployment = "failed to update alerts adapter deployment" + ErrUpdateAlertsAdapterNetworkPolicy = "failed to update alerts adapter network policy" // OpenShift MCP server config errors ErrCreateMCPServerConfigMap = "failed to create MCP server config configmap"