package com.cfdeployer.service; import com.cfdeployer.model.CfDeployRequest; import com.cfdeployer.model.CfDeployResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.FileUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.nio.file.attribute.PosixFilePermission; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; @Slf4j @Service @RequiredArgsConstructor public class CfCliService { @Value("${cf.cli.timeout:600}") private long timeout; @Value("${cf.cli.path:}") private String cfCliPath; public CfDeployResponse deployApplication(CfDeployRequest request, MultipartFile jarFile, MultipartFile manifest) { Path tempDir = null; try { tempDir = Files.createTempDirectory("cf-deploy-"); log.info("Created temporary directory: {}", tempDir); Path jarPath = tempDir.resolve(jarFile.getOriginalFilename()); Path manifestPath = tempDir.resolve("manifest.yml"); 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"); StringBuilder output = new StringBuilder(); login(request, output); pushApplication(request, tempDir, output); logout(output); 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); return CfDeployResponse.failure(e.getMessage(), e.toString()); } finally { if (tempDir != null) { cleanupTempDirectory(tempDir); } } } private void login(CfDeployRequest request, StringBuilder output) throws Exception { log.info("Logging into Cloud Foundry at: {}", request.getApiEndpoint()); List command = new ArrayList<>(); command.add(getCfCliExecutable()); command.add("login"); command.add("-a"); command.add(request.getApiEndpoint()); command.add("-u"); command.add(request.getUsername()); command.add("-p"); command.add(request.getPassword()); command.add("-o"); command.add(request.getOrganization()); command.add("-s"); command.add(request.getSpace()); if (Boolean.TRUE.equals(request.getSkipSslValidation())) { command.add("--skip-ssl-validation"); } executeCommand(command, output, true); log.info("Successfully logged into Cloud Foundry"); } private void pushApplication(CfDeployRequest request, Path workingDir, StringBuilder output) throws Exception { log.info("Pushing application: {}", request.getAppName()); List command = new ArrayList<>(); command.add(getCfCliExecutable()); command.add("push"); command.add(request.getAppName()); command.add("-f"); command.add("manifest.yml"); executeCommand(command, output, false, workingDir.toFile()); log.info("Successfully pushed application: {}", request.getAppName()); } private void logout(StringBuilder output) throws Exception { log.info("Logging out from Cloud Foundry"); List command = new ArrayList<>(); command.add(getCfCliExecutable()); command.add("logout"); executeCommand(command, output, false); log.info("Successfully logged out from Cloud Foundry"); } private void executeCommand(List command, StringBuilder output, boolean maskPassword) throws Exception { executeCommand(command, output, maskPassword, null); } private void executeCommand(List command, StringBuilder output, boolean maskPassword, File workingDir) throws Exception { ProcessBuilder processBuilder = new ProcessBuilder(command); if (workingDir != null) { processBuilder.directory(workingDir); } processBuilder.redirectErrorStream(true); if (maskPassword) { List maskedCommand = new ArrayList<>(command); for (int i = 0; i < maskedCommand.size(); i++) { if ("-p".equals(maskedCommand.get(i)) && i + 1 < maskedCommand.size()) { maskedCommand.set(i + 1, "********"); } } log.debug("Executing command: {}", String.join(" ", maskedCommand)); } else { log.debug("Executing command: {}", String.join(" ", command)); } Process process = processBuilder.start(); try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { String line; while ((line = reader.readLine()) != null) { output.append(line).append("\n"); log.debug("CF CLI output: {}", line); } } boolean finished = process.waitFor(timeout, TimeUnit.SECONDS); if (!finished) { process.destroyForcibly(); throw new RuntimeException("Command execution timed out after " + timeout + " seconds"); } int exitCode = process.exitValue(); if (exitCode != 0) { throw new RuntimeException("Command failed with exit code: " + exitCode); } } private String getCfCliExecutable() throws IOException { if (cfCliPath != null && !cfCliPath.isEmpty()) { return cfCliPath; } String os = getOperatingSystem(); String executable = os.equals("windows") ? "cf.exe" : "cf"; String resourcePath = String.format("/cf-cli/%s/%s", os, executable); File tempFile = File.createTempFile("cf-cli-", os.equals("windows") ? ".exe" : ""); tempFile.deleteOnExit(); try (var inputStream = getClass().getResourceAsStream(resourcePath)) { if (inputStream == null) { throw new IOException("CF CLI binary not found for OS: " + os); } Files.copy(inputStream, tempFile.toPath(), StandardCopyOption.REPLACE_EXISTING); } if (!os.equals("windows")) { Set perms = new HashSet<>(); perms.add(PosixFilePermission.OWNER_READ); perms.add(PosixFilePermission.OWNER_WRITE); perms.add(PosixFilePermission.OWNER_EXECUTE); Files.setPosixFilePermissions(tempFile.toPath(), perms); } log.debug("Using CF CLI executable: {}", tempFile.getAbsolutePath()); return tempFile.getAbsolutePath(); } private String getOperatingSystem() { String osName = System.getProperty("os.name").toLowerCase(); if (osName.contains("win")) { return "windows"; } else if (osName.contains("mac")) { return "macos"; } else { return "linux"; } } private void cleanupTempDirectory(Path tempDir) { try { FileUtils.deleteDirectory(tempDir.toFile()); log.debug("Cleaned up temporary directory: {}", tempDir); } catch (IOException e) { log.warn("Failed to clean up temporary directory: {}", tempDir, e); } } }