Skip to content

feat: add requireExistingSecret for OAuth2Client - #305

Open
matzegebbe wants to merge 2 commits into
ory:masterfrom
matzegebbe:feat/requireExistringSecret
Open

feat: add requireExistingSecret for OAuth2Client#305
matzegebbe wants to merge 2 commits into
ory:masterfrom
matzegebbe:feat/requireExistringSecret

Conversation

@matzegebbe

@matzegebbe matzegebbe commented Aug 6, 2026

Copy link
Copy Markdown

Adds an opt-in option to wait for the Secret referenced by spec.secretName
instead of registering the OAuth2 client with a Hydra-generated secret and
creating that Secret itself.

Why. When spec.secretName points at an externally managed Secret — one
produced by the External Secrets Operator, sealed-secrets, or any other
controller syncing it from a vault — the current "secret not found" branch in
controllers/oauth2client_controller.go
loses a race. If the OAuth2Client reconciles before the Secret has been
materialized, hydra-maester registers the client in Hydra with a random
secret and creates its own Secret with an OwnerReference. The external
controller then either refuses to adopt that pre-existing, unmanaged Secret
(External Secrets Operator with creationPolicy: Owner) or ends up disagreeing
with the value Hydra holds. The end state depends purely on which controller
wins the race.

What. The behavior is available controller-wide and per resource.

Controller-wide, via a new flag that also reads from an environment variable:

--require-existing-secret        # or REQUIRE_EXISTING_SECRET=true

Per resource, via spec.requireExistingSecret, which overrides the
controller-wide default in both directions — a resource can opt in while the
controller default is off, and opt out while it is on:

apiVersion: hydra.ory.sh/v1alpha1
kind: OAuth2Client
spec:
  secretName: hydra-client-oauth2-secret
  # if true and the secret does not exist yet, requeue instead of
  # registering the client with a generated secret
  requireExistingSecret: true

While the referenced Secret is missing, the controller does not register the
client in Hydra, does not create a Secret, reports a pending condition, and
requeues with an exponential backoff (15s, doubling, capped at 5m):

status:
  conditions:
    - type: Ready
      status: "False"
  reconciliationError:
    statusCode: SECRET_NOT_FOUND
    description: secret my-namespace/hydra-client-oauth2-secret does not exist

Once the Secret appears, reconciliation proceeds normally: the client is
registered with the externally provided CLIENT_ID / CLIENT_SECRET, and the
Secret is left untouched — in particular no OwnerReference is added to it.

No breaking changes. The flag defaults to false and the spec field is
optional, so without any configuration the controller behaves exactly as before.
Documented in the
Externally managed secrets section of
the README.

Related Issue or Design Document

Fixes #294 Fixes #304

Checklist

  • I have read the contributing guidelines
    and signed the CLA.
  • I have referenced an issue containing the design document if my change
    introduces a new feature.
  • I have read the security policy.
  • I confirm that this pull request does not address a security
    vulnerability. If this pull request addresses a security vulnerability, I
    confirm that I got approval (please contact
    security@ory.com) from the maintainers to push
    the changes.
  • I have added tests that prove my fix is effective or that my feature
    works.
  • I have added the necessary documentation within the code base (if
    appropriate).

Further comments

Design decisions

  • Both a flag and a spec field. The linked issue proposed either one. A
    controller-level switch fits operators who run hydra-maester exclusively in a
    bring-your-own-secret setup, while the per-resource field is needed for mixed
    clusters. Making spec.requireExistingSecret a *bool keeps "unset"
    distinguishable from "explicitly false", so a single resource can opt out of a
    globally enabled default.
  • ObservedGeneration is deliberately left untouched while pending. Unlike
    updateReconciliationStatusError, the new pending-status writer does not
    advance it. Otherwise a client that goes pending and then receives its Secret
    could hit the Generation == ObservedGeneration short-circuit in Reconcile
    and stay Ready=False forever.
  • Explicit RequeueAfter instead of the workqueue rate limiter.
    Result.Requeue is deprecated in controller-runtime v0.24, and returning an
    error to get the built-in backoff would log an expected pending state at error
    level on every attempt. The attempt counter is tracked per object and cleared
    as soon as the Secret is found or the object is deleted.

