This is an automated email from the ASF dual-hosted git repository.
kevdoran pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git
The following commit(s) were added to refs/heads/main by this push:
new e54dc5ec15d NIFI-16242: Inherit default values for newly added
Connector properties (#11581)
e54dc5ec15d is described below
commit e54dc5ec15dec122c30d38c7ddf8d59d1acbd6bf
Author: Mark Payne <[email protected]>
AuthorDate: Tue Aug 25 10:46:20 2026 -0400
NIFI-16242: Inherit default values for newly added Connector properties
(#11581)
---
.../connector/StandardConnectorNode.java | 48 ++-
.../connector/TestStandardConnectorNode.java | 409 ++++++++++++++++++++-
2 files changed, 452 insertions(+), 5 deletions(-)
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/connector/StandardConnectorNode.java
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/connector/StandardConnectorNode.java
index d5c1e3fcf0d..552a95f90ea 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/connector/StandardConnectorNode.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/connector/StandardConnectorNode.java
@@ -78,6 +78,7 @@ import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -419,11 +420,56 @@ public class StandardConnectorNode implements
ConnectorNode, GroupedComponent {
initial.put(versionedConfigStep.getName(), new
StepConfiguration(toValueReferenceMap(versionedConfigStep)));
}
+ final Set<String> persistedStepNames = new
LinkedHashSet<>(initial.keySet());
final StandardConnectorPropertyConfiguration propertyConfiguration =
new StandardConnectorPropertyConfiguration(initial, this.toString());
try (final NarCloseable ignored =
NarCloseable.withComponentNarLoader(extensionManager,
getConnector().getClass(), getIdentifier())) {
getConnector().migrateProperties(propertyConfiguration);
+ return
applyMissingRequiredPropertyDefaults(propertyConfiguration.getMutatedProperties(),
persistedStepNames, getConnector().getConfigurationSteps());
}
- return propertyConfiguration.getMutatedProperties();
+ }
+
+ /**
+ * Fills in the default value for any required property that has no value
in the migrated configuration, so a NAR
+ * upgrade that adds a required property with a default does not make the
Connector invalid. A step the Connector
+ * removed during migration (present in {@code persistedStepNames} but
absent from {@code migratedProperties}) is
+ * not re-created; a declared step in neither is newly added by this
version and is created, but only if at least
+ * one required default applies to it.
+ */
+ private Map<String, StepConfiguration>
applyMissingRequiredPropertyDefaults(final Map<String, StepConfiguration>
migratedProperties,
+ final Set<String> persistedStepNames, final
List<ConfigurationStep> configurationSteps) {
+ if (configurationSteps == null || configurationSteps.isEmpty()) {
+ return migratedProperties;
+ }
+
+ final Map<String, StepConfiguration> propertiesWithDefaults = new
LinkedHashMap<>(migratedProperties);
+ for (final ConfigurationStep configurationStep : configurationSteps) {
+ final String stepName = configurationStep.getName();
+ final StepConfiguration existingConfiguration =
propertiesWithDefaults.get(stepName);
+ if (existingConfiguration == null &&
persistedStepNames.contains(stepName)) {
+ continue;
+ }
+
+ final Map<String, ConnectorValueReference> existingValues =
existingConfiguration == null ? null :
existingConfiguration.getPropertyValues();
+ final Map<String, ConnectorValueReference> propertyValues =
existingValues == null ? new LinkedHashMap<>() : new
LinkedHashMap<>(existingValues);
+ boolean appliedMissingDefault = false;
+ for (final ConnectorPropertyGroup propertyGroup :
configurationStep.getPropertyGroups()) {
+ for (final ConnectorPropertyDescriptor descriptor :
propertyGroup.getProperties()) {
+ if (!descriptor.isRequired() ||
descriptor.getDefaultValue() == null ||
propertyValues.containsKey(descriptor.getName())) {
+ continue;
+ }
+
+ propertyValues.put(descriptor.getName(), new
StringLiteralValue(descriptor.getDefaultValue()));
+ appliedMissingDefault = true;
+ logger.debug("Applied default value for required property
[{}] of configuration step [{}] on {}", descriptor.getName(), stepName, this);
+ }
+ }
+
+ if (appliedMissingDefault) {
+ propertiesWithDefaults.put(stepName, new
StepConfiguration(propertyValues));
+ }
+ }
+
+ return propertiesWithDefaults;
}
private Map<String, ConnectorValueReference> toValueReferenceMap(final
VersionedConfigurationStep step) {
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/components/connector/TestStandardConnectorNode.java
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/components/connector/TestStandardConnectorNode.java
index 545ba41a9b9..22a148bb709 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/components/connector/TestStandardConnectorNode.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/components/connector/TestStandardConnectorNode.java
@@ -37,9 +37,12 @@ import org.apache.nifi.controller.queue.QueueSize;
import org.apache.nifi.controller.state.StandardStateMap;
import org.apache.nifi.engine.FlowEngine;
import org.apache.nifi.flow.Bundle;
+import org.apache.nifi.flow.VersionedConfigurationStep;
+import org.apache.nifi.flow.VersionedConnectorValueReference;
import org.apache.nifi.flow.VersionedExternalFlow;
import org.apache.nifi.groups.ProcessGroup;
import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.migration.ConnectorPropertyConfiguration;
import org.apache.nifi.nar.ExtensionManager;
import org.apache.nifi.util.MockComponentLog;
import org.junit.jupiter.api.AfterEach;
@@ -1108,6 +1111,131 @@ public class TestStandardConnectorNode {
assertTrue(node.isModified());
}
+ @Test
+ public void
testVerifyCanStartAfterInheritingConfigurationMissingRequiredPropertyWithDefault()
throws FlowUpdateException {
+ final DefaultValueConnector connector = new DefaultValueConnector();
+ final StandardConnectorNode connectorNode =
createConnectorNode(connector);
+
+ final VersionedConnectorValueReference greetingReference = new
VersionedConnectorValueReference();
+ greetingReference.setValueType("STRING_LITERAL");
+ greetingReference.setValue("Welcome");
+
+ final VersionedConfigurationStep persistedStep = new
VersionedConfigurationStep();
+ persistedStep.setName("settings");
+ persistedStep.setProperties(Map.of("Greeting", greetingReference));
+
+ connectorNode.transitionStateForUpdating();
+ connectorNode.prepareForUpdate();
+ connectorNode.inheritConfiguration(List.of(persistedStep),
List.of(persistedStep), createConnectorBundle());
+
+ connectorNode.verifyCanStart();
+ assertEquals("Welcome",
connectorNode.getActiveFlowContext().getConfigurationContext().getProperty("settings",
"Greeting").getValue());
+ assertEquals("1",
connectorNode.getActiveFlowContext().getConfigurationContext().getProperty("settings",
"Repeat Count").getValue());
+ }
+
+ @Test
+ public void
testInheritingConfigurationDoesNotApplyOptionalPropertyDefault() throws
FlowUpdateException {
+ final DependentDefaultValueConnector connector = new
DependentDefaultValueConnector();
+ final StandardConnectorNode connectorNode =
createConnectorNode(connector);
+
+ final VersionedConfigurationStep persistedStep = new
VersionedConfigurationStep();
+ persistedStep.setName("settings");
+ persistedStep.setProperties(Map.of());
+
+ connectorNode.transitionStateForUpdating();
+ connectorNode.prepareForUpdate();
+ connectorNode.inheritConfiguration(List.of(persistedStep),
List.of(persistedStep), createConnectorBundle());
+
+ // "SSL Mode" is optional, so its default must not be inserted. If it
were, the "REQUIRED" default would
+ // satisfy the dependency of "Truststore Filename" and make that
required property report as missing.
+
assertFalse(connectorNode.getActiveFlowContext().getConfigurationContext().getPropertyNames("settings").contains("SSL
Mode"));
+ connectorNode.verifyCanStart();
+ }
+
+ @Test
+ public void
testInheritingConfigurationCreatesNewlyDeclaredStepWithRequiredDefaults()
throws FlowUpdateException {
+ final DeclaredStepRecordingConnector connector = new
DeclaredStepRecordingConnector();
+ final StandardConnectorNode connectorNode =
createConnectorNode(connector);
+
+ final VersionedConnectorValueReference greetingReference = new
VersionedConnectorValueReference();
+ greetingReference.setValueType("STRING_LITERAL");
+ greetingReference.setValue("Welcome");
+
+ final VersionedConfigurationStep persistedStep = new
VersionedConfigurationStep();
+ persistedStep.setName("settings");
+ persistedStep.setProperties(Map.of("Greeting", greetingReference));
+
+ connectorNode.transitionStateForUpdating();
+ connectorNode.prepareForUpdate();
+ connectorNode.inheritConfiguration(List.of(persistedStep),
List.of(persistedStep), createConnectorBundle());
+
+ // The "extra" step is newly declared by this version of the Connector
and was never persisted, so it is
+ // created with its required default and its configuration callback
fires under its new name.
+ assertTrue(connector.getConfiguredStepNames().contains("extra"));
+ assertEquals("default",
connectorNode.getActiveFlowContext().getConfigurationContext().getProperty("extra",
"Extra Property").getValue());
+ connectorNode.verifyCanStart();
+ }
+
+ @Test
+ public void
testInheritingConfigurationDoesNotRecreateStepRemovedDuringMigration() throws
FlowUpdateException {
+ final LegacyStepRemovingConnector connector = new
LegacyStepRemovingConnector();
+ final StandardConnectorNode connectorNode =
createConnectorNode(connector);
+
+ final VersionedConnectorValueReference greetingReference = new
VersionedConnectorValueReference();
+ greetingReference.setValueType("STRING_LITERAL");
+ greetingReference.setValue("Welcome");
+
+ final VersionedConnectorValueReference legacyReference = new
VersionedConnectorValueReference();
+ legacyReference.setValueType("STRING_LITERAL");
+ legacyReference.setValue("retained");
+
+ final VersionedConfigurationStep settingsStep = new
VersionedConfigurationStep();
+ settingsStep.setName("settings");
+ settingsStep.setProperties(Map.of("Greeting", greetingReference));
+
+ final VersionedConfigurationStep legacyStep = new
VersionedConfigurationStep();
+ legacyStep.setName("legacy");
+ legacyStep.setProperties(Map.of("Legacy Property", legacyReference));
+
+ connectorNode.transitionStateForUpdating();
+ connectorNode.prepareForUpdate();
+ connectorNode.inheritConfiguration(List.of(settingsStep, legacyStep),
List.of(settingsStep, legacyStep), createConnectorBundle());
+
+ // The Connector removed the persisted "legacy" step during migration.
Even though it still declares the step
+ // and the step's required property has a default, the step must not
be re-created and its callback must not fire.
+ assertFalse(connector.getConfiguredStepNames().contains("legacy"));
+
assertTrue(connectorNode.getActiveFlowContext().getConfigurationContext().getPropertyNames("legacy").isEmpty());
+ }
+
+ @Test
+ public void
testInheritingConfigurationKeepsTransitivelyGatedRequiredPropertyIrrelevant()
throws FlowUpdateException {
+ final TransitiveDependencyConnector connector = new
TransitiveDependencyConnector();
+ final StandardConnectorNode connectorNode =
createConnectorNode(connector);
+
+ final VersionedConfigurationStep persistedStep = new
VersionedConfigurationStep();
+ persistedStep.setName("settings");
+ persistedStep.setProperties(Map.of());
+
+ connectorNode.transitionStateForUpdating();
+ connectorNode.prepareForUpdate();
+ connectorNode.inheritConfiguration(List.of(persistedStep),
List.of(persistedStep), createConnectorBundle());
+
+ // "Username" is required with a default, so the back-fill
materializes it even though it is gated off by the
+ // unset "Authentication". "Password" is required with no default and
depends on "Username". If dependency
+ // evaluation is transitive, "Password" stays gated off because
"Authentication" is unset, so materializing
+ // "Username" does not make "Password" required and the Connector
remains startable.
+ assertEquals("admin",
connectorNode.getActiveFlowContext().getConfigurationContext().getProperty("settings",
"Username").getValue());
+ connectorNode.verifyCanStart();
+ }
+
+ private static Bundle createConnectorBundle() {
+ final Bundle bundle = new Bundle();
+ bundle.setGroup("org.apache.nifi");
+ bundle.setArtifact("test-bundle");
+ bundle.setVersion("1.0.0");
+ return bundle;
+ }
+
private static void seedActiveConfiguration(final StandardConnectorNode
node, final String stepName, final Map<String, ConnectorValueReference>
properties) {
node.getActiveFlowContext().getConfigurationContext().setProperties(stepName,
new StepConfiguration(properties));
}
@@ -1187,6 +1315,7 @@ public class TestStandardConnectorNode {
final FrameworkConnectorInitializationContext initializationContext =
mock(FrameworkConnectorInitializationContext.class);
when(initializationContext.getSecretsManager()).thenReturn(initializedSecretsManager);
+ when(initializationContext.getAssetManager()).thenReturn(assetManager);
node.initializeConnector(initializationContext);
node.loadInitialFlow();
@@ -1502,10 +1631,6 @@ public class TestStandardConnectorNode {
}
}
- /**
- * Test connector declaring a single configuration step with two
properties that have default values. Used to
- * exercise the configuration-versus-default comparison performed by
{@code isModified()}.
- */
private static class DefaultValueConnector extends AbstractConnector {
@Override
public VersionedExternalFlow getInitialFlow() {
@@ -1565,6 +1690,282 @@ public class TestStandardConnectorNode {
}
}
+ private static class DependentDefaultValueConnector extends
AbstractConnector {
+ @Override
+ public VersionedExternalFlow getInitialFlow() {
+ return null;
+ }
+
+ @Override
+ public VersionedExternalFlow getActiveFlow(final FlowContext
activeFlowContext) {
+ return null;
+ }
+
+ @Override
+ public void prepareForUpdate(final FlowContext workingContext, final
FlowContext activeContext) {
+ }
+
+ @Override
+ public List<ConfigurationStep> getConfigurationSteps() {
+ final ConnectorPropertyDescriptor sslMode = new
ConnectorPropertyDescriptor.Builder()
+ .name("SSL Mode")
+ .description("Whether SSL is required")
+ .required(false)
+ .defaultValue("REQUIRED")
+ .build();
+
+ final ConnectorPropertyDescriptor truststoreFilename = new
ConnectorPropertyDescriptor.Builder()
+ .name("Truststore Filename")
+ .description("Location of the truststore")
+ .required(true)
+ .dependsOn(sslMode, "REQUIRED")
+ .build();
+
+ final ConnectorPropertyGroup propertyGroup =
ConnectorPropertyGroup.builder()
+ .name("Security")
+ .description("Security settings")
+ .properties(List.of(sslMode, truststoreFilename))
+ .build();
+
+ final ConfigurationStep step = new ConfigurationStep.Builder()
+ .name("settings")
+ .propertyGroups(List.of(propertyGroup))
+ .build();
+
+ return List.of(step);
+ }
+
+ @Override
+ public void applyUpdate(final FlowContext workingContext, final
FlowContext activeContext) {
+ }
+
+ @Override
+ protected void onStepConfigured(final String stepName, final
FlowContext workingContext) {
+ }
+
+ @Override
+ public List<ConfigVerificationResult> verifyConfigurationStep(final
String stepName, final Map<String, String> overrides, final FlowContext
flowContext) {
+ return List.of();
+ }
+ }
+
+ private static class DeclaredStepRecordingConnector extends
AbstractConnector {
+ private final Set<String> configuredStepNames = new HashSet<>();
+
+ @Override
+ public VersionedExternalFlow getInitialFlow() {
+ return null;
+ }
+
+ @Override
+ public VersionedExternalFlow getActiveFlow(final FlowContext
activeFlowContext) {
+ return null;
+ }
+
+ @Override
+ public void prepareForUpdate(final FlowContext workingContext, final
FlowContext activeContext) {
+ }
+
+ @Override
+ public List<ConfigurationStep> getConfigurationSteps() {
+ final ConnectorPropertyDescriptor greeting = new
ConnectorPropertyDescriptor.Builder()
+ .name("Greeting")
+ .description("Greeting text")
+ .required(true)
+ .defaultValue("Hello")
+ .build();
+
+ final ConfigurationStep settings = new ConfigurationStep.Builder()
+ .name("settings")
+ .propertyGroups(List.of(ConnectorPropertyGroup.builder()
+ .name("General")
+ .description("General settings")
+ .properties(List.of(greeting))
+ .build()))
+ .build();
+
+ final ConnectorPropertyDescriptor extraProperty = new
ConnectorPropertyDescriptor.Builder()
+ .name("Extra Property")
+ .description("Property added by a newer version of the
Connector")
+ .required(true)
+ .defaultValue("default")
+ .build();
+
+ final ConfigurationStep extra = new ConfigurationStep.Builder()
+ .name("extra")
+ .propertyGroups(List.of(ConnectorPropertyGroup.builder()
+ .name("Extra Group")
+ .description("Group added by a newer version of the
Connector")
+ .properties(List.of(extraProperty))
+ .build()))
+ .build();
+
+ return List.of(settings, extra);
+ }
+
+ @Override
+ public void applyUpdate(final FlowContext workingContext, final
FlowContext activeContext) {
+ }
+
+ @Override
+ protected void onStepConfigured(final String stepName, final
FlowContext workingContext) {
+ configuredStepNames.add(stepName);
+ }
+
+ @Override
+ public List<ConfigVerificationResult> verifyConfigurationStep(final
String stepName, final Map<String, String> overrides, final FlowContext
flowContext) {
+ return List.of();
+ }
+
+ Set<String> getConfiguredStepNames() {
+ return configuredStepNames;
+ }
+ }
+
+ private static class LegacyStepRemovingConnector extends AbstractConnector
{
+ private final Set<String> configuredStepNames = new HashSet<>();
+
+ @Override
+ public VersionedExternalFlow getInitialFlow() {
+ return null;
+ }
+
+ @Override
+ public VersionedExternalFlow getActiveFlow(final FlowContext
activeFlowContext) {
+ return null;
+ }
+
+ @Override
+ public void prepareForUpdate(final FlowContext workingContext, final
FlowContext activeContext) {
+ }
+
+ @Override
+ public void migrateProperties(final ConnectorPropertyConfiguration
configuration) {
+ configuration.removeStep("legacy");
+ }
+
+ @Override
+ public List<ConfigurationStep> getConfigurationSteps() {
+ final ConnectorPropertyDescriptor greeting = new
ConnectorPropertyDescriptor.Builder()
+ .name("Greeting")
+ .description("Greeting text")
+ .required(true)
+ .defaultValue("Hello")
+ .build();
+
+ final ConfigurationStep settings = new ConfigurationStep.Builder()
+ .name("settings")
+ .propertyGroups(List.of(ConnectorPropertyGroup.builder()
+ .name("General")
+ .description("General settings")
+ .properties(List.of(greeting))
+ .build()))
+ .build();
+
+ final ConnectorPropertyDescriptor legacyProperty = new
ConnectorPropertyDescriptor.Builder()
+ .name("Legacy Property")
+ .description("Property of a step removed during migration")
+ .required(true)
+ .defaultValue("old")
+ .build();
+
+ final ConfigurationStep legacy = new ConfigurationStep.Builder()
+ .name("legacy")
+ .propertyGroups(List.of(ConnectorPropertyGroup.builder()
+ .name("Legacy Group")
+ .description("Group of a step removed during migration")
+ .properties(List.of(legacyProperty))
+ .build()))
+ .build();
+
+ return List.of(settings, legacy);
+ }
+
+ @Override
+ public void applyUpdate(final FlowContext workingContext, final
FlowContext activeContext) {
+ }
+
+ @Override
+ protected void onStepConfigured(final String stepName, final
FlowContext workingContext) {
+ configuredStepNames.add(stepName);
+ }
+
+ @Override
+ public List<ConfigVerificationResult> verifyConfigurationStep(final
String stepName, final Map<String, String> overrides, final FlowContext
flowContext) {
+ return List.of();
+ }
+
+ Set<String> getConfiguredStepNames() {
+ return configuredStepNames;
+ }
+ }
+
+ private static class TransitiveDependencyConnector extends
AbstractConnector {
+ @Override
+ public VersionedExternalFlow getInitialFlow() {
+ return null;
+ }
+
+ @Override
+ public VersionedExternalFlow getActiveFlow(final FlowContext
activeFlowContext) {
+ return null;
+ }
+
+ @Override
+ public void prepareForUpdate(final FlowContext workingContext, final
FlowContext activeContext) {
+ }
+
+ @Override
+ public List<ConfigurationStep> getConfigurationSteps() {
+ final ConnectorPropertyDescriptor authentication = new
ConnectorPropertyDescriptor.Builder()
+ .name("Authentication")
+ .description("Authentication mode")
+ .required(false)
+ .build();
+
+ final ConnectorPropertyDescriptor username = new
ConnectorPropertyDescriptor.Builder()
+ .name("Username")
+ .description("Username used for authentication")
+ .required(true)
+ .defaultValue("admin")
+ .dependsOn(authentication, "Basic")
+ .build();
+
+ final ConnectorPropertyDescriptor password = new
ConnectorPropertyDescriptor.Builder()
+ .name("Password")
+ .description("Password used for authentication")
+ .required(true)
+ .dependsOn(username)
+ .build();
+
+ final ConnectorPropertyGroup propertyGroup =
ConnectorPropertyGroup.builder()
+ .name("Security")
+ .description("Security settings")
+ .properties(List.of(authentication, username, password))
+ .build();
+
+ final ConfigurationStep step = new ConfigurationStep.Builder()
+ .name("settings")
+ .propertyGroups(List.of(propertyGroup))
+ .build();
+
+ return List.of(step);
+ }
+
+ @Override
+ public void applyUpdate(final FlowContext workingContext, final
FlowContext activeContext) {
+ }
+
+ @Override
+ protected void onStepConfigured(final String stepName, final
FlowContext workingContext) {
+ }
+
+ @Override
+ public List<ConfigVerificationResult> verifyConfigurationStep(final
String stepName, final Map<String, String> overrides, final FlowContext
flowContext) {
+ return List.of();
+ }
+ }
+
/**
* Test connector that allows control over when drainFlowFiles completes
via a CompletableFuture
*/