From b7405c644b8b63df99a6f228712a7bfa0d2ff7dd Mon Sep 17 00:00:00 2001 From: pratik Date: Mon, 20 Oct 2025 23:25:13 -0500 Subject: [PATCH] Tanzu Changes --- README.md | 142 ++++++++++++++-- build.gradle | 39 +++++ .../controller/CfDeployController.java | 66 +++++++- .../com/cfdeployer/service/CfCliService.java | 151 ++++++++++++++++-- 4 files changed, 371 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 7605e2c..934bea6 100644 --- a/README.md +++ b/README.md @@ -26,9 +26,9 @@ java -jar build/libs/cf-uploader-1.0.0.jar The application will start on `http://localhost:8080` -## API Endpoint +## API Endpoints -### Deploy Application to Cloud Foundry +### 1. Deploy Application to Cloud Foundry **Endpoint:** `POST /api/cf/deploy` @@ -39,7 +39,7 @@ The application will start on `http://localhost:8080` - `jarFile` (file): The Java application JAR file - `manifest` (file): Cloud Foundry manifest.yml file -### Sample cURL Request +#### Sample cURL Request ```bash curl -X POST http://localhost:8080/api/cf/deploy \ @@ -56,7 +56,7 @@ curl -X POST http://localhost:8080/api/cf/deploy \ -F 'manifest=@/path/to/manifest.yml' ``` -### Sample Request with Skip SSL Validation +#### Sample Request with Skip SSL Validation ```bash curl -X POST http://localhost:8080/api/cf/deploy \ @@ -73,6 +73,115 @@ curl -X POST http://localhost:8080/api/cf/deploy \ -F 'manifest=@./manifest.yml' ``` +--- + +### 2. List Applications + +**Endpoint:** `POST /api/cf/apps` + +**Content-Type:** `application/json` + +Lists all applications in the specified organization and space. + +#### Sample cURL Request + +```bash +curl -X POST http://localhost:8080/api/cf/apps \ + -H "Content-Type: application/json" \ + -d '{ + "apiEndpoint": "https://api.cf.example.com", + "username": "your-username", + "password": "your-password", + "organization": "your-org", + "space": "your-space", + "appName": "", + "skipSslValidation": false + }' +``` + +--- + +### 3. List Routes + +**Endpoint:** `POST /api/cf/routes` + +**Content-Type:** `application/json` + +Lists all routes in the specified organization and space. + +#### Sample cURL Request + +```bash +curl -X POST http://localhost:8080/api/cf/routes \ + -H "Content-Type: application/json" \ + -d '{ + "apiEndpoint": "https://api.cf.example.com", + "username": "your-username", + "password": "your-password", + "organization": "your-org", + "space": "your-space", + "appName": "", + "skipSslValidation": false + }' +``` + +--- + +### 4. Get Application Details + +**Endpoint:** `POST /api/cf/app/{appName}` + +**Content-Type:** `application/json` + +Gets detailed information about a specific application. + +#### Sample cURL Request + +```bash +curl -X POST http://localhost:8080/api/cf/app/my-app \ + -H "Content-Type: application/json" \ + -d '{ + "apiEndpoint": "https://api.cf.example.com", + "username": "your-username", + "password": "your-password", + "organization": "your-org", + "space": "your-space", + "appName": "", + "skipSslValidation": false + }' +``` + +--- + +### 5. Get Application Logs + +**Endpoint:** `POST /api/cf/logs/{appName}?recent=true` + +**Content-Type:** `application/json` + +**Query Parameters:** +- `recent` (optional, default: `true`): If true, gets recent logs; if false, tails logs + +Gets logs for a specific application. + +#### Sample cURL Request (Recent Logs) + +```bash +curl -X POST "http://localhost:8080/api/cf/logs/my-app?recent=true" \ + -H "Content-Type: application/json" \ + -d '{ + "apiEndpoint": "https://api.cf.example.com", + "username": "your-username", + "password": "your-password", + "organization": "your-org", + "space": "your-space", + "appName": "", + "skipSslValidation": false + }' +``` + +--- + ### Sample Manifest File (manifest.yml) ```yaml @@ -88,7 +197,9 @@ applications: SPRING_PROFILES_ACTIVE: production ``` -## Request Parameters +## Common Request Parameters + +All endpoints require the following CF credentials and target information: | Field | Type | Required | Description | |-------|------|----------|-------------| @@ -97,9 +208,11 @@ applications: | `password` | String | Yes | CF password | | `organization` | String | Yes | Target CF organization | | `space` | String | Yes | Target CF space | -| `appName` | String | Yes | Application name | +| `appName` | String | No* | Application name (required only for `/deploy` endpoint) | | `skipSslValidation` | Boolean | Yes | Skip SSL certificate validation | +**Note:** Even though `appName` is not used by utility endpoints (`/apps`, `/routes`, `/app/{appName}`, `/logs/{appName}`), it must still be included in the JSON request body (can be empty string). The organization and space determine which apps/routes are listed or queried. + ## Response Format ### Success Response @@ -147,13 +260,16 @@ cf.cli.path= ## Features -- RESTful API for CF deployments -- Automatic CF CLI binary management (Linux, macOS, Windows) -- Secure password handling (masked in logs) -- Configurable timeout for long-running deployments -- Comprehensive error handling -- Multipart file upload support up to 500MB -- Automatic cleanup of temporary files +- **Application Deployment**: Deploy JAR files to Cloud Foundry with manifest support +- **Application Management**: List apps, view details, and access logs +- **Route Management**: List all routes in your CF space +- **Automatic CF CLI Management**: Bundled CF CLI binaries for Linux, macOS, and Windows +- **Secure Password Handling**: Passwords are masked in all log output +- **Comprehensive Logging**: Detailed DEBUG-level logging for troubleshooting deployments +- **Configurable Timeouts**: Adjustable timeout for long-running deployments (default: 600s) +- **Large File Support**: Multipart file upload support up to 500MB +- **Automatic Cleanup**: Temporary files are automatically cleaned up after operations +- **Error Handling**: Comprehensive exception handling with detailed error messages ## Error Handling diff --git a/build.gradle b/build.gradle index 27d4140..d2e1fcd 100644 --- a/build.gradle +++ b/build.gradle @@ -42,6 +42,8 @@ task downloadCfCli { def resourcesDir = file("$projectDir/src/main/resources/cf-cli") resourcesDir.mkdirs() + // Download for all platforms (useful for local development on different OSes) + // For production Tanzu deployment, only Linux binary will be used def downloads = [ [os: 'linux', url: "https://packages.cloudfoundry.org/stable?release=linux64-binary&version=${cfCliVersion}&source=github-rel", ext: 'tgz', executable: 'cf'], [os: 'macos', url: "https://packages.cloudfoundry.org/stable?release=macosx64-binary&version=${cfCliVersion}&source=github-rel", ext: 'tgz', executable: 'cf'], @@ -81,5 +83,42 @@ task downloadCfCli { } } +// Task to download only Linux CF CLI (for production Tanzu builds) +task downloadCfCliLinuxOnly { + group = 'build' + description = 'Downloads CF CLI binary for Linux only (production builds)' + + doLast { + def cfCliVersion = '8.7.10' + def resourcesDir = file("$projectDir/src/main/resources/cf-cli") + resourcesDir.mkdirs() + + def download = [os: 'linux', url: "https://packages.cloudfoundry.org/stable?release=linux64-binary&version=${cfCliVersion}&source=github-rel", ext: 'tgz', executable: 'cf'] + + def osDir = file("${resourcesDir}/${download.os}") + osDir.mkdirs() + + def archiveFile = file("${osDir}/cf-cli.${download.ext}") + + if (!archiveFile.exists()) { + println "Downloading CF CLI for Linux..." + ant.get(src: download.url, dest: archiveFile, verbose: true) + + println "Extracting CF CLI for Linux..." + copy { + from tarTree(resources.gzip(archiveFile)) + into osDir + } + + archiveFile.delete() + println "CF CLI for Linux downloaded and extracted successfully" + } else { + println "CF CLI for Linux already exists, skipping download" + } + } +} + // Ensure CF CLI is downloaded before building +// Use downloadCfCli for local development (all platforms) +// Use downloadCfCliLinuxOnly for production Tanzu builds (Linux only) processResources.dependsOn downloadCfCli diff --git a/src/main/java/com/cfdeployer/controller/CfDeployController.java b/src/main/java/com/cfdeployer/controller/CfDeployController.java index 04ac556..7d53341 100644 --- a/src/main/java/com/cfdeployer/controller/CfDeployController.java +++ b/src/main/java/com/cfdeployer/controller/CfDeployController.java @@ -11,10 +11,7 @@ import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestPart; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; @RestController @@ -60,6 +57,67 @@ public class CfDeployController { } } + @PostMapping("/apps") + public ResponseEntity listApps(@RequestBody String requestJson) { + try { + log.info("Received request to list apps"); + CfDeployRequest request = objectMapper.readValue(requestJson, CfDeployRequest.class); + String output = cfCliService.listApps(request); + return ResponseEntity.ok(output); + } catch (Exception e) { + log.error("Error listing apps", e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("Failed to list apps: " + e.getMessage()); + } + } + + @PostMapping("/routes") + public ResponseEntity listRoutes(@RequestBody String requestJson) { + try { + log.info("Received request to list routes"); + CfDeployRequest request = objectMapper.readValue(requestJson, CfDeployRequest.class); + String output = cfCliService.listRoutes(request); + return ResponseEntity.ok(output); + } catch (Exception e) { + log.error("Error listing routes", e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("Failed to list routes: " + e.getMessage()); + } + } + + @PostMapping("/app/{appName}") + public ResponseEntity getAppDetails( + @PathVariable String appName, + @RequestBody String requestJson) { + try { + log.info("Received request to get details for app: {}", appName); + CfDeployRequest request = objectMapper.readValue(requestJson, CfDeployRequest.class); + String output = cfCliService.getAppDetails(request, appName); + return ResponseEntity.ok(output); + } catch (Exception e) { + log.error("Error getting app details for: {}", appName, e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("Failed to get app details: " + e.getMessage()); + } + } + + @PostMapping("/logs/{appName}") + public ResponseEntity getAppLogs( + @PathVariable String appName, + @RequestParam(defaultValue = "true") boolean recent, + @RequestBody String requestJson) { + try { + log.info("Received request to get {} logs for app: {}", recent ? "recent" : "tail", appName); + CfDeployRequest request = objectMapper.readValue(requestJson, CfDeployRequest.class); + String output = cfCliService.getAppLogs(request, appName, recent); + return ResponseEntity.ok(output); + } catch (Exception e) { + log.error("Error getting logs for: {}", appName, e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("Failed to get app logs: " + e.getMessage()); + } + } + private void validateFiles(MultipartFile jarFile, MultipartFile manifest) { if (jarFile.isEmpty()) { throw new IllegalArgumentException("JAR file is empty"); diff --git a/src/main/java/com/cfdeployer/service/CfCliService.java b/src/main/java/com/cfdeployer/service/CfCliService.java index 1879ee8..ffd47b5 100644 --- a/src/main/java/com/cfdeployer/service/CfCliService.java +++ b/src/main/java/com/cfdeployer/service/CfCliService.java @@ -39,28 +39,41 @@ public class CfCliService { public CfDeployResponse deployApplication(CfDeployRequest request, MultipartFile jarFile, MultipartFile manifest) { Path tempDir = null; try { + log.info("=== Starting deployment for app: {} ===", request.getAppName()); + log.info("Target: {}/{}/{}", request.getApiEndpoint(), request.getOrganization(), request.getSpace()); + tempDir = Files.createTempDirectory("cf-deploy-"); log.info("Created temporary directory: {}", tempDir); Path jarPath = tempDir.resolve(jarFile.getOriginalFilename()); Path manifestPath = tempDir.resolve("manifest.yml"); + log.debug("Copying uploaded files - JAR: {}, Manifest: {}", jarFile.getOriginalFilename(), manifest.getOriginalFilename()); + log.debug("JAR file size: {} bytes", jarFile.getSize()); + log.debug("Manifest file size: {} bytes", manifest.getSize()); + Files.copy(jarFile.getInputStream(), jarPath, StandardCopyOption.REPLACE_EXISTING); Files.copy(manifest.getInputStream(), manifestPath, StandardCopyOption.REPLACE_EXISTING); - log.info("Copied JAR and manifest files to temporary directory"); + log.info("Successfully copied JAR: {} ({} bytes) to {}", jarFile.getOriginalFilename(), jarFile.getSize(), jarPath); + log.info("Successfully copied manifest to {}", manifestPath); + log.debug("JAR absolute path: {}", jarPath.toAbsolutePath()); + log.debug("JAR file exists: {}", Files.exists(jarPath)); + log.debug("JAR file is readable: {}", Files.isReadable(jarPath)); StringBuilder output = new StringBuilder(); login(request, output); - pushApplication(request, tempDir, output); + pushApplication(request, tempDir, jarPath, output); logout(output); - log.info("Deployment completed successfully for app: {}", request.getAppName()); + log.info("=== Deployment completed successfully for app: {} ===", request.getAppName()); return CfDeployResponse.success(output.toString()); } catch (Exception e) { - log.error("Deployment failed for app: {}", request.getAppName(), e); + log.error("=== Deployment failed for app: {} ===", request.getAppName()); + log.error("Error type: {}", e.getClass().getName()); + log.error("Error message: {}", e.getMessage(), e); return CfDeployResponse.failure(e.getMessage(), e.toString()); } finally { if (tempDir != null) { @@ -94,7 +107,7 @@ public class CfCliService { log.info("Successfully logged into Cloud Foundry"); } - private void pushApplication(CfDeployRequest request, Path workingDir, StringBuilder output) throws Exception { + private void pushApplication(CfDeployRequest request, Path workingDir, Path jarPath, StringBuilder output) throws Exception { log.info("Pushing application: {}", request.getAppName()); List command = new ArrayList<>(); @@ -103,6 +116,8 @@ public class CfCliService { command.add(request.getAppName()); command.add("-f"); command.add("manifest.yml"); + command.add("-p"); + command.add(jarPath.toAbsolutePath().toString()); executeCommand(command, output, false, workingDir.toFile()); log.info("Successfully pushed application: {}", request.getAppName()); @@ -127,6 +142,7 @@ public class CfCliService { ProcessBuilder processBuilder = new ProcessBuilder(command); if (workingDir != null) { processBuilder.directory(workingDir); + log.debug("Working directory: {}", workingDir.getAbsolutePath()); } processBuilder.redirectErrorStream(true); @@ -137,50 +153,70 @@ public class CfCliService { maskedCommand.set(i + 1, "********"); } } - log.debug("Executing command: {}", String.join(" ", maskedCommand)); + log.info("Executing CF CLI command: {}", String.join(" ", maskedCommand)); } else { - log.debug("Executing command: {}", String.join(" ", command)); + log.info("Executing CF CLI command: {}", String.join(" ", command)); } + long startTime = System.currentTimeMillis(); Process process = processBuilder.start(); + log.debug("Process started with PID: {}", process.pid()); try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { String line; + int lineCount = 0; while ((line = reader.readLine()) != null) { + lineCount++; output.append(line).append("\n"); - log.debug("CF CLI output: {}", line); + log.info("CF CLI: {}", line); } + log.debug("Command produced {} lines of output", lineCount); } boolean finished = process.waitFor(timeout, TimeUnit.SECONDS); + long duration = System.currentTimeMillis() - startTime; + if (!finished) { + log.error("Command execution timed out after {} seconds", timeout); process.destroyForcibly(); throw new RuntimeException("Command execution timed out after " + timeout + " seconds"); } int exitCode = process.exitValue(); + log.info("Command completed in {}ms with exit code: {}", duration, exitCode); + if (exitCode != 0) { + log.error("Command failed with exit code: {}", exitCode); + log.error("Command output:\n{}", output.toString()); throw new RuntimeException("Command failed with exit code: " + exitCode); } } private String getCfCliExecutable() throws IOException { if (cfCliPath != null && !cfCliPath.isEmpty()) { + log.info("Using custom CF CLI path: {}", cfCliPath); return cfCliPath; } String os = getOperatingSystem(); String executable = os.equals("windows") ? "cf.exe" : "cf"; + log.info("Detected operating system: {}", os); + log.debug("CF CLI executable name: {}", executable); + String resourcePath = String.format("/cf-cli/%s/%s", os, executable); File tempFile = File.createTempFile("cf-cli-", os.equals("windows") ? ".exe" : ""); tempFile.deleteOnExit(); + log.debug("Extracting CF CLI from resource path: {}", resourcePath); + try (var inputStream = getClass().getResourceAsStream(resourcePath)) { if (inputStream == null) { - throw new IOException("CF CLI binary not found for OS: " + os); + log.error("CF CLI binary not found in resources for OS: {}. Expected path: {}", os, resourcePath); + throw new IOException("CF CLI binary not found for OS: " + os + " at path: " + resourcePath); } Files.copy(inputStream, tempFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + log.debug("CF CLI extracted to temporary file: {}", tempFile.getAbsolutePath()); } if (!os.equals("windows")) { @@ -189,14 +225,16 @@ public class CfCliService { perms.add(PosixFilePermission.OWNER_WRITE); perms.add(PosixFilePermission.OWNER_EXECUTE); Files.setPosixFilePermissions(tempFile.toPath(), perms); + log.debug("Set executable permissions on CF CLI binary"); } - log.debug("Using CF CLI executable: {}", tempFile.getAbsolutePath()); + log.info("Using CF CLI executable: {}", tempFile.getAbsolutePath()); return tempFile.getAbsolutePath(); } private String getOperatingSystem() { String osName = System.getProperty("os.name").toLowerCase(); + log.debug("System OS name property: {}", osName); if (osName.contains("win")) { return "windows"; } else if (osName.contains("mac")) { @@ -222,4 +260,97 @@ public class CfCliService { log.warn("Failed to clean up temporary directory: {}", tempDir, e); } } + + public String listApps(CfDeployRequest request) { + try { + log.info("Listing applications for org: {}, space: {}", request.getOrganization(), request.getSpace()); + StringBuilder output = new StringBuilder(); + + login(request, output); + + List command = new ArrayList<>(); + command.add(getCfCliExecutable()); + command.add("apps"); + + executeCommand(command, output, false); + logout(output); + + log.info("Successfully retrieved apps list"); + return output.toString(); + } catch (Exception e) { + log.error("Failed to list apps", e); + throw new RuntimeException("Failed to list apps: " + e.getMessage(), e); + } + } + + public String listRoutes(CfDeployRequest request) { + try { + log.info("Listing routes for org: {}, space: {}", request.getOrganization(), request.getSpace()); + StringBuilder output = new StringBuilder(); + + login(request, output); + + List command = new ArrayList<>(); + command.add(getCfCliExecutable()); + command.add("routes"); + + executeCommand(command, output, false); + logout(output); + + log.info("Successfully retrieved routes list"); + return output.toString(); + } catch (Exception e) { + log.error("Failed to list routes", e); + throw new RuntimeException("Failed to list routes: " + e.getMessage(), e); + } + } + + public String getAppDetails(CfDeployRequest request, String appName) { + try { + log.info("Getting details for app: {} in org: {}, space: {}", appName, request.getOrganization(), request.getSpace()); + StringBuilder output = new StringBuilder(); + + login(request, output); + + List command = new ArrayList<>(); + command.add(getCfCliExecutable()); + command.add("app"); + command.add(appName); + + executeCommand(command, output, false); + logout(output); + + log.info("Successfully retrieved app details for: {}", appName); + return output.toString(); + } catch (Exception e) { + log.error("Failed to get app details for: {}", appName, e); + throw new RuntimeException("Failed to get app details: " + e.getMessage(), e); + } + } + + public String getAppLogs(CfDeployRequest request, String appName, boolean recent) { + try { + log.info("Getting {} logs for app: {}", recent ? "recent" : "tail", appName); + StringBuilder output = new StringBuilder(); + + login(request, output); + + List command = new ArrayList<>(); + command.add(getCfCliExecutable()); + command.add("logs"); + command.add(appName); + if (recent) { + command.add("--recent"); + } + + executeCommand(command, output, false); + logout(output); + + log.info("Successfully retrieved logs for: {}", appName); + return output.toString(); + } catch (Exception e) { + log.error("Failed to get logs for: {}", appName, e); + throw new RuntimeException("Failed to get app logs: " + e.getMessage(), e); + } + } }