Skip to content

feat: support hint prerequisite chains in the Challenge CRD - #2

Merged
AYDEV-FR merged 2 commits into
mainfrom
feat/hint-prerequisites
Jun 26, 2026
Merged

feat: support hint prerequisite chains in the Challenge CRD#2
AYDEV-FR merged 2 commits into
mainfrom
feat/hint-prerequisites

Conversation

@AYDEV-FR

Copy link
Copy Markdown
Owner

Summary

Hints in the Challenge CRD were reconciled as an unordered multiset, so neither their order nor any unlock dependency between them could be expressed. CTFd models hint ordering through per-hint prerequisites (the IDs of hints that must be unlocked first); this PR exposes that declaratively.

Changes

  • CRD (ChallengeHint): two new optional fields
    • prerequisites []int — 0-based indexes into the same hints list of the hints that must be unlocked first (CTFd assigns hint IDs only at creation, so prerequisites are referenced positionally).
    • anonymize (true/false/preview) — behavior while prerequisites are unmet; no effect without prerequisites.
  • Controller (flagshints.go): hints now keep their identity across reconciles.
    • planHints matches desired↔existing by value while mapping each desired index to its real CTFd hint ID.
    • syncHints: create/reuse → wire prerequisites in a second pass (once every ID is known, so it handles references to hints created in the same pass) → delete extras.
    • A hint whose only change is its prerequisites is patched in place instead of delete+recreate, preserving its ID and players' unlock state.
    • hintsUpToDate now also compares the resolved prerequisite chain and anonymize.
    • challengeHints always fetches the full hint (the CTFd list omits requirements).
    • validateHintPrereqs rejects out-of-range indexes, self-references, and cycles (DFS) — a cycle would make hints impossible to unlock.

Example

hints:
  - title: Stuck?
    content: Look at how the license key is validated.
    cost: 10
  - content: The checksum is little-endian.
    cost: 25
    prerequisites: [0]   # unlocks only after hint #0

Notes

  • Raw display order (by ID) is still not forced — CTFd enforces unlock chains, not display order, which is what carries the ordering semantics here.
  • Matching stays value-based: two hints with identical content/title/cost aren't individually distinguishable (edge case), but prerequisite reconciliation remains consistent.

Testing

  • go test ./... — green
  • golangci-lint run — 0 issues
  • Added TestHintsUpToDatePrerequisites and TestValidateHintPrereqs; CRD + deepcopy regenerated via go generate ./apis/....

AYDEV-FR added 2 commits June 25, 2026 18:37
Hints were reconciled as an unordered multiset, so neither their order
nor any unlock dependency between them could be expressed. CTFd models
hint ordering through per-hint prerequisites (the IDs of hints that must
be unlocked first), which this exposes declaratively.

ChallengeHint gains `prerequisites` (0-based indexes into the same
hints list, since CTFd assigns hint IDs only at creation) and
`anonymize`. The controller now preserves each hint's identity across
reconciles: hints are created/reused, their prerequisites wired up in a
second pass once every ID is known, then unwanted hints deleted. A hint
whose only change is its prerequisites is patched in place rather than
recreated, preserving its ID and players' unlock state. Indexes are
validated for range, self-reference, and cycles.
The e2e only checked hint counts, so the new prerequisite wiring went
uncovered. ctfdctl -mode verify now fetches the example challenge's
hints individually (the listing omits requirements) and asserts the
25-point hint requires the 10-point one, which itself has no
prerequisite.
@AYDEV-FR

Copy link
Copy Markdown
Owner Author

✅ e2e passed locally

Ran the full suite (./test/e2e/run.sh) against a real CTFd in a kind cluster — including the new prerequisite-chain assertion:

```
[e2e] waiting for managed resources to become Ready
challenge.resources.ctfd.crossplane.io/break-the-license condition met
...
[e2e] verifying CTFd state through the API
[ctfdctl] all assertions passed
[e2e] ✅ e2e succeeded
```

ctfdctl -mode verify confirmed, through the CTFd API, that the 25-point hint requires the 10-point one (which itself has no prerequisite) — the feature is exercised end-to-end, not just in unit tests.

Also green: go test ./... and golangci-lint run (0 issues).

@AYDEV-FR
AYDEV-FR merged commit 7892039 into main Jun 26, 2026
3 checks passed
@AYDEV-FR
AYDEV-FR deleted the feat/hint-prerequisites branch June 26, 2026 08:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant