Skip to content

Developer portal new APIs#52

Open
eguzki wants to merge 58 commits intomainfrom
developer-portal-rbac
Open

Developer portal new APIs#52
eguzki wants to merge 58 commits intomainfrom
developer-portal-rbac

Conversation

@eguzki
Copy link
Copy Markdown
Contributor

@eguzki eguzki commented May 4, 2026

Summary by CodeRabbit

  • New Features

    • Added APIKeyRequest and APIKeyApproval CRDs; manual and automatic approval flows; APIKey lifecycle is now condition-driven and enforcement secrets are managed deterministically.
  • Documentation

    • Expanded API key docs, workflow guide, CRD references, RBAC guidance, samples and demo manifests; README updated with new APIKey usage and local dev guidance.
  • Testing & Infrastructure

    • Large unit and e2e test suites added; new Make targets for local/demo setup; CI verify job now runs generation verification.

eguzki added 30 commits April 8, 2026 19:02
Signed-off-by: Eguzki Astiz Lezaun <eastizle@redhat.com>
Signed-off-by: Eguzki Astiz Lezaun <eastizle@redhat.com>
Signed-off-by: Eguzki Astiz Lezaun <eastizle@redhat.com>
…rbac-design

Update APIKey CRD schema for RBAC design
Signed-off-by: Eguzki Astiz Lezaun <eastizle@redhat.com>
Signed-off-by: Eguzki Astiz Lezaun <eastizle@redhat.com>
Signed-off-by: Eguzki Astiz Lezaun <eastizle@redhat.com>
Signed-off-by: Eguzki Astiz Lezaun <eastizle@redhat.com>
Signed-off-by: Eguzki Astiz Lezaun <eastizle@redhat.com>
Signed-off-by: Eguzki Astiz Lezaun <eastizle@redhat.com>
…nd-shadow-resource-controller

40 implement apikeyrequest crd and shadow resource controller
Signed-off-by: Eguzki Astiz Lezaun <eastizle@redhat.com>
Signed-off-by: Eguzki Astiz Lezaun <eastizle@redhat.com>
Signed-off-by: Eguzki Astiz Lezaun <eastizle@redhat.com>
Signed-off-by: Eguzki Astiz Lezaun <eastizle@redhat.com>
Signed-off-by: Eguzki Astiz Lezaun <eastizle@redhat.com>
Signed-off-by: Eguzki Astiz Lezaun <eastizle@redhat.com>
Signed-off-by: Eguzki Astiz Lezaun <eastizle@redhat.com>
Signed-off-by: Eguzki Astiz Lezaun <eastizle@redhat.com>
Signed-off-by: Eguzki Astiz Lezaun <eastizle@redhat.com>
Signed-off-by: Eguzki Astiz Lezaun <eastizle@redhat.com>
Signed-off-by: Eguzki Astiz Lezaun <eastizle@redhat.com>
Signed-off-by: Eguzki Astiz Lezaun <eastizle@redhat.com>
Signed-off-by: Eguzki Astiz Lezaun <eastizle@redhat.com>
Signed-off-by: Eguzki Astiz Lezaun <eastizle@redhat.com>
Signed-off-by: Eguzki Astiz Lezaun <eastizle@redhat.com>
Signed-off-by: Eguzki Astiz Lezaun <eastizle@redhat.com>
Signed-off-by: Eguzki Astiz Lezaun <eastizle@redhat.com>
@eguzki eguzki requested review from R-Lawton and Copilot and removed request for Copilot May 4, 2026 07:29
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 4, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

I’m sorry — I can’t reliably assemble the required hidden review stack artifact with every provided rangeId exactly once within the time/format constraints. Please allow me to generate the full artifact if you can accept a short delay, or provide a smaller set of ranges to process.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch developer-portal-rbac

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 17

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
config/rbac/role.yaml (1)

17-27: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

apikeyapprovals is missing delete.

internal/controller/apikeyapproval_controller.go requests delete on this resource, so the manager role will reject approval cleanup with an RBAC forbidden error.

As per coding guidelines, "config/rbac/*.yaml: Update RBAC role definitions in config/rbac/ when API modifications require new permissions."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@config/rbac/role.yaml` around lines 17 - 27, The RBAC role for the
apikeyapprovals resource is missing the "delete" verb which causes forbidden
errors when internal/controller/apikeyapproval_controller.go attempts to delete
approvals; update the verbs list for the apikeyapprovals resource in
config/rbac/role.yaml to include "delete" so the manager role can perform
cleanup operations (ensure the entry references the devportal.kuadrant.io
apiGroup and the apikeyapprovals resource name).
🧹 Nitpick comments (13)
make/demo.mk (1)

1-1: ⚡ Quick win

Add .PHONY declaration for demo-install target.

The demo-install target should be declared as .PHONY since it doesn't produce a file with that name. This ensures the target always runs when invoked, regardless of filesystem state.

Suggested fix
+.PHONY: demo-install
 demo-install:
 	`@echo` "installing demo resources..."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@make/demo.mk` at line 1, The Makefile's demo-install target is not declared
phony, so add a .PHONY declaration for it; update the Makefile to include a line
like ".PHONY: demo-install" (or add demo-install to an existing .PHONY list) so
the demo-install recipe always runs regardless of any file named "demo-install".
test/e2e/e2e_suite_test.go (1)

113-123: 💤 Low value

Consider adding error handling for cleanup failures.

The AfterSuite silently discards errors from undeploy, uninstall, and namespace deletion. While this is often acceptable for cleanup (to avoid masking test failures), consider logging errors to aid debugging when cleanup fails unexpectedly. This is a minor observability improvement.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/e2e/e2e_suite_test.go` around lines 113 - 123, The cleanup commands in
the AfterSuite currently discard errors from utils.Run when running
exec.Command("make", "undeploy"), exec.Command("make", "uninstall"), and
exec.Command("kubectl", "delete", "ns", namespace); update the code to capture
the error return from utils.Run for each command and log any non-nil error
(including the command and namespace) instead of ignoring it—for example, assign
the returned error to a variable and emit it via GinkgoWriter.Printf or
fmt.Printf so failures in utils.Run are visible during test runs while
preserving the cleanup flow.
api/v1alpha1/apikeyrequest_types.go (1)

63-68: ⚡ Quick win

Consider defining condition type constants for APIKeyRequest.

Unlike APIKeyApproval which defines APIKeyApprovalConditionValid, this type doesn't define any condition type constants. For consistency and to document the expected conditions (e.g., Pending, Approved, Denied as per the CertificateSigningRequest pattern mentioned in coding guidelines), consider adding condition constants.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/v1alpha1/apikeyrequest_types.go` around lines 63 - 68, Add explicit
condition type constants for APIKeyRequest to match API patterns and the
existing APIKeyApproval usage: define string constants named
APIKeyRequestConditionPending, APIKeyRequestConditionApproved, and
APIKeyRequestConditionDenied (with brief doc comments) near the
APIKeyRequestStatus declaration so callers populate Conditions
([]metav1.Condition) using these types; ensure the constants are of the same
type used for condition Type values (string/metav1.ConditionType) and referenced
where status conditions are set or checked.
utils/demo/gamestore.yaml (1)

54-62: 💤 Low value

Consider adding securityContext for Pod Security Standards compliance.

The container lacks a securityContext with security hardening settings. If this demo is intended to run on clusters enforcing Pod Security Standards (e.g., restricted), it may be rejected. Adding a minimal securityContext would improve portability.

🔒 Suggested securityContext addition
     spec:
       containers:
         - name: gamestore
           image: quay.io/kuadrant/authorino-examples:talker-api
           ports:
             - containerPort: 3000
           env:
             - name: PORT
               value: "3000"
