├── .github ├── CODEOWNERS ├── dependabot.yml ├── terraform │ ├── hazelcast-client.yaml │ ├── hazelcast.yaml │ ├── main.tf │ ├── scripts │ │ ├── start_aws_hazelcast_management_center.sh │ │ ├── start_aws_hazelcast_member.sh │ │ ├── verify_mancenter.sh │ │ └── verify_member_count.sh │ ├── terraform.tfvars │ └── variables.tf └── workflows │ ├── build.yml │ └── terraform-integration-tests.yml ├── .gitignore ├── LICENSE ├── README.md ├── checkstyle ├── ClassHeader.txt ├── checkstyle.xml └── suppressions.xml ├── findbugs └── findbugs-exclude.xml ├── markdown └── images │ └── aws-autoscaling-architecture.png ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── hazelcast │ │ └── aws │ │ ├── AwsClient.java │ │ ├── AwsClientConfigurator.java │ │ ├── AwsConfig.java │ │ ├── AwsCredentials.java │ │ ├── AwsCredentialsProvider.java │ │ ├── AwsDiscoveryStrategy.java │ │ ├── AwsDiscoveryStrategyFactory.java │ │ ├── AwsEc2Api.java │ │ ├── AwsEc2Client.java │ │ ├── AwsEcsApi.java │ │ ├── AwsEcsClient.java │ │ ├── AwsMetadataApi.java │ │ ├── AwsProperties.java │ │ ├── AwsRequestSigner.java │ │ ├── AwsRequestUtils.java │ │ ├── Environment.java │ │ ├── Filter.java │ │ ├── NoCredentialsException.java │ │ ├── PortRange.java │ │ ├── RegionValidator.java │ │ ├── RestClient.java │ │ ├── RestClientException.java │ │ ├── RetryUtils.java │ │ ├── StringUtils.java │ │ ├── Tag.java │ │ ├── XmlNode.java │ │ └── package-info.java └── resources │ ├── META-INF │ └── services │ │ └── com.hazelcast.spi.discovery.DiscoveryStrategyFactory │ └── hazelcast-community-license.txt └── test ├── java └── com │ └── hazelcast │ └── aws │ ├── AwsClientConfiguratorTest.java │ ├── AwsCredentialsProviderTest.java │ ├── AwsDiscoveryStrategyFactoryTest.java │ ├── AwsDiscoveryStrategyTest.java │ ├── AwsEc2ApiTest.java │ ├── AwsEc2ClientTest.java │ ├── AwsEcsApiTest.java │ ├── AwsEcsClientTest.java │ ├── AwsMetadataApiTest.java │ ├── AwsRequestSignerTest.java │ ├── AwsRequestUtilsTest.java │ ├── FilterTest.java │ ├── PortRangeTest.java │ ├── RegionValidatorTest.java │ ├── RestClientTest.java │ ├── RetryUtilsTest.java │ ├── StringUtilsTest.java │ ├── TagTest.java │ └── XmlNodeTest.java └── resources └── test-aws-config.xml /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @leszko @alparslanavci 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "maven" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | time: "02:00" 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "daily" 12 | time: "02:00" -------------------------------------------------------------------------------- /.github/terraform/hazelcast-client.yaml: -------------------------------------------------------------------------------- 1 | hazelcast-client: 2 | network: 3 | aws: 4 | enabled: true 5 | region: REGION 6 | tag-key: TAG-KEY 7 | tag-value: TAG-VALUE 8 | -------------------------------------------------------------------------------- /.github/terraform/hazelcast.yaml: -------------------------------------------------------------------------------- 1 | hazelcast: 2 | network: 3 | join: 4 | multicast: 5 | enabled: false 6 | aws: 7 | enabled: true 8 | # discovery_role is created by main terraform script 9 | iam-role: SET_PREFIX_discovery_role 10 | hz-port: 5701 11 | region: REGION 12 | tag-key: TAG_KEY 13 | tag-value: TAG_VALUE 14 | rest-api: 15 | enabled: true 16 | -------------------------------------------------------------------------------- /.github/terraform/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "= 3.2" 6 | } 7 | } 8 | required_version = ">= 0.13" 9 | } 10 | 11 | provider "aws" { 12 | region = var.aws_region 13 | } 14 | 15 | data "aws_ami" "image" { 16 | most_recent = true 17 | 18 | filter { 19 | name = "name" 20 | values = ["ubuntu/images/hvm-ssd/ubuntu-bionic-18.04-amd64-server-*"] 21 | } 22 | 23 | filter { 24 | name = "virtualization-type" 25 | values = ["hvm"] 26 | } 27 | 28 | owners = ["099720109477"] 29 | } 30 | 31 | # IAM Role required for Hazelcast AWS Discovery 32 | resource "aws_iam_role" "discovery_role" { 33 | name = "${var.prefix}_discovery_role" 34 | 35 | assume_role_policy = <<-EOF 36 | { 37 | "Version": "2012-10-17", 38 | "Statement": [ 39 | { 40 | "Action": "sts:AssumeRole", 41 | "Principal": { 42 | "Service": "ec2.amazonaws.com" 43 | }, 44 | "Effect": "Allow", 45 | "Sid": "" 46 | } 47 | ] 48 | } 49 | EOF 50 | } 51 | 52 | resource "aws_iam_role_policy" "discovery_policy" { 53 | name = "${var.prefix}_discovery_policy" 54 | role = aws_iam_role.discovery_role.id 55 | 56 | policy = <<-EOF 57 | { 58 | "Version": "2012-10-17", 59 | "Statement": [ 60 | { 61 | "Action": [ 62 | "ec2:DescribeInstances" 63 | ], 64 | "Effect": "Allow", 65 | "Resource": "*" 66 | } 67 | ] 68 | } 69 | EOF 70 | } 71 | 72 | 73 | resource "aws_iam_instance_profile" "discovery_instance_profile" { 74 | name = "${var.prefix}_discovery_instance_profile" 75 | role = aws_iam_role.discovery_role.name 76 | } 77 | 78 | resource "aws_security_group" "sg" { 79 | name = "${var.prefix}_sg" 80 | 81 | ingress { 82 | from_port = 22 83 | to_port = 22 84 | protocol = "tcp" 85 | cidr_blocks = ["0.0.0.0/0"] 86 | } 87 | 88 | ingress { 89 | from_port = 5701 90 | to_port = 5707 91 | protocol = "tcp" 92 | cidr_blocks = ["0.0.0.0/0"] 93 | } 94 | 95 | ingress { 96 | from_port = 8080 97 | to_port = 8080 98 | protocol = "tcp" 99 | cidr_blocks = ["0.0.0.0/0"] 100 | } 101 | 102 | 103 | # Allow outgoing traffic to anywhere. 104 | egress { 105 | from_port = 0 106 | to_port = 0 107 | protocol = "-1" 108 | cidr_blocks = ["0.0.0.0/0"] 109 | } 110 | } 111 | 112 | resource "aws_key_pair" "keypair" { 113 | key_name = "${var.prefix}_${var.aws_key_name}" 114 | public_key = file("${var.local_key_path}/${var.aws_key_name}.pub") 115 | } 116 | ########################################################################### 117 | 118 | resource "aws_instance" "hazelcast_member" { 119 | count = var.member_count 120 | ami = data.aws_ami.image.id 121 | instance_type = var.aws_instance_type 122 | iam_instance_profile = aws_iam_instance_profile.discovery_instance_profile.name 123 | security_groups = [aws_security_group.sg.name] 124 | key_name = aws_key_pair.keypair.key_name 125 | tags = { 126 | Name = "${var.prefix}-AWS-Member-${count.index}" 127 | "${var.aws_tag_key}" = var.aws_tag_value 128 | } 129 | connection { 130 | type = "ssh" 131 | user = var.username 132 | host = self.public_ip 133 | timeout = "180s" 134 | agent = false 135 | private_key = file("${var.local_key_path}/${var.aws_key_name}") 136 | } 137 | 138 | provisioner "remote-exec" { 139 | inline = [ 140 | "mkdir -p /home/${var.username}/jars", 141 | "mkdir -p /home/${var.username}/logs", 142 | "while [ ! -f /var/lib/cloud/instance/boot-finished ]; do echo 'Waiting for cloud-init...'; sleep 1; done", 143 | "sudo apt-get update", 144 | "sudo apt-get -y install openjdk-8-jdk wget", 145 | ] 146 | } 147 | 148 | provisioner "file" { 149 | source = "scripts/start_aws_hazelcast_member.sh" 150 | destination = "/home/${var.username}/start_aws_hazelcast_member.sh" 151 | } 152 | 153 | provisioner "file" { 154 | source = "scripts/verify_member_count.sh" 155 | destination = "/home/${var.username}/verify_member_count.sh" 156 | } 157 | 158 | provisioner "file" { 159 | source = "~/lib/hazelcast-aws.jar" 160 | destination = "/home/${var.username}/jars/hazelcast-aws.jar" 161 | } 162 | 163 | provisioner "file" { 164 | source = "~/lib/hazelcast.jar" 165 | destination = "/home/${var.username}/jars/hazelcast.jar" 166 | } 167 | 168 | provisioner "file" { 169 | source = "hazelcast.yaml" 170 | destination = "/home/${var.username}/hazelcast.yaml" 171 | } 172 | 173 | provisioner "remote-exec" { 174 | inline = [ 175 | "cd /home/${var.username}", 176 | "chmod 0755 start_aws_hazelcast_member.sh", 177 | "./start_aws_hazelcast_member.sh ${var.aws_region} ${var.aws_tag_key} ${var.aws_tag_value} ", 178 | "sleep 5", 179 | ] 180 | } 181 | } 182 | 183 | resource "null_resource" "verify_members" { 184 | count = var.member_count 185 | depends_on = [aws_instance.hazelcast_member] 186 | connection { 187 | type = "ssh" 188 | user = var.username 189 | host = aws_instance.hazelcast_member[count.index].public_ip 190 | timeout = "180s" 191 | agent = false 192 | private_key = file("${var.local_key_path}/${var.aws_key_name}") 193 | } 194 | 195 | 196 | provisioner "remote-exec" { 197 | inline = [ 198 | "cd /home/${var.username}", 199 | "tail -n 20 ./logs/hazelcast.stdout.log", 200 | "chmod 0755 verify_member_count.sh", 201 | "./verify_member_count.sh ${var.member_count}", 202 | ] 203 | } 204 | } 205 | 206 | 207 | resource "aws_instance" "hazelcast_mancenter" { 208 | ami = data.aws_ami.image.id 209 | instance_type = var.aws_instance_type 210 | iam_instance_profile = aws_iam_instance_profile.discovery_instance_profile.name 211 | security_groups = [aws_security_group.sg.name] 212 | key_name = aws_key_pair.keypair.key_name 213 | tags = { 214 | Name = "${var.prefix}-AWS-Management-Center" 215 | "${var.aws_tag_key}" = var.aws_tag_value 216 | } 217 | 218 | connection { 219 | type = "ssh" 220 | user = var.username 221 | host = self.public_ip 222 | timeout = "180s" 223 | agent = false 224 | private_key = file("${var.local_key_path}/${var.aws_key_name}") 225 | } 226 | 227 | provisioner "file" { 228 | source = "scripts/start_aws_hazelcast_management_center.sh" 229 | destination = "/home/${var.username}/start_aws_hazelcast_management_center.sh" 230 | } 231 | 232 | provisioner "file" { 233 | source = "scripts/verify_mancenter.sh" 234 | destination = "/home/${var.username}/verify_mancenter.sh" 235 | } 236 | 237 | provisioner "file" { 238 | source = "hazelcast-client.yaml" 239 | destination = "/home/${var.username}/hazelcast-client.yaml" 240 | } 241 | 242 | provisioner "remote-exec" { 243 | inline = [ 244 | "while [ ! -f /var/lib/cloud/instance/boot-finished ]; do echo 'Waiting for cloud-init...'; sleep 1; done", 245 | "sudo apt-get update", 246 | "sudo apt-get -y install openjdk-8-jdk wget unzip", 247 | ] 248 | } 249 | 250 | provisioner "remote-exec" { 251 | inline = [ 252 | "cd /home/${var.username}", 253 | "chmod 0755 start_aws_hazelcast_management_center.sh", 254 | "./start_aws_hazelcast_management_center.sh ${var.hazelcast_mancenter_version} ${var.aws_region} ${var.aws_tag_key} ${var.aws_tag_value} ", 255 | "sleep 5", 256 | ] 257 | } 258 | } 259 | 260 | resource "null_resource" "verify_mancenter" { 261 | 262 | depends_on = [aws_instance.hazelcast_member, aws_instance.hazelcast_mancenter] 263 | connection { 264 | type = "ssh" 265 | user = var.username 266 | host = aws_instance.hazelcast_mancenter.public_ip 267 | timeout = "180s" 268 | agent = false 269 | private_key = file("${var.local_key_path}/${var.aws_key_name}") 270 | } 271 | 272 | 273 | provisioner "remote-exec" { 274 | inline = [ 275 | "cd /home/${var.username}", 276 | "tail -n 20 ./logs/mancenter.stdout.log", 277 | "chmod 0755 verify_mancenter.sh", 278 | "./verify_mancenter.sh ${var.member_count}", 279 | ] 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /.github/terraform/scripts/start_aws_hazelcast_management_center.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -x 4 | 5 | MANCENTER_VERSION=$1 6 | REGION=$2 7 | TAG_KEY=$3 8 | TAG_VALUE=$4 9 | 10 | mkdir -p ${HOME}/lib 11 | mkdir -p ${HOME}/logs 12 | mkdir -p ${HOME}/man 13 | 14 | LOG_DIR=${HOME}/logs 15 | MAN_CENTER_HOME=${HOME}/man 16 | 17 | MANCENTER_JAR_URL=https://download.hazelcast.com/management-center/hazelcast-management-center-${MANCENTER_VERSION}.zip 18 | 19 | pushd ${HOME}/lib 20 | echo "Downloading JAR..." 21 | if wget -q "$MANCENTER_JAR_URL"; then 22 | echo "Hazelcast Management JAR downloaded successfully." 23 | else 24 | echo "Hazelcast Management JAR could NOT be downloaded!" 25 | exit 1; 26 | fi 27 | unzip hazelcast-management-center-${MANCENTER_VERSION}.zip 28 | cp -R hazelcast-management-center-${MANCENTER_VERSION}/* ./ 29 | popd 30 | 31 | 32 | sed -i -e "s/REGION/${REGION}/g" ${HOME}/hazelcast-client.yaml 33 | sed -i -e "s/TAG-KEY/${TAG_KEY}/g" ${HOME}/hazelcast-client.yaml 34 | sed -i -e "s/TAG-VALUE/${TAG_VALUE}/g" ${HOME}/hazelcast-client.yaml 35 | 36 | 37 | java -cp ${HOME}/lib/hazelcast-management-center-${MANCENTER_VERSION}.jar com.hazelcast.webmonitor.cli.MCConfCommandLine cluster add -H ${MAN_CENTER_HOME} --client-config ${HOME}/hazelcast-client.yaml \ 38 | >> $LOG_DIR/mancenter.conf.stdout.log 2>> $LOG_DIR/mancenter.conf.stderr.log 39 | 40 | 41 | nohup java -Dhazelcast.mc.home=${MAN_CENTER_HOME} \ 42 | -jar ${HOME}/lib/hazelcast-management-center-${MANCENTER_VERSION}.jar >> $LOG_DIR/mancenter.stdout.log 2>> $LOG_DIR/mancenter.stderr.log & 43 | 44 | sleep 5 45 | -------------------------------------------------------------------------------- /.github/terraform/scripts/start_aws_hazelcast_member.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -x 4 | 5 | REGION=$1 6 | TAG_KEY=$2 7 | TAG_VALUE=$3 8 | 9 | 10 | sed -i -e "s/REGION/${REGION}/g" ${HOME}/hazelcast.yaml 11 | sed -i -e "s/TAG_KEY/${TAG_KEY}/g" ${HOME}/hazelcast.yaml 12 | sed -i -e "s/TAG_VALUE/${TAG_VALUE}/g" ${HOME}/hazelcast.yaml 13 | 14 | CLASSPATH="${HOME}/jars/hazelcast.jar:${HOME}/jars/hazelcast-aws.jar:${HOME}/hazelcast.yaml" 15 | 16 | nohup java -cp ${CLASSPATH} -server com.hazelcast.core.server.HazelcastMemberStarter >> ${HOME}/logs/hazelcast.stderr.log 2>> ${HOME}/logs/hazelcast.stdout.log & 17 | 18 | sleep 5 19 | -------------------------------------------------------------------------------- /.github/terraform/scripts/verify_mancenter.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | EXPECTED_SIZE=$1 6 | 7 | verify_hazelcast_cluster_size() { 8 | EXPECTED_SIZE=$1 9 | for i in `seq 1 6`; do 10 | local MEMBER_COUNT=$(cat ~/logs/mancenter.stdout.log | grep -E " Started communication with (a new )?member" | wc -l) 11 | 12 | if [ "$MEMBER_COUNT" == "$EXPECTED_SIZE" ] ; then 13 | echo "Hazelcast cluster size equal to ${EXPECTED_SIZE}" 14 | return 0 15 | else 16 | echo "Hazelcast cluster size NOT equal to ${EXPECTED_SIZE}!. Waiting.." 17 | sleep 10 18 | fi 19 | done 20 | return 1 21 | } 22 | 23 | echo "Verifying the Hazelcast cluster connected to Management Center" 24 | verify_hazelcast_cluster_size $EXPECTED_SIZE -------------------------------------------------------------------------------- /.github/terraform/scripts/verify_member_count.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | EXPECTED_SIZE=$1 6 | 7 | verify_hazelcast_cluster_size() { 8 | EXPECTED_SIZE=$1 9 | for i in `seq 1 6`; do 10 | local MEMBER_COUNT=$( curl -sS http://127.0.0.1:5701/hazelcast/health/cluster-size ) 11 | 12 | if [ "$MEMBER_COUNT" == "$EXPECTED_SIZE" ] ; then 13 | echo "Hazelcast cluster size equal to ${EXPECTED_SIZE}" 14 | return 0 15 | else 16 | echo "Hazelcast cluster size NOT equal to ${EXPECTED_SIZE}!. Waiting.." 17 | sleep 10 18 | fi 19 | done 20 | return 1 21 | } 22 | 23 | echo "Checking Hazelcast cluster size" 24 | verify_hazelcast_cluster_size $EXPECTED_SIZE 25 | -------------------------------------------------------------------------------- /.github/terraform/terraform.tfvars: -------------------------------------------------------------------------------- 1 | prefix = SET_PREFIX 2 | 3 | aws_key_name = "id_rsa" 4 | local_key_path = "~/.ssh" 5 | 6 | member_count = "2" 7 | aws_instance_type = "t2.micro" 8 | aws_region = "eu-central-1" 9 | aws_tag_key = "Category" 10 | aws_tag_value = "hazelcast-aws-discovery" 11 | 12 | hazelcast_mancenter_version = "4.2020.08" 13 | 14 | username = "ubuntu" 15 | -------------------------------------------------------------------------------- /.github/terraform/variables.tf: -------------------------------------------------------------------------------- 1 | # existing key pair name to be assigned to EC2 instance 2 | variable "aws_key_name" { 3 | type = string 4 | default = "id_rsa" 5 | } 6 | 7 | # local path of pem file for SSH connection - local_key_path/aws_key_name.pem 8 | variable "local_key_path" { 9 | type = string 10 | default = "~/.ssh/" 11 | } 12 | 13 | variable "username" { 14 | default = "ubuntu" 15 | } 16 | 17 | variable "member_count" { 18 | default = "2" 19 | } 20 | 21 | variable "aws_instance_type" { 22 | type = string 23 | default = "t2.micro" 24 | } 25 | 26 | variable "aws_region" { 27 | type = string 28 | default = "us-east-1" 29 | } 30 | 31 | variable "aws_tag_key" { 32 | type = string 33 | default = "Category" 34 | } 35 | 36 | variable "aws_tag_value" { 37 | type = string 38 | default = "hazelcast-aws-discovery" 39 | } 40 | 41 | variable "hazelcast_mancenter_version" { 42 | type = string 43 | default = "4.2020.08" 44 | } 45 | 46 | variable "prefix" { 47 | type = string 48 | } 49 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: 3 | push: 4 | paths-ignore: 5 | - '**.md' 6 | pull_request: 7 | paths-ignore: 8 | - '**.md' 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | java: [ '8' ] 16 | architecture: [ 'x64' ] 17 | name: Build with JDK ${{ matrix.java }} on ${{ matrix.architecture }} 18 | steps: 19 | - uses: actions/checkout@v2.3.4 20 | - name: Setup JDK 21 | uses: actions/setup-java@v1 22 | with: 23 | java-version: ${{ matrix.java }} 24 | architecture: ${{ matrix.architecture }} 25 | 26 | - uses: actions/cache@v2.1.6 27 | with: 28 | path: ~/.m2/repository 29 | key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} 30 | restore-keys: ${{ runner.os }}-maven- 31 | 32 | - name: Build with Maven 33 | run: mvn -B verify 34 | -------------------------------------------------------------------------------- /.github/workflows/terraform-integration-tests.yml: -------------------------------------------------------------------------------- 1 | name: Integration-tests 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - 'src/**' 9 | - 'pom.xml' 10 | - '.github/terraform/**' 11 | 12 | jobs: 13 | build: 14 | defaults: 15 | run: 16 | shell: bash 17 | env: 18 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 19 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 20 | SSH_PUBLIC_KEY: ${{ secrets.SSH_PUBLIC_KEY }} 21 | SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} 22 | runs-on: ubuntu-latest 23 | strategy: 24 | matrix: 25 | java: [ '8' ] 26 | architecture: [ 'x64' ] 27 | steps: 28 | # SET UP JDK 29 | - name: Setup JDK 30 | uses: actions/setup-java@v1 31 | with: 32 | java-version: ${{ matrix.java }} 33 | architecture: ${{ matrix.architecture }} 34 | 35 | - run: java -version 36 | 37 | - run: mvn --version 38 | 39 | - run : mkdir ~/lib 40 | 41 | # BUILD HAZELCAST AWS SNAPSHOT 42 | - uses: actions/checkout@v2.3.4 43 | with: 44 | path: hazelcast-aws 45 | 46 | - name: Build hazelcast-aws jar 47 | run: | 48 | cd hazelcast-aws 49 | mvn clean install -DskipTests 50 | echo "Hazelcast AWS jar is: " target/hazelcast-aws-*-SNAPSHOT.jar 51 | cp target/hazelcast-aws-*-SNAPSHOT.jar ~/lib/hazelcast-aws.jar 52 | 53 | # DOWNLOAD HAZELCAST JAR WITH VERSION DEFINED IN POM.XML 54 | - name: Download latest Hazelcast version 55 | run: | 56 | HZ_VERSION=$(cat hazelcast-aws/pom.xml | grep -m 1 -Po "\K[-_.a-zA-Z0-9]+(?= 2 | 3 | 17 | 18 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /findbugs/findbugs-exclude.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /markdown/images/aws-autoscaling-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hazelcast/hazelcast-aws/c103d889e8f7fcfdaa6565a1729b7e4da0e4e7a1/markdown/images/aws-autoscaling-architecture.png -------------------------------------------------------------------------------- /src/main/java/com/hazelcast/aws/AwsClient.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 | /** 22 | * Responsible for fetching discovery information from AWS APIs. 23 | */ 24 | interface AwsClient { 25 | Map getAddresses(); 26 | 27 | String getAvailabilityZone(); 28 | 29 | /** 30 | * Returns the placement group name of the service if specified. 31 | * 32 | * @see Placement Groups 33 | */ 34 | default Optional getPlacementGroup() { 35 | return Optional.empty(); 36 | } 37 | 38 | /** 39 | * Returns the partition number of the service if it belongs to a partition placement group. 40 | * 41 | * @see Partition Placement Groups 42 | */ 43 | default Optional getPlacementPartitionNumber() { 44 | return Optional.empty(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/hazelcast/aws/AwsClientConfigurator.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 com.hazelcast.logging.ILogger; 20 | import com.hazelcast.logging.Logger; 21 | 22 | import java.time.Clock; 23 | import java.util.Comparator; 24 | import java.util.HashMap; 25 | import java.util.List; 26 | import java.util.Map; 27 | import java.util.stream.Collectors; 28 | 29 | import static com.hazelcast.aws.RegionValidator.validateRegion; 30 | import static com.hazelcast.aws.StringUtils.isNotEmpty; 31 | 32 | /** 33 | * Responsible for creating the correct {@code AwsClient} implementation. 34 | *

