Skip to content

plexctl project

Synopsis

plexctl project is the operator surface for the Project aggregate. It wraps the typed /v1/projects operations (CreateProject, ListProjects, GetProject, PatchProject, DeleteProject) so an operator can manage Projects (create, list, get, update, delete) without hand-rolling curl calls.

A Project lives inside exactly one Domain and owns its mesh-IP sub-range reservation carved out of the parent Domain's mesh-CIDR. The CRUD surface documented here operates on the Project aggregate body itself; the parent Domain identifier is required at create time and is an optional server-side filter at list time. The sibling parent commands are documented in domain.md (the tenancy root the Project lives inside) and identity.md (the per-Domain identity surface). Two wire-shape decisions surface at the CLI layer: --display-name maps onto the JSON field name to keep the flag spelling consistent across domain create, project create, and group create; and --slug is immutable after creation — there is no --slug flag on update.

Invocation

text
plexctl project <subcommand> [flags]

The five subcommands are enumerated below. Every subcommand is a private cobra constructor; flags are validated at parse time so an invalid UUID or a missing required flag surfaces as exit 2 instead of a wire round-trip.

plexctl project create

Creates a Project via POST /v1/projects. The handler marks --domain, --slug, and --display-name as required flags: the wire schema declares domain_id as required and the server's authorisation check pivots on the parent Domain UUID, so a missing --domain becomes an exit-2 cobra error rather than a 400 round-trip. The --display-name flag maps to the JSON field name — the rest of the CLI consistently uses --display-name for the human-readable label and --slug for the URL handle, so a Project-only --name would surprise operators who alias across aggregates.

plexctl project list

Lists Projects via GET /v1/projects. Unlike domain list --slug (which is a client-side filter applied after the page is fetched), project list --domain <uuid> is forwarded to the server as the domain_id query parameter — the wire ListProjectsParams natively supports it, so the server returns only the Projects in the named Domain. The opt-in --all flag walks every page until next_cursor is empty, capped at 100 000 items as defence-in-depth against a runaway server; --all and --cursor are cobra-mutually-exclusive .

plexctl project get

Returns a single Project by UUID. The positional argument must be a UUID — there is no slug fallback. domain get resolves slugs because ListDomains is bounded; for Projects the surface fans out across every Domain a caller can see, and a slug-to-id resolution would either need a mandatory --domain hint (changing the get UX) or scan every visible Project (a runaway-cost trap). Operators who only have a slug look up the id via plexctl project list --domain <id> first.

No --domain flag on project get. Unlike plexctl identity get, which requires --domain (the wire endpoint is /v1/domains/{id}/identities/{principal_id}, nested under the Domain), plexctl project get accepts only the positional UUID. The wire endpoint /v1/projects/{id} is Domain-scoped server-side via the Project's stored domain_id, so the CLI does not need a --domain hint to disambiguate. Passing --domain here is rejected at flag-parse time with cobra's unknown flag error (exit 2); use plexctl project list --domain <id> if you need to enumerate Projects within a specific Domain first.

plexctl project update

PATCHes the mutable fields of a Project. Only the flags the operator explicitly set are forwarded in the request body, so the server's empty_patch rule never fires twice and a sparse update is genuinely sparse on the wire. --slug is intentionally absent because the OpenAPI ProjectPatchRequest schema documents the slug as immutable and the handler rejects any PATCH body carrying a slug key with 400 slug_immutable at decode time. --sub-range-cidr and --release-sub-range are cobra-mutually-exclusive: combining them would race the server's 422 sub_range_invalidates_allocation rule, so the CLI fails locally with exit 2 instead of round-tripping .

plexctl project delete

Deletes a Project via DELETE /v1/projects/{id}. The destructive operation is gated behind the root persistent --yes flag (not a local --force); the RunE refuses to call the API without the explicit confirmation, so a single typo of the project id cannot detonate without intent. On success the server returns 204 No Content and the command exits 0 with no body.

Flags

plexctl project create

