Deployment safety and ownership¶
Status¶
Required specification. Safety requirements override backward compatibility with the current orphan-deletion behavior.
Invariants¶
- A live resource is never deleted solely because it is absent from the desired project.
- A resource can be mutated only when its ownership mode permits it.
- Deletion is computed from previously applied streamt state, not by subtracting desired resources from the entire cluster.
- A partial or selected plan cannot alter resources outside its selection.
applyexecutes the exact reviewed plan and rejects stale or modified plans.- Destructive behavior defaults to disabled in every environment mode.
- Unsupported migrations are explicit plan blockers and cannot be bypassed by destructive flags.
Ownership modes¶
Every declared runtime resource has one of three modes:
| Mode | Meaning | Observe | Create/update | Delete |
|---|---|---|---|---|
external |
Exists outside streamt ownership | yes | no | never |
managed |
Created and lifecycle-managed by this project | yes | yes | explicit only |
adopted |
Existing resource explicitly claimed by this project | yes | yes | explicit only |
Sources default to external. Output resources default to managed only after
the project successfully creates them. Existing output resources require
explicit adoption before mutation unless the user selects a backend-specific
create-if-absent policy.
Stable resource identity¶
Compiled artifacts carry a stable identity independent of display names:
They also carry:
- Project and environment.
- Artifact kind and physical target name.
- Owning source/model/test/exposure.
- Ownership mode.
- Source file and logical configuration path when available.
- Content checksum.
Backend APIs may not support storing this metadata. The state backend is the authority for ownership; discovery heuristics are never sufficient to delete.
State model¶
The last applied state records only resources streamt owns or has adopted:
{
"state_version": 1,
"project": "payments",
"environment": "prod",
"serial": 12,
"resources": {
"streamt://payments/prod/topic/payments_clean": {
"physical_name": "payments.clean.v1",
"ownership": "managed",
"artifact_checksum": "sha256:...",
"backend": "direct-kafka"
}
}
}
Local state is acceptable for development but must warn that it is unsuitable
for shared CI. A production direct-apply backend requires remote state and
locking. External deployment backends may use their own state authority.
Local snapshots are isolated by environment at
.streamt/state/<environment>.json. On one host, apply and adopt hold an
exclusive environment operation lock from their authoritative state read
through live observation and state commit; apply also holds it through runtime
mutation and rollback. This closes the prior same-host stale-serial mutation
race. A strict control sidecar records durable intent before mutation, ordered
progress, and conservative recovery-required state, so an interrupted mutation
blocks later local apply/adopt. The file lock is still not distributed, and the
recovery command is not implemented.
Planning algorithm¶
For each desired resource:
- Desired + no live resource: propose create when ownership permits.
- Desired + live + prior ownership: propose an update or no-op.
- Desired + live + no prior ownership: report
requires_adoption; do not mutate.
For each resource in prior owned state but absent from the full desired project:
- Report a potential removal.
- Propose deletion only when destructive changes are enabled and explicitly requested.
For every other live resource:
- Ignore it for lifecycle planning.
- It may still be used as read-only evidence for impact analysis.
When the plan is selected or targeted, removal detection is disabled outside the selected closure.
Deterministic safety blockers¶
Plans carry an ordered, machine-readable safety_blockers array. The initial
blockers are:
kafka_partition_reductionwhen desired partitions are lower than live partitions.schema_incompatiblewhen Schema Registry rejects the desired schema under the subject's compatibility policy.flink_update_requires_savepointfor every existing Flink job update until a savepoint-safe or explicitly stateless upgrade workflow exists.flink_resubmit_requires_state_evidencewhen a non-running existing job would be submitted again without equivalent state evidence.
The canonical order follows backend apply order (Schema Registry, Kafka, then Flink), followed by physical resource name and blocker code. Verified new-resource creates and no-ops do not produce safety blockers. An ownership decision that neutralizes a resource to observe-only/no-op also does not produce one.
Planning succeeds when blockers exist so reviewers and automation can inspect
them. Applying does not: direct apply and reviewed-plan apply both fail with
E417_SAFETY_BLOCKED before any backend mutation. --force does not override
these blockers.
Plan/apply protocol¶
A version 2 saved plan contains the desired manifest checksum, prior-state serial, environment fingerprint, live-state observations used by the plan, proposed actions, ordered ownership and safety decisions, and plan checksum. Version 1 files are rejected as unsupported rather than interpreted without safety-blocker semantics.
Protected environments always require apply --plan. Other shared deployment
workflows opt in explicitly with safety.require_reviewed_plan: true; streamt
does not infer sharing or criticality from an environment name. A direct apply
under either policy fails with E418_REVIEWED_PLAN_REQUIRED before deployers
are constructed, including when --confirm, --force, or --dry-run is
present. Structured output includes executable plan --out and apply --plan
next steps with the resolved --project-dir preserved, shell-quoted paths, and
a fixed .streamt/reviewed-plan.json output name. A reviewed-plan apply still
requires protected-environment confirmation and all checksum, project,
environment, state-serial, live-drift, ownership, and safety-blocker checks
below.
apply must reject a plan when:
- Project content changed after planning.
- State serial changed.
- The environment differs.
- The plan is expired under configured policy.
- Required approval or destructive confirmation is absent.
- Safety blockers differ from the reviewed plan or remain unresolved.
Adoption¶
streamt import discovers resources and emits external declarations.
The Kafka import MVP reads the selected environment's runtime configuration,
discovers topics with repeatable include/exclude filters, and optionally reads
the conventional {topic}-value Schema Registry subject. It emits explicit
external source declarations only. Exact topic matches already present in the
project are skipped; generated-name collisions with sources or models fail the
whole operation. The output must be a new direct child of sources/, is strict-
validated before creation, and is durably staged then atomically installed without
replacement through a verified directory handle. Import never overwrites a file,
mutates infrastructure, or writes ownership state. Avro and JSON Schema may populate
columns; Protobuf is retained as a pinned external reference.
streamt adopt:
- Reads the live resource.
- Shows the exact attributes streamt will begin managing.
- Requires explicit resource and environment confirmation.
- Writes an ownership entry without changing the resource.
- Produces a new plan before any later mutation.
Bulk adoption requires a saved selection and non-interactive confirmation token suitable for CI review.
Single-resource adoption¶
The command intentionally supports one resource at a time. Kafka topic adoption uses:
LOGICAL_NAME must resolve through compiled artifact ownership to exactly one
physical topic, and the declaration must explicitly use
ownership.mode: adopted. The command performs only Kafka topic observation;
it never creates, updates, or deletes a topic. It shows the live and desired
managed attributes plus pending differences before confirmation, with secrets
redacted.
Interactive confirmation uses an exact token containing the canonical resource
ID and environment. Non-interactive confirmation requires both
--confirm-resource streamt://... and --confirm-env ENV to match. On success,
only the adopted record is added to environment-scoped local state, all other
records are retained, and the serial advances once. An identical existing claim
is idempotent. Conflicting state fails closed. Users must produce and review a
fresh plan before later mutation; normal apply also replans against the new
state.
Schema Registry subject adoption uses the same protocol:
The logical owner must resolve to exactly one compiled subject with explicit
ownership.mode: adopted. streamt reads that exact subject and validates its
version, schema ID, type, compatibility, and references. Review output contains
only metadata and deterministic hashes, never schema bodies. The state record
uses the planner-identical artifact checksum and schema-registry backend, so a
fresh plan can safely distinguish the adopted subject from unowned live data.
No register, compatibility-update, list, or delete API is called.
Flink, Kafka Connect, and Gateway adoption remain unsupported until their exact management surfaces and compound resource identities have equivalent fail-closed workflows.
Destructive operations¶
Topic deletion, subject deletion, connector deletion, state reset, and state-incompatible Flink replacement are destructive. Partition reduction is not an overridable destructive operation because Kafka does not support it.
They require:
- A full, non-targeted plan.
- Previous streamt ownership.
- Environment policy permitting destructive changes.
- An explicit destructive flag or dedicated destroy command.
- A reviewed plan checksum.
Topic and stateful-job deletion should support an environment-level policy that forbids them entirely.
Until stateful migration semantics land, every planned Flink update is blocked; it cannot reach the current cancel-and-resubmit implementation.
Immediate compatibility behavior¶
Local ownership state is persisted after successful direct applies. Automatic orphan deletion remains disabled until removal is an explicit, reviewed workflow backed by state appropriate to the deployment environment. This intentionally trades cleanup convenience for safety.