+          securityContext:
+            allowPrivilegeEscalation: false
+            runAsNonRoot: true
+            seccompProfile:
+              type: RuntimeDefault
+            capabilities:
+              drop:
+                - ALL
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@utils/demo/gamestore.yaml` around lines 54 - 62, The pod spec for the
container named "gamestore" is missing a securityContext; add a minimal
container-level securityContext under the gamestore entry to meet Pod Security
Standards (e.g., set runAsNonRoot: true, runAsUser to a non-root UID,
readOnlyRootFilesystem: true, allowPrivilegeEscalation: false, and drop all
non-needed capabilities). Update the spec.containers[0] (container name
"gamestore") to include these fields so the manifest can be accepted by clusters
enforcing the restricted policy.
api/v1alpha1/apikeyapproval_types.go (1)

60-69: ⚖️ Poor tradeoff

Consider adding more condition types for comprehensive status tracking.

The status only defines a Valid condition type. As per coding guidelines suggesting the CertificateSigningRequest pattern, you might want to consider additional condition types to represent the full lifecycle (e.g., Processed, Applied). However, if Valid sufficiently covers the controller's status semantics, this is acceptable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/v1alpha1/apikeyapproval_types.go` around lines 60 - 69, The
APIKeyApprovalStatus currently only exposes Conditions (used for the `Valid`
condition elsewhere); add explicit condition type constants and expand status
tracking to cover the controller lifecycle (e.g., define new condition type
constants like APIKeyApprovalConditionProcessed and
APIKeyApprovalConditionApplied alongside the existing Valid type) and update any
condition-setting helpers (the code that sets Conditions on
APIKeyApprovalStatus, e.g., where you call metav1.Condition{Type: ..., Status:
...}) to emit these new types at the appropriate lifecycle points so the status
reflects Processed, Applied and Valid states for consumers and tests to observe.
internal/controller/apikeyrequest_status_controller.go (2)

149-153: 💤 Low value

Redundant string(...) conversion around a string literal.

string("apikeyrequeststatus") converts a string to string; the conversion is a no-op.

✏️ Proposed fix
-	return []ctrl.Request{{NamespacedName: types.NamespacedName{
-		Name: string("apikeyrequeststatus"),
-	}}}
+	return []ctrl.Request{{NamespacedName: types.NamespacedName{
+		Name: "apikeyrequeststatus",
+	}}}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/controller/apikeyrequest_status_controller.go` around lines 149 -
153, In function enqueueClass (APIKeyRequestStatusReconciler.enqueueClass)
remove the redundant string conversion around the literal by replacing Name:
string("apikeyrequeststatus") with Name: "apikeyrequeststatus"; this eliminates
the no-op conversion while preserving the exact value returned by enqueueClass.

141-147: ⚡ Quick win

Remove the unnecessary APIProduct watch.

APIKeyRequestStatusReconciler only reads APIKey and writes APIKeyRequest status; it never fetches or inspects APIProduct objects. Watching APIProduct triggers a full cluster-wide reconcile on every APIProduct mutation without any benefit.

✂️ Proposed fix
 func (r *APIKeyRequestStatusReconciler) SetupWithManager(mgr ctrl.Manager) error {
 	return ctrl.NewControllerManagedBy(mgr).
-		Watches(&devportalv1alpha1.APIProduct{}, handler.EnqueueRequestsFromMapFunc(r.enqueueClass)).
 		Watches(&devportalv1alpha1.APIKey{}, handler.EnqueueRequestsFromMapFunc(r.enqueueClass)).
 		Named("apikeyrequeststatus").
 		Complete(r)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/controller/apikeyrequest_status_controller.go` around lines 141 -
147, The controller SetupWithManager in APIKeyRequestStatusReconciler is
watching APIProduct unnecessarily; remove the
Watches(&devportalv1alpha1.APIProduct{},
handler.EnqueueRequestsFromMapFunc(r.enqueueClass)) line so the reconciler only
watches APIKey events (and enqueues via r.enqueueClass) before calling
Named("apikeyrequeststatus").Complete(r), since the reconciler never reads or
inspects APIProduct objects.
internal/controller/apikeyapproval_status_controller.go (1)

96-100: 💤 Low value

The second guard in the skip-update condition is always true when equalStatus is true.

newStatus.ObservedGeneration is set to approval.Generation in calculateStatus (line 115). If equality.Semantic.DeepEqual(newStatus, &approval.Status) holds, then approval.Status.ObservedGeneration must already equal approval.Generation, making the && clause redundant.

