Skip to content

plexctl sink

Synopsis

plexctl sink is the operator surface for the telemetry sinks a Domain declares and for the grants that make one of them usable in a Project. It spans three /v1 surfaces under a single cobra parent:

  • /v1/domains/{domain_id}/sinks (create, list) and /v1/sinks/{id} (get, update, delete): the tenant sinks one Domain owns.
  • /v1/sinks/built-in (list): the platform's own backends. They are read-only, belong to no Domain, and a Telemetry Route may target one without a grant.
  • /v1/projects/{project_id}/sink-enablements (request, list), /v1/sinks/{id}/sink-enablements (grant) and /v1/sink-enablements/{id}/revoke: the enablement lifecycle under the nested plexctl sink enablement subgroup. Approving and rejecting a request belongs to plexctl approval, not here.
text
plexctl sink <subcommand> [flags]
plexctl sink enablement <subcommand> [flags]

Subcommands

plexctl sink create

POSTs /v1/domains/{domain_id}/sinks. --domain-id is required, and so are the four fields every write states: --slug, --display-name, --type and --endpoint. The type decides which endpoint grammar holds (otlp takes an https URL, syslog a host and port pair), and an invalid pairing is refused by the server with 422 sink_invalid naming the field. A slug another sink of the same Domain already holds fails with 409 sink_slug_taken.

--id declares the sink under an identifier the caller mints. That is what a caller who stores connection material needs: the KV path is derived from the owning Domain and the sink id, so the material is written before the sink exists. Omit the flag and the server mints the id.

An otlp sink that states no --dataset files its telemetry under the owning Domain's slug.

plexctl sink list

Reads either the tenant sinks of one Domain (--domain-id, GETs /v1/domains/{domain_id}/sinks) or the platform's built-in sinks (--built-in, GETs /v1/sinks/built-in). Exactly one of the two arms must be set; naming both, or neither, fails locally with exit 2 before the request is sent. Both sets are bounded, so neither is paginated.

plexctl sink get <id>

GETs /v1/sinks/{id}. Resolves a tenant sink and a built-in sink alike. The projection carries the sink's credential coordinates and never the material stored at them.

plexctl sink update <id>

PUTs /v1/sinks/{id} with the full post-image, so a field the command does not state is cleared rather than left untouched: an update that omits --ca-pem-file drops the stored bundle. The owning Domain, the identifier and the built-in marker are immutable and carry no flags; a write addressing a built-in sink fails with 409 sink_conflict.

--expected-updated-at is required and states the updated_at you read the sink at, which plexctl sink get prints. It is the compare-and-swap token: because the body replaces the whole sink, an update written from a stale read would restore every field you did not state, including the TLS posture and the credential reference. A sink that changed since that read fails with 409 sink_cas_conflict and nothing is written, so the fix is to re-read the sink and restate the post-image.

plexctl sink delete <id>

DELETEs /v1/sinks/{id}. Requires --yes. A Telemetry Route that still targets the sink is what makes this destructive, so the confirmation is fail-closed and decided locally.

plexctl sink enablement request

POSTs /v1/projects/{project_id}/sink-enablements. Both --project-id and --sink-id are required. The enablement opens in the requested state and is decided on the approvals queue. The sink and the Project must belong to one Domain, otherwise the request fails with 422 sink_domain_mismatch; a built-in sink needs no enablement at all and is refused with 422 sink_not_enableable.

plexctl sink enablement grant

POSTs /v1/sinks/{id}/sink-enablements. This is the owner's push, the counterpart to request. Both --sink-id and --project-id are required, and the server gates the call on sink#assign over the named sink. The enablement lands already approved, so the sink is usable in the Project the moment the command returns. That is deliberate rather than a shortcut: requesting and then approving one's own request is refused by the self-approval guard, so without this route a sink's owner could not place it in a Project at all.

A second live enablement for the same (Project, sink) pair fails with 409 duplicate_live_sink_enablement.

plexctl sink enablement list

GETs /v1/projects/{project_id}/sink-enablements (the --project-id flag is required). Pagination is keyset-based via --cursor / --limit; --all follows next_cursor until exhausted and is mutually exclusive with --cursor.

plexctl sink enablement revoke <id>

POSTs /v1/sink-enablements/{id}/revoke. Requires --yes and a --reason audit-rationale string. Either side may revoke: the server first checks assign on the sink, and on that denial falls back to deploy on the consuming Project. Revocation is legal from approved only; any other source state answers 409 illegal_transition.

A 502 revocation_sync_pending is not a completed revocation. The row moved to revoked while the grant is still live in the authorization graph, so the Project keeps delivering. Retrying is refused; the removal replays through the committed event.

Deciding a request

