This is an automated email from the ASF dual-hosted git repository.

pvillard 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 3c0c64619b NIFI-15296 Fixed enabling Controller Services with 
dependencies
3c0c64619b is described below

commit 3c0c64619bb29e06516308491331ba4e9c20caae
Author: exceptionfactory <[email protected]>
AuthorDate: Thu Dec 4 21:35:37 2025 -0600

    NIFI-15296 Fixed enabling Controller Services with dependencies
    
    - Filtered list of required Controller Services based on whether the 
property descriptor has satisfied dependencies to avoid enabling unnecessary 
referenced Controller Services
    - Added unit test with Primary Controller Service referencing Secondary 
Controller Service depending on specified properties
    
    Signed-off-by: Pierre Villard <[email protected]>
    
    This closes #10603.
---
 .../service/StandardControllerServiceNode.java     |  37 +++--
 .../service/StandardControllerServiceProvider.java |  16 +-
 .../StandardControllerServiceProviderTest.java     | 183 ++++++++++++++-------
 .../controller/service/mock/PrimaryService.java    |  49 ++++++
 .../controller/service/mock/SecondaryService.java  |  23 +++
 5 files changed, 236 insertions(+), 72 deletions(-)

diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceNode.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceNode.java
index ac0e91fada..18202bc10d 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceNode.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceNode.java
@@ -319,21 +319,36 @@ public class StandardControllerServiceNode extends 
AbstractComponentNode impleme
         return processGroup == null ? null : 
processGroup.getParameterContext();
     }
 
