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 5a6c47278d7 NIFI-16058 Treat external Controller Service reference
changes in versioned flows as environmental (#11383)
5a6c47278d7 is described below
commit 5a6c47278d70a4c98a3b1403a80bc1ff748bf586
Author: Pierre Villard <[email protected]>
AuthorDate: Thu Jul 23 15:52:27 2026 +0200
NIFI-16058 Treat external Controller Service reference changes in versioned
flows as environmental (#11383)
Signed-off-by: David Handermann <[email protected]>
---
.../apache/nifi/groups/StandardProcessGroup.java | 82 +---------------
.../apache/nifi/util/FlowDifferenceFilters.java | 100 +++++++++++++++++++-
.../nifi/util/TestFlowDifferenceFilters.java | 84 +++++++++++++++++
.../apache/nifi/web/StandardNiFiServiceFacade.java | 5 -
.../org/apache/nifi/web/api/dto/DtoFactory.java | 2 +-
.../ExternalControllerServiceVersioningIT.java | 103 ++++++++++++++++-----
6 files changed, 260 insertions(+), 116 deletions(-)
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
index 1aa7d4c5959..b20a5787ece 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
@@ -68,13 +68,9 @@ import
org.apache.nifi.controller.service.ControllerServiceState;
import org.apache.nifi.controller.service.StandardConfigurationContext;
import org.apache.nifi.encrypt.PropertyEncryptor;
import org.apache.nifi.flow.ExecutionEngine;
-import org.apache.nifi.flow.ExternalControllerServiceReference;
import org.apache.nifi.flow.VersionedComponent;
-import org.apache.nifi.flow.VersionedControllerService;
import org.apache.nifi.flow.VersionedExternalFlow;
import org.apache.nifi.flow.VersionedProcessGroup;
-import org.apache.nifi.flow.VersionedProcessor;
-import org.apache.nifi.flow.VersionedPropertyDescriptor;
import
org.apache.nifi.flow.synchronization.StandardVersionedComponentSynchronizer;
import
org.apache.nifi.flow.synchronization.VersionedFlowSynchronizationContext;
import org.apache.nifi.lifecycle.ProcessorStopLifecycleMethods;
@@ -3867,7 +3863,6 @@ public final class StandardProcessGroup implements
ProcessGroup {
final FlowSnapshotContainer registrySnapshotContainer =
flowRegistry.getFlowContents(
FlowRegistryClientContextFactory.getAnonymousContext(), flowVersionLocation,
false);
final RegisteredFlowSnapshot registrySnapshot =
registrySnapshotContainer.getFlowSnapshot();
- resolveExternalServiceReferences(registrySnapshot);
final VersionedProcessGroup registryFlow =
registrySnapshot.getFlowContents();
vci.setFlowSnapshot(registryFlow);
} catch (final IOException | FlowRegistryException e) {
@@ -4094,81 +4089,6 @@ public final class StandardProcessGroup implements
ProcessGroup {
return uuid.toString();
}
- private void resolveExternalServiceReferences(final RegisteredFlowSnapshot
snapshot) {
- final Map<String, ExternalControllerServiceReference> externalRefs =
snapshot.getExternalControllerServices();
- if (externalRefs == null || externalRefs.isEmpty()) {
- return;
- }
-
- final ProcessGroup parentGroup = getParent();
- if (parentGroup == null) {
- return;
- }
-
- final Map<String, String> serviceNameToVersionedId = new HashMap<>();
- for (final ControllerServiceNode serviceNode :
parentGroup.getControllerServices(true)) {
- final String versionedId =
serviceNode.getVersionedComponentId().orElse(
-
VersionedComponentFlowMapper.generateVersionedComponentId(serviceNode.getIdentifier()));
- serviceNameToVersionedId.put(serviceNode.getName(), versionedId);
- }
-
- final Map<String, String> foreignToLocalId = new HashMap<>();
- for (final Map.Entry<String, ExternalControllerServiceReference> entry
: externalRefs.entrySet()) {
- final String foreignId = entry.getKey();
- final String serviceName = entry.getValue().getName();
- final String localId = serviceNameToVersionedId.get(serviceName);
- if (localId != null && !localId.equals(foreignId)) {
- foreignToLocalId.put(foreignId, localId);
- }
- }
-
- if (!foreignToLocalId.isEmpty()) {
- replaceExternalServiceIds(snapshot.getFlowContents(),
foreignToLocalId);
- }
- }
-
- private void replaceExternalServiceIds(final VersionedProcessGroup group,
final Map<String, String> foreignToLocalId) {
- if (group.getProcessors() != null) {
- for (final VersionedProcessor processor : group.getProcessors()) {
- replaceServicePropertyIds(processor.getProperties(),
processor.getPropertyDescriptors(), foreignToLocalId);
- }
- }
-
- if (group.getControllerServices() != null) {
- for (final VersionedControllerService service :
group.getControllerServices()) {
- replaceServicePropertyIds(service.getProperties(),
service.getPropertyDescriptors(), foreignToLocalId);
- }
- }
-
- if (group.getProcessGroups() != null) {
- for (final VersionedProcessGroup child : group.getProcessGroups())
{
- replaceExternalServiceIds(child, foreignToLocalId);
- }
- }
- }
-
- private void replaceServicePropertyIds(final Map<String, String>
properties, final Map<String, VersionedPropertyDescriptor> descriptors,
- final Map<String, String>
foreignToLocalId) {
- if (properties == null || descriptors == null) {
- return;
- }
-
- for (final Map.Entry<String, String> entry : properties.entrySet()) {
- final String propertyValue = entry.getValue();
- if (propertyValue == null) {
- continue;
- }
-
- final VersionedPropertyDescriptor descriptor =
descriptors.get(entry.getKey());
- if (descriptor != null &&
descriptor.getIdentifiesControllerService()) {
- final String localId = foreignToLocalId.get(propertyValue);
- if (localId != null) {
- entry.setValue(localId);
- }
- }
- }
- }
-
private Set<FlowDifference> getModifications() {
final StandardVersionControlInformation vci = versionControlInfo.get();
@@ -4202,7 +4122,7 @@ public final class StandardProcessGroup implements
ProcessGroup {
final FlowComparison comparison = flowComparator.compare();
final Collection<FlowDifference> comparisonDifferences =
comparison.getDifferences();
final FlowDifferenceFilters.EnvironmentalChangeContext
environmentalContext =
-
FlowDifferenceFilters.buildEnvironmentalChangeContext(comparisonDifferences,
flowManager);
+
FlowDifferenceFilters.buildEnvironmentalChangeContext(comparisonDifferences,
versionedGroup, flowManager);
final Set<FlowDifference> differences =
comparisonDifferences.stream()
.filter(difference ->
!FlowDifferenceFilters.isEnvironmentalChange(difference, versionedGroup,
flowManager, environmentalContext))
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 1da9653ae50..72fd48d7637 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
@@ -48,7 +48,9 @@ import org.apache.nifi.registry.flow.diff.FlowDifference;
import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedComponent;
import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedConnection;
import
org.apache.nifi.registry.flow.mapping.InstantiatedVersionedControllerService;
+import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedProcessGroup;
import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedProcessor;
+import org.apache.nifi.registry.flow.mapping.VersionedComponentFlowMapper;
import java.util.ArrayList;
import java.util.Collection;
@@ -104,7 +106,8 @@ public class FlowDifferenceFilters {
|| isPropertyParameterizationRename(difference, evaluatedContext)
|| isPropertyRenameWithMatchingValue(difference, evaluatedContext)
|| isSelectedRelationshipChangeForNewRelationship(difference,
flowManager)
- || isPropertyAddedFromMigration(difference, flowManager);
+ || isPropertyAddedFromMigration(difference, flowManager)
+ || isExternalControllerServiceReferenceChange(difference,
flowManager, evaluatedContext);
}
/**
@@ -705,10 +708,17 @@ public class FlowDifferenceFilters {
}
public static EnvironmentalChangeContext
buildEnvironmentalChangeContext(final Collection<FlowDifference> differences,
final FlowManager flowManager) {
+ return buildEnvironmentalChangeContext(differences, null, flowManager);
+ }
+
+ public static EnvironmentalChangeContext
buildEnvironmentalChangeContext(final Collection<FlowDifference> differences,
final VersionedProcessGroup localGroup,
+
final FlowManager flowManager) {
if (differences == null || differences.isEmpty() || flowManager ==
null) {
return EnvironmentalChangeContext.empty();
}
+ final Set<String> ancestorControllerServiceIds =
computeAncestorControllerServiceIds(localGroup, flowManager);
+
final Map<String, List<PropertyDiffInfo>> parameterizedAddsByComponent
= new HashMap<>();
final Map<String, List<PropertyDiffInfo>>
parameterizationRemovalsByComponent = new HashMap<>();
final Map<String, List<PropertyDiffInfo>>
controllerServicePropertyAddsByValue = new HashMap<>();
@@ -854,11 +864,83 @@ public class FlowDifferenceFilters {
}
}
- if (serviceIdsWithMatchingAdditions.isEmpty() &&
parameterizedPropertyRenameDifferences.isEmpty() &&
propertyRenamesWithMatchingValues.isEmpty()) {
+ if (serviceIdsWithMatchingAdditions.isEmpty() &&
parameterizedPropertyRenameDifferences.isEmpty() &&
propertyRenamesWithMatchingValues.isEmpty()
+ && ancestorControllerServiceIds.isEmpty()) {
return EnvironmentalChangeContext.empty();
}
- return new EnvironmentalChangeContext(serviceIdsWithMatchingAdditions,
parameterizedPropertyRenameDifferences, propertyRenamesWithMatchingValues);
+ return new EnvironmentalChangeContext(serviceIdsWithMatchingAdditions,
parameterizedPropertyRenameDifferences, propertyRenamesWithMatchingValues,
+ ancestorControllerServiceIds);
+ }
+
+ /**
+ * Determines whether a Flow Difference represents a change to a property
that references an <em>external</em> controller service
+ * (one defined outside the versioned Process Group, e.g. in an ancestor
group). Such a reference is environment-specific: the same
+ * logical service typically has a different identifier in each
environment, so pointing the property at a different external service
+ * is not treated as a local modification. The change is environmental
only when the versioned-snapshot value does not resolve to a
+ * locally-accessible ancestor service while the local value does.
+ */
+ private static boolean isExternalControllerServiceReferenceChange(final
FlowDifference difference, final FlowManager flowManager,
+ final
EnvironmentalChangeContext context) {
+ if (difference.getDifferenceType() != DifferenceType.PROPERTY_CHANGED)
{
+ return false;
+ }
+
+ final Set<String> ancestorServiceIds =
context.ancestorControllerServiceIds();
+ if (ancestorServiceIds.isEmpty()) {
+ return false;
+ }
+
+ if (!isControllerServiceProperty(difference, flowManager)) {
+ return false;
+ }
+
+ final Optional<String> snapshotValue = getPropertyValue(difference,
true);
+ final Optional<String> localValue = getPropertyValue(difference,
false);
+ if (snapshotValue.isEmpty() || localValue.isEmpty()) {
+ return false;
+ }
+
+ // Parameter-referenced controller services are environment-portable
through Parameter Contexts and are not evaluated here.
+ if (isParameterReference(snapshotValue.get()) ||
isParameterReference(localValue.get())) {
+ return false;
+ }
+
+ // Environmental only when the snapshot reference is not a
locally-accessible ancestor controller service but the local reference
+ // is. A switch between two locally-accessible ancestor services
remains a reported change, and a reference to a service defined
+ // inside the versioned Process Group is never in the ancestor set, so
it always remains a reported change.
+ final boolean snapshotAccessible =
ancestorServiceIds.contains(snapshotValue.get());
+ final boolean localAccessible =
ancestorServiceIds.contains(localValue.get());
+ return !snapshotAccessible && localAccessible;
+ }
+
+ /**
+ * Computes the set of versioned component identifiers for all controller
services accessible from the ancestors of the given (local)
+ * versioned Process Group -- i.e. services defined in the parent group
and above. These identify references to external controller
+ * services. Returns an empty set when the group is not an instantiated
(live-backed) group, so callers that pass a plain snapshot
+ * group get no suppression.
+ */
+ private static Set<String> computeAncestorControllerServiceIds(final
VersionedProcessGroup localGroup, final FlowManager flowManager) {
+ if (flowManager == null || !(localGroup instanceof
InstantiatedVersionedProcessGroup instantiatedGroup)) {
+ return Collections.emptySet();
+ }
+
+ final ProcessGroup liveGroup =
flowManager.getGroup(instantiatedGroup.getInstanceIdentifier());
+ if (liveGroup == null) {
+ return Collections.emptySet();
+ }
+
+ final ProcessGroup parentGroup = liveGroup.getParent();
+ if (parentGroup == null) {
+ return Collections.emptySet();
+ }
+
+ final Set<String> ancestorServiceIds = new HashSet<>();
+ for (final ControllerServiceNode serviceNode :
parentGroup.getControllerServices(true)) {
+
ancestorServiceIds.add(serviceNode.getVersionedComponentId().orElseGet(
+ () ->
VersionedComponentFlowMapper.generateVersionedComponentId(serviceNode.getIdentifier())));
+ }
+ return ancestorServiceIds;
}
public static boolean isControllerServiceCreatedForNewProperty(final
FlowDifference difference, final EnvironmentalChangeContext context) {
@@ -1211,18 +1293,22 @@ public class FlowDifferenceFilters {
}
public static final class EnvironmentalChangeContext {
- private static final EnvironmentalChangeContext EMPTY = new
EnvironmentalChangeContext(Collections.emptySet(), Collections.emptySet(),
Collections.emptySet());
+ private static final EnvironmentalChangeContext EMPTY =
+ new EnvironmentalChangeContext(Collections.emptySet(),
Collections.emptySet(), Collections.emptySet(), Collections.emptySet());
private final Set<String> serviceIdsCreatedForNewProperties;
private final Set<FlowDifference> parameterizedPropertyRenames;
private final Set<FlowDifference> propertyRenamesWithMatchingValues;
+ private final Set<String> ancestorControllerServiceIds;
private EnvironmentalChangeContext(final Set<String>
serviceIdsCreatedForNewProperties,
final Set<FlowDifference>
parameterizedPropertyRenames,
- final Set<FlowDifference>
propertyRenamesWithMatchingValues) {
+ final Set<FlowDifference>
propertyRenamesWithMatchingValues,
+ final Set<String>
ancestorControllerServiceIds) {
this.serviceIdsCreatedForNewProperties =
Collections.unmodifiableSet(new HashSet<>(serviceIdsCreatedForNewProperties));
this.parameterizedPropertyRenames =
Collections.unmodifiableSet(new HashSet<>(parameterizedPropertyRenames));
this.propertyRenamesWithMatchingValues =
Collections.unmodifiableSet(new HashSet<>(propertyRenamesWithMatchingValues));
+ this.ancestorControllerServiceIds =
Collections.unmodifiableSet(new HashSet<>(ancestorControllerServiceIds));
}
static EnvironmentalChangeContext empty() {
@@ -1240,5 +1326,9 @@ public class FlowDifferenceFilters {
Set<FlowDifference> propertyRenamesWithMatchingValues() {
return propertyRenamesWithMatchingValues;
}
+
+ Set<String> ancestorControllerServiceIds() {
+ return ancestorControllerServiceIds;
+ }
}
}
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 81c45668eef..cbb5489ef13 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
@@ -44,6 +44,7 @@ import org.apache.nifi.registry.flow.diff.FlowDifference;
import org.apache.nifi.registry.flow.diff.StandardFlowDifference;
import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedConnection;
import
org.apache.nifi.registry.flow.mapping.InstantiatedVersionedControllerService;
+import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedProcessGroup;
import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedProcessor;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
@@ -51,6 +52,7 @@ import org.mockito.Mockito;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -393,6 +395,88 @@ public class TestFlowDifferenceFilters {
assertTrue(FlowDifferenceFilters.isEnvironmentalChange(controllerServiceDifference,
null, flowManager, context));
}
+ /**
+ * Changing a processor property that references an external (ancestor)
controller service is not reported as a local
+ * modification, because the same logical service typically has a
different identifier in each environment. This exercises the
+ * directional rule over the set of accessible ancestor controller service
ids: suppression happens only when the versioned-snapshot
+ * reference is not a locally-accessible ancestor service while the local
reference is.
+ */
+ @Test
+ public void testExternalControllerServiceReferenceChangeDirectionalRule() {
+ final String pgInstanceId = "pg-instance";
+ final String parentGroupId = "parent-group";
+ final String propertyName = "SSL Context Service";
+
+ final String ancestorServiceX = "ancestor-service-x";
+ final String ancestorServiceY = "ancestor-service-y";
+ final String foreignServiceId = "foreign-service-id";
+ final String internalServiceId = "internal-service-id";
+
+ // Two controller services defined in the parent (ancestor) group,
i.e. external to the versioned Process Group.
+ final ControllerServiceNode serviceX =
Mockito.mock(ControllerServiceNode.class);
+
Mockito.when(serviceX.getVersionedComponentId()).thenReturn(Optional.of(ancestorServiceX));
+ final ControllerServiceNode serviceY =
Mockito.mock(ControllerServiceNode.class);
+
Mockito.when(serviceY.getVersionedComponentId()).thenReturn(Optional.of(ancestorServiceY));
+
+ final ProcessGroup parentGroup = Mockito.mock(ProcessGroup.class);
+
Mockito.when(parentGroup.getControllerServices(true)).thenReturn(Set.of(serviceX,
serviceY));
+
+ final ProcessGroup liveGroup = Mockito.mock(ProcessGroup.class);
+ Mockito.when(liveGroup.getParent()).thenReturn(parentGroup);
+
+ final FlowManager flowManager = Mockito.mock(FlowManager.class);
+ Mockito.when(flowManager.getGroup(pgInstanceId)).thenReturn(liveGroup);
+
+ final InstantiatedVersionedProcessGroup localGroup = new
InstantiatedVersionedProcessGroup(pgInstanceId, parentGroupId);
+
+ assertExternalServiceChange(localGroup, flowManager, parentGroupId,
propertyName, foreignServiceId, ancestorServiceX, true,
+ "Switching from an unavailable external service to a local
ancestor service must be treated as environmental");
+
+ assertExternalServiceChange(localGroup, flowManager, parentGroupId,
propertyName, ancestorServiceY, ancestorServiceX, false,
+ "Switching between two locally-accessible ancestor services
must remain a reported change");
+
+ assertExternalServiceChange(localGroup, flowManager, parentGroupId,
propertyName, ancestorServiceX, internalServiceId, false,
+ "Switching to a service that is not an accessible ancestor
(e.g. internal to the group) must remain a reported change");
+
+ assertExternalServiceChange(localGroup, flowManager, parentGroupId,
propertyName, "foreign-1", "foreign-2", false,
+ "A change between two non-ancestor services must remain a
reported change");
+ }
+
+ private void assertExternalServiceChange(final
InstantiatedVersionedProcessGroup localGroup, final FlowManager flowManager,
+ final String groupId, final
String propertyName, final String snapshotValue,
+ final String localValue, final
boolean expectedEnvironmental, final String message) {
+ final VersionedPropertyDescriptor descriptor = new
VersionedPropertyDescriptor();
+ descriptor.setName(propertyName);
+ descriptor.setDisplayName(propertyName);
+ descriptor.setDynamic(false);
+ descriptor.setIdentifiesControllerService(true);
+
+ final InstantiatedVersionedProcessor snapshotProcessor = new
InstantiatedVersionedProcessor("processor-instance", groupId);
+ snapshotProcessor.setComponentType(ComponentType.PROCESSOR);
+ snapshotProcessor.setIdentifier("processor-instance");
+ snapshotProcessor.setProperties(Map.of(propertyName, snapshotValue));
+ snapshotProcessor.setPropertyDescriptors(Map.of(propertyName,
descriptor));
+
+ final InstantiatedVersionedProcessor localProcessor = new
InstantiatedVersionedProcessor("processor-instance", groupId);
+ localProcessor.setComponentType(ComponentType.PROCESSOR);
+ localProcessor.setIdentifier("processor-instance");
+ localProcessor.setProperties(Map.of(propertyName, localValue));
+ localProcessor.setPropertyDescriptors(Map.of(propertyName,
descriptor));
+
+ final FlowDifference difference = new StandardFlowDifference(
+ DifferenceType.PROPERTY_CHANGED, snapshotProcessor,
localProcessor, propertyName, snapshotValue, localValue,
+ "Property '" + propertyName + "' changed");
+
+ final FlowDifferenceFilters.EnvironmentalChangeContext context =
+
FlowDifferenceFilters.buildEnvironmentalChangeContext(List.of(difference),
localGroup, flowManager);
+
+ if (expectedEnvironmental) {
+ assertTrue(FlowDifferenceFilters.isEnvironmentalChange(difference,
localGroup, flowManager, context), message);
+ } else {
+
assertFalse(FlowDifferenceFilters.isEnvironmentalChange(difference, localGroup,
flowManager, context), message);
+ }
+ }
+
@Test
public void
testPropertyRenameWithParameterizationObservedAsEnvironmentalChange() {
final FlowManager flowManager = Mockito.mock(FlowManager.class);
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 aca6949c908..ad6f1568e7c 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
@@ -6597,11 +6597,6 @@ public class StandardNiFiServiceFacade implements
NiFiServiceFacade {
final FlowSnapshotContainer flowSnapshotContainer =
flowRegistry.getFlowContents(FlowRegistryClientContextFactory.getContextForUser(NiFiUserUtils.getNiFiUser()),
flowVersionLocation, true);
- // Resolve external controller service references by name so that
cross-instance
- // ID differences do not appear as phantom local modifications.
- final String parentGroupId = processGroup.getParent() == null ?
processGroup.getIdentifier() : processGroup.getParent().getIdentifier();
-
controllerFacade.getControllerServiceResolver().resolveInheritedControllerServices(flowSnapshotContainer,
parentGroupId, NiFiUserUtils.getNiFiUser());
-
final RegisteredFlowSnapshot versionedFlowSnapshot =
flowSnapshotContainer.getFlowSnapshot();
registryGroup = versionedFlowSnapshot.getFlowContents();
} catch (final IOException | FlowRegistryException e) {
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
index 1df40ad87db..5c2a35e18b7 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
@@ -2856,7 +2856,7 @@ public final class DtoFactory {
final Collection<FlowDifference> comparisonDifferences =
comparison.getDifferences();
final FlowDifferenceFilters.EnvironmentalChangeContext
environmentalContext =
-
FlowDifferenceFilters.buildEnvironmentalChangeContext(comparisonDifferences,
flowManager);
+
FlowDifferenceFilters.buildEnvironmentalChangeContext(comparisonDifferences,
localGroup, flowManager);
for (final FlowDifference difference : comparisonDifferences) {
// capture bundle differences and dedupe those differences
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 d74000f7935..1ee3a7a029a 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
@@ -61,9 +61,9 @@ public class ExternalControllerServiceVersioningIT extends
NiFiSystemIT {
* then the original PG and service are removed and a new service with the
same name (but
* different ID) is created before re-importing from the registry.
*
- * After import, the flow should be UP_TO_DATE because the external
service was resolved
- * by name during import and the cached snapshot should also be resolved.
- * Both the state badge and the "Show Local Changes" dialog should agree.
+ * After import, the flow should be UP_TO_DATE: the committed reference
points at the (now-removed) dev service id, which
+ * is not a locally-accessible ancestor service, while the local reference
points at the same-named prod service, so the
+ * external-service reference change is treated as environment-specific.
Both the badge and the dialog should agree.
*/
@Test
public void testCrossInstanceImportWithExternalServiceShowsUpToDate()
throws NiFiClientException, IOException, InterruptedException {
@@ -106,6 +106,57 @@ public class ExternalControllerServiceVersioningIT extends
NiFiSystemIT {
"After cross-instance import, Show Local Changes should report
no differences");
}
+ /**
+ * A flow committed against one external service is imported into an
instance whose equivalent external service has a
+ * DIFFERENT name (and a different id). After pointing the processor at
the local service, the flow is UP_TO_DATE with no
+ * local modifications, because an external controller service reference
is environment-specific regardless of its name or id.
+ */
+ @Test
+ public void
testCrossInstanceImportWithDifferentlyNamedExternalServiceShowsUpToDate()
throws NiFiClientException, IOException, InterruptedException {
+ final FlowRegistryClientEntity registryClient = registerClient();
+ final NiFiClientUtil util = getClientUtil();
+
+ final ControllerServiceEntity devService =
util.createControllerService(COUNT_SERVICE_TYPE, "root");
+ util.enableControllerService(devService);
+
+ final ProcessGroupEntity child = util.createProcessGroup("Child",
"root");
+ final ProcessorEntity counter = util.createProcessor("CountFlowFiles",
child.getId());
+ util.updateProcessorProperties(counter,
Collections.singletonMap("Count Service", devService.getComponent().getId()));
+ final ProcessorEntity terminate =
util.createProcessor("TerminateFlowFile", child.getId());
+ util.createConnection(counter, terminate, "success");
+
+ final VersionControlInformationEntity vci =
util.startVersionControl(child, registryClient, TEST_FLOWS_BUCKET,
"cross-instance-diff-name-flow");
+ util.assertFlowUpToDate(child.getId());
+ final VersionControlInformationDTO vciDto =
vci.getVersionControlInformation();
+
+ getNifiClient().getVersionsClient().stopVersionControl(
+
getNifiClient().getProcessGroupClient().getProcessGroup(child.getId()));
+ deleteProcessGroupContents(child.getId());
+ getNifiClient().getProcessGroupClient().deleteProcessGroup(
+
getNifiClient().getProcessGroupClient().getProcessGroup(child.getId()));
+
+ deleteControllerService(devService);
+
+ ControllerServiceEntity prodService =
util.createControllerService(COUNT_SERVICE_TYPE, "root");
+ prodService = renameControllerService(prodService,
"DifferentlyNamedCountService");
+ assertNotEquals(devService.getComponent().getId(),
prodService.getComponent().getId(),
+ "Prod service should have a different ID than dev service");
+ util.enableControllerService(prodService);
+
+ final ProcessGroupEntity imported =
util.importFlowFromRegistry("root", vciDto.getRegistryId(),
+ vciDto.getBucketId(), vciDto.getFlowId(), vciDto.getVersion());
+
+ // Point the imported processor at the differently-named local
external service.
+ final ProcessorEntity importedCounter =
findProcessorByType(imported.getId(), "CountFlowFiles");
+ util.updateProcessorProperties(importedCounter,
Collections.singletonMap("Count Service", prodService.getComponent().getId()));
+
+ waitForVersionedFlowState(imported.getId(), "root", "UP_TO_DATE");
+
+ final FlowComparisonEntity localMods =
getNifiClient().getProcessGroupClient().getLocalModifications(imported.getId());
+ assertTrue(localMods.getComponentDifferences().isEmpty(),
+ "After pointing at a differently-named external service, Show
Local Changes should report no differences");
+ }
+
/**
* Simulates a cross-instance upgrade where v1 and v2 both reference the
same external
* service but differ in a non-service property (scheduling period). The
original PG and
@@ -167,17 +218,16 @@ public class ExternalControllerServiceVersioningIT
extends NiFiSystemIT {
}
/**
- * Reproduces the NIFI-15697 scenario on a single instance:
- *
- * 1. Create an external service "StandardCountService" and a child PG
referencing it, commit.
- * 2. Create a second external service "AlternateCountService", switch the
processor to it.
- * 3. Delete the original service.
- * 4. Verify that both the state badge (LOCALLY_MODIFIED) and the dialog
(shows the change) agree.
+ * Switching a processor's reference from one external service to another
and then deleting the originally-referenced service
+ * keeps the state badge and the "Show Local Changes" dialog consistent at
every step.
*
- * The two services have different names so the name-based resolver cannot
falsely reconcile them.
+ * While both services exist, the switch is a genuine, reported local
change (both are locally-accessible ancestor services).
+ * Once the originally-referenced service is removed, the committed
reference no longer resolves to a local service, so the
+ * change becomes environment-specific and the flow returns to UP_TO_DATE.
The badge and the dialog agree throughout -- there
+ * is never a badge that reports LOCALLY_MODIFIED while the dialog reports
no changes.
*/
@Test
- public void
testSwitchExternalServiceAndDeleteOriginalShowsLocalModification() throws
NiFiClientException, IOException, InterruptedException {
+ public void
testSwitchExternalServiceAndDeleteOriginalKeepsBadgeAndDialogConsistent()
throws NiFiClientException, IOException, InterruptedException {
final FlowRegistryClientEntity registryClient = registerClient();
final NiFiClientUtil util = getClientUtil();
@@ -199,22 +249,19 @@ public class ExternalControllerServiceVersioningIT
extends NiFiSystemIT {
util.updateProcessorProperties(counter,
Collections.singletonMap("Count Service", serviceB.getComponent().getId()));
- String state = util.getVersionedFlowState(child.getId(), "root");
- assertEquals("LOCALLY_MODIFIED", state, "After switching external
service reference, PG should be LOCALLY_MODIFIED");
+ // While both external services exist, switching between them is a
genuine local change; badge and dialog must agree.
+ final String stateAfterSwitch =
util.getVersionedFlowState(child.getId(), "root");
+ assertEquals("LOCALLY_MODIFIED", stateAfterSwitch, "While both
external services exist, switching the reference should be LOCALLY_MODIFIED");
+
assertFalse(getNifiClient().getProcessGroupClient().getLocalModifications(child.getId()).getComponentDifferences().isEmpty(),
+ "Badge is LOCALLY_MODIFIED, so the dialog must also report
differences");
deleteControllerService(serviceA);
- state = util.getVersionedFlowState(child.getId(), "root");
- assertEquals("LOCALLY_MODIFIED", state, "After deleting original
service, PG should still be LOCALLY_MODIFIED");
-
- final FlowComparisonEntity localMods =
getNifiClient().getProcessGroupClient().getLocalModifications(child.getId());
- assertFalse(localMods.getComponentDifferences().isEmpty(), "Show Local
Changes should report differences");
-
- final boolean hasPropertyValueChange =
localMods.getComponentDifferences().stream()
- .flatMap(dto -> dto.getDifferences().stream())
- .map(DifferenceDTO::getDifferenceType)
- .anyMatch(type -> type.contains("Property Value Changed"));
- assertTrue(hasPropertyValueChange, "Differences should include a
Property Value Changed difference");
+ // Once the originally-referenced external service is gone, the
reference change is environment-specific: the flow
+ // returns to UP_TO_DATE and the dialog reports no differences. Badge
and dialog agree (no stuck LOCALLY_MODIFIED).
+ waitForVersionedFlowState(child.getId(), "root", "UP_TO_DATE");
+
assertTrue(getNifiClient().getProcessGroupClient().getLocalModifications(child.getId()).getComponentDifferences().isEmpty(),
+ "After removing the original external service, the dialog must
report no differences (consistent with the UP_TO_DATE badge)");
}
/**
@@ -262,6 +309,14 @@ public class ExternalControllerServiceVersioningIT extends
NiFiSystemIT {
}
}
+ private ProcessorEntity findProcessorByType(final String groupId, final
String simpleType) throws NiFiClientException, IOException {
+ final ProcessGroupFlowEntity flowEntity =
getNifiClient().getFlowClient().getProcessGroup(groupId);
+ return
flowEntity.getProcessGroupFlow().getFlow().getProcessors().stream()
+ .filter(processor ->
processor.getComponent().getType().endsWith(simpleType))
+ .findFirst()
+ .orElseThrow(() -> new AssertionError("Could not find
processor of type " + simpleType + " in group " + groupId));
+ }
+
private ControllerServiceEntity renameControllerService(final
ControllerServiceEntity service, final String newName)
throws NiFiClientException, IOException {
final ControllerServiceDTO dto = new ControllerServiceDTO();