Skip to content

Commit

Permalink
fix(executions): Only import executions if application exists in Fron…
Browse files Browse the repository at this point in the history
…t50 (#3316)

* fix(executions): Only import executions if applicaiton exists in Front50

* fix(executions): visible only if config is enabled

* Fix(executions): typo fix

* fix(executions): Favor POJO instead of a Map response
  • Loading branch information
srekapalli authored and mergify[bot] committed Nov 27, 2019
1 parent e81778b commit 0b4557c
Show file tree
Hide file tree
Showing 5 changed files with 252 additions and 113 deletions.
Expand Up @@ -18,23 +18,18 @@
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 Down Expand Up @@ -88,34 +83,6 @@ class AdminController {
}


@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
@@ -0,0 +1,113 @@
/*
* 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.front50.Front50Service;
import com.netflix.spinnaker.orca.front50.model.Application;
import com.netflix.spinnaker.orca.model.ExecutionImportResponse;
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 java.util.Collections;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
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.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import retrofit.RetrofitError;

@RestController
@RequestMapping("/admin/executions")
@Slf4j
@ConditionalOnProperty(value = "executions.import.enabled", matchIfMissing = false)
public class ExecutionsImportController {

private final Front50Service front50Service;

private final ExecutionRepository executionRepository;

private Set<ExecutionStatus> ALLOWED_STATUSES =
Collections.unmodifiableSet(
Stream.of(ExecutionStatus.CANCELED, ExecutionStatus.SUCCEEDED, ExecutionStatus.TERMINAL)
.collect(Collectors.toSet()));

@Autowired
ExecutionsImportController(
ExecutionRepository executionRepository, Front50Service front50Service) {
this.front50Service = front50Service;
this.executionRepository = executionRepository;
}

@PostMapping(value = "")
@ResponseStatus(HttpStatus.CREATED)
ExecutionImportResponse createExecution(@RequestBody Execution execution) {

// Check if app exists before importing execution.
Application application = null;
try {
application = front50Service.get(execution.getApplication());
log.info("Importing application with name: {}", application.name);
} catch (RetrofitError e) {
if (e.getKind() == RetrofitError.Kind.HTTP && e.getResponse().getStatus() != 404) {
log.warn("Exception received while retrieving application from front50", e);
}
}

if (application == null) {
log.info(
"Application {} not found in front50, but still importing it",
execution.getApplication());
}

// Continue importing even if we can't retrieve the APP.
try {
executionRepository.retrieve(execution.getType(), execution.getId());
throw new InvalidRequestException("Execution already exists with id: " + execution.getId());
} catch (ExecutionNotFoundException e) {
log.info("Execution not found: {}, Will continue with importing..", execution.getId());
}

if (ALLOWED_STATUSES.contains(execution.getStatus())) {
log.info(
"Importing execution with id: {}, status: {} , stages: {}",
execution.getId(),
execution.getStatus(),
execution.getStages().size());
execution
.getStages()
.forEach(
stage -> {
stage.setExecution(execution);
});
executionRepository.store(execution);
return new ExecutionImportResponse(
execution.getId(), execution.getStatus(), execution.getStages().size());
}

throw new InvalidRequestException(
"Cannot import provided execution, Status: " + execution.getStatus());
}
}
@@ -0,0 +1,32 @@
/*
* 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.model;

import com.netflix.spinnaker.orca.ExecutionStatus;
import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class ExecutionImportResponse {

private String executionId;

private ExecutionStatus executionStatus;

private Integer totalStages;
}

This file was deleted.

@@ -0,0 +1,107 @@
/*
* 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.front50.Front50Service
import com.netflix.spinnaker.orca.front50.model.Application
import com.netflix.spinnaker.orca.model.ExecutionImportResponse
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 retrofit.RetrofitError
import spock.lang.Specification
import spock.lang.Unroll

class ExecutionsImportControllerSpec extends Specification {

ExecutionRepository executionRepository = Mock(ExecutionRepository)

Front50Service front50Service = Mock(Front50Service)

ExecutionsImportController controller

def setup() {
executionRepository = Mock(ExecutionRepository)
controller = new ExecutionsImportController(executionRepository, front50Service)
}

@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 * front50Service.get('testapp') >> { new Application(name: 'testapp') }
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
ExecutionImportResponse result = controller.createExecution(execution)

then:
noExceptionThrown()
1 * front50Service.get('testapp') >> { new Application(name: 'testapp') }
1 * executionRepository.retrieve(Execution.ExecutionType.PIPELINE, executionId) >> { throw new ExecutionNotFoundException('No execution')}
1 * executionRepository.store(execution)
0 * _
result.executionId == executionId
result.executionStatus == validStatus

where:
validStatus << [ExecutionStatus.SUCCEEDED, ExecutionStatus.CANCELED, ExecutionStatus.TERMINAL]
}

def 'should succeed to import if unable to retrieve app from front50'() {
given:
String executionId = new ULID().nextULID()
Execution execution = new Execution(Execution.ExecutionType.PIPELINE, executionId, 'testapp')
execution.status = ExecutionStatus.SUCCEEDED

when:
ExecutionImportResponse result = controller.createExecution(execution)

then:
noExceptionThrown()
1 * front50Service.get('testapp') >> { throw RetrofitError.unexpectedError('http://test.front50.com', new RuntimeException())}
1 * executionRepository.retrieve(Execution.ExecutionType.PIPELINE, executionId) >> { throw new ExecutionNotFoundException('No execution')}
1 * executionRepository.store(execution)
0 * _
result.executionId == executionId
result.executionStatus == ExecutionStatus.SUCCEEDED
}

}

0 comments on commit 0b4557c

Please sign in to comment.