Skip to content

WIP: OLS-3273: MutatingAdmissionWebhook for ProposalApproval identity injection#201

Open
vimalk78 wants to merge 2 commits into
openshift:mainfrom
vimalk78:ols-3273-approval-webhook
Open

WIP: OLS-3273: MutatingAdmissionWebhook for ProposalApproval identity injection#201
vimalk78 wants to merge 2 commits into
openshift:mainfrom
vimalk78:ols-3273-approval-webhook

Conversation

@vimalk78

@vimalk78 vimalk78 commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a MutatingAdmissionWebhook that intercepts UPDATE operations on ProposalApproval CRs and injects the authenticated user's identity into
spec.approver. This closes the gap where audit logs and OTEL traces showed empty approver fields — the API server now stamps every approval/denial with who
did it.

Also includes a bug fix for missing audit events on deny paths (OLS-3024), discovered during end-to-end testing.

Commits

  1. Emit audit events on deny paths before rejecting proposals — All four deny paths (Analysis, Execution, Verification, Escalation) called denyProposal
    without emitting audit.approval.received or ending the approval wait span. Denied proposals were invisible in traces.

  2. Add mutating webhook for ProposalApproval identity injection — The webhook, CRD changes, audit integration, install/uninstall scripts, and
    approval.decision attribute on the human_approval OTEL span.

What changed

  • CRD: Added spec.approver (uid, username, timestamp) to ProposalApproval
  • Webhook: ProposalApprovalMutator injects spec.approver via JSON patch on every UPDATE; CREATE passes through
  • Audit: EndApprovalWait now records approver identity and approval.decision (Approved/Denied) on the human_approval span; EmitApprovalReceived
    reads from spec.approver instead of hardcoded empty strings
  • Deny paths: All four stage-deny branches now emit audit.approval.received and end the approval wait span before calling denyProposal
  • Manager wiring: Webhook server on port 9443 with cert mount from agentic-operator-webhook-certs secret
  • Install script: Deploys webhook Service + MutatingWebhookConfiguration (step 5/6); GITHUB_RAW is overridable for fork-based testing;
    IMAGE_PULL_POLICY support
  • Uninstall script: Deletes MutatingWebhookConfiguration before the operator deployment to avoid fail-closed dead-endpoint window
  • deploy-jaeger.sh: Fixed oc new-project switching active namespace (--skip-config-write)

How it works

User patches ProposalApproval (approve/deny)
  → API server intercepts UPDATE
  → MutatingAdmissionWebhook injects spec.approver from AdmissionReview userInfo
  → Controller reconciles, reads spec.approver
  → audit.approval.received emitted with real identity
  → human_approval span closed with approver.uid, approver.username, approval.decision

Testing

- Unit tests for webhook: approver injection on UPDATE, CREATE passthrough, add vs replace patch, client-submitted approver overwrite
- Existing audit tests updated for new EndApprovalWait signature
- End-to-end tested on cluster: approved path, denied path, auto-approval, Jaeger trace verification

Deny paths (Analysis, Execution, Verification, Escalation) called
denyProposal without first emitting audit.approval.received or ending
the approval wait span, leaving denied proposals invisible in traces.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Vimal Kumar <vimal78@gmail.com>
@openshift-ci-robot openshift-ci-robot added the jira/valid-reference Indicates that this PR references a valid Jira ticket of any type. label Jun 22, 2026
@openshift-ci-robot

openshift-ci-robot commented Jun 22, 2026

Copy link
Copy Markdown

@vimalk78: This pull request references OLS-3273 which is a valid jira issue.

Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the story to target the "5.0.0" version, but no target version was set.

Details

In response to this:

Summary

Adds a MutatingAdmissionWebhook that intercepts UPDATE operations on ProposalApproval CRs and injects the authenticated user's identity into
spec.approver. This closes the gap where audit logs and OTEL traces showed empty approver fields — the API server now stamps every approval/denial with who
did it.

Also includes a bug fix for missing audit events on deny paths (OLS-3024), discovered during end-to-end testing.

Commits

  1. OLS-3024: Emit audit events on deny paths before rejecting proposals — All four deny paths (Analysis, Execution, Verification, Escalation) called
    denyProposal without emitting audit.approval.received or ending the approval wait span. Denied proposals were invisible in traces.

  2. OLS-3273: Add mutating webhook for ProposalApproval identity injection — The webhook, CRD changes, audit integration, install/uninstall scripts, and
    approval.decision attribute on the human_approval OTEL span.

