-
-
Notifications
You must be signed in to change notification settings - Fork 10.2k
Feature: Support application-level export configuration #5517
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -16,6 +16,7 @@ | |||||||||||||||||||
| */ | ||||||||||||||||||||
| package com.ctrip.framework.apollo.portal.controller; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| import com.ctrip.framework.apollo.common.exception.BadRequestException; | ||||||||||||||||||||
| import com.google.common.base.Splitter; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| import com.ctrip.framework.apollo.core.enums.ConfigFileFormat; | ||||||||||||||||||||
|
|
@@ -44,10 +45,10 @@ | |||||||||||||||||||
| @RestController | ||||||||||||||||||||
| public class ConfigsImportController { | ||||||||||||||||||||
| private static final String ENV_SEPARATOR = ","; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| private static final String CONFLICT_ACTION_IGNORE = "ignore"; | ||||||||||||||||||||
| private static final String CONFLICT_ACTION_COVER = "cover"; | ||||||||||||||||||||
| private final ConfigsImportService configsImportService; | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| public ConfigsImportController(final ConfigsImportService configsImportService) { | ||||||||||||||||||||
| this.configsImportService = configsImportService; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
@@ -76,31 +77,42 @@ public void importConfigFile(@PathVariable String appId, @PathVariable String en | |||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| @PreAuthorize(value = "@unifiedPermissionValidator.isSuperAdmin()") | ||||||||||||||||||||
| @PostMapping(value = "/configs/import", params = "conflictAction=cover") | ||||||||||||||||||||
| public void importConfigByZipWithCoverConflictNamespace(@RequestParam(value = "envs") String envs, | ||||||||||||||||||||
| @PostMapping(value = "/configs/import") | ||||||||||||||||||||
| public void importConfigByZip(@RequestParam(value = "envs") String envs, | ||||||||||||||||||||
| @RequestParam(defaultValue = CONFLICT_ACTION_IGNORE) String conflictAction, | ||||||||||||||||||||
| @RequestParam("file") MultipartFile file) throws IOException { | ||||||||||||||||||||
| validateConflictAction(conflictAction); | ||||||||||||||||||||
| boolean ignoreConflictNamespace = conflictAction.equals(CONFLICT_ACTION_IGNORE); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| List<Env> importEnvs = Splitter.on(ENV_SEPARATOR).splitToList(envs).stream() | ||||||||||||||||||||
| .map(env -> Env.valueOf(env)).collect(Collectors.toList()); | ||||||||||||||||||||
| List<Env> importEnvs = Splitter.on(ENV_SEPARATOR).splitToList(envs).stream().map(Env::valueOf) | ||||||||||||||||||||
| .collect(Collectors.toList()); | ||||||||||||||||||||
|
Comment on lines
+87
to
+88
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Potential unhandled Per the Consider wrapping with proper exception handling: - List<Env> importEnvs = Splitter.on(ENV_SEPARATOR).splitToList(envs).stream().map(Env::valueOf)
- .collect(Collectors.toList());
+ List<Env> importEnvs;
+ try {
+ importEnvs = Splitter.on(ENV_SEPARATOR).splitToList(envs).stream().map(Env::valueOf)
+ .collect(Collectors.toList());
+ } catch (IllegalArgumentException e) {
+ throw new BadRequestException("Invalid environment: " + e.getMessage());
+ }📝 Committable suggestion
Suggested change
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| byte[] bytes = file.getBytes(); | ||||||||||||||||||||
| try (ZipInputStream zipInputStream = new ZipInputStream(new ByteArrayInputStream(bytes))) { | ||||||||||||||||||||
| configsImportService.importDataFromZipFile(importEnvs, zipInputStream, false); | ||||||||||||||||||||
| configsImportService.importDataFromZipFile(importEnvs, zipInputStream, | ||||||||||||||||||||
| ignoreConflictNamespace); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
Comment on lines
90
to
94
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid Both ZIP endpoints buffer the entire upload into memory before creating - byte[] bytes = file.getBytes();
- try (ZipInputStream zipInputStream = new ZipInputStream(new ByteArrayInputStream(bytes))) {
+ try (ZipInputStream zipInputStream = new ZipInputStream(file.getInputStream())) {
...
}Optionally also validate “zip-ness” (content-type/filename) and enforce/verify upload size limits. Also applies to: 105-109 🤖 Prompt for AI Agents |
||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| @PreAuthorize(value = "@unifiedPermissionValidator.isSuperAdmin()") | ||||||||||||||||||||
| @PostMapping(value = "/configs/import", params = "conflictAction=ignore") | ||||||||||||||||||||
| public void importConfigByZipWithIgnoreConflictNamespace( | ||||||||||||||||||||
| @RequestParam(value = "envs") String envs, @RequestParam("file") MultipartFile file) | ||||||||||||||||||||
| throws IOException { | ||||||||||||||||||||
|
|
||||||||||||||||||||
| List<Env> importEnvs = Splitter.on(ENV_SEPARATOR).splitToList(envs).stream() | ||||||||||||||||||||
| .map(env -> Env.valueOf(env)).collect(Collectors.toList()); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| @PreAuthorize(value = "@unifiedPermissionValidator.isAppAdmin(#appId)") | ||||||||||||||||||||
| @PostMapping(value = "/apps/{appId}/envs/{env}/clusters/{clusterName}/import") | ||||||||||||||||||||
| public void importAppConfigByZip(@PathVariable String appId, @PathVariable String env, | ||||||||||||||||||||
| @PathVariable String clusterName, | ||||||||||||||||||||
| @RequestParam(defaultValue = CONFLICT_ACTION_IGNORE) String conflictAction, | ||||||||||||||||||||
| @RequestParam("file") MultipartFile file) throws IOException { | ||||||||||||||||||||
| validateConflictAction(conflictAction); | ||||||||||||||||||||
| boolean ignoreConflictNamespace = conflictAction.equals(CONFLICT_ACTION_IGNORE); | ||||||||||||||||||||
| byte[] bytes = file.getBytes(); | ||||||||||||||||||||
| try (ZipInputStream zipInputStream = new ZipInputStream(new ByteArrayInputStream(bytes))) { | ||||||||||||||||||||
| configsImportService.importDataFromZipFile(importEnvs, zipInputStream, true); | ||||||||||||||||||||
| configsImportService.importAppConfigFromZipFile(appId, Env.valueOf(env), clusterName, | ||||||||||||||||||||
| zipInputStream, ignoreConflictNamespace); | ||||||||||||||||||||
|
Comment on lines
+107
to
+108
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same The + Env envObj;
+ try {
+ envObj = Env.valueOf(env);
+ } catch (IllegalArgumentException e) {
+ throw new BadRequestException("Invalid environment: " + env);
+ }
byte[] bytes = file.getBytes();
try (ZipInputStream zipInputStream = new ZipInputStream(new ByteArrayInputStream(bytes))) {
- configsImportService.importAppConfigFromZipFile(appId, Env.valueOf(env), clusterName,
+ configsImportService.importAppConfigFromZipFile(appId, envObj, clusterName,
zipInputStream, ignoreConflictNamespace);
}
🤖 Prompt for AI Agents |
||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| private void validateConflictAction(String conflictAction) { | ||||||||||||||||||||
| if (!conflictAction.equals(CONFLICT_ACTION_COVER) | ||||||||||||||||||||
| && !conflictAction.equals(CONFLICT_ACTION_IGNORE)) { | ||||||||||||||||||||
| throw new BadRequestException("ConflictAction is incorrect."); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
Comment on lines
+112
to
117
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Make conflictAction validation a bit more robust/clear. Minor hardening/readability:
🤖 Prompt for AI Agents |
||||||||||||||||||||
| } | ||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -100,6 +100,43 @@ public void exportData(OutputStream outputStream, List<Env> exportEnvs) { | |
| exportApps(exportEnvs, outputStream); | ||
| } | ||
|
|
||
| /** | ||
| * Export all configurations of an application in a specified environment and cluster | ||
| * <p> | ||
| * File Struts: | ||
| * <p> | ||
| * | ||
| * List<App> -> List<Env> -> List<Namespace> | ||
| * | ||
| * @param outputStream network file download stream to user | ||
| */ | ||
| public void exportAppConfigByEnvAndCluster(String appId, Env env, String clusterName, | ||
| OutputStream outputStream) { | ||
| App app = appService.load(appId); | ||
| if (app == null) { | ||
| throw new BadRequestException("App not found: " + appId); | ||
| } | ||
| ClusterDTO cluster = clusterService.loadCluster(appId, env, clusterName); | ||
| if (cluster == null) { | ||
| throw new BadRequestException( | ||
| "The app does not exist in the specified environment and cluster."); | ||
| } | ||
|
|
||
| try (final ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream)) { | ||
| try { | ||
| this.exportNamespaces(env, app, cluster, zipOutputStream, true); | ||
| } catch (BadRequestException badRequestException) { | ||
|
nobodyiam marked this conversation as resolved.
|
||
| // ignore | ||
| } catch (Exception e) { | ||
| logger.error("export namespace error. appId = {}, env = {}, cluster = {}", app.getAppId(), | ||
| env.getName(), cluster.getName(), e); | ||
| } | ||
|
Comment on lines
+126
to
+133
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Silently ignoring Catching and ignoring Consider at least logging a warning: try {
this.exportNamespaces(env, app, cluster, zipOutputStream, true);
} catch (BadRequestException badRequestException) {
- // ignore
+ logger.warn("export namespace warning. appId = {}, env = {}, cluster = {}, message = {}",
+ app.getAppId(), env.getName(), cluster.getName(), badRequestException.getMessage());
} catch (Exception e) {🤖 Prompt for AI Agents |
||
| } catch (IOException e) { | ||
| logger.error("export app config error", e); | ||
| throw new ServiceException("export app config error", e); | ||
| } | ||
| } | ||
|
|
||
| private void exportApps(final Collection<Env> exportEnvs, OutputStream outputStream) { | ||
| List<App> hasPermissionApps = findHasPermissionApps(); | ||
|
|
||
|
|
@@ -217,7 +254,7 @@ private void exportCluster(final Env env, final App exportApp, ZipOutputStream z | |
| // export namespaces | ||
| exportClusters.parallelStream().forEach(cluster -> { | ||
| try { | ||
| this.exportNamespaces(env, exportApp, cluster, zipOutputStream); | ||
| this.exportNamespaces(env, exportApp, cluster, zipOutputStream, false); | ||
| } catch (BadRequestException badRequestException) { | ||
| // ignore | ||
| } catch (Exception e) { | ||
|
|
@@ -228,7 +265,7 @@ private void exportCluster(final Env env, final App exportApp, ZipOutputStream z | |
| } | ||
|
|
||
| private void exportNamespaces(final Env env, final App exportApp, final ClusterDTO exportCluster, | ||
| ZipOutputStream zipOutputStream) { | ||
| ZipOutputStream zipOutputStream, boolean ignoreUserDir) { | ||
| String clusterName = exportCluster.getName(); | ||
|
|
||
| List<NamespaceBO> namespaceBOS = | ||
|
|
@@ -241,11 +278,11 @@ private void exportNamespaces(final Env env, final App exportApp, final ClusterD | |
| Stream<ConfigBO> configBOStream = namespaceBOS.stream().map(namespaceBO -> new ConfigBO(env, | ||
| exportApp.getOwnerName(), exportApp.getAppId(), clusterName, namespaceBO)); | ||
|
|
||
| writeNamespacesToZip(configBOStream, zipOutputStream); | ||
| writeNamespacesToZip(configBOStream, zipOutputStream, ignoreUserDir); | ||
| } | ||
|
|
||
| private void writeNamespacesToZip(Stream<ConfigBO> configBOStream, | ||
| ZipOutputStream zipOutputStream) { | ||
| ZipOutputStream zipOutputStream, boolean ignoreUserDir) { | ||
| final Consumer<ConfigBO> configBOConsumer = configBO -> { | ||
| try { | ||
| synchronized (zipOutputStream) { | ||
|
|
@@ -257,8 +294,10 @@ private void writeNamespacesToZip(Stream<ConfigBO> configBOStream, | |
|
|
||
| String configFileName = | ||
| ConfigFileUtils.toFilename(appId, clusterName, namespace, configFileFormat); | ||
| String filePath = ConfigFileUtils.genNamespacePath(configBO.getOwnerName(), appId, | ||
| configBO.getEnv(), configFileName); | ||
| String filePath = ignoreUserDir | ||
| ? ConfigFileUtils.genNamespacePathIgnoreUser(appId, configBO.getEnv(), configFileName) | ||
| : ConfigFileUtils.genNamespacePath(configBO.getOwnerName(), appId, configBO.getEnv(), | ||
| configFileName); | ||
|
|
||
| writeToZip(filePath, configFileContent, zipOutputStream); | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Validate env values before calling
Env.valueOfin export endpoints.Both the bulk export (
/configs/export) and the new app-level export (/apps/{appId}/envs/{env}/clusters/{clusterName}/export) callEnv.valueOfon user input. For unknown env names this throwsIllegalArgumentException, which will be surfaced as a 500.To keep the API contract clear, consider translating invalid envs into a client error instead, e.g.:
And similarly wrap the single
envpath variable before passing it toEnv.valueOfinexportAppConfig.Also applies to: 137-152
🤖 Prompt for AI Agents