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 87e65dbbe81 NIFI-15785 Update Parameter and Parameter Context 
descriptions on versioned flow upgrades (#11096)
87e65dbbe81 is described below

commit 87e65dbbe81b1ba6a1921bd542dbd5db5f56ab19
Author: Pierre Villard <[email protected]>
AuthorDate: Thu Apr 9 17:06:17 2026 +0200

    NIFI-15785 Update Parameter and Parameter Context descriptions on versioned 
flow upgrades (#11096)
    
    Signed-off-by: David Handermann <[email protected]>
---
 .../StandardVersionedComponentSynchronizer.java    |  13 +-
 ...StandardVersionedComponentSynchronizerTest.java | 315 ++++++++++++++-------
 .../serialization/VersionedFlowSynchronizer.java   |   6 +-
 .../registry/ParameterContextPreservationIT.java   |  94 ++++++
 4 files changed, 322 insertions(+), 106 deletions(-)

diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/flow/synchronization/StandardVersionedComponentSynchronizer.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/flow/synchronization/StandardVersionedComponentSynchronizer.java
index 092d2f7e7bb..3334380aaa7 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/flow/synchronization/StandardVersionedComponentSynchronizer.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/flow/synchronization/StandardVersionedComponentSynchronizer.java
@@ -2375,7 +2375,14 @@ public class StandardVersionedComponentSynchronizer 
implements VersionedComponen
         for (final VersionedParameter versionedParameter : 
versionedParameterContext.getParameters()) {
             final Optional<Parameter> parameterOption = 
currentParameterContext.getParameter(versionedParameter.getName());
             if (parameterOption.isPresent()) {
-                // Skip this parameter, since it is already defined. We only 
want to add missing parameters
+                final Parameter existingParameter = parameterOption.get();
+                if 
(!Objects.equals(existingParameter.getDescriptor().getDescription(), 
versionedParameter.getDescription())) {
+                    final Parameter updatedParameter = new Parameter.Builder()
+                        .fromParameter(existingParameter)
+                        .description(versionedParameter.getDescription())
+                        .build();
+                    parameters.put(versionedParameter.getName(), 
updatedParameter);
+                }
                 continue;
             }
 
@@ -2385,6 +2392,10 @@ public class StandardVersionedComponentSynchronizer 
implements VersionedComponen
 
         currentParameterContext.setParameters(parameters);
 
+        if (!Objects.equals(currentParameterContext.getDescription(), 
versionedParameterContext.getDescription())) {
+            
currentParameterContext.setDescription(versionedParameterContext.getDescription());
+        }
+
         // If the current parameter context doesn't have any inherited param 
contexts but the versioned one does,
         // add the versioned ones.
         if (currentParameterContext.getInheritedParameterContexts().isEmpty()
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/flow/synchronization/StandardVersionedComponentSynchronizerTest.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/flow/synchronization/StandardVersionedComponentSynchronizerTest.java
index 1dd5bd17f7f..6793d09216f 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/flow/synchronization/StandardVersionedComponentSynchronizerTest.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/flow/synchronization/StandardVersionedComponentSynchronizerTest.java
@@ -144,6 +144,44 @@ public class StandardVersionedComponentSynchronizerTest {
 
     private static final String SENSITIVE_PROPERTY_NAME = "Access Token";
 
+    private static final String PARAM_ABC = "abc";
+
+    private static final String PARAM_SECRET = "secret";
+
+    private static final String VALUE_XYZ = "xyz";
+
+    private static final String VALUE_YES = "yes";
+
+    private static final String VALUE_123 = "123";
+
+    private static final String VALUE_MAYBE = "maybe";
+
+    private static final String CONTEXT_NAME_1 = "Context 1";
+
+    private static final String CONTEXT_NAME_2 = "Context 2";
+
+    private static final String CONTEXT_NAME_PARAMS = "Params";
+
+    private static final Set<String> SENSITIVE_PARAM_NAMES = 
Set.of(PARAM_SECRET);
+
+    private static final Map<String, String> SINGLE_PARAMETER = 
Map.of(PARAM_ABC, VALUE_XYZ);
+
+    private static final Map<String, String> INITIAL_PARAMETERS = 
Map.of(PARAM_ABC, VALUE_XYZ, PARAM_SECRET, VALUE_YES);
+
+    private static final Map<String, String> UPDATED_PARAMETERS = 
Map.of(PARAM_ABC, VALUE_123, PARAM_SECRET, VALUE_MAYBE);
+
+    private static final String ORIGINAL_PARAMETER_DESCRIPTION = "Original 
description";
+
+    private static final String UPDATED_PARAMETER_DESCRIPTION = "Updated 
description";
+
+    private static final Map<String, String> ORIGINAL_DESCRIPTION_MAP = 
Map.of(PARAM_ABC, ORIGINAL_PARAMETER_DESCRIPTION);
+
+    private static final Map<String, String> UPDATED_DESCRIPTION_MAP = 
Map.of(PARAM_ABC, UPDATED_PARAMETER_DESCRIPTION);
+
+    private static final String ORIGINAL_CONTEXT_DESCRIPTION = "Generated for 
unit test";
+
+    private static final String UPDATED_CONTEXT_DESCRIPTION = "Updated context 
description";
+
     private ProcessorNode processorA;
     private ProcessorNode processorB;
     private Connection connectionAB;
@@ -1010,10 +1048,7 @@ public class StandardVersionedComponentSynchronizerTest {
 
     @Test
     public void testCreatingParameterContext() throws 
FlowSynchronizationException, InterruptedException, TimeoutException {
-        final Map<String, String> parameterMap = new HashMap<>();
-        parameterMap.put("abc", "xyz");
-        parameterMap.put("secret", "yes");
-        final VersionedParameterContext proposed = 
createVersionedParameterContext("Context 1", parameterMap, 
Collections.singleton("secret"));
+        final VersionedParameterContext proposed = 
createVersionedParameterContext(CONTEXT_NAME_1, INITIAL_PARAMETERS, 
SENSITIVE_PARAM_NAMES);
 
         synchronizer.synchronize(null, proposed, synchronizationOptions);
 
@@ -1026,75 +1061,56 @@ public class StandardVersionedComponentSynchronizerTest 
{
         final Map<ParameterDescriptor, Parameter> createdParameters = 
created.getParameters();
         assertEquals(2, createdParameters.size());
 
-        final Parameter abc = created.getParameter("abc").get();
-        assertEquals("abc", abc.getDescriptor().getName());
+        final Parameter abc = created.getParameter(PARAM_ABC).get();
+        assertEquals(PARAM_ABC, abc.getDescriptor().getName());
         assertFalse(abc.getDescriptor().isSensitive());
-        assertEquals("xyz", abc.getValue());
+        assertEquals(VALUE_XYZ, abc.getValue());
 
-        final Parameter secret = created.getParameter("secret").get();
-        assertEquals("secret", secret.getDescriptor().getName());
+        final Parameter secret = created.getParameter(PARAM_SECRET).get();
+        assertEquals(PARAM_SECRET, secret.getDescriptor().getName());
         assertTrue(secret.getDescriptor().isSensitive());
-        assertEquals("yes", secret.getValue());
+        assertEquals(VALUE_YES, secret.getValue());
     }
 
     @Test
     public void testUpdateParametersNoReferences() throws 
FlowSynchronizationException, InterruptedException, TimeoutException {
-        // Create the initial context
         testCreatingParameterContext();
 
         final ParameterContext existing = 
parameterContextManager.getParameterContexts().iterator().next();
-
-        final Map<String, String> parameterMap = new HashMap<>();
-        parameterMap.put("abc", "123");
-        parameterMap.put("secret", "maybe");
-
-        final VersionedParameterContext proposed = 
createVersionedParameterContext("Context 2", parameterMap, 
Collections.singleton("secret"));
+        final VersionedParameterContext proposed = 
createVersionedParameterContext(CONTEXT_NAME_2, UPDATED_PARAMETERS, 
SENSITIVE_PARAM_NAMES);
 
         synchronizer.synchronize(existing, proposed, synchronizationOptions);
 
-        assertEquals("123", existing.getParameter("abc").get().getValue());
-        assertEquals("maybe", 
existing.getParameter("secret").get().getValue());
-        assertEquals("Context 2", existing.getName());
+        assertEquals(VALUE_123, 
existing.getParameter(PARAM_ABC).get().getValue());
+        assertEquals(VALUE_MAYBE, 
existing.getParameter(PARAM_SECRET).get().getValue());
+        assertEquals(CONTEXT_NAME_2, existing.getName());
     }
 
     @Test
     public void testUpdateParametersReferenceProcessorNotStopping() throws 
FlowSynchronizationException, InterruptedException, TimeoutException {
-        // Create the initial context
         testCreatingParameterContext();
 
         final ParameterContext existing = 
parameterContextManager.getParameterContexts().iterator().next();
-
-        final Map<String, String> parameterMap = new HashMap<>();
-        parameterMap.put("abc", "123");
-        parameterMap.put("secret", "maybe");
-
-        final VersionedParameterContext proposed = 
createVersionedParameterContext("Context 2", parameterMap, 
Collections.singleton("secret"));
+        final VersionedParameterContext proposed = 
createVersionedParameterContext(CONTEXT_NAME_2, UPDATED_PARAMETERS, 
SENSITIVE_PARAM_NAMES);
 
         final ProcessorNode processorA = createMockProcessor();
         startProcessor(processorA, false);
         synchronizationOptions = 
createQuickFailSynchronizationOptions(FlowSynchronizationOptions.ComponentStopTimeoutAction.THROW_TIMEOUT_EXCEPTION);
-        when(parameterReferenceManager.getProcessorsReferencing(existing, 
"abc")).thenReturn(Collections.singleton(processorA));
+        when(parameterReferenceManager.getProcessorsReferencing(existing, 
PARAM_ABC)).thenReturn(Collections.singleton(processorA));
 
         assertThrows(TimeoutException.class, () -> 
synchronizer.synchronize(existing, proposed, synchronizationOptions));
 
-        // Updates should not occur.
-        assertEquals("xyz", existing.getParameter("abc").get().getValue());
-        assertEquals("yes", existing.getParameter("secret").get().getValue());
-        assertEquals("Context 1", existing.getName());
+        assertEquals(VALUE_XYZ, 
existing.getParameter(PARAM_ABC).get().getValue());
+        assertEquals(VALUE_YES, 
existing.getParameter(PARAM_SECRET).get().getValue());
+        assertEquals(CONTEXT_NAME_1, existing.getName());
     }
 
     @Test
     public void testUpdateParametersReferenceStopping() throws 
FlowSynchronizationException, InterruptedException, TimeoutException {
-        // Create the initial context
         testCreatingParameterContext();
 
         final ParameterContext existing = 
parameterContextManager.getParameterContexts().iterator().next();
-
-        final Map<String, String> parameterMap = new HashMap<>();
-        parameterMap.put("abc", "123");
-        parameterMap.put("secret", "maybe");
-
-        final VersionedParameterContext proposed = 
createVersionedParameterContext("Context 2", parameterMap, 
Collections.singleton("secret"));
+        final VersionedParameterContext proposed = 
createVersionedParameterContext(CONTEXT_NAME_2, UPDATED_PARAMETERS, 
SENSITIVE_PARAM_NAMES);
 
         final ProcessorNode processorA = createMockProcessor();
         startProcessor(processorA, true);
@@ -1107,7 +1123,6 @@ public class StandardVersionedComponentSynchronizerTest {
         when(service.isActive()).thenAnswer(invocation -> serviceActive.get());
         when(service.getState()).thenAnswer(invocation -> serviceActive.get() 
? ControllerServiceState.ENABLED : ControllerServiceState.DISABLED);
 
-        // Make Processors A and B reference the controller service and start 
them
         setReferences(service, processorA, processorB);
         startProcessor(processorB);
 
@@ -1117,17 +1132,15 @@ public class StandardVersionedComponentSynchronizerTest 
{
             return CompletableFuture.completedFuture(null);
         });
 
-        when(parameterReferenceManager.getProcessorsReferencing(existing, 
"abc")).thenReturn(Collections.emptySet());
-        
when(parameterReferenceManager.getControllerServicesReferencing(existing, 
"abc")).thenReturn(Collections.singleton(service));
+        when(parameterReferenceManager.getProcessorsReferencing(existing, 
PARAM_ABC)).thenReturn(Collections.emptySet());
+        
when(parameterReferenceManager.getControllerServicesReferencing(existing, 
PARAM_ABC)).thenReturn(Collections.singleton(service));
 
         synchronizer.synchronize(existing, proposed, synchronizationOptions);
 
-        // Updates should occur.
-        assertEquals("123", existing.getParameter("abc").get().getValue());
-        assertEquals("maybe", 
existing.getParameter("secret").get().getValue());
-        assertEquals("Context 2", existing.getName());
+        assertEquals(VALUE_123, 
existing.getParameter(PARAM_ABC).get().getValue());
+        assertEquals(VALUE_MAYBE, 
existing.getParameter(PARAM_SECRET).get().getValue());
+        assertEquals(CONTEXT_NAME_2, existing.getName());
 
-        // Verify controller service/reference lifecycles
         
verify(controllerServiceProvider).unscheduleReferencingComponents(service);
         
verify(controllerServiceProvider).disableControllerServicesAsync(Collections.singleton(service));
         
verify(controllerServiceProvider).enableControllerServicesAsync(Collections.singleton(service));
@@ -1136,16 +1149,10 @@ public class StandardVersionedComponentSynchronizerTest 
{
 
     @Test
     public void testUpdateParametersControllerServiceNotDisabling() throws 
FlowSynchronizationException, InterruptedException, TimeoutException {
-        // Create the initial context
         testCreatingParameterContext();
 
         final ParameterContext existing = 
parameterContextManager.getParameterContexts().iterator().next();
-
-        final Map<String, String> parameterMap = new HashMap<>();
-        parameterMap.put("abc", "123");
-        parameterMap.put("secret", "maybe");
-
-        final VersionedParameterContext proposed = 
createVersionedParameterContext("Context 2", parameterMap, 
Collections.singleton("secret"));
+        final VersionedParameterContext proposed = 
createVersionedParameterContext(CONTEXT_NAME_2, UPDATED_PARAMETERS, 
SENSITIVE_PARAM_NAMES);
 
         final ProcessorNode processorA = createMockProcessor();
         final ProcessorNode processorB = createMockProcessor();
@@ -1154,7 +1161,6 @@ public class StandardVersionedComponentSynchronizerTest {
         when(service.isActive()).thenReturn(true);
         when(service.getState()).thenReturn(ControllerServiceState.ENABLED);
 
-        // Make Processors A and B reference the controller service and start 
them
         setReferences(service, processorA, processorB);
         startProcessor(processorA, true);
         startProcessor(processorB);
@@ -1164,21 +1170,19 @@ public class StandardVersionedComponentSynchronizerTest 
{
         completedFutureMap.put(processorB, 
CompletableFuture.completedFuture(null));
 
         
when(controllerServiceProvider.unscheduleReferencingComponents(service)).thenReturn(completedFutureMap);
-        
when(controllerServiceProvider.disableControllerServicesAsync(anyCollection())).thenReturn(new
 CompletableFuture<>()); // Never complete future = never disable service
+        
when(controllerServiceProvider.disableControllerServicesAsync(anyCollection())).thenReturn(new
 CompletableFuture<>());
 
         synchronizationOptions = 
createQuickFailSynchronizationOptions(FlowSynchronizationOptions.ComponentStopTimeoutAction.TERMINATE);
 
-        when(parameterReferenceManager.getProcessorsReferencing(existing, 
"abc")).thenReturn(Collections.emptySet());
-        
when(parameterReferenceManager.getControllerServicesReferencing(existing, 
"abc")).thenReturn(Collections.singleton(service));
+        when(parameterReferenceManager.getProcessorsReferencing(existing, 
PARAM_ABC)).thenReturn(Collections.emptySet());
+        
when(parameterReferenceManager.getControllerServicesReferencing(existing, 
PARAM_ABC)).thenReturn(Collections.singleton(service));
 
         assertThrows(TimeoutException.class, () -> 
synchronizer.synchronize(existing, proposed, synchronizationOptions));
 
-        // Updates should not occur.
-        assertEquals("xyz", existing.getParameter("abc").get().getValue());
-        assertEquals("yes", existing.getParameter("secret").get().getValue());
-        assertEquals("Context 1", existing.getName());
+        assertEquals(VALUE_XYZ, 
existing.getParameter(PARAM_ABC).get().getValue());
+        assertEquals(VALUE_YES, 
existing.getParameter(PARAM_SECRET).get().getValue());
+        assertEquals(CONTEXT_NAME_1, existing.getName());
 
-        // Verify controller service/reference lifecycles
         
verify(controllerServiceProvider).unscheduleReferencingComponents(service);
         
verify(controllerServiceProvider).disableControllerServicesAsync(Collections.singleton(service));
         verify(controllerServiceProvider, 
times(0)).enableControllerServicesAsync(Collections.singleton(service));
@@ -1191,68 +1195,54 @@ public class StandardVersionedComponentSynchronizerTest 
{
         testCreatingParameterContext();
         final ParameterContext existing = 
parameterContextManager.getParameterContexts().iterator().next();
 
-        final Map<String, String> originalParams = new HashMap<>();
-        originalParams.put("abc", "xyz");
-        originalParams.put("secret", "yes");
-
-        // Test no changes
-        Map<String, String> parameterMap = new HashMap<>(originalParams);
-        VersionedParameterContext proposed = 
createVersionedParameterContext("Context 1", parameterMap, 
Collections.singleton("secret"));
+        Map<String, String> parameterMap = new HashMap<>(INITIAL_PARAMETERS);
+        VersionedParameterContext proposed = 
createVersionedParameterContext(CONTEXT_NAME_1, parameterMap, 
SENSITIVE_PARAM_NAMES);
         assertEquals(Collections.emptySet(), 
synchronizer.getUpdatedParameterNames(existing, proposed));
 
-        // Test non-sensitive param change
-        parameterMap = new HashMap<>(originalParams);
-        parameterMap.put("abc", "hello");
-        proposed = createVersionedParameterContext("Context 1", parameterMap, 
Collections.singleton("secret"));
-        assertEquals(Collections.singleton("abc"), 
synchronizer.getUpdatedParameterNames(existing, proposed));
+        parameterMap = new HashMap<>(INITIAL_PARAMETERS);
+        parameterMap.put(PARAM_ABC, "hello");
+        proposed = createVersionedParameterContext(CONTEXT_NAME_1, 
parameterMap, SENSITIVE_PARAM_NAMES);
+        assertEquals(Collections.singleton(PARAM_ABC), 
synchronizer.getUpdatedParameterNames(existing, proposed));
 
-        // Test sensitive param change
-        parameterMap = new HashMap<>(originalParams);
-        parameterMap.put("secret", "secret");
-        proposed = createVersionedParameterContext("Context 1", parameterMap, 
Collections.singleton("secret"));
-        assertEquals(Collections.singleton("secret"), 
synchronizer.getUpdatedParameterNames(existing, proposed));
+        parameterMap = new HashMap<>(INITIAL_PARAMETERS);
+        parameterMap.put(PARAM_SECRET, PARAM_SECRET);
+        proposed = createVersionedParameterContext(CONTEXT_NAME_1, 
parameterMap, SENSITIVE_PARAM_NAMES);
+        assertEquals(Collections.singleton(PARAM_SECRET), 
synchronizer.getUpdatedParameterNames(existing, proposed));
 
-        // Test removed parameters
         parameterMap.clear();
-        proposed = createVersionedParameterContext("Context 1", parameterMap, 
Collections.singleton("secret"));
-        assertEquals(new HashSet<>(Arrays.asList("abc", "secret")), 
synchronizer.getUpdatedParameterNames(existing, proposed));
+        proposed = createVersionedParameterContext(CONTEXT_NAME_1, 
parameterMap, SENSITIVE_PARAM_NAMES);
+        assertEquals(new HashSet<>(Arrays.asList(PARAM_ABC, PARAM_SECRET)), 
synchronizer.getUpdatedParameterNames(existing, proposed));
 
-        // Test added parameter
-        parameterMap = new HashMap<>(originalParams);
+        parameterMap = new HashMap<>(INITIAL_PARAMETERS);
         parameterMap.put("Added", "Added");
-        proposed = createVersionedParameterContext("Context 1", parameterMap, 
Collections.singleton("secret"));
+        proposed = createVersionedParameterContext(CONTEXT_NAME_1, 
parameterMap, SENSITIVE_PARAM_NAMES);
         assertEquals(Collections.singleton("Added"), 
synchronizer.getUpdatedParameterNames(existing, proposed));
 
-        // Test added, removed, and updated parameters
-        parameterMap = new HashMap<>(originalParams);
+        parameterMap = new HashMap<>(INITIAL_PARAMETERS);
         parameterMap.put("Added", "Added");
         parameterMap.put("Added 2", "Added");
-        parameterMap.remove("secret");
-        parameterMap.put("abc", "hello");
-        proposed = createVersionedParameterContext("Context 1", parameterMap, 
Collections.singleton("secret"));
-        assertEquals(new HashSet<>(Arrays.asList("abc", "secret", "Added", 
"Added 2")), synchronizer.getUpdatedParameterNames(existing, proposed));
+        parameterMap.remove(PARAM_SECRET);
+        parameterMap.put(PARAM_ABC, "hello");
+        proposed = createVersionedParameterContext(CONTEXT_NAME_1, 
parameterMap, SENSITIVE_PARAM_NAMES);
+        assertEquals(new HashSet<>(Arrays.asList(PARAM_ABC, PARAM_SECRET, 
"Added", "Added 2")), synchronizer.getUpdatedParameterNames(existing, 
proposed));
 
-        // Test change value due to inherited parameter context reordering
         final Map<String, String> inheritedParameters = new HashMap<>();
-        // Context 1: abc = xyz
-        // Context 3: abc = def
-        inheritedParameters.put("abc", "def");
+        inheritedParameters.put(PARAM_ABC, "def");
         final VersionedParameterContext context3 = 
createVersionedParameterContext("Context 3", inheritedParameters, 
Collections.emptySet());
 
         synchronizer.synchronize(null, context3, synchronizationOptions);
 
         parameterMap = new HashMap<>();
-        proposed = createVersionedParameterContext("Context 2", parameterMap, 
Collections.emptySet());
+        proposed = createVersionedParameterContext(CONTEXT_NAME_2, 
parameterMap, Collections.emptySet());
         synchronizer.synchronize(null, proposed, synchronizationOptions);
 
-        ParameterContext context2 = 
parameterContextManager.getParameterContextNameMapping().get("Context 2");
-        proposed.setInheritedParameterContexts(List.of("Context 1", "Context 
3"));
+        ParameterContext context2 = 
parameterContextManager.getParameterContextNameMapping().get(CONTEXT_NAME_2);
+        proposed.setInheritedParameterContexts(List.of(CONTEXT_NAME_1, 
"Context 3"));
         synchronizer.synchronize(context2, proposed, synchronizationOptions);
 
-        proposed.setInheritedParameterContexts(List.of("Context 3", "Context 
1"));
-        context2 = 
parameterContextManager.getParameterContextNameMapping().get("Context 2");
-        // The effective value of abc should change here due to the reordering
-        assertEquals(Collections.singleton("abc"), 
synchronizer.getUpdatedParameterNames(context2, proposed));
+        proposed.setInheritedParameterContexts(List.of("Context 3", 
CONTEXT_NAME_1));
+        context2 = 
parameterContextManager.getParameterContextNameMapping().get(CONTEXT_NAME_2);
+        assertEquals(Collections.singleton(PARAM_ABC), 
synchronizer.getUpdatedParameterNames(context2, proposed));
     }
 
     @Test
@@ -1353,6 +1343,123 @@ public class StandardVersionedComponentSynchronizerTest 
{
         assertTrue(p2.getParameter("paramA").isPresent(), "paramA should still 
exist on P2");
     }
 
+    @Test
+    public void testParameterDescriptionUpdatedDuringSync() throws 
FlowSynchronizationException, InterruptedException, TimeoutException {
+        final VersionedParameterContext versionedContext = 
createVersionedParameterContextWithDescriptions(CONTEXT_NAME_1,
+            SINGLE_PARAMETER, ORIGINAL_DESCRIPTION_MAP, 
Collections.emptySet());
+        synchronizer.synchronize(null, versionedContext, 
synchronizationOptions);
+
+        final ParameterContext context = 
parameterContextManager.getParameterContextNameMapping().get(CONTEXT_NAME_1);
+        assertEquals(ORIGINAL_PARAMETER_DESCRIPTION, 
context.getParameter(PARAM_ABC).get().getDescriptor().getDescription());
+
+        final VersionedParameterContext proposed = 
createVersionedParameterContextWithDescriptions(CONTEXT_NAME_1,
+            SINGLE_PARAMETER, UPDATED_DESCRIPTION_MAP, Collections.emptySet());
+
+        synchronizer.synchronize(context, proposed, synchronizationOptions);
+
+        assertEquals(UPDATED_PARAMETER_DESCRIPTION, 
context.getParameter(PARAM_ABC).get().getDescriptor().getDescription());
+        assertEquals(VALUE_XYZ, 
context.getParameter(PARAM_ABC).get().getValue());
+    }
+
+    @Test
+    public void testParameterDescriptionUpdatedDuringProcessGroupSync() throws 
FlowSynchronizationException, InterruptedException, TimeoutException {
+        final VersionedParameterContext versionedContext = 
createVersionedParameterContextWithDescriptions(CONTEXT_NAME_PARAMS,
+            SINGLE_PARAMETER, ORIGINAL_DESCRIPTION_MAP, 
Collections.emptySet());
+        synchronizer.synchronize(null, versionedContext, 
synchronizationOptions);
+
+        final ParameterContext paramContext = 
parameterContextManager.getParameterContextNameMapping().get(CONTEXT_NAME_PARAMS);
+        assertEquals(ORIGINAL_PARAMETER_DESCRIPTION, 
paramContext.getParameter(PARAM_ABC).get().getDescriptor().getDescription());
+
+        final ProcessGroup processGroup = createMockProcessGroup();
+        when(processGroup.getParameterContext()).thenReturn(paramContext);
+
+        final VersionedParameterContext proposedParams = 
createVersionedParameterContextWithDescriptions(CONTEXT_NAME_PARAMS,
+            SINGLE_PARAMETER, UPDATED_DESCRIPTION_MAP, Collections.emptySet());
+
+        final Map<String, VersionedParameterContext> parameterContextMap = 
Map.of(CONTEXT_NAME_PARAMS, proposedParams);
+
+        final VersionedProcessGroup rootGroup = new VersionedProcessGroup();
+        rootGroup.setIdentifier(processGroup.getIdentifier());
+        rootGroup.setParameterContextName(CONTEXT_NAME_PARAMS);
+
+        final VersionedExternalFlow externalFlow = new VersionedExternalFlow();
+        externalFlow.setFlowContents(rootGroup);
+        externalFlow.setParameterContexts(parameterContextMap);
+
+        synchronizer.synchronize(processGroup, externalFlow, 
synchronizationOptions);
+
+        assertEquals(UPDATED_PARAMETER_DESCRIPTION, 
paramContext.getParameter(PARAM_ABC).get().getDescriptor().getDescription());
+        assertEquals(VALUE_XYZ, 
paramContext.getParameter(PARAM_ABC).get().getValue());
+    }
+
+    @Test
+    public void testParameterContextDescriptionUpdatedDuringProcessGroupSync() 
throws FlowSynchronizationException, InterruptedException, TimeoutException {
+        final VersionedParameterContext versionedContext = 
createVersionedParameterContext(CONTEXT_NAME_PARAMS, SINGLE_PARAMETER, 
Collections.emptySet());
+        synchronizer.synchronize(null, versionedContext, 
synchronizationOptions);
+
+        final ParameterContext paramContext = 
parameterContextManager.getParameterContextNameMapping().get(CONTEXT_NAME_PARAMS);
+        assertEquals(ORIGINAL_CONTEXT_DESCRIPTION, 
paramContext.getDescription());
+
+        final ProcessGroup processGroup = createMockProcessGroup();
+        when(processGroup.getParameterContext()).thenReturn(paramContext);
+
+        final VersionedParameterContext proposedParams = 
createVersionedParameterContext(CONTEXT_NAME_PARAMS, SINGLE_PARAMETER, 
Collections.emptySet());
+        proposedParams.setDescription(UPDATED_CONTEXT_DESCRIPTION);
+
+        final Map<String, VersionedParameterContext> parameterContextMap = 
Map.of(CONTEXT_NAME_PARAMS, proposedParams);
+
+        final VersionedProcessGroup rootGroup = new VersionedProcessGroup();
+        rootGroup.setIdentifier(processGroup.getIdentifier());
+        rootGroup.setParameterContextName(CONTEXT_NAME_PARAMS);
+
+        final VersionedExternalFlow externalFlow = new VersionedExternalFlow();
+        externalFlow.setFlowContents(rootGroup);
+        externalFlow.setParameterContexts(parameterContextMap);
+
+        synchronizer.synchronize(processGroup, externalFlow, 
synchronizationOptions);
+
+        assertEquals(UPDATED_CONTEXT_DESCRIPTION, 
paramContext.getDescription());
+    }
+
+    @Test
+    public void testParameterDescriptionUnchangedWhenValueSame() throws 
FlowSynchronizationException, InterruptedException, TimeoutException {
+        final VersionedParameterContext versionedContext = 
createVersionedParameterContextWithDescriptions(CONTEXT_NAME_1,
+            SINGLE_PARAMETER, ORIGINAL_DESCRIPTION_MAP, 
Collections.emptySet());
+        synchronizer.synchronize(null, versionedContext, 
synchronizationOptions);
+
+        final ParameterContext context = 
parameterContextManager.getParameterContextNameMapping().get(CONTEXT_NAME_1);
+        assertEquals(ORIGINAL_PARAMETER_DESCRIPTION, 
context.getParameter(PARAM_ABC).get().getDescriptor().getDescription());
+
+        final VersionedParameterContext proposed = 
createVersionedParameterContextWithDescriptions(CONTEXT_NAME_1,
+            SINGLE_PARAMETER, ORIGINAL_DESCRIPTION_MAP, 
Collections.emptySet());
+
+        synchronizer.synchronize(context, proposed, synchronizationOptions);
+
+        assertEquals(ORIGINAL_PARAMETER_DESCRIPTION, 
context.getParameter(PARAM_ABC).get().getDescriptor().getDescription());
+        assertEquals(VALUE_XYZ, 
context.getParameter(PARAM_ABC).get().getValue());
+    }
+
+    private VersionedParameterContext 
createVersionedParameterContextWithDescriptions(final String name, final 
Map<String, String> parameters,
+                                                                               
       final Map<String, String> descriptions, final Set<String> 
sensitiveParamNames) {
+        final Set<VersionedParameter> versionedParameters = new HashSet<>();
+        for (final Map.Entry<String, String> entry : parameters.entrySet()) {
+            final VersionedParameter param = new VersionedParameter();
+            param.setName(entry.getKey());
+            param.setValue(entry.getValue());
+            param.setSensitive(sensitiveParamNames.contains(entry.getKey()));
+            param.setDescription(descriptions.get(entry.getKey()));
+            versionedParameters.add(param);
+        }
+
+        final VersionedParameterContext context = new 
VersionedParameterContext();
+        context.setName(name);
+        context.setDescription(ORIGINAL_CONTEXT_DESCRIPTION);
+        context.setParameters(versionedParameters);
+        context.setIdentifier(UUID.randomUUID().toString());
+
+        return context;
+    }
+
     private VersionedParameterContext createVersionedParameterContext(final 
String name, final Map<String, String> parameters, final Set<String> 
sensitiveParamNames) {
         final Set<VersionedParameter> versionedParameters = new HashSet<>();
         for (final Map.Entry<String, String> entry : parameters.entrySet()) {
@@ -1365,7 +1472,7 @@ public class StandardVersionedComponentSynchronizerTest {
 
         final VersionedParameterContext context = new 
VersionedParameterContext();
         context.setName(name);
-        context.setDescription("Generated for unit test");
+        context.setDescription(ORIGINAL_CONTEXT_DESCRIPTION);
         context.setParameters(versionedParameters);
         context.setIdentifier(UUID.randomUUID().toString());
 
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/VersionedFlowSynchronizer.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/VersionedFlowSynchronizer.java
index 2a07ac621fe..f2893fca8b1 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/VersionedFlowSynchronizer.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/VersionedFlowSynchronizer.java
@@ -982,9 +982,11 @@ public class VersionedFlowSynchronizer implements 
FlowSynchronizer {
 
         final Map<String, String> currentValues = new HashMap<>();
         final Map<String, Set<String>> currentAssetReferences = new 
HashMap<>();
+        final Map<String, String> currentDescriptions = new HashMap<>();
         parameterContext.getParameters().values().forEach(param -> {
             currentValues.put(param.getDescriptor().getName(), 
param.getValue());
             currentAssetReferences.put(param.getDescriptor().getName(), 
getAssetIds(param));
+            currentDescriptions.put(param.getDescriptor().getName(), 
param.getDescriptor().getDescription());
         });
 
         final Map<String, Parameter> updatedParameters = new HashMap<>();
@@ -993,11 +995,13 @@ public class VersionedFlowSynchronizer implements 
FlowSynchronizer {
             final String parameterName = parameter.getName();
             final String currentValue = currentValues.get(parameterName);
             final Set<String> currentAssetIds = 
currentAssetReferences.getOrDefault(parameterName, Collections.emptySet());
+            final String currentDescription = 
currentDescriptions.get(parameterName);
 
             final Parameter updatedParameterObject = 
parameters.get(parameterName);
             final String updatedValue = updatedParameterObject.getValue();
             final Set<String> updatedAssetIds = 
getAssetIds(updatedParameterObject);
-            if (!Objects.equals(currentValue, updatedValue) || 
!currentAssetIds.equals(updatedAssetIds)) {
+            final String updatedDescription = 
updatedParameterObject.getDescriptor().getDescription();
+            if (!Objects.equals(currentValue, updatedValue) || 
!currentAssetIds.equals(updatedAssetIds) || !Objects.equals(currentDescription, 
updatedDescription)) {
                 updatedParameters.put(parameterName, updatedParameterObject);
             }
             proposedParameterNames.add(parameterName);
diff --git 
a/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/registry/ParameterContextPreservationIT.java
 
b/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/registry/ParameterContextPreservationIT.java
index 592bd29958b..931763f3ebe 100644
--- 
a/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/registry/ParameterContextPreservationIT.java
+++ 
b/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/registry/ParameterContextPreservationIT.java
@@ -27,6 +27,7 @@ import 
org.apache.nifi.web.api.entity.FlowRegistryClientEntity;
 import org.apache.nifi.web.api.entity.ParameterContextEntity;
 import org.apache.nifi.web.api.entity.ParameterContextReferenceEntity;
 import org.apache.nifi.web.api.entity.ParameterContextUpdateRequestEntity;
+import org.apache.nifi.web.api.entity.ParameterEntity;
 import org.apache.nifi.web.api.entity.ProcessGroupEntity;
 import org.apache.nifi.web.api.entity.ProcessGroupFlowEntity;
 import org.apache.nifi.web.api.entity.ProcessorEntity;
@@ -64,6 +65,11 @@ class ParameterContextPreservationIT extends NiFiSystemIT {
     private static final String RELATIONSHIP_SUCCESS = "success";
     private static final String VERSION_1 = "1";
     private static final String VERSION_2 = "2";
+    private static final String DESCRIPTION_UPDATE_FLOW_NAME = 
"DescriptionUpdateFlow";
+    private static final String PARAMETER_DESCRIPTION_V1 = "Description for 
version 1";
+    private static final String PARAMETER_DESCRIPTION_V2 = "Description for 
version 2";
+    private static final String CONTEXT_DESCRIPTION_V1 = "Context description 
v1";
+    private static final String CONTEXT_DESCRIPTION_V2 = "Context description 
v2";
 
     @Test
     void testNewProcessGroupUsesCorrectParameterContextDuringUpgrade() throws 
NiFiClientException, IOException, InterruptedException {
@@ -228,6 +234,94 @@ class ParameterContextPreservationIT extends NiFiSystemIT {
         assertTrue(p2NamesAfterUpgrade.contains("paramX"), "paramX should 
exist on P2 after upgrading to version 2");
     }
 
+    /**
+     * Verifies that parameter descriptions are updated when upgrading a 
versioned process group from one version
+     * to the next, even when the parameter value itself remains unchanged.
+     */
+    @Test
+    void testParameterDescriptionUpdatedDuringVersionUpgrade() throws 
NiFiClientException, IOException, InterruptedException {
+        final FlowRegistryClientEntity clientEntity = registerClient();
+        final NiFiClientUtil util = getClientUtil();
+
+        final Set<ParameterEntity> parameterEntitiesV1 = 
Set.of(util.createParameterEntity(PARAMETER_NAME, PARAMETER_DESCRIPTION_V1, 
false, PARAMETER_VALUE));
+        final ParameterContextEntity paramContext = 
getNifiClient().getParamContextClient().createParamContext(
+            util.createParameterContextEntity(PARAMETER_CONTEXT_NAME, 
CONTEXT_DESCRIPTION_V1, parameterEntitiesV1));
+
+        final ProcessGroupEntity groupA = 
util.createProcessGroup(GROUP_A_NAME, "root");
+        util.setParameterContext(groupA.getId(), paramContext);
+
+        final ProcessorEntity processor = util.createProcessor(PROCESSOR_TYPE, 
groupA.getId());
+        util.updateProcessorProperties(processor, 
Collections.singletonMap(PROCESSOR_PROPERTY_TEXT, PARAMETER_REFERENCE));
+        util.setAutoTerminatedRelationships(processor, RELATIONSHIP_SUCCESS);
+
+        final VersionControlInformationEntity vciV1 = 
util.startVersionControl(groupA, clientEntity, TEST_FLOWS_BUCKET, 
DESCRIPTION_UPDATE_FLOW_NAME);
+        final String flowId = vciV1.getVersionControlInformation().getFlowId();
+
+        // Update the parameter description (keeping the same value) and 
context description, then save as version 2
+        final ParameterContextEntity currentContext = 
getNifiClient().getParamContextClient().getParamContext(paramContext.getId(), 
false);
+        final Set<ParameterEntity> parameterEntitiesV2 = 
Set.of(util.createParameterEntity(PARAMETER_NAME, PARAMETER_DESCRIPTION_V2, 
false, PARAMETER_VALUE));
+        final ParameterContextEntity entityUpdate = 
util.createParameterContextEntity(PARAMETER_CONTEXT_NAME, 
CONTEXT_DESCRIPTION_V2, parameterEntitiesV2);
+        entityUpdate.setId(currentContext.getId());
+        entityUpdate.setRevision(currentContext.getRevision());
+        
entityUpdate.getComponent().setId(currentContext.getComponent().getId());
+        final ParameterContextUpdateRequestEntity updateRequest = 
getNifiClient().getParamContextClient().updateParamContext(entityUpdate);
+        util.waitForParameterContextRequestToComplete(paramContext.getId(), 
updateRequest.getRequest().getRequestId());
+
+        final ProcessGroupEntity refreshedGroupA = 
getNifiClient().getProcessGroupClient().getProcessGroup(groupA.getId());
+        util.saveFlowVersion(refreshedGroupA, clientEntity, vciV1);
+
+        // Clean up the original flow
+        final ProcessGroupEntity groupAForStopVc = 
getNifiClient().getProcessGroupClient().getProcessGroup(groupA.getId());
+        
getNifiClient().getVersionsClient().stopVersionControl(groupAForStopVc);
+        util.deleteAll(groupA.getId());
+        final ProcessGroupEntity groupAToDelete = 
getNifiClient().getProcessGroupClient().getProcessGroup(groupA.getId());
+        
getNifiClient().getProcessGroupClient().deleteProcessGroup(groupAToDelete);
+
+        final ParameterContextEntity contextToDelete = 
getNifiClient().getParamContextClient().getParamContext(paramContext.getId(), 
false);
+        
getNifiClient().getParamContextClient().deleteParamContext(paramContext.getId(),
 String.valueOf(contextToDelete.getRevision().getVersion()));
+
+        // Import version 1 fresh
+        final ProcessGroupEntity importedGroup = 
importFlowWithReplaceParameterContext(clientEntity.getId(), flowId, VERSION_1);
+
+        final ProcessGroupEntity fetchedGroup = 
getNifiClient().getProcessGroupClient().getProcessGroup(importedGroup.getId());
+        final String importedContextId = 
fetchedGroup.getComponent().getParameterContext().getId();
+
+        // Verify version 1 descriptions
+        final ParameterContextEntity importedContext = 
getNifiClient().getParamContextClient().getParamContext(importedContextId, 
false);
+        final String descriptionAfterV1 = 
getParameterDescription(importedContext, PARAMETER_NAME);
+        assertEquals(PARAMETER_DESCRIPTION_V1, descriptionAfterV1);
+        assertEquals(CONTEXT_DESCRIPTION_V1, 
importedContext.getComponent().getDescription());
+
+        // Upgrade from version 1 to version 2
+        util.changeFlowVersion(importedGroup.getId(), VERSION_2);
+
+        // Verify descriptions were updated to version 2
+        final ParameterContextEntity contextAfterUpgrade = 
getNifiClient().getParamContextClient().getParamContext(importedContextId, 
false);
+        final String descriptionAfterV2 = 
getParameterDescription(contextAfterUpgrade, PARAMETER_NAME);
+        assertEquals(PARAMETER_DESCRIPTION_V2, descriptionAfterV2);
+        assertEquals(CONTEXT_DESCRIPTION_V2, 
contextAfterUpgrade.getComponent().getDescription());
+
+        // Verify the value was not changed
+        final String valueAfterUpgrade = 
getParameterValue(contextAfterUpgrade, PARAMETER_NAME);
+        assertEquals(PARAMETER_VALUE, valueAfterUpgrade);
+    }
+
+    private String getParameterDescription(final ParameterContextEntity 
context, final String parameterName) {
+        return context.getComponent().getParameters().stream()
+            .filter(entity -> 
parameterName.equals(entity.getParameter().getName()))
+            .map(entity -> entity.getParameter().getDescription())
+            .findFirst()
+            .orElse(null);
+    }
+
+    private String getParameterValue(final ParameterContextEntity context, 
final String parameterName) {
+        return context.getComponent().getParameters().stream()
+            .filter(entity -> 
parameterName.equals(entity.getParameter().getName()))
+            .map(entity -> entity.getParameter().getValue())
+            .findFirst()
+            .orElse(null);
+    }
+
     private Set<String> getParameterNames(final ParameterContextEntity 
context) {
         return context.getComponent().getParameters().stream()
             .map(entity -> entity.getParameter().getName())


Reply via email to