diff --git a/.github/workflows/graalvm.yml b/.github/workflows/graalvm.yml index 54aec915a0..23d722775d 100644 --- a/.github/workflows/graalvm.yml +++ b/.github/workflows/graalvm.yml @@ -58,7 +58,7 @@ jobs: PREDICTIVE_TEST_SELECTION: "${{ github.event_name == 'pull_request' && 'true' || 'false' }}" - name: Publish Test Report if: always() - uses: mikepenz/action-junit-report@v3.3.3 + uses: mikepenz/action-junit-report@v3.5.2 with: check_name: GraalVM CE CI / Test Report (Java ${{ matrix.java }}) report_paths: '**/build/test-results/test/TEST-*.xml' diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index ec42d9708e..6f29dbb728 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -60,7 +60,7 @@ jobs: PREDICTIVE_TEST_SELECTION: "${{ github.event_name == 'pull_request' && 'true' || 'false' }}" - name: Publish Test Report if: always() - uses: mikepenz/action-junit-report@v3.3.3 + uses: mikepenz/action-junit-report@v3.5.2 with: check_name: Java CI / Test Report (${{ matrix.java }}) report_paths: '**/build/test-results/test/TEST-*.xml' diff --git a/aws-alexa-httpserver/build.gradle b/aws-alexa-httpserver/build.gradle deleted file mode 100644 index 8fd4f5ae3e..0000000000 --- a/aws-alexa-httpserver/build.gradle +++ /dev/null @@ -1,24 +0,0 @@ -plugins { - id("io.micronaut.build.internal.aws-module") -} - -dependencies { - annotationProcessor mn.micronaut.validation - - implementation mn.micronaut.validation - - api projects.awsAlexa - - implementation mn.micronaut.http.server - api(libs.managed.alexa.ask.sdk.core) - - testImplementation mn.micronaut.http.client - testImplementation mn.micronaut.http.server.netty - testImplementation libs.bouncycastle.provider - testImplementation (libs.alexa.ask.sdk) { - transitive(false) - } - testImplementation libs.alexa.ask.sdk.apache.client - - testRuntimeOnly libs.jcl.over.slf4j -} diff --git a/aws-alexa-httpserver/build.gradle.kts b/aws-alexa-httpserver/build.gradle.kts new file mode 100644 index 0000000000..10d83b53da --- /dev/null +++ b/aws-alexa-httpserver/build.gradle.kts @@ -0,0 +1,24 @@ +plugins { + id("io.micronaut.build.internal.module") +} + +dependencies { + annotationProcessor(mn.micronaut.validation) + + implementation(mn.micronaut.validation) + + api(project(":aws-alexa")) + + implementation(mn.micronaut.http.server) + api(libs.managed.alexa.ask.sdk.core) + + testImplementation(mn.micronaut.http.client) + testImplementation(mn.micronaut.http.server.netty) + testImplementation(libs.bouncycastle.provider) + testImplementation(libs.alexa.ask.sdk) { + isTransitive = false + } + testImplementation(libs.alexa.ask.sdk.apache.client) + + testRuntimeOnly(libs.jcl.over.slf4j) +} diff --git a/aws-alexa/build.gradle b/aws-alexa/build.gradle deleted file mode 100644 index a6f4a138fc..0000000000 --- a/aws-alexa/build.gradle +++ /dev/null @@ -1,23 +0,0 @@ -plugins { - id("io.micronaut.build.internal.aws-module") -} - -dependencies { - annotationProcessor mn.micronaut.validation - - implementation mn.micronaut.validation - - compileOnly(libs.alexa.ask.sdk) - api(libs.managed.alexa.ask.sdk.core) - - testAnnotationProcessor mn.micronaut.inject.java - testImplementation(libs.alexa.ask.sdk) { - transitive(false) - } - testImplementation libs.alexa.ask.sdk.apache.client - - testImplementation mn.micronaut.http.client - testImplementation mn.micronaut.http.server.netty - testImplementation mn.groovy.json - testRuntimeOnly libs.jcl.over.slf4j -} diff --git a/aws-alexa/build.gradle.kts b/aws-alexa/build.gradle.kts new file mode 100644 index 0000000000..0cb72bdf30 --- /dev/null +++ b/aws-alexa/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + id("io.micronaut.build.internal.module") +} + +dependencies { + annotationProcessor(mn.micronaut.validation) + + implementation(mn.micronaut.validation) + + compileOnly(libs.alexa.ask.sdk) + api(libs.managed.alexa.ask.sdk.core) + + testAnnotationProcessor(mn.micronaut.inject.java) + testImplementation(libs.alexa.ask.sdk) { + isTransitive = false + } + testImplementation(libs.alexa.ask.sdk.apache.client) + + testImplementation(mn.micronaut.http.client) + testImplementation(mn.micronaut.http.server.netty) + testImplementation(mn.groovy.json) + testRuntimeOnly(libs.jcl.over.slf4j) +} diff --git a/aws-alexa/src/main/java/io/micronaut/aws/alexa/locale/CompositeHandlerInputLocaleResolver.java b/aws-alexa/src/main/java/io/micronaut/aws/alexa/locale/CompositeHandlerInputLocaleResolver.java new file mode 100644 index 0000000000..1220b2fefa --- /dev/null +++ b/aws-alexa/src/main/java/io/micronaut/aws/alexa/locale/CompositeHandlerInputLocaleResolver.java @@ -0,0 +1,58 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.aws.alexa.locale; + +import com.amazon.ask.dispatcher.request.handler.HandlerInput; +import io.micronaut.context.annotation.Primary; +import io.micronaut.core.annotation.NonNull; +import jakarta.inject.Singleton; + +import java.util.Arrays; +import java.util.Locale; +import java.util.Optional; + + +/** + * {@link Primary} {@link HandlerInputLocaleResolver} which evaluates every {@link HandlerInputLocaleResolver} by order to resolve a {@link java.util.Locale}. + * @author Sergio del Amo + * @since 3.10.0 + */ +@Primary +@Singleton +public class CompositeHandlerInputLocaleResolver extends HandlerInputAbstractLocaleResolver { + + private final HandlerInputLocaleResolver[] localeResolvers; + + /** + * @param localeResolvers Locale Resolvers + * @param handlerInputLocaleResolutionConfiguration Locale Resolution configuration for HTTP Requests + */ + public CompositeHandlerInputLocaleResolver(HandlerInputLocaleResolver[] localeResolvers, + HandlerInputLocaleResolutionConfiguration handlerInputLocaleResolutionConfiguration) { + super(handlerInputLocaleResolutionConfiguration); + this.localeResolvers = localeResolvers; + } + + @Override + @NonNull + public Optional resolve(@NonNull HandlerInput request) { + return Arrays.stream(localeResolvers) + .map(resolver -> resolver.resolve(request)) + .filter(Optional::isPresent) + .findFirst() + .orElse(Optional.empty()); + } +} diff --git a/aws-alexa/src/main/java/io/micronaut/aws/alexa/locale/DefaultHandlerInputLocaleResolver.java b/aws-alexa/src/main/java/io/micronaut/aws/alexa/locale/DefaultHandlerInputLocaleResolver.java new file mode 100644 index 0000000000..20c68653fc --- /dev/null +++ b/aws-alexa/src/main/java/io/micronaut/aws/alexa/locale/DefaultHandlerInputLocaleResolver.java @@ -0,0 +1,51 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.aws.alexa.locale; + +import com.amazon.ask.dispatcher.request.handler.HandlerInput; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.util.StringUtils; +import jakarta.inject.Singleton; + +import java.util.Locale; +import java.util.Optional; + +/** + * Resolves {@link Locale} from the {@link HandlerInput} request. + * @see Request Locale. + * @author Sergio del Amo + * @since 3.10.0 + */ +@Singleton +public class DefaultHandlerInputLocaleResolver extends HandlerInputAbstractLocaleResolver { + + /** + * @param localeResolutionConfiguration The locale resolution configuration + */ + protected DefaultHandlerInputLocaleResolver(HandlerInputLocaleResolutionConfiguration localeResolutionConfiguration) { + super(localeResolutionConfiguration); + } + + @Override + @NonNull + public Optional resolve(@NonNull HandlerInput input) { + String languageTag = input.getRequest().getLocale(); + if (StringUtils.isEmpty(languageTag)) { + return Optional.empty(); + } + return Optional.of(Locale.forLanguageTag(languageTag)); + } +} diff --git a/aws-alexa/src/main/java/io/micronaut/aws/alexa/locale/HandlerInputAbstractLocaleResolver.java b/aws-alexa/src/main/java/io/micronaut/aws/alexa/locale/HandlerInputAbstractLocaleResolver.java new file mode 100644 index 0000000000..862967c7ae --- /dev/null +++ b/aws-alexa/src/main/java/io/micronaut/aws/alexa/locale/HandlerInputAbstractLocaleResolver.java @@ -0,0 +1,43 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.aws.alexa.locale; + +import com.amazon.ask.dispatcher.request.handler.HandlerInput; +import io.micronaut.core.util.locale.AbstractLocaleResolver; + +/** + * Provides an abstract class which implements {@link io.micronaut.core.util.LocaleResolver} and handles default locale resolution. + * @author Sergio del Amo + * @since 3.10.0 + */ +public abstract class HandlerInputAbstractLocaleResolver extends AbstractLocaleResolver implements HandlerInputLocaleResolver { + public static final Integer ORDER = 50; + + protected final HandlerInputLocaleResolutionConfiguration localeResolutionConfiguration; + + /** + * @param localeResolutionConfiguration The locale resolution configuration + */ + protected HandlerInputAbstractLocaleResolver(HandlerInputLocaleResolutionConfiguration localeResolutionConfiguration) { + super(localeResolutionConfiguration.getDefaultLocale()); + this.localeResolutionConfiguration = localeResolutionConfiguration; + } + + @Override + public int getOrder() { + return ORDER; + } +} diff --git a/aws-alexa/src/main/java/io/micronaut/aws/alexa/locale/HandlerInputFixedLocaleResolver.java b/aws-alexa/src/main/java/io/micronaut/aws/alexa/locale/HandlerInputFixedLocaleResolver.java new file mode 100644 index 0000000000..8eacd0d08f --- /dev/null +++ b/aws-alexa/src/main/java/io/micronaut/aws/alexa/locale/HandlerInputFixedLocaleResolver.java @@ -0,0 +1,47 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.aws.alexa.locale; + +import com.amazon.ask.dispatcher.request.handler.HandlerInput; +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.order.Ordered; +import io.micronaut.core.util.locale.FixedLocaleResolver; +import jakarta.inject.Singleton; + +/** + * Generic implementation of {@link io.micronaut.core.util.LocaleResolver} for fixed locale resolution. + * + * @author Sergio del Amo + * @since 3.10.0 + */ +@Singleton +@Requires(property = HandlerInputLocaleResolutionConfigurationProperties.PREFIX + ".fixed") +public class HandlerInputFixedLocaleResolver extends FixedLocaleResolver implements HandlerInputLocaleResolver { + + public static final Integer ORDER = Ordered.HIGHEST_PRECEDENCE + 100; + + /** + * @param localeResolutionConfiguration The locale resolution configuration + */ + public HandlerInputFixedLocaleResolver(HandlerInputLocaleResolutionConfiguration localeResolutionConfiguration) { + super(localeResolutionConfiguration.getFixed().orElseThrow(() -> new IllegalArgumentException("The fixed locale must be set"))); + } + + @Override + public int getOrder() { + return ORDER; + } +} diff --git a/aws-alexa/src/main/java/io/micronaut/aws/alexa/locale/HandlerInputLocaleResolutionConfiguration.java b/aws-alexa/src/main/java/io/micronaut/aws/alexa/locale/HandlerInputLocaleResolutionConfiguration.java new file mode 100644 index 0000000000..31ec58f6ac --- /dev/null +++ b/aws-alexa/src/main/java/io/micronaut/aws/alexa/locale/HandlerInputLocaleResolutionConfiguration.java @@ -0,0 +1,25 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.aws.alexa.locale; + +import io.micronaut.core.util.locale.LocaleResolutionConfiguration; + +/** + * @author Sergio del Amo + * @since 3.10.0 + */ +public interface HandlerInputLocaleResolutionConfiguration extends LocaleResolutionConfiguration { +} diff --git a/aws-alexa/src/main/java/io/micronaut/aws/alexa/locale/HandlerInputLocaleResolutionConfigurationProperties.java b/aws-alexa/src/main/java/io/micronaut/aws/alexa/locale/HandlerInputLocaleResolutionConfigurationProperties.java new file mode 100644 index 0000000000..a6b23a5317 --- /dev/null +++ b/aws-alexa/src/main/java/io/micronaut/aws/alexa/locale/HandlerInputLocaleResolutionConfigurationProperties.java @@ -0,0 +1,80 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.aws.alexa.locale; + +import io.micronaut.aws.alexa.conf.AlexaSkillConfigurationProperties; +import io.micronaut.context.annotation.ConfigurationProperties; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; + +import java.util.Locale; +import java.util.Optional; + +/** + * {@link ConfigurationProperties} implementation of {@link HandlerInputLocaleResolutionConfiguration}. + * @author Sergio del Amo + * @since 3.10.0 + */ +@ConfigurationProperties(HandlerInputLocaleResolutionConfigurationProperties.PREFIX) +public class HandlerInputLocaleResolutionConfigurationProperties implements HandlerInputLocaleResolutionConfiguration { + public static final String PREFIX = AlexaSkillConfigurationProperties.PREFIX + ".locale-resolution"; + + /** + * The default locale. + */ + @SuppressWarnings("WeakerAccess") + public static final String DEFAULT_LOCALE = "en-US"; + + private Locale fixed; + private Locale defaultLocale = Locale.forLanguageTag(DEFAULT_LOCALE); + + /** + * @return The fixed locale + */ + @NonNull + public Optional getFixed() { + return Optional.ofNullable(fixed); + } + + /** + * Sets the fixed locale. Any of ar-SA, de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR + * + * @param fixed The fixed locale + */ + public void setFixed(@Nullable Locale fixed) { + this.fixed = fixed; + } + + /** + * @return The locale to be used if one cannot be resolved. + */ + @Override + @NonNull + public Locale getDefaultLocale() { + return defaultLocale; + } + + /** + * Sets the locale that will be used if the locale cannot be + * resolved through any means. Defaults to {@value #DEFAULT_LOCALE}. + * + * @param defaultLocale The default locale. + */ + public void setDefaultLocale(@NonNull Locale defaultLocale) { + this.defaultLocale = defaultLocale; + } + +} diff --git a/aws-alexa/src/main/java/io/micronaut/aws/alexa/locale/HandlerInputLocaleResolver.java b/aws-alexa/src/main/java/io/micronaut/aws/alexa/locale/HandlerInputLocaleResolver.java new file mode 100644 index 0000000000..11b8fd6561 --- /dev/null +++ b/aws-alexa/src/main/java/io/micronaut/aws/alexa/locale/HandlerInputLocaleResolver.java @@ -0,0 +1,29 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.aws.alexa.locale; + +import com.amazon.ask.dispatcher.request.handler.HandlerInput; +import io.micronaut.core.annotation.Indexed; +import io.micronaut.core.util.LocaleResolver; + +/** + * Responsible for determining the current locale for a {@link HandlerInput} event. + * @author Sergio del Amo + * @since 3.10.0 + */ +@Indexed(HandlerInputLocaleResolver.class) +public interface HandlerInputLocaleResolver extends LocaleResolver { +} diff --git a/aws-alexa/src/test/groovy/io/micronaut/aws/alexa/locale/DefaultLocaleResolverSpec.groovy b/aws-alexa/src/test/groovy/io/micronaut/aws/alexa/locale/DefaultLocaleResolverSpec.groovy new file mode 100644 index 0000000000..31228af674 --- /dev/null +++ b/aws-alexa/src/test/groovy/io/micronaut/aws/alexa/locale/DefaultLocaleResolverSpec.groovy @@ -0,0 +1,35 @@ +package io.micronaut.aws.alexa.locale + +import com.amazon.ask.dispatcher.request.handler.HandlerInput +import com.amazon.ask.model.Request +import io.micronaut.context.BeanContext +import io.micronaut.context.annotation.Property +import io.micronaut.core.util.LocaleResolver +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import jakarta.inject.Inject +import spock.lang.Specification + +@MicronautTest(startApplication = false) +class DefaultLocaleResolverSpec extends Specification { + + @Inject + BeanContext beanContext + + @Inject + LocaleResolver localeResolver + + void "default locale is en-US"() { + given: + def request = Stub(Request) { + getLocale() >> "" + } + def input = Stub(HandlerInput) { + getRequest() >> request + } + when: + Locale locale = localeResolver.resolveOrDefault(input) + + then: + new Locale("en", "US") == locale + } +} diff --git a/aws-alexa/src/test/groovy/io/micronaut/aws/alexa/locale/HandlerInputFixedLocaleResolverDisabledSpec.groovy b/aws-alexa/src/test/groovy/io/micronaut/aws/alexa/locale/HandlerInputFixedLocaleResolverDisabledSpec.groovy new file mode 100644 index 0000000000..1782c9beec --- /dev/null +++ b/aws-alexa/src/test/groovy/io/micronaut/aws/alexa/locale/HandlerInputFixedLocaleResolverDisabledSpec.groovy @@ -0,0 +1,19 @@ +package io.micronaut.aws.alexa.locale + +import io.micronaut.context.BeanContext +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import jakarta.inject.Inject +import spock.lang.Specification + +@MicronautTest(startApplication = false) +public class HandlerInputFixedLocaleResolverDisabledSpec extends Specification { + + @Inject + BeanContext beanContext + + void "HandlerInputFixedLocaleResolver is disabled by default"() { + expect: + !beanContext.containsBean(HandlerInputFixedLocaleResolver) + } + +} diff --git a/aws-alexa/src/test/groovy/io/micronaut/aws/alexa/locale/HandlerInputFixedLocaleResolverSpec.groovy b/aws-alexa/src/test/groovy/io/micronaut/aws/alexa/locale/HandlerInputFixedLocaleResolverSpec.groovy new file mode 100644 index 0000000000..ff7aa95f46 --- /dev/null +++ b/aws-alexa/src/test/groovy/io/micronaut/aws/alexa/locale/HandlerInputFixedLocaleResolverSpec.groovy @@ -0,0 +1,40 @@ +package io.micronaut.aws.alexa.locale + +import com.amazon.ask.dispatcher.request.handler.HandlerInput +import com.amazon.ask.model.Request +import io.micronaut.context.BeanContext +import io.micronaut.context.annotation.Property +import io.micronaut.core.util.LocaleResolver +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import jakarta.inject.Inject +import spock.lang.Specification + +@Property(name = "alexa.locale-resolution.fixed", value = "es-ES") +@MicronautTest(startApplication = false) +class HandlerInputFixedLocaleResolverSpec extends Specification { + + @Inject + BeanContext beanContext + + @Inject + LocaleResolver localeResolver + + void "fixed locale takes precedence"() { + given: + def request = Stub(Request) { + getLocale() >> "en-US" + } + def input = Stub(HandlerInput) { + getRequest() >> request + } + + expect: + beanContext.containsBean(HandlerInputFixedLocaleResolver) + + when: + Locale locale = localeResolver.resolveOrDefault(input) + + then: + new Locale("es", "ES") == locale + } +} diff --git a/aws-alexa/src/test/groovy/io/micronaut/aws/alexa/locale/HandlerInputLocaleResolverSpec.groovy b/aws-alexa/src/test/groovy/io/micronaut/aws/alexa/locale/HandlerInputLocaleResolverSpec.groovy new file mode 100644 index 0000000000..d35b0b948e --- /dev/null +++ b/aws-alexa/src/test/groovy/io/micronaut/aws/alexa/locale/HandlerInputLocaleResolverSpec.groovy @@ -0,0 +1,52 @@ +package io.micronaut.aws.alexa.locale + +import com.amazon.ask.dispatcher.request.handler.HandlerInput +import com.amazon.ask.model.Request +import io.micronaut.core.util.LocaleResolver +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import jakarta.inject.Inject +import spock.lang.Specification +import spock.lang.Unroll + +@MicronautTest(startApplication = false) +class HandlerInputLocaleResolverSpec extends Specification { + + @Inject + LocaleResolver localeResolver + + @Unroll + void "resolve locale from handler input"(String languageCode, Locale expected) { + given: + def request = Stub(Request) { + getLocale() >> languageCode + } + def input = Stub(HandlerInput) { + getRequest() >> request + } + + when: + Locale locale = localeResolver.resolveOrDefault(input) + + then: + expected == locale + + where: + languageCode | expected + 'ar-SA' | new Locale("ar", "SA") + 'de-DE' | Locale.GERMANY + 'en-AU' | new Locale("en", "AU") + 'en-CA' | Locale.CANADA + 'en-GB' | Locale.UK + 'en-IN' | new Locale("en", "IN") + 'en-US' | Locale.US + 'es-ES' | new Locale("es", "ES") + 'es-MX' | new Locale("es", "MX") + 'es-US' | new Locale("es", "US") + 'fr-CA' | Locale.CANADA_FRENCH + 'fr-FR' | Locale.FRANCE + 'hi-IN' | new Locale("hi", "IN") + 'it-IT' | Locale.ITALY + 'ja-JP' | Locale.JAPAN + 'pt-BR' | new Locale("pt", "BR") + } +} diff --git a/aws-bom/build.gradle b/aws-bom/build.gradle deleted file mode 100644 index a2429aa5ba..0000000000 --- a/aws-bom/build.gradle +++ /dev/null @@ -1,3 +0,0 @@ -plugins { - id 'io.micronaut.build.internal.bom' -} diff --git a/aws-bom/build.gradle.kts b/aws-bom/build.gradle.kts new file mode 100644 index 0000000000..1f39ff0d93 --- /dev/null +++ b/aws-bom/build.gradle.kts @@ -0,0 +1,3 @@ +plugins { + id("io.micronaut.build.internal.bom") +} diff --git a/aws-cdk/build.gradle b/aws-cdk/build.gradle.kts similarity index 64% rename from aws-cdk/build.gradle rename to aws-cdk/build.gradle.kts index 1e5525fdf1..8c732a8a42 100644 --- a/aws-cdk/build.gradle +++ b/aws-cdk/build.gradle.kts @@ -2,8 +2,11 @@ plugins { id("io.micronaut.build.internal.aws-module") } +val micronautVersion: String by project +val micronautStarterVersion: String by project + dependencies { - api libs.aws.cdk.lib + api(libs.aws.cdk.lib) api("io.micronaut.starter:micronaut-starter-api:$micronautStarterVersion") testImplementation(projects.functionAwsApiProxy) } diff --git a/aws-cloudwatch-logging/build.gradle.kts b/aws-cloudwatch-logging/build.gradle.kts new file mode 100644 index 0000000000..1555a71ef1 --- /dev/null +++ b/aws-cloudwatch-logging/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + id("io.micronaut.build.internal.module") +} + +dependencies { + api(project(":aws-sdk-v2")) + implementation(libs.logback.json.classic) + api(libs.awssdk.cloudwatchlogs) + api(mn.micronaut.runtime) + api(mn.micronaut.serde.jackson) + +} + +// TODO temporarily disable binary compatibility checks +micronautBuild { + binaryCompatibility { + enabled.set(false) + } +} diff --git a/aws-cloudwatch-logging/src/main/java/io/micronaut/aws/cloudwatch/logging/CloudWatchJsonFormatter.java b/aws-cloudwatch-logging/src/main/java/io/micronaut/aws/cloudwatch/logging/CloudWatchJsonFormatter.java new file mode 100644 index 0000000000..b304261356 --- /dev/null +++ b/aws-cloudwatch-logging/src/main/java/io/micronaut/aws/cloudwatch/logging/CloudWatchJsonFormatter.java @@ -0,0 +1,43 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.aws.cloudwatch.logging; + +import ch.qos.logback.contrib.json.JsonFormatter; +import io.micronaut.serde.ObjectMapper; +import io.micronaut.core.annotation.Internal; + +import java.io.IOException; +import java.util.Map; + +/** + * CloudWatch's implementation of the {@link JsonFormatter}. + * + * @author Nemanja Mikic + * @since 3.9.0 + */ +@Internal +public final class CloudWatchJsonFormatter implements JsonFormatter { + private ObjectMapper objectMapper; + + @Override + public String toJsonString(Map m) throws IOException { + if (objectMapper == null) { + objectMapper = ObjectMapper.getDefault(); + } + return objectMapper.writeValueAsString(m); + } + +} diff --git a/aws-cloudwatch-logging/src/main/java/io/micronaut/aws/cloudwatch/logging/CloudWatchLoggingAppender.java b/aws-cloudwatch-logging/src/main/java/io/micronaut/aws/cloudwatch/logging/CloudWatchLoggingAppender.java new file mode 100644 index 0000000000..28a26b4624 --- /dev/null +++ b/aws-cloudwatch-logging/src/main/java/io/micronaut/aws/cloudwatch/logging/CloudWatchLoggingAppender.java @@ -0,0 +1,362 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.aws.cloudwatch.logging; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.Appender; +import ch.qos.logback.core.AppenderBase; +import ch.qos.logback.core.encoder.Encoder; +import ch.qos.logback.core.net.QueueFactory; +import ch.qos.logback.core.spi.AppenderAttachable; +import ch.qos.logback.core.util.Duration; +import io.micronaut.core.annotation.Internal; +import software.amazon.awssdk.core.exception.SdkException; +import software.amazon.awssdk.services.cloudwatchlogs.model.CreateLogGroupRequest; +import software.amazon.awssdk.services.cloudwatchlogs.model.CreateLogStreamRequest; +import software.amazon.awssdk.services.cloudwatchlogs.model.InputLogEvent; +import software.amazon.awssdk.services.cloudwatchlogs.model.InvalidSequenceTokenException; +import software.amazon.awssdk.services.cloudwatchlogs.model.PutLogEventsRequest; +import software.amazon.awssdk.services.cloudwatchlogs.model.PutLogEventsResponse; +import software.amazon.awssdk.services.cloudwatchlogs.model.ResourceAlreadyExistsException; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.BlockingDeque; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +/** + * Cloudwatch log appender for logback. + * + * @author Nemanja Mikic + * @since 3.9.0 + */ +@Internal +public final class CloudWatchLoggingAppender extends AppenderBase implements AppenderAttachable { + + private static final int DEFAULT_QUEUE_SIZE = 128; + private static final int DEFAULT_MAX_BATCH_SIZE = 128; + private static final int PUT_REQUEST_RETRY_COUNT = 2; + private static final long DEFAULT_PUBLISH_PERIOD = 100; + private final QueueFactory queueFactory = new QueueFactory(); + private Duration eventDelayLimit; + private final List blackListLoggerName = new ArrayList<>(); + private Encoder encoder; + private Future task; + private BlockingDeque deque; + private int queueSize = DEFAULT_QUEUE_SIZE; + private long publishPeriod = DEFAULT_PUBLISH_PERIOD; + private Appender emergencyAppender; + private String sequenceToken = null; + private boolean configuredSuccessfully = false; + private boolean createGroupAndStream = true; + private int maxBatchSize = DEFAULT_MAX_BATCH_SIZE; + private String groupName; + private String streamName; + + public int getQueueSize() { + return queueSize; + } + + public void setQueueSize(int queueSize) { + this.queueSize = queueSize; + } + + public void addBlackListLoggerName(String test) { + this.blackListLoggerName.add(test); + } + + public String getGroupName() { + return groupName; + } + + public void setGroupName(String groupName) { + this.groupName = groupName; + } + + public String getStreamName() { + return streamName; + } + + public void setStreamName(String streamName) { + this.streamName = streamName; + } + + public long getPublishPeriod() { + return publishPeriod; + } + + public void setPublishPeriod(long publishPeriod) { + this.publishPeriod = publishPeriod; + } + + public int getMaxBatchSize() { + return maxBatchSize; + } + + public void setMaxBatchSize(int maxBatchSize) { + this.maxBatchSize = maxBatchSize; + } + + public boolean isCreateGroupAndStream() { + return createGroupAndStream; + } + + public void setCreateGroupAndStream(boolean createGroupAndStream) { + this.createGroupAndStream = createGroupAndStream; + } + + @Override + public void start() { + if (isStarted()) { + return; + } + + if (queueSize == 0) { + addWarn("Queue size of zero is deprecated, use a size of one to indicate synchronous processing"); + } + + if (queueSize < 0) { + addError("Queue size must be greater than zero"); + return; + } + + if (publishPeriod <= 0) { + addError("Publish period must be greater than zero"); + return; + } + + if (maxBatchSize <= 0) { + addError("Max Batch size must be greater than zero"); + return; + } + + if (encoder == null) { + addError("No encoder set for the appender named [" + name + "]."); + return; + } + + if (emergencyAppender != null && !emergencyAppender.isStarted()) { + emergencyAppender.start(); + } + + eventDelayLimit = new Duration(publishPeriod); + + deque = queueFactory.newLinkedBlockingDeque(queueSize); + + task = getContext().getScheduledExecutorService().scheduleAtFixedRate(() -> { + try { + dispatchEvents(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }, 0, 100, TimeUnit.MILLISECONDS); + super.start(); + } + + @Override + public void stop() { + if (!isStarted()) { + return; + } + task.cancel(true); + super.stop(); + } + + @Override + protected void append(ILoggingEvent eventObject) { + if (eventObject == null || !isStarted() || blackListLoggerName.contains(eventObject.getLoggerName())) { + return; + } + + try { + final boolean inserted = deque.offer(eventObject, eventDelayLimit.getMilliseconds(), TimeUnit.MILLISECONDS); + if (!inserted) { + addInfo("Dropping event due to timeout limit of [" + eventDelayLimit + "] being exceeded"); + } + } catch (InterruptedException e) { + addError("Interrupted while appending event to SocketAppender", e); + Thread.currentThread().interrupt(); + } + } + + public Encoder getEncoder() { + return encoder; + } + + public void setEncoder(Encoder encoder) { + this.encoder = encoder; + } + + private boolean tryToConfigure() { + + if (!CloudWatchLoggingClient.isReady()) { + return false; + } + + if (groupName == null) { + groupName = CloudWatchLoggingClient.getAppName(); + } + + if (streamName == null) { + streamName = CloudWatchLoggingClient.getHost(); + } + + if (createGroupAndStream) { + CreateLogGroupRequest createLogGroupRequest = CreateLogGroupRequest.builder().logGroupName(groupName).build(); + try { + CloudWatchLoggingClient.createLogGroup(createLogGroupRequest); + } catch (ResourceAlreadyExistsException e) { + addInfo(String.format("Log group %s already exists", groupName)); + } catch (SdkException e) { + addError(String.format("Error creating log group %s", groupName), e); + } + + CreateLogStreamRequest createLogStreamRequest = CreateLogStreamRequest.builder().logStreamName(streamName).logGroupName(groupName).build(); + try { + CloudWatchLoggingClient.createLogStream(createLogStreamRequest); + } catch (ResourceAlreadyExistsException e) { + addInfo(String.format("Log stream %s already exists", streamName)); + } catch (SdkException e) { + addError(String.format("Error stream log %s", streamName), e); + } + } + + configuredSuccessfully = true; + + return true; + } + + private void dispatchEvents() throws InterruptedException { + if (!configuredSuccessfully && !tryToConfigure()) { + return; + } + + List logEvents = new ArrayList<>(maxBatchSize); + List iLoggingEvents = new ArrayList<>(maxBatchSize); + + while (!deque.isEmpty() && logEvents.size() < maxBatchSize) { + ILoggingEvent event = deque.takeFirst(); + final InputLogEvent inputLogEvent = InputLogEvent.builder().message( + new String(encoder.encode(event)) + ).timestamp(event.getTimeStamp()).build(); + + iLoggingEvents.add(event); + logEvents.add(inputLogEvent); + } + if (!logEvents.isEmpty() && !sendLogsToCloudWatch(logEvents) && emergencyAppender != null) { + iLoggingEvents.forEach(emergencyAppender::doAppend); + } + + } + + private boolean sendLogsToCloudWatch(List logEvents) { + if (sequenceToken == null) { + try { + sequenceToken = CloudWatchLoggingClient.getToken(groupName, streamName); + } catch (SdkException e) { + addError("Getting token got error", e); + } + } + for (int i = 0; i < PUT_REQUEST_RETRY_COUNT; i++) { + try { + PutLogEventsResponse putLogEventsResponse = putLogs(logEvents, groupName, streamName, sequenceToken); + if (putLogEventsResponse == null || putLogEventsResponse.nextSequenceToken() == null) { + addError("Sending log request failed"); + } else { + sequenceToken = putLogEventsResponse.nextSequenceToken(); + return true; + } + } catch (InvalidSequenceTokenException e) { + sequenceToken = e.expectedSequenceToken(); + } catch (Exception e) { + addError("Sending log request failed", e); + return false; + } + } + return false; + } + + @Override + public void addAppender(Appender newAppender) { + if (emergencyAppender == null) { + emergencyAppender = newAppender; + } else { + addWarn("One and only one appender may be attached to " + getClass().getSimpleName()); + addWarn("Ignoring additional appender named [" + newAppender.getName() + "]"); + } + } + + @Override + public Iterator> iteratorForAppenders() { + throw new UnsupportedOperationException("Don't know how to create iterator"); + } + + @Override + public Appender getAppender(String name) { + if (emergencyAppender != null && name != null && name.equals(emergencyAppender.getName())) { + return emergencyAppender; + } else { + return null; + } + } + + @Override + public boolean isAttached(Appender appender) { + return (emergencyAppender == appender); + } + + @Override + public void detachAndStopAllAppenders() { + if (emergencyAppender != null) { + emergencyAppender.stop(); + emergencyAppender = null; + } + } + + @Override + public boolean detachAppender(Appender appender) { + if (emergencyAppender == appender) { + emergencyAppender = null; + return true; + } else { + return false; + } + } + + @Override + public boolean detachAppender(String name) { + if (emergencyAppender != null && emergencyAppender.getName().equals(name)) { + emergencyAppender = null; + return true; + } else { + return false; + } + } + + private PutLogEventsResponse putLogs(List logEvents, + String groupName, + String streamName, + String sequenceToken) { + return CloudWatchLoggingClient.putLogs(PutLogEventsRequest.builder() + .logEvents(logEvents) + .logGroupName(groupName) + .logStreamName(streamName) + .sequenceToken(sequenceToken) + .build()); + } +} diff --git a/aws-cloudwatch-logging/src/main/java/io/micronaut/aws/cloudwatch/logging/CloudWatchLoggingClient.java b/aws-cloudwatch-logging/src/main/java/io/micronaut/aws/cloudwatch/logging/CloudWatchLoggingClient.java new file mode 100644 index 0000000000..6fdf61b8dd --- /dev/null +++ b/aws-cloudwatch-logging/src/main/java/io/micronaut/aws/cloudwatch/logging/CloudWatchLoggingClient.java @@ -0,0 +1,129 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.aws.cloudwatch.logging; + +import io.micronaut.context.annotation.Context; +import io.micronaut.context.event.ApplicationEventListener; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.discovery.event.ServiceReadyEvent; +import io.micronaut.runtime.ApplicationConfiguration; +import jakarta.annotation.PreDestroy; +import jakarta.inject.Singleton; +import software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsClient; +import software.amazon.awssdk.services.cloudwatchlogs.model.CreateLogGroupRequest; +import software.amazon.awssdk.services.cloudwatchlogs.model.CreateLogStreamRequest; +import software.amazon.awssdk.services.cloudwatchlogs.model.DescribeLogStreamsRequest; +import software.amazon.awssdk.services.cloudwatchlogs.model.LogStream; +import software.amazon.awssdk.services.cloudwatchlogs.model.PutLogEventsRequest; +import software.amazon.awssdk.services.cloudwatchlogs.model.PutLogEventsResponse; + +import java.util.List; +import java.util.Optional; + +/** + * CloudWatchLoggingClient is a {@link CloudWatchLogsClient} client that is required for {@link CloudWatchLoggingAppender}. + * + * @author Nemanja Mikic + * @since 3.9.0 + */ +@Context +@Internal +@Singleton +final class CloudWatchLoggingClient implements ApplicationEventListener { + + private static CloudWatchLogsClient logging; + private static String host; + private static String appName; + private final CloudWatchLogsClient internalLogging; + private final String internalAppName; + + public CloudWatchLoggingClient(CloudWatchLogsClient logging, ApplicationConfiguration applicationConfiguration) { + this.internalLogging = logging; + this.internalAppName = applicationConfiguration.getName().orElse(""); + } + + static synchronized boolean isReady() { + return logging != null; + } + + static synchronized String getHost() { + return host; + } + + static synchronized String getAppName() { + return appName; + } + + private static synchronized void setLogging(CloudWatchLogsClient logging, String host, String appName) { + CloudWatchLoggingClient.logging = logging; + CloudWatchLoggingClient.host = host; + CloudWatchLoggingClient.appName = appName; + } + + static synchronized void destroy() { + CloudWatchLoggingClient.logging.close(); + CloudWatchLoggingClient.logging = null; + CloudWatchLoggingClient.host = null; + CloudWatchLoggingClient.appName = null; + } + + static synchronized PutLogEventsResponse putLogs(PutLogEventsRequest putLogsRequest) { + if (logging != null) { + return logging.putLogEvents(putLogsRequest); + } + return null; + } + + static synchronized void createLogGroup(CreateLogGroupRequest createLogGroupRequest) { + if (logging != null) { + logging.createLogGroup(createLogGroupRequest); + } + } + + static synchronized void createLogStream(CreateLogStreamRequest createLogStreamRequest) { + if (logging != null) { + logging.createLogStream(createLogStreamRequest); + } + } + + @Nullable + static synchronized String getToken(String groupName, String streamName) { + List logStreams = logging.describeLogStreams( + DescribeLogStreamsRequest.builder() + .logGroupName(groupName) + .logStreamNamePrefix(streamName) + .build()).logStreams(); + if (!logStreams.isEmpty()) { + Optional first = logStreams.stream().filter(x -> x.logStreamName().equals(streamName)).findFirst(); + if (first.isPresent()) { + return first.get().uploadSequenceToken(); + } + } + return null; + } + + @PreDestroy + public void close() { + CloudWatchLoggingClient.destroy(); + } + + @Override + public void onApplicationEvent(ServiceReadyEvent event) { + setLogging(internalLogging, event.getSource().getHost(), internalAppName); + } + +} diff --git a/aws-cloudwatch-logging/src/main/java/io/micronaut/aws/cloudwatch/logging/package-info.java b/aws-cloudwatch-logging/src/main/java/io/micronaut/aws/cloudwatch/logging/package-info.java new file mode 100644 index 0000000000..6f6d34129f --- /dev/null +++ b/aws-cloudwatch-logging/src/main/java/io/micronaut/aws/cloudwatch/logging/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Integration with AWS CloudWatch logging. + * + * @author Nemanja Mikic + * @since 3.9.0 + */ +package io.micronaut.aws.cloudwatch.logging; diff --git a/aws-cloudwatch-logging/src/test/groovy/io/micronaut/aws/cloudwatch/logging/CloudWatchLoggingAppenderSpec.groovy b/aws-cloudwatch-logging/src/test/groovy/io/micronaut/aws/cloudwatch/logging/CloudWatchLoggingAppenderSpec.groovy new file mode 100644 index 0000000000..696b643fe2 --- /dev/null +++ b/aws-cloudwatch-logging/src/test/groovy/io/micronaut/aws/cloudwatch/logging/CloudWatchLoggingAppenderSpec.groovy @@ -0,0 +1,282 @@ +package io.micronaut.aws.cloudwatch.logging + +import ch.qos.logback.classic.Level +import ch.qos.logback.classic.LoggerContext +import ch.qos.logback.classic.PatternLayout +import ch.qos.logback.classic.spi.LoggingEvent +import ch.qos.logback.core.encoder.LayoutWrappingEncoder +import io.micronaut.discovery.ServiceInstance +import io.micronaut.discovery.event.ServiceReadyEvent +import io.micronaut.runtime.ApplicationConfiguration +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +class CloudWatchLoggingAppenderSpec extends Specification { + + CloudWatchLoggingAppender appender + LoggerContext context + PatternLayout layout + LayoutWrappingEncoder encoder + CloudwatchLoggingSpec.MockLogging cloudWatchLogsClient + + def setup() { + context = new LoggerContext() + layout = new PatternLayout() + layout.context = context + layout.pattern = "[%thread] %level %logger{20} - %msg%n%xThrowable" + layout.start() + encoder = new LayoutWrappingEncoder() + encoder.layout = layout + encoder.start() + appender = new CloudWatchLoggingAppender() + appender.context = context + appender.encoder = encoder + def config = Stub(ApplicationConfiguration) { + getName() >> Optional.of("my-awesome-app") + } + def instance = Mock(ServiceInstance.class) + instance.getHost() >> "testHost" + def serviceReadyEvent = new ServiceReadyEvent(instance) + + cloudWatchLogsClient = new CloudwatchLoggingSpec.MockLogging() + + new CloudWatchLoggingClient(cloudWatchLogsClient, config).onApplicationEvent(serviceReadyEvent) + + } + + def cleanup() { + layout.stop() + encoder.stop() + appender.stop() + CloudWatchLoggingClient.destroy() + } + + void 'test error queue size less then 0'() { + when: + appender.queueSize = -1 + appender.start() + + then: + def statuses = context.getStatusManager().getCopyOfStatusList() + statuses.find { it.message == "Queue size must be greater than zero" } + } + + void 'test error queue size equal to 0'() { + when: + appender.queueSize = 0 + appender.start() + + then: + def statuses = context.getStatusManager().getCopyOfStatusList() + statuses.find { it.message == "Queue size of zero is deprecated, use a size of one to indicate synchronous processing" } + } + + void 'test error publish period less or equal to 0'() { + when: + appender.queueSize = 100 + appender.publishPeriod = 0 + appender.start() + + then: + def statuses = context.getStatusManager().getCopyOfStatusList() + statuses.find { it.message == "Publish period must be greater than zero" } + } + + void 'test error max batch size less or equal to 0'() { + when: + appender.maxBatchSize = 0 + appender.start() + + then: + def statuses = context.getStatusManager().getCopyOfStatusList() + statuses.find { it.message == "Max Batch size must be greater than zero" } + } + + void 'encoder not set'() { + when: + appender.queueSize = 100 + appender.publishPeriod = 100 + appender.encoder = null + appender.start() + + then: + def statuses = context.getStatusManager().getCopyOfStatusList() + statuses.find { it.message == "No encoder set for the appender named [null]." } + } + + void 'register multiple emergency appender'() { + when: + def mockAppender = new MockAppender() + appender.queueSize = 100 + appender.publishPeriod = 100 + appender.encoder = new LayoutWrappingEncoder() + appender.addAppender(mockAppender) + appender.addAppender(mockAppender) + + then: + def statuses = context.getStatusManager().getCopyOfStatusList() + statuses.find { it.message == "One and only one appender may be attached to CloudWatchLoggingAppender" } + statuses.find { it.message == "Ignoring additional appender named [MockAppender]" } + appender.getAppender("MockAppender") != null + appender.getAppender("NotExistingOne") == null + appender.isAttached(mockAppender) + appender.encoder != null + appender.queueSize == 100 + appender.publishPeriod == 100 + + appender.detachAndStopAllAppenders() + !appender.isAttached(mockAppender) + } + + void 'detach emergency appender by name'() { + when: + def mockAppender = new MockAppender() + appender.queueSize = 100 + appender.publishPeriod = 100 + appender.encoder = new LayoutWrappingEncoder() + appender.addAppender(mockAppender) + + then: + appender.detachAppender("MockAppender") + !appender.detachAppender("NotExistingOne") + } + + void 'detach emergency appender by instance'() { + when: + def mockAppender = new MockAppender() + appender.queueSize = 100 + appender.publishPeriod = 100 + appender.encoder = new LayoutWrappingEncoder() + appender.addAppender(mockAppender) + + then: + appender.detachAppender(mockAppender) + !appender.detachAppender(mockAppender) + } + + void 'try to create iterator for emergency appender'() { + when: + def mockAppender = new MockAppender() + appender.queueSize = 100 + appender.publishPeriod = 100 + appender.encoder = new LayoutWrappingEncoder() + appender.addAppender(mockAppender) + appender.iteratorForAppenders() + + then: + thrown(UnsupportedOperationException) + } + + void 'custom groupName and StreamName'() { + given: + def testGroup = "testGroup" + def testStream = "testStream" + def testMessage = "testMessage" + PollingConditions conditions = new PollingConditions(timeout: 10, initialDelay: 1.5, factor: 1.25) + LoggingEvent event = createEvent("name", Level.INFO, testMessage, System.currentTimeMillis()) + + when: + appender.groupName = testGroup + appender.streamName = testStream + appender.start() + appender.doAppend(event) + + then: + appender.groupName == testGroup + appender.streamName == testStream + conditions.eventually { + cloudWatchLogsClient.putLogsRequestList.size() == 1 + } + cloudWatchLogsClient.putLogsRequestList.get(0).logGroupName() == testGroup + cloudWatchLogsClient.putLogsRequestList.get(0).logStreamName() == testStream + + } + + void 'test create group and stream flag'() { + given: + def testMessage = "testMessage" + PollingConditions conditions = new PollingConditions(timeout: 10, initialDelay: 1.5, factor: 1.25) + LoggingEvent event = createEvent("name", Level.INFO, testMessage, System.currentTimeMillis()) + + when: + appender.maxBatchSize = 10 + appender.createGroupAndStream = false + appender.groupName = "test" + appender.streamName = "test" + cloudWatchLogsClient.resetCalls() + appender.start() + appender.doAppend(event) + + then: + appender.maxBatchSize == 10 + !appender.isCreateGroupAndStream() + conditions.eventually { + cloudWatchLogsClient.putLogsRequestList.size() == 1 + } + cloudWatchLogsClient.numberOfCalls.isEmpty() + } + + void 'test resource already exists when creating group and stream'() { + given: + def testMessage = "testMessage2" + def testGroup = "testGroup" + def testStream = "testStream" + PollingConditions conditions = new PollingConditions(timeout: 10, initialDelay: 1.5, factor: 1.25) + LoggingEvent event = createEvent("name", Level.INFO, testMessage, System.currentTimeMillis()) + + when: + appender.groupName = testGroup + appender.streamName = testStream + cloudWatchLogsClient.state = CloudwatchLoggingSpec.MockState.NOT_SUCCESSFUL + appender.start() + appender.doAppend(event) + + then: + conditions.eventually { + cloudWatchLogsClient.putLogsRequestList.size() == 2 + } + def statuses = context.getStatusManager().getCopyOfStatusList() + statuses.find { it.message == "Log group " + testGroup + " already exists" } + statuses.find { it.message == "Log stream " + testStream + " already exists" } + statuses.find { it.message == "Sending log request failed" } + } + + void 'test exception handling'() { + given: + def testMessage = "testMessage2" + def testGroup = "testGroup" + def testStream = "testStream" + + PollingConditions conditions = new PollingConditions(timeout: 10, initialDelay: 1.5, factor: 1.25) + LoggingEvent event = createEvent("name", Level.INFO, testMessage, System.currentTimeMillis()) + + when: + appender.groupName = testGroup + appender.streamName = testStream + cloudWatchLogsClient.state = CloudwatchLoggingSpec.MockState.EXCEPTION + appender.start() + appender.doAppend(event) + + then: + conditions.eventually { + cloudWatchLogsClient.putLogsRequestList.size() == 1 + } + def statuses = context.getStatusManager().getCopyOfStatusList() + statuses.find { it.message == "Error creating log group " + testGroup } + statuses.find { it.message == "Error stream log " + testStream } + statuses.find { it.message == "Sending log request failed" } + statuses.findAll { it.throwable != null }.size() == 3 + } + + LoggingEvent createEvent(String name, Level level, String message, Long time) { + LoggingEvent event = new LoggingEvent() + event.loggerName = name + event.level = level + event.message = message + if (time != null) { + event.timeStamp = time + } + return event + } + +} diff --git a/aws-cloudwatch-logging/src/test/groovy/io/micronaut/aws/cloudwatch/logging/CloudwatchLoggingSpec.groovy b/aws-cloudwatch-logging/src/test/groovy/io/micronaut/aws/cloudwatch/logging/CloudwatchLoggingSpec.groovy new file mode 100644 index 0000000000..54ff77902f --- /dev/null +++ b/aws-cloudwatch-logging/src/test/groovy/io/micronaut/aws/cloudwatch/logging/CloudwatchLoggingSpec.groovy @@ -0,0 +1,181 @@ +package io.micronaut.aws.cloudwatch.logging + +import io.micronaut.context.annotation.Property +import io.micronaut.context.annotation.Replaces +import io.micronaut.context.annotation.Requires +import io.micronaut.context.event.ApplicationEventPublisher +import io.micronaut.discovery.ServiceInstance +import io.micronaut.discovery.event.ServiceReadyEvent +import io.micronaut.runtime.ApplicationConfiguration +import io.micronaut.serde.ObjectMapper +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import jakarta.inject.Inject +import jakarta.inject.Singleton +import org.slf4j.LoggerFactory +import software.amazon.awssdk.awscore.exception.AwsServiceException +import software.amazon.awssdk.core.exception.SdkClientException +import software.amazon.awssdk.core.exception.SdkException +import software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsClient +import software.amazon.awssdk.services.cloudwatchlogs.model.* +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +@Property(name = "spec.name", value = "CloudwatchLoggingSpec") +@MicronautTest +class CloudwatchLoggingSpec extends Specification { + + @Inject + CloudWatchLogsClient logging + + @Inject + ApplicationEventPublisher eventPublisher + + @Inject + ApplicationConfiguration applicationConfiguration + + void "test Cloudwatch logging"() { + given: + def logMessage = 'test logging' + def testHost = 'testHost' + def logger = LoggerFactory.getLogger(CloudwatchLoggingSpec.class) + PollingConditions conditions = new PollingConditions(timeout: 10, initialDelay: 1.5, factor: 1.25) + + def mockLogging = (MockLogging) logging + def instance = Mock(ServiceInstance.class) + instance.getHost() >> testHost + def event = new ServiceReadyEvent(instance) + eventPublisher.publishEvent(event) + + when: + logger.info(logMessage) + + then: + conditions.eventually { + mockLogging.getPutLogsRequestList().size() != 0 + } + + def putLogRequestList = ((MockLogging) logging).getPutLogsRequestList() + putLogRequestList.stream().allMatch(x -> x.logGroupName() == applicationConfiguration.getName().get()) + putLogRequestList.stream().allMatch(x -> x.logStreamName() == testHost) + + ObjectMapper mapper = ObjectMapper.getDefault() + + def logEntries = new ArrayList>() + + putLogRequestList.forEach( + x -> { + x.logEvents().stream().forEach(y -> logEntries.add( + mapper.readValue(y.message(), HashMap.class) as Map + )) + } + ) + + logEntries.stream().anyMatch(x -> x.logger == 'io.micronaut.context.env.DefaultEnvironment') + logEntries.stream().anyMatch(x -> x.logger == 'io.micronaut.aws.cloudwatch.logging.CloudwatchLoggingSpec') + logEntries.stream().anyMatch(x -> x.message == logMessage) + MockAppender.getEvents().size() == 0 + + when: + mockLogging.state = MockState.NOT_SUCCESSFUL + logger.info(logMessage) + conditions = new PollingConditions(timeout: 10, initialDelay: 1.5, factor: 1.25) + + then: + conditions.eventually { + MockAppender.getEvents().size() == 1 + } + MockAppender.getEvents().get(0).message == logMessage + + when: + mockLogging.state = MockState.EXCEPTION + logger.info(logMessage + " from Exception") + conditions = new PollingConditions(timeout: 10, initialDelay: 1.5, factor: 1.25) + + then: + conditions.eventually { + MockAppender.getEvents().size() == 2 + } + MockAppender.getEvents().get(1).message == logMessage + " from Exception" + } + + static enum MockState { + SUCCESS, + NOT_SUCCESSFUL, + EXCEPTION + } + + @Requires(property = "spec.name", value = "CloudwatchLoggingSpec") + @Singleton + @Replaces(CloudWatchLogsClient) + static class MockLogging implements CloudWatchLogsClient { + + final List putLogsRequestList = Collections.synchronizedList(new ArrayList<>()) + + MockState state = MockState.SUCCESS + + Map numberOfCalls = new HashMap<>() + + @Override + CreateLogGroupResponse createLogGroup(CreateLogGroupRequest createLogGroupRequest) throws InvalidParameterException, ResourceAlreadyExistsException, LimitExceededException, OperationAbortedException, ServiceUnavailableException, AwsServiceException, SdkClientException, CloudWatchLogsException { + incrementVisit("createLogGroup") + if (state == MockState.SUCCESS) { + return CreateLogGroupResponse.builder().build() as CreateLogGroupResponse + } else if (state == MockState.NOT_SUCCESSFUL) { + throw ResourceAlreadyExistsException.builder().build() + } + throw SdkException.builder().message("testMessage").build() + } + + @Override + CreateLogStreamResponse createLogStream(CreateLogStreamRequest createLogStreamRequest) throws InvalidParameterException, ResourceAlreadyExistsException, ResourceNotFoundException, ServiceUnavailableException, AwsServiceException, SdkClientException, CloudWatchLogsException { + incrementVisit("createLogStream") + if (state == MockState.SUCCESS) { + return CreateLogStreamResponse.builder().build() as CreateLogStreamResponse + } else if (state == MockState.NOT_SUCCESSFUL) { + throw ResourceAlreadyExistsException.builder().build() + } + throw SdkException.builder().message("testMessage").build() + } + + @Override + DescribeLogStreamsResponse describeLogStreams(DescribeLogStreamsRequest describeLogStreamsRequest) throws InvalidParameterException, ResourceNotFoundException, ServiceUnavailableException, AwsServiceException, SdkClientException, CloudWatchLogsException { + if (state == MockState.SUCCESS) { + return DescribeLogStreamsResponse.builder() + .logStreams(LogStream.builder().uploadSequenceToken("dummyToken") + .logStreamName(describeLogStreamsRequest.logStreamNamePrefix()).build()) + .build() as DescribeLogStreamsResponse + } + return DescribeLogStreamsResponse.builder().build() as DescribeLogStreamsResponse + } + + @Override + PutLogEventsResponse putLogEvents(PutLogEventsRequest putLogEventsRequest) throws InvalidParameterException, InvalidSequenceTokenException, DataAlreadyAcceptedException, ResourceNotFoundException, ServiceUnavailableException, UnrecognizedClientException, AwsServiceException, SdkClientException, CloudWatchLogsException { + putLogsRequestList.add(putLogEventsRequest) + if (state == MockState.SUCCESS) { + return PutLogEventsResponse.builder().nextSequenceToken("nextSeqToken").build() as PutLogEventsResponse + } else if (state == MockState.NOT_SUCCESSFUL) { + return PutLogEventsResponse.builder().build() as PutLogEventsResponse + } else { + throw SdkException.builder().message("testMessage").build() + } + } + + @Override + void close() { + + } + + @Override + String serviceName() { + return null + } + + void resetCalls() { + numberOfCalls = new HashMap<>() + } + + void incrementVisit(String methodName) { + numberOfCalls.compute(methodName, (k, v) -> (v == null) ? 1 : v + 1) + } + } +} diff --git a/aws-cloudwatch-logging/src/test/java/io/micronaut/aws/cloudwatch/logging/MockAppender.java b/aws-cloudwatch-logging/src/test/java/io/micronaut/aws/cloudwatch/logging/MockAppender.java new file mode 100644 index 0000000000..717006a88f --- /dev/null +++ b/aws-cloudwatch-logging/src/test/java/io/micronaut/aws/cloudwatch/logging/MockAppender.java @@ -0,0 +1,30 @@ +package io.micronaut.aws.cloudwatch.logging; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.AppenderBase; + +import java.util.ArrayList; +import java.util.List; + +public class MockAppender extends AppenderBase { + + private static final List events = new ArrayList<>(); + + static List getEvents() { + synchronized (events) { + return new ArrayList<>(events); + } + } + + @Override + public String getName() { + return "MockAppender"; + } + + @Override + protected void append(ILoggingEvent eventObject) { + synchronized (events) { + events.add(eventObject); + } + } +} diff --git a/aws-cloudwatch-logging/src/test/resources/application.yml b/aws-cloudwatch-logging/src/test/resources/application.yml new file mode 100644 index 0000000000..9039104907 --- /dev/null +++ b/aws-cloudwatch-logging/src/test/resources/application.yml @@ -0,0 +1,3 @@ +micronaut: + application: + name: testapp diff --git a/aws-cloudwatch-logging/src/test/resources/logback.xml b/aws-cloudwatch-logging/src/test/resources/logback.xml new file mode 100644 index 0000000000..775ac60582 --- /dev/null +++ b/aws-cloudwatch-logging/src/test/resources/logback.xml @@ -0,0 +1,23 @@ + + + + + + + + name1 + name2 + 300 + 120 + + + + + + + + + + + diff --git a/aws-common/build.gradle b/aws-common/build.gradle deleted file mode 100644 index 795db05164..0000000000 --- a/aws-common/build.gradle +++ /dev/null @@ -1,8 +0,0 @@ -plugins { - id("io.micronaut.build.internal.aws-module") -} - -dependencies { - compileOnly mn.micronaut.runtime - testImplementation mn.micronaut.runtime -} diff --git a/aws-common/build.gradle.kts b/aws-common/build.gradle.kts new file mode 100644 index 0000000000..638495482a --- /dev/null +++ b/aws-common/build.gradle.kts @@ -0,0 +1,8 @@ +plugins { + id("io.micronaut.build.internal.module") +} + +dependencies { + compileOnly(mn.micronaut.runtime) + testImplementation(mn.micronaut.runtime) +} diff --git a/aws-distributed-configuration/build.gradle b/aws-distributed-configuration/build.gradle deleted file mode 100644 index cc99b4035d..0000000000 --- a/aws-distributed-configuration/build.gradle +++ /dev/null @@ -1,9 +0,0 @@ -plugins { - id("io.micronaut.build.internal.aws-module") -} - -dependencies { - api projects.awsCommon - api(mn.micronaut.discovery) - testImplementation mn.micronaut.http.server.netty -} diff --git a/aws-distributed-configuration/build.gradle.kts b/aws-distributed-configuration/build.gradle.kts new file mode 100644 index 0000000000..a1231c4edb --- /dev/null +++ b/aws-distributed-configuration/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + id("io.micronaut.build.internal.module") +} + +dependencies { + api(project(":aws-common")) + api(mn.micronaut.discovery) + testImplementation(mn.micronaut.http.server.netty) +} diff --git a/aws-distributed-configuration/src/main/java/io/micronaut/aws/distributedconfiguration/AwsDistributedConfigurationClient.java b/aws-distributed-configuration/src/main/java/io/micronaut/aws/distributedconfiguration/AwsDistributedConfigurationClient.java index 9c8620a296..4449e71d2b 100644 --- a/aws-distributed-configuration/src/main/java/io/micronaut/aws/distributedconfiguration/AwsDistributedConfigurationClient.java +++ b/aws-distributed-configuration/src/main/java/io/micronaut/aws/distributedconfiguration/AwsDistributedConfigurationClient.java @@ -26,13 +26,13 @@ import org.reactivestreams.Publisher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import java.util.ArrayList; import java.util.HashMap; -import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.Set; /** * Base implementation for AWS services contributing distributed configuration. @@ -52,7 +52,7 @@ public abstract class AwsDistributedConfigurationClient implements Configuration /** * * @param awsDistributedConfiguration AWS Distributed Configuration - * @param keyValueFetcher a Key Value Fetcher + * @param keyValueFetcher Key Value Fetcher * @param applicationConfiguration Application Configuration */ public AwsDistributedConfigurationClient(AwsDistributedConfiguration awsDistributedConfiguration, @@ -73,38 +73,41 @@ public AwsDistributedConfigurationClient(AwsDistributedConfiguration awsDistribu @Override public Publisher getPropertySources(Environment environment) { List configurationResolutionPrefixes = generateConfigurationResolutionPrefixes(environment); - - Map configurationResolutionPrefixesValues = new HashMap<>(); + Map>> configurationResolutionPrefixKeyValueGroups = new LinkedHashMap<>(); + int allKeysCount = 0; for (String prefix : configurationResolutionPrefixes) { - Optional keyValuesOptional = keyValueFetcher.keyValuesByPrefix(prefix); - if (keyValuesOptional.isPresent()) { - Map keyValues = keyValuesOptional.get(); - configurationResolutionPrefixesValues.put(prefix, keyValues); + Optional keyValueGroupsOptional = keyValueFetcher.keyValuesByPrefix(prefix); + if (keyValueGroupsOptional.isPresent()) { + Map> keyValueGroups = keyValueGroupsOptional.get(); + configurationResolutionPrefixKeyValueGroups.put(prefix, keyValueGroups); + for (Map keyValues: keyValueGroups.values()) { + allKeysCount += keyValues.size(); + } } } - Set allKeys = new HashSet<>(); - for (Map m : configurationResolutionPrefixesValues.values()) { - allKeys.addAll(m.keySet()); - } - Map result = new HashMap<>(); if (LOG.isTraceEnabled()) { - LOG.trace("evaluating {} keys", allKeys.size()); + LOG.trace("evaluating {} keys", allKeysCount); } - for (String k : allKeys) { - if (!result.containsKey(k)) { - for (String prefix : configurationResolutionPrefixes) { - if (configurationResolutionPrefixesValues.containsKey(prefix)) { - Map values = configurationResolutionPrefixesValues.get(prefix); - if (values.containsKey(k)) { - if (LOG.isTraceEnabled()) { - LOG.trace("adding property {} from prefix {}", k, prefix); - } - result.put(k, values.get(k)); - break; + Map result = new HashMap<>(); + for (Map.Entry>> configurationMapEntry : configurationResolutionPrefixKeyValueGroups.entrySet()) { + String prefix = configurationMapEntry.getKey(); + Map> keyValueGroups = configurationMapEntry.getValue(); + for (Map.Entry> keyValueGroupEntry : keyValueGroups.entrySet()) { + String groupName = keyValueGroupEntry.getKey(); + Map keyValues = keyValueGroupEntry.getValue(); + + for (Map.Entry keyValuesEntry: keyValues.entrySet()) { + String key = keyValuesEntry.getKey(); + String adaptedPropertyKey = adaptPropertyKey(key, groupName); + if (!result.containsKey(adaptedPropertyKey)) { + if (LOG.isTraceEnabled()) { + LOG.trace("adding property {} from prefix {}", adaptedPropertyKey, prefix); } + result.put(adaptedPropertyKey, keyValuesEntry.getValue()); } } + } } String propertySourceName = getPropertySourceName(); @@ -119,6 +122,17 @@ public Publisher getPropertySources(Environment environment) { return Publishers.just(new MapPropertySource(propertySourceName, result)); } + /** + * Adapts an original key. For example, key could be appended to a prefix in order to avoid naming ambiguity. + * * + * @since 3.8.0 + * @param originalKey an original property key + * @param groupName a property group name + * @return An adapted property key (e.g. key that has been appended to a prefix) + */ + @NonNull + protected abstract String adaptPropertyKey(String originalKey, String groupName); + /** * * @return The name of the property source diff --git a/aws-distributed-configuration/src/test/groovy/io/micronaut/aws/distributedconfiguration/AwsDistributedConfigurationSpec.groovy b/aws-distributed-configuration/src/test/groovy/io/micronaut/aws/distributedconfiguration/AwsDistributedConfigurationSpec.groovy index e26caa603c..a5268c4656 100644 --- a/aws-distributed-configuration/src/test/groovy/io/micronaut/aws/distributedconfiguration/AwsDistributedConfigurationSpec.groovy +++ b/aws-distributed-configuration/src/test/groovy/io/micronaut/aws/distributedconfiguration/AwsDistributedConfigurationSpec.groovy @@ -99,6 +99,12 @@ class AwsDistributedConfigurationSpec extends Specification { super(awsDistributedConfiguration, keyValueFetcher, applicationConfiguration) } + @Override + @NonNull + protected String adaptPropertyKey(String originalKey, String groupName) { + return originalKey + } + @Override @NonNull protected String getPropertySourceName() { @@ -137,9 +143,9 @@ class AwsDistributedConfigurationSpec extends Specification { ] @Override - Optional keyValuesByPrefix(@NonNull String prefix) { + Optional> keyValuesByPrefix(@NonNull String prefix) { String k = m.keySet().find { it.startsWith(prefix) } - (k) ? Optional.of(m[k]) : Optional.empty() + (k) ? Optional.of([(k): m[k]] as Map) : Optional.empty() } } } diff --git a/aws-parameter-store/build.gradle b/aws-parameter-store/build.gradle deleted file mode 100644 index d737dd13d5..0000000000 --- a/aws-parameter-store/build.gradle +++ /dev/null @@ -1,14 +0,0 @@ -plugins { - id("io.micronaut.build.internal.aws-module") -} - -dependencies { - api mn.micronaut.discovery - api projects.awsServiceDiscovery - api projects.awsSdkV2 - implementation libs.aws.ssm - - implementation mn.reactor - - testImplementation mn.micronaut.http.server.netty -} diff --git a/aws-parameter-store/build.gradle.kts b/aws-parameter-store/build.gradle.kts new file mode 100644 index 0000000000..1f4db50878 --- /dev/null +++ b/aws-parameter-store/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + id("io.micronaut.build.internal.module") +} + +dependencies { + api(mn.micronaut.discovery) + api(project(":aws-service-discovery")) + api(project(":aws-sdk-v2")) + implementation(libs.aws.ssm) + implementation(libs.projectreactor) + testImplementation(mn.micronaut.http.server.netty) +} diff --git a/aws-parameter-store/src/main/java/io/micronaut/discovery/aws/parameterstore/AWSParameterStoreConfiguration.java b/aws-parameter-store/src/main/java/io/micronaut/discovery/aws/parameterstore/AWSParameterStoreConfiguration.java index bab572951d..ab7d431c4f 100644 --- a/aws-parameter-store/src/main/java/io/micronaut/discovery/aws/parameterstore/AWSParameterStoreConfiguration.java +++ b/aws-parameter-store/src/main/java/io/micronaut/discovery/aws/parameterstore/AWSParameterStoreConfiguration.java @@ -98,7 +98,7 @@ public boolean getUseSecureParameters() { } /** - * Use auto-decryption via MKS for SecureString parameters. Default value ({@value #DEFAULT_SECURE}). + * Use auto-decryption via KMS for SecureString parameters. Default value ({@value #DEFAULT_SECURE}). * If set to false, you will not get unencrypted values. * * @param useSecureParameters True if secure parameters should be used diff --git a/aws-sdk-v1/build.gradle b/aws-sdk-v1/build.gradle deleted file mode 100644 index 3735a2e849..0000000000 --- a/aws-sdk-v1/build.gradle +++ /dev/null @@ -1,16 +0,0 @@ -plugins { - id("io.micronaut.build.internal.aws-module") -} - -dependencies { - api platform(libs.boms.aws.java.sdk.v1) - api libs.managed.aws.java.sdk.core - - api projects.awsCommon - - runtimeOnly libs.jcl.over.slf4j - - testImplementation mn.micronaut.http.server.netty - testRuntimeOnly mn.snakeyaml - -} diff --git a/aws-sdk-v1/build.gradle.kts b/aws-sdk-v1/build.gradle.kts new file mode 100644 index 0000000000..1560ae9127 --- /dev/null +++ b/aws-sdk-v1/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + id("io.micronaut.build.internal.module") +} + +dependencies { + api(platform(libs.boms.aws.java.sdk.v1)) + api(libs.managed.aws.java.sdk.core) + + api(project(":aws-common")) + + runtimeOnly(libs.jcl.over.slf4j) + + testImplementation(mn.micronaut.http.server.netty) +} diff --git a/aws-sdk-v2/build.gradle b/aws-sdk-v2/build.gradle deleted file mode 100644 index a3a10dfe24..0000000000 --- a/aws-sdk-v2/build.gradle +++ /dev/null @@ -1,44 +0,0 @@ -plugins { - id("io.micronaut.build.internal.aws-module") -} - -dependencies { - api platform(libs.boms.aws.java.sdk.v2) - api projects.awsCommon - - compileOnly mn.graal - - // Clients - compileOnly libs.awssdk.url.connection.client - compileOnly libs.awssdk.netty.nio.client - compileOnly libs.awssdk.apache.client - - // Services - compileOnly libs.awssdk.apigatewaymanagementapi - compileOnly libs.awssdk.s3 - compileOnly libs.awssdk.dynamodb - compileOnly libs.awssdk.ses - compileOnly libs.awssdk.sns - compileOnly libs.awssdk.sqs - compileOnly libs.awssdk.ssm - compileOnly libs.awssdk.secretsmanager - compileOnly libs.awssdk.servicediscovery - - // Tests - testAnnotationProcessor mn.micronaut.inject.java - testImplementation libs.awssdk.apigatewaymanagementapi - testImplementation libs.awssdk.servicediscovery - testImplementation libs.awssdk.url.connection.client - testImplementation libs.awssdk.netty.nio.client - testImplementation libs.awssdk.apache.client - testImplementation libs.awssdk.s3 - testImplementation libs.awssdk.dynamodb - testImplementation libs.awssdk.ses - testImplementation libs.awssdk.secretsmanager - testImplementation libs.awssdk.sns - testImplementation libs.awssdk.sqs - testImplementation libs.awssdk.ssm - testImplementation libs.awssdk.rekognition - testRuntimeOnly libs.jcl.over.slf4j - testRuntimeOnly mn.snakeyaml -} diff --git a/aws-sdk-v2/build.gradle.kts b/aws-sdk-v2/build.gradle.kts new file mode 100644 index 0000000000..6480579d15 --- /dev/null +++ b/aws-sdk-v2/build.gradle.kts @@ -0,0 +1,45 @@ +plugins { + id("io.micronaut.build.internal.module") +} + +dependencies { + api(platform(libs.boms.aws.java.sdk.v2)) + api(project(":aws-common")) + + compileOnly(libs.graal) + + // Clients + compileOnly(libs.awssdk.url.connection.client) + compileOnly(libs.awssdk.netty.nio.client) + compileOnly(libs.awssdk.apache.client) + + // Services + compileOnly(libs.awssdk.apigatewaymanagementapi) + compileOnly(libs.awssdk.s3) + compileOnly(libs.awssdk.dynamodb) + compileOnly(libs.awssdk.ses) + compileOnly(libs.awssdk.sns) + compileOnly(libs.awssdk.sqs) + compileOnly(libs.awssdk.ssm) + compileOnly(libs.awssdk.secretsmanager) + compileOnly(libs.awssdk.servicediscovery) + compileOnly(libs.awssdk.cloudwatchlogs) + + // Tests + testAnnotationProcessor(mn.micronaut.inject.java) + testImplementation(libs.awssdk.cloudwatchlogs) + testImplementation(libs.awssdk.apigatewaymanagementapi) + testImplementation(libs.awssdk.servicediscovery) + testImplementation(libs.awssdk.url.connection.client) + testImplementation(libs.awssdk.netty.nio.client) + testImplementation(libs.awssdk.apache.client) + testImplementation(libs.awssdk.s3) + testImplementation(libs.awssdk.dynamodb) + testImplementation(libs.awssdk.ses) + testImplementation(libs.awssdk.secretsmanager) + testImplementation(libs.awssdk.sns) + testImplementation(libs.awssdk.sqs) + testImplementation(libs.awssdk.ssm) + testImplementation(libs.awssdk.rekognition) + testRuntimeOnly(libs.jcl.over.slf4j) +} diff --git a/aws-sdk-v2/src/main/java/io/micronaut/aws/sdk/v2/service/cloudwatchlogs/CloudwatchLogsClientFactory.java b/aws-sdk-v2/src/main/java/io/micronaut/aws/sdk/v2/service/cloudwatchlogs/CloudwatchLogsClientFactory.java new file mode 100644 index 0000000000..d85b08edb3 --- /dev/null +++ b/aws-sdk-v2/src/main/java/io/micronaut/aws/sdk/v2/service/cloudwatchlogs/CloudwatchLogsClientFactory.java @@ -0,0 +1,90 @@ +/* + * Copyright 2017-2021 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.aws.sdk.v2.service.cloudwatchlogs; + +import io.micronaut.aws.sdk.v2.service.AwsClientFactory; +import io.micronaut.context.annotation.Bean; +import io.micronaut.context.annotation.BootstrapContextCompatible; +import io.micronaut.context.annotation.Factory; +import io.micronaut.context.annotation.Requires; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProviderChain; +import software.amazon.awssdk.http.SdkHttpClient; +import software.amazon.awssdk.http.async.SdkAsyncHttpClient; +import software.amazon.awssdk.regions.providers.AwsRegionProviderChain; +import software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsAsyncClient; +import software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsAsyncClientBuilder; +import software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsClient; +import software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsClientBuilder; + +import jakarta.inject.Singleton; + +/** + * Factory that creates a CloudWatch Logs client. + * @author Nemanja Mikic + * @since 2.6.0 + */ +@Factory +@BootstrapContextCompatible +public class CloudwatchLogsClientFactory extends AwsClientFactory { + + /** + * Constructor. + * + * @param credentialsProvider The credentials provider + * @param regionProvider The region provider + */ + protected CloudwatchLogsClientFactory(AwsCredentialsProviderChain credentialsProvider, AwsRegionProviderChain regionProvider) { + super(credentialsProvider, regionProvider); + } + + @Override + protected CloudWatchLogsClientBuilder createSyncBuilder() { + return CloudWatchLogsClient.builder(); + } + + @Override + protected CloudWatchLogsAsyncClientBuilder createAsyncBuilder() { + return CloudWatchLogsAsyncClient.builder(); + } + + @Override + @Singleton + public CloudWatchLogsClientBuilder syncBuilder(SdkHttpClient httpClient) { + return super.syncBuilder(httpClient); + } + + @Override + @Bean(preDestroy = "close") + @Singleton + public CloudWatchLogsClient syncClient(CloudWatchLogsClientBuilder builder) { + return super.syncClient(builder); + } + + @Override + @Singleton + @Requires(beans = SdkAsyncHttpClient.class) + public CloudWatchLogsAsyncClientBuilder asyncBuilder(SdkAsyncHttpClient httpClient) { + return super.asyncBuilder(httpClient); + } + + @Override + @Bean(preDestroy = "close") + @Singleton + @Requires(beans = SdkAsyncHttpClient.class) + public CloudWatchLogsAsyncClient asyncClient(CloudWatchLogsAsyncClientBuilder builder) { + return super.asyncClient(builder); + } +} diff --git a/aws-sdk-v2/src/main/java/io/micronaut/aws/sdk/v2/service/cloudwatchlogs/package-info.java b/aws-sdk-v2/src/main/java/io/micronaut/aws/sdk/v2/service/cloudwatchlogs/package-info.java new file mode 100644 index 0000000000..877f397761 --- /dev/null +++ b/aws-sdk-v2/src/main/java/io/micronaut/aws/sdk/v2/service/cloudwatchlogs/package-info.java @@ -0,0 +1,29 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * DynamoDB client factory. + * + * @author Álvaro Sánchez-Mariscal + * @since 2.0.0 + */ +@Requires(classes = {CloudWatchLogsClient.class, CloudWatchLogsAsyncClient.class}) +@Configuration +package io.micronaut.aws.sdk.v2.service.cloudwatchlogs; + +import io.micronaut.context.annotation.Configuration; +import io.micronaut.context.annotation.Requires; +import software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsAsyncClient; +import software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsClient; diff --git a/aws-sdk-v2/src/main/resources/META-INF/native-image/io.micronaut.aws/micronaut-aws-sdk-v2/reflect-config.json b/aws-sdk-v2/src/main/resources/META-INF/native-image/io.micronaut.aws/micronaut-aws-sdk-v2/reflect-config.json new file mode 100644 index 0000000000..0a8a692723 --- /dev/null +++ b/aws-sdk-v2/src/main/resources/META-INF/native-image/io.micronaut.aws/micronaut-aws-sdk-v2/reflect-config.json @@ -0,0 +1,17 @@ +[ + { + "name":"org.apache.commons.logging.impl.Jdk14Logger", + "methods":[{"name":"","parameterTypes":["java.lang.String"] }] + }, + { + "name":"org.apache.commons.logging.impl.Log4JLogger" + }, + { + "name":"org.apache.commons.logging.impl.LogFactoryImpl", + "methods":[{"name":"","parameterTypes":[] }] + }, + { + "name":"org.apache.commons.logging.impl.WeakHashtable", + "methods":[{"name":"","parameterTypes":[] }] + } +] diff --git a/aws-sdk-v2/src/test/groovy/io/micronaut/aws/sdk/v2/service/CloudWatchLogsClientSpec.groovy b/aws-sdk-v2/src/test/groovy/io/micronaut/aws/sdk/v2/service/CloudWatchLogsClientSpec.groovy new file mode 100644 index 0000000000..64d63e6485 --- /dev/null +++ b/aws-sdk-v2/src/test/groovy/io/micronaut/aws/sdk/v2/service/CloudWatchLogsClientSpec.groovy @@ -0,0 +1,24 @@ +package io.micronaut.aws.sdk.v2.service + +import io.micronaut.aws.sdk.v2.ApplicationContextSpecification +import software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsAsyncClient +import software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsClient + +class CloudWatchLogsClientSpec extends ApplicationContextSpecification { + + void "it can configure a sync client"() { + when: + CloudWatchLogsClient client = applicationContext.getBean(CloudWatchLogsClient) + + then: + client.serviceName() == CloudWatchLogsClient.SERVICE_NAME + } + + void "it can configure an async client"() { + when: + CloudWatchLogsAsyncClient client = applicationContext.getBean(CloudWatchLogsAsyncClient) + + then: + client.serviceName() == CloudWatchLogsClient.SERVICE_NAME + } +} diff --git a/aws-secretsmanager/build.gradle b/aws-secretsmanager/build.gradle deleted file mode 100644 index 565577a79a..0000000000 --- a/aws-secretsmanager/build.gradle +++ /dev/null @@ -1,9 +0,0 @@ -plugins { - id("io.micronaut.build.internal.aws-module") -} - -dependencies { - api projects.awsSdkV2 - api projects.awsDistributedConfiguration - api libs.awssdk.secretsmanager -} diff --git a/aws-secretsmanager/build.gradle.kts b/aws-secretsmanager/build.gradle.kts new file mode 100644 index 0000000000..9b46e86662 --- /dev/null +++ b/aws-secretsmanager/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + id("io.micronaut.build.internal.module") +} + +dependencies { + api(project(":aws-sdk-v2")) + api(project(":aws-distributed-configuration")) + api(libs.awssdk.secretsmanager) +} diff --git a/aws-secretsmanager/src/main/java/io/micronaut/aws/secretsmanager/SecretsKeyValueFetcher.java b/aws-secretsmanager/src/main/java/io/micronaut/aws/secretsmanager/SecretsKeyValueFetcher.java new file mode 100644 index 0000000000..bb544fb74e --- /dev/null +++ b/aws-secretsmanager/src/main/java/io/micronaut/aws/secretsmanager/SecretsKeyValueFetcher.java @@ -0,0 +1,30 @@ +/* + * Copyright 2017-2021 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.aws.secretsmanager; + +import io.micronaut.aws.distributedconfiguration.KeyValueFetcher; +import io.micronaut.context.annotation.DefaultImplementation; + +/** + * Key Value fetcher for AWS Secrets Manager. + * {@link KeyValueFetcher} + * + * @author sbodvanski + * @since 3.8.0 + */ +@DefaultImplementation(SecretsManagerGroupNameAwareKeyValueFetcher.class) +public interface SecretsKeyValueFetcher extends KeyValueFetcher { +} diff --git a/aws-secretsmanager/src/main/java/io/micronaut/aws/secretsmanager/SecretsManagerConfiguration.java b/aws-secretsmanager/src/main/java/io/micronaut/aws/secretsmanager/SecretsManagerConfiguration.java index 82ce8be887..3dcc901f17 100644 --- a/aws-secretsmanager/src/main/java/io/micronaut/aws/secretsmanager/SecretsManagerConfiguration.java +++ b/aws-secretsmanager/src/main/java/io/micronaut/aws/secretsmanager/SecretsManagerConfiguration.java @@ -17,10 +17,21 @@ import io.micronaut.core.util.Toggleable; +import java.util.List; + /** * Configuration for Secrets Manager. * @author Sergio del Amo * @since 2.8.0 */ public interface SecretsManagerConfiguration extends Toggleable { + + /** + * Provide a list of secret configurations that allows for flexibility in secret key naming. + * This is provided by an option to define a key group prefix for any secret name. + * + * @since 3.8.0 + * @return the AWS Secrets Manager secrets configuration. + */ + List getSecrets(); } diff --git a/aws-secretsmanager/src/main/java/io/micronaut/aws/secretsmanager/SecretsManagerConfigurationClient.java b/aws-secretsmanager/src/main/java/io/micronaut/aws/secretsmanager/SecretsManagerConfigurationClient.java index 0d12fd486c..a6ac79ce5a 100644 --- a/aws-secretsmanager/src/main/java/io/micronaut/aws/secretsmanager/SecretsManagerConfigurationClient.java +++ b/aws-secretsmanager/src/main/java/io/micronaut/aws/secretsmanager/SecretsManagerConfigurationClient.java @@ -19,12 +19,15 @@ import io.micronaut.aws.distributedconfiguration.AwsDistributedConfigurationClient; import io.micronaut.context.annotation.BootstrapContextCompatible; import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.Creator; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.util.CollectionUtils; import io.micronaut.runtime.ApplicationConfiguration; - import jakarta.inject.Singleton; +import java.util.Optional; + /** * Distributed configuration client for AWS Secrets Manager. * @see AWS Secrets Manager @@ -33,22 +36,55 @@ */ @Requires(beans = { AwsDistributedConfiguration.class, - SecretsManagerKeyValueFetcher.class + SecretsManagerGroupNameAwareKeyValueFetcher.class }) @Singleton @BootstrapContextCompatible public class SecretsManagerConfigurationClient extends AwsDistributedConfigurationClient { + private final Optional secretsManagerConfiguration; + /** - * * @param awsDistributedConfiguration AWS Distributed Configuration * @param secretsManagerKeyValueFetcher Secrets Manager Key Value Fetcher * @param applicationConfiguration Application Configuration */ + @Deprecated public SecretsManagerConfigurationClient(AwsDistributedConfiguration awsDistributedConfiguration, SecretsManagerKeyValueFetcher secretsManagerKeyValueFetcher, @Nullable ApplicationConfiguration applicationConfiguration) { + this(awsDistributedConfiguration, secretsManagerKeyValueFetcher, applicationConfiguration, null); + } + + /** + * @param awsDistributedConfiguration AWS Distributed Configuration + * @param secretsManagerKeyValueFetcher Secrets Manager Key Value Fetcher + * @param applicationConfiguration Application Configuration + * @param secretsManagerConfiguration Secrets Configuration + */ + @Creator + public SecretsManagerConfigurationClient(AwsDistributedConfiguration awsDistributedConfiguration, + SecretsManagerKeyValueFetcher secretsManagerKeyValueFetcher, + @Nullable ApplicationConfiguration applicationConfiguration, + SecretsManagerConfiguration secretsManagerConfiguration) { super(awsDistributedConfiguration, secretsManagerKeyValueFetcher, applicationConfiguration); + this.secretsManagerConfiguration = Optional.of(secretsManagerConfiguration); + } + + @Override + @NonNull + protected String adaptPropertyKey(String originalKey, String groupName) { + if (secretsManagerConfiguration.isPresent()) { + SecretsManagerConfiguration secretsConfiguration = secretsManagerConfiguration.get(); + if (CollectionUtils.isNotEmpty(secretsConfiguration.getSecrets())) { + for (SecretsManagerConfigurationProperties.SecretConfiguration secret : secretsConfiguration.getSecrets()) { + if (groupName.endsWith(secret.getSecretName())) { + return secret.getPrefix() + "." + originalKey; + } + } + } + } + return originalKey; } @Override @@ -58,7 +94,9 @@ protected String getPropertySourceName() { } @Override + @NonNull public String getDescription() { return "AWS Secrets Manager"; } + } diff --git a/aws-secretsmanager/src/main/java/io/micronaut/aws/secretsmanager/SecretsManagerConfigurationProperties.java b/aws-secretsmanager/src/main/java/io/micronaut/aws/secretsmanager/SecretsManagerConfigurationProperties.java index fa8eb66c79..f725cc2f55 100644 --- a/aws-secretsmanager/src/main/java/io/micronaut/aws/secretsmanager/SecretsManagerConfigurationProperties.java +++ b/aws-secretsmanager/src/main/java/io/micronaut/aws/secretsmanager/SecretsManagerConfigurationProperties.java @@ -18,6 +18,10 @@ import io.micronaut.aws.AWSConfiguration; import io.micronaut.context.annotation.BootstrapContextCompatible; import io.micronaut.context.annotation.ConfigurationProperties; +import io.micronaut.context.annotation.EachProperty; +import io.micronaut.core.annotation.Introspected; + +import java.util.List; /** * {@link ConfigurationProperties} implementation of {@link SecretsManagerConfiguration}. @@ -41,6 +45,8 @@ public class SecretsManagerConfigurationProperties implements SecretsManagerConf private boolean enabled = DEFAULT_ENABLED; + protected List secrets; + /** * @return Whether the AWS Secrets Manager configuration is enabled */ @@ -56,4 +62,56 @@ public boolean isEnabled() { public void setEnabled(boolean enabled) { this.enabled = enabled; } + + @Override + public List getSecrets() { + return secrets; + } + + /** + * Secret configuration holder that allows for flexibility in secret key naming in the Micronaut context to avoid a potential keys name collision. + * This is provided by an option to define a key group prefix for any secret name. + * + * @author sbodvanski + * @since 3.8.0 + */ + @Introspected + @EachProperty(value = "secrets", list = true) + @BootstrapContextCompatible + public static class SecretConfiguration { + private String secretName; + private String prefix; + + /** + * Sets secret name. + * + * @param secretName secret name + */ + public void setSecretName(String secretName) { + this.secretName = secretName; + } + + /** + * Sets the group key prefix. + * + * @param prefix prefix + */ + public void setPrefix(String prefix) { + this.prefix = prefix; + } + + /** + * @return a secret name + */ + public String getSecretName() { + return secretName; + } + + /** + * @return a secret key group prefix + */ + public String getPrefix() { + return prefix; + } + } } diff --git a/aws-secretsmanager/src/main/java/io/micronaut/aws/secretsmanager/SecretsManagerGroupNameAwareKeyValueFetcher.java b/aws-secretsmanager/src/main/java/io/micronaut/aws/secretsmanager/SecretsManagerGroupNameAwareKeyValueFetcher.java new file mode 100644 index 0000000000..abc5583d87 --- /dev/null +++ b/aws-secretsmanager/src/main/java/io/micronaut/aws/secretsmanager/SecretsManagerGroupNameAwareKeyValueFetcher.java @@ -0,0 +1,74 @@ +/* + * Copyright 2017-2021 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.aws.secretsmanager; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.micronaut.context.annotation.BootstrapContextCompatible; +import io.micronaut.context.annotation.Replaces; +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.Experimental; +import io.micronaut.core.annotation.NonNull; +import jakarta.inject.Singleton; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient; +import software.amazon.awssdk.services.secretsmanager.model.SecretListEntry; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +/** + * Key Value fetcher for AWS Secrets Manager that is aware of Secret Key Value group names. + * + * @author sbodvanski + * @since 3.8.0 + */ +@Experimental +@Requires(beans = {SecretsManagerClient.class}) +@BootstrapContextCompatible +@Singleton +@Replaces(SecretsManagerKeyValueFetcher.class) +public class SecretsManagerGroupNameAwareKeyValueFetcher extends SecretsManagerKeyValueFetcher { + private static final Logger LOG = LoggerFactory.getLogger(SecretsManagerGroupNameAwareKeyValueFetcher.class); + + /** + * @param secretsClient Secrets Client + * @param objectMapper Object Mapper + */ + public SecretsManagerGroupNameAwareKeyValueFetcher(SecretsManagerClient secretsClient, + ObjectMapper objectMapper) { + super(secretsClient, objectMapper); + } + + @Override + @NonNull + protected void addSecretDetailsToResults(SecretListEntry secret, Map result) { + Map keyValues = new HashMap<>(); + Optional secretValueOptional = fetchSecretValue(secretsClient, secret.name()); + if (secretValueOptional.isPresent()) { + try { + keyValues.putAll(objectMapper.readValue(secretValueOptional.get(), Map.class)); + result.put(secret.name(), keyValues); + } catch (JsonProcessingException e) { + if (LOG.isWarnEnabled()) { + LOG.warn("could not read secret ({}) value from JSON to Map", secret.name()); + } + } + } + } +} diff --git a/aws-secretsmanager/src/main/java/io/micronaut/aws/secretsmanager/SecretsManagerKeyValueFetcher.java b/aws-secretsmanager/src/main/java/io/micronaut/aws/secretsmanager/SecretsManagerKeyValueFetcher.java index 5b739f2169..8caef9fea6 100644 --- a/aws-secretsmanager/src/main/java/io/micronaut/aws/secretsmanager/SecretsManagerKeyValueFetcher.java +++ b/aws-secretsmanager/src/main/java/io/micronaut/aws/secretsmanager/SecretsManagerKeyValueFetcher.java @@ -17,7 +17,6 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import io.micronaut.aws.distributedconfiguration.KeyValueFetcher; import io.micronaut.context.annotation.BootstrapContextCompatible; import io.micronaut.context.annotation.Requires; import io.micronaut.core.annotation.Experimental; @@ -46,7 +45,7 @@ import java.util.Optional; /** - * {@link KeyValueFetcher} implementations for AWS Secrets Manager. + * {@link SecretsKeyValueFetcher} implementations for AWS Secrets Manager. * @author Sergio del Amo * @since 2.8.0 */ @@ -54,11 +53,11 @@ @Requires(beans = {SecretsManagerClient.class}) @BootstrapContextCompatible @Singleton -public class SecretsManagerKeyValueFetcher implements KeyValueFetcher { +public class SecretsManagerKeyValueFetcher implements SecretsKeyValueFetcher { private static final Logger LOG = LoggerFactory.getLogger(SecretsManagerKeyValueFetcher.class); - private final SecretsManagerClient secretsClient; - private final ObjectMapper objectMapper; + protected final SecretsManagerClient secretsClient; + protected final ObjectMapper objectMapper; /** * @@ -101,16 +100,7 @@ public Optional keyValuesByPrefix(@NonNull String prefix) { if (LOG.isTraceEnabled()) { LOG.trace("Evaluating secret {}", secret.name()); } - Optional secretValueOptional = fetchSecretValue(secretsClient, secret.name()); - if (secretValueOptional.isPresent()) { - try { - result.putAll(objectMapper.readValue(secretValueOptional.get(), Map.class)); - } catch (JsonProcessingException e) { - if (LOG.isWarnEnabled()) { - LOG.warn("could not read secret ({}) value from JSON to Map", secret.name()); - } - } - } + addSecretDetailsToResults(secret, result); } nextToken = secretsResponse.nextToken(); @@ -126,8 +116,35 @@ public Optional keyValuesByPrefix(@NonNull String prefix) { return result.isEmpty() ? Optional.empty() : Optional.of(result); } + /** + * Add secret details to the result map. + * + * @param secret a secret list entry + * @param result a map that collects the results + */ + @NonNull + protected void addSecretDetailsToResults(SecretListEntry secret, Map result) { + Optional secretValueOptional = fetchSecretValue(secretsClient, secret.name()); + if (secretValueOptional.isPresent()) { + try { + result.putAll(objectMapper.readValue(secretValueOptional.get(), Map.class)); + } catch (JsonProcessingException e) { + if (LOG.isWarnEnabled()) { + LOG.warn("could not read secret ({}) value from JSON to Map", secret.name()); + } + } + } + } + + /** + * Fetches secret value. + * + * @param secretsClient a secret manager cleint + * @param secretName a secret name + * @return secret value optional + */ @NonNull - private Optional fetchSecretValue(@NonNull SecretsManagerClient secretsClient, + protected Optional fetchSecretValue(@NonNull SecretsManagerClient secretsClient, @NonNull String secretName) { GetSecretValueRequest getSecretValueRequest = GetSecretValueRequest.builder() .secretId(secretName) diff --git a/aws-secretsmanager/src/test/groovy/io/micronaut/aws/secretsmanager/SecretsManagerConfigurationClientSpec.groovy b/aws-secretsmanager/src/test/groovy/io/micronaut/aws/secretsmanager/SecretsManagerConfigurationClientSpec.groovy index a88589fb26..69da61d38a 100644 --- a/aws-secretsmanager/src/test/groovy/io/micronaut/aws/secretsmanager/SecretsManagerConfigurationClientSpec.groovy +++ b/aws-secretsmanager/src/test/groovy/io/micronaut/aws/secretsmanager/SecretsManagerConfigurationClientSpec.groovy @@ -14,6 +14,17 @@ class SecretsManagerConfigurationClientSpec extends ApplicationContextSpecificat 'SecretsManagerConfigurationClientSpec' } + @Override + Map getConfiguration() { + super.configuration + [ + 'aws.secretsmanager.enabled': true, + 'aws.secretsmanager.secrets': [ + ["secret-name": "rds_default", "prefix": "datasources.default"], + ["secret-name": "rds_backup", "prefix": "datasources.backup"] + ] + ] + } + void "SecretsManagerConfigurationClient is annotated with BootstrapContextCompatible"() { when: BeanDefinition beanDefinition = applicationContext.getBeanDefinition(SecretsManagerConfigurationClient) @@ -22,6 +33,15 @@ class SecretsManagerConfigurationClientSpec extends ApplicationContextSpecificat beanDefinition.getAnnotationNameByStereotype(BootstrapContextCompatible).isPresent() } + void "The adaptPropertyKey method call when secret manager configuration is provided"() { + when: + SecretsManagerConfigurationClient bean = applicationContext.getBean(SecretsManagerConfigurationClient) + String adaptedPropertyKey = bean.adaptPropertyKey('host', 'rds_default') + + then: + adaptedPropertyKey == 'datasources.default.host' + } + @Requires(property = 'spec.name', value = 'SecretsManagerConfigurationClientSpec') @Primary @Singleton diff --git a/aws-secretsmanager/src/test/groovy/io/micronaut/aws/secretsmanager/SecretsManagerKeyValueFetcherSpec.groovy b/aws-secretsmanager/src/test/groovy/io/micronaut/aws/secretsmanager/SecretsManagerKeyValueFetcherSpec.groovy index 586f14ffb0..10699d5a7a 100644 --- a/aws-secretsmanager/src/test/groovy/io/micronaut/aws/secretsmanager/SecretsManagerKeyValueFetcherSpec.groovy +++ b/aws-secretsmanager/src/test/groovy/io/micronaut/aws/secretsmanager/SecretsManagerKeyValueFetcherSpec.groovy @@ -30,7 +30,7 @@ class SecretsManagerKeyValueFetcherSpec extends ApplicationContextSpecification void "SecretsManagerKeyValueFetcher is annotated with BootstrapContextCompatible"() { when: - BeanDefinition beanDefinition = applicationContext.getBeanDefinition(SecretsManagerKeyValueFetcher) + BeanDefinition beanDefinition = applicationContext.getBeanDefinition(SecretsManagerGroupNameAwareKeyValueFetcher) then: beanDefinition.getAnnotationNameByStereotype(BootstrapContextCompatible).isPresent() @@ -38,7 +38,7 @@ class SecretsManagerKeyValueFetcherSpec extends ApplicationContextSpecification void "bean of type SecretsManagerKeyValueFetcher exists"() { when: - SecretsManagerKeyValueFetcher secretsManagerKeyValueFetcher = applicationContext.getBean(SecretsManagerKeyValueFetcher) + SecretsManagerGroupNameAwareKeyValueFetcher secretsManagerKeyValueFetcher = applicationContext.getBean(SecretsManagerGroupNameAwareKeyValueFetcher) then: noExceptionThrown() @@ -50,18 +50,29 @@ class SecretsManagerKeyValueFetcherSpec extends ApplicationContextSpecification mapOptional.isPresent() when: - Map map = mapOptional.get() + Map keyValueGroups = mapOptional.get() then: - map - map.containsKey('micronaut.security.oauth2.clients.companyauthserver.client-id') - map['micronaut.security.oauth2.clients.companyauthserver.client-id'] == 'XXX' - map.containsKey('micronaut.security.oauth2.clients.companyauthserver.client-secret') - map['micronaut.security.oauth2.clients.companyauthserver.client-secret'] == 'YYY' - map.containsKey('micronaut.security.oauth2.clients.google.client-id') - map['micronaut.security.oauth2.clients.google.client-id'] == 'ZZZ' - map.containsKey('micronaut.security.oauth2.clients.google.client-secret') - map['micronaut.security.oauth2.clients.google.client-secret'] == 'PPP' + keyValueGroups.containsKey('/config/myapp_dev/oauthcompanyauthserver') + keyValueGroups.containsKey('/config/myapp_dev/oauthgoogle') + + when: + Map keyValues = keyValueGroups['/config/myapp_dev/oauthcompanyauthserver'] + + then: + keyValues.containsKey('micronaut.security.oauth2.clients.companyauthserver.client-id') + keyValues['micronaut.security.oauth2.clients.companyauthserver.client-id'] == 'XXX' + keyValues.containsKey('micronaut.security.oauth2.clients.companyauthserver.client-secret') + keyValues['micronaut.security.oauth2.clients.companyauthserver.client-secret'] == 'YYY' + + when: + keyValues = keyValueGroups['/config/myapp_dev/oauthgoogle'] + + then: + keyValues.containsKey('micronaut.security.oauth2.clients.google.client-id') + keyValues['micronaut.security.oauth2.clients.google.client-id'] == 'ZZZ' + keyValues.containsKey('micronaut.security.oauth2.clients.google.client-secret') + keyValues['micronaut.security.oauth2.clients.google.client-secret'] == 'PPP' } @Requires(property = 'spec.name', value = 'SecretsManagerKeyValueFetcherSpec') diff --git a/aws-secretsmanager/src/test/groovy/io/micronaut/aws/secretsmanager/SecretsManagerSecretsConfigurationSpec.groovy b/aws-secretsmanager/src/test/groovy/io/micronaut/aws/secretsmanager/SecretsManagerSecretsConfigurationSpec.groovy new file mode 100644 index 0000000000..bc77cd532c --- /dev/null +++ b/aws-secretsmanager/src/test/groovy/io/micronaut/aws/secretsmanager/SecretsManagerSecretsConfigurationSpec.groovy @@ -0,0 +1,35 @@ +package io.micronaut.aws.secretsmanager + +class SecretsManagerSecretsConfigurationSpec extends ApplicationContextSpecification { + + @Override + Map getConfiguration() { + super.configuration + [ + 'aws.secretsmanager.enabled': true, + 'aws.secretsmanager.secrets': [ + ["secret-name": "rds_default", "prefix": "datasources.default"], + ["secret-name": "rds_backup", "prefix": "datasources.backup"] + ] + ] + } + + void "secrets manager loads secrets configuration"() { + def secretsManagerConfiguration = applicationContext.getBean(SecretsManagerConfiguration) + + expect: + !secretsManagerConfiguration.getSecrets().isEmpty() + + when: + List secretConfigurations = secretsManagerConfiguration.getSecrets() + + then: + secretConfigurations.size() == 2 + for (secretConfiguration in secretConfigurations) { + if (secretConfiguration.getSecretName() == 'rds_default') { + secretConfiguration.getPrefix() == 'datasources.default' + } else if (secretConfiguration.getSecretName() == 'rds_backup') { + secretConfiguration.getPrefix() == 'datasources.backup' + } + } + } +} diff --git a/aws-service-discovery/build.gradle b/aws-service-discovery/build.gradle deleted file mode 100644 index 7f4d994fc3..0000000000 --- a/aws-service-discovery/build.gradle +++ /dev/null @@ -1,13 +0,0 @@ -plugins { - id("io.micronaut.build.internal.aws-module") -} - -dependencies { - api mn.micronaut.discovery - api projects.awsSdkV2 - - implementation libs.awssdk.servicediscovery - - testImplementation mn.reactor - testImplementation mn.micronaut.http.server.netty -} diff --git a/aws-service-discovery/build.gradle.kts b/aws-service-discovery/build.gradle.kts new file mode 100644 index 0000000000..dabe66a6d6 --- /dev/null +++ b/aws-service-discovery/build.gradle.kts @@ -0,0 +1,11 @@ +plugins { + id("io.micronaut.build.internal.module") +} + +dependencies { + api(mn.micronaut.discovery) + api(project(":aws-sdk-v2")) + implementation(libs.awssdk.servicediscovery) + testImplementation(libs.projectreactor) + testImplementation(mn.micronaut.http.server.netty) +} diff --git a/build.gradle b/build.gradle deleted file mode 100644 index 99b6df68fc..0000000000 --- a/build.gradle +++ /dev/null @@ -1,10 +0,0 @@ -plugins { - id "io.micronaut.build.internal.docs" - id "io.micronaut.build.internal.quality-reporting" -} - -repositories { - mavenCentral() - maven { url "https://s01.oss.sonatype.org/content/repositories/snapshots/" } -} - diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000000..e97ee10faf --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,5 @@ +plugins { + id("io.micronaut.build.internal.docs") + id("io.micronaut.build.internal.dependency-updates") + id("io.micronaut.build.internal.quality-reporting") +} diff --git a/function-aws-alexa/build.gradle b/function-aws-alexa/build.gradle deleted file mode 100644 index c4a8d5fce3..0000000000 --- a/function-aws-alexa/build.gradle +++ /dev/null @@ -1,23 +0,0 @@ -plugins { - id("io.micronaut.build.internal.aws-module") -} - -dependencies { - annotationProcessor mn.micronaut.validation - - implementation mn.micronaut.runtime - implementation mn.micronaut.validation - - implementation projects.functionAws - - api libs.managed.alexa.ask.sdk.lambda - api projects.awsAlexa - - runtimeOnly libs.jcl.over.slf4j - - testAnnotationProcessor mn.micronaut.inject.java - testImplementation (libs.alexa.ask.sdk) { - transitive(false) - } - testImplementation libs.alexa.ask.sdk.apache.client -} diff --git a/function-aws-alexa/build.gradle.kts b/function-aws-alexa/build.gradle.kts new file mode 100644 index 0000000000..1cf7d3edbd --- /dev/null +++ b/function-aws-alexa/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + id("io.micronaut.build.internal.module") +} + +dependencies { + annotationProcessor(mn.micronaut.validation) + + implementation(mn.micronaut.runtime) + implementation(mn.micronaut.validation) + + implementation(project(":function-aws")) + + api(libs.managed.alexa.ask.sdk.lambda) + api(project(":aws-alexa")) + + runtimeOnly(libs.jcl.over.slf4j) + + testAnnotationProcessor(mn.micronaut.inject.java) + testImplementation(libs.alexa.ask.sdk) { + isTransitive = false + } + testImplementation(libs.alexa.ask.sdk.apache.client) +} diff --git a/function-aws-api-proxy-test/build.gradle b/function-aws-api-proxy-test/build.gradle deleted file mode 100644 index 75ebf79ae9..0000000000 --- a/function-aws-api-proxy-test/build.gradle +++ /dev/null @@ -1,10 +0,0 @@ -plugins { - id("io.micronaut.build.internal.aws-module") -} - -dependencies { - api mn.micronaut.http.server - api projects.functionAwsApiProxy - implementation libs.jetty.server - testImplementation mn.micronaut.http.client -} diff --git a/function-aws-api-proxy-test/build.gradle.kts b/function-aws-api-proxy-test/build.gradle.kts new file mode 100644 index 0000000000..e1fc31f764 --- /dev/null +++ b/function-aws-api-proxy-test/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + id("io.micronaut.build.internal.module") +} + +dependencies { + api(mn.micronaut.http.server) + api(project(":function-aws-api-proxy")) + implementation(libs.jetty.server) + testImplementation(mn.micronaut.http.client) +} diff --git a/function-aws-api-proxy/build.gradle b/function-aws-api-proxy/build.gradle deleted file mode 100644 index f750b7f37f..0000000000 --- a/function-aws-api-proxy/build.gradle +++ /dev/null @@ -1,42 +0,0 @@ -plugins { - id("io.micronaut.build.internal.aws-module") -} - -dependencies { - annotationProcessor mn.micronaut.graal - - compileOnly mn.micronaut.security - - implementation mn.micronaut.http.netty - implementation mn.micronaut.core.processor - implementation mn.reactor - - api mn.micronaut.http.server - api(libs.managed.aws.serverless.core) { - exclude group:'javax.servlet', module:'javax.servlet-api' - exclude group:'com.fasterxml.jackson.module', module:'jackson-module-afterburner' - exclude group: "commons-logging" - } - api libs.managed.jcl.over.slf4j - api projects.functionAws - api projects.awsCommon - - testAnnotationProcessor mn.micronaut.validation - testImplementation mn.micronaut.validation - - testImplementation mn.micronaut.inject.java - testImplementation mn.micronaut.http.client - testImplementation mn.micronaut.security - - testImplementation mn.micronaut.views.handlebars - - testImplementation libs.jackson.afterburner - testImplementation libs.servlet.api - testImplementation libs.fileupload -} - -spotless { - java { - targetExclude '**/io/micronaut/function/aws/proxy/QueryStringDecoder.java' - } -} diff --git a/function-aws-api-proxy/build.gradle.kts b/function-aws-api-proxy/build.gradle.kts new file mode 100644 index 0000000000..cc053f2e5e --- /dev/null +++ b/function-aws-api-proxy/build.gradle.kts @@ -0,0 +1,42 @@ +plugins { + id("io.micronaut.build.internal.module") +} + +dependencies { + annotationProcessor(mn.micronaut.graal) + + compileOnly(mn.micronaut.security) + + implementation(mn.micronaut.http.netty) + + implementation(libs.projectreactor) + + api(mn.micronaut.http.server) + api(libs.managed.aws.serverless.core) { + exclude(group = "javax.servlet", module = "javax.servlet-api") + exclude(group = "com.fasterxml.jackson.module", module = "jackson-module-afterburner") + exclude(group = "commons-logging") + } + api(libs.managed.jcl.over.slf4j) + api(project(":function-aws")) + api(project(":aws-common")) + + testAnnotationProcessor(mn.micronaut.validation) + testImplementation(mn.micronaut.validation) + + testImplementation(mn.micronaut.inject.java) + testImplementation(mn.micronaut.http.client) + testImplementation(mn.micronaut.security) + + testImplementation(mn.micronaut.views.handlebars) + + testImplementation(libs.jackson.afterburner) + testImplementation(libs.servlet.api) + testImplementation(libs.fileupload) +} + +spotless { + java { + targetExclude("**/io/micronaut/function/aws/proxy/QueryStringDecoder.java") + } +} diff --git a/function-aws-custom-runtime/build.gradle b/function-aws-custom-runtime/build.gradle deleted file mode 100644 index 65a0d692b9..0000000000 --- a/function-aws-custom-runtime/build.gradle +++ /dev/null @@ -1,17 +0,0 @@ -plugins { - id("io.micronaut.build.internal.aws-module") -} - -dependencies { - annotationProcessor mn.micronaut.graal - compileOnly projects.functionAwsApiProxy - - api mn.micronaut.http.client - api libs.managed.aws.lambda.events - - testImplementation projects.functionAwsApiProxy, { - exclude group:'com.fasterxml.jackson.module', module:'jackson-module-afterburner' - } - testImplementation mn.micronaut.inject.java - testImplementation mn.micronaut.http.server.netty -} diff --git a/function-aws-custom-runtime/build.gradle.kts b/function-aws-custom-runtime/build.gradle.kts new file mode 100644 index 0000000000..22a5b83d27 --- /dev/null +++ b/function-aws-custom-runtime/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + id("io.micronaut.build.internal.module") +} + +dependencies { + annotationProcessor(mn.micronaut.graal) + compileOnly(project(":function-aws-api-proxy")) + + api(mn.micronaut.http.client) + api(libs.managed.aws.lambda.events) + + testImplementation(project(":function-aws-api-proxy")) { + exclude(group = "com.fasterxml.jackson.module", module = "jackson-module-afterburner") + } + testImplementation(mn.micronaut.inject.java) + testImplementation(mn.micronaut.http.server.netty) +} diff --git a/function-aws-custom-runtime/src/test/groovy/io/micronaut/function/aws/runtime/MicronautLambdaRuntimeSpec.groovy b/function-aws-custom-runtime/src/test/groovy/io/micronaut/function/aws/runtime/RuntimeApiSpec.groovy similarity index 58% rename from function-aws-custom-runtime/src/test/groovy/io/micronaut/function/aws/runtime/MicronautLambdaRuntimeSpec.groovy rename to function-aws-custom-runtime/src/test/groovy/io/micronaut/function/aws/runtime/RuntimeApiSpec.groovy index 3237926511..1c94ab0af0 100644 --- a/function-aws-custom-runtime/src/test/groovy/io/micronaut/function/aws/runtime/MicronautLambdaRuntimeSpec.groovy +++ b/function-aws-custom-runtime/src/test/groovy/io/micronaut/function/aws/runtime/RuntimeApiSpec.groovy @@ -1,73 +1,34 @@ -/* - * Copyright 2017-2019 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ package io.micronaut.function.aws.runtime import com.amazonaws.serverless.proxy.model.ApiGatewayRequestIdentity import com.amazonaws.serverless.proxy.model.AwsProxyRequest import com.amazonaws.serverless.proxy.model.AwsProxyRequestContext import com.amazonaws.serverless.proxy.model.AwsProxyResponse +import com.amazonaws.services.lambda.runtime.Context import io.micronaut.context.ApplicationContext +import io.micronaut.context.BeanProvider +import io.micronaut.context.annotation.Any import io.micronaut.context.annotation.Requires -import io.micronaut.function.aws.MicronautRequestHandler -import io.micronaut.http.HttpHeaders import io.micronaut.http.HttpResponse +import io.micronaut.http.MediaType import io.micronaut.http.annotation.Body import io.micronaut.http.annotation.Controller import io.micronaut.http.annotation.Get import io.micronaut.http.annotation.PathVariable import io.micronaut.http.annotation.Post +import io.micronaut.http.annotation.Produces import io.micronaut.runtime.server.EmbeddedServer import spock.lang.Specification import spock.util.concurrent.PollingConditions -import spock.util.environment.RestoreSystemProperties - -class MicronautLambdaRuntimeSpec extends Specification { - - @RestoreSystemProperties - void "Tracing header propagated as system property"() { - given: - String traceHeader = 'Root=1-5759e988-bd862e3fe1be46a994272793;Sampled=1' - EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer, ['spec.name': 'MicronautLambdaRuntimeSpec']) - String serverUrl = "localhost:$embeddedServer.port" - CustomMicronautLambdaRuntime customMicronautLambdaRuntime = new CustomMicronautLambdaRuntime(serverUrl) - Thread t = new Thread({ -> - customMicronautLambdaRuntime.run([] as String[]) - }) - t.start() - - when: - def httpHeaders = Stub(HttpHeaders) { - get(LambdaRuntimeInvocationResponseHeaders.LAMBDA_RUNTIME_TRACE_ID) >> traceHeader - } - customMicronautLambdaRuntime.propagateTraceId(httpHeaders) - - then: - System.getProperty(MicronautRequestHandler.LAMBDA_TRACE_HEADER_PROP) == traceHeader - - cleanup: - embeddedServer.close() - } +class RuntimeApiSpec extends Specification { void "test runtime API loop"() { given: - EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer, ['spec.name': 'MicronautLambdaRuntimeSpec']) + EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer, ['spec.name': 'RuntimeApiSpec']) String serverUrl = "localhost:$embeddedServer.port" CustomMicronautLambdaRuntime customMicronautLambdaRuntime = new CustomMicronautLambdaRuntime(serverUrl) Thread t = new Thread({ -> - customMicronautLambdaRuntime.run([] as String[]) + customMicronautLambdaRuntime.run([] as String[]) }) t.start() @@ -77,7 +38,7 @@ class MicronautLambdaRuntimeSpec extends Specification { new PollingConditions(timeout: 5).eventually { assert lambadaRuntimeApi.responses assert lambadaRuntimeApi.responses['123456'] - assert lambadaRuntimeApi.responses['123456'].body == 'Hello 123456' + assert lambadaRuntimeApi.responses['123456'].body == "Hello 123456" } cleanup: @@ -103,14 +64,16 @@ class MicronautLambdaRuntimeSpec extends Specification { @Controller("/hello") static class HelloController { + @Any BeanProvider context + @Produces(MediaType.TEXT_PLAIN) @Get("/world") - String index(AwsProxyRequest request) { - return "Hello " + request.getRequestContext().getRequestId() + String index() { + return "Hello " + context.get().awsRequestId } } - @Requires(property = 'spec.name', value = 'MicronautLambdaRuntimeSpec') + @Requires(property = 'spec.name', value = 'RuntimeApiSpec') @Controller("/") static class MockLambadaRuntimeApi { @@ -126,7 +89,7 @@ class MicronautLambdaRuntimeSpec extends Specification { context.setIdentity(new ApiGatewayRequestIdentity()) req.setRequestContext(context) HttpResponse.ok(req) - .header(LambdaRuntimeInvocationResponseHeaders.LAMBDA_RUNTIME_AWS_REQUEST_ID, "123456") + .header(LambdaRuntimeInvocationResponseHeaders.LAMBDA_RUNTIME_AWS_REQUEST_ID, "123456") } @Post("/2018-06-01/runtime/invocation/{requestId}/response") @@ -135,5 +98,4 @@ class MicronautLambdaRuntimeSpec extends Specification { return HttpResponse.accepted() } } - } diff --git a/function-aws-custom-runtime/src/test/groovy/io/micronaut/function/aws/runtime/TracingHeaderPropagationSysPropertySpec.groovy b/function-aws-custom-runtime/src/test/groovy/io/micronaut/function/aws/runtime/TracingHeaderPropagationSysPropertySpec.groovy new file mode 100644 index 0000000000..2ec238d394 --- /dev/null +++ b/function-aws-custom-runtime/src/test/groovy/io/micronaut/function/aws/runtime/TracingHeaderPropagationSysPropertySpec.groovy @@ -0,0 +1,69 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.function.aws.runtime + + +import io.micronaut.context.ApplicationContext +import io.micronaut.function.aws.MicronautRequestHandler +import io.micronaut.http.HttpHeaders +import io.micronaut.runtime.server.EmbeddedServer +import spock.lang.Specification +import spock.util.environment.RestoreSystemProperties + +class TracingHeaderPropagationSysPropertySpec extends Specification { + + @RestoreSystemProperties + void "Tracing header propagated as system property"() { + given: + String traceHeader = 'Root=1-5759e988-bd862e3fe1be46a994272793;Sampled=1' + EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer, [:]) + String serverUrl = "localhost:$embeddedServer.port" + CustomMicronautLambdaRuntime customMicronautLambdaRuntime = new CustomMicronautLambdaRuntime(serverUrl) + Thread t = new Thread({ -> + customMicronautLambdaRuntime.run([] as String[]) + }) + t.start() + + when: + def httpHeaders = Stub(HttpHeaders) { + get(LambdaRuntimeInvocationResponseHeaders.LAMBDA_RUNTIME_TRACE_ID) >> traceHeader + } + customMicronautLambdaRuntime.propagateTraceId(httpHeaders) + + then: + System.getProperty(MicronautRequestHandler.LAMBDA_TRACE_HEADER_PROP) == traceHeader + + cleanup: + embeddedServer.close() + } + + class CustomMicronautLambdaRuntime extends MicronautLambdaRuntime { + + String serverUrl + + CustomMicronautLambdaRuntime(String serverUrl) { + super() + this.serverUrl = serverUrl + } + + @Override + String getEnv(String name) { + if (name == ReservedRuntimeEnvironmentVariables.AWS_LAMBDA_RUNTIME_API) { + return serverUrl + } + } + } +} diff --git a/function-aws-test/build.gradle b/function-aws-test/build.gradle deleted file mode 100644 index ff4d62dfe7..0000000000 --- a/function-aws-test/build.gradle +++ /dev/null @@ -1,11 +0,0 @@ -plugins { - id("io.micronaut.build.internal.aws-module") -} - -dependencies { - implementation libs.micronaut.test.junit5 - - api projects.functionAws - api mn.micronaut.function - testAnnotationProcessor mn.micronaut.inject.java -} diff --git a/function-aws-test/build.gradle.kts b/function-aws-test/build.gradle.kts new file mode 100644 index 0000000000..799040d238 --- /dev/null +++ b/function-aws-test/build.gradle.kts @@ -0,0 +1,11 @@ +plugins { + id("io.micronaut.build.internal.module") +} + +dependencies { + implementation(mn.micronaut.test.junit5) + + api(project(":function-aws")) + api(mn.micronaut.function) + testAnnotationProcessor(mn.micronaut.inject.java) +} diff --git a/function-aws/build.gradle b/function-aws/build.gradle deleted file mode 100644 index 121ec6b55a..0000000000 --- a/function-aws/build.gradle +++ /dev/null @@ -1,14 +0,0 @@ -plugins { - id("io.micronaut.build.internal.aws-module") -} - -dependencies { - api mn.micronaut.function - api libs.managed.aws.lambda.core - api mn.micronaut.jackson.databind - - testImplementation(libs.micronaut.mongo.sync) - testImplementation(libs.testcontainers.spock) - testImplementation(libs.testcontainers.mongodb) - testImplementation(libs.testcontainers) -} diff --git a/function-aws/build.gradle.kts b/function-aws/build.gradle.kts new file mode 100644 index 0000000000..63f0e04342 --- /dev/null +++ b/function-aws/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + id("io.micronaut.build.internal.module") +} + +dependencies { + api(mn.micronaut.function) + api(libs.managed.aws.lambda.core) + + testImplementation(mn.micronaut.mongo.sync) + testImplementation(libs.testcontainers.spock) + testImplementation(libs.testcontainers.mongodb) + testImplementation(libs.testcontainers) +} diff --git a/function-aws/src/main/java/io/micronaut/function/aws/MicronautRequestHandler.java b/function-aws/src/main/java/io/micronaut/function/aws/MicronautRequestHandler.java index 9954e109f0..a0e54c5082 100644 --- a/function-aws/src/main/java/io/micronaut/function/aws/MicronautRequestHandler.java +++ b/function-aws/src/main/java/io/micronaut/function/aws/MicronautRequestHandler.java @@ -19,12 +19,14 @@ import com.amazonaws.services.lambda.runtime.RequestHandler; import io.micronaut.context.ApplicationContext; import io.micronaut.context.ApplicationContextBuilder; +import io.micronaut.context.event.ApplicationEventPublisher; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.convert.ArgumentConversionContext; import io.micronaut.core.convert.ConversionContext; import io.micronaut.core.convert.ConversionError; import io.micronaut.core.reflect.GenericTypeUtils; import io.micronaut.core.util.ArrayUtils; +import io.micronaut.function.aws.event.AfterExecutionEvent; import io.micronaut.function.executor.AbstractFunctionExecutor; import java.util.Optional; @@ -47,13 +49,20 @@ public abstract class MicronautRequestHandler extends AbstractFunctionExec @SuppressWarnings("unchecked") private final Class inputType = initTypeArgument(); + private ApplicationEventPublisher eventPublisher; + /** * Default constructor; will initialize a suitable {@link ApplicationContext} for * Lambda deployment. */ public MicronautRequestHandler() { - buildApplicationContext(null); - injectIntoApplicationContext(); + try { + buildApplicationContext(null); + injectIntoApplicationContext(); + } catch (Exception e) { + LOG.error("Exception initializing handler", e); + throw e; + } } /** @@ -62,8 +71,14 @@ public MicronautRequestHandler() { */ public MicronautRequestHandler(ApplicationContext applicationContext) { this.applicationContext = applicationContext; - startEnvironment(applicationContext); - injectIntoApplicationContext(); + + try { + startEnvironment(applicationContext); + injectIntoApplicationContext(); + } catch (Exception e) { + LOG.error("Exception initializing handler: " + e.getMessage() , e); + throw e; + } } /** @@ -84,7 +99,14 @@ public final O handleRequest(I input, Context context) { if (!inputType.isInstance(input)) { input = convertInput(input); } - return this.execute(input); + try { + O output = this.execute(input); + resolveAfterExecutionPublisher().publishEvent(AfterExecutionEvent.success(context, output)); + return output; + } catch (Throwable re) { + resolveAfterExecutionPublisher().publishEvent(AfterExecutionEvent.failure(context, re)); + throw re; + } } /** @@ -130,4 +152,11 @@ private Class initTypeArgument() { return Object.class; } } + + private ApplicationEventPublisher resolveAfterExecutionPublisher() { + if (eventPublisher == null) { + eventPublisher = applicationContext.getEventPublisher(AfterExecutionEvent.class); + } + return eventPublisher; + } } diff --git a/function-aws/src/main/java/io/micronaut/function/aws/MicronautRequestStreamHandler.java b/function-aws/src/main/java/io/micronaut/function/aws/MicronautRequestStreamHandler.java index beac31e37a..2c30d20a03 100644 --- a/function-aws/src/main/java/io/micronaut/function/aws/MicronautRequestStreamHandler.java +++ b/function-aws/src/main/java/io/micronaut/function/aws/MicronautRequestStreamHandler.java @@ -21,8 +21,12 @@ import io.micronaut.context.ApplicationContextBuilder; import io.micronaut.context.env.Environment; import io.micronaut.core.annotation.NonNull; +import io.micronaut.context.event.ApplicationEventPublisher; import io.micronaut.core.annotation.Nullable; +import io.micronaut.function.aws.event.AfterExecutionEvent; import io.micronaut.function.executor.StreamFunctionExecutor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.InputStream; @@ -36,6 +40,13 @@ */ public class MicronautRequestStreamHandler extends StreamFunctionExecutor implements RequestStreamHandler, MicronautLambdaContext { + /** + * Logger for the application context creation errors. + */ + private static final Logger LOG = LoggerFactory.getLogger(MicronautRequestStreamHandler.class); + + private ApplicationEventPublisher eventPublisher; + @Nullable private String ctxFunctionName; @@ -47,7 +58,12 @@ public MicronautRequestStreamHandler() { // initialize the application context in the constructor // this is faster in Lambda as init cost is giving higher processor priority // see https://github.com/micronaut-projects/micronaut-aws/issues/18#issuecomment-530903419 - buildApplicationContext(null); + try { + buildApplicationContext(null); + } catch (Exception e) { + LOG.error("Exception initializing handler: " + e.getMessage(), e); + throw e; + } } /** @@ -64,7 +80,13 @@ public void handleRequest(InputStream input, OutputStream output, Context contex this.ctxFunctionName = context.getFunctionName(); } HandlerUtils.configureWithContext(this, context); - execute(input, output, context); + try { + execute(input, output, context); + resolveAfterExecutionPublisher().publishEvent(AfterExecutionEvent.success(context, null)); + } catch (Throwable e) { + resolveAfterExecutionPublisher().publishEvent(AfterExecutionEvent.failure(context, e)); + throw e; + } } @Override @@ -84,6 +106,13 @@ protected String resolveFunctionName(Environment env) { return (functionName != null) ? functionName : ctxFunctionName; } + private ApplicationEventPublisher resolveAfterExecutionPublisher() { + if (eventPublisher == null) { + eventPublisher = applicationContext.getEventPublisher(AfterExecutionEvent.class); + } + return eventPublisher; + } + @Override public void close() { // Don't close the application context diff --git a/function-aws/src/main/java/io/micronaut/function/aws/event/AfterExecutionEvent.java b/function-aws/src/main/java/io/micronaut/function/aws/event/AfterExecutionEvent.java new file mode 100644 index 0000000000..42be5321a2 --- /dev/null +++ b/function-aws/src/main/java/io/micronaut/function/aws/event/AfterExecutionEvent.java @@ -0,0 +1,99 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.function.aws.event; + +import com.amazonaws.services.lambda.runtime.Context; +import io.micronaut.core.annotation.Nullable; + +/** + * This event is published after the execution of {@link io.micronaut.function.aws.MicronautRequestHandler#execute(Object)} + * and {@link io.micronaut.function.aws.MicronautRequestStreamHandler#execute(java.io.InputStream, java.io.OutputStream)} methods to allow + * performing actions before the Lambda function run is finished and the JVM is hibernated. + *

