diff --git a/pom.xml b/pom.xml index 39316d7d2..9e4be63fc 100644 --- a/pom.xml +++ b/pom.xml @@ -71,6 +71,8 @@ spring-cloud-aws-integration-test docs spring-cloud-aws-samples + spring-cloud-aws-cloudmap + spring-cloud-starter-aws-cloudmap diff --git a/spring-cloud-aws-cloudmap/pom.xml b/spring-cloud-aws-cloudmap/pom.xml new file mode 100644 index 000000000..97f783afc --- /dev/null +++ b/spring-cloud-aws-cloudmap/pom.xml @@ -0,0 +1,64 @@ + + + 4.0.0 + + io.awspring.cloud + spring-cloud-aws + 2.3.1 + + + spring-cloud-aws-cloudmap + Spring Cloud Cloud Map + Spring Cloud AWS Cloud Map + + + + org.springframework + spring-context + + + + org.springframework.cloud + spring-cloud-commons + + + + org.springframework.boot + spring-boot-autoconfigure + + + + org.springframework.cloud + spring-cloud-context + + + + com.amazonaws + aws-java-sdk-servicediscovery + + + + com.amazonaws + aws-java-sdk-ec2 + + + + org.springframework + spring-web + + + + org.springframework + spring-webflux + + + + io.projectreactor.netty + reactor-netty-http + + + + + diff --git a/spring-cloud-aws-cloudmap/src/main/java/org/springframework/cloud/aws/cloudmap/CloudMapUtils.java b/spring-cloud-aws-cloudmap/src/main/java/org/springframework/cloud/aws/cloudmap/CloudMapUtils.java new file mode 100644 index 000000000..80ba64264 --- /dev/null +++ b/spring-cloud-aws-cloudmap/src/main/java/org/springframework/cloud/aws/cloudmap/CloudMapUtils.java @@ -0,0 +1,606 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * 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 + * + * https://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. + */ + +// @checkstyle:off +package org.springframework.cloud.aws.cloudmap; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Random; +import java.util.UUID; +import java.util.stream.Collectors; + +import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; +import com.amazonaws.services.ec2.AmazonEC2; +import com.amazonaws.services.ec2.AmazonEC2ClientBuilder; +import com.amazonaws.services.ec2.model.DescribeSubnetsRequest; +import com.amazonaws.services.ec2.model.Filter; +import com.amazonaws.services.servicediscovery.AWSServiceDiscovery; +import com.amazonaws.services.servicediscovery.model.CreatePrivateDnsNamespaceRequest; +import com.amazonaws.services.servicediscovery.model.CreateServiceRequest; +import com.amazonaws.services.servicediscovery.model.DeregisterInstanceRequest; +import com.amazonaws.services.servicediscovery.model.DiscoverInstancesRequest; +import com.amazonaws.services.servicediscovery.model.DnsConfig; +import com.amazonaws.services.servicediscovery.model.DnsRecord; +import com.amazonaws.services.servicediscovery.model.DuplicateRequestException; +import com.amazonaws.services.servicediscovery.model.GetOperationRequest; +import com.amazonaws.services.servicediscovery.model.HttpInstanceSummary; +import com.amazonaws.services.servicediscovery.model.InvalidInputException; +import com.amazonaws.services.servicediscovery.model.ListNamespacesRequest; +import com.amazonaws.services.servicediscovery.model.ListNamespacesResult; +import com.amazonaws.services.servicediscovery.model.ListServicesRequest; +import com.amazonaws.services.servicediscovery.model.ListServicesResult; +import com.amazonaws.services.servicediscovery.model.NamespaceAlreadyExistsException; +import com.amazonaws.services.servicediscovery.model.NamespaceSummary; +import com.amazonaws.services.servicediscovery.model.Operation; +import com.amazonaws.services.servicediscovery.model.RecordType; +import com.amazonaws.services.servicediscovery.model.RegisterInstanceRequest; +import com.amazonaws.services.servicediscovery.model.ResourceLimitExceededException; +import com.amazonaws.services.servicediscovery.model.ServiceAlreadyExistsException; +import com.amazonaws.services.servicediscovery.model.ServiceFilter; +import com.amazonaws.services.servicediscovery.model.ServiceSummary; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.cloud.aws.cloudmap.discovery.CloudMapServiceInstance; +import org.springframework.cloud.aws.cloudmap.exceptions.CreateNameSpaceException; +import org.springframework.cloud.aws.cloudmap.exceptions.CreateServiceException; +import org.springframework.cloud.aws.cloudmap.exceptions.MaxRetryExceededException; +import org.springframework.cloud.aws.cloudmap.model.discovery.CloudMapDiscoveryProperties; +import org.springframework.cloud.aws.cloudmap.model.registration.CloudMapRegistryProperties; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.core.env.Environment; +import org.springframework.util.StringUtils; +import org.springframework.web.reactive.function.client.WebClient; + +/** + * Uses Fargate Metadata URL to retrieve IPv4 address and VPC ID to register instances to + * cloudmap. + * + * @author Hari Ohm Prasath + * @since 2.3.2 + */ +public class CloudMapUtils { + + /* + * Singleton instance + */ + private static CloudMapUtils cloudMapUtils = null; + + public static final String EC2_METADATA_URL = "http://169.254.169.254/latest/meta-data"; + /* + * AWS VPC ID + */ + private static final String VPC_ID = "VPC_ID"; + /* + * Local IP address + */ + private static final String AWS_INSTANCE_IPV_4 = "AWS_INSTANCE_IPV4"; + /* + * AWS Region + */ + private static final String REGION = "REGION"; + /* + * Logger + */ + private static final Logger LOGGER = LoggerFactory.getLogger(CloudMapUtils.class); + /* + * Namespace status - SUBMITTED + */ + private static final String SUBMITTED = "SUBMITTED"; + /* + * Namespace status - PENDING + */ + private static final String PENDING = "PENDING"; + /* + * Maximum number of polling before returning error + */ + private static final int MAX_POLL = 30; + /* + * Deployment platform environment variable + */ + public final String DEPLOYMENT_PLATFORM = "DEPLOYMENT_PLATFORM"; + + /* + * Deployment platform type ECS + */ + public final String EKS = "EKS"; + + /* + * Metadata URL + */ + public final String ECS_CONTAINER_METADATA_URI_V_4 = "ECS_CONTAINER_METADATA_URI_V4"; + + /* + * Request attributes - NamespaceID + */ + public final String NAMESPACE_ID = "NAMESPACE_ID"; + + /* + * Request attributes - ServiceID + */ + public final String SERVICE_ID = "SERVICE_ID"; + + /* + * Request attributes - ServiceInstanceID + */ + public final String SERVICE_INSTANCE_ID = "SERVICE_INSTANCE_ID"; + + /* + * Request attributes - IP address + */ + public final String IPV_4_ADDRESS = "IPV4_ADDRESS"; + + final ObjectMapper JSON_MAPPER = new ObjectMapper(); + + private final WebClient WEB_CLIENT = WebClient.create(); + + private AmazonEC2 ec2Client; + + /** + * Uses metadata URL to fetch all the required details around IP address and VpcID to + * register instances to cloudmap service. If Deployment platform is not passed in then we consider it + * as classic EC2 or ECS based deployment platform + * @return map containing ip address and vpcid + */ + public Map getRegistrationAttributes() { + String deploymentPlatform = System.getenv(DEPLOYMENT_PLATFORM); + LOGGER.info("Deployment platform passed in {} ", deploymentPlatform); + if (StringUtils.hasText(deploymentPlatform) && EKS.equalsIgnoreCase(deploymentPlatform.trim())) + return getEksRegistrationAttributes(); + return getEcsRegistrationAttributes(); + } + + /** + * Get Cloudmap namespaceID based on name + * @param serviceDiscovery AWS Service discovery + * @param nameSpace cloudmap namespace + * @return namespaceID + */ + public String getNameSpaceId(final AWSServiceDiscovery serviceDiscovery, final String nameSpace) { + String token; + do { + ListNamespacesRequest namespacesRequest = new ListNamespacesRequest(); + ListNamespacesResult namespacesResult = serviceDiscovery.listNamespaces(namespacesRequest); + token = namespacesRequest.getNextToken(); + + List namespaceSummaries = namespacesResult.getNamespaces(); + if (namespaceSummaries != null) { + Optional namespaceId = namespaceSummaries.stream().filter(n -> n.getName().equals(nameSpace)) + .map(NamespaceSummary::getId).findFirst(); + if (namespaceId.isPresent()) + return namespaceId.get(); + } + else + LOGGER.warn("Namespace {} not available", nameSpace); + } + while (StringUtils.hasText(token)); + + return null; + } + + /** + * List services based on namespace and filter them based on name + * @param serviceDiscovery AWS service discovery + * @param discoveryProperties discovery properties (includes namespace and service + * name) + * @return list of cloudmap services + */ + public List listServices(final AWSServiceDiscovery serviceDiscovery, + List discoveryProperties) { + final List serviceList = new ArrayList<>(); + + if (discoveryProperties != null && !discoveryProperties.isEmpty()) { + for (CloudMapDiscoveryProperties d : discoveryProperties) { + final String serviceName = d.getService(); + final String nameSpace = d.getNameSpace(); + String token = null; + + do { + // Get namespaceID + final String nameSpaceId = getNameSpaceId(serviceDiscovery, nameSpace); + if (StringUtils.hasText(nameSpaceId)) { + // Filter cloudmap services + final ServiceFilter serviceFilter = new ServiceFilter().withName(NAMESPACE_ID) + .withCondition("EQ").withValues(nameSpaceId); + final ListServicesRequest servicesRequest = new ListServicesRequest() + .withFilters(serviceFilter); + Optional.ofNullable(token).ifPresent(servicesRequest::withNextToken); + final ListServicesResult result = serviceDiscovery.listServices(servicesRequest); + token = result.getNextToken(); + + if (StringUtils.hasText(serviceName)) { + serviceList.addAll(result.getServices().stream() + .filter(r -> r.getName().equals(d.getService())) + .map(r -> generateServiceId(r.getName(), nameSpace)).collect(Collectors.toList())); + if (serviceList.size() == discoveryProperties.size()) + return serviceList; + } + else + serviceList.addAll(result.getServices().stream() + .map(r -> generateServiceId(r.getName(), nameSpace)).collect(Collectors.toList())); + } + else + LOGGER.warn("Namespace is empty"); + } + while (StringUtils.hasText(token)); + } + } + + return serviceList; + } + + /** + * List cloudmap instances based on service name and namespace + * @param serviceDiscovery AWS Service discovery + * @param namespace cloudmap namespace + * @param serviceName cloudmap service name + * @return list of http instances + */ + public List listInstances(final AWSServiceDiscovery serviceDiscovery, final String namespace, + String serviceName) { + final DiscoverInstancesRequest dRequest = new DiscoverInstancesRequest().withNamespaceName(namespace) + .withServiceName(serviceName); + + return serviceDiscovery.discoverInstances(dRequest).getInstances(); + } + + /** + * Get service instance from http instance summary + * @param instanceSummary HTTP instance summary - Cloudmap object + * @return Service instance - Spring object + */ + public ServiceInstance getServiceInstance(HttpInstanceSummary instanceSummary) { + return new CloudMapServiceInstance(instanceSummary); + } + + /** + * Register with cloudmap, the method takes care of the following: 1. Create + * namespace, if not exists 2. Create service, if not exists 3. Register the instance + * with the created namespace and service + * @param serviceDiscovery AWS Service discovery service + * @param properties Cloud map registry properties + * @param environment Spring environment + * @return map of registration properties + */ + public Map registerInstance(final AWSServiceDiscovery serviceDiscovery, + final CloudMapRegistryProperties properties, final Environment environment) { + if (properties != null && StringUtils.hasText(properties.getNameSpace()) + && StringUtils.hasText(properties.getService())) { + + String nameSpace = properties.getNameSpace(); + if (!StringUtils.hasText(nameSpace)) + nameSpace = "default-namespace"; + + String service = properties.getService(); + if (!StringUtils.hasText(service)) + service = environment.getProperty("spring.application.name"); + + if (!StringUtils.hasText(service)) + service = "default-service"; + + final String serviceInstanceId = UUID.randomUUID().toString(); + + LOGGER.info("Registration details namespace {} - service {} - serviceInstance {}", nameSpace, service, + serviceInstanceId); + Map registrationDetails = getRegistrationAttributes(); + String nameSpaceId = getNameSpaceId(serviceDiscovery, properties.getNameSpace()); + try { + // Create namespace if not exists + if (!StringUtils.hasText(nameSpaceId)) { + LOGGER.debug("Namespace " + nameSpace + "not available so creating"); + nameSpaceId = createNameSpace(serviceDiscovery, properties, registrationDetails.get(VPC_ID)); + } + + // Create service if not exists + String serviceId = getServiceId(serviceDiscovery, nameSpaceId, service); + if (!StringUtils.hasText(serviceId)) { + LOGGER.debug("Service " + service + " doesnt exist so creating new one"); + serviceId = createService(serviceDiscovery, nameSpaceId, service); + } + + Map attributes = new HashMap<>(); + attributes.put(AWS_INSTANCE_IPV_4, registrationDetails.get(IPV_4_ADDRESS)); + attributes.put(REGION, System.getenv("AWS_REGION")); + attributes.put(NAMESPACE_ID, nameSpaceId); + attributes.put(SERVICE_ID, serviceId); + attributes.put(SERVICE_INSTANCE_ID, serviceInstanceId); + + // Register instance + final String operationId = serviceDiscovery.registerInstance(new RegisterInstanceRequest() + .withInstanceId(serviceInstanceId).withServiceId(serviceId).withAttributes(attributes)) + .getOperationId(); + LOGGER.debug("Register instance initiated, polling for completion {}", operationId); + + // Poll for completion + pollForCompletion(serviceDiscovery, operationId); + + return attributes; + } + catch (InvalidInputException e) { + LOGGER.error("Invalid input passed into the service {} - {}", nameSpaceId, e.getMessage(), e); + } + catch (CreateNameSpaceException e) { + LOGGER.error("Error while creating namespace {} - {}", nameSpace, e.getMessage()); + } + catch (InterruptedException e) { + LOGGER.error("Error while polling for status update {} with error {}", nameSpace, e.getMessage()); + } + catch (CreateServiceException e) { + LOGGER.error("Error while creating service {} with {} - {}", service, nameSpace, e.getMessage()); + } + catch (MaxRetryExceededException e) { + LOGGER.error("Maximum number of retry exceeded for registering instance with {} for {}", nameSpace, + service, e); + } + } + else { + LOGGER.info("Service registration skipped"); + } + + return null; + } + + /** + * Create Cloudmap namespace. + * @param serviceDiscovery AWS Service discovery + * @param properties cloudmap properties + * @param vpcId VPC ID + * @return NamespaceID + * @throws CreateNameSpaceException thrown in case of runtime exception + */ + private String createNameSpace(AWSServiceDiscovery serviceDiscovery, CloudMapRegistryProperties properties, + String vpcId) throws CreateNameSpaceException { + final String nameSpace = properties.getNameSpace(); + try { + // Create namespace + final String operationId = serviceDiscovery.createPrivateDnsNamespace(new CreatePrivateDnsNamespaceRequest() + .withName(nameSpace).withVpc(vpcId).withDescription(properties.getDescription())).getOperationId(); + LOGGER.info("Creating namespace {} with operationId {}", nameSpace, operationId); + + // Wait till completion + pollForCompletion(serviceDiscovery, operationId); + + return getNameSpaceId(serviceDiscovery, nameSpace); + } + catch (NamespaceAlreadyExistsException e) { + LOGGER.warn("Namespace {} already exists", nameSpace); + return getNameSpaceId(serviceDiscovery, nameSpace); + } + catch (InvalidInputException | ResourceLimitExceededException | DuplicateRequestException e) { + LOGGER.error("Error while registering with cloudmap {} with error {}", nameSpace, e.getMessage(), e); + throw new CreateNameSpaceException(e); + } + catch (InterruptedException e) { + LOGGER.error("Error while polling for status update {} with error {}", nameSpace, e.getMessage(), e); + throw new CreateNameSpaceException(e); + } + catch (MaxRetryExceededException e) { + LOGGER.error("Maximum number of retry exceeded for namespace {}", nameSpace, e); + throw new CreateNameSpaceException(e); + } + } + + /** + * Create service. + * @param serviceDiscovery AWS Service Discovery + * @param nameSpaceId CloudMap Namespace ID + * @param service Service name + * @return Service ID + * @throws CreateServiceException thrown in case of runtime exception + */ + private String createService(AWSServiceDiscovery serviceDiscovery, String nameSpaceId, String service) + throws CreateServiceException { + try { + CreateServiceRequest serviceRequest = new CreateServiceRequest().withName(service) + .withNamespaceId(nameSpaceId).withDnsConfig( + new DnsConfig().withDnsRecords(new DnsRecord().withType(RecordType.A).withTTL(300L))); + + final String serviceId = serviceDiscovery.createService(serviceRequest).getService().getId(); + LOGGER.info("Service ID create {} for {} with namespace {}", serviceId, service, nameSpaceId); + return serviceId; + } + catch (ServiceAlreadyExistsException e) { + LOGGER.warn("Service {} already exists", service); + return getServiceId(serviceDiscovery, service, nameSpaceId); + } + catch (InvalidInputException | ResourceLimitExceededException e) { + LOGGER.error("Error while creating service {} with namespace {}", service, nameSpaceId); + throw new CreateServiceException(e); + } + } + + public String generateServiceId(final String namespace, final String serviceName) { + return String.format("%s@%s", namespace, serviceName); + } + + /** + * Automatically deregister the instance when the container is stopped. + * @param serviceDiscovery AWS Service Discovery Service + * @param attributeMap Service discovery attributes + */ + public void deregisterInstance(final AWSServiceDiscovery serviceDiscovery, final Map attributeMap) { + try { + final String serviceInstanceId = attributeMap.get(SERVICE_INSTANCE_ID); + final String serviceId = attributeMap.get(SERVICE_ID); + LOGGER.info("Initiating de-registration process {} - {}", serviceInstanceId, serviceId); + + // Deregister instance + String operationId = serviceDiscovery + .deregisterInstance( + new DeregisterInstanceRequest().withInstanceId(serviceInstanceId).withServiceId(serviceId)) + .getOperationId(); + + // Wait till completion + pollForCompletion(serviceDiscovery, operationId); + } + catch (InterruptedException e) { + LOGGER.error("Error while polling for status while de-registering instance {}", e.getMessage(), e); + } + catch (MaxRetryExceededException e) { + LOGGER.error("Maximum number of retry exceeded {}", e.getMessage(), e); + } + } + + public static CloudMapUtils getInstance() { + if (cloudMapUtils == null){ + cloudMapUtils = new CloudMapUtils(); + } + return cloudMapUtils; + } + + /** + * Get service ID based on service name and namespace ID. + * @param serviceDiscovery AWS Service discovery + * @param nameSpaceId Namespace ID + * @param serviceName name of the cloudmap service + * @return Cloudmap service ID + */ + private String getServiceId(AWSServiceDiscovery serviceDiscovery, String nameSpaceId, String serviceName) { + ServiceFilter filter = new ServiceFilter(); + filter.setName(NAMESPACE_ID); + filter.setValues(Collections.singletonList(nameSpaceId)); + Optional serviceSummary = serviceDiscovery + .listServices(new ListServicesRequest().withFilters(filter)).getServices().stream() + .filter(s -> serviceName.equals(s.getName())).findFirst(); + return serviceSummary.map(ServiceSummary::getId).orElse(null); + } + + /** + * Poll for completion. + * @param serviceDiscovery AWS Service discovery + * @param operationId cloudmap operationID + * @throws InterruptedException thrown in case of thread.sleep() exception + * @throws MaxRetryExceededException thrown if maximum polling duration has exceeded + */ + private void pollForCompletion(AWSServiceDiscovery serviceDiscovery, String operationId) + throws InterruptedException, MaxRetryExceededException { + Operation operation = serviceDiscovery.getOperation(new GetOperationRequest().withOperationId(operationId)) + .getOperation(); + int counter = 0; + LOGGER.info("Operation ID {} will be polled", operationId); + while ((SUBMITTED.equalsIgnoreCase(operation.getStatus()) || PENDING.equalsIgnoreCase(operation.getStatus())) + && counter < MAX_POLL) { + operation = serviceDiscovery.getOperation(new GetOperationRequest().withOperationId(operationId)) + .getOperation(); + Thread.sleep(2000); + counter++; + } + + if (counter > MAX_POLL) { + throw new MaxRetryExceededException("Maximum of retry exceeded for " + operationId); + } + } + + /** + * Get CloudMap attributes for EKS platform + * @return map of cloud map attributes with Ipaddress and vpcid + */ + private Map getEksRegistrationAttributes() { + try { + String ipAddress = getUrlResponse(String.format("%s/local-ipv4", EC2_METADATA_URL)); + final String macId = getUrlResponse(String.format("%s/network/interfaces/macs", EC2_METADATA_URL)); + if (StringUtils.hasText(macId) && macId.contains("/")) { + final String macAddress = macId.split("/")[0]; + final String vpcUrl = String.format("%s/network/interfaces/macs/%s/vpc-id", EC2_METADATA_URL, macAddress); + final String vpcId = getUrlResponse(vpcUrl); + LOGGER.info("Meta data details IP Address {}, macAddress {} - VPCId {}", ipAddress, macAddress, vpcId); + return getCloudMapAttributes(ipAddress, vpcId); + } + } + catch (Exception e) { + LOGGER.error("Error while getting registration details {}", e.getMessage(), e); + } + return getCloudMapAttributes(generateIP(), "vpc-13c33d6e"); + } + + /** + * Get CloudMap attributes for ECS platform + * @return map of cloud map attributes with Ipaddress and vpcid + */ + private Map getEcsRegistrationAttributes() { + try { + String metaDataUrl = System.getenv(ECS_CONTAINER_METADATA_URI_V_4); + if (!StringUtils.hasText(metaDataUrl)) + metaDataUrl = EC2_METADATA_URL; + final String responseBody = getUrlResponse(metaDataUrl + "/task"); + JsonNode root = JSON_MAPPER.readTree(responseBody); + JsonNode jsonNode = root.get("Containers").get(0).get("Networks").get(0); + final String ipv4Address = jsonNode.get("IPv4Addresses").get(0).asText(); + final String cidrBlock = jsonNode.get("IPv4SubnetCIDRBlock").asText(); + final String vpcId = getEc2Client() + .describeSubnets(new DescribeSubnetsRequest() + .withFilters(new Filter().withName("cidr-block").withValues(cidrBlock))) + .getSubnets().get(0).getVpcId(); + LOGGER.info("IPv4Address {} - CIDR Block {} - VPC ID {}", ipv4Address, cidrBlock, vpcId); + +// String ipv4Address = "10.20.10.111"; +// String vpcId = "vpc-0294915e2f02ac742"; + return getCloudMapAttributes(ipv4Address, vpcId); + } + catch (Exception e) { + LOGGER.error("Error while fetching network details - {}", e.getMessage(), e); + return getCloudMapAttributes(generateIP(), "vpc-13c33d6e"); + } + } + + /** + * Helper method to fetch contents of URL as string + * @param url URL to fetch from + * @return response as string + */ + private String getUrlResponse(String url) { + return WEB_CLIENT.get().uri(url).retrieve().bodyToMono(String.class).block(); + } + + /** + * Returns hash map of cloudmap attributes + * @param ipv4Address IP Address of the instance + * @param vpcId VPC ID in which the instance is hosted + * @return hash map of cloudmap attributes + */ + private Map getCloudMapAttributes(String ipv4Address, String vpcId) { + Map attributes = new HashMap<>(); + attributes.put(IPV_4_ADDRESS, ipv4Address); + attributes.put(VPC_ID, vpcId); + return attributes; + } + + // TODO: Will be removed after testing + private String generateIP() { + Random r = new Random(); + return r.nextInt(256) + "." + r.nextInt(256) + "." + r.nextInt(256) + "." + r.nextInt(256); + } + + /** + * Get Ec2 client + * @return ec2 client object + */ + AmazonEC2 getEc2Client() { + if (ec2Client == null) { + ec2Client = AmazonEC2ClientBuilder.standard().withRegion(System.getenv("AWS_REGION")) + .withCredentials(new DefaultAWSCredentialsProviderChain()).build(); + } + return ec2Client; + } + +} diff --git a/spring-cloud-aws-cloudmap/src/main/java/org/springframework/cloud/aws/cloudmap/discovery/CloudMapDiscoveryClient.java b/spring-cloud-aws-cloudmap/src/main/java/org/springframework/cloud/aws/cloudmap/discovery/CloudMapDiscoveryClient.java new file mode 100644 index 000000000..c923a22ce --- /dev/null +++ b/spring-cloud-aws-cloudmap/src/main/java/org/springframework/cloud/aws/cloudmap/discovery/CloudMapDiscoveryClient.java @@ -0,0 +1,82 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * 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 + * + * https://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 org.springframework.cloud.aws.cloudmap.discovery; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import com.amazonaws.services.servicediscovery.AWSServiceDiscovery; + +import org.springframework.cloud.aws.cloudmap.CloudMapUtils; +import org.springframework.cloud.aws.cloudmap.model.CloudMapProperties; +import org.springframework.cloud.aws.cloudmap.model.discovery.CloudMapDiscoveryProperties; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.discovery.DiscoveryClient; + +// @checkstyle: off +public class CloudMapDiscoveryClient implements DiscoveryClient { + + /** + * Description of the service. + */ + public static final String DESCRIPTION = "AWS CloudMap Discovery Client"; + + private static final CloudMapUtils UTILS = CloudMapUtils.getInstance(); + + private final AWSServiceDiscovery serviceDiscovery; + + private final CloudMapProperties properties; + + public CloudMapDiscoveryClient(AWSServiceDiscovery serviceDiscovery, CloudMapProperties properties) { + this.serviceDiscovery = serviceDiscovery; + this.properties = properties; + } + + @Override + public int getOrder() { + return DiscoveryClient.super.getOrder(); + } + + @Override + public String description() { + return DESCRIPTION; + } + + @Override + public List getServices() { + final List discoveryProperties = properties.getDiscovery().getDiscoveryList(); + if (discoveryProperties != null && !discoveryProperties.isEmpty()) { + return UTILS.listServices(serviceDiscovery, discoveryProperties); + } + + return Collections.emptyList(); + } + + @Override + public List getInstances(String serviceId) { + // Service ID maintained as _ + String[] split = serviceId.split("@"); + if (split.length == 2) { + return UTILS.listInstances(serviceDiscovery, split[0], split[1]).stream().map(UTILS::getServiceInstance) + .collect(Collectors.toList()); + } + + return Collections.emptyList(); + } + +} diff --git a/spring-cloud-aws-cloudmap/src/main/java/org/springframework/cloud/aws/cloudmap/discovery/CloudMapServiceInstance.java b/spring-cloud-aws-cloudmap/src/main/java/org/springframework/cloud/aws/cloudmap/discovery/CloudMapServiceInstance.java new file mode 100644 index 000000000..e6b66ddba --- /dev/null +++ b/spring-cloud-aws-cloudmap/src/main/java/org/springframework/cloud/aws/cloudmap/discovery/CloudMapServiceInstance.java @@ -0,0 +1,79 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * 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 + * + * https://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. + */ + +// @checkstyle:off +package org.springframework.cloud.aws.cloudmap.discovery; + +import com.amazonaws.services.servicediscovery.model.HttpInstanceSummary; +import org.springframework.cloud.aws.cloudmap.CloudMapUtils; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.util.StringUtils; + +import java.net.URI; +import java.util.Map; + +public class CloudMapServiceInstance implements ServiceInstance { + + private final CloudMapUtils UTILS = CloudMapUtils.getInstance(); + + HttpInstanceSummary instanceSummary; + + public CloudMapServiceInstance(HttpInstanceSummary httpInstanceSummary) { + this.instanceSummary = httpInstanceSummary; + } + + @Override + public String getInstanceId() { + return instanceSummary.getInstanceId(); + } + + @Override + public String getScheme() { + return getUri().getScheme(); + } + + @Override + public String getServiceId() { + return UTILS.generateServiceId(instanceSummary.getNamespaceName(), instanceSummary.getServiceName()); + } + + @Override + public String getHost() { + return instanceSummary.getAttributes().get("AWS_INSTANCE_IPV4"); + } + + @Override + public int getPort() { + String port = instanceSummary.getAttributes().get("AWS_INSTANCE_PORT"); + return StringUtils.hasText(port) ? Integer.parseInt(port) : 0; + } + + @Override + public boolean isSecure() { + return false; + } + + @Override + public URI getUri() { + return URI.create(String.format("http://%s:%s", this.getHost(), this.getPort())); + } + + @Override + public Map getMetadata() { + return instanceSummary.getAttributes(); + } + +} diff --git a/spring-cloud-aws-cloudmap/src/main/java/org/springframework/cloud/aws/cloudmap/exceptions/CreateNameSpaceException.java b/spring-cloud-aws-cloudmap/src/main/java/org/springframework/cloud/aws/cloudmap/exceptions/CreateNameSpaceException.java new file mode 100644 index 000000000..22385d38a --- /dev/null +++ b/spring-cloud-aws-cloudmap/src/main/java/org/springframework/cloud/aws/cloudmap/exceptions/CreateNameSpaceException.java @@ -0,0 +1,26 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * 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 + * + * https://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 org.springframework.cloud.aws.cloudmap.exceptions; + +// Thrown in case of namespace exception. +public class CreateNameSpaceException extends RuntimeException { + + public CreateNameSpaceException(Throwable cause) { + super(cause); + } + +} diff --git a/spring-cloud-aws-cloudmap/src/main/java/org/springframework/cloud/aws/cloudmap/exceptions/CreateServiceException.java b/spring-cloud-aws-cloudmap/src/main/java/org/springframework/cloud/aws/cloudmap/exceptions/CreateServiceException.java new file mode 100644 index 000000000..6e57d6285 --- /dev/null +++ b/spring-cloud-aws-cloudmap/src/main/java/org/springframework/cloud/aws/cloudmap/exceptions/CreateServiceException.java @@ -0,0 +1,26 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * 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 + * + * https://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 org.springframework.cloud.aws.cloudmap.exceptions; + +// Throw in case of cloudmap service exception. +public class CreateServiceException extends RuntimeException { + + public CreateServiceException(Throwable cause) { + super(cause); + } + +} diff --git a/spring-cloud-aws-cloudmap/src/main/java/org/springframework/cloud/aws/cloudmap/exceptions/MaxRetryExceededException.java b/spring-cloud-aws-cloudmap/src/main/java/org/springframework/cloud/aws/cloudmap/exceptions/MaxRetryExceededException.java new file mode 100644 index 000000000..2eeaaf9a2 --- /dev/null +++ b/spring-cloud-aws-cloudmap/src/main/java/org/springframework/cloud/aws/cloudmap/exceptions/MaxRetryExceededException.java @@ -0,0 +1,26 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * 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 + * + * https://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 org.springframework.cloud.aws.cloudmap.exceptions; + +// Thrown in case maximum retry for polling has exceeded. +public class MaxRetryExceededException extends RuntimeException { + + public MaxRetryExceededException(String message) { + super(message); + } + +} diff --git a/spring-cloud-aws-cloudmap/src/main/java/org/springframework/cloud/aws/cloudmap/model/CloudMapProperties.java b/spring-cloud-aws-cloudmap/src/main/java/org/springframework/cloud/aws/cloudmap/model/CloudMapProperties.java new file mode 100644 index 000000000..54b698d7d --- /dev/null +++ b/spring-cloud-aws-cloudmap/src/main/java/org/springframework/cloud/aws/cloudmap/model/CloudMapProperties.java @@ -0,0 +1,121 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * 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 + * + * https://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 org.springframework.cloud.aws.cloudmap.model; + +import java.net.URI; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.cloud.aws.cloudmap.model.discovery.CloudMapDiscovery; +import org.springframework.cloud.aws.cloudmap.model.registration.CloudMapRegistryProperties; +import org.springframework.context.EnvironmentAware; +import org.springframework.core.env.Environment; + +/** + * POJO to capture all cloudmap integration parameters (both registry and discovery). + * + * @author Hari Ohm Prasath Rajagopal + * @since 2.3.2 + */ +@ConfigurationProperties(CloudMapProperties.CONFIG_PREFIX) +public class CloudMapProperties implements EnvironmentAware { + + /** + * Default cloudmap prefix. + */ + public static final String CONFIG_PREFIX = "aws.cloudmap"; + + private CloudMapRegistryProperties registry; + + private CloudMapDiscovery discovery; + + private String region; + + /** + * Overrides the default endpoint. + */ + private URI endpoint; + + private boolean enabled; + + private String annotationBasePackage; + + private Environment environment; + + public String getAnnotationBasePackage() { + return this.annotationBasePackage; + } + + public void setAnnotationBasePackage(String annotationBasePackage) { + this.annotationBasePackage = annotationBasePackage; + } + + public String getRegion() { + return this.region; + } + + public void setRegion(String region) { + this.region = region; + } + + public CloudMapRegistryProperties getRegistry() { + return this.registry; + } + + public void setRegistry(CloudMapRegistryProperties registry) { + this.registry = registry; + } + + public CloudMapDiscovery getDiscovery() { + return this.discovery; + } + + public void setDiscovery(CloudMapDiscovery discovery) { + this.discovery = discovery; + } + + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public Environment getEnvironment() { + return environment; + } + + @Override + public void setEnvironment(Environment environment) { + this.environment = environment; + } + + public URI getEndpoint() { + return endpoint; + } + + public void setEndpoint(URI endpoint) { + this.endpoint = endpoint; + } + + @Override + public String toString() { + return "AwsCloudMapProperties{" + "registry=" + registry + ", discovery=" + discovery + ", region='" + region + + '\'' + ", enabled=" + enabled + ", annotationBasePackage='" + annotationBasePackage + '\'' + '}'; + } + +} diff --git a/spring-cloud-aws-cloudmap/src/main/java/org/springframework/cloud/aws/cloudmap/model/discovery/CloudMapDiscovery.java b/spring-cloud-aws-cloudmap/src/main/java/org/springframework/cloud/aws/cloudmap/model/discovery/CloudMapDiscovery.java new file mode 100644 index 000000000..6bb86a446 --- /dev/null +++ b/spring-cloud-aws-cloudmap/src/main/java/org/springframework/cloud/aws/cloudmap/model/discovery/CloudMapDiscovery.java @@ -0,0 +1,55 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * 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 + * + * https://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 org.springframework.cloud.aws.cloudmap.model.discovery; + +import java.util.List; + +/** + * Pojo class to capture all the discovery parameters. + * + * @author Hari Ohm Prasath + * @since 2.3.2 + */ +public class CloudMapDiscovery { + + // Default to fail if discovery has failed + private boolean failFast = true; + + private List discoveryList; + + public boolean isFailFast() { + return this.failFast; + } + + public void setFailFast(boolean failFast) { + this.failFast = failFast; + } + + public List getDiscoveryList() { + return this.discoveryList; + } + + public void setDiscoveryList(List discoveryList) { + this.discoveryList = discoveryList; + } + + @Override + public String toString() { + return "AwsCloudMapDiscovery{" + "failFast=" + failFast + ", discoveryList=" + discoveryList.toString() + '}'; + } + +} diff --git a/spring-cloud-aws-cloudmap/src/main/java/org/springframework/cloud/aws/cloudmap/model/discovery/CloudMapDiscoveryProperties.java b/spring-cloud-aws-cloudmap/src/main/java/org/springframework/cloud/aws/cloudmap/model/discovery/CloudMapDiscoveryProperties.java new file mode 100644 index 000000000..94eb57f0d --- /dev/null +++ b/spring-cloud-aws-cloudmap/src/main/java/org/springframework/cloud/aws/cloudmap/model/discovery/CloudMapDiscoveryProperties.java @@ -0,0 +1,70 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * 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 + * + * https://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 org.springframework.cloud.aws.cloudmap.model.discovery; + +import java.util.Map; + +/** + * POJO class to capture cloudmap discovery attributes. + * + * @author Hari Ohm Prasath + * @since 2.3.2 + */ +public class CloudMapDiscoveryProperties { + + private String nameSpace; + + private String service; + + private Map filterAttributes; + + public String getNameSpace() { + return this.nameSpace; + } + + public void setNameSpace(String nameSpace) { + this.nameSpace = nameSpace; + } + + public String getService() { + return this.service; + } + + public void setService(String service) { + this.service = service; + } + + public Map getFilterAttributes() { + return this.filterAttributes; + } + + public void setFilterAttributes(Map filterAttributes) { + this.filterAttributes = filterAttributes; + } + + @Override + public String toString() { + String data = "AwsCloudMapDiscoveryProperties{" + "serviceNameSpace=" + nameSpace + ", service=" + service; + if (filterAttributes != null) { + data += filterAttributes.keySet().stream().map(f -> "key = " + f + ":" + filterAttributes.get(f)) + .reduce((a, b) -> a + "," + b).get(); + } + data += "}"; + return data; + } + +} diff --git a/spring-cloud-aws-cloudmap/src/main/java/org/springframework/cloud/aws/cloudmap/model/registration/CloudMapRegistryProperties.java b/spring-cloud-aws-cloudmap/src/main/java/org/springframework/cloud/aws/cloudmap/model/registration/CloudMapRegistryProperties.java new file mode 100644 index 000000000..151db7de2 --- /dev/null +++ b/spring-cloud-aws-cloudmap/src/main/java/org/springframework/cloud/aws/cloudmap/model/registration/CloudMapRegistryProperties.java @@ -0,0 +1,57 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * 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 + * + * https://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 org.springframework.cloud.aws.cloudmap.model.registration; + +/** + * POJO class to capture cloudmap registration parameters. + * + * @author Hari Ohm Prasath + * @since 2.3.2 + */ +public class CloudMapRegistryProperties { + + private String nameSpace; + + private String service; + + private String description; + + public String getNameSpace() { + return nameSpace; + } + + public void setNameSpace(String nameSpace) { + this.nameSpace = nameSpace; + } + + public String getService() { + return service; + } + + public void setService(String service) { + this.service = service; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + +} diff --git a/spring-cloud-aws-cloudmap/src/main/java/org/springframework/cloud/aws/cloudmap/model/registration/ServiceRegistration.java b/spring-cloud-aws-cloudmap/src/main/java/org/springframework/cloud/aws/cloudmap/model/registration/ServiceRegistration.java new file mode 100644 index 000000000..1e4a0ccd1 --- /dev/null +++ b/spring-cloud-aws-cloudmap/src/main/java/org/springframework/cloud/aws/cloudmap/model/registration/ServiceRegistration.java @@ -0,0 +1,79 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * 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 + * + * https://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 org.springframework.cloud.aws.cloudmap.model.registration; + +import java.net.URI; +import java.util.Map; +import java.util.UUID; + +import org.springframework.cloud.aws.cloudmap.CloudMapUtils; +import org.springframework.cloud.client.serviceregistry.Registration; + +public class ServiceRegistration implements Registration { + + private final CloudMapRegistryProperties properties; + + private final Map registrationDetails; + + private final CloudMapUtils UTILS = CloudMapUtils.getInstance(); + + public ServiceRegistration(CloudMapRegistryProperties properties) { + registrationDetails = UTILS.getRegistrationAttributes(); + this.properties = properties; + } + + @Override + public String getInstanceId() { + return UUID.randomUUID().toString(); + } + + @Override + public String getScheme() { + return Registration.super.getScheme(); + } + + @Override + public String getServiceId() { + return UTILS.generateServiceId(properties.getNameSpace(), properties.getService()); + } + + @Override + public String getHost() { + return registrationDetails.get(UTILS.IPV_4_ADDRESS); + } + + @Override + public int getPort() { + return 0; + } + + @Override + public boolean isSecure() { + return false; + } + + @Override + public URI getUri() { + return null; + } + + @Override + public Map getMetadata() { + return registrationDetails; + } + +} diff --git a/spring-cloud-aws-cloudmap/src/main/java/org/springframework/cloud/aws/cloudmap/registration/CloudMapAutoRegistration.java b/spring-cloud-aws-cloudmap/src/main/java/org/springframework/cloud/aws/cloudmap/registration/CloudMapAutoRegistration.java new file mode 100644 index 000000000..6b5470225 --- /dev/null +++ b/spring-cloud-aws-cloudmap/src/main/java/org/springframework/cloud/aws/cloudmap/registration/CloudMapAutoRegistration.java @@ -0,0 +1,128 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * 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 + * + * https://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. + */ + +// @checkstyle:off +package org.springframework.cloud.aws.cloudmap.registration; + +import com.amazonaws.services.servicediscovery.AWSServiceDiscovery; +import org.springframework.cloud.aws.cloudmap.CloudMapUtils; +import org.springframework.cloud.aws.cloudmap.model.registration.CloudMapRegistryProperties; +import org.springframework.cloud.client.discovery.event.InstanceRegisteredEvent; +import org.springframework.cloud.client.serviceregistry.AutoServiceRegistration; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.EnvironmentAware; +import org.springframework.context.SmartLifecycle; +import org.springframework.context.event.ContextClosedEvent; +import org.springframework.context.event.SmartApplicationListener; +import org.springframework.core.Ordered; +import org.springframework.core.env.Environment; + +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + +public class CloudMapAutoRegistration + implements AutoServiceRegistration, SmartLifecycle, Ordered, SmartApplicationListener, EnvironmentAware { + + private final AWSServiceDiscovery serviceDiscovery; + + private final CloudMapRegistryProperties properties; + + private final ApplicationContext context; + + private final AtomicBoolean running = new AtomicBoolean(false); + + private final CloudMapUtils UTILS = CloudMapUtils.getInstance(); + + private Environment environment; + + private Map attributesMap; + + public CloudMapAutoRegistration(ApplicationContext context, AWSServiceDiscovery serviceDiscovery, + CloudMapRegistryProperties properties) { + this.context = context; + this.serviceDiscovery = serviceDiscovery; + this.properties = properties; + } + + @Override + public boolean isAutoStartup() { + return true; + } + + @Override + public void stop(Runnable callback) { + stop(); + callback.run(); + } + + @Override + public int getPhase() { + return 0; + } + + @Override + public int getOrder() { + return 0; + } + + @Override + public void onApplicationEvent(ApplicationEvent event) { + if (event instanceof ContextClosedEvent) { + onApplicationEvent((ContextClosedEvent) event); + } + } + + @Override + public void start() { + if (!this.running.get()) { + final Map attributesMap = UTILS.registerInstance(serviceDiscovery, properties, environment); + if (attributesMap != null && attributesMap.containsKey(UTILS.SERVICE_INSTANCE_ID)) { + this.attributesMap = attributesMap; + this.context.publishEvent(new InstanceRegisteredEvent<>(this, attributesMap)); + this.running.set(true); + } + } + } + + @Override + public void stop() { + if (this.running.get() && attributesMap != null && attributesMap.containsKey(UTILS.SERVICE_INSTANCE_ID)) { + UTILS.deregisterInstance(serviceDiscovery, attributesMap); + this.running.set(false); + } + } + + @Override + public boolean isRunning() { + return this.running.get(); + } + + @Override + public boolean supportsEventType(Class eventType) { + return true; + } + + @Override + public void setEnvironment(Environment environment) { + this.environment = environment; + } + + public void onApplicationEvent(ContextClosedEvent event) { + stop(); + } + +} diff --git a/spring-cloud-aws-cloudmap/src/test/java/org/springframework/cloud/aws/cloudmap/CloudMapRegisterServiceTest.java b/spring-cloud-aws-cloudmap/src/test/java/org/springframework/cloud/aws/cloudmap/CloudMapRegisterServiceTest.java new file mode 100644 index 000000000..0585a2fff --- /dev/null +++ b/spring-cloud-aws-cloudmap/src/test/java/org/springframework/cloud/aws/cloudmap/CloudMapRegisterServiceTest.java @@ -0,0 +1,205 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * 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 + * + * https://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 org.springframework.cloud.aws.cloudmap; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import com.amazonaws.services.servicediscovery.AWSServiceDiscovery; +import com.amazonaws.services.servicediscovery.model.CreatePrivateDnsNamespaceRequest; +import com.amazonaws.services.servicediscovery.model.CreatePrivateDnsNamespaceResult; +import com.amazonaws.services.servicediscovery.model.CreateServiceRequest; +import com.amazonaws.services.servicediscovery.model.CreateServiceResult; +import com.amazonaws.services.servicediscovery.model.DeregisterInstanceRequest; +import com.amazonaws.services.servicediscovery.model.DeregisterInstanceResult; +import com.amazonaws.services.servicediscovery.model.GetOperationRequest; +import com.amazonaws.services.servicediscovery.model.GetOperationResult; +import com.amazonaws.services.servicediscovery.model.ListNamespacesRequest; +import com.amazonaws.services.servicediscovery.model.ListNamespacesResult; +import com.amazonaws.services.servicediscovery.model.ListServicesRequest; +import com.amazonaws.services.servicediscovery.model.ListServicesResult; +import com.amazonaws.services.servicediscovery.model.NamespaceSummary; +import com.amazonaws.services.servicediscovery.model.Operation; +import com.amazonaws.services.servicediscovery.model.RegisterInstanceRequest; +import com.amazonaws.services.servicediscovery.model.RegisterInstanceResult; +import com.amazonaws.services.servicediscovery.model.Service; +import com.amazonaws.services.servicediscovery.model.ServiceSummary; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import org.springframework.cloud.aws.cloudmap.model.registration.CloudMapRegistryProperties; +import org.springframework.cloud.aws.cloudmap.model.registration.ServiceRegistration; +import org.springframework.core.env.Environment; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Unit testcase for {@link ServiceRegistration} + * + * @author Hari Ohm Prasath + * @since 2.3.2 + */ +public class CloudMapRegisterServiceTest { + + private final AWSServiceDiscovery serviceDiscovery = mock(AWSServiceDiscovery.class); + + private final CloudMapUtils cloudMapUtils = CloudMapUtils.getInstance(); + + private final Environment environment = mock(Environment.class); + + @Test + public void cloudMapRegisterInstancesNameSpaceAndServiceExists() { + final ListNamespacesResult result = getListNamespacesResult(); + final ListServicesResult listServicesResult = getListServicesResult(); + final RegisterInstanceResult registerInstanceRequest = getRegisterInstanceResult(); + final GetOperationResult operationResult = getOperationResult(); + + when(serviceDiscovery.listNamespaces(any(ListNamespacesRequest.class))).thenReturn(result); + when(serviceDiscovery.listServices(any(ListServicesRequest.class))).thenReturn(listServicesResult); + when(serviceDiscovery.registerInstance((any(RegisterInstanceRequest.class)))) + .thenReturn(registerInstanceRequest); + when(serviceDiscovery.getOperation((any(GetOperationRequest.class)))).thenReturn(operationResult); + + when(serviceDiscovery.listServices(any(ListServicesRequest.class))).thenReturn(listServicesResult); + assertThat(cloudMapUtils.registerInstance(serviceDiscovery, getProperties(), environment)).isNotEmpty(); + } + + @Test + public void cloudMapRegisterInstanceWithNoNameSpace() { + final ListNamespacesResult namespacesResult = getListNamespacesResult(); + final ListServicesResult listServicesResult = getListServicesResult(); + final RegisterInstanceResult registerInstanceRequest = getRegisterInstanceResult(); + final GetOperationResult operationResult = getOperationResult(); + final CreatePrivateDnsNamespaceResult nameSpaceResult = getCreatePrivateDnsNamespaceResult(); + + when(serviceDiscovery.listNamespaces(any(ListNamespacesRequest.class))) + .thenReturn(new ListNamespacesResult().withNamespaces(Collections.emptyList()), namespacesResult); + when(serviceDiscovery.createPrivateDnsNamespace(any(CreatePrivateDnsNamespaceRequest.class))) + .thenReturn(nameSpaceResult); + when(serviceDiscovery.getOperation((any(GetOperationRequest.class)))).thenReturn(operationResult); + + when(serviceDiscovery.listServices(any(ListServicesRequest.class))).thenReturn(listServicesResult); + when(serviceDiscovery.registerInstance((any(RegisterInstanceRequest.class)))) + .thenReturn(registerInstanceRequest); + when(serviceDiscovery.getOperation((any(GetOperationRequest.class)))).thenReturn(operationResult); + + when(serviceDiscovery.listServices(any(ListServicesRequest.class))).thenReturn(listServicesResult); + assertThat(cloudMapUtils.registerInstance(serviceDiscovery, getProperties(), environment)).isNotEmpty(); + } + + @Test + public void cloudMapRegisterInstancesWithNoService() { + final ListNamespacesResult result = getListNamespacesResult(); + final ListServicesResult listServicesResult = getListServicesResult(); + final RegisterInstanceResult registerInstanceRequest = getRegisterInstanceResult(); + final GetOperationResult operationResult = getOperationResult(); + final CreateServiceResult createServiceResult = new CreateServiceResult(); + createServiceResult + .setService(new Service().withName(CloudMapTestUtils.SERVICE).withId(CloudMapTestUtils.SERVICE)); + + when(serviceDiscovery.listNamespaces(any(ListNamespacesRequest.class))).thenReturn(result); + + when(serviceDiscovery.listServices(any(ListServicesRequest.class))) + .thenReturn(new ListServicesResult().withServices(Collections.emptyList()), listServicesResult); + when(serviceDiscovery.createService(any(CreateServiceRequest.class))).thenReturn(createServiceResult); + + when(serviceDiscovery.registerInstance((any(RegisterInstanceRequest.class)))) + .thenReturn(registerInstanceRequest); + when(serviceDiscovery.getOperation((any(GetOperationRequest.class)))).thenReturn(operationResult); + + assertThat(cloudMapUtils.registerInstance(serviceDiscovery, getProperties(), environment)).isNotEmpty(); + } + + @Test + public void deRegisterInstances() { + try { + final Map attributeMap = new HashMap<>(); + attributeMap.put("SERVICE_INSTANCE_ID", "SERVICE_INSTANCE_ID"); + attributeMap.put("SERVICE_ID", "SERVICE_ID"); + + DeregisterInstanceResult result = new DeregisterInstanceResult(); + result.setOperationId(CloudMapTestUtils.OPERATION_ID); + when(serviceDiscovery.deregisterInstance(any(DeregisterInstanceRequest.class))).thenReturn(result); + + final GetOperationResult waitingResult = getOperationResult(); + waitingResult.setOperation(new Operation().withStatus("PENDING")); + + final GetOperationResult successResult = getOperationResult(); + when(serviceDiscovery.getOperation((any(GetOperationRequest.class)))).thenReturn(waitingResult, + successResult); + cloudMapUtils.deregisterInstance(serviceDiscovery, attributeMap); + } + catch (Exception e) { + Assertions.fail(); + } + } + + private CreatePrivateDnsNamespaceResult getCreatePrivateDnsNamespaceResult() { + CreatePrivateDnsNamespaceResult createPrivateDnsNamespaceResult = new CreatePrivateDnsNamespaceResult(); + createPrivateDnsNamespaceResult.setOperationId(CloudMapTestUtils.OPERATION_ID); + return createPrivateDnsNamespaceResult; + } + + private GetOperationResult getOperationResult() { + GetOperationResult operationResult = new GetOperationResult(); + operationResult.setOperation(new Operation().withStatus("SUCCESS")); + return operationResult; + } + + private RegisterInstanceResult getRegisterInstanceResult() { + RegisterInstanceResult registerInstanceRequest = new RegisterInstanceResult(); + registerInstanceRequest.setOperationId(CloudMapTestUtils.OPERATION_ID); + return registerInstanceRequest; + } + + private ListServicesResult getListServicesResult() { + ServiceSummary serviceSummary = new ServiceSummary(); + serviceSummary.setId(CloudMapTestUtils.SERVICE); + serviceSummary.setName(CloudMapTestUtils.SERVICE); + ListServicesResult listServicesResult = new ListServicesResult(); + listServicesResult.setServices(Collections.singletonList(serviceSummary)); + return listServicesResult; + } + + private ListNamespacesResult getListNamespacesResult() { + NamespaceSummary summary = new NamespaceSummary(); + summary.setId(CloudMapTestUtils.NAMESPACE); + summary.setName(CloudMapTestUtils.NAMESPACE); + ListNamespacesResult result = new ListNamespacesResult(); + result.setNamespaces(Collections.singleton(summary)); + return result; + } + + private Map getAttributesMap() { + Map attributeMap = new HashMap<>(); + attributeMap.put(cloudMapUtils.IPV_4_ADDRESS, "10.1.1.23"); + return attributeMap; + } + + private CloudMapRegistryProperties getProperties() { + CloudMapRegistryProperties properties = new CloudMapRegistryProperties(); + properties.setService(CloudMapTestUtils.SERVICE); + properties.setNameSpace(CloudMapTestUtils.NAMESPACE); + properties.setDescription("DESCRIPTION"); + return properties; + } + +} diff --git a/spring-cloud-aws-cloudmap/src/test/java/org/springframework/cloud/aws/cloudmap/CloudMapTestUtils.java b/spring-cloud-aws-cloudmap/src/test/java/org/springframework/cloud/aws/cloudmap/CloudMapTestUtils.java new file mode 100644 index 000000000..e5165c5f1 --- /dev/null +++ b/spring-cloud-aws-cloudmap/src/test/java/org/springframework/cloud/aws/cloudmap/CloudMapTestUtils.java @@ -0,0 +1,33 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * 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 + * + * https://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 org.springframework.cloud.aws.cloudmap; + +/** + * Unit testcase for {@link CloudMapUtils} + * + * @author Hari Ohm Prasath + * @since 2.3.2 + */ +public class CloudMapTestUtils { + + public static final String NAMESPACE = "NAMESPACE"; + + public static final String SERVICE = "SERVICE"; + + public static final String OPERATION_ID = "OPERATION_ID"; + +} diff --git a/spring-cloud-aws-samples/pom.xml b/spring-cloud-aws-samples/pom.xml index 69b1b76b3..0f2727595 100644 --- a/spring-cloud-aws-samples/pom.xml +++ b/spring-cloud-aws-samples/pom.xml @@ -16,6 +16,7 @@ spring-cloud-aws-sns-sample spring-cloud-aws-parameter-store-sample spring-cloud-aws-secrets-manager-sample + spring-cloud-aws-cloud-map-sample diff --git a/spring-cloud-aws-samples/spring-cloud-aws-cloud-map-sample/Dockerfile b/spring-cloud-aws-samples/spring-cloud-aws-cloud-map-sample/Dockerfile new file mode 100644 index 000000000..105b1cfcf --- /dev/null +++ b/spring-cloud-aws-samples/spring-cloud-aws-cloud-map-sample/Dockerfile @@ -0,0 +1,5 @@ +FROM amazoncorretto:11 +ARG JAR_FILE=target/spring-cloud-aws-cloud-map-sample-2.3.1.jar +COPY ${JAR_FILE} app.jar +ENTRYPOINT ["java","-jar","/app.jar"] +EXPOSE 8080 diff --git a/spring-cloud-aws-samples/spring-cloud-aws-cloud-map-sample/pom.xml b/spring-cloud-aws-samples/spring-cloud-aws-cloud-map-sample/pom.xml new file mode 100644 index 000000000..a1daa7a90 --- /dev/null +++ b/spring-cloud-aws-samples/spring-cloud-aws-cloud-map-sample/pom.xml @@ -0,0 +1,53 @@ + + + + spring-cloud-aws-samples + io.awspring.cloud + 2.3.1 + + 4.0.0 + + spring-cloud-aws-cloud-map-sample + + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-configuration-processor + + + org.springframework.cloud + spring-cloud-starter-bootstrap + + + io.awspring.cloud + spring-cloud-starter-aws-cloudmap + 2.3.1 + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + io.awspring.cloud.cloudmap.sample.SpringCloudAwsCloudMapSample + + + + + + + diff --git a/spring-cloud-aws-samples/spring-cloud-aws-cloud-map-sample/run.sh b/spring-cloud-aws-samples/spring-cloud-aws-cloud-map-sample/run.sh new file mode 100755 index 000000000..01328bab2 --- /dev/null +++ b/spring-cloud-aws-samples/spring-cloud-aws-cloud-map-sample/run.sh @@ -0,0 +1,4 @@ +mvn clean install -DskipTests=true +docker build -t aws-samples-cloudmap . +docker tag aws-samples-cloudmap:latest 775492342640.dkr.ecr.us-east-1.amazonaws.com/aws-samples-cloudmap:latest +docker push 775492342640.dkr.ecr.us-east-1.amazonaws.com/aws-samples-cloudmap:latest diff --git a/spring-cloud-aws-samples/spring-cloud-aws-cloud-map-sample/src/main/java/io/awspring/cloud/cloudmap/sample/SpringCloudAwsCloudMapSample.java b/spring-cloud-aws-samples/spring-cloud-aws-cloud-map-sample/src/main/java/io/awspring/cloud/cloudmap/sample/SpringCloudAwsCloudMapSample.java new file mode 100644 index 000000000..6f0b37781 --- /dev/null +++ b/spring-cloud-aws-samples/spring-cloud-aws-cloud-map-sample/src/main/java/io/awspring/cloud/cloudmap/sample/SpringCloudAwsCloudMapSample.java @@ -0,0 +1,42 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * 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 + * + * https://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 io.awspring.cloud.cloudmap.sample; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; + +@SpringBootApplication +@EnableDiscoveryClient +public class SpringCloudAwsCloudMapSample implements ApplicationRunner { + @Autowired + private DiscoveryClient discoveryClient; + + public static void main(String[] args) { + SpringApplication.run(SpringCloudAwsCloudMapSample.class, args); + } + + @Override + public void run(ApplicationArguments args) { + this.discoveryClient.getServices(); + } + +} diff --git a/spring-cloud-aws-samples/spring-cloud-aws-cloud-map-sample/src/main/resources/application.properties b/spring-cloud-aws-samples/spring-cloud-aws-cloud-map-sample/src/main/resources/application.properties new file mode 100644 index 000000000..15693f6b4 --- /dev/null +++ b/spring-cloud-aws-samples/spring-cloud-aws-cloud-map-sample/src/main/resources/application.properties @@ -0,0 +1,2 @@ +# importing cloudmap configuration files +spring.config.import=bootstrap.properties diff --git a/spring-cloud-aws-samples/spring-cloud-aws-cloud-map-sample/src/main/resources/bootstrap.properties b/spring-cloud-aws-samples/spring-cloud-aws-cloud-map-sample/src/main/resources/bootstrap.properties new file mode 100644 index 000000000..6a5b67b49 --- /dev/null +++ b/spring-cloud-aws-samples/spring-cloud-aws-cloud-map-sample/src/main/resources/bootstrap.properties @@ -0,0 +1,14 @@ +aws.cloudmap.region=us-east-1 +aws.cloudmap.enabled=true +spring.application.name=cloudmap-namespace-here + +# Discover existing cloudmap instances +aws.cloudmap.discovery.failFast=false +aws.cloudmap.discovery.discoveryList[0].service=TestService +aws.cloudmap.discovery.discoveryList[0].nameSpace=ECS-CloudMap + +# Register new instance +aws.cloudmap.registry.description=Namespace for sample cloudmap registry service +aws.cloudmap.registry.port=80 +aws.cloudmap.registry.service=a-service +aws.cloudmap.registry.nameSpace=a-namespace diff --git a/spring-cloud-starter-aws-cloudmap/pom.xml b/spring-cloud-starter-aws-cloudmap/pom.xml new file mode 100644 index 000000000..05009a316 --- /dev/null +++ b/spring-cloud-starter-aws-cloudmap/pom.xml @@ -0,0 +1,39 @@ + + + 4.0.0 + + io.awspring.cloud + spring-cloud-aws + 2.3.1 + + + spring-cloud-starter-aws-cloudmap + Spring Cloud AWS Cloud Map Starter + Spring Cloud AWS Cloud Map Starter + https://projects.spring.io/spring-cloud + + Pivotal Software, Inc. + https://www.spring.io + + + ${basedir}/../.. + + + + + io.awspring.cloud + spring-cloud-aws-cloudmap + 2.3.1 + + + io.awspring.cloud + spring-cloud-aws-core + + + + diff --git a/spring-cloud-starter-aws-cloudmap/src/main/java/org/springframework/cloud/aws/autoconfigure/cloudmap/CloudMapBootstrapConfiguration.java b/spring-cloud-starter-aws-cloudmap/src/main/java/org/springframework/cloud/aws/autoconfigure/cloudmap/CloudMapBootstrapConfiguration.java new file mode 100644 index 000000000..313adff72 --- /dev/null +++ b/spring-cloud-starter-aws-cloudmap/src/main/java/org/springframework/cloud/aws/autoconfigure/cloudmap/CloudMapBootstrapConfiguration.java @@ -0,0 +1,94 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * 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 + * + * https://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 org.springframework.cloud.aws.autoconfigure.cloudmap; + +import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; +import com.amazonaws.client.builder.AwsClientBuilder; +import com.amazonaws.services.servicediscovery.AWSServiceDiscovery; +import com.amazonaws.services.servicediscovery.AWSServiceDiscoveryClientBuilder; +import com.amazonaws.util.StringUtils; +import io.awspring.cloud.core.SpringCloudClientConfiguration; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.aws.cloudmap.discovery.CloudMapDiscoveryClient; +import org.springframework.cloud.aws.cloudmap.model.CloudMapProperties; +import org.springframework.cloud.aws.cloudmap.model.registration.ServiceRegistration; +import org.springframework.cloud.aws.cloudmap.registration.CloudMapAutoRegistration; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Cloudmap BootstrapConfiguration configuration class to create the required beans. + * + * @author Hari Ohm Prasath + * @since 2.3.2 + */ +@Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties(CloudMapProperties.class) +@ConditionalOnClass({ AWSServiceDiscovery.class, ServiceRegistration.class, CloudMapAutoRegistration.class }) +@ConditionalOnProperty(prefix = CloudMapProperties.CONFIG_PREFIX, name = "enabled", matchIfMissing = true) +public class CloudMapBootstrapConfiguration { + + private final ApplicationContext context; + + private final AWSServiceDiscovery serviceDiscovery; + + private final CloudMapProperties properties; + + public CloudMapBootstrapConfiguration(CloudMapProperties properties, ApplicationContext context) { + AWSServiceDiscoveryClientBuilder builder = AWSServiceDiscoveryClientBuilder.standard() + .withClientConfiguration(SpringCloudClientConfiguration.getClientConfiguration()) + .withCredentials(new DefaultAWSCredentialsProviderChain()); + + if (!StringUtils.isNullOrEmpty(properties.getRegion())) { + builder.withRegion(properties.getRegion()); + } + + if (properties.getEndpoint() != null) { + AwsClientBuilder.EndpointConfiguration endpointConfiguration = new AwsClientBuilder.EndpointConfiguration( + properties.getEndpoint().toString(), null); + builder.withEndpointConfiguration(endpointConfiguration); + } + + this.serviceDiscovery = builder.build(); + this.properties = properties; + this.context = context; + } + + @Bean + @ConditionalOnMissingBean + CloudMapAutoRegistration createAutoRegistration() { + return new CloudMapAutoRegistration(context, serviceDiscovery, properties.getRegistry()); + } + + @Bean + @ConditionalOnMissingBean + public CloudMapDiscoveryClient discoveryClient() { + return new CloudMapDiscoveryClient(serviceDiscovery, properties); + } + + @Bean + @ConditionalOnMissingBean + public ServiceRegistration serviceRegistration() { + return new ServiceRegistration(properties.getRegistry()); + } + +} diff --git a/spring-cloud-starter-aws-cloudmap/src/main/resources/META-INF/spring.factories b/spring-cloud-starter-aws-cloudmap/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000..7a201cb2b --- /dev/null +++ b/spring-cloud-starter-aws-cloudmap/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.cloud.bootstrap.BootstrapConfiguration=\ +org.springframework.cloud.aws.autoconfigure.cloudmap.CloudMapBootstrapConfiguration