Skip to content

Publish the Management API reference, guides and Terraform docs - #393

Merged
xgp merged 3 commits into
mainfrom
management-api-docs
Sep 18, 2026
Merged

xgp merged 3 commits into
mainfrom
management-api-docs

Conversation

@xgp

@xgp xgp commented Sep 10, 2026

Copy link
Copy Markdown
Member

Summary

Splits /api into two APIs and documents the Management API end to end — reference, guides, Terraform provider, and two draft launch posts.

Depends on p2-inc/phasetwo-keycloak#667, which fixes three spec defects that this surfaced; the vendored spec here already includes them.

The /api fork

/api now forks into two sections:

Generated from Targets
Management API (new, 73 endpoints) openapi-management.yamlapi/management/ The Phase Two control plane
Extensions API (existing, 84 endpoints) openapi.yamlapi/ The customer's own Keycloak realm

The Extensions API pages did not move. Those 84 endpoint URLs are published and indexed, and relocating them under api/extensions/ purely for symmetry with the new section would break every inbound link. The new API takes the nested path instead. Spot-checked /api/get-organizations, /api/create-organization and /api/phase-two-admin-rest-api in the build output — all unchanged.

Three hand-written pages carry the framing a generated reference can't:

  • /api — which API you want, that they target different servers, and that a token for one is useless to the other
  • /api/management-api-index — host, auth, conventions, and the asynchronous/restart/deferred-delete behaviours
  • /api/extensions-api-index — the previous /api content, rehomed

One thing worth a look: both APIs use the word "extension" for different things. In the Management API an extensions endpoint manages a jar or theme you upload; in the Extensions API it means our additions to Keycloak. They now sit adjacent in one sidebar, so I named the Management category "Cluster Extensions" and called the collision out explicitly on the overview page.

New guide section: docs/management-api/

Page
index What it is; the control-plane vs realm split, with a mermaid diagram
api-keys Creating a secret, the client-credentials exchange, least privilege, rotation, troubleshooting table
recipes Seven worked examples: provision a cluster, onboard a tenant, attach a domain, IP lockdown, env vars + restarts, rotate credentials, pull logs
terraform The p2-inc/phasetwo provider

These lead with the two-hostname model — authenticate against app.phasetwo.io, call api.phasetwo.io — because that is the single most common source of confusion, and they state the asynchronous provisioning, restart serialization and deferred-delete semantics up front rather than leaving them to be discovered on a Friday.

The Management sidebar is generated, not hand-maintained

api-sidebar.js hand-maintains the Extensions API's 12 categories. Doing the same for 73 more entries would rot immediately, so scripts/gen-management-sidebar.mjs derives that half from the spec and is wired into regen-api-docs. It:

  • converts the plugin's TypeScript sidebar to CommonJS (no TypeScript toolchain in this repo, and api-sidebar.js is CJS)
  • maps lowercase OpenAPI tag names to display labels — ip-rulesIP Rules — with a de-kebab fallback so a new upstream tag degrades gracefully instead of breaking the build
  • strips trailing periods from summary-derived endpoint labels, matching the hand-written half
  • retitles the tag landing pages, which the plugin emits with the raw tag as both title and description

Also

Extended the RFC 9727 linkset at /.well-known/api-catalog to all four spec documents and both reference pages — the overview page claims that, so it needed to be true.

Blog posts

Two, dated to publish across one week, both draft: true so they stay out of production builds until you flip them:

Date Post
2026-09-15 Your Keycloak Infrastructure, Now Scriptable — the Management API, API secrets, the provisioning walkthrough, and the three gotchas
2026-09-17 Keycloak Clusters as Code — the Terraform provider, why it exists given the API, and the two-apply custom domain flow

I went with two rather than three: API keys don't stand alone as a post (they're only interesting as the way into the API), so they're the second act of post one. The Terraform provider is a genuinely different audience and use case, so it gets its own. Both cross-link, and the second opens by referring back to the first.

Test plan

  • pnpm buildpasses, zero broken links
  • All new routes render: /api, both section preambles, /api/management/clusters, /api/management/org-api-secret-create, and all four docs/management-api/*
  • Extensions API URLs verified unmoved
  • Both blog posts confirmed absent from the production build (draft gating works)
  • prettier --check clean on every file I touched
  • regen-api-docs exercised end to end: clean-api-docsgen-api-docs → sidebar conversion

Two things to flag

pnpm fmt will corrupt MDX comments. Running prettier over api/index.mdx rewrote {/* … */} into {/_ … _/} — it parsed the * as emphasis — which broke the MDX compile. I hit this and reverted it. The repo is already 781 files away from prettier-clean, so fmt is presumably never run wholesale; worth knowing before someone tries. I formatted only my own files.

