Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update endpoints to support Jenkins folder plugin #161

Merged
merged 2 commits into from
Dec 23, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions gate-web/gate-web.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ dependencies {
compile('org.springframework.session:spring-session-data-redis:1.0.1.RELEASE')
compile('org.opensaml:opensaml:2.6.4')

testCompile 'com.squareup.okhttp:mockwebserver:2.1.0'

//this brings in the jetty GzipFilter which boot will autoconfigure
runtime 'org.eclipse.jetty:jetty-servlets:9.2.11.v20150529'
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,41 +19,76 @@ package com.netflix.spinnaker.gate.controllers

import com.netflix.spinnaker.gate.services.BuildService
import groovy.transform.CompileStatic
import javax.servlet.http.HttpServletRequest
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestMethod
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.servlet.HandlerMapping

@CompileStatic
@RequestMapping("/builds")
@RestController
class BuildController {
/*
* Job names can have '/' in them if using the Jenkins Folder plugin.
* Because of this, always put the job name at the end of the URL.
*/
@Autowired
BuildService buildService

@RequestMapping(method = RequestMethod.GET)
@RequestMapping(value = "v2/builds", method = RequestMethod.GET)
List<String> getBuildMasters() {
buildService.getBuildMasters()
}

@RequestMapping(value = "/{buildMaster}/jobs", method = RequestMethod.GET)
@RequestMapping(value = "/v2/builds/{buildMaster}/jobs", method = RequestMethod.GET)
List<String> getJobsForBuildMaster(@PathVariable("buildMaster") String buildMaster) {
buildService.getJobsForBuildMaster(buildMaster)
}

@RequestMapping(value = "/{buildMaster}/jobs/{job:.+}", method = RequestMethod.GET)
Map getJobConfig(@PathVariable("buildMaster") String buildMaster, @PathVariable("job") String job) {
@RequestMapping(value = "/v2/builds/{buildMaster}/jobs/**", method = RequestMethod.GET)
Map getJobConfig(@PathVariable("buildMaster") String buildMaster, HttpServletRequest request) {
def job = request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE).toString().split('/').drop(5).join('/')
buildService.getJobConfig(buildMaster, job)
}

@RequestMapping(value = "/{buildMaster}/jobs/{job}/builds", method = RequestMethod.GET)
List getBuilds(@PathVariable("buildMaster") String buildMaster, @PathVariable("job") String job) {
@RequestMapping(value = "/v2/builds/{buildMaster}/builds/**", method = RequestMethod.GET)
List getBuilds(@PathVariable("buildMaster") String buildMaster, HttpServletRequest request) {
def job = request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE)toString().split('/').drop(5).join('/')
buildService.getBuilds(buildMaster, job)
}

@RequestMapping(value = "/{buildMaster}/jobs/{job}/builds/{number}", method = RequestMethod.GET)
Map getBuilds(@PathVariable("buildMaster") String buildMaster, @PathVariable("job") String job, @PathVariable("number") String number) {
@RequestMapping(value = "/v2/builds/{buildMaster}/build/{number}/**", method = RequestMethod.GET)
Map getBuild(@PathVariable("buildMaster") String buildMaster, @PathVariable("number") String number, HttpServletRequest request) {
def job = request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE)toString().split('/').drop(6).join('/')
buildService.getBuild(buildMaster, job, number)
}

// LEGACY ENDPOINTS:

@RequestMapping(value = "/builds", method = RequestMethod.GET)
List<String> getBuildMastersLegacy() {
buildService.getBuildMasters()
}

@RequestMapping(value = "/builds/{buildMaster}/jobs", method = RequestMethod.GET)
List<String> getJobsForBuildMasterLegacy(@PathVariable("buildMaster") String buildMaster) {
buildService.getJobsForBuildMaster(buildMaster)
}

@RequestMapping(value = "/builds/{buildMaster}/jobs/{job:.+}", method = RequestMethod.GET)
Map getJobConfigLegacy(@PathVariable("buildMaster") String buildMaster, @PathVariable("job") String job) {
buildService.getJobConfig(buildMaster, job)
}

@RequestMapping(value = "/builds/{buildMaster}/jobs/{job}/builds", method = RequestMethod.GET)
List getBuildsLegacy(@PathVariable("buildMaster") String buildMaster, @PathVariable("job") String job) {
buildService.getBuilds(buildMaster, job)
}

@RequestMapping(value = "/builds/{buildMaster}/jobs/{job}/builds/{number}", method = RequestMethod.GET)
Map getBuildsLegacy(@PathVariable("buildMaster") String buildMaster, @PathVariable("job") String job, @PathVariable("number") String number) {
buildService.getBuild(buildMaster, job, number)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ 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

@CompileStatic
Expand All @@ -36,6 +37,10 @@ class BuildService {
@Autowired(required = false)
IgorService igorService

private String encode(uri) {
return UriUtils.encodeFragment(uri.toString(), "UTF-8")
}

List<String> getBuildMasters() {
if (!igorService) {
return []
Expand Down Expand Up @@ -69,7 +74,7 @@ class BuildService {
}
HystrixFactory.newMapCommand(GROUP, "jobConfig") {
try {
igorService.getJobConfig(buildMaster, job)
igorService.getJobConfig(buildMaster, encode(job))
} catch (RetrofitError e) {
if (e.response?.status == 404) {
throw new BuildMasterNotFound("Build master '${buildMaster}' not found")
Expand All @@ -86,7 +91,7 @@ class BuildService {
}
HystrixFactory.newListCommand(GROUP, "buildsForJob") {
try {
igorService.getBuilds(buildMaster, job)
igorService.getBuilds(buildMaster, encode(job))
} catch (RetrofitError e) {
if (e.response?.status == 404) {
throw new BuildMasterNotFound("Build master '${buildMaster}' not found")
Expand All @@ -103,7 +108,7 @@ class BuildService {
}
HystrixFactory.newMapCommand(GROUP, "buildDetails") {
try {
igorService.getBuild(buildMaster, job, number)
igorService.getBuild(buildMaster, encode(job), number)
} catch (RetrofitError e) {
if (e.response?.status == 404) {
throw new BuildMasterNotFound("Build master '${buildMaster}' not found")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,28 @@

package com.netflix.spinnaker.gate.services.internal

import retrofit.http.EncodedPath
import retrofit.http.GET
import retrofit.http.Path

interface IgorService {
/*
* Job names can have '/' in them if using the Jenkins Folder plugin.
* Because of this, always put the job name at the end of the URL.
*/

@GET('/masters')
List<String> getBuildMasters()

@GET('/jobs/{buildMaster}')
List<String> getJobsForBuildMaster(@Path("buildMaster") String buildMaster)

@GET('/jobs/{buildMaster}/{job}')
Map getJobConfig(@Path("buildMaster") String buildMaster, @Path("job") String job)

@GET('/jobs/{buildMaster}/{job}/builds')
List<Map> getBuilds(@Path("buildMaster") String buildMaster, @Path("job") String job)
Map getJobConfig(@Path("buildMaster") String buildMaster, @EncodedPath("job") String job)

@GET('/jobs/{buildMaster}/{job}/{number}')
Map getBuild(@Path("buildMaster") String buildMaster, @Path("job") String job, @Path("number") String number)
@GET('/builds/all/{buildMaster}/{job}')
List<Map> getBuilds(@Path("buildMaster") String buildMaster, @EncodedPath("job") String job)

@GET('/builds/status/{number}/{buildMaster}/{job}')
Map getBuild(@Path("buildMaster") String buildMaster, @EncodedPath("job") String job, @Path("number") String number)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
/*
* Copyright 2015 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.gate.controllers

import com.netflix.spinnaker.gate.services.BuildService
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 static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import spock.lang.Shared
import spock.lang.Specification

class BuildControllerSpec extends Specification {

MockMvc mockMvc
BuildService buildService
IgorService igorService

@Shared
MockWebServer server

final MASTER = 'MASTER'
final BUILD_NUMBER = 123
final JOB_NAME = "name/with/slashes and spaces"
final JOB_NAME_LEGACY = "job"
final JOB_NAME_ENCODED = "name/with/slashes%20and%20spaces"

void cleanup() {
server.shutdown()
}

void setup() {
igorService = Mock(IgorService)
buildService = new BuildService(igorService: igorService)
server = new MockWebServer()
mockMvc = MockMvcBuilders.standaloneSetup(new BuildController(buildService: buildService)).build()
}

void 'should get a list of masters'() {
given:
1 * igorService.getBuildMasters() >> [MASTER, "master2"]

when:
MockHttpServletResponse response = mockMvc.perform(get("/v2/builds")
.accept(MediaType.APPLICATION_JSON)).andReturn().response

then:
response.contentAsString == "[\"${MASTER}\",\"master2\"]"
}

void 'should get a list of jobs for a master'() {
given:
1 * igorService.getJobsForBuildMaster(MASTER) >> [JOB_NAME, "another_job"]

when:
MockHttpServletResponse response = mockMvc.perform(get("/v2/builds/${MASTER}/jobs")
.accept(MediaType.APPLICATION_JSON)).andReturn().response

then:
response.contentAsString == "[\"${JOB_NAME}\",\"another_job\"]"
}

void 'should get a list of builds for a job'() {
given:
1 * igorService.getBuilds(MASTER, JOB_NAME_ENCODED) >> [["building":false, "number":111], ["building":false, "number":222]]

when:
MockHttpServletResponse response = mockMvc.perform(get("/v2/builds/${MASTER}/builds/${JOB_NAME}")
.accept(MediaType.APPLICATION_JSON)).andReturn().response

then:
response.contentAsString == "[{\"building\":false,\"number\":111},{\"building\":false,\"number\":222}]"
}

void 'should get a job config'() {
given:
1 * igorService.getJobConfig(MASTER, JOB_NAME_ENCODED) >> ['name': JOB_NAME, 'url': "http://test.com/job/${JOB_NAME}".toString()]

when:
MockHttpServletResponse response = mockMvc.perform(get("/v2/builds/${MASTER}/jobs/${JOB_NAME}")
.accept(MediaType.APPLICATION_JSON)).andReturn().response

then:
response.contentAsString == "{\"name\":\"${JOB_NAME}\",\"url\":\"http://test.com/job/${JOB_NAME}\"}"
}

void 'should get a build'() {
given:
1 * igorService.getBuild(MASTER, JOB_NAME_ENCODED, BUILD_NUMBER.toString()) >> ["building":false, "number":BUILD_NUMBER]

when:
MockHttpServletResponse response = mockMvc.perform(get("/v2/builds/${MASTER}/build/${BUILD_NUMBER}/${JOB_NAME}")
.accept(MediaType.APPLICATION_JSON)).andReturn().response

then:
response.contentAsString == "{\"building\":false,\"number\":${BUILD_NUMBER}}"
}

// LEGACY ENDPOINT TESTS:

void 'should get a list of masters LEGACY'() {
given:
1 * igorService.getBuildMasters() >> [MASTER, "master2"]

when:
MockHttpServletResponse response = mockMvc.perform(get("/builds")
.accept(MediaType.APPLICATION_JSON)).andReturn().response

then:
response.contentAsString == "[\"${MASTER}\",\"master2\"]"
}

void 'should get a list of jobs for a master LEGACY'() {
given:
1 * igorService.getJobsForBuildMaster(MASTER) >> [JOB_NAME_LEGACY, "another_job"]

when:
MockHttpServletResponse response = mockMvc.perform(get("/builds/${MASTER}/jobs")
.accept(MediaType.APPLICATION_JSON)).andReturn().response

then:
response.contentAsString == "[\"${JOB_NAME_LEGACY}\",\"another_job\"]"
}

void 'should get a list of builds for a job LEGACY'() {
given:
1 * igorService.getBuilds(MASTER, JOB_NAME_LEGACY) >> [["building":false, "number":111], ["building":false, "number":222]]

when:
MockHttpServletResponse response = mockMvc.perform(get("/builds/${MASTER}/jobs/${JOB_NAME_LEGACY}/builds")
.accept(MediaType.APPLICATION_JSON)).andReturn().response

then:
response.contentAsString == "[{\"building\":false,\"number\":111},{\"building\":false,\"number\":222}]"
}

void 'should get a job config LEGACY'() {
given:
1 * igorService.getJobConfig(MASTER, JOB_NAME_LEGACY) >> ['name': JOB_NAME_LEGACY, 'url': "http://test.com/job/${JOB_NAME_LEGACY}".toString()]

when:
MockHttpServletResponse response = mockMvc.perform(get("/builds/${MASTER}/jobs/${JOB_NAME_LEGACY}")
.accept(MediaType.APPLICATION_JSON)).andReturn().response

then:
response.contentAsString == "{\"name\":\"${JOB_NAME_LEGACY}\",\"url\":\"http://test.com/job/${JOB_NAME_LEGACY}\"}"
}

void 'should get a build LEGACY'() {
given:
1 * igorService.getBuild(MASTER, JOB_NAME_LEGACY, BUILD_NUMBER.toString()) >> ["building":false, "number":BUILD_NUMBER]

when:
MockHttpServletResponse response = mockMvc.perform(get("/builds/${MASTER}/jobs/${JOB_NAME_LEGACY}/builds/${BUILD_NUMBER}/")
.accept(MediaType.APPLICATION_JSON)).andReturn().response

then:
response.contentAsString == "{\"building\":false,\"number\":${BUILD_NUMBER}}"
}
}