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 6b1116e4b7f NIFI-16235 Support rebasing locally added Controller
Services in versioned Process Groups (#11571)
6b1116e4b7f is described below
commit 6b1116e4b7f087b91143d3a1fe1118f50579b617
Author: Pierre Villard <[email protected]>
AuthorDate: Mon Sep 21 15:37:41 2026 +0200
NIFI-16235 Support rebasing locally added Controller Services in versioned
Process Groups (#11571)
Signed-off-by: David Handermann <[email protected]>
---
.../flow/diff/ComponentAddedRebaseHandler.java | 124 +++++++++
.../nifi/registry/flow/diff/RebaseAnalysis.java | 15 ++
.../registry/flow/diff/RebaseConflictCode.java | 10 +
.../nifi/registry/flow/diff/RebaseHandler.java | 4 +
.../registry/flow/diff/RebaseHandlerUtils.java | 15 ++
.../registry/flow/diff/StandardRebaseEngine.java | 3 +-
.../flow/diff/ComponentAddedRebaseHandlerTest.java | 296 +++++++++++++++++++++
.../nifi/registry/flow/diff/RebaseEngineTest.java | 266 +++++++++++++++++-
.../FakeDynamicPropertiesControllerService.java | 46 ++++
.../org.apache.nifi.controller.ControllerService | 1 +
.../tests/system/registry/RebaseVersionIT.java | 218 +++++++++++++--
11 files changed, 975 insertions(+), 23 deletions(-)
diff --git
a/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/ComponentAddedRebaseHandler.java
b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/ComponentAddedRebaseHandler.java
new file mode 100644
index 00000000000..4fde24f6741
--- /dev/null
+++
b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/ComponentAddedRebaseHandler.java
@@ -0,0 +1,124 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.nifi.registry.flow.diff;
+
+import org.apache.nifi.flow.VersionedComponent;
+import org.apache.nifi.flow.VersionedControllerService;
+import org.apache.nifi.flow.VersionedProcessGroup;
+
+import java.util.HashSet;
+import java.util.Set;
+
+public class ComponentAddedRebaseHandler implements RebaseHandler {
+
+ private static final String NULL_COMPONENT_TYPE = "null";
+
+ @Override
+ public DifferenceType getSupportedType() {
+ return DifferenceType.COMPONENT_ADDED;
+ }
+
+ @Override
+ public RebaseAnalysis.ClassifiedDifference classify(final FlowDifference
localDifference, final Set<FlowDifference> upstreamDifferences,
+ final
VersionedProcessGroup targetSnapshot) {
+ final VersionedComponent addedComponent =
localDifference.getComponentB();
+ if (!(addedComponent instanceof VersionedControllerService
controllerService)) {
+ final String componentType = addedComponent == null ?
NULL_COMPONENT_TYPE : addedComponent.getClass().getSimpleName();
+ return
RebaseAnalysis.ClassifiedDifference.unsupported(localDifference,
RebaseConflictCode.UNSUPPORTED_COMPONENT_TYPE,
+ "Local component addition type %s is not supported for
rebase".formatted(componentType));
+ }
+
+ final String parentGroupIdentifier =
controllerService.getGroupIdentifier();
+ if (parentGroupIdentifier == null) {
+ return
RebaseAnalysis.ClassifiedDifference.unsupported(localDifference,
RebaseConflictCode.COMPONENT_NOT_FOUND,
+ "Controller Service %s does not specify a parent Process
Group".formatted(controllerService.getIdentifier()));
+ }
+
+ final VersionedProcessGroup parentGroup =
resolveParentGroup(targetSnapshot, parentGroupIdentifier, upstreamDifferences);
+ if (parentGroup == null) {
+ return
RebaseAnalysis.ClassifiedDifference.unsupported(localDifference,
RebaseConflictCode.COMPONENT_NOT_FOUND,
+ "Parent Process Group %s for Controller Service %s not
found in target snapshot"
+ .formatted(parentGroupIdentifier,
controllerService.getIdentifier()));
+ }
+
+ final VersionedComponent collidingComponent =
RebaseHandlerUtils.findComponentById(targetSnapshot,
controllerService.getIdentifier());
+ if (collidingComponent != null) {
+ return
RebaseAnalysis.ClassifiedDifference.conflicting(localDifference,
RebaseConflictCode.COMPONENT_IDENTIFIER_COLLISION,
+ "Target snapshot already contains component %s with
identifier %s"
+
.formatted(collidingComponent.getClass().getSimpleName(),
controllerService.getIdentifier()));
+ }
+
+ return RebaseAnalysis.ClassifiedDifference.compatible(localDifference,
parentGroup.getIdentifier());
+ }
+
+ @Override
+ public void apply(final FlowDifference localDifference, final
VersionedProcessGroup mergedFlow) {
+ final VersionedControllerService controllerService =
(VersionedControllerService) localDifference.getComponentB();
+ final VersionedProcessGroup parentGroup =
RebaseHandlerUtils.findProcessGroupById(mergedFlow,
controllerService.getGroupIdentifier());
+ apply(localDifference, mergedFlow, parentGroup,
controllerService.getGroupIdentifier());
+ }
+
+ @Override
+ public void apply(final RebaseAnalysis.ClassifiedDifference
classifiedDifference, final VersionedProcessGroup mergedFlow) {
+ final FlowDifference localDifference =
classifiedDifference.getDifference();
+ final String parentGroupIdentifier = (String)
classifiedDifference.getContext();
+ final VersionedProcessGroup parentGroup =
RebaseHandlerUtils.findProcessGroupById(mergedFlow, parentGroupIdentifier);
+ apply(localDifference, mergedFlow, parentGroup, parentGroupIdentifier);
+ }
+
+ private void apply(final FlowDifference localDifference, final
VersionedProcessGroup mergedFlow, final VersionedProcessGroup parentGroup,
+ final String parentGroupIdentifier) {
+ final VersionedControllerService controllerService =
(VersionedControllerService) localDifference.getComponentB();
+ if (parentGroup == null) {
+ throw new IllegalStateException("Parent Process Group %s for
Controller Service %s was verified during classification but is absent during
apply"
+ .formatted(parentGroupIdentifier,
controllerService.getIdentifier()));
+ }
+
+ controllerService.setGroupIdentifier(parentGroup.getIdentifier());
+
+ final VersionedComponent existingComponent =
RebaseHandlerUtils.findComponentById(mergedFlow,
controllerService.getIdentifier());
+ if (existingComponent != null) {
+ throw new IllegalStateException("Merged flow already contains
component %s with identifier %s"
+ .formatted(existingComponent.getClass().getSimpleName(),
controllerService.getIdentifier()));
+ }
+
+ final Set<VersionedControllerService> controllerServices =
parentGroup.getControllerServices();
+ if (controllerServices == null) {
+ parentGroup.setControllerServices(new HashSet<>());
+ }
+ parentGroup.getControllerServices().add(controllerService);
+ }
+
+ private VersionedProcessGroup resolveParentGroup(final
VersionedProcessGroup targetSnapshot, final String parentGroupIdentifier,
+ final Set<FlowDifference>
upstreamDifferences) {
+ final VersionedProcessGroup parentGroup =
RebaseHandlerUtils.findProcessGroupById(targetSnapshot, parentGroupIdentifier);
+ if (parentGroup != null) {
+ return parentGroup;
+ }
+
+ final boolean parentRemoved = upstreamDifferences.stream()
+ .filter(difference -> difference.getDifferenceType() ==
DifferenceType.COMPONENT_REMOVED)
+ .map(FlowDifference::getComponentA)
+ .filter(VersionedProcessGroup.class::isInstance)
+ .map(VersionedComponent::getIdentifier)
+ .anyMatch(parentGroupIdentifier::equals);
+
+ return parentRemoved ? null : targetSnapshot;
+ }
+
+}
diff --git
a/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/RebaseAnalysis.java
b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/RebaseAnalysis.java
index b5418139e39..0e0c4907aee 100644
---
a/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/RebaseAnalysis.java
+++
b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/RebaseAnalysis.java
@@ -65,19 +65,30 @@ public class RebaseAnalysis {
private final RebaseClassification classification;
private final RebaseConflictCode conflictCode;
private final String conflictDetail;
+ private final Object context;
public ClassifiedDifference(final FlowDifference difference, final
RebaseClassification classification,
final RebaseConflictCode conflictCode,
final String conflictDetail) {
+ this(difference, classification, conflictCode, conflictDetail,
null);
+ }
+
+ public ClassifiedDifference(final FlowDifference difference, final
RebaseClassification classification,
+ final RebaseConflictCode conflictCode,
final String conflictDetail, final Object context) {
this.difference = Objects.requireNonNull(difference, "Difference
is required");
this.classification = Objects.requireNonNull(classification,
"Classification is required");
this.conflictCode = conflictCode;
this.conflictDetail = conflictDetail;
+ this.context = context;
}
public static ClassifiedDifference compatible(final FlowDifference
difference) {
return new ClassifiedDifference(difference,
RebaseClassification.COMPATIBLE, null, null);
}
+ public static ClassifiedDifference compatible(final FlowDifference
difference, final Object context) {
+ return new ClassifiedDifference(difference,
RebaseClassification.COMPATIBLE, null, null, context);
+ }
+
public static ClassifiedDifference conflicting(final FlowDifference
difference, final RebaseConflictCode conflictCode, final String conflictDetail)
{
return new ClassifiedDifference(difference,
RebaseClassification.CONFLICTING, conflictCode, conflictDetail);
}
@@ -101,5 +112,9 @@ public class RebaseAnalysis {
public String getConflictDetail() {
return conflictDetail;
}
+
+ public Object getContext() {
+ return context;
+ }
}
}
diff --git
a/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/RebaseConflictCode.java
b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/RebaseConflictCode.java
index 311ced78b33..ad651b6d017 100644
---
a/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/RebaseConflictCode.java
+++
b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/RebaseConflictCode.java
@@ -32,6 +32,11 @@ public enum RebaseConflictCode {
*/
MISSING_FIELD_NAME,
+ /**
+ * The registered handler does not support the local change's component
type.
+ */
+ UNSUPPORTED_COMPONENT_TYPE,
+
/**
* Both the local and upstream flows modified the same property on the
same component.
*/
@@ -47,6 +52,11 @@ public enum RebaseConflictCode {
*/
COMPONENT_NOT_FOUND,
+ /**
+ * The target version already contains a component with the same
identifier as the local addition.
+ */
+ COMPONENT_IDENTIFIER_COLLISION,
+
/**
* The property descriptor targeted by the local change changed in an
incompatible way in the target version.
*/
diff --git
a/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/RebaseHandler.java
b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/RebaseHandler.java
index acb0b3d4723..431bc9c6982 100644
---
a/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/RebaseHandler.java
+++
b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/RebaseHandler.java
@@ -28,4 +28,8 @@ public interface RebaseHandler {
RebaseAnalysis.ClassifiedDifference classify(FlowDifference
localDifference, Set<FlowDifference> upstreamDifferences, VersionedProcessGroup
targetSnapshot);
void apply(FlowDifference localDifference, VersionedProcessGroup
mergedFlow);
+
+ default void apply(final RebaseAnalysis.ClassifiedDifference
classifiedDifference, final VersionedProcessGroup mergedFlow) {
+ apply(classifiedDifference.getDifference(), mergedFlow);
+ }
}
diff --git
a/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/RebaseHandlerUtils.java
b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/RebaseHandlerUtils.java
index f870ea4b8a8..c1d2702d5c6 100644
---
a/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/RebaseHandlerUtils.java
+++
b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/RebaseHandlerUtils.java
@@ -89,6 +89,21 @@ class RebaseHandlerUtils {
return null;
}
+ static VersionedProcessGroup findProcessGroupById(final
VersionedProcessGroup group, final String identifier) {
+ if (identifier.equals(group.getIdentifier()) ||
identifier.equals(group.getInstanceIdentifier())) {
+ return group;
+ }
+
+ for (final VersionedProcessGroup childGroup :
group.getProcessGroups()) {
+ final VersionedProcessGroup result =
findProcessGroupById(childGroup, identifier);
+ if (result != null) {
+ return result;
+ }
+ }
+
+ return null;
+ }
+
static VersionedConnection findConnectionById(final VersionedProcessGroup
group, final String identifier) {
for (final VersionedConnection connection : group.getConnections()) {
if (identifier.equals(connection.getIdentifier())) {
diff --git
a/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/StandardRebaseEngine.java
b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/StandardRebaseEngine.java
index b942d2bfccf..57153781cdf 100644
---
a/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/StandardRebaseEngine.java
+++
b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/StandardRebaseEngine.java
@@ -46,6 +46,7 @@ public class StandardRebaseEngine implements RebaseEngine {
registerHandler(new PositionChangedRebaseHandler());
registerHandler(new SizeChangedRebaseHandler());
registerHandler(new BendpointsChangedRebaseHandler());
+ registerHandler(new ComponentAddedRebaseHandler());
registerHandler(new PropertyChangedRebaseHandler());
registerHandler(new PropertyAddedRebaseHandler());
registerHandler(new CommentsChangedRebaseHandler());
@@ -76,7 +77,7 @@ public class StandardRebaseEngine implements RebaseEngine {
for (final RebaseAnalysis.ClassifiedDifference classified :
classifiedChanges) {
final RebaseHandler handler =
handlerRegistry.get(classified.getDifference().getDifferenceType());
if (handler != null) {
- handler.apply(classified.getDifference(), mergedSnapshot);
+ handler.apply(classified, mergedSnapshot);
}
}
}
diff --git
a/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/test/java/org/apache/nifi/registry/flow/diff/ComponentAddedRebaseHandlerTest.java
b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/test/java/org/apache/nifi/registry/flow/diff/ComponentAddedRebaseHandlerTest.java
new file mode 100644
index 00000000000..06a2de47b08
--- /dev/null
+++
b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/test/java/org/apache/nifi/registry/flow/diff/ComponentAddedRebaseHandlerTest.java
@@ -0,0 +1,296 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.nifi.registry.flow.diff;
+
+import org.apache.nifi.flow.Bundle;
+import org.apache.nifi.flow.ScheduledState;
+import org.apache.nifi.flow.VersionedControllerService;
+import org.apache.nifi.flow.VersionedProcessGroup;
+import org.apache.nifi.flow.VersionedProcessor;
+import org.apache.nifi.flow.VersionedPropertyDescriptor;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class ComponentAddedRebaseHandlerTest {
+
+ private static final String ROOT = "root";
+ private static final String TARGET_ROOT = "target-root";
+ private static final String CHILD = "child";
+ private static final String PROCESSOR_ID = "processor-a";
+ private static final String EXISTING_SERVICE_ID = "service-x";
+ private static final String ADDED_SERVICE_ID = "service-y";
+ private static final String SERVICE_REFERENCE_PROPERTY =
"delegate.service";
+ private static final String LOCAL_SERVICE_NAME = "Local Controller
Service";
+ private static final String LOCAL_SERVICE_TYPE =
"org.apache.nifi.services.LocalControllerService";
+ private static final String BUNDLE_GROUP = "group";
+ private static final String BUNDLE_ARTIFACT = "artifact";
+ private static final String BUNDLE_VERSION = "1.0.0";
+ private static final String LOCAL_COMMENTS = "local comments";
+ private static final String SERVICE_ENABLED_PROPERTY = "service.enabled";
+ private static final String SERVICE_ENABLED_VALUE = "true";
+
+ private ComponentAddedRebaseHandler handler;
+
+ @BeforeEach
+ void setup() {
+ handler = new ComponentAddedRebaseHandler();
+ }
+
+ @Test
+ void testClassifyReferencedControllerServiceAdditionIsCompatible() {
+ final VersionedControllerService addedService =
createControllerService(ADDED_SERVICE_ID, ROOT);
+ addedService.setProperties(Map.of(SERVICE_REFERENCE_PROPERTY,
EXISTING_SERVICE_ID));
+ addedService.setPropertyDescriptors(Map.of(SERVICE_REFERENCE_PROPERTY,
createDescriptor(SERVICE_REFERENCE_PROPERTY, false, false)));
+
+ final FlowDifference localDifference = new
StandardFlowDifference(DifferenceType.COMPONENT_ADDED, null, addedService,
+ null, addedService, "Referenced controller service added
locally");
+
+ final VersionedProcessGroup targetSnapshot =
createTargetSnapshotWithExistingService(EXISTING_SERVICE_ID, ROOT);
+ targetSnapshot.setIdentifier(TARGET_ROOT);
+
+ final RebaseAnalysis.ClassifiedDifference result =
handler.classify(localDifference, Collections.emptySet(), targetSnapshot);
+
+ assertEquals(RebaseClassification.COMPATIBLE,
result.getClassification());
+ assertEquals(ROOT, addedService.getGroupIdentifier());
+ }
+
+ @Test
+ void testClassifyUnreferencedControllerServiceAdditionIsCompatible() {
+ final VersionedControllerService addedService =
createControllerService(ADDED_SERVICE_ID, ROOT);
+ final FlowDifference localDifference = new
StandardFlowDifference(DifferenceType.COMPONENT_ADDED, null, addedService,
+ null, addedService, "Unreferenced controller service added
locally");
+
+ final VersionedProcessGroup targetSnapshot =
createTargetSnapshotWithExistingService(EXISTING_SERVICE_ID, ROOT);
+
+ final RebaseAnalysis.ClassifiedDifference result =
handler.classify(localDifference, Collections.emptySet(), targetSnapshot);
+
+ assertEquals(RebaseClassification.COMPATIBLE,
result.getClassification());
+ }
+
+ @Test
+ void testClassifyNonControllerServiceAdditionIsUnsupported() {
+ final VersionedProcessor processor = new VersionedProcessor();
+ processor.setIdentifier(PROCESSOR_ID);
+
+ final FlowDifference localDifference = new
StandardFlowDifference(DifferenceType.COMPONENT_ADDED, null, processor,
+ null, processor, "Processor added locally");
+
+ final VersionedProcessGroup targetSnapshot = createRootGroup();
+
+ final RebaseAnalysis.ClassifiedDifference result =
handler.classify(localDifference, Collections.emptySet(), targetSnapshot);
+
+ assertEquals(RebaseClassification.UNSUPPORTED,
result.getClassification());
+ assertEquals(RebaseConflictCode.UNSUPPORTED_COMPONENT_TYPE,
result.getConflictCode());
+ }
+
+ @Test
+ void testClassifyNullParentIsUnsupported() {
+ final VersionedControllerService addedService =
createControllerService(ADDED_SERVICE_ID, null);
+ final FlowDifference localDifference = new
StandardFlowDifference(DifferenceType.COMPONENT_ADDED, null, addedService,
+ null, addedService, "Controller service added without parent");
+
+ final RebaseAnalysis.ClassifiedDifference result =
handler.classify(localDifference, Collections.emptySet(), createRootGroup());
+
+ assertEquals(RebaseClassification.UNSUPPORTED,
result.getClassification());
+ assertEquals(RebaseConflictCode.COMPONENT_NOT_FOUND,
result.getConflictCode());
+ }
+
+ @Test
+ void testClassifyMissingParentIsUnsupported() {
+ final VersionedControllerService addedService =
createControllerService(ADDED_SERVICE_ID, CHILD);
+ final FlowDifference localDifference = new
StandardFlowDifference(DifferenceType.COMPONENT_ADDED, null, addedService,
+ null, addedService, "Controller service added to missing
parent");
+ final VersionedProcessGroup removedParent = createChildGroup(CHILD);
+ final Set<FlowDifference> upstreamDifferences = Set.of(new
StandardFlowDifference(DifferenceType.COMPONENT_REMOVED, removedParent, null,
+ removedParent, null, "Parent removed upstream"));
+
+ final RebaseAnalysis.ClassifiedDifference result =
handler.classify(localDifference, upstreamDifferences, createRootGroup());
+
+ assertEquals(RebaseClassification.UNSUPPORTED,
result.getClassification());
+ assertEquals(RebaseConflictCode.COMPONENT_NOT_FOUND,
result.getConflictCode());
+ }
+
+ @Test
+ void testClassifySameIdentifierTargetCollisionIsConflicting() {
+ final VersionedControllerService addedService =
createControllerService(ADDED_SERVICE_ID, ROOT);
+ final FlowDifference localDifference = new
StandardFlowDifference(DifferenceType.COMPONENT_ADDED, null, addedService,
+ null, addedService, "Controller service added with colliding
identifier");
+
+ final VersionedProcessGroup childGroup = new VersionedProcessGroup();
+ childGroup.setIdentifier(CHILD);
+
+ final VersionedProcessor collidingProcessor = new VersionedProcessor();
+ collidingProcessor.setIdentifier(ADDED_SERVICE_ID);
+ childGroup.getProcessors().add(collidingProcessor);
+
+ final VersionedProcessGroup targetSnapshot = createRootGroup();
+ targetSnapshot.getProcessGroups().add(childGroup);
+
+ final RebaseAnalysis.ClassifiedDifference result =
handler.classify(localDifference, Collections.emptySet(), targetSnapshot);
+
+ assertEquals(RebaseClassification.CONFLICTING,
result.getClassification());
+ assertEquals(RebaseConflictCode.COMPONENT_IDENTIFIER_COLLISION,
result.getConflictCode());
+ }
+
+ @Test
+ void
testApplyAddsControllerServiceToRootPreservingIdentityAndConfiguration() {
+ final VersionedControllerService addedService =
createControllerService(ADDED_SERVICE_ID, ROOT);
+ final FlowDifference localDifference = new
StandardFlowDifference(DifferenceType.COMPONENT_ADDED, null, addedService,
+ null, addedService, "Controller service added to root");
+
+ final VersionedProcessGroup mergedFlow = new VersionedProcessGroup();
+ mergedFlow.setIdentifier(TARGET_ROOT);
+ mergedFlow.setInstanceIdentifier(ROOT);
+
+ handler.apply(localDifference, mergedFlow);
+
+ assertNotNull(mergedFlow.getControllerServices());
+ assertEquals(1, mergedFlow.getControllerServices().size());
+
+ final VersionedControllerService insertedService =
mergedFlow.getControllerServices().iterator().next();
+ assertSame(addedService, insertedService);
+ assertEquals(ADDED_SERVICE_ID, insertedService.getIdentifier());
+ assertEquals(TARGET_ROOT, insertedService.getGroupIdentifier());
+ assertEquals(LOCAL_SERVICE_NAME, insertedService.getName());
+ assertEquals(LOCAL_SERVICE_TYPE, insertedService.getType());
+ assertEquals(BUNDLE_GROUP, insertedService.getBundle().getGroup());
+ assertEquals(BUNDLE_ARTIFACT,
insertedService.getBundle().getArtifact());
+ assertEquals(BUNDLE_VERSION, insertedService.getBundle().getVersion());
+ assertSame(addedService.getProperties(),
insertedService.getProperties());
+ assertSame(addedService.getPropertyDescriptors(),
insertedService.getPropertyDescriptors());
+ assertEquals(LOCAL_COMMENTS, insertedService.getComments());
+ assertEquals(ScheduledState.DISABLED,
insertedService.getScheduledState());
+ }
+
+ @Test
+ void testApplyAddsControllerServiceToNestedParent() {
+ final VersionedControllerService addedService =
createControllerService(ADDED_SERVICE_ID, CHILD);
+ final FlowDifference localDifference = new
StandardFlowDifference(DifferenceType.COMPONENT_ADDED, null, addedService,
+ null, addedService, "Controller service added to nested
group");
+
+ final VersionedProcessGroup childGroup = new VersionedProcessGroup();
+ childGroup.setIdentifier(CHILD);
+
+ final VersionedProcessGroup mergedFlow = createRootGroup();
+ mergedFlow.getProcessGroups().add(childGroup);
+
+ handler.apply(localDifference, mergedFlow);
+
+ assertEquals(1, childGroup.getControllerServices().size());
+ assertSame(addedService,
childGroup.getControllerServices().iterator().next());
+ assertEquals(0, mergedFlow.getControllerServices().size());
+ }
+
+ @Test
+ void testApplyThrowsWhenVerifiedParentIsMissing() {
+ final VersionedControllerService addedService =
createControllerService(ADDED_SERVICE_ID, CHILD);
+ final FlowDifference localDifference = new
StandardFlowDifference(DifferenceType.COMPONENT_ADDED, null, addedService,
+ null, addedService, "Controller service added to missing
parent");
+
+ final IllegalStateException exception =
assertThrows(IllegalStateException.class,
+ () -> handler.apply(localDifference, createRootGroup()));
+
+ assertEquals("Parent Process Group child for Controller Service
service-y was verified during classification but is absent during apply",
+ exception.getMessage());
+ }
+
+ @Test
+ void testApplyThrowsWhenIdentifierAlreadyExists() {
+ final VersionedControllerService addedService =
createControllerService(ADDED_SERVICE_ID, ROOT);
+ final FlowDifference localDifference = new
StandardFlowDifference(DifferenceType.COMPONENT_ADDED, null, addedService,
+ null, addedService, "Controller service added with colliding
identifier");
+
+ final VersionedProcessor existingProcessor = new VersionedProcessor();
+ existingProcessor.setIdentifier(ADDED_SERVICE_ID);
+
+ final VersionedProcessGroup mergedFlow = createRootGroup();
+ mergedFlow.getProcessors().add(existingProcessor);
+
+ final IllegalStateException exception =
assertThrows(IllegalStateException.class,
+ () -> handler.apply(localDifference, mergedFlow));
+
+ assertEquals("Merged flow already contains component
VersionedProcessor with identifier service-y", exception.getMessage());
+ }
+
+ private VersionedProcessGroup
createTargetSnapshotWithExistingService(final String serviceIdentifier, final
String groupIdentifier) {
+ final VersionedProcessGroup rootGroup = createRootGroup();
+ final VersionedProcessGroup parentGroup = ROOT.equals(groupIdentifier)
? rootGroup : createChildGroup(groupIdentifier);
+
+ if (parentGroup != rootGroup) {
+ rootGroup.getProcessGroups().add(parentGroup);
+ }
+
+
parentGroup.getControllerServices().add(createControllerService(serviceIdentifier,
groupIdentifier));
+ return rootGroup;
+ }
+
+ private VersionedProcessGroup createRootGroup() {
+ final VersionedProcessGroup rootGroup = new VersionedProcessGroup();
+ rootGroup.setIdentifier(ROOT);
+ rootGroup.setInstanceIdentifier(ROOT);
+ rootGroup.setControllerServices(new HashSet<>());
+ return rootGroup;
+ }
+
+ private VersionedProcessGroup createChildGroup(final String identifier) {
+ final VersionedProcessGroup childGroup = new VersionedProcessGroup();
+ childGroup.setIdentifier(identifier);
+ childGroup.setInstanceIdentifier(identifier);
+ childGroup.setControllerServices(new HashSet<>());
+ return childGroup;
+ }
+
+ private VersionedControllerService createControllerService(final String
identifier, final String groupIdentifier) {
+ final VersionedControllerService service = new
VersionedControllerService();
+ service.setIdentifier(identifier);
+ service.setGroupIdentifier(groupIdentifier);
+ service.setName(LOCAL_SERVICE_NAME);
+ service.setType(LOCAL_SERVICE_TYPE);
+ service.setBundle(new Bundle(BUNDLE_GROUP, BUNDLE_ARTIFACT,
BUNDLE_VERSION));
+ service.setScheduledState(ScheduledState.DISABLED);
+ service.setComments(LOCAL_COMMENTS);
+
+ final Map<String, String> properties = new HashMap<>();
+ properties.put(SERVICE_ENABLED_PROPERTY, SERVICE_ENABLED_VALUE);
+ service.setProperties(properties);
+
+ final Map<String, VersionedPropertyDescriptor> descriptors = new
HashMap<>();
+ descriptors.put(SERVICE_ENABLED_PROPERTY,
createDescriptor(SERVICE_ENABLED_PROPERTY, false, false));
+ service.setPropertyDescriptors(descriptors);
+ return service;
+ }
+
+ private VersionedPropertyDescriptor createDescriptor(final String name,
final boolean dynamic, final boolean sensitive) {
+ final VersionedPropertyDescriptor descriptor = new
VersionedPropertyDescriptor();
+ descriptor.setName(name);
+ descriptor.setDynamic(dynamic);
+ descriptor.setSensitive(sensitive);
+ return descriptor;
+ }
+}
diff --git
a/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/test/java/org/apache/nifi/registry/flow/diff/RebaseEngineTest.java
b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/test/java/org/apache/nifi/registry/flow/diff/RebaseEngineTest.java
index 74e92993272..66b9287b3a6 100644
---
a/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/test/java/org/apache/nifi/registry/flow/diff/RebaseEngineTest.java
+++
b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/test/java/org/apache/nifi/registry/flow/diff/RebaseEngineTest.java
@@ -17,8 +17,11 @@
package org.apache.nifi.registry.flow.diff;
+import org.apache.nifi.flow.Bundle;
import org.apache.nifi.flow.Position;
+import org.apache.nifi.flow.ScheduledState;
import org.apache.nifi.flow.VersionedConnection;
+import org.apache.nifi.flow.VersionedControllerService;
import org.apache.nifi.flow.VersionedLabel;
import org.apache.nifi.flow.VersionedProcessGroup;
import org.apache.nifi.flow.VersionedProcessor;
@@ -43,6 +46,27 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
class RebaseEngineTest {
+ private static final String ROOT_ID = "root";
+ private static final String VERSION_N_ROOT_ID = "root-n";
+ private static final String TARGET_ROOT_ID = "root-n-plus-one";
+ private static final String PROCESSOR_ID = "proc-a";
+ private static final String PROCESSOR_NAME = "ProcessorA";
+ private static final String SERVICE_A_ID = "service-a";
+ private static final String SERVICE_X_ID = "service-x";
+ private static final String SERVICE_Y_ID = "service-y";
+ private static final String SERVICE_Z_ID = "service-z";
+ private static final String SERVICE_A_NAME = "Service A";
+ private static final String SERVICE_X_NAME = "Service X";
+ private static final String SERVICE_Y_NAME = "Service Y";
+ private static final String SERVICE_Z_NAME = "Service Z";
+ private static final String CONTROLLER_SERVICE_PROPERTY =
"controller.service";
+ private static final String DYNAMIC_Y_PROPERTY = "dynamic.y";
+ private static final String DYNAMIC_Z_PROPERTY = "dynamic.z";
+ private static final String CONTROLLER_SERVICE_TYPE =
"org.apache.nifi.services.LocalControllerService";
+ private static final String BUNDLE_GROUP = "group";
+ private static final String BUNDLE_ARTIFACT = "artifact";
+ private static final String BUNDLE_VERSION = "1.0.0";
+
private RebaseEngine engine;
@BeforeEach
@@ -142,7 +166,7 @@ class RebaseEngineTest {
}
@Test
- void testUnsupportedDifferenceTypeNoHandler() {
+ void
testUnsupportedLocalProcessorAdditionUsesRegisteredComponentAddedHandler() {
final VersionedProcessor processor = createProcessor("proc-a",
"ProcessorA");
final Set<FlowDifference> localDifferences = new HashSet<>();
@@ -159,8 +183,164 @@ class RebaseEngineTest {
final RebaseAnalysis.ClassifiedDifference classified =
analysis.getClassifiedLocalChanges().get(0);
assertEquals(RebaseClassification.UNSUPPORTED,
classified.getClassification());
- assertEquals(RebaseConflictCode.NO_HANDLER,
classified.getConflictCode());
+ assertEquals(RebaseConflictCode.UNSUPPORTED_COMPONENT_TYPE,
classified.getConflictCode());
+ assertNull(analysis.getMergedSnapshot());
+ }
+
+ @Test
+ void
testAnalyzeScenario1PreservesAddedControllerServiceAndProcessorReference() {
+ final VersionedControllerService versionNService =
createControllerService(SERVICE_X_ID, SERVICE_X_NAME, ROOT_ID);
+ final VersionedControllerService localAddedService =
createControllerService(SERVICE_Y_ID, SERVICE_Y_NAME, ROOT_ID);
+
+ final VersionedProcessor versionNProcessor =
createProcessorWithProperty(PROCESSOR_ID, PROCESSOR_NAME,
CONTROLLER_SERVICE_PROPERTY, SERVICE_X_ID);
+ final VersionedProcessor localProcessor =
createProcessorWithProperty(PROCESSOR_ID, PROCESSOR_NAME,
CONTROLLER_SERVICE_PROPERTY, SERVICE_Y_ID);
+
+ final Set<FlowDifference> localDifferences = new HashSet<>();
+ localDifferences.add(new
StandardFlowDifference(DifferenceType.COMPONENT_ADDED, null, localAddedService,
+ null, localAddedService, "Controller service added locally"));
+ localDifferences.add(new
StandardFlowDifference(DifferenceType.PROPERTY_CHANGED, versionNProcessor,
localProcessor, CONTROLLER_SERVICE_PROPERTY,
+ SERVICE_X_ID, SERVICE_Y_ID, "Processor property changed
locally"));
+
+ final VersionedProcessGroup targetSnapshot = new
VersionedProcessGroup();
+ targetSnapshot.setIdentifier(ROOT_ID);
+ targetSnapshot.getControllerServices().add(versionNService);
+
targetSnapshot.getProcessors().add(createProcessorWithProperty(PROCESSOR_ID,
PROCESSOR_NAME, CONTROLLER_SERVICE_PROPERTY, SERVICE_X_ID));
+
+ final RebaseAnalysis analysis = engine.analyze(localDifferences,
Collections.emptySet(), targetSnapshot);
+
+ assertTrue(analysis.isRebaseAllowed());
+ assertEquals(2, analysis.getClassifiedLocalChanges().size());
+ assertAllCompatible(analysis);
+ assertClassification(analysis, DifferenceType.COMPONENT_ADDED,
SERVICE_Y_ID, RebaseClassification.COMPATIBLE, null);
+ assertClassification(analysis, DifferenceType.PROPERTY_CHANGED,
PROCESSOR_ID, RebaseClassification.COMPATIBLE, null);
+
+ final VersionedProcessGroup merged = analysis.getMergedSnapshot();
+ assertNotNull(merged);
+ final VersionedControllerService mergedService =
findControllerServiceById(merged, SERVICE_Y_ID);
+ assertSame(localAddedService, mergedService);
+
+ final VersionedProcessor mergedProcessor = findProcessorById(merged,
PROCESSOR_ID);
+ assertNotNull(mergedProcessor);
+ assertEquals(SERVICE_Y_ID,
mergedProcessor.getProperties().get(CONTROLLER_SERVICE_PROPERTY));
+ }
+
+ @Test
+ void
testAnalyzePreservesRootAdditionWhenTargetRootIdentifierChangedUpstream() {
+ final VersionedControllerService localAddedService =
createControllerService(SERVICE_Y_ID, SERVICE_Y_NAME, VERSION_N_ROOT_ID);
+ final Set<FlowDifference> localDifferences = Set.of(new
StandardFlowDifference(DifferenceType.COMPONENT_ADDED, null, localAddedService,
+ null, localAddedService, "Controller service added locally"));
+
+ final VersionedProcessGroup targetRoot = new VersionedProcessGroup();
+ targetRoot.setIdentifier(TARGET_ROOT_ID);
+ targetRoot.setName("Root");
+
+ final RebaseAnalysis analysis = engine.analyze(localDifferences,
Collections.emptySet(), targetRoot);
+
+ assertTrue(analysis.isRebaseAllowed());
+ assertSame(localAddedService, findControllerServiceById(targetRoot,
SERVICE_Y_ID));
+ assertEquals(TARGET_ROOT_ID, localAddedService.getGroupIdentifier());
+ }
+
+ @Test
+ void testAnalyzeRejectsRootAdditionWhenParentRemovedUpstream() {
+ final VersionedControllerService localAddedService =
createControllerService(SERVICE_Y_ID, SERVICE_Y_NAME, VERSION_N_ROOT_ID);
+ final Set<FlowDifference> localDifferences = Set.of(new
StandardFlowDifference(DifferenceType.COMPONENT_ADDED, null, localAddedService,
+ null, localAddedService, "Controller service added locally"));
+
+ final VersionedProcessGroup removedParent = new
VersionedProcessGroup();
+ removedParent.setIdentifier(VERSION_N_ROOT_ID);
+
+ final VersionedProcessGroup targetRoot = new VersionedProcessGroup();
+ targetRoot.setIdentifier("replacement-root");
+
+ final Set<FlowDifference> upstreamDifferences = Set.of(new
StandardFlowDifference(DifferenceType.COMPONENT_REMOVED, removedParent, null,
+ removedParent, null, "Parent removed upstream"));
+
+ final RebaseAnalysis analysis = engine.analyze(localDifferences,
upstreamDifferences, targetRoot);
+
+ assertFalse(analysis.isRebaseAllowed());
+ assertClassification(analysis, DifferenceType.COMPONENT_ADDED,
SERVICE_Y_ID, RebaseClassification.UNSUPPORTED,
+ RebaseConflictCode.COMPONENT_NOT_FOUND);
+ }
+
+ @Test
+ void
testAnalyzeScenario2PreservesMultipleAddedControllerServicesAndDynamicReferences()
{
+ final VersionedControllerService versionNServiceA =
createControllerService(SERVICE_A_ID, SERVICE_A_NAME, ROOT_ID);
+ versionNServiceA.setProperties(Collections.emptyMap());
+ versionNServiceA.setPropertyDescriptors(Collections.emptyMap());
+
+ final VersionedControllerService localServiceA =
createControllerService(SERVICE_A_ID, SERVICE_A_NAME, ROOT_ID);
+ localServiceA.setProperties(Map.of(DYNAMIC_Y_PROPERTY, SERVICE_Y_ID,
DYNAMIC_Z_PROPERTY, SERVICE_Z_ID));
+ localServiceA.setPropertyDescriptors(Map.of(
+ DYNAMIC_Y_PROPERTY,
createPropertyDescriptor(DYNAMIC_Y_PROPERTY, true, false),
+ DYNAMIC_Z_PROPERTY,
createPropertyDescriptor(DYNAMIC_Z_PROPERTY, true, false)));
+
+ final VersionedControllerService localServiceY =
createControllerService(SERVICE_Y_ID, SERVICE_Y_NAME, ROOT_ID);
+ final VersionedControllerService localServiceZ =
createControllerService(SERVICE_Z_ID, SERVICE_Z_NAME, ROOT_ID);
+
+ final Set<FlowDifference> localDifferences = new HashSet<>();
+ localDifferences.add(new
StandardFlowDifference(DifferenceType.COMPONENT_ADDED, null, localServiceY,
+ null, localServiceY, "Controller service Y added locally"));
+ localDifferences.add(new
StandardFlowDifference(DifferenceType.COMPONENT_ADDED, null, localServiceZ,
+ null, localServiceZ, "Controller service Z added locally"));
+ localDifferences.add(new
StandardFlowDifference(DifferenceType.PROPERTY_ADDED, versionNServiceA,
localServiceA, DYNAMIC_Y_PROPERTY,
+ null, SERVICE_Y_ID, "Dynamic property added for service Y"));
+ localDifferences.add(new
StandardFlowDifference(DifferenceType.PROPERTY_ADDED, versionNServiceA,
localServiceA, DYNAMIC_Z_PROPERTY,
+ null, SERVICE_Z_ID, "Dynamic property added for service Z"));
+
+ final VersionedProcessGroup targetSnapshot = new
VersionedProcessGroup();
+ targetSnapshot.setIdentifier(ROOT_ID);
+ targetSnapshot.getControllerServices().add(versionNServiceA);
+
+ final RebaseAnalysis analysis = engine.analyze(localDifferences,
Collections.emptySet(), targetSnapshot);
+
+ assertTrue(analysis.isRebaseAllowed());
+ assertEquals(4, analysis.getClassifiedLocalChanges().size());
+ assertAllCompatible(analysis);
+ assertClassification(analysis, DifferenceType.COMPONENT_ADDED,
SERVICE_Y_ID, RebaseClassification.COMPATIBLE, null);
+ assertClassification(analysis, DifferenceType.COMPONENT_ADDED,
SERVICE_Z_ID, RebaseClassification.COMPATIBLE, null);
+
+ final VersionedProcessGroup merged = analysis.getMergedSnapshot();
+ assertNotNull(merged);
+ assertSame(localServiceY, findControllerServiceById(merged,
SERVICE_Y_ID));
+ assertSame(localServiceZ, findControllerServiceById(merged,
SERVICE_Z_ID));
+
+ final VersionedControllerService mergedServiceA =
findControllerServiceById(merged, SERVICE_A_ID);
+ assertNotNull(mergedServiceA);
+ assertEquals(SERVICE_Y_ID,
mergedServiceA.getProperties().get(DYNAMIC_Y_PROPERTY));
+ assertEquals(SERVICE_Z_ID,
mergedServiceA.getProperties().get(DYNAMIC_Z_PROPERTY));
+ }
+
+ @Test
+ void testAnalyzeCollisionBlocksRebaseAndDoesNotMutateTargetSnapshot() {
+ final VersionedControllerService collidingTargetService =
createControllerService(SERVICE_Y_ID, "Target Service Y", ROOT_ID);
+ final VersionedControllerService localAddedService =
createControllerService(SERVICE_Y_ID, "Local Service Y", ROOT_ID);
+
+ final VersionedProcessor versionNProcessor =
createProcessorWithProperty(PROCESSOR_ID, PROCESSOR_NAME,
CONTROLLER_SERVICE_PROPERTY, SERVICE_X_ID);
+ final VersionedProcessor localProcessor =
createProcessorWithProperty(PROCESSOR_ID, PROCESSOR_NAME,
CONTROLLER_SERVICE_PROPERTY, SERVICE_Y_ID);
+
+ final Set<FlowDifference> localDifferences = new HashSet<>();
+ localDifferences.add(new
StandardFlowDifference(DifferenceType.COMPONENT_ADDED, null, localAddedService,
+ null, localAddedService, "Controller service added with
collision"));
+ localDifferences.add(new
StandardFlowDifference(DifferenceType.PROPERTY_CHANGED, versionNProcessor,
localProcessor, CONTROLLER_SERVICE_PROPERTY,
+ SERVICE_X_ID, SERVICE_Y_ID, "Processor property changed
locally"));
+
+ final VersionedProcessGroup targetSnapshot = new
VersionedProcessGroup();
+ targetSnapshot.setIdentifier(ROOT_ID);
+ targetSnapshot.getControllerServices().add(collidingTargetService);
+
targetSnapshot.getProcessors().add(createProcessorWithProperty(PROCESSOR_ID,
PROCESSOR_NAME, CONTROLLER_SERVICE_PROPERTY, SERVICE_X_ID));
+
+ final RebaseAnalysis analysis = engine.analyze(localDifferences,
Collections.emptySet(), targetSnapshot);
+
+ assertFalse(analysis.isRebaseAllowed());
assertNull(analysis.getMergedSnapshot());
+ assertClassification(analysis, DifferenceType.COMPONENT_ADDED,
SERVICE_Y_ID, RebaseClassification.CONFLICTING,
+ RebaseConflictCode.COMPONENT_IDENTIFIER_COLLISION);
+ assertEquals(1, countComponentsById(targetSnapshot, SERVICE_Y_ID));
+
+ final VersionedProcessor unchangedProcessor =
findProcessorById(targetSnapshot, PROCESSOR_ID);
+ assertNotNull(unchangedProcessor);
+ assertEquals(SERVICE_X_ID,
unchangedProcessor.getProperties().get(CONTROLLER_SERVICE_PROPERTY));
}
@Test
@@ -652,6 +832,28 @@ class RebaseEngineTest {
return connection;
}
+ private VersionedControllerService createControllerService(final String
identifier, final String name, final String groupIdentifier) {
+ final VersionedControllerService service = new
VersionedControllerService();
+ service.setIdentifier(identifier);
+ service.setName(name);
+ service.setGroupIdentifier(groupIdentifier);
+ service.setType(CONTROLLER_SERVICE_TYPE);
+ service.setBundle(new Bundle(BUNDLE_GROUP, BUNDLE_ARTIFACT,
BUNDLE_VERSION));
+ service.setScheduledState(ScheduledState.DISABLED);
+ service.setComments(name + " comments");
+ service.setProperties(Collections.emptyMap());
+ service.setPropertyDescriptors(Collections.emptyMap());
+ return service;
+ }
+
+ private VersionedPropertyDescriptor createPropertyDescriptor(final String
propertyName, final boolean dynamic, final boolean sensitive) {
+ final VersionedPropertyDescriptor descriptor = new
VersionedPropertyDescriptor();
+ descriptor.setName(propertyName);
+ descriptor.setDynamic(dynamic);
+ descriptor.setSensitive(sensitive);
+ return descriptor;
+ }
+
private VersionedProcessor findProcessorById(final VersionedProcessGroup
group, final String identifier) {
for (final VersionedProcessor processor : group.getProcessors()) {
if (identifier.equals(processor.getIdentifier())) {
@@ -666,4 +868,64 @@ class RebaseEngineTest {
}
return null;
}
+
+ private VersionedControllerService findControllerServiceById(final
VersionedProcessGroup group, final String identifier) {
+ for (final VersionedControllerService service :
group.getControllerServices()) {
+ if (identifier.equals(service.getIdentifier())) {
+ return service;
+ }
+ }
+ for (final VersionedProcessGroup childGroup :
group.getProcessGroups()) {
+ final VersionedControllerService result =
findControllerServiceById(childGroup, identifier);
+ if (result != null) {
+ return result;
+ }
+ }
+ return null;
+ }
+
+ private int countComponentsById(final VersionedProcessGroup group, final
String identifier) {
+ int count = identifier.equals(group.getIdentifier()) ? 1 : 0;
+
+ for (final VersionedProcessor processor : group.getProcessors()) {
+ if (identifier.equals(processor.getIdentifier())) {
+ count++;
+ }
+ }
+ for (final VersionedControllerService service :
group.getControllerServices()) {
+ if (identifier.equals(service.getIdentifier())) {
+ count++;
+ }
+ }
+ for (final VersionedProcessGroup childGroup :
group.getProcessGroups()) {
+ count += countComponentsById(childGroup, identifier);
+ }
+
+ return count;
+ }
+
+ private void assertAllCompatible(final RebaseAnalysis analysis) {
+ for (final RebaseAnalysis.ClassifiedDifference classified :
analysis.getClassifiedLocalChanges()) {
+ assertEquals(RebaseClassification.COMPATIBLE,
classified.getClassification());
+ }
+ }
+
+ private void assertClassification(final RebaseAnalysis analysis, final
DifferenceType differenceType, final String componentIdentifier,
+ final RebaseClassification
expectedClassification, final RebaseConflictCode expectedConflictCode) {
+ RebaseAnalysis.ClassifiedDifference matchingDifference = null;
+ for (final RebaseAnalysis.ClassifiedDifference classified :
analysis.getClassifiedLocalChanges()) {
+ final FlowDifference difference = classified.getDifference();
+ final String differenceComponentId = difference.getComponentB() !=
null
+ ? difference.getComponentB().getIdentifier()
+ : difference.getComponentA().getIdentifier();
+ if (difference.getDifferenceType() == differenceType &&
componentIdentifier.equals(differenceComponentId)) {
+ matchingDifference = classified;
+ break;
+ }
+ }
+
+ assertNotNull(matchingDifference);
+ assertEquals(expectedClassification,
matchingDifference.getClassification());
+ assertEquals(expectedConflictCode,
matchingDifference.getConflictCode());
+ }
}
diff --git
a/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions-services/src/main/java/org/apache/nifi/cs/tests/system/FakeDynamicPropertiesControllerService.java
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions-services/src/main/java/org/apache/nifi/cs/tests/system/FakeDynamicPropertiesControllerService.java
new file mode 100644
index 00000000000..000675731c5
--- /dev/null
+++
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions-services/src/main/java/org/apache/nifi/cs/tests/system/FakeDynamicPropertiesControllerService.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.cs.tests.system;
+
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.Validator;
+import org.apache.nifi.controller.AbstractControllerService;
+
+public class FakeDynamicPropertiesControllerService extends
AbstractControllerService implements BaseFakeService {
+ @Override
+ protected PropertyDescriptor getSupportedDynamicPropertyDescriptor(final
String propertyName) {
+ if (propertyName.startsWith("FCS.")) {
+ return new PropertyDescriptor.Builder()
+ .name(propertyName)
+ .required(false)
+ .dynamic(true)
+ .identifiesControllerService(BaseFakeService.class)
+ .build();
+ }
+
+ return new PropertyDescriptor.Builder()
+ .name(propertyName)
+ .required(false)
+ .addValidator(Validator.VALID)
+ .dynamic(true)
+ .build();
+ }
+
+ @Override
+ public void foo() {
+ }
+}
diff --git
a/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions-services/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions-services/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService
index 3e376b35485..b4bda8321c0 100644
---
a/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions-services/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService
+++
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions-services/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService
@@ -15,6 +15,7 @@
org.apache.nifi.cs.tests.system.ClassloaderIsolationKeyProviderService
org.apache.nifi.cs.tests.system.EnsureControllerServiceConfigurationCorrect
+org.apache.nifi.cs.tests.system.FakeDynamicPropertiesControllerService
org.apache.nifi.cs.tests.system.FakeControllerService1
org.apache.nifi.cs.tests.system.LifecycleFailureService
org.apache.nifi.cs.tests.system.SensitiveDynamicPropertiesService
diff --git
a/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/registry/RebaseVersionIT.java
b/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/registry/RebaseVersionIT.java
index c698dae31bb..ae57b15dbe6 100644
---
a/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/registry/RebaseVersionIT.java
+++
b/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/registry/RebaseVersionIT.java
@@ -20,9 +20,12 @@ package org.apache.nifi.tests.system.registry;
import org.apache.nifi.tests.system.NiFiClientUtil;
import org.apache.nifi.tests.system.NiFiSystemIT;
import org.apache.nifi.toolkit.client.NiFiClientException;
+import org.apache.nifi.web.api.dto.ComponentDifferenceDTO;
+import org.apache.nifi.web.api.dto.DifferenceDTO;
import org.apache.nifi.web.api.dto.RebaseChangeDTO;
import org.apache.nifi.web.api.dto.VersionControlInformationDTO;
import org.apache.nifi.web.api.dto.flow.FlowDTO;
+import org.apache.nifi.web.api.entity.ControllerServiceEntity;
import org.apache.nifi.web.api.entity.FlowComparisonEntity;
import org.apache.nifi.web.api.entity.FlowRegistryClientEntity;
import org.apache.nifi.web.api.entity.ProcessGroupEntity;
@@ -34,6 +37,8 @@ import
org.apache.nifi.web.api.entity.VersionedFlowUpdateRequestEntity;
import org.junit.jupiter.api.Test;
import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
import java.util.Map;
import java.util.Set;
@@ -44,6 +49,19 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
public class RebaseVersionIT extends NiFiSystemIT {
private static final String TEST_FLOWS_BUCKET = "test-flows";
+ private static final String ROOT_GROUP_ID = "root";
+ private static final String ORIGINAL_GROUP_NAME = "Original";
+ private static final String CONTROLLER_SERVICE_TYPE =
"FakeControllerService1";
+ private static final String DYNAMIC_CONTROLLER_SERVICE_TYPE =
"FakeDynamicPropertiesControllerService";
+ private static final String PROCESSOR_TYPE = "FakeProcessor";
+ private static final String GENERATE_FLOW_FILE_TYPE = "GenerateFlowFile";
+ private static final String CONTROLLER_SERVICE_PROPERTY = "Fake Service";
+ private static final String TEXT_PROPERTY = "Text";
+ private static final String UPSTREAM_CHANGE = "upstream-change";
+ private static final String SERVICE_X_PROPERTY = "FCS.X";
+ private static final String SERVICE_Y_PROPERTY = "FCS.Y";
+ private static final String SERVICE_Z_PROPERTY = "FCS.Z";
+ private static final String TARGET_VERSION = "2";
@Test
public void testCleanRebaseWithPositionAndPropertyChanges() throws
NiFiClientException, IOException, InterruptedException {
@@ -63,14 +81,14 @@ public class RebaseVersionIT extends NiFiSystemIT {
util.updateProcessorProperties(generate, Map.of("File Size", "99 B"));
- final RebaseAnalysisEntity analysis =
util.getRebaseAnalysis(originalGroup.getId(), "2");
+ final RebaseAnalysisEntity analysis =
util.getRebaseAnalysis(originalGroup.getId(), TARGET_VERSION);
assertTrue(analysis.getRebaseAllowed(), "Expected rebase to be allowed
but it was not. Failure: " + analysis.getFailureReason()
+ ". Local changes: " + describeLocalChanges(analysis));
- util.rebaseFlowVersion(originalGroup.getId(), "2");
+ util.rebaseFlowVersion(originalGroup.getId(), TARGET_VERSION);
final VersionControlInformationDTO updatedVci =
getVersionControlInfo(originalGroup.getId());
- assertEquals("2", updatedVci.getVersion());
+ assertEquals(TARGET_VERSION, updatedVci.getVersion());
final ProcessorEntity rebasedProcessor =
findSingleProcessor(originalGroup.getId());
final Map<String, String> properties =
rebasedProcessor.getComponent().getConfig().getProperties();
@@ -96,7 +114,7 @@ public class RebaseVersionIT extends NiFiSystemIT {
util.updateProcessorProperties(generate, Map.of("File Size", "50 B"));
- final RebaseAnalysisEntity analysis =
util.getRebaseAnalysis(originalGroup.getId(), "2");
+ final RebaseAnalysisEntity analysis =
util.getRebaseAnalysis(originalGroup.getId(), TARGET_VERSION);
assertFalse(analysis.getRebaseAllowed());
final boolean hasConflicting = analysis.getLocalChanges().stream()
@@ -123,7 +141,7 @@ public class RebaseVersionIT extends NiFiSystemIT {
util.createProcessor("TerminateFlowFile", originalGroup.getId());
- final RebaseAnalysisEntity analysis =
util.getRebaseAnalysis(originalGroup.getId(), "2");
+ final RebaseAnalysisEntity analysis =
util.getRebaseAnalysis(originalGroup.getId(), TARGET_VERSION);
assertFalse(analysis.getRebaseAllowed());
final boolean hasUnsupported = analysis.getLocalChanges().stream()
@@ -150,10 +168,10 @@ public class RebaseVersionIT extends NiFiSystemIT {
util.updateProcessorProperties(generate, Map.of("File Size", "99 B"));
- util.rebaseFlowVersion(originalGroup.getId(), "2");
+ util.rebaseFlowVersion(originalGroup.getId(), TARGET_VERSION);
final VersionControlInformationDTO afterRebase =
getVersionControlInfo(originalGroup.getId());
- assertEquals("2", afterRebase.getVersion());
+ assertEquals(TARGET_VERSION, afterRebase.getVersion());
util.saveFlowVersion(originalGroup, clientEntity,
getVersionControlInformation(originalGroup.getId()));
@@ -179,15 +197,15 @@ public class RebaseVersionIT extends NiFiSystemIT {
util.updateProcessorProperties(generate, Map.of("File Size", "99 B"));
- util.rebaseFlowVersion(originalGroup.getId(), "2");
+ util.rebaseFlowVersion(originalGroup.getId(), TARGET_VERSION);
final VersionControlInformationDTO afterRebase =
getVersionControlInfo(originalGroup.getId());
- assertEquals("2", afterRebase.getVersion());
+ assertEquals(TARGET_VERSION, afterRebase.getVersion());
util.revertChanges(originalGroup);
final VersionControlInformationDTO revertedVci =
getVersionControlInfo(originalGroup.getId());
- assertEquals("2", revertedVci.getVersion());
+ assertEquals(TARGET_VERSION, revertedVci.getVersion());
}
@Test
@@ -252,23 +270,23 @@ public class RebaseVersionIT extends NiFiSystemIT {
util.updateProcessorProperties(generate, Map.of("File Size", "99 B"));
- final RebaseAnalysisEntity initialAnalysis =
util.getRebaseAnalysis(originalGroup.getId(), "2");
+ final RebaseAnalysisEntity initialAnalysis =
util.getRebaseAnalysis(originalGroup.getId(), TARGET_VERSION);
final String staleFingerprint =
initialAnalysis.getAnalysisFingerprint();
util.updateProcessorProperties(generate, Map.of("Max FlowFiles",
"50"));
boolean rebaseWithStaleFailed = false;
try {
- executeRebaseWithFingerprint(originalGroup, "2", staleFingerprint);
+ executeRebaseWithFingerprint(originalGroup, TARGET_VERSION,
staleFingerprint);
} catch (final Exception e) {
rebaseWithStaleFailed = true;
}
assertTrue(rebaseWithStaleFailed);
- util.rebaseFlowVersion(originalGroup.getId(), "2");
+ util.rebaseFlowVersion(originalGroup.getId(), TARGET_VERSION);
final VersionControlInformationDTO updatedVci =
getVersionControlInfo(originalGroup.getId());
- assertEquals("2", updatedVci.getVersion());
+ assertEquals(TARGET_VERSION, updatedVci.getVersion());
}
@Test
@@ -293,7 +311,7 @@ public class RebaseVersionIT extends NiFiSystemIT {
util.updateProcessorProperties(secondParentProcessor, Map.of("File
Size", "20 B"));
util.saveFlowVersion(secondParent, clientEntity,
getVersionControlInformation(secondParent.getId()));
- final RebaseAnalysisEntity analysis =
util.getRebaseAnalysis(parentGroup.getId(), "2");
+ final RebaseAnalysisEntity analysis =
util.getRebaseAnalysis(parentGroup.getId(), TARGET_VERSION);
assertFalse(analysis.getRebaseAllowed(), "Rebase should be blocked due
to descendant modifications");
assertNotNull(analysis.getFailureReason());
}
@@ -315,10 +333,10 @@ public class RebaseVersionIT extends NiFiSystemIT {
util.saveFlowVersion(secondGroup, clientEntity,
getVersionControlInformation(secondGroup.getId()));
util.updateProcessorProperties(generate, Map.of("File Size", "99 B"));
- util.rebaseFlowVersion(originalGroup.getId(), "2");
+ util.rebaseFlowVersion(originalGroup.getId(), TARGET_VERSION);
final VersionControlInformationDTO afterFirstRebase =
getVersionControlInfo(originalGroup.getId());
- assertEquals("2", afterFirstRebase.getVersion());
+ assertEquals(TARGET_VERSION, afterFirstRebase.getVersion());
final ProcessorEntity v3Processor =
findSingleProcessor(secondGroup.getId());
util.updateProcessorProperties(v3Processor, Map.of("Max FlowFiles",
"30"));
@@ -356,7 +374,7 @@ public class RebaseVersionIT extends NiFiSystemIT {
// Locally modify the processor that the target version removed
util.updateProcessorProperties(generate, Map.of("File Size", "99 B"));
- final RebaseAnalysisEntity analysis =
util.getRebaseAnalysis(originalGroup.getId(), "2");
+ final RebaseAnalysisEntity analysis =
util.getRebaseAnalysis(originalGroup.getId(), TARGET_VERSION);
assertFalse(analysis.getRebaseAllowed(), "Rebase should be blocked
because the target version removed the locally modified component");
final boolean removedComponentRejected =
analysis.getLocalChanges().stream()
@@ -383,10 +401,10 @@ public class RebaseVersionIT extends NiFiSystemIT {
util.updateProcessorProperties(generate, Map.of("File Size", "99 B"));
- util.rebaseFlowVersion(originalGroup.getId(), "2");
+ util.rebaseFlowVersion(originalGroup.getId(), TARGET_VERSION);
final VersionControlInformationDTO updatedVci =
getVersionControlInfo(originalGroup.getId());
- assertEquals("2", updatedVci.getVersion());
+ assertEquals(TARGET_VERSION, updatedVci.getVersion());
// The Version Control Information snapshot must be the clean target
version (not the merged snapshot). If it were
// the merged snapshot, the preserved local change would not be
reported as a local modification. This is the key
@@ -396,6 +414,98 @@ public class RebaseVersionIT extends NiFiSystemIT {
"Expected the preserved local change to be reported as a local
modification after rebase, but none were found");
}
+ @Test
+ public void
testRebasePreservesLocallyAddedControllerServiceReferencedByProcessor() throws
NiFiClientException, IOException, InterruptedException {
+ final FlowRegistryClientEntity clientEntity = registerClient();
+ final NiFiClientUtil util = getClientUtil();
+
+ final ProcessGroupEntity originalGroup =
util.createProcessGroup(ORIGINAL_GROUP_NAME, ROOT_GROUP_ID);
+ final ControllerServiceEntity serviceX =
util.createControllerService(CONTROLLER_SERVICE_TYPE, originalGroup.getId());
+ final ProcessorEntity fakeProcessor =
util.createProcessor(PROCESSOR_TYPE, originalGroup.getId());
+ util.updateProcessorProperties(fakeProcessor,
Map.of(CONTROLLER_SERVICE_PROPERTY, serviceX.getId()));
+ util.createProcessor(GENERATE_FLOW_FILE_TYPE, originalGroup.getId());
+
+ final VersionControlInformationEntity vci =
util.startVersionControl(originalGroup, clientEntity, TEST_FLOWS_BUCKET,
+ "RebaseLocalAddedControllerServiceProcessorReference");
+ final String flowId = vci.getVersionControlInformation().getFlowId();
+
+ final ProcessGroupEntity secondGroup =
util.importFlowFromRegistry(ROOT_GROUP_ID, clientEntity.getId(),
TEST_FLOWS_BUCKET, flowId, "1");
+ final ProcessorEntity upstreamGenerate =
findProcessorByType(secondGroup.getId(), GENERATE_FLOW_FILE_TYPE);
+ util.updateProcessorProperties(upstreamGenerate, Map.of(TEXT_PROPERTY,
UPSTREAM_CHANGE));
+ util.saveFlowVersion(secondGroup, clientEntity,
getVersionControlInformation(secondGroup.getId()));
+
+ final ControllerServiceEntity serviceY =
util.createControllerService(CONTROLLER_SERVICE_TYPE, originalGroup.getId());
+ util.updateProcessorProperties(fakeProcessor,
Map.of(CONTROLLER_SERVICE_PROPERTY, serviceY.getId()));
+
+ final RebaseAnalysisEntity analysis =
util.getRebaseAnalysis(originalGroup.getId(), TARGET_VERSION);
+ assertTrue(analysis.getRebaseAllowed(), "Expected rebase to be allowed
but it was not. Failure: " + analysis.getFailureReason()
+ + ". Local changes: " + describeLocalChanges(analysis));
+ assertCompatibleControllerServiceAdditions(analysis, 1);
+
+ util.rebaseFlowVersion(originalGroup.getId(), TARGET_VERSION);
+
+ final VersionControlInformationDTO updatedVci =
getVersionControlInfo(originalGroup.getId());
+ assertEquals(TARGET_VERSION, updatedVci.getVersion());
+
+ final Set<ControllerServiceEntity> rebasedServices =
getNifiClient().getFlowClient().getControllerServices(originalGroup.getId()).getControllerServices();
+ assertControllerServicesPresent(rebasedServices, serviceX.getId(),
serviceY.getId());
+
+ final ProcessorEntity rebasedProcessor =
findProcessorByType(originalGroup.getId(), PROCESSOR_TYPE);
+ assertEquals(serviceY.getId(),
rebasedProcessor.getComponent().getConfig().getProperties().get(CONTROLLER_SERVICE_PROPERTY));
+
+ assertLocalModificationsContainComponents(originalGroup.getId(),
serviceY.getId(), fakeProcessor.getId());
+ }
+
+ @Test
+ public void
testRebasePreservesLocallyAddedControllerServicesReferencedByDynamicControllerServiceProperties()
+ throws NiFiClientException, IOException, InterruptedException {
+ final FlowRegistryClientEntity clientEntity = registerClient();
+ final NiFiClientUtil util = getClientUtil();
+
+ final ProcessGroupEntity originalGroup =
util.createProcessGroup(ORIGINAL_GROUP_NAME, ROOT_GROUP_ID);
+ final ControllerServiceEntity serviceX =
util.createControllerService(CONTROLLER_SERVICE_TYPE, originalGroup.getId());
+ final ControllerServiceEntity dynamicService =
util.createControllerService(DYNAMIC_CONTROLLER_SERVICE_TYPE,
originalGroup.getId());
+ util.updateControllerServiceProperties(dynamicService,
Collections.singletonMap(SERVICE_X_PROPERTY, serviceX.getId()));
+ util.createProcessor(GENERATE_FLOW_FILE_TYPE, originalGroup.getId());
+
+ final VersionControlInformationEntity vci =
util.startVersionControl(originalGroup, clientEntity, TEST_FLOWS_BUCKET,
+ "RebaseLocalAddedControllerServiceDynamicReference");
+ final String flowId = vci.getVersionControlInformation().getFlowId();
+
+ final ProcessGroupEntity secondGroup =
util.importFlowFromRegistry(ROOT_GROUP_ID, clientEntity.getId(),
TEST_FLOWS_BUCKET, flowId, "1");
+ final ProcessorEntity upstreamGenerate =
findProcessorByType(secondGroup.getId(), GENERATE_FLOW_FILE_TYPE);
+ util.updateProcessorProperties(upstreamGenerate, Map.of(TEXT_PROPERTY,
UPSTREAM_CHANGE));
+ util.saveFlowVersion(secondGroup, clientEntity,
getVersionControlInformation(secondGroup.getId()));
+
+ final ControllerServiceEntity serviceY =
util.createControllerService(CONTROLLER_SERVICE_TYPE, originalGroup.getId());
+ final ControllerServiceEntity serviceZ =
util.createControllerService(CONTROLLER_SERVICE_TYPE, originalGroup.getId());
+ util.updateControllerServiceProperties(dynamicService, Map.of(
+ SERVICE_X_PROPERTY, serviceX.getId(),
+ SERVICE_Y_PROPERTY, serviceY.getId(),
+ SERVICE_Z_PROPERTY, serviceZ.getId()));
+
+ final RebaseAnalysisEntity analysis =
util.getRebaseAnalysis(originalGroup.getId(), TARGET_VERSION);
+ assertTrue(analysis.getRebaseAllowed(), "Expected rebase to be allowed
but it was not. Failure: " + analysis.getFailureReason()
+ + ". Local changes: " + describeLocalChanges(analysis));
+ assertCompatibleControllerServiceAdditions(analysis, 2);
+
+ util.rebaseFlowVersion(originalGroup.getId(), TARGET_VERSION);
+
+ final VersionControlInformationDTO updatedVci =
getVersionControlInfo(originalGroup.getId());
+ assertEquals(TARGET_VERSION, updatedVci.getVersion());
+
+ final Set<ControllerServiceEntity> rebasedServices =
getNifiClient().getFlowClient().getControllerServices(originalGroup.getId()).getControllerServices();
+ assertControllerServicesPresent(rebasedServices,
dynamicService.getId(), serviceX.getId(), serviceY.getId(), serviceZ.getId());
+
+ final ControllerServiceEntity rebasedDynamicService =
getNifiClient().getControllerServicesClient().getControllerService(dynamicService.getId());
+ final Map<String, String> dynamicProperties =
rebasedDynamicService.getComponent().getProperties();
+ assertEquals(serviceX.getId(),
dynamicProperties.get(SERVICE_X_PROPERTY));
+ assertEquals(serviceY.getId(),
dynamicProperties.get(SERVICE_Y_PROPERTY));
+ assertEquals(serviceZ.getId(),
dynamicProperties.get(SERVICE_Z_PROPERTY));
+
+ assertLocalModificationsContainComponents(originalGroup.getId(),
dynamicService.getId(), serviceY.getId(), serviceZ.getId());
+ }
+
private String describeLocalChanges(final RebaseAnalysisEntity analysis) {
if (analysis.getLocalChanges() == null ||
analysis.getLocalChanges().isEmpty()) {
return "none";
@@ -434,6 +544,74 @@ public class RebaseVersionIT extends NiFiSystemIT {
.orElseThrow(() -> new AssertionError("No processor of type "
+ simpleTypeName + " found in group " + processGroupId));
}
+ private void assertCompatibleControllerServiceAdditions(final
RebaseAnalysisEntity analysis, final long expectedCount) {
+ final long compatibleAdditions = analysis.getLocalChanges().stream()
+ .filter(change -> "Component
Added".equals(change.getDifferenceType()))
+ .filter(change -> "Controller
Service".equals(change.getComponentType()))
+ .filter(change ->
"COMPATIBLE".equals(change.getClassification()))
+ .count();
+ assertEquals(expectedCount, compatibleAdditions, "Unexpected
compatible controller-service additions. Local changes: "
+ + describeLocalChanges(analysis));
+ }
+
+ private void assertControllerServicesPresent(final
Set<ControllerServiceEntity> services, final String... expectedServiceIds) {
+ for (final String serviceId : expectedServiceIds) {
+ final boolean present = services.stream().anyMatch(service ->
serviceId.equals(service.getId()));
+ assertTrue(present, "Expected controller service " + serviceId + "
to be present. Services: " + describeControllerServices(services));
+ }
+ }
+
+ private void assertLocalModificationsContainComponents(final String
processGroupId, final String... componentIds)
+ throws NiFiClientException, IOException {
+ final FlowComparisonEntity localModifications =
getNifiClient().getProcessGroupClient().getLocalModifications(processGroupId);
+ assertFalse(localModifications.getComponentDifferences().isEmpty(),
+ "Expected preserved local changes to remain visible after
rebase, but none were found");
+
+ for (final String componentId : componentIds) {
+ final boolean reported =
localModifications.getComponentDifferences().stream()
+ .anyMatch(component ->
componentId.equals(component.getComponentId()));
+ assertTrue(reported, "Expected local modifications to include
component " + componentId + ". Reported differences: "
+ +
describeComponentDifferences(localModifications.getComponentDifferences()));
+ }
+ }
+
+ private String describeControllerServices(final
Collection<ControllerServiceEntity> services) {
+ final StringBuilder sb = new StringBuilder();
+ for (final ControllerServiceEntity service : services) {
+ if (!sb.isEmpty()) {
+ sb.append(", ");
+ }
+
sb.append(service.getId()).append("=").append(service.getComponent().getType());
+ }
+ return sb.length() == 0 ? "none" : sb.toString();
+ }
+
+ private String describeComponentDifferences(final
Collection<ComponentDifferenceDTO> componentDifferences) {
+ final StringBuilder sb = new StringBuilder();
+ for (final ComponentDifferenceDTO component : componentDifferences) {
+ if (!sb.isEmpty()) {
+ sb.append("; ");
+ }
+
+ sb.append(component.getComponentType()).append(" ")
+ .append(component.getComponentName()).append(" (")
+ .append(component.getComponentId()).append(")");
+
+ if (component.getDifferences() != null &&
!component.getDifferences().isEmpty()) {
+ sb.append(" -> ");
+ boolean firstDifference = true;
+ for (final DifferenceDTO difference :
component.getDifferences()) {
+ if (!firstDifference) {
+ sb.append(", ");
+ }
+ sb.append(difference.getDifference());
+ firstDifference = false;
+ }
+ }
+ }
+ return sb.length() == 0 ? "none" : sb.toString();
+ }
+
private void executeRebaseWithFingerprint(final ProcessGroupEntity group,
final String targetVersion, final String fingerprint)
throws NiFiClientException, IOException, InterruptedException {