micronaut-projects / micronaut-core

Micronaut Application Framework
http://micronaut.io
Apache License 2.0
6.02k stars 1.05k forks source link

Custom AbstractPropertySourceLoader not loading the configuration #10968

Closed RajeevMasseyTR closed 2 weeks ago

RajeevMasseyTR commented 1 month ago

Issue description

I want to load the configuration into the "env" from aws ssm store which reads the json and loads it into the "env" environment variables which is something like this

{
"MCA_DOWNSTREAM_URL" : "<Some URL>",
"MCA_DOWNSTREAM_TIMEZONE" : "<Some Timezone>"
}

This is the CustomPropertySourceLoader supposed to load the configuration in "env" environment variables;

@Singleton
public class CustomPropertySourceLoader extends EnvJsonPropertySourceLoader {
    public static final String FILE_EXTENSION = "json";

    private final AwsParameterStoreConfigurationLoader awsParameterStoreConfigurationLoader;

    public CustomPropertySourceLoader(AwsParameterStoreConfigurationLoader awsParameterStoreConfigurationLoader) {
        this.awsParameterStoreConfigurationLoader = awsParameterStoreConfigurationLoader;
    }

    @Override
    protected void processInput(String name, InputStream input, Map<String, Object> finalMap) throws IOException {
        if (input != null) {
            var propperties = awsParameterStoreConfigurationLoader.loadConfiguration();
            processMap(finalMap, propperties, "");
        } else {
            log.trace("Property [{}] produced no JSON content", name);
        }
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

    @Override
    public Set<String> getExtensions() {
        return Collections.singleton(FILE_EXTENSION);
    }
}
/**
 * Loads and verifies configuration from AWS Systems Manager Parameter Store.
 * This class is responsible for retrieving configuration from SSM,
 * adding it to the Micronaut environment, and verifying that all required
 * properties are present.
 */
@Singleton
@Context
@Requires(env = {Environment.AMAZON_EC2, MicronautLambdaContext.ENVIRONMENT_LAMBDA})
public class AwsParameterStoreConfigurationLoader {
    private static final Logger LOG = LoggerFactory.getLogger(AwsParameterStoreConfigurationLoader.class);

    private final SSMSecretManager ssmSecretManager;
    private final JsonMapper jsonMapper;
    private final Environment environment;
    private final String configurationParameterStorePath;

    /**
     *
     * Constructs a new AwsParameterStoreConfigurationLoader and immediately loads and verifies the configuration.
     *
     * @param ssmSecretManager The SSM secret manager used to retrieve the configuration.
     * @param jsonMapper       The JSON mapper used to parse the configuration.
     * @param environment      The Micronaut environment to which the configuration will be added.
     * @param configurationParameterStorePath      The Micronaut environment to which the path from the SSM configurations fetch from
     */
    public AwsParameterStoreConfigurationLoader(SSMSecretManager ssmSecretManager, JsonMapper jsonMapper, Environment environment,
                                                @Property(name = "aws.ssm.config.path") String configurationParameterStorePath) {
        this.ssmSecretManager = ssmSecretManager;
        this.jsonMapper = jsonMapper;
        this.environment = environment;
        this.configurationParameterStorePath = configurationParameterStorePath;
    }

    /**
     * Loads the configuration from SSM and adds it to the Micronaut environment.
     */
    public  Map<String, Object> loadConfiguration() {
        try {
            String jsonConfig = ssmSecretManager.getSecretValue(configurationParameterStorePath);
            LOG.info("Retrieved configuration from SSM");

            Map<String, Object> configMap = jsonMapper.readValue(jsonConfig, Map.class);

            //verifying the properties
            verifyConfiguration(configMap.keySet());

            return configMap;
        } catch (Exception e) {
            throw new RuntimeException("Error loading configuration from SSM", e);
        }
    }

    /**
     * Verifies that all required properties are present in the environment.
     */
    private void verifyConfiguration(Set<String> requiredProperties) {

        List<String> missingProperties = requiredProperties.stream()
                .filter(property -> !environment.containsProperty(property))
                .toList();

        if (!missingProperties.isEmpty()) {
            String errorMessage = "The following required properties are missing: " + String.join(", ", missingProperties);
            LOG.error(errorMessage);
            throw new IllegalStateException(errorMessage);
        }

        LOG.info("All required properties have been successfully loaded and verified.");
    }
}

I have also included src/main/resources/META-INF/services/io.micronaut.context.env.PropertySourceLoader with this configuration.CustomPropertySourceLoader

But the problem is still the values are not being loaded into "env" from the CustomPropertySourceLoader

Also these two gradle build dependencies have been included

    implementation "io.micronaut:micronaut-inject"
    implementation "io.micronaut:micronaut-runtime"

Micronaut Version 4.2.x

Note : AwsParameterStoreConfigurationLoader works when inject at other places .

RajeevMasseyTR commented 1 month ago

@graemerocher

dstepanov commented 1 month ago

CustomPropertySourceLoader cannot be a bean. Look at different implementations of io.micronaut.discovery.config.ConfigurationClient

RajeevMasseyTR commented 1 month ago

@dstepanov

Would writing a custom implementation on io.micronaut.discovery.config.ConfigurationClient help. Resolving the purpose

rajeev-massey commented 1 month ago

I think something like this should help


import com.fasterxml.jackson.databind.ObjectMapper;
import io.micronaut.context.annotation.BootstrapContextCompatible;
import io.micronaut.context.annotation.Requires;
import io.micronaut.context.env.Environment;
import io.micronaut.context.env.PropertySource;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.runtime.ApplicationConfiguration;
import io.micronaut.scheduling.TaskExecutors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import software.amazon.awssdk.services.ssm.SsmAsyncClient;
import software.amazon.awssdk.services.ssm.model.GetParameterRequest;
import software.amazon.awssdk.services.ssm.model.GetParameterResponse;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;

@Singleton
@Requires(env = Environment.AMAZON_EC2)
@Requires(beans = {CustomSSMConfiguration.class, SsmAsyncClient.class})
@BootstrapContextCompatible
public class CustomSSMConfigClient implements ConfigurationClient {

