This is an automated email from the ASF dual-hosted git repository.
exceptionfactory 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 4ca22f02c1 NIFI-14985 Handle Removed Property Descriptor as an
Environmental Change (#10317)
4ca22f02c1 is described below
commit 4ca22f02c167af975c1b258a2af11a1deeb73d16
Author: Pierre Villard <[email protected]>
AuthorDate: Thu Sep 18 15:45:15 2025 +0200
NIFI-14985 Handle Removed Property Descriptor as an Environmental Change
(#10317)
Signed-off-by: David Handermann <[email protected]>
---
.../apache/nifi/util/FlowDifferenceFilters.java | 77 ++++++++++++++++++-
.../nifi/util/TestFlowDifferenceFilters.java | 89 ++++++++++++++++++++++
.../apache/nifi/web/StandardNiFiServiceFacade.java | 1 +
3 files changed, 166 insertions(+), 1 deletion(-)
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/util/FlowDifferenceFilters.java
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/util/FlowDifferenceFilters.java
index 09f46b9444..8e917a9058 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/util/FlowDifferenceFilters.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/util/FlowDifferenceFilters.java
@@ -16,6 +16,9 @@
*/
package org.apache.nifi.util;
+import org.apache.nifi.annotation.behavior.DynamicProperties;
+import org.apache.nifi.annotation.behavior.DynamicProperty;
+import org.apache.nifi.components.ConfigurableComponent;
import org.apache.nifi.components.PropertyDescriptor;
import org.apache.nifi.controller.ComponentNode;
import org.apache.nifi.controller.ProcessorNode;
@@ -70,7 +73,8 @@ public class FlowDifferenceFilters {
|| isNewZIndexConnectionConfigWithDefaultValue(difference,
flowManager)
|| isRegistryUrlChange(difference)
|| isParameterContextChange(difference)
- || isLogFileSuffixChange(difference);
+ || isLogFileSuffixChange(difference)
+ || isStaticPropertyRemoved(difference, flowManager);
}
private static boolean isSensitivePropertyDueToGhosting(final
FlowDifference difference, final FlowManager flowManager) {
@@ -108,6 +112,16 @@ public class FlowDifferenceFilters {
}
+ private static boolean supportsDynamicProperties(final
ConfigurableComponent component, final String propertyName) {
+ final PropertyDescriptor descriptor =
component.getPropertyDescriptor(propertyName);
+ if (descriptor != null && descriptor.isDynamic()) {
+ return true;
+ }
+
+ final Class<?> componentClass = component.getClass();
+ return componentClass.isAnnotationPresent(DynamicProperty.class) ||
componentClass.isAnnotationPresent(DynamicProperties.class);
+ }
+
// The Registry URL may change if, for instance, a registry is moved to a
new host, or is made secure, the port changes, etc.
// Since this can be handled by the client anyway, there's no need to flag
this as a 'local modification'
private static boolean isRegistryUrlChange(final FlowDifference
difference) {
@@ -406,6 +420,67 @@ public class FlowDifferenceFilters {
return value == null ? replacement : value;
}
+ /**
+ * Determines whether a property difference is caused by a statically
defined property being removed from the component definition.
+ * When a processor or controller service drops a property (for example,
as part of a version upgrade that invokes {@code removeProperty}
+ * during migration), the reconciled component in NiFi should not report a
"local change" so long as the component does not support
+ * dynamic properties.
+ *
+ * @param difference the flow difference under evaluation
+ * @param flowManager the flow manager used to resolve instantiated
components
+ * @return {@code true} if the property is no longer exposed by the
component definition and the component does not support dynamic
+ * properties; {@code false} otherwise
+ */
+ public static boolean isStaticPropertyRemoved(final FlowDifference
difference, final FlowManager flowManager) {
+ final DifferenceType differenceType = difference.getDifferenceType();
+ if (differenceType != DifferenceType.PROPERTY_REMOVED) {
+ return false;
+ }
+
+ final Optional<String> fieldName = difference.getFieldName();
+ if (!fieldName.isPresent()) {
+ return false;
+ }
+
+ final VersionedComponent componentB = difference.getComponentB();
+
+ if (componentB instanceof InstantiatedVersionedProcessor) {
+ final InstantiatedVersionedProcessor instantiatedProcessor =
(InstantiatedVersionedProcessor) componentB;
+ final ProcessorNode processorNode =
flowManager.getProcessorNode(instantiatedProcessor.getInstanceIdentifier());
+ return isStaticPropertyRemoved(fieldName.get(), processorNode);
+ } else if (componentB instanceof
InstantiatedVersionedControllerService) {
+ final InstantiatedVersionedControllerService
instantiatedControllerService = (InstantiatedVersionedControllerService)
componentB;
+ final ControllerServiceNode controllerService =
flowManager.getControllerServiceNode(instantiatedControllerService.getInstanceIdentifier());
+ return isStaticPropertyRemoved(fieldName.get(), controllerService);
+ }
+
+ return false;
+ }
+
+ private static boolean isStaticPropertyRemoved(String propertyName,
ComponentNode componentNode) {
+ if (componentNode == null) {
+ return false;
+ }
+
+ final ConfigurableComponent configurableComponent =
componentNode.getComponent();
+ if (configurableComponent == null) {
+ return false;
+ }
+
+ final boolean staticallyDefined =
configurableComponent.getPropertyDescriptors().stream()
+ .map(PropertyDescriptor::getName)
+ .anyMatch(propertyName::equals);
+ if (staticallyDefined) {
+ return false;
+ }
+
+ if (supportsDynamicProperties(configurableComponent, propertyName)) {
+ return false;
+ }
+
+ return true;
+ }
+
/**
* If a property is removed from a ghosted component, we may want to
ignore it. This is because all properties will be considered sensitive for
* a ghosted component and as a result, the property map may not be
populated with its property value, resulting in an indication that the property
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/util/TestFlowDifferenceFilters.java
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/util/TestFlowDifferenceFilters.java
index 7868979685..88679e74b5 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/util/TestFlowDifferenceFilters.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/util/TestFlowDifferenceFilters.java
@@ -16,6 +16,10 @@
*/
package org.apache.nifi.util;
+import org.apache.nifi.components.ConfigurableComponent;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.controller.ProcessorNode;
+import org.apache.nifi.controller.flow.FlowManager;
import org.apache.nifi.flow.ComponentType;
import org.apache.nifi.flow.ScheduledState;
import org.apache.nifi.flow.VersionedControllerService;
@@ -25,9 +29,12 @@ import org.apache.nifi.flow.VersionedRemoteGroupPort;
import org.apache.nifi.registry.flow.diff.DifferenceType;
import org.apache.nifi.registry.flow.diff.FlowDifference;
import org.apache.nifi.registry.flow.diff.StandardFlowDifference;
+import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedProcessor;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
+import java.util.List;
+
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -155,4 +162,86 @@ public class TestFlowDifferenceFilters {
// Should not throw and should return false since no local component
assertFalse(FlowDifferenceFilters.isLocalScheduleStateChange(flowDifference));
}
+
+ @Test
+ public void testIsStaticPropertyRemovedFromDefinitionWhenPropertyDropped()
{
+ final FlowManager flowManager = Mockito.mock(FlowManager.class);
+ final ProcessorNode processorNode = Mockito.mock(ProcessorNode.class);
+ final ConfigurableComponent configurableComponent =
Mockito.mock(ConfigurableComponent.class);
+
+ final String propertyName = "Obsolete Property";
+ final String instanceId = "processor-instance";
+
+
Mockito.when(flowManager.getProcessorNode(instanceId)).thenReturn(processorNode);
+
Mockito.when(processorNode.getComponent()).thenReturn(configurableComponent);
+
Mockito.when(configurableComponent.getPropertyDescriptors()).thenReturn(List.of(new
PropertyDescriptor.Builder().name("Retained Property").build()));
+
Mockito.when(configurableComponent.getPropertyDescriptor(propertyName)).thenReturn(null);
+
+ final InstantiatedVersionedProcessor localProcessor = new
InstantiatedVersionedProcessor(instanceId, "group-id");
+ final FlowDifference difference = new StandardFlowDifference(
+ DifferenceType.PROPERTY_REMOVED,
+ localProcessor,
+ localProcessor,
+ propertyName,
+ "old",
+ null,
+ "Property removed in component definition");
+
+ assertTrue(FlowDifferenceFilters.isStaticPropertyRemoved(difference,
flowManager));
+ }
+
+ @Test
+ public void
testIsStaticPropertyRemovedFromDefinitionWhenDescriptorStillExists() {
+ final FlowManager flowManager = Mockito.mock(FlowManager.class);
+ final ProcessorNode processorNode = Mockito.mock(ProcessorNode.class);
+ final ConfigurableComponent configurableComponent =
Mockito.mock(ConfigurableComponent.class);
+
+ final String propertyName = "Still Supported";
+ final String instanceId = "processor-instance";
+
+
Mockito.when(flowManager.getProcessorNode(instanceId)).thenReturn(processorNode);
+
Mockito.when(processorNode.getComponent()).thenReturn(configurableComponent);
+
Mockito.when(configurableComponent.getPropertyDescriptors()).thenReturn(List.of(new
PropertyDescriptor.Builder().name(propertyName).build()));
+
+ final InstantiatedVersionedProcessor localProcessor = new
InstantiatedVersionedProcessor(instanceId, "group-id");
+ final FlowDifference difference = new StandardFlowDifference(
+ DifferenceType.PROPERTY_REMOVED,
+ localProcessor,
+ localProcessor,
+ propertyName,
+ "old",
+ null,
+ "Property still defined");
+
+ assertFalse(FlowDifferenceFilters.isStaticPropertyRemoved(difference,
flowManager));
+ }
+
+ @Test
+ public void
testIsStaticPropertyRemovedFromDefinitionWhenDynamicSupported() {
+ final FlowManager flowManager = Mockito.mock(FlowManager.class);
+ final ProcessorNode processorNode = Mockito.mock(ProcessorNode.class);
+ final ConfigurableComponent configurableComponent =
Mockito.mock(ConfigurableComponent.class);
+
+ final String propertyName = "Dynamic Property";
+ final String instanceId = "processor-instance";
+
+ final PropertyDescriptor dynamicDescriptor = new
PropertyDescriptor.Builder().name(propertyName).dynamic(true).build();
+
+
Mockito.when(flowManager.getProcessorNode(instanceId)).thenReturn(processorNode);
+
Mockito.when(processorNode.getComponent()).thenReturn(configurableComponent);
+
Mockito.when(configurableComponent.getPropertyDescriptors()).thenReturn(List.of());
+
Mockito.when(configurableComponent.getPropertyDescriptor(propertyName)).thenReturn(dynamicDescriptor);
+
+ final InstantiatedVersionedProcessor localProcessor = new
InstantiatedVersionedProcessor(instanceId, "group-id");
+ final FlowDifference difference = new StandardFlowDifference(
+ DifferenceType.PROPERTY_REMOVED,
+ localProcessor,
+ localProcessor,
+ propertyName,
+ "old",
+ null,
+ "Dynamic property removed");
+
+ assertFalse(FlowDifferenceFilters.isStaticPropertyRemoved(difference,
flowManager));
+ }
}
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
index 7129508775..85246e1b87 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
@@ -5771,6 +5771,7 @@ public class StandardNiFiServiceFacade implements
NiFiServiceFacade {
.filter(diff ->
!FlowDifferenceFilters.isScheduledStateNew(diff))
.filter(diff ->
!FlowDifferenceFilters.isLocalScheduleStateChange(diff))
.filter(diff ->
!FlowDifferenceFilters.isPropertyMissingFromGhostComponent(diff, flowManager))
+ .filter(diff ->
!FlowDifferenceFilters.isStaticPropertyRemoved(diff, flowManager))
.filter(difference -> difference.getDifferenceType() !=
DifferenceType.POSITION_CHANGED)
.map(difference -> {
final VersionedComponent localComponent =
difference.getComponentA();