Skip to content

plexctl provider-bundle

Synopsis

plexctl provider-bundle is the operator surface for the ProviderBundle aggregate, the shared provider configuration a Cloud points at instead of repeating the same declaration inline. One bundle carries a set of Crossplane provider packages plus the one <group>/<version> those packages serve their ProviderConfig under. It wraps three /v1 surfaces under a single cobra parent:

  • /v1/provider-bundles — CRUD on bundles.
  • /v1/provider-bundles/{id}/clouds — the roster of Clouds that take their provider configuration from one bundle, through list-clouds.
  • /v1/provider-bundles/{id}/versions — the declarations the bundle has published, through versions.

Editing a bundle's packages or its apiVersion publishes a new immutable version and moves no Cloud. Each referencing Cloud keeps resolving the version it pins until a write moves that pin, which is what promote issues, one Cloud at a time.

Referencing a bundle from a Cloud is not done here: it is the --provider-bundle and --provider-bundle-version flags on plexctl cloud create and update.

text
plexctl provider-bundle <subcommand> [flags]

Subcommands

plexctl provider-bundle create

POSTs to /v1/provider-bundles. Required flags: --slug, --display-name, --provider, --provider-package, --provider-config-api-version. Every field of the aggregate's construction invariant is a required flag, so a body the server would reject as incomplete never reaches the wire. --provider-package is repeatable and takes <source>@<version>; at least one occurrence is required, and each source may be named only once. The declaration the create states is the bundle's version 1, and the LATEST_VERSION cell of the row it prints back says so.

The 201 is ahead of the ReBAC graph. Both grants that make the new bundle readable are written by the authz-sync consumer draining the create event, so a read-back issued immediately can answer 403 until it has drained. Retry rather than treating that as a permanent denial.

plexctl provider-bundle list

GETs /v1/provider-bundles. Pagination is keyset-based via --cursor / --limit; the --all flag follows next_cursor until exhausted (mutually exclusive with --cursor). The row collapses the declared set to its size in a PACKAGES column so a page stays readable across bundles that pin a dozen packages each; get spells the set out.

plexctl provider-bundle get <ref>

GETs /v1/provider-bundles/{id}. The row spells the declared set out rather than counting it, and carries LATEST_VERSION, the newest declaration the bundle has published. That is the number a Cloud attaching without an explicit version pins, and the number promote --to-version usually names.

plexctl provider-bundle update <ref>

PATCHes /v1/provider-bundles/{id}. Only flags explicitly set on the command line are sent in the PATCH body. --slug and --provider are not exposed because the API rejects mutating them, with 400 slug_immutable and 400 provider_immutable. An invocation that sets none of the three flags is refused locally with exit 2 before the reference is resolved and before the PATCH is issued.

--provider-package replaces the whole declared set rather than adding to it: the occurrences on the command line become the bundle's packages and every package they omit is dropped. --provider-config-api-version patches on its own, without restating the package set.

Either of those two flags publishes a new immutable version of the bundle, and a patch naming both publishes one version between them. The LATEST_VERSION cell on the row the command prints back is the number that version was published under. --display-name alone publishes nothing and leaves LATEST_VERSION where it stands: a rename authors no declaration.

The patch moves no Cloud. Every Cloud that references the bundle keeps resolving the version it pins, so nothing on any management cluster changes until a promotion moves a pin. promote is the command that does that; list-clouds shows where each Cloud stands first.

plexctl provider-bundle delete <ref>

DELETEs /v1/provider-bundles/{id}. Requires the persistent --yes flag. A bundle at least one Cloud still references is refused with 409 provider_bundle_referenced; the count of blocking Clouds travels in the problem body's referencing_clouds extension and is rendered at the end of the stderr error line, so the size of the re-pointing job is known before the retry. A successful delete prints nothing and exits 0.

plexctl provider-bundle lift <cloud-id>

Reads the Cloud named by the positional UUID and creates a bundle carrying that Cloud's provider, package set, and ProviderConfig apiVersion, so a declaration that started inline on one Cloud becomes shareable. Required flags: --slug, --display-name. The call is a GET /v1/clouds/{id} followed by a POST /v1/provider-bundles, so it needs both the read on the Cloud and the platform grant the create requires.

The Cloud itself is left untouched: lift creates the bundle and nothing else. Point the Cloud at the new bundle with plexctl cloud update <id> --provider-bundle <slug>.

A Cloud already in bundle mode has no inline set to lift, so the command refuses locally with exit 2, naming the bundle the Cloud already references, and issues no write. The positional argument is a UUID only. Unlike <ref> on the bundle subcommands, it is not resolved from a slug.

plexctl provider-bundle list-clouds <ref>

