Core-only installations
On this page
What stays the same and what changes when you upgrade to a release with the plugin host but install no plugin.
You do not need a plugin, a private image, a pull secret, or trust material to run the Platform Orchestrator. Install and upgrade a Core-only installation as described in Self-hosting.
What stays the same
Every plugin setting in the Helm chart is off by default:
| Value | Default |
|---|---|
control-plane.configurationProfiles.nativePluginGateway.enabled | false |
control-plane.configurationProfiles.nativePluginEntitlements.enabled | false |
data-plane.configurationProfiles.nativeExternalDeployment.enabled | false |
iam.configurationProfiles.pluginWorkloadIdentity.enabled | false |
iam.configurationProfiles.pluginWorkloadStaticJWKS.enabled | false |
iam.config.PLUGIN_MANAGER_READERS | empty |
iam.config.PLUGIN_MANAGER_WRITERS | empty |
console.gatewayApi.route.pluginUiOrigins | [] |
With these defaults, the chart renders only public Stellwerk and third-party
images. Plugin discovery returns an empty list, and every plugin-dependent
client command reports plugin_not_installed. Core commands, Core provider
resources, and the Console work as before.
A failed ordinary Deployment never blocks the next Deployment of its Environment. Only failed Deployments that a plugin dispatched can block an Environment; see Plugin recovery.
What changes
Concurrent Deployments to one Environment
Deploy and rollback Deployments change their Environment, so they still run one at a time under a per-Environment deployment lease. What changed is what a second request does while the lease is held. The raw API and the standard clients now differ:
| Caller | Request while the Environment is busy |
|---|---|
Raw API without when_busy, or with when_busy: queue | HTTP 202 and a queue item that becomes a Deployment when the Environment is free |
Raw API with when_busy: reject | HTTP 409 with the error code environment_busy |
Raw API with an organization token, whatever when_busy says | HTTP 409 with the error code environment_busy |
octl deploy and octl rollback, the provider, and the Console | Wait, as the raw API does |
Waiting is the default of the API. A client that does not send when_busy,
including a script written before the queue existed, now waits where it used to
get a 409. Send when_busy: reject (octl deploy --no-queue) to keep the fail
fast answer.
Earlier releases answered the busy case with the messages
deployment '<id>' is still executing or
environment has an active deployment lease. Match the error code
environment_busy instead of the text. The lease is released when the running
Deployment succeeds or fails. The Data Plane renews it while the Deployment
runs, and a lease that is neither released nor renewed expires after 65
minutes.
How the queue behaves
- Items run first in, first out per Environment, one at a time.
- A queue item stores your request, not a resolved plan. Core resolves it when it promotes the item, against the Environment as it is then, including Pins and Module Version lifecycle. A queued item therefore never reverts what the item ahead of it deployed, and it can still be rejected later, for example because a Pin was created in the meantime.
- A failed Deployment does not stop the queue, unless the waiting item asks for
it with
halt_on_failure; see Stop after a failure. A failed Deployment that a plugin dispatched and whose outcome is unknown does stop it; see Plugin recovery. - While an Environment has a queued item, no other path, including a plugin, starts a Deployment there.
- Plan-only requests and dry runs are never queued; see Plans and dry runs beside the queue. Destroy is never queued; it waits for the running Deployment. Deleting an Environment cancels its queued items.
- A request that a deprecated organization token authorizes is never queued,
because no user remains whose permission Core could check again at promotion.
It answers
environment_busywhen the Environment is busy, whether it omitswhen_busyor sendsqueue. Use a user or a service user token to wait. - Promotion is safe with several Data Plane replicas and survives a restart without duplicating or losing an item.
A request that was derived from a specific state, such as a merge or a diff you
confirmed at a prompt, can name expected_base_deployment_id. If the
Environment’s current Deployment differs when the item is promoted, the item is
rejected with deployment_base_changed instead of deploying onto a different
base. octl sends it automatically for --merge, --drop-workload,
--drop-shared, octl score deploy, and every diff that you confirm.
Stop after a failure
By default a queued request starts even when the Deployment ahead of it failed.
If the request only makes sense after a successful predecessor, set
halt_on_failure: true on it. When Core promotes the item and the Deployment it
waited for failed, the item is not started. It ends rejected with the reason
code previous_deployment_failed, its reason_message names the failed
Deployment, and the queue continues with the next item.
- The Deployment it waited for is the latest deploy, rollback or destroy of the Environment at promotion, whether it came from the queue, a direct request or a plugin, provided it had not ended yet when the item was queued.
- A failure that happened before the item was queued does not halt it. A later Deployment that succeeds replaces an earlier failure. Plan runs never count. A Deployment that an operator force-fails counts as a failure.
- One failure halts every waiting item that set the flag. Items without it start.
- A request that starts at once has nothing to wait for and never halts.
halt_on_failureis refused (400) together withwhen_busy: reject.
Plans and dry runs beside the queue
Plan-only requests and dry runs change nothing. They are never queued and they
ignore when_busy, queue_ttl_seconds and halt_on_failure, so a client can
send its usual queue settings with them.
- A dry run is answered while Deployments run or wait.
- A plan-only request starts while items wait in the queue, and the head of the
queue then waits for the plan run to finish. It does not start while a deploy,
rollback or destroy runs, or while another plan run of the Environment runs.
It answers HTTP 409
environment_busy, whateverwhen_busysays, because every Runner step locks the Environment’s Terraform/OpenTofu state and does not wait for the lock.
When Deployments run or wait, the response that creates the plan-only Deployment
or answers the dry run carries pending_changes:
"pending_changes": {"code": "result_may_change", "running": 1, "queued": 2}
running is the number of deploy, rollback or destroy Deployments changing the
Environment now (0 or 1), and queued the number of requests waiting. The plan
or diff describes the Environment as it is now, so it may still change. The
field is absent when nothing runs or waits, and later reads of the Deployment do
not carry it.
Queue limits
| Variable | Default | Meaning |
|---|---|---|
DEPLOYMENT_QUEUE_MAX_DEPTH | 10 | Waiting items per Environment. More fail with deployment_queue_full |
DEPLOYMENT_QUEUE_DEFAULT_TTL | 2h | How long an item may wait. It exceeds one lease, so a full-length predecessor does not expire the next item |
DEPLOYMENT_QUEUE_MAX_TTL | 24h | Upper bound of a requested TTL. It must not be below the default |
Set them on the Data Plane. A client can request a shorter or longer TTL within
the maximum with queue_ttl_seconds.
Watch and cancel queued work
octl follows a queued item until it starts, polling every 5 seconds and
showing its position and any transient promotion failure. If the item is
rejected, expires, or is cancelled, the command fails with the reason code and
the next step. Pressing Ctrl+C leaves the item queued and prints the command to
cancel it.
| Command | Purpose |
|---|---|
octl get deployment-queue-items [project] [env] --state | List queue items in order, optionally by state |
octl get deployment-queue-item <id> | Show one item, including its requested manifest |
octl deployments cancel-queued <id> [--reason] | Cancel a waiting item |
octl deploy --no-queue, octl rollback --no-queue | Fail at once with environment_busy when the Environment is busy |
octl deploy --queue-ttl <duration> | Wait for at most this long, in whole seconds of at least 1s |
octl deploy --halt-on-failure, octl rollback --halt-on-failure | Do not start if the Deployment it waits for fails |
--queue-ttl and --halt-on-failure each cannot be combined with
--no-queue, --plan-only, or --dry-run. When a plan-only request or a dry
run finds Deployments that run or wait, octl prints
The result may change: N Deployment running, M queued. They change the environment first.
(Deployments when N is not 1). If a request queued with --halt-on-failure
is rejected, octl names the failed Deployment and the command to inspect it
(octl get deployment <id>). Listing and reading items needs deployment_read on the
organization. Anyone with deployment_write on the Environment can cancel a
waiting item. The Data Plane records the actor and reason of every
cancellation, and records expiry and rejection with their reason.
The API routes are GET /orgs/{orgId}/deployment-queue,
GET /orgs/{orgId}/deployment-queue/{queueItemId}, and
POST /orgs/{orgId}/deployment-queue/{queueItemId}/actions/cancel. A
cancellation that meets a promotion in progress answers HTTP 409
queue_item_dispatching; read the item again. Cancelling an item that has
already settled answers queue_item_settled.
Terraform and OpenTofu
The platform-orchestrator_deployment resource accepts when_busy (queue or
reject). Unset means queue for mode = deploy, and a plan-only resource
never queues. The create and update timeouts (default 20 minutes) cover the
request, the queue wait and, with wait_for, completion. If the timeout passes
while the item is still queued, the provider cancels it. Changing only
when_busy or timeouts updates the state without a new Deployment.
Upgrade note
The queue schema is additive. Its down migration refuses to run while an item is
queued, so cancel waiting items or
let them settle before you roll back. Clients that never send when_busy now
wait for a busy Environment instead of getting a 409, so a script that expects
environment_busy must send when_busy: reject. Requests authorized by an
organization token still get the 409.
Runner execution budget
The one-hour Runner execution budget now starts at the first durable dispatch attempt, after the Runner bundle is prepared. Time spent waiting for bundle construction no longer counts against a new Deployment. Restarts, broker redelivery, and retries never reset the budget.
Existing Deployments have no recorded first-dispatch time. The upgrade backfills their budget from their original admission time. It does not change their status, history, resources, or outputs.
Extension event journal
The Control Plane keeps an extension event journal only for organizations in which an installed plugin subscribes to resource events. Without the plugin host, or without a subscribing plugin, it writes no journal entries and uses the same transactional outbox path as before. The journal retention job runs only while the plugin host is enabled. There is no separate setting to turn the journal on.
Database schema
The upgrade adds tables to the Control Plane, Data Plane, and IAM databases, even if you never install a plugin. The migrations are additive and leave existing data unchanged. Several down migrations refuse to run once they would discard recorded evidence, such as dispatch attempts or event history.
Back up all three databases before you upgrade. IAM 2.4.0 and earlier refuse to start on the upgraded IAM schema, and older Control Plane and Data Plane images are not qualified against the upgraded schemas. A rollback to older images is therefore not a recovery path. To recover, deploy schema-compatible images of the new release.
New routes and permission
- The API gateway routes
POST /orgs/{orgId}/projects/{projectId}/envs/{envId}/deployments/{deploymentId}/actions/acknowledge-outcometo the Control Plane. Core-only installations never need it, because ordinary Deployments do not block their Environment. - The API gateway routes
POST /auth/plugin-workload-token,/plugin-workload-bindings, and/platform-principals(list, create, read, delete, andactions/regenerate) to IAM. While workload identity is off, the token exchange answers HTTP 404. Only principals listed inPLUGIN_WORKLOAD_BINDING_WRITERScan manage bindings and platform principals, and the list is empty by default. - The API gateway routes the Plugin Manager operator API (
/plugin-packages,/plugin-installations, includingorganization-modeand the organization enablements,/plugin-entitlement-bundles, and/plugin-audit-events) to the Control Plane. With the plugin host off, every route answers HTTP 404 with the codeplugin_host_disabled. Only the users that IAM lists inPLUGIN_MANAGER_READERSandPLUGIN_MANAGER_WRITERScan call it, and both lists are empty by default. - The IAM permission catalog contains
deployment.outcome.acknowledge. The built-in Admin role includes it. - IAM also knows the platform permissions
plugin_manager_readandplugin_manager_write. Only the configured users hold them, and no organization role grants them. The Console shows its read-only Plugin Manager view only to holders ofplugin_manager_read. - A Core-only installation has no platform principals and enables no organization for any plugin. The IAM database gains the scope of a service user for platform principals, which changes no organization service user. The Control Plane database gains the tables for several organizations per plugin installation, the organization mode, and installation-wide license grants. Nothing in them applies until you install a plugin.
Clients
The standard octl CLI and Terraform/OpenTofu provider contain commands and
resources for Stellwerk Enterprise plugins. Their help text and generated
documentation name the required plugin ID. On a Core-only installation they
fail with plugin_not_installed before contacting a plugin. See
Plugin availability diagnostics.