FlagTypeRequiredDefaultDescription
--domainUUIDyesParent Domain UUID. Required by the wire schema (domain_id) and the server's authorisation pivot.
--slugstringyesKebab-case URL handle, unique within the Domain. Frozen after creation.
--display-namestringyesHuman-readable Project name. Maps to the JSON field name.
--descriptionstringno(empty)Optional free-form description. Omitted from the request body when empty.
--sub-range-cidrstringno(empty)Optional canonical RFC 4632 sub-range reservation inside the parent Domain mesh-CIDR. Omitted from the request body when empty.

plexctl project list

FlagTypeRequiredDefaultDescription
--domainUUIDnoOptional parent Domain filter. Forwarded to the server as the domain_id query parameter (server-side filter, not a client-side scan like domain list --slug).
--limitintnoserver defaultMaximum items per page. 0 lets the server pick.
--cursorstringnoContinuation token from a previous call's next_cursor. Mutually exclusive with --all.
--allboolnofalseFollow next_cursor until exhausted. Capped at 100 000 items as defence-in-depth against a runaway server.

plexctl project get

FlagTypeRequiredDefaultDescription
<id> (positional)UUIDyesProject UUID. No slug fallback — use project list --domain <id> to resolve a slug first.

plexctl project update

FlagTypeRequiredDefaultDescription
<id> (positional)UUIDyesProject UUID.
--display-namestringconditionalNew human-readable Project name (maps to JSON field name). At least one update flag must be set.
--descriptionstringconditionalNew free-form description. Setting an empty string clears the field on the wire. At least one update flag must be set.
--sub-range-cidrstringconditionalNew canonical RFC 4632 sub-range reservation. Mutually exclusive with --release-sub-range.
--release-sub-rangeboolconditionalfalseClear the existing sub-range reservation. Mutually exclusive with --sub-range-cidr.

A call with no update flag set fails locally with exit 2 and the message project update: at least one of --display-name, --description, --sub-range-cidr, --release-sub-range must be set .

plexctl project delete

FlagTypeRequiredDefaultDescription
<id> (positional)UUIDyesProject UUID.
--yes (persistent)boolyesfalseRequired confirmation for the destructive operation. Inherited from the root, not a local --force.

Persistent flags inherited from root

--server, --profile, --token-file, --output, --yes (consumed by delete), --reveal-secrets. See ../plexctl.md for the canonical list and the resolution order between flags, profile, and the PLEXSPHERE_URL environment variable.

Output JSON schema reference

Every successful create / get / update returns a single ProjectResponse; list returns a ProjectList envelope carrying items[] and an opaque next_cursor token. Both schemas are pinned in the OpenAPI 3.1 spec at ../../../api/openapi/plexsphere-v1.yaml under the ProjectResponse and ProjectList definitions.

The text renderer projects the typed shape onto a tabwriter grid with the columns ID, DOMAIN, SLUG, DISPLAY_NAME, CREATED_AT. The DOMAIN column carries the parent Domain UUID (not the Project UUID) so an operator can pivot directly to plexctl domain get <domain-uuid> without round-tripping through JSON.

Exit codes

plexctl collapses every failure into the taxonomy below; the single source of truth is exitCodeFor in ../../../cmd/plexctl/app.go.

