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 a60401d22b5 NIFI-14944 Fixed upgrade when CS reference goes from
external to scoped Controller Service (#10286)
a60401d22b5 is described below
commit a60401d22b58b2a7700fe58920f23b1d006cb57c
Author: Pierre Villard <[email protected]>
AuthorDate: Fri Aug 14 15:47:57 2026 +0200
NIFI-14944 Fixed upgrade when CS reference goes from external to scoped
Controller Service (#10286)
Signed-off-by: David Handermann <[email protected]>
---
.../StandardVersionedComponentSynchronizer.java | 38 +++++------
...StandardVersionedComponentSynchronizerTest.java | 75 ++++++++++++++++++++++
2 files changed, 90 insertions(+), 23 deletions(-)
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/flow/synchronization/StandardVersionedComponentSynchronizer.java
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/flow/synchronization/StandardVersionedComponentSynchronizer.java
index 62c666d664c..0b233d489bb 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/flow/synchronization/StandardVersionedComponentSynchronizer.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/flow/synchronization/StandardVersionedComponentSynchronizer.java
@@ -1672,34 +1672,26 @@ public class StandardVersionedComponentSynchronizer
implements VersionedComponen
final String proposedValue =
proposedProperties.get(propertyName);
final String value;
if (descriptor != null && referencesService && proposedValue
!= null && !isReferencingParameter(proposedValue)) {
- // Need to determine if the component's property
descriptor for this service is already set to an id
- // of an existing service that is outside the current
processor group, and if it is, we want to leave
- // the property set to that value
- String existingExternalServiceId = null;
- final String componentDescriptorValue =
componentNode.getEffectivePropertyValue(descriptor);
- if (componentDescriptorValue != null) {
- final ProcessGroup parentGroup =
topLevelGroup.getParent();
- if (parentGroup != null) {
- final ControllerServiceNode serviceNode =
parentGroup.findControllerService(componentDescriptorValue, false, true);
- if (serviceNode != null) {
- existingExternalServiceId =
componentDescriptorValue;
- }
- }
- }
+ final String instanceId =
getServiceInstanceId(proposedValue, group);
- // If the component's property descriptor is not already
set to an id of an existing external service,
- // then we need to take the Versioned Component ID and
resolve this to the instance ID of the service
- if (existingExternalServiceId == null) {
- String instanceId =
getServiceInstanceId(proposedValue, group);
- value = (instanceId == null) ? proposedValue :
instanceId;
-
- // Find the same property descriptor in the
component's CreatedExtension and replace it with the
- // instance ID of the service
+ if (instanceId != null) {
+ value = instanceId;
createdAndModifiedExtensions.stream().filter(ce ->
ce.extension.equals(componentNode)).forEach(createdOrModifiedExtension -> {
createdOrModifiedExtension.propertyValues.replace(propertyName, value);
});
} else {
- value = existingExternalServiceId;
+ final String componentDescriptorValue =
componentNode.getEffectivePropertyValue(descriptor);
+ final ProcessGroup parentGroup =
topLevelGroup.getParent();
+ final ControllerServiceNode externalService =
componentDescriptorValue == null || parentGroup == null
+ ? null
+ :
parentGroup.findControllerService(componentDescriptorValue, false, true);
+
+ value = externalService == null ? proposedValue :
componentDescriptorValue;
+ if (externalService == null) {
+ createdAndModifiedExtensions.stream().filter(ce ->
ce.extension.equals(componentNode)).forEach(createdOrModifiedExtension -> {
+
createdOrModifiedExtension.propertyValues.replace(propertyName, value);
+ });
+ }
}
} else {
value = proposedValue;
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/flow/synchronization/StandardVersionedComponentSynchronizerTest.java
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/flow/synchronization/StandardVersionedComponentSynchronizerTest.java
index a5709712834..ec977b5453f 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/flow/synchronization/StandardVersionedComponentSynchronizerTest.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/flow/synchronization/StandardVersionedComponentSynchronizerTest.java
@@ -1070,6 +1070,81 @@ public class StandardVersionedComponentSynchronizerTest {
assertNull(properties.get("cs"));
}
+ @Test
+ public void testScopedControllerServiceReplacesExternalReference() throws
FlowSynchronizationException, InterruptedException, TimeoutException {
+ // A processor currently references a service outside the versioned
group. The proposed flow introduces a
+ // resolvable scoped service, which must take precedence over the
existing external reference.
+ final String externalServiceId = "external-service-id";
+ final String scopedServiceId = "scoped-service-id";
+ final String scopedServiceVersionedId = "scoped-service-versioned-id";
+
+ final PropertyDescriptor descriptor = new
PropertyDescriptor.Builder().name("cs")
+ .identifiesControllerService(ControllerService.class).build();
+ final VersionedPropertyDescriptor versionedDescriptor = new
VersionedPropertyDescriptor();
+ versionedDescriptor.setName(descriptor.getName());
+ versionedDescriptor.setIdentifiesControllerService(true);
+
+ final ProcessorNode processorNode = createMockProcessor();
+ final ProcessGroup parentGroup = mock(ProcessGroup.class);
+
when(processorNode.getPropertyDescriptor(descriptor.getName())).thenReturn(descriptor);
+ when(processorNode.getProperties()).thenReturn(Map.of(descriptor, new
PropertyConfiguration(externalServiceId, null, null, null)));
+
when(processorNode.getRawPropertyValues()).thenReturn(Map.of(descriptor,
externalServiceId));
+
when(processorNode.getEffectivePropertyValue(descriptor)).thenReturn(externalServiceId);
+ when(group.getParent()).thenReturn(parentGroup);
+
+ final ControllerServiceNode scopedService =
createMockControllerService();
+ when(scopedService.getIdentifier()).thenReturn(scopedServiceId);
+
when(scopedService.getVersionedComponentId()).thenReturn(Optional.of(scopedServiceVersionedId));
+
when(group.getControllerServices(false)).thenReturn(Set.of(scopedService));
+
+ final ControllerServiceNode externalService =
createMockControllerService();
+ when(parentGroup.findControllerService(externalServiceId, false,
true)).thenReturn(externalService);
+
+ final VersionedProcessor versionedProcessor =
createMinimalVersionedProcessor();
+ versionedProcessor.setPropertyDescriptors(Map.of(descriptor.getName(),
versionedDescriptor));
+ versionedProcessor.setProperties(Map.of(descriptor.getName(),
scopedServiceVersionedId));
+
+ final ArgumentCaptor<Map<String, String>> captor =
ArgumentCaptor.captor();
+ synchronizer.synchronize(processorNode, versionedProcessor, group,
synchronizationOptions);
+ verify(processorNode).setProperties(captor.capture(), anyBoolean(),
any());
+
+ assertEquals(scopedServiceId,
captor.getValue().get(descriptor.getName()));
+ }
+
+ @Test
+ public void
testExternalControllerServiceRetainedWhenProposedServiceNotResolvable() throws
FlowSynchronizationException, InterruptedException, TimeoutException {
+ // When the proposed service is not visible from the component's
group, preserve the existing external reference
+ // instead of replacing it with an unresolved versioned component
identifier.
+ final String externalServiceId = "external-service-id";
+ final String proposedServiceVersionedId =
"unresolved-service-versioned-id";
+
+ final PropertyDescriptor descriptor = new
PropertyDescriptor.Builder().name("cs")
+ .identifiesControllerService(ControllerService.class).build();
+ final VersionedPropertyDescriptor versionedDescriptor = new
VersionedPropertyDescriptor();
+ versionedDescriptor.setName(descriptor.getName());
+ versionedDescriptor.setIdentifiesControllerService(true);
+
+ final ProcessorNode processorNode = createMockProcessor();
+ final ProcessGroup parentGroup = mock(ProcessGroup.class);
+ final ControllerServiceNode externalService =
createMockControllerService();
+
when(processorNode.getPropertyDescriptor(descriptor.getName())).thenReturn(descriptor);
+ when(processorNode.getProperties()).thenReturn(Map.of(descriptor, new
PropertyConfiguration(externalServiceId, null, null, null)));
+
when(processorNode.getRawPropertyValues()).thenReturn(Map.of(descriptor,
externalServiceId));
+
when(processorNode.getEffectivePropertyValue(descriptor)).thenReturn(externalServiceId);
+ when(group.getParent()).thenReturn(parentGroup);
+ when(parentGroup.findControllerService(externalServiceId, false,
true)).thenReturn(externalService);
+
+ final VersionedProcessor versionedProcessor =
createMinimalVersionedProcessor();
+ versionedProcessor.setPropertyDescriptors(Map.of(descriptor.getName(),
versionedDescriptor));
+ versionedProcessor.setProperties(Map.of(descriptor.getName(),
proposedServiceVersionedId));
+
+ final ArgumentCaptor<Map<String, String>> captor =
ArgumentCaptor.captor();
+ synchronizer.synchronize(processorNode, versionedProcessor, group,
synchronizationOptions);
+ verify(processorNode).setProperties(captor.capture(), anyBoolean(),
any());
+
+ assertEquals(externalServiceId,
captor.getValue().get(descriptor.getName()));
+ }
+
@Test
public void testExternalControllerServiceParameterReferencePreserved()
throws FlowSynchronizationException, InterruptedException, TimeoutException {
// A controller-service-identifying property is configured with a
Parameter reference (#{svc}) that resolves to a