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 e573d454d14 NIFI-16149 Handled migration-created Controller Services 
as environmental changes in Versioned Flows (#11480)
e573d454d14 is described below

commit e573d454d142736013a9c4e5d145edd1002689f7
Author: Pierre Villard <[email protected]>
AuthorDate: Mon Jul 27 23:11:32 2026 +0200

    NIFI-16149 Handled migration-created Controller Services as environmental 
changes in Versioned Flows (#11480)
    
    - Added standard string in comments to indicate migration-created status
    
    Signed-off-by: David Handermann <[email protected]>
---
 .../StandardControllerServiceFactory.java          |  13 +++
 .../apache/nifi/util/FlowDifferenceFilters.java    |  14 +++
 .../nifi/util/TestFlowDifferenceFilters.java       |  60 ++++++++++++
 .../system/migration/PropertyMigrationIT.java      |   7 ++
 .../ExternalControllerServiceVersioningIT.java     | 109 +++++++++++++++++++++
 5 files changed, 203 insertions(+)

diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/migration/StandardControllerServiceFactory.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/migration/StandardControllerServiceFactory.java
index dbc56fd0e20..da29dc401de 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/migration/StandardControllerServiceFactory.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/migration/StandardControllerServiceFactory.java
@@ -45,6 +45,14 @@ import java.util.UUID;
 public class StandardControllerServiceFactory implements 
ControllerServiceFactory {
     private static final Logger logger = 
LoggerFactory.getLogger(StandardControllerServiceFactory.class);
 
+    /**
+     * Comment applied to any Controller Service created as part of a 
component's property migration. This marks the
+     * service as migration-created so that flow-difference filtering can 
distinguish it from a service that a user
+     * added, and avoid reporting migration-created services as local 
modifications.
+     */
+    public static final String MIGRATION_CREATED_COMMENT = "Created during 
property migration";
+
+
     private final ExtensionManager extensionManager;
     private final FlowManager flowManager;
     private final ControllerServiceProvider serviceProvider;
@@ -99,6 +107,11 @@ public class StandardControllerServiceFactory implements 
ControllerServiceFactor
         final ControllerServiceFactory serviceFactory = new 
StandardControllerServiceFactory(extensionManager, flowManager, 
serviceProvider, serviceNode);
         serviceNode.migrateConfiguration(creationDetails.serviceProperties(), 
serviceFactory);
 
+        // Mark the service as migration-created so that flow-difference 
filtering can distinguish it from a
+        // service added by a user and avoid reporting it as a local 
modification.
+        serviceNode.setComments(MIGRATION_CREATED_COMMENT);
+
+
         if (isEnable()) {
             final ValidationStatus validationStatus = 
serviceNode.performValidation();
             if (validationStatus == ValidationStatus.VALID) {
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 72fd48d7637..1ec0dc97ede 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
@@ -41,6 +41,7 @@ import org.apache.nifi.flow.VersionedProcessGroup;
 import org.apache.nifi.flow.VersionedProcessor;
 import org.apache.nifi.flow.VersionedPropertyDescriptor;
 import org.apache.nifi.groups.ProcessGroup;
+import org.apache.nifi.migration.StandardControllerServiceFactory;
 import org.apache.nifi.processor.Processor;
 import org.apache.nifi.processor.Relationship;
 import org.apache.nifi.registry.flow.diff.DifferenceType;
@@ -779,6 +780,12 @@ public class FlowDifferenceFilters {
                 continue;
             }
 
+            // Only treat the added Controller Service as environmental when 
it was created by property migration.
+            // A service that a user added has no such marker and must be 
reported as a local modification.
+            if (!isMigrationCreatedControllerService(difference)) {
+                continue;
+            }
+
             for (final String candidateId : 
getControllerServiceIdentifiers(difference)) {
                 if 
(controllerServicePropertyAddsByValue.containsKey(candidateId)) {
                     serviceIdsWithMatchingAdditions.add(candidateId);
@@ -971,6 +978,13 @@ public class FlowDifferenceFilters {
         return false;
     }
 
+    private static boolean isMigrationCreatedControllerService(final 
FlowDifference difference) {
+        if (difference.getComponentB() instanceof final 
VersionedControllerService service) {
+            return 
StandardControllerServiceFactory.MIGRATION_CREATED_COMMENT.equals(service.getComments());
+        }
+        return false;
+    }
+
     private static Set<String> getControllerServiceIdentifiers(final 
FlowDifference difference) {
         final Set<String> identifiers = new HashSet<>();
 
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 cbb5489ef13..94f3410d177 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
@@ -35,6 +35,7 @@ import org.apache.nifi.flow.VersionedProcessor;
 import org.apache.nifi.flow.VersionedPropertyDescriptor;
 import org.apache.nifi.flow.VersionedRemoteGroupPort;
 import org.apache.nifi.groups.ProcessGroup;
+import org.apache.nifi.migration.StandardControllerServiceFactory;
 import org.apache.nifi.processor.AbstractProcessor;
 import org.apache.nifi.processor.ProcessContext;
 import org.apache.nifi.processor.ProcessSession;
@@ -325,6 +326,7 @@ public class TestFlowDifferenceFilters {
 
         final InstantiatedVersionedControllerService 
instantiatedControllerService = new 
InstantiatedVersionedControllerService(controllerServiceId, groupId);
         
instantiatedControllerService.setComponentType(ComponentType.CONTROLLER_SERVICE);
+        
instantiatedControllerService.setComments(StandardControllerServiceFactory.MIGRATION_CREATED_COMMENT);
 
         final FlowDifference controllerServiceDifference = new 
StandardFlowDifference(
                 DifferenceType.COMPONENT_ADDED,
@@ -346,6 +348,63 @@ public class TestFlowDifferenceFilters {
         
assertTrue(FlowDifferenceFilters.isEnvironmentalChange(controllerServiceDifference,
 null, flowManager, context));
     }
 
+    /**
+     * A Controller Service that a user adds (i.e. without the migration 
marker comment) and references from a new
+     * processor property must be reported as a local modification -- neither 
the service creation nor the property
+     * addition may be treated as an environmental change.
+     */
+    @Test
+    public void 
testUserAddedControllerServiceReferencedByNewPropertyIsNotEnvironmentalChange() 
{
+        final FlowManager flowManager = Mockito.mock(FlowManager.class);
+        final ProcessorNode processorNode = Mockito.mock(ProcessorNode.class);
+
+        final String processorId = "processor-instance";
+        final String groupId = "group-id";
+        final String propertyName = "ABC";
+        final String controllerServiceId = "controller-service-id";
+
+        
Mockito.when(flowManager.getProcessorNode(processorId)).thenReturn(processorNode);
+
+        final PropertyDescriptor propertyDescriptor = new 
PropertyDescriptor.Builder()
+                .name(propertyName)
+                .identifiesControllerService(ControllerService.class)
+                .build();
+        
Mockito.when(processorNode.getPropertyDescriptor(propertyName)).thenReturn(propertyDescriptor);
+
+        final InstantiatedVersionedProcessor instantiatedProcessor = new 
InstantiatedVersionedProcessor(processorId, groupId);
+        instantiatedProcessor.setComponentType(ComponentType.PROCESSOR);
+
+        final FlowDifference propertyDifference = new StandardFlowDifference(
+                DifferenceType.PROPERTY_ADDED,
+                instantiatedProcessor,
+                instantiatedProcessor,
+                propertyName,
+                null,
+                controllerServiceId,
+                "Controller service reference added");
+
+        // No migration marker comment -> this represents a user-added service.
+        final InstantiatedVersionedControllerService 
instantiatedControllerService = new 
InstantiatedVersionedControllerService(controllerServiceId, groupId);
+        
instantiatedControllerService.setComponentType(ComponentType.CONTROLLER_SERVICE);
+
+        final FlowDifference controllerServiceDifference = new 
StandardFlowDifference(
+                DifferenceType.COMPONENT_ADDED,
+                null,
+                instantiatedControllerService,
+                null,
+                null,
+                "Controller service created");
+
+        final FlowDifferenceFilters.EnvironmentalChangeContext context = 
FlowDifferenceFilters.buildEnvironmentalChangeContext(
+                List.of(propertyDifference, controllerServiceDifference), 
flowManager);
+
+        
assertFalse(FlowDifferenceFilters.isControllerServiceCreatedForNewProperty(propertyDifference,
 context));
+        
assertFalse(FlowDifferenceFilters.isControllerServiceCreatedForNewProperty(controllerServiceDifference,
 context));
+
+        
assertFalse(FlowDifferenceFilters.isEnvironmentalChange(propertyDifference, 
null, flowManager, context));
+        
assertFalse(FlowDifferenceFilters.isEnvironmentalChange(controllerServiceDifference,
 null, flowManager, context));
+    }
+
     @Test
     public void 
testControllerServiceCreationEnvironmentalChangeWithoutComponentNode() {
         final FlowManager flowManager = Mockito.mock(FlowManager.class);
@@ -379,6 +438,7 @@ public class TestFlowDifferenceFilters {
         final InstantiatedVersionedControllerService 
instantiatedControllerService = new 
InstantiatedVersionedControllerService(controllerServiceInstanceId, groupId);
         
instantiatedControllerService.setComponentType(ComponentType.CONTROLLER_SERVICE);
         
instantiatedControllerService.setIdentifier(controllerServiceVersionedId);
+        
instantiatedControllerService.setComments(StandardControllerServiceFactory.MIGRATION_CREATED_COMMENT);
 
         final FlowDifference controllerServiceDifference = new 
StandardFlowDifference(
                 DifferenceType.COMPONENT_ADDED,
diff --git 
a/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/migration/PropertyMigrationIT.java
 
b/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/migration/PropertyMigrationIT.java
index 4c74e0fbd1e..faab9470af1 100644
--- 
a/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/migration/PropertyMigrationIT.java
+++ 
b/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/migration/PropertyMigrationIT.java
@@ -17,6 +17,7 @@
 
 package org.apache.nifi.tests.system.migration;
 
+import org.apache.nifi.migration.StandardControllerServiceFactory;
 import org.apache.nifi.tests.system.NiFiSystemIT;
 import org.apache.nifi.toolkit.client.ControllerServicesClient;
 import org.apache.nifi.toolkit.client.NiFiClientException;
@@ -140,16 +141,22 @@ public class PropertyMigrationIT extends NiFiSystemIT {
         final Map<String, String> service1Props = 
service1.getComponent().getProperties();
         assertEquals(Map.of("Initial Value", "17"), service1Props);
         assertEquals(2, 
service1.getComponent().getReferencingComponents().size());
+        
assertEquals(StandardControllerServiceFactory.MIGRATION_CREATED_COMMENT, 
service1.getComponent().getComments(),
+                "Migration-created Controller Service should carry the 
migration marker comment");
 
         final ControllerServiceEntity service4 = 
serviceClient.getControllerService(proc4UpdatedProps.get(SERVICE));
         final Map<String, String> service4Props = 
service4.getComponent().getProperties();
         assertEquals(Map.of("Initial Value", "17"), service4Props);
         assertEquals(1, 
service4.getComponent().getReferencingComponents().size());
+        
assertEquals(StandardControllerServiceFactory.MIGRATION_CREATED_COMMENT, 
service4.getComponent().getComments(),
+                "Migration-created Controller Service should carry the 
migration marker comment");
 
         final ControllerServiceEntity service3 = 
serviceClient.getControllerService(proc3UpdatedProps.get(SERVICE));
         final Map<String, String> service3Props = 
service3.getComponent().getProperties();
         assertEquals(Map.of("Initial Value", "41"), service3Props);
         assertEquals(1, 
service3.getComponent().getReferencingComponents().size());
+        
assertEquals(StandardControllerServiceFactory.MIGRATION_CREATED_COMMENT, 
service3.getComponent().getComments(),
+                "Migration-created Controller Service should carry the 
migration marker comment");
     }
 
     private Map<String, String> getProperties(final ProcessorEntity processor) 
throws NiFiClientException, IOException {
diff --git 
a/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/registry/ExternalControllerServiceVersioningIT.java
 
b/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/registry/ExternalControllerServiceVersioningIT.java
index 1ee3a7a029a..84d5d799136 100644
--- 
a/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/registry/ExternalControllerServiceVersioningIT.java
+++ 
b/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/registry/ExternalControllerServiceVersioningIT.java
@@ -17,9 +17,11 @@
 
 package org.apache.nifi.tests.system.registry;
 
+import org.apache.nifi.migration.StandardControllerServiceFactory;
 import org.apache.nifi.tests.system.NiFiClientUtil;
 import org.apache.nifi.tests.system.NiFiSystemIT;
 import org.apache.nifi.toolkit.client.NiFiClientException;
+import org.apache.nifi.web.api.dto.ComponentDifferenceDTO;
 import org.apache.nifi.web.api.dto.ControllerServiceDTO;
 import org.apache.nifi.web.api.dto.DifferenceDTO;
 import org.apache.nifi.web.api.dto.VersionControlInformationDTO;
@@ -290,6 +292,99 @@ public class ExternalControllerServiceVersioningIT extends 
NiFiSystemIT {
                 "Show Local Changes should report no differences for an 
unmodified flow");
     }
 
+    /**
+     * Reproduces a "Show Local Changes" gap: a Controller Service that a user 
adds to a <em>nested (child)</em>
+     * Process Group of a versioned Process Group is not reported as a local 
modification.
+     *
+     * This mirrors a customer scenario -- a processor inside a nested group 
of a versioned connector had a service
+     * (e.g. an SSL Context Service) added from the processor's property 
dialog. The service was created in the
+     * nested group, but "Show Local Changes" did not list it, so the 
customization was missed on upgrade.
+     *
+     * Adding a Controller Service from a processor property produces two flow 
differences: a processor
+     * PROPERTY_ADDED (the reference) and a COMPONENT_ADDED (the service). 
These are paired and classified as an
+     * environmental change (see {@code 
FlowDifferenceFilters#isControllerServiceCreatedForNewProperty}), so
+     * {@code getLocalModifications} drops both and the service is never shown.
+     */
+    @Test
+    public void testControllerServiceAddedInNestedGroupAppearsInLocalChanges() 
throws NiFiClientException, IOException, InterruptedException {
+        final FlowRegistryClientEntity registryClient = registerClient();
+        final NiFiClientUtil util = getClientUtil();
+
+        // Versioned parent PG containing a nested child PG. The child holds a 
processor that will later reference
+        // a newly-created service, and a terminate processor so the committed 
flow is complete.
+        final ProcessGroupEntity parent = util.createProcessGroup("Parent", 
"root");
+        final ProcessGroupEntity nested = util.createProcessGroup("Nested", 
parent.getId());
+        final ProcessorEntity counter = util.createProcessor("CountFlowFiles", 
nested.getId());
+        final ProcessorEntity terminate = 
util.createProcessor("TerminateFlowFile", nested.getId());
+        util.createConnection(counter, terminate, "success");
+
+        // Commit the flow WITHOUT any controller service, so the service 
added below is a genuine local addition.
+        util.startVersionControl(parent, registryClient, TEST_FLOWS_BUCKET, 
"nested-cs-local-changes");
+        util.assertFlowUpToDate(parent.getId());
+
+        // Simulate "add Controller Service from the processor property 
dialog": create a new service in the NESTED
+        // (child) group and point the processor's controller-service property 
at it.
+        final ControllerServiceEntity nestedService = 
util.createControllerService(COUNT_SERVICE_TYPE, nested.getId());
+        util.updateProcessorProperties(counter, 
Collections.singletonMap("Count Service", 
nestedService.getComponent().getId()));
+
+        // "Show Local Changes" must report the newly added Controller Service.
+        final FlowComparisonEntity localMods = 
getNifiClient().getProcessGroupClient().getLocalModifications(parent.getId());
+
+        final String addedServiceId = nestedService.getComponent().getId();
+        final boolean serviceReported = 
localMods.getComponentDifferences().stream()
+                .anyMatch(diff -> 
addedServiceId.equals(diff.getComponentId()));
+
+        assertTrue(serviceReported,
+                "Show Local Changes should report the Controller Service added 
in the nested process group, but it did not. "
+                        + "Reported differences: " + 
describeDifferences(localMods));
+    }
+
+    private String describeDifferences(final FlowComparisonEntity localMods) {
+        final StringBuilder sb = new StringBuilder();
+        for (final ComponentDifferenceDTO component : 
localMods.getComponentDifferences()) {
+            sb.append("\n  - ").append(component.getComponentType()).append(" 
'").append(component.getComponentName()).append("' (")
+                    .append(component.getComponentId()).append(")");
+            if (component.getDifferences() != null) {
+                for (final DifferenceDTO difference : 
component.getDifferences()) {
+                    sb.append("\n      * ").append(difference.getDifference());
+                }
+            }
+        }
+        return sb.length() == 0 ? "<none>" : sb.toString();
+    }
+
+    /**
+     * A Controller Service marked as migration-created (via the migration 
marker comment) must NOT be reported by
+     * "Show Local Changes", even when it is added after the flow was 
committed and referenced by a processor. This is
+     * the counterpart to the user-added case, which is reported. The marker 
mirrors what property migration applies
+     * when it creates a service, so this exercises the suppression end-to-end 
without requiring a real migration.
+     */
+    @Test
+    public void testMigrationMarkedControllerServiceNotReportedAsLocalChange() 
throws NiFiClientException, IOException {
+        final FlowRegistryClientEntity registryClient = registerClient();
+        final NiFiClientUtil util = getClientUtil();
+
+        final ProcessGroupEntity group = util.createProcessGroup("Parent", 
"root");
+        final ProcessorEntity counter = util.createProcessor("CountFlowFiles", 
group.getId());
+        final ProcessorEntity terminate = 
util.createProcessor("TerminateFlowFile", group.getId());
+        util.createConnection(counter, terminate, "success");
+
+        util.startVersionControl(group, registryClient, TEST_FLOWS_BUCKET, 
"migration-marked-service");
+        util.assertFlowUpToDate(group.getId());
+
+        // Add a Controller Service carrying the migration marker comment (as 
property migration would) and reference it.
+        final ControllerServiceEntity service = 
util.createControllerService(COUNT_SERVICE_TYPE, group.getId());
+        setControllerServiceComments(service, 
StandardControllerServiceFactory.MIGRATION_CREATED_COMMENT);
+        util.updateProcessorProperties(counter, 
Collections.singletonMap("Count Service", service.getComponent().getId()));
+
+        final String serviceId = service.getComponent().getId();
+        final boolean serviceReported = 
getNifiClient().getProcessGroupClient().getLocalModifications(group.getId())
+                .getComponentDifferences().stream()
+                .anyMatch(diff -> serviceId.equals(diff.getComponentId()));
+        assertFalse(serviceReported,
+                "A migration-marked Controller Service must not be reported as 
a local modification, but it was");
+    }
+
     /**
      * Deletes only the connections and processors within a Process Group, 
without touching
      * Controller Services (which may be inherited from ancestor groups).
@@ -331,6 +426,20 @@ public class ExternalControllerServiceVersioningIT extends 
NiFiSystemIT {
         return 
getNifiClient().getControllerServicesClient().updateControllerService(entity);
     }
 
+    private ControllerServiceEntity setControllerServiceComments(final 
ControllerServiceEntity service, final String comments)
+            throws NiFiClientException, IOException {
+        final ControllerServiceDTO dto = new ControllerServiceDTO();
+        dto.setId(service.getId());
+        dto.setComments(comments);
+
+        final ControllerServiceEntity entity = new ControllerServiceEntity();
+        entity.setId(service.getId());
+        entity.setComponent(dto);
+        entity.setRevision(service.getRevision());
+
+        return 
getNifiClient().getControllerServicesClient().updateControllerService(entity);
+    }
+
     private void deleteControllerService(final ControllerServiceEntity 
service) throws NiFiClientException, IOException, InterruptedException {
         getClientUtil().disableControllerService(service);
         waitForControllerServiceState(service.getId(), "DISABLED");

Reply via email to