feat: config effective (#1815)
* feat: config effective Signed-off-by: evelynwei <evelynwei@tencent.com> * feat: expose config client id in polaris config endpoint The config SDK context is created in the config-data phase, so its startup logs may be dropped before the polaris log appenders are ready. Exposing the client id via the endpoint gives tooling a reliable source instead of grepping logs (the cloud verify script had to derive it from the first SDKContext when no stream-establishment anchor log exists). Co-authored-by: Cursor <cursoragent@cursor.com> --------- Signed-off-by: evelynwei <evelynwei@tencent.com> Co-authored-by: evelynwei <evelynwei@tencent.com> Co-authored-by: Cursor <cursoragent@cursor.com>pull/1816/head
parent
804f7594dc
commit
cae7fe7c91
@ -0,0 +1,70 @@
|
|||||||
|
/*
|
||||||
|
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2021 Tencent. All rights reserved.
|
||||||
|
*
|
||||||
|
* Licensed under the BSD 3-Clause License (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
*
|
||||||
|
* 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 com.tencent.cloud.polaris.config;
|
||||||
|
|
||||||
|
import com.tencent.cloud.polaris.config.adapter.SpringConfigEffectiveValueProvider;
|
||||||
|
import com.tencent.polaris.configuration.api.core.ConfigEffectiveValueRegistration;
|
||||||
|
import com.tencent.polaris.configuration.api.core.ConfigFileService;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.core.env.ConfigurableEnvironment;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers the Spring-Environment-based effective value provider into polaris-java
|
||||||
|
* for config effective-time realtime query.
|
||||||
|
* <p>
|
||||||
|
* This auto-configuration is intentionally NOT registered as a BootstrapConfiguration:
|
||||||
|
* in legacy bootstrap mode the bootstrap context's Environment only carries bootstrap.yml
|
||||||
|
* sources, so a provider registered there would resolve stale or missing effective values.
|
||||||
|
* Being main-context-only guarantees the provider always captures the Environment the
|
||||||
|
* application actually reads. In legacy bootstrap mode the {@link ConfigFileService} bean
|
||||||
|
* is inherited from the bootstrap (parent) context.
|
||||||
|
*
|
||||||
|
* @author evelynwei
|
||||||
|
*/
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
@ConditionalOnPolarisConfigEnabled
|
||||||
|
@AutoConfigureAfter(PolarisConfigBootstrapAutoConfiguration.class)
|
||||||
|
public class PolarisConfigEffectiveValueAutoConfiguration {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enabled by default (aligned with the config watch report switch); set
|
||||||
|
* spring.cloud.polaris.config.report.effective.enabled=false to opt out. The returned
|
||||||
|
* registration is closed on context shutdown so the SDK never holds a destroyed
|
||||||
|
* Environment.
|
||||||
|
* <p>
|
||||||
|
* {@code @AutoConfigureAfter} on this class guarantees the {@code configFileService}
|
||||||
|
* bean definition of {@link PolarisConfigBootstrapAutoConfiguration} is already
|
||||||
|
* processed, so {@code @ConditionalOnBean} sees it in ConfigData mode; in legacy
|
||||||
|
* bootstrap mode the bean is visible in the parent context.
|
||||||
|
*/
|
||||||
|
@Bean(destroyMethod = "close")
|
||||||
|
@ConditionalOnBean(ConfigFileService.class)
|
||||||
|
@ConditionalOnMissingBean
|
||||||
|
@ConditionalOnProperty(prefix = "spring.cloud.polaris.config.report.effective", name = "enabled",
|
||||||
|
havingValue = "true", matchIfMissing = true)
|
||||||
|
public ConfigEffectiveValueRegistration polarisConfigEffectiveValueRegistration(
|
||||||
|
ConfigurableEnvironment environment, ConfigFileService configFileService) {
|
||||||
|
return configFileService.registerEffectiveValueProvider(new SpringConfigEffectiveValueProvider(environment));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,288 @@
|
|||||||
|
/*
|
||||||
|
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2021 Tencent. All rights reserved.
|
||||||
|
*
|
||||||
|
* Licensed under the BSD 3-Clause License (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
*
|
||||||
|
* 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 com.tencent.cloud.polaris.config.adapter;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import com.tencent.polaris.configuration.api.core.ConfigEffectiveValueProvider;
|
||||||
|
import com.tencent.polaris.configuration.api.core.ConfigFileMetadata;
|
||||||
|
import com.tencent.polaris.configuration.api.core.ConfigKVFile;
|
||||||
|
import com.tencent.polaris.configuration.api.core.ConfigKeyConflict;
|
||||||
|
import com.tencent.polaris.configuration.api.core.EffectiveValue;
|
||||||
|
import com.tencent.polaris.configuration.client.internal.CompositeConfigFile;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import org.springframework.cloud.bootstrap.config.BootstrapPropertySource;
|
||||||
|
import org.springframework.core.env.CompositePropertySource;
|
||||||
|
import org.springframework.core.env.ConfigurableEnvironment;
|
||||||
|
import org.springframework.core.env.PropertySource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves key list, file value, effective value and conflict context from Spring
|
||||||
|
* {@link ConfigurableEnvironment}, for polaris-java config effective-time query.
|
||||||
|
* <p>
|
||||||
|
* Config values must never be written to any log (including DEBUG): exception messages
|
||||||
|
* and stack traces may embed raw values (e.g. Spring's unresolvable-placeholder error
|
||||||
|
* quotes the whole value), so logs only carry key names, file coordinates and exception
|
||||||
|
* class names. Per the interface contract, methods must not throw: single-key failures
|
||||||
|
* degrade that key only.
|
||||||
|
*
|
||||||
|
* @author evelynwei
|
||||||
|
*/
|
||||||
|
public class SpringConfigEffectiveValueProvider implements ConfigEffectiveValueProvider {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(SpringConfigEffectiveValueProvider.class);
|
||||||
|
|
||||||
|
private static final String SOURCE_PREFIX = "polaris:";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name of Spring Boot's configuration-properties facade source
|
||||||
|
* ({@code ConfigurationPropertySourcesPropertySource}). Attached at the head of the
|
||||||
|
* Environment, its containsProperty delegates to every underlying source, so it
|
||||||
|
* matches any key and would shadow the real source.
|
||||||
|
*/
|
||||||
|
private static final String CONFIGURATION_PROPERTIES_SOURCE_NAME = "configurationProperties";
|
||||||
|
|
||||||
|
private final ConfigurableEnvironment environment;
|
||||||
|
|
||||||
|
public SpringConfigEffectiveValueProvider(ConfigurableEnvironment environment) {
|
||||||
|
this.environment = environment;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> getKeys(ConfigFileMetadata configFile) {
|
||||||
|
try {
|
||||||
|
ConfigKVFile file = findConfigKVFile(configFile);
|
||||||
|
if (file == null) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
List<String> keys = new ArrayList<>(file.getPropertyNames());
|
||||||
|
// Sort for stable output, friendly to tests and console display.
|
||||||
|
Collections.sort(keys);
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
catch (Throwable t) {
|
||||||
|
LOG.warn("[SCT Config] Get keys failed, file = {}, error = {}", coordinateOf(configFile),
|
||||||
|
t.getClass().getSimpleName());
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public EffectiveValue resolve(String key, ConfigFileMetadata configFile) {
|
||||||
|
String fileValue;
|
||||||
|
try {
|
||||||
|
ConfigKVFile file = findConfigKVFile(configFile);
|
||||||
|
// Raw value in the file (may contain unresolved placeholders).
|
||||||
|
fileValue = file == null ? null : file.getProperty(key, null);
|
||||||
|
}
|
||||||
|
catch (Throwable t) {
|
||||||
|
// Contract: return null instead of throwing; SDK degrades this key only.
|
||||||
|
LOG.warn("[SCT Config] Resolve file value failed, key = {}, file = {}, error = {}", key,
|
||||||
|
coordinateOf(configFile), t.getClass().getSimpleName());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String effectiveValue = null;
|
||||||
|
String propertySource = null;
|
||||||
|
try {
|
||||||
|
// Effective value: Environment has already converged by precedence and
|
||||||
|
// resolves ${} placeholders, i.e. the value the application actually reads.
|
||||||
|
effectiveValue = environment.getProperty(key);
|
||||||
|
propertySource = resolvePropertySourceName(key);
|
||||||
|
}
|
||||||
|
catch (Throwable t) {
|
||||||
|
// Only degrade the effective-value dimension; the file value already in hand is returned.
|
||||||
|
LOG.warn("[SCT Config] Resolve effective value failed, key = {}, error = {}", key,
|
||||||
|
t.getClass().getSimpleName());
|
||||||
|
}
|
||||||
|
return new EffectiveValue(fileValue, effectiveValue, propertySource);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ConfigKeyConflict> resolveConflicts(String key, ConfigFileMetadata excludeFile) {
|
||||||
|
List<ConfigKeyConflict> conflicts = new ArrayList<>();
|
||||||
|
// Static call: PolarisPropertySourceManager is not a bean and cannot be injected.
|
||||||
|
// Used here only to enumerate the watched-file set, never for precedence.
|
||||||
|
for (PolarisPropertySource source : PolarisPropertySourceManager.getAllPropertySources()) {
|
||||||
|
for (ConfigKVFile file : expandSubs(source.getConfigKVFile())) {
|
||||||
|
try {
|
||||||
|
collectIfConflict(conflicts, key, file, excludeFile);
|
||||||
|
}
|
||||||
|
catch (Throwable t) {
|
||||||
|
// Per-file fallback: one broken file must not drop conflicts already collected.
|
||||||
|
LOG.warn("[SCT Config] Resolve conflicts failed, key = {}, error = {}", key,
|
||||||
|
t.getClass().getSimpleName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return conflicts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Walks the ordered PropertySource chain of the Environment and returns the source
|
||||||
|
* identity of the first one containing the key. Iteration order of
|
||||||
|
* MutablePropertySources is exactly Spring's precedence order.
|
||||||
|
*/
|
||||||
|
private String resolvePropertySourceName(String key) {
|
||||||
|
for (PropertySource<?> source : environment.getPropertySources()) {
|
||||||
|
String name = matchSource(source, key);
|
||||||
|
if (name != null) {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches the key inside a single PropertySource, unwrapping wrappers first:
|
||||||
|
* in bootstrap mode polaris sources are wrapped into a CompositePropertySource
|
||||||
|
* ("polaris-config") and then into BootstrapPropertySource, so recursion is needed
|
||||||
|
* to reach the real PolarisPropertySource. Polaris sources are translated to a
|
||||||
|
* normalized coordinate {@code polaris:namespace/group/fileName}.
|
||||||
|
*/
|
||||||
|
private String matchSource(PropertySource<?> source, String key) {
|
||||||
|
if (CONFIGURATION_PROPERTIES_SOURCE_NAME.equals(source.getName())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (source instanceof CompositePropertySource) {
|
||||||
|
for (PropertySource<?> sub : ((CompositePropertySource) source).getPropertySources()) {
|
||||||
|
String name = matchSource(sub, key);
|
||||||
|
if (name != null) {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (source instanceof BootstrapPropertySource) {
|
||||||
|
return matchSource(((BootstrapPropertySource<?>) source).getDelegate(), key);
|
||||||
|
}
|
||||||
|
if (!source.containsProperty(key)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (source instanceof PolarisPropertySource) {
|
||||||
|
PolarisPropertySource polarisSource = (PolarisPropertySource) source;
|
||||||
|
ConfigKVFile file = polarisSource.getConfigKVFile();
|
||||||
|
if (file instanceof CompositeConfigFile) {
|
||||||
|
// Group-dimension load: merge semantics is first-file-wins, so the first sub
|
||||||
|
// file containing the key is the effective one.
|
||||||
|
for (ConfigKVFile sub : ((CompositeConfigFile) file).getConfigKVFiles()) {
|
||||||
|
if (sub.getProperty(key, null) != null) {
|
||||||
|
return formatCoordinate(sub);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fallback: the composite's sub list is frozen at startup and does not cover
|
||||||
|
// files added to the group at runtime; look them up by group coordinate.
|
||||||
|
ConfigKVFile added = findInGroup(polarisSource.getNamespace(), polarisSource.getGroup(), key);
|
||||||
|
if (added != null) {
|
||||||
|
return formatCoordinate(added);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return formatCoordinate(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return source.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the watched ConfigKVFile by coordinate. Sources loaded by file dimension match
|
||||||
|
* directly; sources loaded by group dimension carry an empty fileName and their
|
||||||
|
* CompositeConfigFile must be expanded to match sub files.
|
||||||
|
*/
|
||||||
|
private ConfigKVFile findConfigKVFile(ConfigFileMetadata metadata) {
|
||||||
|
for (PolarisPropertySource source : PolarisPropertySourceManager.getAllPropertySources()) {
|
||||||
|
for (ConfigKVFile file : expandSubs(source.getConfigKVFile())) {
|
||||||
|
if (sameFile(file, metadata)) {
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds a watched file in the given namespace/group containing the key. Used as the
|
||||||
|
* fallback for group sources whose frozen composite misses runtime-added files.
|
||||||
|
*/
|
||||||
|
private ConfigKVFile findInGroup(String namespace, String group, String key) {
|
||||||
|
for (PolarisPropertySource source : PolarisPropertySourceManager.getAllPropertySources()) {
|
||||||
|
for (ConfigKVFile file : expandSubs(source.getConfigKVFile())) {
|
||||||
|
if (Objects.equals(file.getNamespace(), namespace) && Objects.equals(file.getFileGroup(), group)
|
||||||
|
&& file.getProperty(key, null) != null) {
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expands a CompositeConfigFile to its sub files; a plain file expands to itself.
|
||||||
|
*/
|
||||||
|
private List<ConfigKVFile> expandSubs(ConfigKVFile file) {
|
||||||
|
if (file instanceof CompositeConfigFile) {
|
||||||
|
List<ConfigKVFile> subs = ((CompositeConfigFile) file).getConfigKVFiles();
|
||||||
|
return subs == null ? Collections.emptyList() : subs;
|
||||||
|
}
|
||||||
|
return Collections.singletonList(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void collectIfConflict(List<ConfigKeyConflict> conflicts, String key, ConfigKVFile candidate,
|
||||||
|
ConfigFileMetadata excludeFile) {
|
||||||
|
if (sameFile(candidate, excludeFile)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String value = candidate.getProperty(key, null);
|
||||||
|
if (value == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// The same file may be watched at both file dimension and group dimension; dedupe by coordinate.
|
||||||
|
for (ConfigKeyConflict existing : conflicts) {
|
||||||
|
if (Objects.equals(existing.getNamespace(), candidate.getNamespace())
|
||||||
|
&& Objects.equals(existing.getGroup(), candidate.getFileGroup())
|
||||||
|
&& Objects.equals(existing.getFileName(), candidate.getFileName())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Coordinate + value only; never the full content of the conflict file.
|
||||||
|
conflicts.add(new ConfigKeyConflict(candidate.getNamespace(), candidate.getFileGroup(),
|
||||||
|
candidate.getFileName(), value));
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean sameFile(ConfigKVFile candidate, ConfigFileMetadata metadata) {
|
||||||
|
return metadata != null
|
||||||
|
&& Objects.equals(candidate.getNamespace(), metadata.getNamespace())
|
||||||
|
&& Objects.equals(candidate.getFileGroup(), metadata.getFileGroup())
|
||||||
|
&& Objects.equals(candidate.getFileName(), metadata.getFileName());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatCoordinate(ConfigKVFile file) {
|
||||||
|
return SOURCE_PREFIX + file.getNamespace() + "/" + file.getFileGroup() + "/" + file.getFileName();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String coordinateOf(ConfigFileMetadata metadata) {
|
||||||
|
if (metadata == null) {
|
||||||
|
return "null";
|
||||||
|
}
|
||||||
|
return metadata.getNamespace() + "/" + metadata.getFileGroup() + "/" + metadata.getFileName();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,5 @@
|
|||||||
com.tencent.cloud.polaris.config.PolarisConfigAutoConfiguration
|
com.tencent.cloud.polaris.config.PolarisConfigAutoConfiguration
|
||||||
com.tencent.cloud.polaris.config.endpoint.PolarisConfigEndpointAutoConfiguration
|
com.tencent.cloud.polaris.config.endpoint.PolarisConfigEndpointAutoConfiguration
|
||||||
com.tencent.cloud.polaris.config.PolarisConfigBootstrapAutoConfiguration
|
com.tencent.cloud.polaris.config.PolarisConfigBootstrapAutoConfiguration
|
||||||
|
com.tencent.cloud.polaris.config.PolarisConfigEffectiveValueAutoConfiguration
|
||||||
com.tencent.cloud.polaris.config.tsf.PolarisAdaptorTsfConfigAutoConfiguration
|
com.tencent.cloud.polaris.config.tsf.PolarisAdaptorTsfConfigAutoConfiguration
|
||||||
|
|||||||
@ -0,0 +1,97 @@
|
|||||||
|
/*
|
||||||
|
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2021 Tencent. All rights reserved.
|
||||||
|
*
|
||||||
|
* Licensed under the BSD 3-Clause License (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
*
|
||||||
|
* 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 com.tencent.cloud.polaris.config;
|
||||||
|
|
||||||
|
import com.tencent.cloud.polaris.config.adapter.SpringConfigEffectiveValueProvider;
|
||||||
|
import com.tencent.polaris.configuration.api.core.ConfigEffectiveValueProvider;
|
||||||
|
import com.tencent.polaris.configuration.api.core.ConfigEffectiveValueRegistration;
|
||||||
|
import com.tencent.polaris.configuration.api.core.ConfigFileService;
|
||||||
|
import com.tencent.polaris.configuration.api.core.EffectiveValue;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||||
|
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test for {@link PolarisConfigEffectiveValueAutoConfiguration}. The registration bean must
|
||||||
|
* only be created in the main application context: in legacy bootstrap mode the bootstrap
|
||||||
|
* context's Environment carries only bootstrap.yml sources, so a provider registered there
|
||||||
|
* would report stale effective values.
|
||||||
|
*
|
||||||
|
* @author evelynwei
|
||||||
|
*/
|
||||||
|
// The mocked AutoCloseable registrations are never closed; the real bean is closed via destroyMethod.
|
||||||
|
@SuppressWarnings("try")
|
||||||
|
class PolarisConfigEffectiveValueRegistrationTest {
|
||||||
|
|
||||||
|
private final ApplicationContextRunner runner = new ApplicationContextRunner()
|
||||||
|
.withConfiguration(AutoConfigurations.of(PolarisConfigEffectiveValueAutoConfiguration.class));
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testRegistrationBeanCreatedWhenConfigFileServicePresent() {
|
||||||
|
ConfigFileService configFileService = mock(ConfigFileService.class);
|
||||||
|
ConfigEffectiveValueRegistration registration = mock(ConfigEffectiveValueRegistration.class);
|
||||||
|
when(configFileService.registerEffectiveValueProvider(any(ConfigEffectiveValueProvider.class)))
|
||||||
|
.thenReturn(registration);
|
||||||
|
|
||||||
|
runner.withBean(ConfigFileService.class, () -> configFileService)
|
||||||
|
.run(context -> {
|
||||||
|
assertThat(context).hasSingleBean(ConfigEffectiveValueRegistration.class);
|
||||||
|
verify(configFileService).registerEffectiveValueProvider(
|
||||||
|
any(SpringConfigEffectiveValueProvider.class));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testRegistrationBeanSkippedWhenConfigFileServiceAbsent() {
|
||||||
|
// local data source etc.: no ConfigFileService bean, provider must not be registered
|
||||||
|
runner.run(context -> assertThat(context).doesNotHaveBean(ConfigEffectiveValueRegistration.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testRegistrationBeanSkippedWhenExplicitlyDisabled() {
|
||||||
|
ConfigFileService configFileService = mock(ConfigFileService.class);
|
||||||
|
runner.withBean(ConfigFileService.class, () -> configFileService)
|
||||||
|
.withPropertyValues("spring.cloud.polaris.config.report.effective.enabled=false")
|
||||||
|
.run(context -> assertThat(context).doesNotHaveBean(ConfigEffectiveValueRegistration.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testProviderResolvesAgainstRegisteringContextEnvironment() {
|
||||||
|
ConfigFileService configFileService = mock(ConfigFileService.class);
|
||||||
|
ConfigEffectiveValueRegistration registration = mock(ConfigEffectiveValueRegistration.class);
|
||||||
|
ArgumentCaptor<ConfigEffectiveValueProvider> providerCaptor =
|
||||||
|
ArgumentCaptor.forClass(ConfigEffectiveValueProvider.class);
|
||||||
|
when(configFileService.registerEffectiveValueProvider(providerCaptor.capture()))
|
||||||
|
.thenReturn(registration);
|
||||||
|
|
||||||
|
runner.withBean(ConfigFileService.class, () -> configFileService)
|
||||||
|
.withPropertyValues("sct.test.key=from-main-environment")
|
||||||
|
.run(context -> {
|
||||||
|
EffectiveValue value = providerCaptor.getValue().resolve("sct.test.key", null);
|
||||||
|
assertThat(value.getEffectiveValue()).isEqualTo("from-main-environment");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,313 @@
|
|||||||
|
/*
|
||||||
|
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2021 Tencent. All rights reserved.
|
||||||
|
*
|
||||||
|
* Licensed under the BSD 3-Clause License (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
*
|
||||||
|
* 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 com.tencent.cloud.polaris.config.adapter;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import com.tencent.polaris.configuration.api.core.ConfigFileMetadata;
|
||||||
|
import com.tencent.polaris.configuration.api.core.ConfigKVFile;
|
||||||
|
import com.tencent.polaris.configuration.api.core.ConfigKeyConflict;
|
||||||
|
import com.tencent.polaris.configuration.api.core.EffectiveValue;
|
||||||
|
import com.tencent.polaris.configuration.client.internal.CompositeConfigFile;
|
||||||
|
import com.tencent.polaris.configuration.client.internal.DefaultConfigFileMetadata;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
import org.springframework.cloud.bootstrap.config.BootstrapPropertySource;
|
||||||
|
import org.springframework.core.env.CompositePropertySource;
|
||||||
|
import org.springframework.core.env.ConfigurableEnvironment;
|
||||||
|
import org.springframework.core.env.MapPropertySource;
|
||||||
|
import org.springframework.core.env.StandardEnvironment;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.Mockito.lenient;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test for {@link SpringConfigEffectiveValueProvider}.
|
||||||
|
*
|
||||||
|
* @author evelynwei
|
||||||
|
*/
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class SpringConfigEffectiveValueProviderTest {
|
||||||
|
|
||||||
|
private static final String NAMESPACE = "default";
|
||||||
|
|
||||||
|
private static final String GROUP = "order-service";
|
||||||
|
|
||||||
|
private static final String FILE_NAME = "application.yaml";
|
||||||
|
|
||||||
|
private StandardEnvironment environment;
|
||||||
|
|
||||||
|
private SpringConfigEffectiveValueProvider provider;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
PolarisPropertySourceManager.clearPropertySources();
|
||||||
|
environment = new StandardEnvironment();
|
||||||
|
provider = new SpringConfigEffectiveValueProvider(environment);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void tearDown() {
|
||||||
|
PolarisPropertySourceManager.clearPropertySources();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a file-dimension PolarisPropertySource into both the Environment chain
|
||||||
|
* (at the tail, below system properties like the real runtime) and the static manager.
|
||||||
|
*/
|
||||||
|
private PolarisPropertySource addFileSource(String namespace, String group, String fileName,
|
||||||
|
Map<String, String> props) {
|
||||||
|
ConfigKVFile kvFile = mockConfigKVFile(namespace, group, fileName, props);
|
||||||
|
PolarisPropertySource source = new PolarisPropertySource(namespace, group, fileName, kvFile,
|
||||||
|
new HashMap<String, Object>(props));
|
||||||
|
PolarisPropertySourceManager.addPropertySource(source);
|
||||||
|
environment.getPropertySources().addLast(source);
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ConfigKVFile mockConfigKVFile(String namespace, String group, String fileName,
|
||||||
|
Map<String, String> props) {
|
||||||
|
ConfigKVFile kvFile = mock(ConfigKVFile.class);
|
||||||
|
lenient().when(kvFile.getNamespace()).thenReturn(namespace);
|
||||||
|
lenient().when(kvFile.getFileGroup()).thenReturn(group);
|
||||||
|
lenient().when(kvFile.getFileName()).thenReturn(fileName);
|
||||||
|
lenient().when(kvFile.getPropertyNames()).thenReturn(props.keySet());
|
||||||
|
lenient().when(kvFile.getProperty(anyString(), any())).thenAnswer(invocation -> {
|
||||||
|
String key = invocation.getArgument(0);
|
||||||
|
return props.getOrDefault(key, invocation.getArgument(1));
|
||||||
|
});
|
||||||
|
return kvFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ConfigFileMetadata metadata(String namespace, String group, String fileName) {
|
||||||
|
return new DefaultConfigFileMetadata(namespace, group, fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCommandLineArgsOverridesPolarisValue() {
|
||||||
|
addFileSource(NAMESPACE, GROUP, FILE_NAME, Collections.singletonMap("server.port", "8080"));
|
||||||
|
environment.getPropertySources().addFirst(new MapPropertySource("commandLineArgs",
|
||||||
|
Collections.singletonMap("server.port", "9090")));
|
||||||
|
|
||||||
|
EffectiveValue value = provider.resolve("server.port", metadata(NAMESPACE, GROUP, FILE_NAME));
|
||||||
|
|
||||||
|
assertThat(value).isNotNull();
|
||||||
|
assertThat(value.getFileValue()).isEqualTo("8080");
|
||||||
|
assertThat(value.getEffectiveValue()).isEqualTo("9090");
|
||||||
|
assertThat(value.getPropertySource()).isEqualTo("commandLineArgs");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testEffectiveValueResolvesPlaceholder() {
|
||||||
|
Map<String, String> props = new LinkedHashMap<>();
|
||||||
|
props.put("server.port", "${http.port:8080}");
|
||||||
|
props.put("http.port", "8081");
|
||||||
|
addFileSource(NAMESPACE, GROUP, FILE_NAME, props);
|
||||||
|
|
||||||
|
EffectiveValue value = provider.resolve("server.port", metadata(NAMESPACE, GROUP, FILE_NAME));
|
||||||
|
|
||||||
|
assertThat(value).isNotNull();
|
||||||
|
// file value keeps the raw placeholder, effective value is what the application reads
|
||||||
|
assertThat(value.getFileValue()).isEqualTo("${http.port:8080}");
|
||||||
|
assertThat(value.getEffectiveValue()).isEqualTo("8081");
|
||||||
|
// still sourced from the polaris file itself: normalized coordinate
|
||||||
|
assertThat(value.getPropertySource()).isEqualTo("polaris:default/order-service/application.yaml");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetKeysSortedAndComplete() {
|
||||||
|
Map<String, String> props = new LinkedHashMap<>();
|
||||||
|
props.put("b.key", "2");
|
||||||
|
props.put("a.key", "1");
|
||||||
|
props.put("c.key", "3");
|
||||||
|
addFileSource(NAMESPACE, GROUP, FILE_NAME, props);
|
||||||
|
|
||||||
|
List<String> keys = provider.getKeys(metadata(NAMESPACE, GROUP, FILE_NAME));
|
||||||
|
|
||||||
|
assertThat(keys).containsExactly("a.key", "b.key", "c.key");
|
||||||
|
// unknown coordinate: empty list, and SDK will omit the whole properties field
|
||||||
|
assertThat(provider.getKeys(metadata(NAMESPACE, GROUP, "unknown.yaml"))).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testConflictsAcrossMultipleWatchedFiles() {
|
||||||
|
addFileSource(NAMESPACE, GROUP, FILE_NAME, Collections.singletonMap("k", "1"));
|
||||||
|
addFileSource(NAMESPACE, "common", "common.yaml", Collections.singletonMap("k", "2"));
|
||||||
|
|
||||||
|
List<ConfigKeyConflict> conflicts = provider.resolveConflicts("k", metadata(NAMESPACE, GROUP, FILE_NAME));
|
||||||
|
|
||||||
|
assertThat(conflicts).hasSize(1);
|
||||||
|
ConfigKeyConflict conflict = conflicts.get(0);
|
||||||
|
assertThat(conflict.getNamespace()).isEqualTo(NAMESPACE);
|
||||||
|
assertThat(conflict.getGroup()).isEqualTo("common");
|
||||||
|
assertThat(conflict.getFileName()).isEqualTo("common.yaml");
|
||||||
|
assertThat(conflict.getValue()).isEqualTo("2");
|
||||||
|
|
||||||
|
// querying from the other side excludes itself and reports application.yaml
|
||||||
|
List<ConfigKeyConflict> reverse = provider.resolveConflicts("k", metadata(NAMESPACE, "common", "common.yaml"));
|
||||||
|
assertThat(reverse).hasSize(1);
|
||||||
|
assertThat(reverse.get(0).getFileName()).isEqualTo(FILE_NAME);
|
||||||
|
assertThat(reverse.get(0).getValue()).isEqualTo("1");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCompositeConfigFileExpanded() {
|
||||||
|
// group-dimension loading: sub files merged into a CompositeConfigFile, first file wins
|
||||||
|
ConfigKVFile subA = mockConfigKVFile(NAMESPACE, "mygroup", "a.yaml", Collections.singletonMap("k", "1"));
|
||||||
|
ConfigKVFile subB = mockConfigKVFile(NAMESPACE, "mygroup", "b.yaml", Collections.singletonMap("k", "2"));
|
||||||
|
CompositeConfigFile composite = new CompositeConfigFile(Arrays.asList(subA, subB));
|
||||||
|
Map<String, Object> merged = new HashMap<>();
|
||||||
|
merged.put("k", "1");
|
||||||
|
PolarisPropertySource groupSource = new PolarisPropertySource(NAMESPACE, "mygroup", "", composite, merged);
|
||||||
|
PolarisPropertySourceManager.addPropertySource(groupSource);
|
||||||
|
environment.getPropertySources().addLast(groupSource);
|
||||||
|
|
||||||
|
// query the losing sub file b.yaml
|
||||||
|
EffectiveValue value = provider.resolve("k", metadata(NAMESPACE, "mygroup", "b.yaml"));
|
||||||
|
|
||||||
|
assertThat(value).isNotNull();
|
||||||
|
// file value from sub file b, effective value from the merged group map (a wins)
|
||||||
|
assertThat(value.getFileValue()).isEqualTo("2");
|
||||||
|
assertThat(value.getEffectiveValue()).isEqualTo("1");
|
||||||
|
// property source points to the winning sub file, not the opaque group source name
|
||||||
|
assertThat(value.getPropertySource()).isEqualTo("polaris:default/mygroup/a.yaml");
|
||||||
|
|
||||||
|
// conflicts are sub-file grained: b is excluded, a is reported
|
||||||
|
List<ConfigKeyConflict> conflicts = provider.resolveConflicts("k", metadata(NAMESPACE, "mygroup", "b.yaml"));
|
||||||
|
assertThat(conflicts).hasSize(1);
|
||||||
|
assertThat(conflicts.get(0).getFileName()).isEqualTo("a.yaml");
|
||||||
|
assertThat(conflicts.get(0).getValue()).isEqualTo("1");
|
||||||
|
|
||||||
|
// getKeys finds the sub file inside the composite
|
||||||
|
assertThat(provider.getKeys(metadata(NAMESPACE, "mygroup", "a.yaml"))).containsExactly("k");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testResolveKeepsFileValueWhenEffectiveResolutionFails() {
|
||||||
|
// environment.getProperty 抛异常(如不可解析占位符):生效值维度降级为 null,
|
||||||
|
// 但已到手的文件原始值照常返回
|
||||||
|
addFileSource(NAMESPACE, GROUP, FILE_NAME, Collections.singletonMap("k", "file-v"));
|
||||||
|
ConfigurableEnvironment broken = mock(ConfigurableEnvironment.class);
|
||||||
|
when(broken.getProperty(anyString()))
|
||||||
|
.thenThrow(new IllegalArgumentException("Could not resolve placeholder"));
|
||||||
|
SpringConfigEffectiveValueProvider failingProvider = new SpringConfigEffectiveValueProvider(broken);
|
||||||
|
|
||||||
|
EffectiveValue value = failingProvider.resolve("k", metadata(NAMESPACE, GROUP, FILE_NAME));
|
||||||
|
|
||||||
|
assertThat(value).isNotNull();
|
||||||
|
assertThat(value.getFileValue()).isEqualTo("file-v");
|
||||||
|
assertThat(value.getEffectiveValue()).isNull();
|
||||||
|
assertThat(value.getPropertySource()).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testConflictsSkipsBrokenFile() {
|
||||||
|
// 一个坏文件(getProperty 抛异常)不应丢弃已从健康文件收集的冲突
|
||||||
|
addFileSource(NAMESPACE, "common", "common.yaml", Collections.singletonMap("k", "2"));
|
||||||
|
ConfigKVFile broken = mock(ConfigKVFile.class);
|
||||||
|
lenient().when(broken.getNamespace()).thenReturn(NAMESPACE);
|
||||||
|
lenient().when(broken.getFileGroup()).thenReturn("broken-group");
|
||||||
|
lenient().when(broken.getFileName()).thenReturn("broken.yaml");
|
||||||
|
when(broken.getProperty(anyString(), any())).thenThrow(new RuntimeException("boom"));
|
||||||
|
PolarisPropertySource brokenSource = new PolarisPropertySource(NAMESPACE, "broken-group", "broken.yaml",
|
||||||
|
broken, new HashMap<String, Object>());
|
||||||
|
PolarisPropertySourceManager.addPropertySource(brokenSource);
|
||||||
|
|
||||||
|
List<ConfigKeyConflict> conflicts = provider.resolveConflicts("k", metadata(NAMESPACE, GROUP, FILE_NAME));
|
||||||
|
|
||||||
|
assertThat(conflicts).hasSize(1);
|
||||||
|
assertThat(conflicts.get(0).getFileName()).isEqualTo("common.yaml");
|
||||||
|
assertThat(conflicts.get(0).getValue()).isEqualTo("2");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testConflictsDedupWhenWatchedAtBothDimensions() {
|
||||||
|
// 同一文件同时以文件维度与 group 维度监听注册:冲突按坐标去重
|
||||||
|
Map<String, String> props = Collections.singletonMap("k", "2");
|
||||||
|
addFileSource(NAMESPACE, "common", "common.yaml", props);
|
||||||
|
ConfigKVFile sub = mockConfigKVFile(NAMESPACE, "common", "common.yaml", props);
|
||||||
|
CompositeConfigFile composite = new CompositeConfigFile(Collections.singletonList(sub));
|
||||||
|
PolarisPropertySource groupSource = new PolarisPropertySource(NAMESPACE, "common", "", composite,
|
||||||
|
new HashMap<String, Object>(props));
|
||||||
|
PolarisPropertySourceManager.addPropertySource(groupSource);
|
||||||
|
|
||||||
|
List<ConfigKeyConflict> conflicts = provider.resolveConflicts("k", metadata(NAMESPACE, GROUP, FILE_NAME));
|
||||||
|
|
||||||
|
assertThat(conflicts).hasSize(1);
|
||||||
|
assertThat(conflicts.get(0).getFileName()).isEqualTo("common.yaml");
|
||||||
|
assertThat(conflicts.get(0).getValue()).isEqualTo("2");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testBootstrapWrappedSourcesUnwrapped() {
|
||||||
|
// bootstrap 模式:PolarisPropertySource 被 CompositePropertySource("polaris-config") 聚合,
|
||||||
|
// 外层再被 BootstrapPropertySource 包装——来源解析需递归解包出坐标
|
||||||
|
PolarisPropertySource polarisSource = addFileSource(NAMESPACE, GROUP, FILE_NAME,
|
||||||
|
Collections.singletonMap("k", "1"));
|
||||||
|
environment.getPropertySources().remove(polarisSource.getName());
|
||||||
|
CompositePropertySource composite = new CompositePropertySource("polaris-config");
|
||||||
|
composite.addPropertySource(polarisSource);
|
||||||
|
environment.getPropertySources().addLast(new BootstrapPropertySource<>(composite));
|
||||||
|
|
||||||
|
EffectiveValue value = provider.resolve("k", metadata(NAMESPACE, GROUP, FILE_NAME));
|
||||||
|
|
||||||
|
assertThat(value).isNotNull();
|
||||||
|
assertThat(value.getEffectiveValue()).isEqualTo("1");
|
||||||
|
assertThat(value.getPropertySource()).isEqualTo("polaris:default/order-service/application.yaml");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testConfigurationPropertiesFacadeSourceSkipped() {
|
||||||
|
// Spring Boot 的 configurationProperties facade source 挂在链头,
|
||||||
|
// 其 containsProperty 委托给所有底层 source(任意 key 都命中),
|
||||||
|
// 不跳过会遮蔽真实来源,property_source 恒为 "configurationProperties"
|
||||||
|
addFileSource(NAMESPACE, GROUP, FILE_NAME, Collections.singletonMap("k", "1"));
|
||||||
|
environment.getPropertySources().addFirst(new MapPropertySource("configurationProperties",
|
||||||
|
Collections.singletonMap("k", "1")));
|
||||||
|
|
||||||
|
EffectiveValue value = provider.resolve("k", metadata(NAMESPACE, GROUP, FILE_NAME));
|
||||||
|
|
||||||
|
assertThat(value).isNotNull();
|
||||||
|
assertThat(value.getPropertySource()).isEqualTo("polaris:default/order-service/application.yaml");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testKeyNotPresentAnywhere() {
|
||||||
|
addFileSource(NAMESPACE, GROUP, FILE_NAME, Collections.singletonMap("other.key", "1"));
|
||||||
|
|
||||||
|
EffectiveValue value = provider.resolve("missing.key", metadata(NAMESPACE, GROUP, FILE_NAME));
|
||||||
|
|
||||||
|
assertThat(value).isNotNull();
|
||||||
|
assertThat(value.getFileValue()).isNull();
|
||||||
|
assertThat(value.getEffectiveValue()).isNull();
|
||||||
|
assertThat(value.getPropertySource()).isNull();
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in new issue