diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 684facb14ebbd..58fadf7b6c9cb 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -2111,6 +2111,16 @@ quarkus-amazon-alexa-deployment ${project.version} + + io.quarkus + quarkus-amazon-secretsmanager + ${project.version} + + + io.quarkus + quarkus-amazon-secretsmanager-deployment + ${project.version} + io.quarkus quarkus-resteasy-reactive-common diff --git a/core/deployment/src/main/java/io/quarkus/deployment/Feature.java b/core/deployment/src/main/java/io/quarkus/deployment/Feature.java index 39e7f140283f1..8875deb9f7678 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/Feature.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/Feature.java @@ -19,6 +19,7 @@ public enum Feature { AMAZON_SES, AMAZON_KMS, AMAZON_SSM, + AMAZON_SECRETS_MANAGER, APICURIO_REGISTRY_AVRO, ARTEMIS_CORE, ARTEMIS_JMS, diff --git a/devtools/bom-descriptor-json/pom.xml b/devtools/bom-descriptor-json/pom.xml index 539b46999d48e..ef649f972743a 100644 --- a/devtools/bom-descriptor-json/pom.xml +++ b/devtools/bom-descriptor-json/pom.xml @@ -253,6 +253,19 @@ + + io.quarkus + quarkus-amazon-secretsmanager + ${project.version} + pom + test + + + * + * + + + io.quarkus quarkus-amazon-ses diff --git a/docs/pom.xml b/docs/pom.xml index 3993678be99c4..fd24497a190ac 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -213,6 +213,19 @@ + + io.quarkus + quarkus-amazon-secretsmanager-deployment + ${project.version} + pom + test + + + * + * + + + io.quarkus quarkus-amazon-ses-deployment diff --git a/docs/src/main/asciidoc/amazon-secrets-manager.adoc b/docs/src/main/asciidoc/amazon-secrets-manager.adoc new file mode 100644 index 0000000000000..1a73e23d42d08 --- /dev/null +++ b/docs/src/main/asciidoc/amazon-secrets-manager.adoc @@ -0,0 +1,316 @@ +//// +This guide is maintained in the main Quarkus repository +and pull requests should be submitted there: +https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc +//// += Amazon Secrets Manager Client +:extension-status: preview + +include::./attributes.adoc[] + +AWS Secrets Manager enables you to replace hardcoded credentials in your code, including passwords, with an API call to Secrets Manager to retrieve the secret programmatically. +This helps ensure the secret can't be compromised by someone examining your code, because the secret no longer exists in the code. + +You can find more information about Secrets Manager at https://docs.aws.amazon.com/secretsmanager/[the AWS Secrets Manager website]. + +NOTE: The Secrets Manager extension is based on https://docs.aws.amazon.com/sdk-for-java/v2/developer-guide/welcome.html[AWS Java SDK 2.x]. +It's a major rewrite of the 1.x code base that offers two programming models (Blocking & Async). + +include::./status-include.adoc[] + +The Quarkus extension supports two programming models: + +* Blocking access using URL Connection HTTP client (by default) or the Apache HTTP Client +* https://docs.aws.amazon.com/sdk-for-java/v2/developer-guide/basics-async.html[Asynchronous programming] based on JDK's `CompletableFuture` objects and the Netty HTTP client. + +In this guide, we see how you can get your REST services to use Secrets Manager locally and on AWS. + +== Prerequisites + +To complete this guide, you need: + +* JDK 11+ installed with `JAVA_HOME` configured appropriately +* an IDE +* Apache Maven {maven-version} +* An AWS Account to access the Secrets Manager service +* Docker for your system to run Secrets Manager locally for testing purposes + +=== Set up Secrets Manager locally + +The easiest way to start working with Secrets Manager is to run a local instance as a container. + +[source,bash,subs="verbatim,attributes"] +---- +docker run --rm --name local-secrets-manager --publish 8014:4584 -e SERVICES=secretsmanager -e START_WEB=0 -d localstack/localstack:0.11.1 +---- +This starts a Secrets Manager instance that is accessible on port `8014`. + +Create an AWS profile for your local instance using AWS CLI: +[source,shell,subs="verbatim,attributes"] +---- +$ aws configure --profile localstack +AWS Access Key ID [None]: test-key +AWS Secret Access Key [None]: test-secret +Default region name [None]: us-east-1 +Default output format [None]: +---- + +== Solution +The application built here allows to store and retrieve credentials using Secrets Manager. + +We recommend that you follow the instructions in the next sections and create the application step by step. +However, you can go right to the completed example. + +Clone the Git repository: `git clone {quickstarts-clone-url}`, or download an {quickstarts-archive-url}[archive]. + +The solution is located in the `amazon-secretsmanager-quickstart` {quickstarts-tree-url}/amazon-secretsmanager-quickstart[directory]. + +== Creating the Maven project + +First, we need a new project. Create a new project with the following command: + +[source,bash,subs=attributes+] +---- +mvn io.quarkus.platform:quarkus-maven-plugin:{quarkus-version}:create \ + -DprojectGroupId=org.acme \ + -DprojectArtifactId=amazon-secretsmanager-quickstart \ + -DclassName="org.acme.secretsmanager.QuarkusSecretsManagerSyncResource" \ + -Dpath="/sync" \ + -Dextensions="resteasy,resteasy-jackson,amazon-secretsmanager,resteasy-mutiny" +cd amazon-secretsmanager-quickstart +---- + +This command generates a Maven structure importing the RESTEasy/JAX-RS, Mutiny and Amazon Secrets Manager Client extensions. +After this, the `amazon-secretsmanager` extension has been added to your `pom.xml` as well as the Mutiny support for RESTEasy. + +== Creating JSON REST service + +In this example, we will create an application that allows us to store and retrieve parameters to and from SSM parameter store using a RESTful API. +The example application will demonstrate the two programming models supported by the extension. + +Let's start with an abstract `org.acme.secretsmanager.QuarkusSecretsManagerResource` class to provide the common functionality we will need for both the synchronous and asynchrounous exposures. + +[source,java] +---- +package org.acme.secretsmanager; + +import static java.lang.Boolean.TRUE; +import static java.util.stream.Collectors.toMap; + +import java.util.Map; +import java.util.stream.Collector; + +import javax.annotation.PostConstruct; + +import org.eclipse.microprofile.config.inject.ConfigProperty; + +import software.amazon.awssdk.services.secretsmanager.model.CreateSecretRequest; +import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueRequest; + +public abstract class QuarkusSecretsManagerResource { + public static final String VERSION_STAGE = "AWSCURRENT"; + + @ConfigProperty(name = "secret.name") <1> + String secretName; + + protected GetSecretValueRequest generateGetSecretValueRequest() { + return GetSecretValueRequest.builder() <2> + .secretId(secretName) + .versionStage(VERSION_STAGE) + .build(); + } + + protected CreateSecretRequest generateCreateSecretRequest(String name, String secret) { + return CreateSecretRequest.builder() <3> + .name(name) + .secretString(secret) + .build(); + } + +} +---- + +<1> Inject a configured name under which is stored the secret +<2> Generate a request for the credentials with the configured name +<3> Generate a request to create a specific secret + +Now, we can extend the class and create the synchronous implementation in the `org.acme.ssm.QuarkusSecretsManagerSyncResource` class. + +[source,java] +---- +package org.acme.secretsmanager; + +import java.util.Map; + +import javax.inject.Inject; +import javax.ws.rs.Consumes; +import javax.ws.rs.DefaultValue; +import javax.ws.rs.GET; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.MediaType; + +import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient; + +@Path("/sync") +public class QuarkusSecretsManagerSyncResource extends QuarkusSsmResource { + + @Inject <1> + SecretsManagerClient secretsManagerClient; + + @GET + @Produces(MediaType.APPLICATION_JSON) + public String getSecret() { + return secretsManagerClient.getSecretValue(generateGetSecretValueRequest()).secretString(); + } + + @POST + @Path("/{name}") + @Consumes(MediaType.TEXT_PLAIN) + public void createSecret(@PathParam("name") String name, String value) { + secretsManagerClient.createSecret(generateCreateSecretRequest(name, secret)); + } + +} +---- + +<1> Inject the client provided by the amazon-secretsmanager extension + +Using the Amazon Secrets Manager SDK, we can easily store and retrieve secrets. + +== Configuring Secrets Manager clients + +Both Secrets Manager clients (sync and async) are configurable via the `application.properties` file that can be provided in the `src/main/resources` directory. +Additionally, you need to add to the classpath a proper implementation of the sync client. By default the extension uses the URL connection HTTP client, so +you need to add a URL connection client dependency to the `pom.xml` file: + +[source,xml] +---- + + software.amazon.awssdk + url-connection-client + +---- + +If you want to use Apache HTTP client instead, configure it as follows: + +[source,properties] +---- +quarkus.secretsmanager.sync-client.type=apache +---- + +And add the following dependency to the application `pom.xml`: + +[source,xml] +---- + + software.amazon.awssdk + apache-client + + + io.quarkus + quarkus-apache-httpclient + +---- + +If you're going to use a local Secrets Manager instance, configure it as follows: + +[source,properties] +---- +quarkus.secretsmanager.endpoint-override=http://localhost:8014 <1> + +quarkus.secretsmanager.aws.region=us-east-1 <2> +quarkus.secretsmanager.aws.credentials.type=static <3> +quarkus.secretsmanager.aws.credentials.static-provider.access-key-id=test-key +quarkus.secretsmanager.aws.credentials.static-provider.secret-access-key=test-secret +---- + +<1> Override the Secret Manager client to use localstack instead of the actual AWS service +<2> Localstack defaults to `us-east-1` +<3> The `static` credentials provider lets you set the `access-key-id` and `secret-access-key` directly + +If you want to work with an AWS account, you can simply remove or comment out all Amazon SSM related properties. By default, the Secrets Manager client extension will use the `default` credentials provider chain that looks for credentials in this order: + +include::./amazon-credentials.adoc[] + +And the region from your AWS CLI profile will be used. + +== Next steps + +=== Packaging + +Packaging your application is as simple as `./mvnw clean package`. +It can then be run with `java -Dparameters.path=/quarkus/is/awesome/ -jar target/quarkus-app/quarkus-run.jar`. + +With GraalVM installed, you can also create a native executable binary: `./mvnw clean package -Dnative`. +Depending on your system, that will take some time. + +=== Going asynchronous + +Thanks to the AWS SDK v2.x used by the Quarkus extension, you can use the asynchronous programming model out of the box. + +Create a `org.acme.secretsmanager.QuarkusSecretsManagerAsyncResource` REST resource that will be similar to our `QuarkusSecretsManagerSyncResource` but using an asynchronous programming model. + +[source,java] +---- +package org.acme.secretsmanager; + +import java.util.Map; + +import javax.inject.Inject; +import javax.ws.rs.Consumes; +import javax.ws.rs.DefaultValue; +import javax.ws.rs.GET; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.MediaType; + +import io.smallrye.mutiny.Uni; +import software.amazon.awssdk.services.secretsmanager.SecretsManagerAsyncClient; + +@Path("/async") +public class QuarkusSecretsManagerAsyncResource extends QuarkusSecretsManagerResource { + + @Inject + SecretsManagerAsyncClient secretsManagerAsyncClient; + + @GET + @Produces(MediaType.APPLICATION_JSON) + public Uni getSecret() { + return Uni.createFrom().completionStage(secretsManagerAsyncClient.getSecretValue(generateGetSecretValueRequest())) + .onItem().transform(r -> r.secretString()); + } + + @PUT + @Path("/{name}") + @Consumes(MediaType.TEXT_PLAIN) + public Uni createSecret(@PathParam("name") String name, String value) { + + return Uni.createFrom().completionStage(secretsManagerAsyncClient.createSecret(generateCreateSecretRequest(name, secret))) + .onItem().transform(r -> null); + } + +} +---- + +Note that the `SecretsManagerAsyncClient` behaves just like the `SecretsManagerClient`, but returns `CompletionStage` objects which we use to create `Uni` instances, and then transform the emitted item. + +To enable the asynchronous client, we also need to add the Netty HTTP client dependency to the `pom.xml`: + +[source,xml] +---- + + software.amazon.awssdk + netty-nio-client + +---- + +== Configuration Reference + +include::{generated-dir}/config/quarkus-amazon-secretsmanager.adoc[opts=optional, leveloffset=+1] diff --git a/extensions/amazon-services/pom.xml b/extensions/amazon-services/pom.xml index 149cf3c37006b..d4f6080857f7c 100644 --- a/extensions/amazon-services/pom.xml +++ b/extensions/amazon-services/pom.xml @@ -24,5 +24,6 @@ ses kms ssm + secretsmanager diff --git a/extensions/amazon-services/secretsmanager/deployment/pom.xml b/extensions/amazon-services/secretsmanager/deployment/pom.xml new file mode 100644 index 0000000000000..1b6ba9f4e422a --- /dev/null +++ b/extensions/amazon-services/secretsmanager/deployment/pom.xml @@ -0,0 +1,77 @@ + + + 4.0.0 + + + io.quarkus + quarkus-amazon-secretsmanager-parent + 999-SNAPSHOT + + + quarkus-amazon-secretsmanager-deployment + Quarkus - Amazon Services - Secrets Manager - Deployment + + + + io.quarkus + quarkus-core-deployment + + + io.quarkus + quarkus-arc-deployment + + + io.quarkus + quarkus-netty-deployment + + + io.quarkus + quarkus-amazon-common-deployment + + + io.quarkus + quarkus-amazon-secretsmanager + + + + + io.quarkus + quarkus-junit5-internal + test + + + io.rest-assured + rest-assured + test + + + software.amazon.awssdk + netty-nio-client + test + + + software.amazon.awssdk + url-connection-client + test + + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + diff --git a/extensions/amazon-services/secretsmanager/deployment/src/main/java/io/quarkus/amazon/secretsmanager/deployment/SecretsManagerProcessor.java b/extensions/amazon-services/secretsmanager/deployment/src/main/java/io/quarkus/amazon/secretsmanager/deployment/SecretsManagerProcessor.java new file mode 100644 index 0000000000000..f6930b3d83bb6 --- /dev/null +++ b/extensions/amazon-services/secretsmanager/deployment/src/main/java/io/quarkus/amazon/secretsmanager/deployment/SecretsManagerProcessor.java @@ -0,0 +1,140 @@ +package io.quarkus.amazon.secretsmanager.deployment; + +import java.util.List; + +import org.jboss.jandex.DotName; + +import io.quarkus.amazon.common.deployment.AbstractAmazonServiceProcessor; +import io.quarkus.amazon.common.deployment.AmazonClientAsyncTransportBuildItem; +import io.quarkus.amazon.common.deployment.AmazonClientBuildItem; +import io.quarkus.amazon.common.deployment.AmazonClientInterceptorsPathBuildItem; +import io.quarkus.amazon.common.deployment.AmazonClientSyncTransportBuildItem; +import io.quarkus.amazon.common.deployment.AmazonHttpClients; +import io.quarkus.amazon.common.runtime.AmazonClientApacheTransportRecorder; +import io.quarkus.amazon.common.runtime.AmazonClientNettyTransportRecorder; +import io.quarkus.amazon.common.runtime.AmazonClientRecorder; +import io.quarkus.amazon.common.runtime.AmazonClientUrlConnectionTransportRecorder; +import io.quarkus.amazon.secretsmanager.runtime.SecretsManagerBuildTimeConfig; +import io.quarkus.amazon.secretsmanager.runtime.SecretsManagerClientProducer; +import io.quarkus.amazon.secretsmanager.runtime.SecretsManagerConfig; +import io.quarkus.amazon.secretsmanager.runtime.SecretsManagerRecorder; +import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.arc.deployment.BeanRegistrationPhaseBuildItem; +import io.quarkus.arc.deployment.SyntheticBeanBuildItem; +import io.quarkus.deployment.Feature; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.ExecutionTime; +import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.ExtensionSslNativeSupportBuildItem; +import io.quarkus.deployment.builditem.FeatureBuildItem; +import software.amazon.awssdk.services.secretsmanager.SecretsManagerAsyncClient; +import software.amazon.awssdk.services.secretsmanager.SecretsManagerAsyncClientBuilder; +import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient; +import software.amazon.awssdk.services.secretsmanager.SecretsManagerClientBuilder; + +public class SecretsManagerProcessor extends AbstractAmazonServiceProcessor { + + SecretsManagerBuildTimeConfig buildTimeConfig; + + @Override + protected Feature amazonServiceClientName() { + return Feature.AMAZON_SECRETS_MANAGER; + } + + @Override + protected String configName() { + return "secretsmanager"; + } + + @Override + protected DotName syncClientName() { + return DotName.createSimple(SecretsManagerClient.class.getName()); + } + + @Override + protected DotName asyncClientName() { + return DotName.createSimple(SecretsManagerAsyncClient.class.getName()); + } + + @Override + protected String builtinInterceptorsPath() { + return "software/amazon/awssdk/services/secretsmanager/execution.interceptors"; + } + + @BuildStep + AdditionalBeanBuildItem producer() { + return AdditionalBeanBuildItem.unremovableOf(SecretsManagerClientProducer.class); + } + + @BuildStep + void setup(BeanRegistrationPhaseBuildItem beanRegistrationPhase, + BuildProducer extensionSslNativeSupport, + BuildProducer feature, + BuildProducer interceptors, + BuildProducer clientProducer) { + + setupExtension(beanRegistrationPhase, extensionSslNativeSupport, feature, interceptors, clientProducer, + buildTimeConfig.sdk, buildTimeConfig.syncClient); + } + + @BuildStep(onlyIf = AmazonHttpClients.IsAmazonApacheHttpServicePresent.class) + @Record(ExecutionTime.RUNTIME_INIT) + void setupApacheSyncTransport(List amazonClients, SecretsManagerRecorder recorder, + AmazonClientApacheTransportRecorder transportRecorder, + SecretsManagerConfig runtimeConfig, BuildProducer syncTransports) { + + createApacheSyncTransportBuilder(amazonClients, + transportRecorder, + buildTimeConfig.syncClient, + recorder.getSyncConfig(runtimeConfig), + syncTransports); + } + + @BuildStep(onlyIf = AmazonHttpClients.IsAmazonUrlConnectionHttpServicePresent.class) + @Record(ExecutionTime.RUNTIME_INIT) + void setupUrlConnectionSyncTransport(List amazonClients, SecretsManagerRecorder recorder, + AmazonClientUrlConnectionTransportRecorder transportRecorder, + SecretsManagerConfig runtimeConfig, BuildProducer syncTransports) { + + createUrlConnectionSyncTransportBuilder(amazonClients, + transportRecorder, + buildTimeConfig.syncClient, + recorder.getSyncConfig(runtimeConfig), + syncTransports); + } + + @BuildStep(onlyIf = AmazonHttpClients.IsAmazonNettyHttpServicePresent.class) + @Record(ExecutionTime.RUNTIME_INIT) + void setupNettyAsyncTransport(List amazonClients, SecretsManagerRecorder recorder, + AmazonClientNettyTransportRecorder transportRecorder, + SecretsManagerConfig runtimeConfig, BuildProducer asyncTransports) { + + createNettyAsyncTransportBuilder(amazonClients, + transportRecorder, + recorder.getAsyncConfig(runtimeConfig), + asyncTransports); + } + + @BuildStep + @Record(ExecutionTime.RUNTIME_INIT) + void createClientBuilders(SecretsManagerRecorder recorder, + AmazonClientRecorder commonRecorder, + SecretsManagerConfig runtimeConfig, + List syncTransports, + List asyncTransports, + BuildProducer syntheticBeans) { + + createClientBuilders(commonRecorder, + recorder.getAwsConfig(runtimeConfig), + recorder.getSdkConfig(runtimeConfig), + buildTimeConfig.sdk, + syncTransports, + asyncTransports, + SecretsManagerClientBuilder.class, + (syncTransport) -> recorder.createSyncBuilder(runtimeConfig, syncTransport), + SecretsManagerAsyncClientBuilder.class, + (asyncTransport) -> recorder.createAsyncBuilder(runtimeConfig, asyncTransport), + syntheticBeans); + } +} diff --git a/extensions/amazon-services/secretsmanager/deployment/src/test/java/io/quarkus/amazon/secretsmanager/deployment/SecretsManagerSyncClientFullConfigTest.java b/extensions/amazon-services/secretsmanager/deployment/src/test/java/io/quarkus/amazon/secretsmanager/deployment/SecretsManagerSyncClientFullConfigTest.java new file mode 100644 index 0000000000000..b595d2e759e3c --- /dev/null +++ b/extensions/amazon-services/secretsmanager/deployment/src/test/java/io/quarkus/amazon/secretsmanager/deployment/SecretsManagerSyncClientFullConfigTest.java @@ -0,0 +1,31 @@ +package io.quarkus.amazon.secretsmanager.deployment; + +import javax.inject.Inject; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import software.amazon.awssdk.services.secretsmanager.SecretsManagerAsyncClient; +import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient; + +public class SecretsManagerSyncClientFullConfigTest { + + @Inject + SecretsManagerClient client; + + @Inject + SecretsManagerAsyncClient async; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addAsResource("sync-urlconn-full-config.properties", "application.properties")); + + @Test + public void test() { + // should finish with success + } +} diff --git a/extensions/amazon-services/secretsmanager/deployment/src/test/resources/sync-urlconn-full-config.properties b/extensions/amazon-services/secretsmanager/deployment/src/test/resources/sync-urlconn-full-config.properties new file mode 100644 index 0000000000000..ee50426870598 --- /dev/null +++ b/extensions/amazon-services/secretsmanager/deployment/src/test/resources/sync-urlconn-full-config.properties @@ -0,0 +1,10 @@ +quarkus.secretsmanager.endpoint-override=http://localhost:9090 + +quarkus.secretsmanager.aws.region=us-east-1 +quarkus.secretsmanager.aws.credentials.type=static +quarkus.secretsmanager.aws.credentials.static-provider.access-key-id=test-key +quarkus.secretsmanager.aws.credentials.static-provider.secret-access-key=test-secret + +quarkus.secretsmanager.sync-client.type = url +quarkus.secretsmanager.sync-client.connection-timeout = 0.100S +quarkus.secretsmanager.sync-client.socket-timeout = 0.100S \ No newline at end of file diff --git a/extensions/amazon-services/secretsmanager/pom.xml b/extensions/amazon-services/secretsmanager/pom.xml new file mode 100644 index 0000000000000..4cd391a62f747 --- /dev/null +++ b/extensions/amazon-services/secretsmanager/pom.xml @@ -0,0 +1,22 @@ + + + 4.0.0 + + + io.quarkus + quarkus-amazon-services-parent + 999-SNAPSHOT + + + quarkus-amazon-secretsmanager-parent + Quarkus - Amazon Services - Secrets Manager + pom + + + runtime + deployment + + + diff --git a/extensions/amazon-services/secretsmanager/runtime/pom.xml b/extensions/amazon-services/secretsmanager/runtime/pom.xml new file mode 100644 index 0000000000000..dc0aa465d4663 --- /dev/null +++ b/extensions/amazon-services/secretsmanager/runtime/pom.xml @@ -0,0 +1,96 @@ + + + 4.0.0 + + + io.quarkus + quarkus-amazon-secretsmanager-parent + 999-SNAPSHOT + + + quarkus-amazon-secretsmanager + Quarkus - Amazon Services - Secrets Manager - Runtime + Connect to Amazon Secrets Manager + + + + io.quarkus + quarkus-core + + + io.quarkus + quarkus-arc + + + io.quarkus + quarkus-netty + + + + io.quarkus + quarkus-amazon-common + + + software.amazon.awssdk + secretsmanager + + + + software.amazon.awssdk + netty-nio-client + + + software.amazon.awssdk + url-connection-client + + + software.amazon.awssdk + apache-client + + + + + software.amazon.awssdk + netty-nio-client + true + + + software.amazon.awssdk + url-connection-client + true + + + software.amazon.awssdk + apache-client + true + + + + org.jboss.logging + commons-logging-jboss-logging + + + + + + + io.quarkus + quarkus-bootstrap-maven-plugin + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + diff --git a/extensions/amazon-services/secretsmanager/runtime/src/main/java/io/quarkus/amazon/secretsmanager/runtime/SecretsManagerBuildTimeConfig.java b/extensions/amazon-services/secretsmanager/runtime/src/main/java/io/quarkus/amazon/secretsmanager/runtime/SecretsManagerBuildTimeConfig.java new file mode 100644 index 0000000000000..773446cdb500a --- /dev/null +++ b/extensions/amazon-services/secretsmanager/runtime/src/main/java/io/quarkus/amazon/secretsmanager/runtime/SecretsManagerBuildTimeConfig.java @@ -0,0 +1,26 @@ +package io.quarkus.amazon.secretsmanager.runtime; + +import io.quarkus.amazon.common.runtime.SdkBuildTimeConfig; +import io.quarkus.amazon.common.runtime.SyncHttpClientBuildTimeConfig; +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +/** + * Amazon Secrets Manager build time configuration + */ +@ConfigRoot(name = "secretsmanager", phase = ConfigPhase.BUILD_AND_RUN_TIME_FIXED) +public class SecretsManagerBuildTimeConfig { + + /** + * SDK client configurations for AWS Secrets Manager client + */ + @ConfigItem(name = ConfigItem.PARENT) + public SdkBuildTimeConfig sdk; + + /** + * Sync HTTP transport configuration for Amazon Secrets Manager client + */ + @ConfigItem + public SyncHttpClientBuildTimeConfig syncClient; +} diff --git a/extensions/amazon-services/secretsmanager/runtime/src/main/java/io/quarkus/amazon/secretsmanager/runtime/SecretsManagerClientProducer.java b/extensions/amazon-services/secretsmanager/runtime/src/main/java/io/quarkus/amazon/secretsmanager/runtime/SecretsManagerClientProducer.java new file mode 100644 index 0000000000000..cb25405aeab41 --- /dev/null +++ b/extensions/amazon-services/secretsmanager/runtime/src/main/java/io/quarkus/amazon/secretsmanager/runtime/SecretsManagerClientProducer.java @@ -0,0 +1,51 @@ +package io.quarkus.amazon.secretsmanager.runtime; + +import javax.annotation.PreDestroy; +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.Instance; +import javax.enterprise.inject.Produces; + +import software.amazon.awssdk.services.secretsmanager.SecretsManagerAsyncClient; +import software.amazon.awssdk.services.secretsmanager.SecretsManagerAsyncClientBuilder; +import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient; +import software.amazon.awssdk.services.secretsmanager.SecretsManagerClientBuilder; + +@ApplicationScoped +public class SecretsManagerClientProducer { + private final SecretsManagerClient syncClient; + private final SecretsManagerAsyncClient asyncClient; + + SecretsManagerClientProducer(Instance syncClientBuilderInstance, + Instance asyncClientBuilderInstance) { + this.syncClient = syncClientBuilderInstance.isResolvable() ? syncClientBuilderInstance.get().build() : null; + this.asyncClient = asyncClientBuilderInstance.isResolvable() ? asyncClientBuilderInstance.get().build() : null; + } + + @Produces + @ApplicationScoped + public SecretsManagerClient client() { + if (syncClient == null) { + throw new IllegalStateException("The SecretsManagerClient is required but has not been detected/configured."); + } + return syncClient; + } + + @Produces + @ApplicationScoped + public SecretsManagerAsyncClient asyncClient() { + if (asyncClient == null) { + throw new IllegalStateException("The SecretsManagerAsyncClient is required but has not been detected/configured."); + } + return asyncClient; + } + + @PreDestroy + public void destroy() { + if (syncClient != null) { + syncClient.close(); + } + if (asyncClient != null) { + asyncClient.close(); + } + } +} diff --git a/extensions/amazon-services/secretsmanager/runtime/src/main/java/io/quarkus/amazon/secretsmanager/runtime/SecretsManagerConfig.java b/extensions/amazon-services/secretsmanager/runtime/src/main/java/io/quarkus/amazon/secretsmanager/runtime/SecretsManagerConfig.java new file mode 100644 index 0000000000000..8c454542322a1 --- /dev/null +++ b/extensions/amazon-services/secretsmanager/runtime/src/main/java/io/quarkus/amazon/secretsmanager/runtime/SecretsManagerConfig.java @@ -0,0 +1,42 @@ +package io.quarkus.amazon.secretsmanager.runtime; + +import io.quarkus.amazon.common.runtime.AwsConfig; +import io.quarkus.amazon.common.runtime.NettyHttpClientConfig; +import io.quarkus.amazon.common.runtime.SdkConfig; +import io.quarkus.amazon.common.runtime.SyncHttpClientConfig; +import io.quarkus.runtime.annotations.ConfigDocSection; +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +@ConfigRoot(name = "secretsmanager", phase = ConfigPhase.RUN_TIME) +public class SecretsManagerConfig { + + /** + * AWS SDK client configurations + */ + @ConfigItem(name = ConfigItem.PARENT) + @ConfigDocSection + public SdkConfig sdk; + + /** + * AWS services configurations + */ + @ConfigItem + @ConfigDocSection + public AwsConfig aws; + + /** + * Sync HTTP transport configurations + */ + @ConfigItem + @ConfigDocSection + public SyncHttpClientConfig syncClient; + + /** + * Netty HTTP transport configurations + */ + @ConfigItem + @ConfigDocSection + public NettyHttpClientConfig asyncClient; +} diff --git a/extensions/amazon-services/secretsmanager/runtime/src/main/java/io/quarkus/amazon/secretsmanager/runtime/SecretsManagerRecorder.java b/extensions/amazon-services/secretsmanager/runtime/src/main/java/io/quarkus/amazon/secretsmanager/runtime/SecretsManagerRecorder.java new file mode 100644 index 0000000000000..4a19099757ea3 --- /dev/null +++ b/extensions/amazon-services/secretsmanager/runtime/src/main/java/io/quarkus/amazon/secretsmanager/runtime/SecretsManagerRecorder.java @@ -0,0 +1,54 @@ +package io.quarkus.amazon.secretsmanager.runtime; + +import io.quarkus.amazon.common.runtime.AwsConfig; +import io.quarkus.amazon.common.runtime.NettyHttpClientConfig; +import io.quarkus.amazon.common.runtime.SdkConfig; +import io.quarkus.amazon.common.runtime.SyncHttpClientConfig; +import io.quarkus.runtime.RuntimeValue; +import io.quarkus.runtime.annotations.Recorder; +import software.amazon.awssdk.awscore.client.builder.AwsClientBuilder; +import software.amazon.awssdk.http.SdkHttpClient; +import software.amazon.awssdk.http.async.SdkAsyncHttpClient; +import software.amazon.awssdk.services.secretsmanager.SecretsManagerAsyncClient; +import software.amazon.awssdk.services.secretsmanager.SecretsManagerAsyncClientBuilder; +import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient; +import software.amazon.awssdk.services.secretsmanager.SecretsManagerClientBuilder; + +@Recorder +public class SecretsManagerRecorder { + + public RuntimeValue getSyncConfig(SecretsManagerConfig config) { + return new RuntimeValue<>(config.syncClient); + } + + public RuntimeValue getAsyncConfig(SecretsManagerConfig config) { + return new RuntimeValue<>(config.asyncClient); + } + + public RuntimeValue getAwsConfig(SecretsManagerConfig config) { + return new RuntimeValue<>(config.aws); + } + + public RuntimeValue getSdkConfig(SecretsManagerConfig config) { + return new RuntimeValue<>(config.sdk); + } + + public RuntimeValue createSyncBuilder(SecretsManagerConfig config, + RuntimeValue transport) { + SecretsManagerClientBuilder builder = SecretsManagerClient.builder(); + if (transport != null) { + builder.httpClientBuilder(transport.getValue()); + } + return new RuntimeValue<>(builder); + } + + public RuntimeValue createAsyncBuilder(SecretsManagerConfig config, + RuntimeValue transport) { + + SecretsManagerAsyncClientBuilder builder = SecretsManagerAsyncClient.builder(); + if (transport != null) { + builder.httpClientBuilder(transport.getValue()); + } + return new RuntimeValue<>(builder); + } +} diff --git a/extensions/amazon-services/secretsmanager/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/amazon-services/secretsmanager/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 0000000000000..a49c80f350b69 --- /dev/null +++ b/extensions/amazon-services/secretsmanager/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,14 @@ +--- +artifact: ${project.groupId}:${project.artifactId}:${project.version} +name: "Amazon Secrets Manager" +metadata: + keywords: + - "secretsmanager" + - "aws" + - "amazon" + guide: "https://quarkus.io/guides/amazon-secretsmanager" + categories: + - "data" + status: "preview" + config: + - "quarkus.secretsmanager." diff --git a/integration-tests/amazon-services/pom.xml b/integration-tests/amazon-services/pom.xml index 8eb69b689a76f..6f20e89f7a5eb 100644 --- a/integration-tests/amazon-services/pom.xml +++ b/integration-tests/amazon-services/pom.xml @@ -22,6 +22,7 @@ http://localhost:8012 http://localhost:8013 http://localhost:8014 + http://localhost:8015 @@ -69,6 +70,10 @@ io.quarkus quarkus-amazon-ssm + + io.quarkus + quarkus-amazon-secretsmanager + software.amazon.awssdk netty-nio-client @@ -201,6 +206,19 @@ + + io.quarkus + quarkus-amazon-secretsmanager-deployment + ${project.version} + pom + test + + + * + * + + + io.quarkus quarkus-arc-deployment @@ -321,7 +339,7 @@ aws-local-stack - s3,dynamodb,sns,sqs,kms,ssm,ses + s3,dynamodb,sns,sqs,kms,ssm,ses,secretsmanager 0 @@ -333,6 +351,7 @@ 8012:4566 8013:4593 8014:4583 + 8015:4584 diff --git a/integration-tests/amazon-services/src/main/java/io/quarkus/it/amazon/secretsmanager/SecretsManagerResource.java b/integration-tests/amazon-services/src/main/java/io/quarkus/it/amazon/secretsmanager/SecretsManagerResource.java new file mode 100644 index 0000000000000..1d3ea2d146bc6 --- /dev/null +++ b/integration-tests/amazon-services/src/main/java/io/quarkus/it/amazon/secretsmanager/SecretsManagerResource.java @@ -0,0 +1,54 @@ +package io.quarkus.it.amazon.secretsmanager; + +import static javax.ws.rs.core.MediaType.TEXT_PLAIN; + +import java.util.UUID; +import java.util.concurrent.CompletionStage; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; + +import org.jboss.logging.Logger; + +import software.amazon.awssdk.services.secretsmanager.SecretsManagerAsyncClient; +import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient; +import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueResponse; + +@Path("/secretsmanager") +public class SecretsManagerResource { + + private static final Logger LOG = Logger.getLogger(SecretsManagerResource.class); + public final static String TEXT = "Quarkus is awsome"; + private static final String SYNC_PARAM = "quarkus/sync-" + UUID.randomUUID().toString(); + private static final String ASYNC_PARAM = "quarkus/async-" + UUID.randomUUID().toString(); + + @Inject + SecretsManagerClient secretsManagerClient; + + @Inject + SecretsManagerAsyncClient secretsManagerAsyncClient; + + @GET + @Path("sync") + @Produces(TEXT_PLAIN) + public String testSync() { + LOG.info("Testing Sync Secrets Manager client with secret name: " + SYNC_PARAM); + //Put parameter + secretsManagerClient.createSecret(r -> r.name(SYNC_PARAM).secretString(TEXT)); + //Get parameter + return secretsManagerClient.getSecretValue(r -> r.secretId(SYNC_PARAM)).secretString(); + } + + @GET + @Path("async") + @Produces(TEXT_PLAIN) + public CompletionStage testAsync() { + LOG.info("Testing Async SSM client with parameter: " + ASYNC_PARAM); + //Put and get parameter + return secretsManagerAsyncClient.createSecret(r -> r.name(ASYNC_PARAM).secretString(TEXT)) + .thenCompose(result -> secretsManagerAsyncClient.getSecretValue(r -> r.secretId(ASYNC_PARAM))) + .thenApply(GetSecretValueResponse::secretString); + } +} diff --git a/integration-tests/amazon-services/src/main/resources/application.properties b/integration-tests/amazon-services/src/main/resources/application.properties index 9bdb8d9282aa5..81164394640ae 100644 --- a/integration-tests/amazon-services/src/main/resources/application.properties +++ b/integration-tests/amazon-services/src/main/resources/application.properties @@ -48,4 +48,10 @@ quarkus.iam.endpoint-override=${iam.url} quarkus.iam.aws.region=us-east-1 quarkus.iam.aws.credentials.type=static quarkus.iam.aws.credentials.static-provider.access-key-id=test-key -quarkus.iam.aws.credentials.static-provider.secret-access-key=test-secret \ No newline at end of file +quarkus.iam.aws.credentials.static-provider.secret-access-key=test-secret + +quarkus.secretsmanager.endpoint-override=${secretsmanager.url} +quarkus.secretsmanager.aws.region=us-east-1 +quarkus.secretsmanager.aws.credentials.type=static +quarkus.secretsmanager.aws.credentials.static-provider.access-key-id=test-key +quarkus.secretsmanager.aws.credentials.static-provider.secret-access-key=test-secret \ No newline at end of file diff --git a/integration-tests/amazon-services/src/test/java/io/quarkus/it/amazon/AmazonSecretsManagerITCase.java b/integration-tests/amazon-services/src/test/java/io/quarkus/it/amazon/AmazonSecretsManagerITCase.java new file mode 100644 index 0000000000000..7f05a348fa4b3 --- /dev/null +++ b/integration-tests/amazon-services/src/test/java/io/quarkus/it/amazon/AmazonSecretsManagerITCase.java @@ -0,0 +1,8 @@ +package io.quarkus.it.amazon; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class AmazonSecretsManagerITCase extends AmazonSecretsManagerTest { + +} diff --git a/integration-tests/amazon-services/src/test/java/io/quarkus/it/amazon/AmazonSecretsManagerTest.java b/integration-tests/amazon-services/src/test/java/io/quarkus/it/amazon/AmazonSecretsManagerTest.java new file mode 100644 index 0000000000000..ab6cad2016625 --- /dev/null +++ b/integration-tests/amazon-services/src/test/java/io/quarkus/it/amazon/AmazonSecretsManagerTest.java @@ -0,0 +1,22 @@ +package io.quarkus.it.amazon; + +import static org.hamcrest.Matchers.is; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.RestAssured; + +@QuarkusTest +public class AmazonSecretsManagerTest { + + @Test + public void testSecretsManagerAsync() { + RestAssured.when().get("/test/secretsmanager/async").then().body(is("Quarkus is awsome")); + } + + @Test + public void testSecretsManagerSync() { + RestAssured.when().get("/test/secretsmanager/sync").then().body(is("Quarkus is awsome")); + } +}