initial commit
This commit is contained in:
12
src/main/java/com/cfdeployer/CfDeployerApplication.java
Normal file
12
src/main/java/com/cfdeployer/CfDeployerApplication.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
36
src/main/java/com/cfdeployer/model/CfDeployRequest.java
Normal file
36
src/main/java/com/cfdeployer/model/CfDeployRequest.java
Normal 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;
|
||||
}
|
||||
40
src/main/java/com/cfdeployer/model/CfDeployResponse.java
Normal file
40
src/main/java/com/cfdeployer/model/CfDeployResponse.java
Normal 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();
|
||||
}
|
||||
}
|
||||
215
src/main/java/com/cfdeployer/service/CfCliService.java
Normal file
215
src/main/java/com/cfdeployer/service/CfCliService.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
24
src/main/resources/application.yml
Normal file
24
src/main/resources/application.yml
Normal 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"
|
||||
Reference in New Issue
Block a user