Alternatives considered

  • Watching Secrets and enqueuing the referencing OAuth2Client. This would
    make pickup immediate rather than bounded by the backoff (up to 5 minutes in
    the worst case), and costs little, since the manager cache already runs a
    Secret informer for the existing r.Get on secrets. It was left out because
    it changes reconcile triggers for every user, not just those who opt in. Happy
    to add it if maintainers prefer that.
  • Enforcing ordering externally (ArgoCD sync-waves, Helm pre-install hooks).
    Brittle, tool-specific, and no protection against re-creation or drift after
    the initial apply.

Changes

File Change
api/v1alpha1/oauth2client_types.go new StatusSecretNotFound status code and spec.requireExistingSecret field
api/v1alpha1/zz_generated.deepcopy.go regenerated
config/crd/bases/hydra.ory.sh_oauth2clients.yaml regenerated
controllers/oauth2client_controller.go WithRequireExistingSecret option, wait-and-requeue branch, backoff tracking, pending-status write
main.go --require-existing-secret flag, defaulting from REQUIRE_EXISTING_SECRET
README.md flag/env var tables and an "Externally managed secrets" section
config/samples/hydra_v1alpha1_oauth2client_external_secret.yaml sample manifest

Tests

Four new integration specs in
controllers/oauth2client_controller_integration_test.go:

  1. resource-level opt-in → no client registered, no Secret created, status
    reports SECRET_NOT_FOUND with Ready=False, ObservedGeneration stays
    behind
  2. Secret appears later → client registered with the externally provided
    credentials, Secret left without an OwnerReference
  3. controller-wide default → applies to resources that do not set the field
  4. per-resource opt-out → Secret is created as before, even with the
    controller-wide default enabled

make test passes: 13 specs, all green.

Summary by CodeRabbit

  • New Features
    • Added support for externally managed OAuth2 client Secrets.
    • Resources can wait for a pre-provisioned Secret before registration and report a pending status when it is missing.
    • Added controller-wide configuration through REQUIRE_EXISTING_SECRET, with per-resource overrides.
    • Missing Secrets are retried automatically with capped exponential backoff.
    • Existing Secrets are used without generating or creating replacement Secrets.
    • Added configuration documentation and an example manifest.

Signed-off-by: Mathias Gebbe <mgebbe@hellmann.com>
@CLAassistant

CLAassistant commented Aug 6, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d46d5554-0b8f-448e-bc85-5875bf91bf70

📥 Commits

Reviewing files that changed from the base of the PR and between f5e9848 and dd90251.

📒 Files selected for processing (1)
  • config/crd/bases/hydra.ory.sh_oauth2clients.yaml

📝 Walkthrough

Walkthrough

The change adds controller-wide and per-resource settings for externally managed OAuth2Client secrets. Missing secrets produce pending status and exponential requeues. The API, CRD, CLI, environment configuration, sample manifest, documentation, and integration tests cover the new behavior.

Changes

Externally managed secret support

Layer / File(s) Summary
Secret requirement contract
api/v1alpha1/oauth2client_types.go, api/v1alpha1/zz_generated.deepcopy.go, config/crd/bases/hydra.ory.sh_oauth2clients.yaml, config/samples/hydra_v1alpha1_oauth2client_external_secret.yaml
Adds StatusSecretNotFound and optional spec.requireExistingSecret. Updates deep-copy logic, the CRD schema, and an external-secret sample.
Controller configuration wiring
main.go, controllers/oauth2client_controller.go
Adds REQUIRE_EXISTING_SECRET, the require-existing-secret flag, envBool, WithRequireExistingSecret, and reconciler state for the controller-wide default.
Missing-secret reconciliation
controllers/oauth2client_controller.go, README.md
Uses per-resource overrides, records pending status for missing secrets, applies exponential requeue from 15 seconds to five minutes, and documents the behavior.
Integration validation
controllers/oauth2client_controller_integration_test.go
Tests missing-secret handling, external-secret registration, controller defaults, per-resource opt-out, and controller option propagation.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant OAuth2ClientReconciler
  participant KubernetesAPI
  participant Hydra
  OAuth2ClientReconciler->>KubernetesAPI: Read referenced Secret
  alt Secret is missing and existing secret is required
    OAuth2ClientReconciler->>KubernetesAPI: Set SECRET_NOT_FOUND pending status
    OAuth2ClientReconciler->>OAuth2ClientReconciler: Schedule exponential requeue
  else Secret is available
    OAuth2ClientReconciler->>Hydra: Register OAuth2Client with supplied credentials
    OAuth2ClientReconciler->>KubernetesAPI: Update OAuth2Client status
  end