What changed

  • CRD: Added spec.approver (uid, username, timestamp) to ProposalApproval
  • Webhook: ProposalApprovalMutator injects spec.approver via JSON patch on every UPDATE; CREATE passes through
  • Audit: EndApprovalWait now records approver identity and approval.decision (Approved/Denied) on the human_approval span; EmitApprovalReceived
    reads from spec.approver instead of hardcoded empty strings
  • Deny paths: All four stage-deny branches now emit audit.approval.received and end the approval wait span before calling denyProposal
  • Manager wiring: Webhook server on port 9443 with cert mount from agentic-operator-webhook-certs secret
  • Install script: Deploys webhook Service + MutatingWebhookConfiguration (step 5/6); GITHUB_RAW is overridable for fork-based testing;
    IMAGE_PULL_POLICY support
  • Uninstall script: Deletes MutatingWebhookConfiguration before the operator deployment to avoid fail-closed dead-endpoint window
  • deploy-jaeger.sh: Fixed oc new-project switching active namespace (--skip-config-write)

How it works

User patches ProposalApproval (approve/deny)
  → API server intercepts UPDATE
  → MutatingAdmissionWebhook injects spec.approver from AdmissionReview userInfo
  → Controller reconciles, reads spec.approver
  → audit.approval.received emitted with real identity
  → human_approval span closed with approver.uid, approver.username, approval.decision

Testing

- Unit tests for webhook: approver injection on UPDATE, CREATE passthrough, add vs replace patch, client-submitted approver overwrite
- Existing audit tests updated for new EndApprovalWait signature
- End-to-end tested on cluster: approved path, denied path, auto-approval, Jaeger trace verification

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository.

@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown
📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • ProposalApproval resources now automatically capture and record the identity of approvers, including username, user ID, and approval timestamp.
  • Improvements

    • Audit logging now includes detailed approver information for enhanced compliance tracking and audit trails.

Walkthrough

Adds a mutating admission webhook (ProposalApprovalMutator) that intercepts UPDATE operations on ProposalApproval resources and injects authenticated user identity (uid, username, approvedAt) into spec.approver via JSON Patch. Extends AuditLogger.EndApprovalWait to accept the approval object and propagate approver identity into telemetry spans. Ships Kubernetes manifests, Kustomize wiring, and quickstart scripts for webhook deployment and teardown.

Changes

ProposalApproval Mutating Webhook and Audit Identity Propagation

