diff --git a/src/main/java/io/codeka/gaia/runner/DockerRunner.java b/src/main/java/io/codeka/gaia/runner/DockerRunner.java new file mode 100644 index 000000000..165551d69 --- /dev/null +++ b/src/main/java/io/codeka/gaia/runner/DockerRunner.java @@ -0,0 +1,98 @@ +package io.codeka.gaia.runner; + +import com.spotify.docker.client.DockerClient; +import com.spotify.docker.client.exceptions.DockerException; +import com.spotify.docker.client.messages.ContainerConfig; +import io.codeka.gaia.settings.bo.Settings; +import io.codeka.gaia.stacks.workflow.JobWorkflow; +import org.apache.commons.io.output.WriterOutputStream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.nio.channels.WritableByteChannel; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.concurrent.CompletableFuture; + +/** + * Service to run docker container + */ +@Service +public class DockerRunner { + + private static final Logger LOG = LoggerFactory.getLogger(DockerRunner.class); + + private DockerClient dockerClient; + private ContainerConfig.Builder containerConfigBuilder; + private HttpHijackWorkaround httpHijackWorkaround; + private Settings settings; + + @Autowired + public DockerRunner(DockerClient dockerClient, ContainerConfig.Builder containerConfigBuilder, + HttpHijackWorkaround httpHijackWorkaround, Settings settings) { + this.dockerClient = dockerClient; + this.containerConfigBuilder = containerConfigBuilder; + this.httpHijackWorkaround = httpHijackWorkaround; + this.settings = settings; + } + + int runContainerForJob(JobWorkflow jobWorkflow, String script) { + try { + var env = new ArrayList(); + env.add("TF_IN_AUTOMATION=true"); + env.addAll(settings.env()); + + var job = jobWorkflow.getJob(); + + // FIXME This is certainly no thread safe ! + var containerConfig = containerConfigBuilder + .env(env) + .image("hashicorp/terraform:" + job.getCliVersion()) + .build(); + + // pull the image + dockerClient.pull("hashicorp/terraform:" + job.getCliVersion()); + + var containerCreation = dockerClient.createContainer(containerConfig); + var containerId = containerCreation.id(); + + var dockerContainer = new DockerContainer(containerId, dockerClient, httpHijackWorkaround); + + dockerClient.startContainer(containerId); + + // attaching the outputs in a background thread + var step = jobWorkflow.getCurrentStep(); + CompletableFuture.runAsync(() -> { + try (var writerOutputStream = new WriterOutputStream(step.getLogsWriter(), Charset.defaultCharset())) { + // this code is blocking I/O ! + dockerContainer.attach(writerOutputStream, writerOutputStream); + } catch (IOException | StackRunnerException e) { + LOG.error("Unable to attach logs of container", e); + } + }); + + // write the content of the script to the container's std in + try (WritableByteChannel stdIn = Channels.newChannel(dockerContainer.getStdIn())) { + stdIn.write(ByteBuffer.wrap(script.getBytes())); + } + + // wait for the container to exit + var containerExit = dockerClient.waitContainer(containerCreation.id()); + + dockerClient.removeContainer(containerCreation.id()); + + return Math.toIntExact(containerExit.statusCode()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return 99; + } catch (DockerException | IOException | StackRunnerException e) { + return 99; + } + } + +} diff --git a/src/main/java/io/codeka/gaia/runner/StackCommandBuilder.java b/src/main/java/io/codeka/gaia/runner/StackCommandBuilder.java index 0c30c6bb2..120778d54 100644 --- a/src/main/java/io/codeka/gaia/runner/StackCommandBuilder.java +++ b/src/main/java/io/codeka/gaia/runner/StackCommandBuilder.java @@ -70,6 +70,15 @@ private String buildCommand(Stack stack, TerraformModule module, String command) return String.format("%s %s", command, variablesBuilder.toString()); } + /** + * builds the terraform plan script + * + * @return + */ + String buildPlanScript(Stack stack, TerraformModule module) { + return buildScript(stack, module, this::buildPlanCommand); + } + /** * builds the terraform apply script * @@ -84,8 +93,8 @@ String buildApplyScript(Stack stack, TerraformModule module) { * * @return */ - String buildPlanScript(Stack stack, TerraformModule module) { - return buildScript(stack, module, this::buildPlanCommand); + String buildPlanDestroyScript(Stack stack, TerraformModule module) { + return buildScript(stack, module, this::buildPlanDestroyCommand); } /** @@ -97,6 +106,17 @@ String buildDestroyScript(Stack stack, TerraformModule module) { return buildScript(stack, module, this::buildDestroyCommand); } + /** + * builds the terraform plan command + * + * @param stack + * @param module + * @return + */ + String buildPlanCommand(Stack stack, TerraformModule module) { + return buildCommand(stack, module, "terraform plan -detailed-exitcode"); + } + /** * builds the terraform apply command * @@ -109,14 +129,14 @@ String buildApplyCommand(Stack stack, TerraformModule module) { } /** - * builds the terraform plan command + * builds the terraform plan destroy command * * @param stack * @param module * @return */ - String buildPlanCommand(Stack stack, TerraformModule module) { - return buildCommand(stack, module, "terraform plan -detailed-exitcode"); + String buildPlanDestroyCommand(Stack stack, TerraformModule module) { + return buildCommand(stack, module, "terraform plan -destroy -detailed-exitcode"); } /** diff --git a/src/main/java/io/codeka/gaia/runner/StackRunner.java b/src/main/java/io/codeka/gaia/runner/StackRunner.java index b3d9f62a0..fbf77384d 100644 --- a/src/main/java/io/codeka/gaia/runner/StackRunner.java +++ b/src/main/java/io/codeka/gaia/runner/StackRunner.java @@ -1,30 +1,23 @@ package io.codeka.gaia.runner; -import com.spotify.docker.client.DockerClient; -import com.spotify.docker.client.exceptions.DockerException; -import com.spotify.docker.client.messages.ContainerConfig; import io.codeka.gaia.modules.bo.TerraformModule; -import io.codeka.gaia.settings.bo.Settings; import io.codeka.gaia.stacks.bo.Job; import io.codeka.gaia.stacks.bo.JobType; import io.codeka.gaia.stacks.bo.Stack; import io.codeka.gaia.stacks.bo.StackState; import io.codeka.gaia.stacks.repository.JobRepository; import io.codeka.gaia.stacks.repository.StackRepository; -import org.apache.commons.io.output.WriterOutputStream; +import io.codeka.gaia.stacks.repository.StepRepository; +import io.codeka.gaia.stacks.workflow.JobWorkflow; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.channels.Channels; -import java.nio.channels.WritableByteChannel; -import java.nio.charset.Charset; -import java.util.ArrayList; import java.util.HashMap; import java.util.Map; -import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.function.IntConsumer; +import java.util.function.Supplier; /** * Runs a module instance @@ -32,151 +25,117 @@ @Service public class StackRunner { - private DockerClient dockerClient; - - private ContainerConfig.Builder containerConfigBuilder; - - private Settings settings; - + private DockerRunner dockerRunner; private StackCommandBuilder stackCommandBuilder; - - private Map jobs = new HashMap<>(); - private StackRepository stackRepository; - - private HttpHijackWorkaround httpHijackWorkaround; - private JobRepository jobRepository; + private StepRepository stepRepository; + + private Map jobs = new HashMap<>(); @Autowired - public StackRunner(DockerClient dockerClient, ContainerConfig.Builder containerConfigBuilder, Settings settings, StackCommandBuilder stackCommandBuilder, StackRepository stackRepository, HttpHijackWorkaround httpHijackWorkaround, JobRepository jobRepository) { - this.dockerClient = dockerClient; - this.containerConfigBuilder = containerConfigBuilder; - this.settings = settings; + public StackRunner(DockerRunner dockerRunner, StackCommandBuilder stackCommandBuilder, + StackRepository stackRepository, JobRepository jobRepository, StepRepository stepRepository) { + this.dockerRunner = dockerRunner; this.stackCommandBuilder = stackCommandBuilder; this.stackRepository = stackRepository; - this.httpHijackWorkaround = httpHijackWorkaround; this.jobRepository = jobRepository; + this.stepRepository = stepRepository; } - private int runContainerForJob(Job job, String script) { - try{ - var env = new ArrayList(); - env.add("TF_IN_AUTOMATION=true"); - env.addAll(settings.env()); - - // FIXME This is certainly no thread safe ! - var containerConfig = containerConfigBuilder - .env(env) - .image("hashicorp/terraform:" + job.getCliVersion()) - .build(); - - // pull the image - dockerClient.pull("hashicorp/terraform:" + job.getCliVersion()); - - var containerCreation = dockerClient.createContainer(containerConfig); - var containerId = containerCreation.id(); - - var dockerContainer = new DockerContainer(containerId, dockerClient, httpHijackWorkaround); - - dockerClient.startContainer(containerId); - - // attaching the outputs in a background thread - CompletableFuture.runAsync(() -> { - try( var writerOutputStream = new WriterOutputStream(job.getLogsWriter(), Charset.defaultCharset()) ) { - // this code is blocking I/O ! - dockerContainer.attach(writerOutputStream, writerOutputStream); - } catch (IOException | StackRunnerException e) { - e.printStackTrace(); - } - }); - - // write the content of the script to the container's std in - try( WritableByteChannel stdIn = Channels.newChannel(dockerContainer.getStdIn()) ){ - stdIn.write(ByteBuffer.wrap(script.getBytes())); - } - - // wait for the container to exit - var containerExit = dockerClient.waitContainer(containerCreation.id()); - - dockerClient.removeContainer(containerCreation.id()); - - return Math.toIntExact(containerExit.statusCode()); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - return 99; - } catch (DockerException | IOException | StackRunnerException e) { - return 99; + private String managePlanScript(JobType jobType, Stack stack, TerraformModule module) { + if (jobType == JobType.RUN) { + return stackCommandBuilder.buildPlanScript(stack, module); } + return stackCommandBuilder.buildPlanDestroyScript(stack, module); } - @Async - public void apply(Job job, TerraformModule module, Stack stack) { - this.jobs.put(job.getId(), job); - job.setCliVersion(module.getCliVersion()); - job.start(JobType.RUN); - jobRepository.save(job); - - var applyScript = stackCommandBuilder.buildApplyScript(stack, module); + private void managePlanResult(Integer result, JobWorkflow jobWorkflow, Stack stack) { + if (result == 0) { + // diff is empty + jobWorkflow.end(); + } else if (result == 2) { + // there is a diff, set the status of the stack to : "TO_UPDATE" + if (StackState.NEW != stack.getState() && jobWorkflow.getJob().getType() != JobType.DESTROY) { + stack.setState(StackState.TO_UPDATE); + stackRepository.save(stack); + } + jobWorkflow.end(); + } else { + // error + jobWorkflow.fail(); + } + } - var result = runContainerForJob(job, applyScript); + private String manageApplyScript(JobType jobType, Stack stack, TerraformModule module) { + if (jobType == JobType.RUN) { + return stackCommandBuilder.buildApplyScript(stack, module); + } + return stackCommandBuilder.buildDestroyScript(stack, module); + } - if(result == 0){ - job.end(); + private void manageApplyResult(Integer result, JobWorkflow jobWorkflow, Stack stack) { + if (result == 0) { + jobWorkflow.end(); // update stack information - stack.setState(StackState.RUNNING); + stack.setState(jobWorkflow.getJob().getType() == JobType.RUN ? StackState.RUNNING : StackState.STOPPED); stackRepository.save(stack); + } else { + jobWorkflow.fail(); } - else{ - job.fail(); - } - - // save job to database - jobRepository.save(job); - this.jobs.remove(job.getId()); } /** - * Runs a "plan job". - * A plan job runs a 'terraform plan' - * @param job - * @param module - * @param stack + * @param jobWorkflow + * @param jobActionFn function applying o the job + * @param scriptFn function allowing to get the right script to execute + * @param resultFn function treating the result ot the executed script */ - @Async - public void plan(Job job, TerraformModule module, Stack stack) { + private void treatJob(JobWorkflow jobWorkflow, Consumer jobActionFn, + Supplier scriptFn, IntConsumer resultFn) { + // execute the workflow of the job + jobActionFn.accept(jobWorkflow); + + var job = jobWorkflow.getJob(); this.jobs.put(job.getId(), job); - job.setCliVersion(module.getCliVersion()); - job.start(JobType.PREVIEW); + stepRepository.saveAll(job.getSteps()); jobRepository.save(job); - var planScript = stackCommandBuilder.buildPlanScript(stack, module); + // get the wanted script + var script = scriptFn.get(); - var result = runContainerForJob(job, planScript); + var result = this.dockerRunner.runContainerForJob(jobWorkflow, script); - if(result == 0){ - // diff is empty - job.end(); - } - else if(result == 2){ - // there is a diff, set the status of the stack to : "TO_UPDATE" - if(StackState.NEW != stack.getState()){ - stack.setState(StackState.TO_UPDATE); - stackRepository.save(stack); - } - job.end(); - } - else{ - // error - job.fail(); - } + // manage the result of the execution of the script + resultFn.accept(result); // save job to database + stepRepository.saveAll(job.getSteps()); jobRepository.save(job); this.jobs.remove(job.getId()); } + @Async + public void plan(JobWorkflow jobWorkflow, TerraformModule module, Stack stack) { + treatJob( + jobWorkflow, + JobWorkflow::plan, + () -> managePlanScript(jobWorkflow.getJob().getType(), stack, module), + result -> managePlanResult(result, jobWorkflow, stack) + ); + } + + @Async + public void apply(JobWorkflow jobWorkflow, TerraformModule module, Stack stack) { + treatJob( + jobWorkflow, + JobWorkflow::apply, + () -> manageApplyScript(jobWorkflow.getJob().getType(), stack, module), + result -> manageApplyResult(result, jobWorkflow, stack) + ); + } + public Job getJob(String jobId) { if (this.jobs.containsKey(jobId)) { // try in memory @@ -186,37 +145,4 @@ public Job getJob(String jobId) { return this.jobRepository.findById(jobId).orElseThrow(() -> new RuntimeException("job not found")); } - /** - * Runs a "stop job". - * A stop job runs a 'terraform destroy' - * @param job - * @param module - * @param stack - */ - @Async - public void stop(Job job, TerraformModule module, Stack stack) { - this.jobs.put(job.getId(), job); - job.setCliVersion(module.getCliVersion()); - job.start(JobType.STOP); - jobRepository.save(job); - - var destroyScript = stackCommandBuilder.buildDestroyScript(stack, module); - - var result = runContainerForJob(job, destroyScript); - - if(result == 0){ - job.end(); - // update state - stack.setState(StackState.STOPPED); - stackRepository.save(stack); - } else{ - // error - job.fail(); - } - - // save job to database - jobRepository.save(job); - this.jobs.remove(job.getId()); - } - } diff --git a/src/main/java/io/codeka/gaia/stacks/bo/Job.java b/src/main/java/io/codeka/gaia/stacks/bo/Job.java index b92742ef8..83480d8e2 100644 --- a/src/main/java/io/codeka/gaia/stacks/bo/Job.java +++ b/src/main/java/io/codeka/gaia/stacks/bo/Job.java @@ -1,51 +1,52 @@ package io.codeka.gaia.stacks.bo; -import com.fasterxml.jackson.annotation.JsonIgnore; import io.codeka.gaia.teams.bo.User; -import org.springframework.data.annotation.Transient; import org.springframework.data.mongodb.core.mapping.DBRef; +import org.springframework.util.CollectionUtils; -import java.io.StringWriter; -import java.io.Writer; -import java.time.Duration; import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.UUID; /** - * A job is the instanciation of a stack + * A job instantiates or stops a stack */ public class Job { private String id; - private String stackId; - private LocalDateTime startDateTime; - private LocalDateTime endDateTime; - - private Long executionTime; - - @Transient - private StringWriter stringWriter = new StringWriter(); - - private String logs; - - private JobStatus jobStatus; - - private JobType jobType; - + private JobType type; + private JobStatus status; private String cliVersion; - + @DBRef + private List steps = new ArrayList<>(2); @DBRef private User user; - public Job(User user) { - if (user == null) { - throw new AssertionError("A job must have a non null user!"); - } + public Job() { + } + + public Job(JobType jobType, String stackId, User user) { + this.id = UUID.randomUUID().toString(); + this.type = jobType; + this.stackId = stackId; this.user = user; } + public void start() { + this.status = JobStatus.PLAN_STARTED; + this.startDateTime = LocalDateTime.now(); + } + + public void end(JobStatus jobStatus) { + this.endDateTime = LocalDateTime.now(); + this.status = jobStatus; + } + public String getId() { return id; } @@ -54,54 +55,44 @@ public void setId(String id) { this.id = id; } - public String getLogs() { - if(jobStatus == JobStatus.FINISHED || jobStatus == JobStatus.FAILED){ - return logs; - } - return stringWriter.toString(); + public String getStackId() { + return stackId; } - @JsonIgnore - public Writer getLogsWriter(){ - return stringWriter; + public void setStackId(String stackId) { + this.stackId = stackId; } - public JobStatus getStatus(){ - return this.jobStatus; + public LocalDateTime getStartDateTime() { + return startDateTime; } - public void start(JobType jobType) { - this.jobStatus = JobStatus.RUNNING; - this.jobType = jobType; - this.startDateTime = LocalDateTime.now(); + public void setStartDateTime(LocalDateTime startDateTime) { + this.startDateTime = startDateTime; } - public void end() { - this.jobStatus = JobStatus.FINISHED; - // getting final logs - this.logs = this.stringWriter.toString(); - this.endDateTime = LocalDateTime.now(); - this.executionTime = Duration.between(startDateTime, endDateTime).toMillis(); + public LocalDateTime getEndDateTime() { + return endDateTime; } - public void fail() { - this.jobStatus = JobStatus.FAILED; - // getting final logs - this.logs = this.stringWriter.toString(); - this.endDateTime = LocalDateTime.now(); - this.executionTime = Duration.between(startDateTime, endDateTime).toMillis(); + public void setEndDateTime(LocalDateTime endDateTime) { + this.endDateTime = endDateTime; } - public String getStackId() { - return stackId; + public JobType getType() { + return type; } - public void setStackId(String stackId) { - this.stackId = stackId; + public void setType(JobType type) { + this.type = type; } - public JobType getType() { - return this.jobType; + public JobStatus getStatus() { + return status; + } + + public void setStatus(JobStatus status) { + this.status = status; } public String getCliVersion() { @@ -112,31 +103,29 @@ public void setCliVersion(String cliVersion) { this.cliVersion = cliVersion; } - public LocalDateTime getStartDateTime() { - return startDateTime; + public List getSteps() { + return steps; } - public void setStartDateTime(LocalDateTime startDateTime) { - this.startDateTime = startDateTime; + public void setSteps(List steps) { + this.steps = steps; } - public LocalDateTime getEndDateTime() { - return endDateTime; + public User getUser() { + return user; } - public void setEndDateTime(LocalDateTime endDateTime) { - this.endDateTime = endDateTime; + public void setUser(User user) { + this.user = user; } public Long getExecutionTime() { - return executionTime; - } - - public void setExecutionTime(Long executionTime) { - this.executionTime = executionTime; - } - - public User getUser() { - return user; + if (CollectionUtils.isEmpty(this.steps)) { + return null; + } + return this.steps.stream() + .map(Step::getExecutionTime) + .filter(Objects::nonNull) + .reduce(null, (a, b) -> a == null ? b : a + b); } } diff --git a/src/main/java/io/codeka/gaia/stacks/bo/JobStatus.java b/src/main/java/io/codeka/gaia/stacks/bo/JobStatus.java index aa8006f76..b54d240ec 100644 --- a/src/main/java/io/codeka/gaia/stacks/bo/JobStatus.java +++ b/src/main/java/io/codeka/gaia/stacks/bo/JobStatus.java @@ -1,5 +1,6 @@ package io.codeka.gaia.stacks.bo; public enum JobStatus { - RUNNING, FINISHED, FAILED + PLAN_STARTED, PLAN_FINISHED, PLAN_FAILED, + APPLY_STARTED, APPLY_FINISHED, APPLY_FAILED, } diff --git a/src/main/java/io/codeka/gaia/stacks/bo/JobType.java b/src/main/java/io/codeka/gaia/stacks/bo/JobType.java index 51a6224e6..22c9b181f 100644 --- a/src/main/java/io/codeka/gaia/stacks/bo/JobType.java +++ b/src/main/java/io/codeka/gaia/stacks/bo/JobType.java @@ -1,5 +1,5 @@ package io.codeka.gaia.stacks.bo; public enum JobType { - PREVIEW, RUN, STOP + RUN, DESTROY } diff --git a/src/main/java/io/codeka/gaia/stacks/bo/Step.java b/src/main/java/io/codeka/gaia/stacks/bo/Step.java new file mode 100644 index 000000000..0be57c5ab --- /dev/null +++ b/src/main/java/io/codeka/gaia/stacks/bo/Step.java @@ -0,0 +1,128 @@ +package io.codeka.gaia.stacks.bo; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import org.springframework.data.annotation.Transient; + +import java.io.StringWriter; +import java.io.Writer; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.UUID; + +/** + * A step is a part of a job. It can be a plan, an apply, etc. + */ +public class Step { + + private String id; + private String jobId; + private LocalDateTime startDateTime; + private LocalDateTime endDateTime; + private Long executionTime; + private StepType type; + private StepStatus status; + @Transient + private StringWriter logsWriter = new StringWriter(); + private String logs; + + public Step() { + } + + public Step(StepType stepType, String jobId) { + this.id = UUID.randomUUID().toString(); + this.type = stepType; + this.jobId = jobId; + } + + public void start() { + this.startDateTime = LocalDateTime.now(); + this.status = StepStatus.STARTED; + } + + public void end() { + this.endDateTime = LocalDateTime.now(); + this.executionTime = Duration.between(this.startDateTime, this.endDateTime).toMillis(); + this.logs = this.logsWriter.toString(); + this.status = StepStatus.FINISHED; + } + + public void fail() { + this.endDateTime = LocalDateTime.now(); + this.executionTime = Duration.between(this.startDateTime, this.endDateTime).toMillis(); + this.logs = this.logsWriter.toString(); + this.status = StepStatus.FAILED; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getJobId() { + return jobId; + } + + public void setJobId(String jobId) { + this.jobId = jobId; + } + + public LocalDateTime getStartDateTime() { + return startDateTime; + } + + public void setStartDateTime(LocalDateTime startDateTime) { + this.startDateTime = startDateTime; + } + + public LocalDateTime getEndDateTime() { + return endDateTime; + } + + public void setEndDateTime(LocalDateTime endDateTime) { + this.endDateTime = endDateTime; + } + + public Long getExecutionTime() { + return executionTime; + } + + public void setExecutionTime(Long executionTime) { + this.executionTime = executionTime; + } + + public StepType getType() { + return type; + } + + public void setType(StepType type) { + this.type = type; + } + + public StepStatus getStatus() { + return status; + } + + public void setStatus(StepStatus status) { + this.status = status; + } + + public String getLogs() { + if (status == StepStatus.FINISHED || status == StepStatus.FAILED) { + return logs; + } + return logsWriter.toString(); + } + + public void setLogs(String logs) { + this.logs = logs; + } + + @JsonIgnore + public Writer getLogsWriter() { + return logsWriter; + } + +} diff --git a/src/main/java/io/codeka/gaia/stacks/bo/StepStatus.java b/src/main/java/io/codeka/gaia/stacks/bo/StepStatus.java new file mode 100644 index 000000000..e2b16b32a --- /dev/null +++ b/src/main/java/io/codeka/gaia/stacks/bo/StepStatus.java @@ -0,0 +1,5 @@ +package io.codeka.gaia.stacks.bo; + +public enum StepStatus { + STARTED, FINISHED, FAILED +} diff --git a/src/main/java/io/codeka/gaia/stacks/bo/StepType.java b/src/main/java/io/codeka/gaia/stacks/bo/StepType.java new file mode 100644 index 000000000..4d13333fb --- /dev/null +++ b/src/main/java/io/codeka/gaia/stacks/bo/StepType.java @@ -0,0 +1,5 @@ +package io.codeka.gaia.stacks.bo; + +public enum StepType { + PLAN, APPLY +} diff --git a/src/main/java/io/codeka/gaia/stacks/controller/JobRestController.java b/src/main/java/io/codeka/gaia/stacks/controller/JobRestController.java index d4a0ce81c..bbf0e7bdd 100644 --- a/src/main/java/io/codeka/gaia/stacks/controller/JobRestController.java +++ b/src/main/java/io/codeka/gaia/stacks/controller/JobRestController.java @@ -1,7 +1,12 @@ package io.codeka.gaia.stacks.controller; +import io.codeka.gaia.modules.repository.TerraformModuleRepository; +import io.codeka.gaia.runner.StackRunner; import io.codeka.gaia.stacks.bo.Job; +import io.codeka.gaia.stacks.bo.StepType; import io.codeka.gaia.stacks.repository.JobRepository; +import io.codeka.gaia.stacks.repository.StackRepository; +import io.codeka.gaia.stacks.workflow.JobWorkflow; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; @@ -13,10 +18,17 @@ public class JobRestController { private JobRepository jobRepository; + private StackRepository stackRepository; + private TerraformModuleRepository moduleRepository; + private StackRunner stackRunner; @Autowired - public JobRestController(JobRepository jobRepository) { + public JobRestController(JobRepository jobRepository, StackRepository stackRepository, + TerraformModuleRepository moduleRepository, StackRunner stackRunner) { this.jobRepository = jobRepository; + this.stackRepository = stackRepository; + this.moduleRepository = moduleRepository; + this.stackRunner = stackRunner; } @GetMapping(params = "stackId") @@ -29,6 +41,19 @@ public Job job(@PathVariable String id){ return this.jobRepository.findById(id).orElseThrow(JobNotFoundException::new); } + @PostMapping("/{id}/{stepType}") + public void planOrApplyJob(@PathVariable String id, @PathVariable StepType stepType) { + var job = this.jobRepository.findById(id).orElseThrow(JobNotFoundException::new); + var stack = this.stackRepository.findById(job.getStackId()).orElseThrow(); + var module = this.moduleRepository.findById(stack.getModuleId()).orElseThrow(); + + if (StepType.PLAN == stepType) { + this.stackRunner.plan(new JobWorkflow(job), module, stack); + } else { + this.stackRunner.apply(new JobWorkflow(job), module, stack); + } + } + } @ResponseStatus(HttpStatus.NOT_FOUND) diff --git a/src/main/java/io/codeka/gaia/stacks/controller/StackController.java b/src/main/java/io/codeka/gaia/stacks/controller/StackController.java index d22335c74..d7a008bce 100644 --- a/src/main/java/io/codeka/gaia/stacks/controller/StackController.java +++ b/src/main/java/io/codeka/gaia/stacks/controller/StackController.java @@ -3,6 +3,7 @@ import io.codeka.gaia.modules.repository.TerraformModuleRepository; import io.codeka.gaia.runner.StackRunner; import io.codeka.gaia.stacks.bo.Job; +import io.codeka.gaia.stacks.bo.JobType; import io.codeka.gaia.stacks.repository.JobRepository; import io.codeka.gaia.stacks.repository.StackRepository; import io.codeka.gaia.teams.bo.User; @@ -13,8 +14,6 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.ResponseBody; -import java.util.UUID; - @Controller public class StackController { @@ -35,70 +34,62 @@ public StackController(StackRepository stackRepository, StackRunner stackRunner, } @GetMapping("/modules/{moduleId}/run") - public String newStack(@PathVariable String moduleId, Model model){ + public String newStack(@PathVariable String moduleId, Model model) { model.addAttribute("moduleId", moduleId); return "new_stack"; } @GetMapping("/stacks") - public String listStacks(){ + public String listStacks() { return "stacks"; } @GetMapping("/stacks/{stackId}") - public String editStack(@PathVariable String stackId, Model model){ + public String editStack(@PathVariable String stackId, Model model) { // checking if the stack exists // TODO throw an exception (404) if not - if(stackRepository.existsById(stackId)){ + if (stackRepository.existsById(stackId)) { model.addAttribute("stackId", stackId); } return "stack"; } @GetMapping("/stacks/{stackId}/{jobType}") - public String startJob(@PathVariable String stackId, @PathVariable String jobType, Model model, User user){ + public String launchJob(@PathVariable String stackId, @PathVariable JobType jobType, Model model, User user) { // get the stack var stack = this.stackRepository.findById(stackId).orElseThrow(StackNotFoundException::new); model.addAttribute("stackId", stackId); - // create a new job - var job = new Job(user); - job.setId(UUID.randomUUID().toString()); - job.setStackId(stackId); - - model.addAttribute("jobId", job.getId()); - // get the module var module = this.terraformModuleRepository.findById(stack.getModuleId()).orElseThrow(); - if ("apply".equals(jobType)) { - this.stackRunner.apply(job, module, stack); - } else if ("preview".equals(jobType)) { - this.stackRunner.plan(job, module, stack); - } else if ("stop".equals(jobType)) { - this.stackRunner.stop(job, module, stack); - } + // create a new job + var job = new Job(jobType, stackId, user); + job.setCliVersion(module.getCliVersion()); + jobRepository.save(job); + model.addAttribute("jobId", job.getId()); return "job"; } @GetMapping("/stacks/{stackId}/jobs/{jobId}") - public String viewJob(@PathVariable String stackId, @PathVariable String jobId, Model model){ + public String viewJob(@PathVariable String stackId, @PathVariable String jobId, Model model) { // checking if the stack exists // TODO throw an exception (404) if not - if(stackRepository.existsById(stackId)){ + if (stackRepository.existsById(stackId)) { model.addAttribute("stackId", stackId); } - if(jobRepository.existsById(jobId)){ + if (jobRepository.existsById(jobId)) { model.addAttribute("jobId", jobId); } + model.addAttribute("edition", true); return "job"; } @GetMapping("/api/stacks/{stackId}/jobs/{jobId}") @ResponseBody - public Job getJob(@PathVariable String stackId, @PathVariable String jobId){ + public Job getJob(@PathVariable String stackId, @PathVariable String jobId) { return this.stackRunner.getJob(jobId); } diff --git a/src/main/java/io/codeka/gaia/stacks/repository/StepRepository.java b/src/main/java/io/codeka/gaia/stacks/repository/StepRepository.java new file mode 100644 index 000000000..418a6d7e9 --- /dev/null +++ b/src/main/java/io/codeka/gaia/stacks/repository/StepRepository.java @@ -0,0 +1,12 @@ +package io.codeka.gaia.stacks.repository; + +import io.codeka.gaia.stacks.bo.Step; +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.stereotype.Repository; + +/** + * Repository for steps + */ +@Repository +public interface StepRepository extends MongoRepository { +} diff --git a/src/main/java/io/codeka/gaia/stacks/service/StackCostCalculator.java b/src/main/java/io/codeka/gaia/stacks/service/StackCostCalculator.java index f3a3afa5c..9026c31e2 100644 --- a/src/main/java/io/codeka/gaia/stacks/service/StackCostCalculator.java +++ b/src/main/java/io/codeka/gaia/stacks/service/StackCostCalculator.java @@ -55,11 +55,11 @@ public BigDecimal calculateRunningCostEstimation(Stack stack){ for(var job : jobs ){ // add - if(job.getType() == JobType.RUN && job.getStatus() == JobStatus.FINISHED){ + if(job.getType() == JobType.RUN && job.getStatus() == JobStatus.APPLY_FINISHED){ start =job.getStartDateTime(); end = LocalDateTime.now(); } - if(job.getType() == JobType.STOP && job.getStatus() == JobStatus.FINISHED){ + if(job.getType() == JobType.DESTROY && job.getStatus() == JobStatus.APPLY_FINISHED){ end = job.getStartDateTime(); duration = duration.plus(Duration.between(start, end)); diff --git a/src/main/java/io/codeka/gaia/stacks/workflow/JobWorkflow.java b/src/main/java/io/codeka/gaia/stacks/workflow/JobWorkflow.java new file mode 100644 index 000000000..15ee1cc98 --- /dev/null +++ b/src/main/java/io/codeka/gaia/stacks/workflow/JobWorkflow.java @@ -0,0 +1,93 @@ +package io.codeka.gaia.stacks.workflow; + +import io.codeka.gaia.stacks.bo.Job; +import io.codeka.gaia.stacks.bo.JobStatus; +import io.codeka.gaia.stacks.bo.Step; +import io.codeka.gaia.stacks.workflow.state.*; + +import java.util.Objects; + +/** + * Manages the workflow of a job and its steps + */ +public class JobWorkflow { + + private Job job; + private Step currentStep; + private JobState state; + + public JobWorkflow(Job job) { + this.job = Objects.requireNonNull(job, "the job must be not null"); + this.state = evalInitialState(job.getStatus()); + } + + public void plan() { + this.state.plan(this); + } + + public void apply() { + this.state.apply(this); + } + + public void end() { + this.state.end(this); + } + + public void fail() { + this.state.fail(this); + } + + public Job getJob() { + return job; + } + + public Step getCurrentStep() { + return currentStep; + } + + public void setCurrentStep(Step currentStep) { + this.currentStep = currentStep; + } + + public JobState getState() { + return state; + } + + public void setState(JobState state) { + this.state = state; + } + + /** + * To enable to continue an interrupted workflow, we need to set the state at the right position. + * + * @param jobStatus status of the job + * @return then correct state + */ + JobState evalInitialState(JobStatus jobStatus) { + JobState result = new NotStartedState(); + if (jobStatus == null) { + return result; + } + switch (jobStatus) { + case PLAN_STARTED: + result = new PlanStartedState(); + break; + case PLAN_FINISHED: + result = new PlanFinishedState(); + break; + case PLAN_FAILED: + result = new PlanFailedState(); + break; + case APPLY_STARTED: + result = new ApplyStartedState(); + break; + case APPLY_FINISHED: + result = new ApplyFinishedState(); + break; + case APPLY_FAILED: + result = new ApplyFailedState(); + break; + } + return result; + } +} diff --git a/src/main/java/io/codeka/gaia/stacks/workflow/state/ApplyFailedState.java b/src/main/java/io/codeka/gaia/stacks/workflow/state/ApplyFailedState.java new file mode 100644 index 000000000..b6e825fc0 --- /dev/null +++ b/src/main/java/io/codeka/gaia/stacks/workflow/state/ApplyFailedState.java @@ -0,0 +1,28 @@ +package io.codeka.gaia.stacks.workflow.state; + +import io.codeka.gaia.stacks.workflow.JobWorkflow; + +/** + * Describes a job which apply has been failed + */ +public class ApplyFailedState implements JobState { + @Override + public void plan(JobWorkflow jobWorkflow) { + throw new UnsupportedOperationException("Unable to start a plan after an apply failed"); + } + + @Override + public void apply(JobWorkflow jobWorkflow) { + throw new UnsupportedOperationException("Unable to start an apply after an apply failed"); + } + + @Override + public void end(JobWorkflow jobWorkflow) { + throw new UnsupportedOperationException("Unable to end an apply failed"); + } + + @Override + public void fail(JobWorkflow jobWorkflow) { + throw new UnsupportedOperationException("Unable to fail an apply failed"); + } +} diff --git a/src/main/java/io/codeka/gaia/stacks/workflow/state/ApplyFinishedState.java b/src/main/java/io/codeka/gaia/stacks/workflow/state/ApplyFinishedState.java new file mode 100644 index 000000000..321a7c705 --- /dev/null +++ b/src/main/java/io/codeka/gaia/stacks/workflow/state/ApplyFinishedState.java @@ -0,0 +1,28 @@ +package io.codeka.gaia.stacks.workflow.state; + +import io.codeka.gaia.stacks.workflow.JobWorkflow; + +/** + * Describes a job which apply has been finished + */ +public class ApplyFinishedState implements JobState { + @Override + public void plan(JobWorkflow jobWorkflow) { + throw new UnsupportedOperationException("Unable to start a plan after an apply finished"); + } + + @Override + public void apply(JobWorkflow jobWorkflow) { + throw new UnsupportedOperationException("Unable to start an apply after an apply finished"); + } + + @Override + public void end(JobWorkflow jobWorkflow) { + throw new UnsupportedOperationException("Unable to end an apply finished"); + } + + @Override + public void fail(JobWorkflow jobWorkflow) { + throw new UnsupportedOperationException("Unable to fail an apply finished"); + } +} diff --git a/src/main/java/io/codeka/gaia/stacks/workflow/state/ApplyStartedState.java b/src/main/java/io/codeka/gaia/stacks/workflow/state/ApplyStartedState.java new file mode 100644 index 000000000..a6ea87d16 --- /dev/null +++ b/src/main/java/io/codeka/gaia/stacks/workflow/state/ApplyStartedState.java @@ -0,0 +1,33 @@ +package io.codeka.gaia.stacks.workflow.state; + +import io.codeka.gaia.stacks.bo.JobStatus; +import io.codeka.gaia.stacks.workflow.JobWorkflow; + +/** + * Describes a job which apply has been started + */ +public class ApplyStartedState implements JobState { + @Override + public void plan(JobWorkflow jobWorkflow) { + throw new UnsupportedOperationException("Unable to start a plan after an apply started"); + } + + @Override + public void apply(JobWorkflow jobWorkflow) { + throw new UnsupportedOperationException("Unable to start an apply after an apply started"); + } + + @Override + public void end(JobWorkflow jobWorkflow) { + jobWorkflow.getCurrentStep().end(); + jobWorkflow.getJob().end(JobStatus.APPLY_FINISHED); + jobWorkflow.setState(new ApplyFinishedState()); + } + + @Override + public void fail(JobWorkflow jobWorkflow) { + jobWorkflow.getCurrentStep().fail(); + jobWorkflow.getJob().end(JobStatus.APPLY_FAILED); + jobWorkflow.setState(new ApplyFailedState()); + } +} diff --git a/src/main/java/io/codeka/gaia/stacks/workflow/state/JobState.java b/src/main/java/io/codeka/gaia/stacks/workflow/state/JobState.java new file mode 100644 index 000000000..e4c5abc68 --- /dev/null +++ b/src/main/java/io/codeka/gaia/stacks/workflow/state/JobState.java @@ -0,0 +1,18 @@ +package io.codeka.gaia.stacks.workflow.state; + +import io.codeka.gaia.stacks.workflow.JobWorkflow; + +/** + * Describe the state of job and its possible actions + */ +public interface JobState { + + void plan(JobWorkflow jobWorkflow); + + void apply(JobWorkflow jobWorkflow); + + void end(JobWorkflow jobWorkflow); + + void fail(JobWorkflow jobWorkflow); + +} diff --git a/src/main/java/io/codeka/gaia/stacks/workflow/state/NotStartedState.java b/src/main/java/io/codeka/gaia/stacks/workflow/state/NotStartedState.java new file mode 100644 index 000000000..56621badc --- /dev/null +++ b/src/main/java/io/codeka/gaia/stacks/workflow/state/NotStartedState.java @@ -0,0 +1,38 @@ +package io.codeka.gaia.stacks.workflow.state; + +import io.codeka.gaia.stacks.bo.Step; +import io.codeka.gaia.stacks.bo.StepType; +import io.codeka.gaia.stacks.workflow.JobWorkflow; + +/** + * Describes a new job before any action + */ +public class NotStartedState implements JobState { + @Override + public void plan(JobWorkflow jobWorkflow) { + var job = jobWorkflow.getJob(); + job.start(); + + var step = new Step(StepType.PLAN, job.getId()); + job.getSteps().add(step); + jobWorkflow.setCurrentStep(step); + step.start(); + + jobWorkflow.setState(new PlanStartedState()); + } + + @Override + public void apply(JobWorkflow jobWorkflow) { + throw new UnsupportedOperationException("Unable to start an apply of a job not even started"); + } + + @Override + public void end(JobWorkflow jobWorkflow) { + throw new UnsupportedOperationException("Unable to end the step of a job not even started"); + } + + @Override + public void fail(JobWorkflow jobWorkflow) { + throw new UnsupportedOperationException("Unable to fail the step of a job not even started"); + } +} diff --git a/src/main/java/io/codeka/gaia/stacks/workflow/state/PlanFailedState.java b/src/main/java/io/codeka/gaia/stacks/workflow/state/PlanFailedState.java new file mode 100644 index 000000000..4fc4ee1b6 --- /dev/null +++ b/src/main/java/io/codeka/gaia/stacks/workflow/state/PlanFailedState.java @@ -0,0 +1,28 @@ +package io.codeka.gaia.stacks.workflow.state; + +import io.codeka.gaia.stacks.workflow.JobWorkflow; + +/** + * Describes a job which plan has been failed + */ +public class PlanFailedState implements JobState { + @Override + public void plan(JobWorkflow jobWorkflow) { + throw new UnsupportedOperationException("Unable to start a plan after a plan failed"); + } + + @Override + public void apply(JobWorkflow jobWorkflow) { + throw new UnsupportedOperationException("Unable to start an apply after a plan failed"); + } + + @Override + public void end(JobWorkflow jobWorkflow) { + throw new UnsupportedOperationException("Unable to end a plan failed"); + } + + @Override + public void fail(JobWorkflow jobWorkflow) { + throw new UnsupportedOperationException("Unable to fail a plan failed"); + } +} diff --git a/src/main/java/io/codeka/gaia/stacks/workflow/state/PlanFinishedState.java b/src/main/java/io/codeka/gaia/stacks/workflow/state/PlanFinishedState.java new file mode 100644 index 000000000..08163eaf1 --- /dev/null +++ b/src/main/java/io/codeka/gaia/stacks/workflow/state/PlanFinishedState.java @@ -0,0 +1,39 @@ +package io.codeka.gaia.stacks.workflow.state; + +import io.codeka.gaia.stacks.bo.JobStatus; +import io.codeka.gaia.stacks.bo.Step; +import io.codeka.gaia.stacks.bo.StepType; +import io.codeka.gaia.stacks.workflow.JobWorkflow; + +/** + * Describes a job which plan has been finished + */ +public class PlanFinishedState implements JobState { + @Override + public void plan(JobWorkflow jobWorkflow) { + throw new UnsupportedOperationException("Unable to start a plan after a plan finished"); + } + + @Override + public void apply(JobWorkflow jobWorkflow) { + var job = jobWorkflow.getJob(); + job.setStatus(JobStatus.APPLY_STARTED); + + var step = new Step(StepType.APPLY, job.getId()); + job.getSteps().add(step); + jobWorkflow.setCurrentStep(step); + step.start(); + + jobWorkflow.setState(new ApplyStartedState()); + } + + @Override + public void end(JobWorkflow jobWorkflow) { + throw new UnsupportedOperationException("Unable to end an plan finished"); + } + + @Override + public void fail(JobWorkflow jobWorkflow) { + throw new UnsupportedOperationException("Unable to fail an plan finished"); + } +} diff --git a/src/main/java/io/codeka/gaia/stacks/workflow/state/PlanStartedState.java b/src/main/java/io/codeka/gaia/stacks/workflow/state/PlanStartedState.java new file mode 100644 index 000000000..c45e9a1b7 --- /dev/null +++ b/src/main/java/io/codeka/gaia/stacks/workflow/state/PlanStartedState.java @@ -0,0 +1,33 @@ +package io.codeka.gaia.stacks.workflow.state; + +import io.codeka.gaia.stacks.bo.JobStatus; +import io.codeka.gaia.stacks.workflow.JobWorkflow; + +/** + * Describes a job which plan has been started + */ +public class PlanStartedState implements JobState { + @Override + public void plan(JobWorkflow jobWorkflow) { + throw new UnsupportedOperationException("Unable to start a plan after a plan started"); + } + + @Override + public void apply(JobWorkflow jobWorkflow) { + throw new UnsupportedOperationException("Unable to start an apply after a plan started"); + } + + @Override + public void end(JobWorkflow jobWorkflow) { + jobWorkflow.getCurrentStep().end(); + jobWorkflow.getJob().end(JobStatus.PLAN_FINISHED); + jobWorkflow.setState(new PlanFinishedState()); + } + + @Override + public void fail(JobWorkflow jobWorkflow) { + jobWorkflow.getCurrentStep().fail(); + jobWorkflow.getJob().end(JobStatus.PLAN_FAILED); + jobWorkflow.setState(new PlanFailedState()); + } +} diff --git a/src/main/resources/static/css/style.css b/src/main/resources/static/css/style.css index 8a7ed9aee..666e1984f 100755 --- a/src/main/resources/static/css/style.css +++ b/src/main/resources/static/css/style.css @@ -1813,7 +1813,7 @@ button.main_bt { /* border-left-color: #ff9800;*/ /*}*/ -.job_list li.RUNNING { +.job_list li[class*=STARTED] { border-left-color: #2196f3; } @@ -1821,11 +1821,11 @@ button.main_bt { /* border-left-color: #673ab7;*/ /*}*/ -.job_list li.FAILED { +.job_list li[class*=FAILED] { border-left-color: #e91e63; } -.job_list li.FINISHED { +.job_list li[class*=FINISHED] { border-left-color: #1ed085; } @@ -1862,27 +1862,23 @@ button.main_bt { margin-left: 5px; } -.job_list .job .job_attr_value.FINISHED { - color: #1ed085; -} - -.job_list .job .job_attr_value.RUNNING { +.job_list .job .job_attr_value[class*=STARTED] { color: #2196f3; } -.job_list .job .job_attr_value.FAILED { - color: #e91e63; +.job_list .job .job_attr_value[class*=FINISHED] { + color: #1ed085; } -.job_list .job .job_attr_value.PREVIEW { - color: #17a2b8; +.job_list .job .job_attr_value[class*=FAILED] { + color: #dc3545; } .job_list .job .job_attr_value.RUN { color: #007bff; } -.job_list .job .job_attr_value.STOP { +.job_list .job .job_attr_value.DESTROY { color: #dc3545; } diff --git a/src/main/resources/templates/job.html b/src/main/resources/templates/job.html index 58b0e42d5..58bf2d054 100644 --- a/src/main/resources/templates/job.html +++ b/src/main/resources/templates/job.html @@ -41,19 +41,26 @@

Stack {{stack.name}}

- - - - - + + + + + + + + + + -
@@ -67,6 +74,7 @@

Stack {{stack.name}}

+ @@ -78,63 +86,82 @@

Stack {{stack.name}}

+
+
+
+
diff --git a/src/main/resources/templates/new_stack.html b/src/main/resources/templates/new_stack.html index 77cc39a17..cf1aed71b 100644 --- a/src/main/resources/templates/new_stack.html +++ b/src/main/resources/templates/new_stack.html @@ -228,7 +228,7 @@

Run

method: "POST" }).then(saved => { this.stack = saved; - window.location = `/stacks/${this.stack.id}/apply`; + window.location = `/stacks/${this.stack.id}/RUN`; }) } diff --git a/src/main/resources/templates/stack.html b/src/main/resources/templates/stack.html index 41a42fbe5..b4622c67f 100644 --- a/src/main/resources/templates/stack.html +++ b/src/main/resources/templates/stack.html @@ -54,10 +54,7 @@ Save Run Update - - Preview - - Stop + Destroy @@ -272,32 +269,6 @@