The spec's default server is still the console host. openapi-management.yaml lists https://{environment}.phasetwo.io/auth/realms/self/v2 first and https://api.phasetwo.io/v2 second, because the API host isn't live until p2-inc/phasetwo-infra#350 ships. The prose throughout these docs describes api.phasetwo.io as the API base, per the intended end state. Once #350 is deployed, swap the server order upstream and re-vendor — the docs won't need to change.

Splits the /api section into two APIs and documents the Management API
end to end.

The section now forks at /api into:

  Management API   the Phase Two control plane. Clusters themselves.
                   Generated from openapi-management.yaml, a build
                   artifact of phasetwo-keycloak, into api/management/.
  Extensions API   Phase Two's additions to the Keycloak Admin REST API,
                   served from the customer's own realm. Unchanged.

The Extensions API's 84 endpoint pages stay at the top of api/ rather
than moving under api/extensions/. Those URLs are published and indexed,
and relocating them to gain symmetry with the new section would break
every inbound link. The new API gets the nested path instead.

Three hand-written pages carry the framing the generated reference
cannot: /api explains which API a reader wants and that they target
different servers with different credentials, and each section has a
preamble covering its host, its authentication and its conventions. Both
APIs use the word "extension" for different things -- uploaded jars
versus our Keycloak additions -- so that collision is called out
explicitly where the two sit adjacent in one sidebar.

New guide section under docs/management-api:

  index      what it is, and the control-plane/realm split
  api-keys   creating a secret, the client-credentials exchange, least
             privilege, rotation, and a troubleshooting table
  recipes    seven worked end-to-end examples
  terraform  the p2-inc/phasetwo provider

The guides lead with the two-hostname model, since authenticating
against app.phasetwo.io and then calling api.phasetwo.io is the single
most common source of confusion, and they state the asynchronous
provisioning, restart serialization and deferred-delete behaviours up
front rather than leaving them to be discovered.

The Management API's sidebar is generated rather than hand-maintained:
scripts/gen-management-sidebar.mjs converts the plugin's TypeScript
output to CommonJS (this repo has no TypeScript toolchain), maps
lowercase OpenAPI tag names to display labels, strips trailing periods
from summary-derived labels, and retitles the tag landing pages from the
spec's tag descriptions. Wired into regen-api-docs, so a spec refresh
carries all of it.

Also extends the RFC 9727 linkset to all four spec documents and both
reference pages, which the overview page now claims.

Two draft blog posts, dated to publish across one week and marked
draft: true so they stay out of production builds until flipped.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 10, 2026

Copy link
Copy Markdown

Deploying phasetwo-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: ba9f33c
Status: ✅  Deploy successful!
Preview URL: https://98169fa8.phasetwo-docs.pages.dev
Branch Preview URL: https://management-api-docs.phasetwo-docs.pages.dev

View logs

Four changes, all consequences of the API host going live.

Re-vendors openapi-management.yaml from phasetwo-keycloak. The spec is
now version 2.0.0 rather than 1.0.0 -- the provider is mounted at /v2, so
the old number contradicted every path in the document -- and lists the
API host first, since it now resolves in both environments. It also
carries a second security scheme for staging, because tokenUrl takes no
server variables and so a staging caller cannot retarget the token
endpoint from the server selector alone.

Fixes the links to the spec files, which 404ed. A plain /openapi.yaml in
MDX is treated as a file asset, rewritten to a hashed /assets/files/...
path, and then given a trailing slash by trailingSlash: true -- and the
slashed form does not exist:

  /assets/files/openapi-0796....yaml/   404
  /assets/files/openapi-0796....yaml    200

pathname:// opts out of both the asset pipeline and the router, so the
literal static path survives. This was already broken on the live site,
not introduced here. A comment records why, since the prefix reads like
something to tidy away.

Points the "API" entries in the navbar, the Developers submenu and the
footer at the section root rather than at the Extensions API's generated
introduction, and retitles the submenu to "Phase Two Management and
Extensions APIs". /api/ now forks between two APIs, and the fork page is
what tells a reader which one they want.

Adds a .prettierignore. Prettier rewrites MDX expression comments --
{/* ... */} becomes {/_ ... _/} -- which does not compile, so `pnpm fmt`
breaks the build on any MDX file carrying one. It bit twice while writing
this; now it cannot.
* Document the Keycloak provider credential handoff

The Terraform page told people to point the Keycloak provider at the cluster
with client_id = "admin-cli" and a "# ..." where the credentials go, which is
not something anyone can act on.

Replaces it with the real path: create a per-realm credential with
deployment.credential.create, and the response carries every field the
Keycloak provider needs. Covers listing and revoking, why a credential per
holder is worth the small extra effort, and the fact that the secret lands in
terraform.tfstate in plain text -- true of any provider credential, but this
one is realm-admin, so the state backend inherits that sensitivity.

Depends on the endpoints in p2-inc/phasetwo-keycloak#692.

* Require initial_login = false on the Keycloak provider handoff

