1
0
Эх сурвалжийг харах

Upgrade `discovery-ec2` to AWS SDK v2 (#122062)

David Turner 6 сар өмнө
parent
commit
a2d98e44a1
37 өөрчлөгдсөн 1059 нэмэгдсэн , 1008 устгасан
  1. 54 0
      docs/changelog/122062.yaml
  2. 0 3
      docs/reference/elasticsearch-plugins/discovery-ec2-usage.md
  3. 20 0
      gradle/verification-metadata.xml
  4. 94 21
      plugins/discovery-ec2/build.gradle
  5. 0 63
      plugins/discovery-ec2/licenses/aws-java-sdk-LICENSE.txt
  6. 0 15
      plugins/discovery-ec2/licenses/aws-java-sdk-NOTICE.txt
  7. 206 0
      plugins/discovery-ec2/licenses/aws-sdk-2-LICENSE.txt
  8. 26 0
      plugins/discovery-ec2/licenses/aws-sdk-2-NOTICE.txt
  9. 0 8
      plugins/discovery-ec2/licenses/jackson-LICENSE
  10. 0 20
      plugins/discovery-ec2/licenses/jackson-NOTICE
  11. 7 0
      plugins/discovery-ec2/licenses/reactive-streams-LICENSE.txt
  12. 0 0
      plugins/discovery-ec2/licenses/reactive-streams-NOTICE.txt
  13. 21 0
      plugins/discovery-ec2/licenses/slf4j-api-LICENSE.txt
  14. 0 0
      plugins/discovery-ec2/licenses/slf4j-api-NOTICE.txt
  15. 24 0
      plugins/discovery-ec2/licenses/slf4j-nop-LICENSE.txt
  16. 0 0
      plugins/discovery-ec2/licenses/slf4j-nop-NOTICE.txt
  17. 7 14
      plugins/discovery-ec2/src/internalClusterTest/java/org/elasticsearch/discovery/ec2/DiscoveryEc2SpecialNetworkAddressesIT.java
  18. 0 37
      plugins/discovery-ec2/src/javaRestTest/java/org/elasticsearch/discovery/ec2/DiscoveryEc2AvailabilityZoneAttributeImdsV1IT.java
  19. 1 1
      plugins/discovery-ec2/src/javaRestTest/java/org/elasticsearch/discovery/ec2/DiscoveryEc2AvailabilityZoneAttributeTestCase.java
  20. 1 1
      plugins/discovery-ec2/src/javaRestTest/java/org/elasticsearch/discovery/ec2/DiscoveryEc2InstanceProfileIT.java
  21. 1 1
      plugins/discovery-ec2/src/javaRestTest/java/org/elasticsearch/discovery/ec2/DiscoveryEc2SystemPropertyCredentialsIT.java
  22. 8 9
      plugins/discovery-ec2/src/main/java/org/elasticsearch/discovery/ec2/AmazonEc2Reference.java
  23. 35 33
      plugins/discovery-ec2/src/main/java/org/elasticsearch/discovery/ec2/AwsEc2SeedHostsProvider.java
  24. 9 2
      plugins/discovery-ec2/src/main/java/org/elasticsearch/discovery/ec2/AwsEc2Service.java
  25. 81 56
      plugins/discovery-ec2/src/main/java/org/elasticsearch/discovery/ec2/AwsEc2ServiceImpl.java
  26. 13 50
      plugins/discovery-ec2/src/main/java/org/elasticsearch/discovery/ec2/AwsEc2Utils.java
  27. 35 47
      plugins/discovery-ec2/src/main/java/org/elasticsearch/discovery/ec2/Ec2ClientSettings.java
  28. 17 75
      plugins/discovery-ec2/src/main/java/org/elasticsearch/discovery/ec2/Ec2DiscoveryPlugin.java
  29. 18 68
      plugins/discovery-ec2/src/main/java/org/elasticsearch/discovery/ec2/Ec2NameResolver.java
  30. 25 0
      plugins/discovery-ec2/src/main/java/org/elasticsearch/discovery/ec2/HttpScheme.java
  31. 1 0
      plugins/discovery-ec2/src/main/plugin-metadata/plugin-security.policy
  32. 12 11
      plugins/discovery-ec2/src/test/java/org/elasticsearch/discovery/ec2/AbstractEC2MockAPITestCase.java
  33. 0 181
      plugins/discovery-ec2/src/test/java/org/elasticsearch/discovery/ec2/AwsEc2ServiceImplTests.java
  34. 6 4
      plugins/discovery-ec2/src/test/java/org/elasticsearch/discovery/ec2/EC2RetriesTests.java
  35. 283 260
      plugins/discovery-ec2/src/test/java/org/elasticsearch/discovery/ec2/Ec2DiscoveryPluginTests.java
  36. 29 24
      plugins/discovery-ec2/src/test/java/org/elasticsearch/discovery/ec2/Ec2DiscoveryTests.java
  37. 25 4
      test/fixtures/ec2-imds-fixture/src/main/java/fixture/aws/imds/Ec2ImdsHttpFixture.java

+ 54 - 0
docs/changelog/122062.yaml

@@ -0,0 +1,54 @@
+pr: 122062
+summary: Upgrade `discovery-ec2` to AWS SDK v2
+area: Discovery-Plugins
+type: breaking
+issues: []
+breaking:
+  title: Upgrade `discovery-ec2` to AWS SDK v2
+  area: Cluster and node setting
+  details: >-
+
+    In earlier versions of {es} the `discovery-ec2` plugin was based on the AWS
+    SDK v1. AWS will withdraw support for this SDK before the end of the life
+    of {es} {minor-version} so we must migrate to the newer AWS SDK v2.
+
+    Unfortunately there are several differences between the two AWS SDK
+    versions which may require you to adjust your system configuration when
+    upgrading to {es} {minor-version} or later. These differences include, but
+    may not be limited to, the following items.
+
+    * AWS SDK v2 does not support the EC2 IMDSv1 protocol.
+
+    * AWS SDK v2 does not support the `aws.secretKey` or
+      `com.amazonaws.sdk.ec2MetadataServiceEndpointOverride` system properties.
+
+    * AWS SDK v2 does not permit specifying a choice between HTTP and HTTPS so
+      the `discovery.ec2.protocol` setting is no longer effective.
+
+    * AWS SDK v2 does not accept an access key without a secret key or vice
+      versa.
+
+  impact: >-
+
+    If you use the `discovery-ec2` plugin, test your upgrade thoroughly before
+    upgrading any production workloads.
+
+    Adapt your configuration to the new SDK functionality. This includes, but
+    may not be limited to, the following items.
+
+    * If you use IMDS to determine the availability zone of a node or to obtain
+      credentials for accessing the EC2 API, ensure that it supports the IMDSv2
+      protocol.
+
+    * If applicable, discontinue use of the `aws.secretKey` and
+      `com.amazonaws.sdk.ec2MetadataServiceEndpointOverride` system properties.
+
+    * If applicable, specify that you wish to use the insecure HTTP protocol to
+      access the EC2 API by setting `discovery.ec2.endpoint` to a URL which
+      starts with `http://`.
+
+    * Either supply both an access key and a secret key using the keystore
+      settings `discovery.ec2.access_key` and `discovery.ec2.secret_key`, or
+      configure neither of these settings.
+
+  notable: true

+ 0 - 3
docs/reference/elasticsearch-plugins/discovery-ec2-usage.md

@@ -43,9 +43,6 @@ The available settings for the EC2 discovery plugin are as follows.
 `discovery.ec2.endpoint`
 :   The EC2 service endpoint to which to connect. See [https://docs.aws.amazon.com/general/latest/gr/rande.html#ec2_region](https://docs.aws.amazon.com/general/latest/gr/rande.html#ec2_region) to find the appropriate endpoint for the region. This setting defaults to `ec2.us-east-1.amazonaws.com` which is appropriate for clusters running in the `us-east-1` region.
 
-`discovery.ec2.protocol`
-:   The protocol to use to connect to the EC2 service endpoint, which may be either `http` or `https`. Defaults to `https`.
-
 `discovery.ec2.proxy.host`
 :   The address or host name of an HTTP proxy through which to connect to EC2. If not set, no proxy is used.
 

+ 20 - 0
gradle/verification-metadata.xml

@@ -4684,6 +4684,11 @@
             <sha256 value="64d8c2bcccd33c20ccdbafa101b01d8e0f750c4e4bd227b0b765046f601eb944" origin="Generated by Gradle"/>
          </artifact>
       </component>
+      <component group="software.amazon.awssdk" name="apache-client" version="2.30.38">
+         <artifact name="apache-client-2.30.38.jar">
+            <sha256 value="ebb1d3d05711ccf2aa9bfc43fcc69fbe32e7be69e006e7952679c2f37d149f4d" origin="Generated by Gradle"/>
+         </artifact>
+      </component>
       <component group="software.amazon.awssdk" name="auth" version="2.30.38">
          <artifact name="auth-2.30.38.jar">
             <sha256 value="22d59f9af8111be5219eb33ef480d84c616565913da57cb4eac686076fea370e" origin="Generated by Gradle"/>
@@ -4699,6 +4704,11 @@
             <sha256 value="b62be02560a46135181342afc9fb2d99373a9f04f384caf30863e2e7fe5b3892" origin="Generated by Gradle"/>
          </artifact>
       </component>
+      <component group="software.amazon.awssdk" name="aws-query-protocol" version="2.30.38">
+         <artifact name="aws-query-protocol-2.30.38.jar">
+            <sha256 value="bfd558e937de70c3260df2356b47a25b562c59b5ebeded6b199846cc9a354fe5" origin="Generated by Gradle"/>
+         </artifact>
+      </component>
       <component group="software.amazon.awssdk" name="bedrockruntime" version="2.30.38">
          <artifact name="bedrockruntime-2.30.38.jar">
             <sha256 value="4424437b49fdf263ea460f4da634d3279ada7f4763827d74fea48c0f8f2afea3" origin="Generated by Gradle"/>
@@ -4714,6 +4724,11 @@
             <sha256 value="82d97bcbb18d8f369b00c9971ca8c24ad94769d20836e0c4f86ebcdfea994cdb" origin="Generated by Gradle"/>
          </artifact>
       </component>
+      <component group="software.amazon.awssdk" name="ec2" version="2.30.38">
+         <artifact name="ec2-2.30.38.jar">
+            <sha256 value="a2e52ca80aac79553f9da1463256db0177b12c5c24c1b5660a3bb7874b66222f" origin="Generated by Gradle"/>
+         </artifact>
+      </component>
       <component group="software.amazon.awssdk" name="endpoints-spi" version="2.30.38">
          <artifact name="endpoints-spi-2.30.38.jar">
             <sha256 value="80620e3020a29871073a8a4efbcaa4d546667eeb92dfd478de808dca7e0500aa" origin="Generated by Gradle"/>
@@ -4749,6 +4764,11 @@
             <sha256 value="e784929d8a51591b6ed51344f41b37f2a68582d2e912e8310ea3e57a56d4d6bf" origin="Generated by Gradle"/>
          </artifact>
       </component>
+      <component group="software.amazon.awssdk" name="imds" version="2.30.38">
+         <artifact name="imds-2.30.38.jar">
+            <sha256 value="1586c80dfee0d09e26ad9cb313529530f1fb75d9360b05b1f1f1ca246f39d1b2" origin="Generated by Gradle"/>
+         </artifact>
+      </component>
       <component group="software.amazon.awssdk" name="json-utils" version="2.30.38">
          <artifact name="json-utils-2.30.38.jar">
             <sha256 value="823f565bc6d4031e4b3dada05c1e66c1344f34d498344b7186a2f2d048ba01d8" origin="Generated by Gradle"/>

+ 94 - 21
plugins/discovery-ec2/build.gradle

@@ -6,6 +6,7 @@
  * your election, the "Elastic License 2.0", the "GNU Affero General Public
  * License v3.0 only", or the "Server Side Public License, v 1".
  */
+apply plugin: 'elasticsearch.internal-cluster-test'
 apply plugin: 'elasticsearch.internal-java-rest-test'
 apply plugin: 'elasticsearch.internal-cluster-test'
 
@@ -15,30 +16,83 @@ esplugin {
 }
 
 dependencies {
-  api "com.amazonaws:aws-java-sdk-ec2:${versions.awsv1sdk}"
-  api "com.amazonaws:aws-java-sdk-core:${versions.awsv1sdk}"
-  api "org.apache.httpcomponents:httpclient:${versions.httpclient}"
-  api "org.apache.httpcomponents:httpcore:${versions.httpcore}"
-  api "commons-logging:commons-logging:${versions.commonslogging}"
-  api "org.apache.logging.log4j:log4j-1.2-api:${versions.log4j}"
-  api "commons-codec:commons-codec:${versions.commonscodec}"
-  api "com.fasterxml.jackson.core:jackson-core:${versions.jackson}"
-  api "com.fasterxml.jackson.core:jackson-databind:${versions.jackson}"
-  api "com.fasterxml.jackson.core:jackson-annotations:${versions.jackson}"
-  api "com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:${versions.jackson}"
-  api "joda-time:joda-time:2.10.10"
+
+  implementation "software.amazon.awssdk:annotations:${versions.awsv2sdk}"
+  implementation "software.amazon.awssdk:apache-client:${versions.awsv2sdk}"
+  implementation "software.amazon.awssdk:auth:${versions.awsv2sdk}"
+  implementation "software.amazon.awssdk:aws-core:${versions.awsv2sdk}"
+  implementation "software.amazon.awssdk:ec2:${versions.awsv2sdk}"
+  implementation "software.amazon.awssdk:endpoints-spi:${versions.awsv2sdk}"
+  implementation "software.amazon.awssdk:http-client-spi:${versions.awsv2sdk}"
+  implementation "software.amazon.awssdk:identity-spi:${versions.awsv2sdk}"
+  implementation "software.amazon.awssdk:imds:${versions.awsv2sdk}"
+  implementation "software.amazon.awssdk:retries-spi:${versions.awsv2sdk}"
+  implementation "software.amazon.awssdk:sdk-core:${versions.awsv2sdk}"
+  implementation "software.amazon.awssdk:utils:${versions.awsv2sdk}"
+
+  runtimeOnly "software.amazon.awssdk:aws-query-protocol:${versions.awsv2sdk}"
+  runtimeOnly "software.amazon.awssdk:checksums-spi:${versions.awsv2sdk}"
+  runtimeOnly "software.amazon.awssdk:checksums:${versions.awsv2sdk}"
+  runtimeOnly "software.amazon.awssdk:http-auth-aws:${versions.awsv2sdk}"
+  runtimeOnly "software.amazon.awssdk:http-auth-spi:${versions.awsv2sdk}"
+  runtimeOnly "software.amazon.awssdk:http-auth:${versions.awsv2sdk}"
+  runtimeOnly "software.amazon.awssdk:json-utils:${versions.awsv2sdk}"
+  runtimeOnly "software.amazon.awssdk:metrics-spi:${versions.awsv2sdk}"
+  runtimeOnly "software.amazon.awssdk:profiles:${versions.awsv2sdk}"
+  runtimeOnly "software.amazon.awssdk:protocol-core:${versions.awsv2sdk}"
+  runtimeOnly "software.amazon.awssdk:regions:${versions.awsv2sdk}"
+  runtimeOnly "software.amazon.awssdk:retries:${versions.awsv2sdk}"
+  runtimeOnly "software.amazon.awssdk:third-party-jackson-core:${versions.awsv2sdk}"
+
+  implementation "org.apache.httpcomponents:httpclient:${versions.httpclient}"
+
+  runtimeOnly "commons-codec:commons-codec:${versions.commonscodec}"
+  runtimeOnly "commons-logging:commons-logging:${versions.commonslogging}"
+  runtimeOnly "joda-time:joda-time:2.10.10"
+  runtimeOnly "org.apache.httpcomponents:httpcore:${versions.httpcore}"
+  runtimeOnly "org.apache.logging.log4j:log4j-1.2-api:${versions.log4j}"
+  runtimeOnly "org.slf4j:slf4j-nop:${versions.slf4j}"
+  // runtimeOnly("org.apache.logging.log4j:log4j-slf4j-impl:${versions.log4j}") https://github.com/elastic/elasticsearch/issues/93714
+  runtimeOnly "org.slf4j:slf4j-api:${versions.slf4j}"
+  runtimeOnly "org.reactivestreams:reactive-streams:${versions.reactive_streams}"
 
   javaRestTestImplementation project(':plugins:discovery-ec2')
   javaRestTestImplementation project(':test:fixtures:aws-fixture-utils')
   javaRestTestImplementation project(':test:fixtures:aws-ec2-fixture')
   javaRestTestImplementation project(':test:fixtures:ec2-imds-fixture')
 
+  testImplementation project(':test:fixtures:aws-fixture-utils')
+  testImplementation project(':test:fixtures:ec2-imds-fixture')
+
   internalClusterTestImplementation project(':test:fixtures:ec2-imds-fixture')
 }
 
 tasks.named("dependencyLicenses").configure {
-  mapping from: /aws-java-sdk-.*/, to: 'aws-java-sdk'
-  mapping from: /jackson-.*/, to: 'jackson'
+  mapping from: 'annotations',              to: 'aws-sdk-2'
+  mapping from: 'apache-client',            to: 'aws-sdk-2'
+  mapping from: 'auth',                     to: 'aws-sdk-2'
+  mapping from: 'aws-core',                 to: 'aws-sdk-2'
+  mapping from: 'aws-query-protocol',       to: 'aws-sdk-2'
+  mapping from: 'checksums',                to: 'aws-sdk-2'
+  mapping from: 'checksums-spi',            to: 'aws-sdk-2'
+  mapping from: 'ec2',                      to: 'aws-sdk-2'
+  mapping from: 'endpoints-spi',            to: 'aws-sdk-2'
+  mapping from: 'http-auth',                to: 'aws-sdk-2'
+  mapping from: 'http-auth-aws',            to: 'aws-sdk-2'
+  mapping from: 'http-auth-spi',            to: 'aws-sdk-2'
+  mapping from: 'http-client-spi',          to: 'aws-sdk-2'
+  mapping from: 'identity-spi',             to: 'aws-sdk-2'
+  mapping from: 'imds',                     to: 'aws-sdk-2'
+  mapping from: 'json-utils',               to: 'aws-sdk-2'
+  mapping from: 'metrics-spi',              to: 'aws-sdk-2'
+  mapping from: 'profiles',                 to: 'aws-sdk-2'
+  mapping from: 'protocol-core',            to: 'aws-sdk-2'
+  mapping from: 'regions',                  to: 'aws-sdk-2'
+  mapping from: 'retries',                  to: 'aws-sdk-2'
+  mapping from: 'retries-spi',              to: 'aws-sdk-2'
+  mapping from: 'sdk-core',                 to: 'aws-sdk-2'
+  mapping from: 'third-party-jackson-core', to: 'aws-sdk-2'
+  mapping from: 'utils',                    to: 'aws-sdk-2'
 }
 
 esplugin.bundleSpec.from('config/discovery-ec2') {
@@ -67,11 +121,13 @@ tasks.register("writeTestJavaPolicy") {
           "permission org.bouncycastle.crypto.CryptoServicesPermission \"exportSecretKey\";",
           "permission org.bouncycastle.crypto.CryptoServicesPermission \"exportPrivateKey\";",
           "permission java.io.FilePermission \"\${javax.net.ssl.trustStore}\", \"read\";",
-          "permission java.util.PropertyPermission \"com.amazonaws.sdk.ec2MetadataServiceEndpointOverride\", \"write\";",
           "permission java.security.SecurityPermission \"getProperty.jdk.tls.disabledAlgorithms\";",
           "permission java.security.SecurityPermission \"getProperty.jdk.certpath.disabledAlgorithms\";",
           "permission java.security.SecurityPermission \"getProperty.keystore.type.compat\";",
           "permission java.security.SecurityPermission \"getProperty.org.bouncycastle.ec.max_f2m_field_size\";",
+          "permission java.util.PropertyPermission \"aws.ec2MetadataServiceEndpoint\", \"write\";",
+          "permission java.util.PropertyPermission \"http.proxyHost\", \"read\";",
+          "permission java.util.PropertyPermission \"aws.region\", \"read\";",
           "};"
         ].join("\n")
       )
@@ -79,7 +135,9 @@ tasks.register("writeTestJavaPolicy") {
       javaPolicy.write(
         [
           "grant {",
-          "  permission java.util.PropertyPermission \"com.amazonaws.sdk.ec2MetadataServiceEndpointOverride\", \"write\";",
+          "permission java.util.PropertyPermission \"aws.ec2MetadataServiceEndpoint\", \"write\";",
+          "permission java.util.PropertyPermission \"http.proxyHost\", \"read\";",
+          "permission java.util.PropertyPermission \"aws.region\", \"read\";",
           "};"
         ].join("\n"))
     }
@@ -91,27 +149,42 @@ tasks.withType(Test).configureEach {
   // this is needed for insecure plugins, remove if possible!
   systemProperty 'tests.artifact', project.name
 
-  // this is needed to manipulate com.amazonaws.sdk.ec2MetadataServiceEndpointOverride system property
+  // this is needed to manipulate aws.ec2MetadataServiceEndpoint system property
   // it is better rather disable security manager at all with `systemProperty 'tests.security.manager', 'false'`
   if (buildParams.inFipsJvm){
     nonInputProperties.systemProperty 'java.security.policy', "=file://${layout.buildDirectory.asFile.get()}/tmp/java.policy"
   } else {
     nonInputProperties.systemProperty 'java.security.policy', "file://${layout.buildDirectory.asFile.get()}/tmp/java.policy"
   }
+
+  systemProperty 'aws.region', 'es-test-region'
 }
 
 tasks.named("thirdPartyAudit").configure {
   ignoreMissingClasses(
           // classes are missing
-          'com.amazonaws.jmespath.JmesPathExpression',
-          'com.amazonaws.jmespath.ObjectMapperSingleton',
           'javax.servlet.ServletContextEvent',
           'javax.servlet.ServletContextListener',
           'org.apache.avalon.framework.logger.Logger',
           'org.apache.log.Hierarchy',
           'org.apache.log.Logger',
           'javax.jms.Message',
-          'javax.xml.bind.DatatypeConverter',
-          'javax.xml.bind.JAXBContext'
+
+          // eventstream not used by the sync client
+          'software.amazon.eventstream.HeaderValue',
+          'software.amazon.eventstream.Message',
+          'software.amazon.eventstream.MessageDecoder',
+
+          // crt?
+          'software.amazon.awssdk.crt.auth.credentials.Credentials',
+          'software.amazon.awssdk.crt.auth.signing.AwsSigner',
+          'software.amazon.awssdk.crt.auth.signing.AwsSigningConfig',
+          'software.amazon.awssdk.crt.auth.signing.AwsSigningConfig$AwsSignatureType',
+          'software.amazon.awssdk.crt.auth.signing.AwsSigningConfig$AwsSignedBodyHeaderType',
+          'software.amazon.awssdk.crt.auth.signing.AwsSigningConfig$AwsSigningAlgorithm',
+          'software.amazon.awssdk.crt.auth.signing.AwsSigningResult',
+          'software.amazon.awssdk.crt.http.HttpHeader',
+          'software.amazon.awssdk.crt.http.HttpRequest',
+          'software.amazon.awssdk.crt.http.HttpRequestBodyStream',
   )
 }

+ 0 - 63
plugins/discovery-ec2/licenses/aws-java-sdk-LICENSE.txt

@@ -1,63 +0,0 @@
-Apache License
-Version 2.0, January 2004
-
-TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
-1. Definitions.
-
-"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
-
-"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
-
-"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
-
-"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
-
-"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
-
-"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
-
-"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
-
-"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
-
-"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
-
-"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
-
-2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
-
-3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
-
-4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
-
-   1.   You must give any other recipients of the Work or Derivative Works a copy of this License; and
-   2.   You must cause any modified files to carry prominent notices stating that You changed the files; and
-   3.   You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
-   4.   If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
-
-You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
-
-5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
-
-6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
-
-7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
-
-8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
-
-9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
-
-END OF TERMS AND CONDITIONS
-
-Note: Other license terms may apply to certain, identified software files contained within or distributed with the accompanying software if such terms are included in the directory containing the accompanying software. Such other license terms will then apply in lieu of the terms of the software license above.
-
-JSON processing code subject to the JSON License from JSON.org:
-
-Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
-
-The Software shall be used for Good, not Evil.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ 0 - 15
plugins/discovery-ec2/licenses/aws-java-sdk-NOTICE.txt

@@ -1,15 +0,0 @@
-AWS SDK for Java
-Copyright 2010-2014 Amazon.com, Inc. or its affiliates. All Rights Reserved.
-
-This product includes software developed by
-Amazon Technologies, Inc (http://www.amazon.com/).
-
-**********************
-THIRD PARTY COMPONENTS
-**********************
-This software includes third party software subject to the following copyrights:
-- XML parsing and utility functions from JetS3t - Copyright 2006-2009 James Murty.
-- JSON parsing and utility functions from JSON.org - Copyright 2002 JSON.org.
-- PKCS#1 PEM encoded private key parsing and utility functions from oauth.googlecode.com - Copyright 1998-2010 AOL Inc.
-
-The licenses for these third party components are included in LICENSE.txt

+ 206 - 0
plugins/discovery-ec2/licenses/aws-sdk-2-LICENSE.txt

@@ -0,0 +1,206 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+
+   Note: Other license terms may apply to certain, identified software files contained within or distributed
+   with the accompanying software if such terms are included in the directory containing the accompanying software.
+   Such other license terms will then apply in lieu of the terms of the software license above.

+ 26 - 0
plugins/discovery-ec2/licenses/aws-sdk-2-NOTICE.txt

@@ -0,0 +1,26 @@
+AWS SDK for Java 2.0
+Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+
+This product includes software developed by
+Amazon Technologies, Inc (http://www.amazon.com/).
+
+**********************
+THIRD PARTY COMPONENTS
+**********************
+This software includes third party software subject to the following copyrights:
+- XML parsing and utility functions from JetS3t - Copyright 2006-2009 James Murty.
+- PKCS#1 PEM encoded private key parsing and utility functions from oauth.googlecode.com - Copyright 1998-2010 AOL Inc.
+- Apache Commons Lang - https://github.com/apache/commons-lang
+- Netty Reactive Streams - https://github.com/playframework/netty-reactive-streams
+- Jackson-core - https://github.com/FasterXML/jackson-core
+- Jackson-dataformat-cbor - https://github.com/FasterXML/jackson-dataformats-binary
+
+The licenses for these third party components are included in LICENSE.txt
+
+- For Apache Commons Lang see also this required NOTICE:
+  Apache Commons Lang
+  Copyright 2001-2020 The Apache Software Foundation
+
+  This product includes software developed at
+  The Apache Software Foundation (https://www.apache.org/).
+

+ 0 - 8
plugins/discovery-ec2/licenses/jackson-LICENSE

@@ -1,8 +0,0 @@
-This copy of Jackson JSON processor streaming parser/generator is licensed under the
-Apache (Software) License, version 2.0 ("the License").
-See the License for details about distribution rights, and the
-specific rights regarding derivate works.
-
-You may obtain a copy of the License at:
-
-http://www.apache.org/licenses/LICENSE-2.0

+ 0 - 20
plugins/discovery-ec2/licenses/jackson-NOTICE

@@ -1,20 +0,0 @@
-# Jackson JSON processor
-
-Jackson is a high-performance, Free/Open Source JSON processing library.
-It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has
-been in development since 2007.
-It is currently developed by a community of developers, as well as supported
-commercially by FasterXML.com.
-
-## Licensing
-
-Jackson core and extension components may licensed under different licenses.
-To find the details that apply to this artifact see the accompanying LICENSE file.
-For more information, including possible other licensing options, contact
-FasterXML.com (http://fasterxml.com).
-
-## Credits
-
-A list of contributors may be found from CREDITS file, which is included
-in some artifacts (usually source distributions); but is always available
-from the source code management (SCM) system project uses.

+ 7 - 0
plugins/discovery-ec2/licenses/reactive-streams-LICENSE.txt

@@ -0,0 +1,7 @@
+MIT No Attribution
+
+Copyright 2014 Reactive Streams
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ 0 - 0
plugins/discovery-ec2/licenses/reactive-streams-NOTICE.txt


+ 21 - 0
plugins/discovery-ec2/licenses/slf4j-api-LICENSE.txt

@@ -0,0 +1,21 @@
+Copyright (c) 2004-2014 QOS.ch
+All rights reserved.
+
+Permission is hereby granted, free  of charge, to any person obtaining
+a  copy  of this  software  and  associated  documentation files  (the
+"Software"), to  deal in  the Software without  restriction, including
+without limitation  the rights to  use, copy, modify,  merge, publish,
+distribute,  sublicense, and/or sell  copies of  the Software,  and to
+permit persons to whom the Software  is furnished to do so, subject to
+the following conditions:
+
+The  above  copyright  notice  and  this permission  notice  shall  be
+included in all copies or substantial portions of the Software.
+
+THE  SOFTWARE IS  PROVIDED  "AS  IS", WITHOUT  WARRANTY  OF ANY  KIND,
+EXPRESS OR  IMPLIED, INCLUDING  BUT NOT LIMITED  TO THE  WARRANTIES OF
+MERCHANTABILITY,    FITNESS    FOR    A   PARTICULAR    PURPOSE    AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE,  ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ 0 - 0
plugins/discovery-ec2/licenses/slf4j-api-NOTICE.txt


+ 24 - 0
plugins/discovery-ec2/licenses/slf4j-nop-LICENSE.txt

@@ -0,0 +1,24 @@
+Copyright (c) 2004-2007 QOS.ch
+All rights reserved.
+
+Permission is hereby granted, free  of charge, to any person obtaining
+a  copy  of this  software  and  associated  documentation files  (the
+"Software"), to  deal in  the Software without  restriction, including
+without limitation  the rights to  use, copy, modify,  merge, publish,
+distribute,  sublicense, and/or sell  copies of  the Software,  and to
+permit persons to whom the Software  is furnished to do so, subject to
+the following conditions:
+
+The  above  copyright  notice  and  this permission  notice  shall  be
+included in all copies or substantial portions of the Software.
+
+THE  SOFTWARE IS  PROVIDED  "AS  IS", WITHOUT  WARRANTY  OF ANY  KIND,
+EXPRESS OR  IMPLIED, INCLUDING  BUT NOT LIMITED  TO THE  WARRANTIES OF
+MERCHANTABILITY,    FITNESS    FOR    A   PARTICULAR    PURPOSE    AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE,  ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+

+ 0 - 0
plugins/discovery-ec2/licenses/slf4j-nop-NOTICE.txt


+ 7 - 14
plugins/discovery-ec2/src/internalClusterTest/java/org/elasticsearch/discovery/ec2/DiscoveryEc2SpecialNetworkAddressesIT.java

@@ -17,22 +17,22 @@ import com.carrotsearch.randomizedtesting.annotations.Name;
 import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
 
 import java.util.Map;
-import java.util.stream.Stream;
 
+/**
+ * Verifies that the special network addresses {@code _ec2:*_} work by retrieving information from the IMDS. See {@code discovery-ec2}
+ * plugin docs for more information.
+ */
 public class DiscoveryEc2SpecialNetworkAddressesIT extends DiscoveryEc2NetworkAddressesTestCase {
 
     private final String imdsAddressName;
     private final String elasticsearchAddressName;
-    private final Ec2ImdsVersion imdsVersion;
 
     public DiscoveryEc2SpecialNetworkAddressesIT(
         @Name("imdsAddressName") String imdsAddressName,
-        @Name("elasticsearchAddressName") String elasticsearchAddressName,
-        @Name("imdsVersion") Ec2ImdsVersion imdsVersion
+        @Name("elasticsearchAddressName") String elasticsearchAddressName
     ) {
         this.imdsAddressName = imdsAddressName;
         this.elasticsearchAddressName = elasticsearchAddressName;
-        this.imdsVersion = imdsVersion;
     }
 
     @ParametersFactory
@@ -52,20 +52,13 @@ public class DiscoveryEc2SpecialNetworkAddressesIT extends DiscoveryEc2NetworkAd
             "local-ipv4",
             "_ec2_",
             "local-ipv4"
-        )
-            .entrySet()
-            .stream()
-            .flatMap(
-                addresses -> Stream.of(Ec2ImdsVersion.values())
-                    .map(ec2ImdsVersion -> new Object[] { addresses.getValue(), addresses.getKey(), ec2ImdsVersion })
-            )
-            .toList();
+        ).entrySet().stream().map(addresses -> new Object[] { addresses.getValue(), addresses.getKey() }).toList();
     }
 
     public void testSpecialNetworkAddresses() {
         final var publishAddress = "10.0." + between(0, 255) + "." + between(0, 255);
         Ec2ImdsHttpFixture.runWithFixture(
-            new Ec2ImdsServiceBuilder(imdsVersion).addInstanceAddress(imdsAddressName, publishAddress),
+            new Ec2ImdsServiceBuilder(Ec2ImdsVersion.V2).addInstanceAddress(imdsAddressName, publishAddress),
             imdsFixture -> {
                 try (var ignored = Ec2ImdsHttpFixture.withEc2MetadataServiceEndpointOverride(imdsFixture.getAddress())) {
                     verifyPublishAddress(elasticsearchAddressName, publishAddress);

+ 0 - 37
plugins/discovery-ec2/src/javaRestTest/java/org/elasticsearch/discovery/ec2/DiscoveryEc2AvailabilityZoneAttributeImdsV1IT.java

@@ -1,37 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the "Elastic License
- * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
- * Public License v 1"; you may not use this file except in compliance with, at
- * your election, the "Elastic License 2.0", the "GNU Affero General Public
- * License v3.0 only", or the "Server Side Public License, v 1".
- */
-
-package org.elasticsearch.discovery.ec2;
-
-import fixture.aws.imds.Ec2ImdsHttpFixture;
-import fixture.aws.imds.Ec2ImdsServiceBuilder;
-import fixture.aws.imds.Ec2ImdsVersion;
-
-import org.elasticsearch.test.cluster.ElasticsearchCluster;
-import org.junit.ClassRule;
-import org.junit.rules.RuleChain;
-import org.junit.rules.TestRule;
-
-public class DiscoveryEc2AvailabilityZoneAttributeImdsV1IT extends DiscoveryEc2AvailabilityZoneAttributeTestCase {
-    private static final Ec2ImdsHttpFixture ec2ImdsHttpFixture = new Ec2ImdsHttpFixture(
-        new Ec2ImdsServiceBuilder(Ec2ImdsVersion.V1).availabilityZoneSupplier(
-            DiscoveryEc2AvailabilityZoneAttributeTestCase::getAvailabilityZone
-        )
-    );
-
-    public static ElasticsearchCluster cluster = buildCluster(ec2ImdsHttpFixture::getAddress);
-
-    @ClassRule
-    public static TestRule ruleChain = RuleChain.outerRule(ec2ImdsHttpFixture).around(cluster);
-
-    @Override
-    protected String getTestRestCluster() {
-        return cluster.getHttpAddresses();
-    }
-}

+ 1 - 1
plugins/discovery-ec2/src/javaRestTest/java/org/elasticsearch/discovery/ec2/DiscoveryEc2AvailabilityZoneAttributeTestCase.java

@@ -36,7 +36,7 @@ public abstract class DiscoveryEc2AvailabilityZoneAttributeTestCase extends ESRe
         return ElasticsearchCluster.local()
             .plugin("discovery-ec2")
             .setting(AwsEc2Service.AUTO_ATTRIBUTE_SETTING.getKey(), "true")
-            .systemProperty(Ec2ImdsHttpFixture.ENDPOINT_OVERRIDE_SYSPROP_NAME, imdsFixtureAddressSupplier)
+            .systemProperty(Ec2ImdsHttpFixture.ENDPOINT_OVERRIDE_SYSPROP_NAME_SDK2, imdsFixtureAddressSupplier)
             .build();
     }
 

+ 1 - 1
plugins/discovery-ec2/src/javaRestTest/java/org/elasticsearch/discovery/ec2/DiscoveryEc2InstanceProfileIT.java

@@ -44,7 +44,7 @@ public class DiscoveryEc2InstanceProfileIT extends DiscoveryEc2ClusterFormationT
         .setting(DiscoveryModule.DISCOVERY_SEED_PROVIDERS_SETTING.getKey(), Ec2DiscoveryPlugin.EC2_SEED_HOSTS_PROVIDER_NAME)
         .setting("logger." + AwsEc2SeedHostsProvider.class.getCanonicalName(), "DEBUG")
         .setting(Ec2ClientSettings.ENDPOINT_SETTING.getKey(), ec2ApiFixture::getAddress)
-        .systemProperty(Ec2ImdsHttpFixture.ENDPOINT_OVERRIDE_SYSPROP_NAME, ec2ImdsHttpFixture::getAddress)
+        .systemProperty(Ec2ImdsHttpFixture.ENDPOINT_OVERRIDE_SYSPROP_NAME_SDK2, ec2ImdsHttpFixture::getAddress)
         .build();
 
     private static List<String> getAvailableTransportEndpoints() {

+ 1 - 1
plugins/discovery-ec2/src/javaRestTest/java/org/elasticsearch/discovery/ec2/DiscoveryEc2SystemPropertyCredentialsIT.java

@@ -41,7 +41,7 @@ public class DiscoveryEc2SystemPropertyCredentialsIT extends DiscoveryEc2Cluster
         .setting(Ec2ClientSettings.ENDPOINT_SETTING.getKey(), ec2ApiFixture::getAddress)
         .environment("AWS_REGION", REGION)
         .systemProperty("aws.accessKeyId", ACCESS_KEY)
-        .systemProperty("aws.secretKey", ESTestCase::randomSecretKey)
+        .systemProperty("aws.secretAccessKey", ESTestCase::randomSecretKey)
         .build();
 
     private static List<String> getAvailableTransportEndpoints() {

+ 8 - 9
plugins/discovery-ec2/src/main/java/org/elasticsearch/discovery/ec2/AmazonEc2Reference.java

@@ -9,20 +9,19 @@
 
 package org.elasticsearch.discovery.ec2;
 
-import com.amazonaws.services.ec2.AmazonEC2;
+import software.amazon.awssdk.services.ec2.Ec2Client;
 
 import org.elasticsearch.core.AbstractRefCounted;
 import org.elasticsearch.core.Releasable;
 
 /**
- * Handles the shutdown of the wrapped {@link AmazonEC2} using reference
- * counting.
+ * Handles the shutdown of the wrapped {@link Ec2Client} using reference counting.
  */
 public class AmazonEc2Reference extends AbstractRefCounted implements Releasable {
 
-    private final AmazonEC2 client;
+    private final Ec2Client client;
 
-    AmazonEc2Reference(AmazonEC2 client) {
+    AmazonEc2Reference(Ec2Client client) {
         this.client = client;
     }
 
@@ -35,16 +34,16 @@ public class AmazonEc2Reference extends AbstractRefCounted implements Releasable
     }
 
     /**
-     * Returns the underlying `AmazonEC2` client. All method calls are permitted BUT
-     * NOT shutdown. Shutdown is called when reference count reaches 0.
+     * Returns the underlying {@link Ec2Client} client. All method calls are permitted EXCEPT {@link Ec2Client#close}, which is called
+     * automatically when this object's reference count reaches 0.
      */
-    public AmazonEC2 client() {
+    public Ec2Client client() {
         return client;
     }
 
     @Override
     protected void closeInternal() {
-        client.shutdown();
+        client.close();
     }
 
 }

+ 35 - 33
plugins/discovery-ec2/src/main/java/org/elasticsearch/discovery/ec2/AwsEc2SeedHostsProvider.java

@@ -9,14 +9,13 @@
 
 package org.elasticsearch.discovery.ec2;
 
-import com.amazonaws.AmazonClientException;
-import com.amazonaws.services.ec2.model.DescribeInstancesRequest;
-import com.amazonaws.services.ec2.model.DescribeInstancesResult;
-import com.amazonaws.services.ec2.model.Filter;
-import com.amazonaws.services.ec2.model.GroupIdentifier;
-import com.amazonaws.services.ec2.model.Instance;
-import com.amazonaws.services.ec2.model.Reservation;
-import com.amazonaws.services.ec2.model.Tag;
+import software.amazon.awssdk.services.ec2.model.DescribeInstancesRequest;
+import software.amazon.awssdk.services.ec2.model.DescribeInstancesResponse;
+import software.amazon.awssdk.services.ec2.model.Filter;
+import software.amazon.awssdk.services.ec2.model.GroupIdentifier;
+import software.amazon.awssdk.services.ec2.model.Instance;
+import software.amazon.awssdk.services.ec2.model.Reservation;
+import software.amazon.awssdk.services.ec2.model.Tag;
 
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.common.transport.TransportAddress;
@@ -41,6 +40,9 @@ import static org.elasticsearch.discovery.ec2.AwsEc2Service.HostType.PUBLIC_DNS;
 import static org.elasticsearch.discovery.ec2.AwsEc2Service.HostType.PUBLIC_IP;
 import static org.elasticsearch.discovery.ec2.AwsEc2Service.HostType.TAG_PREFIX;
 
+/**
+ * A {@link SeedHostsProvider} which provides the seed host addresses for discovery by calling the AWS EC2 {@code DescribeInstances} API.
+ */
 class AwsEc2SeedHostsProvider implements SeedHostsProvider {
 
     private static final Logger logger = LogManager.getLogger(AwsEc2SeedHostsProvider.class);
@@ -98,7 +100,7 @@ class AwsEc2SeedHostsProvider implements SeedHostsProvider {
 
         final List<TransportAddress> dynamicHostAddresses = new ArrayList<>();
 
-        final DescribeInstancesResult descInstances;
+        final DescribeInstancesResponse descInstances;
         try (AmazonEc2Reference clientReference = awsEc2Service.client()) {
             // Query EC2 API based on AZ, instance state, and tag.
 
@@ -106,30 +108,30 @@ class AwsEc2SeedHostsProvider implements SeedHostsProvider {
             // 1. differences in VPCs require different parameters during query (ID vs Name)
             // 2. We want to use two different strategies: (all security groups vs. any security groups)
             descInstances = SocketAccess.doPrivileged(() -> clientReference.client().describeInstances(buildDescribeInstancesRequest()));
-        } catch (final AmazonClientException e) {
+        } catch (final Exception e) {
             logger.info("Exception while retrieving instance list from AWS API: {}", e.getMessage());
             logger.debug("Full exception:", e);
             return dynamicHostAddresses;
         }
 
         logger.trace("finding seed nodes...");
-        for (final Reservation reservation : descInstances.getReservations()) {
-            for (final Instance instance : reservation.getInstances()) {
+        for (final Reservation reservation : descInstances.reservations()) {
+            for (final Instance instance : reservation.instances()) {
                 // lets see if we can filter based on groups
                 if (groups.isEmpty() == false) {
-                    final List<GroupIdentifier> instanceSecurityGroups = instance.getSecurityGroups();
+                    final List<GroupIdentifier> instanceSecurityGroups = instance.securityGroups();
                     final List<String> securityGroupNames = new ArrayList<>(instanceSecurityGroups.size());
                     final List<String> securityGroupIds = new ArrayList<>(instanceSecurityGroups.size());
                     for (final GroupIdentifier sg : instanceSecurityGroups) {
-                        securityGroupNames.add(sg.getGroupName());
-                        securityGroupIds.add(sg.getGroupId());
+                        securityGroupNames.add(sg.groupName());
+                        securityGroupIds.add(sg.groupId());
                     }
                     if (bindAnyGroup) {
                         // We check if we can find at least one group name or one group id in groups.
                         if (disjoint(securityGroupNames, groups) && disjoint(securityGroupIds, groups)) {
                             logger.trace(
                                 "filtering out instance {} based on groups {}, not part of {}",
-                                instance.getInstanceId(),
+                                instance.instanceId(),
                                 instanceSecurityGroups,
                                 groups
                             );
@@ -141,7 +143,7 @@ class AwsEc2SeedHostsProvider implements SeedHostsProvider {
                         if ((securityGroupNames.containsAll(groups) || securityGroupIds.containsAll(groups)) == false) {
                             logger.trace(
                                 "filtering out instance {} based on groups {}, does not include all of {}",
-                                instance.getInstanceId(),
+                                instance.instanceId(),
                                 instanceSecurityGroups,
                                 groups
                             );
@@ -153,21 +155,21 @@ class AwsEc2SeedHostsProvider implements SeedHostsProvider {
 
                 String address = null;
                 if (hostType.equals(PRIVATE_DNS)) {
-                    address = instance.getPrivateDnsName();
+                    address = instance.privateDnsName();
                 } else if (hostType.equals(PRIVATE_IP)) {
-                    address = instance.getPrivateIpAddress();
+                    address = instance.privateIpAddress();
                 } else if (hostType.equals(PUBLIC_DNS)) {
-                    address = instance.getPublicDnsName();
+                    address = instance.publicDnsName();
                 } else if (hostType.equals(PUBLIC_IP)) {
-                    address = instance.getPublicIpAddress();
+                    address = instance.publicIpAddress();
                 } else if (hostType.startsWith(TAG_PREFIX)) {
                     // Reading the node host from its metadata
                     final String tagName = hostType.substring(TAG_PREFIX.length());
                     logger.debug("reading hostname from [{}] instance tag", tagName);
-                    final List<Tag> tagList = instance.getTags();
+                    final List<Tag> tagList = instance.tags();
                     for (final Tag tag : tagList) {
-                        if (tag.getKey().equals(tagName)) {
-                            address = tag.getValue();
+                        if (tag.key().equals(tagName)) {
+                            address = tag.value();
                             logger.debug("using [{}] as the instance address", address);
                         }
                     }
@@ -178,15 +180,15 @@ class AwsEc2SeedHostsProvider implements SeedHostsProvider {
                     try {
                         final TransportAddress[] addresses = transportService.addressesFromString(address);
                         for (int i = 0; i < addresses.length; i++) {
-                            logger.trace("adding {}, address {}, transport_address {}", instance.getInstanceId(), address, addresses[i]);
+                            logger.trace("adding {}, address {}, transport_address {}", instance.instanceId(), address, addresses[i]);
                             dynamicHostAddresses.add(addresses[i]);
                         }
                     } catch (final Exception e) {
                         final String finalAddress = address;
-                        logger.warn(() -> format("failed to add %s, address %s", instance.getInstanceId(), finalAddress), e);
+                        logger.warn(() -> format("failed to add %s, address %s", instance.instanceId(), finalAddress), e);
                     }
                 } else {
-                    logger.trace("not adding {}, address is null, host_type {}", instance.getInstanceId(), hostType);
+                    logger.trace("not adding {}, address is null, host_type {}", instance.instanceId(), hostType);
                 }
             }
         }
@@ -197,21 +199,21 @@ class AwsEc2SeedHostsProvider implements SeedHostsProvider {
     }
 
     private DescribeInstancesRequest buildDescribeInstancesRequest() {
-        final DescribeInstancesRequest describeInstancesRequest = new DescribeInstancesRequest().withFilters(
-            new Filter("instance-state-name").withValues("running", "pending")
-        );
+
+        final var filters = new ArrayList<Filter>(2 + tags.size());
+        filters.add(Filter.builder().name("instance-state-name").values("running", "pending").build());
 
         for (final Map.Entry<String, List<String>> tagFilter : tags.entrySet()) {
             // for a given tag key, OR relationship for multiple different values
-            describeInstancesRequest.withFilters(new Filter("tag:" + tagFilter.getKey()).withValues(tagFilter.getValue()));
+            filters.add(Filter.builder().name("tag:" + tagFilter.getKey()).values(tagFilter.getValue()).build());
         }
 
         if (availabilityZones.isEmpty() == false) {
             // OR relationship amongst multiple values of the availability-zone filter
-            describeInstancesRequest.withFilters(new Filter("availability-zone").withValues(availabilityZones));
+            filters.add(Filter.builder().name("availability-zone").values(availabilityZones).build());
         }
 
-        return describeInstancesRequest;
+        return DescribeInstancesRequest.builder().filters(filters).build();
     }
 
     private final class TransportAddressesCache extends SingleObjectCache<List<TransportAddress>> {

+ 9 - 2
plugins/discovery-ec2/src/main/java/org/elasticsearch/discovery/ec2/AwsEc2Service.java

@@ -9,6 +9,8 @@
 
 package org.elasticsearch.discovery.ec2;
 
+import software.amazon.awssdk.services.ec2.Ec2Client;
+
 import org.elasticsearch.common.settings.Setting;
 import org.elasticsearch.common.settings.Setting.Property;
 import org.elasticsearch.core.TimeValue;
@@ -16,6 +18,11 @@ import org.elasticsearch.core.TimeValue;
 import java.io.Closeable;
 import java.util.List;
 
+/**
+ * Abstract representation of a connection to the EC2 API service: exposes an {@link AmazonEc2Reference} via {@link #client()} and allows
+ * to refresh the client settings via {@link #refreshAndClearCache}.
+ */
+// This is kinda pointless extra indirection and only has one implementation; TODO fold it into Ec2DiscoveryPlugin
 interface AwsEc2Service extends Closeable {
     Setting<Boolean> AUTO_ATTRIBUTE_SETTING = Setting.boolSetting("cloud.node.auto_attributes", false, Property.NodeScope);
 
@@ -70,8 +77,8 @@ interface AwsEc2Service extends Closeable {
     );
 
     /**
-     * Builds then caches an {@code AmazonEC2} client using the current client
-     * settings. Returns an {@code AmazonEc2Reference} wrapper which should be
+     * Builds then caches an {@link Ec2Client} client using the current client
+     * settings. Returns an {@link AmazonEc2Reference} wrapper which should be
      * released as soon as it is not required anymore.
      */
     AmazonEc2Reference client();

+ 81 - 56
plugins/discovery-ec2/src/main/java/org/elasticsearch/discovery/ec2/AwsEc2ServiceImpl.java

@@ -9,24 +9,36 @@
 
 package org.elasticsearch.discovery.ec2;
 
-import com.amazonaws.ClientConfiguration;
-import com.amazonaws.auth.AWSCredentials;
-import com.amazonaws.auth.AWSCredentialsProvider;
-import com.amazonaws.auth.AWSStaticCredentialsProvider;
-import com.amazonaws.auth.DefaultAWSCredentialsProviderChain;
-import com.amazonaws.client.builder.AwsClientBuilder;
-import com.amazonaws.http.IdleConnectionReaper;
-import com.amazonaws.services.ec2.AmazonEC2;
-import com.amazonaws.services.ec2.AmazonEC2ClientBuilder;
+import software.amazon.awssdk.auth.credentials.AwsCredentials;
+import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
+import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
+import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
+import software.amazon.awssdk.endpoints.Endpoint;
+import software.amazon.awssdk.http.apache.ApacheHttpClient;
+import software.amazon.awssdk.http.apache.ProxyConfiguration;
+import software.amazon.awssdk.services.ec2.Ec2Client;
+import software.amazon.awssdk.services.ec2.Ec2ClientBuilder;
 
+import org.apache.http.client.utils.URIBuilder;
 import org.elasticsearch.ElasticsearchException;
 import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.util.LazyInitializable;
+import org.elasticsearch.core.AbstractRefCounted;
 import org.elasticsearch.logging.LogManager;
 import org.elasticsearch.logging.Logger;
 
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.time.Duration;
+import java.time.temporal.ChronoUnit;
+import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.atomic.AtomicReference;
 
+/**
+ * Concrete representation of a connection to the EC2 API service: exposes an {@link AmazonEc2Reference} via {@link #client()} and allows
+ * to refresh the client settings via {@link #refreshAndClearCache}.
+ */
+// This is kinda pointless extra indirection; TODO fold it into Ec2DiscoveryPlugin
 class AwsEc2ServiceImpl implements AwsEc2Service {
 
     private static final Logger LOGGER = LogManager.getLogger(AwsEc2ServiceImpl.class);
@@ -34,55 +46,72 @@ class AwsEc2ServiceImpl implements AwsEc2Service {
     private final AtomicReference<LazyInitializable<AmazonEc2Reference, ElasticsearchException>> lazyClientReference =
         new AtomicReference<>();
 
-    private AmazonEC2 buildClient(Ec2ClientSettings clientSettings) {
-        final AWSCredentialsProvider credentials = buildCredentials(LOGGER, clientSettings);
-        final ClientConfiguration configuration = buildConfiguration(clientSettings);
-        return buildClient(credentials, configuration, clientSettings.endpoint);
-    }
+    private Ec2Client buildClient(Ec2ClientSettings clientSettings) {
+        final var httpClientBuilder = getHttpClientBuilder();
+        httpClientBuilder.socketTimeout(Duration.of(clientSettings.readTimeoutMillis, ChronoUnit.MILLIS));
+
+        if (Strings.hasText(clientSettings.proxyHost)) {
+            applyProxyConfiguration(clientSettings, httpClientBuilder);
+        }
+
+        final var ec2ClientBuilder = getEc2ClientBuilder();
+        ec2ClientBuilder.credentialsProvider(getAwsCredentialsProvider(clientSettings));
+        ec2ClientBuilder.httpClientBuilder(httpClientBuilder);
+
+        // Increase the number of retries in case of 5xx API responses
+        ec2ClientBuilder.overrideConfiguration(b -> b.retryStrategy(c -> c.maxAttempts(10)));
 
-    // proxy for testing
-    AmazonEC2 buildClient(AWSCredentialsProvider credentials, ClientConfiguration configuration, String endpoint) {
-        final AmazonEC2ClientBuilder builder = AmazonEC2ClientBuilder.standard()
-            .withCredentials(credentials)
-            .withClientConfiguration(configuration);
-        if (Strings.hasText(endpoint)) {
-            LOGGER.debug("using explicit ec2 endpoint [{}]", endpoint);
-            builder.withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(endpoint, null));
+        if (Strings.hasText(clientSettings.endpoint)) {
+            LOGGER.debug("using explicit ec2 endpoint [{}]", clientSettings.endpoint);
+            final var endpoint = Endpoint.builder().url(URI.create(clientSettings.endpoint)).build();
+            ec2ClientBuilder.endpointProvider(endpointParams -> CompletableFuture.completedFuture(endpoint));
         }
-        return SocketAccess.doPrivileged(builder::build);
+        return SocketAccess.doPrivileged(ec2ClientBuilder::build);
     }
 
-    // pkg private for tests
-    static ClientConfiguration buildConfiguration(Ec2ClientSettings clientSettings) {
-        final ClientConfiguration clientConfiguration = new ClientConfiguration();
-        // the response metadata cache is only there for diagnostics purposes,
-        // but can force objects from every response to the old generation.
-        clientConfiguration.setResponseMetadataCacheSize(0);
-        clientConfiguration.setProtocol(clientSettings.protocol);
-        if (Strings.hasText(clientSettings.proxyHost)) {
-            // TODO: remove this leniency, these settings should exist together and be validated
-            clientConfiguration.setProxyHost(clientSettings.proxyHost);
-            clientConfiguration.setProxyPort(clientSettings.proxyPort);
-            clientConfiguration.setProxyProtocol(clientSettings.proxyScheme);
-            clientConfiguration.setProxyUsername(clientSettings.proxyUsername);
-            clientConfiguration.setProxyPassword(clientSettings.proxyPassword);
+    private static void applyProxyConfiguration(Ec2ClientSettings clientSettings, ApacheHttpClient.Builder httpClientBuilder) {
+        final var uriBuilder = new URIBuilder();
+        uriBuilder.setScheme(clientSettings.proxyScheme.getSchemeString())
+            .setHost(clientSettings.proxyHost)
+            .setPort(clientSettings.proxyPort);
+        final URI proxyUri;
+        try {
+            proxyUri = uriBuilder.build();
+        } catch (URISyntaxException e) {
+            throw new IllegalArgumentException(e);
         }
-        // Increase the number of retries in case of 5xx API responses
-        clientConfiguration.setMaxErrorRetry(10);
-        clientConfiguration.setSocketTimeout(clientSettings.readTimeoutMillis);
-        return clientConfiguration;
+
+        httpClientBuilder.proxyConfiguration(
+            ProxyConfiguration.builder()
+                .endpoint(proxyUri)
+                .scheme(clientSettings.proxyScheme.getSchemeString())
+                .username(clientSettings.proxyUsername)
+                .password(clientSettings.proxyPassword)
+                .build()
+        );
+    }
+
+    // exposed for tests
+    Ec2ClientBuilder getEc2ClientBuilder() {
+        return Ec2Client.builder();
+    }
+
+    // exposed for tests
+    ApacheHttpClient.Builder getHttpClientBuilder() {
+        return ApacheHttpClient.builder();
     }
 
-    // pkg private for tests
-    static AWSCredentialsProvider buildCredentials(Logger logger, Ec2ClientSettings clientSettings) {
-        final AWSCredentials credentials = clientSettings.credentials;
+    private static AwsCredentialsProvider getAwsCredentialsProvider(Ec2ClientSettings clientSettings) {
+        final AwsCredentialsProvider credentialsProvider;
+        final AwsCredentials credentials = clientSettings.credentials;
         if (credentials == null) {
-            logger.debug("Using default provider chain");
-            return DefaultAWSCredentialsProviderChain.getInstance();
+            LOGGER.debug("Using default provider chain");
+            credentialsProvider = DefaultCredentialsProvider.create();
         } else {
-            logger.debug("Using basic key/secret credentials");
-            return new AWSStaticCredentialsProvider(credentials);
+            LOGGER.debug("Using basic key/secret credentials");
+            credentialsProvider = StaticCredentialsProvider.create(credentials);
         }
+        return credentialsProvider;
     }
 
     @Override
@@ -95,16 +124,15 @@ class AwsEc2ServiceImpl implements AwsEc2Service {
     }
 
     /**
-     * Refreshes the settings for the AmazonEC2 client. The new client will be build
-     * using these new settings. The old client is usable until released. On release it
-     * will be destroyed instead of being returned to the cache.
+     * Refreshes the settings for the {@link Ec2Client} client. The new client will be built using these new settings. The old client is
+     * usable until released. On release it will be destroyed instead of being returned to the cache.
      */
     @Override
     public void refreshAndClearCache(Ec2ClientSettings clientSettings) {
         final LazyInitializable<AmazonEc2Reference, ElasticsearchException> newClient = new LazyInitializable<>(
             () -> new AmazonEc2Reference(buildClient(clientSettings)),
-            clientReference -> clientReference.incRef(),
-            clientReference -> clientReference.decRef()
+            AbstractRefCounted::incRef,
+            AbstractRefCounted::decRef
         );
         final LazyInitializable<AmazonEc2Reference, ElasticsearchException> oldClient = this.lazyClientReference.getAndSet(newClient);
         if (oldClient != null) {
@@ -118,9 +146,6 @@ class AwsEc2ServiceImpl implements AwsEc2Service {
         if (clientReference != null) {
             clientReference.reset();
         }
-        // shutdown IdleConnectionReaper background thread
-        // it will be restarted on new client usage
-        IdleConnectionReaper.shutdown();
     }
 
 }

+ 13 - 50
plugins/discovery-ec2/src/main/java/org/elasticsearch/discovery/ec2/AwsEc2Utils.java

@@ -9,62 +9,25 @@
 
 package org.elasticsearch.discovery.ec2;
 
-import com.amazonaws.SDKGlobalConfiguration;
-import com.amazonaws.util.StringUtils;
+import software.amazon.awssdk.http.apache.ApacheHttpClient;
+import software.amazon.awssdk.imds.Ec2MetadataClient;
 
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
 import org.elasticsearch.common.Strings;
-import org.elasticsearch.core.SuppressForbidden;
 
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.net.HttpURLConnection;
-import java.net.URL;
-import java.nio.charset.StandardCharsets;
-import java.util.Optional;
+import java.time.Duration;
 
 class AwsEc2Utils {
+    private static final Duration IMDS_CONNECTION_TIMEOUT = Duration.ofSeconds(2);
 
-    private static final Logger logger = LogManager.getLogger(AwsEc2Utils.class);
-    // The timeout can be configured via the AWS_METADATA_SERVICE_TIMEOUT environment variable
-    private static final int TIMEOUT = Optional.ofNullable(System.getenv(SDKGlobalConfiguration.AWS_METADATA_SERVICE_TIMEOUT_ENV_VAR))
-        .filter(StringUtils::hasValue)
-        .map(s -> Integer.parseInt(s) * 1000)
-        .orElse(2000);
-    private static final int METADATA_TOKEN_TTL_SECONDS = 10;
-    static final String X_AWS_EC_2_METADATA_TOKEN = "X-aws-ec2-metadata-token";
-
-    @SuppressForbidden(reason = "We call getInputStream in doPrivileged and provide SocketPermission")
-    static Optional<String> getMetadataToken(String metadataTokenUrl) {
-        if (Strings.isNullOrEmpty(metadataTokenUrl)) {
-            return Optional.empty();
-        }
-        // Gets a new IMDSv2 token https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html
-        return SocketAccess.doPrivileged(() -> {
-            HttpURLConnection urlConnection;
-            try {
-                urlConnection = (HttpURLConnection) new URL(metadataTokenUrl).openConnection();
-                urlConnection.setRequestMethod("PUT");
-                // Use both timeout for connect and read timeout analogous to AWS SDK.
-                // See com.amazonaws.internal.HttpURLConnection#connectToEndpoint
-                urlConnection.setConnectTimeout(TIMEOUT);
-                urlConnection.setReadTimeout(TIMEOUT);
-                urlConnection.setRequestProperty("X-aws-ec2-metadata-token-ttl-seconds", String.valueOf(METADATA_TOKEN_TTL_SECONDS));
-            } catch (IOException e) {
-                logger.warn("Unable to access the IMDSv2 URI: " + metadataTokenUrl, e);
-                return Optional.empty();
+    static String getInstanceMetadata(String metadataPath) {
+        final var httpClientBuilder = ApacheHttpClient.builder();
+        httpClientBuilder.connectionTimeout(IMDS_CONNECTION_TIMEOUT);
+        try (var ec2Client = SocketAccess.doPrivileged(Ec2MetadataClient.builder().httpClient(httpClientBuilder)::build)) {
+            final var metadataValue = SocketAccess.doPrivileged(() -> ec2Client.get(metadataPath)).asString();
+            if (Strings.hasText(metadataValue) == false) {
+                throw new IllegalStateException("no ec2 metadata returned from " + metadataPath);
             }
-            try (
-                var in = urlConnection.getInputStream();
-                var reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))
-            ) {
-                return Optional.ofNullable(reader.readLine()).filter(s -> s.isBlank() == false);
-            } catch (IOException e) {
-                logger.warn("Unable to get a session token from IMDSv2 URI: " + metadataTokenUrl, e);
-                return Optional.empty();
-            }
-        });
+            return metadataValue;
+        }
     }
 }

+ 35 - 47
plugins/discovery-ec2/src/main/java/org/elasticsearch/discovery/ec2/Ec2ClientSettings.java

@@ -9,14 +9,10 @@
 
 package org.elasticsearch.discovery.ec2;
 
-import com.amazonaws.ClientConfiguration;
-import com.amazonaws.Protocol;
-import com.amazonaws.auth.AWSCredentials;
-import com.amazonaws.auth.BasicAWSCredentials;
-import com.amazonaws.auth.BasicSessionCredentials;
-
-import org.elasticsearch.common.logging.DeprecationCategory;
-import org.elasticsearch.common.logging.DeprecationLogger;
+import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
+import software.amazon.awssdk.auth.credentials.AwsCredentials;
+import software.amazon.awssdk.auth.credentials.AwsSessionCredentials;
+
 import org.elasticsearch.common.settings.SecureSetting;
 import org.elasticsearch.common.settings.SecureString;
 import org.elasticsearch.common.settings.Setting;
@@ -24,6 +20,7 @@ import org.elasticsearch.common.settings.Setting.Property;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.common.settings.SettingsException;
 import org.elasticsearch.core.TimeValue;
+import org.elasticsearch.core.UpdateForV10;
 import org.elasticsearch.logging.LogManager;
 import org.elasticsearch.logging.Logger;
 
@@ -50,10 +47,10 @@ final class Ec2ClientSettings {
     static final Setting<Integer> PROXY_PORT_SETTING = Setting.intSetting("discovery.ec2.proxy.port", 80, 0, 1 << 16, Property.NodeScope);
 
     /** The scheme to use for the proxy connection to ec2. Defaults to "http". */
-    static final Setting<Protocol> PROXY_SCHEME_SETTING = new Setting<>(
+    static final Setting<HttpScheme> PROXY_SCHEME_SETTING = new Setting<>(
         "discovery.ec2.proxy.scheme",
         "http",
-        s -> Protocol.valueOf(s.toUpperCase(Locale.ROOT)),
+        s -> HttpScheme.valueOf(s.toUpperCase(Locale.ROOT)),
         Property.NodeScope
     );
 
@@ -65,33 +62,36 @@ final class Ec2ClientSettings {
         Property.NodeScope
     );
 
-    /** The protocol to use to connect  to ec2. */
-    static final Setting<Protocol> PROTOCOL_SETTING = new Setting<>(
+    /** Previously, the protocol to use to connect to ec2, but now has no effect */
+    @UpdateForV10(owner = UpdateForV10.Owner.DISTRIBUTED_COORDINATION) // no longer used, should be removed in v10
+    static final Setting<HttpScheme> PROTOCOL_SETTING = new Setting<>(
         "discovery.ec2.protocol",
         "https",
-        s -> Protocol.valueOf(s.toUpperCase(Locale.ROOT)),
-        Property.NodeScope
+        s -> HttpScheme.valueOf(s.toUpperCase(Locale.ROOT)),
+        Property.NodeScope,
+        Property.Deprecated
     );
+    // NOMERGE should we now reject this if set to `http` or just silently ignore it?
 
-    /** The username of a proxy to connect to s3 through. */
+    /** The username of a proxy to connect to EC2 through. */
     static final Setting<SecureString> PROXY_USERNAME_SETTING = SecureSetting.secureString("discovery.ec2.proxy.username", null);
 
-    /** The password of a proxy to connect to s3 through. */
+    /** The password of a proxy to connect to EC2 through. */
     static final Setting<SecureString> PROXY_PASSWORD_SETTING = SecureSetting.secureString("discovery.ec2.proxy.password", null);
 
-    /** The socket timeout for connecting to s3. */
+    private static final TimeValue DEFAULT_READ_TIMEOUT = TimeValue.timeValueSeconds(50);
+
+    /** The socket timeout for connecting to EC2. */
     static final Setting<TimeValue> READ_TIMEOUT_SETTING = Setting.timeSetting(
         "discovery.ec2.read_timeout",
-        TimeValue.timeValueMillis(ClientConfiguration.DEFAULT_SOCKET_TIMEOUT),
+        DEFAULT_READ_TIMEOUT,
         Property.NodeScope
     );
 
     private static final Logger logger = LogManager.getLogger(Ec2ClientSettings.class);
 
-    private static final DeprecationLogger deprecationLogger = DeprecationLogger.getLogger(Ec2ClientSettings.class);
-
     /** Credentials to authenticate with ec2. */
-    final AWSCredentials credentials;
+    final AwsCredentials credentials;
 
     /**
      * The ec2 endpoint the client should talk to, or empty string to use the
@@ -99,9 +99,6 @@ final class Ec2ClientSettings {
      */
     final String endpoint;
 
-    /** The protocol to use to talk to ec2. Defaults to https. */
-    final Protocol protocol;
-
     /** An optional proxy host that requests to ec2 should be made through. */
     final String proxyHost;
 
@@ -109,7 +106,7 @@ final class Ec2ClientSettings {
     final int proxyPort;
 
     /** The scheme to use for the proxy connection to ec2 */
-    final Protocol proxyScheme;
+    final HttpScheme proxyScheme;
 
     // these should be "secure" yet the api for the ec2 client only takes String, so
     // storing them
@@ -123,20 +120,18 @@ final class Ec2ClientSettings {
     /** The read timeout for the ec2 client. */
     final int readTimeoutMillis;
 
-    protected Ec2ClientSettings(
-        AWSCredentials credentials,
+    private Ec2ClientSettings(
+        AwsCredentials credentials,
         String endpoint,
-        Protocol protocol,
         String proxyHost,
         int proxyPort,
-        Protocol proxyScheme,
+        HttpScheme proxyScheme,
         String proxyUsername,
         String proxyPassword,
         int readTimeoutMillis
     ) {
         this.credentials = credentials;
         this.endpoint = endpoint;
-        this.protocol = protocol;
         this.proxyHost = proxyHost;
         this.proxyPort = proxyPort;
         this.proxyScheme = proxyScheme;
@@ -145,7 +140,7 @@ final class Ec2ClientSettings {
         this.readTimeoutMillis = readTimeoutMillis;
     }
 
-    static AWSCredentials loadCredentials(Settings settings) {
+    static AwsCredentials loadCredentials(Settings settings) {
         try (
             SecureString key = ACCESS_KEY_SETTING.get(settings);
             SecureString secret = SECRET_KEY_SETTING.get(settings);
@@ -165,33 +160,27 @@ final class Ec2ClientSettings {
                 return null;
             } else {
                 if (key.length() == 0) {
-                    deprecationLogger.warn(
-                        DeprecationCategory.SETTINGS,
-                        "ec2_invalid_settings",
-                        "Setting [{}] is set but [{}] is not, which will be unsupported in future",
+                    throw new SettingsException(
+                        "Setting [{}] is set but [{}] is not",
                         SECRET_KEY_SETTING.getKey(),
                         ACCESS_KEY_SETTING.getKey()
                     );
                 }
                 if (secret.length() == 0) {
-                    deprecationLogger.warn(
-                        DeprecationCategory.SETTINGS,
-                        "ec2_invalid_settings",
-                        "Setting [{}] is set but [{}] is not, which will be unsupported in future",
+                    throw new SettingsException(
+                        "Setting [{}] is set but [{}] is not",
                         ACCESS_KEY_SETTING.getKey(),
                         SECRET_KEY_SETTING.getKey()
                     );
                 }
 
-                final AWSCredentials credentials;
                 if (sessionToken.length() == 0) {
                     logger.debug("Using basic key/secret credentials");
-                    credentials = new BasicAWSCredentials(key.toString(), secret.toString());
+                    return AwsBasicCredentials.create(key.toString(), secret.toString());
                 } else {
-                    logger.debug("Using basic session credentials");
-                    credentials = new BasicSessionCredentials(key.toString(), secret.toString(), sessionToken.toString());
+                    logger.debug("Using session credentials");
+                    return AwsSessionCredentials.create(key.toString(), secret.toString(), sessionToken.toString());
                 }
-                return credentials;
             }
         }
     }
@@ -199,7 +188,7 @@ final class Ec2ClientSettings {
     // pkg private for tests
     /** Parse settings for a single client. */
     static Ec2ClientSettings getClientSettings(Settings settings) {
-        final AWSCredentials credentials = loadCredentials(settings);
+        final AwsCredentials credentials = loadCredentials(settings);
         try (
             SecureString proxyUsername = PROXY_USERNAME_SETTING.get(settings);
             SecureString proxyPassword = PROXY_PASSWORD_SETTING.get(settings)
@@ -207,13 +196,12 @@ final class Ec2ClientSettings {
             return new Ec2ClientSettings(
                 credentials,
                 ENDPOINT_SETTING.get(settings),
-                PROTOCOL_SETTING.get(settings),
                 PROXY_HOST_SETTING.get(settings),
                 PROXY_PORT_SETTING.get(settings),
                 PROXY_SCHEME_SETTING.get(settings),
                 proxyUsername.toString(),
                 proxyPassword.toString(),
-                (int) READ_TIMEOUT_SETTING.get(settings).millis()
+                Math.toIntExact(READ_TIMEOUT_SETTING.get(settings).millis())
             );
         }
     }

+ 17 - 75
plugins/discovery-ec2/src/main/java/org/elasticsearch/discovery/ec2/Ec2DiscoveryPlugin.java

@@ -9,14 +9,10 @@
 
 package org.elasticsearch.discovery.ec2;
 
-import com.amazonaws.util.EC2MetadataUtils;
-import com.amazonaws.util.json.Jackson;
-
 import org.elasticsearch.SpecialPermission;
 import org.elasticsearch.common.network.NetworkService;
 import org.elasticsearch.common.settings.Setting;
 import org.elasticsearch.common.settings.Settings;
-import org.elasticsearch.core.SuppressForbidden;
 import org.elasticsearch.discovery.SeedHostsProvider;
 import org.elasticsearch.logging.LogManager;
 import org.elasticsearch.logging.Logger;
@@ -26,23 +22,12 @@ import org.elasticsearch.plugins.Plugin;
 import org.elasticsearch.plugins.ReloadablePlugin;
 import org.elasticsearch.transport.TransportService;
 
-import java.io.BufferedReader;
 import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.UncheckedIOException;
-import java.net.URL;
-import java.net.URLConnection;
-import java.nio.charset.StandardCharsets;
-import java.security.AccessController;
-import java.security.PrivilegedAction;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
 import java.util.function.Supplier;
 
-import static org.elasticsearch.discovery.ec2.AwsEc2Utils.X_AWS_EC_2_METADATA_TOKEN;
-
 public class Ec2DiscoveryPlugin extends Plugin implements DiscoveryPlugin, ReloadablePlugin {
 
     private static final Logger logger = LogManager.getLogger(Ec2DiscoveryPlugin.class);
@@ -50,20 +35,6 @@ public class Ec2DiscoveryPlugin extends Plugin implements DiscoveryPlugin, Reloa
 
     static {
         SpecialPermission.check();
-        // Initializing Jackson requires RuntimePermission accessDeclaredMembers
-        // The ClientConfiguration class requires RuntimePermission getClassLoader
-        AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
-            try {
-                // kick jackson to do some static caching of declared members info
-                Jackson.jsonNodeOf("{}");
-                // ClientConfiguration clinit has some classloader problems
-                // TODO: fix that
-                Class.forName("com.amazonaws.ClientConfiguration");
-            } catch (final ClassNotFoundException e) {
-                throw new RuntimeException(e);
-            }
-            return null;
-        });
     }
 
     private final Settings settings;
@@ -121,55 +92,28 @@ public class Ec2DiscoveryPlugin extends Plugin implements DiscoveryPlugin, Reloa
 
     @Override
     public Settings additionalSettings() {
-        final Settings.Builder builder = Settings.builder();
-
-        // Adds a node attribute for the ec2 availability zone
-        final String azMetadataUrl = EC2MetadataUtils.getHostAddressForEC2MetadataService()
-            + "/latest/meta-data/placement/availability-zone";
-        String azMetadataTokenUrl = EC2MetadataUtils.getHostAddressForEC2MetadataService() + "/latest/api/token";
-        builder.put(getAvailabilityZoneNodeAttributes(settings, azMetadataUrl, azMetadataTokenUrl));
-        return builder.build();
+        return getAvailabilityZoneNodeAttributes(settings);
     }
 
-    // pkg private for testing
-    @SuppressForbidden(reason = "We call getInputStream in doPrivileged and provide SocketPermission")
-    static Settings getAvailabilityZoneNodeAttributes(Settings settings, String azMetadataUrl, String azMetadataTokenUrl) {
-        if (AwsEc2Service.AUTO_ATTRIBUTE_SETTING.get(settings) == false) {
-            return Settings.EMPTY;
-        }
-        final Settings.Builder attrs = Settings.builder();
-
-        final URL url;
-        final URLConnection urlConnection;
-        try {
-            url = new URL(azMetadataUrl);
-            logger.debug("obtaining ec2 [placement/availability-zone] from ec2 meta-data url {}", url);
-            urlConnection = SocketAccess.doPrivilegedIOException(url::openConnection);
-            urlConnection.setConnectTimeout(2000);
-            AwsEc2Utils.getMetadataToken(azMetadataTokenUrl)
-                .ifPresent(token -> urlConnection.setRequestProperty(X_AWS_EC_2_METADATA_TOKEN, token));
-        } catch (final IOException e) {
-            // should not happen, we know the url is not malformed, and openConnection does not actually hit network
-            throw new UncheckedIOException(e);
-        }
-
-        try (
-            InputStream in = SocketAccess.doPrivilegedIOException(urlConnection::getInputStream);
-            BufferedReader urlReader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))
-        ) {
+    private static final String IMDS_AVAILABILITY_ZONE_PATH = "/latest/meta-data/placement/availability-zone";
 
-            final String metadataResult = urlReader.readLine();
-            if ((metadataResult == null) || (metadataResult.length() == 0)) {
-                throw new IllegalStateException("no ec2 metadata returned from " + url);
-            } else {
-                attrs.put(Node.NODE_ATTRIBUTES.getKey() + "aws_availability_zone", metadataResult);
+    // pkg private for testing
+    static Settings getAvailabilityZoneNodeAttributes(Settings settings) {
+        if (AwsEc2Service.AUTO_ATTRIBUTE_SETTING.get(settings)) {
+            try {
+                return Settings.builder()
+                    .put(
+                        Node.NODE_ATTRIBUTES.getKey() + "aws_availability_zone",
+                        AwsEc2Utils.getInstanceMetadata(IMDS_AVAILABILITY_ZONE_PATH)
+                    )
+                    .build();
+            } catch (Exception e) {
+                // this is lenient so the plugin does not fail when installed outside of ec2
+                logger.error("failed to get metadata for [placement/availability-zone]", e);
             }
-        } catch (final IOException e) {
-            // this is lenient so the plugin does not fail when installed outside of ec2
-            logger.error("failed to get metadata for [placement/availability-zone]", e);
         }
 
-        return attrs.build();
+        return Settings.EMPTY;
     }
 
     @Override
@@ -179,8 +123,6 @@ public class Ec2DiscoveryPlugin extends Plugin implements DiscoveryPlugin, Reloa
 
     @Override
     public void reload(Settings settingsToLoad) {
-        // secure settings should be readable
-        final Ec2ClientSettings clientSettings = Ec2ClientSettings.getClientSettings(settingsToLoad);
-        ec2Service.refreshAndClearCache(clientSettings);
+        ec2Service.refreshAndClearCache(Ec2ClientSettings.getClientSettings(settingsToLoad));
     }
 }

+ 18 - 68
plugins/discovery-ec2/src/main/java/org/elasticsearch/discovery/ec2/Ec2NameResolver.java

@@ -9,51 +9,28 @@
 
 package org.elasticsearch.discovery.ec2;
 
-import com.amazonaws.util.EC2MetadataUtils;
-
 import org.elasticsearch.common.network.NetworkService.CustomNameResolver;
-import org.elasticsearch.core.IOUtils;
-import org.elasticsearch.core.SuppressForbidden;
-import org.elasticsearch.logging.LogManager;
-import org.elasticsearch.logging.Logger;
 
-import java.io.BufferedReader;
 import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
 import java.net.InetAddress;
-import java.net.URL;
-import java.net.URLConnection;
-import java.nio.charset.StandardCharsets;
-
-import static org.elasticsearch.discovery.ec2.AwsEc2Utils.X_AWS_EC_2_METADATA_TOKEN;
 
 /**
- * Resolves certain ec2 related 'meta' hostnames into an actual hostname
- * obtained from ec2 meta-data.
+ * Resolves certain EC2 related 'meta' hostnames into an actual hostname
+ * obtained from the EC2 instance metadata service
  * <p>
  * Valid config values for {@link Ec2HostnameType}s are -
  * <ul>
- * <li>_ec2_ - maps to privateIpv4</li>
- * <li>_ec2:privateIp_ - maps to privateIpv4</li>
- * <li>_ec2:privateIpv4_</li>
- * <li>_ec2:privateDns_</li>
- * <li>_ec2:publicIp_ - maps to publicIpv4</li>
- * <li>_ec2:publicIpv4_</li>
- * <li>_ec2:publicDns_</li>
+ * <li>{@code _ec2_} (maps to privateIpv4)</li>
+ * <li>{@code _ec2:privateIp_} (maps to privateIpv4)</li>
+ * <li>{@code _ec2:privateIpv4_}</li>
+ * <li>{@code _ec2:privateDns_}</li>
+ * <li>{@code _ec2:publicIp_} (maps to publicIpv4)</li>
+ * <li>{@code _ec2:publicIpv4_}</li>
+ * <li>{@code _ec2:publicDns_}</li>
  * </ul>
- *
- * @author Paul_Loy (keteracel)
  */
 class Ec2NameResolver implements CustomNameResolver {
 
-    private static final Logger logger = LogManager.getLogger(Ec2NameResolver.class);
-
-    /**
-     * enum that can be added to over time with more meta-data types (such as ipv6 when this is available)
-     *
-     * @author Paul_Loy
-     */
     private enum Ec2HostnameType {
 
         PRIVATE_IPv4("ec2:privateIpv4", "local-ipv4"),
@@ -75,51 +52,24 @@ class Ec2NameResolver implements CustomNameResolver {
         }
     }
 
-    /**
-     * @param type the ec2 hostname type to discover.
-     * @return the appropriate host resolved from ec2 meta-data, or null if it cannot be obtained.
-     * @see CustomNameResolver#resolveIfPossible(String)
-     */
-    @SuppressForbidden(reason = "We call getInputStream in doPrivileged and provide SocketPermission")
-    public static InetAddress[] resolve(Ec2HostnameType type) throws IOException {
-        InputStream in = null;
-        String metadataUrl = EC2MetadataUtils.getHostAddressForEC2MetadataService() + "/latest/meta-data/" + type.ec2Name;
-        String metadataTokenUrl = EC2MetadataUtils.getHostAddressForEC2MetadataService() + "/latest/api/token";
-        try {
-            URL url = new URL(metadataUrl);
-            logger.debug("obtaining ec2 hostname from ec2 meta-data url {}", url);
-            URLConnection urlConnection = SocketAccess.doPrivilegedIOException(url::openConnection);
-            urlConnection.setConnectTimeout(2000);
-            AwsEc2Utils.getMetadataToken(metadataTokenUrl)
-                .ifPresent(token -> urlConnection.setRequestProperty(X_AWS_EC_2_METADATA_TOKEN, token));
-
-            in = SocketAccess.doPrivilegedIOException(urlConnection::getInputStream);
-            BufferedReader urlReader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
-
-            String metadataResult = urlReader.readLine();
-            if (metadataResult == null || metadataResult.length() == 0) {
-                throw new IOException("no gce metadata returned from [" + url + "] for [" + type.configName + "]");
-            }
-            // only one address: because we explicitly ask for only one via the Ec2HostnameType
-            return new InetAddress[] { InetAddress.getByName(metadataResult) };
-        } catch (IOException e) {
-            throw new IOException("IOException caught when fetching InetAddress from [" + metadataUrl + "]", e);
-        } finally {
-            IOUtils.closeWhileHandlingException(in);
-        }
-    }
-
     @Override
     public InetAddress[] resolveDefault() {
         return null; // using this, one has to explicitly specify _ec2_ in network setting
-        // return resolve(Ec2HostnameType.DEFAULT, false);
     }
 
+    private static final String IMDS_ADDRESS_PATH_PREFIX = "/latest/meta-data/";
+
     @Override
     public InetAddress[] resolveIfPossible(String value) throws IOException {
         for (Ec2HostnameType type : Ec2HostnameType.values()) {
             if (type.configName.equals(value)) {
-                return resolve(type);
+                final var metadataPath = IMDS_ADDRESS_PATH_PREFIX + type.ec2Name;
+                try {
+                    // only one address: IMDS returns just one address/name, and if it's a name then it should resolve to one address
+                    return new InetAddress[] { InetAddress.getByName(AwsEc2Utils.getInstanceMetadata(metadataPath)) };
+                } catch (Exception e) {
+                    throw new IOException("Exception caught when resolving EC2 address from [" + metadataPath + "]", e);
+                }
             }
         }
         return null;

+ 25 - 0
plugins/discovery-ec2/src/main/java/org/elasticsearch/discovery/ec2/HttpScheme.java

@@ -0,0 +1,25 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the "Elastic License
+ * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+package org.elasticsearch.discovery.ec2;
+
+public enum HttpScheme {
+    HTTP("http"),
+    HTTPS("https");
+
+    private final String schemeString;
+
+    HttpScheme(String schemeString) {
+        this.schemeString = schemeString;
+    }
+
+    public String getSchemeString() {
+        return schemeString;
+    }
+}

+ 1 - 0
plugins/discovery-ec2/src/main/plugin-metadata/plugin-security.policy

@@ -19,4 +19,5 @@ grant {
   permission java.net.SocketPermission "*", "connect";
 
   permission java.lang.reflect.ReflectPermission "suppressAccessChecks";
+  permission java.util.PropertyPermission "http.proxyHost", "read";
 };

+ 12 - 11
plugins/discovery-ec2/src/test/java/org/elasticsearch/discovery/ec2/AbstractEC2MockAPITestCase.java

@@ -8,8 +8,9 @@
  */
 package org.elasticsearch.discovery.ec2;
 
-import com.amazonaws.services.ec2.model.Instance;
-import com.amazonaws.services.ec2.model.Tag;
+import software.amazon.awssdk.services.ec2.model.Instance;
+import software.amazon.awssdk.services.ec2.model.Tag;
+
 import com.sun.net.httpserver.HttpServer;
 
 import org.elasticsearch.common.network.InetAddresses;
@@ -115,11 +116,11 @@ public abstract class AbstractEC2MockAPITestCase extends ESTestCase {
                                 sw.writeStartElement("item");
                                 {
                                     sw.writeStartElement("instanceId");
-                                    sw.writeCharacters(instance.getInstanceId());
+                                    sw.writeCharacters(instance.instanceId());
                                     sw.writeEndElement();
 
                                     sw.writeStartElement("imageId");
-                                    sw.writeCharacters(instance.getImageId());
+                                    sw.writeCharacters(instance.imageId());
                                     sw.writeEndElement();
 
                                     sw.writeStartElement("instanceState");
@@ -135,11 +136,11 @@ public abstract class AbstractEC2MockAPITestCase extends ESTestCase {
                                     sw.writeEndElement();
 
                                     sw.writeStartElement("privateDnsName");
-                                    sw.writeCharacters(instance.getPrivateDnsName());
+                                    sw.writeCharacters(instance.privateDnsName());
                                     sw.writeEndElement();
 
                                     sw.writeStartElement("dnsName");
-                                    sw.writeCharacters(instance.getPublicDnsName());
+                                    sw.writeCharacters(instance.publicDnsName());
                                     sw.writeEndElement();
 
                                     sw.writeStartElement("instanceType");
@@ -161,23 +162,23 @@ public abstract class AbstractEC2MockAPITestCase extends ESTestCase {
                                     sw.writeEndElement();
 
                                     sw.writeStartElement("privateIpAddress");
-                                    sw.writeCharacters(instance.getPrivateIpAddress());
+                                    sw.writeCharacters(instance.privateIpAddress());
                                     sw.writeEndElement();
 
                                     sw.writeStartElement("ipAddress");
-                                    sw.writeCharacters(instance.getPublicIpAddress());
+                                    sw.writeCharacters(instance.publicIpAddress());
                                     sw.writeEndElement();
 
                                     sw.writeStartElement("tagSet");
-                                    for (Tag tag : instance.getTags()) {
+                                    for (Tag tag : instance.tags()) {
                                         sw.writeStartElement("item");
                                         {
                                             sw.writeStartElement("key");
-                                            sw.writeCharacters(tag.getKey());
+                                            sw.writeCharacters(tag.key());
                                             sw.writeEndElement();
 
                                             sw.writeStartElement("value");
-                                            sw.writeCharacters(tag.getValue());
+                                            sw.writeCharacters(tag.value());
                                             sw.writeEndElement();
                                         }
                                         sw.writeEndElement();

+ 0 - 181
plugins/discovery-ec2/src/test/java/org/elasticsearch/discovery/ec2/AwsEc2ServiceImplTests.java

@@ -1,181 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the "Elastic License
- * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
- * Public License v 1"; you may not use this file except in compliance with, at
- * your election, the "Elastic License 2.0", the "GNU Affero General Public
- * License v3.0 only", or the "Server Side Public License, v 1".
- */
-
-package org.elasticsearch.discovery.ec2;
-
-import com.amazonaws.ClientConfiguration;
-import com.amazonaws.Protocol;
-import com.amazonaws.auth.AWSCredentials;
-import com.amazonaws.auth.AWSCredentialsProvider;
-import com.amazonaws.auth.BasicSessionCredentials;
-import com.amazonaws.auth.DefaultAWSCredentialsProviderChain;
-
-import org.apache.logging.log4j.Level;
-import org.elasticsearch.common.settings.MockSecureSettings;
-import org.elasticsearch.common.settings.Setting;
-import org.elasticsearch.common.settings.Settings;
-import org.elasticsearch.common.settings.SettingsException;
-import org.elasticsearch.logging.LogManager;
-import org.elasticsearch.logging.Logger;
-import org.elasticsearch.test.ESTestCase;
-
-import static org.hamcrest.Matchers.instanceOf;
-import static org.hamcrest.Matchers.is;
-
-public class AwsEc2ServiceImplTests extends ESTestCase {
-
-    // we need our own ES logger, rather than log4j logging
-    private static final Logger logger = LogManager.getLogger(AwsEc2ServiceImplTests.class);
-
-    public void testAWSCredentialsWithSystemProviders() {
-        final AWSCredentialsProvider credentialsProvider = AwsEc2ServiceImpl.buildCredentials(
-            logger,
-            Ec2ClientSettings.getClientSettings(Settings.EMPTY)
-        );
-        assertThat(credentialsProvider, instanceOf(DefaultAWSCredentialsProviderChain.class));
-    }
-
-    public void testAWSCredentialsWithElasticsearchAwsSettings() {
-        final MockSecureSettings secureSettings = new MockSecureSettings();
-        secureSettings.setString("discovery.ec2.access_key", "aws_key");
-        secureSettings.setString("discovery.ec2.secret_key", "aws_secret");
-        final AWSCredentials credentials = AwsEc2ServiceImpl.buildCredentials(
-            logger,
-            Ec2ClientSettings.getClientSettings(Settings.builder().setSecureSettings(secureSettings).build())
-        ).getCredentials();
-        assertThat(credentials.getAWSAccessKeyId(), is("aws_key"));
-        assertThat(credentials.getAWSSecretKey(), is("aws_secret"));
-    }
-
-    public void testAWSSessionCredentialsWithElasticsearchAwsSettings() {
-        final MockSecureSettings secureSettings = new MockSecureSettings();
-        secureSettings.setString("discovery.ec2.access_key", "aws_key");
-        secureSettings.setString("discovery.ec2.secret_key", "aws_secret");
-        secureSettings.setString("discovery.ec2.session_token", "aws_session_token");
-        final BasicSessionCredentials credentials = (BasicSessionCredentials) AwsEc2ServiceImpl.buildCredentials(
-            logger,
-            Ec2ClientSettings.getClientSettings(Settings.builder().setSecureSettings(secureSettings).build())
-        ).getCredentials();
-        assertThat(credentials.getAWSAccessKeyId(), is("aws_key"));
-        assertThat(credentials.getAWSSecretKey(), is("aws_secret"));
-        assertThat(credentials.getSessionToken(), is("aws_session_token"));
-    }
-
-    public void testDeprecationOfLoneAccessKey() {
-        final MockSecureSettings secureSettings = new MockSecureSettings();
-        secureSettings.setString("discovery.ec2.access_key", "aws_key");
-        final AWSCredentials credentials = AwsEc2ServiceImpl.buildCredentials(
-            logger,
-            Ec2ClientSettings.getClientSettings(Settings.builder().setSecureSettings(secureSettings).build())
-        ).getCredentials();
-        assertThat(credentials.getAWSAccessKeyId(), is("aws_key"));
-        assertThat(credentials.getAWSSecretKey(), is(""));
-        assertSettingDeprecationsAndWarnings(
-            new Setting<?>[] {},
-            new DeprecationWarning(
-                Level.WARN,
-                "Setting [discovery.ec2.access_key] is set but " + "[discovery.ec2.secret_key] is not, which will be unsupported in future"
-            )
-        );
-    }
-
-    public void testDeprecationOfLoneSecretKey() {
-        final MockSecureSettings secureSettings = new MockSecureSettings();
-        secureSettings.setString("discovery.ec2.secret_key", "aws_secret");
-        final AWSCredentials credentials = AwsEc2ServiceImpl.buildCredentials(
-            logger,
-            Ec2ClientSettings.getClientSettings(Settings.builder().setSecureSettings(secureSettings).build())
-        ).getCredentials();
-        assertThat(credentials.getAWSAccessKeyId(), is(""));
-        assertThat(credentials.getAWSSecretKey(), is("aws_secret"));
-        assertSettingDeprecationsAndWarnings(
-            new Setting<?>[] {},
-            new DeprecationWarning(
-                Level.WARN,
-                "Setting [discovery.ec2.secret_key] is set but " + "[discovery.ec2.access_key] is not, which will be unsupported in future"
-            )
-        );
-    }
-
-    public void testRejectionOfLoneSessionToken() {
-        final MockSecureSettings secureSettings = new MockSecureSettings();
-        secureSettings.setString("discovery.ec2.session_token", "aws_session_token");
-        SettingsException e = expectThrows(
-            SettingsException.class,
-            () -> AwsEc2ServiceImpl.buildCredentials(
-                logger,
-                Ec2ClientSettings.getClientSettings(Settings.builder().setSecureSettings(secureSettings).build())
-            )
-        );
-        assertThat(
-            e.getMessage(),
-            is("Setting [discovery.ec2.session_token] is set but [discovery.ec2.access_key] and [discovery.ec2.secret_key] are not")
-        );
-    }
-
-    public void testAWSDefaultConfiguration() {
-        launchAWSConfigurationTest(
-            Settings.EMPTY,
-            Protocol.HTTPS,
-            null,
-            -1,
-            Protocol.HTTP,
-            null,
-            null,
-            ClientConfiguration.DEFAULT_SOCKET_TIMEOUT
-        );
-    }
-
-    public void testAWSConfigurationWithAwsSettings() {
-        final MockSecureSettings secureSettings = new MockSecureSettings();
-        secureSettings.setString("discovery.ec2.proxy.username", "aws_proxy_username");
-        secureSettings.setString("discovery.ec2.proxy.password", "aws_proxy_password");
-        final Settings settings = Settings.builder()
-            .put("discovery.ec2.protocol", "http")
-            .put("discovery.ec2.proxy.host", "aws_proxy_host")
-            .put("discovery.ec2.proxy.port", 8080)
-            .put("discovery.ec2.proxy.scheme", "http")
-            .put("discovery.ec2.read_timeout", "10s")
-            .setSecureSettings(secureSettings)
-            .build();
-        launchAWSConfigurationTest(
-            settings,
-            Protocol.HTTP,
-            "aws_proxy_host",
-            8080,
-            Protocol.HTTP,
-            "aws_proxy_username",
-            "aws_proxy_password",
-            10000
-        );
-    }
-
-    protected void launchAWSConfigurationTest(
-        Settings settings,
-        Protocol expectedProtocol,
-        String expectedProxyHost,
-        int expectedProxyPort,
-        Protocol expectedProxyScheme,
-        String expectedProxyUsername,
-        String expectedProxyPassword,
-        int expectedReadTimeout
-    ) {
-        final ClientConfiguration configuration = AwsEc2ServiceImpl.buildConfiguration(Ec2ClientSettings.getClientSettings(settings));
-
-        assertThat(configuration.getResponseMetadataCacheSize(), is(0));
-        assertThat(configuration.getProtocol(), is(expectedProtocol));
-        assertThat(configuration.getProxyHost(), is(expectedProxyHost));
-        assertThat(configuration.getProxyPort(), is(expectedProxyPort));
-        assertThat(configuration.getProxyProtocol(), is(expectedProxyScheme));
-        assertThat(configuration.getProxyUsername(), is(expectedProxyUsername));
-        assertThat(configuration.getProxyPassword(), is(expectedProxyPassword));
-        assertThat(configuration.getSocketTimeout(), is(expectedReadTimeout));
-    }
-
-}

+ 6 - 4
plugins/discovery-ec2/src/test/java/org/elasticsearch/discovery/ec2/EC2RetriesTests.java

@@ -9,8 +9,8 @@
 
 package org.elasticsearch.discovery.ec2;
 
-import com.amazonaws.http.HttpMethodName;
-import com.amazonaws.services.ec2.model.Instance;
+import software.amazon.awssdk.http.SdkHttpMethod;
+import software.amazon.awssdk.services.ec2.model.Instance;
 
 import org.apache.http.HttpStatus;
 import org.apache.http.NameValuePair;
@@ -72,7 +72,7 @@ public class EC2RetriesTests extends AbstractEC2MockAPITestCase {
         // retry the same request 5 times at most
         final int maxRetries = randomIntBetween(1, 5);
         httpServer.createContext("/", exchange -> {
-            if (exchange.getRequestMethod().equals(HttpMethodName.POST.name())) {
+            if (SdkHttpMethod.POST.name().equals(exchange.getRequestMethod())) {
                 final String request = new String(exchange.getRequestBody().readAllBytes(), UTF_8);
                 final String userAgent = exchange.getRequestHeaders().getFirst("User-Agent");
                 if (userAgent != null && userAgent.startsWith("aws-sdk-java")) {
@@ -92,7 +92,9 @@ public class EC2RetriesTests extends AbstractEC2MockAPITestCase {
                     for (NameValuePair parse : URLEncodedUtils.parse(request, UTF_8)) {
                         if ("Action".equals(parse.getName())) {
                             responseBody = generateDescribeInstancesResponse(
-                                hosts.stream().map(address -> new Instance().withPublicIpAddress(address)).collect(Collectors.toList())
+                                hosts.stream()
+                                    .map(address -> Instance.builder().publicIpAddress(address).build())
+                                    .collect(Collectors.toList())
                             );
                             break;
                         }

+ 283 - 260
plugins/discovery-ec2/src/test/java/org/elasticsearch/discovery/ec2/Ec2DiscoveryPluginTests.java

@@ -9,318 +9,341 @@
 
 package org.elasticsearch.discovery.ec2;
 
-import com.amazonaws.ClientConfiguration;
-import com.amazonaws.Protocol;
-import com.amazonaws.auth.AWSCredentials;
-import com.amazonaws.auth.AWSCredentialsProvider;
-import com.amazonaws.auth.BasicAWSCredentials;
-import com.amazonaws.auth.BasicSessionCredentials;
-import com.amazonaws.services.ec2.AbstractAmazonEC2;
-import com.amazonaws.services.ec2.AmazonEC2;
-import com.sun.net.httpserver.HttpExchange;
-import com.sun.net.httpserver.HttpHandler;
-import com.sun.net.httpserver.HttpServer;
+import fixture.aws.imds.Ec2ImdsHttpFixture;
+import fixture.aws.imds.Ec2ImdsServiceBuilder;
+import fixture.aws.imds.Ec2ImdsVersion;
+import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
+import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
+import software.amazon.awssdk.auth.credentials.AwsSessionCredentials;
+import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
+import software.amazon.awssdk.http.apache.ApacheHttpClient;
+import software.amazon.awssdk.http.apache.ProxyConfiguration;
+import software.amazon.awssdk.services.ec2.Ec2Client;
+import software.amazon.awssdk.services.ec2.Ec2ClientBuilder;
+import software.amazon.awssdk.services.ec2.endpoints.Ec2EndpointParams;
+import software.amazon.awssdk.services.ec2.endpoints.Ec2EndpointProvider;
 
 import org.elasticsearch.common.settings.MockSecureSettings;
 import org.elasticsearch.common.settings.Settings;
-import org.elasticsearch.core.SuppressForbidden;
-import org.elasticsearch.mocksocket.MockHttpServer;
+import org.elasticsearch.common.settings.SettingsException;
+import org.elasticsearch.core.CheckedConsumer;
+import org.elasticsearch.core.TimeValue;
 import org.elasticsearch.node.Node;
 import org.elasticsearch.test.ESTestCase;
-
-import java.io.IOException;
-import java.io.UncheckedIOException;
-import java.net.InetAddress;
-import java.net.InetSocketAddress;
-import java.nio.charset.StandardCharsets;
-
+import org.mockito.ArgumentCaptor;
+
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.function.Consumer;
+
+import static org.elasticsearch.discovery.ec2.Ec2ClientSettings.ACCESS_KEY_SETTING;
+import static org.elasticsearch.discovery.ec2.Ec2ClientSettings.PROXY_HOST_SETTING;
+import static org.elasticsearch.discovery.ec2.Ec2ClientSettings.PROXY_PASSWORD_SETTING;
+import static org.elasticsearch.discovery.ec2.Ec2ClientSettings.PROXY_PORT_SETTING;
+import static org.elasticsearch.discovery.ec2.Ec2ClientSettings.PROXY_SCHEME_SETTING;
+import static org.elasticsearch.discovery.ec2.Ec2ClientSettings.PROXY_USERNAME_SETTING;
+import static org.elasticsearch.discovery.ec2.Ec2ClientSettings.READ_TIMEOUT_SETTING;
+import static org.elasticsearch.discovery.ec2.Ec2ClientSettings.SECRET_KEY_SETTING;
+import static org.elasticsearch.discovery.ec2.Ec2ClientSettings.SESSION_TOKEN_SETTING;
 import static org.hamcrest.Matchers.instanceOf;
-import static org.hamcrest.Matchers.is;
-
-@SuppressForbidden(reason = "Uses an HttpServer to emulate the Instance Metadata Service")
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Mostly just testing that the various plugin settings (see reference docs) result in appropriate calls on the client builder, using mocks.
+ */
 public class Ec2DiscoveryPluginTests extends ESTestCase {
 
-    private Settings getNodeAttributes(Settings settings, String url, String tokenUrl) {
-        final Settings realSettings = Settings.builder().put(AwsEc2Service.AUTO_ATTRIBUTE_SETTING.getKey(), true).put(settings).build();
-        return Ec2DiscoveryPlugin.getAvailabilityZoneNodeAttributes(realSettings, url, tokenUrl);
-    }
-
-    private void assertNodeAttributes(Settings settings, String url, String tokenUrl, String expected) {
-        final Settings additional = getNodeAttributes(settings, url, tokenUrl);
-        if (expected == null) {
-            assertTrue(additional.isEmpty());
-        } else {
-            assertEquals(expected, additional.get(Node.NODE_ATTRIBUTES.getKey() + "aws_availability_zone"));
-        }
+    public void testNodeAttributesDisabledByDefault() {
+        assertTrue(Ec2DiscoveryPlugin.getAvailabilityZoneNodeAttributes(Settings.EMPTY).isEmpty());
     }
 
     public void testNodeAttributesDisabled() {
-        final Settings settings = Settings.builder().put(AwsEc2Service.AUTO_ATTRIBUTE_SETTING.getKey(), false).build();
-        assertNodeAttributes(settings, "bogus", "", null);
+        assertTrue(
+            Ec2DiscoveryPlugin.getAvailabilityZoneNodeAttributes(
+                Settings.builder().put(AwsEc2Service.AUTO_ATTRIBUTE_SETTING.getKey(), false).build()
+            ).isEmpty()
+        );
     }
 
-    public void testNodeAttributes() throws Exception {
-        try (var metadataServer = metadataServerWithoutToken()) {
-            assertNodeAttributes(Settings.EMPTY, metadataServer.metadataUri(), "", "us-east-1c");
-        }
+    public void testNodeAttributesEnabled() {
+        final var availabilityZone = randomIdentifier();
+        Ec2ImdsHttpFixture.runWithFixture(
+            new Ec2ImdsServiceBuilder(Ec2ImdsVersion.V2).availabilityZoneSupplier(() -> availabilityZone),
+            ec2ImdsHttpFixture -> {
+                try (var ignored = Ec2ImdsHttpFixture.withEc2MetadataServiceEndpointOverride(ec2ImdsHttpFixture.getAddress())) {
+                    final var availabilityZoneNodeAttributeSettings = Ec2DiscoveryPlugin.getAvailabilityZoneNodeAttributes(
+                        Settings.builder().put(AwsEc2Service.AUTO_ATTRIBUTE_SETTING.getKey(), true).build()
+                    );
+                    assertEquals(
+                        availabilityZone,
+                        availabilityZoneNodeAttributeSettings.get(Node.NODE_ATTRIBUTES.getKey() + "aws_availability_zone")
+                    );
+                }
+            }
+        );
     }
 
-    public void testNodeAttributesBogusUrl() {
-        final UncheckedIOException e = expectThrows(UncheckedIOException.class, () -> getNodeAttributes(Settings.EMPTY, "bogus", ""));
-        assertNotNull(e.getCause());
-        final String msg = e.getCause().getMessage();
-        assertTrue(msg, msg.contains("no protocol: bogus"));
+    public void testDefaultEndpoint() {
+        // Ec2ClientSettings#ENDPOINT_SETTING is not set, so the builder method shouldn't be called
+        runPluginMockTest(Settings.builder(), plugin -> verify(plugin.ec2ClientBuilder, never()).endpointProvider(any()));
     }
 
-    public void testNodeAttributesEmpty() throws Exception {
-        try (MetadataServer metadataServer = new MetadataServer("/metadata", exchange -> {
-            exchange.sendResponseHeaders(200, -1);
-            exchange.close();
-        })) {
-            final IllegalStateException e = expectThrows(
-                IllegalStateException.class,
-                () -> getNodeAttributes(Settings.EMPTY, metadataServer.metadataUri(), "")
-            );
-            assertTrue(e.getMessage(), e.getMessage().contains("no ec2 metadata returned"));
-        }
+    public void testSpecificEndpoint() {
+        final var argumentCaptor = ArgumentCaptor.forClass(Ec2EndpointProvider.class);
+        final var endpoint = randomIdentifier() + ".local";
+        runPluginMockTest(
+            Settings.builder().put(Ec2ClientSettings.ENDPOINT_SETTING.getKey(), endpoint),
+            plugin -> verify(plugin.ec2ClientBuilder, times(1)).endpointProvider(argumentCaptor.capture())
+        );
+        assertEquals(endpoint, safeGet(argumentCaptor.getValue().resolveEndpoint(Ec2EndpointParams.builder().build())).url().toString());
     }
 
-    public void testNodeAttributesErrorLenient() throws Exception {
-        try (var metadataServer = new MetadataServer("/metadata", exchange -> {
-            exchange.sendResponseHeaders(404, -1);
-            exchange.close();
-        })) {
-            assertNodeAttributes(Settings.EMPTY, metadataServer.metadataUri(), "", null);
-        }
+    public void testDefaultHttpSocketTimeout() {
+        final var argumentCaptor = ArgumentCaptor.forClass(Duration.class);
+        runPluginMockTest(Settings.builder(), plugin -> verify(plugin.httpClientBuilder, times(1)).socketTimeout(argumentCaptor.capture()));
+        assertEquals(READ_TIMEOUT_SETTING.get(Settings.EMPTY).nanos(), argumentCaptor.getValue().toNanos());
     }
 
-    public void testNodeAttributesWithToken() throws Exception {
-        try (var metadataServer = new MetadataServer("/metadata", exchange -> {
-            assertEquals("imdsv2-token", exchange.getRequestHeaders().getFirst("X-aws-ec2-metadata-token"));
-            exchange.sendResponseHeaders(200, 0);
-            exchange.getResponseBody().write("us-east-1c".getBytes(StandardCharsets.UTF_8));
-            exchange.close();
-        }, "/latest/api/token", exchange -> {
-            assertEquals("PUT", exchange.getRequestMethod());
-            assertEquals("10", exchange.getRequestHeaders().getFirst("X-aws-ec2-metadata-token-ttl-seconds"));
-            exchange.sendResponseHeaders(200, 0);
-            exchange.getResponseBody().write("imdsv2-token".getBytes(StandardCharsets.UTF_8));
-            exchange.close();
-        })) {
-            assertNodeAttributes(Settings.EMPTY, metadataServer.metadataUri(), metadataServer.tokenUri(), "us-east-1c");
-        }
+    public void testSpecificHttpSocketTimeout() {
+        final var argumentCaptor = ArgumentCaptor.forClass(Duration.class);
+        final var timeoutValue = TimeValue.timeValueMillis(between(0, 100000));
+        runPluginMockTest(
+            Settings.builder().put(READ_TIMEOUT_SETTING.getKey(), timeoutValue),
+            plugin -> verify(plugin.httpClientBuilder, times(1)).socketTimeout(argumentCaptor.capture())
+        );
+        assertEquals(timeoutValue.nanos(), argumentCaptor.getValue().toNanos());
     }
 
-    public void testTokenMetadataApiIsMisbehaving() throws Exception {
-        try (var metadataServer = new MetadataServer("/metadata", exchange -> {
-            assertNull(exchange.getRequestHeaders().getFirst("X-aws-ec2-metadata-token"));
-            exchange.sendResponseHeaders(200, 0);
-            exchange.getResponseBody().write("us-east-1c".getBytes(StandardCharsets.UTF_8));
-            exchange.close();
-        }, "/latest/api/token", HttpExchange::close)) {
-            assertNodeAttributes(Settings.EMPTY, metadataServer.metadataUri(), metadataServer.tokenUri(), "us-east-1c");
-        }
+    public void testDefaultProxyConfiguration() {
+        runPluginMockTest(Settings.builder(), plugin -> verify(plugin.httpClientBuilder, never()).proxyConfiguration(any()));
     }
 
-    public void testTokenMetadataApiDoesNotRespond() throws Exception {
-        try (var metadataServer = new MetadataServer("/metadata", exchange -> {
-            assertNull(exchange.getRequestHeaders().getFirst("X-aws-ec2-metadata-token"));
-            exchange.sendResponseHeaders(200, 0);
-            exchange.getResponseBody().write("us-east-1c".getBytes(StandardCharsets.UTF_8));
-            exchange.close();
-        }, "/latest/api/token", ex -> {
-            // Intentionally don't close the connection, so the client has to time out
-        })) {
-            assertNodeAttributes(Settings.EMPTY, metadataServer.metadataUri(), metadataServer.tokenUri(), "us-east-1c");
-        }
-    }
+    public void testSpecificProxyConfiguration() {
+        // generates a random proxy configuration (i.e. randomly setting/omitting all the settings) and verifies that the resulting
+        // ProxyConfiguration is as expected with a sequence of assertions that match the configuration we generated
 
-    public void testTokenMetadataApiIsNotAvailable() throws Exception {
-        try (var metadataServer = metadataServerWithoutToken()) {
-            assertNodeAttributes(Settings.EMPTY, metadataServer.metadataUri(), metadataServer.tokenUri(), "us-east-1c");
-        }
-    }
+        final var argumentCaptor = ArgumentCaptor.forClass(ProxyConfiguration.class);
 
-    public void testBogusTokenMetadataUrl() throws Exception {
-        try (var metadataServer = metadataServerWithoutToken();) {
-            assertNodeAttributes(Settings.EMPTY, metadataServer.metadataUri(), "bogus", "us-east-1c");
-        }
-    }
+        final var proxySettings = Settings.builder();
+        final var assertions = new ArrayList<Consumer<ProxyConfiguration>>();
 
-    public void testDefaultEndpoint() throws IOException {
-        try (Ec2DiscoveryPluginMock plugin = new Ec2DiscoveryPluginMock(Settings.EMPTY)) {
-            final String endpoint = ((AmazonEC2Mock) plugin.ec2Service.client().client()).endpoint;
-            assertThat(endpoint, is(""));
-        }
-    }
+        final var proxyHost = "proxy." + randomIdentifier() + ".host";
+        proxySettings.put(PROXY_HOST_SETTING.getKey(), proxyHost);
+        assertions.add(proxyConfiguration -> assertEquals(proxyHost, proxyConfiguration.host()));
 
-    public void testSpecificEndpoint() throws IOException {
-        final Settings settings = Settings.builder().put(Ec2ClientSettings.ENDPOINT_SETTING.getKey(), "ec2.endpoint").build();
-        try (Ec2DiscoveryPluginMock plugin = new Ec2DiscoveryPluginMock(settings)) {
-            final String endpoint = ((AmazonEC2Mock) plugin.ec2Service.client().client()).endpoint;
-            assertThat(endpoint, is("ec2.endpoint"));
+        // randomly set, or not, the port
+        if (randomBoolean()) {
+            final var proxyPort = between(1, 65535);
+            proxySettings.put(PROXY_PORT_SETTING.getKey(), proxyPort);
+            assertions.add(proxyConfiguration -> assertEquals(proxyPort, proxyConfiguration.port()));
+        } else {
+            assertions.add(proxyConfiguration -> assertEquals((int) PROXY_PORT_SETTING.get(Settings.EMPTY), proxyConfiguration.port()));
         }
-    }
 
-    public void testClientSettingsReInit() throws IOException {
-        final MockSecureSettings mockSecure1 = new MockSecureSettings();
-        mockSecure1.setString(Ec2ClientSettings.ACCESS_KEY_SETTING.getKey(), "ec2_access_1");
-        mockSecure1.setString(Ec2ClientSettings.SECRET_KEY_SETTING.getKey(), "ec2_secret_key_1");
-        final boolean mockSecure1HasSessionToken = randomBoolean();
-        if (mockSecure1HasSessionToken) {
-            mockSecure1.setString(Ec2ClientSettings.SESSION_TOKEN_SETTING.getKey(), "ec2_session_token_1");
-        }
-        mockSecure1.setString(Ec2ClientSettings.PROXY_USERNAME_SETTING.getKey(), "proxy_username_1");
-        mockSecure1.setString(Ec2ClientSettings.PROXY_PASSWORD_SETTING.getKey(), "proxy_password_1");
-        final Settings settings1 = Settings.builder()
-            .put(Ec2ClientSettings.PROXY_HOST_SETTING.getKey(), "proxy_host_1")
-            .put(Ec2ClientSettings.PROXY_PORT_SETTING.getKey(), 881)
-            .put(Ec2ClientSettings.PROXY_SCHEME_SETTING.getKey(), "http")
-            .put(Ec2ClientSettings.ENDPOINT_SETTING.getKey(), "ec2_endpoint_1")
-            .setSecureSettings(mockSecure1)
-            .build();
-        final MockSecureSettings mockSecure2 = new MockSecureSettings();
-        mockSecure2.setString(Ec2ClientSettings.ACCESS_KEY_SETTING.getKey(), "ec2_access_2");
-        mockSecure2.setString(Ec2ClientSettings.SECRET_KEY_SETTING.getKey(), "ec2_secret_key_2");
-        final boolean mockSecure2HasSessionToken = randomBoolean();
-        if (mockSecure2HasSessionToken) {
-            mockSecure2.setString(Ec2ClientSettings.SESSION_TOKEN_SETTING.getKey(), "ec2_session_token_2");
+        // randomly set, or not, the scheme
+        if (randomBoolean()) {
+            final var proxyScheme = randomFrom("http", "https");
+            proxySettings.put(PROXY_SCHEME_SETTING.getKey(), proxyScheme);
+            assertions.add(proxyConfiguration -> assertEquals(proxyScheme, proxyConfiguration.scheme()));
+        } else {
+            assertions.add(
+                proxyConfiguration -> assertEquals(PROXY_SCHEME_SETTING.get(Settings.EMPTY).getSchemeString(), proxyConfiguration.scheme())
+            );
         }
-        mockSecure2.setString(Ec2ClientSettings.PROXY_USERNAME_SETTING.getKey(), "proxy_username_2");
-        mockSecure2.setString(Ec2ClientSettings.PROXY_PASSWORD_SETTING.getKey(), "proxy_password_2");
-        final Settings settings2 = Settings.builder()
-            .put(Ec2ClientSettings.PROXY_HOST_SETTING.getKey(), "proxy_host_2")
-            .put(Ec2ClientSettings.PROXY_PORT_SETTING.getKey(), 882)
-            .put(Ec2ClientSettings.PROXY_SCHEME_SETTING.getKey(), "http")
-            .put(Ec2ClientSettings.ENDPOINT_SETTING.getKey(), "ec2_endpoint_2")
-            .setSecureSettings(mockSecure2)
-            .build();
-        try (Ec2DiscoveryPluginMock plugin = new Ec2DiscoveryPluginMock(settings1)) {
-            try (AmazonEc2Reference clientReference = plugin.ec2Service.client()) {
-                {
-                    final AWSCredentials credentials = ((AmazonEC2Mock) clientReference.client()).credentials.getCredentials();
-                    assertThat(credentials.getAWSAccessKeyId(), is("ec2_access_1"));
-                    assertThat(credentials.getAWSSecretKey(), is("ec2_secret_key_1"));
-                    if (mockSecure1HasSessionToken) {
-                        assertThat(credentials, instanceOf(BasicSessionCredentials.class));
-                        assertThat(((BasicSessionCredentials) credentials).getSessionToken(), is("ec2_session_token_1"));
-                    } else {
-                        assertThat(credentials, instanceOf(BasicAWSCredentials.class));
-                    }
-                    assertThat(((AmazonEC2Mock) clientReference.client()).configuration.getProxyUsername(), is("proxy_username_1"));
-                    assertThat(((AmazonEC2Mock) clientReference.client()).configuration.getProxyPassword(), is("proxy_password_1"));
-                    assertThat(((AmazonEC2Mock) clientReference.client()).configuration.getProxyHost(), is("proxy_host_1"));
-                    assertThat(((AmazonEC2Mock) clientReference.client()).configuration.getProxyPort(), is(881));
-                    assertThat(((AmazonEC2Mock) clientReference.client()).configuration.getProxyProtocol(), is(Protocol.HTTP));
-                    assertThat(((AmazonEC2Mock) clientReference.client()).endpoint, is("ec2_endpoint_1"));
-                }
-                // reload secure settings2
-                plugin.reload(settings2);
-                // client is not released, it is still using the old settings
-                {
-                    final AWSCredentials credentials = ((AmazonEC2Mock) clientReference.client()).credentials.getCredentials();
-                    if (mockSecure1HasSessionToken) {
-                        assertThat(credentials, instanceOf(BasicSessionCredentials.class));
-                        assertThat(((BasicSessionCredentials) credentials).getSessionToken(), is("ec2_session_token_1"));
-                    } else {
-                        assertThat(credentials, instanceOf(BasicAWSCredentials.class));
-                    }
-                    assertThat(((AmazonEC2Mock) clientReference.client()).configuration.getProxyUsername(), is("proxy_username_1"));
-                    assertThat(((AmazonEC2Mock) clientReference.client()).configuration.getProxyPassword(), is("proxy_password_1"));
-                    assertThat(((AmazonEC2Mock) clientReference.client()).configuration.getProxyHost(), is("proxy_host_1"));
-                    assertThat(((AmazonEC2Mock) clientReference.client()).configuration.getProxyPort(), is(881));
-                    assertThat(((AmazonEC2Mock) clientReference.client()).configuration.getProxyProtocol(), is(Protocol.HTTP));
-                    assertThat(((AmazonEC2Mock) clientReference.client()).endpoint, is("ec2_endpoint_1"));
-                }
-            }
-            try (AmazonEc2Reference clientReference = plugin.ec2Service.client()) {
-                final AWSCredentials credentials = ((AmazonEC2Mock) clientReference.client()).credentials.getCredentials();
-                assertThat(credentials.getAWSAccessKeyId(), is("ec2_access_2"));
-                assertThat(credentials.getAWSSecretKey(), is("ec2_secret_key_2"));
-                if (mockSecure2HasSessionToken) {
-                    assertThat(credentials, instanceOf(BasicSessionCredentials.class));
-                    assertThat(((BasicSessionCredentials) credentials).getSessionToken(), is("ec2_session_token_2"));
-                } else {
-                    assertThat(credentials, instanceOf(BasicAWSCredentials.class));
-                }
-                assertThat(((AmazonEC2Mock) clientReference.client()).configuration.getProxyUsername(), is("proxy_username_2"));
-                assertThat(((AmazonEC2Mock) clientReference.client()).configuration.getProxyPassword(), is("proxy_password_2"));
-                assertThat(((AmazonEC2Mock) clientReference.client()).configuration.getProxyHost(), is("proxy_host_2"));
-                assertThat(((AmazonEC2Mock) clientReference.client()).configuration.getProxyPort(), is(882));
-                assertThat(((AmazonEC2Mock) clientReference.client()).configuration.getProxyProtocol(), is(Protocol.HTTP));
-                assertThat(((AmazonEC2Mock) clientReference.client()).endpoint, is("ec2_endpoint_2"));
-            }
+
+        // randomly set, or not, the credentials
+        if (randomBoolean()) {
+            final var secureSettings = new MockSecureSettings();
+            final var proxyUsername = randomSecretKey();
+            final var proxyPassword = randomSecretKey();
+            secureSettings.setString(PROXY_USERNAME_SETTING.getKey(), proxyUsername);
+            secureSettings.setString(PROXY_PASSWORD_SETTING.getKey(), proxyPassword);
+            assertions.add(proxyConfiguration -> assertEquals(proxyUsername, proxyConfiguration.username()));
+            assertions.add(proxyConfiguration -> assertEquals(proxyPassword, proxyConfiguration.password()));
+            proxySettings.setSecureSettings(secureSettings);
+        } else {
+            assertions.add(proxyConfiguration -> assertEquals("", proxyConfiguration.username()));
+            assertions.add(proxyConfiguration -> assertEquals("", proxyConfiguration.password()));
         }
-    }
 
-    private static class Ec2DiscoveryPluginMock extends Ec2DiscoveryPlugin {
+        // now verify
+        runPluginMockTest(proxySettings, plugin -> verify(plugin.httpClientBuilder, times(1)).proxyConfiguration(argumentCaptor.capture()));
+        final var proxyConfiguration = argumentCaptor.getValue();
+        assertions.forEach(a -> a.accept(proxyConfiguration));
+    }
 
-        Ec2DiscoveryPluginMock(Settings settings) {
-            super(settings, new AwsEc2ServiceImpl() {
-                @Override
-                AmazonEC2 buildClient(AWSCredentialsProvider credentials, ClientConfiguration configuration, String endpoint) {
-                    return new AmazonEC2Mock(credentials, configuration, endpoint);
-                }
-            });
-        }
+    public void testCredentialsFromEnvironment() {
+        final var argumentCaptor = ArgumentCaptor.forClass(AwsCredentialsProvider.class);
+        runPluginMockTest(
+            Settings.builder(),
+            plugin -> verify(plugin.ec2ClientBuilder, times(1)).credentialsProvider(argumentCaptor.capture())
+        );
+        assertThat(argumentCaptor.getValue(), instanceOf(DefaultCredentialsProvider.class));
     }
 
-    private static class AmazonEC2Mock extends AbstractAmazonEC2 {
+    public void testPermanentCredentialsFromKeystore() {
+        final var accessKey = randomSecretKey();
+        final var secretKey = randomSecretKey();
 
-        String endpoint;
-        final AWSCredentialsProvider credentials;
-        final ClientConfiguration configuration;
+        final var secureSettings = new MockSecureSettings();
+        secureSettings.setString(ACCESS_KEY_SETTING.getKey(), accessKey);
+        secureSettings.setString(SECRET_KEY_SETTING.getKey(), secretKey);
 
-        AmazonEC2Mock(AWSCredentialsProvider credentials, ClientConfiguration configuration, String endpoint) {
-            this.credentials = credentials;
-            this.configuration = configuration;
-            this.endpoint = endpoint;
-        }
+        final var argumentCaptor = ArgumentCaptor.forClass(AwsCredentialsProvider.class);
+
+        runPluginMockTest(
+            Settings.builder().setSecureSettings(secureSettings),
+            plugin -> verify(plugin.ec2ClientBuilder, times(1)).credentialsProvider(argumentCaptor.capture())
+        );
+        final var awsCredentials = asInstanceOf(AwsBasicCredentials.class, argumentCaptor.getValue().resolveCredentials());
+        assertEquals(accessKey, awsCredentials.accessKeyId());
+        assertEquals(secretKey, awsCredentials.secretAccessKey());
+    }
 
-        @Override
-        public void shutdown() {}
+    public void testSessionCredentialsFromKeystore() {
+        final var accessKey = randomSecretKey();
+        final var secretKey = randomSecretKey();
+        final var sessionToken = randomSecretKey();
+
+        final var secureSettings = new MockSecureSettings();
+        secureSettings.setString(ACCESS_KEY_SETTING.getKey(), accessKey);
+        secureSettings.setString(SECRET_KEY_SETTING.getKey(), secretKey);
+        secureSettings.setString(SESSION_TOKEN_SETTING.getKey(), sessionToken);
+
+        final var argumentCaptor = ArgumentCaptor.forClass(AwsCredentialsProvider.class);
+
+        runPluginMockTest(
+            Settings.builder().setSecureSettings(secureSettings),
+            plugin -> verify(plugin.ec2ClientBuilder, times(1)).credentialsProvider(argumentCaptor.capture())
+        );
+        final var awsCredentials = asInstanceOf(AwsSessionCredentials.class, argumentCaptor.getValue().resolveCredentials());
+        assertEquals(accessKey, awsCredentials.accessKeyId());
+        assertEquals(secretKey, awsCredentials.secretAccessKey());
+        assertEquals(sessionToken, awsCredentials.sessionToken());
     }
 
-    @SuppressForbidden(reason = "Uses an HttpServer to emulate the Instance Metadata Service")
-    private static MetadataServer metadataServerWithoutToken() throws IOException {
-        return new MetadataServer("/metadata", exchange -> {
-            assertNull(exchange.getRequestHeaders().getFirst("X-aws-ec2-metadata-token"));
-            exchange.sendResponseHeaders(200, 0);
-            exchange.getResponseBody().write("us-east-1c".getBytes(StandardCharsets.UTF_8));
-            exchange.close();
-        });
+    /**
+     * Sets up a plugin with the given {@code settings}, using mocks, and then calls the {@code pluginConsumer} on it.
+     */
+    private static void runPluginMockTest(Settings.Builder settings, CheckedConsumer<Ec2DiscoveryPluginMock, Exception> pluginConsumer) {
+        final var httpClientBuilder = mock(ApacheHttpClient.Builder.class);
+        final var ec2ClientBuilder = mock(Ec2ClientBuilder.class);
+        when(ec2ClientBuilder.build()).thenReturn(mock(Ec2Client.class));
+
+        try (
+            var plugin = new Ec2DiscoveryPluginMock(settings.build(), httpClientBuilder, ec2ClientBuilder);
+            var ignored = plugin.ec2Service.client()
+        ) {
+            pluginConsumer.accept(plugin);
+        } catch (Exception e) {
+            throw new AssertionError("unexpected", e);
+        }
     }
 
-    @SuppressForbidden(reason = "Uses an HttpServer to emulate the Instance Metadata Service")
-    private static class MetadataServer implements AutoCloseable {
+    public void testLoneAccessKeyError() {
+        final var secureSettings = new MockSecureSettings();
+        secureSettings.setString(ACCESS_KEY_SETTING.getKey(), randomSecretKey());
+        final var settings = Settings.builder().setSecureSettings(secureSettings).build();
+        assertEquals(
+            "Setting [discovery.ec2.access_key] is set but [discovery.ec2.secret_key] is not",
+            expectThrows(SettingsException.class, () -> new Ec2DiscoveryPlugin(settings)).getMessage()
+        );
+    }
 
-        private final HttpServer httpServer;
+    public void testLoneSecretKeyError() {
+        final var secureSettings = new MockSecureSettings();
+        secureSettings.setString(SECRET_KEY_SETTING.getKey(), randomSecretKey());
+        final var settings = Settings.builder().setSecureSettings(secureSettings).build();
+        assertEquals(
+            "Setting [discovery.ec2.secret_key] is set but [discovery.ec2.access_key] is not",
+            expectThrows(SettingsException.class, () -> new Ec2DiscoveryPlugin(settings)).getMessage()
+        );
+    }
 
-        private MetadataServer(String metadataPath, HttpHandler metadataHandler) throws IOException {
-            this(metadataPath, metadataHandler, null, null);
-        }
+    public void testLoneSessionTokenError() {
+        final var secureSettings = new MockSecureSettings();
+        secureSettings.setString(SESSION_TOKEN_SETTING.getKey(), randomSecretKey());
+        final var settings = Settings.builder().setSecureSettings(secureSettings).build();
+        assertEquals(
+            "Setting [discovery.ec2.session_token] is set but [discovery.ec2.access_key] and [discovery.ec2.secret_key] are not",
+            expectThrows(SettingsException.class, () -> new Ec2DiscoveryPlugin(settings)).getMessage()
+        );
+    }
 
-        private MetadataServer(String metadataPath, HttpHandler metadataHandler, String tokenPath, HttpHandler tokenHandler)
-            throws IOException {
-            httpServer = MockHttpServer.createHttp(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0), 0);
-            httpServer.createContext(metadataPath, metadataHandler);
-            if (tokenPath != null && tokenHandler != null) {
-                httpServer.createContext(tokenPath, tokenHandler);
+    public void testReloadSettings() {
+        final var httpClientBuilder = mock(ApacheHttpClient.Builder.class);
+        final var ec2ClientBuilder = mock(Ec2ClientBuilder.class);
+
+        final var accessKey1 = randomSecretKey();
+        final var secretKey1 = randomSecretKey();
+        final var secureSettings1 = new MockSecureSettings();
+        secureSettings1.setString(ACCESS_KEY_SETTING.getKey(), accessKey1);
+        secureSettings1.setString(SECRET_KEY_SETTING.getKey(), secretKey1);
+        final var settings1 = Settings.builder().setSecureSettings(secureSettings1).build();
+
+        try (var plugin = new Ec2DiscoveryPluginMock(settings1, httpClientBuilder, ec2ClientBuilder)) {
+            final var client1 = mock(Ec2Client.class);
+            when(ec2ClientBuilder.build()).thenReturn(client1);
+
+            try (var clientReference = plugin.ec2Service.client()) {
+                assertSame(client1, clientReference.client());
+                final var argumentCaptor = ArgumentCaptor.forClass(AwsCredentialsProvider.class);
+                verify(plugin.ec2ClientBuilder, times(1)).credentialsProvider(argumentCaptor.capture());
+                final var awsCredentials = argumentCaptor.getValue().resolveCredentials();
+                assertEquals(accessKey1, awsCredentials.accessKeyId());
+                assertEquals(secretKey1, awsCredentials.secretAccessKey());
             }
-            httpServer.start();
+            verify(client1, never()).close(); // retaining client for future use
+
+            final var accessKey2 = randomSecretKey();
+            final var secretKey2 = randomSecretKey();
+            final var secureSettings2 = new MockSecureSettings();
+            secureSettings2.setString(ACCESS_KEY_SETTING.getKey(), accessKey2);
+            secureSettings2.setString(SECRET_KEY_SETTING.getKey(), secretKey2);
+            plugin.reload(Settings.builder().setSecureSettings(secureSettings2).build());
+
+            verify(client1, times(1)).close(); // client released on reload
+
+            final var client2 = mock(Ec2Client.class);
+            when(ec2ClientBuilder.build()).thenReturn(client2);
+
+            try (var clientReference = plugin.ec2Service.client()) {
+                assertSame(client2, clientReference.client());
+                final var argumentCaptor = ArgumentCaptor.forClass(AwsCredentialsProvider.class);
+                verify(plugin.ec2ClientBuilder, times(2)).credentialsProvider(argumentCaptor.capture());
+                final var awsCredentials = argumentCaptor.getAllValues().get(1).resolveCredentials();
+                assertEquals(accessKey2, awsCredentials.accessKeyId());
+                assertEquals(secretKey2, awsCredentials.secretAccessKey());
+            }
+        } catch (Exception e) {
+            throw new AssertionError("unexpected", e);
         }
+    }
 
-        @Override
-        public void close() throws Exception {
-            httpServer.stop(0);
-        }
+    private static class Ec2DiscoveryPluginMock extends Ec2DiscoveryPlugin {
+        final ApacheHttpClient.Builder httpClientBuilder;
+        final Ec2ClientBuilder ec2ClientBuilder;
 
-        private String metadataUri() {
-            return "http://" + httpServer.getAddress().getHostString() + ":" + httpServer.getAddress().getPort() + "/metadata";
-        }
+        Ec2DiscoveryPluginMock(Settings settings, ApacheHttpClient.Builder httpClientBuilder, Ec2ClientBuilder ec2ClientBuilder) {
+            super(settings, new AwsEc2ServiceImpl() {
+                @Override
+                ApacheHttpClient.Builder getHttpClientBuilder() {
+                    return httpClientBuilder;
+                }
 
-        private String tokenUri() {
-            return "http://" + httpServer.getAddress().getHostString() + ":" + httpServer.getAddress().getPort() + "/latest/api/token";
+                @Override
+                Ec2ClientBuilder getEc2ClientBuilder() {
+                    return ec2ClientBuilder;
+                }
+            });
+            this.httpClientBuilder = httpClientBuilder;
+            this.ec2ClientBuilder = ec2ClientBuilder;
         }
     }
+
 }

+ 29 - 24
plugins/discovery-ec2/src/test/java/org/elasticsearch/discovery/ec2/Ec2DiscoveryTests.java

@@ -9,11 +9,11 @@
 
 package org.elasticsearch.discovery.ec2;
 
-import com.amazonaws.http.HttpMethodName;
-import com.amazonaws.services.ec2.model.Instance;
-import com.amazonaws.services.ec2.model.InstanceState;
-import com.amazonaws.services.ec2.model.InstanceStateName;
-import com.amazonaws.services.ec2.model.Tag;
+import software.amazon.awssdk.http.SdkHttpMethod;
+import software.amazon.awssdk.services.ec2.model.Instance;
+import software.amazon.awssdk.services.ec2.model.InstanceState;
+import software.amazon.awssdk.services.ec2.model.InstanceStateName;
+import software.amazon.awssdk.services.ec2.model.Tag;
 
 import org.apache.http.HttpStatus;
 import org.apache.http.NameValuePair;
@@ -90,7 +90,7 @@ public class Ec2DiscoveryTests extends AbstractEC2MockAPITestCase {
         try (Ec2DiscoveryPlugin plugin = new Ec2DiscoveryPlugin(buildSettings(accessKey))) {
             AwsEc2SeedHostsProvider provider = new AwsEc2SeedHostsProvider(nodeSettings, transportService, plugin.ec2Service);
             httpServer.createContext("/", exchange -> {
-                if (exchange.getRequestMethod().equals(HttpMethodName.POST.name())) {
+                if (SdkHttpMethod.POST.name().equals(exchange.getRequestMethod())) {
                     final String request = new String(exchange.getRequestBody().readAllBytes(), UTF_8);
                     final String userAgent = exchange.getRequestHeaders().getFirst("User-Agent");
                     if (userAgent != null && userAgent.startsWith("aws-sdk-java")) {
@@ -114,25 +114,26 @@ public class Ec2DiscoveryTests extends AbstractEC2MockAPITestCase {
                         });
                         final List<Instance> instances = IntStream.range(1, nodes + 1).mapToObj(node -> {
                             final String instanceId = "node" + node;
-                            final Instance instance = new Instance().withInstanceId(instanceId)
-                                .withState(new InstanceState().withName(InstanceStateName.Running))
-                                .withPrivateDnsName(PREFIX_PRIVATE_DNS + instanceId + SUFFIX_PRIVATE_DNS)
-                                .withPublicDnsName(PREFIX_PUBLIC_DNS + instanceId + SUFFIX_PUBLIC_DNS)
-                                .withPrivateIpAddress(PREFIX_PRIVATE_IP + node)
-                                .withPublicIpAddress(PREFIX_PUBLIC_IP + node);
+                            final Instance.Builder instanceBuilder = Instance.builder()
+                                .instanceId(instanceId)
+                                .state(InstanceState.builder().name(InstanceStateName.RUNNING).build())
+                                .privateDnsName(PREFIX_PRIVATE_DNS + instanceId + SUFFIX_PRIVATE_DNS)
+                                .publicDnsName(PREFIX_PUBLIC_DNS + instanceId + SUFFIX_PUBLIC_DNS)
+                                .privateIpAddress(PREFIX_PRIVATE_IP + node)
+                                .publicIpAddress(PREFIX_PUBLIC_IP + node);
                             if (tagsList != null) {
-                                instance.setTags(tagsList.get(node - 1));
+                                instanceBuilder.tags(tagsList.get(node - 1));
                             }
-                            return instance;
+                            return instanceBuilder.build();
                         })
                             .filter(
                                 instance -> tagsIncluded.entrySet()
                                     .stream()
                                     .allMatch(
-                                        entry -> instance.getTags()
+                                        entry -> instance.tags()
                                             .stream()
-                                            .filter(t -> t.getKey().equals(entry.getKey()))
-                                            .map(Tag::getValue)
+                                            .filter(t -> t.key().equals(entry.getKey()))
+                                            .map(Tag::value)
                                             .toList()
                                             .containsAll(entry.getValue())
                                     )
@@ -253,10 +254,10 @@ public class Ec2DiscoveryTests extends AbstractEC2MockAPITestCase {
         for (int node = 0; node < nodes; node++) {
             List<Tag> tags = new ArrayList<>();
             if (randomBoolean()) {
-                tags.add(new Tag("stage", "prod"));
+                tags.add(tag("stage", "prod"));
                 prodInstances++;
             } else {
-                tags.add(new Tag("stage", "dev"));
+                tags.add(tag("stage", "dev"));
             }
             tagsList.add(tags);
         }
@@ -266,6 +267,10 @@ public class Ec2DiscoveryTests extends AbstractEC2MockAPITestCase {
         assertThat(dynamicHosts, hasSize(prodInstances));
     }
 
+    private static Tag tag(String key, String value) {
+        return Tag.builder().key(key).value(value).build();
+    }
+
     public void testFilterByMultipleTags() {
         int nodes = randomIntBetween(5, 10);
         Settings nodeSettings = Settings.builder().putList(AwsEc2Service.TAG_SETTING.getKey() + "stage", "prod", "preprod").build();
@@ -276,15 +281,15 @@ public class Ec2DiscoveryTests extends AbstractEC2MockAPITestCase {
         for (int node = 0; node < nodes; node++) {
             List<Tag> tags = new ArrayList<>();
             if (randomBoolean()) {
-                tags.add(new Tag("stage", "prod"));
+                tags.add(tag("stage", "prod"));
                 if (randomBoolean()) {
-                    tags.add(new Tag("stage", "preprod"));
+                    tags.add(tag("stage", "preprod"));
                     prodInstances++;
                 }
             } else {
-                tags.add(new Tag("stage", "dev"));
+                tags.add(tag("stage", "dev"));
                 if (randomBoolean()) {
-                    tags.add(new Tag("stage", "preprod"));
+                    tags.add(tag("stage", "preprod"));
                 }
             }
             tagsList.add(tags);
@@ -311,7 +316,7 @@ public class Ec2DiscoveryTests extends AbstractEC2MockAPITestCase {
 
         for (int node = 0; node < nodes; node++) {
             List<Tag> tags = new ArrayList<>();
-            tags.add(new Tag("foo", "node" + (node + 1)));
+            tags.add(tag("foo", "node" + (node + 1)));
             tagsList.add(tags);
         }
 

+ 25 - 4
test/fixtures/ec2-imds-fixture/src/main/java/fixture/aws/imds/Ec2ImdsHttpFixture.java

@@ -26,8 +26,17 @@ import java.util.Objects;
 
 public class Ec2ImdsHttpFixture extends ExternalResource {
 
+    /**
+     * Name of the JVM system property that allows to override the IMDS endpoint address when using the AWS v1 SDK.
+     * Can be removed once we only use the v2 SDK.
+     */
     public static final String ENDPOINT_OVERRIDE_SYSPROP_NAME = "com.amazonaws.sdk.ec2MetadataServiceEndpointOverride";
 
+    /**
+     * Name of the JVM system property that allows to override the IMDS endpoint address when using the AWS v2 SDK.
+     */
+    public static final String ENDPOINT_OVERRIDE_SYSPROP_NAME_SDK2 = "aws.ec2MetadataServiceEndpoint";
+
     private final Ec2ImdsServiceBuilder ec2ImdsServiceBuilder;
     private HttpServer server;
 
@@ -62,12 +71,18 @@ public class Ec2ImdsHttpFixture extends ExternalResource {
         }
     }
 
+    /**
+     * Overrides the EC2 service endpoint for the lifetime of the method response. Resets back to the original endpoint property when
+     * closed.
+     */
     @SuppressForbidden(reason = "deliberately adjusting system property for endpoint override for use in internal-cluster tests")
     public static Releasable withEc2MetadataServiceEndpointOverride(String endpointOverride) {
-        final PrivilegedAction<String> resetProperty = System.getProperty(ENDPOINT_OVERRIDE_SYSPROP_NAME) instanceof String originalValue
-            ? () -> System.setProperty(ENDPOINT_OVERRIDE_SYSPROP_NAME, originalValue)
-            : () -> System.clearProperty(ENDPOINT_OVERRIDE_SYSPROP_NAME);
-        doPrivileged(() -> System.setProperty(ENDPOINT_OVERRIDE_SYSPROP_NAME, endpointOverride));
+        final PrivilegedAction<String> resetProperty = System.getProperty(
+            ENDPOINT_OVERRIDE_SYSPROP_NAME_SDK2
+        ) instanceof String originalValue
+            ? () -> System.setProperty(ENDPOINT_OVERRIDE_SYSPROP_NAME_SDK2, originalValue)
+            : () -> System.clearProperty(ENDPOINT_OVERRIDE_SYSPROP_NAME_SDK2);
+        doPrivileged(() -> System.setProperty(ENDPOINT_OVERRIDE_SYSPROP_NAME_SDK2, endpointOverride));
         return () -> doPrivileged(resetProperty);
     }
 
@@ -75,6 +90,10 @@ public class Ec2ImdsHttpFixture extends ExternalResource {
         AccessController.doPrivileged(privilegedAction);
     }
 
+    /**
+     * Adapter to allow running a {@link Ec2ImdsHttpFixture} directly rather than via a {@code @ClassRule}. Creates an HTTP handler (see
+     * {@link Ec2ImdsHttpHandler}) from the given builder, and provides the handler to the action, and then cleans up the handler.
+     */
     public static void runWithFixture(Ec2ImdsServiceBuilder ec2ImdsServiceBuilder, CheckedConsumer<Ec2ImdsHttpFixture, Exception> action) {
         final var imdsFixture = new Ec2ImdsHttpFixture(ec2ImdsServiceBuilder);
         try {
@@ -86,6 +105,8 @@ public class Ec2ImdsHttpFixture extends ExternalResource {
             }, Description.EMPTY).evaluate();
         } catch (Throwable e) {
             throw new AssertionError(e);
+        } finally {
+            imdsFixture.stop(0);
         }
     }