    private static final Logger LOG = LoggerFactory.getLogger(CustomSSMConfigClient.class);
    private final CustomSSMConfiguration customSSMConfiguration;
    private final String configurationParameterStorePath;
    private final ObjectMapper jsonMapper;
    private SsmAsyncClient client;
    private ExecutorService executorService;

    @Inject
    public CustomSSMConfigClient(
            SsmAsyncClient asyncClient,
            CustomSSMConfiguration customSSMConfiguration,
            ApplicationConfiguration applicationConfiguration,
            ObjectMapper jsonMapper,
            @Nullable AwsServiceDiscoveryConfiguration serviceDiscoveryConfiguration) {
        this.customSSMConfiguration = customSSMConfiguration;
        this.client = asyncClient;
        this.jsonMapper = jsonMapper;
        this.configurationParameterStorePath = serviceDiscoveryConfiguration != null ? serviceDiscoveryConfiguration.getAwsServiceId() : applicationConfiguration.getName().orElse(null);
    }

    @Override
    public Publisher<PropertySource> getPropertySources(Environment environment) {
        if (!customSSMConfiguration.isEnabled()) {
            return Flux.empty();
        }

        return Mono.fromFuture(this::loadConfiguration)
                .map(configMap -> PropertySource.of(configurationParameterStorePath, configMap))
                .flux()
                .onErrorResume(this::onPropertySourceError);
    }

    @Override
    public String getDescription() {
        return "Custom SSM Configuration Client";
    }

    public CompletableFuture<Map<String, Object>> loadConfiguration() {
        try {
            GetParameterRequest parameterRequest = GetParameterRequest.builder()
                    .name(customSSMConfiguration.getParameterStorePath())
                    .withDecryption(customSSMConfiguration.getUseSecureParameters())
                    .build();

            CompletableFuture<GetParameterResponse> future = client.getParameter(parameterRequest);

            return future.thenApply(response -> {
                String jsonConfig = response.parameter().value();
                LOG.info("Retrieved configuration from SSM");
                try {
                    Map<String, Object> configMap = jsonMapper.readValue(jsonConfig, Map.class);
                    verifyConfiguration(configMap.keySet());
                    return configMap;
                } catch (Exception e) {
                    throw new RuntimeException("Error parsing JSON configuration from SSM", e);
                }
            });
        } catch (Exception e) {
            CompletableFuture<Map<String, Object>> future = new CompletableFuture<>();
            future.completeExceptionally(new RuntimeException("Error loading configuration from SSM", e));
            return future;
        }
    }

    private void verifyConfiguration(Set<String> keys) {
        // Implement your verification logic here
        LOG.info("Verifying configuration keys: {}", keys);
    }

    private Publisher<? extends PropertySource> onPropertySourceError(Throwable throwable) {
        if (throwable instanceof ConfigurationException) {
            return Flux.error(throwable);
        } else {
            return Flux.error(new ConfigurationException("Error reading configuration from SSM: " + throwable.getMessage(), throwable));
        }
    }

    @Inject
    void setExecutionService(@Named(TaskExecutors.IO) @Nullable ExecutorService executorService) {
        if (executorService != null) {
            this.executorService = executorService;
        }
    }

    protected void setClient(SsmAsyncClient client) {
        this.client = client;
    }

    protected SsmAsyncClient getClient() {
        return client;
    }
}

import io.micronaut.context.annotation.ConfigurationProperties;
import io.micronaut.context.annotation.Requires;
import io.micronaut.context.env.Environment;
import io.micronaut.core.util.StringUtils;
import io.micronaut.core.util.Toggleable;

@ConfigurationProperties(CustomSSMConfiguration.CONFIGURATION_PREFIX)
@Requires(env = Environment.AMAZON_EC2)
@Requires(property = CustomSSMConfiguration.ENABLED, value = StringUtils.TRUE, defaultValue = StringUtils.FALSE)
public class CustomSSMConfiguration implements Toggleable {

    public static final String ENABLED = "custom.ssm.parameterstore.enabled";
    public static final String CONFIGURATION_PREFIX = "custom.ssm.parameterstore";
    private static final boolean DEFAULT_SECURE = false;
    private static final boolean DEFAULT_ENABLED = false;

    private boolean useSecureParameters = DEFAULT_SECURE;
    private boolean enabled = DEFAULT_ENABLED;
    private String parameterStorePath;

    @Override
    public boolean isEnabled() {
        return enabled;
    }

    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }

    public boolean getUseSecureParameters() {
        return useSecureParameters;
    }

    public void setUseSecureParameters(boolean useSecureParameters) {
        this.useSecureParameters = useSecureParameters;
    }

    public String getParameterStorePath() {
        return parameterStorePath;
    }

    public void setParameterStorePath(String parameterStorePath) {
        this.parameterStorePath = parameterStorePath;
    }
}
rajeev-massey commented 1 month ago

@dstepanov @graemerocher still the default AWS Configuration is called