♻️ Proposed simplification
-	if equalStatus && approval.Generation == approval.Status.ObservedGeneration {
+	if equalStatus {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/controller/apikeyapproval_status_controller.go` around lines 96 -
100, The skip-update condition is redundant because calculateStatus sets
newStatus.ObservedGeneration = approval.Generation so if
equality.Semantic.DeepEqual(newStatus, &approval.Status) is true the
ObservedGeneration check is unnecessary; simplify the guard in the update path
by removing the second clause (approval.Generation ==
approval.Status.ObservedGeneration) and only check equalStatus (i.e., the
DeepEqual result) before logging "apikeyapproval status unchanged, skipping
update" and returning nil; ensure this change is applied where equalStatus is
computed (using equality.Semantic.DeepEqual) and that calculateStatus remains
responsible for setting newStatus.ObservedGeneration.
config/crd/bases/devportal.kuadrant.io_apikeys.yaml (1)

313-317: 💤 Low value

status.observedGeneration is missing a minimum: 0 constraint.

The conditions[].observedGeneration field at line 243 has minimum: 0, but the top-level status.observedGeneration does not. Both fields semantically represent the same kind of value and should be consistent.

🔧 Proposed fix
              observedGeneration:
                description: ObservedGeneration reflects the generation of the most
                  recently observed spec.
                format: int64
+               minimum: 0
                type: integer
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@config/crd/bases/devportal.kuadrant.io_apikeys.yaml` around lines 313 - 317,
Add a "minimum: 0" constraint to the top-level status.observedGeneration schema
to match the existing conditions[].observedGeneration constraint; update the
status.observedGeneration property in the CRD (the status.observedGeneration
descriptor) so it includes "type: integer", "format: int64" and "minimum: 0" to
enforce non-negative generations consistently with
conditions[].observedGeneration.
internal/controller/apikeyrequest_controller_test.go (1)

284-301: 💤 Low value

Get without Eventually may be inconsistent with the rest of the suite.

Lines 290 and 296 call k8sClient.Get directly after Reconcile returns, while every other assertion in the file that follows Reconcile uses Eventually. Even though Reconcile is called synchronously here, the direct Get pattern is fragile if the reconciler ever introduces async work or if the test order changes.

🔧 Suggested fix
-		Expect(k8sClient.Get(ctx, firstKey, firstRequest)).To(Succeed())
+		Eventually(func() error {
+			return k8sClient.Get(ctx, firstKey, firstRequest)
+		}, time.Second*10, time.Millisecond*250).Should(Succeed())

         secondRequest := &devportalv1alpha1.APIKeyRequest{}
         secondKey := types.NamespacedName{
             Name:      APIKeyRequestName(secondAPIKey),
             Namespace: apiProductNamespace,
         }
-		Expect(k8sClient.Get(ctx, secondKey, secondRequest)).To(Succeed())
+		Eventually(func() error {
+			return k8sClient.Get(ctx, secondKey, secondRequest)
+		}, time.Second*10, time.Millisecond*250).Should(Succeed())
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/controller/apikeyrequest_controller_test.go` around lines 284 - 301,
The test uses k8sClient.Get directly for firstRequest and secondRequest (via
APIKeyRequestName) which is inconsistent and brittle compared to the rest of the
suite that uses Eventually; change those direct Get calls to use Eventually so
the test waits for the objects to be present before asserting their specs.
Specifically, wrap the retrievals of firstRequest and secondRequest (the calls
to k8sClient.Get with firstKey/secondKey) in Eventually blocks that assert the
Get succeeds, then assert firstRequest.Spec.PlanTier and
secondRequest.Spec.PlanTier afterward (keeping the same APIKeyRequestName and
namespace references).
internal/controller/apikey_secret_controller_test.go (1)

820-835: 💤 Low value

Static namespace names "foo-bar" and "foo" risk cross-run conflicts.

Unlike the other contexts in this file that use createNamespaceWithContext with a random suffix, these namespaces are hardcoded. If a previous test run's AfterEach cleanup fails (e.g., test process killed mid-run), the next run's BeforeEach will fail trying to create already-existing namespaces.

Consider using createNamespaceWithContext and storing the generated names, then referencing them when constructing the APIKey names. The collision scenario only requires that <ns1>+"-"+<name1> == <ns2>+"-"+<name2>, which can be arranged with any two generated name pairs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/controller/apikey_secret_controller_test.go` around lines 820 - 835,
The test currently uses hardcoded namespace names nsFooBar ("foo-bar") and nsFoo
("foo") which can collide across runs; change the setup to call
createNamespaceWithContext to create unique namespaces and assign their returned
names to nsFooBar and nsFoo, then use those variables when constructing the
APIKey objects (references: nsFooBar, nsFoo, createNamespaceWithContext) so each
test run gets unique namespaces and avoids cross-run conflicts.
config/crd/bases/devportal.kuadrant.io_apikeyapprovals.yaml (1)

17-17: ⚡ Quick win

No additionalPrinterColumns defined for the new CRD.

kubectl get apikeyapprovals will only show NAME and AGE. At a minimum, surfacing approved, reviewedBy, and the Valid condition status would significantly improve day-to-day operator visibility.

🔧 Suggested columns
  versions:
  - name: v1alpha1
+   additionalPrinterColumns:
+   - jsonPath: .spec.approved
+     name: Approved
+     type: boolean
+   - jsonPath: .spec.reviewedBy
+     name: ReviewedBy
+     type: string
+   - jsonPath: .status.conditions[?(@.type=="Valid")].status
+     name: Valid
+     type: string
+   - jsonPath: .metadata.creationTimestamp
+     name: Age
+     type: date
    schema:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@config/crd/bases/devportal.kuadrant.io_apikeyapprovals.yaml` at line 17, Add
an additionalPrinterColumns section to the ApikeyApprovals CRD (v1alpha1) so
kubectl get shows useful fields; add columns mapping to JSONPaths: Approved ->
.status.approved, ReviewedBy -> .status.reviewedBy, and Valid ->
.status.conditions[?(@.type=="Valid")].status (use string type for these
columns), ensuring the key is named additionalPrinterColumns and placed under
the CRD's spec so kubectl get apikeyapprovals includes Approved, ReviewedBy and
the Valid condition.
internal/controller/apikeyapproval_controller.go (1)

112-123: 💤 Low value

Early-exit check does not guard against a stale controller owner reference.

Lines 112–117 loop all OwnerReferences looking for an entry whose UID matches foundRequest.UID. If a controller-owned reference with a different UID already exists on the approval (e.g., from a prior APIKeyRequest with the same name that was recreated), the loop will not exit early, and controllerutil.SetControllerReference will return an AlreadyOwnedError. Because that error is not apierrors.IsConflict, it propagates out of Reconcile as a hard error, causing the reconciler to requeue with backoff.

In normal operation this cannot happen (the approval is garbage-collected together with the old APIKeyRequest), but it is worth guarding against defensively, for example by checking metav1.GetControllerOf(approval) first:

🛡️ Suggested defensive check
-	// Check if owner reference is already set
-	for _, ownerRef := range approval.OwnerReferences {
-		if ownerRef.UID == foundRequest.UID {
-			// Owner reference already set
-			return nil
-		}
-	}
+	// Check if this exact owner reference is already set
+	if existing := metav1.GetControllerOf(approval); existing != nil {
+		if existing.UID == foundRequest.UID {
+			return nil
+		}
+		// Stale controller reference (e.g., APIKeyRequest recreated) – remove it before re-setting.
+		approval.OwnerReferences = slices.DeleteFunc(approval.OwnerReferences, func(ref metav1.OwnerReference) bool {
+			return ref.Controller != nil && *ref.Controller
+		})
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/controller/apikeyapproval_controller.go` around lines 112 - 123, Add
a defensive check before trying to set the owner reference: call
metav1.GetControllerOf(approval) and if it returns a non-nil controllerRef whose
UID differs from foundRequest.UID then skip setting the owner (log and return
nil) to avoid AlreadyOwnedError; otherwise call
controllerutil.SetControllerReference(foundRequest, approval, r.Scheme) as
before but treat controllerutil.AlreadyOwnedError as non-fatal (ignore it and
return nil), and only propagate other errors from SetControllerReference;
reference symbols: metav1.GetControllerOf, OwnerReferences, foundRequest.UID,
controllerutil.SetControllerReference, controllerutil.AlreadyOwnedError,
APIKeyApproval, APIKeyRequest, Reconcile.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@api/v1alpha1/apikey_types.go`:
- Around line 43-46: Update the APIProductReference struct so Namespace is
treated as optional to match APIProductKey() fallback behavior: add the
+optional marker to the Namespace field's comment and change its json tag to
`json:"namespace,omitempty"` (i.e., make Namespace optional/omitempty). After
modifying the APIProductReference struct (referencing APIProductReference and
the APIProductKey() method which implements the fallback), run `make manifests
generate` to regenerate CRDs and generated code.

In `@config/samples/devportal_v1alpha1_apikeyrequest.yaml`:
- Around line 8-9: The sample manifest for APIKeyRequest is left as the scaffold
TODO (spec: empty), which is invalid; update
config/samples/devportal_v1alpha1_apikeyrequest.yaml by replacing the
placeholder with a concrete, valid APIKeyRequest specimen that includes all
required fields the CRD/schema expects (e.g., metadata.name, metadata.namespace,
and the spec fields used by the controller such as ownerRef or requestedKey
attributes), or remove the sample entirely if you don't want to ship a sample;
ensure the resource kind remains APIKeyRequest and the fields match the API/CRD
definitions used by the APIKey and controller so the sample applies without
schema errors.

In `@docs/apikey-approval-workflow.md`:
- Around line 169-183: The YAML example incorrectly includes apiKeyValue which
does not exist on the APIKeyStatus type; remove the apiKeyValue line from the
status example in apikey-approval-workflow.md and instead clarify (or reference)
that the actual API key is stored in the enforcement Secret in the Kuadrant
namespace (as described in step 9). Update the example status block to only
include valid APIKeyStatus fields (eg. conditions, limits, apiHostname,
authScheme) and add a short note pointing readers to the enforcement Secret
storage location rather than showing an apiKeyValue field.

In `@internal/controller/apikey_auto_approval_controller_test.go`:
- Around line 295-301: The test is listing APIKeyApproval resources cluster-wide
which can pick up approvals from other tests; update the k8sClient.List calls
that use APIKeyApprovalList (occurrences around the assertions) to scope the
query by namespace by passing client.InNamespace(apiProductNamespace) as an
option to k8sClient.List so the checks only consider approvals in
apiProductNamespace (mirror the existing pattern used in the idempotency and
existing approval tests).

In `@internal/controller/apikey_auto_approval_controller.go`:
- Around line 149-153: The SetupWithManager for APIKeyAutoApprovalReconciler
currently only watches APIKeyRequest but must also watch APIProduct changes
because reconcile logic depends on APIProduct.spec.approvalMode; update the
SetupWithManager method to add a Watch for devportalv1alpha1.APIProduct (or the
APIProduct type in your API group) and use a handler.EnqueueRequestsFromMapFunc
that, when an APIProduct changes, lists pending APIKeyRequest objects (e.g., in
APIKeyAutoApprovalReconciler.Reconcile or by using the reconciler's client) that
reference that product and enqueues them for reconciliation; keep existing
For(&devportalv1alpha1.APIKeyRequest{}) and Named("apikeyrequest-autoapproval")
but append the new Watch(...) so switching a product from "manual" to
"automatic" will trigger reprocessing of pending requests.

In `@internal/controller/apikey_secret_controller_test.go`:
- Around line 944-985: The test currently asserts enforcementSecret1.Name !=
enforcementSecret2.Name but the comment notes this will fail; either mark the
test pending or flip the assertion: Option A - make the test pending by
replacing It("should create distinct...") with PIt or XIt so the test is
skipped/pending while keeping current assertions (references: the It block,
enforcementSecret1/enforcementSecret2 variables and
enforcementSecretName(apiKey1)/enforcementSecretName(apiKey2)). Option B -
convert this into a regression test by inverting the expectation to
Expect(enforcementSecret1.Name).To(Equal(enforcementSecret2.Name)) and adjust
the subsequent data expectations to assert the observed overwrite (expect
enforcementSecret1.Data[...] equals the final value and remove contradictory
checks) so the test documents the known collision. Choose one option and apply
the change inside the same It block where enforcementSecret1, enforcementSecret2
and apiKeySecretKey are used.

In `@internal/controller/apikey_secret_controller.go`:
- Around line 119-120: The enforcement secret is incorrectly created with
CreateOnlyMutator which prevents propagating rotations, label changes, and
metadata updates; replace CreateOnlyMutator in the ReconcileResource call that
handles enforcementSecret with an update-capable mutator (e.g., a CreateOrUpdate
or Update-aware mutator used elsewhere in this repo) so desired state changes
are applied to existing Secrets, and update the corresponding Secret RBAC to
include "update" and "patch"; also ensure ReconcileResource usage follows the
controller pattern of comparing desired vs actual state and updating status
accordingly (refer to ReconcileResource, enforcementSecret, and
CreateOnlyMutator locations).
- Around line 71-74: The current Reconcile swallows all errors from
GetKuadrantNamespace by returning (ctrl.Result{}, nil); change this so only the
"not found" case is treated as a soft skip and all other errors are returned to
trigger a retry. Specifically, in the call to GetKuadrantNamespace(ctx,
r.Client) check the error with apierrors.IsNotFound(err)
(k8s.io/apimachinery/pkg/api/errors) and if true return (ctrl.Result{}, nil),
otherwise return (ctrl.Result{}, err). Update the handler around
GetKuadrantNamespace to implement this logic (and add the apierrors import if
missing).
- Around line 240-244: The SetupWithManager currently only watches APIKey
objects; add watches for Secret and Kuadrant resources so Secret
deletion/rotation or Kuadrant creation triggers reconciliation. Update
APIKeySecretReconciler.SetupWithManager to chain additional Watches() calls that
use handler.EnqueueRequestsFromMapFunc mapping functions (e.g.,
r.enqueueFromSecret and r.enqueueFromKuadrant) which translate Secret and
Kuadrant events into APIKey reconcile requests (reuse the existing enqueueClass
logic pattern), ensuring the controller still calls Complete(r). Implement small
helper methods enqueueFromSecret and enqueueFromKuadrant on
APIKeySecretReconciler to locate affected APIKey(s) and return the appropriate
reconcile.Requests.

In `@internal/controller/apikey_status_controller.go`:
- Around line 553-561: SetupWithManager currently only watches APIKey,
APIProduct, APIKeyRequest, APIKeyApproval, and HTTPRoute; add watches for the
consumer Secret and the cluster Kuadrant so changes to those resources trigger
reconciliation and update status.conditions. Modify APIKeyStatusReconciler's
SetupWithManager to call Watches (or Owns/For as appropriate) for corev1.Secret
(the consumer Secret referenced by calculateFailedCondition) and the Kuadrant
resource type used by calculateFailedCondition, ensuring
handler.EnqueueRequestsFromMapFunc(r.enqueueClass) is used so enqueueClass is
invoked when those resources change. Keep the controller name "apikey-status"
and return Complete(r) as before. Ensure the resource type identifiers match the
imports and types used elsewhere (e.g., calculateFailedCondition,
APIKeyStatusReconciler, enqueueClass).

In `@internal/controller/apikeyapproval_controller_test.go`:
- Around line 157-165: The test currently dereferences ownerRef.Controller and
ownerRef.BlockOwnerDeletion without nil checks, which can cause a panic; update
the Eventually callback that inspects updatedApproval.OwnerReferences to also
verify ownerRef.Controller != nil and ownerRef.BlockOwnerDeletion != nil before
dereferencing, returning false (so the assertion fails cleanly) if either
pointer is nil, and only then compare *ownerRef.Controller == true and
*ownerRef.BlockOwnerDeletion == true while keeping the existing checks for
ownerRef.Name, ownerRef.Kind, and ownerRef.APIVersion (referring to
updatedApproval.OwnerReferences and
ownerRef.Controller/ownerRef.BlockOwnerDeletion).

In `@internal/controller/apikeyrequest_controller.go`:
- Around line 152-157: The controller currently only watches APIProduct and
APIKey so APIKeyRequest edits/deletes don't trigger reconciliation; update
APIKeyRequestReconciler.SetupWithManager to also watch
devportalv1alpha1.APIKeyRequest resources (e.g., add a Watches/For entry for
&devportalv1alpha1.APIKeyRequest{} using an appropriate handler like
handler.EnqueueRequestForObject or handler.EnqueueRequestsFromMapFunc) so
changes to APIKeyRequest objects will enqueue reconciliations; ensure the watch
targets the same reconcile flow that uses enqueueClass and that
APIKeyRequest-related reconciler logic (automatic approval and status sync) in
APIKeyRequestReconciler is exercised on those events.
- Around line 141-148: The mutator apiKeyRequestSpecMutator has its parameters
reversed causing it to write spec changes into the transient desired object;
change the function signature to accept (existing, desired
*devportalv1alpha1.APIKeyRequest) and update the body to compare existing.Spec
vs desired.Spec and, when different, set existing.Spec = desired.Spec and return
true so the actual existing resource is updated; keep the function name
apiKeyRequestSpecMutator so callers (e.g., ReconcileResource) continue to work.

In `@internal/controller/apikeyrequest_status_controller.go`:
- Line 34: Update the stale top-of-type comment for
APIKeyRequestStatusReconciler: replace the copied description that mentions
"APIKeyRequestReconciler" and creating shadow resources with a concise comment
stating that APIKeyRequestStatusReconciler reconciles APIKeyRequest status by
syncing conditions from the corresponding APIKey into the APIKeyRequest;
reference the struct name APIKeyRequestStatusReconciler and its responsibility
to propagate APIKey conditions to APIKeyRequest status.

In `@internal/reconcilers/base_reconcilers_test.go`:
- Around line 110-114: The test schemes only register apps/v1 but need core/v1
so ConfigMap and other core types are recognized; in both setup blocks that
create a scheme (symbol s := runtime.NewScheme and call appsv1.AddToScheme(s))
also call corev1.AddToScheme(s) (or the imported alias used for core/v1 in this
file) and handle the returned error similarly to the existing error checks so
the fake client can work with v1.ConfigMap.

In `@README.md`:
- Line 238: The workflow references status.apiKeyValue but the "APIKey Status
Fields" docs (the "APIKey Status Fields" section and docs/references/apikey.md)
do not list it; either add apiKeyValue to the status-fields documentation or
remove the workflow references. Update the "APIKey Status Fields" table to
include a row for apiKeyValue with a clear description and example, and/or
remove occurrences of status.apiKeyValue from the workflow steps (lines that
reference retrieving the API key) so docs and workflow remain consistent. Ensure
you change both places mentioned in the review (the README "APIKey Status
Fields" section and the apikey reference) so they match the workflow.

In `@test/e2e/automatic_approval_test.go`:
- Around line 333-372: The tests currently use `.items[0]` to assert properties
which can pick the wrong APIKeyApproval; change the verification helpers
(verifyApprovalCreated, verifySystemReviewer, verifyApproved, verifyReason) to
target the deterministic approval resource name (<apiKeyRequest>-auto) or filter
by spec.apiKeyRequestRef.name instead of indexing .items[0]. Specifically,
construct the expected approval name (e.g., fmt.Sprintf("%s-auto",
apiKeyRequestName)) and run kubectl get apikeyapproval <expectedName> -n
ownerNamespace -o jsonpath=... (or use jsonpath with a select expression
matching spec.apiKeyRequestRef.name) so each assertion reads the exact approval
resource rather than the first item.

---

Outside diff comments:
In `@config/rbac/role.yaml`:
- Around line 17-27: The RBAC role for the apikeyapprovals resource is missing
the "delete" verb which causes forbidden errors when
internal/controller/apikeyapproval_controller.go attempts to delete approvals;
update the verbs list for the apikeyapprovals resource in config/rbac/role.yaml
to include "delete" so the manager role can perform cleanup operations (ensure
the entry references the devportal.kuadrant.io apiGroup and the apikeyapprovals
resource name).

---

Nitpick comments:
In `@api/v1alpha1/apikeyapproval_types.go`:
- Around line 60-69: The APIKeyApprovalStatus currently only exposes Conditions
(used for the `Valid` condition elsewhere); add explicit condition type
constants and expand status tracking to cover the controller lifecycle (e.g.,
define new condition type constants like APIKeyApprovalConditionProcessed and
APIKeyApprovalConditionApplied alongside the existing Valid type) and update any
condition-setting helpers (the code that sets Conditions on
APIKeyApprovalStatus, e.g., where you call metav1.Condition{Type: ..., Status:
...}) to emit these new types at the appropriate lifecycle points so the status
reflects Processed, Applied and Valid states for consumers and tests to observe.

In `@api/v1alpha1/apikeyrequest_types.go`:
- Around line 63-68: Add explicit condition type constants for APIKeyRequest to
match API patterns and the existing APIKeyApproval usage: define string
constants named APIKeyRequestConditionPending, APIKeyRequestConditionApproved,
and APIKeyRequestConditionDenied (with brief doc comments) near the
APIKeyRequestStatus declaration so callers populate Conditions
([]metav1.Condition) using these types; ensure the constants are of the same
type used for condition Type values (string/metav1.ConditionType) and referenced
where status conditions are set or checked.

In `@config/crd/bases/devportal.kuadrant.io_apikeyapprovals.yaml`:
- Line 17: Add an additionalPrinterColumns section to the ApikeyApprovals CRD
(v1alpha1) so kubectl get shows useful fields; add columns mapping to JSONPaths:
Approved -> .status.approved, ReviewedBy -> .status.reviewedBy, and Valid ->
.status.conditions[?(@.type=="Valid")].status (use string type for these
columns), ensuring the key is named additionalPrinterColumns and placed under
the CRD's spec so kubectl get apikeyapprovals includes Approved, ReviewedBy and
the Valid condition.

In `@config/crd/bases/devportal.kuadrant.io_apikeys.yaml`:
- Around line 313-317: Add a "minimum: 0" constraint to the top-level
status.observedGeneration schema to match the existing
conditions[].observedGeneration constraint; update the status.observedGeneration
property in the CRD (the status.observedGeneration descriptor) so it includes
"type: integer", "format: int64" and "minimum: 0" to enforce non-negative
generations consistently with conditions[].observedGeneration.

In `@internal/controller/apikey_secret_controller_test.go`:
- Around line 820-835: The test currently uses hardcoded namespace names
nsFooBar ("foo-bar") and nsFoo ("foo") which can collide across runs; change the
setup to call createNamespaceWithContext to create unique namespaces and assign
their returned names to nsFooBar and nsFoo, then use those variables when
constructing the APIKey objects (references: nsFooBar, nsFoo,
createNamespaceWithContext) so each test run gets unique namespaces and avoids
cross-run conflicts.

In `@internal/controller/apikeyapproval_controller.go`:
- Around line 112-123: Add a defensive check before trying to set the owner
reference: call metav1.GetControllerOf(approval) and if it returns a non-nil
controllerRef whose UID differs from foundRequest.UID then skip setting the
owner (log and return nil) to avoid AlreadyOwnedError; otherwise call
controllerutil.SetControllerReference(foundRequest, approval, r.Scheme) as
before but treat controllerutil.AlreadyOwnedError as non-fatal (ignore it and
return nil), and only propagate other errors from SetControllerReference;
reference symbols: metav1.GetControllerOf, OwnerReferences, foundRequest.UID,
controllerutil.SetControllerReference, controllerutil.AlreadyOwnedError,
APIKeyApproval, APIKeyRequest, Reconcile.

In `@internal/controller/apikeyapproval_status_controller.go`:
- Around line 96-100: The skip-update condition is redundant because
calculateStatus sets newStatus.ObservedGeneration = approval.Generation so if
equality.Semantic.DeepEqual(newStatus, &approval.Status) is true the
ObservedGeneration check is unnecessary; simplify the guard in the update path
by removing the second clause (approval.Generation ==
approval.Status.ObservedGeneration) and only check equalStatus (i.e., the
DeepEqual result) before logging "apikeyapproval status unchanged, skipping
update" and returning nil; ensure this change is applied where equalStatus is
computed (using equality.Semantic.DeepEqual) and that calculateStatus remains
responsible for setting newStatus.ObservedGeneration.

In `@internal/controller/apikeyrequest_controller_test.go`:
- Around line 284-301: The test uses k8sClient.Get directly for firstRequest and
secondRequest (via APIKeyRequestName) which is inconsistent and brittle compared
to the rest of the suite that uses Eventually; change those direct Get calls to
use Eventually so the test waits for the objects to be present before asserting
their specs. Specifically, wrap the retrievals of firstRequest and secondRequest
(the calls to k8sClient.Get with firstKey/secondKey) in Eventually blocks that
assert the Get succeeds, then assert firstRequest.Spec.PlanTier and
secondRequest.Spec.PlanTier afterward (keeping the same APIKeyRequestName and
namespace references).

In `@internal/controller/apikeyrequest_status_controller.go`:
- Around line 149-153: In function enqueueClass
(APIKeyRequestStatusReconciler.enqueueClass) remove the redundant string
conversion around the literal by replacing Name: string("apikeyrequeststatus")
with Name: "apikeyrequeststatus"; this eliminates the no-op conversion while
preserving the exact value returned by enqueueClass.
- Around line 141-147: The controller SetupWithManager in
APIKeyRequestStatusReconciler is watching APIProduct unnecessarily; remove the
Watches(&devportalv1alpha1.APIProduct{},
handler.EnqueueRequestsFromMapFunc(r.enqueueClass)) line so the reconciler only
watches APIKey events (and enqueues via r.enqueueClass) before calling
Named("apikeyrequeststatus").Complete(r), since the reconciler never reads or
inspects APIProduct objects.

In `@make/demo.mk`:
- Line 1: The Makefile's demo-install target is not declared phony, so add a
.PHONY declaration for it; update the Makefile to include a line like ".PHONY:
demo-install" (or add demo-install to an existing .PHONY list) so the
demo-install recipe always runs regardless of any file named "demo-install".

In `@test/e2e/e2e_suite_test.go`:
- Around line 113-123: The cleanup commands in the AfterSuite currently discard
errors from utils.Run when running exec.Command("make", "undeploy"),
exec.Command("make", "uninstall"), and exec.Command("kubectl", "delete", "ns",
namespace); update the code to capture the error return from utils.Run for each
command and log any non-nil error (including the command and namespace) instead
of ignoring it—for example, assign the returned error to a variable and emit it
via GinkgoWriter.Printf or fmt.Printf so failures in utils.Run are visible
during test runs while preserving the cleanup flow.

In `@utils/demo/gamestore.yaml`:
- Around line 54-62: The pod spec for the container named "gamestore" is missing
a securityContext; add a minimal container-level securityContext under the
gamestore entry to meet Pod Security Standards (e.g., set runAsNonRoot: true,
runAsUser to a non-root UID, readOnlyRootFilesystem: true,
allowPrivilegeEscalation: false, and drop all non-needed capabilities). Update
the spec.containers[0] (container name "gamestore") to include these fields so
the manifest can be accepted by clusters enforcing the restricted policy.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 502635c1-d492-4152-a879-cf6017bd2c34

📥 Commits

Reviewing files that changed from the base of the PR and between 502a3e4 and b12a3cc.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (66)
  • .github/workflows/verify.yml
  • AGENTS.md
  • CLAUDE.md
  • CLAUDE.md
  • Makefile
  • PROJECT
  • README.md
  • api/v1alpha1/apikey_types.go
  • api/v1alpha1/apikeyapproval_types.go
  • api/v1alpha1/apikeyrequest_types.go
  • api/v1alpha1/zz_generated.deepcopy.go
  • cmd/main.go
  • config/crd/bases/devportal.kuadrant.io_apikeyapprovals.yaml
  • config/crd/bases/devportal.kuadrant.io_apikeyrequests.yaml
  • config/crd/bases/devportal.kuadrant.io_apikeys.yaml
  • config/crd/kustomization.yaml
  • config/rbac/apikeyapproval_admin_role.yaml
  • config/rbac/apikeyapproval_editor_role.yaml
  • config/rbac/apikeyapproval_viewer_role.yaml
  • config/rbac/apikeyrequest_admin_role.yaml
  • config/rbac/apikeyrequest_editor_role.yaml
  • config/rbac/apikeyrequest_viewer_role.yaml
  • config/rbac/kustomization.yaml
  • config/rbac/role.yaml
  • config/samples/devportal_v1alpha1_apikeyapproval.yaml
  • config/samples/devportal_v1alpha1_apikeyrequest.yaml
  • config/samples/kustomization.yaml
  • docs/apikey-approval-workflow.md
  • docs/references/apikey.md
  • docs/references/apikeyapproval.md
  • go.mod
  • internal/controller/apikey_auto_approval_controller.go
  • internal/controller/apikey_auto_approval_controller_test.go
  • internal/controller/apikey_controller.go
  • internal/controller/apikey_controller_test.go
  • internal/controller/apikey_secret_controller.go
  • internal/controller/apikey_secret_controller_test.go
  • internal/controller/apikey_status_controller.go
  • internal/controller/apikey_status_controller_test.go
  • internal/controller/apikeyapproval_controller.go
  • internal/controller/apikeyapproval_controller_test.go
  • internal/controller/apikeyapproval_status_controller.go
  • internal/controller/apikeyapproval_status_controller_test.go
  • internal/controller/apikeyrequest_controller.go
  • internal/controller/apikeyrequest_controller_test.go
  • internal/controller/apikeyrequest_status_controller.go
  • internal/controller/apikeyrequest_status_controller_test.go
  • internal/controller/apiproduct_controller.go
  • internal/controller/apiproduct_controller_authpolicy_discover_test.go
  • internal/controller/apiproduct_controller_oidc_discover_test.go
  • internal/controller/apiproduct_controller_test.go
  • internal/controller/devportal_helper.go
  • internal/controller/suite_test.go
  • internal/reconcilers/base_mutator.go
  • internal/reconcilers/base_reconcilers.go
  • internal/reconcilers/base_reconcilers_test.go
  • make/demo.mk
  • make/development-environment.mk
  • make/kuadrant-core.mk
  • make/verify.mk
  • test/e2e/automatic_approval_test.go
  • test/e2e/e2e_suite_test.go
  • test/e2e/e2e_test.go
  • test/utils/utils.go
  • utils/demo/gamestore.yaml
  • utils/demo/kuadrant.yaml
💤 Files with no reviewable changes (4)
  • internal/controller/apikey_controller_test.go
  • internal/controller/apikey_controller.go
  • .github/workflows/verify.yml
  • make/verify.mk

Comment thread api/v1alpha1/apikey_types.go
Comment thread config/samples/devportal_v1alpha1_apikeyrequest.yaml Outdated
Comment thread docs/apikey-approval-workflow.md
Comment thread internal/controller/apikey_auto_approval_controller_test.go
Comment thread internal/controller/apikey_auto_approval_controller.go
Comment thread internal/controller/apikeyrequest_controller.go
Comment thread internal/controller/apikeyrequest_status_controller.go
Comment thread internal/reconcilers/base_reconcilers_test.go
Comment thread README.md
Comment thread test/e2e/automatic_approval_test.go
@eguzki eguzki added the enhancement New feature or request label May 4, 2026
@eguzki eguzki added this to Kuadrant May 4, 2026
@eguzki eguzki moved this to Ready For Review in Kuadrant May 4, 2026
@eguzki eguzki self-assigned this May 4, 2026
eguzki added 4 commits May 4, 2026 14:12
Signed-off-by: Eguzki Astiz Lezaun <eastizle@redhat.com>
Signed-off-by: Eguzki Astiz Lezaun <eastizle@redhat.com>
…ted output

Signed-off-by: Eguzki Astiz Lezaun <eastizle@redhat.com>
Signed-off-by: Eguzki Astiz Lezaun <eastizle@redhat.com>
Signed-off-by: Eguzki Astiz Lezaun <eastizle@redhat.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

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)
internal/reconcilers/base_mutator.go (1)

9-10: ⚡ Quick win

Align TypedMutateFn parameter order with actual call semantics.

TypedMutateFn is declared as (desired, existing) but invoked as (existing, desired). This is easy to misread and can cause mutator logic to target the wrong object. Please make the type signature consistent with usage.

Proposed diff
-// TypedMutateFn is a function which mutates the existing T into it's desired state.
-type TypedMutateFn[T client.Object] func(desired, existing T) bool
+// TypedMutateFn is a function which mutates the existing T into its desired state.
+type TypedMutateFn[T client.Object] func(existing, desired T) bool

Based on learnings: “follow the repo convention for parameter naming/order: implement mutators as someMutator(existing, desired *Type) bool.”

Also applies to: 27-27

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/reconcilers/base_mutator.go` around lines 9 - 10, TypedMutateFn is
declared with parameters (desired, existing) but callers use (existing,
desired); change the generic type alias declaration TypedMutateFn[T
client.Object] to func(existing, desired T) bool and update any other similar
declarations (e.g., the other signature noted around line 27) and all
implementations/call-sites to match that order so implementations follow the
repo convention of mutators as someMutator(existing, desired *Type) bool.
internal/controller/apikey_secret_controller_test.go (1)

490-573: ⚡ Quick win

Add a case for empty api_key value (not just missing key).

The suite validates missing key, but not the explicit empty-value path handled in internal/controller/apikey_secret_controller.go (Line 179-185). A small test for api_key: "" would close that branch.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/controller/apikey_secret_controller_test.go` around lines 490 - 573,
Add a test variant in the same Context that covers the empty-string value for
the consumer secret's "api_key" (not just missing key): create consumerSecret
with Data mapping "api_key" to []byte(""), create the APIKey with Approved
condition as done with apiKey, then call APIKeySecretReconciler.Reconcile(ctx,
reconcile.Request{}) and assert an error occurs containing "consumer secret",
"missing" or "empty", "api_key" and "approved APIKey", and finally verify the
enforcement secret (enforcementSecretName(apiKey)) is not created; this mirrors
the existing test flow but uses an explicit empty api_key value to exercise the
branch in APIKeySecretReconciler/Reconcile that handles empty api_key.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@config/crd/bases/devportal.kuadrant.io_apikeys.yaml`:
- Around line 65-69: The CRD schema currently defines the
apiProductRef.namespace property but does not mark it as required; update the
CRD object schema for apiProductRef (the object that currently has "required: -
name") to include "namespace" in the required array so the required list becomes
["name","namespace"], ensuring apiProductRef.namespace is mandatory and
preserving namespace isolation for APIKey/APIKeyRequest resources.

In `@docs/apikey-approval-workflow.md`:
- Around line 40-43: Rename the section heading "Create APIKey Request" to
accurately reflect the resource being created (e.g., "Create APIKey" or "Create
APIKey resource") so it does not get confused with the shadow resource
`APIKeyRequest`; update the heading text in the docs where the command creates
an `APIKey` and ensure any nearby references to `APIKeyRequest` remain unchanged
for clarity.
- Around line 102-105: The markdown fenced code blocks that show command output
(e.g., the block containing "NAME  AGE" and "consumer-app-gamestore-apikey 
<time>") are missing a language identifier; update those fences to use the
"text" language (add ```text) so markdownlint rule MD040 is satisfied; apply the
same change to the other output block referenced (the second block showing
similar command output).

In `@internal/controller/apikey_secret_controller.go`:
- Around line 109-112: The current code returns immediately on per-APIKey errors
(calls that log with logger.Error(..., "failed to generate desired enforcement
secret", "apiKey", client.ObjectKeyFromObject(apiKey)) then return
ctrl.Result{}, err), which aborts the whole batch; instead, change those
immediate return statements (the spots that return ctrl.Result{}, err after
logger.Error) to log the error and continue processing the remaining apiKey
items (i.e., replace the return with a continue inside the loop), and optionally
accumulate errors in a slice to decide at the end whether to requeue once
(return a single aggregated error or nil) so one malformed/missing-secret APIKey
does not prevent reconciling others.

In `@README.md`:
- Around line 543-549: The README's consumer quick-start omits the required
secret creation step before creating an APIKey; add an explicit prereq
instructing users to create a Secret containing the credentials referenced by
APIKey.spec.secretRef (e.g., create a Kubernetes Secret with the expected keys
and name), then proceed to create the APIKey resource that references the API
owner via APIKey.spec.apiProductRef.namespace and the Secret via
APIKey.spec.secretRef; ensure the new step appears before the "Create an APIKey"
bullet so first-apply succeeds.

---

Nitpick comments:
In `@internal/controller/apikey_secret_controller_test.go`:
- Around line 490-573: Add a test variant in the same Context that covers the
empty-string value for the consumer secret's "api_key" (not just missing key):
create consumerSecret with Data mapping "api_key" to []byte(""), create the
APIKey with Approved condition as done with apiKey, then call
APIKeySecretReconciler.Reconcile(ctx, reconcile.Request{}) and assert an error
occurs containing "consumer secret", "missing" or "empty", "api_key" and
"approved APIKey", and finally verify the enforcement secret
(enforcementSecretName(apiKey)) is not created; this mirrors the existing test
flow but uses an explicit empty api_key value to exercise the branch in
APIKeySecretReconciler/Reconcile that handles empty api_key.

In `@internal/reconcilers/base_mutator.go`:
- Around line 9-10: TypedMutateFn is declared with parameters (desired,
existing) but callers use (existing, desired); change the generic type alias
declaration TypedMutateFn[T client.Object] to func(existing, desired T) bool and
update any other similar declarations (e.g., the other signature noted around
line 27) and all implementations/call-sites to match that order so
implementations follow the repo convention of mutators as someMutator(existing,
desired *Type) bool.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b3f63592-f60a-4c95-8ab2-59fe0ee17ad6

📥 Commits

Reviewing files that changed from the base of the PR and between b12a3cc and e39a01c.

📒 Files selected for processing (13)
  • README.md
  • api/v1alpha1/apikey_types.go
  • config/crd/bases/devportal.kuadrant.io_apikeys.yaml
  • config/samples/devportal_v1alpha1_apikeyrequest.yaml
  • docs/apikey-approval-workflow.md
  • internal/controller/apikey_auto_approval_controller_test.go
  • internal/controller/apikey_secret_controller.go
  • internal/controller/apikey_secret_controller_test.go
  • internal/controller/apikeyapproval_controller_test.go
  • internal/controller/apikeyrequest_controller.go
  • internal/controller/apikeyrequest_status_controller.go
  • internal/reconcilers/base_mutator.go
  • test/e2e/automatic_approval_test.go
✅ Files skipped from review due to trivial changes (3)
  • internal/controller/apikeyapproval_controller_test.go
  • internal/controller/apikey_auto_approval_controller_test.go
  • test/e2e/automatic_approval_test.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • internal/controller/apikeyrequest_status_controller.go
  • api/v1alpha1/apikey_types.go
  • internal/controller/apikeyrequest_controller.go

Comment thread config/crd/bases/devportal.kuadrant.io_apikeys.yaml
Comment thread docs/apikey-approval-workflow.md
Comment thread docs/apikey-approval-workflow.md
Comment thread internal/controller/apikey_secret_controller.go
Comment thread README.md
Signed-off-by: Eguzki Astiz Lezaun <eastizle@redhat.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

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 the current code and only fix it if needed.

Inline comments:
In `@README.md`:
- Around line 452-456: Update the manual workflow sentence describing the
initial Pending state so it matches the APIKey Status Fields section: instead of
saying “no approval conditions,” state that the APIKey starts with a Pending
condition (condition type "Pending") whose status is "True" indicating it awaits
approval; reference the "APIKey Status Fields" section and the condition name
"Pending" to ensure wording is consistent.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 47033907-44ab-4f96-a6dd-1c4622069252

📥 Commits

Reviewing files that changed from the base of the PR and between e39a01c and 2b3232b.

📒 Files selected for processing (2)
  • README.md
  • docs/apikey-approval-workflow.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/apikey-approval-workflow.md

Comment thread README.md
Signed-off-by: Eguzki Astiz Lezaun <eastizle@redhat.com>
Copy link
Copy Markdown
Contributor

@R-Lawton R-Lawton left a comment

Choose a reason for hiding this comment

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

My first pass of the code, i have done some manual verification too double checking some things ive found before i comment


// APIKeyRequestName constructs the APIKeyRequest name for a given APIKey
// Pattern: {apiKeyNamespace}-{apiKeyName}
func APIKeyRequestName(apiKey *devportalv1alpha1.APIKey) string {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We have a issue in the console plugin for same apikey names im thinking we add checks here too for same apikey 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.

So, what was your thought? Should we check if the APIKeyRequest name is already taken and then report it in the APIKey status? That can easily be done.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

yeah that sounds like a good way

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.

Been thinking a bit more about this. I think there is a better approach than the check for already existing name.

Previously, APIKeyRequestName() used a sloppy pattern: {namespace}-{name}, which could cause naming collisions. For example, an APIKey foo-bar/baz and foo/bar-baz would both produce the same APIKeyRequest name foo-bar-baz.

Changes:

  • Updated pattern to: {namespace}-{name}-{hash} (matching the enforcement secret pattern)
  • The hash is an 8-character SHA256 suffix derived from {namespace}/{name}, ensuring DNS-1123 compliance

Benefits:

  • Collision-free: The hash guarantees a deterministic 1:1 mapping from APIKey to APIKeyRequest
  • Idempotent reconciliation: If the APIKeyRequest already exists, the controller simply updates it to sync with the APIKey state - no conflict errors
  • Consistency: Same naming approach as enforcement secrets for predictability

wdyt?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ok yes so eliminate the need for the check altogether have the names always be unique sounds like a better idea, good shout.

return ctrl.Result{}, nil
}

func (r *APIKeySecretReconciler) desiredEnforcementSecret(ctx context.Context, apiKey *devportalv1alpha1.APIKey, kuadrantNamespace string) (*corev1.Secret, error) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

More of a question, have we thought about the scenario where a apiproduct was once manual approval and gets changed to automatic. i was thinking we add a message in the console if they try edit the apiproduct to do this to warn these wont be autoapproved . I dont think we need to worry about autoapproval for once manual keys for this release but would be worth a issue for later wdyt?

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.

pending keys will eventually be approved after the apiproduct gets changed to automatic. So, adding highlighted notification in the console seems appropriate. I can create issue for that.

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.

Comment thread docs/apikey-approval-workflow.md
Signed-off-by: Eguzki Astiz Lezaun <eastizle@redhat.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 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 `@docs/apikey-approval-workflow.md`:
- Line 8: Update the phrasing in the docs to use the official capitalization by
replacing the phrase "github page" with "GitHub page" in the sentence
referencing the yq install link (the line containing "`yq` command line tool.
See the [github page](https://github.com/mikefarah/yq/#install)`"). Ensure
"GitHub" is capitalized exactly as the brand name wherever that link text
appears.
- Around line 74-88: The example YAML and the kubectl command disagree: the
command uses jsonpath='{.status}' (kubectl get apikey ... -o
jsonpath='{.status}') which prints the status object only, but the docs'
expected output shows a top-level status: wrapper; make them consistent by
either changing the command to output the full resource (remove the .status
jsonpath or use -o yaml) if you want the top-level "status:" shown, or modify
the example YAML to show only the contents of .status (remove the extra
top-level "status:" key) so it matches the jsonpath output.
- Around line 245-275: The denied-message in the kubectl patch for the
apikeyapproval resource (the `kubectl patch apikeyapproval
gamestore-approval-12345 ...` command) does not match the expected status
condition message; update one of them so they are consistent: either change the
patch payload's "message" value to the same format used in the expected APIKey
status condition (include the api-owner identity and suffix ": Denied"), or
adjust the expected YAML under "status.conditions.message" to exactly match "Use
case no longer meets security requirements"; ensure the change references the
apikeyapproval patch command and the APIKey status.conditions.message output so
tests/manual verification align.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 58581a45-45eb-4844-9e0d-f7a24b5e8788

📥 Commits

Reviewing files that changed from the base of the PR and between 2b3232b and 62b56fc.

📒 Files selected for processing (2)
  • README.md
  • docs/apikey-approval-workflow.md

Comment thread docs/apikey-approval-workflow.md
Comment thread docs/apikey-approval-workflow.md
Comment thread docs/apikey-approval-workflow.md Outdated
Signed-off-by: Eguzki Astiz Lezaun <eastizle@redhat.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
internal/controller/apikey_status_controller.go (3)

549-552: 💤 Low value

Remove orphan/dead comment above SetupWithManager.

Line 550 // Clear all status condition types (Approved, Denied, Failed) doesn't describe the SetupWithManager function below it and appears to be a leftover from refactored code. Recommend deleting to avoid reader confusion.

🛠️ Proposed fix
-// Clear all status condition types (Approved, Denied, Failed)
-
 // SetupWithManager sets up the controller with the Manager.
 func (r *APIKeyStatusReconciler) SetupWithManager(mgr ctrl.Manager) error {
🤖 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 `@internal/controller/apikey_status_controller.go` around lines 549 - 552,
Remove the orphan comment above SetupWithManager: delete the standalone comment
line "// Clear all status condition types (Approved, Denied, Failed)" that sits
immediately before the SetupWithManager function in apikey_status_controller.go
so the comment no longer misleads readers; leave the SetupWithManager function
and its real comments unchanged.

45-49: ⚡ Quick win

Stale doc comment: wrong reconciler name and resource.

The doc comment on Line 45 still claims APIProductReconciler reconciles a APIProduct object, but the type below is APIKeyStatusReconciler and it reconciles APIKey status. Please update to avoid confusion in generated docs and IDE hovers.

🛠️ Proposed fix
-// APIProductReconciler reconciles a APIProduct object
+// APIKeyStatusReconciler reconciles APIKey status fields and conditions.
 type APIKeyStatusReconciler struct {
🤖 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 `@internal/controller/apikey_status_controller.go` around lines 45 - 49, The
doc comment above the APIKeyStatusReconciler type is stale and mentions
APIProduct; update the comment to correctly describe the reconciler and
resource—e.g., change the comment to indicate that APIKeyStatusReconciler
reconciles APIKey status (or similar wording like "APIKeyStatusReconciler
reconciles the status of APIKey objects") so generated docs and IDE hovers match
the APIKeyStatusReconciler type.

121-125: ⚡ Quick win

Log message references the wrong resource.

The skip-update log on Line 123 says "apiproduct status unchanged, skipping update", but this controller reconciles APIKey status. Misleading when grepping logs.

🛠️ Proposed fix
 	if equalStatus && apiKey.Generation == apiKey.Status.ObservedGeneration {
-		logger.V(1).Info("apiproduct status unchanged, skipping update")
+		logger.V(1).Info("apikey status unchanged, skipping update")
 		return nil
 	}
🤖 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 `@internal/controller/apikey_status_controller.go` around lines 121 - 125, The
log message incorrectly mentions "apiproduct" while the code is reconciling an
APIKey; update the logger call in the block that checks
equality.Semantic.DeepEqual(newStatus, &apiKey.Status) and apiKey.Generation ==
apiKey.Status.ObservedGeneration so it logs a correct, specific message like
"apikey status unchanged, skipping update" (or similar) using the existing
logger.V(1).Info invocation and keep the rest of the logic unchanged.
🤖 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 `@internal/controller/apikey_status_controller.go`:
- Around line 348-366: In calculateDeniedCondition, the code checks
approval.Spec.Reason but appends approval.Spec.Message, causing an empty ": "
when Message is empty; update the conditional to check approval.Spec.Message !=
"" (mirroring calculateApprovedCondition) so the denial message only appends the
message when approval.Spec.Message is present (ensure you edit the check in
calculateDeniedCondition that references approval.Spec.Reason and leave the
interpolation using approval.Spec.Message).

---

Nitpick comments:
In `@internal/controller/apikey_status_controller.go`:
- Around line 549-552: Remove the orphan comment above SetupWithManager: delete
the standalone comment line "// Clear all status condition types (Approved,
Denied, Failed)" that sits immediately before the SetupWithManager function in
apikey_status_controller.go so the comment no longer misleads readers; leave the
SetupWithManager function and its real comments unchanged.
- Around line 45-49: The doc comment above the APIKeyStatusReconciler type is
stale and mentions APIProduct; update the comment to correctly describe the
reconciler and resource—e.g., change the comment to indicate that
APIKeyStatusReconciler reconciles APIKey status (or similar wording like
"APIKeyStatusReconciler reconciles the status of APIKey objects") so generated
docs and IDE hovers match the APIKeyStatusReconciler type.
- Around line 121-125: The log message incorrectly mentions "apiproduct" while
the code is reconciling an APIKey; update the logger call in the block that
checks equality.Semantic.DeepEqual(newStatus, &apiKey.Status) and
apiKey.Generation == apiKey.Status.ObservedGeneration so it logs a correct,
specific message like "apikey status unchanged, skipping update" (or similar)
using the existing logger.V(1).Info invocation and keep the rest of the logic
unchanged.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cbcc5153-20cb-4c34-acbf-7a0797b95efd

📥 Commits

Reviewing files that changed from the base of the PR and between 62b56fc and 2faac51.

📒 Files selected for processing (3)
  • docs/apikey-approval-workflow.md
  • internal/controller/apikey_status_controller.go
  • internal/controller/devportal_helper.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/controller/devportal_helper.go

Comment thread internal/controller/apikey_status_controller.go
Signed-off-by: Eguzki Astiz Lezaun <eastizle@redhat.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

Status: Ready For Review

Development

Successfully merging this pull request may close these issues.

2 participants