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 c39c15eb738 NIFI-16151 Added support for Dynamic Properties to Rebase 
of Versioned Flows (#11483)
c39c15eb738 is described below

commit c39c15eb738c5251d7bcf8a624504c43ce7218d8
Author: Pierre Villard <[email protected]>
AuthorDate: Wed Jul 29 18:01:29 2026 +0200

    NIFI-16151 Added support for Dynamic Properties to Rebase of Versioned 
Flows (#11483)
    
    Signed-off-by: David Handermann <[email protected]>
---
 .../flow/diff/PropertyAddedRebaseHandler.java      |  14 +
 .../flow/diff/PropertyAddedRebaseHandlerTest.java  | 346 +++++++++++++++++++++
 2 files changed, 360 insertions(+)

diff --git 
a/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/PropertyAddedRebaseHandler.java
 
b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/PropertyAddedRebaseHandler.java
index b859e93cc9c..3d2d8414f3e 100644
--- 
a/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/PropertyAddedRebaseHandler.java
+++ 
b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/PropertyAddedRebaseHandler.java
@@ -70,6 +70,9 @@ public class PropertyAddedRebaseHandler implements 
RebaseHandler {
 
         final Map<String, VersionedPropertyDescriptor> descriptors = 
targetComponent.getPropertyDescriptors();
         if (descriptors == null || !descriptors.containsKey(propertyName)) {
+            if (isDynamicProperty(localDifference, propertyName)) {
+                return 
RebaseAnalysis.ClassifiedDifference.compatible(localDifference);
+            }
             return 
RebaseAnalysis.ClassifiedDifference.unsupported(localDifference, 
RebaseConflictCode.DESCRIPTOR_NOT_FOUND,
                     "Property descriptor '%s' does not exist on component %s 
in target version".formatted(propertyName, componentIdentifier));
         }
@@ -77,6 +80,17 @@ public class PropertyAddedRebaseHandler implements 
RebaseHandler {
         return RebaseAnalysis.ClassifiedDifference.compatible(localDifference);
     }
 
+    private boolean isDynamicProperty(final FlowDifference localDifference, 
final String propertyName) {
+        if (localDifference.getComponentB() instanceof 
VersionedConfigurableComponent localComponent) {
+            final Map<String, VersionedPropertyDescriptor> localDescriptors = 
localComponent.getPropertyDescriptors();
+            if (localDescriptors != null) {
+                final VersionedPropertyDescriptor descriptor = 
localDescriptors.get(propertyName);
+                return descriptor != null && descriptor.isDynamic();
+            }
+        }
+        return false;
+    }
+
     @Override
     public void apply(final FlowDifference localDifference, final 
VersionedProcessGroup mergedFlow) {
         final String propertyName = 
localDifference.getFieldName().orElse(null);
diff --git 
a/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/test/java/org/apache/nifi/registry/flow/diff/PropertyAddedRebaseHandlerTest.java
 
b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/test/java/org/apache/nifi/registry/flow/diff/PropertyAddedRebaseHandlerTest.java
new file mode 100644
index 00000000000..ea83042157e
--- /dev/null
+++ 
b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/test/java/org/apache/nifi/registry/flow/diff/PropertyAddedRebaseHandlerTest.java
@@ -0,0 +1,346 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.nifi.registry.flow.diff;
+
+import org.apache.nifi.flow.VersionedControllerService;
+import org.apache.nifi.flow.VersionedProcessGroup;
+import org.apache.nifi.flow.VersionedProcessor;
+import org.apache.nifi.flow.VersionedPropertyDescriptor;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+class PropertyAddedRebaseHandlerTest {
+
+    private static final String ROOT = "root";
+    private static final String PROC_A = "proc-a";
+    private static final String SERVICE_A = "service-a";
+    private static final String DYNAMIC_PROP = "my.dynamic.key";
+    private static final String STATIC_PROP = "Timeout";
+    private static final String LOCAL_VALUE = "localVal";
+    private static final String UPSTREAM_VALUE = "upstreamVal";
+
+    private PropertyAddedRebaseHandler handler;
+
+    @BeforeEach
+    void setup() {
+        handler = new PropertyAddedRebaseHandler();
+    }
+
+    // --- processor: dynamic property ---
+
+    @Test
+    void testDynamicPropertyOnProcessorNoUpstreamConflict_isCompatible() {
+        final VersionedProcessor versionNProcessor = 
createProcessorNoProperties(PROC_A);
+        final VersionedProcessor localProcessor = 
createProcessorWithDynamicProperty(PROC_A, DYNAMIC_PROP, LOCAL_VALUE);
+
+        final FlowDifference localDifference = new 
StandardFlowDifference(DifferenceType.PROPERTY_ADDED,
+                versionNProcessor, localProcessor, DYNAMIC_PROP, null, 
LOCAL_VALUE, "Dynamic property added");
+
+        // target version: processor exists but does not have the dynamic 
property descriptor
+        final VersionedProcessGroup targetSnapshot = new 
VersionedProcessGroup();
+        targetSnapshot.setIdentifier(ROOT);
+        
targetSnapshot.getProcessors().add(createProcessorNoProperties(PROC_A));
+
+        final RebaseAnalysis.ClassifiedDifference result = 
handler.classify(localDifference, Collections.emptySet(), targetSnapshot);
+
+        assertEquals(RebaseClassification.COMPATIBLE, 
result.getClassification());
+    }
+
+    @Test
+    void testNonDynamicPropertyDescriptorNotInTarget_isUnsupported() {
+        final VersionedProcessor versionNProcessor = 
createProcessorNoProperties(PROC_A);
+        final VersionedProcessor localProcessor = 
createProcessorWithStaticProperty(PROC_A, STATIC_PROP, LOCAL_VALUE);
+
+        final FlowDifference localDifference = new 
StandardFlowDifference(DifferenceType.PROPERTY_ADDED,
+                versionNProcessor, localProcessor, STATIC_PROP, null, 
LOCAL_VALUE, "Static property added");
+
+        final VersionedProcessGroup targetSnapshot = new 
VersionedProcessGroup();
+        targetSnapshot.setIdentifier(ROOT);
+        
targetSnapshot.getProcessors().add(createProcessorNoProperties(PROC_A));
+
+        final RebaseAnalysis.ClassifiedDifference result = 
handler.classify(localDifference, Collections.emptySet(), targetSnapshot);
+
+        assertEquals(RebaseClassification.UNSUPPORTED, 
result.getClassification());
+        assertEquals(RebaseConflictCode.DESCRIPTOR_NOT_FOUND, 
result.getConflictCode());
+    }
+
+    @Test
+    void testDynamicPropertyConflictOnProcessor_isConflicting() {
+        final VersionedProcessor versionNProcessor = 
createProcessorNoProperties(PROC_A);
+        final VersionedProcessor localProcessor = 
createProcessorWithDynamicProperty(PROC_A, DYNAMIC_PROP, LOCAL_VALUE);
+
+        final FlowDifference localDifference = new 
StandardFlowDifference(DifferenceType.PROPERTY_ADDED,
+                versionNProcessor, localProcessor, DYNAMIC_PROP, null, 
LOCAL_VALUE, "Dynamic property added locally");
+
+        // upstream also added the same dynamic property but with a different 
value
+        final Set<FlowDifference> upstreamDifferences = new HashSet<>();
+        upstreamDifferences.add(new 
StandardFlowDifference(DifferenceType.PROPERTY_ADDED,
+                versionNProcessor, versionNProcessor, DYNAMIC_PROP, null, 
UPSTREAM_VALUE, "Dynamic property added upstream"));
+
+        final VersionedProcessGroup targetSnapshot = new 
VersionedProcessGroup();
+        targetSnapshot.setIdentifier(ROOT);
+
+        final RebaseAnalysis.ClassifiedDifference result = 
handler.classify(localDifference, upstreamDifferences, targetSnapshot);
+
+        assertEquals(RebaseClassification.CONFLICTING, 
result.getClassification());
+        assertEquals(RebaseConflictCode.SAME_PROPERTY, 
result.getConflictCode());
+    }
+
+    @Test
+    void testDynamicPropertySameValueUpstream_isCompatible() {
+        final VersionedProcessor versionNProcessor = 
createProcessorNoProperties(PROC_A);
+        final VersionedProcessor localProcessor = 
createProcessorWithDynamicProperty(PROC_A, DYNAMIC_PROP, LOCAL_VALUE);
+
+        final FlowDifference localDifference = new 
StandardFlowDifference(DifferenceType.PROPERTY_ADDED,
+                versionNProcessor, localProcessor, DYNAMIC_PROP, null, 
LOCAL_VALUE, "Dynamic property added locally");
+
+        // upstream added the same property with the same value — idempotent
+        final Set<FlowDifference> upstreamDifferences = new HashSet<>();
+        upstreamDifferences.add(new 
StandardFlowDifference(DifferenceType.PROPERTY_ADDED,
+                versionNProcessor, versionNProcessor, DYNAMIC_PROP, null, 
LOCAL_VALUE, "Dynamic property added upstream"));
+
+        final VersionedProcessGroup targetSnapshot = new 
VersionedProcessGroup();
+        targetSnapshot.setIdentifier(ROOT);
+
+        final RebaseAnalysis.ClassifiedDifference result = 
handler.classify(localDifference, upstreamDifferences, targetSnapshot);
+
+        assertEquals(RebaseClassification.COMPATIBLE, 
result.getClassification());
+    }
+
+    @Test
+    void 
testDynamicPropertyDescriptorMissingFromLocalComponent_isUnsupported() {
+        // componentB has an empty descriptor map — cannot determine dynamic 
flag → UNSUPPORTED
+        final VersionedProcessor versionNProcessor = 
createProcessorNoProperties(PROC_A);
+        final VersionedProcessor localProcessor = new VersionedProcessor();
+        localProcessor.setIdentifier(PROC_A);
+        localProcessor.setProperties(Map.of(DYNAMIC_PROP, LOCAL_VALUE));
+        localProcessor.setPropertyDescriptors(Collections.emptyMap());
+
+        final FlowDifference localDifference = new 
StandardFlowDifference(DifferenceType.PROPERTY_ADDED,
+                versionNProcessor, localProcessor, DYNAMIC_PROP, null, 
LOCAL_VALUE, "Property added, no descriptor");
+
+        final VersionedProcessGroup targetSnapshot = new 
VersionedProcessGroup();
+        targetSnapshot.setIdentifier(ROOT);
+        
targetSnapshot.getProcessors().add(createProcessorNoProperties(PROC_A));
+
+        final RebaseAnalysis.ClassifiedDifference result = 
handler.classify(localDifference, Collections.emptySet(), targetSnapshot);
+
+        assertEquals(RebaseClassification.UNSUPPORTED, 
result.getClassification());
+        assertEquals(RebaseConflictCode.DESCRIPTOR_NOT_FOUND, 
result.getConflictCode());
+    }
+
+    @Test
+    void testApplyAddsDynamicPropertyToProcessor() {
+        final VersionedProcessor versionNProcessor = 
createProcessorNoProperties(PROC_A);
+        final VersionedProcessor localProcessor = 
createProcessorWithDynamicProperty(PROC_A, DYNAMIC_PROP, LOCAL_VALUE);
+
+        final FlowDifference localDifference = new 
StandardFlowDifference(DifferenceType.PROPERTY_ADDED,
+                versionNProcessor, localProcessor, DYNAMIC_PROP, null, 
LOCAL_VALUE, "Dynamic property added");
+
+        final VersionedProcessGroup mergedFlow = new VersionedProcessGroup();
+        mergedFlow.setIdentifier(ROOT);
+        mergedFlow.getProcessors().add(createProcessorNoProperties(PROC_A));
+
+        handler.apply(localDifference, mergedFlow);
+
+        final VersionedProcessor mergedProcessor = 
mergedFlow.getProcessors().iterator().next();
+        assertNotNull(mergedProcessor);
+        assertEquals(LOCAL_VALUE, 
mergedProcessor.getProperties().get(DYNAMIC_PROP));
+    }
+
+    // --- controller service: dynamic property ---
+
+    @Test
+    void 
testDynamicPropertyOnControllerServiceNoUpstreamConflict_isCompatible() {
+        final VersionedControllerService versionNService = 
createServiceNoProperties(SERVICE_A);
+        final VersionedControllerService localService = 
createServiceWithDynamicProperty(SERVICE_A, DYNAMIC_PROP, LOCAL_VALUE);
+
+        final FlowDifference localDifference = new 
StandardFlowDifference(DifferenceType.PROPERTY_ADDED,
+                versionNService, localService, DYNAMIC_PROP, null, 
LOCAL_VALUE, "Dynamic property added on CS");
+
+        final VersionedProcessGroup targetSnapshot = new 
VersionedProcessGroup();
+        targetSnapshot.setIdentifier(ROOT);
+        
targetSnapshot.getControllerServices().add(createServiceNoProperties(SERVICE_A));
+
+        final RebaseAnalysis.ClassifiedDifference result = 
handler.classify(localDifference, Collections.emptySet(), targetSnapshot);
+
+        assertEquals(RebaseClassification.COMPATIBLE, 
result.getClassification());
+    }
+
+    @Test
+    void testApplyAddsDynamicPropertyToControllerService() {
+        final VersionedControllerService versionNService = 
createServiceNoProperties(SERVICE_A);
+        final VersionedControllerService localService = 
createServiceWithDynamicProperty(SERVICE_A, DYNAMIC_PROP, LOCAL_VALUE);
+
+        final FlowDifference localDifference = new 
StandardFlowDifference(DifferenceType.PROPERTY_ADDED,
+                versionNService, localService, DYNAMIC_PROP, null, 
LOCAL_VALUE, "Dynamic property added on CS");
+
+        final VersionedProcessGroup mergedFlow = new VersionedProcessGroup();
+        mergedFlow.setIdentifier(ROOT);
+        
mergedFlow.getControllerServices().add(createServiceNoProperties(SERVICE_A));
+
+        handler.apply(localDifference, mergedFlow);
+
+        final VersionedControllerService mergedService = 
mergedFlow.getControllerServices().iterator().next();
+        assertNotNull(mergedService);
+        assertEquals(LOCAL_VALUE, 
mergedService.getProperties().get(DYNAMIC_PROP));
+    }
+
+    @Test
+    void 
testNonDynamicPropertyOnControllerServiceDescriptorNotInTarget_isUnsupported() {
+        final VersionedControllerService versionNService = 
createServiceNoProperties(SERVICE_A);
+        final VersionedControllerService localService = 
createServiceWithStaticProperty(SERVICE_A, STATIC_PROP, LOCAL_VALUE);
+
+        final FlowDifference localDifference = new 
StandardFlowDifference(DifferenceType.PROPERTY_ADDED,
+                versionNService, localService, STATIC_PROP, null, LOCAL_VALUE, 
"Static property added on CS");
+
+        final VersionedProcessGroup targetSnapshot = new 
VersionedProcessGroup();
+        targetSnapshot.setIdentifier(ROOT);
+        
targetSnapshot.getControllerServices().add(createServiceNoProperties(SERVICE_A));
+
+        final RebaseAnalysis.ClassifiedDifference result = 
handler.classify(localDifference, Collections.emptySet(), targetSnapshot);
+
+        assertEquals(RebaseClassification.UNSUPPORTED, 
result.getClassification());
+        assertEquals(RebaseConflictCode.DESCRIPTOR_NOT_FOUND, 
result.getConflictCode());
+    }
+
+    // --- static property present in target: still COMPATIBLE (existing 
behaviour) ---
+
+    @Test
+    void testStaticPropertyDescriptorPresentInTarget_isCompatible() {
+        final VersionedProcessor versionNProcessor = 
createProcessorNoProperties(PROC_A);
+        final VersionedProcessor localProcessor = 
createProcessorWithStaticProperty(PROC_A, STATIC_PROP, LOCAL_VALUE);
+
+        final FlowDifference localDifference = new 
StandardFlowDifference(DifferenceType.PROPERTY_ADDED,
+                versionNProcessor, localProcessor, STATIC_PROP, null, 
LOCAL_VALUE, "Static property added");
+
+        // target has the descriptor for the property
+        final VersionedProcessGroup targetSnapshot = new 
VersionedProcessGroup();
+        targetSnapshot.setIdentifier(ROOT);
+        
targetSnapshot.getProcessors().add(createProcessorWithStaticProperty(PROC_A, 
STATIC_PROP, "defaultVal"));
+
+        final RebaseAnalysis.ClassifiedDifference result = 
handler.classify(localDifference, Collections.emptySet(), targetSnapshot);
+
+        assertEquals(RebaseClassification.COMPATIBLE, 
result.getClassification());
+        assertNull(result.getConflictCode());
+    }
+
+    // --- helpers ---
+
+    private VersionedProcessor createProcessorNoProperties(final String 
identifier) {
+        final VersionedProcessor processor = new VersionedProcessor();
+        processor.setIdentifier(identifier);
+        processor.setProperties(Collections.emptyMap());
+        processor.setPropertyDescriptors(Collections.emptyMap());
+        return processor;
+    }
+
+    private VersionedProcessor createProcessorWithDynamicProperty(final String 
identifier, final String propName, final String propValue) {
+        final VersionedProcessor processor = new VersionedProcessor();
+        processor.setIdentifier(identifier);
+
+        final Map<String, String> properties = new HashMap<>();
+        properties.put(propName, propValue);
+        processor.setProperties(properties);
+
+        final VersionedPropertyDescriptor descriptor = new 
VersionedPropertyDescriptor();
+        descriptor.setName(propName);
+        descriptor.setDynamic(true);
+
+        final Map<String, VersionedPropertyDescriptor> descriptors = new 
HashMap<>();
+        descriptors.put(propName, descriptor);
+        processor.setPropertyDescriptors(descriptors);
+
+        return processor;
+    }
+
+    private VersionedProcessor createProcessorWithStaticProperty(final String 
identifier, final String propName, final String propValue) {
+        final VersionedProcessor processor = new VersionedProcessor();
+        processor.setIdentifier(identifier);
+
+        final Map<String, String> properties = new HashMap<>();
+        properties.put(propName, propValue);
+        processor.setProperties(properties);
+
+        final VersionedPropertyDescriptor descriptor = new 
VersionedPropertyDescriptor();
+        descriptor.setName(propName);
+        descriptor.setDynamic(false);
+
+        final Map<String, VersionedPropertyDescriptor> descriptors = new 
HashMap<>();
+        descriptors.put(propName, descriptor);
+        processor.setPropertyDescriptors(descriptors);
+
+        return processor;
+    }
+
+    private VersionedControllerService createServiceNoProperties(final String 
identifier) {
+        final VersionedControllerService service = new 
VersionedControllerService();
+        service.setIdentifier(identifier);
+        service.setProperties(Collections.emptyMap());
+        service.setPropertyDescriptors(Collections.emptyMap());
+        return service;
+    }
+
+    private VersionedControllerService createServiceWithDynamicProperty(final 
String identifier, final String propName, final String propValue) {
+        final VersionedControllerService service = new 
VersionedControllerService();
+        service.setIdentifier(identifier);
+
+        final Map<String, String> properties = new HashMap<>();
+        properties.put(propName, propValue);
+        service.setProperties(properties);
+
+        final VersionedPropertyDescriptor descriptor = new 
VersionedPropertyDescriptor();
+        descriptor.setName(propName);
+        descriptor.setDynamic(true);
+
+        final Map<String, VersionedPropertyDescriptor> descriptors = new 
HashMap<>();
+        descriptors.put(propName, descriptor);
+        service.setPropertyDescriptors(descriptors);
+
+        return service;
+    }
+
+    private VersionedControllerService createServiceWithStaticProperty(final 
String identifier, final String propName, final String propValue) {
+        final VersionedControllerService service = new 
VersionedControllerService();
+        service.setIdentifier(identifier);
+
+        final Map<String, String> properties = new HashMap<>();
+        properties.put(propName, propValue);
+        service.setProperties(properties);
+
+        final VersionedPropertyDescriptor descriptor = new 
VersionedPropertyDescriptor();
+        descriptor.setName(propName);
+        descriptor.setDynamic(false);
+
+        final Map<String, VersionedPropertyDescriptor> descriptors = new 
HashMap<>();
+        descriptors.put(propName, descriptor);
+        service.setPropertyDescriptors(descriptors);
+
+        return service;
+    }
+}

Reply via email to