diff --git a/orca-web/src/main/groovy/com/netflix/spinnaker/orca/controllers/AdminController.groovy b/orca-web/src/main/groovy/com/netflix/spinnaker/orca/controllers/AdminController.groovy index a0e9102a92..d8fb8739b2 100644 --- a/orca-web/src/main/groovy/com/netflix/spinnaker/orca/controllers/AdminController.groovy +++ b/orca-web/src/main/groovy/com/netflix/spinnaker/orca/controllers/AdminController.groovy @@ -18,16 +18,23 @@ package com.netflix.spinnaker.orca.controllers import com.netflix.spinnaker.kork.exceptions.HasAdditionalAttributes +import com.netflix.spinnaker.kork.web.exceptions.InvalidRequestException import com.netflix.spinnaker.kork.web.exceptions.ValidationException +import com.netflix.spinnaker.orca.ExecutionStatus import com.netflix.spinnaker.orca.commands.ForceExecutionCancellationCommand import com.netflix.spinnaker.orca.eureka.NoDiscoveryApplicationStatusPublisher +import com.netflix.spinnaker.orca.front50.Front50Service +import com.netflix.spinnaker.orca.front50.model.Application import com.netflix.spinnaker.orca.pipeline.model.Execution +import com.netflix.spinnaker.orca.pipeline.persistence.ExecutionNotFoundException +import com.netflix.spinnaker.orca.pipeline.persistence.ExecutionRepository import groovy.util.logging.Slf4j import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Qualifier import org.springframework.context.ApplicationListener import org.springframework.context.event.ContextRefreshedEvent import org.springframework.http.HttpStatus +import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestMethod @@ -43,6 +50,12 @@ class AdminController { @Qualifier("discoveryStatusPoller") ApplicationListener discoveryStatusPoller + @Autowired + ExecutionRepository executionRepository + + @Autowired(required = false) + Front50Service front50Service + @Autowired ForceExecutionCancellationCommand forceExecutionCancellationCommand @@ -74,6 +87,35 @@ class AdminController { forceExecutionCancellationCommand.forceCancel(executionType, executionId, canceledBy) } + + @PostMapping(value = "/executions/") + @ResponseStatus(HttpStatus.CREATED) + Map createExecution(@RequestBody Execution execution) { + + if (front50Service && !front50Service.get(execution.application)) { + log.warn('No application exists with name: ' + execution.application) + } + + try { + executionRepository.retrieve(execution.type, execution.id) + log.warn('Execution found with id: []', execution.id) + throw new InvalidRequestException('Execution already exists with id: ' + execution.id) + } catch(ExecutionNotFoundException e) { + log.info('Execution not found .. can import it..') + } + + if (execution.status in [ExecutionStatus.CANCELED, ExecutionStatus.SUCCEEDED, ExecutionStatus.TERMINAL]) { + log.info('Importing execution with id: {}, status: {} , stages: {}', execution.id, execution.status, execution.stages.size()) + execution.stages.each { + it.execution = execution + } + executionRepository.store(execution) + return ['executionId': execution.id, 'status': execution.status, 'totalStages': execution.stages.size()] + } + + throw new InvalidRequestException('Cannot import provided execution, Status: ' + execution.status) + } + @ResponseStatus(HttpStatus.BAD_REQUEST) private static class DiscoveryUnchangeableException extends IllegalStateException implements HasAdditionalAttributes { DiscoveryUnchangeableException(String message, Class discoveryPoller) { diff --git a/orca-web/src/test/groovy/com/netflix/spinnaker/orca/controllers/AdminControllerSpec.groovy b/orca-web/src/test/groovy/com/netflix/spinnaker/orca/controllers/AdminControllerSpec.groovy new file mode 100644 index 0000000000..b586c27bbe --- /dev/null +++ b/orca-web/src/test/groovy/com/netflix/spinnaker/orca/controllers/AdminControllerSpec.groovy @@ -0,0 +1,80 @@ +/* + * 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.controllers + +import com.netflix.spinnaker.kork.web.exceptions.InvalidRequestException +import com.netflix.spinnaker.orca.ExecutionStatus +import com.netflix.spinnaker.orca.pipeline.model.Execution +import com.netflix.spinnaker.orca.pipeline.persistence.ExecutionNotFoundException +import com.netflix.spinnaker.orca.pipeline.persistence.ExecutionRepository +import de.huxhorn.sulky.ulid.ULID +import spock.lang.Specification +import spock.lang.Unroll + +class AdminControllerSpec extends Specification { + + ExecutionRepository executionRepository = Mock(ExecutionRepository) + + AdminController controller = new AdminController(executionRepository: executionRepository) + + def setup() { + executionRepository = Mock(ExecutionRepository) + controller = new AdminController(executionRepository: executionRepository) + } + + @Unroll + def 'should throw error while saving execution with status: #invalidStatus '() { + given: + String executionId = new ULID().nextULID() + Execution execution = new Execution(Execution.ExecutionType.PIPELINE, executionId, 'testapp') + + when: + execution.status = invalidStatus + controller.createExecution(execution) + + then: + thrown(InvalidRequestException) + 1 * executionRepository.retrieve(Execution.ExecutionType.PIPELINE, executionId) >> { throw new ExecutionNotFoundException('No execution')} + 0 * _ + + where: + invalidStatus << [ExecutionStatus.RUNNING, ExecutionStatus.PAUSED, ExecutionStatus.NOT_STARTED] + } + + @Unroll + def 'should succeed while saving execution with status: #validStatus '() { + given: + String executionId = new ULID().nextULID() + Execution execution = new Execution(Execution.ExecutionType.PIPELINE, executionId, 'testapp') + + when: + execution.status = validStatus + Map result = controller.createExecution(execution) + + then: + noExceptionThrown() + 1 * executionRepository.retrieve(Execution.ExecutionType.PIPELINE, executionId) >> { throw new ExecutionNotFoundException('No execution')} + 1 * executionRepository.store(execution) + 0 * _ + result.executionId == executionId + result.status == validStatus + + where: + validStatus << [ExecutionStatus.SUCCEEDED, ExecutionStatus.CANCELED, ExecutionStatus.TERMINAL] + } + +}