diff --git a/front50-api/src/main/java/com/netflix/spinnaker/front50/api/model/pipeline/Pipeline.java b/front50-api/src/main/java/com/netflix/spinnaker/front50/api/model/pipeline/Pipeline.java index 8f57741f4..e36456231 100644 --- a/front50-api/src/main/java/com/netflix/spinnaker/front50/api/model/pipeline/Pipeline.java +++ b/front50-api/src/main/java/com/netflix/spinnaker/front50/api/model/pipeline/Pipeline.java @@ -21,9 +21,11 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; +@EqualsAndHashCode public class Pipeline implements Timestamped { public static final String TYPE_TEMPLATED = "templatedPipeline"; diff --git a/front50-web/src/test/groovy/com/netflix/spinnaker/front50/controllers/PipelineControllerTck.groovy b/front50-web/src/test/groovy/com/netflix/spinnaker/front50/controllers/PipelineControllerTck.groovy index cfe23c621..fc286855b 100644 --- a/front50-web/src/test/groovy/com/netflix/spinnaker/front50/controllers/PipelineControllerTck.groovy +++ b/front50-web/src/test/groovy/com/netflix/spinnaker/front50/controllers/PipelineControllerTck.groovy @@ -38,6 +38,7 @@ import org.springframework.beans.factory.ObjectProvider import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter import org.springframework.web.util.UriComponentsBuilder +import java.nio.charset.StandardCharsets import java.time.Clock import java.util.concurrent.Callable import java.util.concurrent.Executors @@ -56,6 +57,7 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status @@ -334,6 +336,159 @@ abstract class PipelineControllerTck extends Specification { response.errorMessage == "A pipeline with name pipeline1 already exists in application test" } + void 'should not refresh cache when checking for duplicates when saving'() { + given: + def pipeline = [name: "My Pipeline", application: "test"] + config.save.refreshCacheOnDuplicatesCheck = false + + when: + def response = mockMvc.perform(post('/pipelines') + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(pipeline))) + .andReturn() + .response + + then: + response.status == OK + 1 * pipelineDAO.getPipelinesByApplication("test", false) + + when: + pipeline.name = "My Second Pipeline" + config.save.refreshCacheOnDuplicatesCheck = true + response = mockMvc.perform(post('/pipelines') + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(pipeline))) + .andReturn() + .response + + then: + response.status == OK + 1 * pipelineDAO.getPipelinesByApplication("test", true) + } + + def "should perform batch update"() { + given: + def pipelines = [ + new Pipeline([name: "My Pipeline1", application: "test1", id: "id1", triggers: []]), + new Pipeline([name: "My Pipeline2", application: "test1", id: "id2", triggers: []]), + new Pipeline([name: "My Pipeline3", application: "test2", id: "id3", triggers: []]), + new Pipeline([name: "My Pipeline4", application: "test2", id: "id4", triggers: []]) + ] + + when: + def response = mockMvc.perform(post('/pipelines/batchUpdate') + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(pipelines))) + .andReturn() + .response + + then: + response.status == OK + 1 * fiatPermissionEvaluator.hasPermission(_, "test1", "APPLICATION", "WRITE") >> true + 1 * fiatPermissionEvaluator.hasPermission(_, "test2", "APPLICATION", "WRITE") >> true + 1 * pipelineDAO.bulkImport(pipelines) >> null + new JsonSlurper().parseText(response.getContentAsString()) == [ + successful_pipelines_count: 4, + successful_pipelines : ["My Pipeline1", "My Pipeline2", "My Pipeline3", "My Pipeline4"], + failed_pipelines_count : 0, + failed_pipelines : [] + ] + } + + def "should perform batch updates with failures"() { + given: + def pipelines = [ + new Pipeline([name: "Successful Pipeline 1", application: "test_app", id: "id1", triggers: []]), + new Pipeline([id: "id2", triggers: []]), + new Pipeline([name: "Failed Pipeline 3", application: "test_app_without_permission", id: "id3", triggers: []]), + new Pipeline([name: "Failed Pipeline 4", application: "test_app", id: "id4", triggers: []]), + new Pipeline([name: "Failed Pipeline 5", application: "test_app", id: "id1", triggers: []]), + [name: "Failed Pipeline 6", application: "test_app", id: "id6", triggers: [:]], + new Pipeline([name: "Failed Pipeline 7", application: "test_app", id: "id7", + triggers: [[runAsUser: "not_accessible"]]]) + ] + + // Success case + when: + def response = mockMvc.perform(post('/pipelines/batchUpdate') + .contentType(MediaType.APPLICATION_JSON) + .characterEncoding(StandardCharsets.UTF_8.toString()) + .content(objectMapper.writeValueAsString(pipelines))) + .andDo(print()) + .andReturn() + .response + + then: + 1 * pipelineDAO.all(false) >> [ + [name: "Failed Pipeline 4", application: "test_app", id: "existing_pipeline_id"] as Pipeline + ] + 1 * fiatPermissionEvaluator.hasPermission(_, "test_app", "APPLICATION", "WRITE") >> true + 1 * fiatPermissionEvaluator.hasPermission(_, "test_app_without_permission", "APPLICATION", "WRITE") >> false + 1 * pipelineDAO.bulkImport(pipelines[0..0]) >> null + 1 * authorizationSupport.hasRunAsUserPermission(pipelines[6]) >> false + response.status == OK + new JsonSlurper().parseText(response.getContentAsString()) == [ + successful_pipelines_count: 1, + successful_pipelines: ["Successful Pipeline 1"], + failed_pipelines_count : 6, + failed_pipelines : [ + [ + id : "id6", + name : "Failed Pipeline 6", + application : "test_app", + triggers : [:], + errorMsg : "Failed to deserialize the pipeline json into a valid pipeline: " + + "java.lang.IllegalArgumentException: Cannot deserialize value of type " + + "`java.util.ArrayList` " + + "from Object value (token `JsonToken.START_OBJECT`)\n at [Source: UNKNOWN; byte offset: #UNKNOWN] " + + "(through reference chain: com.netflix.spinnaker.front50.api.model.pipeline.Pipeline[\"triggers\"])" + ], + [ + id : "id7", + name : "Failed Pipeline 7", + application : "test_app", + schema : "1", + triggers: [[runAsUser: "not_accessible"]], + errorMsg : "Validation of runAsUser permissions for pipeline Failed Pipeline 7 " + + "in the application test_app failed." + ], + [ + id : "id2", + schema : "1", + triggers : [], + errorMsg : "Encountered the following error when validating pipeline null in the application null: " + + "A pipeline requires name and application fields" + ], + [ + id : "id3", + name : "Failed Pipeline 3", + application : "test_app_without_permission", + schema : "1", + triggers : [], + errorMsg : "User anonymous does not have WRITE permission " + + "to save the pipeline Failed Pipeline 3 in the application test_app_without_permission." + ], + [ + id : "id4", + name : "Failed Pipeline 4", + application : "test_app", + schema : "1", + triggers : [], + errorMsg : "A pipeline with name Failed Pipeline 4 already exists in the application test_app" + ], + [ + id : "id1", + name : "Failed Pipeline 5", + application : "test_app", + schema : "1", + triggers : [], + errorMsg : "Duplicate pipeline id id1 found when processing pipeline Failed Pipeline 5 " + + "in the application test_app" + ] + ] + ] + } + @Unroll void "pipeline with limitConcurrent = #limitConcurrent and maxConcurrentExecutions = #maxConcurrentExecutions"() { def appName = "test"