diff --git a/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/job/PreconfiguredJobStage.groovy b/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/job/PreconfiguredJobStage.groovy index fc00e51ca8..f7b9be0b29 100644 --- a/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/job/PreconfiguredJobStage.groovy +++ b/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/job/PreconfiguredJobStage.groovy @@ -20,8 +20,10 @@ package com.netflix.spinnaker.orca.clouddriver.pipeline.job import com.netflix.spinnaker.orca.clouddriver.config.PreconfiguredJobStageProperties import com.netflix.spinnaker.orca.clouddriver.exception.PreconfiguredJobNotFoundException import com.netflix.spinnaker.orca.clouddriver.service.JobService +import com.netflix.spinnaker.orca.clouddriver.tasks.job.DestroyJobTask import com.netflix.spinnaker.orca.pipeline.TaskNode import com.netflix.spinnaker.orca.pipeline.model.Stage +import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Component @Component @@ -29,7 +31,9 @@ class PreconfiguredJobStage extends RunJobStage { private JobService jobService - public PreconfiguredJobStage(Optional optionalJobService) { + @Autowired + public PreconfiguredJobStage(DestroyJobTask destroyJobTask, Optional optionalJobService) { + super(destroyJobTask) this.jobService = optionalJobService.orElse(null) } diff --git a/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/job/RunJobStage.java b/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/job/RunJobStage.java index 65394360a1..dcc6091d80 100644 --- a/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/job/RunJobStage.java +++ b/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/job/RunJobStage.java @@ -16,20 +16,36 @@ package com.netflix.spinnaker.orca.clouddriver.pipeline.job; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.netflix.spinnaker.orca.CancellableStage; import com.netflix.spinnaker.orca.clouddriver.tasks.artifacts.ConsumeArtifactTask; +import com.netflix.spinnaker.orca.clouddriver.tasks.job.DestroyJobTask; import com.netflix.spinnaker.orca.clouddriver.tasks.job.MonitorJobTask; import com.netflix.spinnaker.orca.clouddriver.tasks.job.RunJobTask; import com.netflix.spinnaker.orca.clouddriver.tasks.job.WaitOnJobCompletion; +import com.netflix.spinnaker.orca.jackson.OrcaObjectMapper; import com.netflix.spinnaker.orca.pipeline.StageDefinitionBuilder; import com.netflix.spinnaker.orca.pipeline.TaskNode; import com.netflix.spinnaker.orca.pipeline.model.Stage; import com.netflix.spinnaker.orca.pipeline.tasks.artifacts.BindProducedArtifactsTask; import java.util.HashMap; import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component -public class RunJobStage implements StageDefinitionBuilder { +public class RunJobStage implements StageDefinitionBuilder, CancellableStage { + + private final Logger log = LoggerFactory.getLogger(getClass()); + private final DestroyJobTask destroyJobTask; + private final ObjectMapper objectMapper = OrcaObjectMapper.newInstance(); + + @Autowired + public RunJobStage(DestroyJobTask destroyJobTask) { + this.destroyJobTask = destroyJobTask; + } @Override public void taskGraph(Stage stage, TaskNode.Builder builder) { @@ -56,6 +72,43 @@ public void taskGraph(Stage stage, TaskNode.Builder builder) { } } + @Override + public Result cancel(Stage stage) { + log.info( + "Canceling run job stage {} for executionId {}", + stage.getId(), + stage.getExecution().getId()); + Map destroyContext = new HashMap<>(); + + try { + RunJobStageContext context = + objectMapper.convertValue(stage.getContext(), RunJobStageContext.class); + String jobId = context.getJobStatus().getId(); + + if (context.getCloudProvider().equalsIgnoreCase("titus")) { + destroyContext.put("jobId", jobId); + } else { + destroyContext.put("jobName", context.getJobStatus().getName()); + } + + destroyContext.put("cloudProvider", context.getCloudProvider()); + destroyContext.put("region", context.getJobStatus().getRegion()); + destroyContext.put("credentials", context.getCredentials()); + + stage.setContext(destroyContext); + + destroyJobTask.execute(stage); + } catch (Exception e) { + log.error( + String.format( + "Failed to cancel run job (stageId: %s, executionId: %s), e: %s", + stage.getId(), stage.getExecution().getId(), e.getMessage()), + e); + } + + return new Result(stage, destroyContext); + } + @Override public void prepareStageForRestart(Stage stage) { Map context = stage.getContext(); diff --git a/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/job/RunJobStageContext.java b/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/job/RunJobStageContext.java new file mode 100644 index 0000000000..eb1838683b --- /dev/null +++ b/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/job/RunJobStageContext.java @@ -0,0 +1,85 @@ +/* + * + * Copyright 2019 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License") + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.netflix.spinnaker.orca.clouddriver.pipeline.job; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.netflix.spinnaker.orca.clouddriver.pipeline.job.model.JobStatus; +import java.util.HashMap; +import java.util.Map; + +public class RunJobStageContext { + private JobStatus jobStatus; + private String cloudProvider; + private String cloudProviderType; + private String application; + private String credentials; + + @JsonIgnore private Map other = new HashMap<>(); + + @JsonAnyGetter + public Map other() { + return other; + } + + @JsonAnySetter + public void set(String name, Object value) { + other.put(name, value); + } + + public JobStatus getJobStatus() { + return jobStatus; + } + + public void setJobStatus(JobStatus jobStatus) { + this.jobStatus = jobStatus; + } + + public String getCloudProvider() { + return cloudProvider; + } + + public void setCloudProvider(String cloudProvider) { + this.cloudProvider = cloudProvider; + } + + public String getCloudProviderType() { + return cloudProviderType; + } + + public void setCloudProviderType(String cloudProviderType) { + this.cloudProviderType = cloudProviderType; + } + + public String getApplication() { + return application; + } + + public void setApplication(String application) { + this.application = application; + } + + public String getCredentials() { + return credentials; + } + + public void setCredentials(String credentials) { + this.credentials = credentials; + } +} diff --git a/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/job/model/CompletionDetails.java b/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/job/model/CompletionDetails.java new file mode 100644 index 0000000000..30a281b8e6 --- /dev/null +++ b/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/job/model/CompletionDetails.java @@ -0,0 +1,26 @@ +/* + * + * Copyright 2019 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License") + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.netflix.spinnaker.orca.clouddriver.pipeline.job.model; + +import lombok.Data; + +@Data +public class CompletionDetails { + private String instanceId; + private String taskId; +} diff --git a/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/job/model/JobStatus.java b/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/job/model/JobStatus.java new file mode 100644 index 0000000000..5610ba1c68 --- /dev/null +++ b/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/job/model/JobStatus.java @@ -0,0 +1,34 @@ +/* + * + * Copyright 2019 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License") + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.netflix.spinnaker.orca.clouddriver.pipeline.job.model; + +import lombok.Data; + +@Data +public class JobStatus { + private String application; + private String provider; + private CompletionDetails completionDetails; + private String jobState; + private String name; + private Long createdTime; + private String id; + private String type; + private String region; + private String account; +} diff --git a/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/tasks/job/DestroyJobTask.groovy b/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/tasks/job/DestroyJobTask.groovy index 71d4914c71..befd29ebd7 100644 --- a/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/tasks/job/DestroyJobTask.groovy +++ b/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/tasks/job/DestroyJobTask.groovy @@ -41,7 +41,7 @@ class DestroyJobTask extends AbstractCloudProviderAwareTask implements Task { Map outputs = [ "notification.type" : "destroyjob", "kato.last.task.id" : taskId, - "delete.name" : stage.context.jobName, + "delete.name" : stage.context.jobName ?: stage.context.jobId, "delete.region" : stage.context.region, "delete.account.name": account ] diff --git a/orca-clouddriver/src/test/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/job/PreconfiguredJobStageSpec.groovy b/orca-clouddriver/src/test/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/job/PreconfiguredJobStageSpec.groovy index b74aa3289d..99a9c5461e 100644 --- a/orca-clouddriver/src/test/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/job/PreconfiguredJobStageSpec.groovy +++ b/orca-clouddriver/src/test/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/job/PreconfiguredJobStageSpec.groovy @@ -19,6 +19,7 @@ package com.netflix.spinnaker.orca.clouddriver.pipeline.job import com.netflix.spinnaker.orca.clouddriver.config.PreconfiguredJobStageParameter import com.netflix.spinnaker.orca.clouddriver.service.JobService import com.netflix.spinnaker.orca.clouddriver.config.KubernetesPreconfiguredJobProperties +import com.netflix.spinnaker.orca.clouddriver.tasks.job.DestroyJobTask import spock.lang.Specification import static com.netflix.spinnaker.orca.test.model.ExecutionBuilder.stage @@ -41,7 +42,7 @@ class PreconfiguredJobStageSpec extends Specification { } when: - PreconfiguredJobStage preconfiguredJobStage = new PreconfiguredJobStage(Optional.of(jobService)) + PreconfiguredJobStage preconfiguredJobStage = new PreconfiguredJobStage(Mock(DestroyJobTask), Optional.of(jobService)) preconfiguredJobStage.buildTaskGraph(stage) then: