From 7426b617fa94c324824c101acf2a01ddebd8a7a6 Mon Sep 17 00:00:00 2001 From: WangLiNaruto Date: Wed, 30 Aug 2023 10:27:36 +0800 Subject: [PATCH] erraform-boot new deploy and destroy service with script file contents in request body --- .../boot/api/TerraformApiController.java | 84 +++-- .../TerraformExecutorException.java | 4 + ... TerraformDeployFromDirectoryRequest.java} | 6 +- .../TerraformDeployWithScriptsRequest.java | 23 ++ ...TerraformDestroyFromDirectoryRequest.java} | 6 +- .../TerraformDestroyWithScriptsRequest.java | 22 ++ ...aformAsyncDeployFromDirectoryRequest.java} | 4 +- ...formAsyncDestroyFromDirectoryRequest.java} | 4 +- .../boot/models/response/TerraformResult.java | 3 + .../boot/terraform/TerraformExecutor.java | 307 +++++++++++++++--- 10 files changed, 389 insertions(+), 74 deletions(-) rename src/main/java/org/eclipse/xpanse/terraform/boot/models/request/{TerraformDeployRequest.java => TerraformDeployFromDirectoryRequest.java} (86%) create mode 100644 src/main/java/org/eclipse/xpanse/terraform/boot/models/request/TerraformDeployWithScriptsRequest.java rename src/main/java/org/eclipse/xpanse/terraform/boot/models/request/{TerraformDestroyRequest.java => TerraformDestroyFromDirectoryRequest.java} (84%) create mode 100644 src/main/java/org/eclipse/xpanse/terraform/boot/models/request/TerraformDestroyWithScriptsRequest.java rename src/main/java/org/eclipse/xpanse/terraform/boot/models/request/async/{TerraformAsyncDeployRequest.java => TerraformAsyncDeployFromDirectoryRequest.java} (80%) rename src/main/java/org/eclipse/xpanse/terraform/boot/models/request/async/{TerraformAsyncDestroyRequest.java => TerraformAsyncDestroyFromDirectoryRequest.java} (80%) diff --git a/src/main/java/org/eclipse/xpanse/terraform/boot/api/TerraformApiController.java b/src/main/java/org/eclipse/xpanse/terraform/boot/api/TerraformApiController.java index 9686f06..4b6eed0 100644 --- a/src/main/java/org/eclipse/xpanse/terraform/boot/api/TerraformApiController.java +++ b/src/main/java/org/eclipse/xpanse/terraform/boot/api/TerraformApiController.java @@ -11,10 +11,12 @@ import jakarta.validation.Valid; import lombok.extern.slf4j.Slf4j; import org.eclipse.xpanse.terraform.boot.models.TerraformBootSystemStatus; -import org.eclipse.xpanse.terraform.boot.models.request.TerraformDeployRequest; -import org.eclipse.xpanse.terraform.boot.models.request.TerraformDestroyRequest; -import org.eclipse.xpanse.terraform.boot.models.request.async.TerraformAsyncDeployRequest; -import org.eclipse.xpanse.terraform.boot.models.request.async.TerraformAsyncDestroyRequest; +import org.eclipse.xpanse.terraform.boot.models.request.TerraformDeployFromDirectoryRequest; +import org.eclipse.xpanse.terraform.boot.models.request.TerraformDeployWithScriptsRequest; +import org.eclipse.xpanse.terraform.boot.models.request.TerraformDestroyFromDirectoryRequest; +import org.eclipse.xpanse.terraform.boot.models.request.TerraformDestroyWithScriptsRequest; +import org.eclipse.xpanse.terraform.boot.models.request.async.TerraformAsyncDeployFromDirectoryRequest; +import org.eclipse.xpanse.terraform.boot.models.request.async.TerraformAsyncDestroyFromDirectoryRequest; import org.eclipse.xpanse.terraform.boot.models.response.TerraformResult; import org.eclipse.xpanse.terraform.boot.models.validation.TerraformValidationResult; import org.eclipse.xpanse.terraform.boot.terraform.TerraformExecutor; @@ -89,8 +91,10 @@ public TerraformResult deploy( @Parameter(name = "module_directory", description = "directory name where the Terraform module files exist.") @PathVariable("module_directory") String moduleDirectory, - @Valid @RequestBody TerraformDeployRequest terraformDeployRequest) { - return this.terraformExecutor.deploy(terraformDeployRequest, moduleDirectory); + @Valid @RequestBody + TerraformDeployFromDirectoryRequest terraformDeployFromDirectoryRequest) { + return this.terraformExecutor.deployFromDirectory(terraformDeployFromDirectoryRequest, + moduleDirectory); } /** @@ -107,41 +111,77 @@ public TerraformResult destroy( @Parameter(name = "module_directory", description = "directory name where the Terraform module files exist.") @PathVariable("module_directory") String moduleDirectory, - @Valid @RequestBody TerraformDestroyRequest terraformDestroyRequest) { - return this.terraformExecutor.destroy(terraformDestroyRequest, moduleDirectory); + @Valid @RequestBody + TerraformDestroyFromDirectoryRequest terraformDestroyFromDirectoryRequest) { + return this.terraformExecutor.destroyFromDirectory(terraformDestroyFromDirectoryRequest, + moduleDirectory); } /** - * Method to async deploy resources requested in a workspace. + * Method to deploy resources by scripts. * + * @return Returns the status of the deployment. */ @Tag(name = "Terraform", description = "APIs for running Terraform commands") - @Operation(description = "async deploy resources via Terraform") - @PostMapping(value = "/deploy/async/{module_directory}", produces = + @Operation(description = "Deploy resources via Terraform") + @PostMapping(value = "/deploy/scripts/{module_directory}", produces = MediaType.APPLICATION_JSON_VALUE) - @ResponseStatus(HttpStatus.ACCEPTED) - public void asyncDeploy( + @ResponseStatus(HttpStatus.OK) + public TerraformResult deployWithScripts( @Parameter(name = "module_directory", description = "directory name where the Terraform module files exist.") @PathVariable("module_directory") String moduleDirectory, - @Valid @RequestBody TerraformAsyncDeployRequest asyncDeployRequest) { - terraformExecutor.asyncDeploy(asyncDeployRequest, moduleDirectory); + @Valid @RequestBody TerraformDeployWithScriptsRequest request) { + return terraformExecutor.deployWithScripts(request, moduleDirectory); } /** - * Method to async destroy resources requested in a workspace. + * Method to destroy resources by scripts. * + * @return Returns the status of the destroy. + */ + @Tag(name = "Terraform", description = "APIs for running Terraform commands") + @Operation(description = "Deploy resources via Terraform") + @PostMapping(value = "/destroy/scripts/{module_directory}", produces = + MediaType.APPLICATION_JSON_VALUE) + @ResponseStatus(HttpStatus.OK) + public TerraformResult destroyWithScripts( + @Parameter(name = "module_directory", + description = "directory name where the Terraform module files exist.") + @PathVariable("module_directory") String moduleDirectory, + @Valid @RequestBody TerraformDestroyWithScriptsRequest request) { + return terraformExecutor.destroyWithScripts(request, moduleDirectory); + } + + /** + * Method to async deploy resources requested in a workspace. + */ + @Tag(name = "Terraform", description = "APIs for running Terraform commands") + @Operation(description = "async deploy resources via Terraform") + @PostMapping(value = "/deploy/scripts/async/{module_directory}", produces = + MediaType.APPLICATION_JSON_VALUE) + @ResponseStatus(HttpStatus.ACCEPTED) + public void asyncDeployWithScripts( + @Parameter(name = "module_directory", description = "directory name where the " + + "Terraform module files exist.") + @PathVariable("module_directory") String moduleDirectory, + @Valid @RequestBody TerraformAsyncDeployFromDirectoryRequest asyncDeployRequest) { + terraformExecutor.asyncDeployWithScripts(asyncDeployRequest, moduleDirectory); + } + + /** + * Method to async destroy resources requested in a workspace. */ @Tag(name = "Terraform", description = "APIs for running Terraform commands") @Operation(description = "Async destroy the Terraform modules") - @DeleteMapping(value = "/destroy/async/{module_directory}", + @DeleteMapping(value = "/destroy/scripts/async/{module_directory}", produces = MediaType.APPLICATION_JSON_VALUE) @ResponseStatus(HttpStatus.ACCEPTED) - public void asyncDestroy( - @Parameter(name = "module_directory", - description = "directory name where the Terraform module files exist.") + public void asyncDestroyWithScripts( + @Parameter(name = "module_directory", description = "directory name where the " + + "Terraform module files exist.") @PathVariable("module_directory") String moduleDirectory, - @Valid @RequestBody TerraformAsyncDestroyRequest asyncDestroyRequest) { - terraformExecutor.asyncDestroy(asyncDestroyRequest, moduleDirectory); + @Valid @RequestBody TerraformAsyncDestroyFromDirectoryRequest asyncDestroyRequest) { + terraformExecutor.asyncDestroyWithScripts(asyncDestroyRequest, moduleDirectory); } } diff --git a/src/main/java/org/eclipse/xpanse/terraform/boot/models/exceptions/TerraformExecutorException.java b/src/main/java/org/eclipse/xpanse/terraform/boot/models/exceptions/TerraformExecutorException.java index fcaaa61..9488433 100644 --- a/src/main/java/org/eclipse/xpanse/terraform/boot/models/exceptions/TerraformExecutorException.java +++ b/src/main/java/org/eclipse/xpanse/terraform/boot/models/exceptions/TerraformExecutorException.java @@ -17,4 +17,8 @@ public TerraformExecutorException(String message) { public TerraformExecutorException(String message, String output) { super("Executor Exception:" + message + System.lineSeparator() + output); } + + public TerraformExecutorException(String message, Throwable ex) { + super(message, ex); + } } diff --git a/src/main/java/org/eclipse/xpanse/terraform/boot/models/request/TerraformDeployRequest.java b/src/main/java/org/eclipse/xpanse/terraform/boot/models/request/TerraformDeployFromDirectoryRequest.java similarity index 86% rename from src/main/java/org/eclipse/xpanse/terraform/boot/models/request/TerraformDeployRequest.java rename to src/main/java/org/eclipse/xpanse/terraform/boot/models/request/TerraformDeployFromDirectoryRequest.java index d592e45..cd41089 100644 --- a/src/main/java/org/eclipse/xpanse/terraform/boot/models/request/TerraformDeployRequest.java +++ b/src/main/java/org/eclipse/xpanse/terraform/boot/models/request/TerraformDeployFromDirectoryRequest.java @@ -15,7 +15,11 @@ * Data model for the terraform deploy requests. */ @Data -public class TerraformDeployRequest { +public class TerraformDeployFromDirectoryRequest { + + @NotNull + @Schema(description = "Task ID for deployment requests deployed via script") + private String taskId; @NotNull @Schema(description = "Flag to control if the deployment must only generate the terraform " diff --git a/src/main/java/org/eclipse/xpanse/terraform/boot/models/request/TerraformDeployWithScriptsRequest.java b/src/main/java/org/eclipse/xpanse/terraform/boot/models/request/TerraformDeployWithScriptsRequest.java new file mode 100644 index 0000000..30e801c --- /dev/null +++ b/src/main/java/org/eclipse/xpanse/terraform/boot/models/request/TerraformDeployWithScriptsRequest.java @@ -0,0 +1,23 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: Huawei Inc. + * + */ + +package org.eclipse.xpanse.terraform.boot.models.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * Terraform uses the request object deployed by the script. + */ +@Data +public class TerraformDeployWithScriptsRequest extends TerraformDeployFromDirectoryRequest { + + @NotNull + @Schema(description = "List of script files for deployment requests deployed via scripts") + private String files; + +} diff --git a/src/main/java/org/eclipse/xpanse/terraform/boot/models/request/TerraformDestroyRequest.java b/src/main/java/org/eclipse/xpanse/terraform/boot/models/request/TerraformDestroyFromDirectoryRequest.java similarity index 84% rename from src/main/java/org/eclipse/xpanse/terraform/boot/models/request/TerraformDestroyRequest.java rename to src/main/java/org/eclipse/xpanse/terraform/boot/models/request/TerraformDestroyFromDirectoryRequest.java index 1662005..d4f5b9b 100644 --- a/src/main/java/org/eclipse/xpanse/terraform/boot/models/request/TerraformDestroyRequest.java +++ b/src/main/java/org/eclipse/xpanse/terraform/boot/models/request/TerraformDestroyFromDirectoryRequest.java @@ -17,7 +17,11 @@ * Data model for the terraform destroy requests. */ @Data -public class TerraformDestroyRequest { +public class TerraformDestroyFromDirectoryRequest { + + @NotNull + @Schema(description = "Task ID for deployment requests deployed via script") + private String taskId; @NotNull @Schema(description = "Key-value pairs of regular variables that must be used to execute the " diff --git a/src/main/java/org/eclipse/xpanse/terraform/boot/models/request/TerraformDestroyWithScriptsRequest.java b/src/main/java/org/eclipse/xpanse/terraform/boot/models/request/TerraformDestroyWithScriptsRequest.java new file mode 100644 index 0000000..a1779b9 --- /dev/null +++ b/src/main/java/org/eclipse/xpanse/terraform/boot/models/request/TerraformDestroyWithScriptsRequest.java @@ -0,0 +1,22 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: Huawei Inc. + */ + +package org.eclipse.xpanse.terraform.boot.models.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * Terraform uses the request object destroy by the script. + */ +@Data +public class TerraformDestroyWithScriptsRequest extends TerraformDestroyFromDirectoryRequest { + + @NotNull + @Schema(description = "List of script files for destroy requests deployed via scripts") + private String files; + +} diff --git a/src/main/java/org/eclipse/xpanse/terraform/boot/models/request/async/TerraformAsyncDeployRequest.java b/src/main/java/org/eclipse/xpanse/terraform/boot/models/request/async/TerraformAsyncDeployFromDirectoryRequest.java similarity index 80% rename from src/main/java/org/eclipse/xpanse/terraform/boot/models/request/async/TerraformAsyncDeployRequest.java rename to src/main/java/org/eclipse/xpanse/terraform/boot/models/request/async/TerraformAsyncDeployFromDirectoryRequest.java index 64623dd..4c15851 100644 --- a/src/main/java/org/eclipse/xpanse/terraform/boot/models/request/async/TerraformAsyncDeployRequest.java +++ b/src/main/java/org/eclipse/xpanse/terraform/boot/models/request/async/TerraformAsyncDeployFromDirectoryRequest.java @@ -8,13 +8,13 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import lombok.Data; -import org.eclipse.xpanse.terraform.boot.models.request.TerraformDeployRequest; +import org.eclipse.xpanse.terraform.boot.models.request.TerraformDeployWithScriptsRequest; /** * Data model for the terraform async deploy requests. */ @Data -public class TerraformAsyncDeployRequest extends TerraformDeployRequest { +public class TerraformAsyncDeployFromDirectoryRequest extends TerraformDeployWithScriptsRequest { @NotNull @Schema(description = "Configuration information of webhook.") diff --git a/src/main/java/org/eclipse/xpanse/terraform/boot/models/request/async/TerraformAsyncDestroyRequest.java b/src/main/java/org/eclipse/xpanse/terraform/boot/models/request/async/TerraformAsyncDestroyFromDirectoryRequest.java similarity index 80% rename from src/main/java/org/eclipse/xpanse/terraform/boot/models/request/async/TerraformAsyncDestroyRequest.java rename to src/main/java/org/eclipse/xpanse/terraform/boot/models/request/async/TerraformAsyncDestroyFromDirectoryRequest.java index 4a8daf2..abad8e8 100644 --- a/src/main/java/org/eclipse/xpanse/terraform/boot/models/request/async/TerraformAsyncDestroyRequest.java +++ b/src/main/java/org/eclipse/xpanse/terraform/boot/models/request/async/TerraformAsyncDestroyFromDirectoryRequest.java @@ -8,13 +8,13 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import lombok.Data; -import org.eclipse.xpanse.terraform.boot.models.request.TerraformDestroyRequest; +import org.eclipse.xpanse.terraform.boot.models.request.TerraformDestroyWithScriptsRequest; /** * Data model for the terraform async destroy requests. */ @Data -public class TerraformAsyncDestroyRequest extends TerraformDestroyRequest { +public class TerraformAsyncDestroyFromDirectoryRequest extends TerraformDestroyWithScriptsRequest { @NotNull @Schema(description = "Configuration information of webhook.") diff --git a/src/main/java/org/eclipse/xpanse/terraform/boot/models/response/TerraformResult.java b/src/main/java/org/eclipse/xpanse/terraform/boot/models/response/TerraformResult.java index af243ce..ccc4f3c 100644 --- a/src/main/java/org/eclipse/xpanse/terraform/boot/models/response/TerraformResult.java +++ b/src/main/java/org/eclipse/xpanse/terraform/boot/models/response/TerraformResult.java @@ -5,6 +5,7 @@ package org.eclipse.xpanse.terraform.boot.models.response; +import java.util.Map; import lombok.Builder; import lombok.Data; @@ -18,6 +19,8 @@ public class TerraformResult { private boolean isCommandSuccessful; private String commandStdOutput; private String commandStdError; + private String terraformState; + private Map importantFileContentMap; } diff --git a/src/main/java/org/eclipse/xpanse/terraform/boot/terraform/TerraformExecutor.java b/src/main/java/org/eclipse/xpanse/terraform/boot/terraform/TerraformExecutor.java index 9204912..203d1d2 100644 --- a/src/main/java/org/eclipse/xpanse/terraform/boot/terraform/TerraformExecutor.java +++ b/src/main/java/org/eclipse/xpanse/terraform/boot/terraform/TerraformExecutor.java @@ -8,13 +8,20 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; +import java.util.Iterator; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.UUID; @@ -24,10 +31,12 @@ import org.eclipse.xpanse.terraform.boot.models.enums.HealthStatus; import org.eclipse.xpanse.terraform.boot.models.exceptions.TerraformExecutorException; import org.eclipse.xpanse.terraform.boot.models.exceptions.TerraformHealthCheckException; -import org.eclipse.xpanse.terraform.boot.models.request.TerraformDeployRequest; -import org.eclipse.xpanse.terraform.boot.models.request.TerraformDestroyRequest; -import org.eclipse.xpanse.terraform.boot.models.request.async.TerraformAsyncDeployRequest; -import org.eclipse.xpanse.terraform.boot.models.request.async.TerraformAsyncDestroyRequest; +import org.eclipse.xpanse.terraform.boot.models.request.TerraformDeployFromDirectoryRequest; +import org.eclipse.xpanse.terraform.boot.models.request.TerraformDeployWithScriptsRequest; +import org.eclipse.xpanse.terraform.boot.models.request.TerraformDestroyFromDirectoryRequest; +import org.eclipse.xpanse.terraform.boot.models.request.TerraformDestroyWithScriptsRequest; +import org.eclipse.xpanse.terraform.boot.models.request.async.TerraformAsyncDeployFromDirectoryRequest; +import org.eclipse.xpanse.terraform.boot.models.request.async.TerraformAsyncDestroyFromDirectoryRequest; import org.eclipse.xpanse.terraform.boot.models.response.TerraformResult; import org.eclipse.xpanse.terraform.boot.models.validation.TerraformValidationResult; import org.eclipse.xpanse.terraform.boot.terraform.utils.SystemCmd; @@ -44,10 +53,14 @@ @Slf4j @Component public class TerraformExecutor { + private static final String STATE_FILE_NAME = "terraform.tfstate"; + private static final String FILE_SUFFIX = ".tf"; private static final String VARS_FILE_NAME = "variables.tfvars.json"; private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); private static final String TEST_FILE_NAME = "hello-world.tf"; private static final String HEALTH_CHECK_DIR = UUID.randomUUID().toString(); + private static final List EXCLUDED_FILE_SUFFIX_LIST = + Arrays.asList(".tf", ".tfstate", ".hcl"); private static final String HELLO_WORLD_TEMPLATE = """ output "hello_world" { value = "Hello, World!" @@ -82,14 +95,14 @@ public class TerraformExecutor { */ @Autowired public TerraformExecutor(SystemCmd systemCmd, RestTemplate restTemplate, - @Value("${terraform.root.module.directory}") - String moduleParentDirectoryPath, - @Value("${log.terraform.stdout.stderr:true}") - boolean isStdoutStdErrLoggingEnabled, - @Value("${terraform.binary.location}") - String customTerraformBinary, - @Value("${terraform.log.level}") - String terraformLogLevel + @Value("${terraform.root.module.directory}") + String moduleParentDirectoryPath, + @Value("${log.terraform.stdout.stderr:true}") + boolean isStdoutStdErrLoggingEnabled, + @Value("${terraform.binary.location}") + String customTerraformBinary, + @Value("${terraform.log.level}") + String terraformLogLevel ) { if (moduleParentDirectoryPath.isBlank() || moduleParentDirectoryPath.isEmpty()) { this.moduleParentDirectoryPath = @@ -120,7 +133,7 @@ private SystemCmdResult tfInitCommand(String workspace) { * @return Returns result of SystemCmd executes. */ private SystemCmdResult tfPlanCommand(Map variables, - Map envVariables, String workspace) { + Map envVariables, String workspace) { return executeWithVariables( new StringBuilder(getTerraformCommand("plan -input=false -no-color ")), variables, envVariables, workspace); @@ -132,7 +145,7 @@ private SystemCmdResult tfPlanCommand(Map variables, * @return Returns result of SystemCmd executes. */ private SystemCmdResult tfApplyCommand(Map variables, - Map envVariables, String workspace) { + Map envVariables, String workspace) { return executeWithVariables( new StringBuilder( getTerraformCommand("apply -auto-approve -input=false -no-color ")), @@ -145,7 +158,7 @@ private SystemCmdResult tfApplyCommand(Map variables, * @return Returns result of SystemCmd executes. */ private SystemCmdResult tfDestroyCommand(Map variables, - Map envVariables, String workspace) { + Map envVariables, String workspace) { return executeWithVariables( new StringBuilder("terraform destroy -auto-approve -input=false -no-color "), variables, envVariables, workspace); @@ -157,9 +170,9 @@ private SystemCmdResult tfDestroyCommand(Map variables, * @return Returns result of SystemCmd executes. */ private SystemCmdResult executeWithVariables(StringBuilder command, - Map variables, - Map envVariables, - String workspace) { + Map variables, + Map envVariables, + String workspace) { createVariablesFile(variables, workspace); command.append(" -var-file="); command.append(VARS_FILE_NAME); @@ -174,7 +187,7 @@ private SystemCmdResult executeWithVariables(StringBuilder command, * @return SystemCmdResult */ private SystemCmdResult execute(String cmd, String workspace, - @NonNull Map envVariables) { + @NonNull Map envVariables) { envVariables.putAll(getTerraformLogConfig()); return this.systemCmd.execute(cmd, workspace, this.isStdoutStdErrLoggingEnabled, envVariables); @@ -183,14 +196,14 @@ private SystemCmdResult execute(String cmd, String workspace, /** * Deploy a source by terraform. */ - public TerraformResult deploy(TerraformDeployRequest terraformDeployRequest, String workspace) { + public TerraformResult deployFromDirectory(TerraformDeployFromDirectoryRequest request, + String workspace) { + SystemCmdResult result; - if (Boolean.TRUE.equals(terraformDeployRequest.getIsPlanOnly())) { - result = tfPlan(terraformDeployRequest.getVariables(), - terraformDeployRequest.getEnvVariables(), workspace); + if (Boolean.TRUE.equals(request.getIsPlanOnly())) { + result = tfPlan(request.getVariables(), request.getEnvVariables(), workspace); } else { - result = tfApplyCommand(terraformDeployRequest.getVariables(), - terraformDeployRequest.getEnvVariables(), + result = tfApplyCommand(request.getVariables(), request.getEnvVariables(), getModuleFullPath(workspace)); if (!result.isCommandSuccessful()) { log.error("TFExecutor.tfApply failed."); @@ -202,38 +215,105 @@ public TerraformResult deploy(TerraformDeployRequest terraformDeployRequest, Str .commandStdOutput(result.getCommandStdOutput()) .commandStdError(result.getCommandStdError()) .isCommandSuccessful(result.isCommandSuccessful()) + .terraformState(getTerraformState(getModuleFullPath(workspace + + File.separator + request.getTaskId()))) + .build(); + } + + private TerraformResult executeDeploy(TerraformDeployFromDirectoryRequest request, + String moduleDirectory) { + String newModuleDirectory = moduleDirectory + File.separator + request.getTaskId(); + tfPlan(request.getVariables(), request.getEnvVariables(), + newModuleDirectory); + SystemCmdResult result = tfApplyCommand(request.getVariables(), request.getEnvVariables(), + getModuleFullPath(newModuleDirectory)); + if (!result.isCommandSuccessful()) { + log.error("TFExecutor.tfApply failed."); + throw new TerraformExecutorException("TFExecutor.tfApply failed.", + result.getCommandStdError()); + } + return TerraformResult.builder() + .commandStdOutput(result.getCommandStdOutput()) + .commandStdError(result.getCommandStdError()) + .isCommandSuccessful(result.isCommandSuccessful()) + .terraformState(getTerraformState(getModuleFullPath(newModuleDirectory))) + .importantFileContentMap( + getImportantFilesContent(getModuleFullPath(newModuleDirectory))) .build(); } /** * Destroy resource of the service. */ - public TerraformResult destroy(TerraformDestroyRequest terraformDestroyRequest, - String workspace) { - tfPlan(terraformDestroyRequest.getVariables(), terraformDestroyRequest.getEnvVariables(), - workspace); - SystemCmdResult destroyResult = - tfDestroyCommand(terraformDestroyRequest.getVariables(), - terraformDestroyRequest.getEnvVariables(), getModuleFullPath(workspace)); - if (!destroyResult.isCommandSuccessful()) { + public TerraformResult destroyFromDirectory(TerraformDestroyFromDirectoryRequest request, + String moduleDirectory) { + tfPlan(request.getVariables(), request.getEnvVariables(), moduleDirectory); + SystemCmdResult result = tfDestroyCommand(request.getVariables(), + request.getEnvVariables(), getModuleFullPath(moduleDirectory)); + if (!result.isCommandSuccessful()) { log.error("TFExecutor.tfDestroy failed."); throw new TerraformExecutorException("TFExecutor.tfDestroy failed.", - destroyResult.getCommandStdError()); + result.getCommandStdError()); } + deleteWorkspace(getModuleFullPath(moduleDirectory)); return TerraformResult.builder() - .commandStdOutput(destroyResult.getCommandStdOutput()) - .commandStdError(destroyResult.getCommandStdError()) - .isCommandSuccessful(destroyResult.isCommandSuccessful()) + .commandStdOutput(result.getCommandStdOutput()) + .commandStdError(result.getCommandStdError()) + .isCommandSuccessful(result.isCommandSuccessful()) + .terraformState(getTerraformState(getModuleFullPath(moduleDirectory))) + .importantFileContentMap( + getImportantFilesContent(getModuleFullPath(moduleDirectory))) .build(); } + /** + * Method of deployment a service using a script. + */ + public TerraformResult deployWithScripts(TerraformDeployWithScriptsRequest request, + String moduleDirectory) { + try { + buildDeployEnv(request.getTaskId(), request.getFiles(), moduleDirectory); + return executeDeploy(request, moduleDirectory); + } catch (JsonProcessingException e) { + log.error("Deployment service error, taskId: {} , error: {}", request.getTaskId(), + e.getMessage()); + return TerraformResult.builder() + .commandStdOutput(null) + .commandStdError(e.getMessage()) + .isCommandSuccessful(false) + .terraformState(null) + .build(); + } + } + + /** + * Method of destroy a service using a script. + */ + public TerraformResult destroyWithScripts(TerraformDestroyWithScriptsRequest request, + String moduleDirectory) { + try { + buildDestroyEnv(request.getTaskId(), request.getFiles(), moduleDirectory); + return destroyFromDirectory(request, moduleDirectory + File.separator + + request.getTaskId()); + } catch (JsonProcessingException e) { + log.error("Deployment service error, taskId: {} , error: {}", request.getTaskId(), + e.getMessage()); + return TerraformResult.builder() + .commandStdOutput(null) + .commandStdError(e.getMessage()) + .isCommandSuccessful(false) + .terraformState(null) + .build(); + } + } + /** * Async deploy a source by terraform. */ @Async("taskExecutor") - public void asyncDeploy(TerraformAsyncDeployRequest asyncDeployRequest, - String workspace) { - TerraformResult result = deploy(asyncDeployRequest, workspace); + public void asyncDeployWithScripts(TerraformAsyncDeployFromDirectoryRequest asyncDeployRequest, + String moduleDirectory) { + TerraformResult result = deployWithScripts(asyncDeployRequest, moduleDirectory); log.info("Deployment service complete, {}", result.getCommandStdOutput()); restTemplate.postForLocation(asyncDeployRequest.getWebhookConfig().getUrl(), result); } @@ -242,10 +322,11 @@ public void asyncDeploy(TerraformAsyncDeployRequest asyncDeployRequest, * Async destroy resource of the service. */ @Async("taskExecutor") - public void asyncDestroy(TerraformAsyncDestroyRequest asyncDestroyRequest, String workspace) { - TerraformResult result = destroy(asyncDestroyRequest, workspace); + public void asyncDestroyWithScripts(TerraformAsyncDestroyFromDirectoryRequest request, + String moduleDirectory) { + TerraformResult result = destroyWithScripts(request, moduleDirectory); log.info("Destroy service complete, {}", result.getCommandStdOutput()); - restTemplate.postForLocation(asyncDestroyRequest.getWebhookConfig().getUrl(), result); + restTemplate.postForLocation(request.getWebhookConfig().getUrl(), result); } /** @@ -297,12 +378,147 @@ public TerraformBootSystemStatus tfHealthCheck() { return systemStatus; } + private String getTerraformState(String workspace) { + File tfState = new File(workspace + File.separator + STATE_FILE_NAME); + if (!tfState.exists()) { + log.info("Terraform state file not exists."); + return null; + } + try { + return Files.readString(tfState.toPath()); + } catch (IOException ex) { + throw new TerraformExecutorException("Read state file failed.", ex); + } + } + + private void deleteWorkspace(String workspace) { + Path path = Paths.get(workspace); + try { + Files.walk(path).sorted(Comparator.reverseOrder()).map(Path::toFile) + .forEach(File::delete); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private String buildDeployEnv(String taskId, String scripts, String moduleDirectory) + throws JsonProcessingException { + String workspace = getModuleFullPath(moduleDirectory + File.separator + + taskId); + buildWorkspace(workspace); + buildScriptFile(workspace, scripts); + return workspace; + } + + private String buildDestroyEnv(String taskId, String scripts, String moduleDirectory) + throws JsonProcessingException { + File tfState = + new File(getModuleFullPath( + moduleDirectory + File.separator + taskId + File.separator + + STATE_FILE_NAME)); + String workspace = getModuleFullPath(moduleDirectory + File.separator + taskId); + if (!tfState.exists() || !tfState.isFile() && tfState.length() <= 0) { + workspace = buildDeployEnv(taskId, scripts, moduleDirectory); + } + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode rootNode = objectMapper.readTree(scripts); + if (!rootNode.has("tfState")) { + throw new TerraformExecutorException("terraform .tfState file create error"); + } + String tfStateFile = rootNode.get("tfState").asText(); + String fileName = workspace + File.separator + STATE_FILE_NAME; + try (FileWriter scriptWriter = new FileWriter(fileName)) { + scriptWriter.write(tfStateFile); + log.info("terraform .tfState file create success, fileName: {}", fileName); + } catch (IOException ex) { + log.error("terraform .tfState file create failed.", ex); + throw new TerraformExecutorException("terraform .tfState file create failed.", ex); + } + return workspace; + } + + private void buildScriptFile(String workspace, String scripts) throws JsonProcessingException { + log.info("start build terraform script"); + File file = new File(workspace); + File tfStateFile = new File(workspace + File.separator + STATE_FILE_NAME); + if (tfStateFile.exists() && tfStateFile.isFile() && tfStateFile.length() > 0) { + return; + } + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode rootNode = objectMapper.readTree(scripts); + if (!rootNode.has("scripts")) { + throw new TerraformExecutorException("terraform scripts create error, terraform " + + "scripts not exists"); + } + JsonNode scriptsNode = rootNode.get("scripts"); + if (scriptsNode.isArray()) { + Iterator scriptNodes = scriptsNode.elements(); + while (scriptNodes.hasNext()) { + JsonNode scriptNode = scriptNodes.next(); + if (scriptNode.has("script")) { + String script = scriptNode.get("script").asText(); + String fileName = + workspace + File.separator + UUID.randomUUID() + FILE_SUFFIX; + try (FileWriter scriptWriter = new FileWriter(fileName)) { + scriptWriter.write(script); + log.info("terraform script create success, fileName: {}", fileName); + } catch (IOException ex) { + log.error("terraform script create failed.", ex); + throw new TerraformExecutorException("terraform script create failed.", ex); + } + } + } + } + } + + /** + * Build workspace of the `terraform`. + * + * @param workspace The workspace of the task. + */ + private void buildWorkspace(String workspace) { + log.info("start create workspace"); + File ws = new File(workspace); + if (!ws.exists() && !ws.mkdirs()) { + throw new TerraformExecutorException( + "Create workspace failed, File path not created: " + ws.getAbsolutePath()); + } + log.info("workspace create success,Working directory is " + ws.getAbsolutePath()); + } + + private Map getImportantFilesContent(String workspace) { + Map fileContentMap = new HashMap<>(); + File workPath = new File(workspace); + if (workPath.isDirectory() && workPath.exists()) { + File[] files = workPath.listFiles(); + if (Objects.nonNull(files)) { + List importantFiles = Arrays.stream(files) + .filter(file -> file.isFile() && !isExcludedFile(file.getName())).toList(); + for (File importantFile : importantFiles) { + try { + String content = Files.readString(importantFile.toPath()); + fileContentMap.put(importantFile.getName(), content); + } catch (IOException e) { + log.error("Read content of file with name:{} error.", + importantFile.getName(), e); + } + } + } + } + return fileContentMap; + } + + private boolean isExcludedFile(String fileName) { + String fileSuffix = fileName.substring(fileName.lastIndexOf(".")); + return EXCLUDED_FILE_SUFFIX_LIST.contains(fileSuffix); + } + private String getModuleFullPath(String moduleDirectory) { - return this.moduleParentDirectoryPath + File.separator + moduleDirectory; + return this.moduleParentDirectoryPath + moduleDirectory; } private SystemCmdResult tfPlan(Map variables, Map envVariables, - String moduleDirectory) { + String moduleDirectory) { tfInit(moduleDirectory); SystemCmdResult planResult = tfPlanCommand(variables, envVariables, getModuleFullPath(moduleDirectory)); @@ -359,5 +575,4 @@ private void cleanUpVariablesFile(String workspace) { private String getVariablesFilePath(String workspace) { return workspace + File.separator + VARS_FILE_NAME; } - }