diff --git a/src/main/client/app/pages/stacks/job/job-step.vue b/src/main/client/app/pages/stacks/job/job-step.vue index d74ae3167..722e47afc 100644 --- a/src/main/client/app/pages/stacks/job/job-step.vue +++ b/src/main/client/app/pages/stacks/job/job-step.vue @@ -78,7 +78,7 @@ diff --git a/src/main/client/app/pages/stacks/job/job.vue b/src/main/client/app/pages/stacks/job/job.vue index 67fc79419..fffba6a69 100644 --- a/src/main/client/app/pages/stacks/job/job.vue +++ b/src/main/client/app/pages/stacks/job/job.vue @@ -41,7 +41,6 @@ applyJob, deleteJob, getJob, - planJob, retryJob, } from '@/shared/api/jobs-api'; import { getStack } from '@/shared/api/stacks-api'; @@ -83,6 +82,7 @@ }, isSecondStepDoable() { return this.job.status + && !this.job.status.includes('PENDING') && !this.job.status.includes('STARTED') && !this.job.status.includes('FAILED') && !this.job.status.includes('APPLY'); @@ -91,29 +91,20 @@ async created() { [this.stack, this.job] = await Promise.all([getStack(this.stackId), this.refreshJobUntilCompletion()]); this.loaded = true; - if (!this.job.status) { - await this.planJob(); - } + + this.refreshIntervalId = setInterval(this.refreshJobUntilCompletion, INTERVAL_TIMEOUT); }, destroyed() { clearInterval(this.refreshIntervalId); }, methods: { - async planJob() { - await planJob(this.jobId); - await this.waitUntilJobStarted(); - - this.refreshIntervalId = setInterval(this.refreshJobUntilCompletion, INTERVAL_TIMEOUT); - }, async applyJob() { await applyJob(this.jobId); - await this.waitUntilJobStarted(); this.refreshIntervalId = setInterval(this.refreshJobUntilCompletion, INTERVAL_TIMEOUT); }, async retryJob() { await retryJob(this.jobId); - await this.waitUntilJobStarted(); this.refreshIntervalId = setInterval(this.refreshJobUntilCompletion, INTERVAL_TIMEOUT); }, @@ -132,24 +123,11 @@ }, async refreshJobUntilCompletion() { this.job = await getJob(this.jobId); - if (this.job.status && !this.job.status.includes('STARTED')) { + if (this.job.status.includes('FINISHED') || this.job.status.includes('FAILED')) { clearInterval(this.refreshIntervalId); } return this.job; }, - // wait until the job is in "started" state - async waitUntilJobStarted() { - const poll = async (resolve) => { - this.job = await getJob(this.jobId); - if (this.job.status && this.job.status.includes('STARTED')) { - resolve(); - } else { - setTimeout(() => poll(resolve), 500); - } - }; - return new Promise(poll); - }, - }, }; diff --git a/src/main/java/io/gaia_app/runner/DockerRunner.java b/src/main/java/io/gaia_app/runner/DockerRunner.java deleted file mode 100644 index 46d6edebb..000000000 --- a/src/main/java/io/gaia_app/runner/DockerRunner.java +++ /dev/null @@ -1,136 +0,0 @@ -package io.gaia_app.runner; - -import com.github.dockerjava.api.DockerClient; -import com.github.dockerjava.api.async.ResultCallback; -import com.github.dockerjava.api.command.CreateContainerCmd; -import com.github.dockerjava.api.command.CreateContainerResponse; -import com.github.dockerjava.api.exception.DockerException; -import com.github.dockerjava.api.model.*; -import io.gaia_app.settings.bo.Settings; -import io.gaia_app.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.Closeable; -import java.io.IOException; -import java.io.PipedInputStream; -import java.io.PipedOutputStream; -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; -import java.util.concurrent.TimeUnit; - -/** - * Service to run docker container - */ -@Service -public class DockerRunner { - - private static final Logger LOG = LoggerFactory.getLogger(DockerRunner.class); - - private DockerClient dockerClient; - - private Settings settings; - - @Autowired - public DockerRunner(DockerClient dockerClient, Settings settings) { - this.dockerClient = dockerClient; - 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(); - - // add credentials of the job, if any - if (job.getCredentials() != null){ - env.addAll(job.getCredentials().toEnv()); - } - - // pulling the image - dockerClient.pullImageCmd(job.getTerraformImage().image()) - .start() - .awaitCompletion(); - - // create a new container - var createContainerCmd = dockerClient.createContainerCmd(job.getTerraformImage().image()) - // resetting entrypoint to empty - .withEntrypoint(new String[]{}) - // using /bin/sh as command - .withCmd("/bin/sh") - // using env vars - .withEnv(env) - // open stdin - .withTty(false) - .withStdinOpen(true) - // bind mount docker sock (to be able to use docker provider in terraform) - .withHostConfig(HostConfig.newHostConfig().withBinds(new Bind("/var/run/docker.sock", new Volume("/var/run/docker.sock")))); - - var container = createContainerCmd.exec(); - var containerId = container.getId(); - - // start the container - dockerClient.startContainerCmd(containerId).exec(); - - // attaching the outputs in a background thread - var step = jobWorkflow.getCurrentStep(); - - // write the content of the script to the container's std in - try ( - var out = new PipedOutputStream(); - var in = new PipedInputStream(out); - var containerLogsCallback = new ResultCallback.Adapter(){ - @Override - public void onNext(Frame frame) { - try { - step.getLogsWriter().write(new String(frame.getPayload())); - } catch (IOException e) { - e.printStackTrace(); - } - super.onNext(frame); - } - }; - ){ - dockerClient.attachContainerCmd(container.getId()) - .withFollowStream(true) - .withStdOut(true) - .withStdErr(true) - .withStdIn(in) - .exec(containerLogsCallback); - - out.write((script + "\n").getBytes()); - out.flush(); - - containerLogsCallback.awaitCompletion(1800, TimeUnit.SECONDS); - } - - // wait for the container to exit - var containerExit = dockerClient.waitContainerCmd(containerId) - .start() - // with a timeout of 30 minutes - .awaitStatusCode(1800, TimeUnit.SECONDS); - - dockerClient.removeContainerCmd(containerId).exec(); - - return containerExit; - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - LOG.error("Interrupted Exception", e); - return 99; - } catch (DockerException | IOException e) { - LOG.error("Exception when running job", e); - return 99; - } - } - -} diff --git a/src/main/java/io/gaia_app/runner/StackCommandBuilder.java b/src/main/java/io/gaia_app/runner/RunnerCommandBuilder.java similarity index 93% rename from src/main/java/io/gaia_app/runner/StackCommandBuilder.java rename to src/main/java/io/gaia_app/runner/RunnerCommandBuilder.java index ae3f7adb0..3602f00bd 100644 --- a/src/main/java/io/gaia_app/runner/StackCommandBuilder.java +++ b/src/main/java/io/gaia_app/runner/RunnerCommandBuilder.java @@ -20,7 +20,7 @@ * A builder class to create stack commands */ @Component -public class StackCommandBuilder { +public class RunnerCommandBuilder { private Settings settings; private RunnerApiSecurityConfig.RunnerApiSecurityProperties runnerApiSecurityProperties; @@ -28,7 +28,7 @@ public class StackCommandBuilder { private List registryOAuth2Providers; @Autowired - StackCommandBuilder(Settings settings, Mustache terraformMustache, List registryOAuth2Providers, RunnerApiSecurityConfig.RunnerApiSecurityProperties runnerApiSecurityProperties) { + RunnerCommandBuilder(Settings settings, Mustache terraformMustache, List registryOAuth2Providers, RunnerApiSecurityConfig.RunnerApiSecurityProperties runnerApiSecurityProperties) { this.settings = settings; this.terraformMustache = terraformMustache; this.registryOAuth2Providers = registryOAuth2Providers; diff --git a/src/main/java/io/gaia_app/runner/RunnerController.java b/src/main/java/io/gaia_app/runner/RunnerController.java index 3bd7a6288..162f7d217 100644 --- a/src/main/java/io/gaia_app/runner/RunnerController.java +++ b/src/main/java/io/gaia_app/runner/RunnerController.java @@ -1,11 +1,20 @@ package io.gaia_app.runner; +import io.gaia_app.credentials.CredentialsService; +import io.gaia_app.stacks.bo.*; +import io.gaia_app.stacks.repository.JobRepository; import io.gaia_app.stacks.repository.StackRepository; +import io.gaia_app.stacks.repository.StepRepository; +import io.gaia_app.stacks.workflow.JobWorkflow; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.server.ResponseStatusException; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; /** * Controller for the operations that are called by the runner only @@ -17,9 +26,123 @@ public class RunnerController { @Autowired private StackRepository stackRepository; + @Autowired + private JobRepository jobRepository; + + @Autowired + private StepRepository stepRepository; + + @Autowired + private CredentialsService credentialsService; + + @Autowired + private RunnerCommandBuilder runnerCommandBuilder; + @GetMapping(value = "/stacks/{id}.tfvars", produces = "text/plain") public String tfvars(@PathVariable String id){ var stack = stackRepository.findById(id).orElseThrow(); return stack.tfvars(); } + + /** + * Gets the first job step that can be run by the runner (one in "pending" state) + */ + @GetMapping("/steps/request") + public Map findFirstRunnableJob() { + var job = this.jobRepository.findFirstByStatusEqualsOrStatusEquals(JobStatus.PLAN_PENDING, JobStatus.APPLY_PENDING) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); + var stack = this.stackRepository.findById(job.getStackId()).orElseThrow(); + + // get the workflow + var workflow = new JobWorkflow(job); + // start the workflow + var step = workflow.startWorkflow(); + + // saving the job + this.jobRepository.save(job); + this.stepRepository.save(step); + + var script = ""; + // generate the script + if(job.getType() == JobType.RUN){ + if(step.getType() == StepType.PLAN) { + script = runnerCommandBuilder.buildPlanScript(job, stack, stack.getModule()); + } + else { + script = runnerCommandBuilder.buildApplyScript(job, stack, stack.getModule()); + } + } + else { // destroy + if(step.getType() == StepType.PLAN) { + script = runnerCommandBuilder.buildPlanDestroyScript(job, stack, stack.getModule()); + } + else { + script = runnerCommandBuilder.buildDestroyScript(job, stack, stack.getModule()); + } + } + + List env = Collections.emptyList(); + if(stack.getCredentialsId() != null){ + var credentials = this.credentialsService.load(stack.getCredentialsId()).orElse(null); + if(credentials != null){ + env = credentials.toEnv(); + } + } + + return Map.of( + "step", step, + "script", script, + "env", env, + "image", job.getTerraformImage().image()); + } + + /** + * Updates the step logs + */ + @PutMapping("/steps/{stepId}/logs") + public void updateLogs(@PathVariable String stepId, @RequestBody String logs) { + var step = this.stepRepository.findById(stepId).orElseThrow(); + step.getLogs().add(logs); + this.stepRepository.save(step); + } + + /** + * Updates the step status + */ + @PutMapping("/steps/{stepId}/status") + public void updateStepStatus(@PathVariable String stepId, @RequestBody int status) { + var step = this.stepRepository.findById(stepId).orElseThrow(); + + // reload the job to check workflow status + var job = this.jobRepository.findById(step.getJobId()).orElseThrow(); + + // rebuild the workflow + var workflow = new JobWorkflow(job); + workflow.setCurrentStep(step); + workflow.next(status); + + // save the job & step to update their status + this.stepRepository.save(step); + this.jobRepository.save(job); + } + + /** + * Updates the job state + */ + @PutMapping("/steps/{stepId}/start") + public void startStep(@PathVariable String stepId) { + // getting step + var step = this.stepRepository.findById(stepId).orElseThrow(); + + // reload the job to check workflow status + var job = this.jobRepository.findById(step.getJobId()).orElseThrow(); + + // rebuild the workflow and start it + var workflow = new JobWorkflow(job); + workflow.startWorkflow(); + + // save the job & step to update their status + this.stepRepository.save(step); + this.jobRepository.save(job); + } } diff --git a/src/main/java/io/gaia_app/runner/StackRunner.java b/src/main/java/io/gaia_app/runner/StackRunner.java deleted file mode 100644 index 97e72c812..000000000 --- a/src/main/java/io/gaia_app/runner/StackRunner.java +++ /dev/null @@ -1,167 +0,0 @@ -package io.gaia_app.runner; - -import io.gaia_app.credentials.CredentialsService; -import io.gaia_app.modules.bo.TerraformModule; -import io.gaia_app.stacks.bo.Job; -import io.gaia_app.stacks.bo.JobType; -import io.gaia_app.stacks.bo.Stack; -import io.gaia_app.stacks.bo.StackState; -import io.gaia_app.stacks.repository.JobRepository; -import io.gaia_app.stacks.repository.StackRepository; -import io.gaia_app.stacks.repository.StepRepository; -import io.gaia_app.stacks.workflow.JobWorkflow; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.scheduling.annotation.Async; -import org.springframework.stereotype.Service; - -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; -import java.util.function.Consumer; -import java.util.function.IntConsumer; -import java.util.function.Supplier; - -/** - * Runs a module instance - */ -@Service -public class StackRunner { - - private DockerRunner dockerRunner; - private StackCommandBuilder stackCommandBuilder; - private StackRepository stackRepository; - private JobRepository jobRepository; - private StepRepository stepRepository; - private CredentialsService credentialsService; - - private Map jobs = new HashMap<>(); - - @Autowired - public StackRunner(DockerRunner dockerRunner, StackCommandBuilder stackCommandBuilder, - StackRepository stackRepository, JobRepository jobRepository, StepRepository stepRepository, CredentialsService credentialsService) { - this.dockerRunner = dockerRunner; - this.stackCommandBuilder = stackCommandBuilder; - this.stackRepository = stackRepository; - this.jobRepository = jobRepository; - this.stepRepository = stepRepository; - this.credentialsService = credentialsService; - } - - private String managePlanScript(Job job, Stack stack, TerraformModule module) { - if (JobType.RUN == job.getType()) { - return stackCommandBuilder.buildPlanScript(job, stack, module); - } - return stackCommandBuilder.buildPlanDestroyScript(job, 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(); - } - } - - private String manageApplyScript(Job job, Stack stack, TerraformModule module) { - if (JobType.RUN == job.getType()) { - return stackCommandBuilder.buildApplyScript(job, stack, module); - } - return stackCommandBuilder.buildDestroyScript(job, stack, module); - } - - private void manageApplyResult(Integer result, JobWorkflow jobWorkflow, Stack stack) { - if (result == 0) { - jobWorkflow.end(); - - // update stack information - stack.setState(jobWorkflow.getJob().getType() == JobType.RUN ? StackState.RUNNING : StackState.STOPPED); - stackRepository.save(stack); - } else { - jobWorkflow.fail(); - } - } - - /** - * @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 - */ - 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); - stepRepository.saveAll(job.getSteps()); - jobRepository.save(job); - - // get the wanted script - var script = scriptFn.get(); - - var result = this.dockerRunner.runContainerForJob(jobWorkflow, script); - - // 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) { - if(stack.getCredentialsId() != null){ - jobWorkflow.getJob().setCredentials(this.credentialsService.load(stack.getCredentialsId()).orElseThrow()); - } - treatJob( - jobWorkflow, - JobWorkflow::plan, - () -> managePlanScript(jobWorkflow.getJob(), stack, module), - result -> managePlanResult(result, jobWorkflow, stack) - ); - } - - @Async - public void apply(JobWorkflow jobWorkflow, TerraformModule module, Stack stack) { - if(stack.getCredentialsId() != null){ - jobWorkflow.getJob().setCredentials(this.credentialsService.load(stack.getCredentialsId()).orElseThrow()); - } - treatJob( - jobWorkflow, - JobWorkflow::apply, - () -> manageApplyScript(jobWorkflow.getJob(), stack, module), - result -> manageApplyResult(result, jobWorkflow, stack) - ); - } - - @Async - public void retry(JobWorkflow jobWorkflow, TerraformModule module, Stack stack) { - if(stack.getCredentialsId() != null){ - jobWorkflow.getJob().setCredentials(this.credentialsService.load(stack.getCredentialsId()).orElseThrow()); - } - stepRepository.deleteByJobId(jobWorkflow.getJob().getId()); - treatJob( - jobWorkflow, - JobWorkflow::retry, - () -> managePlanScript(jobWorkflow.getJob(), stack, module), - result -> managePlanResult(result, jobWorkflow, stack) - ); - } - - public Optional getJob(String jobId) { - return Optional.ofNullable(this.jobs.get(jobId)); - } - -} diff --git a/src/main/java/io/gaia_app/runner/StackRunnerException.java b/src/main/java/io/gaia_app/runner/StackRunnerException.java deleted file mode 100644 index 1d3234146..000000000 --- a/src/main/java/io/gaia_app/runner/StackRunnerException.java +++ /dev/null @@ -1,9 +0,0 @@ -package io.gaia_app.runner; - -class StackRunnerException extends Exception { - - StackRunnerException(String message, Throwable cause) { - super(message, cause); - } - -} diff --git a/src/main/java/io/gaia_app/runner/config/DockerConfig.java b/src/main/java/io/gaia_app/runner/config/DockerConfig.java deleted file mode 100644 index 4a602b3f3..000000000 --- a/src/main/java/io/gaia_app/runner/config/DockerConfig.java +++ /dev/null @@ -1,41 +0,0 @@ -package io.gaia_app.runner.config; - -import com.github.dockerjava.api.DockerClient; -import com.github.dockerjava.core.DefaultDockerClientConfig; -import com.github.dockerjava.core.DockerClientConfig; -import com.github.dockerjava.core.DockerClientImpl; -import com.github.dockerjava.okhttp.OkHttpDockerCmdExecFactory; -import io.gaia_app.settings.bo.Settings; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * Configuration of the docker transport - */ -@Configuration -public class DockerConfig { - - /** - * builds the DockerClientConfig based on Gaia's settings - * @param settings Gaia's settings - * @return a docker client configuration - */ - @Bean - DockerClientConfig dockerClientConfig(Settings settings) { - return DefaultDockerClientConfig.createDefaultConfigBuilder() - .withDockerHost(settings.getDockerDaemonUrl()) - .build(); - } - - /** - * builds the docker client - * @param config the configuration (host/registries...) - * @return - */ - @Bean - DockerClient client(DockerClientConfig config){ - return DockerClientImpl.getInstance(config) - .withDockerCmdExecFactory(new OkHttpDockerCmdExecFactory()); - } - -} diff --git a/src/main/java/io/gaia_app/stacks/bo/Job.java b/src/main/java/io/gaia_app/stacks/bo/Job.java index 9d8a8e30a..3c5cfeba0 100644 --- a/src/main/java/io/gaia_app/stacks/bo/Job.java +++ b/src/main/java/io/gaia_app/stacks/bo/Job.java @@ -61,7 +61,7 @@ public void proceed(JobStatus jobStatus) { } public void reset() { - this.status = null; + this.status = JobStatus.PLAN_PENDING; this.startDateTime = null; this.endDateTime = null; this.steps.clear(); diff --git a/src/main/java/io/gaia_app/stacks/bo/JobStatus.java b/src/main/java/io/gaia_app/stacks/bo/JobStatus.java index 8d963c2ff..a23e26c63 100644 --- a/src/main/java/io/gaia_app/stacks/bo/JobStatus.java +++ b/src/main/java/io/gaia_app/stacks/bo/JobStatus.java @@ -1,6 +1,6 @@ package io.gaia_app.stacks.bo; public enum JobStatus { - PLAN_STARTED, PLAN_FINISHED, PLAN_FAILED, - APPLY_STARTED, APPLY_FINISHED, APPLY_FAILED, + PLAN_PENDING, PLAN_STARTED, PLAN_FINISHED, PLAN_FAILED, + APPLY_PENDING, APPLY_STARTED, APPLY_FINISHED, APPLY_FAILED, } diff --git a/src/main/java/io/gaia_app/stacks/bo/Step.java b/src/main/java/io/gaia_app/stacks/bo/Step.java index d7ba7da6f..0fc6fc211 100644 --- a/src/main/java/io/gaia_app/stacks/bo/Step.java +++ b/src/main/java/io/gaia_app/stacks/bo/Step.java @@ -1,12 +1,9 @@ package io.gaia_app.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.LinkedList; +import java.util.List; import java.util.UUID; /** @@ -21,9 +18,7 @@ public class Step { private Long executionTime; private StepType type; private StepStatus status; - @Transient - private StringWriter logsWriter = new StringWriter(); - private String logs; + private List logs = new LinkedList<>(); public Step() { } @@ -42,14 +37,12 @@ public void start() { 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; } @@ -109,20 +102,12 @@ public void setStatus(StepStatus status) { this.status = status; } - public String getLogs() { - if (status == StepStatus.FINISHED || status == StepStatus.FAILED) { - return logs; - } - return logsWriter.toString(); + public List getLogs() { + return logs; } - public void setLogs(String logs) { + public void setLogs(List logs) { this.logs = logs; } - @JsonIgnore - public Writer getLogsWriter() { - return logsWriter; - } - } diff --git a/src/main/java/io/gaia_app/stacks/controller/JobRestController.kt b/src/main/java/io/gaia_app/stacks/controller/JobRestController.kt index bc0e4dab3..d6e55cb01 100644 --- a/src/main/java/io/gaia_app/stacks/controller/JobRestController.kt +++ b/src/main/java/io/gaia_app/stacks/controller/JobRestController.kt @@ -1,8 +1,7 @@ package io.gaia_app.stacks.controller -import io.gaia_app.modules.repository.TerraformModuleRepository -import io.gaia_app.runner.StackRunner import io.gaia_app.stacks.bo.Job +import io.gaia_app.stacks.bo.JobStatus import io.gaia_app.stacks.repository.JobRepository import io.gaia_app.stacks.repository.StackRepository import io.gaia_app.stacks.repository.StepRepository @@ -14,8 +13,6 @@ import org.springframework.web.bind.annotation.* @RequestMapping("/api/jobs") class JobRestController( private val jobRepository: JobRepository, - private val stackRepository: StackRepository, - private val stackRunner: StackRunner, private val stepRepository: StepRepository) { @GetMapping(params = ["stackId"]) @@ -23,32 +20,30 @@ class JobRestController( @GetMapping("/{id}") fun job(@PathVariable id: String): Job { - val runningJob = stackRunner.getJob(id); - if (runningJob.isPresent) { - return runningJob.get(); - } return jobRepository.findById(id).orElseThrow { JobNotFoundException() } } @PostMapping("/{id}/plan") fun plan(@PathVariable id: String) { val job = jobRepository.findById(id).orElseThrow { JobNotFoundException() } - val stack = stackRepository.findById(job.stackId).orElseThrow() - stackRunner.plan(JobWorkflow(job), stack.module, stack) + val workflow = JobWorkflow(job) + workflow.plan() + jobRepository.save(job) } @PostMapping("/{id}/apply") fun apply(@PathVariable id: String) { val job = jobRepository.findById(id).orElseThrow { JobNotFoundException() } - val stack = stackRepository.findById(job.stackId).orElseThrow() - stackRunner.apply(JobWorkflow(job), stack.module, stack) + job.status = JobStatus.APPLY_PENDING + jobRepository.save(job) } @PostMapping("/{id}/retry") fun retry(@PathVariable id: String) { val job = jobRepository.findById(id).orElseThrow { JobNotFoundException() } - val stack = stackRepository.findById(job.stackId).orElseThrow() - stackRunner.retry(JobWorkflow(job), stack.module, stack) + val workflow = JobWorkflow(job) + workflow.retry() + jobRepository.save(job) } @DeleteMapping("/{id}") diff --git a/src/main/java/io/gaia_app/stacks/controller/StackRestController.java b/src/main/java/io/gaia_app/stacks/controller/StackRestController.java index 1b85424ea..cbdb7b88d 100644 --- a/src/main/java/io/gaia_app/stacks/controller/StackRestController.java +++ b/src/main/java/io/gaia_app/stacks/controller/StackRestController.java @@ -2,6 +2,7 @@ import io.gaia_app.credentials.CredentialsRepository; import io.gaia_app.stacks.bo.Job; +import io.gaia_app.stacks.bo.JobStatus; import io.gaia_app.stacks.bo.JobType; import io.gaia_app.stacks.bo.Stack; import io.gaia_app.stacks.repository.JobRepository; @@ -90,6 +91,9 @@ public Map launchJob(@PathVariable String id, @PathVariable JobT // create a new job var job = new Job(jobType, id, user); + // setting the status to pending + job.setStatus(JobStatus.PLAN_PENDING); + job.setTerraformImage(stack.getModule().getTerraformImage()); if(stack.getCredentialsId() != null){ this.credentialsRepository.findById(stack.getCredentialsId()) diff --git a/src/main/java/io/gaia_app/stacks/repository/JobRepository.kt b/src/main/java/io/gaia_app/stacks/repository/JobRepository.kt index f8d4052ac..364ecb358 100644 --- a/src/main/java/io/gaia_app/stacks/repository/JobRepository.kt +++ b/src/main/java/io/gaia_app/stacks/repository/JobRepository.kt @@ -1,8 +1,11 @@ package io.gaia_app.stacks.repository import io.gaia_app.stacks.bo.Job +import io.gaia_app.stacks.bo.JobStatus +import org.jetbrains.annotations.NotNull import org.springframework.data.mongodb.repository.MongoRepository import org.springframework.stereotype.Repository +import java.util.* /** * Repository for jobs @@ -12,4 +15,6 @@ interface JobRepository : MongoRepository { fun findAllByStackIdOrderByStartDateTimeDesc(stackId: String): List + fun findFirstByStatusEqualsOrStatusEquals(planPending: JobStatus, applyPending: JobStatus): @NotNull Optional + } diff --git a/src/main/java/io/gaia_app/stacks/workflow/JobWorkflow.java b/src/main/java/io/gaia_app/stacks/workflow/JobWorkflow.java index 9fafb3dee..2eb9392f5 100644 --- a/src/main/java/io/gaia_app/stacks/workflow/JobWorkflow.java +++ b/src/main/java/io/gaia_app/stacks/workflow/JobWorkflow.java @@ -1,8 +1,6 @@ package io.gaia_app.stacks.workflow; -import io.gaia_app.stacks.bo.Job; -import io.gaia_app.stacks.bo.JobStatus; -import io.gaia_app.stacks.bo.Step; +import io.gaia_app.stacks.bo.*; import io.gaia_app.stacks.workflow.state.*; import io.gaia_app.stacks.workflow.state.*; @@ -83,6 +81,9 @@ JobState evalInitialState(JobStatus jobStatus) { case PLAN_FAILED: result = new PlanFailedState(); break; + case APPLY_PENDING: + result = new PlanFinishedState(); + break; case APPLY_STARTED: result = new ApplyStartedState(); break; @@ -95,4 +96,47 @@ JobState evalInitialState(JobStatus jobStatus) { } return result; } + + public Step startWorkflow() { + if(this.job.getStatus() == JobStatus.PLAN_PENDING){ + this.state.plan(this); + } + else if(this.job.getStatus() == JobStatus.APPLY_PENDING) { + this.state.apply(this); + } + return this.currentStep; + } + + /** + * Updates workflow to next status depending of the result code + * @param stepResultCode + */ + public void next(int stepResultCode) { + if(this.job.getStatus() == JobStatus.PLAN_STARTED){ + this.managePlanResult(stepResultCode); + } + else if(this.job.getStatus() == JobStatus.APPLY_STARTED) { + this.managerApplyResult(stepResultCode); + } + } + + private void managePlanResult(int result) { + if (result == 0) { + // diff is empty + this.end(); + } else if (result == 2) { + this.end(); + } else { + // error + this.fail(); + } + } + + private void managerApplyResult(int result){ + if (result == 0) { + this.end(); + } else { + this.fail(); + } + } } diff --git a/src/main/java/io/gaia_app/stacks/workflow/state/RetryableState.java b/src/main/java/io/gaia_app/stacks/workflow/state/RetryableState.java index 28136b1a8..dcf12e0d8 100644 --- a/src/main/java/io/gaia_app/stacks/workflow/state/RetryableState.java +++ b/src/main/java/io/gaia_app/stacks/workflow/state/RetryableState.java @@ -8,6 +8,5 @@ interface RetryableState extends JobState { default void retry(JobWorkflow jobWorkflow) { jobWorkflow.getJob().reset(); jobWorkflow.setState(new NotStartedState()); - jobWorkflow.plan(); } } diff --git a/src/test/java/io/gaia_app/runner/DockerRunnerIT.kt b/src/test/java/io/gaia_app/runner/DockerRunnerIT.kt deleted file mode 100644 index 34e1f5a79..000000000 --- a/src/test/java/io/gaia_app/runner/DockerRunnerIT.kt +++ /dev/null @@ -1,105 +0,0 @@ -package io.gaia_app.runner - -import io.gaia_app.credentials.AWSCredentials -import io.gaia_app.modules.bo.TerraformImage -import io.gaia_app.runner.config.DockerConfig -import io.gaia_app.settings.bo.Settings -import io.gaia_app.stacks.bo.Job -import io.gaia_app.stacks.bo.Step -import io.gaia_app.stacks.workflow.JobWorkflow -import org.assertj.core.api.Assertions.assertThat -import org.junit.Assert -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.context.properties.EnableConfigurationProperties -import org.springframework.boot.test.context.SpringBootTest -import org.springframework.test.context.TestPropertySource - -@SpringBootTest(classes = [DockerRunner::class, DockerConfig::class, Settings::class]) -@EnableConfigurationProperties -@TestPropertySource(properties = ["gaia.dockerDaemonUrl=unix:///var/run/docker.sock"]) -class DockerRunnerIT { - - @Autowired - private lateinit var dockerRunner: DockerRunner - - @Test - fun `runContainerForJob() should work with a simple script`() { - val script = "echo 'Hello World'; exit 0;" - - val job = Job() - job.terraformImage = TerraformImage.defaultInstance() - val jobWorkflow = JobWorkflow(job) - jobWorkflow.currentStep = Step() - - assertEquals(0, dockerRunner.runContainerForJob(jobWorkflow, script).toLong()) - } - - @Test - fun `runContainerForJob() should stop work with a simple script`() { - val script = "set -e; echo 'Hello World'; false; exit 0;" - - val job = Job() - job.terraformImage = TerraformImage.defaultInstance() - val jobWorkflow = JobWorkflow(job) - jobWorkflow.currentStep = Step() - - assertEquals(1, dockerRunner.runContainerForJob(jobWorkflow, script).toLong()) - } - - @Test - fun `runContainerForJob() should return the script exit code`() { - val script = "exit 5" - - val job = Job() - job.terraformImage = TerraformImage.defaultInstance() - val jobWorkflow = JobWorkflow(job) - jobWorkflow.currentStep = Step() - - Assert.assertEquals(5, dockerRunner.runContainerForJob(jobWorkflow, script).toLong()) - } - - @Test - fun `runContainerForJob() should feed step with container logs`() { - val script = "echo 'hello world'; exit 0;" - - val job = Job() - job.terraformImage = TerraformImage.defaultInstance() - val jobWorkflow = JobWorkflow(job) - jobWorkflow.currentStep = Step() - - dockerRunner.runContainerForJob(jobWorkflow, script) - - assertThat(jobWorkflow.currentStep.logs).isEqualTo("hello world\n"); - } - - @Test - fun `runContainerForJob() use credentials of the job`() { - val script = "echo \$AWS_ACCESS_KEY_ID; exit 0;" - - val job = Job() - job.terraformImage = TerraformImage.defaultInstance() - job.credentials = AWSCredentials("SOME_ACCESS_KEY", "SOME_SECRET_KEY") - val jobWorkflow = JobWorkflow(job) - jobWorkflow.currentStep = Step() - - dockerRunner.runContainerForJob(jobWorkflow, script) - - assertThat(jobWorkflow.currentStep.logs).isEqualTo("SOME_ACCESS_KEY\n"); - } - - @Test - fun `runContainerForJob() use TF_IN_AUTOMATION env var`() { - val script = "echo \$TF_IN_AUTOMATION; exit 0;" - - val job = Job() - job.terraformImage = TerraformImage.defaultInstance() - val jobWorkflow = JobWorkflow(job) - jobWorkflow.currentStep = Step() - - dockerRunner.runContainerForJob(jobWorkflow, script) - - assertThat(jobWorkflow.currentStep.logs).isEqualTo("true\n"); - } -} diff --git a/src/test/java/io/gaia_app/runner/StackCommandBuilderTest.java b/src/test/java/io/gaia_app/runner/RunnerCommandBuilderTest.java similarity index 92% rename from src/test/java/io/gaia_app/runner/StackCommandBuilderTest.java rename to src/test/java/io/gaia_app/runner/RunnerCommandBuilderTest.java index c3b2a9fdc..8e09ea5e2 100644 --- a/src/test/java/io/gaia_app/runner/StackCommandBuilderTest.java +++ b/src/test/java/io/gaia_app/runner/RunnerCommandBuilderTest.java @@ -24,12 +24,12 @@ import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) -class StackCommandBuilderTest { +class RunnerCommandBuilderTest { @Mock RegistryOAuth2Provider registryOAuth2Provider; - private StackCommandBuilder stackCommandBuilder; + private RunnerCommandBuilder runnerCommandBuilder; @BeforeEach void setup() { @@ -37,7 +37,7 @@ void setup() { var stateApiSecurityProperties = new RunnerApiSecurityConfig.RunnerApiSecurityProperties("gaia-backend", "password"); - stackCommandBuilder = new StackCommandBuilder(new Settings(), mustache, List.of(registryOAuth2Provider), stateApiSecurityProperties); + runnerCommandBuilder = new RunnerCommandBuilder(new Settings(), mustache, List.of(registryOAuth2Provider), stateApiSecurityProperties); } @Test @@ -47,7 +47,7 @@ void buildPlanScript_shouldGenerateAFullScript() { stack.setModule(module); var job = new Job(); - var script = stackCommandBuilder.buildPlanScript(job, stack, module); + var script = runnerCommandBuilder.buildPlanScript(job, stack, module); assertTrue(script.contains("echo '[gaia] using image hashicorp/terraform:latest'")); assertTrue(script.contains("echo '[gaia] cloning git://test' | awk '{ sub(/oauth2:(.*)@/, \"oauth2:[MASKED]@\");}1'")); @@ -67,7 +67,7 @@ void buildPlanScript_shouldGenerateAFullScript_forAModuleWithoutDirectory() { stack.setModule(module); var job = new Job(); - var script = stackCommandBuilder.buildPlanScript(job, stack, module); + var script = runnerCommandBuilder.buildPlanScript(job, stack, module); assertTrue(script.contains("echo '[gaia] using image hashicorp/terraform:latest'")); assertTrue(script.contains("echo '[gaia] cloning git://test' | awk '{ sub(/oauth2:(.*)@/, \"oauth2:[MASKED]@\");}1'")); @@ -89,7 +89,7 @@ void buildPlanScript_shouldGenerateAFullScript_forAModuleWithAccessToken() { when(registryOAuth2Provider.isAssignableFor(anyString())).thenReturn(true); when(registryOAuth2Provider.getOAuth2Url(anyString(), anyString())).thenReturn("url_with_token"); - var script = stackCommandBuilder.buildPlanScript(job, stack, module); + var script = runnerCommandBuilder.buildPlanScript(job, stack, module); assertTrue(script.contains("echo '[gaia] using image hashicorp/terraform:latest'")); assertTrue(script.contains("echo '[gaia] cloning url_with_token' | awk '{ sub(/oauth2:(.*)@/, \"oauth2:[MASKED]@\");}1'")); @@ -110,7 +110,7 @@ void buildApplyScript_shouldGenerateAFullScript() { stack.setModule(module); var job = new Job(); - var script = stackCommandBuilder.buildApplyScript(job, stack, module); + var script = runnerCommandBuilder.buildApplyScript(job, stack, module); assertTrue(script.contains("echo '[gaia] using image hashicorp/terraform:latest'")); assertTrue(script.contains("echo '[gaia] cloning git://test' | awk '{ sub(/oauth2:(.*)@/, \"oauth2:[MASKED]@\");}1'")); @@ -130,7 +130,7 @@ void buildApplyScript_shouldGenerateAFullScript_forAModuleWithoutDirectory() { stack.setModule(module); var job = new Job(); - var script = stackCommandBuilder.buildApplyScript(job, stack, module); + var script = runnerCommandBuilder.buildApplyScript(job, stack, module); assertTrue(script.contains("echo '[gaia] using image hashicorp/terraform:latest'")); assertTrue(script.contains("echo '[gaia] cloning git://test' | awk '{ sub(/oauth2:(.*)@/, \"oauth2:[MASKED]@\");}1'")); @@ -152,7 +152,7 @@ void buildApplyScript_shouldGenerateAFullScript_forAModuleWithAccessToken() { when(registryOAuth2Provider.isAssignableFor(anyString())).thenReturn(true); when(registryOAuth2Provider.getOAuth2Url(anyString(), anyString())).thenReturn("url_with_token"); - var script = stackCommandBuilder.buildApplyScript(job, stack, module); + var script = runnerCommandBuilder.buildApplyScript(job, stack, module); assertTrue(script.contains("echo '[gaia] using image hashicorp/terraform:latest'")); assertTrue(script.contains("echo '[gaia] cloning url_with_token' | awk '{ sub(/oauth2:(.*)@/, \"oauth2:[MASKED]@\");}1'")); @@ -173,7 +173,7 @@ void buildPlanDestroyScript_shouldGenerateAFullScript() { stack.setModule(module); var job = new Job(); - var script = stackCommandBuilder.buildPlanDestroyScript(job, stack, module); + var script = runnerCommandBuilder.buildPlanDestroyScript(job, stack, module); assertTrue(script.contains("echo '[gaia] using image hashicorp/terraform:latest'")); assertTrue(script.contains("echo '[gaia] cloning git://test' | awk '{ sub(/oauth2:(.*)@/, \"oauth2:[MASKED]@\");}1'")); @@ -193,7 +193,7 @@ void buildPlanDestroyScript_shouldGenerateAFullScript_forAModuleWithoutDirectory stack.setModule(module); var job = new Job(); - var script = stackCommandBuilder.buildPlanDestroyScript(job, stack, module); + var script = runnerCommandBuilder.buildPlanDestroyScript(job, stack, module); assertTrue(script.contains("echo '[gaia] using image hashicorp/terraform:latest'")); assertTrue(script.contains("echo '[gaia] cloning git://test' | awk '{ sub(/oauth2:(.*)@/, \"oauth2:[MASKED]@\");}1'")); @@ -215,7 +215,7 @@ void buildPlanDestroyScript_shouldGenerateAFullScript_forAModuleWithAccessToken( when(registryOAuth2Provider.isAssignableFor(anyString())).thenReturn(true); when(registryOAuth2Provider.getOAuth2Url(anyString(), anyString())).thenReturn("url_with_token"); - var script = stackCommandBuilder.buildPlanDestroyScript(job, stack, module); + var script = runnerCommandBuilder.buildPlanDestroyScript(job, stack, module); assertTrue(script.contains("echo '[gaia] using image hashicorp/terraform:latest'")); assertTrue(script.contains("echo '[gaia] cloning url_with_token' | awk '{ sub(/oauth2:(.*)@/, \"oauth2:[MASKED]@\");}1'")); @@ -236,7 +236,7 @@ void buildDestroyScript_shouldGenerateAFullScript() { stack.setModule(module); var job = new Job(); - var script = stackCommandBuilder.buildDestroyScript(job, stack, module); + var script = runnerCommandBuilder.buildDestroyScript(job, stack, module); assertTrue(script.contains("echo '[gaia] using image hashicorp/terraform:latest'")); assertTrue(script.contains("echo '[gaia] cloning git://test' | awk '{ sub(/oauth2:(.*)@/, \"oauth2:[MASKED]@\");}1'")); @@ -256,7 +256,7 @@ void buildDestroyScript_shouldGenerateAFullScript_forAModuleWithoutDirectory() { stack.setModule(module); var job = new Job(); - var script = stackCommandBuilder.buildDestroyScript(job, stack, module); + var script = runnerCommandBuilder.buildDestroyScript(job, stack, module); assertTrue(script.contains("echo '[gaia] using image hashicorp/terraform:latest'")); assertTrue(script.contains("echo '[gaia] cloning git://test' | awk '{ sub(/oauth2:(.*)@/, \"oauth2:[MASKED]@\");}1'")); @@ -278,7 +278,7 @@ void buildDestroyScript_shouldGenerateAFullScript_forAModuleWithAccessToken() { when(registryOAuth2Provider.isAssignableFor(anyString())).thenReturn(true); when(registryOAuth2Provider.getOAuth2Url(anyString(), anyString())).thenReturn("url_with_token"); - var script = stackCommandBuilder.buildDestroyScript(job, stack, module); + var script = runnerCommandBuilder.buildDestroyScript(job, stack, module); assertTrue(script.contains("echo '[gaia] using image hashicorp/terraform:latest'")); assertTrue(script.contains("echo '[gaia] cloning url_with_token' | awk '{ sub(/oauth2:(.*)@/, \"oauth2:[MASKED]@\");}1'")); diff --git a/src/test/java/io/gaia_app/runner/StackRunnerTest.java b/src/test/java/io/gaia_app/runner/StackRunnerTest.java deleted file mode 100644 index 5fe7e03a6..000000000 --- a/src/test/java/io/gaia_app/runner/StackRunnerTest.java +++ /dev/null @@ -1,450 +0,0 @@ -package io.gaia_app.runner; - -import io.gaia_app.credentials.AWSCredentials; -import io.gaia_app.credentials.CredentialsService; -import io.gaia_app.modules.bo.TerraformModule; -import io.gaia_app.stacks.bo.Job; -import io.gaia_app.stacks.bo.JobType; -import io.gaia_app.stacks.bo.Stack; -import io.gaia_app.stacks.bo.StackState; -import io.gaia_app.stacks.repository.JobRepository; -import io.gaia_app.stacks.repository.StackRepository; -import io.gaia_app.stacks.repository.StepRepository; -import io.gaia_app.stacks.workflow.JobWorkflow; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -class StackRunnerTest { - - @Mock - private DockerRunner dockerRunner; - - @Mock - private StackCommandBuilder stackCommandBuilder; - - @Mock - private StackRepository stackRepository; - - @Mock - private JobRepository jobRepository; - - @Mock - private StepRepository stepRepository; - - @Mock - private CredentialsService credentialsService; - - @Mock - private JobWorkflow jobWorkflow; - - private Job job; - - private TerraformModule module; - - private Stack stack; - - @InjectMocks - private StackRunner stackRunner; - - @BeforeEach - void setUp() { - stack = new Stack(); - job = new Job(JobType.RUN, null, null); - lenient().when(jobWorkflow.getJob()).thenReturn(job); // use lenient to prevent mockito from throwing exception for tests not needing this mock (getLog_*) - module = new TerraformModule(); - } - - @Test - void plan_shouldExecutePlanWorkflow() { - // when - when(dockerRunner.runContainerForJob(any(), any())).thenReturn(0); - stackRunner.plan(jobWorkflow, module, stack); - - // then - verify(jobWorkflow).plan(); - } - - @Test - void plan_shouldUsePlanScript_WhenJobIsRun() { - // when - when(dockerRunner.runContainerForJob(any(), any())).thenReturn(0); - stackRunner.plan(jobWorkflow, module, stack); - - // then - verify(stackCommandBuilder).buildPlanScript(jobWorkflow.getJob(), stack, module); - } - - @Test - void plan_shouldUsePlanDestroyScript_WhenJobIsStop() { - // given - job.setType(JobType.DESTROY); - - // when - when(dockerRunner.runContainerForJob(any(), any())).thenReturn(0); - stackRunner.plan(jobWorkflow, module, stack); - - // then - verify(stackCommandBuilder).buildPlanDestroyScript(jobWorkflow.getJob(), stack, module); - } - - @Test - void plan_shouldEndJob_WhenSuccessful() { - // when - when(dockerRunner.runContainerForJob(any(), any())).thenReturn(0); - stackRunner.plan(jobWorkflow, module, stack); - - // then - verify(jobWorkflow).end(); - } - - @Test - void plan_shouldEndJob_WhenThereIsADiff() { - // when - when(dockerRunner.runContainerForJob(any(), any())).thenReturn(2); - stackRunner.plan(jobWorkflow, module, stack); - - // then - verify(jobWorkflow).end(); - } - - @Test - void plan_shouldFailJob_WhenThereIsAError() { - // when - when(dockerRunner.runContainerForJob(any(), any())).thenReturn(99); - stackRunner.plan(jobWorkflow, module, stack); - - // then - verify(jobWorkflow).fail(); - } - - @Test - void plan_shouldUpdateStack_WhenThereIsADiff() { - // given - stack.setState(StackState.RUNNING); - - // when - when(dockerRunner.runContainerForJob(any(), any())).thenReturn(2); - stackRunner.plan(jobWorkflow, module, stack); - - // then - assertEquals(StackState.TO_UPDATE, stack.getState()); - verify(stackRepository).save(stack); - } - - @Test - void plan_shouldNotUpdateStack_WhenThereIsADiffForNewStacks() { - // given - stack.setState(StackState.NEW); - - // when - when(dockerRunner.runContainerForJob(any(), any())).thenReturn(2); - stackRunner.plan(jobWorkflow, module, stack); - - // then - assertEquals(StackState.NEW, stack.getState()); - verifyNoInteractions(stackRepository); - } - - @Test - void plan_shouldNotUpdateStack_WhenThereIsADiffAndJobIsStop() { - // given - stack.setState(StackState.RUNNING); - job.setType(JobType.DESTROY); - - // when - when(dockerRunner.runContainerForJob(any(), any())).thenReturn(2); - stackRunner.plan(jobWorkflow, module, stack); - - // then - assertEquals(StackState.RUNNING, stack.getState()); - verifyNoInteractions(stackRepository); - } - - @Test - void plan_shouldSaveJobAndSteps() { - // when - stackRunner.plan(jobWorkflow, module, stack); - - // then - verify(jobRepository, times(2)).save(job); - verify(stepRepository, times(2)).saveAll(job.getSteps()); - } - - @Test - void apply_shouldExecuteApplyWorkflow() { - // when - when(dockerRunner.runContainerForJob(any(), any())).thenReturn(0); - stackRunner.apply(jobWorkflow, module, stack); - - // then - verify(jobWorkflow).apply(); - } - - @Test - void apply_shouldUseApplyScript_WhenJobIsRun() { - // when - when(dockerRunner.runContainerForJob(any(), any())).thenReturn(0); - stackRunner.apply(jobWorkflow, module, stack); - - // then - verify(stackCommandBuilder).buildApplyScript(jobWorkflow.getJob(), stack, module); - } - - @Test - void apply_shouldUseDestroyScript_WhenJobIsStop() { - // given - job.setType(JobType.DESTROY); - - // when - when(dockerRunner.runContainerForJob(any(), any())).thenReturn(0); - stackRunner.apply(jobWorkflow, module, stack); - - // then - verify(stackCommandBuilder).buildDestroyScript(jobWorkflow.getJob(), stack, module); - } - - @Test - void apply_shouldEndJob_WhenSuccessful() { - // when - when(dockerRunner.runContainerForJob(any(), any())).thenReturn(0); - stackRunner.apply(jobWorkflow, module, stack); - - // then - verify(jobWorkflow).end(); - } - - @Test - void apply_shouldFailJob_WhenThereIsAError() { - // when - when(dockerRunner.runContainerForJob(any(), any())).thenReturn(99); - stackRunner.apply(jobWorkflow, module, stack); - - // then - verify(jobWorkflow).fail(); - } - - @Test - void apply_shouldUpdateStack_WhenSuccessfulAndJobIsRun() { - // given - stack.setState(StackState.NEW); - - // when - when(dockerRunner.runContainerForJob(any(), any())).thenReturn(0); - stackRunner.apply(jobWorkflow, module, stack); - - // then - assertEquals(StackState.RUNNING, stack.getState()); - verify(stackRepository).save(stack); - } - - @Test - void apply_shouldUpdateStack_WhenSuccessfulAndJobIsStop() { - // given - stack.setState(StackState.RUNNING); - job.setType(JobType.DESTROY); - - // when - when(dockerRunner.runContainerForJob(any(), any())).thenReturn(0); - stackRunner.apply(jobWorkflow, module, stack); - - // then - assertEquals(StackState.STOPPED, stack.getState()); - verify(stackRepository).save(stack); - } - - @Test - void apply_shouldSaveJobAndSteps() { - // when - stackRunner.apply(jobWorkflow, module, stack); - - // then - verify(jobRepository, times(2)).save(job); - verify(stepRepository, times(2)).saveAll(job.getSteps()); - } - - @Test - void retry_shouldDeletePreviousSteps() { - // when - when(dockerRunner.runContainerForJob(any(), any())).thenReturn(0); - stackRunner.retry(jobWorkflow, module, stack); - - // then - verify(stepRepository).deleteByJobId(jobWorkflow.getJob().getId()); - } - - @Test - void retry_shouldExecuteRetryWorkflow() { - // when - when(dockerRunner.runContainerForJob(any(), any())).thenReturn(0); - stackRunner.retry(jobWorkflow, module, stack); - - // then - verify(jobWorkflow).retry(); - } - - @Test - void retry_shouldUsePlanScript_WhenJobIsRun() { - // when - when(dockerRunner.runContainerForJob(any(), any())).thenReturn(0); - stackRunner.retry(jobWorkflow, module, stack); - - // then - verify(stackCommandBuilder).buildPlanScript(jobWorkflow.getJob(), stack, module); - } - - @Test - void retry_shouldUsePlanDestroyScript_WhenJobIsStop() { - // given - job.setType(JobType.DESTROY); - - // when - when(dockerRunner.runContainerForJob(any(), any())).thenReturn(0); - stackRunner.retry(jobWorkflow, module, stack); - - // then - verify(stackCommandBuilder).buildPlanDestroyScript(jobWorkflow.getJob(), stack, module); - } - - @Test - void retry_shouldEndJob_WhenSuccessful() { - // when - when(dockerRunner.runContainerForJob(any(), any())).thenReturn(0); - stackRunner.retry(jobWorkflow, module, stack); - - // then - verify(jobWorkflow).end(); - } - - @Test - void retry_shouldEndJob_WhenThereIsADiff() { - // when - when(dockerRunner.runContainerForJob(any(), any())).thenReturn(2); - stackRunner.retry(jobWorkflow, module, stack); - - // then - verify(jobWorkflow).end(); - } - - @Test - void retry_shouldFailJob_WhenThereIsAError() { - // when - when(dockerRunner.runContainerForJob(any(), any())).thenReturn(99); - stackRunner.retry(jobWorkflow, module, stack); - - // then - verify(jobWorkflow).fail(); - } - - @Test - void retry_shouldUpdateStack_WhenThereIsADiff() { - // given - stack.setState(StackState.RUNNING); - - // when - when(dockerRunner.runContainerForJob(any(), any())).thenReturn(2); - stackRunner.retry(jobWorkflow, module, stack); - - // then - assertEquals(StackState.TO_UPDATE, stack.getState()); - verify(stackRepository).save(stack); - } - - @Test - void retry_shouldNotUpdateStack_WhenThereIsADiffForNewStacks() { - // given - stack.setState(StackState.NEW); - - // when - when(dockerRunner.runContainerForJob(any(), any())).thenReturn(2); - stackRunner.retry(jobWorkflow, module, stack); - - // then - assertEquals(StackState.NEW, stack.getState()); - verifyNoInteractions(stackRepository); - } - - @Test - void retry_shouldNotUpdateStack_WhenThereIsADiffAndJobIsStop() { - // given - stack.setState(StackState.RUNNING); - job.setType(JobType.DESTROY); - - // when - when(dockerRunner.runContainerForJob(any(), any())).thenReturn(2); - stackRunner.retry(jobWorkflow, module, stack); - - // then - assertEquals(StackState.RUNNING, stack.getState()); - verifyNoInteractions(stackRepository); - } - - @Test - void retry_shouldSaveJobAndSteps() { - // when - stackRunner.retry(jobWorkflow, module, stack); - - // then - verify(jobRepository, times(2)).save(job); - verify(stepRepository, times(2)).saveAll(job.getSteps()); - } - - @Test - void credentialsShouldBeLoadedAndInjected_whenRunningAPlan(){ - // given - stack.setCredentialsId("dummy"); - var credentials = new AWSCredentials("dummy","dummy"); - - when(credentialsService.load("dummy")).thenReturn(Optional.of(credentials)); - - // when - stackRunner.plan(jobWorkflow, module, stack); - - // then - verify(credentialsService).load("dummy"); - assertThat(job.getCredentials()).isEqualTo(credentials); - } - - @Test - void credentialsShouldBeLoadedAndInjected_whenRunningAnApply(){ - // given - stack.setCredentialsId("dummy"); - var credentials = new AWSCredentials("dummy","dummy"); - - when(credentialsService.load("dummy")).thenReturn(Optional.of(credentials)); - - // when - stackRunner.apply(jobWorkflow, module, stack); - - // then - verify(credentialsService).load("dummy"); - assertThat(job.getCredentials()).isEqualTo(credentials); - } - - @Test - void credentialsShouldBeLoadedAndInjected_whenRunningARetry(){ - // given - stack.setCredentialsId("dummy"); - var credentials = new AWSCredentials("dummy","dummy"); - - when(credentialsService.load("dummy")).thenReturn(Optional.of(credentials)); - - // when - stackRunner.retry(jobWorkflow, module, stack); - - // then - verify(credentialsService).load("dummy"); - assertThat(job.getCredentials()).isEqualTo(credentials); - } - -} diff --git a/src/test/java/io/gaia_app/runner/config/DockerConfigTest.java b/src/test/java/io/gaia_app/runner/config/DockerConfigTest.java deleted file mode 100644 index a97c15f41..000000000 --- a/src/test/java/io/gaia_app/runner/config/DockerConfigTest.java +++ /dev/null @@ -1,23 +0,0 @@ -package io.gaia_app.runner.config; - - -import io.gaia_app.settings.bo.Settings; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -class DockerConfigTest { - - @Test - void dockerClient_shouldUseDockerDaemonUrlSetting() { - var settings = new Settings(); - settings.setDockerDaemonUrl("tcp://test:2375"); - - var dockerConfig = new DockerConfig(); - - var dockerClientConfig = dockerConfig.dockerClientConfig(settings); - - assertEquals("tcp://test:2375", dockerClientConfig.getDockerHost().toString()); - } - -} diff --git a/src/test/java/io/gaia_app/stacks/bo/StepTest.java b/src/test/java/io/gaia_app/stacks/bo/StepTest.java index ffce40db9..2db01014f 100644 --- a/src/test/java/io/gaia_app/stacks/bo/StepTest.java +++ b/src/test/java/io/gaia_app/stacks/bo/StepTest.java @@ -11,19 +11,6 @@ class StepTest { - @Test - void getLogs_shouldReturnOutputStreamResult() { - var step = new Step(); - - PrintWriter printWriter = new PrintWriter(step.getLogsWriter()); - printWriter.println("Test Line 1"); - printWriter.println("Test Line 2"); - - var logs = step.getLogs(); - - assertEquals("Test Line 1\nTest Line 2\n", logs); - } - @Test void start_shouldSetStatusToStarted() { var step = new Step(); diff --git a/src/test/java/io/gaia_app/stacks/controller/JobRestControllerTest.kt b/src/test/java/io/gaia_app/stacks/controller/JobRestControllerTest.kt index 0be455591..bf0154239 100644 --- a/src/test/java/io/gaia_app/stacks/controller/JobRestControllerTest.kt +++ b/src/test/java/io/gaia_app/stacks/controller/JobRestControllerTest.kt @@ -1,9 +1,8 @@ package io.gaia_app.stacks.controller import io.gaia_app.modules.bo.TerraformModule -import io.gaia_app.modules.repository.TerraformModuleRepository -import io.gaia_app.runner.StackRunner import io.gaia_app.stacks.bo.Job +import io.gaia_app.stacks.bo.JobStatus import io.gaia_app.stacks.bo.Stack import io.gaia_app.stacks.repository.JobRepository import io.gaia_app.stacks.repository.StackRepository @@ -32,12 +31,6 @@ class JobRestControllerTest { @Mock lateinit var stepRepository: StepRepository - @Mock - lateinit var stackRepository: StackRepository - - @Mock - lateinit var stackRunner: StackRunner - @InjectMocks lateinit var controller: JobRestController @@ -51,31 +44,15 @@ class JobRestControllerTest { } @Test - fun `job() should return the running job if exists`() { - // given - val job = mock(Job::class.java) - - // when - whenever(stackRunner.getJob(any())).thenReturn(of(job)) - controller.job("12") - - // then - verify(stackRunner).getJob("12") - verifyNoInteractions(jobRepository) - } - - @Test - fun `job() should return the saved job if no running`() { + fun `job() should return the job`() { // given val job = mock(Job::class.java) // when - whenever(stackRunner.getJob(any())).thenReturn(empty()) whenever(jobRepository.findById("12")).thenReturn(of(job)) controller.job("12") // then - verify(stackRunner, times(1)).getJob("12") verify(jobRepository).findById("12") } @@ -98,33 +75,19 @@ class JobRestControllerTest { assertThrows(JobNotFoundException::class.java) { controller.plan("test_jobId") } } - @Test - fun `plan() should throw an exception for non existing stack`() { - // when - whenever(jobRepository.findById(any())).thenReturn(of(Job())) - whenever(stackRepository.findById(any())).thenReturn(empty()) - - // then - assertThrows(NoSuchElementException::class.java) { controller.plan("test_jobId") } - } - @Test fun `plan() should plan a job`() { // given val job = Job() - val stack = Stack() - stack.module = TerraformModule() + job.status = JobStatus.PLAN_PENDING // when whenever(jobRepository.findById(any())).thenReturn(of(job)) - whenever(stackRepository.findById(any())).thenReturn(of(stack)) controller.plan("test_jobId") // then - val captor = forClass(JobWorkflow::class.java) - verify(stackRunner).plan(captor.capture(), eq(stack.module), eq(stack)) - assertThat(captor.value).isNotNull - assertThat(captor.value.job).isNotNull.isEqualTo(job) + assertThat(job.status).isEqualTo(JobStatus.PLAN_STARTED) + verify(jobRepository).save(job) } @Test @@ -136,33 +99,19 @@ class JobRestControllerTest { assertThrows(JobNotFoundException::class.java) { controller.apply("test_jobId") } } - @Test - fun `apply() should throw an exception for non existing stack`() { - // when - whenever(jobRepository.findById(any())).thenReturn(of(Job())) - whenever(stackRepository.findById(any())).thenReturn(empty()) - - // then - assertThrows(NoSuchElementException::class.java) { controller.apply("test_jobId") } - } - @Test fun `apply() should apply a job`() { // given val job = Job() - val stack = Stack() - stack.module = TerraformModule() + job.status = JobStatus.APPLY_PENDING // when whenever(jobRepository.findById(any())).thenReturn(of(job)) - whenever(stackRepository.findById(any())).thenReturn(of(stack)) controller.apply("test_jobId") // then - val captor = forClass(JobWorkflow::class.java) - verify(stackRunner).apply(captor.capture(), eq(stack.module), eq(stack)) - assertThat(captor.value).isNotNull - assertThat(captor.value.job).isNotNull.isEqualTo(job) + assertThat(job.status).isEqualTo(JobStatus.APPLY_STARTED) + verify(jobRepository).save(job) } @Test @@ -174,33 +123,19 @@ class JobRestControllerTest { assertThrows(JobNotFoundException::class.java) { controller.retry("test_jobId") } } - @Test - fun `retry() should throw an exception for non existing stack`() { - // when - whenever(jobRepository.findById(any())).thenReturn(of(Job())) - whenever(stackRepository.findById(any())).thenReturn(empty()) - - // then - assertThrows(NoSuchElementException::class.java) { controller.retry("test_jobId") } - } - @Test fun `retry() should retry a job`() { // given val job = Job() - val stack = Stack() - stack.module = TerraformModule() + job.status = JobStatus.PLAN_FAILED // when whenever(jobRepository.findById(any())).thenReturn(of(job)) - whenever(stackRepository.findById(any())).thenReturn(of(stack)) controller.retry("test_jobId") // then - val captor = forClass(JobWorkflow::class.java) - verify(stackRunner).retry(captor.capture(), eq(stack.module), eq(stack)) - assertThat(captor.value).isNotNull - assertThat(captor.value.job).isNotNull.isEqualTo(job) + assertThat(job.status).isEqualTo(JobStatus.PLAN_STARTED) + verify(jobRepository).save(job) } @Test diff --git a/src/test/java/io/gaia_app/stacks/repository/StepRepositoryIT.java b/src/test/java/io/gaia_app/stacks/repository/StepRepositoryIT.java index aa399d16b..9ebf6055a 100644 --- a/src/test/java/io/gaia_app/stacks/repository/StepRepositoryIT.java +++ b/src/test/java/io/gaia_app/stacks/repository/StepRepositoryIT.java @@ -10,6 +10,7 @@ import org.springframework.boot.test.autoconfigure.data.mongo.DataMongoTest; import java.io.IOException; +import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -24,7 +25,7 @@ class StepRepositoryIT extends SharedMongoContainerTest { void jobShouldBeSavedWithLogs() throws IOException { var step = new Step(StepType.PLAN, "42"); step.setId("12"); - step.getLogsWriter().write("some logs"); + step.setLogs(List.of("some logs")); step.start(); step.end();