Job history

}), template: "#stack-controls", methods: { - previewStack: function() { - let current_stack = JSON.stringify(this.stack); - if (current_stack === stack_backup) { - window.location = `/stacks/${this.stack.id}/preview`; - } else { - this.confirmDialog('Preview request', 'Modifications must be saved before. Continue?') - .then(value => { - if (value) { - return this.saveStack().then(_ => window.location = `/stacks/${this.stack.id}/preview`); - } - }) - } - }, - runStack: function () { - let current_stack = JSON.stringify(this.stack); - if (current_stack === stack_backup) { - window.location = `/stacks/${this.stack.id}/apply`; - } else { - this.confirmDialog('Run request', 'Modifications must be saved before. Continue?') - .then(value => { - if (value) { - return this.saveStack().then(_ => window.location = `/stacks/${this.stack.id}/apply`); - } - }) - } - }, saveStack: function(){ const message = Messenger().post({ type: "info", @@ -319,13 +290,26 @@

Job history

} }); }, + runStack: function () { + let current_stack = JSON.stringify(this.stack); + if (current_stack === stack_backup) { + window.location = `/stacks/${this.stack.id}/RUN`; + } else { + this.confirmDialog('Run request', 'Modifications must be saved before. Continue?') + .then(value => { + if (value) { + return this.saveStack().then(_ => window.location = `/stacks/${this.stack.id}/RUN`); + } + }) + } + }, stopStack: function () { // ask for confirmation this.confirmDialog('Stop request', 'This will completely stop the stack. Continue?') .then(value => { if (value) { // redirect - window.location = `/stacks/${this.stack.id}/stop`; + window.location = `/stacks/${this.stack.id}/DESTROY`; } }); }, diff --git a/src/main/resources/templates/vue_templates/console.vue b/src/main/resources/templates/vue_templates/console.vue index d077209e3..be2893144 100644 --- a/src/main/resources/templates/vue_templates/console.vue +++ b/src/main/resources/templates/vue_templates/console.vue @@ -1,11 +1,11 @@