diff --git a/.travis.yml b/.travis.yml index fda96e6f9..ae8645c97 100644 --- a/.travis.yml +++ b/.travis.yml @@ -57,13 +57,6 @@ jobs: - echo "checking is percy is enabled on PR $TRAVIS_PULL_REQUEST" - NO_PERCY=$(curl -s https://api.github.com/repos/gaia-app/gaia/pulls/$TRAVIS_PULL_REQUEST | jq '.labels[].name | select(.=="no-percy")' | tr -d \") - if [[ $NO_PERCY != "no-percy" ]]; then npm install @percy/agent; npx percy exec -- mvn test -Dgroups=e2e; fi - - stage: deploy - name: "Deploy to hub.docker.com" - script: - - export DOCKER_TAG=`echo $TRAVIS_TAG | cut -d 'v' -f 2` - - echo "$DOCKER_PASSWORD" | docker login -u codekaio --password-stdin - - docker build -t codekaio/gaia:$DOCKER_TAG . - - docker push codekaio/gaia:$DOCKER_TAG after_success: - wget https://raw.githubusercontent.com/CodeKaio/travis-ci-discord-webhook/master/send.sh diff --git a/docker-compose-with-vault.yml b/docker-compose-with-vault.yml index 2bceee060..0c5e26539 100644 --- a/docker-compose-with-vault.yml +++ b/docker-compose-with-vault.yml @@ -10,8 +10,6 @@ services: - "GAIA_EXTERNAL_URL=http://172.17.0.1:8080" - "SPRING_PROFILES_ACTIVE=vault" - "GAIA_VAULT_URI=http://vault:8200" - volumes: - - /var/run/docker.sock:/var/run/docker.sock mongo: build: diff --git a/docker-compose.yml b/docker-compose.yml index 0c85ea1ac..c6a8abd47 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,11 +8,10 @@ services: environment: - "GAIA_MONGODB_URI=mongodb://mongo/gaia" - "GAIA_EXTERNAL_URL=http://172.17.0.1:8080" - volumes: - - /var/run/docker.sock:/var/run/docker.sock + - "GAIA_RUNNER_API_PASSWORD=123456" mongo: build: context: . dockerfile: ./Dockerfile-db - image: gaia-db \ No newline at end of file + image: gaia-db diff --git a/pom.xml b/pom.xml index 584b6e89e..a4a96e01a 100644 --- a/pom.xml +++ b/pom.xml @@ -42,7 +42,6 @@ 4.8-1 6.7.0 - 3.2.5 11 2.27 5.5.2 @@ -117,17 +116,6 @@ spring-boot-devtools - - com.github.docker-java - docker-java-core - ${docker-java.version} - - - com.github.docker-java - docker-java-transport-okhttp - ${docker-java.version} - - org.springframework.boot spring-boot-starter-security diff --git a/src/main/client/app/pages/stacks/job/job-metadata.vue b/src/main/client/app/pages/stacks/job/job-metadata.vue index 8cc8458ea..35af5f2e5 100644 --- a/src/main/client/app/pages/stacks/job/job-metadata.vue +++ b/src/main/client/app/pages/stacks/job/job-metadata.vue @@ -148,6 +148,15 @@ margin-bottom: 1rem; } + .job-metadata-container[class*=PENDING] { + background: linear-gradient(to right, #91B0B1 0, #91B0B1 1rem, #fff 1rem, #fff 100%) no-repeat; + color: #91B0B1; + } + + .job-metadata-container[class*=PENDING] .job-detail-title { + color: #91B0B1; + } + .job-metadata-container[class*=STARTED] { background: linear-gradient(to right, #2196f3 0, #2196f3 1rem, #fff 1rem, #fff 100%) no-repeat; } 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..9e8cdab7a 100644 --- a/src/main/client/app/pages/stacks/job/job-step.vue +++ b/src/main/client/app/pages/stacks/job/job-step.vue @@ -22,7 +22,7 @@
{{ headerTitle }}
@@ -175,6 +175,11 @@ cursor: default; } + .job-step-container[class*=PENDING] .job-step-header { + background: linear-gradient(to right, #91B0B1 0, #91B0B1 1rem, #fff 1rem, #fff 100%) no-repeat; + color: #91B0B1; + } + .job-step-container[class*=STARTED] .job-step-header { background: linear-gradient(to right, #2196f3 0, #2196f3 1rem, #fff 1rem, #fff 100%) no-repeat; color: #2196f3; 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/client/app/pages/stacks/stack-edition.vue b/src/main/client/app/pages/stacks/stack-edition.vue index 8903c9a77..1cde90a71 100644 --- a/src/main/client/app/pages/stacks/stack-edition.vue +++ b/src/main/client/app/pages/stacks/stack-edition.vue @@ -190,7 +190,7 @@ displayConfirmDialog, displayNotification, } from '@/shared/services/modal-service'; - import { getJobs } from '@/shared/api/jobs-api'; + import { getJobs, planJob } from '@/shared/api/jobs-api'; import { getCredentialsList } from '@/shared/api/credentials-api'; export default { @@ -275,6 +275,7 @@ if (await displayConfirmDialog(this, { title: 'Run request', message })) { await this.saveStack(); const { jobId } = await runStack(this.stack.id); + await planJob(jobId); this.$router.push({ name: 'job', params: { jobId } }); } }, @@ -283,6 +284,7 @@ const message = 'This will completely stop the stack, and destroy all created resources. Continue?'; if (await displayConfirmDialog(this, { title: 'Stop request', message })) { const { jobId } = await destroyStack(this.stack.id); + await planJob(jobId); this.$router.push({ name: 'job', params: { jobId } }); } }, 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 86% rename from src/main/java/io/gaia_app/runner/StackCommandBuilder.java rename to src/main/java/io/gaia_app/runner/RunnerCommandBuilder.java index ae3f7adb0..250db78a0 100644 --- a/src/main/java/io/gaia_app/runner/StackCommandBuilder.java +++ b/src/main/java/io/gaia_app/runner/RunnerCommandBuilder.java @@ -8,7 +8,8 @@ import io.gaia_app.stacks.bo.Job; import io.gaia_app.stacks.bo.Stack; import io.gaia_app.stacks.bo.mustache.TerraformScript; -import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -20,7 +21,9 @@ * A builder class to create stack commands */ @Component -public class StackCommandBuilder { +public class RunnerCommandBuilder { + + private static final Logger logger = LoggerFactory.getLogger(RunnerCommandBuilder.class); private Settings settings; private RunnerApiSecurityConfig.RunnerApiSecurityProperties runnerApiSecurityProperties; @@ -28,7 +31,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; @@ -60,7 +63,7 @@ private String buildScript(Job job, Stack stack, TerraformModule module, String .setGitRepositoryUrl(evalGitRepositoryUrl(module)) .setTerraformImage(job.getTerraformImage().image()); - if (StringUtils.isNotBlank(module.getDirectory())) { + if (module.getDirectory() != null && !module.getDirectory().isBlank()) { script.setGitDirectory(module.getDirectory()); } @@ -71,9 +74,9 @@ private String buildScript(Job job, Stack stack, TerraformModule module, String terraformMustache.execute(writer, script).flush(); return writer.toString(); } catch (IOException e) { - e.printStackTrace(); + logger.error("Unable to generate script : {}", e.getMessage()); } - return StringUtils.EMPTY; + return ""; } /** diff --git a/src/main/java/io/gaia_app/runner/RunnerController.java b/src/main/java/io/gaia_app/runner/RunnerController.java index 3bd7a6288..81adce2fe 100644 --- a/src/main/java/io/gaia_app/runner/RunnerController.java +++ b/src/main/java/io/gaia_app/runner/RunnerController.java @@ -1,11 +1,19 @@ 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.Collections; +import java.util.List; +import java.util.Map; /** * Controller for the operations that are called by the runner only @@ -17,9 +25,127 @@ 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 findFirstRunnableStep() { + var step = this.stepRepository.findFirstByStatus(StepStatus.PENDING) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NO_CONTENT)); + + var job = this.jobRepository.findById(step.getJobId()).orElseThrow(); + var stack = this.stackRepository.findById(job.getStackId()).orElseThrow(); + + 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( + "id", step.getId(), + "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}/end") + public void updateStepStatus(@PathVariable String stepId, @RequestBody int status) { + // getting jobId + var jobId = this.stepRepository.findById(stepId).orElseThrow().getJobId(); + + // reload the job to check workflow status + var job = this.jobRepository.findById(jobId).orElseThrow(); + + // rebuild the workflow + var workflow = new JobWorkflow(job); + workflow.end(status); + + var stack = this.stackRepository.findById(job.getStackId()).orElseThrow(); + if(job.getStatus() == JobStatus.APPLY_FINISHED) { + if(job.getType() == JobType.RUN){ + stack.setState(StackState.RUNNING); + } + else{ + stack.setState(StackState.STOPPED); + } + } + this.stackRepository.save(stack); + + // save the job & step to update their status + this.stepRepository.saveAll(job.getSteps()); + this.jobRepository.save(job); + } + + /** + * Updates the job state + */ + @PutMapping("/steps/{stepId}/start") + public void startStep(@PathVariable String stepId) { + // getting jobId + var jobId = this.stepRepository.findById(stepId).orElseThrow().getJobId(); + + // reload the job to check workflow status + var job = this.jobRepository.findById(jobId).orElseThrow(); + + // rebuild the workflow and start it + var workflow = new JobWorkflow(job); + workflow.start(); + + // save the job & step to update their status + this.stepRepository.saveAll(job.getSteps()); + 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..664daf1b8 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; /** @@ -20,10 +17,8 @@ public class Step { private LocalDateTime endDateTime; private Long executionTime; private StepType type; - private StepStatus status; - @Transient - private StringWriter logsWriter = new StringWriter(); - private String logs; + private StepStatus status = StepStatus.PENDING; + 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/bo/StepStatus.java b/src/main/java/io/gaia_app/stacks/bo/StepStatus.java index d83dab3dd..43adde131 100644 --- a/src/main/java/io/gaia_app/stacks/bo/StepStatus.java +++ b/src/main/java/io/gaia_app/stacks/bo/StepStatus.java @@ -1,5 +1,5 @@ package io.gaia_app.stacks.bo; public enum StepStatus { - STARTED, FINISHED, FAILED + PENDING, STARTED, FINISHED, FAILED } 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..3ff7fbd74 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,37 @@ 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) + stepRepository.save(workflow.currentStep) } @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) + val workflow = JobWorkflow(job) + workflow.apply() + jobRepository.save(job) + stepRepository.save(workflow.currentStep) } @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) + + stepRepository.deleteByJobId(id) + + val workflow = JobWorkflow(job) + workflow.retry() + jobRepository.save(job) + stepRepository.save(workflow.currentStep) } @DeleteMapping("/{id}") 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..56206010b 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 diff --git a/src/main/java/io/gaia_app/stacks/repository/StepRepository.java b/src/main/java/io/gaia_app/stacks/repository/StepRepository.java index 94606abe6..d33cbf244 100644 --- a/src/main/java/io/gaia_app/stacks/repository/StepRepository.java +++ b/src/main/java/io/gaia_app/stacks/repository/StepRepository.java @@ -1,9 +1,12 @@ package io.gaia_app.stacks.repository; import io.gaia_app.stacks.bo.Step; +import io.gaia_app.stacks.bo.StepStatus; import org.springframework.data.mongodb.repository.MongoRepository; import org.springframework.stereotype.Repository; +import java.util.Optional; + /** * Repository for steps */ @@ -12,4 +15,5 @@ public interface StepRepository extends MongoRepository { void deleteByJobId(String jobId); + Optional findFirstByStatus(StepStatus status); } 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..7c2ea0c36 100644 --- a/src/main/java/io/gaia_app/stacks/workflow/JobWorkflow.java +++ b/src/main/java/io/gaia_app/stacks/workflow/JobWorkflow.java @@ -4,7 +4,6 @@ import io.gaia_app.stacks.bo.JobStatus; import io.gaia_app.stacks.bo.Step; import io.gaia_app.stacks.workflow.state.*; -import io.gaia_app.stacks.workflow.state.*; import java.util.Objects; @@ -13,8 +12,7 @@ */ public class JobWorkflow { - private Job job; - private Step currentStep; + private final Job job; private JobState state; public JobWorkflow(Job job) { @@ -30,14 +28,6 @@ public void apply() { this.state.apply(this); } - public void end() { - this.state.end(this); - } - - public void fail() { - this.state.fail(this); - } - public void retry() { this.state.retry(this); } @@ -47,11 +37,20 @@ public Job getJob() { } public Step getCurrentStep() { - return currentStep; - } - - public void setCurrentStep(Step currentStep) { - this.currentStep = currentStep; + // calculating current step depending on the state + switch (this.job.getStatus()){ + case PLAN_PENDING: + case PLAN_STARTED: + case PLAN_FINISHED: + case PLAN_FAILED: + return this.job.getSteps().get(0); + case APPLY_PENDING: + case APPLY_STARTED: + case APPLY_FAILED: + case APPLY_FINISHED: + return this.job.getSteps().get(1); + } + return null; } public JobState getState() { @@ -74,6 +73,9 @@ JobState evalInitialState(JobStatus jobStatus) { return result; } switch (jobStatus) { + case PLAN_PENDING: + result = new PlanPendingState(); + break; case PLAN_STARTED: result = new PlanStartedState(); break; @@ -83,6 +85,9 @@ JobState evalInitialState(JobStatus jobStatus) { case PLAN_FAILED: result = new PlanFailedState(); break; + case APPLY_PENDING: + result = new ApplyPendingState(); + break; case APPLY_STARTED: result = new ApplyStartedState(); break; @@ -95,4 +100,39 @@ JobState evalInitialState(JobStatus jobStatus) { } return result; } + + public void start() { + this.state.start(this); + } + + /** + * Updates workflow to next status depending of the result code + * @param stepResultCode + */ + public void end(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 || result == 2) { + // diff is empty + this.state.end(this); + } else { + // error + this.state.fail(this); + } + } + + private void managerApplyResult(int result){ + if (result == 0) { + this.state.end(this); + } else { + this.state.fail(this);; + } + } } diff --git a/src/main/java/io/gaia_app/stacks/workflow/state/ApplyFailedState.java b/src/main/java/io/gaia_app/stacks/workflow/state/ApplyFailedState.java index ee5fdb2de..621109bec 100644 --- a/src/main/java/io/gaia_app/stacks/workflow/state/ApplyFailedState.java +++ b/src/main/java/io/gaia_app/stacks/workflow/state/ApplyFailedState.java @@ -3,5 +3,5 @@ /** * Describes a job which apply has been failed */ -public class ApplyFailedState implements RetryableState { +public class ApplyFailedState extends RetryableState { } diff --git a/src/main/java/io/gaia_app/stacks/workflow/state/ApplyPendingState.java b/src/main/java/io/gaia_app/stacks/workflow/state/ApplyPendingState.java new file mode 100644 index 000000000..ded41412a --- /dev/null +++ b/src/main/java/io/gaia_app/stacks/workflow/state/ApplyPendingState.java @@ -0,0 +1,17 @@ +package io.gaia_app.stacks.workflow.state; + +import io.gaia_app.stacks.bo.JobStatus; +import io.gaia_app.stacks.workflow.JobWorkflow; + +public class ApplyPendingState implements JobState { + + @Override + public void start(JobWorkflow jobWorkflow) { + var job = jobWorkflow.getJob(); + + job.proceed(JobStatus.APPLY_STARTED); + jobWorkflow.setState(new ApplyStartedState()); + + jobWorkflow.getCurrentStep().start(); + } +} diff --git a/src/main/java/io/gaia_app/stacks/workflow/state/ApplyStartedState.java b/src/main/java/io/gaia_app/stacks/workflow/state/ApplyStartedState.java index b5c27458d..2b20997e7 100644 --- a/src/main/java/io/gaia_app/stacks/workflow/state/ApplyStartedState.java +++ b/src/main/java/io/gaia_app/stacks/workflow/state/ApplyStartedState.java @@ -11,15 +11,17 @@ public class ApplyStartedState implements JobState { @Override public void end(JobWorkflow jobWorkflow) { - jobWorkflow.getCurrentStep().end(); jobWorkflow.getJob().end(JobStatus.APPLY_FINISHED); jobWorkflow.setState(new ApplyFinishedState()); + + jobWorkflow.getCurrentStep().end(); } @Override public void fail(JobWorkflow jobWorkflow) { - jobWorkflow.getCurrentStep().fail(); jobWorkflow.getJob().end(JobStatus.APPLY_FAILED); jobWorkflow.setState(new ApplyFailedState()); + + jobWorkflow.getCurrentStep().fail(); } } diff --git a/src/main/java/io/gaia_app/stacks/workflow/state/JobState.java b/src/main/java/io/gaia_app/stacks/workflow/state/JobState.java index e3f2d49dd..54a71ac45 100644 --- a/src/main/java/io/gaia_app/stacks/workflow/state/JobState.java +++ b/src/main/java/io/gaia_app/stacks/workflow/state/JobState.java @@ -7,6 +7,11 @@ * Describe the state of job and its possible actions */ public interface JobState { + + default void start(JobWorkflow jobWorkflow){ + throw new UnsupportedOperationException(); + } + default void plan(JobWorkflow jobWorkflow) { throw new UnsupportedOperationException(); } diff --git a/src/main/java/io/gaia_app/stacks/workflow/state/NotStartedState.java b/src/main/java/io/gaia_app/stacks/workflow/state/NotStartedState.java index a5ca47f52..fc7a0379c 100644 --- a/src/main/java/io/gaia_app/stacks/workflow/state/NotStartedState.java +++ b/src/main/java/io/gaia_app/stacks/workflow/state/NotStartedState.java @@ -1,7 +1,6 @@ package io.gaia_app.stacks.workflow.state; -import io.gaia_app.stacks.bo.Step; -import io.gaia_app.stacks.bo.StepType; +import io.gaia_app.stacks.bo.*; import io.gaia_app.stacks.workflow.JobWorkflow; import io.gaia_app.stacks.bo.Step; import io.gaia_app.stacks.bo.StepType; @@ -14,13 +13,12 @@ public class NotStartedState implements JobState { @Override public void plan(JobWorkflow jobWorkflow) { var job = jobWorkflow.getJob(); - job.start(); + job.setStatus(JobStatus.PLAN_PENDING); + // creating the PLAN step var step = new Step(StepType.PLAN, job.getId()); job.getSteps().add(step); - jobWorkflow.setCurrentStep(step); - step.start(); - jobWorkflow.setState(new PlanStartedState()); + jobWorkflow.setState(new PlanPendingState()); } } diff --git a/src/main/java/io/gaia_app/stacks/workflow/state/PlanFailedState.java b/src/main/java/io/gaia_app/stacks/workflow/state/PlanFailedState.java index bc4e0403f..1abdf9256 100644 --- a/src/main/java/io/gaia_app/stacks/workflow/state/PlanFailedState.java +++ b/src/main/java/io/gaia_app/stacks/workflow/state/PlanFailedState.java @@ -3,5 +3,5 @@ /** * Describes a job which plan has been failed */ -public class PlanFailedState implements RetryableState { +public class PlanFailedState extends RetryableState { } diff --git a/src/main/java/io/gaia_app/stacks/workflow/state/PlanFinishedState.java b/src/main/java/io/gaia_app/stacks/workflow/state/PlanFinishedState.java index 3b2fb10a4..e44328934 100644 --- a/src/main/java/io/gaia_app/stacks/workflow/state/PlanFinishedState.java +++ b/src/main/java/io/gaia_app/stacks/workflow/state/PlanFinishedState.java @@ -15,14 +15,11 @@ public class PlanFinishedState implements JobState { @Override public void apply(JobWorkflow jobWorkflow) { + jobWorkflow.setState(new ApplyPendingState()); var job = jobWorkflow.getJob(); - job.proceed(JobStatus.APPLY_STARTED); + job.setStatus(JobStatus.APPLY_PENDING); var step = new Step(StepType.APPLY, job.getId()); job.getSteps().add(step); - jobWorkflow.setCurrentStep(step); - step.start(); - - jobWorkflow.setState(new ApplyStartedState()); } } diff --git a/src/main/java/io/gaia_app/stacks/workflow/state/PlanPendingState.java b/src/main/java/io/gaia_app/stacks/workflow/state/PlanPendingState.java new file mode 100644 index 000000000..0a423597f --- /dev/null +++ b/src/main/java/io/gaia_app/stacks/workflow/state/PlanPendingState.java @@ -0,0 +1,19 @@ +package io.gaia_app.stacks.workflow.state; + +import io.gaia_app.stacks.workflow.JobWorkflow; + +/** + * Describes a job which plan has not been started yet + */ +public class PlanPendingState implements JobState { + + @Override + public void start(JobWorkflow jobWorkflow) { + var job = jobWorkflow.getJob(); + + job.start(); + jobWorkflow.setState(new PlanStartedState()); + + jobWorkflow.getCurrentStep().start(); + } +} diff --git a/src/main/java/io/gaia_app/stacks/workflow/state/PlanStartedState.java b/src/main/java/io/gaia_app/stacks/workflow/state/PlanStartedState.java index a19020a42..334c0248f 100644 --- a/src/main/java/io/gaia_app/stacks/workflow/state/PlanStartedState.java +++ b/src/main/java/io/gaia_app/stacks/workflow/state/PlanStartedState.java @@ -11,15 +11,17 @@ public class PlanStartedState implements JobState { @Override public void end(JobWorkflow jobWorkflow) { - jobWorkflow.getCurrentStep().end(); jobWorkflow.getJob().end(JobStatus.PLAN_FINISHED); jobWorkflow.setState(new PlanFinishedState()); + + jobWorkflow.getCurrentStep().end(); } @Override public void fail(JobWorkflow jobWorkflow) { - jobWorkflow.getCurrentStep().fail(); jobWorkflow.getJob().end(JobStatus.PLAN_FAILED); jobWorkflow.setState(new PlanFailedState()); + + jobWorkflow.getCurrentStep().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..a15ce91f0 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 @@ -1,13 +1,19 @@ package io.gaia_app.stacks.workflow.state; -import io.gaia_app.stacks.workflow.JobWorkflow; +import io.gaia_app.stacks.bo.Step; +import io.gaia_app.stacks.bo.StepType; import io.gaia_app.stacks.workflow.JobWorkflow; -interface RetryableState extends JobState { +abstract class RetryableState implements JobState { @Override - default void retry(JobWorkflow jobWorkflow) { + public void retry(JobWorkflow jobWorkflow) { + var job = jobWorkflow.getJob(); + jobWorkflow.getJob().reset(); - jobWorkflow.setState(new NotStartedState()); - jobWorkflow.plan(); + jobWorkflow.setState(new PlanPendingState()); + + // creating the PLAN step + var step = new Step(StepType.PLAN, job.getId()); + job.getSteps().add(step); } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 4aa6175b9..36522030f 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -18,3 +18,9 @@ gaia.externalUrl=http://localhost:${server.port:8080} gaia.dockerDaemonUrl=unix:///var/run/docker.sock docker.registry.api.url=https://registry.hub.docker.com/v2 + +##################### +# runner properties # +##################### +# gaia.runner.api.username=gaia-runner +# gaia.runner.api.password=mysecretpassword diff --git a/src/test/java/io/gaia_app/hcl/HCLParserTest.kt b/src/test/java/io/gaia_app/hcl/HCLParserTest.kt index 11fff290b..47258583e 100644 --- a/src/test/java/io/gaia_app/hcl/HCLParserTest.kt +++ b/src/test/java/io/gaia_app/hcl/HCLParserTest.kt @@ -2,13 +2,14 @@ package io.gaia_app.hcl import io.gaia_app.modules.bo.Output import io.gaia_app.modules.bo.Variable -import org.apache.commons.io.IOUtils import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.springframework.core.io.ClassPathResource import java.io.IOException import java.nio.charset.Charset +import java.nio.file.Files +import java.nio.file.Path class HCLParserTest { @@ -18,7 +19,7 @@ class HCLParserTest { @Throws(IOException::class) fun parsing_variables_shouldWorkWithVisitor() { // given - val fileContent = IOUtils.toString(ClassPathResource("hcl/variables.tf").url, Charset.defaultCharset()) + val fileContent = Files.readString(Path.of(ClassPathResource("hcl/variables.tf").uri), Charset.defaultCharset()) // when val variables = hclParser.parseVariables(fileContent) @@ -35,7 +36,7 @@ class HCLParserTest { @Throws(IOException::class) fun parsing_variables_shouldWork_withComplexFile() { // given - val fileContent = IOUtils.toString(ClassPathResource("hcl/variables_aws_eks.tf").url, Charset.defaultCharset()) + val fileContent = Files.readString(Path.of(ClassPathResource("hcl/variables_aws_eks.tf").uri), Charset.defaultCharset()) // when val variables = hclParser.parseVariables(fileContent) @@ -48,7 +49,7 @@ class HCLParserTest { @Throws(IOException::class) fun parsing_variables_shouldWork_withAnotherComplexFile() { // given - val fileContent = IOUtils.toString(ClassPathResource("hcl/variables_aws_vpc.tf").url, Charset.defaultCharset()) + val fileContent = Files.readString(Path.of(ClassPathResource("hcl/variables_aws_vpc.tf").uri), Charset.defaultCharset()) // when val variables = hclParser.parseVariables(fileContent) @@ -61,7 +62,7 @@ class HCLParserTest { @Throws(IOException::class) fun parsing_outputs_shouldWorkWithVisitor() { // given - val fileContent = IOUtils.toString(ClassPathResource("hcl/outputs.tf").url, Charset.defaultCharset()) + val fileContent = Files.readString(Path.of(ClassPathResource("hcl/outputs.tf").uri), Charset.defaultCharset()) // when val outputs = hclParser.parseOutputs(fileContent) @@ -77,7 +78,7 @@ class HCLParserTest { @Throws(IOException::class) fun parsing_outputs_shouldWork_withComplexFile() { // given - val fileContent = IOUtils.toString(ClassPathResource("hcl/outputs_aws_eks.tf").url, Charset.defaultCharset()) + val fileContent = Files.readString(Path.of(ClassPathResource("hcl/outputs_aws_eks.tf").uri), Charset.defaultCharset()) // when val outputs = hclParser.parseOutputs(fileContent) @@ -96,7 +97,7 @@ class HCLParserTest { @Throws(IOException::class) fun parsing_provider_shouldWork_withMainFile_includingProviderDirective() { // given - val fileContent = IOUtils.toString(ClassPathResource("hcl/terraform_docker_mongo_main_with_provider.tf").url, Charset.defaultCharset()) + val fileContent = Files.readString(Path.of(ClassPathResource("hcl/terraform_docker_mongo_main_with_provider.tf").uri), Charset.defaultCharset()) // when val provider: String = hclParser.parseProvider(fileContent) // then @@ -107,7 +108,7 @@ class HCLParserTest { @Throws(IOException::class) fun parsing_provider_shouldWork_withMainFile_withoutProviderDirective() { // given - val fileContent = IOUtils.toString(ClassPathResource("hcl/terraform_docker_mongo_main_without_provider.tf").url, Charset.defaultCharset()) + val fileContent = Files.readString(Path.of(ClassPathResource("hcl/terraform_docker_mongo_main_without_provider.tf").uri), Charset.defaultCharset()) // when val provider: String = hclParser.parseProvider(fileContent) @@ -120,7 +121,7 @@ class HCLParserTest { @Throws(IOException::class) fun parsing_provider_shouldReturn_unknown_ifNoProviderFound() { // given - val fileContent = IOUtils.toString(ClassPathResource("hcl/variables.tf").url, Charset.defaultCharset()) + val fileContent = Files.readString(Path.of(ClassPathResource("hcl/variables.tf").uri), Charset.defaultCharset()) // when val provider: String = hclParser.parseProvider(fileContent) 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/RunnerControllerIT.java b/src/test/java/io/gaia_app/runner/RunnerControllerIT.java new file mode 100644 index 000000000..6295f968d --- /dev/null +++ b/src/test/java/io/gaia_app/runner/RunnerControllerIT.java @@ -0,0 +1,254 @@ +package io.gaia_app.runner; + +import com.jayway.jsonpath.JsonPath; +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.test.SharedMongoContainerTest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.notNullValue; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +class RunnerControllerIT extends SharedMongoContainerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private StackRepository stackRepository; + + @Autowired + private JobRepository jobRepository; + + @Autowired + private StepRepository stepRepository; + + @BeforeEach + void setUp() { + mongo.emptyDatabase(); + mongo.runScript("00_team.js"); + mongo.runScript("10_user.js"); + mongo.runScript("20_module.js"); + mongo.runScript("30_stack.js"); + } + + + @Test + @WithMockUser("gaia-runner") + void findFirstRunnableJob_shouldReturnNothing_whenNoJobIsPending() throws Exception { + mockMvc.perform(get("/api/runner/steps/request")) + .andExpect(status().isNoContent()); + } + + @Test + @WithMockUser("gaia-runner") + void findFirstRunnableJob_shouldReturnNothing_whenJobIsCreatedButNotPending() throws Exception { + var stackId = "5a215b6b-fe53-4afa-85f0-a10175a7f264"; + + // creating a pending job of type RUN + mockMvc.perform(post("/api/stacks/{stackId}/RUN", stackId).with(csrf())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.jobId", notNullValue())); + + mockMvc.perform(get("/api/runner/steps/request")) + .andExpect(status().isNoContent()); + } + + @Test + @WithMockUser("gaia-runner") + void findFirstRunnableJob_shouldReturnNothing_whenJobIsPending() throws Exception { + var stackId = "5a215b6b-fe53-4afa-85f0-a10175a7f264"; + + // creating a job of type RUN + var result = mockMvc.perform(post("/api/stacks/{stackId}/RUN", stackId).with(csrf())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.jobId", notNullValue())) + .andReturn(); + var jobId = JsonPath.read(result.getResponse().getContentAsString(), "$.jobId"); + + // calling 'plan' to make it pending + mockMvc.perform(post("/api/jobs/{jobId}/plan", jobId).with(csrf())) + .andExpect(status().isOk()); + + // request as the runner will do + mockMvc.perform(get("/api/runner/steps/request")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.env", notNullValue())) + .andExpect(jsonPath("$.id", notNullValue())) + .andExpect(jsonPath("$.script", notNullValue())) + .andExpect(jsonPath("$.image", notNullValue())); + } + + @Test + @WithMockUser("gaia-runner") + void updateLogs_shouldAppendLogsToStep() throws Exception { + var step = new Step(StepType.APPLY, "fakeJobId"); + step.setLogs(List.of("first log line")); + this.stepRepository.save(step); + + mockMvc.perform( + put("/api/runner/steps/{stepId}/logs", step.getId()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content("second log line")) + .andExpect(status().isOk()) + .andReturn(); + + var updatedStep = this.stepRepository.findById(step.getId()).orElseThrow(); + assertThat(updatedStep.getLogs()) + .hasSize(2) + .containsExactly("first log line", "second log line"); + } + + @Test + @WithMockUser("gaia-runner") + void startStep_shouldUpdateStepState() throws Exception { + var step = new Step(StepType.APPLY, "fakeJobId"); + + var job = new Job(); + job.setId("fakeJobId"); + job.setStatus(JobStatus.PLAN_PENDING); + job.setSteps(List.of(step)); + + this.stepRepository.save(step); + this.jobRepository.save(job); + + mockMvc.perform( + put("/api/runner/steps/{stepId}/start", step.getId()) + .with(csrf())) + .andExpect(status().isOk()) + .andReturn(); + + var updatedStep = this.stepRepository.findById(step.getId()).orElseThrow(); + assertThat(updatedStep.getStatus()).isEqualTo(StepStatus.STARTED); + + var updatedJob = this.jobRepository.findById("fakeJobId").orElseThrow(); + assertThat(updatedJob.getStatus()).isEqualTo(JobStatus.PLAN_STARTED); + } + + @Test + @WithMockUser("gaia-runner") + void updateStepStatus_shouldUpdateStepState_inCaseOfSuccess() throws Exception { + var stack = new Stack(); + stack.setId("fakeStack"); + + var step = new Step(StepType.APPLY, "fakeJobId"); + step.setStartDateTime(LocalDateTime.now()); + step.setStatus(StepStatus.STARTED); + + var job = new Job(); + job.setId("fakeJobId"); + job.setStackId("fakeStack"); + job.setStatus(JobStatus.PLAN_STARTED); + job.setSteps(List.of(step)); + + this.stackRepository.save(stack); + this.jobRepository.save(job); + this.stepRepository.save(step); + + mockMvc.perform( + put("/api/runner/steps/{stepId}/end", step.getId()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content("0")) + .andExpect(status().isOk()) + .andReturn(); + + var updatedStep = this.stepRepository.findById(step.getId()).orElseThrow(); + assertThat(updatedStep.getStatus()).isEqualTo(StepStatus.FINISHED); + + var updatedJob = this.jobRepository.findById("fakeJobId").orElseThrow(); + assertThat(updatedJob.getStatus()).isEqualTo(JobStatus.PLAN_FINISHED); + } + + @Test + @WithMockUser("gaia-runner") + void updateStepStatus_shouldUpdateStackStateToRunning_inCaseOfSuccessfulApply() throws Exception { + var stack = new Stack(); + stack.setId("fakeStackId"); + + var planStep = new Step(StepType.PLAN, "fakeJobId"); + planStep.setStatus(StepStatus.FINISHED); + + var applyStep = new Step(StepType.APPLY, "fakeJobId"); + applyStep.setStartDateTime(LocalDateTime.now()); + applyStep.setStatus(StepStatus.STARTED); + + var job = new Job(); + job.setId("fakeJobId"); + job.setStackId("fakeStackId"); + job.setType(JobType.RUN); + job.setStatus(JobStatus.APPLY_STARTED); + job.setSteps(List.of(planStep, applyStep)); + + this.stackRepository.save(stack); + this.jobRepository.save(job); + this.stepRepository.saveAll(job.getSteps()); + + mockMvc.perform( + put("/api/runner/steps/{stepId}/end", planStep.getId()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content("0")) + .andExpect(status().isOk()) + .andReturn(); + + var updatedStack = this.stackRepository.findById("fakeStackId").orElseThrow(); + assertThat(updatedStack.getState()).isEqualTo(StackState.RUNNING); + } + + @Test + @WithMockUser("gaia-runner") + void updateStepStatus_shouldUpdateStackStateToStopped_inCaseOfSuccessfulApply() throws Exception { + var stack = new Stack(); + stack.setState(StackState.RUNNING); + stack.setId("fakeStackId"); + + var planStep = new Step(StepType.PLAN, "fakeJobId"); + planStep.setStatus(StepStatus.FINISHED); + + var applyStep = new Step(StepType.APPLY, "fakeJobId"); + applyStep.setStartDateTime(LocalDateTime.now()); + applyStep.setStatus(StepStatus.STARTED); + + var job = new Job(); + job.setId("fakeJobId"); + job.setStackId("fakeStackId"); + job.setType(JobType.DESTROY); + job.setStatus(JobStatus.APPLY_STARTED); + job.setSteps(List.of(planStep, applyStep)); + + this.stackRepository.save(stack); + this.jobRepository.save(job); + this.stepRepository.saveAll(job.getSteps()); + + mockMvc.perform( + put("/api/runner/steps/{stepId}/end", planStep.getId()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content("0")) + .andExpect(status().isOk()) + .andReturn(); + + var updatedStack = this.stackRepository.findById("fakeStackId").orElseThrow(); + assertThat(updatedStack.getState()).isEqualTo(StackState.STOPPED); + } +} diff --git a/src/test/java/io/gaia_app/runner/RunnerControllerTest.java b/src/test/java/io/gaia_app/runner/RunnerControllerTest.java new file mode 100644 index 000000000..2caf47510 --- /dev/null +++ b/src/test/java/io/gaia_app/runner/RunnerControllerTest.java @@ -0,0 +1,180 @@ +package io.gaia_app.runner; + +import io.gaia_app.credentials.Credentials; +import io.gaia_app.credentials.CredentialsService; +import io.gaia_app.modules.bo.TerraformImage; +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 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.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class RunnerControllerTest { + + @InjectMocks + private RunnerController runnerController; + + @Mock + private JobRepository jobRepository; + + @Mock + private StepRepository stepRepository; + + @Mock + private StackRepository stackRepository; + + @Mock + private RunnerCommandBuilder runnerCommandBuilder; + + @Mock + private CredentialsService credentialsService; + + private Step step; + + private Job job; + + private Stack stack; + + @BeforeEach + void setUp() { + this.job = new Job(); + this.job.setStatus(JobStatus.PLAN_PENDING); + this.job.setStackId("fakeStackId"); + this.job.setSteps(List.of(new Step(), new Step())); + this.job.setTerraformImage(new TerraformImage("hashicorp/terraform", "0.13.0")); + + this.step = new Step(); + this.step.setId("fakeStepId"); + this.step.setStatus(StepStatus.PENDING); + this.step.setType(StepType.PLAN); + this.step.setJobId("fakeJobId"); + + when(stepRepository.findFirstByStatus(StepStatus.PENDING)).thenReturn(Optional.of(step)); + + when(jobRepository.findById("fakeJobId")).thenReturn(Optional.of(job)); + + this.stack = new Stack(); + when(stackRepository.findById("fakeStackId")).thenReturn(Optional.of(stack)); + + lenient().when(runnerCommandBuilder.buildPlanScript(job, stack, null)).thenReturn("plan script"); + lenient().when(runnerCommandBuilder.buildApplyScript(job, stack, null)).thenReturn("apply script"); + lenient().when(runnerCommandBuilder.buildPlanDestroyScript(job, stack, null)).thenReturn("plan destroy script"); + lenient().when(runnerCommandBuilder.buildDestroyScript(job, stack, null)).thenReturn("apply destroy script"); + } + + @Test + void findFirstRunnableStep_shouldUsePlanScript() { + // given + this.job.setType(JobType.RUN); + this.job.setStatus(JobStatus.PLAN_PENDING); + this.job.getSteps().get(0).setType(StepType.PLAN); + + // when + var result = runnerController.findFirstRunnableStep(); + + // then + verify(runnerCommandBuilder).buildPlanScript(job, stack, null); + + assertThat(result) + .containsEntry("script", "plan script"); + } + + @Test + void findFirstRunnableStep_shouldUseApplyScript() { + // given + this.job.setType(JobType.RUN); + this.job.setStatus(JobStatus.APPLY_PENDING); + this.step.setType(StepType.APPLY); + + // when + var result = runnerController.findFirstRunnableStep(); + + // then + verify(runnerCommandBuilder).buildApplyScript(job, stack, null); + + assertThat(result) + .containsEntry("script", "apply script"); + } + + @Test + void findFirstRunnableStep_shouldUsePlanDestroyScript() { + // given + this.job.setType(JobType.DESTROY); + this.job.setStatus(JobStatus.PLAN_PENDING); + + // when + var result = runnerController.findFirstRunnableStep(); + + // then + verify(runnerCommandBuilder).buildPlanDestroyScript(job, stack, null); + + assertThat(result) + .containsEntry("script", "plan destroy script"); + } + + @Test + void findFirstRunnableStep_shouldUseApplyDestroyScript() { + // given + this.job.setType(JobType.DESTROY); + this.job.setStatus(JobStatus.APPLY_PENDING); + this.step.setType(StepType.APPLY); + + // when + var result = runnerController.findFirstRunnableStep(); + + // then + verify(runnerCommandBuilder).buildDestroyScript(job, stack, null); + + assertThat(result) + .containsEntry("script", "apply destroy script"); + } + + @Test + void findFirstRunnableStep_shouldUsePlanStep() { + // when + var result = runnerController.findFirstRunnableStep(); + + // then + assertThat(result) + .containsEntry("id", "fakeStepId"); + } + + @Test + void findFirstRunnableStep_shouldUseStackImage() { + // when + var result = runnerController.findFirstRunnableStep(); + + // then + assertThat(result) + .containsEntry("image", "hashicorp/terraform:0.13.0"); + } + + @Test + void findFirstRunnableStep_shouldStackCredentials() { + //given + stack.setCredentialsId("fakeCredentials"); + + var credentials = mock(Credentials.class); + when(credentialsService.load("fakeCredentials")).thenReturn(Optional.of(credentials)); + when(credentials.toEnv()).thenReturn(List.of("access_key=value","secret_key=secretValue")); + + // when + var result = runnerController.findFirstRunnableStep(); + + // then + assertThat(result) + .containsEntry("env", credentials.toEnv()); + } +} 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/JobTest.java b/src/test/java/io/gaia_app/stacks/bo/JobTest.java index 93106b542..cc694af25 100644 --- a/src/test/java/io/gaia_app/stacks/bo/JobTest.java +++ b/src/test/java/io/gaia_app/stacks/bo/JobTest.java @@ -65,7 +65,7 @@ void end_shouldSetEndDateTime() { } @Test - void reset_shouldResetStatus() { + void reset_shouldResetStatusToPlanPending() { // given var job = new Job(); job.setStatus(JobStatus.PLAN_STARTED); @@ -74,7 +74,7 @@ void reset_shouldResetStatus() { job.reset(); // then - assertNull(job.getStatus()); + assertThat(job.getStatus()).isEqualTo(JobStatus.PLAN_PENDING); } @Test 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..75f8a3b01 100644 --- a/src/test/java/io/gaia_app/stacks/controller/JobRestControllerTest.kt +++ b/src/test/java/io/gaia_app/stacks/controller/JobRestControllerTest.kt @@ -1,10 +1,10 @@ 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.bo.Step import io.gaia_app.stacks.repository.JobRepository import io.gaia_app.stacks.repository.StackRepository import io.gaia_app.stacks.repository.StepRepository @@ -32,12 +32,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 +45,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 +76,18 @@ 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() // 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_PENDING) + verify(jobRepository).save(job) } @Test @@ -136,33 +99,20 @@ 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.PLAN_FINISHED + job.steps.add(Step()) // 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_PENDING) + verify(jobRepository).save(job) } @Test @@ -174,33 +124,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_PENDING) + 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..9fbae034a 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(); @@ -36,7 +37,7 @@ void jobShouldBeSavedWithLogs() throws IOException { Assertions.assertEquals("12", saved.get().getId()); Assertions.assertEquals("42", saved.get().getJobId()); Assertions.assertEquals(StepStatus.FINISHED, saved.get().getStatus()); - Assertions.assertEquals("some logs", saved.get().getLogs()); + Assertions.assertEquals(List.of("some logs"), saved.get().getLogs()); } } diff --git a/src/test/java/io/gaia_app/stacks/workflow/JobWorkflowTest.java b/src/test/java/io/gaia_app/stacks/workflow/JobWorkflowTest.java index d94220dc5..58dedb55c 100644 --- a/src/test/java/io/gaia_app/stacks/workflow/JobWorkflowTest.java +++ b/src/test/java/io/gaia_app/stacks/workflow/JobWorkflowTest.java @@ -47,27 +47,27 @@ void jobWorkflow_shouldSetDefaultState() { } @Test - void jobWorkflow_shouldHaveCurrentStep() { + void plan_shouldPlanState() { // given - var step = new Step(); + jobWorkflow.setState(jobState); // when - jobWorkflow.setCurrentStep(step); + jobWorkflow.plan(); // then - Assertions.assertEquals(step, jobWorkflow.getCurrentStep()); + verify(jobState).plan(jobWorkflow); } @Test - void plan_shouldPlanState() { + void start_shouldStartState() { // given jobWorkflow.setState(jobState); // when - jobWorkflow.plan(); + jobWorkflow.start(); // then - verify(jobState).plan(jobWorkflow); + verify(jobState).start(jobWorkflow); } @Test @@ -83,24 +83,65 @@ void apply_shouldApplyState() { } @Test - void end_shouldEndState() { + void end_shouldEndState_whenPlanIsSuccessful() { // given + job.setStatus(JobStatus.PLAN_STARTED); jobWorkflow.setState(jobState); // when - jobWorkflow.end(); + jobWorkflow.end(0); // then verify(jobState).end(jobWorkflow); } @Test - void fail_shouldFailState() { + void end_shouldEndState_whenPlanIsSuccessfulWithChanges() { // given + job.setStatus(JobStatus.PLAN_STARTED); jobWorkflow.setState(jobState); // when - jobWorkflow.fail(); + jobWorkflow.end(2); + + // then + verify(jobState).end(jobWorkflow); + } + + @Test + void end_shouldFailState_whenPlanIsUnSuccessful() { + // given + job.setStatus(JobStatus.PLAN_STARTED); + jobWorkflow.setState(jobState); + + // when + jobWorkflow.end(99); + + // then + verify(jobState).fail(jobWorkflow); + } + + @Test + void end_shouldEndState_whenApplyIsSuccessful() { + // given + job.setStatus(JobStatus.APPLY_STARTED); + jobWorkflow.setState(jobState); + + // when + jobWorkflow.end(0); + + // then + verify(jobState).end(jobWorkflow); + } + + @Test + void end_shouldFailState_whenApplyIsUnSuccessful() { + // given + job.setStatus(JobStatus.APPLY_STARTED); + jobWorkflow.setState(jobState); + + // when + jobWorkflow.end(99); // then verify(jobState).fail(jobWorkflow); @@ -127,6 +168,15 @@ void evalInitialState_shouldReturnDefaultState() { assertThat(result).isInstanceOf(NotStartedState.class); } + @Test + void evalInitialState_shouldReturnPlanPendingState() { + // when + var result = jobWorkflow.evalInitialState(JobStatus.PLAN_PENDING); + + // then + assertThat(result).isInstanceOf(PlanPendingState.class); + } + @Test void evalInitialState_shouldReturnPlanStartedState() { // when @@ -154,6 +204,15 @@ void evalInitialState_shouldReturnPlanFailedState() { assertThat(result).isInstanceOf(PlanFailedState.class); } + @Test + void evalInitialState_shouldReturnApplyPendingState() { + // when + var result = jobWorkflow.evalInitialState(JobStatus.APPLY_PENDING); + + // then + assertThat(result).isInstanceOf(ApplyPendingState.class); + } + @Test void evalInitialState_shouldReturnApplyStartedState() { // when diff --git a/src/test/java/io/gaia_app/stacks/workflow/state/ApplyFailedStateTest.java b/src/test/java/io/gaia_app/stacks/workflow/state/ApplyFailedStateTest.java index f8560f485..621855be0 100644 --- a/src/test/java/io/gaia_app/stacks/workflow/state/ApplyFailedStateTest.java +++ b/src/test/java/io/gaia_app/stacks/workflow/state/ApplyFailedStateTest.java @@ -1,8 +1,6 @@ package io.gaia_app.stacks.workflow.state; -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.JobWorkflow; import io.gaia_app.stacks.bo.Job; import io.gaia_app.stacks.bo.JobStatus; @@ -17,8 +15,7 @@ import java.time.LocalDateTime; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.verify; @@ -28,6 +25,7 @@ class ApplyFailedStateTest { @Mock JobWorkflow jobWorkflow; + private Job job; private ApplyFailedState state; @@ -45,44 +43,60 @@ void setup() { } @Test - void plan_shouldNotBePossible() { - assertThrows(UnsupportedOperationException.class, () -> state.plan(jobWorkflow)); + void retry_shouldResetJob() { + // when + state.retry(jobWorkflow); + + // then + assertEquals(JobStatus.PLAN_PENDING, job.getStatus()); + assertNull(job.getStartDateTime()); + assertNull(job.getEndDateTime()); } @Test - void apply_shouldNotBePossible() { - assertThrows(UnsupportedOperationException.class, () -> state.apply(jobWorkflow)); + void retry_shouldUpdateWorkflow() { + // when + state.retry(jobWorkflow); + + // then + verify(jobWorkflow).setState(any(PlanPendingState.class)); } @Test - void end_shouldNotBePossible() { - assertThrows(UnsupportedOperationException.class, () -> state.end(jobWorkflow)); + void retry_shouldCreateAPlanStep() { + // when + state.retry(jobWorkflow); + + // then + assertThat(job.getSteps()).isNotEmpty().hasSize(1); + var step = job.getSteps().get(0); + assertNotNull(step.getId()); + assertEquals(StepType.PLAN, step.getType()); + assertEquals(StepStatus.PENDING, step.getStatus()); } @Test - void fail_shouldNotBePossible() { - assertThrows(UnsupportedOperationException.class, () -> state.fail(jobWorkflow)); + void start_shouldNotBePossible() { + assertThrows(UnsupportedOperationException.class, () -> state.start(jobWorkflow)); } @Test - void retry_shouldResetJob() { - // when - state.retry(jobWorkflow); + void plan_shouldNotBePossible() { + assertThrows(UnsupportedOperationException.class, () -> state.plan(jobWorkflow)); + } - // then - assertNull(job.getStatus()); - assertNull(job.getStartDateTime()); - assertNull(job.getEndDateTime()); - assertThat(job.getSteps()).isNotNull().isEmpty(); + @Test + void apply_shouldNotBePossible() { + assertThrows(UnsupportedOperationException.class, () -> state.apply(jobWorkflow)); } @Test - void retry_shouldUpdateWorkflow() { - // when - state.retry(jobWorkflow); + void end_shouldNotBePossible() { + assertThrows(UnsupportedOperationException.class, () -> state.end(jobWorkflow)); + } - // then - verify(jobWorkflow).setState(any(NotStartedState.class)); - verify(jobWorkflow).plan(); + @Test + void fail_shouldNotBePossible() { + assertThrows(UnsupportedOperationException.class, () -> state.fail(jobWorkflow)); } } diff --git a/src/test/java/io/gaia_app/stacks/workflow/state/ApplyFinishedStateTest.java b/src/test/java/io/gaia_app/stacks/workflow/state/ApplyFinishedStateTest.java index e166b3fe9..bda56b954 100644 --- a/src/test/java/io/gaia_app/stacks/workflow/state/ApplyFinishedStateTest.java +++ b/src/test/java/io/gaia_app/stacks/workflow/state/ApplyFinishedStateTest.java @@ -23,6 +23,11 @@ void setup() { state = new ApplyFinishedState(); } + @Test + void start_shouldNotBePossible() { + assertThrows(UnsupportedOperationException.class, () -> state.start(jobWorkflow)); + } + @Test void plan_shouldNotBePossible() { assertThrows(UnsupportedOperationException.class, () -> state.plan(jobWorkflow)); diff --git a/src/test/java/io/gaia_app/stacks/workflow/state/ApplyPendingStateTest.java b/src/test/java/io/gaia_app/stacks/workflow/state/ApplyPendingStateTest.java new file mode 100644 index 000000000..e6c4cf324 --- /dev/null +++ b/src/test/java/io/gaia_app/stacks/workflow/state/ApplyPendingStateTest.java @@ -0,0 +1,97 @@ +package io.gaia_app.stacks.workflow.state; + +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.StepStatus; +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.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class ApplyPendingStateTest { + + @Mock + JobWorkflow jobWorkflow; + + private Job job; + private Step applyStep; + + private ApplyPendingState state; + + + @BeforeEach + void setup() { + job = new Job(); + job.setStatus(JobStatus.APPLY_PENDING); + job.setStartDateTime(LocalDateTime.now()); + job.setEndDateTime(LocalDateTime.now()); + + applyStep = new Step(); + job.getSteps().add(new Step()); // PLAN step + job.getSteps().add(applyStep); + + lenient().when(jobWorkflow.getJob()).thenReturn(job); // use lenient to prevent mockito from throwing exception for tests not needing this mock + lenient().when(jobWorkflow.getCurrentStep()).thenReturn(applyStep); // use lenient to prevent mockito from throwing exception for tests not needing this mock + + state = new ApplyPendingState(); + } + + @Test + void start_shouldStartTheStep(){ + state.start(jobWorkflow); + + assertThat(applyStep.getStatus()).isEqualTo(StepStatus.STARTED); + assertThat(applyStep.getStartDateTime()).isNotNull().isEqualToIgnoringSeconds(LocalDateTime.now()); + } + + @Test + void start_shouldUpdateWorkflow(){ + state.start(jobWorkflow); + + verify(jobWorkflow).setState(any(ApplyStartedState.class)); + } + + @Test + void start_shouldUpdateTheJob(){ + state.start(jobWorkflow); + + assertThat(job.getStatus()).isEqualTo(JobStatus.APPLY_STARTED); + } + + @Test + void plan_shouldNotBePossible() { + assertThrows(UnsupportedOperationException.class, () -> state.plan(jobWorkflow)); + } + + @Test + void apply_shouldNotBePossible() { + assertThrows(UnsupportedOperationException.class, () -> state.apply(jobWorkflow)); + } + + @Test + void end_shouldNotBePossible() { + assertThrows(UnsupportedOperationException.class, () -> state.end(jobWorkflow)); + } + + @Test + void fail_shouldNotBePossible() { + assertThrows(UnsupportedOperationException.class, () -> state.fail(jobWorkflow)); + } + + @Test + void retry_shouldNotBePossible() { + assertThrows(UnsupportedOperationException.class, () -> state.retry(jobWorkflow)); + } +} diff --git a/src/test/java/io/gaia_app/stacks/workflow/state/ApplyStartedStateTest.java b/src/test/java/io/gaia_app/stacks/workflow/state/ApplyStartedStateTest.java index 3c0ff8122..df1391e44 100644 --- a/src/test/java/io/gaia_app/stacks/workflow/state/ApplyStartedStateTest.java +++ b/src/test/java/io/gaia_app/stacks/workflow/state/ApplyStartedStateTest.java @@ -48,16 +48,6 @@ void setup() { state = new ApplyStartedState(); } - @Test - void plan_shouldNotBePossible() { - assertThrows(UnsupportedOperationException.class, () -> state.plan(jobWorkflow)); - } - - @Test - void apply_shouldNotBePossible() { - assertThrows(UnsupportedOperationException.class, () -> state.apply(jobWorkflow)); - } - @Test void end_shouldEndTheStep() { // when @@ -116,6 +106,21 @@ void fail_shouldUpdateWorkflow() { verify(jobWorkflow).setState(any(ApplyFailedState.class)); } + @Test + void start_shouldNotBePossible() { + assertThrows(UnsupportedOperationException.class, () -> state.start(jobWorkflow)); + } + + @Test + void plan_shouldNotBePossible() { + assertThrows(UnsupportedOperationException.class, () -> state.plan(jobWorkflow)); + } + + @Test + void apply_shouldNotBePossible() { + assertThrows(UnsupportedOperationException.class, () -> state.apply(jobWorkflow)); + } + @Test void retry_shouldNotBePossible() { assertThrows(UnsupportedOperationException.class, () -> state.retry(jobWorkflow)); diff --git a/src/test/java/io/gaia_app/stacks/workflow/state/NotStartedStateTest.java b/src/test/java/io/gaia_app/stacks/workflow/state/NotStartedStateTest.java index b530a86a1..4d6214b84 100644 --- a/src/test/java/io/gaia_app/stacks/workflow/state/NotStartedStateTest.java +++ b/src/test/java/io/gaia_app/stacks/workflow/state/NotStartedStateTest.java @@ -30,6 +30,7 @@ class NotStartedStateTest { @Mock JobWorkflow jobWorkflow; + private Job job; private NotStartedState state; @@ -44,17 +45,16 @@ void setup() { } @Test - void plan_shouldStartTheJob() { + void plan_shouldUpdateJob() { // when state.plan(jobWorkflow); // then - Assertions.assertEquals(JobStatus.PLAN_STARTED, job.getStatus()); - assertThat(job.getStartDateTime()).isNotNull().isEqualToIgnoringSeconds(LocalDateTime.now()); + assertEquals(JobStatus.PLAN_PENDING, job.getStatus()); } @Test - void plan_shouldStartAPlanStep() { + void plan_shouldCreateAPlanStep() { // when state.plan(jobWorkflow); @@ -63,9 +63,8 @@ void plan_shouldStartAPlanStep() { var step = job.getSteps().get(0); assertNotNull(step.getId()); assertEquals("test_jobId", step.getJobId()); - Assertions.assertEquals(StepType.PLAN, step.getType()); - Assertions.assertEquals(StepStatus.STARTED, step.getStatus()); - assertThat(step.getStartDateTime()).isNotNull().isEqualToIgnoringSeconds(LocalDateTime.now()); + assertEquals(StepType.PLAN, step.getType()); + assertEquals(StepStatus.PENDING, step.getStatus()); } @Test @@ -74,9 +73,12 @@ void plan_shouldUpdateWorkflow() { state.plan(jobWorkflow); // then - var step = job.getSteps().get(0); - verify(jobWorkflow).setCurrentStep(step); - verify(jobWorkflow).setState(any(PlanStartedState.class)); + verify(jobWorkflow).setState(any(PlanPendingState.class)); + } + + @Test + void start_shouldNotBePossible() { + assertThrows(UnsupportedOperationException.class, () -> state.start(jobWorkflow)); } @Test diff --git a/src/test/java/io/gaia_app/stacks/workflow/state/PlanFailedStateTest.java b/src/test/java/io/gaia_app/stacks/workflow/state/PlanFailedStateTest.java index 5dc0d529b..3765c29a5 100644 --- a/src/test/java/io/gaia_app/stacks/workflow/state/PlanFailedStateTest.java +++ b/src/test/java/io/gaia_app/stacks/workflow/state/PlanFailedStateTest.java @@ -1,8 +1,6 @@ package io.gaia_app.stacks.workflow.state; -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.JobWorkflow; import io.gaia_app.stacks.bo.Job; import io.gaia_app.stacks.bo.JobStatus; @@ -17,8 +15,8 @@ import java.time.LocalDateTime; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.verify; @@ -45,44 +43,60 @@ void setup() { } @Test - void plan_shouldNotBePossible() { - assertThrows(UnsupportedOperationException.class, () -> state.plan(jobWorkflow)); + void retry_shouldResetJob() { + // when + state.retry(jobWorkflow); + + // then + assertThat(job.getStatus()).isEqualTo(JobStatus.PLAN_PENDING); + assertNull(job.getStartDateTime()); + assertNull(job.getEndDateTime()); } @Test - void apply_shouldNotBePossible() { - assertThrows(UnsupportedOperationException.class, () -> state.apply(jobWorkflow)); + void retry_shouldUpdateWorkflow() { + // when + state.retry(jobWorkflow); + + // then + verify(jobWorkflow).setState(any(PlanPendingState.class)); } @Test - void end_shouldNotBePossible() { - assertThrows(UnsupportedOperationException.class, () -> state.end(jobWorkflow)); + void retry_shouldCreateAPlanStep() { + // when + state.retry(jobWorkflow); + + // then + assertThat(job.getSteps()).isNotEmpty().hasSize(1); + var step = job.getSteps().get(0); + assertNotNull(step.getId()); + assertEquals(StepType.PLAN, step.getType()); + assertEquals(StepStatus.PENDING, step.getStatus()); } @Test - void fail_shouldNotBePossible() { - assertThrows(UnsupportedOperationException.class, () -> state.fail(jobWorkflow)); + void start_shouldNotBePossible() { + assertThrows(UnsupportedOperationException.class, () -> state.start(jobWorkflow)); } @Test - void retry_shouldResetJob() { - // when - state.retry(jobWorkflow); + void plan_shouldNotBePossible() { + assertThrows(UnsupportedOperationException.class, () -> state.plan(jobWorkflow)); + } - // then - assertNull(job.getStatus()); - assertNull(job.getStartDateTime()); - assertNull(job.getEndDateTime()); - assertThat(job.getSteps()).isNotNull().isEmpty(); + @Test + void apply_shouldNotBePossible() { + assertThrows(UnsupportedOperationException.class, () -> state.apply(jobWorkflow)); } @Test - void retry_shouldUpdateWorkflow() { - // when - state.retry(jobWorkflow); + void end_shouldNotBePossible() { + assertThrows(UnsupportedOperationException.class, () -> state.end(jobWorkflow)); + } - // then - verify(jobWorkflow).setState(any(NotStartedState.class)); - verify(jobWorkflow).plan(); + @Test + void fail_shouldNotBePossible() { + assertThrows(UnsupportedOperationException.class, () -> state.fail(jobWorkflow)); } } diff --git a/src/test/java/io/gaia_app/stacks/workflow/state/PlanFinishedStateTest.java b/src/test/java/io/gaia_app/stacks/workflow/state/PlanFinishedStateTest.java index 2021870a0..73cc1ccb3 100644 --- a/src/test/java/io/gaia_app/stacks/workflow/state/PlanFinishedStateTest.java +++ b/src/test/java/io/gaia_app/stacks/workflow/state/PlanFinishedStateTest.java @@ -40,17 +40,12 @@ void setup() { } @Test - void plan_shouldNotBePossible() { - assertThrows(UnsupportedOperationException.class, () -> state.plan(jobWorkflow)); - } - - @Test - void apply_shouldStartTheJob() { + void apply_shouldMarkTheJobAsPending() { // when state.apply(jobWorkflow); // then - Assertions.assertEquals(JobStatus.APPLY_STARTED, job.getStatus()); + Assertions.assertEquals(JobStatus.APPLY_PENDING, job.getStatus()); } @Test @@ -64,8 +59,7 @@ void apply_shouldStartAnApplyStep() { assertNotNull(step.getId()); assertEquals("test_jobId", step.getJobId()); Assertions.assertEquals(StepType.APPLY, step.getType()); - Assertions.assertEquals(StepStatus.STARTED, step.getStatus()); - assertThat(step.getStartDateTime()).isNotNull().isEqualToIgnoringSeconds(LocalDateTime.now()); + Assertions.assertEquals(StepStatus.PENDING, step.getStatus()); } @Test @@ -74,9 +68,17 @@ void apply_shouldUpdateWorkflow() { state.apply(jobWorkflow); // then - var step = job.getSteps().get(1); - verify(jobWorkflow).setCurrentStep(step); - verify(jobWorkflow).setState(any(ApplyStartedState.class)); + verify(jobWorkflow).setState(any(ApplyPendingState.class)); + } + + @Test + void start_shouldNotBePossible() { + assertThrows(UnsupportedOperationException.class, () -> state.start(jobWorkflow)); + } + + @Test + void plan_shouldNotBePossible() { + assertThrows(UnsupportedOperationException.class, () -> state.plan(jobWorkflow)); } @Test diff --git a/src/test/java/io/gaia_app/stacks/workflow/state/PlanPendingStateTest.java b/src/test/java/io/gaia_app/stacks/workflow/state/PlanPendingStateTest.java new file mode 100644 index 000000000..607ce1907 --- /dev/null +++ b/src/test/java/io/gaia_app/stacks/workflow/state/PlanPendingStateTest.java @@ -0,0 +1,97 @@ +package io.gaia_app.stacks.workflow.state; + +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.StepStatus; +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.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class PlanPendingStateTest { + + @Mock + JobWorkflow jobWorkflow; + + private Job job; + + private PlanPendingState state; + + private Step planStep; + + @BeforeEach + void setup() { + job = new Job(); + job.setStatus(JobStatus.APPLY_PENDING); + job.setStartDateTime(LocalDateTime.now()); + job.setEndDateTime(LocalDateTime.now()); + + planStep = new Step(); + job.getSteps().add(planStep); + + lenient().when(jobWorkflow.getJob()).thenReturn(job); // use lenient to prevent mockito from throwing exception for tests not needing this mock + lenient().when(jobWorkflow.getCurrentStep()).thenReturn(planStep); // use lenient to prevent mockito from throwing exception for tests not needing this mock + + state = new PlanPendingState(); + } + + @Test + void start_shouldStartTheStep(){ + state.start(jobWorkflow); + + assertThat(planStep.getStatus()).isEqualTo(StepStatus.STARTED); + assertThat(planStep.getStartDateTime()).isNotNull().isEqualToIgnoringSeconds(LocalDateTime.now()); + } + + @Test + void start_shouldUpdateWorkflow(){ + state.start(jobWorkflow); + + verify(jobWorkflow).setState(any(PlanStartedState.class)); + } + + @Test + void start_shouldUpdateTheJob(){ + state.start(jobWorkflow); + + assertThat(job.getStatus()).isEqualTo(JobStatus.PLAN_STARTED); + assertThat(job.getStartDateTime()).isNotNull().isEqualToIgnoringSeconds(LocalDateTime.now()); + } + + @Test + void plan_shouldNotBePossible() { + assertThrows(UnsupportedOperationException.class, () -> state.plan(jobWorkflow)); + } + + @Test + void apply_shouldNotBePossible() { + assertThrows(UnsupportedOperationException.class, () -> state.apply(jobWorkflow)); + } + + @Test + void end_shouldNotBePossible() { + assertThrows(UnsupportedOperationException.class, () -> state.end(jobWorkflow)); + } + + @Test + void fail_shouldNotBePossible() { + assertThrows(UnsupportedOperationException.class, () -> state.fail(jobWorkflow)); + } + + @Test + void retry_shouldNotBePossible() { + assertThrows(UnsupportedOperationException.class, () -> state.retry(jobWorkflow)); + } +} diff --git a/src/test/java/io/gaia_app/stacks/workflow/state/PlanStartedStateTest.java b/src/test/java/io/gaia_app/stacks/workflow/state/PlanStartedStateTest.java index ceeaf13fd..24740e664 100644 --- a/src/test/java/io/gaia_app/stacks/workflow/state/PlanStartedStateTest.java +++ b/src/test/java/io/gaia_app/stacks/workflow/state/PlanStartedStateTest.java @@ -48,16 +48,6 @@ void setup() { state = new PlanStartedState(); } - @Test - void plan_shouldNotBePossible() { - assertThrows(UnsupportedOperationException.class, () -> state.plan(jobWorkflow)); - } - - @Test - void apply_shouldNotBePossible() { - assertThrows(UnsupportedOperationException.class, () -> state.apply(jobWorkflow)); - } - @Test void end_shouldEndTheStep() { // when @@ -116,6 +106,21 @@ void fail_shouldUpdateWorkflow() { verify(jobWorkflow).setState(any(PlanFailedState.class)); } + @Test + void start_shouldNotBePossible() { + assertThrows(UnsupportedOperationException.class, () -> state.start(jobWorkflow)); + } + + @Test + void plan_shouldNotBePossible() { + assertThrows(UnsupportedOperationException.class, () -> state.plan(jobWorkflow)); + } + + @Test + void apply_shouldNotBePossible() { + assertThrows(UnsupportedOperationException.class, () -> state.apply(jobWorkflow)); + } + @Test void retry_shouldNotBePossible() { assertThrows(UnsupportedOperationException.class, () -> state.retry(jobWorkflow));