Spring Cloud integration

This commit is contained in:
Marcin Grzejszczak
2016-03-29 22:40:20 +02:00
parent 538b946c5a
commit 37cee4ccda
30 changed files with 553 additions and 25 deletions

View File

@@ -68,7 +68,7 @@ class WireMockRequestStubStrategy extends BaseWireMockStubStrategy {
requestPattern.bodyPatterns = [new ValuePattern(jsonCompareMode: org.skyscreamer.jsonassert.JSONCompareMode.LENIENT,
equalToJson: JsonOutput.toJson(getMatchingStrategy(request.body.clientValue).clientValue) ) ]
} else {
requestPattern.bodyPatterns = values.collect { new ValuePattern(matchesJsonPath: it.jsonPath().replace("\\\\", "\\")) } ?: null
requestPattern.bodyPatterns = values.collect { new ValuePattern(matchesJsonPath: it.jsonPath().replace("\\\\", "\\")) } ?: null as List<ValuePattern>
}
} else if (contentType == ContentType.XML) {
requestPattern.bodyPatterns = [new ValuePattern(equalToXml: getMatchingStrategy(request.body.clientValue).clientValue.toString())]

View File

@@ -61,6 +61,18 @@ subprojects {
exclude(group: 'org.codehaus.groovy')
}
}
// fixing the groovydoc issue http://stackoverflow.com/questions/20618857/gradle-task-groovydoc-failing-with-noclassdeffounderror
configurations {
jansi.extendsFrom(runtime)
}
groovydoc {
def title = "IPDS ${version}"
groovyClasspath = project.configurations.jansi
}
dependencies {
jansi 'org.fusesource.jansi:jansi:1.11'
}
}
project(':accurest-core') {

View File

@@ -1,6 +1,7 @@
include "accurest-core", "accurest-gradle-plugin", 'accurest-converters', 'accurest-testing-utils'
include ':stub-runner:stub-runner'
include ':stub-runner:stub-runner-spring'
include ':stub-runner:stub-runner-spring-cloud'
include ':stub-runner:stub-runner-junit'
rootProject.name = "accurest"

View File

@@ -58,7 +58,7 @@ public class AccurestRule implements TestRule, StubFinder {
String repoRoot = System.getProperty("stubrunner.stubs.repository.root", "");
String stubSuffix = System.getProperty("stubrunner.stubs.classifier", "stubs");
Boolean workOffline = Boolean.parseBoolean(System.getProperty("stubrunner.work-offline", "false"));
String stubsToDownload = System.getProperty("stubrunner.stubs", "");
String stubsToDownload = System.getProperty("stubrunner.stubs.ids", "");
if (StringUtils.hasText(stubsToDownload)) {
Collections.addAll(stubs, stubsToDownload.split(","));
}

View File

@@ -0,0 +1,13 @@
stub-runner-spring-cloud
========================
Registers the stubs in the provided Service Discovery. It's enough to add the jar
```
io.codearte.accurest:stub-runner-spring-cloud
```
and the Stub Runner autoconfiguration should be picked up.
In order to find the registered server via service discovery, you have to use the
`artifactId` as the name of the application

View File

@@ -0,0 +1,25 @@
description = 'Spring configuration for stub-runner'
dependencies {
compile project(':stub-runner-root:stub-runner-spring')
compile localGroovy()
compile 'org.springframework.cloud:spring-cloud-starter:[1.1.0.RC2,)'
// TODO: should be compile only
compile 'org.springframework.cloud:spring-cloud-starter-ribbon:[1.1.0.RC1,)'
testCompile('org.spockframework:spock-core:1.0-groovy-2.3') {
exclude(group: 'org.codehaus.groovy')
}
testCompile 'cglib:cglib-nodep:2.2'
testCompile 'org.objenesis:objenesis:2.1'
testCompile 'org.springframework.cloud:spring-cloud-starter-zookeeper-discovery:1.0.0.RC1'
testCompile 'org.apache.curator:curator-test:2.9.1'
testCompile 'io.reactivex:rxjava:1.1.1'
testCompile 'org.springframework.boot:spring-boot-starter-test:1.3.3.RELEASE'
testCompile "org.springframework:spring-web:4.2.5.RELEASE"
testCompile('org.spockframework:spock-spring:1.0-groovy-2.3') {
exclude(group: 'org.codehaus.groovy')
}
testCompile 'ch.qos.logback:logback-classic:1.1.3'
}

View File

@@ -0,0 +1,55 @@
package io.codearte.accurest.stubrunner.spring.cloud;
import java.util.HashMap;
import java.util.Map;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* Maps Ivy based ids to service Ids. You might want to name the service you're calling
* in another way than artifact id. If that's the case then this class should be used
* to change do the proper mapping.
*
* Just provide in your properties file for example:
*
* stubrunner.stubs.idsToServiceIds:
* ivyNotation: someValueInsideYourCode
* fraudDetectionServer: someNameThatShouldMapFraudDetectionServer
*
* @author Marcin Grzejszczak
*/
@ConfigurationProperties("stubrunner.stubs")
public class StubMapperProperties {
/**
* Mapping of Ivy notation based ids to serviceIds
* inside your application
*
* Example
*
* "a:b" -> "myService"
* "artifactId" -> "myOtherService"
*/
private Map<String, String> idsToServiceIds = new HashMap<>();
public Map<String, String> getIdsToServiceIds() {
return this.idsToServiceIds;
}
public void setIdsToServiceIds(Map<String, String> idsToServiceIds) {
this.idsToServiceIds = idsToServiceIds;
}
public String fromIvyNotationToId(String ivyNotation) {
return idsToServiceIds.get(ivyNotation);
}
public String fromServiceIdToIvyNotation(String serviceId) {
for (Map.Entry<String, String> entry : idsToServiceIds.entrySet()) {
if (entry.getValue().equals(serviceId)) {
return entry.getKey();
}
}
return null;
}
}

View File

@@ -0,0 +1,72 @@
package io.codearte.accurest.stubrunner.spring.cloud;
import java.net.URI;
import java.net.URL;
import java.util.Collections;
import java.util.List;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import io.codearte.accurest.stubrunner.RunningStubs;
import io.codearte.accurest.stubrunner.StubFinder;
import io.codearte.accurest.stubrunner.util.StringUtils;
/**
* Custom version of {@link DiscoveryClient} that tries to find an instance
* in one of the started WireMock servers
*
* @author Marcin Grzejszczak
*/
public class StubRunnerDiscoveryClient implements DiscoveryClient {
private final DiscoveryClient delegate;
private final StubFinder stubFinder;
private final StubMapperProperties stubMapperProperties;
public StubRunnerDiscoveryClient(DiscoveryClient delegate, StubFinder stubFinder,
StubMapperProperties stubMapperProperties) {
this.delegate = delegate;
this.stubFinder = stubFinder;
this.stubMapperProperties = stubMapperProperties;
}
@Override
public String description() {
return delegate.description();
}
@Override
public ServiceInstance getLocalServiceInstance() {
return delegate.getLocalServiceInstance();
}
@Override
public List<ServiceInstance> getInstances(String serviceId) {
String ivyNotation = stubMapperProperties.fromServiceIdToIvyNotation(serviceId);
String serviceToFind = StringUtils.hasText(ivyNotation) ? ivyNotation : serviceId;
URL stubUrl = stubFinder.findStubUrl(serviceToFind);
if (stubUrl == null) {
return delegate.getInstances(serviceId);
}
return Collections.<ServiceInstance>singletonList(
new StubRunnerServiceInstance(serviceId, stubUrl.getHost(), stubUrl.getPort(), toUri(stubUrl))
);
}
private URI toUri(URL url) {
try {
return url.toURI();
} catch (Exception e) {
return null;
}
}
@Override
public List<String> getServices() {
List<String> services = delegate.getServices();
RunningStubs runningStubs = stubFinder.findAllRunningStubs();
services.addAll(runningStubs.getAllServicesNames());
return services;
}
}

View File

@@ -0,0 +1,57 @@
package io.codearte.accurest.stubrunner.spring.cloud;
import java.net.URI;
import java.util.HashMap;
import java.util.Map;
import org.springframework.cloud.client.ServiceInstance;
/**
* {@link ServiceInstance} with a helpful constructor
*
* @author Marcin Grzejszczak
*/
public class StubRunnerServiceInstance implements ServiceInstance {
private final String serviceId;
private final String host;
private final int port;
private final URI uri;
public StubRunnerServiceInstance(String serviceId, String host, int port, URI uri) {
this.serviceId = serviceId;
this.host = host;
this.port = port;
this.uri = uri;
}
@Override
public String getServiceId() {
return serviceId;
}
@Override
public String getHost() {
return host;
}
@Override
public int getPort() {
return port;
}
@Override
public boolean isSecure() {
return false;
}
@Override
public URI getUri() {
return uri;
}
@Override
public Map<String, String> getMetadata() {
return new HashMap<>();
}
}

View File

@@ -0,0 +1,39 @@
package io.codearte.accurest.stubrunner.spring.cloud;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Primary;
import io.codearte.accurest.stubrunner.StubFinder;
import io.codearte.accurest.stubrunner.spring.StubRunnerConfiguration;
/**
* Wraps {@link DiscoveryClient} in a Stub Runner implementation that tries to find
* a corresponding WireMock server for a searched dependency
*/
@Configuration
@EnableConfigurationProperties
@ConditionalOnClass(DiscoveryClient.class)
@Import(StubRunnerConfiguration.class)
public class StubRunnerSpringCloudAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public StubMapperProperties stubMapperProperties() {
return new StubMapperProperties();
}
@Bean
@Primary
public DiscoveryClient stubRunnerDiscoveryClient(DiscoveryClient discoveryClient,
StubFinder stubFinder,
StubMapperProperties stubMapperProperties) {
return new StubRunnerDiscoveryClient(discoveryClient, stubFinder, stubMapperProperties);
}
}

View File

@@ -0,0 +1,17 @@
package io.codearte.accurest.stubrunner.spring.cloud.ribbon;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.cloud.netflix.ribbon.RibbonAutoConfiguration;
import org.springframework.cloud.netflix.ribbon.RibbonClients;
import org.springframework.context.annotation.Configuration;
import com.netflix.loadbalancer.ServerList;
@Configuration
@ConditionalOnClass(ServerList.class)
@AutoConfigureAfter(RibbonAutoConfiguration.class)
@RibbonClients(defaultConfiguration = StubRunnerRibbonConfiguration.class)
public class StubRunnerRibbonAutoConfiguration {
}

View File

@@ -0,0 +1,94 @@
package io.codearte.accurest.stubrunner.spring.cloud.ribbon;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.Server;
import com.netflix.loadbalancer.ServerList;
import io.codearte.accurest.stubrunner.RunningStubs;
import io.codearte.accurest.stubrunner.StubConfiguration;
import io.codearte.accurest.stubrunner.StubFinder;
import io.codearte.accurest.stubrunner.spring.cloud.StubMapperProperties;
import io.codearte.accurest.stubrunner.util.StringUtils;
/**
* Ribbon AutoConfiguration that manipulates the service id to make the service
* be picked from the list of available WireMock instance if one is available.
*
* @author Marcin Grzejszczak
*/
@Configuration
public class StubRunnerRibbonConfiguration {
@Bean
@Primary
@SuppressWarnings("unchecked")
public ServerList<?> stubRunnerRibbonServerList(final ServerList<?> serverList,
StubFinder stubFinder,
final StubMapperProperties stubMapperProperties,
IClientConfig clientConfig) {
String serviceName = clientConfig.getClientName();
String mappedServiceName = StringUtils
.hasText(stubMapperProperties.fromServiceIdToIvyNotation(serviceName)) ?
stubMapperProperties.fromServiceIdToIvyNotation(serviceName) : serviceName;
RunningStubs runningStubs = stubFinder.findAllRunningStubs();
final Map.Entry<StubConfiguration, Integer> entry = runningStubs.getEntry(mappedServiceName);
final Collection servers = new ArrayList<Server>();
if (entry != null) {
servers.add(new Server("localhost", entry.getValue()) {
@Override
public MetaInfo getMetaInfo() {
return new MetaInfo() {
@Override
public String getAppName() {
return stubMapperProperties.fromIvyNotationToId(entry.getKey().toColonSeparatedDependencyNotation());
}
@Override
public String getServerGroup() {
return null;
}
@Override
public String getServiceIdForDiscovery() {
return stubMapperProperties.fromIvyNotationToId(entry.getKey().getArtifactId());
}
@Override
public String getInstanceId() {
return stubMapperProperties.fromIvyNotationToId(entry.getKey().getArtifactId());
}
};
}
});
}
return new ServerList() {
@Override
public List<?> getInitialListOfServers() {
List combinedList = new ArrayList<>();
combinedList.addAll(servers);
combinedList.addAll(serverList.getInitialListOfServers());
return combinedList;
}
@Override
public List<?> getUpdatedListOfServers() {
List combinedList = new ArrayList<>();
combinedList.addAll(servers);
combinedList.addAll(serverList.getUpdatedListOfServers());
return combinedList;
}
};
}
}

View File

@@ -0,0 +1,4 @@
# Auto Configuration
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
io.codearte.accurest.stubrunner.spring.cloud.StubRunnerSpringCloudAutoConfiguration,\
io.codearte.accurest.stubrunner.spring.cloud.ribbon.StubRunnerRibbonAutoConfiguration

View File

@@ -0,0 +1,58 @@
package io.codearte.accurest.stubrunner.spring.cloud
import io.codearte.accurest.stubrunner.StubFinder
import org.apache.curator.test.TestingServer
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
import org.springframework.boot.test.SpringApplicationContextLoader
import org.springframework.boot.test.WebIntegrationTest
import org.springframework.cloud.client.discovery.EnableDiscoveryClient
import org.springframework.cloud.client.loadbalancer.LoadBalanced
import org.springframework.cloud.zookeeper.discovery.ZookeeperServiceDiscovery
import org.springframework.context.ConfigurableApplicationContext
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.test.context.ContextConfiguration
import org.springframework.web.client.RestTemplate
import spock.lang.AutoCleanup
import spock.lang.Shared
import spock.lang.Specification
/**
* @author Marcin Grzejszczak
*/
@ContextConfiguration(classes = Config, loader = SpringApplicationContextLoader)
@WebIntegrationTest(randomPort = true)
class StubRunnerSpringCloudAutoConfigurationSpec extends Specification {
@Autowired StubFinder stubFinder
@Autowired @LoadBalanced RestTemplate restTemplate
@Autowired ZookeeperServiceDiscovery zookeeperServiceDiscovery
@Autowired ConfigurableApplicationContext applicationContext
@Shared @AutoCleanup TestingServer testingServer = new TestingServer(2181)
def 'should make service discovery work'() {
expect: 'WireMocks are running'
"${stubFinder.findStubUrl('loanIssuance').toString()}/name".toURL().text == 'loanIssuance'
"${stubFinder.findStubUrl('fraudDetectionServer').toString()}/name".toURL().text == 'fraudDetectionServer'
and: 'Stubs can be reached via load service discovery'
restTemplate.getForObject('http://loanIssuance/name', String) == 'loanIssuance'
restTemplate.getForObject('http://someNameThatShouldMapFraudDetectionServer/name', String) == 'fraudDetectionServer'
}
def cleanup() {
zookeeperServiceDiscovery.serviceDiscovery.close()
applicationContext.close()
}
@Configuration
@EnableAutoConfiguration
@EnableDiscoveryClient
static class Config {
@Bean
@LoadBalanced
RestTemplate restTemplate() {
return new RestTemplate()
}
}
}

View File

@@ -0,0 +1,6 @@
stubrunner.stubs.repository.root: classpath:m2repo
stubrunner.stubs.ids: io.codearte.accurest.stubs:loanIssuance,io.codearte.accurest.stubs:fraudDetectionServer
stubrunner.stubs.idsToServiceIds:
ivyNotation: someValueInsideYourCode
fraudDetectionServer: someNameThatShouldMapFraudDetectionServer

View File

@@ -0,0 +1,14 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are assigned the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
</configuration>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>io.codearte.accurest.stubs</groupId>
<artifactId>fraudDetectionServer-stubs</artifactId>
<version>0.0.1-SNAPSHOT</version>
</project>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<metadata>
<groupId>io.codearte.accurest.stubs</groupId>
<artifactId>fraudDetectionServer-stubs</artifactId>
<version>0.0.1-SNAPSHOT</version>
<versioning>
<snapshot>
<localCopy>true</localCopy>
</snapshot>
<lastUpdated>20160326150924</lastUpdated>
</versioning>
</metadata>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<metadata>
<groupId>io.codearte.accurest.stubs</groupId>
<artifactId>fraudDetectionServer-stubs</artifactId>
<versioning>
<versions>
<version>0.0.1-SNAPSHOT</version>
</versions>
<lastUpdated>20160326150924</lastUpdated>
</versioning>
</metadata>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>io.codearte.accurest.stubs</groupId>
<artifactId>loanIssuance-stubs</artifactId>
<version>0.0.1-SNAPSHOT</version>
</project>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<metadata>
<groupId>io.codearte.accurest.stubs</groupId>
<artifactId>loanIssuance-stubs</artifactId>
<version>0.0.1-SNAPSHOT</version>
<versioning>
<snapshot>
<localCopy>true</localCopy>
</snapshot>
<lastUpdated>20160326150924</lastUpdated>
</versioning>
</metadata>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<metadata>
<groupId>io.codearte.accurest.stubs</groupId>
<artifactId>loanIssuance-stubs</artifactId>
<versioning>
<versions>
<version>0.0.1-SNAPSHOT</version>
</versions>
<lastUpdated>20160326150924</lastUpdated>
</versioning>
</metadata>

View File

@@ -32,5 +32,5 @@ for the following configuration file:
```
stubrunner.stubs.repository.root: classpath:m2repo
stubrunner.stubs: io.codearte.accurest.stubs:loanIssuance,io.codearte.accurest.stubs:fraudDetectionServer
stubrunner.stubs.ids: io.codearte.accurest.stubs:loanIssuance,io.codearte.accurest.stubs:fraudDetectionServer
```

View File

@@ -13,6 +13,8 @@ dependencies {
testCompile 'org.objenesis:objenesis:2.1'
testCompile 'org.springframework.boot:spring-boot-starter:1.3.3.RELEASE'
testCompile 'org.springframework.boot:spring-boot-starter-test:1.3.3.RELEASE'
testCompile 'org.spockframework:spock-spring:1.0-groovy-2.3'
testCompile('org.spockframework:spock-spring:1.0-groovy-2.3') {
exclude(group: 'org.codehaus.groovy')
}
testCompile 'ch.qos.logback:logback-classic:1.1.3'
}

View File

@@ -41,7 +41,7 @@ public class StubRunnerConfiguration {
@Value("${stubrunner.stubs.repository.root:}") Resource stubRepositoryRoot,
@Value("${stubrunner.stubs.classifier:stubs}") String stubsSuffix,
@Value("${stubrunner.work-offline:false}") boolean workOffline,
@Value("${stubrunner.stubs:}") String stubs) throws IOException {
@Value("${stubrunner.stubs.ids:}") String stubs) throws IOException {
StubRunnerOptions stubRunnerOptions = new StubRunnerOptions(minPortValue, maxPortValue, uriStringOrEmpty(stubRepositoryRoot),
stubRepositoryRoot == null || workOffline, stubsSuffix);
Set<StubConfiguration> dependencies = StubsParser.fromString(stubs, stubsSuffix);

View File

@@ -1,2 +1,2 @@
stubrunner.stubs.repository.root: classpath:m2repo
stubrunner.stubs: io.codearte.accurest.stubs:loanIssuance,io.codearte.accurest.stubs:fraudDetectionServer
stubrunner.stubs.ids: io.codearte.accurest.stubs:loanIssuance,io.codearte.accurest.stubs:fraudDetectionServer

View File

@@ -9,7 +9,6 @@ dependencies {
compile "com.github.tomakehurst:wiremock:$wiremockVersion"
compile 'javax.servlet:javax.servlet-api:3.1.0'
compile 'args4j:args4j:2.32'
compile 'com.nurkiewicz.asyncretry:asyncretry-jdk7:0.0.6'
if (rootProject.hasProperty("fatJar")) runtime 'ch.qos.logback:logback-classic:1.1.3'

View File

@@ -23,21 +23,25 @@ class RunningStubs {
}
Integer getPort(String artifactId) {
return getEntry(artifactId)?.value
}
Map.Entry<StubConfiguration, Integer> getEntry(String artifactId) {
def strings = artifactId.split(':')
if (strings.length == 1) {
return namesAndPorts.entrySet().find {
it.key.artifactId == artifactId
}?.value
}
} else if(strings.length == 2) {
return namesAndPorts.entrySet().find {
it.key.groupId == strings[0] && it.key.artifactId == strings[1]
}?.value
}
}
return namesAndPorts.entrySet().find {
it.key.groupId == strings[0] &&
it.key.artifactId == strings[1] &&
it.key.classifier == strings[2]
}?.value
}
}
Integer getPort(String groupId, String artifactId) {
@@ -47,21 +51,7 @@ class RunningStubs {
}
boolean isPresent(String artifactId) {
def strings = artifactId.split(':')
if (strings.length == 1) {
return namesAndPorts.entrySet().find {
it.key.artifactId == artifactId
}
} else if(strings.length == 2) {
return namesAndPorts.entrySet().find {
it.key.groupId == strings[0] && it.key.artifactId == strings[1]
}
}
return namesAndPorts.entrySet().find {
it.key.groupId == strings[0] &&
it.key.artifactId == strings[1] &&
it.key.classifier == strings[2]
}
return getEntry(artifactId)
}
boolean isPresent(String groupId, String artifactId) {
@@ -70,6 +60,14 @@ class RunningStubs {
}
}
Set<StubConfiguration> getAllServices() {
return namesAndPorts.keySet()
}
Set<String> getAllServicesNames() {
return namesAndPorts.keySet().collect { it.artifactId } as Set
}
@Override
String toString() {
return namesAndPorts.collect {