Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -776,6 +776,10 @@ private void validateReferencingComponents(final String parameterName, final Par
final boolean isDeletion = (parameter == null);
final String action = isDeletion ? "remove" : "update";
for (final ProcessorNode procNode : parameterReferenceManager.getProcessorsReferencing(this, parameterName)) {
if (procNode.isExtensionMissing()) {
continue;
}

if (procNode.isRunning() && (isDeletion || duringUpdate)) {
throw new IllegalStateException("Cannot " + action + " parameter '" + parameterName + "' because it is referenced by " + procNode + ", which is currently running");
}
Expand All @@ -786,6 +790,10 @@ private void validateReferencingComponents(final String parameterName, final Par
}

for (final ControllerServiceNode serviceNode : parameterReferenceManager.getControllerServicesReferencing(this, parameterName)) {
if (serviceNode.isExtensionMissing()) {
continue;
}

final ControllerServiceState serviceState = serviceNode.getState();
if (serviceState != ControllerServiceState.DISABLED && (isDeletion || duringUpdate)) {
throw new IllegalStateException("Cannot " + action + " parameter '" + parameterName + "' because it is referenced by "
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
*/
package org.apache.nifi.parameter;

import org.apache.nifi.components.PropertyDescriptor;
import org.apache.nifi.controller.ProcessorNode;
import org.apache.nifi.controller.PropertyConfiguration;
import org.apache.nifi.controller.service.ControllerServiceNode;
import org.apache.nifi.controller.service.ControllerServiceState;
import org.apache.nifi.groups.ProcessGroup;
Expand All @@ -40,6 +42,8 @@
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class TestStandardParameterContext {

Expand Down Expand Up @@ -370,8 +374,8 @@ public void testChangingNestedParameterForRunningProcessor() {
}

private static ProcessorNode getProcessorNode(String parameterName, HashMapParameterReferenceManager referenceManager) {
final ProcessorNode procNode = Mockito.mock(ProcessorNode.class);
Mockito.when(procNode.isRunning()).thenReturn(false);
final ProcessorNode procNode = mock(ProcessorNode.class);
when(procNode.isRunning()).thenReturn(false);
referenceManager.addProcessorReference(parameterName, procNode);
return procNode;
}
Expand All @@ -385,27 +389,100 @@ private static void stopProcessor(final ProcessorNode processorNode) {
}

private static void setProcessorRunning(final ProcessorNode processorNode, final boolean isRunning) {
Mockito.when(processorNode.isRunning()).thenReturn(isRunning);
when(processorNode.isRunning()).thenReturn(isRunning);
}

private static void setControllerServiceState(final ControllerServiceNode serviceNode, final ControllerServiceState state) {
Mockito.when(serviceNode.getState()).thenReturn(state);
when(serviceNode.getState()).thenReturn(state);
}

private static void enableControllerService(final ControllerServiceNode serviceNode) {
setControllerServiceState(serviceNode, ControllerServiceState.ENABLED);
}

@Test
public void testGhostedProcessorSkippedDuringParameterValidation() {
final HashMapParameterReferenceManager referenceManager = new HashMapParameterReferenceManager();
final ParameterContext context = createStandardParameterContext(referenceManager);

final ProcessorNode procNode = getProcessorNode("abc", referenceManager);
when(procNode.isExtensionMissing()).thenReturn(true);

// Set up the ghosted processor to reference "abc" via a sensitive property
final PropertyDescriptor sensitiveProperty = new PropertyDescriptor.Builder().name("sensitive-prop").sensitive(true).build();
final ParameterReference paramReference = mock(ParameterReference.class);
when(paramReference.getParameterName()).thenReturn("abc");
final PropertyConfiguration propertyConfig = mock(PropertyConfiguration.class);
when(propertyConfig.getParameterReferences()).thenReturn(Collections.singletonList(paramReference));
when(procNode.getProperties()).thenReturn(Collections.singletonMap(sensitiveProperty, propertyConfig));

// Adding parameter "abc" as non-sensitive should succeed despite the sensitivity mismatch because the processor is ghosted
final ParameterDescriptor abcDescriptor = new ParameterDescriptor.Builder().name("abc").sensitive(false).build();
final Map<String, Parameter> parameters = new HashMap<>();
parameters.put("abc", createParameter(abcDescriptor, "123"));
context.setParameters(parameters);
assertEquals("123", context.getParameter("abc").get().getValue());

// Updating the parameter value should succeed because the processor is ghosted
parameters.clear();
parameters.put("abc", createParameter(abcDescriptor, "321"));
context.setParameters(parameters);
assertEquals("321", context.getParameter("abc").get().getValue());

// Deleting the parameter should succeed because the processor is ghosted
parameters.clear();
parameters.put("abc", null);
context.setParameters(parameters);
assertFalse(context.getParameter("abc").isPresent());
}

@Test
public void testGhostedControllerServiceSkippedDuringParameterValidation() {
final HashMapParameterReferenceManager referenceManager = new HashMapParameterReferenceManager();
final ParameterContext context = createStandardParameterContext(referenceManager);

final ControllerServiceNode serviceNode = mock(ControllerServiceNode.class);
when(serviceNode.isExtensionMissing()).thenReturn(true);
referenceManager.addControllerServiceReference("abc", serviceNode);

// Set up the ghosted controller service to reference "abc" via a non-sensitive property
final PropertyDescriptor nonSensitiveProperty = new PropertyDescriptor.Builder().name("non-sensitive-prop").sensitive(false).build();
final ParameterReference paramReference = mock(ParameterReference.class);
when(paramReference.getParameterName()).thenReturn("abc");
final PropertyConfiguration propertyConfig = mock(PropertyConfiguration.class);
when(propertyConfig.getParameterReferences()).thenReturn(Collections.singletonList(paramReference));
when(serviceNode.getProperties()).thenReturn(Collections.singletonMap(nonSensitiveProperty, propertyConfig));

// Adding parameter "abc" as sensitive should succeed despite the sensitivity mismatch because the controller service is ghosted
final ParameterDescriptor abcDescriptor = new ParameterDescriptor.Builder().name("abc").sensitive(true).build();
final Map<String, Parameter> parameters = new HashMap<>();
parameters.put("abc", createParameter(abcDescriptor, "123"));
context.setParameters(parameters);
assertEquals("123", context.getParameter("abc").get().getValue());

// Updating the parameter value should succeed because the controller service is ghosted
parameters.clear();
parameters.put("abc", createParameter(abcDescriptor, "321"));
context.setParameters(parameters);
assertEquals("321", context.getParameter("abc").get().getValue());

// Deleting the parameter should succeed because the controller service is ghosted
parameters.clear();
parameters.put("abc", null);
context.setParameters(parameters);
assertFalse(context.getParameter("abc").isPresent());
}

@Test
public void testAlertReferencingComponents() {
final String inheritedParamName = "def";
final String originalValue = "123";

final HashMapParameterReferenceManager referenceManager = Mockito.spy(new HashMapParameterReferenceManager());
final Set<ProcessGroup> processGroups = new HashSet<>();
final ProcessGroup processGroup = Mockito.mock(ProcessGroup.class);
final ProcessGroup processGroup = mock(ProcessGroup.class);
processGroups.add(processGroup);
Mockito.when(referenceManager.getProcessGroupsBound(ArgumentMatchers.any())).thenReturn(processGroups);
when(referenceManager.getProcessGroupsBound(ArgumentMatchers.any())).thenReturn(processGroups);
final StandardParameterContextManager parameterContextLookup = new StandardParameterContextManager();
final ParameterContext a = createParameterContext("a", parameterContextLookup, referenceManager);
addParameter(a, "abc", "123");
Expand Down Expand Up @@ -446,7 +523,7 @@ public void testChangingNestedParameterForEnabledControllerService() {
// Param abc
// (Inherited) Param def (from B)

final ControllerServiceNode serviceNode = Mockito.mock(ControllerServiceNode.class);
final ControllerServiceNode serviceNode = mock(ControllerServiceNode.class);
enableControllerService(serviceNode);

referenceManager.addControllerServiceReference(inheritedParamName, serviceNode);
Expand Down Expand Up @@ -476,7 +553,7 @@ public void testChangingNestedParameterForEnabledControllerService() {
public void testChangingParameterForEnabledControllerService() {
final HashMapParameterReferenceManager referenceManager = new HashMapParameterReferenceManager();
final ParameterContext context = createStandardParameterContext(referenceManager);
final ControllerServiceNode serviceNode = Mockito.mock(ControllerServiceNode.class);
final ControllerServiceNode serviceNode = mock(ControllerServiceNode.class);
enableControllerService(serviceNode);

final ParameterDescriptor abcDescriptor = new ParameterDescriptor.Builder().name("abc").sensitive(true).build();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/*
* 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.tests.system.parameters;

import org.apache.nifi.tests.system.NiFiInstance;
import org.apache.nifi.tests.system.NiFiSystemIT;
import org.apache.nifi.toolkit.client.NiFiClientException;
import org.apache.nifi.web.api.entity.ParameterContextEntity;
import org.apache.nifi.web.api.entity.ParameterContextUpdateRequestEntity;
import org.apache.nifi.web.api.entity.ProcessorEntity;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Collections;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;

/**
* System test that verifies parameter updates succeed when a referencing processor is ghosted.
*
* When a processor is ghosted (its NAR is missing), all of its properties are treated as sensitive because
* NiFi does not know the actual property descriptors. This means a non-sensitive parameter that was originally
* referenced from a non-sensitive property now appears to be referenced from a sensitive property, which would
* normally block any update to that parameter. The fix in StandardParameterContext skips validation for ghosted
* components entirely, allowing the parameter to be updated.
*
* This test exercises the full lifecycle:
* 1. A processor references a non-sensitive parameter via a non-sensitive property.
* 2. The processor's NAR is removed and NiFi is restarted, causing the processor to become ghosted.
* 3. While the processor is ghosted, the parameter value is updated (remaining non-sensitive).
* 4. The NAR is restored and NiFi is restarted.
* 5. The processor is no longer ghosted and can still be used with the updated parameter value.
*/
public class ParameterSensitivityWithGhostedComponentIT extends NiFiSystemIT {
private static final Logger logger = LoggerFactory.getLogger(ParameterSensitivityWithGhostedComponentIT.class);

private static final String TEST_EXTENSIONS_NAR_PREFIX = "nifi-system-test-extensions-nar";

@Override
protected boolean isDestroyEnvironmentAfterEachTest() {
return true;
}

@Override
protected boolean isAllowFactoryReuse() {
return false;
}

@Test
public void testParameterUpdateWhileProcessorGhosted() throws NiFiClientException, IOException, InterruptedException {
// Create a parameter context with a non-sensitive parameter
final ParameterContextEntity paramContext = getClientUtil().createParameterContext("TestContext", "myParam", "hello", false);
final String paramContextId = paramContext.getId();

// Create a CountEvents processor and set its non-sensitive "Name" property to reference the parameter
final ProcessorEntity processor = getClientUtil().createProcessor("CountEvents");
getClientUtil().updateProcessorProperties(processor, Collections.singletonMap("Name", "#{myParam}"));
final String processorId = processor.getId();

// Bind parameter context to root process group
getClientUtil().setParameterContext("root", paramContext);

// Verify processor is VALID
getClientUtil().waitForValidProcessor(processorId);
logger.info("Processor {} is VALID with non-sensitive parameter value 'hello'", processorId);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of the system test do not have local logging, it seems like most or all of the test logging should be removed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed that most system tests don't have much of any logging. But I don't think I would agree that is advantageous. I've found of late that adding this type of logging can be valuable when troubleshooting because the timestamps can be easily correlated with the timestamps in the nifi-app.log, nifi-user.log, etc. making it easier to understand what happened. I also find it's helpful when running tests manually from an IDE, if things are taking a bit and especially if it gets "stuck" for some reason it makes it much more clear what's happening. So I think rather than stripping out all of the logging here we need to consider adding in more logging to the system tests going forward.


// Stop NiFi and remove the test extensions NAR to ghost the processor
final NiFiInstance nifiInstance = getNiFiInstance();
nifiInstance.stop();

final File instanceDir = nifiInstance.getInstanceDirectory();
final File libDir = new File(instanceDir, "lib");
final File narFile = findTestExtensionsNar(libDir);
assertNotNull(narFile, "Could not find test extensions NAR in " + libDir.getAbsolutePath());

final Path movedNarPath = instanceDir.toPath().resolve(narFile.getName() + ".removed");
Files.move(narFile.toPath(), movedNarPath, StandardCopyOption.REPLACE_EXISTING);
logger.info("Moved NAR {} to {} to simulate missing extension", narFile.getName(), movedNarPath);

// Restart NiFi without the NAR
nifiInstance.start();
setupClient();

// Verify the processor is now ghosted
waitFor(() -> {
final ProcessorEntity processorEntity = getNifiClient().getProcessorClient().getProcessor(processorId);
return processorEntity.getComponent().getExtensionMissing();
});
logger.info("Processor {} is ghosted (extension missing)", processorId);

// Update the parameter value while the processor is ghosted. Because the processor is ghosted, all of its
// properties are treated as sensitive. Without the fix in StandardParameterContext, this update would fail
// because the ghosted processor would appear to have a sensitive property referencing a non-sensitive parameter.
final ParameterContextEntity currentParamContext = getNifiClient().getParamContextClient().getParamContext(paramContextId, false);
final ParameterContextUpdateRequestEntity updateRequest = getClientUtil().updateParameterContext(currentParamContext, "myParam", "world");
getClientUtil().waitForParameterContextRequestToComplete(paramContextId, updateRequest.getRequest().getRequestId());
logger.info("Successfully updated parameter 'myParam' from 'hello' to 'world' while processor is ghosted");

// Stop NiFi, restore the NAR, and restart
nifiInstance.stop();
Files.move(movedNarPath, narFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
logger.info("Restored NAR {} to {}", narFile.getName(), libDir.getAbsolutePath());

nifiInstance.start();
setupClient();

// Verify the processor is no longer ghosted
waitFor(() -> {
final ProcessorEntity processorEntity = getNifiClient().getProcessorClient().getProcessor(processorId);
return !processorEntity.getComponent().getExtensionMissing();
});
logger.info("Processor {} is no longer ghosted after NAR restore", processorId);

// Verify the processor is VALID with the updated parameter value
getClientUtil().waitForValidProcessor(processorId);
final ProcessorEntity validProcessor = getNifiClient().getProcessorClient().getProcessor(processorId);
assertFalse(validProcessor.getComponent().getExtensionMissing(), "Processor should not be ghosted");
logger.info("Processor {} is VALID after NAR restore", processorId);

// Verify we can still update the parameter now that the processor is no longer ghosted
final ParameterContextEntity restoredParamContext = getNifiClient().getParamContextClient().getParamContext(paramContextId, false);
final ParameterContextUpdateRequestEntity secondUpdate = getClientUtil().updateParameterContext(restoredParamContext, "myParam", "final-value");
getClientUtil().waitForParameterContextRequestToComplete(paramContextId, secondUpdate.getRequest().getRequestId());
logger.info("Successfully updated parameter 'myParam' to 'final-value' after processor is no longer ghosted");

// Verify the processor is still VALID after the second parameter update
getClientUtil().waitForValidProcessor(processorId);
}

private File findTestExtensionsNar(final File libDir) {
final File[] narFiles = libDir.listFiles((dir, name) -> name.startsWith(TEST_EXTENSIONS_NAR_PREFIX) && name.endsWith(".nar"));
if (narFiles == null || narFiles.length == 0) {
return null;
}
return narFiles[0];
}
}
Loading