diff --git a/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/config/s3/AwsS3PropertySourceNotFoundException.java b/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/config/s3/AwsS3PropertySourceNotFoundException.java new file mode 100644 index 000000000..3d9d04568 --- /dev/null +++ b/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/config/s3/AwsS3PropertySourceNotFoundException.java @@ -0,0 +1,24 @@ +/* + * Copyright 2013-2025 the original author or 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.awspring.cloud.autoconfigure.config.s3; + +public class AwsS3PropertySourceNotFoundException extends RuntimeException { + + AwsS3PropertySourceNotFoundException(Exception source) { + super(source); + } + +} diff --git a/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/config/s3/S3ConfigDataLoader.java b/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/config/s3/S3ConfigDataLoader.java index 470db3f1b..413ae5b92 100644 --- a/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/config/s3/S3ConfigDataLoader.java +++ b/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/config/s3/S3ConfigDataLoader.java @@ -22,7 +22,6 @@ import org.springframework.boot.context.config.ConfigData; import org.springframework.boot.context.config.ConfigDataLoader; import org.springframework.boot.context.config.ConfigDataLoaderContext; -import org.springframework.boot.context.config.ConfigDataResourceNotFoundException; import org.springframework.boot.logging.DeferredLogFactory; import org.springframework.core.env.MapPropertySource; import org.springframework.lang.Nullable; @@ -44,29 +43,23 @@ public S3ConfigDataLoader(DeferredLogFactory logFactory) { @Override @Nullable public ConfigData load(ConfigDataLoaderContext context, S3ConfigDataResource resource) { - try { - // resource is disabled if s3 integration is disabled via - // spring.cloud.aws.s3.config.enabled=false - if (resource.isEnabled()) { - S3Client s3Client = context.getBootstrapContext().get(S3Client.class); - S3PropertySource propertySource = resource.getPropertySources() - .createPropertySource(resource.getContext(), resource.isOptional(), s3Client); - if (propertySource != null) { - return new ConfigData(Collections.singletonList(propertySource)); - } - else { - return null; - } + // resource is disabled if s3 integration is disabled via + // spring.cloud.aws.s3.config.enabled=false + if (resource.isEnabled()) { + S3Client s3Client = context.getBootstrapContext().get(S3Client.class); + S3PropertySource propertySource = resource.getPropertySources().createPropertySource(resource.getContext(), + resource.isOptional(), s3Client); + if (propertySource != null) { + return new ConfigData(Collections.singletonList(propertySource)); } else { - // create dummy empty config data - return new ConfigData(Collections.singletonList(new MapPropertySource("aws-s3:" + context, Map.of()))); + return null; } } - catch (Exception e) { - throw new ConfigDataResourceNotFoundException(resource, e); + else { + // create dummy empty config data + return new ConfigData(Collections.singletonList(new MapPropertySource("aws-s3:" + context, Map.of()))); } - } } diff --git a/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/config/s3/S3ExceptionHappenedAnalyzer.java b/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/config/s3/S3ExceptionHappenedAnalyzer.java new file mode 100644 index 000000000..c6fcbd151 --- /dev/null +++ b/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/config/s3/S3ExceptionHappenedAnalyzer.java @@ -0,0 +1,30 @@ +/* + * Copyright 2013-2024 the original author or 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.awspring.cloud.autoconfigure.config.s3; + +import org.springframework.boot.diagnostics.AbstractFailureAnalyzer; +import org.springframework.boot.diagnostics.FailureAnalysis; + +public class S3ExceptionHappenedAnalyzer extends AbstractFailureAnalyzer { + + @Override + protected FailureAnalysis analyze(Throwable rootFailure, AwsS3PropertySourceNotFoundException cause) { + return new FailureAnalysis( + "Could not import properties from AWS S3. Exception happened while trying to load the keys: " + + cause.getMessage(), + "Depending on error message determine action course", cause); + } +} diff --git a/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/config/s3/S3PropertySources.java b/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/config/s3/S3PropertySources.java index 9fd3ed4e6..00b5629b2 100644 --- a/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/config/s3/S3PropertySources.java +++ b/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/config/s3/S3PropertySources.java @@ -59,12 +59,4 @@ public S3PropertySource createPropertySource(String context, boolean optional, S return null; } - static class AwsS3PropertySourceNotFoundException extends RuntimeException { - - AwsS3PropertySourceNotFoundException(Exception source) { - super(source); - } - - } - } diff --git a/spring-cloud-aws-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-cloud-aws-autoconfigure/src/main/resources/META-INF/spring.factories index 6dd5278a9..3479f449a 100644 --- a/spring-cloud-aws-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-aws-autoconfigure/src/main/resources/META-INF/spring.factories @@ -14,5 +14,6 @@ io.awspring.cloud.autoconfigure.config.s3.S3ConfigDataLoader org.springframework.boot.diagnostics.FailureAnalyzer=\ io.awspring.cloud.autoconfigure.config.parameterstore.ParameterStoreMissingKeysFailureAnalyzer, \ io.awspring.cloud.autoconfigure.config.parameterstore.ParameterStoreExceptionHappenedAnalyzer, \ +io.awspring.cloud.autoconfigure.config.s3.S3ExceptionHappenedAnalyzer, \ io.awspring.cloud.autoconfigure.config.secretsmanager.SecretsManagerMissingKeysFailureAnalyzer,\ io.awspring.cloud.autoconfigure.config.s3.S3MissingKeysFailureAnalyzer diff --git a/spring-cloud-aws-autoconfigure/src/test/java/io/awspring/cloud/autoconfigure/config/s3/S3ConfigDataLoaderIntegrationTests.java b/spring-cloud-aws-autoconfigure/src/test/java/io/awspring/cloud/autoconfigure/config/s3/S3ConfigDataLoaderIntegrationTests.java index e8410a6f2..7409e9fd0 100644 --- a/spring-cloud-aws-autoconfigure/src/test/java/io/awspring/cloud/autoconfigure/config/s3/S3ConfigDataLoaderIntegrationTests.java +++ b/spring-cloud-aws-autoconfigure/src/test/java/io/awspring/cloud/autoconfigure/config/s3/S3ConfigDataLoaderIntegrationTests.java @@ -16,6 +16,7 @@ package io.awspring.cloud.autoconfigure.config.s3; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.Mockito.*; import static org.testcontainers.shaded.org.awaitility.Awaitility.await; @@ -27,12 +28,15 @@ import java.util.Map; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.boot.SpringApplication; import org.springframework.boot.WebApplicationType; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.bootstrap.BootstrapRegistry; import org.springframework.boot.bootstrap.BootstrapRegistryInitializer; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.context.ConfigurableApplicationContext; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; @@ -58,12 +62,15 @@ */ @Testcontainers +@ExtendWith(OutputCaptureExtension.class) public class S3ConfigDataLoaderIntegrationTests { private static final String YAML_TYPE = "application/x-yaml"; private static final String YAML_TYPE_ALTERNATIVE = "text/yaml"; private static final String TEXT_TYPE = "text/plain"; private static final String JSON_TYPE = "application/json"; private static String BUCKET = "test-bucket"; + + private static final String NEW_LINE_CHAR = System.lineSeparator(); @Container static LocalStackContainer localstack = new LocalStackContainer( DockerImageName.parse("localstack/localstack:4.4.0")).withReuse(true); @@ -150,6 +157,26 @@ void clientIsConfiguredWithCustomizerProvidedToBootstrapRegistry() throws JsonPr } } + @Test + void failOnKeysMissing(CapturedOutput output) { + SpringApplication application = new SpringApplication(App.class); + application.setWebApplicationType(WebApplicationType.NONE); + + try (ConfigurableApplicationContext context = runApplication(application, + "aws-s3:test-bucket/tst.properties")) { + fail("Context without keys should fail to start"); + } + catch (Exception e) { + assertThat(e).isInstanceOf(AwsS3PropertySourceNotFoundException.class); + // ensure that failure analyzer catches the exception and provides meaningful + // error message + // Ensure that new line character should be platform independent + String errorMessage = "Description:%1$s%1$sCould not import properties from AWS S3. Exception happened while trying to load the keys:" + .formatted(NEW_LINE_CHAR); + assertThat(output.getOut()).contains(errorMessage); + } + } + @Test void reloadPropertiesFromS3() { SpringApplication application = new SpringApplication(S3ConfigDataLoaderIntegrationTests.App.class);