Skip to content

Author a Blueprint

A Blueprint is a catalog entry; an operator provisions a Resource from one of its published versions. The catalog starts empty. The fastest way to fill it is to register an external OCI catalog as a source and import the Blueprints you need; hand-authoring the manifests yourself is the advanced path covered at the end of this guide.

Prerequisites

  • An authenticated session — see Log in with plexctl.
  • The platform-level manage relation, which gates registration and import.

Bind the control-plane URL the steps reference:

shell
export PLEXSPHERE_URL="${PLEXSPHERE_URL:-https://localhost:8080}"

Import from a catalog source

1. Register the catalog source

shell
plexctl blueprint catalog add \
  --server "${PLEXSPHERE_URL}" \
  --name "Platform catalog" \
  --ref ghcr.io/plexsphere/catalog:v1.2.3 \
  --verify-identity '^https://github\.com/plexsphere/catalog/\.github/workflows/release\.yml@refs/tags/v.*$' \
  --verify-issuer https://token.actions.githubusercontent.com

Pin --verify-identity to the exact repository, workflow file, and tag ref of the catalog's release pipeline, as shown above. A broader pattern such as ^https://github\.com/plexsphere/.*$ accepts a signature from any workflow in the organization — including one an attacker could trigger to sign attacker-controlled content — which defeats the point of pinning the signer.

The command prints the new source's id. Pin the bundle by immutable digest with --digest ghcr.io/plexsphere/catalog@sha256:... in place of --ref, or have the source re-resolve its tag on an interval with --track 15m. Scope the registration to one Domain with --domain-id, and point a private registry at its pull secret with --registry-credential-ref namespace/name. Use --insecure-unsigned in place of the --verify-identity/--verify-issuer pair only for a registry you fully trust.

2. Browse what the source offers

shell
plexctl blueprint catalog browse <source-id> \
  --server "${PLEXSPHERE_URL}"

Nothing is imported yet — the response lists the slug, version, and provider kinds of each Blueprint the bundle offers.

3. Import the Blueprints you want

Import a chosen subset by slug, or --all to import everything the source offers:

shell
plexctl blueprint import <source-id> managed-postgres managed-redis \
  --server "${PLEXSPHERE_URL}"

Each requested slug gets one outcome row. A slug already owned by a different source reports conflict; an identical re-import reports unchanged. These are per-Blueprint, not a batch failure, so the command still exits zero.

4. Confirm

shell
plexctl blueprint get <blueprint-id> \
  --server "${PLEXSPHERE_URL}" \
  --output yaml

The response carries the imported Blueprint with its versions array.

Advanced: hand-author a Blueprint

When a Blueprint you need is not published in any catalog, register it and publish its first version from a pair of Crossplane manifests yourself. The registrar is granted owner on the new Blueprint, which confers the publish permission used in step 3. You need three JSON files: the Crossplane XRD, the Composition, and the parameter-schema document. The Composition's compositeTypeRef must name the composite type the XRD declares.

1. Register the Blueprint

shell
plexctl blueprint create \
  --server "${PLEXSPHERE_URL}" \
  --slug managed-postgres \
  --display-name "Managed PostgreSQL" \
  --description "A managed PostgreSQL database instance."

The command prints the new Blueprint's id. A duplicate slug exits non-zero with blueprint_slug_conflict.

2. Prepare the version files

The parameter-schema file is the canonical document the catalog stores:

json
{
  "parameters": [
    { "name": "storage_gb", "type": "integer", "required": true },
    { "name": "high_availability", "type": "boolean", "required": false }
  ]
}

xrd.json and composition.json carry the Crossplane manifests as JSON objects. A minimal pair:

json
{
  "apiVersion": "apiextensions.crossplane.io/v2",
  "kind": "CompositeResourceDefinition",
  "metadata": { "name": "xmanagedpostgres.example.org" },
  "spec": { "group": "example.org", "names": { "kind": "XManagedPostgres", "plural": "xmanagedpostgres" } }
}
json
{
  "apiVersion": "apiextensions.crossplane.io/v1",
  "kind": "Composition",
  "metadata": { "name": "managed-postgres-composition" },
  "spec": { "compositeTypeRef": { "apiVersion": "example.org/v1", "kind": "XManagedPostgres" } }
}

3. Publish a version

Use the Blueprint id from step 1:

shell
plexctl blueprint version create \
  --server "${PLEXSPHERE_URL}" \
  --blueprint-id 0190a8b8-a0c0-7a0a-8a0a-a0a0a0a0a0b0 \
  --version 1.0.0 \
  --injection-strategy helm-values \
  --provider-kinds aws,hetzner \
  --xrd-file ./xrd.json \
  --composition-file ./composition.json \
  --parameter-schema-file ./parameter-schema.json

The version is immutable; re-publishing the same --version exits with blueprint_version_exists.

The owner grant from step 1 is written asynchronously. If you script register → publish back-to-back and the publish returns 403, retry it after a short delay — the grant has not yet propagated.

Do this in the Console

Both hand-authoring steps have a browser equivalent in the Console. Open the Blueprints page under the Blueprints sidebar group.

Step 1 (blueprint create) is the Register Blueprint button above the table. It requires the platform-level manage relation. The dialog takes a kebab-case slug, a display name, an optional description, and a Scope picker that defaults to catalogue-wide and offers every Domain you can read. A duplicate slug is rejected inline on the Slug field as blueprint_slug_conflict without closing the dialog. On success the new Blueprint appears in the list, and you are granted owner on it — the grant that confers publish.

Step 3 (blueprint version create) is the Publish version action in the Blueprint detail masthead. It requires the publish permission the owner grant confers. The dialog takes a version label; a checkbox group over the provider kinds (aws, azure, gcp, hetzner, openstack); an injection strategy (cloud-init-user-data, helm-values, provider-secret); and the XRD, Composition, and parameter-schema documents as three JSON entries you can paste or load from a file. Each document is checked for being a JSON object before it is sent, so a syntax error surfaces on its own field. The server's validation rejections map back to their field — invalid_provider_kind, invalid_injection_strategy, invalid_parameter_schema, and invalid_manifest — and re-publishing a version shows blueprint_version_exists. On success the new version card appears on the Versions tab.

The owner grant is written asynchronously, so right after registering a Blueprint the Publish version control — or a submit — may be refused for a moment. The disabled tooltip and the residual denial both say so; retry shortly.

See also