definitions = new ArrayList<>(props.length);
57 | for (AwsProperties prop : props) {
58 | definitions.add(prop.getDefinition());
59 | }
60 | return definitions;
61 | }
62 |
63 | /**
64 | * Checks if Hazelcast is running on an AWS EC2 instance.
65 | *
66 | * Note that this method returns {@code false} for any ECS environment, since currently there is no way to auto-configure
67 | * Hazelcast network interfaces (required for ECS).
68 | *
69 | * To check if Hazelcast is running on EC2, we first check that the machine uuid starts with "ec2" or "EC2". There is
70 | * a small chance that a non-AWS machine has uuid starting with the mentioned prefix. That is why, to be sure, we make
71 | * an API call to a local, non-routable address http://169.254.169.254/latest/dynamic/instance-identity/. Finally, we also
72 | * check if an IAM Role is attached to the EC2 instance, because without any IAM Role the Hazelcast AWS discovery won't work.
73 | *
74 | * @return true if running on EC2 Instance which has an IAM Role attached
75 | * @see https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/identify_ec2_instances.html
76 | */
77 | @Override
78 | public boolean isAutoDetectionApplicable() {
79 | return isRunningOnEc2() && !isRunningOnEcs();
80 | }
81 |
82 | private static boolean isRunningOnEc2() {
83 | return uuidWithEc2Prefix() && instanceIdentityExists() && iamRoleAttached();
84 | }
85 |
86 | private static boolean uuidWithEc2Prefix() {
87 | String uuidPath = "/sys/hypervisor/uuid";
88 | if (new File(uuidPath).exists()) {
89 | String uuid = readFileContents(uuidPath);
90 | return uuid.startsWith("ec2") || uuid.startsWith("EC2");
91 | }
92 | return false;
93 | }
94 |
95 | static String readFileContents(String fileName) {
96 | InputStream is = null;
97 | try {
98 | File file = new File(fileName);
99 | byte[] data = new byte[(int) file.length()];
100 | is = new FileInputStream(file);
101 | is.read(data);
102 | return new String(data, StandardCharsets.UTF_8);
103 | } catch (IOException e) {
104 | throw new RuntimeException("Could not get " + fileName, e);
105 | } finally {
106 | IOUtil.closeResource(is);
107 | }
108 | }
109 |
110 | private static boolean instanceIdentityExists() {
111 | return isEndpointAvailable("http://169.254.169.254/latest/dynamic/instance-identity/");
112 | }
113 |
114 | private static boolean iamRoleAttached() {
115 | try {
116 | return isEndpointAvailable("http://169.254.169.254/latest/meta-data/iam/security-credentials/");
117 | } catch (Exception e) {
118 | LOGGER.warning("Hazelcast running on EC2 instance, but no IAM Role attached. Cannot use Hazelcast AWS discovery.");
119 | LOGGER.finest(e);
120 | return false;
121 | }
122 | }
123 |
124 | static boolean isEndpointAvailable(String url) {
125 | return !RestClient.create(url)
126 | .withConnectTimeoutSeconds(1)
127 | .withReadTimeoutSeconds(1)
128 | .withRetries(1)
129 | .get()
130 | .getBody()
131 | .isEmpty();
132 | }
133 |
134 | private static boolean isRunningOnEcs() {
135 | return new Environment().isRunningOnEcs();
136 | }
137 |
138 | @Override
139 | public DiscoveryStrategyLevel discoveryStrategyLevel() {
140 | return DiscoveryStrategyLevel.CLOUD_VM;
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/src/main/java/com/hazelcast/aws/AwsEc2Api.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Hazelcast Inc.
3 | *
4 | * Licensed under the Hazelcast Community License (the "License"); you may not use
5 | * this file except in compliance with the License. You may obtain a copy of the
6 | * License at
7 | *
8 | * http://hazelcast.com/hazelcast-community-license
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 | * WARRANTIES OF ANY KIND, either express or implied. See the License for the
13 | * specific language governing permissions and limitations under the License.
14 | */
15 |
16 | package com.hazelcast.aws;
17 |
18 | import com.hazelcast.logging.ILogger;
19 | import com.hazelcast.logging.Logger;
20 | import org.w3c.dom.Node;
21 |
22 | import java.time.Clock;
23 | import java.util.HashMap;
24 | import java.util.List;
25 | import java.util.Map;
26 | import java.util.Optional;
27 |
28 | import static com.hazelcast.aws.AwsRequestUtils.canonicalQueryString;
29 | import static com.hazelcast.aws.AwsRequestUtils.createRestClient;
30 | import static com.hazelcast.aws.AwsRequestUtils.currentTimestamp;
31 | import static com.hazelcast.aws.StringUtils.isNotEmpty;
32 |
33 | /**
34 | * Responsible for connecting to AWS EC2 API.
35 | *
36 | * @see AWS EC2 API
37 | */
38 | class AwsEc2Api {
39 | private static final ILogger LOGGER = Logger.getLogger(AwsEc2Api.class);
40 |
41 | private final String endpoint;
42 | private final AwsConfig awsConfig;
43 | private final AwsRequestSigner requestSigner;
44 | private final Clock clock;
45 |
46 | AwsEc2Api(String endpoint, AwsConfig awsConfig, AwsRequestSigner requestSigner, Clock clock) {
47 | this.endpoint = endpoint;
48 | this.awsConfig = awsConfig;
49 | this.requestSigner = requestSigner;
50 | this.clock = clock;
51 | }
52 |
53 | /**
54 | * Calls AWS EC2 Describe Instances API, parses the response, and returns mapping from private to public IPs.
55 | *
56 | * Note that if EC2 Instance does not have a public IP, then an entry (private-ip, null) is returned.
57 | *
58 | * @return map from private to public IP
59 | * @see EC2 Describe Instances
60 | */
61 | Map describeInstances(AwsCredentials credentials) {
62 | Map attributes = createAttributesDescribeInstances();
63 | Map headers = createHeaders(attributes, credentials);
64 | String response = callAwsService(attributes, headers);
65 | return parseDescribeInstances(response);
66 | }
67 |
68 | private Map createAttributesDescribeInstances() {
69 | Map attributes = createSharedAttributes();
70 | attributes.put("Action", "DescribeInstances");
71 | attributes.putAll(filterAttributesDescribeInstances());
72 | return attributes;
73 | }
74 |
75 | private Map filterAttributesDescribeInstances() {
76 | Filter filter = new Filter();
77 | for (Tag tag : awsConfig.getTags()) {
78 | addTagFilter(filter, tag);
79 | }
80 |
81 | if (isNotEmpty(awsConfig.getSecurityGroupName())) {
82 | filter.add("instance.group-name", awsConfig.getSecurityGroupName());
83 | }
84 |
85 | filter.add("instance-state-name", "running");
86 | return filter.getFilterAttributes();
87 | }
88 |
89 | /**
90 | * Adds filter entry to {@link Filter} base on provided {@link Tag}. Follows AWS API recommendations for
91 | * filtering EC2 instances using tags.
92 | *
93 | * @see
94 | * EC2 Describe Instances - Request Parameters
95 | */
96 | private void addTagFilter(Filter filter, Tag tag) {
97 | if (isNotEmpty(tag.getKey()) && isNotEmpty(tag.getValue())) {
98 | filter.add("tag:" + tag.getKey(), tag.getValue());
99 | } else if (isNotEmpty(tag.getKey())) {
100 | filter.add("tag-key", tag.getKey());
101 | } else if (isNotEmpty(tag.getValue())) {
102 | filter.add("tag-value", tag.getValue());
103 | }
104 | }
105 |
106 | private static Map parseDescribeInstances(String xmlResponse) {
107 | Map result = new HashMap<>();
108 | XmlNode.create(xmlResponse)
109 | .getSubNodes("reservationset").stream()
110 | .flatMap(e -> e.getSubNodes("item").stream())
111 | .flatMap(e -> e.getSubNodes("instancesset").stream())
112 | .flatMap(e -> e.getSubNodes("item").stream())
113 | .filter(e -> e.getValue("privateipaddress") != null)
114 | .peek(AwsEc2Api::logInstanceName)
115 | .forEach(e -> result.put(e.getValue("privateipaddress"), e.getValue("ipaddress")));
116 | return result;
117 | }
118 |
119 | private static void logInstanceName(XmlNode item) {
120 | LOGGER.fine(String.format("Accepting EC2 instance [%s][%s]",
121 | parseInstanceName(item).orElse(""),
122 | item.getValue("privateipaddress")));
123 | }
124 |
125 | private static Optional parseInstanceName(XmlNode nodeHolder) {
126 | return nodeHolder.getSubNodes("tagset").stream()
127 | .flatMap(e -> e.getSubNodes("item").stream())
128 | .filter(AwsEc2Api::isNameField)
129 | .flatMap(e -> e.getSubNodes("value").stream())
130 | .map(XmlNode::getNode)
131 | .map(Node::getFirstChild)
132 | .map(Node::getNodeValue)
133 | .findFirst();
134 | }
135 |
136 | private static boolean isNameField(XmlNode item) {
137 | return item.getSubNodes("key").stream()
138 | .map(XmlNode::getNode)
139 | .map(Node::getFirstChild)
140 | .map(Node::getNodeValue)
141 | .map("Name"::equals)
142 | .findFirst()
143 | .orElse(false);
144 | }
145 |
146 | /**
147 | * Calls AWS EC2 Describe Network Interfaces API, parses the response, and returns mapping from private to public
148 | * IPs.
149 | *
150 | * Note that if the given private IP does not have a public IP association, then an entry (private-ip, null)
151 | * is returned.
152 | *
153 | * @return map from private to public IP
154 | * @see
155 | * EC2 Describe Network Interfaces
156 | */
157 | Map describeNetworkInterfaces(List privateAddresses, AwsCredentials credentials) {
158 | Map attributes = createAttributesDescribeNetworkInterfaces(privateAddresses);
159 | Map headers = createHeaders(attributes, credentials);
160 | String response = callAwsService(attributes, headers);
161 | return parseDescribeNetworkInterfaces(response);
162 | }
163 |
164 | private Map createAttributesDescribeNetworkInterfaces(List privateAddresses) {
165 | Map attributes = createSharedAttributes();
166 | attributes.put("Action", "DescribeNetworkInterfaces");
167 | attributes.putAll(filterAttributesDescribeNetworkInterfaces(privateAddresses));
168 | return attributes;
169 | }
170 |
171 | private Map filterAttributesDescribeNetworkInterfaces(List privateAddresses) {
172 | Filter filter = new Filter();
173 | filter.addMulti("addresses.private-ip-address", privateAddresses);
174 | return filter.getFilterAttributes();
175 | }
176 |
177 | private static Map parseDescribeNetworkInterfaces(String xmlResponse) {
178 | Map result = new HashMap<>();
179 | XmlNode.create(xmlResponse)
180 | .getSubNodes("networkinterfaceset").stream()
181 | .flatMap(e -> e.getSubNodes("item").stream())
182 | .filter(e -> e.getValue("privateipaddress") != null)
183 | .forEach(e -> result.put(
184 | e.getValue("privateipaddress"),
185 | e.getSubNodes("association").stream()
186 | .map(a -> a.getValue("publicip"))
187 | .findFirst()
188 | .orElse(null)
189 | ));
190 | return result;
191 | }
192 |
193 | private static Map createSharedAttributes() {
194 | Map attributes = new HashMap<>();
195 | attributes.put("Version", "2016-11-15");
196 | return attributes;
197 | }
198 |
199 | private Map createHeaders(Map attributes, AwsCredentials credentials) {
200 | Map headers = new HashMap<>();
201 |
202 | if (isNotEmpty(credentials.getToken())) {
203 | headers.put("X-Amz-Security-Token", credentials.getToken());
204 | }
205 | headers.put("Host", endpoint);
206 | String timestamp = currentTimestamp(clock);
207 | headers.put("X-Amz-Date", timestamp);
208 | headers.put("Authorization", requestSigner.authHeader(attributes, headers, "", credentials, timestamp, "GET"));
209 |
210 | return headers;
211 | }
212 |
213 | private String callAwsService(Map attributes, Map headers) {
214 | String query = canonicalQueryString(attributes);
215 | return createRestClient(urlFor(endpoint, query), awsConfig)
216 | .withHeaders(headers)
217 | .get()
218 | .getBody();
219 | }
220 |
221 | private static String urlFor(String endpoint, String query) {
222 | return AwsRequestUtils.urlFor(endpoint) + "/?" + query;
223 | }
224 | }
225 |
--------------------------------------------------------------------------------
/src/main/java/com/hazelcast/aws/AwsEc2Client.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Hazelcast Inc.
3 | *
4 | * Licensed under the Hazelcast Community License (the "License"); you may not use
5 | * this file except in compliance with the License. You may obtain a copy of the
6 | * License at
7 | *
8 | * http://hazelcast.com/hazelcast-community-license
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 | * WARRANTIES OF ANY KIND, either express or implied. See the License for the
13 | * specific language governing permissions and limitations under the License.
14 | */
15 |
16 | package com.hazelcast.aws;
17 |
18 | import java.util.Map;
19 | import java.util.Optional;
20 |
21 | class AwsEc2Client implements AwsClient {
22 | private final AwsEc2Api awsEc2Api;
23 | private final AwsMetadataApi awsMetadataApi;
24 | private final AwsCredentialsProvider awsCredentialsProvider;
25 |
26 | AwsEc2Client(AwsEc2Api awsEc2Api, AwsMetadataApi awsMetadataApi, AwsCredentialsProvider awsCredentialsProvider) {
27 | this.awsEc2Api = awsEc2Api;
28 | this.awsMetadataApi = awsMetadataApi;
29 | this.awsCredentialsProvider = awsCredentialsProvider;
30 | }
31 |
32 | @Override
33 | public Map getAddresses() {
34 | return awsEc2Api.describeInstances(awsCredentialsProvider.credentials());
35 | }
36 |
37 | @Override
38 | public String getAvailabilityZone() {
39 | return awsMetadataApi.availabilityZoneEc2();
40 | }
41 |
42 | @Override
43 | public Optional getPlacementGroup() {
44 | return awsMetadataApi.placementGroupEc2();
45 | }
46 |
47 | @Override
48 | public Optional getPlacementPartitionNumber() {
49 | return awsMetadataApi.placementPartitionNumberEc2();
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/main/java/com/hazelcast/aws/AwsEcsApi.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Hazelcast Inc.
3 | *
4 | * Licensed under the Hazelcast Community License (the "License"); you may not use
5 | * this file except in compliance with the License. You may obtain a copy of the
6 | * License at
7 | *
8 | * http://hazelcast.com/hazelcast-community-license
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 | * WARRANTIES OF ANY KIND, either express or implied. See the License for the
13 | * specific language governing permissions and limitations under the License.
14 | */
15 |
16 | package com.hazelcast.aws;
17 |
18 | import com.hazelcast.internal.json.Json;
19 | import com.hazelcast.internal.json.JsonArray;
20 | import com.hazelcast.internal.json.JsonObject;
21 | import com.hazelcast.internal.json.JsonValue;
22 |
23 | import java.time.Clock;
24 | import java.util.HashMap;
25 | import java.util.List;
26 | import java.util.Map;
27 | import java.util.Optional;
28 | import java.util.stream.Collectors;
29 | import java.util.stream.Stream;
30 | import java.util.stream.StreamSupport;
31 |
32 | import static com.hazelcast.aws.AwsRequestUtils.createRestClient;
33 | import static com.hazelcast.aws.AwsRequestUtils.currentTimestamp;
34 | import static com.hazelcast.aws.AwsRequestUtils.urlFor;
35 | import static com.hazelcast.aws.StringUtils.isNotEmpty;
36 | import static java.util.Collections.emptyMap;
37 |
38 | /**
39 | * Responsible for connecting to AWS ECS API.
40 | *
41 | * @see AWS ECS API
42 | */
43 | class AwsEcsApi {
44 | private final String endpoint;
45 | private final AwsConfig awsConfig;
46 | private final AwsRequestSigner requestSigner;
47 | private final Clock clock;
48 |
49 | AwsEcsApi(String endpoint, AwsConfig awsConfig, AwsRequestSigner requestSigner, Clock clock) {
50 | this.endpoint = endpoint;
51 | this.awsConfig = awsConfig;
52 | this.requestSigner = requestSigner;
53 | this.clock = clock;
54 | }
55 |
56 | List listTasks(String cluster, AwsCredentials credentials) {
57 | String body = createBodyListTasks(cluster);
58 | Map headers = createHeadersListTasks(body, credentials);
59 | String response = callAwsService(body, headers);
60 | return parseListTasks(response);
61 | }
62 |
63 | private String createBodyListTasks(String cluster) {
64 | JsonObject body = new JsonObject();
65 | body.add("cluster", cluster);
66 | if (isNotEmpty(awsConfig.getFamily())) {
67 | body.add("family", awsConfig.getFamily());
68 | }
69 | if (isNotEmpty(awsConfig.getServiceName())) {
70 | body.add("serviceName", awsConfig.getServiceName());
71 | }
72 | return body.toString();
73 | }
74 |
75 | private Map createHeadersListTasks(String body, AwsCredentials credentials) {
76 | return createHeaders(body, credentials, "ListTasks");
77 | }
78 |
79 | private List parseListTasks(String response) {
80 | return toStream(toJson(response).get("taskArns"))
81 | .map(JsonValue::asString)
82 | .collect(Collectors.toList());
83 | }
84 |
85 | List describeTasks(String clusterArn, List taskArns, AwsCredentials credentials) {
86 | String body = createBodyDescribeTasks(clusterArn, taskArns);
87 | Map headers = createHeadersDescribeTasks(body, credentials);
88 | String response = callAwsService(body, headers);
89 | return parseDescribeTasks(response);
90 | }
91 |
92 | private String createBodyDescribeTasks(String cluster, List taskArns) {
93 | JsonArray jsonArray = new JsonArray();
94 | taskArns.stream().map(Json::value).forEach(jsonArray::add);
95 | return new JsonObject()
96 | .add("tasks", jsonArray)
97 | .add("cluster", cluster)
98 | .toString();
99 | }
100 |
101 | private Map createHeadersDescribeTasks(String body, AwsCredentials credentials) {
102 | return createHeaders(body, credentials, "DescribeTasks");
103 | }
104 |
105 | private List parseDescribeTasks(String response) {
106 | return toStream(toJson(response).get("tasks"))
107 | .flatMap(e -> toTask(e).map(Stream::of).orElseGet(Stream::empty))
108 | .collect(Collectors.toList());
109 | }
110 |
111 | private Optional toTask(JsonValue taskJson) {
112 | String availabilityZone = taskJson.asObject().get("availabilityZone").asString();
113 | return toStream(taskJson.asObject().get("containers"))
114 | .flatMap(e -> toStream(e.asObject().get("networkInterfaces")))
115 | .map(e -> e.asObject().get("privateIpv4Address").asString())
116 | .map(e -> new Task(e, availabilityZone))
117 | .findFirst();
118 | }
119 |
120 | private Map createHeaders(String body, AwsCredentials credentials, String awsTargetAction) {
121 | Map headers = new HashMap<>();
122 |
123 | if (isNotEmpty(credentials.getToken())) {
124 | headers.put("X-Amz-Security-Token", credentials.getToken());
125 | }
126 | headers.put("Host", endpoint);
127 | headers.put("X-Amz-Target", String.format("AmazonEC2ContainerServiceV20141113.%s", awsTargetAction));
128 | headers.put("Content-Type", "application/x-amz-json-1.1");
129 | headers.put("Accept-Encoding", "identity");
130 | String timestamp = currentTimestamp(clock);
131 | headers.put("X-Amz-Date", timestamp);
132 | headers.put("Authorization", requestSigner.authHeader(emptyMap(), headers, body, credentials, timestamp, "POST"));
133 |
134 | return headers;
135 | }
136 |
137 | private String callAwsService(String body, Map headers) {
138 | return createRestClient(urlFor(endpoint), awsConfig)
139 | .withHeaders(headers)
140 | .withBody(body)
141 | .post()
142 | .getBody();
143 | }
144 |
145 | private static JsonObject toJson(String jsonString) {
146 | return Json.parse(jsonString).asObject();
147 | }
148 |
149 | private static Stream toStream(JsonValue json) {
150 | return StreamSupport.stream(json.asArray().spliterator(), false);
151 | }
152 |
153 | static class Task {
154 | private final String privateAddress;
155 | private final String availabilityZone;
156 |
157 | Task(String privateAddress, String availabilityZone) {
158 | this.privateAddress = privateAddress;
159 | this.availabilityZone = availabilityZone;
160 | }
161 |
162 | String getPrivateAddress() {
163 | return privateAddress;
164 | }
165 |
166 | String getAvailabilityZone() {
167 | return availabilityZone;
168 | }
169 | }
170 | }
171 |
--------------------------------------------------------------------------------
/src/main/java/com/hazelcast/aws/AwsEcsClient.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Hazelcast Inc.
3 | *
4 | * Licensed under the Hazelcast Community License (the "License"); you may not use
5 | * this file except in compliance with the License. You may obtain a copy of the
6 | * License at
7 | *
8 | * http://hazelcast.com/hazelcast-community-license
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 | * WARRANTIES OF ANY KIND, either express or implied. See the License for the
13 | * specific language governing permissions and limitations under the License.
14 | */
15 |
16 | package com.hazelcast.aws;
17 |
18 | import com.hazelcast.aws.AwsEcsApi.Task;
19 | import com.hazelcast.logging.ILogger;
20 | import com.hazelcast.logging.Logger;
21 |
22 | import java.util.HashMap;
23 | import java.util.List;
24 | import java.util.Map;
25 | import java.util.stream.Collectors;
26 |
27 | import static java.util.Collections.emptyMap;
28 | import static java.util.Collections.singletonList;
29 |
30 | class AwsEcsClient implements AwsClient {
31 | private static final ILogger LOGGER = Logger.getLogger(AwsClient.class);
32 |
33 | private final AwsEcsApi awsEcsApi;
34 | private final AwsEc2Api awsEc2Api;
35 | private final AwsMetadataApi awsMetadataApi;
36 | private final AwsCredentialsProvider awsCredentialsProvider;
37 | private final String cluster;
38 |
39 | private boolean isNoPublicIpAlreadyLogged;
40 |
41 | AwsEcsClient(String cluster, AwsEcsApi awsEcsApi, AwsEc2Api awsEc2Api, AwsMetadataApi awsMetadataApi,
42 | AwsCredentialsProvider awsCredentialsProvider) {
43 | this.cluster = cluster;
44 | this.awsEcsApi = awsEcsApi;
45 | this.awsEc2Api = awsEc2Api;
46 | this.awsMetadataApi = awsMetadataApi;
47 | this.awsCredentialsProvider = awsCredentialsProvider;
48 | }
49 |
50 | @Override
51 | public Map getAddresses() {
52 | AwsCredentials credentials = awsCredentialsProvider.credentials();
53 |
54 | LOGGER.fine(String.format("Listing tasks from cluster: '%s'", cluster));
55 | List taskArns = awsEcsApi.listTasks(cluster, credentials);
56 | LOGGER.fine(String.format("AWS ECS ListTasks found the following tasks: %s", taskArns));
57 |
58 | if (!taskArns.isEmpty()) {
59 | List tasks = awsEcsApi.describeTasks(cluster, taskArns, credentials);
60 | List taskAddresses = tasks.stream().map(Task::getPrivateAddress).collect(Collectors.toList());
61 | LOGGER.fine(String.format("AWS ECS DescribeTasks found the following addresses: %s", taskAddresses));
62 |
63 | return fetchPublicAddresses(taskAddresses, credentials);
64 | }
65 | return emptyMap();
66 | }
67 |
68 | /**
69 | * Fetches private addresses for the tasks.
70 | *
71 | * Note that this is done as best-effort and does not fail if no public addresses are found, because:
72 | *
73 | * - Task may not have public IP addresses
74 | * - Task may not have access rights to query for public addresses
75 | *
76 | *
77 | * Also note that this is performed regardless of the configured use-public-ip value
78 | * to make external smart clients able to work properly when possible.
79 | */
80 | private Map fetchPublicAddresses(List privateAddresses, AwsCredentials credentials) {
81 | try {
82 | return awsEc2Api.describeNetworkInterfaces(privateAddresses, credentials);
83 | } catch (Exception e) {
84 | LOGGER.finest(e);
85 | // Log warning only once.
86 | if (!isNoPublicIpAlreadyLogged) {
87 | LOGGER.warning("Cannot fetch the public IPs of ECS Tasks. You won't be able to use " +
88 | "Hazelcast Smart Client from outside of this VPC.");
89 | isNoPublicIpAlreadyLogged = true;
90 | }
91 |
92 | Map map = new HashMap<>();
93 | privateAddresses.forEach(k -> map.put(k, null));
94 | return map;
95 | }
96 | }
97 |
98 | @Override
99 | public String getAvailabilityZone() {
100 | String taskArn = awsMetadataApi.metadataEcs().getTaskArn();
101 | AwsCredentials credentials = awsCredentialsProvider.credentials();
102 | List tasks = awsEcsApi.describeTasks(cluster, singletonList(taskArn), credentials);
103 | return tasks.stream()
104 | .map(Task::getAvailabilityZone)
105 | .findFirst()
106 | .orElse("unknown");
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/src/main/java/com/hazelcast/aws/AwsMetadataApi.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Hazelcast Inc.
3 | *
4 | * Licensed under the Hazelcast Community License (the "License"); you may not use
5 | * this file except in compliance with the License. You may obtain a copy of the
6 | * License at
7 | *
8 | * http://hazelcast.com/hazelcast-community-license
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 | * WARRANTIES OF ANY KIND, either express or implied. See the License for the
13 | * specific language governing permissions and limitations under the License.
14 | */
15 |
16 | package com.hazelcast.aws;
17 |
18 | import com.hazelcast.internal.json.Json;
19 | import com.hazelcast.internal.json.JsonObject;
20 | import com.hazelcast.logging.ILogger;
21 | import com.hazelcast.logging.Logger;
22 |
23 | import java.util.Optional;
24 |
25 | import static com.hazelcast.aws.AwsRequestUtils.createRestClient;
26 | import static com.hazelcast.aws.RestClient.HTTP_NOT_FOUND;
27 | import static com.hazelcast.aws.RestClient.HTTP_OK;
28 |
29 | /**
30 | * Responsible for connecting to AWS EC2 and ECS Metadata API.
31 | *
32 | * @see EC2 Instance Metatadata
33 | * @see ECS Task IAM Role Metadata
34 | * @see ECS Task Metadata
35 | */
36 | class AwsMetadataApi {
37 | private static final ILogger LOGGER = Logger.getLogger(AwsMetadataApi.class);
38 | private static final String EC2_METADATA_ENDPOINT = "http://169.254.169.254/latest/meta-data";
39 | private static final String ECS_IAM_ROLE_METADATA_ENDPOINT = "http://169.254.170.2" + System.getenv(
40 | "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI");
41 | private static final String ECS_TASK_METADATA_ENDPOINT = System.getenv("ECS_CONTAINER_METADATA_URI");
42 |
43 | private static final String SECURITY_CREDENTIALS_URI = "/iam/security-credentials/";
44 |
45 | private final String ec2MetadataEndpoint;
46 | private final String ecsIamRoleEndpoint;
47 | private final String ecsTaskMetadataEndpoint;
48 | private final AwsConfig awsConfig;
49 |
50 | AwsMetadataApi(AwsConfig awsConfig) {
51 | this.ec2MetadataEndpoint = EC2_METADATA_ENDPOINT;
52 | this.ecsIamRoleEndpoint = ECS_IAM_ROLE_METADATA_ENDPOINT;
53 | this.ecsTaskMetadataEndpoint = ECS_TASK_METADATA_ENDPOINT;
54 | this.awsConfig = awsConfig;
55 | }
56 |
57 | /**
58 | * For test purposes only.
59 | */
60 | AwsMetadataApi(String ec2MetadataEndpoint, String ecsIamRoleEndpoint, String ecsTaskMetadataEndpoint,
61 | AwsConfig awsConfig) {
62 | this.ec2MetadataEndpoint = ec2MetadataEndpoint;
63 | this.ecsIamRoleEndpoint = ecsIamRoleEndpoint;
64 | this.ecsTaskMetadataEndpoint = ecsTaskMetadataEndpoint;
65 | this.awsConfig = awsConfig;
66 | }
67 |
68 | String availabilityZoneEc2() {
69 | String uri = ec2MetadataEndpoint.concat("/placement/availability-zone/");
70 | return createRestClient(uri, awsConfig).get().getBody();
71 | }
72 |
73 | Optional placementGroupEc2() {
74 | return getOptionalMetadata(ec2MetadataEndpoint.concat("/placement/group-name/"), "placement group");
75 | }
76 |
77 | Optional placementPartitionNumberEc2() {
78 | return getOptionalMetadata(ec2MetadataEndpoint.concat("/placement/partition-number/"), "partition number");
79 | }
80 |
81 | /**
82 | * Resolves an optional metadata that exists for some instances only.
83 | * HTTP_OK and HTTP_NOT_FOUND responses are assumed valid. Any other
84 | * response code or an exception thrown during retries will yield
85 | * a warning log and an empty result will be returned.
86 | *
87 | * @param uri Metadata URI
88 | * @param loggedName Metadata name to be used when logging.
89 | * @return The metadata if the endpoint exists, empty otherwise.
90 | */
91 | private Optional getOptionalMetadata(String uri, String loggedName) {
92 | RestClient.Response response;
93 | try {
94 | response = createRestClient(uri, awsConfig)
95 | .expectResponseCodes(HTTP_OK, HTTP_NOT_FOUND)
96 | .get();
97 | } catch (Exception e) {
98 | // Failed to get a response with code OK or NOT_FOUND after retries
99 | LOGGER.warning(String.format("Could not resolve the %s metadata", loggedName));
100 | return Optional.empty();
101 | }
102 | int responseCode = response.getCode();
103 | if (responseCode == HTTP_OK) {
104 | return Optional.of(response.getBody());
105 | } else if (responseCode == HTTP_NOT_FOUND) {
106 | LOGGER.fine(String.format("No %s information is found.", loggedName));
107 | return Optional.empty();
108 | } else {
109 | throw new RuntimeException(String.format("Unexpected response code: %d", responseCode));
110 | }
111 | }
112 |
113 | String defaultIamRoleEc2() {
114 | String uri = ec2MetadataEndpoint.concat(SECURITY_CREDENTIALS_URI);
115 | return createRestClient(uri, awsConfig).get().getBody();
116 | }
117 |
118 | AwsCredentials credentialsEc2(String iamRole) {
119 | String uri = ec2MetadataEndpoint.concat(SECURITY_CREDENTIALS_URI).concat(iamRole);
120 | String response = createRestClient(uri, awsConfig).get().getBody();
121 | return parseCredentials(response);
122 | }
123 |
124 | AwsCredentials credentialsEcs() {
125 | String response = createRestClient(ecsIamRoleEndpoint, awsConfig).get().getBody();
126 | return parseCredentials(response);
127 | }
128 |
129 | private static AwsCredentials parseCredentials(String response) {
130 | JsonObject role = Json.parse(response).asObject();
131 | return AwsCredentials.builder()
132 | .setAccessKey(role.getString("AccessKeyId", null))
133 | .setSecretKey(role.getString("SecretAccessKey", null))
134 | .setToken(role.getString("Token", null))
135 | .build();
136 | }
137 |
138 | EcsMetadata metadataEcs() {
139 | String response = createRestClient(ecsTaskMetadataEndpoint, awsConfig).get().getBody();
140 | return parseEcsMetadata(response);
141 | }
142 |
143 | private EcsMetadata parseEcsMetadata(String response) {
144 | JsonObject metadata = Json.parse(response).asObject();
145 | JsonObject labels = metadata.get("Labels").asObject();
146 | String taskArn = labels.get("com.amazonaws.ecs.task-arn").asString();
147 | String clusterArn = labels.get("com.amazonaws.ecs.cluster").asString();
148 | return new EcsMetadata(taskArn, clusterArn);
149 | }
150 |
151 | static class EcsMetadata {
152 | private final String taskArn;
153 | private final String clusterArn;
154 |
155 | EcsMetadata(String taskArn, String clusterArn) {
156 | this.taskArn = taskArn;
157 | this.clusterArn = clusterArn;
158 | }
159 |
160 | String getTaskArn() {
161 | return taskArn;
162 | }
163 |
164 | String getClusterArn() {
165 | return clusterArn;
166 | }
167 | }
168 | }
169 |
--------------------------------------------------------------------------------
/src/main/java/com/hazelcast/aws/AwsProperties.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Hazelcast Inc.
3 | *
4 | * Licensed under the Hazelcast Community License (the "License"); you may not use
5 | * this file except in compliance with the License. You may obtain a copy of the
6 | * License at
7 | *
8 | * http://hazelcast.com/hazelcast-community-license
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 | * WARRANTIES OF ANY KIND, either express or implied. See the License for the
13 | * specific language governing permissions and limitations under the License.
14 | */
15 |
16 | package com.hazelcast.aws;
17 |
18 | import com.hazelcast.config.properties.PropertyDefinition;
19 | import com.hazelcast.config.properties.PropertyTypeConverter;
20 | import com.hazelcast.config.properties.SimplePropertyDefinition;
21 |
22 | import static com.hazelcast.config.properties.PropertyTypeConverter.INTEGER;
23 | import static com.hazelcast.config.properties.PropertyTypeConverter.STRING;
24 |
25 | /**
26 | * Configuration properties for the Hazelcast Discovery Plugin for AWS. For more information
27 | * see {@link AwsConfig}.
28 | */
29 | enum AwsProperties {
30 |
31 | /**
32 | * Access key of your account on EC2
33 | */
34 | ACCESS_KEY("access-key", STRING, true),
35 |
36 | /**
37 | * Secret key of your account on EC2
38 | */
39 | SECRET_KEY("secret-key", STRING, true),
40 |
41 | /**
42 | * The region where your members are running.
43 | *
44 | * If not defined, the current instance region is used.
45 | */
46 | REGION("region", STRING, true),
47 |
48 | /**
49 | * IAM roles are used to make secure requests from your clients. You can provide the name
50 | * of your IAM role that you created previously on your AWS console.
51 | */
52 | IAM_ROLE("iam-role", STRING, true),
53 |
54 | /**
55 | * The URL that is the entry point for a web service (the address where the EC2 API can be found).
56 | */
57 | HOST_HEADER("host-header", STRING, true),
58 |
59 | /**
60 | * Name of the security group you specified at the EC2 management console. It is used to narrow the Hazelcast members to
61 | * be within this group. It is optional.
62 | */
63 | SECURITY_GROUP_NAME("security-group-name", STRING, true),
64 |
65 | /**
66 | * Tag key as specified in the EC2 console. It is used to narrow the members returned by the discovery mechanism.
67 | *
68 | * Can support multiple tag keys if separated by commas (e.g. {@code "TagKeyA,TagKeyB"}).
69 | */
70 | TAG_KEY("tag-key", STRING, true),
71 |
72 | /**
73 | * Tag value as specified in the EC2 console. It is used to narrow the members returned by the discovery mechanism.
74 | *
75 | * Can support multiple tag values if separated by commas (e.g. {@code "TagValueA,TagValueB"}).
76 | */
77 | TAG_VALUE("tag-value", STRING, true),
78 |
79 | /**
80 | * Sets the connect timeout in seconds.
81 | *
82 | * Its default value is 10.
83 | */
84 | CONNECTION_TIMEOUT_SECONDS("connection-timeout-seconds", INTEGER, true),
85 |
86 | /**
87 | * Number of retries while connecting to AWS Services. Its default value is 3.
88 | *
89 | * Hazelcast AWS plugin uses two AWS services: Describe Instances and EC2 Instance Metadata.
90 | */
91 | CONNECTION_RETRIES("connection-retries", INTEGER, true),
92 |
93 | /**
94 | * Sets the read timeout in seconds. Its default value is 10.
95 | */
96 | READ_TIMEOUT_SECONDS("read-timeout-seconds", INTEGER, true),
97 |
98 | /**
99 | * The discovery mechanism will discover only IP addresses. You can define the port or the port range on which
100 | * Hazelcast is
101 | * expected to be running.
102 | *
103 | * Sample values: "5701", "5701-5710".
104 | *
105 | * The default value is "5701-5708".
106 | */
107 | PORT("hz-port", STRING, true),
108 |
109 | /**
110 | * ECS Cluster name or Cluster ARN.
111 | *
112 | * If not defined, the current ECS Task cluster is used.
113 | */
114 | CLUSTER("cluster", STRING, true),
115 |
116 | /**
117 | * ECS Task Family name that is used to narrow the Hazelcast members to be within the same Task Definition
118 | * family.
119 | *
120 | * It is optional.
121 | *
122 | * Note that this option is mutually exclusive with "service-name".
123 | */
124 | FAMILY("family", STRING, true),
125 |
126 | /**
127 | * ECS Task Service Name that is used to narrow the Hazelcast members to be within the same ECS Service.
128 | *
129 | * It is optional.
130 | *
131 | * Note that this option is mutually exclusive with "family".
132 | */
133 | SERVICE_NAME("service-name", STRING, true);
134 |
135 | private final PropertyDefinition propertyDefinition;
136 |
137 | AwsProperties(String key, PropertyTypeConverter typeConverter, boolean optional) {
138 | this.propertyDefinition = new SimplePropertyDefinition(key, optional, typeConverter);
139 | }
140 |
141 | PropertyDefinition getDefinition() {
142 | return propertyDefinition;
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/src/main/java/com/hazelcast/aws/AwsRequestSigner.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Hazelcast Inc.
3 | *
4 | * Licensed under the Hazelcast Community License (the "License"); you may not use
5 | * this file except in compliance with the License. You may obtain a copy of the
6 | * License at
7 | *
8 | * http://hazelcast.com/hazelcast-community-license
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 | * WARRANTIES OF ANY KIND, either express or implied. See the License for the
13 | * specific language governing permissions and limitations under the License.
14 | */
15 |
16 | package com.hazelcast.aws;
17 |
18 | import com.hazelcast.internal.util.QuickMath;
19 |
20 | import javax.crypto.Mac;
21 | import javax.crypto.spec.SecretKeySpec;
22 | import java.security.InvalidKeyException;
23 | import java.security.MessageDigest;
24 | import java.security.NoSuchAlgorithmException;
25 | import java.util.Map;
26 | import java.util.TreeMap;
27 |
28 | import static com.hazelcast.aws.AwsRequestUtils.canonicalQueryString;
29 | import static java.lang.String.format;
30 | import static java.nio.charset.StandardCharsets.UTF_8;
31 |
32 | /**
33 | * Responsible for signing AWS Requests with the Signature version 4.
34 | *
35 | * The signing steps are described in the AWS Documentation.
36 | *
37 | * @see Signature Version 4 Signing Process
38 | */
39 | class AwsRequestSigner {
40 | private static final String SIGNATURE_METHOD_V4 = "AWS4-HMAC-SHA256";
41 | private static final String HMAC_SHA256 = "HmacSHA256";
42 | private static final int TIMESTAMP_FIELD_LENGTH = 8;
43 |
44 | private final String region;
45 | private final String service;
46 |
47 | AwsRequestSigner(String region, String service) {
48 | this.region = region;
49 | this.service = service;
50 | }
51 |
52 | String authHeader(Map attributes, Map headers, String body,
53 | AwsCredentials credentials, String timestamp, String httpMethod) {
54 | return buildAuthHeader(
55 | credentials.getAccessKey(),
56 | credentialScopeEcs(timestamp),
57 | signedHeaders(headers),
58 | sign(attributes, headers, body, credentials, timestamp, httpMethod)
59 | );
60 | }
61 |
62 | private String buildAuthHeader(String accessKey, String credentialScope, String signedHeaders, String signature) {
63 | return String.format("%s Credential=%s/%s, SignedHeaders=%s, Signature=%s",
64 | SIGNATURE_METHOD_V4, accessKey, credentialScope, signedHeaders, signature);
65 | }
66 |
67 | private String credentialScopeEcs(String timestamp) {
68 | // datestamp/region/service/API_TERMINATOR
69 | return format("%s/%s/%s/%s", datestamp(timestamp), region, service, "aws4_request");
70 | }
71 |
72 | private String sign(Map attributes, Map headers, String body,
73 | AwsCredentials credentials, String timestamp, String httpMethod) {
74 | String canonicalRequest = canonicalRequest(attributes, headers, body, httpMethod);
75 | String stringToSign = stringToSign(canonicalRequest, timestamp);
76 | byte[] signingKey = signingKey(credentials, timestamp);
77 | return createSignature(stringToSign, signingKey);
78 | }
79 |
80 | /* Task 1 */
81 | private String canonicalRequest(Map attributes, Map headers,
82 | String body, String httpMethod) {
83 | return String.format("%s\n/\n%s\n%s\n%s\n%s",
84 | httpMethod,
85 | canonicalQueryString(attributes),
86 | canonicalHeaders(headers),
87 | signedHeaders(headers),
88 | sha256Hashhex(body)
89 | );
90 | }
91 |
92 | private String canonicalHeaders(Map headers) {
93 | StringBuilder canonical = new StringBuilder();
94 | for (Map.Entry entry : sortedLowercase(headers).entrySet()) {
95 | canonical.append(format("%s:%s\n", entry.getKey(), entry.getValue()));
96 | }
97 | return canonical.toString();
98 | }
99 |
100 | /* Task 2 */
101 | private String stringToSign(String canonicalRequest, String timestamp) {
102 | return String.format("%s\n%s\n%s\n%s",
103 | SIGNATURE_METHOD_V4,
104 | timestamp,
105 | credentialScope(timestamp),
106 | sha256Hashhex(canonicalRequest)
107 | );
108 | }
109 |
110 | private String credentialScope(String timestamp) {
111 | // datestamp/region/service/API_TERMINATOR
112 | return format("%s/%s/%s/%s", datestamp(timestamp), region, service, "aws4_request");
113 | }
114 |
115 | /* Task 3 */
116 | private byte[] signingKey(AwsCredentials credentials, String timestamp) {
117 | String signKey = credentials.getSecretKey();
118 | // this is derived from
119 | // http://docs.aws.amazon.com/general/latest/gr/signature-v4-examples.html#signature-v4-examples-python
120 |
121 | try {
122 | String key = "AWS4" + signKey;
123 | Mac mDate = Mac.getInstance(HMAC_SHA256);
124 | SecretKeySpec skDate = new SecretKeySpec(key.getBytes(UTF_8), HMAC_SHA256);
125 | mDate.init(skDate);
126 | byte[] kDate = mDate.doFinal(datestamp(timestamp).getBytes(UTF_8));
127 |
128 | Mac mRegion = Mac.getInstance(HMAC_SHA256);
129 | SecretKeySpec skRegion = new SecretKeySpec(kDate, HMAC_SHA256);
130 | mRegion.init(skRegion);
131 | byte[] kRegion = mRegion.doFinal(region.getBytes(UTF_8));
132 |
133 | Mac mService = Mac.getInstance(HMAC_SHA256);
134 | SecretKeySpec skService = new SecretKeySpec(kRegion, HMAC_SHA256);
135 | mService.init(skService);
136 | byte[] kService = mService.doFinal(service.getBytes(UTF_8));
137 |
138 | Mac mSigning = Mac.getInstance(HMAC_SHA256);
139 | SecretKeySpec skSigning = new SecretKeySpec(kService, HMAC_SHA256);
140 | mSigning.init(skSigning);
141 |
142 | return mSigning.doFinal("aws4_request".getBytes(UTF_8));
143 | } catch (NoSuchAlgorithmException | InvalidKeyException e) {
144 | return null;
145 | }
146 | }
147 |
148 | private String createSignature(String stringToSign, byte[] signingKey) {
149 | try {
150 | Mac signMac = Mac.getInstance(HMAC_SHA256);
151 | SecretKeySpec signKS = new SecretKeySpec(signingKey, HMAC_SHA256);
152 | signMac.init(signKS);
153 | byte[] signature = signMac.doFinal(stringToSign.getBytes(UTF_8));
154 | return QuickMath.bytesToHex(signature);
155 | } catch (NoSuchAlgorithmException | InvalidKeyException e) {
156 | return null;
157 | }
158 | }
159 |
160 | private static String datestamp(String timestamp) {
161 | return timestamp.substring(0, TIMESTAMP_FIELD_LENGTH);
162 | }
163 |
164 | private String signedHeaders(Map headers) {
165 | return String.join(";", sortedLowercase(headers).keySet());
166 | }
167 |
168 | private Map sortedLowercase(Map headers) {
169 | Map sortedHeaders = new TreeMap<>();
170 | for (Map.Entry e : headers.entrySet()) {
171 | sortedHeaders.put(e.getKey().toLowerCase(), e.getValue());
172 | }
173 | return sortedHeaders;
174 | }
175 |
176 | private static String sha256Hashhex(String in) {
177 | String payloadHash;
178 | try {
179 | MessageDigest md = MessageDigest.getInstance("SHA-256");
180 | md.update(in.getBytes(UTF_8));
181 | byte[] digest = md.digest();
182 | payloadHash = QuickMath.bytesToHex(digest);
183 | } catch (NoSuchAlgorithmException e) {
184 | return null;
185 | }
186 | return payloadHash;
187 | }
188 | }
189 |
--------------------------------------------------------------------------------
/src/main/java/com/hazelcast/aws/AwsRequestUtils.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Hazelcast Inc.
3 | *
4 | * Licensed under the Hazelcast Community License (the "License"); you may not use
5 | * this file except in compliance with the License. You may obtain a copy of the
6 | * License at
7 | *
8 | * http://hazelcast.com/hazelcast-community-license
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 | * WARRANTIES OF ANY KIND, either express or implied. See the License for the
13 | * specific language governing permissions and limitations under the License.
14 | */
15 |
16 | package com.hazelcast.aws;
17 |
18 | import com.hazelcast.core.HazelcastException;
19 |
20 | import java.io.UnsupportedEncodingException;
21 | import java.net.URLEncoder;
22 | import java.text.SimpleDateFormat;
23 | import java.time.Clock;
24 | import java.time.Instant;
25 | import java.util.ArrayList;
26 | import java.util.Collections;
27 | import java.util.Iterator;
28 | import java.util.List;
29 | import java.util.Map;
30 | import java.util.TimeZone;
31 |
32 | /**
33 | * Utility class for AWS Requests.
34 | */
35 | final class AwsRequestUtils {
36 |
37 | private AwsRequestUtils() {
38 | }
39 |
40 | static String currentTimestamp(Clock clock) {
41 | SimpleDateFormat df = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'");
42 | df.setTimeZone(TimeZone.getTimeZone("UTC"));
43 | return df.format(Instant.now(clock).toEpochMilli());
44 | }
45 |
46 | static RestClient createRestClient(String url, AwsConfig awsConfig) {
47 | return RestClient.create(url)
48 | .withConnectTimeoutSeconds(awsConfig.getConnectionTimeoutSeconds())
49 | .withReadTimeoutSeconds(awsConfig.getReadTimeoutSeconds())
50 | .withRetries(awsConfig.getConnectionRetries());
51 | }
52 |
53 | static String canonicalQueryString(Map attributes) {
54 | List components = getListOfEntries(attributes);
55 | Collections.sort(components);
56 | return canonicalQueryString(components);
57 | }
58 |
59 | private static List getListOfEntries(Map entries) {
60 | List components = new ArrayList<>();
61 | for (String key : entries.keySet()) {
62 | addComponents(components, entries, key);
63 | }
64 | return components;
65 | }
66 |
67 | private static String canonicalQueryString(List list) {
68 | Iterator it = list.iterator();
69 | StringBuilder result = new StringBuilder();
70 | if (it.hasNext()) {
71 | result.append(it.next());
72 | }
73 | while (it.hasNext()) {
74 | result.append('&').append(it.next());
75 | }
76 | return result.toString();
77 | }
78 |
79 | private static void addComponents(List components, Map attributes, String key) {
80 | components.add(urlEncode(key) + '=' + urlEncode(attributes.get(key)));
81 | }
82 |
83 | private static String urlEncode(String string) {
84 | String encoded;
85 | try {
86 | encoded = URLEncoder.encode(string, "UTF-8")
87 | .replace("+", "%20")
88 | .replace("*", "%2A");
89 | } catch (UnsupportedEncodingException e) {
90 | throw new HazelcastException(e);
91 | }
92 | return encoded;
93 | }
94 |
95 | static String urlFor(String endpoint) {
96 | if (endpoint.startsWith("http")) {
97 | return endpoint;
98 | }
99 | return "https://" + endpoint;
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/src/main/java/com/hazelcast/aws/Environment.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Hazelcast Inc.
3 | *
4 | * Licensed under the Hazelcast Community License (the "License"); you may not use
5 | * this file except in compliance with the License. You may obtain a copy of the
6 | * License at
7 | *
8 | * http://hazelcast.com/hazelcast-community-license
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 | * WARRANTIES OF ANY KIND, either express or implied. See the License for the
13 | * specific language governing permissions and limitations under the License.
14 | */
15 |
16 | package com.hazelcast.aws;
17 |
18 | import static com.hazelcast.aws.StringUtils.isNotEmpty;
19 |
20 | /**
21 | * This class is introduced to lookup system parameters.
22 | */
23 | class Environment {
24 | private static final String AWS_REGION_ON_ECS = System.getenv("AWS_REGION");
25 | private static final boolean IS_RUNNING_ON_ECS = isRunningOnEcsEnvironment();
26 |
27 | String getAwsRegionOnEcs() {
28 | return AWS_REGION_ON_ECS;
29 | }
30 |
31 | boolean isRunningOnEcs() {
32 | return IS_RUNNING_ON_ECS;
33 | }
34 |
35 | private static boolean isRunningOnEcsEnvironment() {
36 | String execEnv = System.getenv("AWS_EXECUTION_ENV");
37 | return isNotEmpty(execEnv) && execEnv.contains("ECS");
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/main/java/com/hazelcast/aws/Filter.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Hazelcast Inc.
3 | *
4 | * Licensed under the Hazelcast Community License (the "License"); you may not use
5 | * this file except in compliance with the License. You may obtain a copy of the
6 | * License at
7 | *
8 | * http://hazelcast.com/hazelcast-community-license
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 | * WARRANTIES OF ANY KIND, either express or implied. See the License for the
13 | * specific language governing permissions and limitations under the License.
14 | */
15 |
16 | package com.hazelcast.aws;
17 |
18 | import java.util.Collection;
19 | import java.util.HashMap;
20 | import java.util.Map;
21 |
22 | /**
23 | * Query filter to narrow down the scope of the queried EC2 instance set.
24 | */
25 | final class Filter {
26 |
27 | private Map filters = new HashMap<>();
28 |
29 | private int index = 1;
30 |
31 | void add(String name, String value) {
32 | filters.put("Filter." + index + ".Name", name);
33 | filters.put("Filter." + index + ".Value.1", value);
34 | ++index;
35 | }
36 |
37 | void addMulti(String name, Collection values) {
38 | if (values.size() > 0) {
39 | filters.put("Filter." + index + ".Name", name);
40 | int valueIndex = 1;
41 | for (String value : values) {
42 | filters.put(String.format("Filter.%d.Value.%d", index, valueIndex++), value);
43 | }
44 | ++index;
45 | }
46 | }
47 |
48 | Map getFilterAttributes() {
49 | return new HashMap<>(filters);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/main/java/com/hazelcast/aws/NoCredentialsException.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Hazelcast Inc.
3 | *
4 | * Licensed under the Hazelcast Community License (the "License"); you may not use
5 | * this file except in compliance with the License. You may obtain a copy of the
6 | * License at
7 | *
8 | * http://hazelcast.com/hazelcast-community-license
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 | * WARRANTIES OF ANY KIND, either express or implied. See the License for the
13 | * specific language governing permissions and limitations under the License.
14 | */
15 |
16 | package com.hazelcast.aws;
17 |
18 | /**
19 | * Exception to indicate that no credentials are possible to retrieve.
20 | */
21 | class NoCredentialsException extends RuntimeException {
22 | }
23 |
--------------------------------------------------------------------------------
/src/main/java/com/hazelcast/aws/PortRange.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Hazelcast Inc.
3 | *
4 | * Licensed under the Hazelcast Community License (the "License"); you may not use
5 | * this file except in compliance with the License. You may obtain a copy of the
6 | * License at
7 | *
8 | * http://hazelcast.com/hazelcast-community-license
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 | * WARRANTIES OF ANY KIND, either express or implied. See the License for the
13 | * specific language governing permissions and limitations under the License.
14 | */
15 |
16 | package com.hazelcast.aws;
17 |
18 | import java.util.regex.Matcher;
19 | import java.util.regex.Pattern;
20 |
21 | /**
22 | * Represents the range of IPv4 Ports.
23 | */
24 | final class PortRange {
25 | private static final Pattern PORT_NUMBER_REGEX = Pattern.compile("^(\\d+)$");
26 | private static final Pattern PORT_RANGE_REGEX = Pattern.compile("^(\\d+)-(\\d+)$");
27 |
28 | private static final int MIN_PORT = 0;
29 | private static final int MAX_PORT = 65535;
30 |
31 | private final int fromPort;
32 | private final int toPort;
33 |
34 | /**
35 | * Creates {@link PortRange} from the {@code spec} String.
36 | *
37 | * @param spec port number (e.g "5701") or port range (e.g. "5701-5708")
38 | * @throws IllegalArgumentException if the specified spec is not a valid port or port range
39 | */
40 | PortRange(String spec) {
41 | Matcher portNumberMatcher = PORT_NUMBER_REGEX.matcher(spec);
42 | Matcher portRangeMatcher = PORT_RANGE_REGEX.matcher(spec);
43 | if (portNumberMatcher.find()) {
44 | int port = Integer.parseInt(spec);
45 | this.fromPort = port;
46 | this.toPort = port;
47 | } else if (portRangeMatcher.find()) {
48 | this.fromPort = Integer.parseInt(portRangeMatcher.group(1));
49 | this.toPort = Integer.parseInt(portRangeMatcher.group(2));
50 | } else {
51 | throw new IllegalArgumentException(String.format("Invalid port range specification: %s", spec));
52 | }
53 |
54 | validatePorts();
55 | }
56 |
57 | private void validatePorts() {
58 | if (fromPort < MIN_PORT || fromPort > MAX_PORT) {
59 | throw new IllegalArgumentException(
60 | String.format("Specified port (%s) outside of port range (%s-%s)", fromPort, MIN_PORT, MAX_PORT));
61 | }
62 | if (toPort < MIN_PORT || toPort > MAX_PORT) {
63 | throw new IllegalArgumentException(
64 | String.format("Specified port (%s) outside of port range (%s-%s)", toPort, MIN_PORT, MAX_PORT));
65 | }
66 | if (fromPort > toPort) {
67 | throw new IllegalArgumentException(String.format("Port %s is greater than %s", fromPort, toPort));
68 | }
69 | }
70 |
71 | int getFromPort() {
72 | return fromPort;
73 | }
74 |
75 | int getToPort() {
76 | return toPort;
77 | }
78 |
79 | @Override
80 | public String toString() {
81 | return String.format("%d-%d", fromPort, toPort);
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/main/java/com/hazelcast/aws/RegionValidator.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Hazelcast Inc.
3 | *
4 | * Licensed under the Hazelcast Community License (the "License"); you may not use
5 | * this file except in compliance with the License. You may obtain a copy of the
6 | * License at
7 | *
8 | * http://hazelcast.com/hazelcast-community-license
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 | * WARRANTIES OF ANY KIND, either express or implied. See the License for the
13 | * specific language governing permissions and limitations under the License.
14 | */
15 |
16 | package com.hazelcast.aws;
17 |
18 | import com.hazelcast.config.InvalidConfigurationException;
19 |
20 | import java.util.regex.Pattern;
21 |
22 | /**
23 | * Helper class used to validate AWS Region.
24 | */
25 | final class RegionValidator {
26 | private static final Pattern AWS_REGION_PATTERN =
27 | Pattern.compile("\\w{2}(-gov-|-)(north|northeast|east|southeast|south|southwest|west|northwest|central)-\\d(?!.+)");
28 |
29 | private RegionValidator() {
30 | }
31 |
32 | static void validateRegion(String region) {
33 | if (!AWS_REGION_PATTERN.matcher(region).matches()) {
34 | String message = String.format("The provided region %s is not a valid AWS region.", region);
35 | throw new InvalidConfigurationException(message);
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/main/java/com/hazelcast/aws/RestClient.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Hazelcast Inc.
3 | *
4 | * Licensed under the Hazelcast Community License (the "License"); you may not use
5 | * this file except in compliance with the License. You may obtain a copy of the
6 | * License at
7 | *
8 | * http://hazelcast.com/hazelcast-community-license
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 | * WARRANTIES OF ANY KIND, either express or implied. See the License for the
13 | * specific language governing permissions and limitations under the License.
14 | */
15 |
16 | package com.hazelcast.aws;
17 |
18 | import java.io.DataOutputStream;
19 | import java.io.IOException;
20 | import java.io.InputStream;
21 | import java.net.HttpURLConnection;
22 | import java.net.URL;
23 | import java.nio.charset.StandardCharsets;
24 | import java.util.ArrayList;
25 | import java.util.Arrays;
26 | import java.util.HashSet;
27 | import java.util.List;
28 | import java.util.Map;
29 | import java.util.Scanner;
30 | import java.util.Set;
31 | import java.util.concurrent.TimeUnit;
32 |
33 | final class RestClient {
34 |
35 | static final int HTTP_OK = 200;
36 | static final int HTTP_NOT_FOUND = 404;
37 |
38 | private final String url;
39 | private final List headers = new ArrayList<>();
40 | private Set expectedResponseCodes;
41 | private String body;
42 | private int readTimeoutSeconds;
43 | private int connectTimeoutSeconds;
44 | private int retries;
45 |
46 | private RestClient(String url) {
47 | this.url = url;
48 | }
49 |
50 | static RestClient create(String url) {
51 | return new RestClient(url);
52 | }
53 |
54 | RestClient withHeaders(Map headers) {
55 | for (Map.Entry entry : headers.entrySet()) {
56 | this.headers.add(new Parameter(entry.getKey(), entry.getValue()));
57 | }
58 | return this;
59 | }
60 |
61 | RestClient withBody(String body) {
62 | this.body = body;
63 | return this;
64 | }
65 |
66 | RestClient withReadTimeoutSeconds(int readTimeoutSeconds) {
67 | this.readTimeoutSeconds = readTimeoutSeconds;
68 | return this;
69 | }
70 |
71 | RestClient withConnectTimeoutSeconds(int connectTimeoutSeconds) {
72 | this.connectTimeoutSeconds = connectTimeoutSeconds;
73 | return this;
74 | }
75 |
76 | RestClient withRetries(int retries) {
77 | this.retries = retries;
78 | return this;
79 | }
80 |
81 | RestClient expectResponseCodes(Integer... codes) {
82 | if (expectedResponseCodes == null) {
83 | expectedResponseCodes = new HashSet<>();
84 | }
85 | expectedResponseCodes.addAll(Arrays.asList(codes));
86 | return this;
87 | }
88 |
89 | Response get() {
90 | return callWithRetries("GET");
91 | }
92 |
93 | Response post() {
94 | return callWithRetries("POST");
95 | }
96 |
97 | private Response callWithRetries(String method) {
98 | return RetryUtils.retry(() -> call(method), retries);
99 | }
100 |
101 | private Response call(String method) {
102 | HttpURLConnection connection = null;
103 | try {
104 | URL urlToConnect = new URL(url);
105 | connection = (HttpURLConnection) urlToConnect.openConnection();
106 | connection.setReadTimeout((int) TimeUnit.SECONDS.toMillis(readTimeoutSeconds));
107 | connection.setConnectTimeout((int) TimeUnit.SECONDS.toMillis(connectTimeoutSeconds));
108 | connection.setRequestMethod(method);
109 | for (Parameter header : headers) {
110 | connection.setRequestProperty(header.getKey(), header.getValue());
111 | }
112 | if (body != null) {
113 | byte[] bodyData = body.getBytes(StandardCharsets.UTF_8);
114 |
115 | connection.setDoOutput(true);
116 | connection.setRequestProperty("charset", "utf-8");
117 | connection.setRequestProperty("Content-Length", Integer.toString(bodyData.length));
118 |
119 | try (DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream())) {
120 | outputStream.write(bodyData);
121 | outputStream.flush();
122 | }
123 | }
124 |
125 | checkResponseCode(method, connection);
126 | return new Response(connection.getResponseCode(), read(connection));
127 | } catch (IOException e) {
128 | throw new RestClientException("Failure in executing REST call", e);
129 | } finally {
130 | if (connection != null) {
131 | connection.disconnect();
132 | }
133 | }
134 | }
135 |
136 | private void checkResponseCode(String method, HttpURLConnection connection)
137 | throws IOException {
138 | int responseCode = connection.getResponseCode();
139 | if (!isExpectedResponseCode(responseCode)) {
140 | String errorMessage;
141 | try {
142 | errorMessage = read(connection);
143 | } catch (Exception e) {
144 | throw new RestClientException(
145 | String.format("Failure executing: %s at: %s", method, url), responseCode);
146 | }
147 | throw new RestClientException(String.format("Failure executing: %s at: %s. Message: %s", method, url, errorMessage),
148 | responseCode);
149 | }
150 | }
151 |
152 | private boolean isExpectedResponseCode(int responseCode) {
153 | // expect HTTP_OK by default
154 | return expectedResponseCodes == null
155 | ? responseCode == HTTP_OK
156 | : expectedResponseCodes.contains(responseCode);
157 | }
158 |
159 | private static String read(HttpURLConnection connection) {
160 | InputStream stream;
161 | try {
162 | stream = connection.getInputStream();
163 | } catch (IOException e) {
164 | stream = connection.getErrorStream();
165 | }
166 | if (stream == null) {
167 | return null;
168 | }
169 | Scanner scanner = new Scanner(stream, "UTF-8");
170 | scanner.useDelimiter("\\Z");
171 | return scanner.next();
172 | }
173 |
174 | static class Response {
175 |
176 | private final int code;
177 | private final String body;
178 |
179 | Response(int code, String body) {
180 | this.code = code;
181 | this.body = body;
182 | }
183 |
184 | int getCode() {
185 | return code;
186 | }
187 |
188 | String getBody() {
189 | return body;
190 | }
191 | }
192 |
193 | private static final class Parameter {
194 | private final String key;
195 | private final String value;
196 |
197 | private Parameter(String key, String value) {
198 | this.key = key;
199 | this.value = value;
200 | }
201 |
202 | private String getKey() {
203 | return key;
204 | }
205 |
206 | private String getValue() {
207 | return value;
208 | }
209 | }
210 |
211 | }
212 |
--------------------------------------------------------------------------------
/src/main/java/com/hazelcast/aws/RestClientException.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Hazelcast Inc.
3 | *
4 | * Licensed under the Hazelcast Community License (the "License"); you may not use
5 | * this file except in compliance with the License. You may obtain a copy of the
6 | * License at
7 | *
8 | * http://hazelcast.com/hazelcast-community-license
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 | * WARRANTIES OF ANY KIND, either express or implied. See the License for the
13 | * specific language governing permissions and limitations under the License.
14 | */
15 |
16 | package com.hazelcast.aws;
17 |
18 | /**
19 | * Exception to indicate any issues while executing a REST call.
20 | */
21 | class RestClientException
22 | extends RuntimeException {
23 | private int httpErrorCode;
24 |
25 | RestClientException(String message, int httpErrorCode) {
26 | super(String.format("%s. HTTP Error Code: %s", message, httpErrorCode));
27 | this.httpErrorCode = httpErrorCode;
28 | }
29 |
30 | RestClientException(String message, Throwable cause) {
31 | super(message, cause);
32 | }
33 |
34 | int getHttpErrorCode() {
35 | return httpErrorCode;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/main/java/com/hazelcast/aws/RetryUtils.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Hazelcast Inc.
3 | *
4 | * Licensed under the Hazelcast Community License (the "License"); you may not use
5 | * this file except in compliance with the License. You may obtain a copy of the
6 | * License at
7 | *
8 | * http://hazelcast.com/hazelcast-community-license
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 | * WARRANTIES OF ANY KIND, either express or implied. See the License for the
13 | * specific language governing permissions and limitations under the License.
14 | */
15 |
16 | package com.hazelcast.aws;
17 |
18 | import com.hazelcast.core.HazelcastException;
19 | import com.hazelcast.logging.ILogger;
20 | import com.hazelcast.logging.Logger;
21 |
22 | import java.util.concurrent.Callable;
23 |
24 | /**
25 | * Static utility class to retry operations related to connecting to AWS Services.
26 | */
27 | final class RetryUtils {
28 | private static final long INITIAL_BACKOFF_MS = 1500L;
29 | private static final long MAX_BACKOFF_MS = 5 * 60 * 1000L;
30 | private static final double BACKOFF_MULTIPLIER = 1.5;
31 |
32 | private static final ILogger LOGGER = Logger.getLogger(RetryUtils.class);
33 |
34 | private static final long MS_IN_SECOND = 1000L;
35 |
36 | private RetryUtils() {
37 | }
38 |
39 | /**
40 | * Calls {@code callable.call()} until it does not throw an exception (but no more than {@code retries} times).
41 | *
42 | * Note that {@code callable} should be an idempotent operation which is a call to the AWS Service.
43 | *
44 | * If {@code callable} throws an unchecked exception, it is wrapped into {@link HazelcastException}.
45 | */
46 | static T retry(Callable callable, int retries) {
47 | int retryCount = 0;
48 | while (true) {
49 | try {
50 | return callable.call();
51 | } catch (Exception e) {
52 | retryCount++;
53 | if (retryCount > retries) {
54 | throw unchecked(e);
55 | }
56 | long waitIntervalMs = backoffIntervalForRetry(retryCount);
57 | LOGGER.fine(String.format("Couldn't connect to the AWS service, [%s] retrying in %s seconds...",
58 | retryCount, waitIntervalMs / MS_IN_SECOND));
59 | sleep(waitIntervalMs);
60 | }
61 | }
62 | }
63 |
64 | private static RuntimeException unchecked(Exception e) {
65 | if (e instanceof RuntimeException) {
66 | return (RuntimeException) e;
67 | }
68 | return new HazelcastException(e);
69 | }
70 |
71 | private static long backoffIntervalForRetry(int retryCount) {
72 | long result = INITIAL_BACKOFF_MS;
73 | for (int i = 1; i < retryCount; i++) {
74 | result *= BACKOFF_MULTIPLIER;
75 | if (result > MAX_BACKOFF_MS) {
76 | return MAX_BACKOFF_MS;
77 | }
78 | }
79 | return result;
80 | }
81 |
82 | private static void sleep(long millis) {
83 | try {
84 | Thread.sleep(millis);
85 | } catch (InterruptedException e) {
86 | Thread.currentThread().interrupt();
87 | throw new HazelcastException(e);
88 | }
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/main/java/com/hazelcast/aws/StringUtils.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Hazelcast Inc.
3 | *
4 | * Licensed under the Hazelcast Community License (the "License"); you may not use
5 | * this file except in compliance with the License. You may obtain a copy of the
6 | * License at
7 | *
8 | * http://hazelcast.com/hazelcast-community-license
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 | * WARRANTIES OF ANY KIND, either express or implied. See the License for the
13 | * specific language governing permissions and limitations under the License.
14 | */
15 |
16 | package com.hazelcast.aws;
17 |
18 | final class StringUtils {
19 |
20 | private StringUtils() {
21 | }
22 |
23 | static boolean isEmpty(String s) {
24 | if (s == null) {
25 | return true;
26 | }
27 | return s.trim().isEmpty();
28 | }
29 |
30 | static boolean isNotEmpty(String s) {
31 | return !isEmpty(s);
32 | }
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/src/main/java/com/hazelcast/aws/Tag.java:
--------------------------------------------------------------------------------
1 | package com.hazelcast.aws;
2 |
3 | /**
4 | * Represents tag key and value pair. Used to narrow the members returned by the discovery mechanism.
5 | */
6 | class Tag {
7 | private final String key;
8 | private final String value;
9 |
10 | Tag(String key, String value) {
11 | if (key == null && value == null) {
12 | throw new IllegalArgumentException("Tag requires at least key or value");
13 | }
14 | this.key = key;
15 | this.value = value;
16 | }
17 |
18 | String getKey() {
19 | return key;
20 | }
21 |
22 | String getValue() {
23 | return value;
24 | }
25 |
26 | @Override
27 | public String toString() {
28 | return String.format("(key=%s, value=%s)", key, value);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/main/java/com/hazelcast/aws/XmlNode.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Hazelcast Inc.
3 | *
4 | * Licensed under the Hazelcast Community License (the "License"); you may not use
5 | * this file except in compliance with the License. You may obtain a copy of the
6 | * License at
7 | *
8 | * http://hazelcast.com/hazelcast-community-license
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 | * WARRANTIES OF ANY KIND, either express or implied. See the License for the
13 | * specific language governing permissions and limitations under the License.
14 | */
15 |
16 | package com.hazelcast.aws;
17 |
18 | import org.w3c.dom.Document;
19 | import org.w3c.dom.Node;
20 |
21 | import javax.xml.parsers.DocumentBuilderFactory;
22 | import java.io.ByteArrayInputStream;
23 | import java.util.List;
24 | import java.util.stream.Collectors;
25 | import java.util.stream.StreamSupport;
26 |
27 | import static com.hazelcast.internal.config.DomConfigHelper.childElements;
28 | import static com.hazelcast.internal.config.DomConfigHelper.cleanNodeName;
29 | import static java.nio.charset.StandardCharsets.UTF_8;
30 |
31 | /**
32 | * Helper class for parsing XML strings
33 | */
34 | final class XmlNode {
35 | private Node node;
36 |
37 | private XmlNode(Node node) {
38 | this.node = node;
39 | }
40 |
41 | static XmlNode create(String xmlString) {
42 | try {
43 | DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
44 | dbf.setNamespaceAware(true);
45 | dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
46 | Document doc = dbf.newDocumentBuilder().parse(new ByteArrayInputStream(xmlString.getBytes(UTF_8)));
47 | return new XmlNode(doc.getDocumentElement());
48 | } catch (Exception e) {
49 | throw new RuntimeException(e);
50 | }
51 | }
52 |
53 | Node getNode() {
54 | return node;
55 | }
56 |
57 | List getSubNodes(String name) {
58 | return StreamSupport.stream(childElements(node).spliterator(), false)
59 | .filter(e -> name.equals(cleanNodeName(e)))
60 | .map(XmlNode::new)
61 | .collect(Collectors.toList());
62 | }
63 |
64 | String getValue(String name) {
65 | return getSubNodes(name).stream()
66 | .map(XmlNode::getNode)
67 | .map(Node::getFirstChild)
68 | .map(Node::getNodeValue)
69 | .findFirst()
70 | .orElse(null);
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/main/java/com/hazelcast/aws/package-info.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Hazelcast Inc.
3 | *
4 | * Licensed under the Hazelcast Community License (the "License"); you may not use
5 | * this file except in compliance with the License. You may obtain a copy of the
6 | * License at
7 | *
8 | * http://hazelcast.com/hazelcast-community-license
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 | * WARRANTIES OF ANY KIND, either express or implied. See the License for the
13 | * specific language governing permissions and limitations under the License.
14 | */
15 |
16 | /**
17 | * Provides interfaces/classes for Hazelcast AWS.
18 | */
19 | package com.hazelcast.aws;
20 |
21 |
--------------------------------------------------------------------------------
/src/main/resources/META-INF/services/com.hazelcast.spi.discovery.DiscoveryStrategyFactory:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright 2020 Hazelcast Inc.
3 | #
4 | # Licensed under the Hazelcast Community License (the "License"); you may not use
5 | # this file except in compliance with the License. You may obtain a copy of the
6 | # License at
7 | #
8 | # http://hazelcast.com/hazelcast-community-license
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 | # WARRANTIES OF ANY KIND, either express or implied. See the License for the
13 | # specific language governing permissions and limitations under the License.
14 | #
15 |
16 | com.hazelcast.aws.AwsDiscoveryStrategyFactory
--------------------------------------------------------------------------------
/src/main/resources/hazelcast-community-license.txt:
--------------------------------------------------------------------------------
1 | Hazelcast Community License
2 |
3 | Version 1.0
4 |
5 |
6 |
7 | This Hazelcast Community License Agreement Version 1.0 (the “Agreement”) sets
8 | forth the terms on which Hazelcast, Inc. (“Hazelcast”) makes available certain
9 | software made available by Hazelcast under this Agreement (the “Software”). BY
10 | INSTALLING, DOWNLOADING, ACCESSING, USING OR DISTRIBUTING ANY OF THE SOFTWARE,
11 | YOU AGREE TO THE TERMS AND CONDITIONS OF THIS AGREEMENT.IF YOU DO NOT AGREE TO
12 | SUCH TERMS AND CONDITIONS, YOU MUST NOT USE THE SOFTWARE. IF YOU ARE RECEIVING
13 | THE SOFTWARE ON BEHALF OF A LEGAL ENTITY, YOU REPRESENT AND WARRANT THAT YOU
14 | HAVE THE ACTUAL AUTHORITY TO AGREE TO THE TERMS AND CONDITIONS OF THIS
15 | AGREEMENT ON BEHALF OF SUCH ENTITY. “Licensee” means you, an individual, or
16 | the entity on whose behalf you are receiving the Software.
17 |
18 |
19 |
20 | 1. LICENSE GRANT AND CONDITIONS.
21 |
22 |
23 | 1.1 License. Subject to the terms and conditions of this Agreement, Hazelcast
24 | hereby grants to Licensee a non-exclusive, royalty-free, worldwide,
25 | non-transferable, non-sublicenseable license during the term of this Agreement
26 | to: (a) use the Software; (b) prepare modifications and derivative works of
27 | the Software; (c) distribute the Software (including without limitation in
28 | source code or object code form); and (d) reproduce copies of the Software (
29 | the “License”). Licensee is not granted the right to, and Licensee shall not,
30 | exercise the License for an Excluded Purpose. For purposes of this Agreement,
31 | “Excluded Purpose” means making available any software-as-a-service,
32 | platform-as-a-service, infrastructure-as-a-service or other similar online
33 | service that competes with Hazelcast products or services that provide the
34 | Software.
35 |
36 |
37 | 1.2 Conditions. In consideration of the License, Licensee’s distribution of
38 | the Software is subject to the following conditions:
39 |
40 | a. Licensee must cause any Software modified by Licensee to carry prominent
41 | notices stating that Licensee modified the Software.
42 |
43 | b. On each Software copy, Licensee shall reproduce and not remove or alter all
44 | Hazelcast or third party copyright or other proprietary notices contained in
45 | the Software, and Licensee must provide the notice below with each copy.
46 |
47 | “This software is made available by Hazelcast, Inc., under the terms of the
48 | Hazelcast Community License Agreement, Version 1.0 located at
49 | http://hazelcast.com/Hazelcast-community-license. BY INSTALLING, DOWNLOADING,
50 | ACCESSING, USING OR DISTRIBUTING ANY OF THE SOFTWARE, YOU AGREE TO THE TERMS
51 | OF SUCH LICENSE AGREEMENT.”
52 |
53 |
54 | 1.3 Licensee Modifications. Licensee may add its own copyright notices to
55 | modifications made by Licensee and may provide additional or different license
56 | terms and conditions for use, reproduction, or distribution of Licensee’s
57 | modifications. While redistributing the Software or modifications thereof,
58 | Licensee may choose to offer, for a fee or free of charge, support, warranty,
59 | indemnity, or other obligations.Licensee, and not Hazelcast, will be
60 | responsible for any such obligations.
61 |
62 |
63 | 1.4 No Sublicensing. The License does not include the right to sublicense the
64 | Software, however, each recipient to which Licensee provides the Software may
65 | exercise the Licenses so long as such recipient agrees to the terms and
66 | conditions of this Agreement.
67 |
68 |
69 |
70 | 2. TERM AND TERMINATION.
71 |
72 | This Agreement will continue unless and until earlier terminated as set forth
73 | herein. If Licensee breaches any of its conditions or obligations under this
74 | Agreement, this Agreement will terminate automatically and the License will
75 | terminate automatically and permanently.
76 |
77 |
78 |
79 | 3. INTELLECTUAL PROPERTY.
80 |
81 | As between the parties, Hazelcast will retain all right, title, and interest
82 | in the Software, and all intellectual property rights therein. Hazelcast
83 | hereby reserves all rights not expressly granted to Licensee in this
84 | Agreement.Hazelcast hereby reserves all rights in its trademarks and service
85 | marks, and no licenses therein are granted in this Agreement.
86 |
87 |
88 |
89 | 4. DISCLAIMER.
90 |
91 | HAZELCAST HEREBY DISCLAIMS ANY AND ALL WARRANTIES AND CONDITIONS, EXPRESS,
92 | IMPLIED, STATUTORY, OR OTHERWISE, AND SPECIFICALLY DISCLAIMS ANY WARRANTY OF
93 | MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, WITH RESPECT TO THE
94 | SOFTWARE.
95 |
96 |
97 |
98 | 5. LIMITATION OF LIABILITY.
99 |
100 | HAZELCAST WILL NOT BE LIABLE FOR ANY DAMAGES OF ANY KIND, INCLUDING BUT NOT
101 | LIMITED TO, LOST PROFITS OR ANY CONSEQUENTIAL, SPECIAL, INCIDENTAL, INDIRECT,
102 | OR DIRECT DAMAGES, HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, ARISING OUT
103 | OF THIS AGREEMENT. THE FOREGOING SHALL APPLY TO THE EXTENT PERMITTED BY
104 | APPLICABLE LAW.
105 |
106 |
107 |
108 | 6. GENERAL.
109 |
110 |
111 | 6.1 Governing Law.This Agreement will be governed by and interpreted in
112 | accordance with the laws of the state of California, without reference to its
113 | conflict of laws principles. If Licensee is located within the United States,
114 | all disputes arising out of this Agreement are subject to the exclusive
115 | jurisdiction of courts located in Santa Clara County, California, USA. If
116 | Licensee is located outside of the United States, any dispute, controversy or
117 | claim arising out of or relating to this Agreement will be referred to and
118 | finally determined by arbitration in accordance with the JAMS International
119 | Arbitration Rules. The tribunal will consist of one arbitrator.The place of
120 | arbitration will be San Francisco, California.The language to be used in the
121 | arbitral proceedings will be English.Judgment upon the award rendered by the
122 | arbitrator may be entered in any court having jurisdiction thereof.
123 |
124 |
125 | 6.2. Assignment. Licensee is not authorized to assign its rights under this
126 | Agreement to any third party.Hazelcast may freely assign its rights under this
127 | Agreement to any third party.
128 |
129 |
130 | 6.3. Other. This Agreement is the entire agreement between the parties
131 | regarding the subject matter hereof. No amendment or modification of this
132 | Agreement will be valid or binding upon the parties unless made in writing and
133 | signed by the duly authorized representatives of both parties. In the event
134 | that any provision, including without limitation any condition, of this
135 | Agreement is held to be unenforceable, this Agreement and all licenses and
136 | rights granted hereunder will immediately terminate. Waiver by Hazelcast of a
137 | breach of any provision of this Agreement or the failure by Hazelcast to
138 | exercise any right hereunder will not be construed as a waiver of any
139 | subsequent breach of that right or as a waiver of any other right.
--------------------------------------------------------------------------------
/src/test/java/com/hazelcast/aws/AwsClientConfiguratorTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Hazelcast Inc.
3 | *
4 | * Licensed under the Hazelcast Community License (the "License"); you may not use
5 | * this file except in compliance with the License. You may obtain a copy of the
6 | * License at
7 | *
8 | * http://hazelcast.com/hazelcast-community-license
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 | * WARRANTIES OF ANY KIND, either express or implied. See the License for the
13 | * specific language governing permissions and limitations under the License.
14 | */
15 |
16 | package com.hazelcast.aws;
17 |
18 | import com.hazelcast.aws.AwsMetadataApi.EcsMetadata;
19 | import com.hazelcast.config.InvalidConfigurationException;
20 | import org.junit.Test;
21 |
22 | import static com.hazelcast.aws.AwsClientConfigurator.resolveEc2Endpoint;
23 | import static com.hazelcast.aws.AwsClientConfigurator.resolveEcsEndpoint;
24 | import static com.hazelcast.aws.AwsClientConfigurator.resolveRegion;
25 | import static org.junit.Assert.assertEquals;
26 | import static org.junit.Assert.assertFalse;
27 | import static org.junit.Assert.assertTrue;
28 | import static org.mockito.BDDMockito.given;
29 | import static org.mockito.Mockito.mock;
30 |
31 | public class AwsClientConfiguratorTest {
32 |
33 | @Test
34 | public void resolveRegionAwsConfig() {
35 | // given
36 | String region = "us-east-1";
37 | AwsConfig awsConfig = AwsConfig.builder().setRegion(region).build();
38 | AwsMetadataApi awsMetadataApi = mock(AwsMetadataApi.class);
39 | Environment environment = mock(Environment.class);
40 |
41 | // when
42 | String result = resolveRegion(awsConfig, awsMetadataApi, environment);
43 |
44 | // then
45 | assertEquals(region, result);
46 | }
47 |
48 | @Test
49 | public void resolveRegionEcsConfig() {
50 | // given
51 | String region = "us-east-1";
52 | AwsConfig awsConfig = AwsConfig.builder().build();
53 | AwsMetadataApi awsMetadataApi = mock(AwsMetadataApi.class);
54 | Environment environment = mock(Environment.class);
55 | given(environment.getAwsRegionOnEcs()).willReturn(region);
56 | given(environment.isRunningOnEcs()).willReturn(true);
57 |
58 | // when
59 | String result = resolveRegion(awsConfig, awsMetadataApi, environment);
60 |
61 | // then
62 | assertEquals(region, result);
63 | }
64 |
65 | @Test
66 | public void resolveRegionEc2Metadata() {
67 | // given
68 | AwsConfig awsConfig = AwsConfig.builder().build();
69 | AwsMetadataApi awsMetadataApi = mock(AwsMetadataApi.class);
70 | Environment environment = mock(Environment.class);
71 | given(awsMetadataApi.availabilityZoneEc2()).willReturn("us-east-1a");
72 |
73 | // when
74 | String result = resolveRegion(awsConfig, awsMetadataApi, environment);
75 |
76 | // then
77 | assertEquals("us-east-1", result);
78 | }
79 |
80 | @Test
81 | public void resolveEc2Endpoints() {
82 | assertEquals("ec2.us-east-1.amazonaws.com", resolveEc2Endpoint(AwsConfig.builder().build(), "us-east-1"));
83 | assertEquals("ec2.us-east-1.amazonaws.com", resolveEc2Endpoint(AwsConfig.builder().setHostHeader("ecs").build(), "us-east-1"));
84 | assertEquals("ec2.us-east-1.amazonaws.com", resolveEc2Endpoint(AwsConfig.builder().setHostHeader("ec2").build(), "us-east-1"));
85 | assertEquals("ec2.us-east-1.something",
86 | resolveEc2Endpoint(AwsConfig.builder().setHostHeader("ec2.something").build(), "us-east-1"));
87 | }
88 |
89 | @Test
90 | public void resolveEcsEndpoints() {
91 | assertEquals("ecs.us-east-1.amazonaws.com", resolveEcsEndpoint(AwsConfig.builder().build(), "us-east-1"));
92 | assertEquals("ecs.us-east-1.amazonaws.com",
93 | resolveEcsEndpoint(AwsConfig.builder().setHostHeader("ecs").build(), "us-east-1"));
94 | assertEquals("ecs.us-east-1.something",
95 | resolveEcsEndpoint(AwsConfig.builder().setHostHeader("ecs.something").build(), "us-east-1"));
96 | }
97 |
98 | @Test
99 | public void explicitlyEcsConfigured() {
100 | assertTrue(AwsClientConfigurator.explicitlyEcsConfigured(AwsConfig.builder().setHostHeader("ecs").build()));
101 | assertTrue(AwsClientConfigurator.explicitlyEcsConfigured(
102 | AwsConfig.builder().setHostHeader("ecs.us-east-1.amazonaws.com").build()));
103 | assertTrue(AwsClientConfigurator.explicitlyEcsConfigured(AwsConfig.builder().setCluster("cluster").build()));
104 | assertFalse(AwsClientConfigurator.explicitlyEcsConfigured(AwsConfig.builder().build()));
105 | }
106 |
107 | @Test
108 | public void explicitlyEc2Configured() {
109 | assertTrue(AwsClientConfigurator.explicitlyEc2Configured(AwsConfig.builder().setHostHeader("ec2").build()));
110 | assertTrue(AwsClientConfigurator.explicitlyEc2Configured(
111 | AwsConfig.builder().setHostHeader("ec2.us-east-1.amazonaws.com").build()));
112 | assertFalse(AwsClientConfigurator.explicitlyEc2Configured(
113 | AwsConfig.builder().setHostHeader("ecs.us-east-1.amazonaws.com").build()));
114 | assertFalse(AwsClientConfigurator.explicitlyEc2Configured(AwsConfig.builder().build()));
115 | }
116 |
117 | @Test
118 | public void resolveClusterAwsConfig() {
119 | // given
120 | String cluster = "service-name";
121 | AwsConfig config = AwsConfig.builder().setCluster(cluster).build();
122 |
123 | // when
124 | String result = AwsClientConfigurator.resolveCluster(config, null, null);
125 |
126 | // then
127 | assertEquals(cluster, result);
128 | }
129 |
130 | @Test
131 | public void resolveClusterAwsEcsMetadata() {
132 | // given
133 | String cluster = "service-name";
134 | AwsConfig config = AwsConfig.builder().build();
135 | AwsMetadataApi metadataApi = mock(AwsMetadataApi.class);
136 | given(metadataApi.metadataEcs()).willReturn(new EcsMetadata(null, cluster));
137 | Environment environment = mock(Environment.class);
138 | given(environment.isRunningOnEcs()).willReturn(true);
139 |
140 | // when
141 | String result = AwsClientConfigurator.resolveCluster(config, metadataApi, environment);
142 |
143 | // then
144 | assertEquals(cluster, result);
145 | }
146 |
147 | @Test(expected = InvalidConfigurationException.class)
148 | public void resolveClusterInvalidConfiguration() {
149 | // given
150 | AwsConfig config = AwsConfig.builder().build();
151 | Environment environment = mock(Environment.class);
152 | given(environment.isRunningOnEcs()).willReturn(false);
153 |
154 | // when
155 | AwsClientConfigurator.resolveCluster(config, null, environment);
156 |
157 | // then
158 | // throws exception
159 | }
160 |
161 | }
--------------------------------------------------------------------------------
/src/test/java/com/hazelcast/aws/AwsCredentialsProviderTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Hazelcast Inc.
3 | *
4 | * Licensed under the Hazelcast Community License (the "License"); you may not use
5 | * this file except in compliance with the License. You may obtain a copy of the
6 | * License at
7 | *
8 | * http://hazelcast.com/hazelcast-community-license
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 | * WARRANTIES OF ANY KIND, either express or implied. See the License for the
13 | * specific language governing permissions and limitations under the License.
14 | */
15 |
16 | package com.hazelcast.aws;
17 |
18 | import com.hazelcast.config.InvalidConfigurationException;
19 | import org.junit.Test;
20 | import org.junit.runner.RunWith;
21 | import org.mockito.Mock;
22 | import org.mockito.runners.MockitoJUnitRunner;
23 |
24 | import static org.junit.Assert.assertEquals;
25 | import static org.junit.Assert.assertNull;
26 | import static org.mockito.BDDMockito.given;
27 |
28 | @RunWith(MockitoJUnitRunner.class)
29 | public class AwsCredentialsProviderTest {
30 | private static final String ACCESS_KEY = "AKIDEXAMPLE";
31 | private static final String SECRET_KEY = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY";
32 | private static final String TOKEN = "IQoJb3JpZ2luX2VjEFIaDGV1LWNlbnRyYWwtMSJGM==";
33 | private static final AwsCredentials CREDENTIALS = AwsCredentials.builder()
34 | .setAccessKey(ACCESS_KEY)
35 | .setSecretKey(SECRET_KEY)
36 | .setToken(TOKEN)
37 | .build();
38 |
39 | @Mock
40 | private AwsMetadataApi awsMetadataApi;
41 |
42 | @Mock
43 | private Environment environment;
44 |
45 | @Test
46 | public void credentialsAccessKey() {
47 | // given
48 | AwsConfig awsConfig = AwsConfig.builder()
49 | .setAccessKey(ACCESS_KEY)
50 | .setSecretKey(SECRET_KEY)
51 | .build();
52 | AwsCredentialsProvider credentialsProvider = new AwsCredentialsProvider(awsConfig, awsMetadataApi, environment);
53 |
54 | // when
55 | AwsCredentials credentials = credentialsProvider.credentials();
56 |
57 | // then
58 | assertEquals(ACCESS_KEY, credentials.getAccessKey());
59 | assertEquals(SECRET_KEY, credentials.getSecretKey());
60 | assertNull(credentials.getToken());
61 | }
62 |
63 | @Test
64 | public void credentialsEc2IamRole() {
65 | // given
66 | String iamRole = "sample-iam-role";
67 | AwsConfig awsConfig = AwsConfig.builder()
68 | .setIamRole(iamRole)
69 | .build();
70 | given(awsMetadataApi.credentialsEc2(iamRole)).willReturn(CREDENTIALS);
71 | given(environment.isRunningOnEcs()).willReturn(false);
72 | AwsCredentialsProvider credentialsProvider = new AwsCredentialsProvider(awsConfig, awsMetadataApi, environment);
73 |
74 | // when
75 | AwsCredentials credentials = credentialsProvider.credentials();
76 |
77 | // then
78 | assertEquals(CREDENTIALS, credentials);
79 | }
80 |
81 | @Test
82 | public void credentialsDefaultEc2IamRole() {
83 | // given
84 | String iamRole = "sample-iam-role";
85 | AwsConfig awsConfig = AwsConfig.builder().build();
86 | given(awsMetadataApi.defaultIamRoleEc2()).willReturn(iamRole);
87 | given(awsMetadataApi.credentialsEc2(iamRole)).willReturn(CREDENTIALS);
88 | given(environment.isRunningOnEcs()).willReturn(false);
89 | AwsCredentialsProvider credentialsProvider = new AwsCredentialsProvider(awsConfig, awsMetadataApi, environment);
90 |
91 | // when
92 | AwsCredentials credentials = credentialsProvider.credentials();
93 |
94 | // then
95 | assertEquals(CREDENTIALS, credentials);
96 | }
97 |
98 | @Test(expected = InvalidConfigurationException.class)
99 | public void credentialsEc2Exception() {
100 | // given
101 | String iamRole = "sample-iam-role";
102 | AwsConfig awsConfig = AwsConfig.builder()
103 | .setIamRole(iamRole)
104 | .build();
105 | given(awsMetadataApi.credentialsEc2(iamRole)).willThrow(new RuntimeException("Error fetching credentials"));
106 | given(environment.isRunningOnEcs()).willReturn(false);
107 | AwsCredentialsProvider credentialsProvider = new AwsCredentialsProvider(awsConfig, awsMetadataApi, environment);
108 |
109 | // when
110 | credentialsProvider.credentials();
111 |
112 | // then
113 | // throws exception
114 | }
115 |
116 | @Test
117 | public void credentialsEcs() {
118 | // given
119 | AwsConfig awsConfig = AwsConfig.builder().build();
120 | given(awsMetadataApi.credentialsEcs()).willReturn(CREDENTIALS);
121 | given(environment.isRunningOnEcs()).willReturn(true);
122 | AwsCredentialsProvider credentialsProvider = new AwsCredentialsProvider(awsConfig, awsMetadataApi, environment);
123 |
124 | // when
125 | AwsCredentials credentials = credentialsProvider.credentials();
126 |
127 | // then
128 | assertEquals(CREDENTIALS, credentials);
129 | }
130 |
131 | @Test(expected = InvalidConfigurationException.class)
132 | public void credentialsEcsException() {
133 | // given
134 | AwsConfig awsConfig = AwsConfig.builder().build();
135 | given(awsMetadataApi.credentialsEcs()).willThrow(new RuntimeException("Error fetching credentials"));
136 | given(environment.isRunningOnEcs()).willReturn(true);
137 | AwsCredentialsProvider credentialsProvider = new AwsCredentialsProvider(awsConfig, awsMetadataApi, environment);
138 |
139 | // when
140 | credentialsProvider.credentials();
141 |
142 | // then
143 | // throws exception
144 | }
145 | }
--------------------------------------------------------------------------------
/src/test/java/com/hazelcast/aws/AwsDiscoveryStrategyFactoryTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Hazelcast Inc.
3 | *
4 | * Licensed under the Hazelcast Community License (the "License"); you may not use
5 | * this file except in compliance with the License. You may obtain a copy of the
6 | * License at
7 | *
8 | * http://hazelcast.com/hazelcast-community-license
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 | * WARRANTIES OF ANY KIND, either express or implied. See the License for the
13 | * specific language governing permissions and limitations under the License.
14 | */
15 |
16 | package com.hazelcast.aws;
17 |
18 | import com.github.tomakehurst.wiremock.junit.WireMockRule;
19 | import com.hazelcast.config.Config;
20 | import com.hazelcast.config.DiscoveryConfig;
21 | import com.hazelcast.config.XmlConfigBuilder;
22 | import com.hazelcast.internal.nio.IOUtil;
23 | import com.hazelcast.spi.discovery.DiscoveryStrategy;
24 | import com.hazelcast.spi.discovery.impl.DefaultDiscoveryService;
25 | import com.hazelcast.spi.discovery.integration.DiscoveryServiceSettings;
26 | import org.junit.Rule;
27 | import org.junit.Test;
28 |
29 | import java.io.BufferedWriter;
30 | import java.io.File;
31 | import java.io.FileOutputStream;
32 | import java.io.IOException;
33 | import java.io.InputStream;
34 | import java.io.OutputStreamWriter;
35 | import java.nio.charset.StandardCharsets;
36 | import java.util.HashMap;
37 | import java.util.Iterator;
38 | import java.util.Map;
39 |
40 | import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
41 | import static com.github.tomakehurst.wiremock.client.WireMock.get;
42 | import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
43 | import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
44 | import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
45 | import static org.junit.Assert.assertEquals;
46 | import static org.junit.Assert.assertTrue;
47 |
48 | public class AwsDiscoveryStrategyFactoryTest {
49 | @Rule
50 | public WireMockRule wireMockRule = new WireMockRule(wireMockConfig().dynamicPort());
51 |
52 | private static void createStrategy(Map props) {
53 | final AwsDiscoveryStrategyFactory factory = new AwsDiscoveryStrategyFactory();
54 | factory.newDiscoveryStrategy(null, null, props);
55 | }
56 |
57 | private static Config createConfig(String xmlFileName) {
58 | final InputStream xmlResource = AwsDiscoveryStrategyFactoryTest.class.getClassLoader().getResourceAsStream(xmlFileName);
59 | return new XmlConfigBuilder(xmlResource).build();
60 | }
61 |
62 | @Test
63 | public void testEc2() {
64 | final Map props = new HashMap<>();
65 | props.put("access-key", "test-value");
66 | props.put("secret-key", "test-value");
67 | props.put("region", "us-east-1");
68 | props.put("host-header", "ec2.test-value");
69 | props.put("security-group-name", "test-value");
70 | props.put("tag-key", "test-value");
71 | props.put("tag-value", "test-value");
72 | props.put("connection-timeout-seconds", 10);
73 | props.put("hz-port", 1234);
74 | createStrategy(props);
75 | }
76 |
77 | @Test
78 | public void testEcs() {
79 | final Map props = new HashMap<>();
80 | props.put("access-key", "test-value");
81 | props.put("secret-key", "test-value");
82 | props.put("region", "us-east-1");
83 | props.put("host-header", "ecs.test-value");
84 | props.put("connection-timeout-seconds", 10);
85 | props.put("hz-port", 1234);
86 | props.put("cluster", "cluster-name");
87 | props.put("service-name", "service-name");
88 | createStrategy(props);
89 | }
90 |
91 | @Test
92 | public void parseAndCreateDiscoveryStrategyPasses() {
93 | final Config config = createConfig("test-aws-config.xml");
94 | validateConfig(config);
95 | }
96 |
97 | private void validateConfig(final Config config) {
98 | final DiscoveryConfig discoveryConfig = config.getNetworkConfig().getJoin().getDiscoveryConfig();
99 | final DiscoveryServiceSettings settings = new DiscoveryServiceSettings().setDiscoveryConfig(discoveryConfig);
100 | final DefaultDiscoveryService service = new DefaultDiscoveryService(settings);
101 | final Iterator strategies = service.getDiscoveryStrategies().iterator();
102 |
103 | assertTrue(strategies.hasNext());
104 | final DiscoveryStrategy strategy = strategies.next();
105 | assertTrue(strategy instanceof AwsDiscoveryStrategy);
106 | }
107 |
108 | @Test
109 | public void isEndpointAvailable() {
110 | // given
111 | String endpoint = "/some-endpoint";
112 | String url = String.format("http://localhost:%d%s", wireMockRule.port(), endpoint);
113 | stubFor(get(urlEqualTo(endpoint)).willReturn(aResponse().withStatus(200).withBody("some-body")));
114 |
115 | // when
116 | boolean isAvailable = AwsDiscoveryStrategyFactory.isEndpointAvailable(url);
117 |
118 | // then
119 | assertTrue(isAvailable);
120 | }
121 |
122 | @Test
123 | public void readFileContents()
124 | throws IOException {
125 | // given
126 | String expectedContents = "Hello, world!\nThis is a test with Unicode ✓.";
127 | String testFile = createTestFile(expectedContents);
128 |
129 | // when
130 | String actualContents = AwsDiscoveryStrategyFactory.readFileContents(testFile);
131 |
132 | // then
133 | assertEquals(expectedContents, actualContents);
134 | }
135 |
136 | private static String createTestFile(String expectedContents)
137 | throws IOException {
138 | File temp = File.createTempFile("test", ".tmp");
139 | temp.deleteOnExit();
140 | BufferedWriter bufferedWriter = null;
141 | try {
142 | bufferedWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(temp), StandardCharsets.UTF_8));
143 | bufferedWriter.write(expectedContents);
144 | } finally {
145 | IOUtil.closeResource(bufferedWriter);
146 | }
147 | return temp.getAbsolutePath();
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/src/test/java/com/hazelcast/aws/AwsDiscoveryStrategyTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Hazelcast Inc.
3 | *
4 | * Licensed under the Hazelcast Community License (the "License"); you may not use
5 | * this file except in compliance with the License. You may obtain a copy of the
6 | * License at
7 | *
8 | * http://hazelcast.com/hazelcast-community-license
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 | * WARRANTIES OF ANY KIND, either express or implied. See the License for the
13 | * specific language governing permissions and limitations under the License.
14 | */
15 |
16 | package com.hazelcast.aws;
17 |
18 | import com.google.common.collect.ImmutableMap;
19 | import com.hazelcast.config.InvalidConfigurationException;
20 | import com.hazelcast.spi.discovery.DiscoveryNode;
21 | import org.junit.Before;
22 | import org.junit.Test;
23 | import org.junit.runner.RunWith;
24 | import org.mockito.Mock;
25 | import org.mockito.runners.MockitoJUnitRunner;
26 |
27 | import java.util.ArrayList;
28 | import java.util.Collections;
29 | import java.util.HashMap;
30 | import java.util.List;
31 | import java.util.Map;
32 | import java.util.Optional;
33 |
34 | import static com.hazelcast.spi.partitiongroup.PartitionGroupMetaData.PARTITION_GROUP_ZONE;
35 | import static java.util.Collections.emptyList;
36 | import static org.hamcrest.MatcherAssert.assertThat;
37 | import static org.hamcrest.collection.IsCollectionWithSize.hasSize;
38 | import static org.junit.Assert.assertEquals;
39 | import static org.mockito.BDDMockito.given;
40 |
41 | @RunWith(MockitoJUnitRunner.class)
42 | public class AwsDiscoveryStrategyTest {
43 | private static final int PORT1 = 5701;
44 | private static final int PORT2 = 5702;
45 | private static final String ZONE = "us-east-1a";
46 | private static final String PLACEMENT_GROUP = "placement-group";
47 | private static final String PLACEMENT_PARTITION_ID = "42";
48 |
49 | // Group name pattern for placement groups
50 | private static final String PG_NAME_PATTERN = "%s-%s";
51 | // Group name pattern for partition placement group
52 | private static final String PPG_NAME_PATTERN = PG_NAME_PATTERN.concat("-%s");
53 |
54 | @Mock
55 | private AwsClient awsClient;
56 |
57 | private AwsDiscoveryStrategy awsDiscoveryStrategy;
58 |
59 | @Before
60 | public void setUp() {
61 | Map properties = new HashMap<>();
62 | properties.put("hz-port", String.format("%s-%s", PORT1, PORT2));
63 | awsDiscoveryStrategy = new AwsDiscoveryStrategy(properties, awsClient);
64 | }
65 |
66 | @Test(expected = InvalidConfigurationException.class)
67 | public void newInvalidPropertiesBothEc2AndEcs() {
68 | // given
69 | Map properties = new HashMap<>();
70 | properties.put("iam-role", "some-role");
71 | properties.put("cluster", "some-cluster");
72 |
73 | // when
74 | new AwsDiscoveryStrategy(properties);
75 |
76 | // then
77 | // throw exception
78 | }
79 |
80 | @Test(expected = InvalidConfigurationException.class)
81 | public void newInvalidPropertiesBothFamilyAndServiceNameDefined() {
82 | // given
83 | Map properties = new HashMap<>();
84 | properties.put("family", "family-name");
85 | properties.put("service-name", "service-name");
86 |
87 | // when
88 | new AwsDiscoveryStrategy(properties);
89 |
90 | // then
91 | // throw exception
92 | }
93 |
94 | @Test(expected = InvalidConfigurationException.class)
95 | public void newInvalidPropertiesAccessKeyWithoutSecretKey() {
96 | // given
97 | Map properties = new HashMap<>();
98 | properties.put("access-key", "access-key");
99 |
100 | // when
101 | new AwsDiscoveryStrategy(properties);
102 |
103 | // then
104 | // throw exception
105 | }
106 |
107 | @Test(expected = InvalidConfigurationException.class)
108 | public void newInvalidPropertiesIamRoleWithAccessKey() {
109 | // given
110 | Map properties = new HashMap<>();
111 | properties.put("iam-role", "iam-role");
112 | properties.put("access-key", "access-key");
113 | properties.put("secret-key", "secret-key");
114 |
115 | // when
116 | new AwsDiscoveryStrategy(properties);
117 |
118 | // then
119 | // throw exception
120 | }
121 |
122 | @Test(expected = InvalidConfigurationException.class)
123 | public void newInvalidPortRangeProperty() {
124 | // given
125 | Map properties = new HashMap<>();
126 | properties.put("hz-port", "invalid");
127 |
128 | // when
129 | new AwsDiscoveryStrategy(properties);
130 |
131 | // then
132 | // throw exception
133 | }
134 |
135 | @Test
136 | public void discoverLocalMetadataWithoutPlacement() {
137 | // given
138 | given(awsClient.getAvailabilityZone()).willReturn(ZONE);
139 | given(awsClient.getPlacementGroup()).willReturn(Optional.empty());
140 | given(awsClient.getPlacementPartitionNumber()).willReturn(Optional.empty());
141 |
142 | // when
143 | Map localMetaData = awsDiscoveryStrategy.discoverLocalMetadata();
144 |
145 | // then
146 | assertEquals(1, localMetaData.size());
147 | assertEquals(ZONE, localMetaData.get(PARTITION_GROUP_ZONE));
148 | }
149 |
150 | @Test
151 | public void discoverLocalMetadataWithPlacement() {
152 | // given
153 | given(awsClient.getAvailabilityZone()).willReturn(ZONE);
154 | given(awsClient.getPlacementGroup()).willReturn(Optional.of(PLACEMENT_GROUP));
155 | given(awsClient.getPlacementPartitionNumber()).willReturn(Optional.empty());
156 | String expectedPartitionGroup = String.format(PG_NAME_PATTERN, ZONE, PLACEMENT_GROUP);
157 |
158 | // when
159 | Map localMetaData = awsDiscoveryStrategy.discoverLocalMetadata();
160 |
161 | // then
162 | assertEquals(2, localMetaData.size());
163 | assertEquals(ZONE, localMetaData.get(PARTITION_GROUP_ZONE));
164 | assertEquals(expectedPartitionGroup, localMetaData.get(AwsDiscoveryStrategy.PARTITION_GROUP_PLACEMENT));
165 | }
166 |
167 | @Test
168 | public void discoverLocalMetadataWithPartitionPlacement() {
169 | // given
170 | given(awsClient.getAvailabilityZone()).willReturn(ZONE);
171 | given(awsClient.getPlacementGroup()).willReturn(Optional.of(PLACEMENT_GROUP));
172 | given(awsClient.getPlacementPartitionNumber()).willReturn(Optional.of(PLACEMENT_PARTITION_ID));
173 | String expectedPartitionGroup = String.format(PPG_NAME_PATTERN, ZONE, PLACEMENT_GROUP, PLACEMENT_PARTITION_ID);
174 |
175 | // when
176 | Map localMetaData = awsDiscoveryStrategy.discoverLocalMetadata();
177 |
178 | // then
179 | assertEquals(2, localMetaData.size());
180 | assertEquals(ZONE, localMetaData.get(PARTITION_GROUP_ZONE));
181 | assertEquals(expectedPartitionGroup, localMetaData.get(AwsDiscoveryStrategy.PARTITION_GROUP_PLACEMENT));
182 | }
183 |
184 | @Test
185 | public void discoverNodes() {
186 | // given
187 | String privateIp = "192.168.1.15";
188 | String publicIp = "38.146.24.2";
189 | given(awsClient.getAddresses()).willReturn(ImmutableMap.of(privateIp, publicIp));
190 |
191 | // when
192 | Iterable nodes = awsDiscoveryStrategy.discoverNodes();
193 |
194 | // then
195 | List nodeList = toList(nodes);
196 | DiscoveryNode node1 = nodeList.get(0);
197 | assertEquals(privateIp, node1.getPrivateAddress().getHost());
198 | assertEquals(PORT1, node1.getPrivateAddress().getPort());
199 | assertEquals(publicIp, node1.getPublicAddress().getHost());
200 |
201 | DiscoveryNode node2 = nodeList.get(1);
202 | assertEquals(privateIp, node2.getPrivateAddress().getHost());
203 | assertEquals(PORT2, node2.getPrivateAddress().getPort());
204 | assertEquals(publicIp, node2.getPublicAddress().getHost());
205 | }
206 |
207 | @Test
208 | public void discoverNodesMultipleAddressesManyPorts() {
209 | // given
210 | // 8 ports in the port range
211 | Map properties = new HashMap<>();
212 | properties.put("hz-port", "5701-5708");
213 | awsDiscoveryStrategy = new AwsDiscoveryStrategy(properties, awsClient);
214 |
215 | // 2 instances found
216 | given(awsClient.getAddresses()).willReturn(ImmutableMap.of(
217 | "192.168.1.15", "38.146.24.2",
218 | "192.168.1.16", "38.146.28.15"
219 | ));
220 |
221 | // when
222 | Iterable nodes = awsDiscoveryStrategy.discoverNodes();
223 |
224 | // then
225 | // 2 * 8 = 16 addresses found
226 | assertThat(toList(nodes), hasSize(16));
227 | }
228 |
229 | @Test
230 | public void discoverNodesEmpty() {
231 | // given
232 | given(awsClient.getAddresses()).willReturn(Collections.emptyMap());
233 |
234 | // when
235 | Iterable result = awsDiscoveryStrategy.discoverNodes();
236 |
237 | // then
238 | assertEquals(emptyList(), result);
239 | }
240 |
241 | @Test
242 | public void discoverNodesException() {
243 | // given
244 | given(awsClient.getAddresses()).willThrow(new RuntimeException("Unknown exception"));
245 |
246 | // when
247 | Iterable result = awsDiscoveryStrategy.discoverNodes();
248 |
249 | // then
250 | assertEquals(emptyList(), result);
251 | }
252 |
253 | private static List toList(Iterable nodes) {
254 | List list = new ArrayList<>();
255 | nodes.forEach(list::add);
256 | return list;
257 | }
258 | }
259 |
--------------------------------------------------------------------------------
/src/test/java/com/hazelcast/aws/AwsEc2ClientTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Hazelcast Inc.
3 | *
4 | * Licensed under the Hazelcast Community License (the "License"); you may not use
5 | * this file except in compliance with the License. You may obtain a copy of the
6 | * License at
7 | *
8 | * http://hazelcast.com/hazelcast-community-license
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 | * WARRANTIES OF ANY KIND, either express or implied. See the License for the
13 | * specific language governing permissions and limitations under the License.
14 | */
15 |
16 | package com.hazelcast.aws;
17 |
18 | import org.junit.Test;
19 | import org.junit.runner.RunWith;
20 | import org.mockito.InjectMocks;
21 | import org.mockito.Mock;
22 | import org.mockito.runners.MockitoJUnitRunner;
23 |
24 | import java.util.Map;
25 | import java.util.Optional;
26 |
27 | import static java.util.Collections.singletonMap;
28 | import static org.junit.Assert.assertEquals;
29 | import static org.mockito.BDDMockito.given;
30 |
31 | @RunWith(MockitoJUnitRunner.class)
32 | public class AwsEc2ClientTest {
33 | @Mock
34 | private AwsEc2Api awsEc2Api;
35 |
36 | @Mock
37 | private AwsMetadataApi awsMetadataApi;
38 |
39 | @Mock
40 | private AwsCredentialsProvider awsCredentialsProvider;
41 |
42 | @InjectMocks
43 | private AwsEc2Client awsEc2Client;
44 |
45 | @Test
46 | public void getAddresses() {
47 | // given
48 | AwsCredentials credentials = AwsCredentials.builder()
49 | .setAccessKey("access-key")
50 | .setSecretKey("secret-key")
51 | .setToken("token")
52 | .build();
53 | Map expectedResult = singletonMap("123.12.1.0", "1.4.6.2");
54 |
55 | given(awsCredentialsProvider.credentials()).willReturn(credentials);
56 | given(awsEc2Api.describeInstances(credentials)).willReturn(expectedResult);
57 |
58 | // when
59 | Map result = awsEc2Client.getAddresses();
60 |
61 | // then
62 | assertEquals(expectedResult, result);
63 | }
64 |
65 | @Test
66 | public void getAvailabilityZone() {
67 | // given
68 | String expectedResult = "us-east-1a";
69 | given(awsMetadataApi.availabilityZoneEc2()).willReturn(expectedResult);
70 |
71 | // when
72 | String result = awsEc2Client.getAvailabilityZone();
73 |
74 | // then
75 | assertEquals(expectedResult, result);
76 | }
77 |
78 | @Test
79 | public void getPlacementGroup() {
80 | // given
81 | String placementGroup = "placement-group";
82 | String partitionNumber = "42";
83 | given(awsMetadataApi.placementGroupEc2()).willReturn(Optional.of(placementGroup));
84 | given(awsMetadataApi.placementPartitionNumberEc2()).willReturn(Optional.of(partitionNumber));
85 |
86 | // when
87 | Optional placementGroupResult = awsEc2Client.getPlacementGroup();
88 | Optional partitionNumberResult = awsEc2Client.getPlacementPartitionNumber();
89 |
90 | // then
91 | assertEquals(placementGroup, placementGroupResult.orElse("N/A"));
92 | assertEquals(partitionNumber, partitionNumberResult.orElse("N/A"));
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/test/java/com/hazelcast/aws/AwsEcsClientTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Hazelcast Inc.
3 | *
4 | * Licensed under the Hazelcast Community License (the "License"); you may not use
5 | * this file except in compliance with the License. You may obtain a copy of the
6 | * License at
7 | *
8 | * http://hazelcast.com/hazelcast-community-license
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 | * WARRANTIES OF ANY KIND, either express or implied. See the License for the
13 | * specific language governing permissions and limitations under the License.
14 | */
15 |
16 | package com.hazelcast.aws;
17 |
18 | import com.hazelcast.aws.AwsEcsApi.Task;
19 | import com.hazelcast.aws.AwsMetadataApi.EcsMetadata;
20 | import org.junit.Before;
21 | import org.junit.Test;
22 | import org.junit.runner.RunWith;
23 | import org.mockito.Mock;
24 | import org.mockito.runners.MockitoJUnitRunner;
25 |
26 | import java.util.List;
27 | import java.util.Map;
28 | import java.util.Optional;
29 |
30 | import static java.util.Collections.emptyList;
31 | import static java.util.Collections.singletonList;
32 | import static java.util.Collections.singletonMap;
33 | import static org.junit.Assert.assertEquals;
34 | import static org.junit.Assert.assertTrue;
35 | import static org.mockito.BDDMockito.given;
36 | import static org.mockito.Mockito.mock;
37 |
38 | @RunWith(MockitoJUnitRunner.class)
39 | public class AwsEcsClientTest {
40 | private static final String TASK_ARN = "task-arn";
41 | private static final String CLUSTER = "cluster-arn";
42 | private static final AwsCredentials CREDENTIALS = AwsCredentials.builder()
43 | .setAccessKey("access-key")
44 | .setSecretKey("secret-key")
45 | .setToken("token")
46 | .build();
47 |
48 | @Mock
49 | private AwsEcsApi awsEcsApi;
50 |
51 | @Mock
52 | private AwsEc2Api awsEc2Api;
53 |
54 | @Mock
55 | private AwsMetadataApi awsMetadataApi;
56 |
57 | @Mock
58 | private AwsCredentialsProvider awsCredentialsProvider;
59 |
60 | private AwsEcsClient awsEcsClient;
61 |
62 | @Before
63 | public void setUp() {
64 | EcsMetadata ecsMetadata = mock(EcsMetadata.class);
65 | given(ecsMetadata.getTaskArn()).willReturn(TASK_ARN);
66 | given(ecsMetadata.getClusterArn()).willReturn(CLUSTER);
67 | given(awsMetadataApi.metadataEcs()).willReturn(ecsMetadata);
68 | given(awsCredentialsProvider.credentials()).willReturn(CREDENTIALS);
69 |
70 | awsEcsClient = new AwsEcsClient(CLUSTER, awsEcsApi, awsEc2Api, awsMetadataApi, awsCredentialsProvider);
71 | }
72 |
73 | @Test
74 | public void getAddresses() {
75 | // given
76 | List taskArns = singletonList("task-arn");
77 | List privateIps = singletonList("123.12.1.0");
78 | List tasks = singletonList(new Task("123.12.1.0", null));
79 | Map expectedResult = singletonMap("123.12.1.0", "1.4.6.2");
80 | given(awsEcsApi.listTasks(CLUSTER, CREDENTIALS)).willReturn(taskArns);
81 | given(awsEcsApi.describeTasks(CLUSTER, taskArns, CREDENTIALS)).willReturn(tasks);
82 | given(awsEc2Api.describeNetworkInterfaces(privateIps, CREDENTIALS)).willReturn(expectedResult);
83 |
84 | // when
85 | Map result = awsEcsClient.getAddresses();
86 |
87 | // then
88 | assertEquals(expectedResult, result);
89 | }
90 |
91 | @Test
92 | public void getAddressesWithAwsConfig() {
93 | // given
94 | List taskArns = singletonList("task-arn");
95 | List privateIps = singletonList("123.12.1.0");
96 | List tasks = singletonList(new Task("123.12.1.0", null));
97 | Map expectedResult = singletonMap("123.12.1.0", "1.4.6.2");
98 | given(awsEcsApi.listTasks(CLUSTER, CREDENTIALS)).willReturn(taskArns);
99 | given(awsEcsApi.describeTasks(CLUSTER, taskArns, CREDENTIALS)).willReturn(tasks);
100 | given(awsEc2Api.describeNetworkInterfaces(privateIps, CREDENTIALS)).willReturn(expectedResult);
101 |
102 | // when
103 | Map result = awsEcsClient.getAddresses();
104 |
105 | // then
106 | assertEquals(expectedResult, result);
107 | }
108 |
109 | @Test
110 | public void getAddressesNoPublicAddresses() {
111 | // given
112 | List taskArns = singletonList("task-arn");
113 | List privateIps = singletonList("123.12.1.0");
114 | List tasks = singletonList(new Task("123.12.1.0", null));
115 | given(awsEcsApi.listTasks(CLUSTER, CREDENTIALS)).willReturn(taskArns);
116 | given(awsEcsApi.describeTasks(CLUSTER, taskArns, CREDENTIALS)).willReturn(tasks);
117 | given(awsEc2Api.describeNetworkInterfaces(privateIps, CREDENTIALS)).willThrow(new RuntimeException());
118 |
119 | // when
120 | Map result = awsEcsClient.getAddresses();
121 |
122 | // then
123 | assertEquals(singletonMap("123.12.1.0", null), result);
124 | }
125 |
126 | @Test
127 | public void getAddressesNoTasks() {
128 | // given
129 | List tasks = emptyList();
130 | given(awsEcsApi.listTasks(CLUSTER, CREDENTIALS)).willReturn(tasks);
131 |
132 | // when
133 | Map result = awsEcsClient.getAddresses();
134 |
135 | // then
136 | assertTrue(result.isEmpty());
137 | }
138 |
139 | @Test
140 | public void getAvailabilityZone() {
141 | // given
142 | String availabilityZone = "us-east-1";
143 | given(awsEcsApi.describeTasks(CLUSTER, singletonList(TASK_ARN), CREDENTIALS))
144 | .willReturn(singletonList(new Task(null, availabilityZone)));
145 |
146 | // when
147 | String result = awsEcsClient.getAvailabilityZone();
148 |
149 | // then
150 | assertEquals(availabilityZone, result);
151 | }
152 |
153 | @Test
154 | public void getAvailabilityZoneUnknown() {
155 | // given
156 | given(awsEcsApi.describeTasks(CLUSTER, singletonList(TASK_ARN), CREDENTIALS)).willReturn(emptyList());
157 |
158 | // when
159 | String result = awsEcsClient.getAvailabilityZone();
160 |
161 | // then
162 | assertEquals("unknown", result);
163 | }
164 |
165 | @Test
166 | public void getPlacementGroup() {
167 | // when
168 | Optional placementGroup = awsEcsClient.getPlacementGroup();
169 | Optional placementPartitionNumber = awsEcsClient.getPlacementPartitionNumber();
170 |
171 | // then
172 | // Placement aware is not supported for ECS
173 | assertEquals(Optional.empty(), placementGroup);
174 | assertEquals(Optional.empty(), placementPartitionNumber);
175 | }
176 | }
--------------------------------------------------------------------------------
/src/test/java/com/hazelcast/aws/AwsMetadataApiTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Hazelcast Inc.
3 | *
4 | * Licensed under the Hazelcast Community License (the "License"); you may not use
5 | * this file except in compliance with the License. You may obtain a copy of the
6 | * License at
7 | *
8 | * http://hazelcast.com/hazelcast-community-license
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 | * WARRANTIES OF ANY KIND, either express or implied. See the License for the
13 | * specific language governing permissions and limitations under the License.
14 | */
15 |
16 | package com.hazelcast.aws;
17 |
18 | import com.github.tomakehurst.wiremock.junit.WireMockRule;
19 | import com.hazelcast.aws.AwsMetadataApi.EcsMetadata;
20 | import org.junit.Before;
21 | import org.junit.Rule;
22 | import org.junit.Test;
23 |
24 | import java.util.Optional;
25 |
26 | import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
27 | import static com.github.tomakehurst.wiremock.client.WireMock.exactly;
28 | import static com.github.tomakehurst.wiremock.client.WireMock.get;
29 | import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor;
30 | import static com.github.tomakehurst.wiremock.client.WireMock.moreThan;
31 | import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
32 | import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
33 | import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching;
34 | import static com.github.tomakehurst.wiremock.client.WireMock.verify;
35 | import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
36 | import static org.junit.Assert.assertEquals;
37 | import static org.junit.Assert.assertThrows;
38 | import static org.junit.Assert.assertTrue;
39 |
40 | public class AwsMetadataApiTest {
41 |
42 | private final String GROUP_NAME_URL = "/placement/group-name/";
43 | private final String PARTITION_NO_URL = "/placement/partition-number/";
44 | private final int RETRY_COUNT = 3;
45 |
46 | private AwsMetadataApi awsMetadataApi;
47 |
48 | @Rule
49 | public WireMockRule wireMockRule = new WireMockRule(wireMockConfig().dynamicPort());
50 |
51 | @Before
52 | public void setUp() {
53 | AwsConfig awsConfig = AwsConfig.builder().setConnectionRetries(RETRY_COUNT).build();
54 | String endpoint = String.format("http://localhost:%s", wireMockRule.port());
55 | awsMetadataApi = new AwsMetadataApi(endpoint, endpoint, endpoint, awsConfig);
56 | }
57 |
58 | @Test
59 | public void availabilityZoneEc2() {
60 | // given
61 | String availabilityZone = "eu-central-1b";
62 | stubFor(get(urlEqualTo("/placement/availability-zone/"))
63 | .willReturn(aResponse().withStatus(200).withBody(availabilityZone)));
64 |
65 | // when
66 | String result = awsMetadataApi.availabilityZoneEc2();
67 |
68 | // then
69 | assertEquals(availabilityZone, result);
70 | }
71 |
72 | @Test
73 | public void placementGroupEc2() {
74 | // given
75 | String placementGroup = "placement-group-1";
76 | stubFor(get(urlEqualTo(GROUP_NAME_URL))
77 | .willReturn(aResponse().withStatus(200).withBody(placementGroup)));
78 |
79 | // when
80 | Optional result = awsMetadataApi.placementGroupEc2();
81 |
82 | // then
83 | assertEquals(placementGroup, result.orElse("N/A"));
84 | verify(exactly(1), getRequestedFor(urlEqualTo(GROUP_NAME_URL)));
85 | }
86 |
87 | @Test
88 | public void partitionPlacementGroupEc2() {
89 | // given
90 | String partitionNumber = "42";
91 | stubFor(get(urlEqualTo(PARTITION_NO_URL))
92 | .willReturn(aResponse().withStatus(200).withBody(partitionNumber)));
93 |
94 | // when
95 | Optional result = awsMetadataApi.placementPartitionNumberEc2();
96 |
97 | // then
98 | assertEquals(partitionNumber, result.orElse("N/A"));
99 | verify(exactly(1), getRequestedFor(urlEqualTo(PARTITION_NO_URL)));
100 | }
101 |
102 | @Test
103 | public void missingPlacementGroupEc2() {
104 | // given
105 | stubFor(get(urlEqualTo(GROUP_NAME_URL))
106 | .willReturn(aResponse().withStatus(404).withBody("Not found")));
107 | stubFor(get(urlEqualTo(PARTITION_NO_URL))
108 | .willReturn(aResponse().withStatus(404).withBody("Not found")));
109 |
110 | // when
111 | Optional placementGroupResult = awsMetadataApi.placementGroupEc2();
112 | Optional partitionNumberResult = awsMetadataApi.placementPartitionNumberEc2();
113 |
114 | // then
115 | assertEquals(Optional.empty(), placementGroupResult);
116 | assertEquals(Optional.empty(), partitionNumberResult);
117 | verify(exactly(1), getRequestedFor(urlEqualTo(GROUP_NAME_URL)));
118 | verify(exactly(1), getRequestedFor(urlEqualTo(PARTITION_NO_URL)));
119 | }
120 |
121 | @Test
122 | public void failToFetchPlacementGroupEc2() {
123 | // given
124 | stubFor(get(urlEqualTo(GROUP_NAME_URL))
125 | .willReturn(aResponse().withStatus(500).withBody("Service Unavailable")));
126 |
127 | // when
128 | Optional placementGroupResult = awsMetadataApi.placementGroupEc2();
129 |
130 | // then
131 | assertEquals(Optional.empty(), placementGroupResult);
132 | verify(moreThan(RETRY_COUNT), getRequestedFor(urlEqualTo(GROUP_NAME_URL)));
133 | }
134 |
135 | @Test
136 | public void defaultIamRoleEc2() {
137 | // given
138 | String defaultIamRole = "default-role-name";
139 | stubFor(get(urlEqualTo("/iam/security-credentials/"))
140 | .willReturn(aResponse().withStatus(200).withBody(defaultIamRole)));
141 |
142 | // when
143 | String result = awsMetadataApi.defaultIamRoleEc2();
144 |
145 | // then
146 | assertEquals(defaultIamRole, result);
147 | }
148 |
149 | @Test
150 | public void credentialsEc2() {
151 | // given
152 | String iamRole = "some-iam-role";
153 | String response = "{\n"
154 | + " \"Code\": \"Success\",\n"
155 | + " \"AccessKeyId\": \"Access1234\",\n"
156 | + " \"SecretAccessKey\": \"Secret1234\",\n"
157 | + " \"Token\": \"Token1234\",\n"
158 | + " \"Expiration\": \"2020-03-27T21:01:33Z\"\n"
159 | + "}";
160 | stubFor(get(urlEqualTo(String.format("/iam/security-credentials/%s", iamRole)))
161 | .willReturn(aResponse().withStatus(200).withBody(response)));
162 |
163 | // when
164 | AwsCredentials result = awsMetadataApi.credentialsEc2(iamRole);
165 |
166 | // then
167 | assertEquals("Access1234", result.getAccessKey());
168 | assertEquals("Secret1234", result.getSecretKey());
169 | assertEquals("Token1234", result.getToken());
170 | }
171 |
172 | @Test
173 | public void credentialsEcs() {
174 | // given
175 | String response = "{\n"
176 | + " \"Code\": \"Success\",\n"
177 | + " \"AccessKeyId\": \"Access1234\",\n"
178 | + " \"SecretAccessKey\": \"Secret1234\",\n"
179 | + " \"Token\": \"Token1234\",\n"
180 | + " \"Expiration\": \"2020-03-27T21:01:33Z\"\n"
181 | + "}";
182 | stubFor(get(urlEqualTo("/"))
183 | .willReturn(aResponse().withStatus(200).withBody(response)));
184 |
185 | // when
186 | AwsCredentials result = awsMetadataApi.credentialsEcs();
187 |
188 | // then
189 | assertEquals("Access1234", result.getAccessKey());
190 | assertEquals("Secret1234", result.getSecretKey());
191 | assertEquals("Token1234", result.getToken());
192 | }
193 |
194 | @Test
195 | public void metadataEcs() {
196 | // given
197 | //language=JSON
198 | String response = "{\n"
199 | + " \"Name\": \"container-name\",\n"
200 | + " \"Labels\": {\n"
201 | + " \"com.amazonaws.ecs.cluster\": \"arn:aws:ecs:eu-central-1:665466731577:cluster/default\",\n"
202 | + " \"com.amazonaws.ecs.container-name\": \"container-name\",\n"
203 | + " \"com.amazonaws.ecs.task-arn\": \"arn:aws:ecs:eu-central-1:665466731577:task/default/0dcf990c3ef3436c84e0c7430d14a3d4\",\n"
204 | + " \"com.amazonaws.ecs.task-definition-family\": \"family-name\"\n"
205 | + " },\n"
206 | + " \"Networks\": [\n"
207 | + " {\n"
208 | + " \"NetworkMode\": \"awsvpc\",\n"
209 | + " \"IPv4Addresses\": [\n"
210 | + " \"10.0.1.174\"\n"
211 | + " ]\n"
212 | + " }\n"
213 | + " ]\n"
214 | + "}";
215 | stubFor(get("/").willReturn(aResponse().withStatus(200).withBody(response)));
216 |
217 | // when
218 | EcsMetadata result = awsMetadataApi.metadataEcs();
219 |
220 | // then
221 | assertEquals("arn:aws:ecs:eu-central-1:665466731577:task/default/0dcf990c3ef3436c84e0c7430d14a3d4",
222 | result.getTaskArn());
223 | assertEquals("arn:aws:ecs:eu-central-1:665466731577:cluster/default", result.getClusterArn());
224 | }
225 |
226 | @Test
227 | public void awsError() {
228 | // given
229 | int errorCode = 401;
230 | String errorMessage = "Error message retrieved from AWS";
231 | stubFor(get(urlMatching("/.*"))
232 | .willReturn(aResponse().withStatus(errorCode).withBody(errorMessage)));
233 |
234 | // when
235 | Exception exception = assertThrows(Exception.class, () -> awsMetadataApi.defaultIamRoleEc2());
236 |
237 | // then
238 | assertTrue(exception.getMessage().contains(Integer.toString(errorCode)));
239 | assertTrue(exception.getMessage().contains(errorMessage));
240 | verify(moreThan(RETRY_COUNT), getRequestedFor(urlMatching("/.*")));
241 | }
242 | }
243 |
--------------------------------------------------------------------------------
/src/test/java/com/hazelcast/aws/AwsRequestSignerTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Hazelcast Inc.
3 | *
4 | * Licensed under the Hazelcast Community License (the "License"); you may not use
5 | * this file except in compliance with the License. You may obtain a copy of the
6 | * License at
7 | *
8 | * http://hazelcast.com/hazelcast-community-license
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 | * WARRANTIES OF ANY KIND, either express or implied. See the License for the
13 | * specific language governing permissions and limitations under the License.
14 | */
15 |
16 | package com.hazelcast.aws;
17 |
18 | import org.junit.Test;
19 |
20 | import java.util.HashMap;
21 | import java.util.Map;
22 |
23 | import static java.util.Collections.emptyMap;
24 | import static org.junit.Assert.assertEquals;
25 |
26 | public class AwsRequestSignerTest {
27 |
28 | @Test
29 | public void authHeaderEc2() {
30 | // given
31 | String timestamp = "20141106T111126Z";
32 |
33 | Map attributes = new HashMap<>();
34 | attributes.put("Action", "DescribeInstances");
35 | attributes.put("Version", "2016-11-15");
36 |
37 | Map headers = new HashMap<>();
38 | headers.put("X-Amz-Date", timestamp);
39 | headers.put("Host", "ec2.eu-central-1.amazonaws.com");
40 |
41 | String body = "";
42 |
43 | AwsCredentials credentials = AwsCredentials.builder()
44 | .setAccessKey("AKIDEXAMPLE")
45 | .setSecretKey("wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY")
46 | .build();
47 |
48 | AwsRequestSigner requestSigner = new AwsRequestSigner("eu-central-1", "ec2");
49 |
50 | // when
51 | String authHeader = requestSigner.authHeader(attributes, headers, body, credentials, timestamp, "POST");
52 |
53 | // then
54 | String expectedAuthHeader = "AWS4-HMAC-SHA256 "
55 | + "Credential=AKIDEXAMPLE/20141106/eu-central-1/ec2/aws4_request, "
56 | + "SignedHeaders=host;x-amz-date, "
57 | + "Signature=cedc903f54260b232ced76caf4a72f061565a51cc583a17da87b1132522f5893";
58 | assertEquals(expectedAuthHeader, authHeader);
59 | }
60 |
61 | @Test
62 | public void authHeaderEcs() {
63 | // given
64 | String timestamp = "20141106T111126Z";
65 |
66 | Map headers = new HashMap<>();
67 | headers.put("X-Amz-Date", timestamp);
68 | headers.put("Host", "ecs.eu-central-1.amazonaws.com");
69 |
70 | //language=JSON
71 | String body = "{\n"
72 | + " \"cluster\": \"123456\",\n"
73 | + " \"family\": \"abcdef\"\n"
74 | + "}";
75 |
76 | AwsCredentials credentials = AwsCredentials.builder()
77 | .setAccessKey("AKIDEXAMPLE")
78 | .setSecretKey("wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY")
79 | .build();
80 |
81 | AwsRequestSigner requestSigner = new AwsRequestSigner("eu-central-1", "ecs");
82 |
83 | // when
84 | String authHeader = requestSigner.authHeader(emptyMap(), headers, body, credentials, timestamp, "GET");
85 |
86 | // then
87 | String expectedAuthHeader = "AWS4-HMAC-SHA256 "
88 | + "Credential=AKIDEXAMPLE/20141106/eu-central-1/ecs/aws4_request, "
89 | + "SignedHeaders=host;x-amz-date, "
90 | + "Signature=d25323cd86f9e960d0303599891d54fb9a1a0975bd132c06e95f767118d5bf55";
91 | assertEquals(expectedAuthHeader, authHeader);
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/src/test/java/com/hazelcast/aws/AwsRequestUtilsTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Hazelcast Inc.
3 | *
4 | * Licensed under the Hazelcast Community License (the "License"); you may not use
5 | * this file except in compliance with the License. You may obtain a copy of the
6 | * License at
7 | *
8 | * http://hazelcast.com/hazelcast-community-license
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 | * WARRANTIES OF ANY KIND, either express or implied. See the License for the
13 | * specific language governing permissions and limitations under the License.
14 | */
15 |
16 | package com.hazelcast.aws;
17 |
18 | import org.junit.Test;
19 |
20 | import java.time.Clock;
21 | import java.time.Instant;
22 | import java.time.ZoneId;
23 | import java.util.HashMap;
24 | import java.util.Map;
25 |
26 | import static org.junit.Assert.assertEquals;
27 |
28 |
29 | public class AwsRequestUtilsTest {
30 |
31 | @Test
32 | public void currentTimestamp() {
33 | // given
34 | Clock clock = Clock.fixed(Instant.ofEpochMilli(1585909518929L), ZoneId.systemDefault());
35 |
36 | // when
37 | String currentTimestamp = AwsRequestUtils.currentTimestamp(clock);
38 |
39 | // then
40 | assertEquals("20200403T102518Z", currentTimestamp);
41 | }
42 |
43 | @Test
44 | public void canonicalQueryString() {
45 | // given
46 | Map attributes = new HashMap<>();
47 | attributes.put("second-attribute", "second-attribute+value");
48 | attributes.put("attribute", "attribute+value");
49 | attributes.put("name", "Name*");
50 |
51 | // when
52 | String result = AwsRequestUtils.canonicalQueryString(attributes);
53 |
54 | assertEquals("attribute=attribute%2Bvalue&name=Name%2A&second-attribute=second-attribute%2Bvalue", result);
55 | }
56 |
57 | @Test
58 | public void urlFor() {
59 | assertEquals("https://some-endpoint", AwsRequestUtils.urlFor("some-endpoint"));
60 | assertEquals("https://some-endpoint", AwsRequestUtils.urlFor("https://some-endpoint"));
61 | assertEquals("http://some-endpoint", AwsRequestUtils.urlFor("http://some-endpoint"));
62 | }
63 |
64 | }
65 |
--------------------------------------------------------------------------------
/src/test/java/com/hazelcast/aws/FilterTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Hazelcast Inc.
3 | *
4 | * Licensed under the Hazelcast Community License (the "License"); you may not use
5 | * this file except in compliance with the License. You may obtain a copy of the
6 | * License at
7 | *
8 | * http://hazelcast.com/hazelcast-community-license
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 | * WARRANTIES OF ANY KIND, either express or implied. See the License for the
13 | * specific language governing permissions and limitations under the License.
14 | */
15 |
16 | package com.hazelcast.aws;
17 |
18 | import org.junit.Test;
19 |
20 | import java.util.Map;
21 |
22 | import static java.util.Arrays.asList;
23 | import static org.junit.Assert.assertEquals;
24 |
25 | public class FilterTest {
26 |
27 | @Test
28 | public void add() {
29 | // given
30 | Filter filter = new Filter();
31 |
32 | // when
33 | filter.add("key", "value");
34 | filter.add("second-key", "second-value");
35 | Map result = filter.getFilterAttributes();
36 |
37 | // then
38 | assertEquals(4, result.size());
39 | assertEquals("key", result.get("Filter.1.Name"));
40 | assertEquals("value", result.get("Filter.1.Value.1"));
41 | assertEquals("second-key", result.get("Filter.2.Name"));
42 | assertEquals("second-value", result.get("Filter.2.Value.1"));
43 | }
44 |
45 | @Test
46 | public void addMulti() {
47 | // given
48 | Filter filter = new Filter();
49 |
50 | // when
51 | filter.addMulti("key", asList("value", "second-value"));
52 |
53 | // then
54 | Map result = filter.getFilterAttributes();
55 |
56 | // then
57 | assertEquals(3, result.size());
58 | assertEquals("key", result.get("Filter.1.Name"));
59 | assertEquals("value", result.get("Filter.1.Value.1"));
60 | assertEquals("second-value", result.get("Filter.1.Value.2"));
61 | }
62 |
63 | }
--------------------------------------------------------------------------------
/src/test/java/com/hazelcast/aws/PortRangeTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Hazelcast Inc.
3 | *
4 | * Licensed under the Hazelcast Community License (the "License"); you may not use
5 | * this file except in compliance with the License. You may obtain a copy of the
6 | * License at
7 | *
8 | * http://hazelcast.com/hazelcast-community-license
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 | * WARRANTIES OF ANY KIND, either express or implied. See the License for the
13 | * specific language governing permissions and limitations under the License.
14 | */
15 |
16 | package com.hazelcast.aws;
17 |
18 | import org.junit.Test;
19 |
20 | import static org.junit.Assert.assertEquals;
21 |
22 | public class PortRangeTest {
23 |
24 | @Test
25 | public void portNumber() {
26 | // given
27 | int portNumber = 12345;
28 | String spec = String.valueOf(portNumber);
29 |
30 | // when
31 | PortRange portRange = new PortRange(spec);
32 |
33 | // then
34 | assertEquals(portNumber, portRange.getFromPort());
35 | assertEquals(portNumber, portRange.getToPort());
36 | }
37 |
38 | @Test(expected = IllegalArgumentException.class)
39 | public void portNumberOutOfPortRange() {
40 | new PortRange("12345678");
41 | }
42 |
43 | @Test(expected = IllegalArgumentException.class)
44 | public void portNumberOutOfIntegerRange() {
45 | new PortRange("123456789012356789123456789");
46 | }
47 |
48 | @Test
49 | public void portRange() {
50 | // given
51 | String spec = "123-456";
52 |
53 | // when
54 | PortRange portRange = new PortRange(spec);
55 |
56 | // then
57 | assertEquals(123, portRange.getFromPort());
58 | assertEquals(456, portRange.getToPort());
59 | }
60 |
61 | @Test(expected = IllegalArgumentException.class)
62 | public void portRangeFromPortOutOfRange() {
63 | new PortRange("12345678-1");
64 | }
65 |
66 | @Test(expected = IllegalArgumentException.class)
67 | public void portRangeToPortOutOfRange() {
68 | new PortRange("1-123456789");
69 | }
70 |
71 | @Test(expected = IllegalArgumentException.class)
72 | public void portRangeFromPortGreaterThanToPort() {
73 | new PortRange("2-1");
74 | }
75 |
76 | @Test(expected = IllegalArgumentException.class)
77 | public void invalidSpec() {
78 | new PortRange("abcd");
79 | }
80 | }
--------------------------------------------------------------------------------
/src/test/java/com/hazelcast/aws/RegionValidatorTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Hazelcast Inc.
3 | *
4 | * Licensed under the Hazelcast Community License (the "License"); you may not use
5 | * this file except in compliance with the License. You may obtain a copy of the
6 | * License at
7 | *
8 | * http://hazelcast.com/hazelcast-community-license
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 | * WARRANTIES OF ANY KIND, either express or implied. See the License for the
13 | * specific language governing permissions and limitations under the License.
14 | */
15 |
16 | package com.hazelcast.aws;
17 |
18 | import com.hazelcast.config.InvalidConfigurationException;
19 | import org.junit.Test;
20 |
21 | import static com.hazelcast.test.HazelcastTestSupport.assertThrows;
22 | import static org.junit.Assert.assertEquals;
23 |
24 | public class RegionValidatorTest {
25 | @Test
26 | public void validateValidRegion() {
27 | RegionValidator.validateRegion("us-west-1");
28 | RegionValidator.validateRegion("us-gov-east-1");
29 | }
30 |
31 | @Test
32 | public void validateInvalidRegion() {
33 | // given
34 | String region = "us-wrong-1";
35 | String expectedMessage = String.format("The provided region %s is not a valid AWS region.", region);
36 |
37 | //when
38 | Runnable validateRegion = () -> RegionValidator.validateRegion(region);
39 |
40 | //then
41 | InvalidConfigurationException thrownEx = assertThrows(InvalidConfigurationException.class, validateRegion);
42 | assertEquals(expectedMessage, thrownEx.getMessage());
43 | }
44 |
45 | @Test
46 | public void validateInvalidGovRegion() {
47 | // given
48 | String region = "us-gov-wrong-1";
49 | String expectedMessage = String.format("The provided region %s is not a valid AWS region.", region);
50 |
51 | // when
52 | Runnable validateRegion = () -> RegionValidator.validateRegion(region);
53 |
54 | //then
55 | InvalidConfigurationException thrownEx = assertThrows(InvalidConfigurationException.class, validateRegion);
56 | assertEquals(expectedMessage, thrownEx.getMessage());
57 | }
58 |
59 | }
--------------------------------------------------------------------------------
/src/test/java/com/hazelcast/aws/RestClientTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Hazelcast Inc.
3 | *
4 | * Licensed under the Hazelcast Community License (the "License"); you may not use
5 | * this file except in compliance with the License. You may obtain a copy of the
6 | * License at
7 | *
8 | * http://hazelcast.com/hazelcast-community-license
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 | * WARRANTIES OF ANY KIND, either express or implied. See the License for the
13 | * specific language governing permissions and limitations under the License.
14 | */
15 |
16 | package com.hazelcast.aws;
17 |
18 | import com.github.tomakehurst.wiremock.junit.WireMockRule;
19 | import org.junit.Before;
20 | import org.junit.Rule;
21 | import org.junit.Test;
22 |
23 | import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
24 | import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
25 | import static com.github.tomakehurst.wiremock.client.WireMock.get;
26 | import static com.github.tomakehurst.wiremock.client.WireMock.post;
27 | import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
28 | import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
29 | import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
30 | import static com.github.tomakehurst.wiremock.stubbing.Scenario.STARTED;
31 | import static java.util.Collections.singletonMap;
32 | import static org.junit.Assert.assertEquals;
33 | import static org.junit.Assert.assertThrows;
34 |
35 | public class RestClientTest {
36 | private static final String API_ENDPOINT = "/some/endpoint";
37 | private static final String BODY_REQUEST = "some body request";
38 | private static final String BODY_RESPONSE = "some body response";
39 |
40 | @Rule
41 | public WireMockRule wireMockRule = new WireMockRule(wireMockConfig().dynamicPort());
42 |
43 | private String address;
44 |
45 | @Before
46 | public void setUp() {
47 | address = String.format("http://localhost:%s", wireMockRule.port());
48 | }
49 |
50 | @Test
51 | public void getSuccess() {
52 | // given
53 | stubFor(get(urlEqualTo(API_ENDPOINT))
54 | .willReturn(aResponse().withStatus(200).withBody(BODY_RESPONSE)));
55 |
56 | // when
57 | String result = RestClient.create(String.format("%s%s", address, API_ENDPOINT)).get().getBody();
58 |
59 | // then
60 | assertEquals(BODY_RESPONSE, result);
61 | }
62 |
63 | @Test
64 | public void getWithHeadersSuccess() {
65 | // given
66 | String headerKey = "Metadata-Flavor";
67 | String headerValue = "Google";
68 | stubFor(get(urlEqualTo(API_ENDPOINT))
69 | .withHeader(headerKey, equalTo(headerValue))
70 | .willReturn(aResponse().withStatus(200).withBody(BODY_RESPONSE)));
71 |
72 | // when
73 | String result = RestClient.create(String.format("%s%s", address, API_ENDPOINT))
74 | .withHeaders(singletonMap(headerKey, headerValue))
75 | .get()
76 | .getBody();
77 |
78 | // then
79 | assertEquals(BODY_RESPONSE, result);
80 | }
81 |
82 | @Test
83 | public void getWithRetries() {
84 | // given
85 | stubFor(get(urlEqualTo(API_ENDPOINT))
86 | .inScenario("Retry Scenario")
87 | .whenScenarioStateIs(STARTED)
88 | .willReturn(aResponse().withStatus(500).withBody("Internal error"))
89 | .willSetStateTo("Second Try"));
90 | stubFor(get(urlEqualTo(API_ENDPOINT))
91 | .inScenario("Retry Scenario")
92 | .whenScenarioStateIs("Second Try")
93 | .willReturn(aResponse().withStatus(200).withBody(BODY_RESPONSE)));
94 |
95 | // when
96 | String result = RestClient.create(String.format("%s%s", address, API_ENDPOINT))
97 | .withReadTimeoutSeconds(1200)
98 | .withConnectTimeoutSeconds(1200)
99 | .withRetries(1)
100 | .get()
101 | .getBody();
102 |
103 | // then
104 | assertEquals(BODY_RESPONSE, result);
105 | }
106 |
107 | @Test(expected = Exception.class)
108 | public void getFailure() {
109 | // given
110 | stubFor(get(urlEqualTo(API_ENDPOINT))
111 | .willReturn(aResponse().withStatus(500).withBody("Internal error")));
112 |
113 | // when
114 | RestClient.create(String.format("%s%s", address, API_ENDPOINT)).get();
115 |
116 | // then
117 | // throw exception
118 | }
119 |
120 | @Test
121 | public void postSuccess() {
122 | // given
123 | stubFor(post(urlEqualTo(API_ENDPOINT))
124 | .withRequestBody(equalTo(BODY_REQUEST))
125 | .willReturn(aResponse().withStatus(200).withBody(BODY_RESPONSE)));
126 |
127 | // when
128 | String result = RestClient.create(String.format("%s%s", address, API_ENDPOINT))
129 | .withBody(BODY_REQUEST)
130 | .post()
131 | .getBody();
132 |
133 | // then
134 | assertEquals(BODY_RESPONSE, result);
135 | }
136 |
137 | @Test
138 | public void expectedResponseCode() {
139 | // given
140 | int expectedCode1 = 201, expectedCode2 = 202;
141 | stubFor(post(urlEqualTo(API_ENDPOINT))
142 | .withRequestBody(equalTo(BODY_REQUEST))
143 | .willReturn(aResponse().withStatus(expectedCode1).withBody(BODY_RESPONSE)));
144 |
145 | // when
146 | String result = RestClient.create(String.format("%s%s", address, API_ENDPOINT))
147 | .withBody(BODY_REQUEST)
148 | .expectResponseCodes(expectedCode1, expectedCode2)
149 | .post()
150 | .getBody();
151 |
152 | // then
153 | assertEquals(BODY_RESPONSE, result);
154 | }
155 |
156 | @Test
157 | public void expectHttpOkByDefault() {
158 | // given
159 | int responseCode = 201;
160 | stubFor(post(urlEqualTo(API_ENDPOINT))
161 | .withRequestBody(equalTo(BODY_REQUEST))
162 | .willReturn(aResponse().withStatus(responseCode).withBody(BODY_RESPONSE)));
163 |
164 | // when
165 | RestClientException exception = assertThrows(RestClientException.class, () ->
166 | RestClient.create(String.format("%s%s", address, API_ENDPOINT))
167 | .withBody(BODY_REQUEST)
168 | .get());
169 |
170 | // then
171 | assertEquals(exception.getHttpErrorCode(), responseCode);
172 | }
173 |
174 | @Test
175 | public void unexpectedResponseCode() {
176 | // given
177 | int expectedCode = 201, unexpectedCode = 202;
178 | stubFor(post(urlEqualTo(API_ENDPOINT))
179 | .withRequestBody(equalTo(BODY_REQUEST))
180 | .willReturn(aResponse().withStatus(unexpectedCode).withBody(BODY_RESPONSE)));
181 |
182 | // when
183 | RestClientException exception = assertThrows(RestClientException.class, () ->
184 | RestClient.create(String.format("%s%s", address, API_ENDPOINT))
185 | .withBody(BODY_REQUEST)
186 | .expectResponseCodes(expectedCode)
187 | .post());
188 |
189 | // then
190 | assertEquals(exception.getHttpErrorCode(), unexpectedCode);
191 | }
192 |
193 | @Test
194 | public void readErrorResponse() {
195 | // given
196 | int responseCode = 418;
197 | String responseMessage = "I'm a teapot";
198 | stubFor(post(urlEqualTo(API_ENDPOINT))
199 | .withRequestBody(equalTo(BODY_REQUEST))
200 | .willReturn(aResponse().withStatus(responseCode).withBody(responseMessage)));
201 |
202 | // when
203 | RestClient.Response response = RestClient.create(String.format("%s%s", address, API_ENDPOINT))
204 | .withBody(BODY_REQUEST)
205 | .expectResponseCodes(responseCode)
206 | .get();
207 |
208 | // then
209 | assertEquals(responseCode, response.getCode());
210 | assertEquals(responseMessage, response.getBody());
211 | }
212 | }
213 |
--------------------------------------------------------------------------------
/src/test/java/com/hazelcast/aws/RetryUtilsTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Hazelcast Inc.
3 | *
4 | * Licensed under the Hazelcast Community License (the "License"); you may not use
5 | * this file except in compliance with the License. You may obtain a copy of the
6 | * License at
7 | *
8 | * http://hazelcast.com/hazelcast-community-license
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 | * WARRANTIES OF ANY KIND, either express or implied. See the License for the
13 | * specific language governing permissions and limitations under the License.
14 | */
15 |
16 | package com.hazelcast.aws;
17 |
18 | import com.hazelcast.core.HazelcastException;
19 | import com.hazelcast.test.HazelcastParallelClassRunner;
20 | import com.hazelcast.test.annotation.ParallelJVMTest;
21 | import com.hazelcast.test.annotation.QuickTest;
22 | import org.junit.Test;
23 | import org.junit.experimental.categories.Category;
24 | import org.junit.runner.RunWith;
25 |
26 | import java.util.concurrent.Callable;
27 |
28 | import static org.junit.Assert.assertEquals;
29 | import static org.mockito.BDDMockito.given;
30 | import static org.mockito.Mockito.mock;
31 | import static org.mockito.Mockito.times;
32 | import static org.mockito.Mockito.verify;
33 |
34 | @RunWith(HazelcastParallelClassRunner.class)
35 | @Category({QuickTest.class, ParallelJVMTest.class})
36 | public class RetryUtilsTest {
37 | private static final Integer RETRIES = 1;
38 | private static final String RESULT = "result string";
39 |
40 | private Callable callable = mock(Callable.class);
41 |
42 | @Test
43 | public void retryNoRetries()
44 | throws Exception {
45 | // given
46 | given(callable.call()).willReturn(RESULT);
47 |
48 | // when
49 | String result = RetryUtils.retry(callable, RETRIES);
50 |
51 | // then
52 | assertEquals(RESULT, result);
53 | verify(callable).call();
54 | }
55 |
56 | @Test
57 | public void retryRetriesSuccessful()
58 | throws Exception {
59 | // given
60 | given(callable.call()).willThrow(new RuntimeException()).willReturn(RESULT);
61 |
62 | // when
63 | String result = RetryUtils.retry(callable, RETRIES);
64 |
65 | // then
66 | assertEquals(RESULT, result);
67 | verify(callable, times(2)).call();
68 | }
69 |
70 | @Test(expected = RuntimeException.class)
71 | public void retryRetriesFailed()
72 | throws Exception {
73 | // given
74 | given(callable.call()).willThrow(new RuntimeException()).willThrow(new RuntimeException()).willReturn(RESULT);
75 |
76 | // when
77 | RetryUtils.retry(callable, RETRIES);
78 |
79 | // then
80 | // throws exception
81 | }
82 |
83 | @Test(expected = HazelcastException.class)
84 | public void retryRetriesFailedUncheckedException()
85 | throws Exception {
86 | // given
87 | given(callable.call()).willThrow(new Exception()).willThrow(new Exception()).willReturn(RESULT);
88 |
89 | // when
90 | RetryUtils.retry(callable, RETRIES);
91 |
92 | // then
93 | // throws exception
94 | }
95 | }
--------------------------------------------------------------------------------
/src/test/java/com/hazelcast/aws/StringUtilsTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Hazelcast Inc.
3 | *
4 | * Licensed under the Hazelcast Community License (the "License"); you may not use
5 | * this file except in compliance with the License. You may obtain a copy of the
6 | * License at
7 | *
8 | * http://hazelcast.com/hazelcast-community-license
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 | * WARRANTIES OF ANY KIND, either express or implied. See the License for the
13 | * specific language governing permissions and limitations under the License.
14 | */
15 |
16 | package com.hazelcast.aws;
17 |
18 | import org.junit.Test;
19 |
20 | import static org.junit.Assert.assertTrue;
21 |
22 | public class StringUtilsTest {
23 |
24 | @Test
25 | public void isEmpty() {
26 | assertTrue(StringUtils.isEmpty(null));
27 | assertTrue(StringUtils.isEmpty(""));
28 | assertTrue(StringUtils.isEmpty(" \t "));
29 | }
30 |
31 | @Test
32 | public void isNotEmpty() {
33 | assertTrue(StringUtils.isNotEmpty("some-string"));
34 | }
35 | }
--------------------------------------------------------------------------------
/src/test/java/com/hazelcast/aws/TagTest.java:
--------------------------------------------------------------------------------
1 | package com.hazelcast.aws;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.assertEquals;
6 | import static org.junit.Assert.assertNull;
7 |
8 | public class TagTest {
9 | @Test
10 | public void tagKeyOnly() {
11 | // given
12 | String key = "Key";
13 | String value = null;
14 |
15 | // when
16 | Tag tag = new Tag(key, value);
17 |
18 | // then
19 | assertEquals(tag.getKey(), key);
20 | assertNull(tag.getValue());
21 | }
22 |
23 | @Test
24 | public void tagValueOnly() {
25 | // given
26 | String key = null;
27 | String value = "Value";
28 |
29 | // when
30 | Tag tag = new Tag(key, value);
31 |
32 | // then
33 | assertNull(tag.getKey());
34 | assertEquals(tag.getValue(), value);
35 | }
36 |
37 | @Test(expected = IllegalArgumentException.class)
38 | public void missingKeyAndValue() {
39 | // given
40 | String key = null;
41 | String value = null;
42 |
43 | // when
44 | new Tag(key, value);
45 |
46 | // then
47 | // throws exception
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/test/java/com/hazelcast/aws/XmlNodeTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Hazelcast Inc.
3 | *
4 | * Licensed under the Hazelcast Community License (the "License"); you may not use
5 | * this file except in compliance with the License. You may obtain a copy of the
6 | * License at
7 | *
8 | * http://hazelcast.com/hazelcast-community-license
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 | * WARRANTIES OF ANY KIND, either express or implied. See the License for the
13 | * specific language governing permissions and limitations under the License.
14 | */
15 |
16 | package com.hazelcast.aws;
17 |
18 | import org.junit.Test;
19 |
20 | import java.util.List;
21 | import java.util.stream.Collectors;
22 |
23 | import static org.hamcrest.MatcherAssert.assertThat;
24 | import static org.hamcrest.Matchers.hasItems;
25 |
26 | public class XmlNodeTest {
27 |
28 | @Test
29 | public void parse() {
30 | // given
31 | //language=XML
32 | String xml = "\n"
33 | + "\n"
34 | + " \n"
35 | + " - \n"
36 | + " value\n"
37 | + "
\n"
38 | + " - \n"
39 | + " second-value\n"
40 | + "
\n"
41 | + " \n"
42 | + "";
43 |
44 | // when
45 | List itemValues = XmlNode.create(xml)
46 | .getSubNodes("parent").stream()
47 | .flatMap(e -> e.getSubNodes("item").stream())
48 | .map(item -> item.getValue("key"))
49 | .collect(Collectors.toList());
50 |
51 | // then
52 | assertThat(itemValues, hasItems("value", "second-value"));
53 | }
54 |
55 | @Test(expected = RuntimeException.class)
56 | public void parseError() {
57 | // given
58 | String xml = "malformed-xml";
59 |
60 | // when
61 | XmlNode.create(xml);
62 |
63 | // then
64 | // throws exception
65 | }
66 |
67 | }
--------------------------------------------------------------------------------
/src/test/resources/test-aws-config.xml:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
20 |
21 |
22 | true
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | test-access-key
34 | test-secret-key
35 | us-east-1
36 | ec2.test-host-header
37 | test-security-group-name
38 | test-tag-key
39 | test-tag-value
40 | 10
41 | 10
42 | 11
43 | 5702
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------