All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.micronaut.discovery.consul.config.ConsulConfigurationClient Maven / Gradle / Ivy

There is a newer version: 4.5.0
Show newest version
/*
 * 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.discovery.consul.config;

import io.micronaut.context.annotation.BootstrapContextCompatible;
import io.micronaut.context.annotation.Requires;
import io.micronaut.context.env.Environment;
import io.micronaut.context.env.EnvironmentPropertySource;
import io.micronaut.context.env.PropertiesPropertySourceLoader;
import io.micronaut.context.env.PropertySource;
import io.micronaut.context.env.PropertySourceLoader;
import io.micronaut.context.env.yaml.YamlPropertySourceLoader;
import io.micronaut.context.exceptions.ConfigurationException;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.util.CollectionUtils;
import io.micronaut.core.util.StringUtils;
import io.micronaut.discovery.client.ClientUtil;
import io.micronaut.discovery.config.ConfigDiscoveryConfiguration;
import io.micronaut.discovery.config.ConfigurationClient;
import io.micronaut.discovery.consul.ConsulConfiguration;
import io.micronaut.discovery.consul.client.v1.ConsulClient;
import io.micronaut.discovery.consul.client.v1.KeyValue;
import io.micronaut.discovery.consul.condition.RequiresConsul;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.client.exceptions.HttpClientResponseException;
import io.micronaut.jackson.env.JsonPropertySourceLoader;
import io.micronaut.scheduling.TaskExecutors;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import jakarta.inject.Singleton;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.FluxSink;
import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * A {@link ConfigurationClient} for Consul.
 */
@Singleton
@RequiresConsul
@Requires(beans = ConsulClient.class)
@Requires(property = ConfigurationClient.ENABLED, value = StringUtils.TRUE, defaultValue = StringUtils.FALSE)
@BootstrapContextCompatible
public class ConsulConfigurationClient implements ConfigurationClient {