-
+    /**
+     * Get Controller Services required to be enabled with filtering based on 
satisfied dependencies
+     *
+     * @return Controller Service Nodes required to be enabled
+     */
     @Override
     public List<ControllerServiceNode> getRequiredControllerServices() {
-        Set<ControllerServiceNode> requiredServices = new HashSet<>();
-        for (Entry<PropertyDescriptor, String> entry : 
getEffectivePropertyValues().entrySet()) {
-            PropertyDescriptor descriptor = entry.getKey();
-            if (descriptor.getControllerServiceDefinition() != null && 
entry.getValue() != null) {
-                // CS property could point to a non-existent CS, so protect 
against requiredNode being null
-                final String referenceId = entry.getValue();
-                final ControllerServiceNode requiredNode = 
serviceProvider.getControllerServiceNode(referenceId);
-                if (requiredNode != null) {
-                    requiredServices.add(requiredNode);
+        final Set<ControllerServiceNode> requiredServices = new HashSet<>();
+
+        final ValidationContext validationContext = getValidationContext();
+        for (final Entry<PropertyDescriptor, String> entry : 
getEffectivePropertyValues().entrySet()) {
+            final PropertyDescriptor descriptor = entry.getKey();
+            final Class<? extends ControllerService> 
controllerServiceDefinition = descriptor.getControllerServiceDefinition();
+            final String referenceId = entry.getValue();
+            // Skip Property Descriptors not referencing Controller Services
+            if (controllerServiceDefinition == null || referenceId == null) {
+                continue;
+            }
+
+            // Controller Services with unsatisfied dependencies are not 
required
+            final boolean dependencySatisfied = 
validationContext.isDependencySatisfied(descriptor, 
this::getPropertyDescriptor);
+            if (dependencySatisfied) {
+                final ControllerServiceNode requiredServiceNode = 
serviceProvider.getControllerServiceNode(referenceId);
+                if (requiredServiceNode == null) {
+                    LOG.warn("Referenced Controller Service [{}] not found for 
Service [{}] Property [{}]", referenceId, getIdentifier(), 
descriptor.getName());
                 } else {
-                    LOG.warn("Unable to locate referenced controller service 
with id {}", referenceId);
+                    requiredServices.add(requiredServiceNode);
                 }
+            } else {
+                LOG.debug("Referenced Controller Service [{}] not required for 
Service [{}] Property [{}]", referenceId, getIdentifier(), 
descriptor.getName());
             }
         }
         return new ArrayList<>(requiredServices);
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceProvider.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceProvider.java
index ee9c2400c3..714a5e0bd2 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceProvider.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceProvider.java
@@ -243,8 +243,7 @@ public class StandardControllerServiceProvider implements 
ControllerServiceProvi
     @Override
     public CompletableFuture<Void> enableControllerService(final 
ControllerServiceNode serviceNode) {
         if (serviceNode.isActive()) {
-            final CompletableFuture<Void> future = 
CompletableFuture.completedFuture(null);
-            return future;
+            return CompletableFuture.completedFuture(null);
         }
 
         serviceNode.verifyCanEnable();
@@ -260,8 +259,8 @@ public class StandardControllerServiceProvider implements 
ControllerServiceProvi
                 final Future<Void> future = 
enableControllerServiceAndDependencies(controllerServiceNode);
 
                 future.get(30, TimeUnit.SECONDS);
-                logger.debug("Successfully enabled {}; service state = {}", 
controllerServiceNode, controllerServiceNode.getState());
-            } catch (final ControllerServiceNotValidException csnve) {
+                logger.debug("{} enabled with state [{}]", 
controllerServiceNode, controllerServiceNode.getState());
+            } catch (final ControllerServiceNotValidException e) {
                 logger.warn("Failed to enable service {} because it is not 
currently valid", controllerServiceNode);
             } catch (Exception e) {
                 logger.error("Failed to enable {}", controllerServiceNode, e);
@@ -284,7 +283,7 @@ public class StandardControllerServiceProvider implements 
ControllerServiceProvi
                 for (ControllerServiceNode requiredService : requiredServices) 
{
                     if (!requiredService.isActive() && 
!serviceNodes.contains(requiredService)) {
                         skipStarting = true;
-                        logger.error("Will not start {} because its required 
service {} is not active and is not part of the collection of things to start", 
serviceNode, requiredService);
+                        logger.error("{} not started: Required Service {} not 
active and not requested for enabling", serviceNode, requiredService);
                     }
                 }
                 if (skipStarting) {
@@ -657,6 +656,7 @@ public class StandardControllerServiceProvider implements 
ControllerServiceProvi
         return null;
     }
 
+    @SuppressWarnings("unchecked")
     private Class<? extends ControllerService> getServiceInterfaceByName(final 
Class<?> serviceClass, final String type) {
         for (final Class<?> serviceInterface : serviceClass.getInterfaces()) {
             if (!ControllerService.class.isAssignableFrom(serviceInterface)) {
@@ -679,6 +679,12 @@ public class StandardControllerServiceProvider implements 
ControllerServiceProvi
         return node == null ? null : node.getName();
     }
 
+    /**
+     * Remove Controller Service and suppress warnings related to 
InstanceClassLoader that may not be defined
+     *
+     * @param serviceNode Controller Service Node to be removed
+     */
+    @SuppressWarnings("resource")
     @Override
     public void removeControllerService(final ControllerServiceNode 
serviceNode) {
         requireNonNull(serviceNode);
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/service/StandardControllerServiceProviderTest.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/service/StandardControllerServiceProviderTest.java
index 366e3f7e24..d18d489649 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/service/StandardControllerServiceProviderTest.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/service/StandardControllerServiceProviderTest.java
@@ -35,121 +35,123 @@ import org.apache.nifi.util.NiFiProperties;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
-import org.mockito.Mockito;
+import org.junit.jupiter.api.Timeout;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
 
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
 
+import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-public class StandardControllerServiceProviderTest {
+@ExtendWith(MockitoExtension.class)
+class StandardControllerServiceProviderTest {
+
+    private static final String NAR_LIBRARY_DIRECTORY = "./target/lib";
+
+    private static final String TEST_SERVICE_CLASS = 
"org.apache.nifi.controller.service.util.TestControllerService";
+    private static final String PRIMARY_SERVICE_CLASS = 
"org.apache.nifi.controller.service.mock.PrimaryService";
+    private static final String SECONDARY_SERVICE_CLASS = 
"org.apache.nifi.controller.service.mock.SecondaryService";
+
+    private static final String SECONDARY_SERVICE_ENABLED_PROPERTY = 
"Secondary Service Enabled";
+    private static final String SECONDARY_SERVICE_PROPERTY = "Secondary 
Service";
+
+    private static final String SERVICE_ID = "service-id";
+    private static final String SECONDARY_SERVICE_ID = "secondary-service-id";
 
-    private ControllerService proxied;
-    private ControllerService implementation;
     private static ExtensionDiscoveringManager extensionManager;
     private static Bundle systemBundle;
 
+    @Mock
+    private ProcessScheduler scheduler;
+
+    @Mock
+    private FlowManager flowManager;
+
+    private ControllerServiceProvider serviceProvider;
+
+    private ControllerService proxied;
+    private ControllerService implementation;
+
     @BeforeAll
-    public static void setupSuite() {
-        final NiFiProperties nifiProperties = 
NiFiProperties.createBasicNiFiProperties(StandardControllerServiceProviderTest.class.getResource("/conf/nifi.properties").getFile());
+    static void setupSuite() {
+        final Map<String, String> additionalProperties = Map.of(
+                NiFiProperties.NAR_LIBRARY_DIRECTORY, NAR_LIBRARY_DIRECTORY
+        );
+        final NiFiProperties nifiProperties = 
NiFiProperties.createBasicNiFiProperties(null, additionalProperties);
 
-        // load the system bundle
         systemBundle = SystemBundle.create(nifiProperties);
         extensionManager = new StandardExtensionDiscoveringManager();
         extensionManager.discoverExtensions(systemBundle, 
Collections.emptySet());
     }
 
     @BeforeEach
-    public void setup() throws Exception {
-        String id = "id";
-        String clazz = 
"org.apache.nifi.controller.service.util.TestControllerService";
-        ControllerServiceProvider provider = new 
StandardControllerServiceProvider(null, null, Mockito.mock(FlowManager.class), 
Mockito.mock(ExtensionManager.class));
-        ControllerServiceNode node = createControllerService(clazz, id, 
systemBundle.getBundleDetails().getCoordinate(), provider);
+    void setup() {
+        serviceProvider = new StandardControllerServiceProvider(scheduler, 
null, flowManager, mock(ExtensionManager.class));
+        final ControllerServiceNode node = createControllerService(SERVICE_ID, 
TEST_SERVICE_CLASS, systemBundle.getBundleDetails().getCoordinate(), 
serviceProvider);
         proxied = node.getProxiedControllerService();
         implementation = node.getControllerServiceImplementation();
     }
 
-    private ControllerServiceNode createControllerService(final String type, 
final String id, final BundleCoordinate bundleCoordinate, final 
ControllerServiceProvider serviceProvider) {
-        return new ExtensionBuilder()
-            .identifier(id)
-            .type(type)
-            .bundleCoordinate(bundleCoordinate)
-            .controllerServiceProvider(serviceProvider)
-            .processScheduler(Mockito.mock(ProcessScheduler.class))
-            .nodeTypeProvider(Mockito.mock(NodeTypeProvider.class))
-            .validationTrigger(Mockito.mock(ValidationTrigger.class))
-            .reloadComponent(Mockito.mock(ReloadComponent.class))
-            .stateManagerProvider(Mockito.mock(StateManagerProvider.class))
-            .extensionManager(extensionManager)
-            .buildControllerService();
-    }
-
     @Test
-    public void testCallProxiedOnPropertyModified() {
+    void testCallProxiedOnPropertyModified() {
         assertThrows(UnsupportedOperationException.class,
                 () -> proxied.onPropertyModified(null, "oldValue", 
"newValue"));
     }
 
     @Test
-    public void testCallImplementationOnPropertyModified() {
+    void testCallImplementationOnPropertyModified() {
         implementation.onPropertyModified(null, "oldValue", "newValue");
     }
 
     @Test
-    public void testCallProxiedInitialized() throws InitializationException {
+    void testCallProxiedInitialized() {
         assertThrows(UnsupportedOperationException.class,
                 () -> proxied.initialize(null));
     }
 
     @Test
-    public void testCallImplementationInitialized() throws 
InitializationException {
+    void testCallImplementationInitialized() throws InitializationException {
         implementation.initialize(null);
     }
 
-    private ControllerServiceNode 
populateControllerService(ControllerServiceNode requiredService) { // 
Collection<ControllerServiceNode> serviceNodes) {
-        ControllerServiceNode controllerServiceNode = 
mock(ControllerServiceNode.class);
-        List<ControllerServiceNode> requiredServices = new ArrayList<>();
-        if (requiredService != null) {
-            requiredServices.add(requiredService);
-        }
-        
when(controllerServiceNode.getRequiredControllerServices()).thenReturn(requiredServices);
-        return controllerServiceNode;
-    }
-
     @Test
-    public void testEnableControllerServicesAllAreEnabled() {
+    void testEnableControllerServicesAllAreEnabled() {
         final CompletableFuture<Void> future = new CompletableFuture<>();
         future.complete(null);
 
-        ProcessScheduler scheduler = Mockito.mock(ProcessScheduler.class);
         when(scheduler.enableControllerService(any())).thenReturn(future);
-        ControllerServiceProvider provider = new 
StandardControllerServiceProvider(scheduler, null, 
Mockito.mock(FlowManager.class), Mockito.mock(ExtensionManager.class));
 
         final List<ControllerServiceNode> serviceNodes = new ArrayList<>();
         serviceNodes.add(populateControllerService(null));
         serviceNodes.add(populateControllerService(null));
-        provider.enableControllerServices(serviceNodes);
+        serviceProvider.enableControllerServices(serviceNodes);
         verify(scheduler).enableControllerService(serviceNodes.get(0));
         verify(scheduler).enableControllerService(serviceNodes.get(1));
     }
 
     @Test
-    public void testEnableControllerServicesSomeAreEnabled() {
+    void testEnableControllerServicesSomeAreEnabled() {
         final CompletableFuture<Void> future = new CompletableFuture<>();
         future.complete(null);
-
-        ProcessScheduler scheduler = Mockito.mock(ProcessScheduler.class);
         when(scheduler.enableControllerService(any())).thenReturn(future);
-        ControllerServiceProvider provider = new 
StandardControllerServiceProvider(scheduler, null, 
Mockito.mock(FlowManager.class), Mockito.mock(ExtensionManager.class));
 
         final List<ControllerServiceNode> serviceNodes = new ArrayList<>();
-        ControllerServiceNode disabledController = 
populateControllerService(null);
+        final ControllerServiceNode disabledController = 
populateControllerService(null);
         // Do not start because disabledController is not in the serviceNodes 
(list of services to start)
         serviceNodes.add(populateControllerService(disabledController));
         // Start this service because it has no required services
@@ -161,11 +163,80 @@ public class StandardControllerServiceProviderTest {
         serviceNodes.add(populateControllerService(serviceNodes.get(2)));
         // Start this service because it has a required service which is in 
the list of services to start
         serviceNodes.add(populateControllerService(serviceNodes.get(1)));
-        provider.enableControllerServices(serviceNodes);
-        verify(scheduler, 
Mockito.times(0)).enableControllerService(serviceNodes.get(0));
-        verify(scheduler, 
Mockito.times(2)).enableControllerService(serviceNodes.get(1));
-        verify(scheduler, 
Mockito.times(0)).enableControllerService(serviceNodes.get(2));
-        verify(scheduler, 
Mockito.times(0)).enableControllerService(serviceNodes.get(3));
-        verify(scheduler, 
Mockito.times(1)).enableControllerService(serviceNodes.get(4));
+
+        serviceProvider.enableControllerServices(serviceNodes);
+
+        verify(scheduler, 
times(0)).enableControllerService(serviceNodes.get(0));
+        verify(scheduler, 
times(2)).enableControllerService(serviceNodes.get(1));
+        verify(scheduler, 
times(0)).enableControllerService(serviceNodes.get(2));
+        verify(scheduler, 
times(0)).enableControllerService(serviceNodes.get(3));
+        verify(scheduler, 
times(1)).enableControllerService(serviceNodes.get(4));
+    }
+
+    @Timeout(10)
+    @Test
+    void testEnableControllerServicesDependencyNotEnabled() throws 
InterruptedException {
+        final BundleCoordinate systemCoordinate = 
systemBundle.getBundleDetails().getCoordinate();
+        final ControllerServiceNode primaryServiceNode = 
createControllerService(SERVICE_ID, PRIMARY_SERVICE_CLASS, systemCoordinate, 
serviceProvider);
+        serviceProvider.onControllerServiceAdded(primaryServiceNode);
+
+        final ControllerServiceNode secondaryServiceNode = 
createControllerService(SECONDARY_SERVICE_ID, SECONDARY_SERVICE_CLASS, 
systemCoordinate, serviceProvider);
+        serviceProvider.onControllerServiceAdded(secondaryServiceNode);
+
+        final Map<String, String> primaryProperties = Map.of(
+                SECONDARY_SERVICE_ENABLED_PROPERTY, Boolean.FALSE.toString(),
+                SECONDARY_SERVICE_PROPERTY, SECONDARY_SERVICE_ID
+        );
+        primaryServiceNode.setProperties(primaryProperties);
+        final List<ControllerServiceNode> serviceNodes = 
List.of(primaryServiceNode);
+
+        // Enable Primary Service using Count Down Latch to signal completion
+        final CountDownLatch enabledLatch = new CountDownLatch(1);
+        
when(scheduler.enableControllerService(eq(primaryServiceNode))).then(invocationOnMock
 -> {
+            try (ScheduledExecutorService executorService = 
Executors.newSingleThreadScheduledExecutor()) {
+                final CompletableFuture<Void> nodeEnabledFuture = 
primaryServiceNode.enable(executorService, 5000, true);
+                nodeEnabledFuture.join();
+                enabledLatch.countDown();
+            }
+            final CompletableFuture<Void> enableFuture = new 
CompletableFuture<>();
+            enableFuture.complete(null);
+            return enableFuture;
+        });
+
+        serviceProvider.enableControllerServices(serviceNodes);
+
+        enabledLatch.await();
+        assertEquals(ControllerServiceState.ENABLED, 
primaryServiceNode.getState());
+
+        assertEquals(ControllerServiceState.DISABLED, 
secondaryServiceNode.getState(), "Secondary Service should remain disabled");
+    }
+
+    private ControllerServiceNode createControllerService(
+            final String identifier,
+            final String type,
+            final BundleCoordinate bundleCoordinate,
+            final ControllerServiceProvider serviceProvider
+    ) {
+        return new ExtensionBuilder()
+                .identifier(identifier)
+                .type(type)
+                .bundleCoordinate(bundleCoordinate)
+                .controllerServiceProvider(serviceProvider)
+                .processScheduler(mock(ProcessScheduler.class))
+                .nodeTypeProvider(mock(NodeTypeProvider.class))
+                .validationTrigger(mock(ValidationTrigger.class))
+                .reloadComponent(mock(ReloadComponent.class))
+                .stateManagerProvider(mock(StateManagerProvider.class))
+                .extensionManager(extensionManager)
+                .buildControllerService();
+    }
+
+    private ControllerServiceNode populateControllerService(final 
ControllerServiceNode requiredService) {
+        final ControllerServiceNode controllerServiceNode = 
mock(ControllerServiceNode.class);
+        if (requiredService != null) {
+            final List<ControllerServiceNode> requiredServices = 
List.of(requiredService);
+            
when(controllerServiceNode.getRequiredControllerServices()).thenReturn(requiredServices);
+        }
+        return controllerServiceNode;
     }
 }
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/service/mock/PrimaryService.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/service/mock/PrimaryService.java
new file mode 100644
index 0000000000..4a67d140a4
--- /dev/null
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/service/mock/PrimaryService.java
@@ -0,0 +1,49 @@
+/*
+ * 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.controller.service.mock;
+
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.controller.AbstractControllerService;
+import org.apache.nifi.controller.ControllerService;
+
+import java.util.List;
+
+public class PrimaryService extends AbstractControllerService {
+
+    public static final PropertyDescriptor SECONDARY_SERVICE_ENABLED = new 
PropertyDescriptor.Builder()
+            .name("Secondary Service Enabled")
+            .required(true)
+            .allowableValues(Boolean.TRUE.toString(), Boolean.FALSE.toString())
+            .build();
+
+    public static final PropertyDescriptor SECONDARY_SERVICE = new 
PropertyDescriptor.Builder()
+            .name("Secondary Service")
+            .identifiesControllerService(ControllerService.class)
+            .required(true)
+            .dependsOn(SECONDARY_SERVICE_ENABLED, Boolean.TRUE.toString())
+            .build();
+
+    private static final List<PropertyDescriptor> PROPERTY_DESCRIPTORS = 
List.of(
+            SECONDARY_SERVICE_ENABLED,
+            SECONDARY_SERVICE
+    );
+
+    @Override
+    protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return PROPERTY_DESCRIPTORS;
+    }
+}
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/service/mock/SecondaryService.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/service/mock/SecondaryService.java
new file mode 100644
index 0000000000..2a25b89ad1
--- /dev/null
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/service/mock/SecondaryService.java
@@ -0,0 +1,23 @@
+/*
+ * 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.controller.service.mock;
+
+import org.apache.nifi.controller.AbstractControllerService;
+
+public class SecondaryService extends AbstractControllerService {
+
+}

Reply via email to