The provider authenticates when Terraform configures it, which happens during
plan. A configuration that creates the cluster and configures inside it in one
go therefore fails at plan time against a cluster that does not exist yet --
so the example as written only worked if the cluster already existed.

Measured against Terraform 1.14.7 and keycloak/keycloak: without the flag,
plan fails with "failed to perform initial login to Keycloak: 401"; with it,
plan succeeds and the login is deferred to first use.

Also notes that the manual credential step is interim. The same flag is what
makes a phasetwo_realm_credential resource viable -- a provider configured
from a resource created in the same apply, which is normally a dead end,
works with the login deferred, destroy ordering included.

* Document credential names and role scoping

The credential endpoint now takes a name and a set of realm-management roles,
so the page needs to say what those are and when to narrow them.

Client ids are api-{name}-{uuid} rather than terraform-{uuid}: the endpoint is
for any service account on the realm, not only Terraform, and the marker
attribute rather than the prefix is what identifies one as ours.

Includes the full role table, because "narrow the roles" is not actionable
without knowing what the roles are, and a caution that too-narrow surfaces as
a 403 partway through an apply rather than up front -- unlike a misspelled
role, which is now a 400 at creation.

* Stop telling people the credential secret cannot be read back

It can. The secret lives in the customer's realm and they hold realm-admin
there, so it was always readable from Keycloak directly; #695 adds
deployment.credential.secret.read so it is reachable through our API too, and
reading does not rotate it.

The old wording had a cost. Telling people the create response was their only
chance is what pushed the secret into terraform.tfstate, which is exactly the
place a realm-admin credential should not be. Now points at fetching it on
demand, and at the ephemeral-resource approach that keeps it out of state
entirely.

* Document the one-apply Terraform handoff

phasetwo_realm_credential and its ephemeral secret now exist, so the page can
stop describing the curl steps as the way to do it and show the declarative
version instead. The curl route stays for people not using our provider.

Spells out why both of the odd-looking lines are there, because both are the
kind of thing a reader would otherwise delete as noise:

ephemeral keeps the secret out of terraform.tfstate, which matters because
Terraform writes every resource attribute to state and this credential holds
realm-admin. That is also why the resource has no client_secret attribute at
all.

initial_login = false is needed for two separate reasons, and the subtle one
is that the Keycloak provider authenticates at configure time, during plan,
against a cluster that does not exist yet.
@xgp
xgp added this pull request to stack #406 September 18, 2026 17:35
xgp added a commit to p2-inc/terraform-provider-phasetwo that referenced this pull request Sep 18, 2026
* Write a real provider overview for the registry

The registry's Overview tab renders docs/index.md, which was tfplugindocs'
stock template: the schema description, one example, and the argument list.
Someone landing there learned what the arguments are called and nothing about
what the provider is for or where to read more.

Adds templates/index.md.tmpl, which is how that page is controlled -- the
generated docs/ is output, so editing it directly is undone by the next
`make docs` and CI's "generated files are up to date" job catches it.

Covers what a first-time reader needs and cannot get from the schema:

- what Phase Two is, and that this is the Terraform front end to the same
  OpenAPI document that generates the API reference
- how authentication works, and that organization roles are enforced per
  operation so a view-only secret is real rather than decorative
- the realm-credential handoff to the Keycloak provider, which is the one thing
  here that is not obvious and not generally possible in a single apply
- four behaviours that are expensive to discover mid-apply: cluster arguments
  all force replacement, destroy is deferred and holds the name, config changes
  restart Keycloak, custom domains need two applies
- a link table back to the guide, the API reference, the recipes, the extension
  docs, the repo and the console

Every link checked. phasetwo.io/docs has no index -- /docs/introduction is the
entry point. The four /docs/management-api/* and /api/management-api-index
links 404 today because those pages are in p2-inc/phasetwo-docs#393, #394 and
#405; they are confirmed present in that branch's build.

Only docs/index.md changes -- the template leaves the other 16 generated pages
untouched.

* Mark the provider experimental, test environments only

It is 0.1.0, the acceptance tests have never run against it, and the resources
it manages are real and billable -- replacing a phasetwo_cluster destroys it
and every realm on it, and destroying one keeps billing and holds the name
until the end of the billing cycle. Someone reaching for this on a production
estate should be told before they apply, not after.

Three places, because each has a different first reader:

- templates/index.md.tmpl, which renders the registry Overview tab. Uses !>
  rather than the ~> this repo uses elsewhere: the registry renders that as its
  strongest callout, and "do not point this at production" earns it.
- README.md, for anyone arriving from the source link.
- docs/index.md is regenerated, not hand-edited.

Matching notices are going into the customer docs and the launch post.
@xgp
xgp merged commit 9773ba2 into main Sep 18, 2026
3 checks passed
@xgp
xgp deleted the management-api-docs branch September 18, 2026 20:42
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.

2 participants