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();

Reply via email to