bbende commented on code in PR #9017:
URL: https://github.com/apache/nifi/pull/9017#discussion_r1664639608


##########
nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ControllerResource.java:
##########
@@ -2411,6 +2429,289 @@ public Date getEndDate() {
         }
     }
 
+    // ------------
+    // NARs
+    // ------------
+
+    @POST
+    @Consumes(MediaType.APPLICATION_OCTET_STREAM)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("nar-manager/upload")
+    @Operation(
+            summary = "Uploads a NAR and requests for it to be installed",
+            responses = @ApiResponse(content = @Content(schema = 
@Schema(implementation = NarSummaryEntity.class))),
+            security = {
+                    @SecurityRequirement(name = "Write - /controller")
+            }
+    )
+    @ApiResponses(
+            value = {
+                    @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 = "409", description = "The 
request was valid but NiFi was not in the appropriate state to process it.")
+            }
+    )
+    public Response uploadNar(
+            @HeaderParam(UploadRequestReplicator.FILENAME_HEADER)
+            final String filename,
+            @Parameter(description = "The contents of the NAR file.", required 
= true)
+            final InputStream inputStream) throws IOException {
+
+        authorizeController(RequestAction.WRITE);
+
+        if (StringUtils.isBlank(filename)) {
+            throw new 
IllegalArgumentException(UploadRequestReplicator.FILENAME_HEADER + " header is 
required");
+        }
+        if (inputStream == null) {
+            throw new IllegalArgumentException("NAR contents are required");
+        }
+
+        // If clustered and not all nodes are connected, do not allow 
uploading a NAR.
+        // Generally, we allow the flow to be modified when nodes are 
disconnected, but we do not allow uploading a NAR because
+        // the cluster has no mechanism for synchronizing those NARs after the 
upload.
+        final ClusterCoordinator clusterCoordinator = getClusterCoordinator();
+        if (clusterCoordinator != null) {
+            final Set<NodeIdentifier> disconnectedNodes = 
clusterCoordinator.getNodeIdentifiers(NodeConnectionState.CONNECTING, 
NodeConnectionState.DISCONNECTED, NodeConnectionState.DISCONNECTING);
+            if (!disconnectedNodes.isEmpty()) {
+                throw new IllegalStateException("Cannot upload NAR because the 
following %s nodes are not currently connected: 
%s".formatted(disconnectedNodes.size(), disconnectedNodes));
+            }
+        }
+
+        final long startTime = System.currentTimeMillis();
+        final InputStream maxLengthInputStream = new 
MaxLengthInputStream(inputStream, (long) DataUnit.GB.toB(1));
+
+        if (isReplicateRequest()) {
+            final UploadRequest<NarSummaryEntity> uploadRequest = new 
UploadRequest.Builder<NarSummaryEntity>()
+                    .user(NiFiUserUtils.getNiFiUser())
+                    .filename(filename)
+                    .identifier(UUID.randomUUID().toString())
+                    .contents(maxLengthInputStream)
+                    .exampleRequestUri(getAbsolutePath())
+                    .responseClass(NarSummaryEntity.class)
+                    .build();
+            final NarSummaryEntity summaryEntity = 
uploadRequestReplicator.upload(uploadRequest);
+            return generateOkResponse(summaryEntity).build();
+        }
+
+        final NarSummaryEntity summaryEntity = 
serviceFacade.uploadNar(maxLengthInputStream);
+        final NarSummaryDTO summary = summaryEntity.getNarSummary();
+
+        final long elapsedTime = System.currentTimeMillis() - startTime;
+        LOGGER.info("Upload completed for NAR [{}] in {} ms", 
summary.getIdentifier(), elapsedTime);
+
+        return generateOkResponse(summaryEntity).build();
+    }
+
+    @GET
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("/nar-manager/nars")
+    @Operation(
+            summary = "Retrieves summary information for installed NARs",
+            responses = @ApiResponse(content = @Content(schema = 
@Schema(implementation = NarSummariesEntity.class))),
+            security = {
+                    @SecurityRequirement(name = "Read - /controller")
+            }
+    )
+    @ApiResponses(
+            value = {
+                    @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 = "409", description = "The 
request was valid but NiFi was not in the appropriate state to process it.")
+            }
+    )
+    public Response getNarSummaries() {
+        authorizeController(RequestAction.READ);
+
+        if (isReplicateRequest()) {
+            return replicate(HttpMethod.GET);
+        }
+
+        final NarSummariesEntity summariesEntity = new NarSummariesEntity();
+        summariesEntity.setNarSummaries(serviceFacade.getNarSummaries());
+        summariesEntity.setCurrentTime(new Date());
+
+        return generateOkResponse(summariesEntity).build();
+    }
+
+    @GET
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("/nar-manager/nars/{id}")
+    @Operation(
+            summary = "Retrieves the summary information for the NAR with the 
given identifier",
+            responses = @ApiResponse(content = @Content(schema = 
@Schema(implementation = NarDetailsEntity.class))),
+            security = {
+                    @SecurityRequirement(name = "Read - /controller")
+            }
+    )
+    @ApiResponses(
+            value = {
+                    @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 = "409", description = "The 
request was valid but NiFi was not in the appropriate state to process it.")
+            }
+    )
+    public Response getNarSummary(
+            @PathParam("id")
+            @Parameter(description = "The id of the NAR.", required = true)
+            final String id) {
+        authorizeController(RequestAction.READ);
+
+        if (StringUtils.isBlank(id)) {
+            throw new IllegalArgumentException("Id is required");
+        }
+
+        if (isReplicateRequest()) {
+            return replicate(HttpMethod.GET);
+        }
+
+        final NarSummaryEntity summaryEntity = serviceFacade.getNarSummary(id);
+        return generateOkResponse(summaryEntity).build();
+    }
+
+    @GET
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("/nar-manager/nars/{id}/details")
+    @Operation(
+            summary = "Retrieves the component types available from the 
installed NARs",
+            responses = @ApiResponse(content = @Content(schema = 
@Schema(implementation = NarDetailsEntity.class))),
+            security = {
+                    @SecurityRequirement(name = "Read - /controller")
+            }
+    )
+    @ApiResponses(
+            value = {
+                    @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 = "409", description = "The 
request was valid but NiFi was not in the appropriate state to process it.")
+            }
+    )
+    public Response getNarDetails(
+            @PathParam("id")
+            @Parameter(description = "The id of the NAR.", required = true)
+            final String id) {
+        authorizeController(RequestAction.READ);
+
+        if (StringUtils.isBlank(id)) {
+            throw new IllegalArgumentException("Id is required");
+        }
+
+        if (isReplicateRequest()) {
+            return replicate(HttpMethod.GET);
+        }
+
+        final NarDetailsEntity detailsEntity = serviceFacade.getNarDetails(id);
+        return generateOkResponse(detailsEntity).build();
+    }
+
+    @GET
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_OCTET_STREAM)
+    @Path("/nar-manager/nars/{id}/download")

Review Comment:
   We already have a `GET` for `nar-manager/nars/{id}` which returns the 
summary for the NAR with the given id.
   
   I guess then can be differentiated by the requested content type? 
   
   We also have similar examples like downloading a flow definition vs 
retrieving JSON for a process process group:
   ```
   /process-groups/{id}/download
   /process-groups/{id}
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to