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 2913d97e7b NIFI-13838 Allow Change Version on a ghosted component when 
bundle becomes available (#10275)
2913d97e7b is described below

commit 2913d97e7b3f73de9ea0e8bc95ad1570c853d098
Author: Pierre Villard <[email protected]>
AuthorDate: Mon Sep 8 20:09:57 2025 +0200

    NIFI-13838 Allow Change Version on a ghosted component when bundle becomes 
available (#10275)
    
    Signed-off-by: David Handermann <[email protected]>
---
 .../org/apache/nifi/web/api/dto/DtoFactory.java    |  18 ++-
 .../apache/nifi/web/api/dto/DtoFactoryTest.java    | 174 +++++++++++++++++++++
 2 files changed, 186 insertions(+), 6 deletions(-)

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 920180b208..6b6b66fade 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
@@ -1607,7 +1607,8 @@ public final class DtoFactory {
        dto.setRestricted(reportingTaskNode.isRestricted());
        dto.setDeprecated(reportingTaskNode.isDeprecated());
        dto.setExtensionMissing(reportingTaskNode.isExtensionMissing());
-       dto.setMultipleVersionsAvailable(compatibleBundles.size() > 1);
+       // Enable changing version on ghost reporting tasks when at least one 
compatible bundle exists
+       dto.setMultipleVersionsAvailable(reportingTaskNode.isExtensionMissing() 
? !compatibleBundles.isEmpty() : compatibleBundles.size() > 1);
 
        final Map<String, String> defaultSchedulingPeriod = new HashMap<>();
        defaultSchedulingPeriod.put(SchedulingStrategy.TIMER_DRIVEN.name(), 
SchedulingStrategy.TIMER_DRIVEN.getDefaultSchedulingPeriod());
@@ -1695,7 +1696,8 @@ public final class DtoFactory {
        dto.setRestricted(parameterProviderNode.isRestricted());
        dto.setDeprecated(parameterProviderNode.isDeprecated());
        dto.setExtensionMissing(parameterProviderNode.isExtensionMissing());
-       dto.setMultipleVersionsAvailable(compatibleBundles.size() > 1);
+       // Enable changing version on ghost parameter providers when at least 
one compatible bundle exists
+       
dto.setMultipleVersionsAvailable(parameterProviderNode.isExtensionMissing() ? 
!compatibleBundles.isEmpty() : compatibleBundles.size() > 1);
 
        // sort a copy of the properties
        final Map<PropertyDescriptor, String> sortedProperties = new 
TreeMap<>((o1, o2) -> Collator.getInstance(Locale.US).compare(o1.getName(), 
o2.getName()));
@@ -1807,7 +1809,8 @@ public final class DtoFactory {
        dto.setRestricted(controllerServiceNode.isRestricted());
        dto.setDeprecated(controllerServiceNode.isDeprecated());
        dto.setExtensionMissing(controllerServiceNode.isExtensionMissing());
-       dto.setMultipleVersionsAvailable(compatibleBundles.size() > 1);
+       // Enable changing version on ghost controller services when at least 
one compatible bundle exists
+       
dto.setMultipleVersionsAvailable(controllerServiceNode.isExtensionMissing() ? 
!compatibleBundles.isEmpty() : compatibleBundles.size() > 1);
        
dto.setVersionedComponentId(controllerServiceNode.getVersionedComponentId().orElse(null));
 
        // sort a copy of the properties
@@ -3339,7 +3342,8 @@ public final class DtoFactory {
        dto.setDeprecated(node.isDeprecated());
        dto.setExecutionNodeRestricted(node.isExecutionNodeRestricted());
        dto.setExtensionMissing(node.isExtensionMissing());
-       dto.setMultipleVersionsAvailable(compatibleBundleCount > 1);
+       // Enable changing version on ghost processors when at least one 
compatible bundle exists
+       dto.setMultipleVersionsAvailable(node.isExtensionMissing() ? 
compatibleBundleCount > 0 : compatibleBundleCount > 1);
        
dto.setVersionedComponentId(node.getVersionedComponentId().orElse(null));
 
        dto.setType(node.getCanonicalClassName());
@@ -4929,7 +4933,8 @@ public final class DtoFactory {
        dto.setRestricted(flowRegistryClientNode.isRestricted());
        dto.setDeprecated(flowRegistryClientNode.isDeprecated());
        dto.setExtensionMissing(flowRegistryClientNode.isExtensionMissing());
-       dto.setMultipleVersionsAvailable(compatibleBundles.size() > 1);
+       // Enable changing version on ghost flow registry clients when at least 
one compatible bundle exists
+       
dto.setMultipleVersionsAvailable(flowRegistryClientNode.isExtensionMissing() ? 
!compatibleBundles.isEmpty() : compatibleBundles.size() > 1);
 
        // sort a copy of the properties
        final Map<PropertyDescriptor, String> sortedProperties = new 
TreeMap<>((o1, o2) -> Collator.getInstance(Locale.US).compare(o1.getName(), 
o2.getName()));
@@ -5005,7 +5010,8 @@ public final class DtoFactory {
        dto.setRestricted(flowAnalysisRuleNode.isRestricted());
        dto.setDeprecated(flowAnalysisRuleNode.isDeprecated());
        dto.setExtensionMissing(flowAnalysisRuleNode.isExtensionMissing());
-       dto.setMultipleVersionsAvailable(compatibleBundles.size() > 1);
+       // Enable changing version on ghost flow analysis rules when at least 
one compatible bundle exists
+       
dto.setMultipleVersionsAvailable(flowAnalysisRuleNode.isExtensionMissing() ? 
!compatibleBundles.isEmpty() : compatibleBundles.size() > 1);
 
        // sort a copy of the properties
        final Map<PropertyDescriptor, String> sortedProperties = new TreeMap<>(
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/api/dto/DtoFactoryTest.java
 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/api/dto/DtoFactoryTest.java
index e6acb76644..cb307412ef 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/api/dto/DtoFactoryTest.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/api/dto/DtoFactoryTest.java
@@ -16,11 +16,17 @@
  */
 package org.apache.nifi.web.api.dto;
 
+import org.apache.nifi.bundle.Bundle;
+import org.apache.nifi.bundle.BundleCoordinate;
+import org.apache.nifi.bundle.BundleDetails;
 import org.apache.nifi.components.AllowableValue;
 import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.validation.ValidationStatus;
 import org.apache.nifi.controller.ControllerService;
 import org.apache.nifi.controller.service.ControllerServiceNode;
 import org.apache.nifi.controller.service.ControllerServiceProvider;
+import org.apache.nifi.controller.service.ControllerServiceState;
+import org.apache.nifi.logging.LogLevel;
 import org.apache.nifi.nar.ExtensionManager;
 import org.apache.nifi.nar.NarManifest;
 import org.apache.nifi.nar.NarNode;
@@ -28,6 +34,7 @@ import org.apache.nifi.nar.NarSource;
 import org.apache.nifi.nar.NarState;
 import org.apache.nifi.nar.StandardExtensionDiscoveringManager;
 import org.apache.nifi.nar.SystemBundle;
+import org.apache.nifi.registry.flow.FlowRegistryClientNode;
 import org.apache.nifi.web.api.entity.AllowableValueEntity;
 import org.junit.jupiter.api.Test;
 import org.slf4j.Logger;
@@ -47,6 +54,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
@@ -222,4 +230,170 @@ public class DtoFactoryTest {
         assertEquals(narManifest.getDependencyId(), 
coordinateDTO.getArtifact());
         assertEquals(narManifest.getDependencyVersion(), 
coordinateDTO.getVersion());
     }
+
+    private Bundle createBundle(final String group, final String id, final 
String version) {
+        final BundleCoordinate coordinate = new BundleCoordinate(group, id, 
version);
+        final BundleDetails details = new BundleDetails.Builder()
+                .workingDir(new File("."))
+                .coordinate(coordinate)
+                .build();
+        return new Bundle(details, getClass().getClassLoader());
+    }
+
+    @Test
+    void 
testControllerServiceMultipleVersionsAvailableGhostWithOneCompatibleBundle() {
+        final String group = "com.example";
+        final String id = "test-service";
+
+        final BundleCoordinate currentCoordinate = new BundleCoordinate(group, 
id, "1.0.0");
+        final Bundle compatible = createBundle(group, id, "1.1.0");
+
+        final ExtensionManager extensionManager = mock(ExtensionManager.class);
+        final String canonicalClassName = "com.example.ControllerService";
+        
when(extensionManager.getBundles(canonicalClassName)).thenReturn(Collections.singletonList(compatible));
+
+        final ControllerServiceNode serviceNode = 
mock(ControllerServiceNode.class);
+        when(serviceNode.getIdentifier()).thenReturn("svc-1");
+        when(serviceNode.getName()).thenReturn("Service");
+        
when(serviceNode.getCanonicalClassName()).thenReturn(canonicalClassName);
+        when(serviceNode.getBundleCoordinate()).thenReturn(currentCoordinate);
+        when(serviceNode.getAnnotationData()).thenReturn(null);
+        when(serviceNode.getComments()).thenReturn(null);
+        when(serviceNode.getBulletinLevel()).thenReturn(LogLevel.INFO);
+        
when(serviceNode.getState()).thenReturn(ControllerServiceState.DISABLED);
+        
when(serviceNode.isSupportsSensitiveDynamicProperties()).thenReturn(false);
+        when(serviceNode.isRestricted()).thenReturn(false);
+        when(serviceNode.isDeprecated()).thenReturn(false);
+        when(serviceNode.isExtensionMissing()).thenReturn(true); // ghost 
component
+        
when(serviceNode.getVersionedComponentId()).thenReturn(java.util.Optional.empty());
+        
when(serviceNode.getRawPropertyValues()).thenReturn(Collections.emptyMap());
+        final ControllerService controllerService = 
mock(ControllerService.class);
+        
when(controllerService.getPropertyDescriptors()).thenReturn(Collections.emptyList());
+        
when(serviceNode.getControllerServiceImplementation()).thenReturn(controllerService);
+        when(serviceNode.getValidationStatus(anyLong(), 
any())).thenReturn(ValidationStatus.VALID);
+        
when(serviceNode.getValidationErrors()).thenReturn(Collections.emptyList());
+
+        final DtoFactory dtoFactory = new DtoFactory();
+        dtoFactory.setExtensionManager(extensionManager);
+
+        final ControllerServiceDTO dto = 
dtoFactory.createControllerServiceDto(serviceNode);
+        assertTrue(dto.getMultipleVersionsAvailable(), "Ghost service with one 
compatible bundle should allow change version");
+    }
+
+    @Test
+    void 
testControllerServiceMultipleVersionsAvailableNotGhostWithOneCompatibleBundle() 
{
+        final String group = "com.example";
+        final String id = "test-service";
+
+        final BundleCoordinate currentCoordinate = new BundleCoordinate(group, 
id, "1.0.0");
+        final Bundle compatible = createBundle(group, id, "1.1.0");
+
+        final ExtensionManager extensionManager = mock(ExtensionManager.class);
+        final String canonicalClassName = "com.example.ControllerService";
+        
when(extensionManager.getBundles(canonicalClassName)).thenReturn(Collections.singletonList(compatible));
+
+        final ControllerServiceNode serviceNode = 
mock(ControllerServiceNode.class);
+        when(serviceNode.getIdentifier()).thenReturn("svc-1");
+        when(serviceNode.getName()).thenReturn("Service");
+        
when(serviceNode.getCanonicalClassName()).thenReturn(canonicalClassName);
+        when(serviceNode.getBundleCoordinate()).thenReturn(currentCoordinate);
+        when(serviceNode.getAnnotationData()).thenReturn(null);
+        when(serviceNode.getComments()).thenReturn(null);
+        when(serviceNode.getBulletinLevel()).thenReturn(LogLevel.INFO);
+        
when(serviceNode.getState()).thenReturn(ControllerServiceState.DISABLED);
+        
when(serviceNode.isSupportsSensitiveDynamicProperties()).thenReturn(false);
+        when(serviceNode.isRestricted()).thenReturn(false);
+        when(serviceNode.isDeprecated()).thenReturn(false);
+        when(serviceNode.isExtensionMissing()).thenReturn(false); // not ghost
+        
when(serviceNode.getVersionedComponentId()).thenReturn(java.util.Optional.empty());
+        
when(serviceNode.getRawPropertyValues()).thenReturn(Collections.emptyMap());
+        final ControllerService controllerService = 
mock(ControllerService.class);
+        
when(controllerService.getPropertyDescriptors()).thenReturn(Collections.emptyList());
+        
when(serviceNode.getControllerServiceImplementation()).thenReturn(controllerService);
+        when(serviceNode.getValidationStatus(anyLong(), 
any())).thenReturn(ValidationStatus.VALID);
+        
when(serviceNode.getValidationErrors()).thenReturn(Collections.emptyList());
+
+        final DtoFactory dtoFactory = new DtoFactory();
+        dtoFactory.setExtensionManager(extensionManager);
+
+        final ControllerServiceDTO dto = 
dtoFactory.createControllerServiceDto(serviceNode);
+        assertFalse(dto.getMultipleVersionsAvailable(), "Non-ghost service 
with one compatible bundle should not allow change version");
+    }
+
+    @Test
+    void 
testControllerServiceMultipleVersionsAvailableNotGhostWithTwoCompatibleBundles()
 {
+        final String group = "com.example";
+        final String id = "test-service";
+
+        final BundleCoordinate currentCoordinate = new BundleCoordinate(group, 
id, "1.0.0");
+        final Bundle compatible1 = createBundle(group, id, "1.1.0");
+        final Bundle compatible2 = createBundle(group, id, "1.2.0");
+
+        final ExtensionManager extensionManager = mock(ExtensionManager.class);
+        final String canonicalClassName = "com.example.ControllerService";
+        
when(extensionManager.getBundles(canonicalClassName)).thenReturn(Arrays.asList(compatible1,
 compatible2));
+
+        final ControllerServiceNode serviceNode = 
mock(ControllerServiceNode.class);
+        when(serviceNode.getIdentifier()).thenReturn("svc-1");
+        when(serviceNode.getName()).thenReturn("Service");
+        
when(serviceNode.getCanonicalClassName()).thenReturn(canonicalClassName);
+        when(serviceNode.getBundleCoordinate()).thenReturn(currentCoordinate);
+        when(serviceNode.getAnnotationData()).thenReturn(null);
+        when(serviceNode.getComments()).thenReturn(null);
+        when(serviceNode.getBulletinLevel()).thenReturn(LogLevel.INFO);
+        
when(serviceNode.getState()).thenReturn(ControllerServiceState.DISABLED);
+        
when(serviceNode.isSupportsSensitiveDynamicProperties()).thenReturn(false);
+        when(serviceNode.isRestricted()).thenReturn(false);
+        when(serviceNode.isDeprecated()).thenReturn(false);
+        when(serviceNode.isExtensionMissing()).thenReturn(false); // not ghost
+        
when(serviceNode.getVersionedComponentId()).thenReturn(java.util.Optional.empty());
+        
when(serviceNode.getRawPropertyValues()).thenReturn(Collections.emptyMap());
+        final ControllerService controllerService = 
mock(ControllerService.class);
+        
when(controllerService.getPropertyDescriptors()).thenReturn(Collections.emptyList());
+        
when(serviceNode.getControllerServiceImplementation()).thenReturn(controllerService);
+        when(serviceNode.getValidationStatus(anyLong(), 
any())).thenReturn(ValidationStatus.VALID);
+        
when(serviceNode.getValidationErrors()).thenReturn(Collections.emptyList());
+
+        final DtoFactory dtoFactory = new DtoFactory();
+        dtoFactory.setExtensionManager(extensionManager);
+
+        final ControllerServiceDTO dto = 
dtoFactory.createControllerServiceDto(serviceNode);
+        assertTrue(dto.getMultipleVersionsAvailable(), "Non-ghost service with 
two compatible bundles should allow change version");
+    }
+
+    @Test
+    void 
testFlowRegistryClientMultipleVersionsAvailableGhostWithOneCompatibleBundle() {
+        final String group = "com.example";
+        final String id = "test-registry-client";
+
+        final BundleCoordinate currentCoordinate = new BundleCoordinate(group, 
id, "1.0.0");
+        final Bundle compatible = createBundle(group, id, "1.1.0");
+
+        final ExtensionManager extensionManager = mock(ExtensionManager.class);
+        final String canonicalClassName = "com.example.FlowRegistryClient";
+        
when(extensionManager.getBundles(canonicalClassName)).thenReturn(Collections.singletonList(compatible));
+
+        final FlowRegistryClientNode clientNode = 
mock(FlowRegistryClientNode.class);
+        when(clientNode.getIdentifier()).thenReturn("client-1");
+        when(clientNode.getName()).thenReturn("Client");
+        when(clientNode.getDescription()).thenReturn("desc");
+        
when(clientNode.getCanonicalClassName()).thenReturn(canonicalClassName);
+        when(clientNode.getBundleCoordinate()).thenReturn(currentCoordinate);
+        when(clientNode.getAnnotationData()).thenReturn(null);
+        
when(clientNode.isSupportsSensitiveDynamicProperties()).thenReturn(false);
+        when(clientNode.isBranchingSupported()).thenReturn(false);
+        when(clientNode.isRestricted()).thenReturn(false);
+        when(clientNode.isDeprecated()).thenReturn(false);
+        when(clientNode.isExtensionMissing()).thenReturn(true); // ghost 
component
+        
when(clientNode.getRawPropertyValues()).thenReturn(Collections.emptyMap());
+        
when(clientNode.getPropertyDescriptors()).thenReturn(Collections.emptyList());
+        when(clientNode.getValidationStatus(anyLong(), 
any())).thenReturn(ValidationStatus.VALID);
+        
when(clientNode.getValidationErrors()).thenReturn(Collections.emptyList());
+
+        final DtoFactory dtoFactory = new DtoFactory();
+        dtoFactory.setExtensionManager(extensionManager);
+
+        final FlowRegistryClientDTO dto = 
dtoFactory.createRegistryDto(clientNode);
+        assertTrue(dto.getMultipleVersionsAvailable(), "Ghost registry client 
with one compatible bundle should allow change version");
+    }
 }

Reply via email to