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