Skip to content

Commit

Permalink
feat(executions): Adding new ADMIN endpoint to import execution (#3310)
Browse files Browse the repository at this point in the history
* feat(executions): Adding new endpoint to import execution from another spinnaker instance.

Useful to get a execution into other envs for troubleshooting.
Only imports executions with TERMINAL, SUCCEEDED or CANCELLED states.

* Disallow if execution already exists and also add some info helper messages
  • Loading branch information
srekapalli authored and mergify[bot] committed Nov 25, 2019
1 parent 24b5b40 commit da46413
Show file tree
Hide file tree
Showing 2 changed files with 122 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -43,6 +50,12 @@ class AdminController {
@Qualifier("discoveryStatusPoller")
ApplicationListener<ContextRefreshedEvent> discoveryStatusPoller

@Autowired
ExecutionRepository executionRepository

@Autowired(required = false)
Front50Service front50Service

@Autowired
ForceExecutionCancellationCommand forceExecutionCancellationCommand

Expand Down Expand Up @@ -74,6 +87,35 @@ class AdminController {
forceExecutionCancellationCommand.forceCancel(executionType, executionId, canceledBy)
}


@PostMapping(value = "/executions/")
@ResponseStatus(HttpStatus.CREATED)
Map<String, String> 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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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]
}

}

0 comments on commit da46413

Please sign in to comment.