fix: keep config fallback and polarisconfig endpoint usable

Serialize actuator polarisconfig as safe DTOs so ClientId is reachable, honor local-file-root-path for remote cache, and continue startup from existing cache when the config server is unreachable.

Co-authored-by: Cursor <cursoragent@cursor.com>
pull/1818/head
evelynwei 1 week ago
parent 333d575215
commit 35501bccfb

@ -16,4 +16,4 @@
- [feat: support audit log](https://github.com/Tencent/spring-cloud-tencent/pull/1812)
- [feat: support config effective value ](https://github.com/Tencent/spring-cloud-tencent/pull/1815)
- [feat: support encoding and decoding TSF headers without TSF Consul](https://github.com/Tencent/spring-cloud-tencent/pull/1816)
- [feat: mask config values of encrypted config files in logs](https://github.com/Tencent/spring-cloud-tencent/pull/1817)
- [feat: protect encrypted config values and improve config cache fallback](https://github.com/Tencent/spring-cloud-tencent/pull/1817)

@ -17,6 +17,7 @@
package com.tencent.cloud.polaris.config;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@ -106,9 +107,10 @@ public class ConfigurationModifier implements PolarisConfigurationConfigModifier
ConnectorConfigImpl connectorConfig = configuration.getConfigFile().getServerConnector();
// set connector type
connectorConfig.setConnectorType(polarisConfigProperties.getDataSource());
String localFileRootPath = polarisConfigProperties.getLocalFileRootPath();
// The same directory stores local-source files and remote-source fallback caches.
connectorConfig.setPersistDir(localFileRootPath);
if (StringUtils.equalsIgnoreCase(polarisConfigProperties.getDataSource(), LOCAL_FILE_CONNECTOR_TYPE)) {
String localFileRootPath = polarisConfigProperties.getLocalFileRootPath();
connectorConfig.setPersistDir(localFileRootPath);
LOGGER.info("[SCT] Run spring cloud tencent config with local data source. localFileRootPath = {}", localFileRootPath);
return;
}
@ -131,7 +133,7 @@ public class ConfigurationModifier implements PolarisConfigurationConfigModifier
// enable close check address for unit tests
if (polarisConfigProperties.isCheckAddress()) {
checkAddressAccessible(configAddresses);
checkAddressAccessible(configAddresses, connectorConfig.getFallbackToLocalCache());
}
connectorConfig.setAddresses(configAddresses);
@ -218,7 +220,7 @@ public class ConfigurationModifier implements PolarisConfigurationConfigModifier
return polarisAddresses;
}
private void checkAddressAccessible(List<String> configAddresses) {
private void checkAddressAccessible(List<String> configAddresses, Boolean fallbackToLocalCache) {
// check address can connect
configAddresses.forEach(address -> {
String[] ipPort;
@ -243,7 +245,13 @@ public class ConfigurationModifier implements PolarisConfigurationConfigModifier
String errMsg = "Config server address (" + address + ") can not be connected. Please check your config in bootstrap.yml"
+ " with spring.cloud.polaris.address or spring.cloud.polaris.config.address.";
if (polarisConfigProperties.isShutdownIfConnectToConfigServerFailed()) {
throw new IllegalArgumentException(errMsg);
if (Boolean.TRUE.equals(fallbackToLocalCache) && hasLocalConfigCache()) {
LOGGER.warn("{} Continue startup to fall back to cached config files in {}.",
errMsg, polarisConfigProperties.getLocalFileRootPath());
}
else {
throw new IllegalArgumentException(errMsg);
}
}
else {
LOGGER.error(errMsg);
@ -251,4 +259,15 @@ public class ConfigurationModifier implements PolarisConfigurationConfigModifier
}
});
}
private boolean hasLocalConfigCache() {
String rootPath = polarisConfigProperties.getLocalFileRootPath();
if (StringUtils.isBlank(rootPath)) {
return false;
}
// Polaris config cache file names follow namespace#group#fileName.yaml.
File[] files = new File(rootPath).listFiles(file -> file.isFile()
&& file.getName().endsWith(".yaml") && file.getName().contains("#"));
return files != null && files.length > 0;
}
}

@ -86,7 +86,7 @@ public class PolarisConfigProperties {
private String dataSource = "polaris";
/**
* The root path of config files, only used in local mode.
* The root path of local config files and remote config fallback caches.
*/
private String localFileRootPath = "./polaris/backup/config";

@ -17,14 +17,18 @@
package com.tencent.cloud.polaris.config.endpoint;
import java.util.HashMap;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import com.tencent.cloud.polaris.config.PolarisConfigSDKContextManager;
import com.tencent.cloud.polaris.config.adapter.PolarisPropertySource;
import com.tencent.cloud.polaris.config.adapter.PolarisPropertySourceManager;
import com.tencent.cloud.polaris.config.config.ConfigFileGroup;
import com.tencent.cloud.polaris.config.config.PolarisConfigProperties;
import com.tencent.polaris.configuration.api.core.ConfigKVFile;
import com.tencent.polaris.configuration.client.internal.CompositeConfigFile;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
@ -45,15 +49,97 @@ public class PolarisConfigEndpoint {
@ReadOperation
public Map<String, Object> polarisConfig() {
Map<String, Object> configInfo = new HashMap<>();
configInfo.put("PolarisConfigProperties", polarisConfigProperties);
Map<String, Object> configInfo = new LinkedHashMap<>();
configInfo.put("PolarisConfigProperties", configProperties());
configInfo.put("PolarisPropertySource", propertySources());
configInfo.put("ClientId", getClientId());
return configInfo;
}
List<PolarisPropertySource> propertySourceList = PolarisPropertySourceManager.getAllPropertySources();
configInfo.put("PolarisPropertySource", propertySourceList);
private Map<String, Object> configProperties() {
Map<String, Object> properties = new LinkedHashMap<>();
properties.put("enabled", polarisConfigProperties.isEnabled());
properties.put("address", polarisConfigProperties.getAddress());
properties.put("port", polarisConfigProperties.getPort());
properties.put("autoRefresh", polarisConfigProperties.isAutoRefresh());
properties.put("shutdownIfConnectToConfigServerFailed",
polarisConfigProperties.isShutdownIfConnectToConfigServerFailed());
properties.put("preference", polarisConfigProperties.isPreference());
properties.put("refreshType", polarisConfigProperties.getRefreshType());
properties.put("groups", configFileGroups(polarisConfigProperties.getGroups()));
properties.put("dataSource", polarisConfigProperties.getDataSource());
properties.put("localFileRootPath", polarisConfigProperties.getLocalFileRootPath());
properties.put("internalEnabled", polarisConfigProperties.isInternalEnabled());
properties.put("checkAddress", polarisConfigProperties.isCheckAddress());
properties.put("emptyProtectionEnabled", polarisConfigProperties.isEmptyProtectionEnabled());
properties.put("emptyProtectionExpiredInterval", polarisConfigProperties.getEmptyProtectionExpiredInterval());
configInfo.put("ClientId", getClientId());
PolarisConfigProperties.Report reportProperties = polarisConfigProperties.getReport();
if (reportProperties != null) {
Map<String, Object> report = new LinkedHashMap<>();
report.put("enabled", reportProperties.isEnabled());
if (reportProperties.getEffective() != null) {
Map<String, Object> effective = new LinkedHashMap<>();
effective.put("enabled", reportProperties.getEffective().isEnabled());
report.put("effective", effective);
}
properties.put("report", report);
}
// token is intentionally omitted: an actuator endpoint must not expose credentials.
return properties;
}
return configInfo;
private List<Map<String, Object>> configFileGroups(List<ConfigFileGroup> groups) {
List<Map<String, Object>> groupInfo = new ArrayList<>();
if (groups == null) {
return groupInfo;
}
for (ConfigFileGroup group : groups) {
Map<String, Object> info = new LinkedHashMap<>();
info.put("namespace", group.getNamespace());
info.put("name", group.getName());
info.put("files", group.getFiles() == null ? new ArrayList<>() : new ArrayList<>(group.getFiles()));
groupInfo.add(info);
}
return groupInfo;
}
private List<Map<String, Object>> propertySources() {
List<Map<String, Object>> sources = new ArrayList<>();
for (PolarisPropertySource source : PolarisPropertySourceManager.getAllPropertySources()) {
Map<String, Object> sourceInfo = new LinkedHashMap<>();
sourceInfo.put("namespace", source.getNamespace());
sourceInfo.put("group", source.getGroup());
sourceInfo.put("fileName", source.getFileName());
sourceInfo.put("propertyNames", new ArrayList<>(source.getSource().keySet()));
sourceInfo.put("configKVFile", configFileInfo(source.getConfigKVFile()));
sources.add(sourceInfo);
}
return sources;
}
private Map<String, Object> configFileInfo(ConfigKVFile configFile) {
Map<String, Object> info = new LinkedHashMap<>();
if (configFile == null) {
return info;
}
info.put("namespace", configFile.getNamespace());
info.put("fileGroup", configFile.getFileGroup());
info.put("fileName", configFile.getFileName());
info.put("fileVersion", configFile.getFileVersion());
info.put("propertyNames", configFile.getPropertyNames() == null
? new ArrayList<>() : new ArrayList<>(configFile.getPropertyNames()));
if (configFile instanceof CompositeConfigFile) {
List<Map<String, Object>> files = new ArrayList<>();
List<ConfigKVFile> configFiles = ((CompositeConfigFile) configFile).getConfigKVFiles();
if (configFiles != null) {
for (ConfigKVFile file : configFiles) {
files.add(configFileInfo(file));
}
}
info.put("configKVFiles", files);
}
return info;
}
/**

@ -83,7 +83,7 @@
"name": "spring.cloud.polaris.config.local-file-root-path",
"type": "java.lang.String",
"defaultValue": "./polaris/backup/config",
"description": "Where to load config file, polaris or local."
"description": "The root path of local config files and remote config fallback caches."
},
{
"name": "spring.cloud.polaris.config.report.enabled",

@ -18,6 +18,8 @@
package com.tencent.cloud.polaris.config;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@ -51,6 +53,7 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
@ -94,6 +97,9 @@ class ConfigurationModifierTest {
@Mock
private ConfigEffectiveQueryConfig configEffectiveCustomizer;
@TempDir
private Path tempDir;
private ConfigurationModifier configurationModifier;
@BeforeEach
@ -318,6 +324,7 @@ class ConfigurationModifierTest {
when(polarisContextProperties.getEnabled()).thenReturn(true);
when(polarisConfigProperties.isEnabled()).thenReturn(true);
when(polarisConfigProperties.getDataSource()).thenReturn("polaris");
when(polarisConfigProperties.getLocalFileRootPath()).thenReturn("/tmp/polaris-config-cache");
when(polarisConfigProperties.getAddress()).thenReturn("grpc://127.0.0.1:8093");
when(polarisConfigProperties.isCheckAddress()).thenReturn(false);
when(polarisConfigProperties.isEmptyProtectionEnabled()).thenReturn(true);
@ -343,6 +350,7 @@ class ConfigurationModifierTest {
// Assert
ConnectorConfigImpl connectorConfig = configuration.getConfigFile().getServerConnector();
verify(connectorConfig).setPersistDir("/tmp/polaris-config-cache");
verify(connectorConfig).setAddresses(parsedAddresses);
verify(connectorConfig).setLbPolicy("roundRobin");
verify(connectorConfig).setServerSwitchInterval(600000L);
@ -699,6 +707,54 @@ class ConfigurationModifierTest {
}
}
/**
* Test an inaccessible address when an existing local cache can be used.
* Scenario: checkAddress and shutdown are enabled, but SDK fallback is enabled and the
* configured persist directory contains a cache file.
* Expect: the preflight check does not abort startup, allowing the SDK to load the cache.
*/
@DisplayName("modify should continue to local cache when config address is unavailable")
@Test
void testModify_CheckAddressNotAccessibleWithLocalCacheFallback() throws Exception {
ConfigurationImpl configuration = buildMockConfiguration();
ConnectorConfigImpl connectorConfig = configuration.getConfigFile().getServerConnector();
Path cacheFile = Files.createFile(tempDir.resolve("default#group#application.properties.yaml"));
when(polarisContextProperties.getEnabled()).thenReturn(true);
when(polarisConfigProperties.isEnabled()).thenReturn(true);
when(polarisConfigProperties.getDataSource()).thenReturn("polaris");
when(polarisConfigProperties.getLocalFileRootPath()).thenReturn(tempDir.toString());
when(polarisConfigProperties.getAddress()).thenReturn("grpc://127.0.0.1:1");
when(polarisConfigProperties.isCheckAddress()).thenReturn(true);
when(polarisConfigProperties.isShutdownIfConnectToConfigServerFailed()).thenReturn(true);
when(polarisConfigProperties.isEmptyProtectionEnabled()).thenReturn(true);
when(polarisConfigProperties.getEmptyProtectionExpiredInterval()).thenReturn(604800000L);
when(polarisCryptoConfigProperties.isEnabled()).thenReturn(false);
when(polarisContextProperties.getAddress()).thenReturn("grpc://127.0.0.1:8091");
when(polarisContextProperties.getAddressLbPolicy()).thenReturn("roundRobin");
when(polarisContextProperties.getServerSwitchInterval()).thenReturn(600000L);
when(connectorConfig.getFallbackToLocalCache()).thenReturn(true);
List<String> parsedAddresses = Collections.singletonList("127.0.0.1:1");
List<String> parsedPolarisAddresses = Collections.singletonList("127.0.0.1:8091");
try (MockedStatic<AddressUtils> mockedAddressUtils = Mockito.mockStatic(AddressUtils.class);
MockedStatic<TsfContextUtils> mockedTsf = Mockito.mockStatic(TsfContextUtils.class)) {
mockedAddressUtils.when(() -> AddressUtils.parseAddressList("grpc://127.0.0.1:1"))
.thenReturn(parsedAddresses);
mockedAddressUtils.when(() -> AddressUtils.parseAddressList("grpc://127.0.0.1:8091"))
.thenReturn(parsedPolarisAddresses);
mockedAddressUtils.when(() -> AddressUtils.accessible("127.0.0.1", 1, 3000))
.thenReturn(false);
mockedTsf.when(TsfContextUtils::isOnlyTsfConsulEnabled).thenReturn(false);
configurationModifier.modify(configuration);
verify(connectorConfig).setPersistDir(tempDir.toString());
verify(connectorConfig).setAddresses(parsedAddresses);
assertThat(cacheFile).exists();
}
}
/**
* Test modify with checkAddress enabled, address not accessible but shutdown disabled.
* Scenario: checkAddress is true, accessible returns false, shutdownIfConnectToConfigServerFailed is false.

@ -17,10 +17,11 @@
package com.tencent.cloud.polaris.config.endpoint;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.tencent.cloud.polaris.config.PolarisConfigSDKContextManager;
import com.tencent.cloud.polaris.config.adapter.MockedConfigKVFile;
import com.tencent.cloud.polaris.config.adapter.PolarisPropertySource;
@ -28,6 +29,7 @@ import com.tencent.cloud.polaris.config.adapter.PolarisPropertySourceManager;
import com.tencent.cloud.polaris.config.config.PolarisConfigProperties;
import com.tencent.polaris.api.plugin.common.ValueContext;
import com.tencent.polaris.client.api.SDKContext;
import com.tencent.polaris.configuration.client.internal.CompositeConfigFile;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@ -64,20 +66,57 @@ public class PolarisConfigEndpointTest {
}
@Test
public void testPolarisConfigEndpoint() {
@SuppressWarnings("unchecked")
public void testPolarisConfigEndpoint() throws Exception {
PolarisConfigProperties properties = new PolarisConfigProperties();
properties.setToken("endpoint-must-not-expose-this-token");
Map<String, Object> content = new HashMap<>();
content.put("k1", "v1");
content.put("k2", "v2");
content.put("k3", "v3");
content.put("k1", "sensitive-value-one");
content.put("k2", "sensitive-value-two");
content.put("k3", "sensitive-value-three");
MockedConfigKVFile file = new MockedConfigKVFile(content);
PolarisPropertySource polarisPropertySource = new PolarisPropertySource(testNamespace, testServiceName, testFileName,
file, content);
PolarisPropertySourceManager.addPropertySource(polarisPropertySource);
PolarisConfigEndpoint endpoint = new PolarisConfigEndpoint(polarisConfigProperties);
PolarisConfigEndpoint endpoint = new PolarisConfigEndpoint(properties);
Map<String, Object> info = endpoint.polarisConfig();
assertThat(polarisConfigProperties).isEqualTo(info.get("PolarisConfigProperties"));
assertThat(Collections.singletonList(polarisPropertySource)).isEqualTo(info.get("PolarisPropertySource"));
assertThat(info.get("PolarisConfigProperties")).isInstanceOf(Map.class);
List<Map<String, Object>> sources = (List<Map<String, Object>>) info.get("PolarisPropertySource");
assertThat(sources).hasSize(1);
assertThat(sources.get(0)).containsEntry("namespace", testNamespace)
.containsEntry("group", testServiceName)
.containsEntry("fileName", testFileName);
// Actuator serializes the return value. Keep it to plain DTO structures and never expose
// property values through this diagnostic endpoint.
String json = new ObjectMapper().writeValueAsString(info);
assertThat(json).contains("\"ClientId\":null", "\"propertyNames\":[")
.doesNotContain("sensitive-value-one", "sensitive-value-two", "sensitive-value-three",
"endpoint-must-not-expose-this-token");
}
@Test
@SuppressWarnings("unchecked")
public void testPolarisConfigEndpointExposesCompositeFileMetadata() {
MockedConfigKVFile first = new MockedConfigKVFile(Map.of("first.key", "first-value"),
"first.properties", testServiceName, testNamespace);
MockedConfigKVFile second = new MockedConfigKVFile(Map.of("second.key", "second-value"),
"second.properties", testServiceName, testNamespace);
CompositeConfigFile composite = new CompositeConfigFile(List.of(first, second));
PolarisPropertySource source = new PolarisPropertySource(testNamespace, testServiceName, "",
composite, new HashMap<>());
PolarisPropertySourceManager.addPropertySource(source);
PolarisConfigEndpoint endpoint = new PolarisConfigEndpoint(polarisConfigProperties);
List<Map<String, Object>> sources =
(List<Map<String, Object>>) endpoint.polarisConfig().get("PolarisPropertySource");
Map<String, Object> configKVFile = (Map<String, Object>) sources.get(0).get("configKVFile");
List<Map<String, Object>> files =
(List<Map<String, Object>>) configKVFile.get("configKVFiles");
assertThat(files).extracting(file -> file.get("fileName"))
.containsExactly("first.properties", "second.properties");
}
@Test

Loading…
Cancel
Save