initial commit

This commit is contained in:
pratik
2025-10-20 11:07:35 -05:00
commit 0dae94011c
14 changed files with 1136 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
package com.cfdeployer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class CfDeployerApplication {
public static void main(String[] args) {
SpringApplication.run(CfDeployerApplication.class, args);
}
}

View File

@@ -0,0 +1,83 @@
package com.cfdeployer.controller;
import com.cfdeployer.model.CfDeployRequest;
import com.cfdeployer.model.CfDeployResponse;
import com.cfdeployer.service.CfCliService;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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.multipart.MultipartFile;
@Slf4j
@RestController
@RequestMapping("/api/cf")
@RequiredArgsConstructor
public class CfDeployController {
private final CfCliService cfCliService;
private final ObjectMapper objectMapper;
@PostMapping(value = "/deploy", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<CfDeployResponse> deploy(
@RequestPart("request") String requestJson,
@RequestPart("jarFile") MultipartFile jarFile,
@RequestPart("manifest") MultipartFile manifest) {
try {
log.info("Received deployment request");
CfDeployRequest request = objectMapper.readValue(requestJson, CfDeployRequest.class);
log.info("Deploying application: {} to org: {} space: {}",
request.getAppName(), request.getOrganization(), request.getSpace());
validateFiles(jarFile, manifest);
CfDeployResponse response = cfCliService.deployApplication(request, jarFile, manifest);
if (Boolean.TRUE.equals(response.getSuccess())) {
return ResponseEntity.ok(response);
} else {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
}
} catch (Exception e) {
log.error("Error processing deployment request", e);
CfDeployResponse errorResponse = CfDeployResponse.failure(
"Failed to process deployment request: " + e.getMessage(),
e.toString()
);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
}
}
private void validateFiles(MultipartFile jarFile, MultipartFile manifest) {
if (jarFile.isEmpty()) {
throw new IllegalArgumentException("JAR file is empty");
}
if (manifest.isEmpty()) {
throw new IllegalArgumentException("Manifest file is empty");
}
String jarFileName = jarFile.getOriginalFilename();
if (jarFileName == null || !jarFileName.toLowerCase().endsWith(".jar")) {
throw new IllegalArgumentException("Invalid JAR file. File must have .jar extension");
}
String manifestFileName = manifest.getOriginalFilename();
if (manifestFileName == null ||
(!manifestFileName.toLowerCase().endsWith(".yml") && !manifestFileName.toLowerCase().endsWith(".yaml"))) {
throw new IllegalArgumentException("Invalid manifest file. File must have .yml or .yaml extension");
}
log.debug("File validation successful - JAR: {}, Manifest: {}", jarFileName, manifestFileName);
}
}

View File

@@ -0,0 +1,79 @@
package com.cfdeployer.exception;
import com.cfdeployer.model.CfDeployResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<CfDeployResponse> handleValidationExceptions(MethodArgumentNotValidException ex) {
log.error("Validation error occurred", ex);
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach((error) -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
String errorMessage = errors.entrySet().stream()
.map(entry -> entry.getKey() + ": " + entry.getValue())
.collect(Collectors.joining(", "));
CfDeployResponse response = CfDeployResponse.failure(
"Validation failed: " + errorMessage,
ex.getMessage()
);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
}
@ExceptionHandler(MaxUploadSizeExceededException.class)
public ResponseEntity<CfDeployResponse> handleMaxUploadSizeExceededException(MaxUploadSizeExceededException ex) {
log.error("File size exceeded maximum allowed size", ex);
CfDeployResponse response = CfDeployResponse.failure(
"File size exceeded maximum allowed size. Please ensure your files are within the 500MB limit.",
ex.getMessage()
);
return ResponseEntity.status(HttpStatus.PAYLOAD_TOO_LARGE).body(response);
}
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<CfDeployResponse> handleIllegalArgumentException(IllegalArgumentException ex) {
log.error("Invalid argument provided", ex);
CfDeployResponse response = CfDeployResponse.failure(
ex.getMessage(),
ex.toString()
);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<CfDeployResponse> handleGenericException(Exception ex) {
log.error("Unexpected error occurred", ex);
CfDeployResponse response = CfDeployResponse.failure(
"An unexpected error occurred: " + ex.getMessage(),
ex.toString()
);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
}
}

View File

@@ -0,0 +1,36 @@
package com.cfdeployer.model;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CfDeployRequest {
@NotBlank(message = "API endpoint is required")
private String apiEndpoint;
@NotBlank(message = "Username is required")
private String username;
@NotBlank(message = "Password is required")
private String password;
@NotBlank(message = "Organization is required")
private String organization;
@NotBlank(message = "Space is required")
private String space;
@NotBlank(message = "Application name is required")
private String appName;
@NotNull(message = "Skip SSL validation flag is required")
private Boolean skipSslValidation;
}

View File

@@ -0,0 +1,40 @@
package com.cfdeployer.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.UUID;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CfDeployResponse {
private Boolean success;
private String message;
private String deploymentId;
private String output;
private String error;
public static CfDeployResponse success(String output) {
return CfDeployResponse.builder()
.success(true)
.message("Application deployed successfully")
.deploymentId(UUID.randomUUID().toString())
.output(output)
.build();
}
public static CfDeployResponse failure(String error, String output) {
return CfDeployResponse.builder()
.success(false)
.message("Application deployment failed")
.deploymentId(UUID.randomUUID().toString())
.error(error)
.output(output)
.build();
}
}

View File

@@ -0,0 +1,215 @@
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<String> 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<String> 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<String> 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<String> command, StringBuilder output, boolean maskPassword) throws Exception {
executeCommand(command, output, maskPassword, null);
}
private void executeCommand(List<String> 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<String> 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<PosixFilePermission> 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);
}
}
}

View File

@@ -0,0 +1,24 @@
server:
port: 8080
spring:
application:
name: cf-deployer
servlet:
multipart:
max-file-size: 500MB
max-request-size: 500MB
enabled: true
cf:
cli:
timeout: 600
path:
logging:
level:
root: INFO
com.cfdeployer: DEBUG
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} - %msg%n"
file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"