diff --git a/api/src/main/java/com/alibaba/nacos/api/plugin/ConfigItemDefinition.java b/api/src/main/java/com/alibaba/nacos/api/plugin/ConfigItemDefinition.java new file mode 100644 index 00000000000..5721aa78fcb --- /dev/null +++ b/api/src/main/java/com/alibaba/nacos/api/plugin/ConfigItemDefinition.java @@ -0,0 +1,167 @@ +/* + * Copyright 1999-2024 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.nacos.api.plugin; + +import java.io.Serializable; +import java.util.List; + +/** + * Plugin configuration item definition. + * + * @author WangzJi + * @since 3.2.0 + */ +public class ConfigItemDefinition implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * Configuration key. + */ + private String key; + + /** + * Display name. + */ + private String name; + + /** + * Description. + */ + private String description; + + /** + * Default value. + */ + private String defaultValue; + + /** + * Configuration item type. + */ + private ConfigItemType type; + + /** + * Whether this item is required. + */ + private boolean required; + + /** + * Enum values (when type is ENUM). + */ + private List enumValues; + + public ConfigItemDefinition() { + } + + public ConfigItemDefinition(String key, String name, ConfigItemType type) { + this.key = key; + this.name = name; + this.type = type; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getDefaultValue() { + return defaultValue; + } + + public void setDefaultValue(String defaultValue) { + this.defaultValue = defaultValue; + } + + public ConfigItemType getType() { + return type; + } + + public void setType(ConfigItemType type) { + this.type = type; + } + + public boolean isRequired() { + return required; + } + + public void setRequired(boolean required) { + this.required = required; + } + + public List getEnumValues() { + return enumValues; + } + + public void setEnumValues(List enumValues) { + this.enumValues = enumValues; + } + + /** + * Builder for ConfigItemDefinition. + */ + public static class Builder { + + private final ConfigItemDefinition definition; + + public Builder(String key, String name, ConfigItemType type) { + this.definition = new ConfigItemDefinition(key, name, type); + } + + public Builder description(String description) { + definition.setDescription(description); + return this; + } + + public Builder defaultValue(String defaultValue) { + definition.setDefaultValue(defaultValue); + return this; + } + + public Builder required(boolean required) { + definition.setRequired(required); + return this; + } + + public Builder enumValues(List enumValues) { + definition.setEnumValues(enumValues); + return this; + } + + public ConfigItemDefinition build() { + return definition; + } + } +} diff --git a/api/src/main/java/com/alibaba/nacos/api/plugin/ConfigItemType.java b/api/src/main/java/com/alibaba/nacos/api/plugin/ConfigItemType.java new file mode 100644 index 00000000000..c6b64b391e9 --- /dev/null +++ b/api/src/main/java/com/alibaba/nacos/api/plugin/ConfigItemType.java @@ -0,0 +1,46 @@ +/* + * Copyright 1999-2024 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.nacos.api.plugin; + +/** + * Configuration item type enumeration. + * + * @author WangzJi + * @since 3.2.0 + */ +public enum ConfigItemType { + + /** + * String type configuration. + */ + STRING, + + /** + * Number type configuration. + */ + NUMBER, + + /** + * Boolean type configuration. + */ + BOOLEAN, + + /** + * Enumeration type configuration. + */ + ENUM +} diff --git a/api/src/main/java/com/alibaba/nacos/api/plugin/PluginConfigSpec.java b/api/src/main/java/com/alibaba/nacos/api/plugin/PluginConfigSpec.java new file mode 100644 index 00000000000..196542e86ff --- /dev/null +++ b/api/src/main/java/com/alibaba/nacos/api/plugin/PluginConfigSpec.java @@ -0,0 +1,51 @@ +/* + * Copyright 1999-2024 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.nacos.api.plugin; + +import java.util.List; +import java.util.Map; + +/** + * Plugin configuration specification interface. + * Allows plugins to declare configurable properties. + * + * @author WangzJi + * @since 3.2.0 + */ +public interface PluginConfigSpec { + + /** + * Get configuration item definitions. + * + * @return list of configuration item definitions + */ + List getConfigDefinitions(); + + /** + * Apply configuration to the plugin. + * + * @param config configuration key-value pairs + */ + void applyConfig(Map config); + + /** + * Get current configuration. + * + * @return current configuration as key-value pairs + */ + Map getCurrentConfig(); +} diff --git a/api/src/main/java/com/alibaba/nacos/api/plugin/PluginProvider.java b/api/src/main/java/com/alibaba/nacos/api/plugin/PluginProvider.java new file mode 100644 index 00000000000..651137b47e3 --- /dev/null +++ b/api/src/main/java/com/alibaba/nacos/api/plugin/PluginProvider.java @@ -0,0 +1,73 @@ +/* + * Copyright 1999-2024 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.nacos.api.plugin; + +import java.util.Map; + +/** + * Plugin provider SPI interface. + * + *

Each plugin type should have one implementation to provide plugin instances. + * This interface enables automatic plugin discovery through SPI mechanism, + * eliminating the need to manually register each plugin type in UnifiedPluginManager. + * + *

Example implementation: + *

{@code
+ * public class AuthPluginProvider implements PluginProvider {
+ *     @Override
+ *     public PluginType getPluginType() {
+ *         return PluginType.AUTH;
+ *     }
+ *
+ *     @Override
+ *     public Map getAllPlugins() {
+ *         return AuthPluginManager.getInstance().getAllPlugins();
+ *     }
+ * }
+ * }
+ * + * @param the plugin service type + * @author WangzJi + * @since 3.2.0 + */ +public interface PluginProvider { + + /** + * Get the plugin type this provider manages. + * + * @return plugin type + */ + PluginType getPluginType(); + + /** + * Get all plugin instances managed by this provider. + * Key is the plugin name, value is the plugin instance. + * + * @return map of plugin name to plugin instance + */ + Map getAllPlugins(); + + /** + * Get the order of this provider. Lower values have higher priority. + * Used when multiple providers exist for same type. + * + * @return order value, default is 0 + */ + default int getOrder() { + return 0; + } +} diff --git a/api/src/main/java/com/alibaba/nacos/api/plugin/PluginStateChecker.java b/api/src/main/java/com/alibaba/nacos/api/plugin/PluginStateChecker.java new file mode 100644 index 00000000000..c454ba052e1 --- /dev/null +++ b/api/src/main/java/com/alibaba/nacos/api/plugin/PluginStateChecker.java @@ -0,0 +1,36 @@ +/* + * Copyright 1999-2024 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.nacos.api.plugin; + +/** + * Plugin state checker interface. + * Used to decouple plugin managers from core module. + * + * @author WangzJi + * @since 3.0.0 + */ +public interface PluginStateChecker { + + /** + * Check if plugin is enabled. + * + * @param pluginType plugin type string + * @param pluginName plugin name + * @return true if plugin is enabled, false otherwise + */ + boolean isPluginEnabled(String pluginType, String pluginName); +} diff --git a/api/src/main/java/com/alibaba/nacos/api/plugin/PluginStateCheckerHolder.java b/api/src/main/java/com/alibaba/nacos/api/plugin/PluginStateCheckerHolder.java new file mode 100644 index 00000000000..e9c57e6b680 --- /dev/null +++ b/api/src/main/java/com/alibaba/nacos/api/plugin/PluginStateCheckerHolder.java @@ -0,0 +1,70 @@ +/* + * Copyright 1999-2024 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.nacos.api.plugin; + +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Static holder for PluginStateChecker. + * Bridges singleton pattern plugin managers with Spring-managed UnifiedPluginManager. + * Uses AtomicReference to ensure thread safety. + * + * @author WangzJi + * @since 3.2.0 + */ +public class PluginStateCheckerHolder { + + private static final AtomicReference INSTANCE = new AtomicReference<>(); + + private PluginStateCheckerHolder() { + } + + /** + * Set the PluginStateChecker instance. + * + * @param checker the PluginStateChecker instance + */ + public static void setInstance(PluginStateChecker checker) { + INSTANCE.set(checker); + } + + /** + * Get the PluginStateChecker instance. + * + * @return Optional containing the PluginStateChecker instance, or empty if not set + */ + public static Optional getInstance() { + return Optional.ofNullable(INSTANCE.get()); + } + + /** + * Check if a plugin is enabled. + * Returns true if no checker is set (backward compatibility). + * + * @param pluginType plugin type string + * @param pluginName plugin name + * @return true if plugin is enabled or no checker is set + */ + public static boolean isPluginEnabled(String pluginType, String pluginName) { + PluginStateChecker checker = INSTANCE.get(); + if (checker == null) { + return true; + } + return checker.isPluginEnabled(pluginType, pluginName); + } +} diff --git a/api/src/main/java/com/alibaba/nacos/api/plugin/PluginType.java b/api/src/main/java/com/alibaba/nacos/api/plugin/PluginType.java new file mode 100644 index 00000000000..6d09cc5c5b2 --- /dev/null +++ b/api/src/main/java/com/alibaba/nacos/api/plugin/PluginType.java @@ -0,0 +1,94 @@ +/* + * Copyright 1999-2024 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.nacos.api.plugin; + +/** + * Plugin type enumeration, supports all Nacos plugin types. + * + * @author WangzJi + * @since 3.2.0 + */ +public enum PluginType { + + /** + * Authentication plugin. + */ + AUTH("auth", "Authentication plugin"), + + /** + * Datasource dialect plugin. + */ + DATASOURCE_DIALECT("datasource-dialect", "Datasource dialect plugin"), + + /** + * Config change plugin. + */ + CONFIG_CHANGE("config-change", "Config change plugin"), + + /** + * Encryption plugin. + */ + ENCRYPTION("encryption", "Encryption plugin"), + + /** + * Trace plugin. + */ + TRACE("trace", "Trace plugin"), + + /** + * Environment plugin. + */ + ENVIRONMENT("environment", "Environment plugin"), + + /** + * Control plugin. + */ + CONTROL("control", "Control plugin"); + + private final String type; + + private final String description; + + PluginType(String type, String description) { + this.type = type; + this.description = description; + } + + public String getType() { + return type; + } + + public String getDescription() { + return description; + } + + /** + * Get PluginType from type string. + * + * @param type type string + * @return PluginType + * @throws IllegalArgumentException if type is unknown + */ + public static PluginType fromType(String type) { + for (PluginType pt : values()) { + if (pt.type.equals(type)) { + return pt; + } + } + throw new IllegalArgumentException("Unknown plugin type: " + type); + } +} diff --git a/core/pom.xml b/core/pom.xml index 7f901f64752..856c829600f 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -168,6 +168,26 @@ nacos-control-plugin ${revision} + + com.alibaba.nacos + nacos-config-plugin + ${revision} + + + com.alibaba.nacos + nacos-encryption-plugin + ${revision} + + + com.alibaba.nacos + nacos-datasource-plugin + ${revision} + + + com.alibaba.nacos + nacos-custom-environment-plugin + ${revision} + org.springframework spring-webmvc diff --git a/core/src/main/java/com/alibaba/nacos/core/controller/v3/PluginControllerV3.java b/core/src/main/java/com/alibaba/nacos/core/controller/v3/PluginControllerV3.java new file mode 100644 index 00000000000..8c54c97f4c0 --- /dev/null +++ b/core/src/main/java/com/alibaba/nacos/core/controller/v3/PluginControllerV3.java @@ -0,0 +1,223 @@ +/* + * Copyright 1999-2025 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.nacos.core.controller.v3; + +import com.alibaba.nacos.api.annotation.NacosApi; +import com.alibaba.nacos.api.exception.api.NacosApiException; +import com.alibaba.nacos.api.model.v2.ErrorCode; +import com.alibaba.nacos.api.model.v2.Result; +import com.alibaba.nacos.auth.annotation.Secured; +import com.alibaba.nacos.common.utils.StringUtils; +import com.alibaba.nacos.core.plugin.UnifiedPluginManager; +import com.alibaba.nacos.core.plugin.model.PluginInfo; +import com.alibaba.nacos.core.plugin.model.vo.PluginDetailVO; +import com.alibaba.nacos.core.plugin.model.vo.PluginInfoVO; +import com.alibaba.nacos.core.plugin.model.form.PluginStatusForm; +import com.alibaba.nacos.core.plugin.model.form.PluginConfigForm; +import com.alibaba.nacos.core.utils.Commons; +import com.alibaba.nacos.plugin.auth.constant.ActionTypes; +import com.alibaba.nacos.plugin.auth.constant.ApiType; +import com.alibaba.nacos.plugin.auth.constant.SignType; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +import static com.alibaba.nacos.core.utils.Commons.NACOS_ADMIN_CORE_CONTEXT_V3; + +/** + * Plugin Management V3 Controller. + * + * @author Nacos + */ +@NacosApi +@RestController +@RequestMapping(NACOS_ADMIN_CORE_CONTEXT_V3 + "/plugin") +public class PluginControllerV3 { + + private final UnifiedPluginManager unifiedPluginManager; + + public PluginControllerV3(UnifiedPluginManager unifiedPluginManager) { + this.unifiedPluginManager = unifiedPluginManager; + } + + /** + * Get plugin list. + * + * @param pluginType plugin type filter (optional) + * @return plugin list + */ + @GetMapping("/list") + @Secured(resource = Commons.NACOS_ADMIN_CORE_CONTEXT_V3 + + "/plugin", action = ActionTypes.READ, signType = SignType.CONSOLE, apiType = ApiType.ADMIN_API) + public Result> getPluginList(@RequestParam(value = "pluginType", required = false) String pluginType) { + List plugins = unifiedPluginManager.listAllPlugins(); + + if (StringUtils.isNotBlank(pluginType)) { + plugins = plugins.stream() + .filter(p -> pluginType.equals(p.getPluginType().getType())) + .collect(Collectors.toList()); + } + + List vos = plugins.stream() + .map(this::convertToVO) + .collect(Collectors.toList()); + + return Result.success(vos); + } + + /** + * Get plugin detail. + * + * @param pluginType plugin type + * @param pluginName plugin name + * @return plugin detail + */ + @GetMapping("/detail") + @Secured(resource = Commons.NACOS_ADMIN_CORE_CONTEXT_V3 + + "/plugin", action = ActionTypes.READ, signType = SignType.CONSOLE, apiType = ApiType.ADMIN_API) + public Result getPluginDetail( + @RequestParam("pluginType") String pluginType, + @RequestParam("pluginName") String pluginName) throws NacosApiException { + + String pluginId = pluginType + ":" + pluginName; + Optional pluginInfoOpt = unifiedPluginManager.getPlugin(pluginId); + if (!pluginInfoOpt.isPresent()) { + throw new NacosApiException(HttpStatus.NOT_FOUND.value(), ErrorCode.RESOURCE_NOT_FOUND, + "Plugin not found: " + pluginId); + } + + PluginDetailVO detailVO = convertToDetailVO(pluginInfoOpt.get()); + return Result.success(detailVO); + } + + /** + * Enable or disable plugin. + * + * @param form plugin status form + * @return success result + */ + @PutMapping("/status") + @Secured(resource = Commons.NACOS_ADMIN_CORE_CONTEXT_V3 + + "/plugin", action = ActionTypes.WRITE, signType = SignType.CONSOLE, apiType = ApiType.ADMIN_API) + public Result updatePluginStatus(@RequestBody PluginStatusForm form) throws NacosApiException { + if (StringUtils.isBlank(form.getPluginType()) || StringUtils.isBlank(form.getPluginName())) { + throw new NacosApiException(HttpStatus.BAD_REQUEST.value(), ErrorCode.PARAMETER_VALIDATE_ERROR, + "Plugin type and name are required"); + } + + try { + String pluginId = form.getPluginType() + ":" + form.getPluginName(); + unifiedPluginManager.setPluginEnabled(pluginId, form.isEnabled()); + return Result.success("Plugin status updated successfully"); + } catch (Exception e) { + throw new NacosApiException(HttpStatus.INTERNAL_SERVER_ERROR.value(), ErrorCode.SERVER_ERROR, + "Failed to update plugin status: " + e.getMessage()); + } + } + + /** + * Update plugin configuration. + * + * @param form plugin config form + * @return success result + */ + @PutMapping("/config") + @Secured(resource = Commons.NACOS_ADMIN_CORE_CONTEXT_V3 + + "/plugin", action = ActionTypes.WRITE, signType = SignType.CONSOLE, apiType = ApiType.ADMIN_API) + public Result updatePluginConfig(@RequestBody PluginConfigForm form) throws NacosApiException { + if (StringUtils.isBlank(form.getPluginType()) || StringUtils.isBlank(form.getPluginName())) { + throw new NacosApiException(HttpStatus.BAD_REQUEST.value(), ErrorCode.PARAMETER_VALIDATE_ERROR, + "Plugin type and name are required"); + } + + if (form.getConfig() == null) { + throw new NacosApiException(HttpStatus.BAD_REQUEST.value(), ErrorCode.PARAMETER_VALIDATE_ERROR, + "Plugin configuration is required"); + } + + try { + String pluginId = form.getPluginType() + ":" + form.getPluginName(); + unifiedPluginManager.updatePluginConfig(pluginId, form.getConfig()); + return Result.success("Plugin configuration updated successfully"); + } catch (Exception e) { + throw new NacosApiException(HttpStatus.INTERNAL_SERVER_ERROR.value(), ErrorCode.SERVER_ERROR, + "Failed to update plugin configuration: " + e.getMessage()); + } + } + + /** + * Get plugin availability across cluster nodes. + * + * @param pluginType plugin type + * @param pluginName plugin name + * @return node availability map + */ + @GetMapping("/availability") + @Secured(resource = Commons.NACOS_ADMIN_CORE_CONTEXT_V3 + + "/plugin", action = ActionTypes.READ, signType = SignType.CONSOLE, apiType = ApiType.ADMIN_API) + public Result> getPluginAvailability( + @RequestParam("pluginType") String pluginType, + @RequestParam("pluginName") String pluginName) throws NacosApiException { + + String pluginId = pluginType + ":" + pluginName; + Optional pluginInfoOpt = unifiedPluginManager.getPlugin(pluginId); + if (!pluginInfoOpt.isPresent()) { + throw new NacosApiException(HttpStatus.NOT_FOUND.value(), ErrorCode.RESOURCE_NOT_FOUND, + "Plugin not found: " + pluginId); + } + + return Result.success(pluginInfoOpt.get().getNodeAvailability()); + } + + private PluginInfoVO convertToVO(PluginInfo pluginInfo) { + PluginInfoVO vo = new PluginInfoVO(); + vo.setPluginId(pluginInfo.getPluginId()); + vo.setPluginType(pluginInfo.getPluginType().getType()); + vo.setPluginName(pluginInfo.getPluginName()); + vo.setEnabled(pluginInfo.isEnabled()); + vo.setCritical(pluginInfo.isCritical()); + vo.setConfigurable(pluginInfo.isConfigurable()); + vo.setAvailableNodeCount(pluginInfo.getAvailableNodeCount()); + vo.setTotalNodeCount(pluginInfo.getTotalNodeCount()); + return vo; + } + + private PluginDetailVO convertToDetailVO(PluginInfo pluginInfo) { + PluginDetailVO vo = new PluginDetailVO(); + vo.setPluginId(pluginInfo.getPluginId()); + vo.setPluginType(pluginInfo.getPluginType().getType()); + vo.setPluginName(pluginInfo.getPluginName()); + vo.setEnabled(pluginInfo.isEnabled()); + vo.setCritical(pluginInfo.isCritical()); + vo.setConfigurable(pluginInfo.isConfigurable()); + vo.setConfig(pluginInfo.getConfig()); + vo.setConfigDefinitions(pluginInfo.getConfigDefinitions()); + vo.setAvailableNodeCount(pluginInfo.getAvailableNodeCount()); + vo.setTotalNodeCount(pluginInfo.getTotalNodeCount()); + vo.setNodeAvailability(pluginInfo.getNodeAvailability()); + return vo; + } +} diff --git a/core/src/main/java/com/alibaba/nacos/core/plugin/CriticalPluginConfig.java b/core/src/main/java/com/alibaba/nacos/core/plugin/CriticalPluginConfig.java new file mode 100644 index 00000000000..7b76b21b9fd --- /dev/null +++ b/core/src/main/java/com/alibaba/nacos/core/plugin/CriticalPluginConfig.java @@ -0,0 +1,67 @@ +/* + * Copyright 1999-2024 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.nacos.core.plugin; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * Critical plugin configuration. + * Defines plugins that cannot be disabled. + * + * @author WangzJi + * @since 3.2.0 + */ +public final class CriticalPluginConfig { + + private static final Set CRITICAL_PLUGINS; + + static { + // Only datasource dialects are critical - Nacos requires at least one database + // backend. + // Auth plugins are NOT critical - users can disable default auth to use custom + // plugins. + Set plugins = new HashSet<>(); + plugins.add("datasource-dialect:mysql"); + plugins.add("datasource-dialect:derby"); + plugins.add("datasource-dialect:postgresql"); + CRITICAL_PLUGINS = Collections.unmodifiableSet(plugins); + } + + private CriticalPluginConfig() { + } + + /** + * Check if a plugin is critical. + * + * @param pluginId plugin ID in format "{type}:{name}" + * @return true if the plugin is critical + */ + public static boolean isCritical(String pluginId) { + return CRITICAL_PLUGINS.contains(pluginId); + } + + /** + * Get all critical plugins. + * + * @return unmodifiable set of critical plugin IDs + */ + public static Set getCriticalPlugins() { + return CRITICAL_PLUGINS; + } +} diff --git a/core/src/main/java/com/alibaba/nacos/core/plugin/UnifiedPluginManager.java b/core/src/main/java/com/alibaba/nacos/core/plugin/UnifiedPluginManager.java new file mode 100644 index 00000000000..031f7621a69 --- /dev/null +++ b/core/src/main/java/com/alibaba/nacos/core/plugin/UnifiedPluginManager.java @@ -0,0 +1,368 @@ +/* + * Copyright 1999-2024 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.nacos.core.plugin; + +import com.alibaba.nacos.api.exception.NacosException; +import com.alibaba.nacos.api.exception.api.NacosApiException; +import com.alibaba.nacos.api.model.v2.ErrorCode; +import com.alibaba.nacos.api.plugin.ConfigItemDefinition; +import com.alibaba.nacos.api.plugin.PluginConfigSpec; +import com.alibaba.nacos.api.plugin.PluginProvider; +import com.alibaba.nacos.api.plugin.PluginStateChecker; +import com.alibaba.nacos.api.plugin.PluginStateCheckerHolder; +import com.alibaba.nacos.api.plugin.PluginType; +import com.alibaba.nacos.common.spi.NacosServiceLoader; +import com.alibaba.nacos.common.utils.StringUtils; +import com.alibaba.nacos.sys.env.EnvUtil; +import com.alibaba.nacos.core.plugin.model.PluginInfo; +import com.alibaba.nacos.core.plugin.storage.PluginStatePersistenceService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Unified Plugin Manager. + * Central manager for all plugin types, implementing plugin state checking and management. + * + * @author WangzJi + * @since 3.2.0 + */ +@Component +public class UnifiedPluginManager implements PluginStateChecker, ApplicationListener { + + private static final Logger LOGGER = LoggerFactory.getLogger(UnifiedPluginManager.class); + + /** + * Configuration property for auth plugin type. + */ + private static final String AUTH_TYPE_PROPERTY = "nacos.core.auth.system.type"; + + /** + * Default auth plugin type. + */ + private static final String AUTH_TYPE_DEFAULT = "nacos"; + + /** + * Configuration property for datasource platform (new). + */ + private static final String DATASOURCE_PLATFORM_PROPERTY = "spring.sql.init.platform"; + + /** + * Configuration property for datasource platform (legacy). + */ + private static final String DATASOURCE_PLATFORM_PROPERTY_OLD = "spring.datasource.platform"; + + /** + * Default datasource platform. + */ + private static final String DATASOURCE_PLATFORM_DEFAULT = "mysql"; + + /** + * Plugin registry: pluginId -> PluginInfo. + */ + private final Map pluginRegistry = new ConcurrentHashMap<>(); + + /** + * Plugin states: pluginId -> enabled. + */ + private final Map pluginStates = new ConcurrentHashMap<>(); + + /** + * Plugin configurations: pluginId -> config. + */ + private final Map> pluginConfigs = new ConcurrentHashMap<>(); + + /** + * Plugin instances: pluginId -> instance. + */ + private final Map pluginInstances = new ConcurrentHashMap<>(); + + private final PluginStatePersistenceService persistence; + + public UnifiedPluginManager(PluginStatePersistenceService persistence) { + this.persistence = persistence; + } + + @Override + public void onApplicationEvent(ApplicationReadyEvent event) { + // Register to static holder + PluginStateCheckerHolder.setInstance(this); + + // Discover all plugins + discoverAllPlugins(); + + // Load persisted states and configs + loadPersistedData(); + + LOGGER.info("[UnifiedPluginManager] Initialized, {} plugins discovered", pluginRegistry.size()); + } + + @Override + public boolean isPluginEnabled(String pluginType, String pluginName) { + String pluginId = pluginType + ":" + pluginName; + return pluginStates.getOrDefault(pluginId, true); + } + + /** + * Set plugin enabled/disabled state. + * + * @param pluginId plugin ID + * @param enabled whether to enable + * @throws NacosApiException if plugin not found or is critical + */ + public void setPluginEnabled(String pluginId, boolean enabled) throws NacosApiException { + PluginInfo info = pluginRegistry.get(pluginId); + if (info == null) { + throw new NacosApiException(NacosException.NOT_FOUND, ErrorCode.RESOURCE_NOT_FOUND, + "Plugin not found: " + pluginId); + } + + // Critical plugins cannot be disabled + if (info.isCritical() && !enabled) { + throw new NacosApiException(NacosException.INVALID_PARAM, ErrorCode.PARAMETER_VALIDATE_ERROR, + "Cannot disable critical plugin: " + pluginId); + } + + pluginStates.put(pluginId, enabled); + info.setEnabled(enabled); + persistence.saveState(pluginId, enabled); + + LOGGER.info("[UnifiedPluginManager] Plugin {} status changed to {}", pluginId, + enabled ? "enabled" : "disabled"); + } + + /** + * Update plugin configuration. + * + * @param pluginId plugin ID + * @param config configuration + * @throws NacosApiException if plugin not found or not configurable + */ + public void updatePluginConfig(String pluginId, Map config) throws NacosApiException { + PluginInfo info = pluginRegistry.get(pluginId); + if (info == null) { + throw new NacosApiException(NacosException.NOT_FOUND, ErrorCode.RESOURCE_NOT_FOUND, + "Plugin not found: " + pluginId); + } + + if (!info.isConfigurable()) { + throw new NacosApiException(NacosException.INVALID_PARAM, ErrorCode.PARAMETER_VALIDATE_ERROR, + "Plugin does not support configuration: " + pluginId); + } + + // Validate config + validateConfig(info, config); + + // Apply config to plugin instance + applyConfigToPlugin(pluginId, config); + + // Save config + pluginConfigs.put(pluginId, new HashMap<>(config)); + info.setConfig(config); + persistence.saveConfig(pluginId, config); + + LOGGER.info("[UnifiedPluginManager] Plugin {} config updated", pluginId); + } + + /** + * List all plugins. + * + * @return list of plugin info + */ + public List listAllPlugins() { + return new ArrayList<>(pluginRegistry.values()); + } + + /** + * Get plugin by ID. + * + * @param pluginId plugin ID + * @return optional plugin info + */ + public Optional getPlugin(String pluginId) { + return Optional.ofNullable(pluginRegistry.get(pluginId)); + } + + /** + * Get local plugin IDs. + * + * @return set of plugin IDs + */ + public Set getLocalPluginIds() { + return new HashSet<>(pluginRegistry.keySet()); + } + + /** + * Discover all plugins using SPI-based PluginProvider mechanism. + * No hard-coded plugin type discovery needed. + */ + @SuppressWarnings("rawtypes") + private void discoverAllPlugins() { + Collection providers = NacosServiceLoader.load(PluginProvider.class); + + for (PluginProvider provider : providers) { + try { + discoverPluginsFromProvider(provider); + } catch (Exception e) { + LOGGER.warn("[UnifiedPluginManager] Failed to discover plugins from provider: {}", + provider.getClass().getName(), e); + } + } + } + + /** + * Discover plugins from a single provider. + * + * @param provider the plugin provider + */ + private void discoverPluginsFromProvider(PluginProvider provider) { + PluginType pluginType = provider.getPluginType(); + Map plugins = provider.getAllPlugins(); + + if (plugins == null || plugins.isEmpty()) { + LOGGER.info("[UnifiedPluginManager] No plugins found for type: {}", pluginType.getType()); + return; + } + + plugins.forEach((name, instance) -> registerPlugin(pluginType, name, instance)); + LOGGER.info("[UnifiedPluginManager] Discovered {} {} plugins", plugins.size(), pluginType.getType()); + } + + private void registerPlugin(PluginType type, String name, Object instance) { + String pluginId = type.getType() + ":" + name; + + PluginInfo info = new PluginInfo(); + info.setPluginId(pluginId); + info.setPluginName(name); + info.setPluginType(type); + info.setClassName(instance.getClass().getName()); + info.setCritical(CriticalPluginConfig.isCritical(pluginId)); + info.setLoadTimestamp(System.currentTimeMillis()); + boolean defaultEnabled = calculateDefaultEnabled(type, name); + info.setEnabled(defaultEnabled); + + // Check if plugin supports configuration + if (instance instanceof PluginConfigSpec) { + PluginConfigSpec configSpec = (PluginConfigSpec) instance; + info.setConfigurable(true); + info.setConfigDefinitions(configSpec.getConfigDefinitions()); + info.setConfig(configSpec.getCurrentConfig()); + } + + pluginRegistry.put(pluginId, info); + pluginInstances.put(pluginId, instance); + pluginStates.put(pluginId, defaultEnabled); + + LOGGER.debug("[UnifiedPluginManager] Registered plugin {} with default enabled={}", pluginId, defaultEnabled); + } + + private void loadPersistedData() { + // Load states + Map states = persistence.loadAllStates(); + states.forEach((pluginId, enabled) -> { + if (pluginRegistry.containsKey(pluginId)) { + pluginStates.put(pluginId, enabled); + pluginRegistry.get(pluginId).setEnabled(enabled); + } + }); + + // Load configs + Map> configs = persistence.loadAllConfigs(); + configs.forEach((pluginId, config) -> { + if (pluginRegistry.containsKey(pluginId)) { + pluginConfigs.put(pluginId, config); + pluginRegistry.get(pluginId).setConfig(config); + applyConfigToPlugin(pluginId, config); + } + }); + } + + private void validateConfig(PluginInfo info, Map config) throws NacosApiException { + List definitions = info.getConfigDefinitions(); + if (definitions == null) { + return; + } + + for (ConfigItemDefinition def : definitions) { + String value = config.get(def.getKey()); + boolean valueIsEmpty = value == null || value.isEmpty(); + if (def.isRequired() && valueIsEmpty) { + throw new NacosApiException(NacosException.INVALID_PARAM, ErrorCode.PARAMETER_MISSING, + "Required config missing: " + def.getKey()); + } + } + } + + private void applyConfigToPlugin(String pluginId, Map config) { + Object instance = pluginInstances.get(pluginId); + if (instance instanceof PluginConfigSpec) { + try { + ((PluginConfigSpec) instance).applyConfig(config); + } catch (Exception e) { + LOGGER.warn("[UnifiedPluginManager] Failed to apply config to plugin {}", pluginId, e); + } + } + } + + /** + * Calculate the default enabled status for a plugin based on its type and configuration. + * For exclusive plugins (AUTH, DATASOURCE), only the configured one is enabled by default. + * For non-exclusive plugins, all are enabled by default. + * + * @param type plugin type + * @param pluginName plugin name + * @return default enabled status + */ + private boolean calculateDefaultEnabled(PluginType type, String pluginName) { + switch (type) { + case AUTH: + String authType = EnvUtil.getProperty(AUTH_TYPE_PROPERTY, AUTH_TYPE_DEFAULT); + return pluginName.equalsIgnoreCase(authType); + case DATASOURCE_DIALECT: + String platform = getDatasourcePlatform(); + return pluginName.equalsIgnoreCase(platform); + default: + // Non-exclusive plugins are enabled by default + return true; + } + } + + /** + * Get the configured datasource platform. + * + * @return datasource platform name + */ + private String getDatasourcePlatform() { + String platform = EnvUtil.getProperty(DATASOURCE_PLATFORM_PROPERTY); + if (StringUtils.isBlank(platform)) { + platform = EnvUtil.getProperty(DATASOURCE_PLATFORM_PROPERTY_OLD); + } + return StringUtils.isBlank(platform) ? DATASOURCE_PLATFORM_DEFAULT : platform; + } +} diff --git a/core/src/main/java/com/alibaba/nacos/core/plugin/model/PluginInfo.java b/core/src/main/java/com/alibaba/nacos/core/plugin/model/PluginInfo.java new file mode 100644 index 00000000000..efca3c760c5 --- /dev/null +++ b/core/src/main/java/com/alibaba/nacos/core/plugin/model/PluginInfo.java @@ -0,0 +1,220 @@ +/* + * Copyright 1999-2024 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.nacos.core.plugin.model; + +import com.alibaba.nacos.api.plugin.ConfigItemDefinition; +import com.alibaba.nacos.api.plugin.PluginType; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; + +/** + * Plugin information model. + * + * @author WangzJi + * @since 3.2.0 + */ +public class PluginInfo implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * Plugin ID, format: "{type}:{name}". + */ + private String pluginId; + + /** + * Plugin name. + */ + private String pluginName; + + /** + * Plugin type. + */ + private PluginType pluginType; + + /** + * Plugin class name. + */ + private String className; + + /** + * Plugin description. + */ + private String description; + + /** + * Whether the plugin is enabled. + */ + private boolean enabled; + + /** + * Whether this is a critical plugin (cannot be disabled). + */ + private boolean critical; + + /** + * Whether the plugin supports configuration. + */ + private boolean configurable; + + /** + * Plugin load timestamp. + */ + private long loadTimestamp; + + /** + * Current configuration. + */ + private Map config; + + /** + * Configuration item definitions. + */ + private List configDefinitions; + + /** + * Number of nodes where plugin is available. + */ + private int availableNodeCount; + + /** + * Total number of nodes in cluster. + */ + private int totalNodeCount; + + /** + * Per-node availability: nodeIp -> available. + */ + private Map nodeAvailability; + + public PluginInfo() { + } + + public String getPluginId() { + return pluginId; + } + + public void setPluginId(String pluginId) { + this.pluginId = pluginId; + } + + public String getPluginName() { + return pluginName; + } + + public void setPluginName(String pluginName) { + this.pluginName = pluginName; + } + + public PluginType getPluginType() { + return pluginType; + } + + public void setPluginType(PluginType pluginType) { + this.pluginType = pluginType; + } + + public String getClassName() { + return className; + } + + public void setClassName(String className) { + this.className = className; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public boolean isCritical() { + return critical; + } + + public void setCritical(boolean critical) { + this.critical = critical; + } + + public boolean isConfigurable() { + return configurable; + } + + public void setConfigurable(boolean configurable) { + this.configurable = configurable; + } + + public long getLoadTimestamp() { + return loadTimestamp; + } + + public void setLoadTimestamp(long loadTimestamp) { + this.loadTimestamp = loadTimestamp; + } + + public Map getConfig() { + return config; + } + + public void setConfig(Map config) { + this.config = config; + } + + public List getConfigDefinitions() { + return configDefinitions; + } + + public void setConfigDefinitions(List configDefinitions) { + this.configDefinitions = configDefinitions; + } + + public int getAvailableNodeCount() { + return availableNodeCount; + } + + public void setAvailableNodeCount(int availableNodeCount) { + this.availableNodeCount = availableNodeCount; + } + + public int getTotalNodeCount() { + return totalNodeCount; + } + + public void setTotalNodeCount(int totalNodeCount) { + this.totalNodeCount = totalNodeCount; + } + + public Map getNodeAvailability() { + return nodeAvailability; + } + + public void setNodeAvailability(Map nodeAvailability) { + this.nodeAvailability = nodeAvailability; + } +} diff --git a/core/src/main/java/com/alibaba/nacos/core/plugin/model/form/PluginConfigForm.java b/core/src/main/java/com/alibaba/nacos/core/plugin/model/form/PluginConfigForm.java new file mode 100644 index 00000000000..a21b6bea492 --- /dev/null +++ b/core/src/main/java/com/alibaba/nacos/core/plugin/model/form/PluginConfigForm.java @@ -0,0 +1,57 @@ +/* + * Copyright 1999-2025 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.nacos.core.plugin.model.form; + +import java.util.Map; + +/** + * Plugin Configuration Update Form. + * + * @author Nacos + */ +public class PluginConfigForm { + + private String pluginType; + + private String pluginName; + + private Map config; + + public String getPluginType() { + return pluginType; + } + + public void setPluginType(String pluginType) { + this.pluginType = pluginType; + } + + public String getPluginName() { + return pluginName; + } + + public void setPluginName(String pluginName) { + this.pluginName = pluginName; + } + + public Map getConfig() { + return config; + } + + public void setConfig(Map config) { + this.config = config; + } +} diff --git a/core/src/main/java/com/alibaba/nacos/core/plugin/model/form/PluginStatusForm.java b/core/src/main/java/com/alibaba/nacos/core/plugin/model/form/PluginStatusForm.java new file mode 100644 index 00000000000..32e50a17c84 --- /dev/null +++ b/core/src/main/java/com/alibaba/nacos/core/plugin/model/form/PluginStatusForm.java @@ -0,0 +1,55 @@ +/* + * Copyright 1999-2025 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.nacos.core.plugin.model.form; + +/** + * Plugin Status Update Form. + * + * @author Nacos + */ +public class PluginStatusForm { + + private String pluginType; + + private String pluginName; + + private boolean enabled; + + public String getPluginType() { + return pluginType; + } + + public void setPluginType(String pluginType) { + this.pluginType = pluginType; + } + + public String getPluginName() { + return pluginName; + } + + public void setPluginName(String pluginName) { + this.pluginName = pluginName; + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } +} diff --git a/core/src/main/java/com/alibaba/nacos/core/plugin/model/vo/PluginDetailVO.java b/core/src/main/java/com/alibaba/nacos/core/plugin/model/vo/PluginDetailVO.java new file mode 100644 index 00000000000..822cd7a3aa5 --- /dev/null +++ b/core/src/main/java/com/alibaba/nacos/core/plugin/model/vo/PluginDetailVO.java @@ -0,0 +1,149 @@ +/* + * Copyright 1999-2025 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.nacos.core.plugin.model.vo; + +import com.alibaba.nacos.api.plugin.ConfigItemDefinition; + +import java.util.List; +import java.util.Map; + +/** + * Plugin Detail VO. + * + * @author Nacos + */ +public class PluginDetailVO { + + private String pluginId; + + private String pluginType; + + private String pluginName; + + private Boolean enabled; + + private Boolean critical; + + private Boolean configurable; + + private Map config; + + private List configDefinitions; + + private Integer availableNodeCount; + + private Integer totalNodeCount; + + private Map nodeAvailability; + + public String getPluginId() { + return pluginId; + } + + public void setPluginId(String pluginId) { + this.pluginId = pluginId; + } + + public String getPluginType() { + return pluginType; + } + + public void setPluginType(String pluginType) { + this.pluginType = pluginType; + } + + public String getPluginName() { + return pluginName; + } + + public void setPluginName(String pluginName) { + this.pluginName = pluginName; + } + + public Boolean getEnabled() { + return enabled; + } + + public void setEnabled(Boolean enabled) { + this.enabled = enabled; + } + + public Boolean getCritical() { + return critical; + } + + public void setCritical(Boolean critical) { + this.critical = critical; + } + + public Boolean getConfigurable() { + return configurable; + } + + public void setConfigurable(Boolean configurable) { + this.configurable = configurable; + } + + public Map getConfig() { + return config; + } + + public void setConfig(Map config) { + this.config = config; + } + + public List getConfigDefinitions() { + return configDefinitions; + } + + public void setConfigDefinitions(List configDefinitions) { + this.configDefinitions = configDefinitions; + } + + public Integer getAvailableNodeCount() { + return availableNodeCount; + } + + public void setAvailableNodeCount(Integer availableNodeCount) { + this.availableNodeCount = availableNodeCount; + } + + public Integer getTotalNodeCount() { + return totalNodeCount; + } + + public void setTotalNodeCount(Integer totalNodeCount) { + this.totalNodeCount = totalNodeCount; + } + + public Map getNodeAvailability() { + return nodeAvailability; + } + + public void setNodeAvailability(Map nodeAvailability) { + this.nodeAvailability = nodeAvailability; + } + + @Override + public String toString() { + return "PluginDetailVO{" + "pluginId='" + pluginId + '\'' + ", pluginType='" + pluginType + '\'' + + ", pluginName='" + pluginName + '\'' + ", enabled=" + enabled + ", critical=" + critical + + ", configurable=" + configurable + ", config=" + config + ", configDefinitions=" + configDefinitions + + ", availableNodeCount=" + availableNodeCount + ", totalNodeCount=" + totalNodeCount + + ", nodeAvailability=" + nodeAvailability + '}'; + } +} diff --git a/core/src/main/java/com/alibaba/nacos/core/plugin/model/vo/PluginInfoVO.java b/core/src/main/java/com/alibaba/nacos/core/plugin/model/vo/PluginInfoVO.java new file mode 100644 index 00000000000..99cfb10e170 --- /dev/null +++ b/core/src/main/java/com/alibaba/nacos/core/plugin/model/vo/PluginInfoVO.java @@ -0,0 +1,113 @@ +/* + * Copyright 1999-2025 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.nacos.core.plugin.model.vo; + +/** + * Plugin Info VO for list display. + * + * @author Nacos + */ +public class PluginInfoVO { + + private String pluginId; + + private String pluginType; + + private String pluginName; + + private Boolean enabled; + + private Boolean critical; + + private Boolean configurable; + + private Integer availableNodeCount; + + private Integer totalNodeCount; + + public String getPluginId() { + return pluginId; + } + + public void setPluginId(String pluginId) { + this.pluginId = pluginId; + } + + public String getPluginType() { + return pluginType; + } + + public void setPluginType(String pluginType) { + this.pluginType = pluginType; + } + + public String getPluginName() { + return pluginName; + } + + public void setPluginName(String pluginName) { + this.pluginName = pluginName; + } + + public Boolean getEnabled() { + return enabled; + } + + public void setEnabled(Boolean enabled) { + this.enabled = enabled; + } + + public Boolean getCritical() { + return critical; + } + + public void setCritical(Boolean critical) { + this.critical = critical; + } + + public Boolean getConfigurable() { + return configurable; + } + + public void setConfigurable(Boolean configurable) { + this.configurable = configurable; + } + + public Integer getAvailableNodeCount() { + return availableNodeCount; + } + + public void setAvailableNodeCount(Integer availableNodeCount) { + this.availableNodeCount = availableNodeCount; + } + + public Integer getTotalNodeCount() { + return totalNodeCount; + } + + public void setTotalNodeCount(Integer totalNodeCount) { + this.totalNodeCount = totalNodeCount; + } + + @Override + public String toString() { + return "PluginInfoVO{" + "pluginId='" + pluginId + '\'' + ", pluginType='" + pluginType + '\'' + + ", pluginName='" + pluginName + '\'' + ", enabled=" + enabled + ", critical=" + critical + + ", configurable=" + configurable + ", availableNodeCount=" + availableNodeCount + ", totalNodeCount=" + + totalNodeCount + '}'; + } +} diff --git a/core/src/main/java/com/alibaba/nacos/core/plugin/storage/FilePluginStatePersistenceImpl.java b/core/src/main/java/com/alibaba/nacos/core/plugin/storage/FilePluginStatePersistenceImpl.java new file mode 100644 index 00000000000..668ba931011 --- /dev/null +++ b/core/src/main/java/com/alibaba/nacos/core/plugin/storage/FilePluginStatePersistenceImpl.java @@ -0,0 +1,211 @@ +/* + * Copyright 1999-2024 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.nacos.core.plugin.storage; + +import com.alibaba.nacos.common.utils.JacksonUtils; +import com.alibaba.nacos.common.utils.StringUtils; +import com.alibaba.nacos.sys.env.EnvUtil; +import com.fasterxml.jackson.core.type.TypeReference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; + +/** + * File-based implementation of plugin state persistence service. + * Stores plugin states and configurations as JSON files in {NACOS_HOME}/data/plugin/. + * + * @author WangzJi + * @since 3.2.0 + */ +@Component +public class FilePluginStatePersistenceImpl implements PluginStatePersistenceService { + + private static final Logger LOGGER = LoggerFactory.getLogger(FilePluginStatePersistenceImpl.class); + + private static final String PLUGIN_STATE_FILE = "plugin-states.json"; + + private static final String PLUGIN_CONFIG_FILE = "plugin-configs.json"; + + private static final String PLUGIN_DATA_DIR = "plugin"; + + private final String dataDir; + + private final Object stateLock = new Object(); + + private final Object configLock = new Object(); + + public FilePluginStatePersistenceImpl() { + this.dataDir = EnvUtil.getNacosHome() + File.separator + "data" + File.separator + PLUGIN_DATA_DIR; + ensureDataDirExists(); + } + + private void ensureDataDirExists() { + File dir = new File(dataDir); + if (!dir.exists()) { + boolean created = dir.mkdirs(); + if (created) { + LOGGER.info("[FilePluginStatePersistenceImpl] Created plugin data directory: {}", dataDir); + } + } + } + + /** + * Save plugin state. + * + * @param pluginId plugin ID + * @param enabled whether the plugin is enabled + */ + @Override + public void saveState(String pluginId, boolean enabled) { + synchronized (stateLock) { + try { + Map states = loadAllStates(); + states.put(pluginId, enabled); + writeJsonToFile(PLUGIN_STATE_FILE, states, stateLock); + LOGGER.debug("[FilePluginStatePersistenceImpl] Saved plugin state: {}={}", pluginId, enabled); + } catch (Exception e) { + LOGGER.error("[FilePluginStatePersistenceImpl] Failed to save plugin state for {}", pluginId, e); + } + } + } + + /** + * Save plugin configuration. + * + * @param pluginId plugin ID + * @param config configuration key-value pairs + */ + @Override + public void saveConfig(String pluginId, Map config) { + synchronized (configLock) { + try { + Map> configs = loadAllConfigs(); + configs.put(pluginId, config); + writeJsonToFile(PLUGIN_CONFIG_FILE, configs, configLock); + LOGGER.debug("[FilePluginStatePersistenceImpl] Saved plugin config for {}", pluginId); + } catch (Exception e) { + LOGGER.error("[FilePluginStatePersistenceImpl] Failed to save plugin config for {}", pluginId, e); + } + } + } + + /** + * Load all plugin states. + * + * @return map of plugin ID to enabled state + */ + @Override + public Map loadAllStates() { + synchronized (stateLock) { + return readJsonFromFile(PLUGIN_STATE_FILE, new TypeReference>() { + }); + } + } + + /** + * Load all plugin configurations. + * + * @return map of plugin ID to configuration + */ + @Override + public Map> loadAllConfigs() { + synchronized (configLock) { + return readJsonFromFile(PLUGIN_CONFIG_FILE, new TypeReference>>() { + }); + } + } + + /** + * Delete plugin state. + * + * @param pluginId plugin ID + */ + @Override + public void deleteState(String pluginId) { + synchronized (stateLock) { + try { + Map states = loadAllStates(); + states.remove(pluginId); + writeJsonToFile(PLUGIN_STATE_FILE, states, stateLock); + LOGGER.debug("[FilePluginStatePersistenceImpl] Deleted plugin state for {}", pluginId); + } catch (Exception e) { + LOGGER.error("[FilePluginStatePersistenceImpl] Failed to delete plugin state for {}", pluginId, e); + } + } + } + + /** + * Delete plugin configuration. + * + * @param pluginId plugin ID + */ + @Override + public void deleteConfig(String pluginId) { + synchronized (configLock) { + try { + Map> configs = loadAllConfigs(); + configs.remove(pluginId); + writeJsonToFile(PLUGIN_CONFIG_FILE, configs, configLock); + LOGGER.debug("[FilePluginStatePersistenceImpl] Deleted plugin config for {}", pluginId); + } catch (Exception e) { + LOGGER.error("[FilePluginStatePersistenceImpl] Failed to delete plugin config for {}", pluginId, e); + } + } + } + + private T readJsonFromFile(String fileName, TypeReference typeRef) { + Path filePath = Paths.get(dataDir, fileName); + if (!Files.exists(filePath)) { + return createEmptyMap(typeRef); + } + + try { + String content = new String(Files.readAllBytes(filePath), StandardCharsets.UTF_8); + if (StringUtils.isBlank(content)) { + return createEmptyMap(typeRef); + } + return JacksonUtils.toObj(content, typeRef); + } catch (Exception e) { + LOGGER.error("[FilePluginStatePersistenceImpl] Failed to read file: {}", fileName, e); + return createEmptyMap(typeRef); + } + } + + @SuppressWarnings("unchecked") + private T createEmptyMap(TypeReference typeRef) { + return (T) new HashMap<>(16); + } + + private void writeJsonToFile(String fileName, Object data, Object lock) { + Path filePath = Paths.get(dataDir, fileName); + try { + String content = JacksonUtils.toJson(data); + Files.write(filePath, content.getBytes(StandardCharsets.UTF_8)); + } catch (IOException e) { + LOGGER.error("[FilePluginStatePersistenceImpl] Failed to write file: {}", fileName, e); + } + } +} diff --git a/core/src/main/java/com/alibaba/nacos/core/plugin/storage/PluginStatePersistenceService.java b/core/src/main/java/com/alibaba/nacos/core/plugin/storage/PluginStatePersistenceService.java new file mode 100644 index 00000000000..b71dc1c2fc6 --- /dev/null +++ b/core/src/main/java/com/alibaba/nacos/core/plugin/storage/PluginStatePersistenceService.java @@ -0,0 +1,73 @@ +/* + * Copyright 1999-2024 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.nacos.core.plugin.storage; + +import java.util.Map; + +/** + * Plugin state persistence service interface. + * Provides abstraction for persisting plugin states and configurations. + * + * @author WangzJi + * @since 3.2.0 + */ +public interface PluginStatePersistenceService { + + /** + * Load all plugin states. + * + * @return map of plugin ID to enabled state + */ + Map loadAllStates(); + + /** + * Save plugin state. + * + * @param pluginId plugin ID + * @param enabled whether the plugin is enabled + */ + void saveState(String pluginId, boolean enabled); + + /** + * Delete plugin state. + * + * @param pluginId plugin ID + */ + void deleteState(String pluginId); + + /** + * Load all plugin configurations. + * + * @return map of plugin ID to configuration + */ + Map> loadAllConfigs(); + + /** + * Save plugin configuration. + * + * @param pluginId plugin ID + * @param config configuration key-value pairs + */ + void saveConfig(String pluginId, Map config); + + /** + * Delete plugin configuration. + * + * @param pluginId plugin ID + */ + void deleteConfig(String pluginId); +} diff --git a/core/src/test/java/com/alibaba/nacos/core/plugin/CriticalPluginConfigTest.java b/core/src/test/java/com/alibaba/nacos/core/plugin/CriticalPluginConfigTest.java new file mode 100644 index 00000000000..583c87c9ffd --- /dev/null +++ b/core/src/test/java/com/alibaba/nacos/core/plugin/CriticalPluginConfigTest.java @@ -0,0 +1,108 @@ +/* + * Copyright 1999-2024 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.nacos.core.plugin; + +import org.junit.jupiter.api.Test; + +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * {@link CriticalPluginConfig} unit test. + * + * @author WangzJi + */ +class CriticalPluginConfigTest { + + @Test + void isCriticalAuthNacosTest() { + // Auth plugins are NOT critical - users can disable default auth to use custom plugins + assertFalse(CriticalPluginConfig.isCritical("auth:nacos")); + } + + @Test + void isCriticalDatasourceDialectMysqlTest() { + assertTrue(CriticalPluginConfig.isCritical("datasource-dialect:mysql")); + } + + @Test + void isCriticalDatasourceDialectDerbyTest() { + assertTrue(CriticalPluginConfig.isCritical("datasource-dialect:derby")); + } + + @Test + void isCriticalDatasourceDialectPostgresqlTest() { + assertTrue(CriticalPluginConfig.isCritical("datasource-dialect:postgresql")); + } + + @Test + void isCriticalNonCriticalPluginTest() { + assertFalse(CriticalPluginConfig.isCritical("trace:test")); + assertFalse(CriticalPluginConfig.isCritical("config:test")); + assertFalse(CriticalPluginConfig.isCritical("encryption:test")); + } + + @Test + void isCriticalNonExistentPluginTest() { + assertFalse(CriticalPluginConfig.isCritical("nonexistent:plugin")); + } + + @Test + void isCriticalNullPluginIdTest() { + assertFalse(CriticalPluginConfig.isCritical(null)); + } + + @Test + void isCriticalEmptyPluginIdTest() { + assertFalse(CriticalPluginConfig.isCritical("")); + } + + @Test + void getCriticalPluginsTest() { + Set criticalPlugins = CriticalPluginConfig.getCriticalPlugins(); + + assertNotNull(criticalPlugins); + assertEquals(3, criticalPlugins.size()); + assertTrue(criticalPlugins.contains("datasource-dialect:mysql")); + assertTrue(criticalPlugins.contains("datasource-dialect:derby")); + assertTrue(criticalPlugins.contains("datasource-dialect:postgresql")); + } + + @Test + void getCriticalPluginsIsUnmodifiableTest() { + Set criticalPlugins = CriticalPluginConfig.getCriticalPlugins(); + + try { + criticalPlugins.add("test:plugin"); + assertTrue(false, "Expected UnsupportedOperationException"); + } catch (UnsupportedOperationException e) { + assertTrue(true); + } + } + + @Test + void isCriticalCaseSensitiveTest() { + // Critical plugin check is case-sensitive + assertTrue(CriticalPluginConfig.isCritical("datasource-dialect:mysql")); + assertFalse(CriticalPluginConfig.isCritical("DATASOURCE-DIALECT:MYSQL")); + assertFalse(CriticalPluginConfig.isCritical("Datasource-Dialect:Mysql")); + } +} diff --git a/core/src/test/java/com/alibaba/nacos/core/plugin/UnifiedPluginManagerTest.java b/core/src/test/java/com/alibaba/nacos/core/plugin/UnifiedPluginManagerTest.java new file mode 100644 index 00000000000..41ba7c7b5cd --- /dev/null +++ b/core/src/test/java/com/alibaba/nacos/core/plugin/UnifiedPluginManagerTest.java @@ -0,0 +1,446 @@ +/* + * Copyright 1999-2024 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.nacos.core.plugin; + +import com.alibaba.nacos.api.exception.NacosException; +import com.alibaba.nacos.api.exception.api.NacosApiException; +import com.alibaba.nacos.api.model.v2.ErrorCode; +import com.alibaba.nacos.api.plugin.ConfigItemDefinition; +import com.alibaba.nacos.api.plugin.PluginConfigSpec; +import com.alibaba.nacos.api.plugin.PluginType; +import com.alibaba.nacos.core.plugin.model.PluginInfo; +import com.alibaba.nacos.core.plugin.storage.PluginStatePersistenceService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * {@link UnifiedPluginManager} unit test. + * + * @author WangzJi + */ +@ExtendWith(MockitoExtension.class) +class UnifiedPluginManagerTest { + + @Mock + private PluginStatePersistenceService persistence; + + @Mock + private ApplicationReadyEvent applicationReadyEvent; + + private UnifiedPluginManager manager; + + @BeforeEach + void setUp() { + lenient().when(persistence.loadAllStates()).thenReturn(new HashMap<>()); + lenient().when(persistence.loadAllConfigs()).thenReturn(new HashMap<>()); + lenient().doNothing().when(persistence).saveState(any(), anyBoolean()); + lenient().doNothing().when(persistence).saveConfig(any(), anyMap()); + + manager = new UnifiedPluginManager(persistence); + } + + @Test + void isPluginEnabledDefaultValueTest() { + boolean enabled = manager.isPluginEnabled("auth", "test"); + assertTrue(enabled); + } + + @Test + void isPluginEnabledExistingPluginTest() throws NacosApiException { + registerTestPlugin("trace", "test", false, false, false); + + manager.setPluginEnabled("trace:test", false); + + boolean enabled = manager.isPluginEnabled("trace", "test"); + assertFalse(enabled); + } + + @Test + void setPluginEnabledNonCriticalPluginTest() throws NacosApiException { + registerTestPlugin("trace", "test", false, false, false); + + manager.setPluginEnabled("trace:test", false); + + assertFalse(manager.isPluginEnabled("trace", "test")); + verify(persistence, times(1)).saveState("trace:test", false); + } + + @Test + void setPluginEnabledPluginNotFoundTest() { + NacosApiException exception = assertThrows(NacosApiException.class, () -> { + manager.setPluginEnabled("nonexistent:plugin", false); + }); + + assertEquals(NacosException.NOT_FOUND, exception.getErrCode()); + assertEquals(ErrorCode.RESOURCE_NOT_FOUND.getCode(), exception.getDetailErrCode()); + } + + @Test + void setPluginEnabledDisableCriticalPluginTest() { + registerTestPlugin("auth", "nacos", true, false, false); + + NacosApiException exception = assertThrows(NacosApiException.class, () -> { + manager.setPluginEnabled("auth:nacos", false); + }); + + assertEquals(NacosException.INVALID_PARAM, exception.getErrCode()); + assertEquals(ErrorCode.PARAMETER_VALIDATE_ERROR.getCode(), exception.getDetailErrCode()); + assertTrue(exception.getErrMsg().contains("Cannot disable critical plugin")); + } + + @Test + void setPluginEnabledEnableCriticalPluginTest() throws NacosApiException { + registerTestPlugin("auth", "nacos", true, false, false); + + manager.setPluginEnabled("auth:nacos", true); + + assertTrue(manager.isPluginEnabled("auth", "nacos")); + verify(persistence, times(1)).saveState("auth:nacos", true); + } + + @Test + void updatePluginConfigPluginNotFoundTest() { + Map config = new HashMap<>(); + config.put("key", "value"); + + NacosApiException exception = assertThrows(NacosApiException.class, () -> { + manager.updatePluginConfig("nonexistent:plugin", config); + }); + + assertEquals(NacosException.NOT_FOUND, exception.getErrCode()); + assertEquals(ErrorCode.RESOURCE_NOT_FOUND.getCode(), exception.getDetailErrCode()); + } + + @Test + void updatePluginConfigNotConfigurablePluginTest() { + registerTestPlugin("trace", "test", false, false, false); + + Map config = new HashMap<>(); + config.put("key", "value"); + + NacosApiException exception = assertThrows(NacosApiException.class, () -> { + manager.updatePluginConfig("trace:test", config); + }); + + assertEquals(NacosException.INVALID_PARAM, exception.getErrCode()); + assertEquals(ErrorCode.PARAMETER_VALIDATE_ERROR.getCode(), exception.getDetailErrCode()); + assertTrue(exception.getErrMsg().contains("does not support configuration")); + } + + @Test + void updatePluginConfigMissingRequiredConfigTest() { + TestConfigurablePlugin plugin = new TestConfigurablePlugin(); + ConfigItemDefinition requiredDef = new ConfigItemDefinition(); + requiredDef.setKey("requiredKey"); + requiredDef.setRequired(true); + + List definitions = new ArrayList<>(); + definitions.add(requiredDef); + plugin.setConfigDefinitions(definitions); + + registerConfigurablePlugin("trace", "test", plugin); + + Map config = new HashMap<>(); + config.put("otherKey", "value"); + + NacosApiException exception = assertThrows(NacosApiException.class, () -> { + manager.updatePluginConfig("trace:test", config); + }); + + assertEquals(NacosException.INVALID_PARAM, exception.getErrCode()); + assertEquals(ErrorCode.PARAMETER_MISSING.getCode(), exception.getDetailErrCode()); + assertTrue(exception.getErrMsg().contains("Required config missing")); + } + + @Test + void updatePluginConfigSuccessTest() throws NacosApiException { + TestConfigurablePlugin plugin = new TestConfigurablePlugin(); + ConfigItemDefinition requiredDef = new ConfigItemDefinition(); + requiredDef.setKey("requiredKey"); + requiredDef.setRequired(true); + + List definitions = new ArrayList<>(); + definitions.add(requiredDef); + plugin.setConfigDefinitions(definitions); + + registerConfigurablePlugin("trace", "test", plugin); + + Map config = new HashMap<>(); + config.put("requiredKey", "value"); + + manager.updatePluginConfig("trace:test", config); + + verify(persistence, times(1)).saveConfig(eq("trace:test"), eq(config)); + assertEquals("value", plugin.getCurrentConfig().get("requiredKey")); + } + + @Test + void listAllPluginsTest() { + registerTestPlugin("trace", "test1", false, false, false); + registerTestPlugin("auth", "test2", true, false, false); + + List plugins = manager.listAllPlugins(); + + assertNotNull(plugins); + assertEquals(2, plugins.size()); + } + + @Test + void listAllPluginsEmptyTest() { + List plugins = manager.listAllPlugins(); + + assertNotNull(plugins); + assertEquals(0, plugins.size()); + } + + @Test + void getPluginExistingPluginTest() { + registerTestPlugin("trace", "test", false, false, false); + + Optional plugin = manager.getPlugin("trace:test"); + + assertTrue(plugin.isPresent()); + assertEquals("trace:test", plugin.get().getPluginId()); + assertEquals("test", plugin.get().getPluginName()); + } + + @Test + void getPluginNonExistingPluginTest() { + Optional plugin = manager.getPlugin("nonexistent:plugin"); + + assertFalse(plugin.isPresent()); + } + + @Test + void onApplicationEventTest() { + manager.onApplicationEvent(applicationReadyEvent); + + verify(persistence, times(1)).loadAllStates(); + verify(persistence, times(1)).loadAllConfigs(); + } + + @Test + void loadPersistedStatesTest() { + registerTestPlugin("trace", "test", false, false, false); + + Map states = new HashMap<>(); + states.put("trace:test", false); + when(persistence.loadAllStates()).thenReturn(states); + + manager.onApplicationEvent(applicationReadyEvent); + + assertFalse(manager.isPluginEnabled("trace", "test")); + } + + @Test + void loadPersistedConfigsTest() { + TestConfigurablePlugin plugin = new TestConfigurablePlugin(); + registerConfigurablePlugin("trace", "test", plugin); + + Map config = new HashMap<>(); + config.put("key", "value"); + + Map> configs = new HashMap<>(); + configs.put("trace:test", config); + when(persistence.loadAllConfigs()).thenReturn(configs); + + manager.onApplicationEvent(applicationReadyEvent); + + assertEquals("value", plugin.getCurrentConfig().get("key")); + } + + @Test + void validateConfigOptionalFieldTest() throws NacosApiException { + TestConfigurablePlugin plugin = new TestConfigurablePlugin(); + ConfigItemDefinition optionalDef = new ConfigItemDefinition(); + optionalDef.setKey("optionalKey"); + optionalDef.setRequired(false); + + List definitions = new ArrayList<>(); + definitions.add(optionalDef); + plugin.setConfigDefinitions(definitions); + + registerConfigurablePlugin("trace", "test", plugin); + + Map config = new HashMap<>(); + + manager.updatePluginConfig("trace:test", config); + + verify(persistence, times(1)).saveConfig(eq("trace:test"), eq(config)); + } + + @Test + void validateConfigEmptyValueTest() { + TestConfigurablePlugin plugin = new TestConfigurablePlugin(); + ConfigItemDefinition requiredDef = new ConfigItemDefinition(); + requiredDef.setKey("requiredKey"); + requiredDef.setRequired(true); + + List definitions = new ArrayList<>(); + definitions.add(requiredDef); + plugin.setConfigDefinitions(definitions); + + registerConfigurablePlugin("trace", "test", plugin); + + Map config = new HashMap<>(); + config.put("requiredKey", ""); + + NacosApiException exception = assertThrows(NacosApiException.class, () -> { + manager.updatePluginConfig("trace:test", config); + }); + + assertEquals(ErrorCode.PARAMETER_MISSING.getCode(), exception.getDetailErrCode()); + } + + @Test + void applyConfigToNonConfigurablePluginTest() throws NacosApiException { + Object plainPlugin = new Object(); + registerPluginInstance("trace", "test", plainPlugin, false, false); + + TestConfigurablePlugin configurablePlugin = new TestConfigurablePlugin(); + registerConfigurablePlugin("trace", "configurable", configurablePlugin); + + Map config = new HashMap<>(); + config.put("key", "value"); + + manager.updatePluginConfig("trace:configurable", config); + + assertEquals("value", configurablePlugin.getCurrentConfig().get("key")); + } + + private void registerTestPlugin(String type, String name, boolean critical, boolean configurable, + boolean enabled) { + Object instance = new Object(); + registerPluginInstance(type, name, instance, critical, enabled); + } + + private void registerConfigurablePlugin(String type, String name, TestConfigurablePlugin plugin) { + String pluginId = type + ":" + name; + + PluginInfo info = new PluginInfo(); + info.setPluginId(pluginId); + info.setPluginName(name); + info.setPluginType(PluginType.fromType(type)); + info.setClassName(plugin.getClass().getName()); + info.setCritical(false); + info.setLoadTimestamp(System.currentTimeMillis()); + info.setEnabled(true); + info.setConfigurable(true); + info.setConfigDefinitions(plugin.getConfigDefinitions()); + info.setConfig(plugin.getCurrentConfig()); + + Map registry = getPluginRegistry(); + registry.put(pluginId, info); + + Map instances = getPluginInstances(); + instances.put(pluginId, plugin); + + Map states = getPluginStates(); + states.put(pluginId, true); + } + + private void registerPluginInstance(String type, String name, Object instance, boolean critical, + boolean enabled) { + String pluginId = type + ":" + name; + + PluginInfo info = new PluginInfo(); + info.setPluginId(pluginId); + info.setPluginName(name); + info.setPluginType(PluginType.fromType(type)); + info.setClassName(instance.getClass().getName()); + info.setCritical(critical); + info.setLoadTimestamp(System.currentTimeMillis()); + info.setEnabled(enabled); + info.setConfigurable(false); + + Map registry = getPluginRegistry(); + registry.put(pluginId, info); + + Map instances = getPluginInstances(); + instances.put(pluginId, instance); + + Map states = getPluginStates(); + states.put(pluginId, enabled); + } + + @SuppressWarnings("unchecked") + private Map getPluginRegistry() { + return (Map) ReflectionTestUtils.getField(manager, "pluginRegistry"); + } + + @SuppressWarnings("unchecked") + private Map getPluginInstances() { + return (Map) ReflectionTestUtils.getField(manager, "pluginInstances"); + } + + @SuppressWarnings("unchecked") + private Map getPluginStates() { + return (Map) ReflectionTestUtils.getField(manager, "pluginStates"); + } + + static class TestConfigurablePlugin implements PluginConfigSpec { + + private List configDefinitions = new ArrayList<>(); + + private Map currentConfig = new HashMap<>(); + + @Override + public List getConfigDefinitions() { + return configDefinitions; + } + + public void setConfigDefinitions(List definitions) { + this.configDefinitions = definitions; + } + + @Override + public void applyConfig(Map config) { + this.currentConfig.clear(); + this.currentConfig.putAll(config); + } + + @Override + public Map getCurrentConfig() { + return currentConfig; + } + } +} diff --git a/core/src/test/java/com/alibaba/nacos/core/plugin/storage/FilePluginStatePersistenceImplTest.java b/core/src/test/java/com/alibaba/nacos/core/plugin/storage/FilePluginStatePersistenceImplTest.java new file mode 100644 index 00000000000..c0296deb278 --- /dev/null +++ b/core/src/test/java/com/alibaba/nacos/core/plugin/storage/FilePluginStatePersistenceImplTest.java @@ -0,0 +1,332 @@ +/* + * Copyright 1999-2024 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.nacos.core.plugin.storage; + +import com.alibaba.nacos.sys.env.EnvUtil; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.springframework.mock.env.MockEnvironment; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * {@link FilePluginStatePersistenceImpl} unit test. + * + * @author WangzJi + */ +class FilePluginStatePersistenceImplTest { + + @TempDir + Path tempDir; + + private FilePluginStatePersistenceImpl persistence; + + private Path pluginDataDir; + + @BeforeAll + static void setUpAll() { + MockEnvironment environment = new MockEnvironment(); + EnvUtil.setEnvironment(environment); + } + + @BeforeEach + void setUp() throws IOException { + // Use EnvUtil.setNacosHomePath to set the static cached path directly + // This ensures proper isolation in CI parallel test environments + EnvUtil.setNacosHomePath(tempDir.toString()); + pluginDataDir = Paths.get(tempDir.toString(), "data", "plugin"); + // Clean up persistence files before each test to ensure isolation + if (Files.exists(pluginDataDir)) { + Files.walk(pluginDataDir) + .sorted((a, b) -> -a.compareTo(b)) + .forEach(path -> { + try { + Files.deleteIfExists(path); + } catch (IOException e) { + // ignore + } + }); + } + persistence = new FilePluginStatePersistenceImpl(); + } + + @AfterEach + void tearDown() { + // Clear the static cached path to avoid affecting other tests + EnvUtil.setNacosHomePath(null); + } + + @Test + void saveStateTest() { + persistence.saveState("trace:test", true); + + Map states = persistence.loadAllStates(); + assertNotNull(states); + assertTrue(states.containsKey("trace:test")); + assertTrue(states.get("trace:test")); + } + + @Test + void saveStateMultiplePluginsTest() { + persistence.saveState("trace:test1", true); + persistence.saveState("auth:test2", false); + + Map states = persistence.loadAllStates(); + assertNotNull(states); + assertEquals(2, states.size()); + assertTrue(states.get("trace:test1")); + assertFalse(states.get("auth:test2")); + } + + @Test + void saveStateOverwriteTest() { + persistence.saveState("trace:test", true); + persistence.saveState("trace:test", false); + + Map states = persistence.loadAllStates(); + assertFalse(states.get("trace:test")); + } + + @Test + void saveConfigTest() { + Map config = new HashMap<>(); + config.put("key1", "value1"); + config.put("key2", "value2"); + + persistence.saveConfig("trace:test", config); + + Map> configs = persistence.loadAllConfigs(); + assertNotNull(configs); + assertTrue(configs.containsKey("trace:test")); + assertEquals("value1", configs.get("trace:test").get("key1")); + assertEquals("value2", configs.get("trace:test").get("key2")); + } + + @Test + void saveConfigMultiplePluginsTest() { + Map config1 = new HashMap<>(); + config1.put("key1", "value1"); + + Map config2 = new HashMap<>(); + config2.put("key2", "value2"); + + persistence.saveConfig("trace:test1", config1); + persistence.saveConfig("auth:test2", config2); + + Map> configs = persistence.loadAllConfigs(); + assertNotNull(configs); + assertEquals(2, configs.size()); + assertEquals("value1", configs.get("trace:test1").get("key1")); + assertEquals("value2", configs.get("auth:test2").get("key2")); + } + + @Test + void loadAllStatesFileNotExistsTest() { + Map states = persistence.loadAllStates(); + + assertNotNull(states); + assertEquals(0, states.size()); + } + + @Test + void loadAllStatesEmptyFileTest() throws IOException { + Path stateFile = pluginDataDir.resolve("plugin-states.json"); + Files.createDirectories(pluginDataDir); + Files.write(stateFile, "".getBytes(StandardCharsets.UTF_8)); + + Map states = persistence.loadAllStates(); + + assertNotNull(states); + assertEquals(0, states.size()); + } + + @Test + void loadAllStatesCorruptedFileTest() throws IOException { + Path stateFile = pluginDataDir.resolve("plugin-states.json"); + Files.createDirectories(pluginDataDir); + Files.write(stateFile, "not a valid json".getBytes(StandardCharsets.UTF_8)); + + Map states = persistence.loadAllStates(); + + assertNotNull(states); + assertEquals(0, states.size()); + } + + @Test + void loadAllConfigsFileNotExistsTest() { + Map> configs = persistence.loadAllConfigs(); + + assertNotNull(configs); + assertEquals(0, configs.size()); + } + + @Test + void loadAllConfigsEmptyFileTest() throws IOException { + Path configFile = pluginDataDir.resolve("plugin-configs.json"); + Files.createDirectories(pluginDataDir); + Files.write(configFile, "".getBytes(StandardCharsets.UTF_8)); + + Map> configs = persistence.loadAllConfigs(); + + assertNotNull(configs); + assertEquals(0, configs.size()); + } + + @Test + void loadAllConfigsCorruptedFileTest() throws IOException { + Path configFile = pluginDataDir.resolve("plugin-configs.json"); + Files.createDirectories(pluginDataDir); + Files.write(configFile, "not a valid json".getBytes(StandardCharsets.UTF_8)); + + Map> configs = persistence.loadAllConfigs(); + + assertNotNull(configs); + assertEquals(0, configs.size()); + } + + @Test + void deleteStateTest() { + persistence.saveState("trace:test1", true); + persistence.saveState("trace:test2", false); + + persistence.deleteState("trace:test1"); + + Map states = persistence.loadAllStates(); + assertFalse(states.containsKey("trace:test1")); + assertTrue(states.containsKey("trace:test2")); + } + + @Test + void deleteStateNonExistingPluginTest() { + persistence.saveState("trace:test1", true); + + persistence.deleteState("nonexistent:plugin"); + + Map states = persistence.loadAllStates(); + assertEquals(1, states.size()); + assertTrue(states.containsKey("trace:test1")); + } + + @Test + void deleteConfigTest() { + Map config1 = new HashMap<>(); + config1.put("key1", "value1"); + + Map config2 = new HashMap<>(); + config2.put("key2", "value2"); + + persistence.saveConfig("trace:test1", config1); + persistence.saveConfig("trace:test2", config2); + + persistence.deleteConfig("trace:test1"); + + Map> configs = persistence.loadAllConfigs(); + assertFalse(configs.containsKey("trace:test1")); + assertTrue(configs.containsKey("trace:test2")); + } + + @Test + void deleteConfigNonExistingPluginTest() { + Map config1 = new HashMap<>(); + config1.put("key1", "value1"); + + persistence.saveConfig("trace:test1", config1); + + persistence.deleteConfig("nonexistent:plugin"); + + Map> configs = persistence.loadAllConfigs(); + assertEquals(1, configs.size()); + assertTrue(configs.containsKey("trace:test1")); + } + + @Test + void ensureDataDirExistsTest() { + File dataDir = new File(pluginDataDir.toString()); + + assertTrue(dataDir.exists()); + assertTrue(dataDir.isDirectory()); + } + + @Test + void concurrentSaveStateTest() throws InterruptedException { + Thread t1 = new Thread(() -> { + for (int i = 0; i < 100; i++) { + persistence.saveState("plugin1", i % 2 == 0); + } + }); + + Thread t2 = new Thread(() -> { + for (int i = 0; i < 100; i++) { + persistence.saveState("plugin2", i % 2 == 0); + } + }); + + t1.start(); + t2.start(); + t1.join(); + t2.join(); + + Map states = persistence.loadAllStates(); + assertEquals(2, states.size()); + assertTrue(states.containsKey("plugin1")); + assertTrue(states.containsKey("plugin2")); + } + + @Test + void concurrentSaveConfigTest() throws InterruptedException { + Thread t1 = new Thread(() -> { + for (int i = 0; i < 100; i++) { + Map config = new HashMap<>(); + config.put("key", "value" + i); + persistence.saveConfig("plugin1", config); + } + }); + + Thread t2 = new Thread(() -> { + for (int i = 0; i < 100; i++) { + Map config = new HashMap<>(); + config.put("key", "value" + i); + persistence.saveConfig("plugin2", config); + } + }); + + t1.start(); + t2.start(); + t1.join(); + t2.join(); + + Map> configs = persistence.loadAllConfigs(); + assertEquals(2, configs.size()); + assertTrue(configs.containsKey("plugin1")); + assertTrue(configs.containsKey("plugin2")); + } +} diff --git a/plugin/auth/src/main/java/com/alibaba/nacos/plugin/auth/spi/server/AuthPluginManager.java b/plugin/auth/src/main/java/com/alibaba/nacos/plugin/auth/spi/server/AuthPluginManager.java index a714d88a089..e1d92ae6886 100644 --- a/plugin/auth/src/main/java/com/alibaba/nacos/plugin/auth/spi/server/AuthPluginManager.java +++ b/plugin/auth/src/main/java/com/alibaba/nacos/plugin/auth/spi/server/AuthPluginManager.java @@ -21,7 +21,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.alibaba.nacos.api.plugin.PluginStateCheckerHolder; +import com.alibaba.nacos.api.plugin.PluginType; + import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Optional; @@ -73,7 +77,21 @@ public static AuthPluginManager getInstance() { * @return AuthPluginService instance. */ public Optional findAuthServiceSpiImpl(String authServiceName) { + // Check if plugin is enabled + if (!PluginStateCheckerHolder.isPluginEnabled(PluginType.AUTH.getType(), authServiceName)) { + LOGGER.debug("[AuthPluginManager] Plugin AUTH:{} is disabled", authServiceName); + return Optional.empty(); + } return Optional.ofNullable(authServiceMap.get(authServiceName)); } - + + /** + * Get all registered auth plugins. + * + * @return unmodifiable map of auth service name to AuthPluginService + */ + public Map getAllPlugins() { + return Collections.unmodifiableMap(authServiceMap); + } + } diff --git a/plugin/auth/src/main/java/com/alibaba/nacos/plugin/auth/spi/server/AuthPluginProvider.java b/plugin/auth/src/main/java/com/alibaba/nacos/plugin/auth/spi/server/AuthPluginProvider.java new file mode 100644 index 00000000000..07f1ecaa7de --- /dev/null +++ b/plugin/auth/src/main/java/com/alibaba/nacos/plugin/auth/spi/server/AuthPluginProvider.java @@ -0,0 +1,41 @@ +/* + * Copyright 1999-2024 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.nacos.plugin.auth.spi.server; + +import com.alibaba.nacos.api.plugin.PluginProvider; +import com.alibaba.nacos.api.plugin.PluginType; + +import java.util.Map; + +/** + * Auth plugin provider implementation. + * + * @author WangzJi + * @since 3.2.0 + */ +public class AuthPluginProvider implements PluginProvider { + + @Override + public PluginType getPluginType() { + return PluginType.AUTH; + } + + @Override + public Map getAllPlugins() { + return AuthPluginManager.getInstance().getAllPlugins(); + } +} diff --git a/plugin/auth/src/main/resources/META-INF/services/com.alibaba.nacos.api.plugin.PluginProvider b/plugin/auth/src/main/resources/META-INF/services/com.alibaba.nacos.api.plugin.PluginProvider new file mode 100644 index 00000000000..d7d55f7ea84 --- /dev/null +++ b/plugin/auth/src/main/resources/META-INF/services/com.alibaba.nacos.api.plugin.PluginProvider @@ -0,0 +1,16 @@ +# +# Copyright 1999-2024 Alibaba Group Holding Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +com.alibaba.nacos.plugin.auth.spi.server.AuthPluginProvider diff --git a/plugin/auth/src/test/java/com/alibaba/nacos/plugin/auth/spi/server/AuthPluginProviderTest.java b/plugin/auth/src/test/java/com/alibaba/nacos/plugin/auth/spi/server/AuthPluginProviderTest.java new file mode 100644 index 00000000000..3b0e651e92f --- /dev/null +++ b/plugin/auth/src/test/java/com/alibaba/nacos/plugin/auth/spi/server/AuthPluginProviderTest.java @@ -0,0 +1,64 @@ +/* + * Copyright 1999-2024 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.nacos.plugin.auth.spi.server; + +import com.alibaba.nacos.api.plugin.PluginType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * {@link AuthPluginProvider} unit test. + * + * @author WangzJi + */ +class AuthPluginProviderTest { + + private AuthPluginProvider provider; + + @BeforeEach + void setUp() { + provider = new AuthPluginProvider(); + } + + @Test + void getPluginTypeTest() { + PluginType pluginType = provider.getPluginType(); + + assertNotNull(pluginType); + assertEquals(PluginType.AUTH, pluginType); + assertEquals("auth", pluginType.getType()); + } + + @Test + void getAllPluginsTest() { + Map plugins = provider.getAllPlugins(); + + assertNotNull(plugins); + } + + @Test + void getOrderTest() { + int order = provider.getOrder(); + + assertEquals(0, order); + } +} diff --git a/plugin/config/src/main/java/com/alibaba/nacos/plugin/config/ConfigChangePluginManager.java b/plugin/config/src/main/java/com/alibaba/nacos/plugin/config/ConfigChangePluginManager.java index 96be0f7e70a..0a15f8ca80c 100644 --- a/plugin/config/src/main/java/com/alibaba/nacos/plugin/config/ConfigChangePluginManager.java +++ b/plugin/config/src/main/java/com/alibaba/nacos/plugin/config/ConfigChangePluginManager.java @@ -16,6 +16,9 @@ package com.alibaba.nacos.plugin.config; +import com.alibaba.nacos.api.plugin.PluginStateChecker; +import com.alibaba.nacos.api.plugin.PluginStateCheckerHolder; +import com.alibaba.nacos.api.plugin.PluginType; import com.alibaba.nacos.common.JustForTest; import com.alibaba.nacos.common.spi.NacosServiceLoader; import com.alibaba.nacos.common.utils.StringUtils; @@ -26,6 +29,7 @@ import java.util.Map; import java.util.Collection; +import java.util.Collections; import java.util.Optional; import java.util.List; import java.util.ArrayList; @@ -100,6 +104,11 @@ public static ConfigChangePluginManager getInstance() { * @return */ public Optional findPluginServiceImpl(String serviceType) { + Optional checker = PluginStateCheckerHolder.getInstance(); + if (checker.isPresent() && !checker.get().isPluginEnabled(PluginType.CONFIG_CHANGE.getType(), serviceType)) { + LOGGER.debug("[ConfigChangePluginManager] Plugin CONFIG_CHANGE:{} is disabled", serviceType); + return Optional.empty(); + } return Optional.ofNullable(CONFIG_CHANGE_PLUGIN_SERVICE_MAP.get(serviceType)); } @@ -153,4 +162,13 @@ public static synchronized void reset() { CONFIG_CHANGE_PLUGIN_SERVICE_MAP.clear(); CONFIG_CHANGE_PLUGIN_SERVICES_MAP.clear(); } + + /** + * Get all config change plugin services. + * + * @return unmodifiable map of all config change plugin services + */ + public Map getAllPlugins() { + return Collections.unmodifiableMap(CONFIG_CHANGE_PLUGIN_SERVICE_MAP); + } } diff --git a/plugin/config/src/main/java/com/alibaba/nacos/plugin/config/ConfigChangePluginProvider.java b/plugin/config/src/main/java/com/alibaba/nacos/plugin/config/ConfigChangePluginProvider.java new file mode 100644 index 00000000000..8833b64beaf --- /dev/null +++ b/plugin/config/src/main/java/com/alibaba/nacos/plugin/config/ConfigChangePluginProvider.java @@ -0,0 +1,42 @@ +/* + * Copyright 1999-2024 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.nacos.plugin.config; + +import com.alibaba.nacos.api.plugin.PluginProvider; +import com.alibaba.nacos.api.plugin.PluginType; +import com.alibaba.nacos.plugin.config.spi.ConfigChangePluginService; + +import java.util.Map; + +/** + * Config change plugin provider implementation. + * + * @author WangzJi + * @since 3.2.0 + */ +public class ConfigChangePluginProvider implements PluginProvider { + + @Override + public PluginType getPluginType() { + return PluginType.CONFIG_CHANGE; + } + + @Override + public Map getAllPlugins() { + return ConfigChangePluginManager.getInstance().getAllPlugins(); + } +} diff --git a/plugin/config/src/main/resources/META-INF/services/com.alibaba.nacos.api.plugin.PluginProvider b/plugin/config/src/main/resources/META-INF/services/com.alibaba.nacos.api.plugin.PluginProvider new file mode 100644 index 00000000000..8fa8d4824cf --- /dev/null +++ b/plugin/config/src/main/resources/META-INF/services/com.alibaba.nacos.api.plugin.PluginProvider @@ -0,0 +1,16 @@ +# +# Copyright 1999-2024 Alibaba Group Holding Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +com.alibaba.nacos.plugin.config.ConfigChangePluginProvider diff --git a/plugin/config/src/test/java/com/alibaba/nacos/plugin/config/ConfigChangePluginProviderTest.java b/plugin/config/src/test/java/com/alibaba/nacos/plugin/config/ConfigChangePluginProviderTest.java new file mode 100644 index 00000000000..3c3b60557f0 --- /dev/null +++ b/plugin/config/src/test/java/com/alibaba/nacos/plugin/config/ConfigChangePluginProviderTest.java @@ -0,0 +1,65 @@ +/* + * Copyright 1999-2024 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.nacos.plugin.config; + +import com.alibaba.nacos.api.plugin.PluginType; +import com.alibaba.nacos.plugin.config.spi.ConfigChangePluginService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * {@link ConfigChangePluginProvider} unit test. + * + * @author WangzJi + */ +class ConfigChangePluginProviderTest { + + private ConfigChangePluginProvider provider; + + @BeforeEach + void setUp() { + provider = new ConfigChangePluginProvider(); + } + + @Test + void getPluginTypeTest() { + PluginType pluginType = provider.getPluginType(); + + assertNotNull(pluginType); + assertEquals(PluginType.CONFIG_CHANGE, pluginType); + assertEquals("config-change", pluginType.getType()); + } + + @Test + void getAllPluginsTest() { + Map plugins = provider.getAllPlugins(); + + assertNotNull(plugins); + } + + @Test + void getOrderTest() { + int order = provider.getOrder(); + + assertEquals(0, order); + } +} diff --git a/plugin/datasource/src/main/java/com/alibaba/nacos/plugin/datasource/MapperManager.java b/plugin/datasource/src/main/java/com/alibaba/nacos/plugin/datasource/MapperManager.java index 2968fca8e1b..9b26c9ba3dd 100644 --- a/plugin/datasource/src/main/java/com/alibaba/nacos/plugin/datasource/MapperManager.java +++ b/plugin/datasource/src/main/java/com/alibaba/nacos/plugin/datasource/MapperManager.java @@ -25,6 +25,7 @@ import org.slf4j.LoggerFactory; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Objects; @@ -120,4 +121,13 @@ public R findMapper(String dataSource, String tableName) { } return (R) mapper; } + + /** + * Get all mappers. + * + * @return unmodifiable map of all mappers + */ + public Map> getAllMappers() { + return Collections.unmodifiableMap(MAPPER_SPI_MAP); + } } diff --git a/plugin/datasource/src/main/java/com/alibaba/nacos/plugin/datasource/manager/DatabaseDialectManager.java b/plugin/datasource/src/main/java/com/alibaba/nacos/plugin/datasource/manager/DatabaseDialectManager.java index 8135b3f302d..a2cc22d2192 100644 --- a/plugin/datasource/src/main/java/com/alibaba/nacos/plugin/datasource/manager/DatabaseDialectManager.java +++ b/plugin/datasource/src/main/java/com/alibaba/nacos/plugin/datasource/manager/DatabaseDialectManager.java @@ -16,12 +16,15 @@ package com.alibaba.nacos.plugin.datasource.manager; +import com.alibaba.nacos.api.plugin.PluginStateCheckerHolder; +import com.alibaba.nacos.api.plugin.PluginType; import com.alibaba.nacos.common.spi.NacosServiceLoader; import com.alibaba.nacos.plugin.datasource.dialect.DatabaseDialect; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Collection; +import java.util.Collections; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -53,20 +56,34 @@ private DatabaseDialectManager() { } public DatabaseDialect getDialect(String databaseType) { + // Check if plugin is enabled + if (!PluginStateCheckerHolder.isPluginEnabled(PluginType.DATASOURCE_DIALECT.getType(), databaseType)) { + LOGGER.debug("[DatabaseDialectManager] Plugin DATASOURCE_DIALECT:{} is disabled", databaseType); + throw new IllegalStateException( + "DatabaseDialect plugin is disabled: " + databaseType + + ". Please enable it via plugin management API."); + } + DatabaseDialect databaseDialect = SUPPORT_DIALECT_MAP.get(databaseType); if (databaseDialect == null) { - LOGGER.warn("[DatabaseDialectManager] No dialect found for type: {}, using first available dialect as fallback", + LOGGER.warn("[DatabaseDialectManager] No dialect found for type: {}, checking for enabled fallback dialects", databaseType); - // Fallback to any available dialect, prefer one that is loaded via SPI - if (!SUPPORT_DIALECT_MAP.isEmpty()) { - return SUPPORT_DIALECT_MAP.values().iterator().next(); + // Find first enabled dialect as fallback + for (Map.Entry entry : SUPPORT_DIALECT_MAP.entrySet()) { + String dialectType = entry.getKey(); + if (PluginStateCheckerHolder.isPluginEnabled(PluginType.DATASOURCE_DIALECT.getType(), dialectType)) { + LOGGER.warn("[DatabaseDialectManager] Using enabled dialect {} as fallback for {}", + dialectType, databaseType); + return entry.getValue(); + } } - throw new IllegalStateException("No DatabaseDialect implementation found. " - + "Please ensure datasource plugin is properly loaded."); + throw new IllegalStateException( + "No enabled DatabaseDialect implementation found. " + + "Please ensure datasource plugin is properly loaded and enabled."); } return databaseDialect; } - + /** * Get DatasourceDialectManager instance. * @@ -75,5 +92,14 @@ public DatabaseDialect getDialect(String databaseType) { public static DatabaseDialectManager getInstance() { return INSTANCE; } - + + /** + * Get all registered database dialects. + * + * @return unmodifiable map of database type to DatabaseDialect + */ + public Map getAllDialects() { + return Collections.unmodifiableMap(SUPPORT_DIALECT_MAP); + } + } diff --git a/plugin/datasource/src/main/java/com/alibaba/nacos/plugin/datasource/manager/DatasourceDialectPluginProvider.java b/plugin/datasource/src/main/java/com/alibaba/nacos/plugin/datasource/manager/DatasourceDialectPluginProvider.java new file mode 100644 index 00000000000..cb1d5f7522c --- /dev/null +++ b/plugin/datasource/src/main/java/com/alibaba/nacos/plugin/datasource/manager/DatasourceDialectPluginProvider.java @@ -0,0 +1,42 @@ +/* + * Copyright 1999-2024 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.nacos.plugin.datasource.manager; + +import com.alibaba.nacos.api.plugin.PluginProvider; +import com.alibaba.nacos.api.plugin.PluginType; +import com.alibaba.nacos.plugin.datasource.dialect.DatabaseDialect; + +import java.util.Map; + +/** + * Datasource dialect plugin provider implementation. + * + * @author WangzJi + * @since 3.2.0 + */ +public class DatasourceDialectPluginProvider implements PluginProvider { + + @Override + public PluginType getPluginType() { + return PluginType.DATASOURCE_DIALECT; + } + + @Override + public Map getAllPlugins() { + return DatabaseDialectManager.getInstance().getAllDialects(); + } +} diff --git a/plugin/datasource/src/main/resources/META-INF/services/com.alibaba.nacos.api.plugin.PluginProvider b/plugin/datasource/src/main/resources/META-INF/services/com.alibaba.nacos.api.plugin.PluginProvider new file mode 100644 index 00000000000..bea8eaef233 --- /dev/null +++ b/plugin/datasource/src/main/resources/META-INF/services/com.alibaba.nacos.api.plugin.PluginProvider @@ -0,0 +1,16 @@ +# +# Copyright 1999-2024 Alibaba Group Holding Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +com.alibaba.nacos.plugin.datasource.manager.DatasourceDialectPluginProvider diff --git a/plugin/datasource/src/test/java/com/alibaba/nacos/plugin/datasource/manager/DatasourceDialectPluginProviderTest.java b/plugin/datasource/src/test/java/com/alibaba/nacos/plugin/datasource/manager/DatasourceDialectPluginProviderTest.java new file mode 100644 index 00000000000..dc2fb878de3 --- /dev/null +++ b/plugin/datasource/src/test/java/com/alibaba/nacos/plugin/datasource/manager/DatasourceDialectPluginProviderTest.java @@ -0,0 +1,65 @@ +/* + * Copyright 1999-2024 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.nacos.plugin.datasource.manager; + +import com.alibaba.nacos.api.plugin.PluginType; +import com.alibaba.nacos.plugin.datasource.dialect.DatabaseDialect; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * {@link DatasourceDialectPluginProvider} unit test. + * + * @author WangzJi + */ +class DatasourceDialectPluginProviderTest { + + private DatasourceDialectPluginProvider provider; + + @BeforeEach + void setUp() { + provider = new DatasourceDialectPluginProvider(); + } + + @Test + void getPluginTypeTest() { + PluginType pluginType = provider.getPluginType(); + + assertNotNull(pluginType); + assertEquals(PluginType.DATASOURCE_DIALECT, pluginType); + assertEquals("datasource-dialect", pluginType.getType()); + } + + @Test + void getAllPluginsTest() { + Map plugins = provider.getAllPlugins(); + + assertNotNull(plugins); + } + + @Test + void getOrderTest() { + int order = provider.getOrder(); + + assertEquals(0, order); + } +} diff --git a/plugin/encryption/src/main/java/com/alibaba/nacos/plugin/encryption/EncryptionPluginManager.java b/plugin/encryption/src/main/java/com/alibaba/nacos/plugin/encryption/EncryptionPluginManager.java index 125b54f57cb..98c85728902 100644 --- a/plugin/encryption/src/main/java/com/alibaba/nacos/plugin/encryption/EncryptionPluginManager.java +++ b/plugin/encryption/src/main/java/com/alibaba/nacos/plugin/encryption/EncryptionPluginManager.java @@ -16,6 +16,9 @@ package com.alibaba.nacos.plugin.encryption; +import com.alibaba.nacos.api.plugin.PluginStateChecker; +import com.alibaba.nacos.api.plugin.PluginStateCheckerHolder; +import com.alibaba.nacos.api.plugin.PluginType; import com.alibaba.nacos.common.spi.NacosServiceLoader; import com.alibaba.nacos.common.utils.StringUtils; import com.alibaba.nacos.plugin.encryption.spi.EncryptionPluginService; @@ -23,6 +26,7 @@ import org.slf4j.LoggerFactory; import java.util.Collection; +import java.util.Collections; import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -79,6 +83,11 @@ public static EncryptionPluginManager instance() { * @return EncryptionPluginService instance. */ public Optional findEncryptionService(String algorithmName) { + Optional checker = PluginStateCheckerHolder.getInstance(); + if (checker.isPresent() && !checker.get().isPluginEnabled(PluginType.ENCRYPTION.getType(), algorithmName)) { + LOGGER.debug("[EncryptionPluginManager] Plugin ENCRYPTION:{} is disabled", algorithmName); + return Optional.empty(); + } return Optional.ofNullable(ENCRYPTION_SPI_MAP.get(algorithmName)); } @@ -94,5 +103,14 @@ public static synchronized void join(EncryptionPluginService encryptionPluginSer ENCRYPTION_SPI_MAP.put(encryptionPluginService.algorithmName(), encryptionPluginService); LOGGER.info("[EncryptionPluginManager] join successfully."); } - + + /** + * Get all encryption plugin services. + * + * @return unmodifiable map of all encryption plugin services + */ + public Map getAllPlugins() { + return Collections.unmodifiableMap(ENCRYPTION_SPI_MAP); + } + } diff --git a/plugin/encryption/src/main/java/com/alibaba/nacos/plugin/encryption/EncryptionPluginProvider.java b/plugin/encryption/src/main/java/com/alibaba/nacos/plugin/encryption/EncryptionPluginProvider.java new file mode 100644 index 00000000000..dbffba35812 --- /dev/null +++ b/plugin/encryption/src/main/java/com/alibaba/nacos/plugin/encryption/EncryptionPluginProvider.java @@ -0,0 +1,42 @@ +/* + * Copyright 1999-2024 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.nacos.plugin.encryption; + +import com.alibaba.nacos.api.plugin.PluginProvider; +import com.alibaba.nacos.api.plugin.PluginType; +import com.alibaba.nacos.plugin.encryption.spi.EncryptionPluginService; + +import java.util.Map; + +/** + * Encryption plugin provider implementation. + * + * @author WangzJi + * @since 3.2.0 + */ +public class EncryptionPluginProvider implements PluginProvider { + + @Override + public PluginType getPluginType() { + return PluginType.ENCRYPTION; + } + + @Override + public Map getAllPlugins() { + return EncryptionPluginManager.instance().getAllPlugins(); + } +} diff --git a/plugin/encryption/src/main/resources/META-INF/services/com.alibaba.nacos.api.plugin.PluginProvider b/plugin/encryption/src/main/resources/META-INF/services/com.alibaba.nacos.api.plugin.PluginProvider new file mode 100644 index 00000000000..8cf8391b532 --- /dev/null +++ b/plugin/encryption/src/main/resources/META-INF/services/com.alibaba.nacos.api.plugin.PluginProvider @@ -0,0 +1,16 @@ +# +# Copyright 1999-2024 Alibaba Group Holding Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +com.alibaba.nacos.plugin.encryption.EncryptionPluginProvider diff --git a/plugin/encryption/src/test/java/com/alibaba/nacos/plugin/encryption/EncryptionPluginProviderTest.java b/plugin/encryption/src/test/java/com/alibaba/nacos/plugin/encryption/EncryptionPluginProviderTest.java new file mode 100644 index 00000000000..dec4b7197b2 --- /dev/null +++ b/plugin/encryption/src/test/java/com/alibaba/nacos/plugin/encryption/EncryptionPluginProviderTest.java @@ -0,0 +1,65 @@ +/* + * Copyright 1999-2024 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.nacos.plugin.encryption; + +import com.alibaba.nacos.api.plugin.PluginType; +import com.alibaba.nacos.plugin.encryption.spi.EncryptionPluginService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * {@link EncryptionPluginProvider} unit test. + * + * @author WangzJi + */ +class EncryptionPluginProviderTest { + + private EncryptionPluginProvider provider; + + @BeforeEach + void setUp() { + provider = new EncryptionPluginProvider(); + } + + @Test + void getPluginTypeTest() { + PluginType pluginType = provider.getPluginType(); + + assertNotNull(pluginType); + assertEquals(PluginType.ENCRYPTION, pluginType); + assertEquals("encryption", pluginType.getType()); + } + + @Test + void getAllPluginsTest() { + Map plugins = provider.getAllPlugins(); + + assertNotNull(plugins); + } + + @Test + void getOrderTest() { + int order = provider.getOrder(); + + assertEquals(0, order); + } +} diff --git a/plugin/trace/src/main/java/com/alibaba/nacos/plugin/trace/NacosTracePluginManager.java b/plugin/trace/src/main/java/com/alibaba/nacos/plugin/trace/NacosTracePluginManager.java index 558a2219747..9eb661a4c41 100644 --- a/plugin/trace/src/main/java/com/alibaba/nacos/plugin/trace/NacosTracePluginManager.java +++ b/plugin/trace/src/main/java/com/alibaba/nacos/plugin/trace/NacosTracePluginManager.java @@ -16,15 +16,21 @@ package com.alibaba.nacos.plugin.trace; +import com.alibaba.nacos.api.plugin.PluginStateChecker; +import com.alibaba.nacos.api.plugin.PluginStateCheckerHolder; +import com.alibaba.nacos.api.plugin.PluginType; import com.alibaba.nacos.common.spi.NacosServiceLoader; import com.alibaba.nacos.plugin.trace.spi.NacosTraceSubscriber; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Collection; +import java.util.Collections; import java.util.HashSet; import java.util.Map; +import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; /** * Nacos trace event subscriber manager. @@ -52,8 +58,29 @@ private NacosTracePluginManager() { public static NacosTracePluginManager getInstance() { return INSTANCE; } - + public Collection getAllTraceSubscribers() { + Optional checker = PluginStateCheckerHolder.getInstance(); + if (checker.isPresent()) { + return traceSubscribers.values().stream() + .filter(subscriber -> { + boolean enabled = checker.get().isPluginEnabled(PluginType.TRACE.getType(), subscriber.getName()); + if (!enabled) { + LOGGER.debug("[TracePluginManager] Plugin TRACE:{} is disabled", subscriber.getName()); + } + return enabled; + }) + .collect(Collectors.toSet()); + } return new HashSet<>(traceSubscribers.values()); } + + /** + * Get all trace subscribers without filtering. + * + * @return unmodifiable map of all trace subscribers + */ + public Map getAllPlugins() { + return Collections.unmodifiableMap(traceSubscribers); + } } diff --git a/plugin/trace/src/main/java/com/alibaba/nacos/plugin/trace/TracePluginProvider.java b/plugin/trace/src/main/java/com/alibaba/nacos/plugin/trace/TracePluginProvider.java new file mode 100644 index 00000000000..1ffb7011155 --- /dev/null +++ b/plugin/trace/src/main/java/com/alibaba/nacos/plugin/trace/TracePluginProvider.java @@ -0,0 +1,42 @@ +/* + * Copyright 1999-2024 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.nacos.plugin.trace; + +import com.alibaba.nacos.api.plugin.PluginProvider; +import com.alibaba.nacos.api.plugin.PluginType; +import com.alibaba.nacos.plugin.trace.spi.NacosTraceSubscriber; + +import java.util.Map; + +/** + * Trace plugin provider implementation. + * + * @author WangzJi + * @since 3.2.0 + */ +public class TracePluginProvider implements PluginProvider { + + @Override + public PluginType getPluginType() { + return PluginType.TRACE; + } + + @Override + public Map getAllPlugins() { + return NacosTracePluginManager.getInstance().getAllPlugins(); + } +} diff --git a/plugin/trace/src/main/resources/META-INF/services/com.alibaba.nacos.api.plugin.PluginProvider b/plugin/trace/src/main/resources/META-INF/services/com.alibaba.nacos.api.plugin.PluginProvider new file mode 100644 index 00000000000..487467de353 --- /dev/null +++ b/plugin/trace/src/main/resources/META-INF/services/com.alibaba.nacos.api.plugin.PluginProvider @@ -0,0 +1,16 @@ +# +# Copyright 1999-2024 Alibaba Group Holding Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +com.alibaba.nacos.plugin.trace.TracePluginProvider diff --git a/plugin/trace/src/test/java/com/alibaba/nacos/plugin/trace/TracePluginProviderTest.java b/plugin/trace/src/test/java/com/alibaba/nacos/plugin/trace/TracePluginProviderTest.java new file mode 100644 index 00000000000..07fd415d8a6 --- /dev/null +++ b/plugin/trace/src/test/java/com/alibaba/nacos/plugin/trace/TracePluginProviderTest.java @@ -0,0 +1,65 @@ +/* + * Copyright 1999-2024 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.nacos.plugin.trace; + +import com.alibaba.nacos.api.plugin.PluginType; +import com.alibaba.nacos.plugin.trace.spi.NacosTraceSubscriber; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * {@link TracePluginProvider} unit test. + * + * @author WangzJi + */ +class TracePluginProviderTest { + + private TracePluginProvider provider; + + @BeforeEach + void setUp() { + provider = new TracePluginProvider(); + } + + @Test + void getPluginTypeTest() { + PluginType pluginType = provider.getPluginType(); + + assertNotNull(pluginType); + assertEquals(PluginType.TRACE, pluginType); + assertEquals("trace", pluginType.getType()); + } + + @Test + void getAllPluginsTest() { + Map plugins = provider.getAllPlugins(); + + assertNotNull(plugins); + } + + @Test + void getOrderTest() { + int order = provider.getOrder(); + + assertEquals(0, order); + } +}