35 | * Note that it also creates and injects all dependencies. 36 | */ 37 | final class AwsClientConfigurator { 38 | private static final ILogger LOGGER = Logger.getLogger(AwsClientConfigurator.class); 39 | 40 | private static final String DEFAULT_EC2_HOST_HEADER = "ec2.amazonaws.com"; 41 | private static final String DEFAULT_ECS_HOST_HEADER = "ecs.amazonaws.com"; 42 | 43 | private static final String EC2_SERVICE_NAME = "ec2"; 44 | private static final String ECS_SERVICE_NAME = "ecs"; 45 | 46 | private AwsClientConfigurator() { 47 | } 48 | 49 | static AwsClient createAwsClient(AwsConfig awsConfig) { 50 | Environment environment = new Environment(); 51 | AwsMetadataApi metadataApi = new AwsMetadataApi(awsConfig); 52 | 53 | String region = resolveRegion(awsConfig, metadataApi, environment); 54 | validateRegion(region); 55 | 56 | AwsCredentialsProvider credentialsProvider = new AwsCredentialsProvider(awsConfig, metadataApi, environment); 57 | AwsEc2Api ec2Api = createEc2Api(awsConfig, region); 58 | 59 | // EC2 Discovery 60 | if (explicitlyEc2Configured(awsConfig) || (!explicitlyEcsConfigured(awsConfig) && !environment.isRunningOnEcs())) { 61 | logEc2Environment(awsConfig, region); 62 | return new AwsEc2Client(ec2Api, metadataApi, credentialsProvider); 63 | } 64 | 65 | // ECS Discovery 66 | String cluster = resolveCluster(awsConfig, metadataApi, environment); 67 | AwsEcsApi ecsApi = createEcsApi(awsConfig, region); 68 | logEcsEnvironment(awsConfig, region, cluster); 69 | return new AwsEcsClient(cluster, ecsApi, ec2Api, metadataApi, credentialsProvider); 70 | } 71 | 72 | static String resolveRegion(AwsConfig awsConfig, AwsMetadataApi metadataApi, Environment environment) { 73 | if (isNotEmpty(awsConfig.getRegion())) { 74 | return awsConfig.getRegion(); 75 | } 76 | 77 | if (environment.isRunningOnEcs()) { 78 | return environment.getAwsRegionOnEcs(); 79 | } 80 | 81 | return regionFrom(metadataApi.availabilityZoneEc2()); 82 | } 83 | 84 | private static String regionFrom(String availabilityZone) { 85 | return availabilityZone.substring(0, availabilityZone.length() - 1); 86 | } 87 | 88 | private static AwsEc2Api createEc2Api(AwsConfig awsConfig, String region) { 89 | String ec2Endpoint = resolveEc2Endpoint(awsConfig, region); 90 | AwsRequestSigner ec2RequestSigner = new AwsRequestSigner(region, EC2_SERVICE_NAME); 91 | return new AwsEc2Api(ec2Endpoint, awsConfig, ec2RequestSigner, Clock.systemUTC()); 92 | } 93 | 94 | private static AwsEcsApi createEcsApi(AwsConfig awsConfig, String region) { 95 | String ecsEndpoint = resolveEcsEndpoint(awsConfig, region); 96 | AwsRequestSigner ecsRequestSigner = new AwsRequestSigner(region, ECS_SERVICE_NAME); 97 | return new AwsEcsApi(ecsEndpoint, awsConfig, ecsRequestSigner, Clock.systemUTC()); 98 | } 99 | 100 | static String resolveEc2Endpoint(AwsConfig awsConfig, String region) { 101 | String ec2HostHeader = awsConfig.getHostHeader(); 102 | if (StringUtils.isEmpty(ec2HostHeader) 103 | || ec2HostHeader.startsWith("ecs") 104 | || ec2HostHeader.equals("ec2") 105 | ) { 106 | ec2HostHeader = DEFAULT_EC2_HOST_HEADER; 107 | } 108 | return ec2HostHeader.replace("ec2.", "ec2." + region + "."); 109 | } 110 | 111 | static String resolveEcsEndpoint(AwsConfig awsConfig, String region) { 112 | String ecsHostHeader = awsConfig.getHostHeader(); 113 | if (StringUtils.isEmpty(ecsHostHeader) 114 | || ecsHostHeader.equals("ecs") 115 | ) { 116 | ecsHostHeader = DEFAULT_ECS_HOST_HEADER; 117 | } 118 | return ecsHostHeader.replace("ecs.", "ecs." + region + "."); 119 | } 120 | 121 | /** 122 | * Checks if EC2 environment was explicitly configured in the Hazelcast configuration. 123 | *

124 | * Hazelcast may run inside ECS, but use EC2 discovery when: 125 | *

    126 | *
  • EC2 cluster uses EC2 mode (not Fargate)
  • 127 | *
  • Containers are run in "host" network mode
  • 128 | *
129 | */ 130 | static boolean explicitlyEc2Configured(AwsConfig awsConfig) { 131 | return isNotEmpty(awsConfig.getHostHeader()) && awsConfig.getHostHeader().startsWith("ec2"); 132 | } 133 | 134 | static boolean explicitlyEcsConfigured(AwsConfig awsConfig) { 135 | return isNotEmpty(awsConfig.getCluster()) 136 | || (isNotEmpty(awsConfig.getHostHeader()) && awsConfig.getHostHeader().startsWith("ecs")); 137 | } 138 | 139 | static String resolveCluster(AwsConfig awsConfig, AwsMetadataApi metadataApi, Environment environment) { 140 | if (isNotEmpty(awsConfig.getCluster())) { 141 | return awsConfig.getCluster(); 142 | } 143 | if (environment.isRunningOnEcs()) { 144 | String clusterArn = metadataApi.metadataEcs().getClusterArn(); 145 | LOGGER.info("No ECS cluster defined, using current cluster: " + clusterArn); 146 | return clusterArn; 147 | } 148 | throw new InvalidConfigurationException("You must define 'cluster' property if not running inside ECS cluster"); 149 | } 150 | 151 | private static void logEc2Environment(AwsConfig awsConfig, String region) { 152 | Map filters = new HashMap<>(); 153 | filters.put("tag-key", combineTagKeys(awsConfig.getTags())); 154 | filters.put("tag-value", combineTagValues(awsConfig.getTags())); 155 | filters.put("security-group-name", awsConfig.getSecurityGroupName()); 156 | filters.put("hz-port", awsConfig.getHzPort().toString()); 157 | 158 | LOGGER.info(String.format( 159 | "AWS plugin performing discovery in EC2 environment for region: '%s' filtered by: '%s'", 160 | region, logFilters(filters)) 161 | ); 162 | } 163 | 164 | private static String combineTagKeys(List tags) { 165 | return tags.stream() 166 | .map(Tag::getKey) 167 | .collect(Collectors.joining(",")); 168 | } 169 | 170 | private static String combineTagValues(List tags) { 171 | return tags.stream() 172 | .map(Tag::getValue) 173 | .collect(Collectors.joining(",")); 174 | } 175 | 176 | private static void logEcsEnvironment(AwsConfig awsConfig, String region, String cluster) { 177 | Map filters = new HashMap<>(); 178 | filters.put("family", awsConfig.getFamily()); 179 | filters.put("service-name", awsConfig.getServiceName()); 180 | filters.put("hz-port", awsConfig.getHzPort().toString()); 181 | 182 | LOGGER.info(String.format( 183 | "AWS plugin performing discovery in ECS environment for region: '%s' for cluster: '%s' filtered by: '%s'", 184 | region, cluster, logFilters(filters)) 185 | ); 186 | } 187 | 188 | private static String logFilters(Map parameters) { 189 | return parameters.entrySet().stream() 190 | .filter(e -> e.getValue() != null) 191 | .sorted(Comparator.comparing(Map.Entry::getKey)) 192 | .map(e -> String.format("%s:%s", e.getKey(), e.getValue())) 193 | .collect(Collectors.joining(", ")); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/main/java/com/hazelcast/aws/AwsCredentials.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.Objects; 19 | 20 | final class AwsCredentials { 21 | private String accessKey; 22 | private String secretKey; 23 | private String token; 24 | 25 | private AwsCredentials(String accessKey, String secretKey, String token) { 26 | this.accessKey = accessKey; 27 | this.secretKey = secretKey; 28 | this.token = token; 29 | } 30 | 31 | String getAccessKey() { 32 | return accessKey; 33 | } 34 | 35 | String getSecretKey() { 36 | return secretKey; 37 | } 38 | 39 | String getToken() { 40 | return token; 41 | } 42 | 43 | static Builder builder() { 44 | return new Builder(); 45 | } 46 | 47 | static class Builder { 48 | private String accessKey; 49 | private String secretKey; 50 | private String token; 51 | 52 | Builder setAccessKey(String accessKey) { 53 | this.accessKey = accessKey; 54 | return this; 55 | } 56 | 57 | Builder setSecretKey(String secretKey) { 58 | this.secretKey = secretKey; 59 | return this; 60 | } 61 | 62 | Builder setToken(String token) { 63 | this.token = token; 64 | return this; 65 | } 66 | 67 | AwsCredentials build() { 68 | return new AwsCredentials(accessKey, secretKey, token); 69 | } 70 | } 71 | 72 | @Override 73 | public boolean equals(Object o) { 74 | if (this == o) { 75 | return true; 76 | } 77 | if (o == null || getClass() != o.getClass()) { 78 | return false; 79 | } 80 | AwsCredentials that = (AwsCredentials) o; 81 | return Objects.equals(accessKey, that.accessKey) 82 | && Objects.equals(secretKey, that.secretKey) 83 | && Objects.equals(token, that.token); 84 | } 85 | 86 | @Override 87 | public int hashCode() { 88 | return Objects.hash(accessKey, secretKey, token); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/com/hazelcast/aws/AwsCredentialsProvider.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 com.hazelcast.logging.ILogger; 20 | import com.hazelcast.logging.Logger; 21 | 22 | class AwsCredentialsProvider { 23 | private static final ILogger LOGGER = Logger.getLogger(AwsCredentialsProvider.class); 24 | 25 | private static final int HTTP_NOT_FOUND = 404; 26 | 27 | private final AwsConfig awsConfig; 28 | private final AwsMetadataApi awsMetadataApi; 29 | private final Environment environment; 30 | private final String ec2IamRole; 31 | 32 | AwsCredentialsProvider(AwsConfig awsConfig, AwsMetadataApi awsMetadataApi, Environment environment) { 33 | this.awsConfig = awsConfig; 34 | this.awsMetadataApi = awsMetadataApi; 35 | this.environment = environment; 36 | this.ec2IamRole = resolveEc2IamRole(); 37 | } 38 | 39 | private String resolveEc2IamRole() { 40 | if (StringUtils.isNotEmpty(awsConfig.getAccessKey())) { 41 | // no need to resolve IAM Role, since using hardcoded Access/Secret keys takes precedence 42 | return null; 43 | } 44 | 45 | if (StringUtils.isNotEmpty(awsConfig.getIamRole()) && !"DEFAULT".equals(awsConfig.getIamRole())) { 46 | return awsConfig.getIamRole(); 47 | } 48 | 49 | if (environment.isRunningOnEcs()) { 50 | // ECS has only one role assigned and no need to resolve it here 51 | LOGGER.info("Using IAM Task Role attached to ECS Task"); 52 | return null; 53 | } 54 | 55 | try { 56 | String ec2IamRole = awsMetadataApi.defaultIamRoleEc2(); 57 | LOGGER.info(String.format("Using IAM Role attached to EC2 Instance: '%s'", ec2IamRole)); 58 | return ec2IamRole; 59 | } catch (RestClientException e) { 60 | if (e.getHttpErrorCode() == HTTP_NOT_FOUND) { 61 | // no IAM Role attached to EC2 instance, no need to log any warning at this point 62 | LOGGER.finest("IAM Role not found", e); 63 | } else { 64 | LOGGER.warning("Couldn't retrieve IAM Role from EC2 instance", e); 65 | } 66 | } catch (Exception e) { 67 | LOGGER.warning("Couldn't retrieve IAM Role from EC2 instance", e); 68 | } 69 | return null; 70 | } 71 | 72 | AwsCredentials credentials() { 73 | if (StringUtils.isNotEmpty(awsConfig.getAccessKey())) { 74 | return AwsCredentials.builder() 75 | .setAccessKey(awsConfig.getAccessKey()) 76 | .setSecretKey(awsConfig.getSecretKey()) 77 | .build(); 78 | } 79 | if (StringUtils.isNotEmpty(ec2IamRole)) { 80 | return fetchCredentialsFromEc2(); 81 | } 82 | if (environment.isRunningOnEcs()) { 83 | return fetchCredentialsFromEcs(); 84 | } 85 | throw new NoCredentialsException(); 86 | } 87 | 88 | private AwsCredentials fetchCredentialsFromEc2() { 89 | LOGGER.fine(String.format("Fetching AWS Credentials using EC2 IAM Role: %s", ec2IamRole)); 90 | 91 | try { 92 | return awsMetadataApi.credentialsEc2(ec2IamRole); 93 | } catch (Exception e) { 94 | throw new InvalidConfigurationException(String.format("Unable to retrieve credentials from IAM Role: " 95 | + "'%s', please make sure it's attached to your EC2 Instance", awsConfig.getIamRole()), e); 96 | } 97 | } 98 | 99 | private AwsCredentials fetchCredentialsFromEcs() { 100 | LOGGER.fine("Fetching AWS Credentials from ECS IAM Task Role"); 101 | 102 | try { 103 | return awsMetadataApi.credentialsEcs(); 104 | } catch (Exception e) { 105 | throw new InvalidConfigurationException("Unable to retrieve credentials from IAM Role attached to ECS Task," 106 | + " please check your configuration"); 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/main/java/com/hazelcast/aws/AwsDiscoveryStrategyFactory.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.internal.nio.IOUtil; 20 | import com.hazelcast.logging.ILogger; 21 | import com.hazelcast.logging.Logger; 22 | import com.hazelcast.spi.discovery.DiscoveryNode; 23 | import com.hazelcast.spi.discovery.DiscoveryStrategy; 24 | import com.hazelcast.spi.discovery.DiscoveryStrategyFactory; 25 | 26 | import java.io.File; 27 | import java.io.FileInputStream; 28 | import java.io.IOException; 29 | import java.io.InputStream; 30 | import java.nio.charset.StandardCharsets; 31 | import java.util.ArrayList; 32 | import java.util.Collection; 33 | import java.util.Map; 34 | 35 | /** 36 | * Factory class which returns {@link AwsDiscoveryStrategy} to Discovery SPI 37 | */ 38 | public class AwsDiscoveryStrategyFactory 39 | implements DiscoveryStrategyFactory { 40 | private static final ILogger LOGGER = Logger.getLogger(AwsDiscoveryStrategyFactory.class); 41 | 42 | @Override 43 | public Class getDiscoveryStrategyType() { 44 | return AwsDiscoveryStrategy.class; 45 | } 46 | 47 | @Override 48 | public DiscoveryStrategy newDiscoveryStrategy(DiscoveryNode discoveryNode, ILogger logger, 49 | Map properties) { 50 | return new AwsDiscoveryStrategy(properties); 51 | } 52 | 53 | @Override 54 | public Collection getConfigurationProperties() { 55 | final AwsProperties[] props = AwsProperties.values(); 56 | final ArrayList 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 | --------------------------------------------------------------------------------