Loading

Suggested reviewers: piotrmsc, demonsthere

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.56% 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
Description check ✅ Passed The description explains the problem, solution, design decisions, linked issues, tests, documentation, and checklist completion.
Linked Issues check ✅ Passed The changes satisfy issues [#294] and [#304] by waiting for missing Secrets, reporting pending status, requeuing, and using external credentials.
Out of Scope Changes check ✅ Passed The API, controller, configuration, documentation, sample, and integration tests directly support the linked issue objectives.
Title check ✅ Passed The title clearly and concisely identifies the main feature: adding requireExistingSecret support for OAuth2Client.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@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: 2

🤖 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 `@controllers/oauth2client_controller.go`:
- Around line 499-516: Update updateReconciliationStatusPending to mark
ObservedGeneration stale whenever a resource enters pending status, or adjust
the reconciliation early-return guard to proceed when ReconciliationError is
non-empty. Add a regression test covering a ready resource losing its Secret and
recovering after the Secret returns, verifying the error clears and Ready
becomes true.

In `@README.md`:
- Line 73: Update the hydra-url example in the README table to remove the
leading space from the sample value, ensuring copied values begin directly with
the Hydra hostname.
🪄 Autofix

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 Plus

Run ID: f9d50670-34ab-419c-b4dc-7af730d72314

📥 Commits

Reviewing files that changed from the base of the PR and between 0493362 and f5e9848.

📒 Files selected for processing (8)
  • README.md
  • api/v1alpha1/oauth2client_types.go
  • api/v1alpha1/zz_generated.deepcopy.go
  • config/crd/bases/hydra.ory.sh_oauth2clients.yaml
  • config/samples/hydra_v1alpha1_oauth2client_external_secret.yaml
  • controllers/oauth2client_controller.go
  • controllers/oauth2client_controller_integration_test.go
  • main.go

Comment on lines +499 to +516
// updateReconciliationStatusPending reports that reconciliation has not finished
// yet and will be retried. Unlike updateReconciliationStatusError it leaves
// ObservedGeneration untouched, so the current generation is still reconciled
// from scratch once the blocker is gone.
func (r *OAuth2ClientReconciler) updateReconciliationStatusPending(ctx context.Context, c *hydrav1alpha1.OAuth2Client, code hydrav1alpha1.StatusCode, err error) error {
_, updateErr := controllerutil.CreateOrPatch(ctx, r.Client, c, func() error {
c.Status.ReconciliationError = hydrav1alpha1.ReconciliationError{
Code: code,
Description: err.Error(),
}
c.Status.Conditions = []hydrav1alpha1.OAuth2ClientCondition{
{
Type: hydrav1alpha1.OAuth2ClientConditionReady,
Status: hydrav1alpha1.ConditionFalse,
},
}

return nil

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 | 🟠 Major | ⚡ Quick win

Keep ObservedGeneration stale while the resource is pending.

If a ready resource later loses its Secret, ObservedGeneration already equals Generation. This method preserves that equality. When the Secret returns, the early return at lines 258-262 skips updateRegisteredOAuth2Client, so SECRET_NOT_FOUND and Ready=False remain until the spec changes.

Mark the observed generation as stale in the pending status, or make the early return require an empty reconciliation error. Add a regression test for this recovery path.

Proposed fix
 func (r *OAuth2ClientReconciler) updateReconciliationStatusPending(ctx context.Context, c *hydrav1alpha1.OAuth2Client, code hydrav1alpha1.StatusCode, err error) error {
 	_, updateErr := controllerutil.CreateOrPatch(ctx, r.Client, c, func() error {
+		if c.Status.ObservedGeneration >= c.Generation {
+			c.Status.ObservedGeneration = c.Generation - 1
+		}
 		c.Status.ReconciliationError = hydrav1alpha1.ReconciliationError{
📝 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
// updateReconciliationStatusPending reports that reconciliation has not finished
// yet and will be retried. Unlike updateReconciliationStatusError it leaves
// ObservedGeneration untouched, so the current generation is still reconciled
// from scratch once the blocker is gone.
func (r *OAuth2ClientReconciler) updateReconciliationStatusPending(ctx context.Context, c *hydrav1alpha1.OAuth2Client, code hydrav1alpha1.StatusCode, err error) error {
_, updateErr := controllerutil.CreateOrPatch(ctx, r.Client, c, func() error {
c.Status.ReconciliationError = hydrav1alpha1.ReconciliationError{
Code: code,
Description: err.Error(),
}
c.Status.Conditions = []hydrav1alpha1.OAuth2ClientCondition{
{
Type: hydrav1alpha1.OAuth2ClientConditionReady,
Status: hydrav1alpha1.ConditionFalse,
},
}
return nil
// updateReconciliationStatusPending reports that reconciliation has not finished
// yet and will be retried. Unlike updateReconciliationStatusError it leaves
// ObservedGeneration untouched, so the current generation is still reconciled
// from scratch once the blocker is gone.
func (r *OAuth2ClientReconciler) updateReconciliationStatusPending(ctx context.Context, c *hydrav1alpha1.OAuth2Client, code hydrav1alpha1.StatusCode, err error) error {
_, updateErr := controllerutil.CreateOrPatch(ctx, r.Client, c, func() error {
if c.Status.ObservedGeneration >= c.Generation {
c.Status.ObservedGeneration = c.Generation - 1
}
c.Status.ReconciliationError = hydrav1alpha1.ReconciliationError{
Code: code,
Description: err.Error(),
}
c.Status.Conditions = []hydrav1alpha1.OAuth2ClientCondition{
{
Type: hydrav1alpha1.OAuth2ClientConditionReady,
Status: hydrav1alpha1.ConditionFalse,
},
}
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 `@controllers/oauth2client_controller.go` around lines 499 - 516, Update
updateReconciliationStatusPending to mark ObservedGeneration stale whenever a
resource enters pending status, or adjust the reconciliation early-return guard
to proceed when ReconciliationError is non-empty. Add a regression test covering
a ready resource losing its Secret and recovering after the Secret returns,
verifying the error clears and Ready becomes true.

Comment thread README.md
| **leader-elector-namespace** | no | Leader elector namespace where controller should be set. | `""` | `"my-namespace"` |
| Name | Required | Description | Default value | Example values |
| ---------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | ---------------------------------------- |
| **hydra-url** | yes | ORY Hydra's service address | - | ` ory-hydra-admin.ory.svc.cluster.local` |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove the leading space from the hydra-url example.

The current example renders a value that starts with a space. Copying it can produce an invalid Hydra URL.

Proposed fix
-| **hydra-url**                | yes      | ORY Hydra's service address                                                                                                                 | -             | ` ory-hydra-admin.ory.svc.cluster.local` |
+| **hydra-url**                | yes      | ORY Hydra's service address                                                                                                                 | -             | `ory-hydra-admin.ory.svc.cluster.local` |
📝 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
| **hydra-url** | yes | ORY Hydra's service address | - | ` ory-hydra-admin.ory.svc.cluster.local` |
| **hydra-url** | yes | ORY Hydra's service address | - | `ory-hydra-admin.ory.svc.cluster.local` |
🧰 Tools
🪛 markdownlint-cli2 (0.23.2)

[warning] 73-73: Spaces inside code span elements

(MD038, no-space-in-code)

🤖 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 `@README.md` at line 73, Update the hydra-url example in the README table to
remove the leading space from the sample value, ensuring copied values begin
directly with the Hydra hostname.

Source: Linters/SAST tools

controller-gen rewrites config/crd/bases with its own indentation, which
fails the format CI job. Re-apply prettier to the generated manifest.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Mathias Gebbe <mgebbe@hellmann.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants