awspring / spring-cloud-aws

The New Home for Spring Cloud AWS
http://awspring.io
Apache License 2.0
882 stars 300 forks source link

Simplify using Localstack with ParameterStore and SecretsManager #1080

Open maciejwalkowiak opened 8 months ago

maciejwalkowiak commented 8 months ago

@ServiceConnection support that's being added in https://github.com/awspring/spring-cloud-aws/pull/1075 does not help with ParameterStore and SecretsManager, as those integration are initiated in the bootstrap phase, before service connection related factories kick in.

We can provide a BootstrapRegistryInitializer implementation for Localstack:

import io.awspring.cloud.autoconfigure.core.AwsProperties;
import io.awspring.cloud.autoconfigure.core.CredentialsProperties;
import io.awspring.cloud.autoconfigure.core.RegionProperties;
import org.springframework.boot.BootstrapRegistry;
import org.springframework.boot.BootstrapRegistryInitializer;
import org.springframework.util.Assert;
import org.testcontainers.containers.localstack.LocalStackContainer;

public class LocalstackBootstrapInitializer implements BootstrapRegistryInitializer {

    private final AwsProperties awsProperties;
    private final RegionProperties regionProperties;
    private final CredentialsProperties credentialsProperties;

    public LocalstackBootstrapInitializer(LocalStackContainer localStackContainer) {
        Assert.notNull(localStackContainer, "localstack container cannot be null");
        this.awsProperties = awsProperties(localStackContainer);
        this.regionProperties = regionProperties(localStackContainer);
        this.credentialsProperties = credentialsProperties(localStackContainer);
    }

    @Override
    public void initialize(BootstrapRegistry registry) {
        registry.register(AwsProperties.class, context -> awsProperties);
        registry.register(RegionProperties.class, context -> regionProperties);
        registry.register(CredentialsProperties.class, context -> credentialsProperties);
    }

    private static CredentialsProperties credentialsProperties(LocalStackContainer localStackContainer) {
        CredentialsProperties properties = new CredentialsProperties();
        properties.setAccessKey(localStackContainer.getAccessKey());
        properties.setSecretKey(localStackContainer.getSecretKey());
        return properties;
    }

    private static RegionProperties regionProperties(LocalStackContainer localStackContainer) {
        RegionProperties properties = new RegionProperties();
        properties.setStatic(localStackContainer.getRegion());
        return properties;
    }

    private static AwsProperties awsProperties(LocalStackContainer localStackContainer) {
        AwsProperties properties = new AwsProperties();
        properties.setEndpoint(localStackContainer.getEndpoint());
        return properties;
    }
}

that can be used like this:

@SpringBootTest(classes = SpringCloudAwsParameterStoreSampleTest.TestApp.class, useMainMethod = SpringBootTest.UseMainMethod.ALWAYS)
@Testcontainers
class SpringCloudAwsParameterStoreSampleTest {

    @Container
    private static LocalStackContainer localStackContainer = new LocalStackContainer(DockerImageName.parse("localstack/localstack:3.2.0"));

    @Test
    void foo() {

    }

    @SpringBootApplication
    public static class TestApp {

        public static void main(String[] args) {
            var app = new SpringApplication(SpringCloudAwsParameterStoreSampleTest.class);
            app.addBootstrapRegistryInitializer(new LocalstackBootstrapInitializer(localStackContainer));
            app.run(args);
        }
    }
}
maciejwalkowiak commented 7 months ago

@rieckpil @spencergibb perhaps you have some ideas how this can be done in a better way?

The issue is, Parameter Store and Secrets Manager integrations run in the bootstrap phase - for spring.config.import. Service connections kick in too late in the process and @SpringBootTest does not pick up BootstrapRegistryInitializers created through spring.factory. So having custom "main" class and @SpringBootTest with useMainMethod was the only way to get it working.

rieckpil commented 7 months ago

Good point, I guess I'm lacking some in-depth knowledge of the Spring bootstrap phase here, maybe @sbrannen has a quick idea

spencergibb commented 7 months ago

I don't have anything else to add. Maybe @philwebb might.

eddumelendez commented 7 months ago

Hi, @ContextConfiguration(initializers = ConfigDataApplicationContextInitializer.class) should be added at class level. I have some examples running for parameter store and secrets manager working with @DynamicPropertySource and it should work with @ServiceConnection as well. Looking forward for the release :)

So far there is no issues with spring-cloud-aws but if ConfigDataMissingEnvironmentPostProcessor has been implemented for those integrations recently look at this issue in other spring-cloud projects.

maciejwalkowiak commented 7 months ago

@eddumelendez thanks for chiming in. With ConfigDataApplicationContextInitializer, service connections are initialized indeed before the config data loaders, BUT the connection details beans are not available in the bootstrap context.

Considering that the amount of boilerplate required to use bootstrap context initializers in tests is so high, I think sticking to @DynamicPropertySource provides better dev experience.

Perhaps we can provide a static method like this just to make it a bit simpler:

static void configureLocalStack(DynamicPropertyRegistry registry, LocalStackContainer localstack) {
    registry.add("spring.cloud.aws.credentials.access-key", localstack::getAccessKey);
    registry.add("spring.cloud.aws.credentials.secret-key", localstack::getSecretKey);
    registry.add("spring.cloud.aws.region.static", localstack::getRegion);
    registry.add("spring.cloud.aws.endpoint", localstack::getEndpoint);
}

Sample usage:

@Container
private static LocalStackContainer localstack = new LocalStackContainer(
        DockerImageName.parse("localstack/localstack:3.2.0"));

@DynamicPropertySource
static void properties(DynamicPropertyRegistry registry) {
    configureLocalStack(registry, localstack);
}
philwebb commented 7 months ago

I don't have anything else to add. Maybe @philwebb might.

Sorry, not off the top of my head. The bootstrap logic is always quite difficult to get right.