CodeReachableMeaning
0yesThe API returned the expected status (201 for create, 200 for list / get / update, 204 for delete).
1yesRuntime / API error: transport failure, unexpected status code, a malformed response body, or delete invoked without --yes (the destructive-confirmation refusal is a plain errors.New which falls through exitCodeFor's default branch, mirroring group delete / audit erase-identity).
2yesFlag-parse / misconfiguration: missing required flag, malformed UUID, mutually-exclusive combination (--all + --cursor, --sub-range-cidr + --release-sub-range), or an empty update patch.
3yesMissing or insecure credentials, or 401 Unauthorized from the API.
4yesPermission denied or resource not addressable: 403 Forbidden where the body's reason is not rebac_denied (e.g. token-audience mismatch), or 404 Not Found from the API.
64noNot reachable — project is fully wired and never returns *NotImplementedError.
77yesReBAC denial: 403 Forbidden with body reason == "rebac_denied". Distinct from generic exit 4 so CI can branch on a ReBAC-specific authorization failure without parsing the response body.

Examples

Create a Project under a Domain

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

plexctl project create \
  --server         "${PLEXSPHERE_URL}" \
  --domain         0190a8b8-a0c0-7a0a-8a0a-a0a0a0a0a0a0 \
  --slug           billing-prod \
  --display-name   "Billing — Production" \
  --description    "Production billing services for the EMEA region." \
  --sub-range-cidr 10.42.16.0/20

List Projects filtered to one Domain (server-side --domain)

shell
plexctl project list \
  --server "${PLEXSPHERE_URL}" \
  --domain 0190a8b8-a0c0-7a0a-8a0a-a0a0a0a0a0a0 \
  --output json

The --domain flag is forwarded to the server as the domain_id query parameter; the server returns only Projects in that Domain. This contrasts with plexctl domain list --slug …, which fetches the page first and filters client-side because ListDomains does not expose a slug parameter.

Page through every Project in a Domain

shell
plexctl project list \
  --server "${PLEXSPHERE_URL}" \
  --domain 0190a8b8-a0c0-7a0a-8a0a-a0a0a0a0a0a0 \
  --all \
  --output json

--all walks next_cursor until empty and aborts with an exit-1 error after 100 000 accumulated items as a runaway-server guard. --all and --cursor are mutually exclusive at the cobra layer.

Sparse update (only --display-name)

shell
plexctl project update 0190a8b8-a0c0-7a0a-8a0a-a0a0a0a0a0b1 \
  --server       "${PLEXSPHERE_URL}" \
  --display-name "Billing — Production (EMEA)"

Only the display-name field is forwarded in the PATCH body; the description and sub-range remain untouched on the server.

Update releasing the sub-range reservation

shell
plexctl project update 0190a8b8-a0c0-7a0a-8a0a-a0a0a0a0a0b1 \
  --server            "${PLEXSPHERE_URL}" \
  --release-sub-range

Combining --release-sub-range with --sub-range-cidr is rejected locally by cobra:

shell
plexctl project update 0190a8b8-a0c0-7a0a-8a0a-a0a0a0a0a0b1 \
  --server            "${PLEXSPHERE_URL}" \
  --sub-range-cidr    10.42.32.0/20 \
  --release-sub-range
# stderr: Error: if any flags in the group [sub-range-cidr release-sub-range] are set none of the others can be; [release-sub-range sub-range-cidr] were all set
echo $?  # 2

Delete a Project

shell
plexctl project delete 0190a8b8-a0c0-7a0a-8a0a-a0a0a0a0a0b2 \
  --server "${PLEXSPHERE_URL}" \
  --yes
echo $?  # 0

The server returns 204 No Content and the command exits 0 with no body. Without --yes the command refuses to call the API and exits 1 with the error project delete: refusing without --yes (destructive operation).

ReBAC denial (exit 77)

shell
plexctl project delete 0190a8b8-a0c0-7a0a-8a0a-a0a0a0a0a0b2 \
  --server "${PLEXSPHERE_URL}" \
  --yes
# stderr: plexctl: 403 Forbidden: rebac_denied (need project:delete on project/0190a8b8-a0c0-7a0a-8a0a-a0a0a0a0a0b2)
echo $?  # 77

The 403 carries reason == "rebac_denied" in the problem-details body; exitCodeFor routes it to exit 77 via the IsRebacDenied branch ahead of the generic *output.APIError mapping.

Cross-references

  • ../../../api/openapi/plexsphere-v1.yamlCreateProject, ListProjects, GetProject, PatchProject, DeleteProject operation definitions and the ProjectResponse / ProjectList schemas.
  • domain.md — sibling reference for the parent Domain aggregate the Project lives inside; plexctl domain get <domain-uuid> is the natural pivot from the DOMAIN column of the table view.
  • identity.md — sibling reference for the per-Domain identity surface.
  • ../../../cmd/plexctl/commands/project.go — source of truth for the cobra command, flag wiring, the --all cap, the --sub-range-cidr--release-sub-range mutual exclusion, and the table projection.