    /**
     * Handle key name in the format.
     * 
    *
  • application
  • *
  • application[envName]
  • *
  • appName
  • *
  • appName[envName]
  • *
* Note: {@code envName} can contain only word character and "underscore" or "dash" */ private static final String MATCHING_APPLICATION = "^(application|%s)(\\[[a-zA-Z0-9_-]+])?$"; private final ConsulClient consulClient; private final ConsulConfiguration consulConfiguration; private final Map loaderByFormatMap = new ConcurrentHashMap<>(); private ExecutorService executionService; /** * @param consulClient The consul client * @param consulConfiguration The consul configuration * @param environment The environment */ public ConsulConfigurationClient(ConsulClient consulClient, ConsulConfiguration consulConfiguration, Environment environment) { this.consulClient = consulClient; this.consulConfiguration = consulConfiguration; if (environment != null) { Collection loaders = environment.getPropertySourceLoaders(); for (PropertySourceLoader loader : loaders) { Set extensions = loader.getExtensions(); for (String extension : extensions) { loaderByFormatMap.put(extension, loader); } } } } @Override public String getDescription() { return consulClient.getDescription(); } @SuppressWarnings("MagicNumber") @Override public Publisher getPropertySources(Environment environment) { if (!consulConfiguration.getConfiguration().isEnabled()) { return Flux.empty(); } List activeNames = new ArrayList<>(environment.getActiveNames()); Optional serviceId = consulConfiguration.getServiceId(); ConsulConfiguration.ConsulConfigDiscoveryConfiguration configDiscoveryConfiguration = consulConfiguration.getConfiguration(); ConfigDiscoveryConfiguration.Format format = configDiscoveryConfiguration.getFormat(); String path = configDiscoveryConfiguration.getPath().orElse(ConfigDiscoveryConfiguration.DEFAULT_PATH); if (!path.endsWith("/")) { path += "/"; } String pathPrefix = path; String commonConfigPath = path + Environment.DEFAULT_NAME; final boolean hasApplicationSpecificConfig = serviceId.isPresent(); String applicationSpecificPath = hasApplicationSpecificConfig ? path + serviceId.get() : null; final Optional patternApplication = serviceId .map(args -> String.format(MATCHING_APPLICATION, args)) .map(Pattern::compile); final Predicate isMatchingApplication = name -> patternApplication .map(pattern -> pattern.matcher(name)) .map(Matcher::matches) .orElse(Boolean.TRUE); String dc = configDiscoveryConfiguration.getDatacenter().orElse(null); Scheduler scheduler = null; if (executionService != null) { scheduler = Schedulers.fromExecutor(executionService); } List>> keyValueFlowables = new ArrayList<>(); Function>> errorHandler = throwable -> { if (throwable instanceof HttpClientResponseException hcre && hcre.getStatus() == HttpStatus.NOT_FOUND) { return Flux.empty(); } return Flux.error(new ConfigurationException("Error reading distributed configuration from Consul: " + throwable.getMessage(), throwable)); }; Flux> applicationConfig = Flux.from( consulClient.readValues(commonConfigPath, dc, null, null)) .onErrorResume(errorHandler); if (scheduler != null) { applicationConfig = applicationConfig.subscribeOn(scheduler); } keyValueFlowables.add(applicationConfig); if (hasApplicationSpecificConfig) { Flux> appSpecificConfig = Flux.from( consulClient.readValues(applicationSpecificPath, dc, null, null)) .onErrorResume(errorHandler); if (scheduler != null) { appSpecificConfig = appSpecificConfig.subscribeOn(scheduler); } keyValueFlowables.add(appSpecificConfig); } int basePriority = EnvironmentPropertySource.POSITION + 100; int envBasePriority = basePriority + 50; return Flux.merge(keyValueFlowables).flatMap(keyValues -> Flux.create(emitter -> { if (CollectionUtils.isEmpty(keyValues)) { emitter.complete(); } else { Map propertySources = new HashMap<>(); Base64.Decoder base64Decoder = Base64.getDecoder(); for (KeyValue keyValue : keyValues) { String key = keyValue.getKey(); String value = keyValue.getValue(); boolean isFolder = key.endsWith("/") && value == null; boolean isCommonConfigKey = key.startsWith(commonConfigPath); boolean isApplicationSpecificConfigKey = hasApplicationSpecificConfig && key.startsWith(applicationSpecificPath); boolean validKey = isCommonConfigKey || isApplicationSpecificConfigKey; if (!isFolder && validKey) { switch (format) { case FILE: String fileName = key.substring(pathPrefix.length()); int i = fileName.lastIndexOf('.'); if (i > -1) { String ext = fileName.substring(i + 1); fileName = fileName.substring(0, i); PropertySourceLoader propertySourceLoader = resolveLoader(ext); if (propertySourceLoader != null) { String propertySourceName = resolvePropertySourceName(Environment.DEFAULT_NAME, fileName, activeNames); if (hasApplicationSpecificConfig && propertySourceName == null) { propertySourceName = resolvePropertySourceName(serviceId.get(), fileName, activeNames); } if (propertySourceName != null && isMatchingApplication.test(propertySourceName)) { String finalName = propertySourceName; byte[] decoded = base64Decoder.decode(value); Map properties = propertySourceLoader.read(propertySourceName, decoded); String envName = ClientUtil.resolveEnvironment(fileName, activeNames); LocalSource localSource = propertySources.computeIfAbsent(propertySourceName, s -> new LocalSource(isApplicationSpecificConfigKey, envName, finalName)); localSource.putAll(properties); } } } break; case NATIVE: String property = null; Set propertySourceNames = null; if (isCommonConfigKey) { property = resolvePropertyName(commonConfigPath, key); propertySourceNames = resolvePropertySourceNames(pathPrefix, key, activeNames); } else if (isApplicationSpecificConfigKey) { property = resolvePropertyName(applicationSpecificPath, key); propertySourceNames = resolvePropertySourceNames(pathPrefix, key, activeNames); } if (property != null && propertySourceNames != null) { for (String propertySourceName : propertySourceNames) { if (isMatchingApplication.test(propertySourceName)) { String envName = ClientUtil.resolveEnvironment(propertySourceName, activeNames); LocalSource localSource = propertySources.computeIfAbsent(propertySourceName, s -> new LocalSource(isApplicationSpecificConfigKey, envName, propertySourceName)); byte[] decoded = base64Decoder.decode(value); localSource.put(property, new String(decoded)); } } } break; case JSON, YAML, PROPERTIES: String fullName = key.substring(pathPrefix.length()); if (!fullName.contains("/")) { propertySourceNames = ClientUtil.calcPropertySourceNames(fullName, activeNames, ","); String formatName = format.name().toLowerCase(Locale.ENGLISH); PropertySourceLoader propertySourceLoader = resolveLoader(formatName); if (propertySourceLoader == null) { emitter.error(new ConfigurationException("No PropertySourceLoader found for format [" + format + "]. Ensure ConfigurationClient is running within Micronaut container.")); return; } else { if (propertySourceLoader.isEnabled()) { byte[] decoded = base64Decoder.decode(value); Map properties = propertySourceLoader.read(fullName, decoded); for (String propertySourceName : propertySourceNames) { if (isMatchingApplication.test(propertySourceName)) { String envName = ClientUtil.resolveEnvironment(propertySourceName, activeNames); LocalSource localSource = propertySources.computeIfAbsent(propertySourceName, s -> new LocalSource(isApplicationSpecificConfigKey, envName, propertySourceName)); localSource.putAll(properties); } } } } } break; default: // no-op } } } for (LocalSource localSource: propertySources.values()) { int priority; if (localSource.appSpecific) { if (localSource.environment != null) { priority = envBasePriority + ((activeNames.indexOf(localSource.environment) + 1) * 2); } else { priority = envBasePriority + 1; } } else { if (localSource.environment != null) { priority = basePriority + ((activeNames.indexOf(localSource.environment) + 1) * 2); } else { priority = basePriority + 1; } } emitter.next(PropertySource.of(ConsulClient.SERVICE_ID + '-' + localSource.name, localSource.values, priority)); } emitter.complete(); } }, FluxSink.OverflowStrategy.ERROR)); } private String resolvePropertySourceName(String rootName, String fileName, List activeNames) { String propertySourceName = null; if (fileName.startsWith(rootName)) { String envString = fileName.substring(rootName.length()); if (StringUtils.isEmpty(envString)) { propertySourceName = rootName; } else if (envString.startsWith("-")) { String env = envString.substring(1); if (activeNames.contains(env)) { propertySourceName = rootName + '[' + env + ']'; } } } return propertySourceName; } private PropertySourceLoader resolveLoader(String formatName) { return loaderByFormatMap.computeIfAbsent(formatName, f -> defaultLoader(formatName)); } private PropertySourceLoader defaultLoader(String format) { return switch (format) { case "json" -> new JsonPropertySourceLoader(); case "properties" -> new PropertiesPropertySourceLoader(); case "yml", "yaml" -> new YamlPropertySourceLoader(); default -> throw new ConfigurationException("Unsupported properties file format: " + format); }; } /** * @param executionService The execution service */ @Inject void setExecutionService(@Named(TaskExecutors.IO) @Nullable ExecutorService executionService) { if (executionService != null) { this.executionService = executionService; } } private Set resolvePropertySourceNames(String finalPath, String key, List activeNames) { Set propertySourceNames = null; String prefix = key.substring(finalPath.length()); int i = prefix.indexOf('/'); if (i > -1) { prefix = prefix.substring(0, i); propertySourceNames = ClientUtil.calcPropertySourceNames(prefix, activeNames, ","); } return propertySourceNames; } private String resolvePropertyName(String commonConfigPath, String key) { String property = key.substring(commonConfigPath.length()); if (StringUtils.isNotEmpty(property)) { if (property.charAt(0) == '/') { property = property.substring(1); } else if (property.lastIndexOf('/') > -1) { property = property.substring(property.lastIndexOf('/') + 1); } } if (property.indexOf('/') == -1) { return property; } return null; } /** * A local property source. */ private static class LocalSource { private final boolean appSpecific; private final String environment; private final String name; private final Map values = new LinkedHashMap<>(); LocalSource(boolean appSpecific, String environment, String name) { this.appSpecific = appSpecific; this.environment = environment; this.name = name; } void put(String key, Object value) { this.values.put(key, value); } void putAll(Map values) { this.values.putAll(values); } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy