From 3c58ab7ca50ec96d58634cb1f99a1deab47ea6d5 Mon Sep 17 00:00:00 2001 From: Andres Castano Date: Wed, 13 Nov 2019 16:03:45 +1100 Subject: [PATCH] feat(gcb): Ability to invoke existing GCB triggers (#956) * feat(gcb): Ability to invoke existing GCB triggers * style(gcb): Extracted some inner classes to their own file * fix(gcb): Renamed json parameter to match igor api --- .../GoogleCloudBuildController.java | 16 ++- .../gate/services/BuildMasterNotFound.java | 28 +++++ .../gate/services/BuildService.groovy | 25 +++-- .../gate/services/GCBAccountNotFound.java | 28 +++++ .../internal/GoogleCloudBuildTrigger.java | 35 ++++++ .../gate/services/internal/IgorService.groovy | 6 +- .../GoogleCloudBuildControllerSpec.groovy | 103 ++++++++++++++++++ 7 files changed, 232 insertions(+), 9 deletions(-) create mode 100644 gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/BuildMasterNotFound.java create mode 100644 gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/GCBAccountNotFound.java create mode 100644 gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/internal/GoogleCloudBuildTrigger.java create mode 100644 gate-web/src/test/groovy/com/netflix/spinnaker/gate/controllers/GoogleCloudBuildControllerSpec.groovy diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/GoogleCloudBuildController.java b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/GoogleCloudBuildController.java index 9fd78324f0..9f33a2d23b 100644 --- a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/GoogleCloudBuildController.java +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/GoogleCloudBuildController.java @@ -16,12 +16,15 @@ package com.netflix.spinnaker.gate.controllers; +import com.netflix.spinnaker.gate.services.BuildService; +import com.netflix.spinnaker.gate.services.internal.GoogleCloudBuildTrigger; import com.netflix.spinnaker.gate.services.internal.IgorService; import io.swagger.annotations.ApiOperation; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -30,10 +33,12 @@ @RequestMapping("/gcb") public class GoogleCloudBuildController { private IgorService igorService; + private BuildService buildService; @Autowired - public GoogleCloudBuildController(IgorService igorService) { + public GoogleCloudBuildController(IgorService igorService, BuildService buildService) { this.igorService = igorService; + this.buildService = buildService; } @ApiOperation(value = "Retrieve the list of Google Cloud Build accounts", response = List.class) @@ -41,4 +46,13 @@ public GoogleCloudBuildController(IgorService igorService) { List getAccounts() { return igorService.getGoogleCloudBuildAccounts(); } + + @ApiOperation( + value = "Retrieve the list of Google Cloud Build triggers for a given account", + response = List.class) + @GetMapping(value = "/triggers/{account}") + List getGoogleCloudBuildTriggers( + @PathVariable("account") String account) { + return buildService.getGoogleCloudBuildTriggersForAccount(account); + } } diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/BuildMasterNotFound.java b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/BuildMasterNotFound.java new file mode 100644 index 0000000000..0034c22deb --- /dev/null +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/BuildMasterNotFound.java @@ -0,0 +1,28 @@ +/* + * Copyright 2019 Andres Castano. + * + * 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.gate.services; + +import com.netflix.hystrix.exception.HystrixBadRequestException; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.NOT_FOUND) +public class BuildMasterNotFound extends HystrixBadRequestException { + public BuildMasterNotFound(String message) { + super(message); + } +} diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/BuildService.groovy b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/BuildService.groovy index 0499f93baa..12478eae38 100644 --- a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/BuildService.groovy +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/BuildService.groovy @@ -18,15 +18,12 @@ package com.netflix.spinnaker.gate.services -import com.netflix.hystrix.exception.HystrixBadRequestException import com.netflix.spinnaker.gate.services.commands.HystrixFactory +import com.netflix.spinnaker.gate.services.internal.GoogleCloudBuildTrigger import com.netflix.spinnaker.gate.services.internal.IgorService import groovy.transform.CompileStatic -import groovy.transform.InheritConstructors import org.springframework.beans.factory.annotation.Autowired -import org.springframework.http.HttpStatus import org.springframework.stereotype.Component -import org.springframework.web.bind.annotation.ResponseStatus import org.springframework.web.util.UriUtils import retrofit.RetrofitError @@ -82,6 +79,23 @@ class BuildService { } execute() } + List getGoogleCloudBuildTriggersForAccount(String account) { + if (!igorService) { + return [] + } + HystrixFactory.newListCommand(GROUP, "triggersForGcbAccount") { + try { + igorService.getGoogleCloudBuildTriggers(account) + } catch (RetrofitError e) { + if (e.response?.status == 404) { + throw new GCBAccountNotFound("Account '${account}' not found") + } + + throw e + } + } execute() + } + Map getJobConfig(String buildMaster, String job) { if (!igorService) { return [:] @@ -133,7 +147,4 @@ class BuildService { } execute() } - @ResponseStatus(HttpStatus.NOT_FOUND) - @InheritConstructors - static class BuildMasterNotFound extends HystrixBadRequestException {} } diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/GCBAccountNotFound.java b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/GCBAccountNotFound.java new file mode 100644 index 0000000000..50cb64c4df --- /dev/null +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/GCBAccountNotFound.java @@ -0,0 +1,28 @@ +/* + * Copyright 2019 Andres Castano. + * + * 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.gate.services; + +import com.netflix.hystrix.exception.HystrixBadRequestException; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.NOT_FOUND) +public class GCBAccountNotFound extends HystrixBadRequestException { + public GCBAccountNotFound(String message) { + super(message); + } +} diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/internal/GoogleCloudBuildTrigger.java b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/internal/GoogleCloudBuildTrigger.java new file mode 100644 index 0000000000..8ced7a0283 --- /dev/null +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/internal/GoogleCloudBuildTrigger.java @@ -0,0 +1,35 @@ +/* + * Copyright 2019 Andres Castano + * + * 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.gate.services.internal; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class GoogleCloudBuildTrigger { + GoogleCloudBuildTrigger() {} + + public GoogleCloudBuildTrigger(String id, String name, String description) { + this.id = id; + this.name = name; + this.description = description; + } + + @JsonProperty private String id; + + @JsonProperty private String name; + + @JsonProperty private String description; +} diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/internal/IgorService.groovy b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/internal/IgorService.groovy index 91a735a28f..499e9735fb 100644 --- a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/internal/IgorService.groovy +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/internal/IgorService.groovy @@ -18,7 +18,7 @@ package com.netflix.spinnaker.gate.services.internal - +import com.fasterxml.jackson.annotation.JsonProperty import retrofit.http.EncodedPath import retrofit.http.GET import retrofit.http.Path @@ -71,6 +71,9 @@ interface IgorService { @GET('/gcb/accounts') List getGoogleCloudBuildAccounts(); + @GET('/gcb/triggers/{account}') + List getGoogleCloudBuildTriggers(@Path("account") String account); + @GET('/artifacts/{provider}/{packageName}') List getArtifactVersions(@Path("provider") String provider, @Path("packageName") String packageName, @@ -83,4 +86,5 @@ interface IgorService { @GET('/concourse/{buildMaster}/teams/{team}/pipelines/{pipeline}/resources') List getConcourseResources(@Path("buildMaster") String buildMaster, @Path("team") String team, @Path("pipeline") String pipeline); + } diff --git a/gate-web/src/test/groovy/com/netflix/spinnaker/gate/controllers/GoogleCloudBuildControllerSpec.groovy b/gate-web/src/test/groovy/com/netflix/spinnaker/gate/controllers/GoogleCloudBuildControllerSpec.groovy new file mode 100644 index 0000000000..84ca8ddcec --- /dev/null +++ b/gate-web/src/test/groovy/com/netflix/spinnaker/gate/controllers/GoogleCloudBuildControllerSpec.groovy @@ -0,0 +1,103 @@ +/* + * Copyright 2019 Andres Castano + * + * 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.gate.controllers + +import com.fasterxml.jackson.databind.ObjectMapper +import com.netflix.spinnaker.gate.services.BuildService +import com.netflix.spinnaker.gate.services.internal.GoogleCloudBuildTrigger +import com.netflix.spinnaker.gate.services.internal.IgorService +import com.squareup.okhttp.mockwebserver.MockWebServer +import org.springframework.http.MediaType +import org.springframework.mock.web.MockHttpServletResponse +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.setup.MockMvcBuilders +import retrofit.RetrofitError +import retrofit.client.Response +import spock.lang.Shared +import spock.lang.Specification + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get + +class GoogleCloudBuildControllerSpec extends Specification { + + MockMvc mockMvc + BuildService buildService + IgorService igorService + + def server = new MockWebServer() + + @Shared def objectMapper = new ObjectMapper() + @Shared def ACCOUNT = 'myAccount' + + void cleanup() { + server.shutdown() + } + + void setup() { + igorService = Mock(IgorService) + buildService = new BuildService(igorService: igorService) + server.start() + mockMvc = MockMvcBuilders.standaloneSetup( + new GoogleCloudBuildController(igorService, buildService)).build() + } + + void 'should get a list of triggers for a given account'() { + def triggers = [ + new GoogleCloudBuildTrigger("trigger1", "myTrigger1", "My desc 1"), + new GoogleCloudBuildTrigger("trigger2", "myTrigger2", "My desc 2") + ] + given: + 1 * igorService.getGoogleCloudBuildTriggers(ACCOUNT) >> triggers + + when: + MockHttpServletResponse response = mockMvc.perform(get("/gcb/triggers/${ACCOUNT}") + .accept(MediaType.APPLICATION_JSON)).andReturn().response + + then: + response.status == 200 + response.contentAsString == objectMapper.writeValueAsString(triggers) + } + + void 'should get an empty list when no triggers defined for a given account'() { + given: + 1 * igorService.getGoogleCloudBuildTriggers(ACCOUNT) >> [] + + when: + MockHttpServletResponse response = mockMvc.perform(get("/gcb/triggers/${ACCOUNT}") + .accept(MediaType.APPLICATION_JSON)).andReturn().response + + then: + response.status == 200 + response.contentAsString == '[]' + } + + void 'Should return correct message when account not found in downstream service'() { + def downstreamResponse = new Response("blah", 404, "Not found", [], null) + given: + 1 * igorService.getGoogleCloudBuildTriggers(ACCOUNT) >> {String account -> + throw RetrofitError.httpError("blah", downstreamResponse, null, null) + } + + when: + MockHttpServletResponse response = mockMvc.perform(get("/gcb/triggers/${ACCOUNT}") + .accept(MediaType.APPLICATION_JSON)).andReturn().response + + then: + response.status == 404 + } + +}