Appearance
🛡️ adminadmin@example.com · Domain admin in Acme Corp
Chapter 8 — The provisioning catalog
Three things before anything can be provisioned
Blueprint — the recipe. What a bucket or a node is.
Cloud — the provider account to build it in.
Credential — the vaulted secret that pays for it.
This chapter builds all three, and it takes two different hats to do it.
Register a catalog — and verify who signed it
bash
CATALOG_ID=$(plexctl blueprint catalog add \
--name "Official Blueprint catalog" \
--ref ghcr.io/plexsphere/blueprints:v1 \
--domain-id "$DOMAIN_ID" \
--verify-identity '^https://github\.com/plexsphere/blueprints/\.github/workflows/release\.yml@refs/tags/v.*$' \
--verify-issuer https://token.actions.githubusercontent.com \
--output json | jq -r '.id')
echo "$CATALOG_ID"The two --verify-* flags are the point.
A catalog is an OCI artifact, and plexsphere will not import from it unless the signature chains to the identity you named. Not "we pulled it over TLS" — "this was built by that workflow, on a tag, in that repo".
Browse what is in it
bash
plexctl blueprint catalog browse "$CATALOG_ID"Browsing reads the source. Nothing has entered your Domain yet — this is the shop window, not the shelf.
Import one
bash
plexctl blueprint import "$CATALOG_ID" aws-s3-bucketbash
plexctl blueprint list --allImport is deliberate and per-Blueprint. Registering a catalog does not dump its whole contents into your Domain — you choose what your tenant carries.
Capture the version id
bash
BLUEPRINT_ID=$(plexctl blueprint list --all --output json \
| jq -r '.items[] | select(.slug == "aws-s3-bucket") | .id')
BLUEPRINT_VERSION_ID=$(plexctl blueprint get "$BLUEPRINT_ID" --output json \
| jq -r '.versions[] | select(.version == "v1alpha1") | .id')
echo "$BLUEPRINT_VERSION_ID"Resources are provisioned from a version, not from a Blueprint.
A Blueprint that ships a fix does not silently change what your existing Resources were built from.
👤 membermember@example.com · ordinary member of Acme Corp
Optional — an ordinary member can read it
bash
plexctl login --profile-name member --domain-id "$DOMAIN_ID"bash
plexctl blueprint catalog list --profile member --output json \
| jq -r '.items[] | {id, name, domain_id}'member@example.com holds no authoring grant and sees the catalog anyway. Authoring and reading are separate rights.
Presenter note: costs a browser sign-in for one point. Skip when short on time.
🏗️ operatoroperator@example.com · platform grant, no Domain
Change hats
bash
plexctl login --profile-name operator --platformEverything so far was the tenant hat — Domain admin in Acme Corp.
Clouds and Credentials are not tenant objects. They are catalog-global, and they gate on a platform grant that no Domain admin holds.
A genuinely different principal
bash
plexctl whoami --profile operatortext
PRINCIPAL SCOPE SUBJECT DOMAIN ACR AMR
user platform 019100c0-0d0c-7000-8000-000000000002 - - -SCOPE = platform. DOMAIN = -.
This session belongs to no tenant at all. Compare it with the whoami from chapter 2, which named Acme Corp in that column.
This is not a bigger admin. It is a different axis of authority.
Create the Cloud
bash
CLOUD_ID=$(plexctl cloud create \
--slug demo-cloud \
--display-name "Demo Cloud" \
--provider aws \
--external-id 000000000000 \
--endpoint '{"region":"us-east-1","partition":"aws"}' \
--region-defaults '{"default_region":"us-east-1"}' \
--provider-package xpkg.upbound.io/upbound/provider-aws-s3@v2.6.1@sha256:7e00c910bdbf39b2dcc8d7632153b6be1c31d202ede0f2d12ade053eb77aae96 \
--provider-config-api-version aws.m.upbound.io/v1beta1 \
--profile operator --output json | jq -r '.id')
echo "$CLOUD_ID"An AWS account, as far as the platform is concerned. Locally it is backed by an emulator, so there is no real account, no real token, and no bill.
--provider-package declares one Crossplane provider package as <source>@<version>, and it is repeatable: a Cloud can name several. The one above is the exact package the local stack runs.
Read it back
bash
plexctl cloud list --all --profile operator --output json \
| jq '.items[] | {id, slug, provider, external_id}'json
{
"id": "019fb9c7-97ba-7d4a-8867-29e77d21c5e6",
"slug": "demo-cloud",
"provider": "aws",
"external_id": "000000000000"
}Creating a Cloud granted its creator manage on it, which folds into the observe relation reads gate on. The list is relation-filtered, same as every other list in this demo.
Vault the Credential
bash
printf '{"access_key_id":"AKIAEXAMPLE","secret_access_key":"example"}' > /tmp/demo-credential.json
CREDENTIAL_ID=$(plexctl cloud credential create \
--cloud-id "$CLOUD_ID" \
--display-name demo-credential \
--payload-file /tmp/demo-credential.json \
--profile operator --output json | jq -r '.id')
echo "$CREDENTIAL_ID"The payload goes in and never comes back out. It is accepted inbound only; the material lives in the secrets engine.
What a read gives you
bash
plexctl cloud credential list --cloud-id "$CLOUD_ID" --profile operator --output jsonjson
{
"items": [
{
"cloud_id": "019fb9c7-97ba-7d4a-8867-29e77d21c5e6",
"created_at": "2026-06-24T18:00:36.153507Z",
"display_name": "demo-credential",
"expires_at": "2026-06-25T18:00:36.146475Z",
"id": "019fb9c7-fe03-7dcf-b671-382f2b307d6f",
"status": "active",
"updated_at": "2026-06-24T18:00:36.153507Z",
"version": 1
}
]
}Lifecycle metadata. No access_key_id, no secret_access_key.
There is no read surface anywhere that returns them — not for the operator who created it, not for anyone.