This is an automated email from the ASF dual-hosted git repository.
rfellows 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 b653e0a78f5 NIFI-16156: Add connector-scoped Controller Service GET so
View Canva… (#11487)
b653e0a78f5 is described below
commit b653e0a78f59ccb9c9eecbcd96c2b90e8e423022
Author: Matt Gilman <[email protected]>
AuthorDate: Mon Aug 3 09:59:52 2026 -0400
NIFI-16156: Add connector-scoped Controller Service GET so View Canva…
(#11487)
* NIFI-16156: Add connector-scoped Controller Service GET so View Canvas Go
To Service works without Troubleshooting.
- Adding response merging for the Controller Service endpoints.
* NIFI-16156: Addressing review feedback.
This closes #11487
---
.../endpoints/ControllerServiceEndpointMerger.java | 3 +
.../ControllerServicesEndpointMerger.java | 6 +-
.../ControllerServiceEndpointMergerTest.java | 59 ++++++++++++++
.../ControllerServicesEndpointMergerTest.java | 63 +++++++++++++++
.../org/apache/nifi/web/NiFiServiceFacade.java | 11 +++
.../apache/nifi/web/StandardNiFiServiceFacade.java | 7 ++
.../org/apache/nifi/web/api/ConnectorResource.java | 60 ++++++++++++++
.../nifi/web/StandardNiFiServiceFacadeTest.java | 20 +++++
.../apache/nifi/web/api/TestConnectorResource.java | 45 +++++++++++
.../pages/connectors/service/connector.service.ts | 10 ++-
.../state/connector-canvas/bind-go-to-service.ts | 90 +++++++++++++++++++++
.../connector-canvas.effects.spec.ts | 87 ++++++++++++++++++++
.../connector-canvas/connector-canvas.effects.ts | 10 ++-
.../connector-controller-services.effects.spec.ts | 93 ++++++++++++++++++++--
.../connector-controller-services.effects.ts | 11 ++-
.../nifi/toolkit/client/ConnectorClient.java | 12 +++
.../toolkit/client/impl/JerseyConnectorClient.java | 20 +++++
17 files changed, 598 insertions(+), 9 deletions(-)
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/ControllerServiceEndpointMerger.java
b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/ControllerServiceEndpointMerger.java
index 3d79a83e778..4e740cc84b0 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/ControllerServiceEndpointMerger.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/ControllerServiceEndpointMerger.java
@@ -33,12 +33,15 @@ public class ControllerServiceEndpointMerger extends
AbstractSingleEntityEndpoin
public static final Pattern PROCESS_GROUPS_CONTROLLER_SERVICES_URI =
Pattern.compile("/nifi-api/process-groups/(?:(?:root)|(?:[a-f0-9\\-]{36}))/controller-services");
public static final Pattern CONTROLLER_SERVICE_URI_PATTERN =
Pattern.compile("/nifi-api/controller-services/[a-f0-9\\-]{36}");
public static final Pattern CONTROLLER_SERVICE_RUN_STATUS_URI_PATTERN =
Pattern.compile("/nifi-api/controller-services/[a-f0-9\\-]{36}/run-status");
+ public static final Pattern CONNECTOR_CONTROLLER_SERVICE_URI_PATTERN =
Pattern.compile("/nifi-api/connectors/[a-f0-9\\-]{36}/controller-services/[a-f0-9\\-]{36}");
private final ControllerServiceEntityMerger controllerServiceEntityMerger
= new ControllerServiceEntityMerger();
@Override
public boolean canHandle(URI uri, String method) {
if (("GET".equalsIgnoreCase(method) || "PUT".equalsIgnoreCase(method))
&& CONTROLLER_SERVICE_URI_PATTERN.matcher(uri.getPath()).matches()) {
return true;
+ } else if ("GET".equalsIgnoreCase(method) &&
CONNECTOR_CONTROLLER_SERVICE_URI_PATTERN.matcher(uri.getPath()).matches()) {
+ return true;
} else if ("PUT".equalsIgnoreCase(method) &&
CONTROLLER_SERVICE_RUN_STATUS_URI_PATTERN.matcher(uri.getPath()).matches()) {
return true;
} else if ("POST".equalsIgnoreCase(method) &&
(CONTROLLER_CONTROLLER_SERVICES_URI.equals(uri.getPath()) ||
PROCESS_GROUPS_CONTROLLER_SERVICES_URI.matcher(uri.getPath()).matches())) {
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/ControllerServicesEndpointMerger.java
b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/ControllerServicesEndpointMerger.java
index 78c1aade0e1..03cd860f860 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/ControllerServicesEndpointMerger.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/ControllerServicesEndpointMerger.java
@@ -33,10 +33,14 @@ import java.util.regex.Pattern;
public class ControllerServicesEndpointMerger implements
EndpointResponseMerger {
public static final String CONTROLLER_SERVICES_URI =
"/nifi-api/flow/controller/controller-services";
public static final Pattern PROCESS_GROUPS_CONTROLLER_SERVICES_URI =
Pattern.compile("/nifi-api/flow/process-groups/(?:(?:root)|(?:[a-f0-9\\-]{36}))/controller-services");
+ public static final Pattern
CONNECTOR_PROCESS_GROUPS_CONTROLLER_SERVICES_URI =
+
Pattern.compile("/nifi-api/connectors/[a-f0-9\\-]{36}/flow/process-groups/[a-f0-9\\-]{36}/controller-services");
@Override
public boolean canHandle(URI uri, String method) {
- return "GET".equalsIgnoreCase(method) &&
(CONTROLLER_SERVICES_URI.equals(uri.getPath()) ||
PROCESS_GROUPS_CONTROLLER_SERVICES_URI.matcher(uri.getPath()).matches());
+ return "GET".equalsIgnoreCase(method) &&
(CONTROLLER_SERVICES_URI.equals(uri.getPath())
+ ||
PROCESS_GROUPS_CONTROLLER_SERVICES_URI.matcher(uri.getPath()).matches()
+ ||
CONNECTOR_PROCESS_GROUPS_CONTROLLER_SERVICES_URI.matcher(uri.getPath()).matches());
}
@Override
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/http/endpoints/ControllerServiceEndpointMergerTest.java
b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/http/endpoints/ControllerServiceEndpointMergerTest.java
new file mode 100644
index 00000000000..9f08e200252
--- /dev/null
+++
b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/http/endpoints/ControllerServiceEndpointMergerTest.java
@@ -0,0 +1,59 @@
+/*
+ * 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.cluster.coordination.http.endpoints;
+
+import org.junit.jupiter.api.Test;
+
+import java.net.URI;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class ControllerServiceEndpointMergerTest {
+
+ private static final String CONNECTOR_ID =
"12345678-1234-1234-1234-123456789012";
+ private static final String CONTROLLER_SERVICE_ID =
"abcdef01-2345-6789-abcd-ef0123456789";
+
+ @Test
+ public void testCanHandleControllerServiceUri() {
+ final ControllerServiceEndpointMerger merger = new
ControllerServiceEndpointMerger();
+ final String controllerServiceUri = "/nifi-api/controller-services/" +
CONTROLLER_SERVICE_ID;
+
+ assertTrue(merger.canHandle(URI.create(controllerServiceUri), "GET"));
+ assertTrue(merger.canHandle(URI.create(controllerServiceUri), "PUT"));
+ assertFalse(merger.canHandle(URI.create(controllerServiceUri),
"DELETE"));
+ }
+
+ @Test
+ public void testCanHandleConnectorControllerServiceUri() {
+ final ControllerServiceEndpointMerger merger = new
ControllerServiceEndpointMerger();
+ final String connectorControllerServiceUri = "/nifi-api/connectors/" +
CONNECTOR_ID + "/controller-services/" + CONTROLLER_SERVICE_ID;
+
+ // Test valid URIs
+ assertTrue(merger.canHandle(URI.create(connectorControllerServiceUri),
"GET"));
+ assertTrue(merger.canHandle(URI.create(connectorControllerServiceUri +
"?uiOnly=true"), "GET"));
+
+ // A connector-managed Controller Service is only ever retrieved via
GET; PUT is not a supported operation on this path.
+
assertFalse(merger.canHandle(URI.create(connectorControllerServiceUri), "PUT"));
+
+ // Test invalid URIs
+ assertFalse(merger.canHandle(URI.create(connectorControllerServiceUri
+ "/state"), "GET"));
+ assertFalse(merger.canHandle(URI.create("/nifi-api/connectors/" +
CONNECTOR_ID + "/controller-services"), "GET"));
+
assertFalse(merger.canHandle(URI.create("/nifi-api/connectors/not-a-uuid/controller-services/"
+ CONTROLLER_SERVICE_ID), "GET"));
+ assertFalse(merger.canHandle(URI.create("/nifi-api/connectors/" +
CONNECTOR_ID + "/controller-services/not-a-uuid"), "GET"));
+ }
+}
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/http/endpoints/ControllerServicesEndpointMergerTest.java
b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/http/endpoints/ControllerServicesEndpointMergerTest.java
new file mode 100644
index 00000000000..a3122eec1ac
--- /dev/null
+++
b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/http/endpoints/ControllerServicesEndpointMergerTest.java
@@ -0,0 +1,63 @@
+/*
+ * 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.cluster.coordination.http.endpoints;
+
+import org.junit.jupiter.api.Test;
+
+import java.net.URI;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class ControllerServicesEndpointMergerTest {
+
+ private static final String CONNECTOR_ID =
"12345678-1234-1234-1234-123456789012";
+ private static final String PROCESS_GROUP_ID =
"abcdef01-2345-6789-abcd-ef0123456789";
+
+ @Test
+ public void testCanHandleControllerControllerServicesUri() {
+ final ControllerServicesEndpointMerger merger = new
ControllerServicesEndpointMerger();
+
+
assertTrue(merger.canHandle(URI.create("/nifi-api/flow/controller/controller-services"),
"GET"));
+
assertFalse(merger.canHandle(URI.create("/nifi-api/flow/controller/controller-services"),
"POST"));
+ }
+
+ @Test
+ public void testCanHandleProcessGroupControllerServicesUri() {
+ final ControllerServicesEndpointMerger merger = new
ControllerServicesEndpointMerger();
+
+
assertTrue(merger.canHandle(URI.create("/nifi-api/flow/process-groups/root/controller-services"),
"GET"));
+
assertTrue(merger.canHandle(URI.create("/nifi-api/flow/process-groups/" +
PROCESS_GROUP_ID + "/controller-services"), "GET"));
+ }
+
+ @Test
+ public void testCanHandleConnectorProcessGroupControllerServicesUri() {
+ final ControllerServicesEndpointMerger merger = new
ControllerServicesEndpointMerger();
+ final String connectorProcessGroupControllerServicesUri =
+ "/nifi-api/connectors/" + CONNECTOR_ID +
"/flow/process-groups/" + PROCESS_GROUP_ID + "/controller-services";
+
+ // Test valid URIs
+
assertTrue(merger.canHandle(URI.create(connectorProcessGroupControllerServicesUri),
"GET"));
+
assertTrue(merger.canHandle(URI.create(connectorProcessGroupControllerServicesUri
+ "?includeDescendantGroups=true"), "GET"));
+
+ // Test invalid URIs
+
assertFalse(merger.canHandle(URI.create(connectorProcessGroupControllerServicesUri),
"POST"));
+ assertFalse(merger.canHandle(URI.create("/nifi-api/connectors/" +
CONNECTOR_ID + "/flow/process-groups/" + PROCESS_GROUP_ID), "GET"));
+
assertFalse(merger.canHandle(URI.create("/nifi-api/connectors/not-a-uuid/flow/process-groups/"
+ PROCESS_GROUP_ID + "/controller-services"), "GET"));
+ assertFalse(merger.canHandle(URI.create("/nifi-api/connectors/" +
CONNECTOR_ID + "/flow/process-groups/not-a-uuid/controller-services"), "GET"));
+ }
+}
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
index 031ca9acafd..2313b34f977 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
@@ -2320,6 +2320,17 @@ public interface NiFiServiceFacade {
*/
ComponentStateDTO clearConnectorProcessorState(String connectorId, String
processorId, ComponentStateDTO componentStateDTO);
+ /**
+ * Gets a controller service within a connector's managed process group.
Available regardless of whether
+ * the Connector is in Troubleshooting mode.
+ *
+ * @param connectorId the connector id
+ * @param controllerServiceId the controller service id
+ * @param includeReferencingComponents whether to include referencing
components in the response
+ * @return the controller service entity
+ */
+ ControllerServiceEntity getConnectorControllerService(String connectorId,
String controllerServiceId, boolean includeReferencingComponents);
+
/**
* Gets the state for a controller service within a connector's managed
process group.
*
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
index 8ee7f4f64f2..6688dd48152 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
@@ -2192,6 +2192,13 @@ public class StandardNiFiServiceFacade implements
NiFiServiceFacade {
return getConnectorProcessorState(connectorId, processorId);
}
+ @Override
+ public ControllerServiceEntity getConnectorControllerService(final String
connectorId, final String controllerServiceId,
+ final boolean includeReferencingComponents) {
+ final ControllerServiceNode controllerService =
locateConnectorControllerService(connectorId, controllerServiceId);
+ return createControllerServiceEntity(controllerService,
includeReferencingComponents);
+ }
+
@Override
public ComponentStateDTO getConnectorControllerServiceState(final String
connectorId, final String controllerServiceId) {
final ControllerServiceNode controllerService =
locateConnectorControllerService(connectorId, controllerServiceId);
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ConnectorResource.java
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ConnectorResource.java
index 2e28320c1c9..7836582db82 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ConnectorResource.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ConnectorResource.java
@@ -3094,6 +3094,66 @@ public class ConnectorResource extends
ApplicationResource {
);
}
+ // -----------------
+ // Controller Services
+ // -----------------
+
+ /**
+ * Gets a controller service within a connector. Available regardless of
whether the Connector is in Troubleshooting mode.
+ *
+ * @param connectorId the connector id
+ * @param controllerServiceId the controller service id
+ * @param uiOnly whether to strip non-UI-relevant fields from
the response
+ * @return a ControllerServiceEntity
+ */
+ @GET
+ @Consumes(MediaType.WILDCARD)
+ @Produces(MediaType.APPLICATION_JSON)
+ @Path("{id}/controller-services/{controllerServiceId}")
+ @Operation(
+ summary = "Gets a controller service within a connector",
+ responses = {
+ @ApiResponse(responseCode = "200", content =
@Content(schema = @Schema(implementation = ControllerServiceEntity.class))),
+ @ApiResponse(responseCode = "400", description = "NiFi was
unable to complete the request because it was invalid. The request should not
be retried without modification."),
+ @ApiResponse(responseCode = "401", description = "Client
could not be authenticated."),
+ @ApiResponse(responseCode = "403", description = "Client
is not authorized to make this request."),
+ @ApiResponse(responseCode = "404", description = "The
specified resource could not be found."),
+ @ApiResponse(responseCode = "409", description = "The
request was valid but NiFi was not in the appropriate state to process it.")
+ },
+ security = {
+ @SecurityRequirement(name = "Read - /connectors/{uuid}")
+ },
+ description = "If the uiOnly query parameter is provided with a
value of true, the returned entity may only contain " +
+ "fields that are necessary for rendering the NiFi User
Interface. As such, the selected fields may change " +
+ "at any time, even during incremental releases, without
warning. As a result, this parameter should not be " +
+ "provided by any client other than the UI."
+ )
+ public Response getConnectorControllerService(
+ @Parameter(description = "The connector id.", required = true)
+ @PathParam("id") final String connectorId,
+ @Parameter(description = "The controller service id.", required =
true)
+ @PathParam("controllerServiceId") final String controllerServiceId,
+ @QueryParam("uiOnly") @DefaultValue("false") final boolean uiOnly)
{
+
+ if (isReplicateRequest()) {
+ return replicate(HttpMethod.GET);
+ }
+
+ serviceFacade.authorizeAccess(lookup -> {
+ final Authorizable connector = lookup.getConnector(connectorId);
+ connector.authorize(authorizer, RequestAction.READ,
NiFiUserUtils.getNiFiUser());
+ });
+
+ // Resolve the controller service from the managed Process Group tree
so the Troubleshooting access gate does not apply.
+ final ControllerServiceEntity entity =
serviceFacade.getConnectorControllerService(connectorId, controllerServiceId,
true);
+ if (uiOnly) {
+ stripNonUiRelevantFields(entity);
+ }
+
controllerServiceResource.populateRemainingControllerServiceEntityContent(entity);
+
+ return generateOkResponse(entity).build();
+ }
+
// -----------------
// Controller Service State
// -----------------
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/StandardNiFiServiceFacadeTest.java
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/StandardNiFiServiceFacadeTest.java
index 8598124c546..e73cdf17023 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/StandardNiFiServiceFacadeTest.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/StandardNiFiServiceFacadeTest.java
@@ -2167,6 +2167,26 @@ public class StandardNiFiServiceFacadeTest {
verify(componentStateDAO).clearState(processorNode, null);
}
+ @Test
+ public void testGetConnectorControllerServiceNotFound() {
+ final String connectorId = "connector-id";
+ final String controllerServiceId =
"non-existent-controller-service-id";
+
+ final ConnectorDAO connectorDAO = mock(ConnectorDAO.class);
+ serviceFacade.setConnectorDAO(connectorDAO);
+
+ final ConnectorNode connectorNode = mock(ConnectorNode.class);
+ final FrameworkFlowContext flowContext =
mock(FrameworkFlowContext.class);
+ final ProcessGroup managedProcessGroup = mock(ProcessGroup.class);
+
+ when(connectorDAO.getConnector(connectorId,
ConnectorSyncMode.LOCAL_ONLY)).thenReturn(connectorNode);
+ when(connectorNode.getActiveFlowContext()).thenReturn(flowContext);
+
when(flowContext.getManagedProcessGroup()).thenReturn(managedProcessGroup);
+ when(managedProcessGroup.findControllerService(controllerServiceId,
false, true)).thenReturn(null);
+
+ assertThrows(ResourceNotFoundException.class, () ->
serviceFacade.getConnectorControllerService(connectorId, controllerServiceId,
false));
+ }
+
@Test
public void testGetConnectorControllerServiceState() {
final String connectorId = "connector-id";
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/api/TestConnectorResource.java
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/api/TestConnectorResource.java
index 5f9d2373807..b09de3509be 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/api/TestConnectorResource.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/api/TestConnectorResource.java
@@ -921,6 +921,51 @@ public class TestConnectorResource {
verify(serviceFacade,
never()).clearConnectorProcessorState(anyString(), anyString(), any());
}
+ @Test
+ public void testGetConnectorControllerService() {
+ final ControllerServiceEntity controllerServiceEntity = new
ControllerServiceEntity();
+ controllerServiceEntity.setId(CONTROLLER_SERVICE_ID);
+ when(serviceFacade.getConnectorControllerService(CONNECTOR_ID,
CONTROLLER_SERVICE_ID, true)).thenReturn(controllerServiceEntity);
+
when(controllerServiceResource.populateRemainingControllerServiceEntityContent(controllerServiceEntity)).thenReturn(controllerServiceEntity);
+
+ try (Response response =
connectorResource.getConnectorControllerService(CONNECTOR_ID,
CONTROLLER_SERVICE_ID, false)) {
+ assertEquals(200, response.getStatus());
+ assertEquals(controllerServiceEntity, response.getEntity());
+ }
+
+ verify(serviceFacade).authorizeAccess(any(AuthorizeAccess.class));
+ verify(serviceFacade).getConnectorControllerService(CONNECTOR_ID,
CONTROLLER_SERVICE_ID, true);
+
verify(controllerServiceResource).populateRemainingControllerServiceEntityContent(controllerServiceEntity);
+ }
+
+ @Test
+ public void testGetConnectorControllerServiceUiOnly() {
+ final ControllerServiceEntity controllerServiceEntity = new
ControllerServiceEntity();
+ controllerServiceEntity.setId(CONTROLLER_SERVICE_ID);
+ when(serviceFacade.getConnectorControllerService(CONNECTOR_ID,
CONTROLLER_SERVICE_ID, true)).thenReturn(controllerServiceEntity);
+
when(controllerServiceResource.populateRemainingControllerServiceEntityContent(controllerServiceEntity)).thenReturn(controllerServiceEntity);
+
+ try (Response response =
connectorResource.getConnectorControllerService(CONNECTOR_ID,
CONTROLLER_SERVICE_ID, true)) {
+ assertEquals(200, response.getStatus());
+ assertEquals(controllerServiceEntity, response.getEntity());
+ }
+
+ verify(serviceFacade).authorizeAccess(any(AuthorizeAccess.class));
+ verify(serviceFacade).getConnectorControllerService(CONNECTOR_ID,
CONTROLLER_SERVICE_ID, true);
+
verify(controllerServiceResource).populateRemainingControllerServiceEntityContent(controllerServiceEntity);
+ }
+
+ @Test
+ public void testGetConnectorControllerServiceNotAuthorized() {
+
doThrow(AccessDeniedException.class).when(serviceFacade).authorizeAccess(any(AuthorizeAccess.class));
+
+ assertThrows(AccessDeniedException.class, () ->
+ connectorResource.getConnectorControllerService(CONNECTOR_ID,
CONTROLLER_SERVICE_ID, false));
+
+ verify(serviceFacade).authorizeAccess(any(AuthorizeAccess.class));
+ verify(serviceFacade,
never()).getConnectorControllerService(anyString(), anyString(), eq(true));
+ }
+
@Test
public void testGetConnectorControllerServiceState() {
final ComponentStateDTO stateDTO = new ComponentStateDTO();
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/service/connector.service.ts
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/service/connector.service.ts
index 991c2e13bf7..2c8dcb56744 100644
---
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/service/connector.service.ts
+++
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/service/connector.service.ts
@@ -23,7 +23,7 @@ import { Client } from '../../../service/client.service';
import { ClusterConnectionService } from
'../../../service/cluster-connection.service';
import { ConnectorsResponse, CreateConnectorRequest } from '../state';
import { ConnectorEntity } from '@nifi/shared';
-import { ParameterContextEntity, SearchResultsEntity } from
'../../../state/shared';
+import { ControllerServiceEntity, ParameterContextEntity, SearchResultsEntity
} from '../../../state/shared';
import { DropRequestEntity } from '../../../state/empty-queue';
@Injectable({ providedIn: 'root' })
@@ -124,6 +124,14 @@ export class ConnectorService {
);
}
+ getControllerService(connectorId: string, controllerServiceId: string):
Observable<ControllerServiceEntity> {
+ const params: Record<string, boolean> = { uiOnly: true };
+ return this.httpClient.get<ControllerServiceEntity>(
+
`${ConnectorService.API}/connectors/${connectorId}/controller-services/${controllerServiceId}`,
+ { params }
+ );
+ }
+
getConnectorParameterContext(
connectorId: string,
processGroupId: string
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/state/connector-canvas/bind-go-to-service.ts
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/state/connector-canvas/bind-go-to-service.ts
new file mode 100644
index 00000000000..cf6818cdd43
--- /dev/null
+++
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/state/connector-canvas/bind-go-to-service.ts
@@ -0,0 +1,90 @@
+/*
+ * 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.
+ */
+
+import { Store } from '@ngrx/store';
+import { Observable } from 'rxjs';
+import { take, takeUntil } from 'rxjs/operators';
+import { HttpErrorResponse } from '@angular/common/http';
+import { ConnectorService } from '../../service/connector.service';
+import { ErrorHelper } from '../../../../service/error-helper.service';
+import { ErrorContextKey } from '../../../../state/error';
+import * as ErrorActions from '../../../../state/error/error.actions';
+import * as ConnectorCanvasActions from './connector-canvas.actions';
+
+interface GoToServiceDialogRef {
+ afterClosed(): Observable<unknown>;
+ close(): void;
+}
+
+/**
+ * Builds a `goToService` callback for a read-only property-table dialog:
resolves the
+ * current Connector id from the route, fetches the target Controller Service
via the
+ * connector-scoped endpoint (bypassing the Troubleshooting gate), then
navigates to it
+ * and closes the dialog. The fetch is torn down if the dialog closes first.
+ *
+ * Shared between the connector canvas and controller-services effects so the
two
+ * read-only dialogs cannot drift from each other.
+ */
+export function bindGoToService(
+ store: Store,
+ connectorService: ConnectorService,
+ errorHelper: ErrorHelper,
+ dialogRef: GoToServiceDialogRef,
+ connectorId$: Observable<string | null>,
+ errorContext: ErrorContextKey
+): (serviceId: string) => void {
+ return (serviceId: string) => {
+ connectorId$.pipe(take(1)).subscribe((connectorId) => {
+ if (!connectorId) {
+ store.dispatch(
+ ErrorActions.addBannerError({
+ errorContext: {
+ errors: ['Unable to determine Connector id for
navigation.'],
+ context: errorContext
+ }
+ })
+ );
+ return;
+ }
+
+ connectorService
+ .getControllerService(connectorId, serviceId)
+ .pipe(takeUntil(dialogRef.afterClosed()))
+ .subscribe({
+ next: (serviceEntity) => {
+ store.dispatch(
+
ConnectorCanvasActions.navigateToControllerService({
+ processGroupId:
serviceEntity.component.parentGroupId,
+ serviceId: serviceEntity.id
+ })
+ );
+ dialogRef.close();
+ },
+ error: (errorResponse: HttpErrorResponse) => {
+ store.dispatch(
+ ErrorActions.addBannerError({
+ errorContext: {
+ errors:
[errorHelper.getErrorString(errorResponse)],
+ context: errorContext
+ }
+ })
+ );
+ }
+ });
+ });
+ };
+}
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/state/connector-canvas/connector-canvas.effects.spec.ts
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/state/connector-canvas/connector-canvas.effects.spec.ts
index 671c2dcd008..3c6567eccbf 100644
---
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/state/connector-canvas/connector-canvas.effects.spec.ts
+++
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/state/connector-canvas/connector-canvas.effects.spec.ts
@@ -647,6 +647,93 @@ describe('ConnectorCanvasEffects', () => {
expect(mockDialogRef.componentInstance.parameterContext).toBeUndefined();
expect(mockDialogRef.componentInstance.goToParameter).toBeUndefined();
});
+
+ it('wires goToService to fetch via the connector-scoped API and
navigate to the controller service', async () => {
+ const { effects, actions$, store, mockConnectorService,
mockDialogRef } = await setup();
+ const dispatchSpy = vi.spyOn(store, 'dispatch');
+ mockConnectorService.getControllerService = vi
+ .fn()
+ .mockReturnValue(of({ id: 'svc-1', component: { parentGroupId:
'pg-cs', id: 'svc-1' } }));
+ mockDialogRef.close = vi.fn();
+
+ actions$(
+ of(
+ viewComponentConfiguration({
+ request: { entity: baseEntity, componentType:
ComponentType.Processor }
+ })
+ )
+ );
+ await firstValueFrom(effects.viewComponentConfiguration$);
+
+
expect(mockDialogRef.componentInstance.goToService).toBeInstanceOf(Function);
+ mockDialogRef.componentInstance.goToService('svc-1');
+
+
expect(mockConnectorService.getControllerService).toHaveBeenCalledWith('connector-1',
'svc-1');
+ expect(mockDialogRef.close).toHaveBeenCalled();
+ expect(dispatchSpy).toHaveBeenCalledWith(
+ navigateToControllerService({
+ processGroupId: 'pg-cs',
+ serviceId: 'svc-1'
+ })
+ );
+ });
+
+ it('dispatches a banner error when goToService fails to fetch the
controller service', async () => {
+ const { effects, actions$, store, mockConnectorService,
mockDialogRef, mockErrorHelper } = await setup();
+ const dispatchSpy = vi.spyOn(store, 'dispatch');
+ const errorResponse = new HttpErrorResponse({ status: 404,
statusText: 'Not Found' });
+ mockConnectorService.getControllerService =
vi.fn().mockReturnValue(throwError(() => errorResponse));
+
+ actions$(
+ of(
+ viewComponentConfiguration({
+ request: { entity: baseEntity, componentType:
ComponentType.Processor }
+ })
+ )
+ );
+ await firstValueFrom(effects.viewComponentConfiguration$);
+
+ mockDialogRef.componentInstance.goToService('svc-missing');
+
+
expect(mockErrorHelper.getErrorString).toHaveBeenCalledWith(errorResponse);
+ expect(dispatchSpy).toHaveBeenCalledWith(
+ ErrorActions.addBannerError({
+ errorContext: {
+ errors: ['Error message'],
+ context: ErrorContextKey.CONNECTOR_CANVAS
+ }
+ })
+ );
+ });
+
+ it('dispatches a banner error when goToService cannot resolve the
connector id', async () => {
+ const { effects, actions$, store, mockConnectorService,
mockDialogRef } = await setup({
+ connectorId: null
+ });
+ const dispatchSpy = vi.spyOn(store, 'dispatch');
+ mockConnectorService.getControllerService = vi.fn();
+
+ actions$(
+ of(
+ viewComponentConfiguration({
+ request: { entity: baseEntity, componentType:
ComponentType.Processor }
+ })
+ )
+ );
+ await firstValueFrom(effects.viewComponentConfiguration$);
+
+ mockDialogRef.componentInstance.goToService('svc-1');
+
+
expect(mockConnectorService.getControllerService).not.toHaveBeenCalled();
+ expect(dispatchSpy).toHaveBeenCalledWith(
+ ErrorActions.addBannerError({
+ errorContext: {
+ errors: ['Unable to determine Connector id for
navigation.'],
+ context: ErrorContextKey.CONNECTOR_CANVAS
+ }
+ })
+ );
+ });
});
describe('navigateToProvenanceForComponent$', () => {
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/state/connector-canvas/connector-canvas.effects.ts
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/state/connector-canvas/connector-canvas.effects.ts
index c8cec7a5804..f75e4481e4d 100644
---
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/state/connector-canvas/connector-canvas.effects.ts
+++
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/state/connector-canvas/connector-canvas.effects.ts
@@ -65,6 +65,7 @@ import { EditRemoteProcessGroup } from
'../../../../ui/common/component-dialogs/
import * as ConnectorCanvasActions from './connector-canvas.actions';
import * as ConnectorControllerServicesActions from
'../connector-controller-services/connector-controller-services.actions';
import { bindConnectorParameterContext } from
'./bind-connector-parameter-context';
+import { bindGoToService } from './bind-go-to-service';
import { SelectedComponent } from './connector-canvas.actions';
import * as EmptyQueueActions from
'../../../../state/empty-queue/empty-queue.actions';
import {
@@ -720,7 +721,14 @@ export class ConnectorCanvasEffects {
instance.createNewProperty = () => NEVER;
instance.createNewService = () => NEVER;
instance.convertToParameter = () => NEVER;
- instance.goToService = () => undefined;
+ instance.goToService = bindGoToService(
+ this.store,
+ this.connectorService,
+ this.errorHelper,
+ dialogRef,
+ this.store.select(selectConnectorIdFromRoute),
+ ErrorContextKey.CONNECTOR_CANVAS
+ );
// EditProcessor only exposes `parameterContext`; the underlying
property table
// disables parameter affordances on its own when parameterContext is
undefined.
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/state/connector-controller-services/connector-controller-services.effects.spec.ts
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/state/connector-controller-services/connector-controller-services.effects.spec.ts
index 4b13e76b082..3345e3f8f86 100644
---
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/state/connector-controller-services/connector-controller-services.effects.spec.ts
+++
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/state/connector-controller-services/connector-controller-services.effects.spec.ts
@@ -31,6 +31,8 @@ import {
selectConnectorControllerService
} from './connector-controller-services.actions';
import { selectConnectorParameterContext } from
'../connector-canvas/connector-canvas.selectors';
+import { navigateToControllerService } from
'../connector-canvas/connector-canvas.actions';
+import { selectConnectorIdFromRoute } from
'./connector-controller-services.selectors';
import { ConnectorService } from '../../service/connector.service';
import { ErrorHelper } from '../../../../service/error-helper.service';
import { ErrorContextKey } from '../../../../state/error';
@@ -38,6 +40,8 @@ import * as ErrorActions from
'../../../../state/error/error.actions';
import { ControllerServiceEntity, ParameterContextEntity } from
'../../../../state/shared';
import { createParameterContextFixture } from
'../../testing/parameter-context-fixture';
import { EditControllerService } from
'../../../../ui/common/controller-service/edit-controller-service/edit-controller-service.component';
+import { HttpErrorResponse } from '@angular/common/http';
+import { MockStore } from '@ngrx/store/testing';
function buildService(overrides: Partial<ControllerServiceEntity> = {}):
ControllerServiceEntity {
return {
@@ -51,6 +55,7 @@ function buildService(overrides:
Partial<ControllerServiceEntity> = {}): Control
describe('ConnectorControllerServicesEffects', () => {
interface SetupOptions {
parameterContext?: ParameterContextEntity | null;
+ connectorId?: string | null;
}
async function setup(options: SetupOptions = {}) {
@@ -58,7 +63,8 @@ describe('ConnectorControllerServicesEffects', () => {
const mockConnectorService = {
getConnectorControllerServices: vi.fn(),
- getConnectorFlow: vi.fn()
+ getConnectorFlow: vi.fn(),
+ getControllerService: vi.fn()
};
const mockErrorHelper = {
@@ -78,11 +84,13 @@ describe('ConnectorControllerServicesEffects', () => {
parameterContext: undefined as any,
supportsParameters: true as any
};
+ const mockDialogRef = {
+ componentInstance: mockDialogInstance,
+ afterClosed: () => afterClosed$.asObservable(),
+ close: vi.fn()
+ };
const mockDialog = {
- open: vi.fn().mockReturnValue({
- componentInstance: mockDialogInstance,
- afterClosed: () => afterClosed$.asObservable()
- })
+ open: vi.fn().mockReturnValue(mockDialogRef)
};
await TestBed.configureTestingModule({
@@ -94,6 +102,10 @@ describe('ConnectorControllerServicesEffects', () => {
{
selector: selectConnectorParameterContext,
value: options.parameterContext ?? null
+ },
+ {
+ selector: selectConnectorIdFromRoute,
+ value: options.connectorId !== undefined ?
options.connectorId : 'conn-1'
}
]
}),
@@ -106,10 +118,13 @@ describe('ConnectorControllerServicesEffects', () => {
return {
effects: TestBed.inject(ConnectorControllerServicesEffects),
+ store: TestBed.inject(MockStore),
mockConnectorService,
+ mockErrorHelper,
mockRouter,
mockDialog,
mockDialogInstance,
+ mockDialogRef,
afterClosed$,
actions$: (stream: Observable<Action>) => {
actions$ = stream;
@@ -256,5 +271,73 @@ describe('ConnectorControllerServicesEffects', () => {
expect(mockDialogInstance.supportsParameters).toBe(false);
expect(mockDialogInstance.goToParameter).toBeUndefined();
});
+
+ it('should wire goToService to fetch via the connector-scoped API and
navigate to the controller service', async () => {
+ const { effects, actions$, store, mockConnectorService,
mockDialogInstance, mockDialogRef } = await setup();
+ const dispatchSpy = vi.spyOn(store, 'dispatch');
+ mockConnectorService.getControllerService.mockReturnValue(
+ of({ id: 'svc-2', component: { parentGroupId: 'pg-cs', id:
'svc-2' } })
+ );
+
+ actions$(of(openViewControllerServiceDialog({ controllerService:
buildService() })));
+ await firstValueFrom(effects.openViewControllerServiceDialog$);
+
+ expect(mockDialogInstance.goToService).toBeInstanceOf(Function);
+ mockDialogInstance.goToService('svc-2');
+
+
expect(mockConnectorService.getControllerService).toHaveBeenCalledWith('conn-1',
'svc-2');
+ expect(mockDialogRef.close).toHaveBeenCalled();
+ expect(dispatchSpy).toHaveBeenCalledWith(
+ navigateToControllerService({
+ processGroupId: 'pg-cs',
+ serviceId: 'svc-2'
+ })
+ );
+ });
+
+ it('should dispatch a banner error when goToService fails to fetch the
controller service', async () => {
+ const { effects, actions$, store, mockConnectorService,
mockDialogInstance, mockErrorHelper } =
+ await setup();
+ const dispatchSpy = vi.spyOn(store, 'dispatch');
+ const errorResponse = new HttpErrorResponse({ status: 404,
statusText: 'Not Found' });
+
mockConnectorService.getControllerService.mockReturnValue(throwError(() =>
errorResponse));
+
+ actions$(of(openViewControllerServiceDialog({ controllerService:
buildService() })));
+ await firstValueFrom(effects.openViewControllerServiceDialog$);
+
+ mockDialogInstance.goToService('svc-missing');
+
+
expect(mockErrorHelper.getErrorString).toHaveBeenCalledWith(errorResponse);
+ expect(dispatchSpy).toHaveBeenCalledWith(
+ ErrorActions.addBannerError({
+ errorContext: {
+ errors: ['Error message'],
+ context: ErrorContextKey.CONTROLLER_SERVICES
+ }
+ })
+ );
+ });
+
+ it('should dispatch a banner error when goToService cannot resolve the
connector id', async () => {
+ const { effects, actions$, store, mockConnectorService,
mockDialogInstance } = await setup({
+ connectorId: null
+ });
+ const dispatchSpy = vi.spyOn(store, 'dispatch');
+
+ actions$(of(openViewControllerServiceDialog({ controllerService:
buildService() })));
+ await firstValueFrom(effects.openViewControllerServiceDialog$);
+
+ mockDialogInstance.goToService('svc-1');
+
+
expect(mockConnectorService.getControllerService).not.toHaveBeenCalled();
+ expect(dispatchSpy).toHaveBeenCalledWith(
+ ErrorActions.addBannerError({
+ errorContext: {
+ errors: ['Unable to determine Connector id for
navigation.'],
+ context: ErrorContextKey.CONTROLLER_SERVICES
+ }
+ })
+ );
+ });
});
});
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/state/connector-controller-services/connector-controller-services.effects.ts
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/state/connector-controller-services/connector-controller-services.effects.ts
index cfe655e33d9..05d870af659 100644
---
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/state/connector-controller-services/connector-controller-services.effects.ts
+++
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/state/connector-controller-services/connector-controller-services.effects.ts
@@ -30,7 +30,9 @@ import { ErrorHelper } from
'../../../../service/error-helper.service';
import { EditControllerService } from
'../../../../ui/common/controller-service/edit-controller-service/edit-controller-service.component';
import { EditControllerServiceDialogRequest } from '../../../../state/shared';
import * as ConnectorControllerServicesActions from
'./connector-controller-services.actions';
+import { selectConnectorIdFromRoute } from
'./connector-controller-services.selectors';
import { bindConnectorParameterContext } from
'../connector-canvas/bind-connector-parameter-context';
+import { bindGoToService } from '../connector-canvas/bind-go-to-service';
@Injectable()
export class ConnectorControllerServicesEffects {
@@ -148,7 +150,14 @@ export class ConnectorControllerServicesEffects {
// values still render in the value tip.
instance.createNewService = () => NEVER;
instance.convertToParameter = () => NEVER;
- instance.goToService = () => undefined;
+ instance.goToService = bindGoToService(
+ this.store,
+ this.connectorService,
+ this.errorHelper,
+ dialogRef,
+ this.store.select(selectConnectorIdFromRoute),
+ ErrorContextKey.CONTROLLER_SERVICES
+ );
bindConnectorParameterContext(
this.store,
diff --git
a/nifi-toolkit/nifi-toolkit-client/src/main/java/org/apache/nifi/toolkit/client/ConnectorClient.java
b/nifi-toolkit/nifi-toolkit-client/src/main/java/org/apache/nifi/toolkit/client/ConnectorClient.java
index 607548c3a8b..e1327ca484c 100644
---
a/nifi-toolkit/nifi-toolkit-client/src/main/java/org/apache/nifi/toolkit/client/ConnectorClient.java
+++
b/nifi-toolkit/nifi-toolkit-client/src/main/java/org/apache/nifi/toolkit/client/ConnectorClient.java
@@ -24,6 +24,7 @@ import org.apache.nifi.web.api.entity.ConfigurationStepEntity;
import org.apache.nifi.web.api.entity.ConfigurationStepNamesEntity;
import org.apache.nifi.web.api.entity.ConnectorEntity;
import org.apache.nifi.web.api.entity.ConnectorPropertyAllowableValuesEntity;
+import org.apache.nifi.web.api.entity.ControllerServiceEntity;
import org.apache.nifi.web.api.entity.DropRequestEntity;
import org.apache.nifi.web.api.entity.MigrationPayloadEntity;
import org.apache.nifi.web.api.entity.MigrationRequestEntity;
@@ -470,6 +471,17 @@ public interface ConnectorClient {
*/
ComponentStateEntity clearProcessorState(String connectorId, String
processorId) throws NiFiClientException, IOException;
+ /**
+ * Gets a controller service within a connector.
+ *
+ * @param connectorId the connector ID
+ * @param controllerServiceId the controller service ID
+ * @return the controller service entity
+ * @throws NiFiClientException if an error occurs during the request
+ * @throws IOException if an I/O error occurs
+ */
+ ControllerServiceEntity getControllerService(String connectorId, String
controllerServiceId) throws NiFiClientException, IOException;
+
/**
* Gets the state for a controller service within a connector.
*
diff --git
a/nifi-toolkit/nifi-toolkit-client/src/main/java/org/apache/nifi/toolkit/client/impl/JerseyConnectorClient.java
b/nifi-toolkit/nifi-toolkit-client/src/main/java/org/apache/nifi/toolkit/client/impl/JerseyConnectorClient.java
index 751b22d3a46..30abe262863 100644
---
a/nifi-toolkit/nifi-toolkit-client/src/main/java/org/apache/nifi/toolkit/client/impl/JerseyConnectorClient.java
+++
b/nifi-toolkit/nifi-toolkit-client/src/main/java/org/apache/nifi/toolkit/client/impl/JerseyConnectorClient.java
@@ -34,6 +34,7 @@ import
org.apache.nifi.web.api.entity.ConfigurationStepNamesEntity;
import org.apache.nifi.web.api.entity.ConnectorEntity;
import org.apache.nifi.web.api.entity.ConnectorPropertyAllowableValuesEntity;
import org.apache.nifi.web.api.entity.ConnectorRunStatusEntity;
+import org.apache.nifi.web.api.entity.ControllerServiceEntity;
import org.apache.nifi.web.api.entity.DropRequestEntity;
import org.apache.nifi.web.api.entity.MigrationPayloadEntity;
import org.apache.nifi.web.api.entity.MigrationRequestEntity;
@@ -844,6 +845,25 @@ public class JerseyConnectorClient extends
AbstractJerseyClient implements Conne
});
}
+ @Override
+ public ControllerServiceEntity getControllerService(final String
connectorId, final String controllerServiceId) throws NiFiClientException,
IOException {
+ if (StringUtils.isBlank(connectorId)) {
+ throw new IllegalArgumentException("Connector id cannot be null or
blank");
+ }
+ if (StringUtils.isBlank(controllerServiceId)) {
+ throw new IllegalArgumentException("Controller service id cannot
be null or blank");
+ }
+
+ return executeAction("Error retrieving controller service for
Connector " + connectorId, () -> {
+ final WebTarget target = connectorTarget
+ .path("/controller-services/{controllerServiceId}")
+ .resolveTemplate("id", connectorId)
+ .resolveTemplate("controllerServiceId", controllerServiceId);
+
+ return
getRequestBuilder(target).get(ControllerServiceEntity.class);
+ });
+ }
+
@Override
public ComponentStateEntity getControllerServiceState(final String
connectorId, final String controllerServiceId) throws NiFiClientException,
IOException {
if (StringUtils.isBlank(connectorId)) {