diff --git a/service/plugin/src/main/java/com/solace/maas/ep/event/management/agent/plugin/command/model/CommandBundle.java b/service/plugin/src/main/java/com/solace/maas/ep/event/management/agent/plugin/command/model/CommandBundle.java index b5771091..35b5584b 100644 --- a/service/plugin/src/main/java/com/solace/maas/ep/event/management/agent/plugin/command/model/CommandBundle.java +++ b/service/plugin/src/main/java/com/solace/maas/ep/event/management/agent/plugin/command/model/CommandBundle.java @@ -12,7 +12,7 @@ @NoArgsConstructor @AllArgsConstructor public class CommandBundle { - private String executionType; // (serial / parallel) + private ExecutionType executionType; private Boolean exitOnFailure; private List commands; } diff --git a/service/terraform-plugin/pom.xml b/service/terraform-plugin/pom.xml index b634c294..5134e345 100644 --- a/service/terraform-plugin/pom.xml +++ b/service/terraform-plugin/pom.xml @@ -14,7 +14,6 @@ 3.1.5 3.1.5 5.3.1 - 4.0.0-RC2 4.4 2.0 3.3.0 diff --git a/service/terraform-plugin/src/main/java/com/solace/maas/ep/event/management/agent/plugin/terraform/client/TerraformClient.java b/service/terraform-plugin/src/main/java/com/solace/maas/ep/event/management/agent/plugin/terraform/client/TerraformClient.java index d3898546..125b8eb6 100644 --- a/service/terraform-plugin/src/main/java/com/solace/maas/ep/event/management/agent/plugin/terraform/client/TerraformClient.java +++ b/service/terraform-plugin/src/main/java/com/solace/maas/ep/event/management/agent/plugin/terraform/client/TerraformClient.java @@ -143,7 +143,7 @@ private CompletableFuture run(String... commands) throws IOException { private CompletableFuture run(Map envVars, String... commands) throws IOException { assert commands.length > 0; ProcessLauncher[] launchers = new ProcessLauncher[commands.length]; - for (int i = 0; i < commands.length; i++) { + for (int i = 0; i < launchers.length; i++) { launchers[i] = getTerraformLauncher(commands[i], envVars); } diff --git a/service/terraform-plugin/src/main/java/com/solace/maas/ep/event/management/agent/plugin/terraform/manager/TerraformLogProcessingService.java b/service/terraform-plugin/src/main/java/com/solace/maas/ep/event/management/agent/plugin/terraform/manager/TerraformLogProcessingService.java index d1cc7aad..df0ee678 100644 --- a/service/terraform-plugin/src/main/java/com/solace/maas/ep/event/management/agent/plugin/terraform/manager/TerraformLogProcessingService.java +++ b/service/terraform-plugin/src/main/java/com/solace/maas/ep/event/management/agent/plugin/terraform/manager/TerraformLogProcessingService.java @@ -60,7 +60,7 @@ public void saveLogToFile(CommandRequest request, List logs) throws IOEx public CommandResult buildTfCommandResult(List jsonLogs) { if (CollectionUtils.isEmpty(jsonLogs)) { - throw new IllegalArgumentException("Cannot extract information empty logs"); + throw new IllegalArgumentException("No terraform logs were collected. Unable to process response."); } List> successLogs = jsonLogs.stream() @@ -128,11 +128,11 @@ private String extractDiagnosticDetailMessage(Object diagnostic) { if (diagnostic == null) { return ""; } - if (!(diagnostic instanceof Map)) { - throw new IllegalArgumentException("Unsupported object type encountered"); + if (diagnostic instanceof Map diagnosticMap) { + return String.valueOf(diagnosticMap.get("detail")); } - return String.valueOf(((Map) diagnostic).get("detail")); + throw new IllegalArgumentException("Unsupported terraform log object type encountered"); } private String extractResourceAddressFromDiagnostic(Object diagnostic) { diff --git a/service/terraform-plugin/src/main/java/com/solace/maas/ep/event/management/agent/plugin/terraform/manager/TerraformManager.java b/service/terraform-plugin/src/main/java/com/solace/maas/ep/event/management/agent/plugin/terraform/manager/TerraformManager.java index 049bd2ca..461b9f17 100644 --- a/service/terraform-plugin/src/main/java/com/solace/maas/ep/event/management/agent/plugin/terraform/manager/TerraformManager.java +++ b/service/terraform-plugin/src/main/java/com/solace/maas/ep/event/management/agent/plugin/terraform/manager/TerraformManager.java @@ -21,6 +21,7 @@ import java.util.Base64; import java.util.List; import java.util.Map; +import java.util.concurrent.ExecutionException; import static com.solace.maas.ep.event.management.agent.plugin.constants.RouteConstants.COMMAND_CORRELATION_ID; import static com.solace.maas.ep.event.management.agent.plugin.constants.RouteConstants.MESSAGING_SERVICE_ID; @@ -54,55 +55,79 @@ public void execute(CommandRequest request, Command command, Map try (TerraformClient terraformClient = terraformClientFactory.createClient()) { Path configPath = createConfigPath(request); - terraformClient.setWorkingDirectory(configPath.toFile()); - List output = new ArrayList<>(); - - // Write each terraform to the output list so that it can be processed later - // Also write the output to the main log to be streamed back to EP - terraformClient.setOutputListener(tfLog -> { - MDC.put("traceId", traceId); - MDC.put("spanId", spanId); - output.add(tfLog); - log.debug("Terraform output: {}", tfLog); - }); - String commandVerb = command.getCommand(); - - switch (commandVerb) { - case "apply" -> { - writeHclToFile(command, configPath); - terraformClient.plan(envVars).get(); - terraformClient.apply(envVars).get(); - } - case "write_HCL" -> writeHclToFile(command, configPath); - default -> log.error("Cannot handle arbitrary commands."); + List logOutput = setupTerraformClient(terraformClient, configPath, traceId, spanId); + String commandVerb = executeTerraformCommand(command, envVars, configPath, terraformClient); + processTerraformResponse(request, command, commandVerb, logOutput); + } catch (InterruptedException e) { + log.error("Received a thread interrupt while executing the terraform command", e); + Thread.currentThread().interrupt(); + } catch (Exception e) { + log.error("An error was encountered while executing the terraform command", e); + setCommandError(command, e); + } + } + + private static List setupTerraformClient(TerraformClient terraformClient, Path configPath, String traceId, String spanId) { + terraformClient.setWorkingDirectory(configPath.toFile()); + List output = new ArrayList<>(); + + // Write each terraform to the output list so that it can be processed later + // Also write the output to the main log to be streamed back to EP + terraformClient.setOutputListener(tfLog -> { + MDC.put("traceId", traceId); + MDC.put("spanId", spanId); + output.add(tfLog); + log.debug("Terraform output: {}", tfLog); + }); + return output; + } + + private static String executeTerraformCommand(Command command, Map envVars, Path configPath, TerraformClient terraformClient) throws IOException, InterruptedException, ExecutionException { + String commandVerb = command.getCommand(); + switch (commandVerb) { + case "apply" -> { + writeHclToFile(command, configPath); + terraformClient.plan(envVars).get(); + terraformClient.apply(envVars).get(); } + case "write_HCL" -> writeHclToFile(command, configPath); + default -> throw new IllegalArgumentException("Unsupported command " + commandVerb); + } + return commandVerb; + } - // Process logs and create the result - if (Boolean.TRUE.equals(command.getIgnoreResult())) { + private void processTerraformResponse(CommandRequest request, Command command, String commandVerb, List output) throws IOException { + // Process logs and create the result + if (Boolean.TRUE.equals(command.getIgnoreResult())) { + command.setResult(CommandResult.builder() + .status(JobStatus.success) + .logs(List.of()) + .errors(List.of()) + .build()); + } else { + if (!"write_HCL".equals(commandVerb)) { + terraformLogProcessingService.saveLogToFile(request, output); + command.setResult(terraformLogProcessingService.buildTfCommandResult(output)); + } else { command.setResult(CommandResult.builder() .status(JobStatus.success) .logs(List.of()) .errors(List.of()) .build()); - } else { - if (!"write_HCL".equals(commandVerb)) { - terraformLogProcessingService.saveLogToFile(request, output); - command.setResult(terraformLogProcessingService.buildTfCommandResult(output)); - } else { - command.setResult(CommandResult.builder() - .status(JobStatus.success) - .logs(List.of()) - .errors(List.of()) - .build()); - } } - } catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - } catch (Exception e) { - throw new IllegalArgumentException(e); } } + private void setCommandError(Command command, Exception e) { + command.setResult(CommandResult.builder() + .status(JobStatus.error) + .logs(List.of()) + .errors(List.of( + Map.of("message", e.getMessage(), + "errorType", e.getClass().getName()))) + .build()); + } + private Path createConfigPath(CommandRequest request) { Path configPath = Paths.get(terraformProperties.getWorkingDirectoryRoot() + File.separator + request.getContext() @@ -123,8 +148,18 @@ private Path createConfigPath(CommandRequest request) { private static void writeHclToFile(Command command, Path configPath) throws IOException { if (StringUtils.isNotEmpty(command.getBody())) { - byte[] decodedBytes = Base64.getDecoder().decode(command.getBody()); - Files.write(configPath.resolve(TF_CONFIG_FILENAME), decodedBytes); + // At the moment, we only support base64 decoding + Map parameters = command.getParameters(); + if (parameters != null && parameters.containsKey("Content-Encoding") && "base64".equals(parameters.get("Content-Encoding"))) { + byte[] decodedBytes = Base64.getDecoder().decode(command.getBody()); + Files.write(configPath.resolve(TF_CONFIG_FILENAME), decodedBytes); + } else { + if (parameters == null || !parameters.containsKey("Content-Encoding")) { + throw new IllegalArgumentException("Missing Content-Encoding property in command parameters."); + } + + throw new IllegalArgumentException("Unsupported encoding type " + parameters.get("Content-Encoding")); + } } } } diff --git a/service/terraform-plugin/src/test/java/com/solace/maas/ep/event/management/agent/plugin/terraform/TerraformCommandIT.java b/service/terraform-plugin/src/test/java/com/solace/maas/ep/event/management/agent/plugin/terraform/TerraformCommandIT.java index aa39ae43..d6710cc9 100644 --- a/service/terraform-plugin/src/test/java/com/solace/maas/ep/event/management/agent/plugin/terraform/TerraformCommandIT.java +++ b/service/terraform-plugin/src/test/java/com/solace/maas/ep/event/management/agent/plugin/terraform/TerraformCommandIT.java @@ -5,6 +5,7 @@ import com.solace.maas.ep.event.management.agent.plugin.command.model.CommandRequest; import com.solace.maas.ep.event.management.agent.plugin.command.model.CommandResult; import com.solace.maas.ep.event.management.agent.plugin.command.model.CommandType; +import com.solace.maas.ep.event.management.agent.plugin.command.model.ExecutionType; import com.solace.maas.ep.event.management.agent.plugin.command.model.JobStatus; import com.solace.maas.ep.event.management.agent.plugin.terraform.client.TerraformClient; import com.solace.maas.ep.event.management.agent.plugin.terraform.configuration.TerraformProperties; @@ -115,7 +116,7 @@ public void testCreateResourceHappyPath() throws IOException { } @Test - public void testCreateResourceFailurePath() throws IOException { + public void testCreateResourceTerraformErrorFailurePath() throws IOException { String newQueueTf = getResourceAsString(resourceLoader.getResource("classpath:tfFiles/newQueue.tf")); List newQueueTfLogs = getResourceAsStringArray(resourceLoader.getResource("classpath:tfLogs/tfAddErrorSubscriptionAlreadyPresent.txt")); @@ -146,6 +147,83 @@ public void testCreateResourceFailurePath() throws IOException { } } + @Test + public void testCreateResourceMissingParameterFailurePath() { + + String newQueueTf = getResourceAsString(resourceLoader.getResource("classpath:tfFiles/newQueue.tf")); + + // Generate a command without the expected base64 parameters + Command command = generateCommand("apply", newQueueTf, false, Map.of()); + CommandRequest terraformRequest = generateCommandRequest(command); + + terraformManager.execute(terraformRequest, command, Map.of()); + + // Validate that the plan and apply apis are called + for (CommandBundle commandBundle : terraformRequest.getCommandBundles()) { + for (Command tfCommand : commandBundle.getCommands()) { + + CommandResult result = tfCommand.getResult(); + assertEquals(JobStatus.error, result.getStatus()); + assertTrue(result.getErrors().get(0).get("errorType").toString().contains("java.lang.IllegalArgumentException")); + assertTrue(result.getErrors().get(0).get("message").toString().contains("Missing Content-Encoding property in command parameters.")); + } + } + } + + @Test + public void testCreateResourceNoLogsFailurePath() throws IOException { + + String newQueueTf = getResourceAsString(resourceLoader.getResource("classpath:tfFiles/newQueue.tf")); + + Command command = generateCommand("apply", newQueueTf); + CommandRequest terraformRequest = generateCommandRequest(command); + + when(terraformClient.plan(Map.of())).thenReturn(CompletableFuture.supplyAsync(() -> true)); + when(terraformClient.apply(Map.of())).thenReturn(CompletableFuture.supplyAsync(() -> true)); + + // Setup the empty output + setupLogMock(List.of()); + + terraformManager.execute(terraformRequest, command, Map.of()); + + // Validate that the plan api is called + verify(terraformClient, times(1)).plan(any()); + verify(terraformClient, times(1)).apply(any()); + + // Validate that the plan and apply apis are called + for (CommandBundle commandBundle : terraformRequest.getCommandBundles()) { + for (Command tfCommand : commandBundle.getCommands()) { + + CommandResult result = tfCommand.getResult(); + assertEquals(JobStatus.error, result.getStatus()); + assertTrue(result.getErrors().get(0).get("errorType").toString().contains("java.lang.IllegalArgumentException")); + assertTrue(result.getErrors().get(0).get("message").toString().contains("No terraform logs were collected. Unable to process response.")); + } + } + } + + @Test + public void testCreateResourceUnknownCommandFailurePath() { + + String newQueueTf = getResourceAsString(resourceLoader.getResource("classpath:tfFiles/newQueue.tf")); + + Command command = generateCommand("appply", newQueueTf); + CommandRequest terraformRequest = generateCommandRequest(command); + + terraformManager.execute(terraformRequest, command, Map.of()); + + // Validate that the plan and apply apis are called + for (CommandBundle commandBundle : terraformRequest.getCommandBundles()) { + for (Command tfCommand : commandBundle.getCommands()) { + + CommandResult result = tfCommand.getResult(); + assertEquals(JobStatus.error, result.getStatus()); + assertTrue(result.getErrors().get(0).get("errorType").toString().contains("java.lang.IllegalArgumentException")); + assertTrue(result.getErrors().get(0).get("message").toString().contains("Unsupported command appply")); + } + } + } + @Test public void testIgnoreResult() throws IOException { @@ -217,6 +295,12 @@ private static Command generateCommand(String tfCommand, String body) { } private static Command generateCommand(String tfCommand, String body, Boolean ignoreResult) { + return generateCommand(tfCommand, body, ignoreResult, + Map.of("Content-Type", "application/hcl", + "Content-Encoding", "base64")); + } + + private static Command generateCommand(String tfCommand, String body, Boolean ignoreResult, Map parameters) { return Command.builder() .body(Optional.ofNullable(body) .map(b -> Base64.getEncoder().encodeToString(b.getBytes(UTF_8))) @@ -224,14 +308,16 @@ private static Command generateCommand(String tfCommand, String body, Boolean ig .command(tfCommand) .commandType(CommandType.terraform) .ignoreResult(ignoreResult) + .parameters(parameters) .build(); } + private static CommandRequest generateCommandRequest(Command commandRequest) { return CommandRequest.builder() .commandBundles(List.of( CommandBundle.builder() - .executionType("serial") + .executionType(ExecutionType.serial) .exitOnFailure(false) .commands(List.of(commandRequest)) .build())) diff --git a/service/terraform-plugin/src/test/java/com/solace/maas/ep/event/management/agent/plugin/terraform/real/TerraformClientRealTests.java b/service/terraform-plugin/src/test/java/com/solace/maas/ep/event/management/agent/plugin/terraform/real/TerraformClientRealTests.java index 92f64e5b..39f9dedd 100644 --- a/service/terraform-plugin/src/test/java/com/solace/maas/ep/event/management/agent/plugin/terraform/real/TerraformClientRealTests.java +++ b/service/terraform-plugin/src/test/java/com/solace/maas/ep/event/management/agent/plugin/terraform/real/TerraformClientRealTests.java @@ -147,6 +147,9 @@ private List executeTerraformCommand(String hclFileName, String t Command commandRequest = Command.builder() .body(Base64.getEncoder().encodeToString(terraformString.getBytes(UTF_8))) .command(tfVerb) + .parameters(Map.of( + "Content-Type", "application/hcl", + "Content-Encoding", "base64")) .build(); CommandRequest terraformRequest = CommandRequest.builder() .commandBundles(List.of(CommandBundle.builder()