plexctl sink enablement has no approve or reject subcommand. Deciding a filed request happens on the dual-control queue: plexctl approval list --kind sink_enablement shows the rows the caller may decide, and plexctl approval approve or plexctl approval reject takes the enablement id directly. See approval.md.

Connection material

A sink states where its connection material lives, never the material itself. --credential-kv-mount names the KV mount and --credential-kv-version the revision to read (the latest one when the flag is omitted). The path under the mount is derived server-side from the owning Domain and the sink id as domains/<domain-id>/sinks/<sink-id>, so no flag carries it and a reference cannot address another tenant's material.

Write the material at that path before the sink exists, which is why --id is worth minting client-side. With --credential-kv-mount secret:

shell
bao kv put secret/domains/<domain-id>/sinks/<sink-id> token='<auth-token>'

--credential-kv-version without --credential-kv-mount fails locally with exit 2: a sink that names no mount reads no material, so there is nothing for a version to select.

--ca-pem-file is the one flag that carries bytes rather than coordinates. The request body holds the PEM bundle itself, not a path the server could open, so the file is read client-side.

Flags

plexctl sink create

FlagTypeRequiredDescription
--domain-idUUIDyesOwning Domain UUID.
--idUUIDnoIdentifier to declare the sink under (UUIDv7); server-minted when omitted.
--slugstringyesKebab-case handle, unique among the Domain's sinks.
--display-namestringyesHuman-readable name the sink is listed under.
--typestringyesProtocol to deliver with: otlp or syslog.
--endpointstringyesAddress to deliver to: an https URL for otlp, a host and port pair for syslog.
--ca-pem-filepathnoPath to the PEM bundle the destination's certificate is verified against. Read client-side.
--insecure-skip-verifyboolnoConnect without verifying the destination's certificate.
--credential-kv-mountstringnoKV mount the connection material lives under.
--credential-kv-versionint64noKV version to read; latest when omitted. Requires --credential-kv-mount.
--datasetstringnoLogical stream an otlp destination files the delivered telemetry under. Defaults to the Domain slug.

plexctl sink list

FlagTypeRequiredDescription
--domain-idUUIDconditionalDomain UUID whose tenant sinks to list. Mutually exclusive with --built-in.
--built-inboolconditionalList the platform's built-in sinks instead of a Domain's. Mutually exclusive with --domain-id.

plexctl sink update <id>

Takes the same field flags as plexctl sink create minus --domain-id and --id, both of which address immutable state. The four required ones stay required, because the body is the full post-image, and one flag is added:

FlagTypeRequiredDescription
--expected-updated-atRFC 3339 timestampyesThe updated_at the sink was read at. A sink changed since then fails with 409 sink_cas_conflict.

plexctl sink delete <id>

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

plexctl sink enablement request

FlagTypeRequiredDescription
--project-idUUIDyesRequesting Project UUID.
--sink-idUUIDyesSink UUID to request delivery to.

plexctl sink enablement grant

FlagTypeRequiredDescription
--sink-idUUIDyesSink UUID to grant.
--project-idUUIDyesProject UUID to enable the sink in.

plexctl sink enablement list

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

plexctl sink enablement revoke <id>

FlagTypeRequiredDescription
--reasonstringyesAudit-recorded revocation rationale.
--yes (persistent)boolyesRequired confirmation for the destructive operation.

Destructive subcommands

plexctl sink delete and plexctl sink enablement revoke consume the persistent --yes flag. Both reject the call without explicit confirmation, and the refusal is decided locally with exit 2 before any request is sent.

Persistent flags inherited from root

--server, --profile, --token-file, --output, --yes, --reveal-secrets. See ../plexctl.md.

Exit codes

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

Examples

Declare a tenant sink with connection material

Mint the id first, write the material at the derived path, then declare the sink against the mount:

shell
plexctl sink create \
  --server                "${PLEXSPHERE_URL}" \
  --domain-id             0190a8b8-a0c0-7a0a-8a0a-a0a0a0a0a0d0 \
  --id                    0190a8b8-a0c0-7a0a-8a0a-a0a0a0a0a0e0 \
  --slug                  central-otlp \
  --display-name          "Central OTLP" \
  --type                  otlp \
  --endpoint              https://otlp.example.com:4318 \
  --credential-kv-mount   secret

Grant a sink to a Project and read the enablements back

shell
plexctl sink enablement grant \
  --server     "${PLEXSPHERE_URL}" \
  --sink-id    0190a8b8-a0c0-7a0a-8a0a-a0a0a0a0a0e0 \
  --project-id 0190a8b8-a0c0-7a0a-8a0a-a0a0a0a0a0c0

plexctl sink enablement list \
  --server     "${PLEXSPHERE_URL}" \
  --project-id 0190a8b8-a0c0-7a0a-8a0a-a0a0a0a0a0c0 \
  --all

Cross-references