Layer / File(s) Summary
API Contract: ApproverInfo Type and Field
api/v1alpha1/proposalapproval_types.go
Introduces ApproverInfo struct with webhook-authoritative UID, Username, and server-side ApprovedAt fields. Adds non-pointer Approver field to ProposalApprovalSpec with omitempty,omitzero JSON tags. Updates ProposalApproval example YAML comment.
Webhook Handler Implementation and Test Coverage
controller/proposal/approval_webhook.go, controller/proposal/approval_webhook_test.go, go.mod
Implements ProposalApprovalMutator.Handle: passes CREATE unchanged; builds ApproverInfo from req.UserInfo and UTC timestamp on UPDATE; detects existing spec.approver and returns JSON Patch add or replace operation. Tests cover mutation injection, client-submitted overwrite, CREATE pass-through, missing ownerReferences, and absent approver scenarios. Promotes gomodules.xyz/jsonpatch/v2 to direct dependency.
Webhook Server and Registration Wiring
cmd/main.go, controller/setup.go
Configures webhook server on port 9443 with serving-certs directory at /tmp/k8s-webhook-server/serving-certs. Registers ProposalApprovalMutator at /mutate-proposalapproval path.
Kubernetes Webhook Manifests and Manager Updates
config/webhook/*, config/manager/manager.yaml, config/default/kustomization.yaml
Adds MutatingWebhookConfiguration for UPDATE proposalapprovals, Service with OpenShift certificate annotation, and NetworkPolicy restricting port 9443 ingress. Updates manager Deployment to expose port 9443 and mount webhook-certs volume from secret. Includes webhook resources in default kustomization.
Audit Logger Interface and Approver Identity Propagation
controller/proposal/audit.go
Updates AuditLogger.EndApprovalWait signature to accept *ProposalApproval. ProductionAuditLogger sets span attributes for approver identity and approval.decision when approver UID is non-empty. EmitApprovalReceived conditionally includes approver payload fields. NoOpAuditLogger updated for signature compatibility.
Handler, Reconciler, and Test Call-Site Updates
controller/proposal/handlers.go, controller/proposal/reconciler.go, controller/proposal/audit_test.go
All EndApprovalWait calls updated to pass approval argument or nil. Denial handlers in Analysis, Execution, Verification, and Escalation now emit EndApprovalWait and EmitApprovalReceived before denying. Existing audit tests updated to match new signature.
Quickstart Install and Uninstall Scripts
hack/quickstart/install.sh, hack/quickstart/uninstall.sh, hack/deploy-jaeger.sh
install.sh adds Step 5 webhook Service deployment, updates Deployment manifest with webhook port/volume mount, renumbers to 7-step flow. uninstall.sh inserts Step 4 webhook resource deletion, renumbers to 7 steps. deploy-jaeger.sh adds --skip-config-write to oc new-project.

Sequence Diagram

sequenceDiagram
  participant User as User/kubectl
  participant APIServer as Kubernetes API Server
  participant Mutator as ProposalApprovalMutator
  participant Reconciler as ProposalReconciler
  participant Audit as ProductionAuditLogger

  User->>APIServer: UPDATE ProposalApproval
  APIServer->>Mutator: AdmissionReview (UPDATE)
  Mutator->>Mutator: Build ApproverInfo from UserInfo + UTC timestamp
  Mutator->>Mutator: Check spec.approver presence
  Mutator-->>APIServer: admission.Patched with /spec/approver (add or replace)
  APIServer-->>User: 200 OK (patched object stored)

  APIServer->>Reconciler: Reconcile triggered
  Reconciler->>Reconciler: Load ProposalApproval, evaluate stage decision
  alt Stage Approved
    Reconciler->>Audit: EndApprovalWait(proposal, approval)
    Audit->>Audit: Set span attrs approver.uid, approver.username, approver.approvedAt
    Audit->>Audit: Set approval.decision from last stage
    Reconciler->>Audit: EmitApprovalReceived(proposal, approval)
    Audit->>Audit: Emit payload with ApproverInfo fields
  else Stage Denied
    Reconciler->>Audit: EndApprovalWait(proposal, approval)
    Reconciler->>Audit: EmitApprovalReceived(proposal, approval)
    Reconciler->>Reconciler: Deny proposal
  end
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately describes the main change: implementing a MutatingAdmissionWebhook for ProposalApproval identity injection as referenced in the OLS-3273 ticket.
Description check ✅ Passed The PR description comprehensively covers the changeset, detailing the webhook implementation, audit fixes, CRD changes, and testing approach. It clearly relates to the actual modifications across all affected files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 OpenGrep (1.23.0)
cmd/main.go

┌──────────────┐
│ Opengrep CLI │
└──────────────┘

�[32m✔�[39m �[1mOpengrep OSS�[0m
�[32m✔�[39m Basic security coverage for first-party code vulnerabilities.

[00.17][ERROR]: unable to find a config; path .coderabbit-opengrep-fallback.yml does not exist

controller/proposal/approval_webhook.go

┌──────────────┐
│ Opengrep CLI │
└──────────────┘

�[32m✔�[39m �[1mOpengrep OSS�[0m
�[32m✔�[39m Basic security coverage for first-party code vulnerabilities.

[00.19][ERROR]: unable to find a config; path .coderabbit-opengrep-fallback.yml does not exist

api/v1alpha1/proposalapproval_types.go

┌──────────────┐
│ Opengrep CLI │
└──────────────┘

�[32m✔�[39m �[1mOpengrep OSS�[0m
�[32m✔�[39m Basic security coverage for first-party code vulnerabilities.

[00.29][ERROR]: unable to find a config; path .coderabbit-opengrep-fallback.yml does not exist

  • 6 others

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@openshift-ci openshift-ci Bot requested review from blublinsky and raptorsun June 22, 2026 18:43
@openshift-ci

openshift-ci Bot commented Jun 22, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign onmete for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@vimalk78 vimalk78 force-pushed the ols-3273-approval-webhook branch from 34f93df to 1bfef28 Compare June 22, 2026 18:47

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (2)
controller/proposal/approval_webhook_test.go (1)

26-254: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Add a regression test for UPDATE requests with missing or null spec.

Current tests always include Spec, so they won’t catch invalid child-path patching when /spec is missing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@controller/proposal/approval_webhook_test.go` around lines 26 - 254, Add a
new test function (similar to the existing tests like
TestApprovalWebhook_InjectsApproverOnUpdate) that creates a ProposalApproval
object with either a missing or null Spec field, sends it through the
ProposalApprovalMutator.Handle() method with an UPDATE operation, and verifies
that the mutator handles this edge case gracefully without attempting to patch
child paths of a non-existent spec. This regression test will catch issues where
the code tries to patch `/spec/approver` when the spec field itself doesn't
exist.
controller/proposal/audit_test.go (1)

1513-1513: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Add a focused test for non-nil approval in EndApprovalWait.

These updates only validate the nil path. Add one test that passes an approval containing spec.approver and stage decisions, then assert span attributes include approver.uid, approver.username, approver.timestamp, and approval.decision.

Diff sketch
+func TestEndApprovalWait_SetsApproverAndDecisionAttributes(t *testing.T) {
+    sr := tracetest.NewSpanRecorder()
+    tp := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(sr), sdktrace.WithIDGenerator(&ProposalIDGenerator{}))
+    otel.SetTracerProvider(tp)
+    defer otel.SetTracerProvider(sdktrace.NewTracerProvider())
+
+    auditLogger := NewProductionAuditLogger(zap.NewNop()).(*ProductionAuditLogger)
+    proposal := &agenticv1alpha1.Proposal{
+        ObjectMeta: metav1.ObjectMeta{
+            Name: "test", Namespace: "test-ns",
+            UID: types.UID("a1b2c3d4-e5f6-7890-1234-567890abcdef"),
+        },
+    }
+    approval := &agenticv1alpha1.ProposalApproval{
+        Spec: agenticv1alpha1.ProposalApprovalSpec{
+            Approver: &agenticv1alpha1.ApproverInfo{
+                UID: "u-1", Username: "alice", Timestamp: "2026-06-22T10:00:00Z",
+            },
+            Stages: []agenticv1alpha1.ApprovalStage{
+                {Type: agenticv1alpha1.ApprovalStageAnalysis, Decision: agenticv1alpha1.ApprovalDecisionApproved},
+            },
+        },
+    }
+
+    auditLogger.StartApprovalWait(context.Background(), proposal)
+    auditLogger.EndApprovalWait(proposal, approval)
+
+    // Assert proposal.human_approval span contains approver.* + approval.decision attrs.
+}

Also applies to: 1561-1561, 1603-1603

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@controller/proposal/audit_test.go` at line 1513, The current tests for
EndApprovalWait in the audit_test.go file only test the nil approval path. Add a
new test case that constructs a non-nil approval object with populated
spec.approver details and stage decisions, then calls EndApprovalWait with this
approval instead of nil. After the call, assert that the resulting span
attributes contain the expected fields: approver.uid, approver.username,
approver.timestamp, and approval.decision to validate that approval metadata is
properly captured in the audit logs when an actual approval is provided.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@config/default/kustomization.yaml`:
- Line 9: The kustomization.yaml file includes the webhook configuration but is
missing the required NetworkPolicy resource to restrict ingress to the webhook
service. Create a NetworkPolicy resource that restricts ingress to the
agentic-operator-webhook-service on ports 443 and 9443, ensuring only authorized
traffic can reach the webhook endpoint. Add this NetworkPolicy manifest to the
configuration stack and reference it in the kustomization.yaml file alongside
the webhook include to ensure network security requirements are met.

In `@config/manager/manager.yaml`:
- Around line 82-86: The webhook-certs secret volume mount in the manager
deployment is configured with `optional: true`, which allows the pod to start
without the certificate secret, causing webhook TLS initialization failures.
Change the `optional: true` field to `optional: false` in the secret volume
specification for webhook-certs to ensure the secret must exist for the pod to
start, preventing admission failures due to missing certificates.

In `@config/webhook/manifests.yaml`:
- Around line 15-19: The webhook rule defined in the
ValidatingWebhookConfiguration for proposalapprovals resources is currently
matching across all namespaces. Since the controller is namespaced and
failurePolicy is set to Fail, this creates unnecessary cluster-wide admission
coupling when the webhook becomes unavailable. Add a namespaceSelector field to
the webhook rule to restrict it to only the operator namespace where the
controller is running. This will ensure the webhook only affects resources in
the specific namespace rather than creating a cluster-wide dependency.

In `@controller/proposal/approval_webhook.go`:
- Around line 44-58: Before attempting to add the `/spec/approver` patch
operation in the switch case, verify that the spec field actually exists in the
object. If the spec field is absent (when the `ok` check on line 45 fails), you
cannot patch `/spec/approver` because the parent path `/spec` does not exist.
Add a check for the missing spec case and either: create the spec first with an
appropriate patch operation before adding the approver, or handle this scenario
separately to ensure the JSON Patch operations target valid parent paths.

In `@hack/quickstart/install.sh`:
- Around line 255-276: The MutatingWebhookConfiguration for
agentic-operator-mutating-webhook with failurePolicy set to Fail is being
applied before the webhook backend and certificates are ready, creating a window
where ProposalApproval UPDATE requests can be rejected. Reorder the install
sequence to first apply the agentic-operator-webhook-service, perform the
readiness wait check, and only then apply the MutatingWebhookConfiguration
resource. This ensures the webhook backend is fully initialized and responding
before the fail-closed policy takes effect.

---

Nitpick comments:
In `@controller/proposal/approval_webhook_test.go`:
- Around line 26-254: Add a new test function (similar to the existing tests
like TestApprovalWebhook_InjectsApproverOnUpdate) that creates a
ProposalApproval object with either a missing or null Spec field, sends it
through the ProposalApprovalMutator.Handle() method with an UPDATE operation,
and verifies that the mutator handles this edge case gracefully without
attempting to patch child paths of a non-existent spec. This regression test
will catch issues where the code tries to patch `/spec/approver` when the spec
field itself doesn't exist.

In `@controller/proposal/audit_test.go`:
- Line 1513: The current tests for EndApprovalWait in the audit_test.go file
only test the nil approval path. Add a new test case that constructs a non-nil
approval object with populated spec.approver details and stage decisions, then
calls EndApprovalWait with this approval instead of nil. After the call, assert
that the resulting span attributes contain the expected fields: approver.uid,
approver.username, approver.timestamp, and approval.decision to validate that
approval metadata is properly captured in the audit logs when an actual approval
is provided.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 6923565b-0377-4ba8-9526-4aba2d95a389

📥 Commits

Reviewing files that changed from the base of the PR and between db11db3 and 34f93df.

⛔ Files ignored due to path filters (2)
  • api/v1alpha1/zz_generated.deepcopy.go is excluded by !**/zz_generated.deepcopy.go
  • config/crd/bases/agentic.openshift.io_proposalapprovals.yaml is excluded by !config/crd/bases/**
📒 Files selected for processing (18)
  • api/v1alpha1/proposalapproval_types.go
  • cmd/main.go
  • config/default/kustomization.yaml
  • config/manager/manager.yaml
  • config/webhook/kustomization.yaml
  • config/webhook/manifests.yaml
  • config/webhook/service.yaml
  • controller/proposal/approval_webhook.go
  • controller/proposal/approval_webhook_test.go
  • controller/proposal/audit.go
  • controller/proposal/audit_test.go
  • controller/proposal/handlers.go
  • controller/proposal/reconciler.go
  • controller/setup.go
  • go.mod
  • hack/deploy-jaeger.sh
  • hack/quickstart/install.sh
  • hack/quickstart/uninstall.sh
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • openshift/lightspeed-agentic-sandbox (manual)

Comment thread config/default/kustomization.yaml
Comment thread config/manager/manager.yaml
Comment thread config/webhook/manifests.yaml
Comment on lines +44 to +58
var spec map[string]json.RawMessage
if specRaw, ok := obj["spec"]; ok {
if err := json.Unmarshal(specRaw, &spec); err != nil {
return admission.Errored(http.StatusBadRequest, err)
}
}

var patches []jsonpatch.JsonPatchOperation
_, hasApprover := spec["approver"]

switch {
case !hasApprover:
patches = []jsonpatch.JsonPatchOperation{
{Operation: "add", Path: "/spec/approver", Value: &rawApprover},
}

@coderabbitai coderabbitai Bot Jun 22, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle missing spec before patching /spec/approver.

If spec is absent (or null), Line 57 emits add /spec/approver with a non-existent parent path. JSON Patch application then fails and the UPDATE gets rejected.

Suggested fix
@@
-	var spec map[string]json.RawMessage
-	if specRaw, ok := obj["spec"]; ok {
+	var spec map[string]json.RawMessage
+	hasSpec := false
+	if specRaw, ok := obj["spec"]; ok && string(specRaw) != "null" {
+		hasSpec = true
 		if err := json.Unmarshal(specRaw, &spec); err != nil {
 			return admission.Errored(http.StatusBadRequest, err)
 		}
 	}
 
 	var patches []jsonpatch.JsonPatchOperation
-	_, hasApprover := spec["approver"]
+	if !hasSpec {
+		patches = []jsonpatch.JsonPatchOperation{
+			{Operation: "add", Path: "/spec", Value: map[string]json.RawMessage{"approver": rawApprover}},
+		}
+		return admission.Patched("injected spec.approver", patches...)
+	}
+	_, hasApprover := spec["approver"]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var spec map[string]json.RawMessage
if specRaw, ok := obj["spec"]; ok {
if err := json.Unmarshal(specRaw, &spec); err != nil {
return admission.Errored(http.StatusBadRequest, err)
}
}
var patches []jsonpatch.JsonPatchOperation
_, hasApprover := spec["approver"]
switch {
case !hasApprover:
patches = []jsonpatch.JsonPatchOperation{
{Operation: "add", Path: "/spec/approver", Value: &rawApprover},
}
var spec map[string]json.RawMessage
hasSpec := false
if specRaw, ok := obj["spec"]; ok && string(specRaw) != "null" {
hasSpec = true
if err := json.Unmarshal(specRaw, &spec); err != nil {
return admission.Errored(http.StatusBadRequest, err)
}
}
var patches []jsonpatch.JsonPatchOperation
if !hasSpec {
patches = []jsonpatch.JsonPatchOperation{
{Operation: "add", Path: "/spec", Value: map[string]json.RawMessage{"approver": rawApprover}},
}
return admission.Patched("injected spec.approver", patches...)
}
_, hasApprover := spec["approver"]
switch {
case !hasApprover:
patches = []jsonpatch.JsonPatchOperation{
{Operation: "add", Path: "/spec/approver", Value: &rawApprover},
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@controller/proposal/approval_webhook.go` around lines 44 - 58, Before
attempting to add the `/spec/approver` patch operation in the switch case,
verify that the spec field actually exists in the object. If the spec field is
absent (when the `ok` check on line 45 fails), you cannot patch `/spec/approver`
because the parent path `/spec` does not exist. Add a check for the missing spec
case and either: create the spec first with an appropriate patch operation
before adding the approver, or handle this scenario separately to ensure the
JSON Patch operations target valid parent paths.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please review again, on the new pushed code

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
cat controller/proposal/approval_webhook.go

Length of output: 2066


🏁 Script executed:

#!/bin/bash
# Fetch the current file content via GitHub API
gh api repos/openshift/lightspeed-agentic-operator/contents/controller/proposal/approval_webhook.go \
  --jq '.content' | base64 -d

Length of output: 575


🏁 Script executed:

#!/bin/bash
# Get PR `#201` head branch
PR_INFO=$(gh api repos/openshift/lightspeed-agentic-operator/pulls/201 --jq '{head_sha: .head.sha, head_ref: .head.ref}')
echo "$PR_INFO"

HEAD_SHA=$(echo "$PR_INFO" | jq -r '.head_sha')

# Fetch file at the PR head commit
gh api "repos/openshift/lightspeed-agentic-operator/contents/controller/proposal/approval_webhook.go?ref=${HEAD_SHA}" \
  --jq '.content' | base64 -d

Length of output: 2515



@vimalk78 — the code on the current HEAD (f292ce5) is identical to the original diff. The concern is still unaddressed.

The exact problem: if obj["spec"] is absent or spec is null, the spec map stays nil. The map lookup _, hasApprover := spec["approver"] returns false on a nil map (no panic, but silently falls through). The switch then emits add /spec/approver — but JSON Patch RFC 6902 requires the parent path /spec to exist for an add operation, so the patch application fails and the admission webhook rejects the UPDATE request.

For ProposalApproval CRDs the spec will almost always be present in an UPDATE, but an explicit spec: null via strategic merge patch is a realistic edge case that would trigger this.

The fix from the earlier suggestion still applies:

var spec map[string]json.RawMessage
hasSpec := false
if specRaw, ok := obj["spec"]; ok && string(specRaw) != "null" {
    hasSpec = true
    if err := json.Unmarshal(specRaw, &spec); err != nil {
        return admission.Errored(http.StatusBadRequest, err)
    }
}

var patches []jsonpatch.JsonPatchOperation
if !hasSpec {
    patches = []jsonpatch.JsonPatchOperation{
        {Operation: "add", Path: "/spec", Value: map[string]json.RawMessage{"approver": rawApprover}},
    }
    return admission.Patched("injected spec.approver", patches...)
}
_, hasApprover := spec["approver"]

switch {
case !hasApprover:
    patches = []jsonpatch.JsonPatchOperation{
        {Operation: "add", Path: "/spec/approver", Value: &rawApprover},
    }
// ...

🧠 Learnings used
Learnt from: sakshipatels98-byte
Repo: openshift/lightspeed-agentic-operator PR: 17
File: controller/proposal/sandbox_templates.go:197-205
Timestamp: 2026-06-09T17:19:11.943Z
Learning: In this repository’s Go code, error wrapping may use inline `fmt.Errorf("...: %w", err)` (with literal message strings) without requiring the project’s non-use of a `const ErrFoo = "..."` + `fmt.Errorf("%s: %w", ErrFoo, err)` style. During code reviews, do not flag inline `fmt.Errorf` message strings as violations of any const-error-message guideline for this repo.

Learnt from: onmete
Repo: openshift/lightspeed-agentic-operator PR: 116
File: cli/system/helpers.go:18-23
Timestamp: 2026-06-15T07:37:06.162Z
Learning: In this repo, when registering Kubernetes schemes via `SomeScheme.AddToScheme(s)` (e.g., `_ = Scheme.AddToScheme(s)` to intentionally discard the returned error), treat the blank-identifier error discard as an established convention. Code reviewers should NOT flag these `_ = <scheme>.AddToScheme(<schemeBuilder>)` / `_ = <scheme>.AddToScheme(s)` usages as violations of the “never ignore error returns” guideline. Only apply this exception to the scheme-registration `AddToScheme` pattern; ignore/flag other blank identifier error discards as usual.

Comment thread hack/quickstart/install.sh
@vimalk78 vimalk78 force-pushed the ols-3273-approval-webhook branch from 1bfef28 to 9e5122f Compare June 22, 2026 18:54
@vimalk78 vimalk78 changed the title OLS-3273: MutatingAdmissionWebhook for ProposalApproval identity injection WIP: OLS-3273: MutatingAdmissionWebhook for ProposalApproval identity injection Jun 22, 2026
@openshift-ci openshift-ci Bot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Jun 22, 2026
@vimalk78 vimalk78 force-pushed the ols-3273-approval-webhook branch from 9e5122f to a4c879e Compare June 22, 2026 19:04
Intercepts UPDATE operations on ProposalApproval CRs to inject the
authenticated user's identity (uid, username, approvedAt) from the
AdmissionReview userInfo into spec.approver, overwriting any
client-submitted values. Fail-closed webhook using OpenShift
service-serving-certs for TLS, scoped to the operator namespace.

The webhook only mutates — audit logging is handled by the controller's
EmitApprovalReceived (JSON log + OTEL span event). EndApprovalWait now
sets approver attributes and approval.decision on the human_approval
span before ending it.

Also adds NetworkPolicy restricting ingress to the webhook port, and
reorders install.sh to register the MutatingWebhookConfiguration only
after the operator is ready.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Vimal Kumar <vimal78@gmail.com>
@vimalk78 vimalk78 force-pushed the ols-3273-approval-webhook branch from a4c879e to f292ce5 Compare June 22, 2026 19:22
@openshift-ci

openshift-ci Bot commented Jun 22, 2026

Copy link
Copy Markdown

@vimalk78: all tests passed!

Full PR test history. Your PR dashboard.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here.

@vimalk78

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@api/v1alpha1/proposalapproval_types.go`:
- Around line 258-261: The YAML documentation example for the approver section
in the ProposalApproval types shows an incorrect field name. Change the field
name from `timestamp` to `approvedAt` in the example comment (around the
approver section showing uid, username, and the timestamp field) to match the
actual JSON field name defined in the struct at line 45. This ensures users
copying the example will use the correct field name.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: bebf849c-f03c-4f77-8fde-baf580edf72e

📥 Commits

Reviewing files that changed from the base of the PR and between 34f93df and f292ce5.

⛔ Files ignored due to path filters (2)
  • api/v1alpha1/zz_generated.deepcopy.go is excluded by !**/zz_generated.deepcopy.go
  • config/crd/bases/agentic.openshift.io_proposalapprovals.yaml is excluded by !config/crd/bases/**
📒 Files selected for processing (19)
  • api/v1alpha1/proposalapproval_types.go
  • cmd/main.go
  • config/default/kustomization.yaml
  • config/manager/manager.yaml
  • config/webhook/kustomization.yaml
  • config/webhook/manifests.yaml
  • config/webhook/networkpolicy.yaml
  • config/webhook/service.yaml
  • controller/proposal/approval_webhook.go
  • controller/proposal/approval_webhook_test.go
  • controller/proposal/audit.go
  • controller/proposal/audit_test.go
  • controller/proposal/handlers.go
  • controller/proposal/reconciler.go
  • controller/setup.go
  • go.mod
  • hack/deploy-jaeger.sh
  • hack/quickstart/install.sh
  • hack/quickstart/uninstall.sh
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • openshift/lightspeed-agentic-sandbox (manual)
✅ Files skipped from review due to trivial changes (5)
  • hack/deploy-jaeger.sh
  • config/webhook/kustomization.yaml
  • config/manager/manager.yaml
  • go.mod
  • controller/setup.go
🚧 Files skipped from review as they are similar to previous changes (11)
  • config/webhook/manifests.yaml
  • config/default/kustomization.yaml
  • controller/proposal/reconciler.go
  • cmd/main.go
  • config/webhook/service.yaml
  • controller/proposal/audit_test.go
  • controller/proposal/audit.go
  • hack/quickstart/uninstall.sh
  • controller/proposal/handlers.go
  • controller/proposal/approval_webhook.go
  • controller/proposal/approval_webhook_test.go

Comment on lines +258 to +261
// approver:
// uid: "abc123"
// username: "admin@example.com"
// timestamp: "2026-06-22T10:00:00Z"

@coderabbitai coderabbitai Bot Jun 23, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Example uses wrong key timestamp. The field's JSON name is approvedAt (Line 45), but the doc example renders it as timestamp. Users copying this example will produce an unrecognized field.

Proposed fix
 //	  approver:
 //	    uid: "abc123"
 //	    username: "admin@example.com"
-//	    timestamp: "2026-06-22T10:00:00Z"
+//	    approvedAt: "2026-06-22T10:00:00Z"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// approver:
// uid: "abc123"
// username: "admin@example.com"
// timestamp: "2026-06-22T10:00:00Z"
// approver:
// uid: "abc123"
// username: "admin@example.com"
// approvedAt: "2026-06-22T10:00:00Z"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/v1alpha1/proposalapproval_types.go` around lines 258 - 261, The YAML
documentation example for the approver section in the ProposalApproval types
shows an incorrect field name. Change the field name from `timestamp` to
`approvedAt` in the example comment (around the approver section showing uid,
username, and the timestamp field) to match the actual JSON field name defined
in the struct at line 45. This ensures users copying the example will use the
correct field name.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please check previous comments

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
gh pr view 201 --repo openshift/lightspeed-agentic-operator --comments | head -200

Length of output: 2105


🏁 Script executed:

#!/bin/bash
gh api repos/openshift/lightspeed-agentic-operator/pulls/201/comments \
  --jq '.[].body' | head -300

Length of output: 9689


🏁 Script executed:

#!/bin/bash
gh api repos/openshift/lightspeed-agentic-operator/pulls/201/comments \
  --jq '.[].body' | tail -300

Length of output: 10175


🏁 Script executed:

#!/bin/bash
# Get the PR head branch and check the current file content
PR_DATA=$(gh api repos/openshift/lightspeed-agentic-operator/pulls/201 --jq '{head_sha: .head.sha, head_ref: .head.ref}')
echo "PR data: $PR_DATA"

HEAD_SHA=$(echo "$PR_DATA" | jq -r '.head_sha')
echo "Head SHA: $HEAD_SHA"

# Get the current content of proposalapproval_types.go
gh api "repos/openshift/lightspeed-agentic-operator/contents/api/v1alpha1/proposalapproval_types.go?ref=$HEAD_SHA" \
  --jq '.content' | base64 -d | grep -n -A5 -B5 'timestamp\|approvedAt\|approver:' | head -60

Length of output: 1849


🏁 Script executed:

#!/bin/bash
HEAD_SHA="f292ce5c4402931caaf6ed8c60b046c288df707e"

echo "=== manager.yaml - optional cert ==="
gh api "repos/openshift/lightspeed-agentic-operator/contents/config/manager/manager.yaml?ref=$HEAD_SHA" \
  --jq '.content' | base64 -d | grep -n -A2 -B2 'optional'

echo ""
echo "=== approval_webhook.go - missing spec handling ==="
gh api "repos/openshift/lightspeed-agentic-operator/contents/controller/proposal/approval_webhook.go?ref=$HEAD_SHA" \
  --jq '.content' | base64 -d | cat -n

echo ""
echo "=== NetworkPolicy in webhook config ==="
gh api "repos/openshift/lightspeed-agentic-operator/contents/config/webhook/kustomization.yaml?ref=$HEAD_SHA" \
  --jq '.content' | base64 -d

echo ""
echo "=== webhook manifests.yaml - namespaceSelector ==="
gh api "repos/openshift/lightspeed-agentic-operator/contents/config/webhook/manifests.yaml?ref=$HEAD_SHA" \
  --jq '.content' | base64 -d

Length of output: 4571



@vimalk78, here's the status after reviewing the latest commit (f292ce5):

✅ Fixed

  • namespaceSelector scoped to operator namespace — manifests.yaml now has the correct namespaceSelector.
  • NetworkPolicynetworkpolicy.yaml is now included in config/webhook/kustomization.yaml.

❌ Still open (3 issues)

1. timestampapprovedAt in the doc example (api/v1alpha1/proposalapproval_types.go line 261)
The example comment still says timestamp but the JSON field is approvedAt.

-//	    timestamp: "2026-06-22T10:00:00Z"
+//	    approvedAt: "2026-06-22T10:00:00Z"

2. optional: true on webhook cert secret (config/manager/manager.yaml line 86)
The pod can still start without the cert, causing TLS failures at admission time with failurePolicy: Fail. Remove optional: true (or set it to false).

3. Missing spec guard in approval_webhook.go (lines 44–57)
When obj["spec"] is absent, spec stays nil, hasApprover is false, and the patch emits add /spec/approver — but JSON Patch requires the parent path /spec to exist first. The update will be rejected. Need to handle the no-spec case:

var spec map[string]json.RawMessage
hasSpec := false
if specRaw, ok := obj["spec"]; ok && string(specRaw) != "null" {
    hasSpec = true
    if err := json.Unmarshal(specRaw, &spec); err != nil {
        return admission.Errored(http.StatusBadRequest, err)
    }
}

var patches []jsonpatch.JsonPatchOperation
if !hasSpec {
    patches = []jsonpatch.JsonPatchOperation{
        {Operation: "add", Path: "/spec", Value: map[string]json.RawMessage{"approver": rawApprover}},
    }
    return admission.Patched("injected spec.approver", patches...)
}
_, hasApprover := spec["approver"]
// ... rest unchanged

🧠 Learnings used
Learnt from: sakshipatels98-byte
Repo: openshift/lightspeed-agentic-operator PR: 17
File: controller/proposal/sandbox_templates.go:197-205
Timestamp: 2026-06-09T17:19:11.943Z
Learning: In this repository’s Go code, error wrapping may use inline `fmt.Errorf("...: %w", err)` (with literal message strings) without requiring the project’s non-use of a `const ErrFoo = "..."` + `fmt.Errorf("%s: %w", ErrFoo, err)` style. During code reviews, do not flag inline `fmt.Errorf` message strings as violations of any const-error-message guideline for this repo.

Learnt from: onmete
Repo: openshift/lightspeed-agentic-operator PR: 116
File: cli/system/helpers.go:18-23
Timestamp: 2026-06-15T07:37:06.162Z
Learning: In this repo, when registering Kubernetes schemes via `SomeScheme.AddToScheme(s)` (e.g., `_ = Scheme.AddToScheme(s)` to intentionally discard the returned error), treat the blank-identifier error discard as an established convention. Code reviewers should NOT flag these `_ = <scheme>.AddToScheme(<schemeBuilder>)` / `_ = <scheme>.AddToScheme(s)` usages as violations of the “never ignore error returns” guideline. Only apply this exception to the scheme-registration `AddToScheme` pattern; ignore/flag other blank identifier error discards as usual.

@vimalk78

Copy link
Copy Markdown
Contributor Author

@CodeRabbit please release the lock

@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown

@vimalk78 Resuming automatic reviews for this PR.

✅ Action performed

Reviews resumed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. jira/valid-reference Indicates that this PR references a valid Jira ticket of any type.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants