diff --git a/src/main/java/com/cfdeployer/CfDeployerApplication.java b/src/main/java/com/cfdeployer/CfDeployerApplication.java index ebde791..62fdb4b 100644 --- a/src/main/java/com/cfdeployer/CfDeployerApplication.java +++ b/src/main/java/com/cfdeployer/CfDeployerApplication.java @@ -2,10 +2,12 @@ package com.cfdeployer; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication @EnableScheduling +@EnableAsync public class CfDeployerApplication { public static void main(String[] args) { diff --git a/src/main/java/com/cfdeployer/controller/CfDeployController.java b/src/main/java/com/cfdeployer/controller/CfDeployController.java index 0a37b34..d7b02c9 100644 --- a/src/main/java/com/cfdeployer/controller/CfDeployController.java +++ b/src/main/java/com/cfdeployer/controller/CfDeployController.java @@ -4,6 +4,8 @@ import com.cfdeployer.model.CfDeployRequest; import com.cfdeployer.model.CfDeployResponse; import com.cfdeployer.model.ChunkUploadRequest; import com.cfdeployer.model.ChunkUploadResponse; +import com.cfdeployer.model.DeploymentStatus; +import com.cfdeployer.service.AsyncDeploymentService; import com.cfdeployer.service.CfCliService; import com.cfdeployer.service.ChunkedUploadService; import com.fasterxml.jackson.databind.ObjectMapper; @@ -26,6 +28,7 @@ public class CfDeployController { private final CfCliService cfCliService; private final ChunkedUploadService chunkedUploadService; + private final AsyncDeploymentService asyncDeploymentService; private final ObjectMapper objectMapper; @PostMapping(value = "/deploy", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @@ -180,19 +183,29 @@ public class CfDeployController { } @PostMapping("/upload/finalize") - public ResponseEntity finalizeUpload(@RequestParam("uploadSessionId") String uploadSessionId) { + public ResponseEntity finalizeUpload( + @RequestParam("uploadSessionId") String uploadSessionId, + @RequestParam(value = "async", defaultValue = "true") boolean async) { try { - log.info("Finalizing upload for session: {}", uploadSessionId); + log.info("Finalizing upload for session: {} (async={})", uploadSessionId, async); if (!chunkedUploadService.isSessionReady(uploadSessionId)) { return ResponseEntity.status(HttpStatus.BAD_REQUEST) - .body(CfDeployResponse.failure("Upload incomplete. Not all file chunks received.", null)); + .body(DeploymentStatus.builder() + .uploadSessionId(uploadSessionId) + .status(DeploymentStatus.Status.FAILED) + .message("Upload incomplete. Not all file chunks received.") + .build()); } var session = chunkedUploadService.getSession(uploadSessionId); if (session == null) { return ResponseEntity.status(HttpStatus.NOT_FOUND) - .body(CfDeployResponse.failure("Upload session not found or expired", null)); + .body(DeploymentStatus.builder() + .uploadSessionId(uploadSessionId) + .status(DeploymentStatus.Status.FAILED) + .message("Upload session not found or expired") + .build()); } CfDeployRequest request = objectMapper.readValue(session.getRequestJson(), CfDeployRequest.class); @@ -203,25 +216,77 @@ public class CfDeployController { var jarState = session.getFileStates().get("jarFile"); var manifestState = session.getFileStates().get("manifest"); - CfDeployResponse response = cfCliService.deployApplicationFromPaths( - request, - jarState.getTargetPath(), - manifestState.getTargetPath()); + if (async) { + // Start async deployment - returns immediately + asyncDeploymentService.deployAsync(uploadSessionId, request, + jarState.getTargetPath(), manifestState.getTargetPath()); - // Clean up session after deployment - chunkedUploadService.deleteSession(uploadSessionId); - - if (Boolean.TRUE.equals(response.getSuccess())) { - return ResponseEntity.ok(response); + return ResponseEntity.accepted().body(DeploymentStatus.builder() + .uploadSessionId(uploadSessionId) + .status(DeploymentStatus.Status.IN_PROGRESS) + .message("Deployment started. Use /deployment/status endpoint to check progress.") + .progress(0) + .build()); } else { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); + // Synchronous deployment - waits for completion (may timeout!) + CfDeployResponse response = cfCliService.deployApplicationFromPaths( + request, + jarState.getTargetPath(), + manifestState.getTargetPath()); + + // Clean up session after deployment + chunkedUploadService.deleteSession(uploadSessionId); + + if (Boolean.TRUE.equals(response.getSuccess())) { + return ResponseEntity.ok(DeploymentStatus.builder() + .uploadSessionId(uploadSessionId) + .status(DeploymentStatus.Status.COMPLETED) + .message(response.getMessage()) + .output(response.getOutput()) + .progress(100) + .build()); + } else { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(DeploymentStatus.builder() + .uploadSessionId(uploadSessionId) + .status(DeploymentStatus.Status.FAILED) + .message(response.getMessage()) + .error(response.getError()) + .build()); + } } } catch (Exception e) { log.error("Error finalizing upload", e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(CfDeployResponse.failure( - "Failed to finalize deployment: " + e.getMessage(), - e.toString())); + .body(DeploymentStatus.builder() + .uploadSessionId(uploadSessionId) + .status(DeploymentStatus.Status.FAILED) + .message("Failed to finalize deployment: " + e.getMessage()) + .error(e.toString()) + .build()); + } + } + + @GetMapping("/deployment/status/{uploadSessionId}") + public ResponseEntity getDeploymentStatus(@PathVariable String uploadSessionId) { + try { + DeploymentStatus status = asyncDeploymentService.getDeploymentStatus(uploadSessionId); + + // Clean up session and deployment status if completed or failed + if (status.getStatus() == DeploymentStatus.Status.COMPLETED || + status.getStatus() == DeploymentStatus.Status.FAILED) { + chunkedUploadService.deleteSession(uploadSessionId); + } + + return ResponseEntity.ok(status); + } catch (Exception e) { + log.error("Error getting deployment status", e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(DeploymentStatus.builder() + .uploadSessionId(uploadSessionId) + .status(DeploymentStatus.Status.FAILED) + .message("Failed to get deployment status: " + e.getMessage()) + .build()); } }