Bob Paulin created NIP-36:
-----------------------------
Summary: Add immutable option to ConnectorPropertyDescriptor
Key: NIP-36
URL: https://issues.apache.org/jira/browse/NIP-36
Project: NiFi Improvement Proposal
Issue Type: Improvement
Reporter: Bob Paulin
h2. Motivation
Some Connector configuration properties describe decisions that cannot be
safely changed after the Connector has started producing data. Typical examples:
* The output format written to a destination. Once objects have been written
to an S3 bucket as Parquet, silently switching the property to Avro produces a
bucket whose contents can no longer be read by a single reader.
* The physical destination identifier. The S3 bucket, Kafka topic, or
object-store prefix that the Connector writes into. Changing this after data
has been written silently forks the destination and leaves the previously
written data orphaned.
* The identity used to track source progress. The Kafka consumer group id or
source offset-tracking key. Changing this after commit either re-reads
already-processed data or leaves committed offsets behind, in both cases
producing duplicates or gaps.
Today there is no first-class way for a Connector author to declare that such a
property is frozen after the Connector has started. The intent lives only in ad
hoc validation code inside each Connector, is not exposed to the REST API, is
not visible in the extension manifest, and is not visible in the UI. Users can
freely edit these fields in the configuration dialog and see a generic error
only when they attempt to apply the change, if the Connector happens to defend
it at all.
This proposal makes _"once the Connector has been started at least once, this
property is frozen at its current value"_ a first-class descriptor attribute,
enforced by the framework, surfaced through the REST API and extension
manifest, and rendered as read-only in the UI so the user never has the
opportunity to make an unappliable change in the first place.
h2. Scope
Out of Scope:
Immutable Properties for Processors or Controller Services are not part of the
scope of this NIP. Processors or Controller Services have done without this
feature and have implemented ways to handle property value changes within the
component code. Connectors integrate an entire flow worth of components
together so trying to force connectors to handle any possible connector
property change may not always be possible.
{{nifi-api}} additions:
* {{ConnectorPropertyDescriptor.isImmutable()}} and
{{{}ConnectorPropertyDescriptor.Builder.immutable(boolean){}}}, defaulting to
{{{}false{}}}. {{Builder.from(other)}} copies the flag.
* {{VersionedConnector.hasBeenStarted}} ({{{}boolean{}}}) with getter/setter,
serialized alongside the Connector's other flow-level metadata.
* {{XmlConnectorDocumentationWriter.writeConnectorProperty(...)}} emits a new
{{<immutable>}} element so the extension manifest exposes the flag.
Framework additions:
* New methods on {{{}org.apache.nifi.components.connector.ConnectorNode{}}}:
{{boolean hasBeenStarted()}} and {{{}void setHasBeenStarted(boolean){}}}.
* {{StandardConnectorNode}} sets {{hasBeenStarted = true}} after every
successful transition to {{{}RUNNING{}}}, and rejects changes to any immutable
property in {{setConfiguration(...)}} and {{replaceWorkingConfiguration(...)}}
once the flag is set. Comparison is done on the raw {{ConnectorValueReference}}
instances so asset and secret references are compared by identity, not by their
resolved values.
* {{VersionedComponentFlowMapper.mapConnector(...)}} persists the flag;
{{StandardConnectorRepository.createConnectorNode(VersionedConnector)}}
restores it via {{{}ConnectorNode.setHasBeenStarted(...){}}}. The flag survives
restarts, flow imports, and Connector-version upgrades.
Model / DTO / manifest plumbing:
* {{ConnectorPropertyDescriptorDTO}} (nifi-client-dto) gains an {{immutable}}
field, populated by
{{{}DtoFactory.createConnectorPropertyDescriptorDto(...){}}}.
* {{ConnectorDTO}} (nifi-client-dto) gains a {{hasBeenStarted}} field,
populated by {{DtoFactory}} from {{{}ConnectorNode.hasBeenStarted(){}}}, so
that any client can determine whether a Connector's immutable properties are
currently locked.
* {{org.apache.nifi.extension.manifest.ConnectorProperty}} and
{{org.apache.nifi.c2.protocol.component.api.ConnectorPropertyDescriptor}} gain
the same {{immutable}} field;
{{StandardRuntimeManifestBuilder.getConnectorPropertyDescriptor(...)}} copies
it through.
UI additions ({{{}nifi-frontend{}}}):
* The Connector property-editor components consume {{descriptor.immutable}}
together with {{{}connector.hasBeenStarted{}}}. When both are true, the
corresponding form control is rendered as disabled and no editable value
control is offered.
* A locked visual indicator (padlock icon plus tooltip text) is displayed next
to the disabled control, explaining that the property was fixed at the value
from the Connector's first successful start and cannot be changed..
h2. Description
Descriptor contract.
A property built with {{.immutable(true)}} behaves identically to a
non-immutable property until the Connector transitions to {{RUNNING}} for the
first time. Attempts to change it before that point succeed, which preserves
normal initial configuration, verification, and staged flow deployment.
Immutability begins the moment {{StandardConnectorNode}} sets
{{{}hasBeenStarted = true{}}}, and from then on the value locked in is whatever
was in the active configuration at the point of transition.
Enforcement point.
The check lives in a single private helper —
{{StandardConnectorNode.verifyImmutablePropertiesUnchanged(stepName, incoming,
replace)}} — called from the two mutation entry points a user request can reach:
* {{setConfiguration(String, StepConfiguration)}} — the merge path used by
REST configuration updates and by {{{}ConnectorTestRunner.configure(...){}}}.
* {{replaceWorkingConfiguration(String, StepConfiguration)}} — the whole-step
replacement path used by configuration providers.
Restore paths (flow deserialization, snapshot inheritance,
{{{}recreateWorkingFlowContext{}}}) deliberately bypass the check because they
must be able to reconstruct the Connector's existing state without the
framework second-guessing itself. A violating change throws
{{FlowUpdateException}} with a message that names the offending property, the
step, and the Connector.
Persistence.
{{hasBeenStarted}} is stored on {{{}VersionedConnector{}}}, not on the
descriptor. On save, {{VersionedComponentFlowMapper}} reads
{{{}connectorNode.hasBeenStarted(){}}}. On load,
{{StandardConnectorRepository}} reads the field off the {{VersionedConnector}}
and calls {{setHasBeenStarted(true)}} before the Connector is exposed to any
user request, so enforcement is active immediately after restart. Because the
flag lives on the Connector and not on the descriptor, values locked under
Connector v1 remain locked under Connector v2 even if v2 changes the descriptor
set: whether a property is immutable is decided by the current v2 descriptors,
and _"the Connector has been started"_ is decided by the persisted flag.
Backwards compatibility.
{{isImmutable()}} defaults to {{{}false{}}}, so existing Connectors are
unaffected. {{VersionedConnector.hasBeenStarted}} defaults to {{false}} in the
JSON representation, so flows saved by older frameworks come back unlocked,
which is the same behaviour as today. The new DTO / manifest / c2 fields are
additive, and building an older Connector against the new {{nifi-api}}
continues to compile because both new descriptor methods have default
implementations.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)