GETs /v1/provider-bundles/{id}/clouds and lists the Clouds that take their provider configuration from the bundle. That is the same set the server counts when it refuses a delete. Each row carries the Cloud's id, slug, display name, and the VERSION it pins. Rows on one page may differ in that last column: a content patch publishes a version and moves no Cloud, so VERSION is where an operator sees how far the last promotion got and which Clouds a further stage still has to cover. Pagination is keyset-based via --cursor / --limit, and --all follows next_cursor until the roster is exhausted (mutually exclusive with --cursor). Text output renders the rows and nothing else, so a page boundary is invisible there — reach for --all whenever the roster may be longer than one page. A bundle no Cloud references answers with an empty list, which is what tells an operator the bundle is safe to delete.

The roster needs provider_bundle#manage on the bundle — the permission update and delete need, not the observe that get needs. It is served whole, with no per-Cloud visibility filter: it names every Cloud a promotion of this bundle can reach, across every Domain, so it is scoped to the caller who authors the versions rather than filtered per row for a wider audience. A filtered page would under-report the blast radius and contradict the unfiltered referencing_clouds count on a refused delete.

plexctl provider-bundle versions <ref>

GETs /v1/provider-bundles/{id}/versions and lists the declarations the bundle has published, newest first. That is the order the server computes; the client passes it through unchanged. Pagination is keyset-based via --cursor / --limit, and --all follows next_cursor until the history is exhausted (mutually exclusive with --cursor).

The row carries VERSION, CREATED, and PACKAGES, the last collapsed to the size of the set, the way the list row collapses it. The full content of each version travels under --output json: every package with its source and version, the ProviderConfig apiVersion, and the subject that published it. That is the output to compare two versions in before picking the one to promote onto. created_by is empty for the versions the schema migration backfilled from bundles that predate the history.

The history is gated on provider_bundle#observe, the permission get needs, not the manage list-clouds narrowed itself to: a version row is bundle content and names no Cloud.

plexctl provider-bundle promote <ref>

Moves referencing Clouds onto one published version by PATCHing each Cloud's provider_bundle_version in turn. --to-version names the version, and exactly one of --cloud (repeatable, a Cloud UUID) or --all-clouds names the targets.

There is no bulk endpoint behind this: the walk is composed client-side out of single-Cloud PATCHes, each admitted against that Cloud's own reference and its own authorisation. Staging a rollout is naming a few Clouds first; halting one is not issuing the next command. A rollback is the same command pointed at an earlier --to-version.

One outcome line is written per Cloud as it settles, not a table at the end, so an interrupted --all-clouds run still leaves a record of which Clouds moved:

text
0190a8b8-a0c0-7a0a-8a0a-b0b0b0b0b0b0: promoted to version 3
0190a8b8-a0c0-7a0a-8a0a-b0b0b0b0b0b1: not promoted: <problem>

A per-Cloud failure does not stop the walk. The Clouds already promoted stay promoted, the remaining targets are still attempted, and the command exits 1 naming how many of how many were not promoted, so a partial rollout is never reported as a success.

Three refusals are local and come before the first request, at exit 2: naming neither --cloud nor --all-clouds, naming both, and a --to-version below 1. A --cloud value that is not a UUID is refused the same way.

--all-clouds drains the whole roster rather than its first page: a walk that stopped at a page boundary would leave the Clouds beyond it on the old version while reporting the rollout complete. When the roster is empty the command prints No Cloud references provider bundle <ref>. and exits 0: nothing to promote is not a failure.

Resolving a bundle reference

get, update, delete, list-clouds, versions, and promote take a <ref> that is either the bundle's UUID or its slug, and so does the --provider-bundle flag on plexctl cloud. promote resolves it even when --cloud names the targets: a ref no bundle answers for is a typo, and failing on it once is kinder than failing on it per Cloud. A value that parses as a UUID is used as the id directly; anything else is treated as a slug and resolved by walking /v1/provider-bundles pages. The walk asks for the endpoint's maximum page size and stops at the first page carrying the slug, so a bundle near the front of the inventory costs one request. It gives up after 500 pages — the same 100 000-row budget list --all allows itself, counted in pages because the list endpoint filters per row after the read and can hand back an empty page that still continues. list --all and list-clouds --all bound themselves in pages for the same reason, at the 2 000 pages that budget occupies at the server's default page size. An unmatched slug after the walk exits 1 with the message provider-bundle: no bundle with slug "<value>".

Flags

plexctl provider-bundle create

FlagTypeRequiredDescription
--slugstringyeskebab-case URL handle. Frozen after creation.
--display-namestringyesHuman-readable bundle name.
--providerenumyesUpstream provider: aws | azure | gcp | hetzner | openstack. Frozen after creation.
--provider-packagestringyesOne Crossplane provider package the bundle declares, as <source>@<version>: the OCI repository without a tag or digest, then an OCI tag optionally pinned with @sha256:<digest>. Repeatable; at least one occurrence is required.
--provider-config-api-versionstringyes<group>/<version> every declared package serves its ProviderConfig under.

plexctl provider-bundle list

FlagTypeRequiredDescription
--limitintnoMaximum items per page (server default when zero).
--cursorstringnoContinuation token from a previous call.
--allboolnoFollow next_cursor until exhausted. Mutually exclusive with --cursor.

