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

Reply via email to