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