Skip to content

Commit 6d485a2

Browse files
authored
Merge pull request #130 from jeyrschabu/application_api_v2
Add V2 Application API endpoints
2 parents ed46b40 + a532765 commit 6d485a2

File tree

9 files changed

+581
-25
lines changed

9 files changed

+581
-25
lines changed

.editorconfig

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
root = true
2+
3+
[*]
4+
charset = utf-8
5+
end_of_line = lf
6+
insert_final_newline = true
7+
trim_trailing_whitespace = true
8+
indent_style = space
9+
indent_size = 2

front50-web/src/main/groovy/com/netflix/spinnaker/front50/config/Front50WebConfig.groovy

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import org.springframework.web.bind.annotation.ExceptionHandler
3636
import org.springframework.web.bind.annotation.ResponseBody
3737
import org.springframework.web.bind.annotation.ResponseStatus
3838
import org.springframework.web.servlet.config.annotation.InterceptorRegistry
39+
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer
3940
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter
4041

4142
@Configuration
@@ -53,6 +54,11 @@ public class Front50WebConfig extends WebMvcConfigurerAdapter {
5354
)
5455
}
5556

57+
@Override
58+
void configurePathMatch(PathMatchConfigurer configurer) {
59+
configurer.setUseRegisteredSuffixPatternMatch(false)
60+
}
61+
5662
@Bean
5763
FilterRegistrationBean authenticatedRequestFilter() {
5864
def frb = new FilterRegistrationBean(new AuthenticatedRequestFilter(true))

front50-web/src/main/groovy/com/netflix/spinnaker/front50/controllers/ApplicationsController.groovy

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,22 +36,14 @@ import org.springframework.http.HttpStatus
3636
import org.springframework.security.access.AccessDeniedException
3737
import org.springframework.security.access.prepost.PostFilter
3838
import org.springframework.security.access.prepost.PreAuthorize
39-
import org.springframework.security.access.prepost.PreFilter
4039
import org.springframework.validation.Errors
4140
import org.springframework.validation.ObjectError
42-
import org.springframework.web.bind.annotation.ExceptionHandler
43-
import org.springframework.web.bind.annotation.PathVariable
44-
import org.springframework.web.bind.annotation.RequestBody
45-
import org.springframework.web.bind.annotation.RequestMapping
46-
import org.springframework.web.bind.annotation.RequestMethod
47-
import org.springframework.web.bind.annotation.RequestParam
48-
import org.springframework.web.bind.annotation.ResponseStatus
49-
import org.springframework.web.bind.annotation.RestController
41+
import org.springframework.web.bind.annotation.*
5042

5143
import javax.servlet.http.HttpServletResponse
5244

5345
@Slf4j
54-
@RestController
46+
@RestController("legacyApplicationsController")
5547
@RequestMapping(["/default/applications", "/global/applications"])
5648
@Api(value = "application", description = "Application API")
5749
public class ApplicationsController {

front50-web/src/main/groovy/com/netflix/spinnaker/front50/controllers/exception/ApplicationException.groovy

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,10 @@
1717

1818
package com.netflix.spinnaker.front50.controllers.exception
1919

20+
import groovy.transform.InheritConstructors
2021
import org.springframework.http.HttpStatus
2122
import org.springframework.web.bind.annotation.ResponseStatus
2223

24+
@InheritConstructors
2325
@ResponseStatus(value = HttpStatus.SERVICE_UNAVAILABLE, reason = "Exception, baby")
24-
class ApplicationException extends RuntimeException {
25-
public ApplicationException(Throwable cause) {
26-
super(cause)
27-
}
28-
}
26+
class ApplicationException extends RuntimeException {}

front50-web/src/main/groovy/com/netflix/spinnaker/front50/controllers/exception/ApplicationNotFoundException.groovy

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,10 @@
1717

1818
package com.netflix.spinnaker.front50.controllers.exception
1919

20+
import groovy.transform.InheritConstructors
2021
import org.springframework.http.HttpStatus
2122
import org.springframework.web.bind.annotation.ResponseStatus
2223

24+
@InheritConstructors
2325
@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "Application not found")
24-
class ApplicationNotFoundException extends RuntimeException {
25-
public ApplicationNotFoundException(Throwable cause) {
26-
super(cause)
27-
}
28-
}
26+
class ApplicationNotFoundException extends RuntimeException {}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright 2016 Netflix, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
18+
package com.netflix.spinnaker.front50.controllers.exception
19+
20+
import com.netflix.hystrix.exception.HystrixBadRequestException
21+
import groovy.transform.InheritConstructors
22+
import org.springframework.http.HttpStatus
23+
import org.springframework.web.bind.annotation.ResponseStatus;
24+
25+
@InheritConstructors
26+
@ResponseStatus(value = HttpStatus.BAD_REQUEST)
27+
public class InvalidApplicationRequestException extends HystrixBadRequestException {}

front50-web/src/main/groovy/com/netflix/spinnaker/front50/controllers/exception/NoApplicationsFoundException.groovy

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,10 @@
1717

1818
package com.netflix.spinnaker.front50.controllers.exception
1919

20+
import groovy.transform.InheritConstructors
2021
import org.springframework.http.HttpStatus
2122
import org.springframework.web.bind.annotation.ResponseStatus
2223

24+
@InheritConstructors
2325
@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "No applications found")
24-
class NoApplicationsFoundException extends RuntimeException {
25-
public NoApplicationsFoundException(Throwable cause) {
26-
super(cause)
27-
}
28-
}
26+
class NoApplicationsFoundException extends RuntimeException {}
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
package com.netflix.spinnaker.front50.controllers.v2
2+
3+
import com.netflix.spectator.api.Registry
4+
import com.netflix.spinnaker.front50.events.ApplicationEventListener
5+
import com.netflix.spinnaker.front50.controllers.exception.InvalidApplicationRequestException
6+
import com.netflix.spinnaker.front50.model.application.Application
7+
import com.netflix.spinnaker.front50.model.application.ApplicationDAO
8+
import com.netflix.spinnaker.front50.model.notification.NotificationDAO
9+
import com.netflix.spinnaker.front50.model.pipeline.PipelineDAO
10+
import com.netflix.spinnaker.front50.model.pipeline.PipelineStrategyDAO
11+
import com.netflix.spinnaker.front50.model.project.ProjectDAO
12+
import com.netflix.spinnaker.front50.validator.ApplicationValidator
13+
import groovy.util.logging.Slf4j
14+
import io.swagger.annotations.Api
15+
import io.swagger.annotations.ApiOperation
16+
import org.springframework.beans.factory.annotation.Autowired
17+
import org.springframework.context.MessageSource
18+
import org.springframework.context.i18n.LocaleContextHolder
19+
import org.springframework.http.HttpStatus
20+
import org.springframework.security.access.AccessDeniedException
21+
import org.springframework.security.access.prepost.PostFilter
22+
import org.springframework.security.access.prepost.PreAuthorize
23+
import org.springframework.validation.Errors
24+
import org.springframework.validation.ObjectError
25+
import org.springframework.web.bind.annotation.*
26+
27+
import javax.servlet.http.HttpServletResponse
28+
29+
@Slf4j
30+
@RestController
31+
@RequestMapping("/v2/applications")
32+
@Api(value = "application", description = "Application API")
33+
public class ApplicationsController {
34+
@Autowired
35+
MessageSource messageSource
36+
37+
@Autowired
38+
ApplicationDAO applicationDAO
39+
40+
@Autowired
41+
ProjectDAO projectDAO
42+
43+
@Autowired
44+
NotificationDAO notificationDAO
45+
46+
@Autowired
47+
PipelineDAO pipelineDAO
48+
49+
@Autowired
50+
PipelineStrategyDAO pipelineStrategyDAO
51+
52+
@Autowired
53+
List<ApplicationValidator> applicationValidators
54+
55+
@Autowired(required = false)
56+
List<ApplicationEventListener> applicationEventListeners = []
57+
58+
@Autowired
59+
Registry registry
60+
61+
@PreAuthorize("@fiatPermissionEvaluator.storeWholePermission()")
62+
@PostFilter("hasPermission(filterObject.name, 'APPLICATION', 'READ')")
63+
@ApiOperation(value = "", notes = """Fetch all applications.
64+
65+
Supports filtering by one or more attributes:
66+
- ?email=my@email.com
67+
- ?email=my@email.com&name=flex""")
68+
@RequestMapping(method = RequestMethod.GET)
69+
Set<Application> applications(@RequestParam Map<String, String> params) {
70+
if (params.isEmpty()) {
71+
return applicationDAO.all()
72+
}
73+
74+
return applicationDAO.search(params)
75+
}
76+
77+
@PreAuthorize("hasPermission(#app.name, 'APPLICATION', 'CREATE')")
78+
@ApiOperation(value = "", notes = "Create an application")
79+
@RequestMapping(method = RequestMethod.POST)
80+
Application create(@RequestBody final Application app) {
81+
return getApplication().initialize(app).withName(app.getName()).save()
82+
}
83+
84+
@PreAuthorize("hasPermission(#applicationName, 'APPLICATION', 'WRITE')")
85+
@ApiOperation(value = "", notes = "Delete an application")
86+
@RequestMapping(method = RequestMethod.DELETE, value = "/{applicationName}")
87+
void delete(@PathVariable String applicationName, HttpServletResponse response) {
88+
getApplication().initialize(new Application().withName(applicationName)).delete()
89+
response.setStatus(HttpStatus.NO_CONTENT.value())
90+
}
91+
92+
@PreAuthorize("hasPermission(#app.name, 'APPLICATION', 'WRITE')")
93+
@ApiOperation(value = "", notes = "Update an existing application")
94+
@RequestMapping(method = RequestMethod.PATCH, value = "/{applicationName}")
95+
Application update(@PathVariable String applicationName, @RequestBody final Application app) {
96+
if (!applicationName.trim().equalsIgnoreCase(app.getName())) {
97+
throw new InvalidApplicationRequestException("Application name '${app.getName()}' does not match path parameter '${applicationName}'")
98+
}
99+
100+
def application = getApplication()
101+
Application existingApplication = application.findByName(app.getName())
102+
application.initialize(existingApplication).withName(app.getName()).update(app)
103+
return app
104+
}
105+
106+
@PreAuthorize("hasPermission(#applicationName, 'APPLICATION', 'READ')")
107+
@ApiOperation(value = "", notes = "Fetch a single application by name")
108+
@RequestMapping(method = RequestMethod.GET, value = "/{applicationName}")
109+
Application get(@PathVariable final String applicationName) {
110+
return applicationDAO.findByName(applicationName.toUpperCase())
111+
}
112+
113+
@PreAuthorize("hasPermission(#applicationName, 'APPLICATION', 'READ')")
114+
@RequestMapping(value = '{applicationName}/history', method = RequestMethod.GET)
115+
Collection<Application> getHistory(@PathVariable String applicationName,
116+
@RequestParam(value = "limit", defaultValue = "20") int limit) {
117+
return applicationDAO.getApplicationHistory(applicationName, limit)
118+
}
119+
120+
@PreAuthorize("@fiatPermissionEvaluator.isAdmin()")
121+
@RequestMapping(method = RequestMethod.POST, value = "/batch/applications")
122+
void batchUpdate(@RequestBody final Collection<Application> applications) {
123+
applicationDAO.bulkImport(applications)
124+
}
125+
126+
@ExceptionHandler(Application.ValidationException)
127+
@ResponseStatus(HttpStatus.BAD_REQUEST)
128+
Map handleValidationException(Application.ValidationException ex) {
129+
def locale = LocaleContextHolder.locale
130+
def errorStrings = []
131+
ex.errors.each { Errors errors ->
132+
errors.allErrors.each { ObjectError objectError ->
133+
def message = messageSource.getMessage(objectError.code, objectError.arguments, objectError.defaultMessage, locale)
134+
errorStrings << message
135+
}
136+
}
137+
return [error: "Validation Failed.", errors: errorStrings, status: HttpStatus.BAD_REQUEST]
138+
}
139+
140+
@ExceptionHandler(AccessDeniedException)
141+
@ResponseStatus(HttpStatus.FORBIDDEN)
142+
Map handleAccessDeniedException(AccessDeniedException ade) {
143+
return [error: "Access is denied", status: HttpStatus.FORBIDDEN.value()]
144+
}
145+
146+
private Application getApplication() {
147+
return new Application(
148+
dao: applicationDAO,
149+
projectDao: projectDAO,
150+
notificationDao: notificationDAO,
151+
pipelineDao: pipelineDAO,
152+
pipelineStrategyDao: pipelineStrategyDAO,
153+
validators: applicationValidators,
154+
applicationEventListeners: applicationEventListeners
155+
)
156+
}
157+
}

0 commit comments

Comments
 (0)