Explicit Kafka Connect connector removal¶
Status¶
This is the normative target contract for explicit Connector removal. It is not an implementation claim. Until every release gate in the accompanying implementation plan passes, Connector lifecycle deletion remains unsupported.
The words MUST, MUST NOT, REQUIRED, SHOULD, and MAY are used as normative requirements.
Decision¶
streamt will support deletion of one exact, previously managed Kafka Connect
connector through a project lifecycle tombstone and the ordinary reviewed
plan/apply transaction. The first supported release is deliberately
PostgreSQL deployment-state schema version 2 only.
An actionable removal MUST have all of the following:
- an explicit, strict, secret-neutral lifecycle tombstone;
- one exact canonical
ownership == "managed"PostgreSQL state record; - one exact runtime-derived Connect cluster binding;
- one complete fresh managed Connector observation;
- a full online reviewed plan with exact secret-neutral action evidence;
- destructive authorization;
- a PostgreSQL session advisory lock held through re-planning, durable intent, provider mutation, postcondition observation, and atomic state commit; and
- exact reviewed recovery for every uncertain mutation or commit outcome.
Removing a sink model, omitting a connector artifact, selecting another model, or observing an absent provider resource MUST NOT create deletion authority.
Scope¶
The first release supports connectors that can already enter the strict bound Connector planning and state contract:
- one connector name encoded as one Connect REST path segment;
- the explicitly named default Connect cluster;
- the canonical version-1 Connect backend identity;
- one strict
GET /connectors/<percent-encoded-name>observation; - a reconstructible compiled
ConnectorArtifactwhose exact checksum matches the managed PostgreSQL ownership record; and - the current
modelownership type.
The workflow deletes the Connector object only. It does not delete Kafka topics, records written by the connector, consumer offsets, schemas, external systems, credentials, connector plugins, or any provider object inferred from connector configuration.
Non-goals and hard boundaries¶
This specification does not:
- infer deletion from manifest absence or
LocalState.removal_candidates(); - support local deployment state, another future remote backend, PostgreSQL schema version 1, or a PostgreSQL store whose exact version-2 writer and ACL contract cannot be proved;
- add a direct
streamt connector deletecommand; - allow direct apply, direct dry-run, offline planning, partial planning,
--target, or--selectto authorize a removal; - delete an adopted, external, legacy-unbound, missing, ambiguous, or checksum-mismatched Connector;
- persist a raw connector configuration in the tombstone, reviewed plan, operation control, history, output, logs, or recovery plan;
- treat a Connect
DELETEas transactional with PostgreSQL; - claim exclusion against a non-streamt Connect writer; or
- enable topic deletion, Schema Registry subject deletion, or Flink job cancellation. Those resource kinds are hard-deferred below.
Identity model¶
The logical ownership identity, provider name, cluster alias, and canonical backend identity are distinct values:
resource_id
streamt://<project>/<environment>/connector/<logical_owner>
provider locator
(<backend_identity>, <connector_name>)
backend_identity
kafka-connect:v1:<cluster_alias>:sha256:<normalized-endpoint-digest>
logical_owner selects the state record. connector_name selects the Connect
object. cluster_alias selects one configured runtime cluster. The endpoint
digest binds that alias to the normalized runtime endpoint without exposing the
endpoint. None of these values may be substituted for another.
The tombstone MUST contain the cluster alias explicitly. Default-cluster
inference is forbidden for removal, even if omission is accepted for desired
Connector artifacts. In the first release, that explicit alias MUST equal
runtime.connect.default; a configured non-default alias fails before provider
construction. Alias-indexed deployer construction, grouping, closing, and
recovery are separate work. YAML MUST NOT contain an endpoint, endpoint
fingerprint, backend identity, state address, ownership object, or credential.
The collision key for the provider object is
(normalized_endpoint_digest, connector_name), not the alias-bearing backend
identity. Two aliases that normalize to the same endpoint address the same
Connector namespace. Desired artifacts, tombstones, and prior state MUST be
checked against this alias-independent key even though the canonical evidence
continues to retain the exact default alias.
Public YAML contract¶
The exact additive project shape is:
# streamt:skip -- normative planned schema; unsupported until implementation lands
apiVersion: streamt.dev/v1alpha1
project:
name: payments
lifecycle:
connector_removals:
- logical_owner: archive_orders
name: archive-orders-sink
cluster: primary-connect
The strict typed shape is:
LifecycleConfig
connector_removals: list[ConnectorRemovalDeclaration] = []
ConnectorRemovalDeclaration
logical_owner: 1..128 control-free characters without "/"
name: 1..256 control-free characters
cluster: 1..128 control-free characters naming runtime.connect.default
All three fields are required within each entry. Models MUST use
extra="forbid". Null collections, null fields, empty or whitespace-only
strings, implicit singleton objects, and author-supplied config,
connector_class, topics, ownership, backend, endpoint, or fingerprint
fields MUST be rejected. Unicode control and surrogate code points are
forbidden. The collection is capped at 256 declarations, and the resolved
canonical resource ID MUST fit the existing 512-character durable-action
boundary. Repository-wide duplicate-YAML-key detection is not introduced by
this slice; the strict typed value produced by the existing YAML loader is the
only input to removal compilation.
Declarations MUST be unique by canonical resource_id and provider locator.
The complete project MUST reject a desired Connector and a removal declaration
that claim the same logical resource or provider locator. It MUST also reject
two declarations that reuse a logical owner or (cluster, name) pair.
The tombstone contains no prior artifact because Connector configuration may contain credentials. Exact prior-artifact proof is reconstructed from the strict live observation and compared with authoritative state instead.
Compiled manifest contract¶
Non-empty declarations compile into a separate artifact collection:
{
"artifacts": {
"connectors": [],
"connector_removals": [
{
"logicalOwner": "archive_orders",
"name": "archive-orders-sink",
"cluster": "primary-connect"
}
]
}
}
connector_removals is included in the manifest checksum but is not included
in connectors, desired managed records, the DAG, model selection closure, or
provider deployment requirements. The collection SHOULD be omitted when empty
so projects without removals retain their prior manifest checksum.
Compilation validates syntax and deterministic ordering only. It MUST NOT read
deployment state, resolve an endpoint, construct a provider client, observe a
connector, or emit a ConnectorChange.
Remote-state authority and locking¶
Connector removal intrinsically requires deployment_state.backend: postgres
and an exact PostgreSQL schema-version-2 ordinary writer. This requirement does
not depend on safety.require_remote_state; setting that option to false
cannot enable local removal. --force cannot bypass it.
Both online plan creation and apply MUST use the configured PostgreSQL state service. Plan MUST hold the address advisory lock from its authoritative state/control observation through target preflight, live Connect observation, ordered action creation, and atomic reviewed-plan file replacement. It MUST then release the lock without changing ownership or operation control.
Apply MUST reacquire the same address lock and hold it from authoritative state/control observation through fresh compilation, fresh live re-planning, reviewed-plan equality, durable intent and progress, managed Connector deletion, absence confirmation, atomic ownership/control/history commit, and verified lock release. Existing lock timeout, lock loss, unknown commit, and release-after-commit semantics remain mandatory.
A pending in_progress or recovery_required control record MUST block plan
creation and apply before Connect access. Failure to initialize or strictly
read PostgreSQL MUST never fall back to local or empty state.
Provider-free preflight¶
After strict compilation and one locked authoritative state/control read, but before Connect client construction or provider access, preflight MUST:
- resolve each declaration to its canonical logical
resource_id; - require the explicit alias to equal
runtime.connect.default, resolve that cluster through project runtime configuration, and reject an unknown or non-default alias withE209_INVALID_CLUSTER_REF; - derive the canonical backend identity from that cluster's normalized endpoint;
- reject malformed, duplicate, desired/removal-colliding, or cross-project identities;
- find at most one prior record for both
resource_idand the alias-independent(normalized_endpoint_digest, connector_name)provider target; - when a prior record exists, require
ownership == "managed", exactphysical_name == name, and exact canonical backend equality; - reject
backend: kafka-connect, every other legacy backend, an adopted record, or a second state record claiming the provider locator; and - return one immutable
ResolvedConnectorRemovalwithout creating an action.
The pure resolved shape is:
ResolvedConnectorRemoval
resource_id
logical_owner
connector_name
binding: ConnectClusterBinding
prior_record: ManagedResourceRecord | None
The state record's artifact checksum cannot be proved until the provider has returned a complete config. Preflight MUST still finish every identity and collision check possible without provider data first.
Strict observation and prior-artifact reconstruction¶
Planning MUST use observe_managed_connector(name), which performs exactly one
bounded, redirect-disabled resource GET and returns an immutable
ManagedConnectorObservation. List discovery, /config plus /status
assembly, task status, worker identity, traces, and plugin metadata are not
accepted evidence.
For a present observation, the config MUST contain exact scalar values for the
reserved name, connector.class, and topics keys. Planning reconstructs:
ConnectorArtifact
name = tombstone.name
connector_class = observed config["connector.class"]
topics = observed config["topics"].split(",")
cluster = tombstone.cluster
config = observed config excluding name, connector.class, and topics
ownership = {
mode: "managed",
project: current project,
type: "model",
name: tombstone.logical_owner
}
No trimming, case folding, scalar coercion, default insertion, or key omission
is allowed beyond the established compiled Connector parser. The reconstructed
artifact MUST pass that parser, and artifact_checksum(artifact.to_dict())
MUST equal the exact prior state checksum before a delete can be planned.
A provider that masks, expands, drops, or otherwise changes config such that the prior checksum cannot be reconstructed is unsupported for removal and fails closed. The user cannot work around this by copying config into YAML.
Planning outcomes¶
Planning produces a delete only for an exact managed state record and a matching complete present observation.
| Prior PostgreSQL record | Fresh observation | Result |
|---|---|---|
| Exact managed locator and checksum | Present and reconstructs checksum | One destructive ConnectorChange(action="delete"). |
| Exact managed locator | Absent | Blocking state_provider_drift; retain state. |
| Exact managed locator | Present but checksum differs | Blocking state_provider_drift; retain state. |
| Adopted, external, legacy, mismatched, or ambiguous record | Not read | Fail provider-free preflight. |
| No prior record | Absent | Visible already_absent assessment; no provider action or ownership-state change. |
| No prior record | Present | Blocking ownership_required; no mutation. |
| Desired Connector collision | Not read | Fail the complete plan. |
Non-actionable results are represented separately as immutable,
secret-neutral ConnectorRemovalAssessment values:
ConnectorRemovalAssessment
resource_id
logical_owner
connector_name
backend_identity
status: "already_absent" | "state_provider_drift" | "ownership_required"
Offline plans do not produce assessments: a project containing any Connector removal is rejected as an unsupported authorization path.
Reviewed-plan and durable evidence¶
The reviewed plan format advances from version 4 to version 5. Every version-5
action has exact keys index, resource_id, action, gateway_evidence, and
connector_evidence. The two evidence fields are mutually exclusive. Existing
non-Connector actions serialize connector_evidence: null.
An actionable removal has this exact secret-neutral action:
{
"index": 0,
"resource_id": "streamt://payments/prod/connector/archive_orders",
"action": "delete",
"gateway_evidence": null,
"connector_evidence": {
"version": 1,
"backend_identity": "kafka-connect:v1:primary-connect:sha256:<endpoint-digest>",
"connector_name": "archive-orders-sink",
"prior_artifact_checksum": "sha256:<managed-artifact-digest>",
"current": {
"exists": true,
"fingerprint": "sha256:<bound-present-observation>"
},
"desired": {
"exists": false,
"fingerprint": "sha256:<bound-absence-observation>"
}
}
}
The evidence version is outside both surface fingerprints. current.fingerprint
MUST equal the existing ManagedConnectorObservation.fingerprint, whose exact
canonical preimage contains only alias-bearing backend identity, connector
name, exact presence, and canonical exact config. desired.fingerprint MUST
equal the same observation fingerprint algorithm applied to an exact absent
observation for that binding and name. Raw config MUST NOT be stored beside the
fingerprint. Golden-vector tests freeze both preimages and digests.
The plan checksum covers the complete ordered action. Fresh apply re-planning,
the reviewed plan, the durable OperationIntent, mutation dispatcher, state
projection, and recovery MUST use equal action values. Reconstructing a similar
action independently at mutation time is forbidden.
Operation control advances from version 2 to version 3 with the same nullable
connector_evidence field. Recovery-plan format advances from version 2 to
version 3. Every reader MUST explicitly accept versions 1, 2, and 3. Clear v2
control remains readable by ordinary plan/apply/adopt and upgrades only on a
subsequent write; active v1/v2 operations retain their exact legacy recovery
semantics. Neither legacy version can represent or authorize a Connector
delete, and their serialized bytes/checksums do not change. Reviewed plan
versions 1 through 4 cannot authorize apply after the global version-5 bump and
receive regeneration guidance.
CLI authorization¶
The only supported workflow is:
streamt plan --project-dir . --env prod \
--out .streamt/connector-removal.plan.json
# Review the resource, cluster/backend fingerprint, current fingerprint,
# desired absence, state binding, ordered action, and destructive risk.
streamt apply --project-dir . --env prod \
--plan .streamt/connector-removal.plan.json \
--confirm-env prod --force
safety.allow_destructive: true may replace --force; it does not replace the
reviewed plan, environment confirmation, PostgreSQL authority, or exact state
and provider evidence. A tombstone assessed already_absent produces no
delete and requires no destructive override.
The following MUST fail before Connect access:
- direct apply, including
--dry-run, with any Connector tombstone; apply --target,apply --select, or any future partial-selection option;- offline plan creation or an offline reviewed plan;
- a reviewed plan using format version 1 through 4;
- local state or any non-PostgreSQL-v2 state authority;
- stale manifest, runtime, state, control, action, or provider identity;
- pending recovery; and
- missing or mismatched Connect cluster configuration.
Managed mutation protocol¶
The bare delete_connector(name) primitive is not sufficient authorization.
The lifecycle path MUST call:
The method MUST require a complete present exact observation whose binding equals the deployer's current binding. It MUST NOT accept a string, state record, tombstone, dictionary, or absent observation.
After durable progress_started, the method MUST:
- perform one immediate strict resource observation;
- require it to equal the reviewed
currentobservation exactly; - issue one redirect-disabled, bounded
DELETE /connectors/<percent-encoded-name>; - require exactly HTTP 204 with an empty response body;
- poll the same strict resource observation with a bounded attempt count and deadline until exact absence is returned; and
- return only the literal
"deleted"after that postcondition.
The DELETE request is direct and non-retrying. After progress_started, a 404,
redirect, any non-204 status, nonempty 204 body, timeout, connection loss,
malformed response, or missing absence postcondition has the single stable
E428_CONNECTOR_REMOVAL_DRIFT uncertain outcome. It MUST produce failed
durable progress and recovery_required; it MUST NOT remove ownership state,
retry DELETE, or continue to a later runtime action. Existing transport codes
remain available before durable mutation begins, but never downgrade an
uncertain started delete.
The pre-delete GET narrows but cannot eliminate the race with an external Connect writer because the API offers no conditional delete transaction. This TOCTOU boundary MUST remain documented. streamt's PostgreSQL advisory lock serializes streamt writers only.
Exact state projection¶
State projection uses an exact Connector-specific value:
ManagedConnectorResourceDeletion
resource_id
backend_identity
connector_name
prior_artifact_checksum
It is constructed only from a successfully completed durable Connector delete
action with valid ConnectorActionEvidence. Projection MUST require one exact
prior managed record matching all four fields, no duplicate resource or
provider locator, and no desired record claiming either identity.
Only that record is removed. Unrelated schema, topic, Flink, Connector, and
Gateway records remain byte-for-byte equal. The logical ownership serial
increments exactly once. A tombstone, live absence, plan assessment, provider
response, or manually constructed ConnectorChange cannot remove state.
PostgreSQL atomically commits the replacement ownership payload, clears control, and appends state and operation history. No ownership-state or PostgreSQL schema migration is required.
Recovery¶
Recovery MUST work from the durable version-3 action even if the lifecycle tombstone or sink model is no longer in the current project. It revalidates the project/environment, exact PostgreSQL store and address, runtime-derived binding, action identity, prior state record, and evidence before Connect access.
For a blocked Connector delete:
| Fresh exact observation | Allowed classification |
|---|---|
| Absent | Completed candidate. observed removes the exact prior state record and clears control. |
| Present with the exact durable current fingerprint and reconstructible prior checksum | Prior candidate. rolled_back retains state and clears control. |
| Present with any other fingerprint | Third state; no resolution is permitted. |
| Partial, malformed, oversized, redirected, unauthorized, or unavailable | Observation failure; retain blocker. |
abandoned_before_mutation remains legal only when durable progress proves no
runtime action started. A DELETE-time 404 may later resolve as completed only
through an explicit reviewed recovery plan proving exact absence. The failing
apply itself never calls that 404 success.
Recovery MUST use the normal PostgreSQL advisory lock and version-2 writer, strictly compare state/control revisions, and atomically finalize state, control, and history. Local recovery cannot resolve a Connector removal.
Recovery tests MUST cover mixed ordered actions: one completed Connector delete followed by a failed Connector delete, Connector deletes before and after non-Connector actions, mixed candidate/prior targets, exact subset state removal, and preservation of records for actions that never started.
Errors and warnings¶
The implementation reuses these existing structured codes:
| Code | Connector-removal use |
|---|---|
E501_PARSE_ERROR |
Invalid strict YAML shape. |
E205_CONNECT_REQUIRED |
No usable Connect runtime is configured. |
E209_INVALID_CLUSTER_REF |
Explicit alias is unknown or is not the configured default. |
E408_PLAN_FILE_INVALID |
Unsupported or malformed reviewed-plan version/shape. |
E409_PLAN_STALE |
Manifest, runtime, state, action, or fresh observation differs from review. |
E417_SAFETY_BLOCKED |
A removal assessment blocks apply. |
E418_REVIEWED_PLAN_REQUIRED |
Direct, partial, selected, targeted, or offline workflow. |
E419_STATE_RECOVERY_REQUIRED |
An unfinished operation already blocks the address. |
E420_STATE_BACKEND_UNAVAILABLE |
Configured PostgreSQL is v1, incompatible, unavailable, or cannot prove its exact v2 authority. |
E421_REMOTE_STATE_REQUIRED |
Local or another non-PostgreSQL backend is selected. |
E422_STATE_LOCK_TIMEOUT |
The PostgreSQL address lock cannot be acquired in time. |
E423_STATE_LOCK_LOST |
Lock ownership is lost before finalization. |
E424_STATE_CONFLICT |
Locked state/control preconditions change. |
E425_STATE_UNKNOWN_OUTCOME |
Ownership/control finalization cannot be confirmed. |
E426_STATE_RELEASE_FAILED_AFTER_COMMIT |
Commit is verified but lock release cannot be verified. |
E503_ENVIRONMENT_ERROR |
Destructive or environment confirmation is absent. |
E407_DEPLOY_ERROR |
Non-classified provider mutation failure. |
Two new codes are reserved:
E427_CONNECTOR_REMOVAL_INVALID: malformed compiled removal, impossible identity, legacy/adopted state, collision, or unreconstructible prior artifact; andE428_CONNECTOR_REMOVAL_DRIFT: pre-delete mismatch, DELETE-time 404, or failure to prove the absent postcondition after an accepted delete.
An actionable plan emits one aggregate W119_CONNECTOR_REMOVAL_DESTRUCTIVE
before an output file is saved; an authorized apply emits the same aggregate
warning after fresh reviewed-plan equality and destructive authorization but
before durable intent. The warning includes only the ordered delete count, not
resource IDs, names, aliases, endpoints, or config. Text/JSON/quiet behavior
uses the existing formatter warning contract. The existing
W106_LOCAL_STATE_ONLY warning MUST NOT downgrade the intrinsic PostgreSQL
requirement; removal with local state is an error.
Planner safety blocker codes are
connector_removal_state_provider_drift and
connector_removal_ownership_required. Both classify the plan as blocked and
destructive authority is never evaluated as a bypass.
Secrecy and bounded output¶
The following MUST NOT appear in YAML validation errors, compiled removal artifacts, reviewed plans, operation control/history, recovery plans, CLI output, logs, telemetry, or test snapshots:
- raw connector config values;
- passwords, tokens, JAAS strings, secret-provider expansions, or credentials;
- Connect REST endpoints, request headers, TLS paths, or response bodies;
- PostgreSQL DSNs, roles, schemas, lock keys, or connection details; or
- exception text not passed through the established sanitizer.
Allowed evidence is limited to logical resource ID, connector name, cluster alias, canonical backend identity containing only an endpoint digest, artifact checksum, observation fingerprints, presence, action, stable categories, and fixed error codes. Provider bodies and exception text have independent hard bounds; plan and assessment size is bounded by the 256-declaration project limit.
Version and compatibility contract¶
- Project
apiVersionremainsstreamt.dev/v1alpha1; the lifecycle field is additive and strict. - Compiled manifest format adds
connector_removalsonly when non-empty. - Ownership
CURRENT_STATE_VERSIONremains 1. - PostgreSQL deployment-state schema remains version 2; no DDL migration is required.
- Reviewed
PLAN_FILE_VERSIONbecomes 5 and versions 1 through 4 require regeneration. CURRENT_CONTROL_VERSIONbecomes 3. Every reader explicitly accepts 1, 2, and 3; clear v2 state remains valid for ordinary commands and upgrades only on write, while active v1/v2 operations retain legacy recovery semantics.RECOVERY_PLAN_FILE_VERSIONbecomes 3. Existing version-½ plans remain readable only under their original action semantics and cannot represent a Connector removal.- The canonical Connect binding remains version 1.
A legacy backend: kafka-connect record is never upgraded implicitly and has
no supported in-place migration in this slice. Re-apply and adoption both fail
closed for that record. A separate reviewed state-reconciliation contract is
required before it can become canonical; no state-only forget operation is
included.
Hard deferrals¶
The following deletion kinds require separate normative specifications:
- Kafka topics: deletion destroys retained records and needs complete consumer-group, downstream graph, protection, and asynchronous broker postcondition semantics.
- Schema Registry subjects: deletion needs explicit soft versus permanent modes, version/reference closure, compatibility-state ownership, and multi-subject ordering.
- Flink jobs: cancellation is deferred until stable job identity, exact SQL and execution-setting evidence, cluster routing, savepoint policy, and recoverable cancellation postconditions exist.
Connector removal MUST NOT add generic deletion declarations or state projection capable of activating any of these kinds.
Release acceptance¶
The feature is releasable only when strict DSL, compilation, pure preflight, plan/action/control serialization, managed mutation, state projection, recovery, PostgreSQL concurrency, secrecy, installed-wheel, and real Connect tests all pass. Documentation MUST continue to state that manifest absence is inert and that only Connector deletion is added by this contract.