diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/ApplicationConfigurationProperties.java b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/ApplicationConfigurationProperties.java new file mode 100644 index 000000000..a32da2e32 --- /dev/null +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/ApplicationConfigurationProperties.java @@ -0,0 +1,33 @@ +/* + * Copyright 2024 Salesforce.com, 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.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Data +@Configuration +@ConfigurationProperties(prefix = "application") +public class ApplicationConfigurationProperties { + /** + * defaults to false. When enabled, this will ignore all the applications that are only known to + * clouddriver but are missing from front50. This is done so that we can treat front50 as the + * source of truth for applications + */ + private boolean useFront50AsSourceOfTruth; +} diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/GateConfig.groovy b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/GateConfig.groovy index a7b3756e3..8044cb624 100644 --- a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/GateConfig.groovy +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/GateConfig.groovy @@ -73,7 +73,7 @@ import static retrofit.Endpoints.newFixedEndpoint @CompileStatic @Configuration @Slf4j -@EnableConfigurationProperties([PipelineControllerConfigProperties.class]) +@EnableConfigurationProperties([PipelineControllerConfigProperties, ApplicationConfigurationProperties]) @Import([PluginsAutoConfiguration, DeckPluginConfiguration, PluginWebConfiguration]) class GateConfig { diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/ApplicationService.groovy b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/ApplicationService.groovy index 599dd5f66..8d1f69b08 100644 --- a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/ApplicationService.groovy +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/ApplicationService.groovy @@ -16,6 +16,7 @@ package com.netflix.spinnaker.gate.services +import com.netflix.spinnaker.gate.config.ApplicationConfigurationProperties import com.netflix.spinnaker.gate.config.Service import com.netflix.spinnaker.gate.config.ServiceConfiguration import com.netflix.spinnaker.gate.services.internal.ClouddriverService @@ -37,25 +38,40 @@ import java.util.concurrent.ExecutionException import java.util.concurrent.ExecutorService import java.util.concurrent.Future import java.util.concurrent.atomic.AtomicReference +import java.util.stream.Collectors @CompileStatic @Component @Slf4j class ApplicationService { + private ServiceConfiguration serviceConfiguration + private ClouddriverServiceSelector clouddriverServiceSelector + private Front50Service front50Service + private ExecutorService executorService - @Autowired - ServiceConfiguration serviceConfiguration - - @Autowired - ClouddriverServiceSelector clouddriverServiceSelector + private AtomicReference> allApplicationsCache + private ApplicationConfigurationProperties applicationConfigurationProperties @Autowired - Front50Service front50Service - - @Autowired - ExecutorService executorService + ApplicationService( + ServiceConfiguration serviceConfiguration, + ClouddriverServiceSelector clouddriverServiceSelector, + Front50Service front50Service, + ExecutorService executorService, + ApplicationConfigurationProperties applicationConfigurationProperties + ){ + this.serviceConfiguration = serviceConfiguration + this.clouddriverServiceSelector = clouddriverServiceSelector + this.front50Service = front50Service + this.executorService = executorService + this.applicationConfigurationProperties = applicationConfigurationProperties + this.allApplicationsCache = new AtomicReference<>([]) + } - private AtomicReference> allApplicationsCache = new AtomicReference<>([]) + // used in tests + AtomicReference> getAllApplicationsCache(){ + this.allApplicationsCache + } @Scheduled(fixedDelayString = '${services.front50.applicationRefreshIntervalMs:5000}') void refreshApplicationsCache() { @@ -78,14 +94,19 @@ class ApplicationService { * @return Applications */ List> tick(boolean expandClusterNames = true) { - def applicationListRetrievers = buildApplicationListRetrievers(expandClusterNames) - List>> futures = executorService.invokeAll(applicationListRetrievers) List> all - try { - all = futures.collect { it.get() } - } catch (ExecutionException ee) { - throw ee.cause + if (applicationConfigurationProperties.useFront50AsSourceOfTruth) { + all = getApplicationsWithFront50AsSourceOfTruth(expandClusterNames) + } else { + def applicationListRetrievers = buildApplicationListRetrievers(expandClusterNames) + List>> futures = executorService.invokeAll(applicationListRetrievers) + try { + all = futures.collect { it.get() } + } catch (ExecutionException ee) { + throw ee.cause + } } + List flat = (List) all?.flatten()?.toList() return mergeApps(flat, serviceConfiguration.getService('front50')).collect { it.attributes @@ -97,19 +118,24 @@ class ApplicationService { } Map getApplication(String name, boolean expand) { - def applicationRetrievers = buildApplicationRetrievers(name, expand) - def futures = executorService.invokeAll(applicationRetrievers) List applications - try { - applications = (List) futures.collect { it.get() } - } catch (ExecutionException ee) { - throw ee.cause - } - if (!expand) { - def cachedApplication = allApplicationsCache.get().find { name.equalsIgnoreCase(it.name as String) } - if (cachedApplication) { - // ensure that `cachedApplication` attributes are overridden by any previously fetched metadata from front50 - applications.add(0, cachedApplication) + if (applicationConfigurationProperties.useFront50AsSourceOfTruth) { + applications = getApplicationWithFront50AsSourceOfTruth(name, expand) + } else { + def applicationRetrievers = buildApplicationRetrievers(name, expand) + def futures = executorService.invokeAll(applicationRetrievers) + try { + applications = (List) futures.collect { it.get() } + } catch (ExecutionException ee) { + throw ee.cause + } + + if (!expand) { + def cachedApplication = allApplicationsCache.get().find { name.equalsIgnoreCase(it.name as String) } + if (cachedApplication) { + // ensure that `cachedApplication` attributes are overridden by any previously fetched metadata from front50 + applications.add(0, cachedApplication) + } } } List mergedApps = mergeApps(applications, serviceConfiguration.getService('front50')) @@ -134,10 +160,13 @@ class ApplicationService { } private Collection>> buildApplicationListRetrievers(boolean expandClusterNames) { - return [ - new Front50ApplicationListRetriever(front50Service, allApplicationsCache), - new ClouddriverApplicationListRetriever(clouddriverServiceSelector.select(), allApplicationsCache, expandClusterNames - )] as Collection>> + [ + new Front50ApplicationListRetriever(front50Service, allApplicationsCache) as Callable>, + new ClouddriverApplicationListRetriever( + clouddriverServiceSelector.select(), + allApplicationsCache, + expandClusterNames) as Callable> + ] } private Collection> buildApplicationRetrievers(String applicationName, boolean expand) { @@ -145,7 +174,11 @@ class ApplicationService { new Front50ApplicationRetriever(applicationName, front50Service, allApplicationsCache) as Callable ] if (expand) { - retrievers.add(new ClouddriverApplicationRetriever(applicationName, clouddriverServiceSelector.select()) as Callable) + retrievers.add( + new ClouddriverApplicationRetriever( + applicationName, + clouddriverServiceSelector.select()) as Callable + ) } return retrievers } @@ -228,6 +261,96 @@ class ApplicationService { }.flatten().toSet().sort().join(',') } + /** + * gets the applications from front50 and clouddriver, and only considers the applications + * returned from front50 to be the source of truth. All applications obtained from clouddriver + * that are not known to front50 are ignored + * + * @param expandClusterNames gets passed along to the ClouddriverApplicationListRetriever + * @return a list of type List that contains the responses from front50 and clouddriver + */ + private List> getApplicationsWithFront50AsSourceOfTruth(boolean expandClusterNames) { + List front50Apps, clouddriverApps + try { + Future> front50future = executorService.submit( + new Front50ApplicationListRetriever(front50Service, allApplicationsCache) as Callable> + ) + + Future> clouddriverFuture = executorService.submit( + new ClouddriverApplicationListRetriever( + clouddriverServiceSelector.select(), + allApplicationsCache, + expandClusterNames) as Callable> + ) + // capture the results from both front50 and clouddriver + front50Apps = front50future.get() + clouddriverApps = clouddriverFuture.get() + } catch (ExecutionException ee) { + log.error("error occurred when retrieving applications. Error: ", ee) + throw ee.cause + } + + // get all app names from front50. This becomes our source of truth for known applications + Set allFront50AppNames = front50Apps.stream() + .map({ it -> it.get("name") as String }) + .collect(Collectors.toSet()) + + // iterate through all the results from clouddriver and only consider those applications that + // are known to front50 + Set clouddriverAppsKnownToFront50 = clouddriverApps.stream() + .filter({ it -> + String clouddriverAppName = it.get("name") + allFront50AppNames.any { front50AppName -> front50AppName.equalsIgnoreCase(clouddriverAppName) } + }) + .collect(Collectors.toSet()) + + List> all = [] + all.add(front50Apps) + all.add(clouddriverAppsKnownToFront50.toList()) + return all + } + + /** + * gets the application from front50 first. If it does not contain the application, then processing + * stops as there is no need to check clouddriver for the application. Otherwise, depending on + * the parameter expand, clouddriver is queried for the application. + * + * @param name application name + * @param expand if true, then clouddriver is queried for the application only if front50 contains + * the application + * @return a list of type Map that contains the responses from front50 and/or clouddriver + */ + private List getApplicationWithFront50AsSourceOfTruth(String name, boolean expand) { + Map front50App, clouddriverApp + List result = [] + try { + Future front50future = executorService.submit( + new Front50ApplicationRetriever(name, front50Service, allApplicationsCache) as Callable + ) + // capture the result from front50 + front50App = front50future.get() + if (front50App) { + result.add(front50App) + if (expand) { + Future clouddriverFuture = executorService.submit( + new ClouddriverApplicationRetriever(name, clouddriverServiceSelector.select()) as Callable + ) + // capture the result from clouddriver + clouddriverApp = clouddriverFuture.get() + String clouddriverAppName = clouddriverApp.get("name") + if (clouddriverAppName?.equalsIgnoreCase(front50App.get("name") as String)) { + result.add(clouddriverApp) + } + } + } + } catch (ExecutionException ee) { + log.error("error occurred when retrieving application: ${name}. Error: ", ee) + throw ee.cause + } + + return result + } + static class Front50ApplicationListRetriever implements Callable> { private final Front50Service front50 private final AtomicReference> allApplicationsCache diff --git a/gate-web/src/test/groovy/com/netflix/spinnaker/gate/ApplicationServiceSpec.groovy b/gate-web/src/test/groovy/com/netflix/spinnaker/gate/ApplicationServiceSpec.groovy index 45e09672f..8b8efbf39 100644 --- a/gate-web/src/test/groovy/com/netflix/spinnaker/gate/ApplicationServiceSpec.groovy +++ b/gate-web/src/test/groovy/com/netflix/spinnaker/gate/ApplicationServiceSpec.groovy @@ -16,15 +16,17 @@ package com.netflix.spinnaker.gate +import com.netflix.spinnaker.gate.config.ApplicationConfigurationProperties import com.netflix.spinnaker.gate.config.Service import com.netflix.spinnaker.gate.config.ServiceConfiguration import com.netflix.spinnaker.gate.services.ApplicationService import com.netflix.spinnaker.gate.services.internal.ClouddriverServiceSelector import com.netflix.spinnaker.gate.services.internal.Front50Service import com.netflix.spinnaker.gate.services.internal.ClouddriverService -import spock.lang.Shared +import retrofit.RetrofitError +import retrofit.client.Response +import retrofit.mime.TypedByteArray import spock.lang.Specification -import spock.lang.Subject import spock.lang.Unroll import java.util.concurrent.Executors @@ -36,27 +38,31 @@ class ApplicationServiceSpec extends Specification { select() >> clouddriver } - @Subject - def service = applicationService() - private applicationService() { - def service = new ApplicationService() - def config = new ServiceConfiguration(services: [front50: new Service()]) - - service.serviceConfiguration = config - service.front50Service = front50 - service.clouddriverServiceSelector = clouddriverSelector - service.executorService = Executors.newFixedThreadPool(1) + applicationService(new ApplicationConfigurationProperties()) + } + private applicationService(ApplicationConfigurationProperties applicationConfigurationProperties) { + def config = new ServiceConfiguration(services: [front50: new Service()]) + def service = new ApplicationService( + config, + clouddriverSelector, + front50, + Executors.newFixedThreadPool(1), + applicationConfigurationProperties + ) return service } - void "should properly aggregate application data from Front50 and Clouddriver"() { + void "should properly aggregate application data from Front50 and Clouddriver when useFront50AsSourceOfTruth: #useFront50AsSourceOfTruth"() { given: def clouddriverApp = [name: name, attributes: [clouddriverName: name, name: "bad"], clusters: [(account): [cluster]]] def front50App = [name: name, email: email, owner: owner, accounts: account] + ApplicationConfigurationProperties applicationConfigurationProperties = new ApplicationConfigurationProperties() + applicationConfigurationProperties.setUseFront50AsSourceOfTruth(useFront50AsSourceOfTruth) when: + def service = applicationService(applicationConfigurationProperties) def app = service.getApplication(name, true) then: @@ -66,6 +72,7 @@ class ApplicationServiceSpec extends Specification { app == [name: name, attributes: (clouddriverApp.attributes + front50App), clusters: clouddriverApp.clusters] where: + useFront50AsSourceOfTruth << [true, false] name = "foo" email = "bar@baz.bz" owner = "danw" @@ -74,12 +81,138 @@ class ApplicationServiceSpec extends Specification { providerType = "aws" } - void "should ignore accounts from front50 and only include those from clouddriver clusters"() { + @Unroll + void "when UseFront50AsSourceOfTruth: #checkFront50 and application exists only in clouddriver"() { + given: + def clouddriverApp = [name: name, attributes: [clouddriverName: name, name: "bad"], clusters: [(account): [cluster]]] + def front50App = [:] + ApplicationConfigurationProperties applicationConfigurationProperties = new ApplicationConfigurationProperties() + applicationConfigurationProperties.setUseFront50AsSourceOfTruth(checkFront50) + + when: + def service = applicationService(applicationConfigurationProperties) + def app = service.getApplication(name, true) + + then: + 1 * front50.getApplication(name) >> front50App + + numberOfClouddriverInvocations * clouddriver.getApplication(name) >> clouddriverApp + + assert app == result + + where: + checkFront50 | numberOfClouddriverInvocations | result + true | 0 | null + false | 1 | [name:"foo", attributes:[clouddriverName:"foo", name:"foo", accounts: "test"], clusters:["test":["cluster1"]]] + + name = "foo" + email = "bar@baz.bz" + owner = "danw" + cluster = "cluster1" + account = "test" + providerType = "aws" + } + + @Unroll + void "when UseFront50AsSourceOfTruth: #checkFront50 and application exists only in clouddriver, but front50 throws an exception"() { + given: + def clouddriverApp = [name: name, attributes: [clouddriverName: name, name: "bad"], clusters: [(account): [cluster]]] + ApplicationConfigurationProperties applicationConfigurationProperties = new ApplicationConfigurationProperties() + applicationConfigurationProperties.setUseFront50AsSourceOfTruth(checkFront50) + + when: + def service = applicationService(applicationConfigurationProperties) + def app = service.getApplication(name, true) + + then: + 1 * front50.getApplication(name) >> exception + + numberOfClouddriverInvocations * clouddriver.getApplication(name) >> clouddriverApp + + assert app == result + + where: + checkFront50 | numberOfClouddriverInvocations | result | exception + true | 0 | null | new Exception("fatal exception") + true | 0 | null | retrofit404() + false | 1 | [name:"foo", attributes:[clouddriverName:"foo", name:"foo", accounts: "test"], clusters:["test":["cluster1"]]] | new Exception("fatal exception") + false | 1 | [name:"foo", attributes:[clouddriverName:"foo", name:"foo", accounts: "test"], clusters:["test":["cluster1"]]] | retrofit404() + + name = "foo" + email = "bar@baz.bz" + owner = "danw" + cluster = "cluster1" + account = "test" + providerType = "aws" + } + + @Unroll + void "when UseFront50AsSourceOfTruth: #checkFront50 and application exists only in front50, but clouddriver throws an exception"() { + given: + def front50App = [name: name, email: email, owner: owner, accounts: account] + ApplicationConfigurationProperties applicationConfigurationProperties = new ApplicationConfigurationProperties() + applicationConfigurationProperties.setUseFront50AsSourceOfTruth(checkFront50) + + when: + def service = applicationService(applicationConfigurationProperties) + def app = service.getApplication(name, true) + + then: + 1 * front50.getApplication(name) >> front50App + 1 * clouddriver.getApplication(name) >> exception + + assert app == [name: name, attributes:[name: name, email: email, owner: owner, accounts: account], clusters:[:]] + + where: + checkFront50 | exception + true | new Exception("fatal exception") + true | retrofit404() + false | new Exception("fatal exception") + false | retrofit404() + + name = "foo" + email = "bar@baz.bz" + owner = "danw" + cluster = "cluster1" + account = "test" + providerType = "aws" + } + + @Unroll + void "when UseFront50AsSourceOfTruth: #checkFront50 and both front50 and clouddriver throw an exception"() { + given: + ApplicationConfigurationProperties applicationConfigurationProperties = new ApplicationConfigurationProperties() + applicationConfigurationProperties.setUseFront50AsSourceOfTruth(checkFront50) + + when: + def service = applicationService(applicationConfigurationProperties) + def app = service.getApplication(name, true) + + then: + 1 * front50.getApplication(name) >> exception + numberOfClouddriverInvocations * clouddriver.getApplication(name) >> exception + + assert app == null + + where: + checkFront50 | numberOfClouddriverInvocations | exception + true | 0 | new Exception("fatal exception") + true | 0 | retrofit404() + false | 1 | new Exception("fatal exception") + false | 1 | retrofit404() + + name = "foo" + } + + void "should ignore accounts from front50 and only include those from clouddriver clusters when UseFront50AsSourceOfTruth: #checkFront50"() { given: def clouddriverApp = [name: name, attributes: [clouddriverName: name, name: "bad"], clusters: [(clouddriverAccount): [cluster]]] def front50App = [name: name, email: email, owner: owner, accounts: front50Account] + ApplicationConfigurationProperties applicationConfigurationProperties = new ApplicationConfigurationProperties() + applicationConfigurationProperties.setUseFront50AsSourceOfTruth(checkFront50) when: + def service = applicationService() def app = service.getApplication(name, true) then: @@ -89,6 +222,7 @@ class ApplicationServiceSpec extends Specification { app == [name: name, attributes: (clouddriverApp.attributes + front50App + [accounts: [clouddriverAccount].toSet().sort().join(',')]), clusters: clouddriverApp.clusters] where: + checkFront50 << [true, false] name = "foo" email = "bar@baz.bz" owner = "danw" @@ -101,9 +235,14 @@ class ApplicationServiceSpec extends Specification { @Unroll void "should return null when application account does not match includedAccounts"() { setup: - def serviceWithDifferentConfig = applicationService() def config = new ServiceConfiguration(services: [front50: new Service(config: [includedAccounts: includedAccount])]) - serviceWithDifferentConfig.serviceConfiguration = config + ApplicationService serviceWithDifferentConfig = new ApplicationService( + config, + clouddriverSelector, + front50, + Executors.newFixedThreadPool(1), + new ApplicationConfigurationProperties() + ) when: def app = serviceWithDifferentConfig.getApplication(name, true) @@ -131,6 +270,7 @@ class ApplicationServiceSpec extends Specification { void "should return null when no application attributes are available"() { when: + def service = applicationService() def app = service.getApplication(name, true) then: @@ -150,6 +290,7 @@ class ApplicationServiceSpec extends Specification { def front50App = [name: name.toLowerCase(), email: email] when: + def service = applicationService() service.refreshApplicationsCache() def apps = service.getAllApplications() @@ -175,6 +316,7 @@ class ApplicationServiceSpec extends Specification { def front50App = [name: name.toLowerCase(), email: email, accounts: "test"] when: + def service = applicationService() service.refreshApplicationsCache() def apps = service.getAllApplications() @@ -193,6 +335,119 @@ class ApplicationServiceSpec extends Specification { email = "foo@bar.bz" } + @Unroll + def "should properly merge accounts for retrieved apps with clusterNames when useFront50AsSourceOfTruth is #checkFront50"() { + given: + def clouddriverApp1 = [name: "appname1", attributes: [name: "appname1"], clusterNames: [prod: ["cluster-prod"]]] + def clouddriverApp2 = [name: "appname2", attributes: [name: "appname2"], clusterNames: [dev: ["cluster-dev"]]] + def front50App = [name: "appname1", email: "foo@bar.bz", accounts: "test"] + ApplicationConfigurationProperties applicationConfigurationProperties = new ApplicationConfigurationProperties() + applicationConfigurationProperties.setUseFront50AsSourceOfTruth(checkFront50) + + when: + def service = applicationService(applicationConfigurationProperties) + service.refreshApplicationsCache() + def apps = service.getAllApplications() + + then: + 1 * clouddriver.getAllApplicationsUnrestricted(true) >> [clouddriverApp1, clouddriverApp2] + 1 * front50.getAllApplicationsUnrestricted() >> [front50App] + + assert apps.size() == resultSize + assert apps == result + + where: + checkFront50 | resultSize | result + true | 1 | [[name:"appname1", email:"foo@bar.bz", accounts:"prod"]] + false | 2 | [[name:"appname1", email:"foo@bar.bz", accounts:"prod"], [name:"appname2", accounts:"dev"]] + } + + @Unroll + def "should handle front50 returning an exception when useFront50AsSourceOfTruth is #checkFront50"() { + given: + def clouddriverApp1 = [name: "appname1", attributes: [name: "appname1"], clusterNames: [prod: ["cluster-prod"]]] + def clouddriverApp2 = [name: "appname2", attributes: [name: "appname2"], clusterNames: [dev: ["cluster-dev"]]] + ApplicationConfigurationProperties applicationConfigurationProperties = new ApplicationConfigurationProperties() + applicationConfigurationProperties.setUseFront50AsSourceOfTruth(checkFront50) + + when: + def service = applicationService(applicationConfigurationProperties) + service.refreshApplicationsCache() + def apps = service.getAllApplications() + + then: + 1 * front50.getAllApplicationsUnrestricted() >> exception + 1 * clouddriver.getAllApplicationsUnrestricted(true) >> [clouddriverApp1, clouddriverApp2] + + assert apps.size() == resultSize + assert apps == result + + where: + checkFront50 | resultSize | result | exception + true | 0 | [] | new Exception("fatal exception") + true | 0 | [] | retrofit404() + false | 2 | [[name:"appname1", accounts:"prod"], [name:"appname2", accounts:"dev"]] | new Exception("fatal exception") + false | 2 | [[name:"appname1", accounts:"prod"], [name:"appname2", accounts:"dev"]] | retrofit404() + } + + @Unroll + def "should handle clouddriver returning an exception when useFront50AsSourceOfTruth is #checkFront50"() { + given: + def front50App = [name: name, email: email, accounts: account] + ApplicationConfigurationProperties applicationConfigurationProperties = new ApplicationConfigurationProperties() + applicationConfigurationProperties.setUseFront50AsSourceOfTruth(checkFront50) + + when: + def service = applicationService(applicationConfigurationProperties) + service.refreshApplicationsCache() + def apps = service.getAllApplications() + + then: + 1 * front50.getAllApplicationsUnrestricted() >> [front50App] + 1 * clouddriver.getAllApplicationsUnrestricted(true) >> exception + + assert apps.size() == 1 + assert apps == [[name: name, email: email, accounts: account]] + + where: + checkFront50 | exception + true | new Exception("fatal exception") + true | retrofit404() + false | new Exception("fatal exception") + false | retrofit404() + + name = "appname1" + email = "foo@bar.bz" + account = "test" + } + + @Unroll + def "should handle both front50 and clouddriver returning an exception when useFront50AsSourceOfTruth is #checkFront50"() { + given: + ApplicationConfigurationProperties applicationConfigurationProperties = new ApplicationConfigurationProperties() + applicationConfigurationProperties.setUseFront50AsSourceOfTruth(checkFront50) + + when: + def service = applicationService(applicationConfigurationProperties) + service.refreshApplicationsCache() + def apps = service.getAllApplications() + + then: + 1 * front50.getAllApplicationsUnrestricted() >> exception + 1 * clouddriver.getAllApplicationsUnrestricted(true) >> exception + + assert apps.size() == 0 + + where: + + checkFront50 | exception + true | new Exception("fatal exception") + true | retrofit404() + false | new Exception("fatal exception") + false | retrofit404() + + } + @Unroll void "should merge accounts"() { expect: @@ -210,6 +465,7 @@ class ApplicationServiceSpec extends Specification { @Unroll void "should return pipeline config based on name or id"() { when: + def service = applicationService() def result = service.getPipelineConfigForApplication(app, nameOrId) != null then: @@ -229,7 +485,7 @@ class ApplicationServiceSpec extends Specification { given: def name = 'myApp' def serviceWithApplicationsCache = applicationService() - serviceWithApplicationsCache.allApplicationsCache.set([ + serviceWithApplicationsCache.getAllApplicationsCache().set([ [name: name, email: "cached@email.com"] ]) @@ -247,4 +503,8 @@ class ApplicationServiceSpec extends Specification { app.attributes.email == "updated@email.com" } + + def retrofit404(){ + RetrofitError.httpError("http://localhost", new Response("http://localhost", 404, "Not Found", [], new TypedByteArray("application/json", new byte[0])), null, Map) + } } diff --git a/gate-web/src/test/groovy/com/netflix/spinnaker/gate/controllers/ApplicationControllerSpec.groovy b/gate-web/src/test/groovy/com/netflix/spinnaker/gate/controllers/ApplicationControllerSpec.groovy index d3d8fdcd0..f73f491c6 100644 --- a/gate-web/src/test/groovy/com/netflix/spinnaker/gate/controllers/ApplicationControllerSpec.groovy +++ b/gate-web/src/test/groovy/com/netflix/spinnaker/gate/controllers/ApplicationControllerSpec.groovy @@ -16,7 +16,11 @@ package com.netflix.spinnaker.gate.controllers +import com.netflix.spinnaker.gate.config.ApplicationConfigurationProperties +import com.netflix.spinnaker.gate.config.ServiceConfiguration import com.netflix.spinnaker.gate.services.ApplicationService +import com.netflix.spinnaker.gate.services.internal.ClouddriverService +import com.netflix.spinnaker.gate.services.internal.ClouddriverServiceSelector import com.netflix.spinnaker.gate.services.internal.Front50Service import com.squareup.okhttp.mockwebserver.MockWebServer import groovy.json.JsonSlurper @@ -28,6 +32,8 @@ import org.springframework.web.util.NestedServletException import spock.lang.Specification import spock.lang.Unroll +import java.util.concurrent.Executors + import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get class ApplicationControllerSpec extends Specification { @@ -43,7 +49,18 @@ class ApplicationControllerSpec extends Specification { void setup(){ front50Service = Mock(Front50Service) - applicationService = new ApplicationService(front50Service: front50Service) + def clouddriver = Mock(ClouddriverService) + def clouddriverSelector = Mock(ClouddriverServiceSelector) { + select() >> clouddriver + } + + applicationService = new ApplicationService( + new ServiceConfiguration(), + clouddriverSelector, + front50Service, + Executors.newFixedThreadPool(1), + new ApplicationConfigurationProperties() + ) server.start() mockMvc = MockMvcBuilders.standaloneSetup(new ApplicationController(applicationService: applicationService)).build() }