parent
804f7594dc
commit
61e6bc78f1
@ -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.endpoint.PolarisConfigEndpointAutoConfiguration
|
||||
com.tencent.cloud.polaris.config.PolarisConfigBootstrapAutoConfiguration
|
||||
com.tencent.cloud.polaris.config.PolarisConfigEffectiveValueAutoConfiguration
|
||||
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