diff --git a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/services/internal/ClouddriverService.groovy b/gate-core/src/main/groovy/com/netflix/spinnaker/gate/services/internal/ClouddriverService.groovy index 36d8fa6e06..00b5e334e2 100644 --- a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/services/internal/ClouddriverService.groovy +++ b/gate-core/src/main/groovy/com/netflix/spinnaker/gate/services/internal/ClouddriverService.groovy @@ -18,7 +18,12 @@ package com.netflix.spinnaker.gate.services.internal import com.fasterxml.jackson.annotation.JsonIgnoreProperties import retrofit.client.Response -import retrofit.http.* +import retrofit.http.GET +import retrofit.http.Headers +import retrofit.http.Path +import retrofit.http.Query +import retrofit.http.QueryMap +import retrofit.http.Streaming interface ClouddriverService { @@ -109,6 +114,7 @@ interface ClouddriverService { @Headers("Accept: application/json") @GET("/serverGroups") List getServerGroups(@Query("applications") List applications, + @Query("ids") List ids, @Query("cloudProvider") String cloudProvider) @Headers("Accept: application/json") @@ -222,6 +228,9 @@ interface ClouddriverService { Map getSecurityGroup(@Path("account") String account, @Path("type") String type, @Path("name") String name, @Path("region") String region, @Query("vpcId") String vpcId) + @GET("/applications/{application}/serverGroupManagers") + List getServerGroupManagersForApplication(@Path("application") String application) + @GET('/instanceTypes') List getInstanceTypes() @@ -277,6 +286,14 @@ interface ClouddriverService { @Path(value = 'bucketId', encode = false) String bucketId, @Path(value = 'objectId', encode = false) String objectId) + @GET('/storage') + List getStorageAccounts() + + @GET('/manifests/{account}/{location}/{name}') + Map getManifest(@Path(value = 'account') String account, + @Path(value = 'location') String location, + @Path(value = 'name') String name) + @GET('/roles/{cloudProvider}') List getRoles(@Path("cloudProvider") String cloudProvider) @@ -285,7 +302,4 @@ interface ClouddriverService { @GET('/ecs/cloudmetrics/alarms') List getEcsAllMetricAlarms() - - @GET('/storage') - List getStorageAccounts() } diff --git a/gate-web/config/gate.yml b/gate-web/config/gate.yml index 46a40ee126..1169d71e56 100644 --- a/gate-web/config/gate.yml +++ b/gate-web/config/gate.yml @@ -77,6 +77,7 @@ swagger: - .*networks.* - .*bakery.* - .*executions.* + - .*webhooks.* hystrix: command: diff --git a/gate-web/lib/systemd/system/gate.service b/gate-web/lib/systemd/system/gate.service new file mode 100644 index 0000000000..68db11be74 --- /dev/null +++ b/gate-web/lib/systemd/system/gate.service @@ -0,0 +1,14 @@ +[Unit] +Description=Spinnaker Gate +PartOf=spinnaker.service + +[Service] +ExecStart=/opt/gate/bin/gate +WorkingDirectory=/opt/gate/bin +SuccessExitStatus=143 +User=spinnaker +Group=spinnaker +Type=simple + +[Install] +WantedBy=multi-user.target diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/AuthController.groovy b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/AuthController.groovy index 90f5c17590..09291a6147 100644 --- a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/AuthController.groovy +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/AuthController.groovy @@ -46,7 +46,7 @@ class AuthController { "Roads? Where we're going we don't need roads!", "Say hello to my little friend!", "I wish we could chat longer, but I'm having an old friend for dinner. Bye!", - "Hodor.", + "Hodor. :(", ] private final Random r = new Random() private final URL deckBaseUrl @@ -126,7 +126,10 @@ class AuthController { return matcher.matches() } + def toURLPort = (toURL.port == -1 && toURL.protocol == 'https') ? 443 : toURL.port + def deckBaseUrlPort = (deckBaseUrl.port == -1 && deckBaseUrl.protocol == 'https') ? 443 : deckBaseUrl.port + return toURL.host == deckBaseUrl.host && - toURL.port == deckBaseUrl.port + toURLPort == deckBaseUrlPort } } diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/ManifestController.groovy b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/ManifestController.groovy new file mode 100644 index 0000000000..2341aa4be5 --- /dev/null +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/ManifestController.groovy @@ -0,0 +1,39 @@ +/* + * Copyright 2017 Google, 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.ManifestService +import groovy.transform.CompileStatic +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 + +@CompileStatic +@RequestMapping("/manifests") +@RestController +class ManifestController { + @Autowired + ManifestService manifestService + + @RequestMapping(value = "/{account:.+}/{location:.+}/{name:.+}", method = RequestMethod.GET) + Map getManifest(@PathVariable String account, @PathVariable String location, @PathVariable String name) { + return manifestService.getManifest(account, location, name); + } +} diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/PipelineTemplatesController.java b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/PipelineTemplatesController.java index 9b84713e51..7a170c744f 100644 --- a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/PipelineTemplatesController.java +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/PipelineTemplatesController.java @@ -58,10 +58,9 @@ public PipelineTemplatesController(PipelineTemplateService pipelineTemplateServi this.objectMapper = objectMapper; } - @ApiOperation(value = "Returns a list of pipeline templates by scope", - notes = "If no scope is provided, 'global' will be defaulted") + @ApiOperation(value = "Returns a list of pipeline templates by scope") @RequestMapping(method = RequestMethod.GET) - public Collection list(@RequestParam(defaultValue = "global") List scopes) { + public Collection list(@RequestParam(required = false) List scopes) { return pipelineTemplateService.findByScope(scopes); } @@ -91,9 +90,8 @@ public Map create(@RequestBody Map pipelineTemplate) { } @RequestMapping(value = "/resolve", method = RequestMethod.GET) - public Map resolveTemplates(@RequestParam("source") String source) { - Map template = pipelineTemplateService.resolve(source); - return template; + public Map resolveTemplates(@RequestParam String source, @RequestParam(required = false) String executionId, @RequestParam(required = false) String pipelineConfigId) { + return pipelineTemplateService.resolve(source, executionId, pipelineConfigId); } @RequestMapping(value = "/{id}", method = RequestMethod.GET) diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/ServerGroupController.groovy b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/ServerGroupController.groovy index e30060971f..a47aeff665 100644 --- a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/ServerGroupController.groovy +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/ServerGroupController.groovy @@ -37,21 +37,29 @@ class ServerGroupController { @ApiOperation(value = "Retrieve a list of server groups for a given application") @RequestMapping(value = "/applications/{applicationName}/serverGroups", method = RequestMethod.GET) - List getServerGroups(@PathVariable String applicationName, - @RequestParam(required = false, value = 'expand', defaultValue = 'false') String expand, - @RequestParam(required = false, value = 'cloudProvider') String cloudProvider, - @RequestParam(required = false, value = 'clusters') String clusters, - @RequestHeader(value = "X-RateLimit-App", required = false) String sourceApp) { + List getServerGroupsForApplication(@PathVariable String applicationName, + @RequestParam(required = false, value = 'expand', defaultValue = 'false') String expand, + @RequestParam(required = false, value = 'cloudProvider') String cloudProvider, + @RequestParam(required = false, value = 'clusters') String clusters, + @RequestHeader(value = "X-RateLimit-App", required = false) String sourceApp) { serverGroupService.getForApplication(applicationName, expand, cloudProvider, clusters, sourceApp) } - @ApiOperation(value = "Retrieve a list of server groups for a list of applications") + @ApiOperation(value = "Retrieve a list of server groups for a list of applications or a list of servergroups by 'account:region:name'") @RequestMapping(value = "/serverGroups", method = RequestMethod.GET) - List getServerGroupsByApplications(@RequestParam(value = 'applications') List applications, - @RequestParam(required = false, value = 'cloudProvider') String cloudProvider, - @RequestHeader(value = "X-RateLimit-App", required = false) String sourceApp) { + List getServerGroups(@RequestParam(required = false, value = 'applications') List applications, + @RequestParam(required = false, value = 'ids') List ids, + @RequestParam(required = false, value = 'cloudProvider') String cloudProvider, + @RequestHeader(value = "X-RateLimit-App", required = false) String sourceApp) { + if ((applications && ids) || (!applications && !ids)) { + throw new IllegalArgumentException("Provide either 'applications' or 'ids' parameter, but not both"); + } - serverGroupService.getForApplications(applications, cloudProvider, sourceApp) + if (applications) { + return serverGroupService.getForApplications(applications, cloudProvider, sourceApp) + } else { + return serverGroupService.getForIds(ids, cloudProvider, sourceApp) + } } @ApiOperation(value = "Retrieve a server group's details") diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/ServerGroupManagerController.java b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/ServerGroupManagerController.java new file mode 100644 index 0000000000..13ac91a63a --- /dev/null +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/ServerGroupManagerController.java @@ -0,0 +1,45 @@ +/* + * Copyright 2017 Google, 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.ServerGroupManagerService; +import io.swagger.annotations.ApiOperation; +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 java.util.List; +import java.util.Map; + +@RestController +@RequestMapping(value = "/applications/{application}/serverGroupManagers") +public class ServerGroupManagerController { + private final ServerGroupManagerService serverGroupManagerService; + + @Autowired + ServerGroupManagerController(ServerGroupManagerService serverGroupManagerService) { + this.serverGroupManagerService = serverGroupManagerService; + } + + @ApiOperation("Retrieve a list of server group managers for an application") + @RequestMapping(method = RequestMethod.GET) + public List getServerGroupManagersForApplication(@PathVariable String application) { + return this.serverGroupManagerService.getServerGroupManagersForApplication(application); + } +} diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/V2CanaryConfigController.groovy b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/V2CanaryConfigController.groovy index 9c03ff72c2..bf0d82b0d1 100644 --- a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/V2CanaryConfigController.groovy +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/V2CanaryConfigController.groovy @@ -24,6 +24,7 @@ import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestMethod +import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController @RestController @@ -36,31 +37,37 @@ class V2CanaryConfigController { @ApiOperation(value = "Retrieve a list of canary configurations") @RequestMapping(method = RequestMethod.GET) - List getCanaryConfigs() { - canaryConfigService.getCanaryConfigs() + List getCanaryConfigs(@RequestParam(value = "application", required = false) String application, + @RequestParam(value = "configurationAccountName", required = false) String configurationAccountName) { + canaryConfigService.getCanaryConfigs(application, configurationAccountName) } @ApiOperation(value = "Retrieve a canary configuration by id") @RequestMapping(value = "/{id}", method = RequestMethod.GET) - Map getCanaryConfig(@PathVariable String id) { - canaryConfigService.getCanaryConfig(id) + Map getCanaryConfig(@PathVariable String id, + @RequestParam(value = "configurationAccountName", required = false) String configurationAccountName) { + canaryConfigService.getCanaryConfig(id, configurationAccountName) } @ApiOperation(value = "Create a canary configuration") @RequestMapping(method = RequestMethod.POST) - Map createCanaryConfig(@RequestBody Map config) { - [id: canaryConfigService.createCanaryConfig(config)] + Map createCanaryConfig(@RequestBody Map config, + @RequestParam(value = "configurationAccountName", required = false) String configurationAccountName) { + canaryConfigService.createCanaryConfig(config, configurationAccountName) } @ApiOperation(value = "Update a canary configuration") @RequestMapping(value = "/{id}", method = RequestMethod.PUT) - Map updateCanaryConfig(@PathVariable String id, @RequestBody Map config) { - [id: canaryConfigService.updateCanaryConfig(id, config)] + Map updateCanaryConfig(@PathVariable String id, + @RequestParam(value = "configurationAccountName", required = false) String configurationAccountName, + @RequestBody Map config) { + canaryConfigService.updateCanaryConfig(id, config, configurationAccountName) } @ApiOperation(value = "Delete a canary configuration") @RequestMapping(value = "/{id}", method = RequestMethod.DELETE) - void deleteCanaryConfig(@PathVariable String id) { - canaryConfigService.deleteCanaryConfig(id) + void deleteCanaryConfig(@PathVariable String id, + @RequestParam(value = "configurationAccountName", required = false) String configurationAccountName) { + canaryConfigService.deleteCanaryConfig(id, configurationAccountName) } } diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/V2CanaryController.groovy b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/V2CanaryController.groovy index 54a1e07c4b..b0eb31f7ec 100644 --- a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/V2CanaryController.groovy +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/V2CanaryController.groovy @@ -16,42 +16,51 @@ package com.netflix.spinnaker.gate.controllers -import com.netflix.spinnaker.gate.services.internal.KayentaService +import com.netflix.spinnaker.gate.services.V2CanaryService import io.swagger.annotations.ApiOperation import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression 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.RequestParam import org.springframework.web.bind.annotation.RestController @RestController +@RequestMapping('/v2/canaries') @ConditionalOnExpression('${services.kayenta.enabled:false}') class V2CanaryController { + @Autowired - KayentaService kayentaService + V2CanaryService v2CanaryService - @ApiOperation(value = "Retrieve a list of configured Kayenta accounts") - @RequestMapping(value = '/v2/canaries/credentials', method = RequestMethod.GET) + @ApiOperation(value = 'Retrieve a list of configured Kayenta accounts') + @RequestMapping(value = '/credentials', method = RequestMethod.GET) List listCredentials() { - kayentaService.getCredentials() + v2CanaryService.getCredentials() } - @ApiOperation(value = "Retrieve a list of all configured canary judges") - @RequestMapping(value = "/v2/canaries/judges", method = RequestMethod.GET) + @ApiOperation(value = 'Retrieve a list of all configured canary judges') + @RequestMapping(value = '/judges', method = RequestMethod.GET) List listJudges() { - kayentaService.listJudges() + v2CanaryService.listJudges() } - @ApiOperation(value = "Retrieve a list of canary judge results") - @RequestMapping(value = "/v2/canaries/canaryJudgeResult", method = RequestMethod.GET) - List listResults() { - kayentaService.listResults() + @ApiOperation(value = 'Retrieve a canary result') + @RequestMapping(value = '/canary/{canaryConfigId}/{canaryExecutionId}', method = RequestMethod.GET) + Map getCanaryResult(@PathVariable String canaryConfigId, + @PathVariable String canaryExecutionId, + @RequestParam(value='storageAccountName', required = false) String storageAccountName) { + v2CanaryService.getCanaryResults(canaryConfigId, canaryExecutionId, storageAccountName) } - @ApiOperation(value = "Retrieve a canary judge result by id") - @RequestMapping(value = "/v2/canaries/canaryJudgeResult/{id}", method = RequestMethod.GET) - Map getResult(@PathVariable String id) { - kayentaService.getResult(id) + + // TODO(dpeach): remove this endpoint when a Kayenta endpoint for + // retrieving a single metric set pair exists. + @ApiOperation(value = 'Retrieve a metric set pair list') + @RequestMapping(value = '/metricSetPairList/{metricSetPairListId}', method = RequestMethod.GET) + List getMetricSetPairList(@PathVariable String metricSetPairListId, + @RequestParam(value='storageAccountName', required = false) String storageAccountName) { + v2CanaryService.getMetricSetPairList(metricSetPairListId, storageAccountName) } } diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/WebhookController.groovy b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/WebhookController.groovy index a7cf29592f..d815207678 100644 --- a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/WebhookController.groovy +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/WebhookController.groovy @@ -33,6 +33,7 @@ class WebhookController { @Autowired WebhookService webhookService + @ApiOperation(value = "Endpoint for posting webhooks to Spinnaker's webhook service") @RequestMapping(value = "/{type}/{source}", method = RequestMethod.POST) void webhooks(@PathVariable("type") String type, @PathVariable("source") String source, diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/security/ldap/LdapSsoConfig.groovy b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/security/ldap/LdapSsoConfig.groovy index 206e010c85..b98974f66a 100644 --- a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/security/ldap/LdapSsoConfig.groovy +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/security/ldap/LdapSsoConfig.groovy @@ -53,6 +53,9 @@ class LdapSsoConfig extends WebSecurityConfigurerAdapter { @Autowired LdapUserContextMapper ldapUserContextMapper + @Autowired(required = false) + List configurers + @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { def ldapConfigurer = @@ -83,6 +86,9 @@ class LdapSsoConfig extends WebSecurityConfigurerAdapter { protected void configure(HttpSecurity http) throws Exception { http.formLogin() authConfig.configure(http) + configurers?.each { + it.configure(http) + } } @Component diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/security/ldap/LdapSsoConfigurer.groovy b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/security/ldap/LdapSsoConfigurer.groovy new file mode 100644 index 0000000000..f581ef1b15 --- /dev/null +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/security/ldap/LdapSsoConfigurer.groovy @@ -0,0 +1,7 @@ +package com.netflix.spinnaker.gate.security.ldap + +import org.springframework.security.config.annotation.web.builders.HttpSecurity + +interface LdapSsoConfigurer { + void configure(HttpSecurity http) throws Exception +} \ No newline at end of file diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/security/x509/X509Config.groovy b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/security/x509/X509Config.groovy index 8d3dead6d9..4c659cf83a 100644 --- a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/security/x509/X509Config.groovy +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/security/x509/X509Config.groovy @@ -18,6 +18,8 @@ package com.netflix.spinnaker.gate.security.x509 import com.netflix.spinnaker.gate.security.AuthConfig import com.netflix.spinnaker.gate.security.SpinnakerAuthConfig +import com.netflix.spinnaker.gate.security.ldap.LdapSsoConfig +import com.netflix.spinnaker.gate.security.ldap.LdapSsoConfigurer import com.netflix.spinnaker.gate.security.oauth2.OAuth2SsoConfig import com.netflix.spinnaker.gate.security.oauth2.OAuthSsoConfigurer import com.netflix.spinnaker.gate.security.saml.SamlSsoConfig @@ -107,7 +109,7 @@ class X509Config { /** * See {@link OAuth2SsoConfig} for why these classes and conditionals exist! */ - @ConditionalOnMissingBean([OAuth2SsoConfig, SamlSsoConfig]) + @ConditionalOnMissingBean([OAuth2SsoConfig, SamlSsoConfig, LdapSsoConfig]) @Bean X509StandaloneAuthConfig standaloneConfig() { new X509StandaloneAuthConfig() @@ -159,4 +161,18 @@ class X509Config { http.securityContext().securityContextRepository(new X509SecurityContextRepository()) } } + + @ConditionalOnBean(LdapSsoConfig) + @Bean + X509LDAPConfig withLDAPConfig() { + new X509LDAPConfig() + } + + class X509LDAPConfig implements LdapSsoConfigurer { + @Override + void configure(HttpSecurity http) throws Exception { + X509Config.this.configure(http) + http.securityContext().securityContextRepository(new X509SecurityContextRepository()) + } + } } diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/CanaryConfigService.groovy b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/CanaryConfigService.groovy index f967299ef1..acd4f0484e 100644 --- a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/CanaryConfigService.groovy +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/CanaryConfigService.groovy @@ -17,9 +17,9 @@ package com.netflix.spinnaker.gate.services interface CanaryConfigService { - List getCanaryConfigs() - Map getCanaryConfig(String id) - String createCanaryConfig(Map config) - String updateCanaryConfig(String id, Map config) - void deleteCanaryConfig(String id) + List getCanaryConfigs(String application, String configurationAccountName) + Map getCanaryConfig(String id, String configurationAccountName) + Map createCanaryConfig(Map config, String configurationAccountName) + Map updateCanaryConfig(String id, Map config, String configurationAccountName) + void deleteCanaryConfig(String id, String configurationAccountName) } diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/KayentaCanaryConfigService.groovy b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/KayentaCanaryConfigService.groovy index 62457766e1..0c43f013a6 100644 --- a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/KayentaCanaryConfigService.groovy +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/KayentaCanaryConfigService.groovy @@ -23,49 +23,70 @@ import groovy.util.logging.Slf4j import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression import org.springframework.stereotype.Component +import retrofit.RetrofitError import retrofit.client.Response import retrofit.mime.TypedByteArray +import static com.netflix.spinnaker.gate.retrofit.UpstreamBadRequest.classifyError @CompileStatic @Component @Slf4j @ConditionalOnExpression('${services.kayenta.enabled:false} and ${services.kayenta.canaryConfigStore:false}') class KayentaCanaryConfigService implements CanaryConfigService { - private static final String GROUP = "canaryConfigs" + + private static final String HYSTRIX_GROUP = "v2-canaryConfigs" @Autowired KayentaService kayentaService - List getCanaryConfigs() { - HystrixFactory.newListCommand(GROUP, "getCanaryConfigs") { - kayentaService.getCanaryConfigs() - } execute() + List getCanaryConfigs(String application, String configurationAccountName) { + return HystrixFactory.newListCommand(HYSTRIX_GROUP, "getCanaryConfigs", { + try { + return kayentaService.getCanaryConfigs(application, configurationAccountName) + } catch (RetrofitError e) { + throw classifyError(e) + } + }).execute() as List } - Map getCanaryConfig(String id) { - HystrixFactory.newMapCommand(GROUP, "getCanaryConfig") { - kayentaService.getCanaryConfig(id) - } execute() + Map getCanaryConfig(String id, String configurationAccountName) { + return HystrixFactory.newMapCommand(HYSTRIX_GROUP, "getCanaryConfig", { + try { + return kayentaService.getCanaryConfig(id, configurationAccountName) + } catch (RetrofitError e) { + throw classifyError(e) + } + }).execute() as Map } - String createCanaryConfig(Map config) { - HystrixFactory.newMapCommand(GROUP, "createCanaryConfig") { - Response response = kayentaService.createCanaryConfig(config) - return new String(((TypedByteArray)response.getBody()).getBytes()) - } execute() + Map createCanaryConfig(Map config, String configurationAccountName) { + return HystrixFactory.newMapCommand(HYSTRIX_GROUP, "createCanaryConfig", { + try { + return kayentaService.createCanaryConfig(config, configurationAccountName) + } catch (RetrofitError e) { + throw classifyError(e) + } + }).execute() as Map } - String updateCanaryConfig(String id, Map config) { - HystrixFactory.newMapCommand(GROUP, "updateCanaryConfig") { - Response response = kayentaService.updateCanaryConfig(id, config) - return new String(((TypedByteArray)response.getBody()).getBytes()) - } execute() + Map updateCanaryConfig(String id, Map config, String configurationAccountName) { + return HystrixFactory.newMapCommand(HYSTRIX_GROUP, "updateCanaryConfig", { + try { + return kayentaService.updateCanaryConfig(id, config, configurationAccountName) + } catch (RetrofitError e) { + throw classifyError(e) + } + }).execute() as Map } - void deleteCanaryConfig(String id) { - HystrixFactory.newMapCommand(GROUP, "deleteCanaryConfig") { - kayentaService.deleteCanaryConfig(id) - } execute() + void deleteCanaryConfig(String id, String configurationAccountName) { + HystrixFactory.newVoidCommand(HYSTRIX_GROUP, "deleteCanaryConfig", { + try { + kayentaService.deleteCanaryConfig(id, configurationAccountName) + } catch (RetrofitError e) { + throw classifyError(e) + } + }).execute() } } diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/ManifestService.java b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/ManifestService.java new file mode 100644 index 0000000000..7c10b07b61 --- /dev/null +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/ManifestService.java @@ -0,0 +1,66 @@ +/* + * Copyright 2017 Google, 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.services; + +import com.netflix.hystrix.exception.HystrixBadRequestException; +import com.netflix.spinnaker.gate.services.commands.HystrixFactory; +import com.netflix.spinnaker.gate.services.internal.ClouddriverService; +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 retrofit.RetrofitError; + +import java.util.Map; + +@Component +public class ManifestService { + private static final String GROUP = "manifests"; + + final private ProviderLookupService providerLookupService; + final private ClouddriverService clouddriverService; + + @Autowired + public ManifestService(ClouddriverService clouddriverService, ProviderLookupService providerLookupService) { + this.clouddriverService = clouddriverService; + this.providerLookupService = providerLookupService; + } + + public Map getManifest(String account, String location, String name) { + return (Map) HystrixFactory.newMapCommand( + GROUP, + "getManifest-" + providerLookupService.providerForAccount(account), + () -> { + try { + return clouddriverService.getManifest(account, location, name); + } catch (RetrofitError re) { + if (re.getKind() == RetrofitError.Kind.HTTP && re.getResponse() != null && re.getResponse().getStatus() == 404) { + throw new ManifestNotFound("Unable to find " + name + " in " + account + "/" + location); + } + throw re; + } + } + ).execute(); + } + + @ResponseStatus(HttpStatus.NOT_FOUND) + static class ManifestNotFound extends HystrixBadRequestException { + ManifestNotFound(String message) { + super(message); + } + } +} diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/PipelineService.groovy b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/PipelineService.groovy index b2dd6e4952..9af1956304 100644 --- a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/PipelineService.groovy +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/PipelineService.groovy @@ -75,7 +75,18 @@ class PipelineService { if (pipelineConfig.notifications) { pipelineConfig.notifications = (List) pipelineConfig.notifications + (List) trigger.notifications } else { - pipelineConfig.notifications = trigger.notifications; + pipelineConfig.notifications = trigger.notifications + } + } + if (pipelineConfig.parameterConfig) { + Map triggerParams = (Map) trigger.parameters ?: [:] + pipelineConfig.parameterConfig.each { Map paramConfig -> + String paramName = paramConfig.name + if (paramConfig.required && paramConfig.default == null) { + if (triggerParams[paramName] == null) { + throw new IllegalArgumentException("Required parameter ${paramName} is missing") + } + } } } orcaService.startPipeline(pipelineConfig, trigger.user?.toString()) diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/PipelineTemplateService.groovy b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/PipelineTemplateService.groovy index f6c8f6f3dc..1d2d76b7a8 100644 --- a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/PipelineTemplateService.groovy +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/PipelineTemplateService.groovy @@ -44,10 +44,10 @@ class PipelineTemplateService { } List findByScope(List scopes) { - front50Service.getPipelineTemplates((String[]) scopes.toArray()) + front50Service.getPipelineTemplates((String[]) scopes?.toArray()) } - Map resolve(String source) { - orcaService.resolvePipelineTemplate(source) + Map resolve(String source, String executionId, String pipelineConfigId) { + orcaService.resolvePipelineTemplate(source, executionId, pipelineConfigId) } } diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/ServerGroupManagerService.java b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/ServerGroupManagerService.java new file mode 100644 index 0000000000..048550e53c --- /dev/null +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/ServerGroupManagerService.java @@ -0,0 +1,38 @@ +/* + * Copyright 2017 Google, 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.services; + +import com.netflix.spinnaker.gate.services.internal.ClouddriverService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; + +@Component +public class ServerGroupManagerService { + private final ClouddriverService clouddriverService; + + @Autowired + ServerGroupManagerService(ClouddriverService clouddriverService) { + this.clouddriverService = clouddriverService; + } + + public List getServerGroupManagersForApplication(String application) { + return this.clouddriverService.getServerGroupManagersForApplication(application); + } +} diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/ServerGroupService.groovy b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/ServerGroupService.groovy index 5682d16c25..c4174a9aab 100644 --- a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/ServerGroupService.groovy +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/ServerGroupService.groovy @@ -49,7 +49,13 @@ class ServerGroupService { List getForApplications(List applications, String cloudProvider, String selectorKey) { HystrixFactory.newListCommand(GROUP, "getServerGroupsForApplications") { - clouddriverServiceSelector.select(selectorKey).getServerGroups(applications, cloudProvider) + clouddriverServiceSelector.select(selectorKey).getServerGroups(applications, null, cloudProvider) + } execute() + } + + List getForIds(List ids, String cloudProvider, String selectorKey) { + HystrixFactory.newListCommand(GROUP, "getServerGroupsForIds") { + clouddriverServiceSelector.select(selectorKey).getServerGroups(null, ids, cloudProvider) } execute() } diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/V2CanaryService.groovy b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/V2CanaryService.groovy new file mode 100644 index 0000000000..4a52d89f29 --- /dev/null +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/V2CanaryService.groovy @@ -0,0 +1,78 @@ +/* + * Copyright 2017 Google, 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.services + +import static com.netflix.spinnaker.gate.retrofit.UpstreamBadRequest.classifyError + +import com.netflix.spinnaker.gate.services.commands.HystrixFactory +import com.netflix.spinnaker.gate.services.internal.KayentaService +import groovy.transform.CompileStatic +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression +import org.springframework.stereotype.Component +import retrofit.RetrofitError + +@Component +@CompileStatic +@ConditionalOnExpression('${services.kayenta.enabled:false}') +class V2CanaryService { + + private static final String HYSTRIX_GROUP = "v2-canaries" + + @Autowired + KayentaService kayentaService + + List getCredentials() { + return HystrixFactory.newListCommand(HYSTRIX_GROUP, "getCanaryCredentials", { + try { + return kayentaService.getCredentials() + } catch (RetrofitError error) { + throw classifyError(error) + } + }).execute() as List + } + + List listJudges() { + return HystrixFactory.newListCommand(HYSTRIX_GROUP, "listCanaryJudges", { + try { + return kayentaService.listJudges() + } catch (RetrofitError error) { + throw classifyError(error) + } + }).execute() as List + } + + Map getCanaryResults(String canaryConfigId, String canaryExecutionId, String storageAccountName) { + return HystrixFactory.newMapCommand(HYSTRIX_GROUP, "getCanaryResults", { + try { + return kayentaService.getCanaryResult(canaryConfigId, canaryExecutionId, storageAccountName) + } catch (RetrofitError error) { + throw classifyError(error) + } + }).execute() as Map + } + + List getMetricSetPairList(String metricSetPairListId, String storageAccountName) { + return HystrixFactory.newListCommand(HYSTRIX_GROUP, "getMetricSetPairList", { + try { + return kayentaService.getMetricSetPairList(metricSetPairListId, storageAccountName) + } catch (RetrofitError error) { + throw classifyError(error) + } + }).execute() as List + } +} diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/internal/KayentaService.groovy b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/internal/KayentaService.groovy index 3fb88cea46..dc3e3a8e3d 100644 --- a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/internal/KayentaService.groovy +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/internal/KayentaService.groovy @@ -24,26 +24,35 @@ interface KayentaService { List getCredentials() @GET("/canaryConfig") - List getCanaryConfigs() + List getCanaryConfigs(@Query("application") String application, + @Query("configurationAccountName") String configurationAccountName) @GET("/canaryConfig/{id}") - Map getCanaryConfig(@Path("id") String id) + Map getCanaryConfig(@Path("id") String id, + @Query("configurationAccountName") String configurationAccountName) @POST("/canaryConfig") - Response createCanaryConfig(@Body Map config) + Map createCanaryConfig(@Body Map config, + @Query("configurationAccountName") String configurationAccountName) @PUT("/canaryConfig/{id}") - Response updateCanaryConfig(@Path("id") String id, @Body Map config) + Map updateCanaryConfig(@Path("id") String id, + @Body Map config, + @Query("configurationAccountName") String configurationAccountName) @DELETE("/canaryConfig/{id}") - Response deleteCanaryConfig(@Path("id") String id) + Response deleteCanaryConfig(@Path("id") String id, + @Query("configurationAccountName") String configurationAccountName) @GET("/judges") List listJudges() - @GET("/canaryJudgeResult") - List listResults() + @GET("/canary/{canaryConfigId}/{canaryExecutionId}") + Map getCanaryResult(@Path("canaryConfigId") String canaryConfigId, + @Path("canaryExecutionId") String canaryExecutionId, + @Query("storageAccountName") String storageAccountName) - @GET("/canaryJudgeResult/{id}") - Map getResult(@Path("id") String id) + @GET("/metricSetPairList/{metricSetPairListId}") + List getMetricSetPairList(@Path("metricSetPairListId") metricSetPairListId, + @Query("accountName") String storageAccountName) } diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/internal/OrcaService.groovy b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/internal/OrcaService.groovy index d58dfbfe41..e8ccb35f06 100644 --- a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/internal/OrcaService.groovy +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/internal/OrcaService.groovy @@ -115,7 +115,7 @@ interface OrcaService { @Headers("Accept: application/json") @GET("/pipelineTemplate") - Map resolvePipelineTemplate(@Query("source") String source) + Map resolvePipelineTemplate(@Query("source") String source, @Query("executionId") String executionId, @Query("pipelineConfigId") String pipelineConfigId) @POST("/convertPipelineToTemplate") Response convertToPipelineTemplate(@Body Map pipelineConfig) diff --git a/gate-web/src/test/groovy/com/netflix/spinnaker/gate/services/PipelineServiceSpec.groovy b/gate-web/src/test/groovy/com/netflix/spinnaker/gate/services/PipelineServiceSpec.groovy index 75349aff12..b6893ed24a 100644 --- a/gate-web/src/test/groovy/com/netflix/spinnaker/gate/services/PipelineServiceSpec.groovy +++ b/gate-web/src/test/groovy/com/netflix/spinnaker/gate/services/PipelineServiceSpec.groovy @@ -71,4 +71,38 @@ class PipelineServiceSpec extends Specification { [[type: 'a'], [type: 'b']] | null || [[type: 'a'], [type: 'b']] [[type: 'a'], [type: 'b']] | [[type: 'c']] || [[type: 'a'], [type: 'b'], [type: 'c']] } + + @Unroll + void 'startPipeline should throw exceptions if required parameters are not supplied'() { + given: + OrcaService orcaService = Mock(OrcaService) + def service = new PipelineService( + applicationService: Mock(ApplicationService) { + 1 * getPipelineConfigForApplication('app', 'p-id') >> [ + parameterConfig: [[name: 'param1', required: true]] + ] + }, + orcaService: orcaService + ) + when: + def didThrow = false + try { + service.trigger('app', 'p-id', trigger) + } catch (IllegalArgumentException ignored) { + didThrow = true + } + + then: + didThrow == isThrown + + where: + trigger || isThrown + [:] || true + [parameters: null] || true + [parameters: [:]] || true + [parameters: [param1: null]] || true + [parameters: [param1: false]] || false + [parameters: [param1: 0]] || false + [parameters: [param1: 'a']] || false + } }