+ * This event must be processed synchronously to guarantee it has been processed before the Lambda funciton is hibernated. + * + * @author Vladimir Orany + * @since 3.9.0 + */ +public final class AfterExecutionEvent { + + @Nullable + private final Context context; + + @Nullable + private final Throwable exception; + @Nullable + private final Object output; + + private AfterExecutionEvent(@Nullable Context context, @Nullable Object output, @Nullable Throwable exception) { + this.context = context; + this.output = output; + this.exception = exception; + } + + /** + * Creates a new {@link AfterExecutionEvent} with an optional result of the execution. + * + * @param context AWS Lambda context + * @param output an optional result of the exectuion + * @return a new {@link AfterExecutionEvent} with an optional result of the execution + */ + public static AfterExecutionEvent success(@Nullable Context context, @Nullable Object output) { + return new AfterExecutionEvent(context, output, null); + } + + /** + * Creates a new {@link AfterExecutionEvent} with an exception been thrown. + * + * @param context AWS Lambda context + * @param exception the exception which has been thrown during the execution + * @return a new {@link AfterExecutionEvent} with an exception been thrown. + */ + public static AfterExecutionEvent failure(@Nullable Context context, Throwable exception) { + return new AfterExecutionEvent(context, null, exception); + } + + /** + * @return true if there were no exception thrown + */ + public boolean isSuccess() { + return exception == null; + } + + /** + * @return the optional result of the execution + */ + @Nullable + public Object getOutput() { + return output; + } + + /** + * @return the optional exception which has been thrown + */ + @Nullable + public Throwable getException() { + return exception; + } + + /** + * @return the optional Lambda context + */ + @Nullable + public Context getContext() { + return context; + } +} diff --git a/function-aws/src/main/java/io/micronaut/function/aws/event/package-info.java b/function-aws/src/main/java/io/micronaut/function/aws/event/package-info.java new file mode 100644 index 0000000000..be196a7c54 --- /dev/null +++ b/function-aws/src/main/java/io/micronaut/function/aws/event/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Event classes to support AWS Lambda lifecycle. + * + * @author Vladimir Orany + * @since 3.8.0 + */ +package io.micronaut.function.aws.event; diff --git a/function-aws/src/test/groovy/io/micronaut/function/aws/AfterExecutionEventSpec.groovy b/function-aws/src/test/groovy/io/micronaut/function/aws/AfterExecutionEventSpec.groovy new file mode 100644 index 0000000000..f9e7a9eb15 --- /dev/null +++ b/function-aws/src/test/groovy/io/micronaut/function/aws/AfterExecutionEventSpec.groovy @@ -0,0 +1,179 @@ +package io.micronaut.function.aws + +import com.amazonaws.services.lambda.runtime.Context +import groovy.transform.CompileStatic +import groovy.transform.InheritConstructors +import io.micronaut.context.ApplicationContext +import io.micronaut.context.ApplicationContextBuilder +import io.micronaut.context.annotation.Requires +import io.micronaut.context.env.Environment +import io.micronaut.context.event.ApplicationEventListener +import io.micronaut.core.annotation.NonNull +import io.micronaut.function.FunctionBean +import io.micronaut.function.aws.event.AfterExecutionEvent +import jakarta.inject.Singleton +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import spock.lang.Specification +import java.util.function.Function + +class AfterExecutionEventSpec extends Specification { + void 'micronaut request handler with success without AfterExecutionEvent listener'() { + given: + Handler handler = new Handler() + ApplicationContext context = handler.applicationContext + + when: + String output = handler.handleRequest('hello', Mock(Context)) + then: + output == 'olleh' + !context.containsBean(AfterExecutionEventListener) + + cleanup: + handler.close() + } + + void 'micronaut request handler with success'() { + given: + Handler handler = new Handler(builderWithSpecName("AfterExecutionEventSpec")) + ApplicationContext context = handler.applicationContext + + when: + String output = handler.handleRequest('hello', Mock(Context)) + then: + output == 'olleh' + context.containsBean(AfterExecutionEventListener) + context.getBean(AfterExecutionEventListener).lastEvent + context.getBean(AfterExecutionEventListener).lastEvent.success + context.getBean(AfterExecutionEventListener).lastEvent.output == output + + cleanup: + handler.close() + } + + void 'micronaut request handler with failure'() { + given: + Handler handler = new Handler(builderWithSpecName("AfterExecutionEventSpec")) + ApplicationContext context = handler.applicationContext + + when: + handler.handleRequest('foo', Mock(Context)) + then: + thrown(IllegalArgumentException) + + context.containsBean(AfterExecutionEventListener) + context.getBean(AfterExecutionEventListener).lastEvent + !context.getBean(AfterExecutionEventListener).lastEvent.success + context.getBean(AfterExecutionEventListener).lastEvent.exception instanceof IllegalArgumentException + + cleanup: + handler.close() + } + + void 'micronaut steam request handler with success'() { + given: + StreamHandler handler = new StreamHandler() + ApplicationContext context = handler.applicationContext + + when: + ByteArrayOutputStream outputStream = new ByteArrayOutputStream() + handler.handleRequest(new ByteArrayInputStream('hello'.bytes), outputStream, Mock(Context)) + then: + outputStream.toString() == 'olleh' + context.containsBean(AfterExecutionEventListener) + context.getBean(AfterExecutionEventListener).lastEvent + context.getBean(AfterExecutionEventListener).lastEvent.success + // no output capturing for streaming implementation + context.getBean(AfterExecutionEventListener).lastEvent.output == null + + cleanup: + handler.close() + } + + void 'micronaut steam request handler with failure'() { + given: + StreamHandler handler = new StreamHandler() + ApplicationContext context = handler.applicationContext + + when: + ByteArrayOutputStream outputStream = new ByteArrayOutputStream() + handler.handleRequest(new ByteArrayInputStream('foo'.bytes), outputStream, Mock(Context)) + + then: + thrown(IllegalArgumentException) + context.containsBean(AfterExecutionEventListener) + context.getBean(AfterExecutionEventListener).lastEvent + !context.getBean(AfterExecutionEventListener).lastEvent.success + context.getBean(AfterExecutionEventListener).lastEvent.exception instanceof IllegalArgumentException + + cleanup: + handler.close() + } + + @NonNull + private static ApplicationContextBuilder builderWithSpecName(@NonNull String specName) { + Map properties = Collections.singletonMap("spec.name", specName) + ApplicationContextBuilder contextBuilder = new LambdaApplicationContextBuilder() + contextBuilder.properties(properties) + contextBuilder + } + + @Requires(property = "spec.name", value = "AfterExecutionEventSpec") + @Singleton + @CompileStatic + static class AfterExecutionEventListener implements ApplicationEventListener { + + AfterExecutionEvent lastEvent + + @Override + void onApplicationEvent(AfterExecutionEvent event) { + lastEvent = event + } + + } + + @Requires(property = "spec.name", value = "AfterExecutionEventSpec") + @FunctionBean("afterExecutionFun") + static class EventLogger implements Function { + private static final Logger LOG = LoggerFactory.getLogger(EventLogger.class); + @Override + String apply(String input) { + testScenario().apply(input) + } + } + + @InheritConstructors + class Handler extends MicronautRequestHandler { + @Override + String execute(String input) { + testScenario().apply(input) + } + } + + @InheritConstructors + class StreamHandler extends MicronautRequestStreamHandler { + + @Override + protected String resolveFunctionName(Environment env) { + return "afterExecutionFun" + } + + @Override + @NonNull + protected ApplicationContextBuilder newApplicationContextBuilder() { + return super.newApplicationContextBuilder().properties(Collections.singletonMap("spec.name", "AfterExecutionEventSpec")) + } + } + + private static Function testScenario() { + return new Function() { + @Override + String apply(String input) { + if (input == 'foo') { + throw new IllegalArgumentException('No foo allowed!') + } + return input.reverse() + } + } + } +} diff --git a/function-aws/src/test/groovy/io/micronaut/function/aws/MicronautStreamHandlerLambdaContextSpec.groovy b/function-aws/src/test/groovy/io/micronaut/function/aws/MicronautStreamHandlerLambdaContextSpec.groovy index c5386d54fe..9af69c8a5f 100644 --- a/function-aws/src/test/groovy/io/micronaut/function/aws/MicronautStreamHandlerLambdaContextSpec.groovy +++ b/function-aws/src/test/groovy/io/micronaut/function/aws/MicronautStreamHandlerLambdaContextSpec.groovy @@ -96,6 +96,7 @@ class MicronautStreamHandlerLambdaContextSpec extends Specification { RequestIdProvider requestIdProvider @Override + @NonNull protected ApplicationContextBuilder newApplicationContextBuilder() { super.newApplicationContextBuilder().properties(Collections.singletonMap( "spec.name", "MicronautStreamHandlerLambdaContextSpec" diff --git a/function-client-aws/build.gradle b/function-client-aws/build.gradle deleted file mode 100644 index 7ebb79d932..0000000000 --- a/function-client-aws/build.gradle +++ /dev/null @@ -1,17 +0,0 @@ -plugins { - id("io.micronaut.build.internal.aws-module") -} - -dependencies { - api projects.awsSdkV1 - implementation "com.amazonaws:aws-java-sdk-lambda" - implementation mn.reactor - api libs.micronaut.function.client - - testAnnotationProcessor mn.micronaut.inject.java - testImplementation mn.micronaut.inject.java - testImplementation mn.micronaut.http.server.netty - testImplementation mn.micronaut.function.web - testImplementation libs.micronaut.function.groovy - testImplementation mn.micronaut.runtime.groovy -} diff --git a/function-client-aws/build.gradle.kts b/function-client-aws/build.gradle.kts new file mode 100644 index 0000000000..e76a301c0d --- /dev/null +++ b/function-client-aws/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + id("io.micronaut.build.internal.module") +} + +dependencies { + api(project(":aws-sdk-v1")) + implementation("com.amazonaws:aws-java-sdk-lambda") + implementation(libs.projectreactor) + api(mn.micronaut.function.client) + + testAnnotationProcessor(mn.micronaut.inject.java) + testImplementation(mn.micronaut.inject.java) + testImplementation(mn.micronaut.http.server.netty) + testImplementation(mn.micronaut.function.web) + testImplementation(mn.micronaut.function.groovy) + testImplementation(mn.micronaut.runtime.groovy) +} diff --git a/gradle.properties b/gradle.properties index ac4304c988..a495e2b96f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ projectGroup=io.micronaut.aws micronautDocsVersion=2.0.0 micronautVersion=4.0.0-SNAPSHOT -micronautStarterVersion=3.6.1 +micronautStarterVersion=3.7.3 micronautTestVersion=4.0.0-SNAPSHOT groovyVersion=4.0.6 spockVersion=2.3-groovy-4.0 @@ -15,6 +15,7 @@ githubSlug=micronaut-projects/micronaut-aws developers=Graeme Rocher githubCoreBranch=4.0.x + bomProperty=micronautAwsVersion org.gradle.caching=true org.gradle.parallel=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3dbceb1bac..894cd5d208 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,14 +1,15 @@ [versions] bouncycastle = '1.70' fileupload = '0.0.5' -jetty = '9.4.48.v20220622' -managed-alexa-ask-sdk = "2.43.7" -managed-aws-java-sdk-v1 = '1.12.292' -managed-aws-java-sdk-v2 = '2.17.263' +jetty = '9.4.49.v20220914' +logback-json-classic = '0.1.5' +managed-alexa-ask-sdk = "2.44.0" +managed-aws-java-sdk-v1 = '1.12.321' +managed-aws-java-sdk-v2 = '2.17.292' managed-aws-lambda = '1.2.1' managed-aws-lambda-events = '3.11.0' -managed-aws-serverless-core = '1.8.2' -managed-aws-cdk-lib='2.39.1' +managed-aws-serverless-core = '1.9' +managed-aws-cdk-lib='2.46.0' servlet-api = "2.5" slf4j = "1.7.36" @@ -22,6 +23,7 @@ alexa-ask-sdk-apache-client = { module = 'com.amazon.alexa:ask-sdk-apache-client aws-ssm = { module = 'software.amazon.awssdk:ssm' } awssdk-apache-client = { module = 'software.amazon.awssdk:apache-client' } awssdk-apigatewaymanagementapi = { module = 'software.amazon.awssdk:apigatewaymanagementapi' } +awssdk-cloudwatchlogs = { module = 'software.amazon.awssdk:cloudwatchlogs'} awssdk-dynamodb = { module = 'software.amazon.awssdk:dynamodb' } awssdk-netty-nio-client = { module = 'software.amazon.awssdk:netty-nio-client' } awssdk-rekognition = { module = 'software.amazon.awssdk:rekognition' } @@ -43,6 +45,8 @@ jackson-afterburner = { module = 'com.fasterxml.jackson.module:jackson-module-af jetty-server = { module = 'org.eclipse.jetty:jetty-server', version.ref = 'jetty' } jcl-over-slf4j = { module = 'org.slf4j:jcl-over-slf4j', version.ref = 'slf4j' } jupiter-engine = { module = 'org.junit.jupiter:junit-jupiter-engine' } +logback-json-classic = { module = 'ch.qos.logback.contrib:logback-json-classic', version.ref = 'logback-json-classic' } +jackson-databind = { module = 'com.fasterxml.jackson.core:jackson-databind' } managed-alexa-ask-sdk-core = { module = 'com.amazon.alexa:ask-sdk-core', version.ref = 'managed-alexa-ask-sdk' } managed-alexa-ask-sdk-lambda = { module = 'com.amazon.alexa:ask-sdk-lambda-support', version.ref = 'managed-alexa-ask-sdk' } @@ -53,15 +57,6 @@ managed-awssdk-secretsmanager = { module = 'software.amazon.awssdk:secretsmanage managed-aws-serverless-core = { module = 'com.amazonaws.serverless:aws-serverless-java-container-core', version.ref = 'managed-aws-serverless-core' } managed-jcl-over-slf4j = { module = 'org.slf4j:jcl-over-slf4j', version.ref = 'slf4j' } -micronaut-function = { module = 'io.micronaut:micronaut-function' } -micronaut-function-client = { module = 'io.micronaut:micronaut-function-client' } -micronaut-function-groovy = { module = 'io.micronaut.groovy:micronaut-function-groovy' } -micronaut-function-web = { module = 'io.micronaut:micronaut-function-web' } -micronaut-mongo-sync = { module = 'io.micronaut.mongodb:micronaut-mongo-sync' } -micronaut-runtime-groovy = { module = 'io.micronaut.groovy:micronaut-runtime-groovy' } -micronaut-test-junit5 = { module = 'io.micronaut.test:micronaut-test-junit5' } -micronaut-views-handlebars = { module = 'io.micronaut.views:micronaut-views-handlebars' } - servlet-api = { module = 'javax.servlet:servlet-api', version.ref = 'servlet-api' } testcontainers = { module = 'org.testcontainers:testcontainers' } testcontainers-mongodb = { module = 'org.testcontainers:mongodb' } diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index 87f118164d..0000000000 --- a/settings.gradle +++ /dev/null @@ -1,47 +0,0 @@ -pluginManagement { - repositories { - gradlePluginPortal() - mavenCentral() - } -} - -plugins { - id 'io.micronaut.build.shared.settings' version '6.0.1' -} - -enableFeaturePreview 'TYPESAFE_PROJECT_ACCESSORS' - -rootProject.name = 'aws-parent' - -include 'aws-bom' -include 'function-aws' -include 'function-client-aws' -include 'function-aws-api-proxy' -include 'function-aws-api-proxy-test' -include 'aws-secretsmanager' -include 'aws-alexa' -include 'aws-distributed-configuration' -include 'aws-parameter-store' -include 'aws-service-discovery' -include 'aws-alexa-httpserver' -include 'function-aws-alexa' -include 'function-aws-custom-runtime' -include 'aws-common' -include 'aws-sdk-v1' -include 'aws-sdk-v2' -include 'aws-cdk' -include 'function-aws-test' -include 'test-suite' -include 'test-suite-groovy' -include 'test-suite-kotlin' - -dependencyResolutionManagement { - repositories { - mavenCentral() - maven { url "https://s01.oss.sonatype.org/content/repositories/snapshots/" } - } -} - -micronautBuild { - importMicronautCatalog() -} diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000000..3eed27aa43 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,49 @@ +pluginManagement { + repositories { + gradlePluginPortal() + mavenCentral() + } +} + +plugins { + id("io.micronaut.build.shared.settings") version "6.0.1" +} + +rootProject.name = "aws-parent" + +include("aws-bom") +include("function-aws") +include("function-client-aws") +include("function-aws-api-proxy") +include("function-aws-api-proxy-test") +include("aws-secretsmanager") +include("aws-alexa") +include("aws-distributed-configuration") +include("aws-parameter-store") +include("aws-service-discovery") +include("aws-alexa-httpserver") +include("function-aws-alexa") +include("function-aws-custom-runtime") +include("aws-common") +include("aws-sdk-v1") +include("aws-sdk-v2") +include("aws-cdk") +include("aws-cloudwatch-logging") +include("function-aws-test") +include("test-suite") +include("test-suite-aws-sdk-v2") +include("test-suite-groovy") +include("test-suite-kotlin") + +configure { + importMicronautCatalog() +} + +dependencyResolutionManagement { + repositories { + mavenCentral() + maven { + setUrl("https://s01.oss.sonatype.org/content/repositories/snapshots/") + } + } +} diff --git a/src/main/docs/guide/alexa/localeresoultion.adoc b/src/main/docs/guide/alexa/localeresoultion.adoc new file mode 100644 index 0000000000..74af387d54 --- /dev/null +++ b/src/main/docs/guide/alexa/localeresoultion.adoc @@ -0,0 +1,7 @@ +To resolve the https://developer.amazon.com/en-US/docs/alexa/custom-skills/request-and-response-json-reference.html#request-locale[Request Locale] inject a bean of type `api:aws.alexa.locale.HandlerInputLocaleResolver[]`. + +You can force a fixed locale or a custom default locale via configuration: + +include::{includedir}configurationProperties/io.micronaut.aws.alexa.locale.HandlerInputLocaleResolutionConfigurationProperties.adoc[] + +If the built-in methods do not meet your use case, create a bean of type api:aws.alexa.locale.HandlerInputLocaleResolver[] and set its order (through the `getOrder` method) relative to the existing resolvers. diff --git a/src/main/docs/guide/distributedconfiguration/distributedconfigurationsecretsmanager.adoc b/src/main/docs/guide/distributedconfiguration/distributedconfigurationsecretsmanager.adoc index 7e5b02127d..0c0d23a313 100644 --- a/src/main/docs/guide/distributedconfiguration/distributedconfigurationsecretsmanager.adoc +++ b/src/main/docs/guide/distributedconfiguration/distributedconfigurationsecretsmanager.adoc @@ -17,4 +17,25 @@ If you add `micronaut.application.name: myapp` to `bootstrap.yml` and you start image::secretsmanager.png[AWS Secrets Manager] -include::{includedir}configurationProperties/io.micronaut.aws.secretsmanager.SecretsManagerConfigurationProperties.adoc[] \ No newline at end of file +include::{includedir}configurationProperties/io.micronaut.aws.secretsmanager.SecretsManagerConfigurationProperties.adoc[] + +To avoid secret keys naming collision in the Micronaut application context, which is caused by the strict naming convention specified by some AWS services, add a configuration section to `src/main/resources/bootstrap.yml`. +For example, if you would like to use multiple RDS instances, you can do it in the following way: +[source, yaml] +.src/main/resources/bootstrap.yml +---- +micronaut: + config-client: + enabled: true +aws: + secretsmanager: + secrets: + - secret-name: rds + prefix: datasources.default + - secret-name: rds_backup + prefix: datasources.backup +---- +Note that `secret-name` is a name suffix of the secret configured in AWS Secret Manager. For example, to add `rds_backup` configuration for a `production` environment, add the `rds_backup` configuration entry to the `src/main/resources/bootstrap.yml` as shown in the example above. Also, create RDS secret in AWS Secret Manager with name `/config/application_prod/rds_backup` or `/config/[APPLICATION_NAME]_prod/rds_backup`. +Note that `prefix` is a unique key prefix that is prepended to all keys that belong to a given secret. + +To learn more about the Micronaut environments, go to https://docs.micronaut.io/latest/guide/#environments[Environments] diff --git a/src/main/docs/guide/lambda/afterExecutionEvent.adoc b/src/main/docs/guide/lambda/afterExecutionEvent.adoc new file mode 100644 index 0000000000..d3cad98899 --- /dev/null +++ b/src/main/docs/guide/lambda/afterExecutionEvent.adoc @@ -0,0 +1,3 @@ +AWS Lambda runtime will reuse the instances of your handler classes but the Java Virtual Machine will be suspended between the invocation. Every call on a single instance of your handler will reuse the same application context. If you need to perform actions before the JVM gets suspended then you can create **synchronous** handler for `AfterExecutionEvent` event which is published right before the execution of the method `handleRequest` in `MicronautRequestHandler` and `MicronautRequestStreamHandler` is finished. + +To subscribe to `AfterExecutionEvent` create a `@Singleton` class which implements `ApplicationEventListener`. Learn more about https://docs.micronaut.io/latest/guide/#contextEvents[Context Events]. diff --git a/src/main/docs/guide/sdkv2/apiGatewayManagementApi.adoc b/src/main/docs/guide/sdkv2/apiGatewayManagementApi.adoc new file mode 100644 index 0000000000..67782d553c --- /dev/null +++ b/src/main/docs/guide/sdkv2/apiGatewayManagementApi.adoc @@ -0,0 +1,15 @@ +To use an API Gateway Management Api client, add the following dependency: + +dependency:apigatewaymanagementapi[groupId="software.amazon.awssdk"] + +Then, the following beans will be created: + +* `software.amazon.awssdk.services.apigatewaymanagementapi.ApiGatewayManagementApiClientBuilder` +* `software.amazon.awssdk.services.apigatewaymanagementapi.ApiGatewayManagementApiClient`. + +And: + +* `software.amazon.awssdk.services.apigatewaymanagementapi.ApiGatewayManagementApiAsyncClientBuilder` +* `software.amazon.awssdk.services.apigatewaymanagementapi.ApiGatewayManagementApiAsyncClient`. + +The HTTP client, credentials and region will be configured as per described in the <>. diff --git a/src/main/docs/guide/sdkv2/cloudWatchLogging.adoc b/src/main/docs/guide/sdkv2/cloudWatchLogging.adoc new file mode 100644 index 0000000000..7014c71119 --- /dev/null +++ b/src/main/docs/guide/sdkv2/cloudWatchLogging.adoc @@ -0,0 +1,17 @@ +NOTE: This does not apply to AWS Lambda. AWS Lambda publishes automatically to CloudWatch logs produced to standard out. + +To use the https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/WhatIsCloudWatchLogs.html[Cloudwatch Logs], add the following dependency to your project: + +dependency:io.micronaut.aws:micronaut-aws-cloudwatch-logging[] + +Then, the following beans will be created: + +* `software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsClient` +* `software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsClientBuilder` + +And: + +* `software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsAsyncClient` +* `software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsAsyncClientBuilder` + +The HTTP client, credentials and region will be configured as per described in the <>. diff --git a/src/main/docs/guide/sdkv2/cloudWatchLogging/browsingTheLogs.adoc b/src/main/docs/guide/sdkv2/cloudWatchLogging/browsingTheLogs.adoc new file mode 100644 index 0000000000..b86faa86c9 --- /dev/null +++ b/src/main/docs/guide/sdkv2/cloudWatchLogging/browsingTheLogs.adoc @@ -0,0 +1,3 @@ +When you have completed a setup, you can browse your logs on the https://console.aws.amazon.com/cloudwatch/home[Cloudwatch home]. In the Log groups menu and choose the group name and stream name of your application. In the "Log events" section you should be able to see your service logs. + +image::logs.png[Log events] diff --git a/src/main/docs/guide/sdkv2/cloudWatchLogging/emergencyAppender.adoc b/src/main/docs/guide/sdkv2/cloudWatchLogging/emergencyAppender.adoc new file mode 100644 index 0000000000..2cfb716dbb --- /dev/null +++ b/src/main/docs/guide/sdkv2/cloudWatchLogging/emergencyAppender.adoc @@ -0,0 +1,35 @@ +Since this appender is queuing up log messages and then writing them remotely, there are a number of situations which might result in log messages not getting remoted correctly. To address such scenarios you can configure the emergency appender to preserve those messages. + +Inside your `src/main/resources/logback.xml` you should add a new appender, in the example it is `STDOUT`. Inside the `CloudWatchLoggingAppender` add `appender-ref` that points to the new crated appender. + +.src/main/resources/logback.xml +[source,xml] +---- + + + true + + + %cyan(%d{HH:mm:ss.SSS}) %gray([%thread]) %highlight(%-5level) %magenta(%logger{36}) - %msg%n + + + + + + + + + + + + + + + + + + + + +---- diff --git a/src/main/docs/guide/sdkv2/cloudWatchLogging/logbackConfiguration.adoc b/src/main/docs/guide/sdkv2/cloudWatchLogging/logbackConfiguration.adoc new file mode 100644 index 0000000000..5362cc14ad --- /dev/null +++ b/src/main/docs/guide/sdkv2/cloudWatchLogging/logbackConfiguration.adoc @@ -0,0 +1,28 @@ + +Edit a `src/main/resources/logback.xml` file and make it look like this: + +.src/main/resources/logback.xml +[source,xml] +---- + + + + + + + + + + + + + + + + + + +---- +You can customize your JsonLayout with additional parameters the are available on official docs of https://javadoc.io/static/ch.qos.logback.contrib/logback-json-classic/0.1.5/ch/qos/logback/contrib/json/classic/JsonLayout.html[Logback's JsonLayout]. + +The `CloudWatchLoggingAppender` supports blacklisting the loggers by specifying the logger name. That might come handy if you want to use `level=DEBUG` or `level=TRACE` for the root logger level. diff --git a/src/main/docs/guide/sdkv2/cloudWatchLogging/openTelemetryAndLogging.adoc b/src/main/docs/guide/sdkv2/cloudWatchLogging/openTelemetryAndLogging.adoc new file mode 100644 index 0000000000..ef3b7062ba --- /dev/null +++ b/src/main/docs/guide/sdkv2/cloudWatchLogging/openTelemetryAndLogging.adoc @@ -0,0 +1,32 @@ +If you are using the https://opentelemetry.io/[OpenTelemetry] for tracing you can include `traceId` and `spanId` fields into your logs. First you have to add next dependency into your project: + +dependency:io.opentelemetry.instrumentation:opentelemetry-logback-mdc-1.0:1.16.0-alpha[scope="runtime"] + +Inside your `src/main/resources/logback.xml` you should add a new appender https://javadoc.io/doc/io.opentelemetry.instrumentation/opentelemetry-logback-1.0/latest/io/opentelemetry/instrumentation/logback/v1_0/OpenTelemetryAppender.html[io.opentelemetry.instrumentation.logback.v1_0.OpenTelemetryAppender]. + +.src/main/resources/logback.xml +[source,xml] +---- + + + + + + + + + + + + + + + + + + + + + + +---- diff --git a/src/main/docs/guide/sdkv2/cloudWatchLogging/setCloudWatchLogging.adoc b/src/main/docs/guide/sdkv2/cloudWatchLogging/setCloudWatchLogging.adoc new file mode 100644 index 0000000000..03af42a203 --- /dev/null +++ b/src/main/docs/guide/sdkv2/cloudWatchLogging/setCloudWatchLogging.adoc @@ -0,0 +1,42 @@ +By default, Micronaut application will try to create the group and stream for you using AWS client. To disable this behavior you have to set the `createGroupAndStream` flag to `false` inside your appender. +Default value for group name is your application name and for stream name is your hostname. If you want you can change them by setting value `groupName` for group name and `streamName` for stream name. + +.Configurable CloudWatchLoggingAppender Appender Properties +|=== +|Property|Type|Default value|Description + +|`groupName` +|`String` +|Application name +|Cloudwatch Log group name + +|`streamName` +|`String` +|Host name +|Cloudwatch Log stream name + +|`publishPeriod` +|`Integer` +|100 +|Time in ms between two batch publishing of logs + +|`maxBatchSize` +|`Integer` +|128 +|Time maximum number of log lines that will be sent in one batch request + +|`queueSize` +|`Integer` +|128 +|The size of publishing log queue + +|`createGroupAndStream` +|`Boolean` +|true +|If flag is set to true the Micronaut application will try to create group and stream on the AWS + +|`blackListLoggerName` +|`List` +|empty +|List of logger names that won't be published +|=== diff --git a/src/main/docs/guide/sdkv2/s3.adoc b/src/main/docs/guide/sdkv2/s3.adoc index 85fc1499fc..6d10558b15 100644 --- a/src/main/docs/guide/sdkv2/s3.adoc +++ b/src/main/docs/guide/sdkv2/s3.adoc @@ -1,4 +1,12 @@ -To use an S3 client, add the following dependency: +Micronaut provides a high-level, uniform object storage API that works across the major cloud providers: https://micronaut-projects.github.io/micronaut-object-storage/latest/guide/[Micronaut Object Storage]. + +To get started, select the `object-storage-aws` feature in https://micronaut.io/launch?features=object-storage-aws[Micronaut Launch], or add the following dependency: + +dependency:io.micronaut.objectstorage:micronaut-object-storage-aws[] + +For more information, check the https://micronaut-projects.github.io/micronaut-object-storage/latest/guide/#aws[Micronaut Object Storage AWS support] documentation. + +If you still need the low-level AWS S3 client, add the following dependency: dependency:s3[groupId="software.amazon.awssdk"] @@ -16,4 +24,4 @@ The HTTP client, credentials and region will be configured as per described in t Additionally, this service accepts the following configuration properties: -include::{includedir}configurationProperties/io.micronaut.aws.sdk.v2.service.s3.S3ConfigurationProperties.adoc[] \ No newline at end of file +include::{includedir}configurationProperties/io.micronaut.aws.sdk.v2.service.s3.S3ConfigurationProperties.adoc[] diff --git a/src/main/docs/guide/toc.yml b/src/main/docs/guide/toc.yml index 1706a8af1a..e57e56ab7f 100644 --- a/src/main/docs/guide/toc.yml +++ b/src/main/docs/guide/toc.yml @@ -20,6 +20,7 @@ sdkv2: nettyClient: Netty client awsCredentials: Supplying AWS credentials awsRegionSelection: AWS region selection + apiGatewayManagementApi: API Gateway Management API Client s3: S3 dynamodb: Dynamo DB ses: SES @@ -28,6 +29,13 @@ sdkv2: ssm: SSM secretsmanager: Secrets Manager servicediscovery: Service discovery + cloudWatchLogging: + title: CloudWatch logging + setCloudWatchLogging: Set CloudWatch Logging + logbackConfiguration: Logback configuration + openTelemetryAndLogging: OpenTelemetry and logging + emergencyAppender: Emergency Appender + browsingTheLogs: Browsing the Logs advancedConfiguration: Advanced configuration otherServices: Other services lambda: @@ -39,6 +47,7 @@ lambda: lambdatriggers: Lambda Triggers lambdacontext: Lambda Context requestHandlers: Lambda Handlers + afterExecutionEvent: AfterExecutionEvent coldstartups: Cold Startups customRuntimes: GraalVM and AWS Custom runtimes mdc: MDC Logging @@ -60,6 +69,7 @@ alexa: webservice: Alexa Skill as a Web Service ssmlbuilder: SSML Builder flashbriefings: Flash Briefings + localeresoultion: Locale Resolution distributedconfiguration: title: Distributed Configuration distributedconfigurationsecretsmanager: AWS Secrets Manager diff --git a/src/main/docs/resources/img/logs.png b/src/main/docs/resources/img/logs.png new file mode 100644 index 0000000000..7bfd5080ca Binary files /dev/null and b/src/main/docs/resources/img/logs.png differ diff --git a/test-suite/build.gradle b/test-suite-aws-sdk-v2/build.gradle.kts similarity index 52% rename from test-suite/build.gradle rename to test-suite-aws-sdk-v2/build.gradle.kts index 92e3949ae3..a6069aff9b 100644 --- a/test-suite/build.gradle +++ b/test-suite-aws-sdk-v2/build.gradle.kts @@ -1,27 +1,35 @@ plugins { - id 'java-library' + id("java-library") id("io.micronaut.build.internal.aws-tests") } +repositories { + mavenCentral() +} + +val micronautVersion: String by project + dependencies { testAnnotationProcessor(platform("io.micronaut:micronaut-bom:$micronautVersion")) - testAnnotationProcessor "io.micronaut:micronaut-inject-java" - + testAnnotationProcessor("io.micronaut:micronaut-inject-java") testImplementation(platform("io.micronaut:micronaut-bom:$micronautVersion")) testImplementation("org.junit.jupiter:junit-jupiter-api") - testImplementation "io.micronaut.test:micronaut-test-junit5" + testImplementation("io.micronaut.test:micronaut-test-junit5") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") + testImplementation projects.functionAws testImplementation projects.functionClientAws testRuntimeOnly('org.yaml:snakeyaml') -} -tasks.named("test") { - systemProperty "aws.accessKeyId", 'XXX' - systemProperty "aws.secretKey", 'YYY' - systemProperty "aws.region", 'us-east-1' -} -tasks.named('test') { - useJUnitPlatform() + testImplementation(project(":aws-sdk-v2")) +} +tasks { + named("test", Test::class) { + useJUnitPlatform() + } +} +java { + sourceCompatibility = JavaVersion.toVersion("17") + targetCompatibility = JavaVersion.toVersion("17") } diff --git a/test-suite-aws-sdk-v2/src/test/java/io/micronaut/aws/sdk/v2/service/cloudwatch/CloudwatchLogsClientFactoryTest.java b/test-suite-aws-sdk-v2/src/test/java/io/micronaut/aws/sdk/v2/service/cloudwatch/CloudwatchLogsClientFactoryTest.java new file mode 100644 index 0000000000..a83f2ab4da --- /dev/null +++ b/test-suite-aws-sdk-v2/src/test/java/io/micronaut/aws/sdk/v2/service/cloudwatch/CloudwatchLogsClientFactoryTest.java @@ -0,0 +1,19 @@ +package io.micronaut.aws.sdk.v2.service.cloudwatch; + +import io.micronaut.aws.sdk.v2.service.cloudwatchlogs.CloudwatchLogsClientFactory; +import io.micronaut.context.BeanContext; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +@MicronautTest(startApplication = false) +class CloudwatchLogsClientFactoryTest { + @Inject + BeanContext beanContext; + @Test + void beanOfTypeCloudwatchLogsClientFactoryDoesNotExists() { + assertFalse(beanContext.containsBean(CloudwatchLogsClientFactory.class)); + } +} diff --git a/test-suite-groovy/build.gradle b/test-suite-groovy/build.gradle deleted file mode 100644 index ea842e6f2e..0000000000 --- a/test-suite-groovy/build.gradle +++ /dev/null @@ -1,20 +0,0 @@ -plugins { - id 'groovy' - id 'java-library' - id("io.micronaut.build.internal.aws-tests") -} - -dependencies { - testCompileOnly("io.micronaut:micronaut-inject-groovy:$micronautVersion") - testImplementation("org.spockframework:spock-core") - testImplementation "io.micronaut.test:micronaut-test-spock" - - testImplementation(platform("io.micronaut:micronaut-bom:$micronautVersion")) - testImplementation projects.functionAws - testImplementation projects.functionClientAws - testRuntimeOnly('org.yaml:snakeyaml') -} - -tasks.named('test') { - useJUnitPlatform() -} diff --git a/test-suite-groovy/build.gradle.kts b/test-suite-groovy/build.gradle.kts new file mode 100644 index 0000000000..c13c6d7fe7 --- /dev/null +++ b/test-suite-groovy/build.gradle.kts @@ -0,0 +1,34 @@ +plugins { + id("groovy") + id("java-library") +} + +repositories { + mavenCentral() +} + +val micronautVersion: String by project +val micronautTestVersion: String by project +val spockVersion: String by project + +dependencies { + testCompileOnly("io.micronaut:micronaut-inject-groovy:$micronautVersion") + testImplementation("org.spockframework:spock-core:${spockVersion}") { + exclude(module = "groovy-all") + } + testImplementation("io.micronaut.test:micronaut-test-spock:$micronautTestVersion") + testImplementation(platform("io.micronaut:micronaut-bom:$micronautVersion")) + testImplementation(project(":function-aws")) + testImplementation(project(":function-client-aws")) +} + +tasks { + named("test", Test::class) { + useJUnitPlatform() + } +} + +java { + sourceCompatibility = JavaVersion.toVersion("1.8") + targetCompatibility = JavaVersion.toVersion("1.8") +} diff --git a/test-suite-kotlin/build.gradle b/test-suite-kotlin/build.gradle deleted file mode 100644 index 7f92a0bcc9..0000000000 --- a/test-suite-kotlin/build.gradle +++ /dev/null @@ -1,30 +0,0 @@ -plugins { - id "org.jetbrains.kotlin.jvm" version "1.7.20" - id("org.jetbrains.kotlin.kapt") version "1.7.20" - id("io.micronaut.build.internal.aws-tests") -} - -dependencies { - kaptTest "io.micronaut:micronaut-inject-java:$micronautVersion" - - testImplementation(platform("io.micronaut:micronaut-bom:$micronautVersion")) - testImplementation("org.junit.jupiter:junit-jupiter-api") - testImplementation "io.micronaut.test:micronaut-test-junit5" - testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") - - testImplementation projects.functionAws - testImplementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' - testImplementation projects.functionClientAws - testRuntimeOnly('org.yaml:snakeyaml') -} - -tasks.named('test') { - useJUnitPlatform() -} - -tasks.named("compileTestKotlin") { - kotlinOptions { - jvmTarget = '17' - javaParameters = true - } -} diff --git a/test-suite-kotlin/build.gradle.kts b/test-suite-kotlin/build.gradle.kts new file mode 100644 index 0000000000..a5df3ea661 --- /dev/null +++ b/test-suite-kotlin/build.gradle.kts @@ -0,0 +1,41 @@ +plugins { + id("org.jetbrains.kotlin.jvm") version ("1.7.10") + id("org.jetbrains.kotlin.kapt") version ("1.7.10") +} + +repositories { + mavenCentral() +} + +val micronautVersion: String by project + +dependencies { + kaptTest("io.micronaut:micronaut-inject-java:$micronautVersion") + + testImplementation(platform("io.micronaut:micronaut-bom:$micronautVersion")) + testImplementation("org.junit.jupiter:junit-jupiter-api") + testImplementation("io.micronaut.test:micronaut-test-junit5") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") + + testImplementation(project(":function-aws")) + testImplementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.20") + testImplementation(project(":function-client-aws")) +} + +tasks { + named("test", Test::class) { + useJUnitPlatform() + } + + named("compileTestKotlin", org.jetbrains.kotlin.gradle.tasks.KotlinCompile::class) { + kotlinOptions { + jvmTarget = "1.8" + javaParameters = true + } + } +} + +java { + sourceCompatibility = JavaVersion.toVersion("1.8") + targetCompatibility = JavaVersion.toVersion("1.8") +} diff --git a/test-suite/build.gradle.kts b/test-suite/build.gradle.kts new file mode 100644 index 0000000000..398c51132d --- /dev/null +++ b/test-suite/build.gradle.kts @@ -0,0 +1,37 @@ +plugins { + id("java-library") +} + +repositories { + mavenCentral() +} + +val micronautVersion: String by project + +dependencies { + testAnnotationProcessor(platform("io.micronaut:micronaut-bom:$micronautVersion")) + testAnnotationProcessor("io.micronaut:micronaut-inject-java") + + testImplementation(platform("io.micronaut:micronaut-bom:$micronautVersion")) + testImplementation("org.junit.jupiter:junit-jupiter-api") + testImplementation("io.micronaut.test:micronaut-test-junit5") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") + + testImplementation(project(":function-aws")) + testImplementation(project(":function-client-aws")) +} + +tasks { + named("test", Test::class) { + useJUnitPlatform() + + systemProperty("aws.accessKeyId", "XXX") + systemProperty("aws.secretKey", "YYY") + systemProperty("aws.region", "us-east-1") + } +} + +java { + sourceCompatibility = JavaVersion.toVersion("1.8") + targetCompatibility = JavaVersion.toVersion("1.8") +}