package com.thinkbiganalytics.feedmgr.rest.controller; /*- * #%L * thinkbig-feed-manager-controller * %% * Copyright (C) 2017 ThinkBig Analytics * %% * Licensed 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. * #L% */ import com.google.common.collect.ImmutableMap; import com.thinkbiganalytics.discovery.schema.TableSchema; import com.thinkbiganalytics.feedmgr.nifi.CleanupStaleFeedRevisions; import com.thinkbiganalytics.feedmgr.nifi.DBCPConnectionPoolTableInfo; import com.thinkbiganalytics.feedmgr.nifi.NifiConnectionService; import com.thinkbiganalytics.feedmgr.nifi.PropertyExpressionResolver; import com.thinkbiganalytics.feedmgr.nifi.SpringEnvironmentProperties; import com.thinkbiganalytics.feedmgr.service.template.FeedManagerTemplateService; import com.thinkbiganalytics.nifi.rest.client.LegacyNifiRestClient; import com.thinkbiganalytics.nifi.rest.client.NiFiRestClient; import com.thinkbiganalytics.nifi.rest.client.layout.AlignNiFiComponents; import com.thinkbiganalytics.nifi.rest.client.layout.AlignProcessGroupComponents; import com.thinkbiganalytics.nifi.rest.model.NiFiClusterSummary; import com.thinkbiganalytics.nifi.rest.model.NiFiPropertyDescriptorTransform; import com.thinkbiganalytics.nifi.rest.model.flow.NifiFlowDeserializer; import com.thinkbiganalytics.nifi.rest.model.flow.NifiFlowProcessGroup; import com.thinkbiganalytics.rest.model.RestResponseStatus; import org.apache.commons.lang3.StringUtils; import org.apache.nifi.web.api.dto.ControllerServiceDTO; import org.apache.nifi.web.api.dto.DocumentedTypeDTO; import org.apache.nifi.web.api.dto.PortDTO; import org.apache.nifi.web.api.dto.ProcessGroupDTO; import org.apache.nifi.web.api.entity.ControllerServiceTypesEntity; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.ResourceBundle; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.inject.Inject; import javax.ws.rs.BadRequestException; import javax.ws.rs.DefaultValue; import javax.ws.rs.GET; import javax.ws.rs.NotFoundException; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiResponse; import io.swagger.annotations.ApiResponses; import io.swagger.annotations.SwaggerDefinition; import io.swagger.annotations.Tag; @Api(tags = "Feed Manager - NiFi", produces = "application/json") @Path(NifiIntegrationRestController.BASE) @Component @SwaggerDefinition(tags = @Tag(name = "Feed Manager - NiFi", description = "integration with NiFi")) public class NifiIntegrationRestController { private static final Logger log = LoggerFactory.getLogger(NifiIntegrationRestController.class); /** * Messages for the default locale */ private static final ResourceBundle STRINGS = ResourceBundle.getBundle("com.thinkbiganalytics.feedmgr.rest.controller.NiFiIntegrationMessages"); public static final String BASE = "/v1/feedmgr/nifi"; public static final String FLOWS = "/flows"; public static final String REUSABLE_INPUT_PORTS = "/reusable-input-ports"; @Inject DBCPConnectionPoolTableInfo dbcpConnectionPoolTableInfo; @Inject FeedManagerTemplateService feedManagerTemplateService; @Inject NiFiPropertyDescriptorTransform propertyDescriptorTransform; @Inject NifiConnectionService nifiConnectionService; /** * Legacy NiFi REST client */ @Inject private LegacyNifiRestClient legacyNifiRestClient; /** * New NiFi REST client */ @Inject private NiFiRestClient nifiRestClient; @Inject private SpringEnvironmentProperties environmentProperties; @GET @Path("/auto-align/{processGroupId}") @Produces(MediaType.APPLICATION_JSON) @ApiOperation("Organizes the components of the specified process group.") @ApiResponses( @ApiResponse(code = 200, message = "The result of the operation.", response = RestResponseStatus.class) ) public Response autoAlign(@PathParam("processGroupId") String processGroupId) { RestResponseStatus status; if ("all".equals(processGroupId)) { AlignNiFiComponents alignNiFiComponents = new AlignNiFiComponents(); alignNiFiComponents.setNiFiRestClient(legacyNifiRestClient.getNiFiRestClient()); alignNiFiComponents.autoLayout(); String message = ""; if (alignNiFiComponents.isAligned()) { message = "Aligned All of NiFi. " + alignNiFiComponents.getAlignedProcessGroups() + " process groups were aligned "; } else { message = "Alignment failed while attempting to align all of NiFi. " + alignNiFiComponents.getAlignedProcessGroups() + " were successfully aligned. Please look at the logs for more information"; } status = new RestResponseStatus.ResponseStatusBuilder().message(message).buildSuccess(); } else { AlignProcessGroupComponents alignProcessGroupComponents = new AlignProcessGroupComponents(legacyNifiRestClient.getNiFiRestClient(), processGroupId); ProcessGroupDTO alignedGroup = alignProcessGroupComponents.autoLayout(); String message = ""; if (alignProcessGroupComponents.isAligned()) { message = "Aligned " + alignedGroup.getContents().getProcessGroups().size() + " process groups under " + alignedGroup.getName(); } else { message = "Alignment failed for process group " + processGroupId + ". Please look at the logs for more information"; } status = new RestResponseStatus.ResponseStatusBuilder().message(message).buildSuccess(); } return Response.ok(status).build(); } @GET @Path("/cleanup-versions/{processGroupId}") @Produces(MediaType.APPLICATION_JSON) @ApiOperation(value = "Performs a cleanup of the specified process group.", notes = "This method will list all of the child process groups and delete the ones where the name matches the regular expression: .* - \\d{13}") @ApiResponses({ @ApiResponse(code = 200, message = "Returns the number of process groups deleted.", response = RestResponseStatus.class), @ApiResponse(code = 500, message = "The process group is unavailable.", response = RestResponseStatus.class) }) public Response cleanupVersionedProcessGroups(@PathParam("processGroupId") String processGroupId) { RestResponseStatus status; CleanupStaleFeedRevisions cleanupStaleFeedRevisions = new CleanupStaleFeedRevisions(legacyNifiRestClient, processGroupId, propertyDescriptorTransform); cleanupStaleFeedRevisions.cleanup(); String msg = "Cleaned up " + cleanupStaleFeedRevisions.getDeletedProcessGroups().size() + " Process Groups"; status = new RestResponseStatus.ResponseStatusBuilder().message(msg).buildSuccess(); return Response.ok(status).build(); } @GET @Path("/flow/{processGroupId}") @Produces(MediaType.APPLICATION_JSON) @ApiOperation("Gets the flow of the specified process group.") @ApiResponses({ @ApiResponse(code = 200, message = "Returns the flow.", response = NifiFlowProcessGroup.class), @ApiResponse(code = 500, message = "The process group is unavailable.", response = RestResponseStatus.class) }) public Response getFlow(@PathParam("processGroupId") String processGroupId) { NifiFlowProcessGroup flow = legacyNifiRestClient.getFeedFlow(processGroupId); NifiFlowDeserializer.prepareForSerialization(flow); return Response.ok(flow).build(); } @GET @Path("/flow/feed/{categoryAndFeedName}") @Produces(MediaType.APPLICATION_JSON) @ApiOperation("Gets the flow of the specified feed.") @ApiResponses({ @ApiResponse(code = 200, message = "Returns the flow.", response = NifiFlowProcessGroup.class), @ApiResponse(code = 500, message = "The process group is unavailable.", response = RestResponseStatus.class) }) public Response getFlowForCategoryAndFeed(@PathParam("categoryAndFeedName") String categoryAndFeedName) { NifiFlowProcessGroup flow = legacyNifiRestClient.getFeedFlowForCategoryAndFeed(categoryAndFeedName); NifiFlowDeserializer.prepareForSerialization(flow); return Response.ok(flow).build(); } //walk entire graph @GET @Path(FLOWS) @Produces(MediaType.APPLICATION_JSON) @ApiOperation("Gets a list of all flows.") @ApiResponses({ @ApiResponse(code = 200, message = "Returns the flows.", response = NifiFlowProcessGroup.class, responseContainer = "List"), @ApiResponse(code = 500, message = "NiFi is unavailable.", response = RestResponseStatus.class) }) public Response getFlows() { List<NifiFlowProcessGroup> feedFlows = legacyNifiRestClient.getFeedFlows(); if (feedFlows != null) { log.info("********************** getAllFlows ({})", feedFlows.size()); feedFlows.stream().forEach(group -> NifiFlowDeserializer.prepareForSerialization(group)); } return Response.ok(feedFlows).build(); } @GET @Path("/configuration/properties") @Produces(MediaType.APPLICATION_JSON) @ApiOperation(value = "Gets user properties for NiFi.", notes = "These are the properties beginning with 'config.' in the application.properties file.") @ApiResponses( @ApiResponse(code = 200, message = "Returns the user properties.", response = Map.class) ) public Response getFeeds() { Map<String, Object> properties = environmentProperties.getPropertiesStartingWith(PropertyExpressionResolver.configPropertyPrefix); if (properties == null) { properties = new HashMap<>(); } return Response.ok(properties).build(); } @GET @Path(REUSABLE_INPUT_PORTS) @Produces(MediaType.APPLICATION_JSON) @ApiOperation("Gets the input ports to reusable templates.") @ApiResponses({ @ApiResponse(code = 200, message = "Returns the input ports.", response = PortDTO.class, responseContainer = "Set"), @ApiResponse(code = 500, message = "NiFi is unavailable.", response = RestResponseStatus.class) }) public Response getReusableFeedInputPorts() { Set<PortDTO> ports = feedManagerTemplateService.getReusableFeedInputPorts(); return Response.ok(ports).build(); } /** * Finds controller services of the specified type. * * @param processGroupId the process group id * @param type the type to match * @return the list of matching controller services */ @GET @Path("/controller-services/process-group/{processGroupId}") @Produces(MediaType.APPLICATION_JSON) @ApiOperation("Finds controller services of the specified type.") @ApiResponses({ @ApiResponse(code = 200, message = "Returns the matching controller services.", response = ControllerServiceDTO.class, responseContainer = "Set"), @ApiResponse(code = 400, message = "The type cannot be empty.", response = RestResponseStatus.class), @ApiResponse(code = 404, message = "The process group cannot be found.", response = RestResponseStatus.class), @ApiResponse(code = 500, message = "The process group is unavailable.", response = RestResponseStatus.class) }) public Response getControllerServices(@Nonnull @PathParam("processGroupId") final String processGroupId, @Nullable @QueryParam("type") final String type) { // Verify parameters if (StringUtils.isBlank(processGroupId)) { throw new NotFoundException(STRINGS.getString("getControllerServices.missingProcessGroup")); } if (StringUtils.isBlank(type)) { throw new BadRequestException(STRINGS.getString("getControllerServices.missingType")); } // Determine allowed service types final Stream<String> subTypes = nifiRestClient.controllerServices().getTypes(type).stream().map(DocumentedTypeDTO::getType); final Set<String> allowedTypes = Stream.concat(Stream.of(type), subTypes).collect(Collectors.toSet()); // Filter controller services final Set<ControllerServiceDTO> controllerServices = ("all".equalsIgnoreCase(processGroupId) || "root".equalsIgnoreCase(processGroupId)) ? nifiRestClient.processGroups().getControllerServices("root") : nifiRestClient.processGroups().getControllerServices(processGroupId); final Set<ControllerServiceDTO> matchingControllerServices = controllerServices.stream() .filter(controllerService -> allowedTypes.contains(controllerService.getType())) .collect(Collectors.toSet()); return Response.ok(matchingControllerServices).build(); } @GET @Path("/controller-services") @Produces(MediaType.APPLICATION_JSON) @ApiOperation("Gets a list of available controller services.") @ApiResponses({ @ApiResponse(code = 200, message = "Returns the controller services.", response = ControllerServiceDTO.class, responseContainer = "Set"), @ApiResponse(code = 500, message = "NiFi is unavailable.", response = RestResponseStatus.class) }) public Response getServices() { final Set<ControllerServiceDTO> controllerServices = legacyNifiRestClient.getControllerServices(); return Response.ok(ImmutableMap.of("controllerServices", controllerServices)).build(); } @GET @Path("/controller-services/types") @Produces(MediaType.APPLICATION_JSON) @ApiOperation("Gets a list of the available controller service types.") @ApiResponses({ @ApiResponse(code = 200, message = "Returns the controller service types.", response = ControllerServiceTypesEntity.class), @ApiResponse(code = 500, message = "NiFi is unavailable.", response = RestResponseStatus.class) }) public Response getServiceTypes() { final ControllerServiceTypesEntity entity = new ControllerServiceTypesEntity(); entity.setControllerServiceTypes(legacyNifiRestClient.getControllerServiceTypes()); return Response.ok(entity).build(); } @GET @Path("/controller-services/{serviceId}/tables") @Produces(MediaType.APPLICATION_JSON) @ApiOperation(value = "Gets a list of table names from the specified database.", notes = "Connects to the database specified by the controller service using the password defined in Kylo's application.properties file.") @ApiResponses({ @ApiResponse(code = 200, message = "Returns the table names.", response = String.class, responseContainer = "List"), @ApiResponse(code = 500, message = "Nifi or the database are unavailable.", response = RestResponseStatus.class) }) public Response getTableNames(@PathParam("serviceId") String serviceId, @QueryParam("serviceName") @DefaultValue("") String serviceName, @QueryParam("schema") String schema, @QueryParam("tableName") String tableName) { log.info("Query for Table Names against service: {}({})", serviceName, serviceId); List<String> tables = dbcpConnectionPoolTableInfo.getTableNamesForControllerService(serviceId, serviceName, schema, tableName); return Response.ok(tables).build(); } @GET @Path("/controller-services/{serviceId}/tables/{tableName}") @Produces(MediaType.APPLICATION_JSON) @ApiOperation(value = "Gets the schema of the specified table.", notes = "Connects to the database specified by the controller service using the password defined in Kylo's application.properties file.") @ApiResponses({ @ApiResponse(code = 200, message = "Returns the table schema.", response = TableSchema.class), @ApiResponse(code = 500, message = "Nifi or the database are unavailable.", response = RestResponseStatus.class) }) public Response describeTable(@PathParam("serviceId") String serviceId, @PathParam("tableName") String tableName, @QueryParam("serviceName") @DefaultValue("") String serviceName, @QueryParam("schema") String schema) { log.info("Describe Table {} against service: {}({})", tableName, serviceName, serviceId); TableSchema tableSchema = dbcpConnectionPoolTableInfo.describeTableForControllerService(serviceId, serviceName, schema, tableName); return Response.ok(tableSchema).build(); } @GET @Path("/controller-services/{serviceId}") @Produces(MediaType.APPLICATION_JSON) @ApiOperation(value = "Gets a controller service.", notes = "returns a Nifi controller service object by the supplied identifier") @ApiResponses({ @ApiResponse(code = 200, message = "Returns the controller service.", response = ControllerServiceDTO.class), @ApiResponse(code = 500, message = "Unable to find the controller service", response = RestResponseStatus.class) }) public Response getControllerService(@PathParam("serviceId") String serviceId) { try { final ControllerServiceDTO controllerService = legacyNifiRestClient.getControllerService(null, serviceId); return Response.ok(controllerService).build(); } catch (Exception e) { RestResponseStatus error = new RestResponseStatus.ResponseStatusBuilder().message("Unable to find controller service for " + serviceId).buildError(); return Response.ok(error).build(); } } /** * Gets the NiFi cluster status. * * @return the cluster summary */ @GET @Path("/cluster/summary") @Produces(MediaType.APPLICATION_JSON) @ApiOperation("Gets the status of the NiFi cluster.") @ApiResponses({ @ApiResponse(code = 200, message = "Returns the cluster status.", response = NiFiClusterSummary.class), @ApiResponse(code = 500, message = "NiFi is unavailable.", response = RestResponseStatus.class) }) public Response getClusterSummary() { final NiFiClusterSummary clusterSummary = nifiRestClient.clusterSummary(); return Response.ok(clusterSummary).build(); } /** * Checks to see if NiFi is up and running * * @return true if running, false if not */ @GET @Path("/running") @Produces(MediaType.APPLICATION_JSON) @ApiOperation("Gets the status of the NiFi cluster.") @ApiResponses({ @ApiResponse(code = 200, message = "Returns the status of NiFi if its running or not"), @ApiResponse(code = 500, message = "An error occurred accessing the NiFi status.", response = RestResponseStatus.class) }) public Response getRunning() { boolean isRunning = nifiConnectionService.isNiFiRunning(); return Response.ok(isRunning).build(); } }