plexctl provider-bundle update <ref>

FlagTypeRequiredDescription
--display-namestringnoNew human-readable bundle name. Publishes no version.
--provider-packagestringnoReplacement package as <source>@<version>. Repeatable; the occurrences replace the whole declared set rather than adding to it. Publishes a version.
--provider-config-api-versionstringnoNew <group>/<version> for the ProviderConfig. Patches on its own: every declared package serves that one group, so changing it does not require restating the package set. Publishes a version.

At least one of the three must be set; an empty patch exits 2. The two content flags publish one version between them when both are set.

plexctl provider-bundle delete <ref>

FlagTypeRequiredDescription
--yes (persistent)boolyesRequired confirmation for the destructive operation.

plexctl provider-bundle lift <cloud-id>

FlagTypeRequiredDescription
--slugstringyeskebab-case URL handle for the new bundle.
--display-namestringyesHuman-readable name for the new bundle.

The provider, the package set, and the ProviderConfig apiVersion are read off the source Cloud, so none of them has a flag.

plexctl provider-bundle list-clouds <ref>

FlagTypeRequiredDescription
--limitintnoMaximum items per page (server default when zero).
--cursorstringnoContinuation token from a previous call.
--allboolnoFollow next_cursor until the roster is exhausted. Mutually exclusive with --cursor.

plexctl provider-bundle versions <ref>

FlagTypeRequiredDescription
--limitintnoMaximum items per page (server default when zero).
--cursorstringnoContinuation token from a previous call.
--allboolnoFollow next_cursor until the history is exhausted. Mutually exclusive with --cursor.

plexctl provider-bundle promote <ref>

FlagTypeRequiredDescription
--to-versionintyesPublished version number every named Cloud is pinned to. A value below 1 exits 2 before any request.
--cloudstringsee belowCloud UUID to promote. Repeatable. Mutually exclusive with --all-clouds.
--all-cloudsboolsee belowPromote every Cloud on the bundle's roster. Mutually exclusive with --cloud.

Exactly one of --cloud and --all-clouds must be set. Naming neither and naming both are both refused locally with exit 2.

Persistent flags inherited from root

--server, --profile, --token-file, --output, --yes, --reveal-secrets. See ../plexctl.md for the canonical list.

Exit codes

See ../plexctl.md#exit-code-taxonomy.

Examples

Create a bundle

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

plexctl provider-bundle create \
  --server           "${PLEXSPHERE_URL}" \
  --slug             acme-aws-standard \
  --display-name     "Acme AWS standard" \
  --provider         aws \
  --provider-package xpkg.upbound.io/upbound/provider-aws-ec2@v2.6.1 \
  --provider-package xpkg.upbound.io/upbound/provider-aws-s3@v2.6.1 \
  --provider-config-api-version aws.m.upbound.io/v1beta1

Lift an inline Cloud's declaration and point the Cloud at it

shell
plexctl provider-bundle lift 0190a8b8-a0c0-7a0a-8a0a-b0b0b0b0b0b0 \
  --server       "${PLEXSPHERE_URL}" \
  --slug         acme-aws-standard \
  --display-name "Acme AWS standard"

plexctl cloud update 0190a8b8-a0c0-7a0a-8a0a-b0b0b0b0b0b0 \
  --server          "${PLEXSPHERE_URL}" \
  --provider-bundle acme-aws-standard

Read the roster before editing a bundle

shell
plexctl provider-bundle list-clouds acme-aws-standard \
  --server "${PLEXSPHERE_URL}" \
  --output json

Repin the declared packages

shell
plexctl provider-bundle update acme-aws-standard \
  --server           "${PLEXSPHERE_URL}" \
  --provider-package xpkg.upbound.io/upbound/provider-aws-ec2@v2.7.0 \
  --provider-package xpkg.upbound.io/upbound/provider-aws-s3@v2.7.0

The LATEST_VERSION cell of the row this prints back is the version the edit published. No Cloud has moved onto it yet.

Read the published history

shell
plexctl provider-bundle versions acme-aws-standard \
  --server "${PLEXSPHERE_URL}" \
  --all

plexctl provider-bundle versions acme-aws-standard \
  --server "${PLEXSPHERE_URL}" \
  --output json

Promote a first stage, then the rest

shell
plexctl provider-bundle promote acme-aws-standard \
  --server     "${PLEXSPHERE_URL}" \
  --to-version 2 \
  --cloud      0190a8b8-a0c0-7a0a-8a0a-b0b0b0b0b0b0

plexctl provider-bundle list-clouds acme-aws-standard \
  --server "${PLEXSPHERE_URL}" \
  --all

plexctl provider-bundle promote acme-aws-standard \
  --server     "${PLEXSPHERE_URL}" \
  --to-version 2 \
  --all-clouds

Delete a bundle (requires --yes)

shell
plexctl provider-bundle delete acme-aws-standard \
  --server "${PLEXSPHERE_URL}" \
  --yes

Cross-references