├── gradle.properties ├── .travis.yml ├── src ├── main │ └── java │ │ └── de │ │ └── otto │ │ └── edison │ │ └── discovery │ │ ├── DiscoveryService.java │ │ ├── marathon │ │ ├── AppIdParser.java │ │ ├── MarathonAppInfo.java │ │ ├── DefaultAppIdParser.java │ │ ├── MarathonConfiguration.java │ │ ├── HttpService.java │ │ └── MarathonDiscoveryService.java │ │ ├── UriTemplate.java │ │ ├── ServiceUrlFactory.java │ │ ├── ServiceUrl.java │ │ ├── LinkRelationType.java │ │ ├── NodeInfo.java │ │ ├── ClusterInfo.java │ │ └── DefaultServiceUrlFactory.java ├── test │ ├── java │ │ └── de │ │ │ └── otto │ │ │ └── edison │ │ │ └── discovery │ │ │ ├── testsupport │ │ │ ├── dsl │ │ │ │ ├── When.java │ │ │ │ ├── Given.java │ │ │ │ └── Then.java │ │ │ ├── applicationdriver │ │ │ │ └── SpringTestBase.java │ │ │ ├── TestServer.java │ │ │ └── MarathonStubController.java │ │ │ ├── acceptance │ │ │ ├── api │ │ │ │ └── DiscoveryApi.java │ │ │ └── DiscoveryServiceAcceptanceTest.java │ │ │ ├── marathon │ │ │ └── DefaultAppIdParserTest.java │ │ │ └── DefaultServiceUrlFactoryTest.java │ └── resources │ │ ├── application.properties │ │ └── marathon │ │ ├── marathon-shoppingcart-ci-response.json │ │ ├── marathon-shoppingcart-live-response.json │ │ └── marathon-apps-response.json └── example │ ├── java │ └── de │ │ └── otto │ │ └── edison │ │ └── discovery │ │ ├── ExampleApp.java │ │ └── ExampleClient.java │ └── resources │ └── application.properties ├── .gitignore ├── dependencies.gradle ├── README.md └── LICENSE /gradle.properties: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT THIS! SET PROPERTIES IN YOUR LOCAL .gradle DIR! 2 | sonatypeUsername= 3 | sonatypePassword= 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - oraclejdk8 4 | install: /bin/true # skip gradle assemble 5 | script: gradle check 6 | -------------------------------------------------------------------------------- /src/main/java/de/otto/edison/discovery/DiscoveryService.java: -------------------------------------------------------------------------------- 1 | package de.otto.edison.discovery; 2 | 3 | import java.util.List; 4 | 5 | public interface DiscoveryService { 6 | 7 | List discover(); 8 | 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | 3 | # Package Files # 4 | *.jar 5 | 6 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 7 | hs_err_pid* 8 | *.iml 9 | *.ipr 10 | *.iws 11 | build 12 | .gradle/ 13 | out/ 14 | -------------------------------------------------------------------------------- /src/test/java/de/otto/edison/discovery/testsupport/dsl/When.java: -------------------------------------------------------------------------------- 1 | package de.otto.edison.discovery.testsupport.dsl; 2 | 3 | public class When { 4 | public static final When INSTANCE = new When(); 5 | 6 | public static void when(When... actions) {} 7 | public static When and(When actions, When... more) { return When.INSTANCE; } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/de/otto/edison/discovery/marathon/AppIdParser.java: -------------------------------------------------------------------------------- 1 | package de.otto.edison.discovery.marathon; 2 | 3 | import java.util.Map; 4 | 5 | /** 6 | * Created by guido on 10.03.16. 7 | */ 8 | public interface AppIdParser { 9 | 10 | Map clusterAttrOf(String appId); 11 | 12 | enum ClusterAttr {ENV, GROUP, SERVICE, COLOR}; 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/test/java/de/otto/edison/discovery/testsupport/dsl/Given.java: -------------------------------------------------------------------------------- 1 | package de.otto.edison.discovery.testsupport.dsl; 2 | 3 | public class Given { 4 | public static final Given INSTANCE = new Given(); 5 | 6 | public static void given(final Given... givenStuff) {} 7 | 8 | public static Given and(final Given givenStuff, final Given... moreGivenStuff) { return INSTANCE; } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/test/java/de/otto/edison/discovery/testsupport/applicationdriver/SpringTestBase.java: -------------------------------------------------------------------------------- 1 | package de.otto.edison.discovery.testsupport.applicationdriver; 2 | 3 | import de.otto.edison.discovery.testsupport.TestServer; 4 | import org.springframework.context.ApplicationContext; 5 | 6 | public class SpringTestBase { 7 | 8 | static { 9 | TestServer.main(new String[0]); 10 | } 11 | 12 | public static ApplicationContext applicationContext() { 13 | return TestServer.applicationContext(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/test/java/de/otto/edison/discovery/testsupport/dsl/Then.java: -------------------------------------------------------------------------------- 1 | package de.otto.edison.discovery.testsupport.dsl; 2 | 3 | import org.hamcrest.Matcher; 4 | import org.hamcrest.MatcherAssert; 5 | 6 | public class Then { 7 | 8 | public static final Then INSTANCE = new Then(); 9 | 10 | public static void then(final Then... thens) {} 11 | 12 | public static Then and(final Then then, final Then... more) { return Then.INSTANCE; } 13 | 14 | public static Then assertThat(T actual, Matcher matcher) { 15 | MatcherAssert.assertThat(actual, matcher); 16 | return INSTANCE; 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/de/otto/edison/discovery/UriTemplate.java: -------------------------------------------------------------------------------- 1 | package de.otto.edison.discovery; 2 | 3 | import java.util.Map; 4 | 5 | public class UriTemplate { 6 | private final String template; 7 | 8 | public UriTemplate(final String template) { 9 | this.template = template; 10 | } 11 | 12 | public String expand(final Map parameters) { 13 | String uri = template; 14 | for (String s : parameters.keySet()) { 15 | uri = uri.replaceAll("\\{" + s + "\\}", parameters.get(s)); 16 | } 17 | return uri; 18 | } 19 | 20 | @Override 21 | public String toString() { 22 | return template; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/example/java/de/otto/edison/discovery/ExampleApp.java: -------------------------------------------------------------------------------- 1 | package de.otto.edison.discovery; 2 | 3 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 4 | import org.springframework.context.ApplicationContext; 5 | import org.springframework.context.annotation.ComponentScan; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | import static org.springframework.boot.SpringApplication.run; 9 | 10 | @Configuration 11 | @EnableAutoConfiguration 12 | @ComponentScan(basePackages = {"de.otto.edison"}) 13 | public class ExampleApp { 14 | 15 | private static ApplicationContext ctx; 16 | 17 | public static ApplicationContext applicationContext() { 18 | return ctx; 19 | } 20 | 21 | public static void main(String[] args) { 22 | ctx = run(ExampleApp.class, args); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/test/java/de/otto/edison/discovery/testsupport/TestServer.java: -------------------------------------------------------------------------------- 1 | package de.otto.edison.discovery.testsupport; 2 | 3 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 4 | import org.springframework.context.ApplicationContext; 5 | import org.springframework.context.annotation.ComponentScan; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | import static org.springframework.boot.SpringApplication.run; 9 | 10 | @Configuration 11 | @EnableAutoConfiguration 12 | @ComponentScan(basePackages = {"de.otto.edison"}) 13 | public class TestServer { 14 | 15 | private static ApplicationContext ctx; 16 | 17 | public static ApplicationContext applicationContext() { 18 | return ctx; 19 | } 20 | 21 | public static void main(String[] args) { 22 | ctx = run(TestServer.class, args); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/test/java/de/otto/edison/discovery/acceptance/api/DiscoveryApi.java: -------------------------------------------------------------------------------- 1 | package de.otto.edison.discovery.acceptance.api; 2 | 3 | import de.otto.edison.discovery.ClusterInfo; 4 | import de.otto.edison.discovery.DiscoveryService; 5 | import de.otto.edison.discovery.marathon.MarathonDiscoveryService; 6 | import de.otto.edison.discovery.testsupport.applicationdriver.SpringTestBase; 7 | import de.otto.edison.discovery.testsupport.dsl.When; 8 | 9 | import java.io.IOException; 10 | import java.util.List; 11 | 12 | public class DiscoveryApi extends SpringTestBase { 13 | 14 | private static List clusters; 15 | 16 | public static When discovery_is_executed() throws IOException { 17 | final DiscoveryService discoveryService = applicationContext().getBean(MarathonDiscoveryService.class); 18 | clusters = discoveryService.discover(); 19 | return When.INSTANCE; 20 | } 21 | 22 | public static List the_cluster_info() { 23 | return clusters; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/de/otto/edison/discovery/ServiceUrlFactory.java: -------------------------------------------------------------------------------- 1 | package de.otto.edison.discovery; 2 | 3 | import de.otto.edison.annotations.Beta; 4 | 5 | import java.util.List; 6 | 7 | /** 8 | * A factory used to generate the URLs of a discovered cluster of services. 9 | * 10 | * Created by guido on 09.03.16. 11 | */ 12 | @Beta 13 | public interface ServiceUrlFactory { 14 | 15 | /** 16 | * Generates one or more ServiceUrl instances for the given parameters. 17 | * 18 | * @param appId the application identifier 19 | * @param service the name of the service 20 | * @param group the name of the service group 21 | * @param environment the environment / stage of the deployed service. 22 | * @return List of ServiceUrls 23 | */ 24 | public List getServiceUrls(String appId, 25 | String service, 26 | String group, 27 | String environment); 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/test/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.application.name=test 2 | 3 | # context + port of the application 4 | server.context-path=/test 5 | server.port=8080 6 | 7 | # context of the management endpoints like metrics, health, and so on 8 | management.context-path=/internal 9 | 10 | # Jackson configuration for JSON serialization 11 | spring.jackson.serialization.INDENT_OUTPUT=true 12 | # Without timezone (time will be rendered in GMT) 13 | #spring.jackson.date-format=com.fasterxml.jackson.databind.util.ISO8601DateFormat 14 | spring.jackson.date-format=yyyy-MM-dd'T'hh:mm:ss.sssZ 15 | 16 | edison.gracefulshutdown.enabled=false 17 | 18 | edison.servicediscovery.marathon.servers=http://localhost:8080/test/marathon/stub 19 | edison.servicediscovery.marathon.username= 20 | edison.servicediscovery.marathon.password= 21 | edison.servicediscovery.serviceUriTemplate=http://{env}.example.org/{group}-{service},live=https://www.example.org/{group}-{service} 22 | edison.servicediscovery.internalUriTemplate=http://{service}.{group}.{env}.example.org/{group}-{service} 23 | -------------------------------------------------------------------------------- /src/example/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.application.name=example 2 | 3 | # context + port of the application 4 | server.context-path=/example 5 | server.port=8080 6 | 7 | # context of the management endpoints like metrics, health, and so on 8 | management.context-path=/internal 9 | 10 | # Jackson configuration for JSON serialization 11 | spring.jackson.serialization.INDENT_OUTPUT=true 12 | # Without timezone (time will be rendered in GMT) 13 | #spring.jackson.date-format=com.fasterxml.jackson.databind.util.ISO8601DateFormat 14 | spring.jackson.date-format=yyyy-MM-dd'T'hh:mm:ss.sssZ 15 | 16 | # Configuration of the Marathon server: 17 | edison.servicediscovery.marathon.servers=http://marathon.develop.example.org,http://marathon.live.example.org 18 | edison.servicediscovery.marathon.username= 19 | edison.servicediscovery.marathon.password= 20 | 21 | # configuration of the patterns used to build URLs for the discovered services: 22 | edison.servicediscovery.serviceUriTemplate=http://{env}.example.org/{group}-{service},live=https://www.example.org/{service} 23 | edison.servicediscovery.internalUriTemplate=http://{service}.{group}.{env}.example.org/{group}-{service} 24 | -------------------------------------------------------------------------------- /dependencies.gradle: -------------------------------------------------------------------------------- 1 | ext.springBootVersion="1.3.3.RELEASE" 2 | ext.edisonVersion="0.52.2" 3 | ext.libs = [ 4 | edisonCore: [ 5 | "de.otto.edison:edison-core:" + edisonVersion], 6 | jackson: [ 7 | "com.fasterxml.jackson.core:jackson-databind:2.7.2" 8 | ], 9 | springBoot: [ 10 | "org.springframework.boot:spring-boot:" + springBootVersion, 11 | "org.springframework.boot:spring-boot-autoconfigure:" + springBootVersion], 12 | springBootWeb: [ 13 | "org.springframework.boot:spring-boot-starter-web:" + springBootVersion, 14 | "org.springframework.boot:spring-boot-starter-actuator:" + springBootVersion], 15 | asyncHttp: 16 | "com.ning:async-http-client:1.9.33", 17 | jcip: [ 18 | "net.jcip:jcip-annotations:1.0"], 19 | logging: [ 20 | "ch.qos.logback:logback-classic:1.1.6"], 21 | testUnit: [ 22 | "org.testng:testng:6.9.10", 23 | "org.hamcrest:hamcrest-core:1.3", 24 | "org.hamcrest:hamcrest-library:1.3", 25 | "org.mockito:mockito-core:1.10.19", 26 | "org.springframework:spring-test:4.2.5.RELEASE" 27 | ] 28 | ] 29 | -------------------------------------------------------------------------------- /src/test/java/de/otto/edison/discovery/testsupport/MarathonStubController.java: -------------------------------------------------------------------------------- 1 | package de.otto.edison.discovery.testsupport; 2 | 3 | import org.springframework.core.io.ClassPathResource; 4 | import org.springframework.core.io.Resource; 5 | import org.springframework.web.bind.annotation.RequestMapping; 6 | import org.springframework.web.bind.annotation.RestController; 7 | 8 | import java.io.IOException; 9 | 10 | @RestController 11 | public class MarathonStubController { 12 | 13 | @RequestMapping( 14 | value = "/marathon/stub/v2/apps", 15 | produces = "application/json" 16 | ) 17 | public Resource getApps() throws IOException { 18 | return new ClassPathResource("marathon/marathon-apps-response.json"); 19 | } 20 | 21 | @RequestMapping( 22 | value = "/marathon/stub/v2/apps/ci/order/shoppingcart", 23 | produces = "application/json" 24 | ) 25 | public Resource getShoppingcartCi() throws IOException { 26 | return new ClassPathResource("marathon/marathon-shoppingcart-ci-response.json"); 27 | } 28 | 29 | @RequestMapping( 30 | value = "/marathon/stub/v2/apps/live/order/shoppingcart", 31 | produces = "application/json" 32 | ) 33 | public Resource getShoppingCartLive() throws IOException { 34 | return new ClassPathResource("marathon/marathon-shoppingcart-live-response.json"); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/example/java/de/otto/edison/discovery/ExampleClient.java: -------------------------------------------------------------------------------- 1 | package de.otto.edison.discovery; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.stereotype.Component; 7 | 8 | import javax.annotation.PostConstruct; 9 | import java.util.Optional; 10 | 11 | import static java.util.Comparator.*; 12 | 13 | @Component 14 | public class ExampleClient { 15 | 16 | private static final Logger LOG = LoggerFactory.getLogger(ExampleClient.class); 17 | 18 | @Autowired 19 | private DiscoveryService discoveryService; 20 | 21 | @PostConstruct 22 | public void logDiscoveredServices() { 23 | discoveryService.discover().stream().sorted(comparing(ClusterInfo::getId)).forEach(cluster->{ 24 | final String internalLink = internalHrefOf(cluster); 25 | LOG.info("id={}, env={}, group={}, service={}, href={}", 26 | cluster.getId(), 27 | cluster.getEnvironment(), 28 | cluster.getGroup(), 29 | cluster.getServiceName(), 30 | internalLink); 31 | }); 32 | } 33 | 34 | private String internalHrefOf(ClusterInfo cluster) { 35 | final Optional serviceUrl = cluster.getServiceUrls() 36 | .stream() 37 | .filter(s -> s.getRel().equals(LinkRelationType.REL_SERVICE_INTERNAL)) 38 | .findAny(); 39 | return serviceUrl.isPresent() ? serviceUrl.get().getHref(): ""; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/de/otto/edison/discovery/marathon/MarathonAppInfo.java: -------------------------------------------------------------------------------- 1 | package de.otto.edison.discovery.marathon; 2 | 3 | import de.otto.edison.annotations.Beta; 4 | import net.jcip.annotations.Immutable; 5 | 6 | @Immutable 7 | @Beta 8 | class MarathonAppInfo { 9 | 10 | public final int currentInstances; 11 | public final int maxInstances; 12 | public final int minInstances; 13 | 14 | public MarathonAppInfo(final int currentInstances, 15 | final int maxInstances, 16 | final int minInstances) { 17 | this.currentInstances = currentInstances; 18 | this.maxInstances = maxInstances; 19 | this.minInstances = minInstances; 20 | } 21 | 22 | @Override 23 | public boolean equals(Object o) { 24 | if (this == o) return true; 25 | if (o == null || getClass() != o.getClass()) return false; 26 | 27 | MarathonAppInfo that = (MarathonAppInfo) o; 28 | 29 | if (currentInstances != that.currentInstances) return false; 30 | if (maxInstances != that.maxInstances) return false; 31 | if (minInstances != that.minInstances) return false; 32 | 33 | return true; 34 | } 35 | 36 | @Override 37 | public int hashCode() { 38 | int result = currentInstances; 39 | result = 31 * result + maxInstances; 40 | result = 31 * result + minInstances; 41 | return result; 42 | } 43 | 44 | @Override 45 | public String toString() { 46 | return "MarathonAppInfo{" + 47 | "currentInstances=" + currentInstances + 48 | ", maxInstances=" + maxInstances + 49 | ", minInstances=" + minInstances + 50 | '}'; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/de/otto/edison/discovery/marathon/DefaultAppIdParser.java: -------------------------------------------------------------------------------- 1 | package de.otto.edison.discovery.marathon; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | import java.util.StringTokenizer; 9 | 10 | import static de.otto.edison.discovery.marathon.AppIdParser.ClusterAttr.*; 11 | 12 | /** 13 | * Created by guido on 10.03.16. 14 | */ 15 | public class DefaultAppIdParser implements AppIdParser { 16 | 17 | private static final Logger LOG = LoggerFactory.getLogger(DefaultAppIdParser.class); 18 | 19 | @Override 20 | public Map clusterAttrOf(final String appId) { 21 | final Map data = new HashMap<>(); 22 | try { 23 | StringTokenizer tokenizer = new StringTokenizer(appId, "/", false); 24 | data.put(ENV, tokenizer.nextToken()); 25 | 26 | final String group = tokenizer.nextToken(); 27 | data.put(GROUP, group); 28 | 29 | if (tokenizer.hasMoreTokens()) { 30 | String token = tokenizer.nextToken(); 31 | if (token.equals("blu") || token.equals("grn")) { 32 | data.put(SERVICE, group); 33 | data.put(COLOR, token); 34 | } else { 35 | data.put(SERVICE, token); 36 | data.put(COLOR, tokenizer.hasMoreTokens() ? tokenizer.nextToken() : ""); 37 | } 38 | } else { 39 | data.put(SERVICE, group); 40 | data.put(COLOR, ""); 41 | } 42 | } catch (final Exception e) { 43 | LOG.warn("Unable to parse appId '" + appId + "': " + e.getMessage()); 44 | } 45 | return data; 46 | } 47 | 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/de/otto/edison/discovery/ServiceUrl.java: -------------------------------------------------------------------------------- 1 | package de.otto.edison.discovery; 2 | 3 | import de.otto.edison.annotations.Beta; 4 | 5 | /** 6 | * A link to a service, consisting of a href, title and link-relation type. 7 | * 8 | * Created by guido on 08.03.16. 9 | */ 10 | @Beta 11 | public class ServiceUrl { 12 | 13 | private final String href; 14 | private final String title; 15 | private final LinkRelationType rel; 16 | 17 | public ServiceUrl(final String href, 18 | final String title, 19 | final LinkRelationType rel) { 20 | this.href = href; 21 | this.title = title; 22 | this.rel = rel; 23 | } 24 | 25 | public String getHref() { 26 | return href; 27 | } 28 | 29 | public String getTitle() { 30 | return title; 31 | } 32 | 33 | public LinkRelationType getRel() { 34 | return rel; 35 | } 36 | 37 | @Override 38 | public boolean equals(Object o) { 39 | if (this == o) return true; 40 | if (o == null || getClass() != o.getClass()) return false; 41 | 42 | ServiceUrl that = (ServiceUrl) o; 43 | 44 | if (href != null ? !href.equals(that.href) : that.href != null) return false; 45 | if (title != null ? !title.equals(that.title) : that.title != null) return false; 46 | return rel == that.rel; 47 | 48 | } 49 | 50 | @Override 51 | public int hashCode() { 52 | int result = href != null ? href.hashCode() : 0; 53 | result = 31 * result + (title != null ? title.hashCode() : 0); 54 | result = 31 * result + (rel != null ? rel.hashCode() : 0); 55 | return result; 56 | } 57 | 58 | @Override 59 | public String toString() { 60 | return "ServiceUrl{" + 61 | "href='" + href + '\'' + 62 | ", title='" + title + '\'' + 63 | ", rel=" + rel + 64 | '}'; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/de/otto/edison/discovery/LinkRelationType.java: -------------------------------------------------------------------------------- 1 | package de.otto.edison.discovery; 2 | 3 | import de.otto.edison.annotations.Beta; 4 | 5 | /** 6 | * Predefined Link-Relation Types used in Edison to identify hyperlinks to services. 7 | * 8 | * Created by guido on 08.03.16. 9 | */ 10 | @Beta 11 | public enum LinkRelationType { 12 | 13 | /** 14 | * Relation type of a link to a Edison service. 15 | * 16 | * Example for a service-url: http://example.org:8080/my-service 17 | */ 18 | REL_SERVICE("http://github.com/otto-de/edison/link-relations/service"), 19 | /** 20 | * Relation type of a link to internal APIs of Edison services. 21 | * 22 | * Example for a service-url: http://internal.live.example.org:8080/my-service/internal 23 | */ 24 | REL_SERVICE_INTERNAL("http://github.com/otto-de/edison/link-relations/service/internal"), 25 | /** 26 | * Relation type of a link to the status page of Edison services. 27 | * 28 | * Example for a service-url: http://internal.live.example.org:8080/my-service/internal/status 29 | */ 30 | REL_SERVICE_STATUS("http://github.com/otto-de/edison/link-relations/service/status"), 31 | /** 32 | * Relation type of a link to service's healthcheck. 33 | * 34 | * Example for a service-url: http://internal.live.example.org:8080/my-service/internal/healthcheck 35 | */ 36 | REL_SERVICE_HEALTH("http://github.com/otto-de/edison/link-relations/service/health"), 37 | /** 38 | * Relation type of a link to single cluster nodes of Edison services. 39 | * 40 | * Example for a service-url: http://10.106.42.42:8080/my-service/internal 41 | */ 42 | REL_SERVICE_NODE("http://github.com/otto-de/edison/link-relations/service/node"); 43 | 44 | private final String rel; 45 | 46 | LinkRelationType(final String rel) { 47 | this.rel = rel; 48 | }; 49 | 50 | @Override 51 | public String toString() { 52 | return rel; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/de/otto/edison/discovery/marathon/MarathonConfiguration.java: -------------------------------------------------------------------------------- 1 | package de.otto.edison.discovery.marathon; 2 | 3 | import de.otto.edison.annotations.Beta; 4 | import de.otto.edison.discovery.DefaultServiceUrlFactory; 5 | import de.otto.edison.discovery.DiscoveryService; 6 | import de.otto.edison.discovery.ServiceUrlFactory; 7 | import org.springframework.beans.factory.annotation.Value; 8 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 9 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 10 | import org.springframework.context.annotation.Bean; 11 | import org.springframework.context.annotation.Configuration; 12 | 13 | import static java.util.Arrays.asList; 14 | 15 | 16 | @Configuration 17 | @ConditionalOnProperty(name = "edison.servicediscovery.marathon.servers") 18 | @Beta 19 | public class MarathonConfiguration { 20 | 21 | private @Value("${edison.servicediscovery.marathon.servers}") String marathonApiUrls; 22 | private @Value("${edison.servicediscovery.marathon.username}") String marathonApiUsername; 23 | private @Value("${edison.servicediscovery.marathon.password}") String marathonApiPassword; 24 | 25 | private @Value("${edison.servicediscovery.serviceUriTemplate}") String serviceUriTemplate; 26 | private @Value("${edison.servicediscovery.internalUriTemplate}") String internalUriTemplate; 27 | 28 | @Bean 29 | @ConditionalOnMissingBean(ServiceUrlFactory.class) 30 | public ServiceUrlFactory serviceUrlFactory() { 31 | return new DefaultServiceUrlFactory(serviceUriTemplate, internalUriTemplate); 32 | } 33 | 34 | @Beta 35 | @ConditionalOnMissingBean(AppIdParser.class) 36 | public AppIdParser appIdParser() { 37 | return new DefaultAppIdParser(); 38 | } 39 | 40 | @Bean 41 | @ConditionalOnMissingBean(DiscoveryService.class) 42 | public DiscoveryService discoveryService() { 43 | return new MarathonDiscoveryService(asList(marathonApiUrls.split(",")), marathonApiUsername, marathonApiPassword, serviceUrlFactory(), appIdParser()); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/de/otto/edison/discovery/NodeInfo.java: -------------------------------------------------------------------------------- 1 | package de.otto.edison.discovery; 2 | 3 | import de.otto.edison.annotations.Beta; 4 | 5 | /** 6 | * Information about a single node in a {@link ClusterInfo cluster}. 7 | * 8 | * Created by guido on 08.03.16. 9 | */ 10 | @Beta 11 | public final class NodeInfo { 12 | private final String id; 13 | private final String host; 14 | private final int port; 15 | private final String href; 16 | 17 | public NodeInfo(final String id, 18 | final String host, 19 | final int port, 20 | final String href) { 21 | this.id = id; 22 | this.host = host; 23 | this.port = port; 24 | this.href = href; 25 | } 26 | 27 | public String getId() { 28 | return id; 29 | } 30 | 31 | public String getHost() { 32 | return host; 33 | } 34 | 35 | public int getPort() { 36 | return port; 37 | } 38 | 39 | public String getHref() { 40 | return href; 41 | } 42 | 43 | @Override 44 | public boolean equals(Object o) { 45 | if (this == o) return true; 46 | if (o == null || getClass() != o.getClass()) return false; 47 | 48 | NodeInfo nodeInfo = (NodeInfo) o; 49 | 50 | if (port != nodeInfo.port) return false; 51 | if (id != null ? !id.equals(nodeInfo.id) : nodeInfo.id != null) return false; 52 | if (host != null ? !host.equals(nodeInfo.host) : nodeInfo.host != null) return false; 53 | return !(href != null ? !href.equals(nodeInfo.href) : nodeInfo.href != null); 54 | 55 | } 56 | 57 | @Override 58 | public int hashCode() { 59 | int result = id != null ? id.hashCode() : 0; 60 | result = 31 * result + (host != null ? host.hashCode() : 0); 61 | result = 31 * result + port; 62 | result = 31 * result + (href != null ? href.hashCode() : 0); 63 | return result; 64 | } 65 | 66 | @Override 67 | public String toString() { 68 | return "NodeInfo{" + 69 | "id='" + id + '\'' + 70 | ", host='" + host + '\'' + 71 | ", port=" + port + 72 | ", href='" + href + '\'' + 73 | '}'; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/test/java/de/otto/edison/discovery/marathon/DefaultAppIdParserTest.java: -------------------------------------------------------------------------------- 1 | package de.otto.edison.discovery.marathon; 2 | 3 | import org.testng.annotations.Test; 4 | 5 | import static de.otto.edison.discovery.marathon.AppIdParser.ClusterAttr.COLOR; 6 | import static de.otto.edison.discovery.marathon.AppIdParser.ClusterAttr.ENV; 7 | import static de.otto.edison.discovery.marathon.AppIdParser.ClusterAttr.GROUP; 8 | import static de.otto.edison.discovery.marathon.AppIdParser.ClusterAttr.SERVICE; 9 | import static org.hamcrest.MatcherAssert.assertThat; 10 | import static org.hamcrest.core.Is.is; 11 | 12 | public class DefaultAppIdParserTest { 13 | 14 | @Test 15 | public void shouldParseAppIdWithoutColor() { 16 | final String appId = "/test/grp/service"; 17 | final DefaultAppIdParser service = new DefaultAppIdParser(); 18 | assertThat(service.clusterAttrOf(appId).get(SERVICE), is("service")); 19 | assertThat(service.clusterAttrOf(appId).get(GROUP), is("grp")); 20 | assertThat(service.clusterAttrOf(appId).get(ENV), is("test")); 21 | assertThat(service.clusterAttrOf(appId).get(COLOR), is("")); 22 | } 23 | 24 | @Test 25 | public void shouldParseAppIdWithColor() { 26 | final String appId = "/test/grp/service/blu"; 27 | final DefaultAppIdParser service = new DefaultAppIdParser(); 28 | assertThat(service.clusterAttrOf(appId).get(SERVICE), is("service")); 29 | assertThat(service.clusterAttrOf(appId).get(GROUP), is("grp")); 30 | assertThat(service.clusterAttrOf(appId).get(ENV), is("test")); 31 | assertThat(service.clusterAttrOf(appId).get(COLOR), is("blu")); 32 | } 33 | 34 | @Test 35 | public void shouldParseAppIdWithoutGroupAndWithColor() { 36 | final String appId = "/test/service/grn"; 37 | final DefaultAppIdParser service = new DefaultAppIdParser(); 38 | assertThat(service.clusterAttrOf(appId).get(SERVICE), is("service")); 39 | assertThat(service.clusterAttrOf(appId).get(GROUP), is("service")); 40 | assertThat(service.clusterAttrOf(appId).get(ENV), is("test")); 41 | assertThat(service.clusterAttrOf(appId).get(COLOR), is("grn")); 42 | } 43 | 44 | @Test 45 | public void shouldParseAppIdWithoutGroupAndWithoutColor() { 46 | final String appId = "/test/service"; 47 | final DefaultAppIdParser service = new DefaultAppIdParser(); 48 | assertThat(service.clusterAttrOf(appId).get(SERVICE), is("service")); 49 | assertThat(service.clusterAttrOf(appId).get(GROUP), is("service")); 50 | assertThat(service.clusterAttrOf(appId).get(ENV), is("test")); 51 | assertThat(service.clusterAttrOf(appId).get(COLOR), is("")); 52 | } 53 | 54 | } -------------------------------------------------------------------------------- /src/main/java/de/otto/edison/discovery/marathon/HttpService.java: -------------------------------------------------------------------------------- 1 | package de.otto.edison.discovery.marathon; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.ning.http.client.AsyncHttpClient; 5 | import com.ning.http.client.Response; 6 | import de.otto.edison.annotations.Beta; 7 | 8 | import java.io.IOException; 9 | import java.util.Base64; 10 | import java.util.Map; 11 | import java.util.concurrent.ExecutionException; 12 | import java.util.concurrent.TimeoutException; 13 | 14 | import static java.nio.charset.StandardCharsets.UTF_8; 15 | import static java.util.concurrent.TimeUnit.MILLISECONDS; 16 | 17 | @Beta 18 | class HttpService { 19 | 20 | private static final long HTTP_CLIENT_TIMEOUT_IN_MS = 1000; 21 | 22 | private final AsyncHttpClient asyncHttpClient = new AsyncHttpClient(); 23 | private final ObjectMapper objectMapper = new ObjectMapper(); 24 | 25 | public Response getJson(final String username, 26 | final String password, 27 | final String url) throws ExecutionException, InterruptedException, IOException, TimeoutException { 28 | final AsyncHttpClient.BoundRequestBuilder requestBuilder = asyncHttpClient 29 | .prepareGet(url); 30 | 31 | if (null != username && null != password && !username.isEmpty() && !password.isEmpty()) { 32 | requestBuilder.setHeader("Authorization", encodeAuth(username, password)); 33 | } 34 | 35 | return requestBuilder 36 | .setHeader("Accept", "application/json") 37 | .execute() 38 | .get(HTTP_CLIENT_TIMEOUT_IN_MS, MILLISECONDS); 39 | } 40 | 41 | public Response getJson(final String url) throws ExecutionException, InterruptedException, IOException, TimeoutException { 42 | return getJson(null, null, url); 43 | } 44 | 45 | public Response putJson(final String username, 46 | final String password, 47 | final String url, 48 | final Map body) throws ExecutionException, InterruptedException, IOException, TimeoutException { 49 | return asyncHttpClient 50 | .preparePut(url) 51 | .setHeader("Authorization", encodeAuth(username, password)) 52 | .setBody(objectMapper.writeValueAsString(body)) 53 | .setHeader("Accept", "application/json") 54 | .execute() 55 | .get(HTTP_CLIENT_TIMEOUT_IN_MS, MILLISECONDS); 56 | } 57 | 58 | private String encodeAuth(final String username, final String password) { 59 | return "Basic " + Base64.getEncoder().encodeToString((username + ':' + password).getBytes(UTF_8)); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Edison Service Discovery 2 | 3 | Some services may have to figure out, which services are running in your system. There are a number of open source 4 | discovery-services like Eureka or Consul. Because at otto.de, services are running in a Mesos infrastructure and 5 | managed using the Marathon framework, we do not need a dedicated discovery-server: the Marathon API contains all 6 | information that is needed to discover running services. 7 | 8 | [![Build Status](https://travis-ci.org/otto-de/edison-servicediscovery.svg)](https://travis-ci.org/otto-de/edison-servicediscovery) 9 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/de.otto.edison/edison-servicediscovery/badge.svg)](https://maven-badges.herokuapp.com/maven-central/de.otto.edison/edison-servicediscovery) 10 | 11 | 12 | ## Environment Properties 13 | 14 | Enable Marathon service discovery by providing the following properties: 15 | 16 | * edison.servicediscovery.marathon.servers: The comma-separated list of marathon server (http://marathon.example.org) urls 17 | * edison.servicediscovery.marathon.username: Optional user used for authentication at the marathon server 18 | * edison.servicediscovery.marathon.password: Optional password used for authentication at the marathon server 19 | 20 | Configure the generation of service URIs: 21 | 22 | * edison.servicediscovery.serviceUriTemplate=http://{env}.example.org/{group}-{service},live=https://www.example.org/{group}-{service} 23 | * edison.servicediscovery.internalUriTemplate=http://{service}.{group}.{env}.example.org/{group}-{service} 24 | 25 | URI templates may contain the following placeholders: 26 | 27 | * service: the name of the service 28 | * group: the group of the service (several service may belong to a group, like, for example, a team or bounded context. 29 | * env: the staging environment of the deployed service. Something like "ci", "prelive" or "live". 30 | 31 | Environment-specific templates can be configured by configuring a comma-separated list of URI templates, where the 32 | first entry is the default template, the following templates are =