From a634065d48396006212891cbc534c63cf2b57964 Mon Sep 17 00:00:00 2001 From: guqing Date: Sat, 14 Sep 2024 16:08:50 +0800 Subject: [PATCH] feat: support JSON-based retrieval and update for theme and plugin configs --- api-docs/openapi/v3_0/aggregated.json | 139 +++++++++++++++++- .../v3_0/apis_console.api_v1alpha1.json | 139 +++++++++++++++++- .../extension/endpoint/PluginEndpoint.java | 76 +++++++++- .../service/SettingConfigService.java | 19 +++ .../impl/SettingConfigServiceImpl.java | 57 +++++++ .../core/extension/theme/SettingUtils.java | 25 +++- .../core/extension/theme/ThemeEndpoint.java | 74 +++++++++- .../extensions/role-template-plugin.yaml | 6 +- .../extensions/role-template-theme.yaml | 6 +- .../endpoint/PluginEndpointTest.java | 38 +++++ .../extension/theme/ThemeEndpointTest.java | 44 +++++- 11 files changed, 603 insertions(+), 20 deletions(-) create mode 100644 application/src/main/java/run/halo/app/core/extension/service/SettingConfigService.java create mode 100644 application/src/main/java/run/halo/app/core/extension/service/impl/SettingConfigServiceImpl.java diff --git a/api-docs/openapi/v3_0/aggregated.json b/api-docs/openapi/v3_0/aggregated.json index 7006547132..0c4adcb15a 100644 --- a/api-docs/openapi/v3_0/aggregated.json +++ b/api-docs/openapi/v3_0/aggregated.json @@ -2871,7 +2871,7 @@ }, "/apis/api.console.halo.run/v1alpha1/plugins/{name}/config": { "get": { - "description": "Fetch configMap of plugin by configured configMapName.", + "description": "Fetch configMap of plugin by configured configMapName. it is deprecated since 2.20.0", "operationId": "fetchPluginConfig", "parameters": [ { @@ -2900,7 +2900,8 @@ ] }, "put": { - "description": "Update the configMap of plugin setting.", + "deprecated": true, + "description": "Update the configMap of plugin setting, it is deprecated since 2.20.0", "operationId": "updatePluginConfig", "parameters": [ { @@ -2939,6 +2940,70 @@ ] } }, + "/apis/api.console.halo.run/v1alpha1/plugins/{name}/json-config": { + "get": { + "description": "Fetch converted json config of plugin by configured configMapName.", + "operationId": "fetchPluginJsonConfig", + "parameters": [ + { + "in": "path", + "name": "name", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "content": { + "*/*": { + "schema": { + "type": "object" + } + } + }, + "description": "default response" + } + }, + "tags": [ + "PluginV1alpha1Console" + ] + }, + "put": { + "description": "Update the config of plugin setting.", + "operationId": "updatePluginJsonConfig", + "parameters": [ + { + "in": "path", + "name": "name", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + }, + "required": true + }, + "responses": { + "204": { + "content": {}, + "description": "No Content" + } + }, + "tags": [ + "PluginV1alpha1Console" + ] + } + }, "/apis/api.console.halo.run/v1alpha1/plugins/{name}/plugin-state": { "put": { "description": "Change the running state of a plugin by name.", @@ -4545,7 +4610,8 @@ }, "/apis/api.console.halo.run/v1alpha1/themes/{name}/config": { "get": { - "description": "Fetch configMap of theme by configured configMapName.", + "deprecated": true, + "description": "Fetch configMap of theme by configured configMapName. It is deprecated.", "operationId": "fetchThemeConfig", "parameters": [ { @@ -4574,7 +4640,8 @@ ] }, "put": { - "description": "Update the configMap of theme setting.", + "deprecated": true, + "description": "Update the configMap of theme setting. It is deprecated.", "operationId": "updateThemeConfig", "parameters": [ { @@ -4637,6 +4704,70 @@ ] } }, + "/apis/api.console.halo.run/v1alpha1/themes/{name}/json-config": { + "get": { + "description": "Fetch converted json config of theme by configured configMapName.", + "operationId": "fetchThemeJsonConfig", + "parameters": [ + { + "in": "path", + "name": "name", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "content": { + "*/*": { + "schema": { + "type": "object" + } + } + }, + "description": "default response" + } + }, + "tags": [ + "ThemeV1alpha1Console" + ] + }, + "put": { + "description": "Update the configMap of theme setting.", + "operationId": "updateThemeJsonConfig", + "parameters": [ + { + "in": "path", + "name": "name", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + }, + "required": true + }, + "responses": { + "204": { + "content": {}, + "description": "No Content" + } + }, + "tags": [ + "ThemeV1alpha1Console" + ] + } + }, "/apis/api.console.halo.run/v1alpha1/themes/{name}/reload": { "put": { "description": "Reload theme setting.", diff --git a/api-docs/openapi/v3_0/apis_console.api_v1alpha1.json b/api-docs/openapi/v3_0/apis_console.api_v1alpha1.json index 9ff6d788c3..eaca77f802 100644 --- a/api-docs/openapi/v3_0/apis_console.api_v1alpha1.json +++ b/api-docs/openapi/v3_0/apis_console.api_v1alpha1.json @@ -738,7 +738,7 @@ }, "/apis/api.console.halo.run/v1alpha1/plugins/{name}/config": { "get": { - "description": "Fetch configMap of plugin by configured configMapName.", + "description": "Fetch configMap of plugin by configured configMapName. it is deprecated since 2.20.0", "operationId": "fetchPluginConfig", "parameters": [ { @@ -767,7 +767,8 @@ ] }, "put": { - "description": "Update the configMap of plugin setting.", + "deprecated": true, + "description": "Update the configMap of plugin setting, it is deprecated since 2.20.0", "operationId": "updatePluginConfig", "parameters": [ { @@ -806,6 +807,70 @@ ] } }, + "/apis/api.console.halo.run/v1alpha1/plugins/{name}/json-config": { + "get": { + "description": "Fetch converted json config of plugin by configured configMapName.", + "operationId": "fetchPluginJsonConfig", + "parameters": [ + { + "in": "path", + "name": "name", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "content": { + "*/*": { + "schema": { + "type": "object" + } + } + }, + "description": "default response" + } + }, + "tags": [ + "PluginV1alpha1Console" + ] + }, + "put": { + "description": "Update the config of plugin setting.", + "operationId": "updatePluginJsonConfig", + "parameters": [ + { + "in": "path", + "name": "name", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + }, + "required": true + }, + "responses": { + "204": { + "content": {}, + "description": "No Content" + } + }, + "tags": [ + "PluginV1alpha1Console" + ] + } + }, "/apis/api.console.halo.run/v1alpha1/plugins/{name}/plugin-state": { "put": { "description": "Change the running state of a plugin by name.", @@ -2412,7 +2477,8 @@ }, "/apis/api.console.halo.run/v1alpha1/themes/{name}/config": { "get": { - "description": "Fetch configMap of theme by configured configMapName.", + "deprecated": true, + "description": "Fetch configMap of theme by configured configMapName. It is deprecated.", "operationId": "fetchThemeConfig", "parameters": [ { @@ -2441,7 +2507,8 @@ ] }, "put": { - "description": "Update the configMap of theme setting.", + "deprecated": true, + "description": "Update the configMap of theme setting. It is deprecated.", "operationId": "updateThemeConfig", "parameters": [ { @@ -2504,6 +2571,70 @@ ] } }, + "/apis/api.console.halo.run/v1alpha1/themes/{name}/json-config": { + "get": { + "description": "Fetch converted json config of theme by configured configMapName.", + "operationId": "fetchThemeJsonConfig", + "parameters": [ + { + "in": "path", + "name": "name", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "content": { + "*/*": { + "schema": { + "type": "object" + } + } + }, + "description": "default response" + } + }, + "tags": [ + "ThemeV1alpha1Console" + ] + }, + "put": { + "description": "Update the configMap of theme setting.", + "operationId": "updateThemeJsonConfig", + "parameters": [ + { + "in": "path", + "name": "name", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + }, + "required": true + }, + "responses": { + "204": { + "content": {}, + "description": "No Content" + } + }, + "tags": [ + "ThemeV1alpha1Console" + ] + } + }, "/apis/api.console.halo.run/v1alpha1/themes/{name}/reload": { "put": { "description": "Reload theme setting.", diff --git a/application/src/main/java/run/halo/app/core/extension/endpoint/PluginEndpoint.java b/application/src/main/java/run/halo/app/core/extension/endpoint/PluginEndpoint.java index a05a216bdd..eadd73c9f0 100644 --- a/application/src/main/java/run/halo/app/core/extension/endpoint/PluginEndpoint.java +++ b/application/src/main/java/run/halo/app/core/extension/endpoint/PluginEndpoint.java @@ -17,6 +17,7 @@ import static run.halo.app.extension.router.QueryParamBuildUtil.sortParameter; import static run.halo.app.infra.utils.FileUtils.deleteFileSilently; +import com.fasterxml.jackson.databind.node.ObjectNode; import io.swagger.v3.oas.annotations.enums.ParameterIn; import io.swagger.v3.oas.annotations.media.Schema; import java.io.FileNotFoundException; @@ -41,6 +42,7 @@ import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.data.domain.Sort; import org.springframework.http.CacheControl; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.codec.multipart.FilePart; import org.springframework.http.codec.multipart.FormFieldPart; @@ -61,6 +63,7 @@ import run.halo.app.core.extension.Plugin; import run.halo.app.core.extension.Setting; import run.halo.app.core.extension.service.PluginService; +import run.halo.app.core.extension.service.SettingConfigService; import run.halo.app.core.extension.theme.SettingUtils; import run.halo.app.extension.ConfigMap; import run.halo.app.extension.ListOptions; @@ -80,6 +83,8 @@ public class PluginEndpoint implements CustomEndpoint, InitializingBean { private final ReactiveUrlDataBufferFetcher reactiveUrlDataBufferFetcher; + private final SettingConfigService settingConfigService; + private final WebProperties webProperties; private final Scheduler scheduler = Schedulers.boundedElastic(); @@ -91,10 +96,12 @@ public class PluginEndpoint implements CustomEndpoint, InitializingBean { public PluginEndpoint(ReactiveExtensionClient client, PluginService pluginService, ReactiveUrlDataBufferFetcher reactiveUrlDataBufferFetcher, + SettingConfigService settingConfigService, WebProperties webProperties) { this.client = client; this.pluginService = pluginService; this.reactiveUrlDataBufferFetcher = reactiveUrlDataBufferFetcher; + this.settingConfigService = settingConfigService; this.webProperties = webProperties; } @@ -157,9 +164,28 @@ public RouterFunction endpoint() { .content(contentBuilder().mediaType(MediaType.MULTIPART_FORM_DATA_VALUE) .schema(schemaBuilder().implementation(InstallRequest.class)))) ) + .PUT("plugins/{name}/json-config", this::updatePluginJsonConfig, + builder -> builder.operationId("updatePluginJsonConfig") + .description("Update the config of plugin setting.") + .tag(tag) + .parameter(parameterBuilder() + .name("name") + .in(ParameterIn.PATH) + .required(true) + .implementation(String.class) + ) + .requestBody(requestBodyBuilder() + .required(true) + .content(contentBuilder().mediaType(MediaType.APPLICATION_JSON_VALUE) + .schema(schemaBuilder().implementation(ObjectNode.class)))) + .response(responseBuilder() + .responseCode(String.valueOf(HttpStatus.NO_CONTENT.value())) + .implementation(Void.class)) + ) .PUT("plugins/{name}/config", this::updatePluginConfig, builder -> builder.operationId("updatePluginConfig") - .description("Update the configMap of plugin setting.") + .description( + "Update the configMap of plugin setting, it is deprecated since 2.20.0") .tag(tag) .parameter(parameterBuilder() .name("name") @@ -167,6 +193,7 @@ public RouterFunction endpoint() { .required(true) .implementation(String.class) ) + .deprecated(true) .requestBody(requestBodyBuilder() .required(true) .content(contentBuilder().mediaType(MediaType.APPLICATION_JSON_VALUE) @@ -243,7 +270,9 @@ public RouterFunction endpoint() { ) .GET("plugins/{name}/config", this::fetchPluginConfig, builder -> builder.operationId("fetchPluginConfig") - .description("Fetch configMap of plugin by configured configMapName.") + .description( + "Fetch configMap of plugin by configured configMapName. it is deprecated " + + "since 2.20.0") .tag(tag) .parameter(parameterBuilder() .name("name") @@ -254,6 +283,20 @@ public RouterFunction endpoint() { .response(responseBuilder() .implementation(ConfigMap.class)) ) + .GET("plugins/{name}/json-config", this::fetchPluginJsonConfig, + builder -> builder.operationId("fetchPluginJsonConfig") + .description( + "Fetch converted json config of plugin by configured configMapName.") + .tag(tag) + .parameter(parameterBuilder() + .name("name") + .in(ParameterIn.PATH) + .required(true) + .implementation(String.class) + ) + .response(responseBuilder() + .implementation(ObjectNode.class)) + ) .GET("plugin-presets", this::listPresets, builder -> builder.operationId("ListPluginPresets") .description("List all plugin presets in the system.") @@ -275,6 +318,35 @@ public RouterFunction endpoint() { .build(); } + private Mono fetchPluginJsonConfig(ServerRequest request) { + final var name = request.pathVariable("name"); + return client.fetch(Plugin.class, name) + .mapNotNull(plugin -> plugin.getSpec().getConfigMapName()) + .flatMap(settingConfigService::fetchConfig) + .flatMap(json -> ServerResponse.ok().bodyValue(json)); + } + + private Mono updatePluginJsonConfig(ServerRequest request) { + final var pluginName = request.pathVariable("name"); + return client.fetch(Plugin.class, pluginName) + .doOnNext(plugin -> { + String configMapName = plugin.getSpec().getConfigMapName(); + if (!StringUtils.hasText(configMapName)) { + throw new ServerWebInputException( + "Unable to complete the request because the plugin configMapName is blank"); + } + }) + .flatMap(plugin -> { + final String configMapName = plugin.getSpec().getConfigMapName(); + return request.bodyToMono(ObjectNode.class) + .switchIfEmpty( + Mono.error(new ServerWebInputException("Required request body is missing"))) + .flatMap(configMapJsonData -> + settingConfigService.upsertConfig(configMapName, configMapJsonData)); + }) + .then(ServerResponse.noContent().build()); + } + Mono changePluginRunningState(ServerRequest request) { final var name = request.pathVariable("name"); return request.bodyToMono(RunningStateRequest.class) diff --git a/application/src/main/java/run/halo/app/core/extension/service/SettingConfigService.java b/application/src/main/java/run/halo/app/core/extension/service/SettingConfigService.java new file mode 100644 index 0000000000..99bfce199a --- /dev/null +++ b/application/src/main/java/run/halo/app/core/extension/service/SettingConfigService.java @@ -0,0 +1,19 @@ +package run.halo.app.core.extension.service; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import reactor.core.publisher.Mono; +import run.halo.app.core.extension.Setting; +import run.halo.app.extension.ConfigMap; + +/** + * {@link Setting} related {@link ConfigMap} service. + * + * @author guqing + * @since 2.20.0 + */ +public interface SettingConfigService { + + Mono upsertConfig(String configMapName, ObjectNode configJsonData); + + Mono fetchConfig(String configMapName); +} diff --git a/application/src/main/java/run/halo/app/core/extension/service/impl/SettingConfigServiceImpl.java b/application/src/main/java/run/halo/app/core/extension/service/impl/SettingConfigServiceImpl.java new file mode 100644 index 0000000000..bb97003580 --- /dev/null +++ b/application/src/main/java/run/halo/app/core/extension/service/impl/SettingConfigServiceImpl.java @@ -0,0 +1,57 @@ +package run.halo.app.core.extension.service.impl; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import java.time.Duration; +import lombok.RequiredArgsConstructor; +import org.springframework.dao.OptimisticLockingFailureException; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; +import reactor.core.publisher.Mono; +import reactor.util.retry.Retry; +import run.halo.app.core.extension.Setting; +import run.halo.app.core.extension.service.SettingConfigService; +import run.halo.app.core.extension.theme.SettingUtils; +import run.halo.app.extension.ConfigMap; +import run.halo.app.extension.Metadata; +import run.halo.app.extension.ReactiveExtensionClient; + +/** + * {@link Setting} related {@link ConfigMap} service implementation. + * + * @author guqing + * @since 2.20.0 + */ +@Component +@RequiredArgsConstructor +public class SettingConfigServiceImpl implements SettingConfigService { + private final ReactiveExtensionClient client; + + @Override + public Mono upsertConfig(String configMapName, ObjectNode configJsonData) { + Assert.notNull(configMapName, "Config map name must not be null"); + Assert.notNull(configJsonData, "Config json data must not be null"); + var data = SettingUtils.settingConfigJsonToMap(configJsonData); + return Mono.defer(() -> client.fetch(ConfigMap.class, configMapName) + .flatMap(persisted -> { + persisted.setData(data); + return client.update(persisted); + })) + .retryWhen(Retry.backoff(5, Duration.ofMillis(100)) + .filter(OptimisticLockingFailureException.class::isInstance) + ) + .switchIfEmpty(Mono.defer(() -> { + var configMap = new ConfigMap(); + configMap.setMetadata(new Metadata()); + configMap.getMetadata().setName(configMapName); + configMap.setData(data); + return client.create(configMap); + })) + .then(); + } + + @Override + public Mono fetchConfig(String configMapName) { + return client.fetch(ConfigMap.class, configMapName) + .map(SettingUtils::settingConfigToJson); + } +} diff --git a/application/src/main/java/run/halo/app/core/extension/theme/SettingUtils.java b/application/src/main/java/run/halo/app/core/extension/theme/SettingUtils.java index 0a3b33bdfb..9c92e82bb8 100644 --- a/application/src/main/java/run/halo/app/core/extension/theme/SettingUtils.java +++ b/application/src/main/java/run/halo/app/core/extension/theme/SettingUtils.java @@ -129,7 +129,30 @@ public static Map mergePatch(Map modified, } } - JsonNode mapToJsonNode(Map map) { + /** + * Convert {@link Setting} related configMap data to JsonNode. + * + * @param configMap {@link ConfigMap} instance + * @return JsonNode + */ + public static ObjectNode settingConfigToJson(ConfigMap configMap) { + if (configMap.getData() == null) { + return JsonNodeFactory.instance.objectNode(); + } + return mapToJsonNode(configMap.getData()); + } + + /** + * Convert the result of {@link #settingConfigToJson(ConfigMap)} in reverse to Map. + * + * @param node JsonNode object + * @return {@link ConfigMap#getData()} + */ + public static Map settingConfigJsonToMap(ObjectNode node) { + return jsonNodeToStringMap(node); + } + + ObjectNode mapToJsonNode(Map map) { ObjectNode objectNode = JsonNodeFactory.instance.objectNode(); map.forEach((k, v) -> { if (isJson(v)) { diff --git a/application/src/main/java/run/halo/app/core/extension/theme/ThemeEndpoint.java b/application/src/main/java/run/halo/app/core/extension/theme/ThemeEndpoint.java index 9cc4210925..90ebad7a9c 100644 --- a/application/src/main/java/run/halo/app/core/extension/theme/ThemeEndpoint.java +++ b/application/src/main/java/run/halo/app/core/extension/theme/ThemeEndpoint.java @@ -9,6 +9,7 @@ import static org.springframework.http.HttpStatus.NO_CONTENT; import static org.springframework.web.reactive.function.server.RequestPredicates.contentType; +import com.fasterxml.jackson.databind.node.ObjectNode; import io.swagger.v3.oas.annotations.enums.ParameterIn; import io.swagger.v3.oas.annotations.media.Schema; import java.net.URI; @@ -38,6 +39,7 @@ import run.halo.app.core.extension.Setting; import run.halo.app.core.extension.Theme; import run.halo.app.core.extension.endpoint.CustomEndpoint; +import run.halo.app.core.extension.service.SettingConfigService; import run.halo.app.extension.ConfigMap; import run.halo.app.extension.ListResult; import run.halo.app.extension.ReactiveExtensionClient; @@ -73,6 +75,8 @@ public class ThemeEndpoint implements CustomEndpoint { private final ReactiveUrlDataBufferFetcher urlDataBufferFetcher; + private final SettingConfigService settingConfigService; + @Override public RouterFunction endpoint() { var tag = "ThemeV1alpha1Console"; @@ -161,8 +165,9 @@ public RouterFunction endpoint() { ) .PUT("themes/{name}/config", this::updateThemeConfig, builder -> builder.operationId("updateThemeConfig") - .description("Update the configMap of theme setting.") + .description("Update the configMap of theme setting. It is deprecated.") .tag(tag) + .deprecated(true) .parameter(parameterBuilder() .name("name") .in(ParameterIn.PATH) @@ -176,6 +181,24 @@ public RouterFunction endpoint() { .response(responseBuilder() .implementation(ConfigMap.class)) ) + .PUT("themes/{name}/json-config", this::updateThemeJsonConfig, + builder -> builder.operationId("updateThemeJsonConfig") + .description("Update the configMap of theme setting.") + .tag(tag) + .parameter(parameterBuilder() + .name("name") + .in(ParameterIn.PATH) + .required(true) + .implementation(String.class) + ) + .requestBody(requestBodyBuilder() + .required(true) + .content(contentBuilder().mediaType(MediaType.APPLICATION_JSON_VALUE) + .schema(schemaBuilder().implementation(ObjectNode.class)))) + .response(responseBuilder() + .responseCode(String.valueOf(NO_CONTENT.value())) + .implementation(Void.class)) + ) .PUT("themes/{name}/activation", this::activateTheme, builder -> builder.operationId("activateTheme") .description("Activate a theme by name.") @@ -235,8 +258,10 @@ public RouterFunction endpoint() { ) .GET("themes/{name}/config", this::fetchThemeConfig, builder -> builder.operationId("fetchThemeConfig") - .description("Fetch configMap of theme by configured configMapName.") + .description( + "Fetch configMap of theme by configured configMapName. It is deprecated.") .tag(tag) + .deprecated(true) .parameter(parameterBuilder() .name("name") .in(ParameterIn.PATH) @@ -246,9 +271,52 @@ public RouterFunction endpoint() { .response(responseBuilder() .implementation(ConfigMap.class)) ) + .GET("themes/{name}/json-config", this::fetchThemeJsonConfig, + builder -> builder.operationId("fetchThemeJsonConfig") + .description( + "Fetch converted json config of theme by configured configMapName.") + .tag(tag) + .parameter(parameterBuilder() + .name("name") + .in(ParameterIn.PATH) + .required(true) + .implementation(String.class) + ) + .response(responseBuilder() + .implementation(ObjectNode.class)) + ) .build(); } + private Mono fetchThemeJsonConfig(ServerRequest request) { + return themeNameInPathVariableOrActivated(request) + .flatMap(themeName -> client.fetch(Theme.class, themeName)) + .mapNotNull(theme -> theme.getSpec().getConfigMapName()) + .flatMap(settingConfigService::fetchConfig) + .flatMap(json -> ServerResponse.ok().bodyValue(json)); + } + + private Mono updateThemeJsonConfig(ServerRequest request) { + final var themeName = request.pathVariable("name"); + return client.fetch(Theme.class, themeName) + .doOnNext(theme -> { + String configMapName = theme.getSpec().getConfigMapName(); + if (StringUtils.isBlank(configMapName)) { + throw new ServerWebInputException( + "Unable to complete the request because the theme configMapName is blank."); + } + }) + .flatMap(theme -> { + final var configMapName = theme.getSpec().getConfigMapName(); + return request.bodyToMono(ObjectNode.class) + .switchIfEmpty( + Mono.error(new ServerWebInputException("Required request body is missing"))) + .flatMap(configJsonData -> + settingConfigService.upsertConfig(configMapName, configJsonData)); + }) + .then(ServerResponse.noContent().build()); + } + private Mono invalidateCache(ServerRequest request) { final var name = request.pathVariable("name"); return client.get(Theme.class, name) @@ -306,6 +374,7 @@ private Mono activateTheme(ServerRequest request) { .flatMap(activatedTheme -> ServerResponse.ok().bodyValue(activatedTheme)); } + @Deprecated(since = "2.20.0", forRemoval = true) private Mono updateThemeConfig(ServerRequest request) { final var themeName = request.pathVariable("name"); return client.fetch(Theme.class, themeName) @@ -343,6 +412,7 @@ private Mono updateThemeConfig(ServerRequest request) { .flatMap(configMap -> ServerResponse.ok().bodyValue(configMap)); } + @Deprecated(since = "2.20.0", forRemoval = true) private Mono fetchThemeConfig(ServerRequest request) { return themeNameInPathVariableOrActivated(request) .flatMap(themeName -> client.fetch(Theme.class, themeName)) diff --git a/application/src/main/resources/extensions/role-template-plugin.yaml b/application/src/main/resources/extensions/role-template-plugin.yaml index 82ff040a29..8bdd23e394 100644 --- a/application/src/main/resources/extensions/role-template-plugin.yaml +++ b/application/src/main/resources/extensions/role-template-plugin.yaml @@ -16,8 +16,8 @@ rules: resources: [ "plugins" ] verbs: [ "create", "patch", "update", "delete", "deletecollection" ] - apiGroups: [ "api.console.halo.run" ] - resources: [ "plugins/upgrade", "plugins/resetconfig", "plugins/config", "plugins/reload", - "plugins/install-from-uri", "plugins/upgrade-from-uri", "plugins/plugin-state" ] + resources: [ "plugins/upgrade", "plugins/resetconfig", "plugins/config", "plugins/json-config", + "plugins/reload", "plugins/install-from-uri", "plugins/upgrade-from-uri", "plugins/plugin-state" ] verbs: [ "*" ] - apiGroups: [ "api.console.halo.run" ] resources: [ "plugin-presets" ] @@ -41,5 +41,5 @@ rules: resources: [ "plugins" ] verbs: [ "get", "list" ] - apiGroups: [ "api.console.halo.run" ] - resources: [ "plugins", "plugins/setting", "plugins/config" ] + resources: [ "plugins", "plugins/setting", "plugins/config", "plugins/json-config" ] verbs: [ "get", "list" ] diff --git a/application/src/main/resources/extensions/role-template-theme.yaml b/application/src/main/resources/extensions/role-template-theme.yaml index 6b6c375bf5..ea46fecbaa 100644 --- a/application/src/main/resources/extensions/role-template-theme.yaml +++ b/application/src/main/resources/extensions/role-template-theme.yaml @@ -15,8 +15,8 @@ rules: resources: [ "themes" ] verbs: [ "*" ] - apiGroups: [ "api.console.halo.run" ] - resources: [ "themes", "themes/reload", "themes/resetconfig", "themes/config", "themes/activation", - "themes/install-from-uri", "themes/upgrade-from-uri", "themes/invalidate-cache" ] + resources: [ "themes", "themes/reload", "themes/resetconfig", "themes/config", "themes/json-config", + "themes/activation", "themes/install-from-uri", "themes/upgrade-from-uri", "themes/invalidate-cache" ] verbs: [ "*" ] - nonResourceURLs: [ "/apis/api.console.halo.run/themes/install" ] verbs: [ "create" ] @@ -37,6 +37,6 @@ rules: resources: [ "themes" ] verbs: [ "get", "list" ] - apiGroups: [ "api.console.halo.run" ] - resources: [ "themes", "themes/activation", "themes/setting", "themes/config" ] + resources: [ "themes", "themes/activation", "themes/setting", "themes/config", "themes/json-config" ] verbs: [ "get", "list" ] diff --git a/application/src/test/java/run/halo/app/core/extension/endpoint/PluginEndpointTest.java b/application/src/test/java/run/halo/app/core/extension/endpoint/PluginEndpointTest.java index 3de56eb726..5e53efd144 100644 --- a/application/src/test/java/run/halo/app/core/extension/endpoint/PluginEndpointTest.java +++ b/application/src/test/java/run/halo/app/core/extension/endpoint/PluginEndpointTest.java @@ -22,6 +22,7 @@ import java.nio.file.Paths; import java.time.Instant; import java.util.List; +import java.util.Map; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -45,6 +46,7 @@ import run.halo.app.core.extension.Plugin; import run.halo.app.core.extension.Setting; import run.halo.app.core.extension.service.PluginService; +import run.halo.app.core.extension.service.SettingConfigService; import run.halo.app.extension.ConfigMap; import run.halo.app.extension.ListOptions; import run.halo.app.extension.ListResult; @@ -67,6 +69,9 @@ class PluginEndpointTest { @Mock PluginService pluginService; + @Mock + SettingConfigService settingConfigService; + @Spy WebProperties webProperties = new WebProperties(); @@ -278,6 +283,22 @@ void updateWhenConfigMapNameMatch() { .exchange() .expectStatus().isOk(); } + + @Test + void updateJsonConfigTest() { + Plugin plugin = createPlugin("fake-plugin"); + plugin.getSpec().setConfigMapName("fake-config-map"); + + when(client.fetch(eq(Plugin.class), eq("fake-plugin"))).thenReturn(Mono.just(plugin)); + when(settingConfigService.upsertConfig(eq("fake-config-map"), any())) + .thenReturn(Mono.empty()); + + webClient.put() + .uri("/plugins/fake-plugin/json-config") + .body(Mono.just(Map.of()), Map.class) + .exchange() + .expectStatus().is2xxSuccessful(); + } } @Nested @@ -325,6 +346,23 @@ void fetchConfig() { verify(client).fetch(eq(ConfigMap.class), eq("fake-config")); verify(client).fetch(eq(Plugin.class), eq("fake")); } + + @Test + void fetchJsonConfig() { + Plugin plugin = createPlugin("fake"); + plugin.getSpec().setConfigMapName("fake-config"); + + when(settingConfigService.fetchConfig(eq("fake-config"))) + .thenReturn(Mono.empty()); + when(client.fetch(eq(Plugin.class), eq("fake"))).thenReturn(Mono.just(plugin)); + webClient.get() + .uri("/plugins/fake/json-config") + .exchange() + .expectStatus().isOk(); + + verify(settingConfigService).fetchConfig(eq("fake-config")); + verify(client).fetch(eq(Plugin.class), eq("fake")); + } } Plugin createPlugin(String name) { diff --git a/application/src/test/java/run/halo/app/core/extension/theme/ThemeEndpointTest.java b/application/src/test/java/run/halo/app/core/extension/theme/ThemeEndpointTest.java index 4fc3194bbe..ba529755ce 100644 --- a/application/src/test/java/run/halo/app/core/extension/theme/ThemeEndpointTest.java +++ b/application/src/test/java/run/halo/app/core/extension/theme/ThemeEndpointTest.java @@ -14,6 +14,7 @@ import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Map; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; @@ -33,6 +34,7 @@ import reactor.core.publisher.Mono; import run.halo.app.core.extension.Setting; import run.halo.app.core.extension.Theme; +import run.halo.app.core.extension.service.SettingConfigService; import run.halo.app.extension.ConfigMap; import run.halo.app.extension.Metadata; import run.halo.app.extension.ReactiveExtensionClient; @@ -69,6 +71,9 @@ class ThemeEndpointTest { @Mock private ReactiveUrlDataBufferFetcher urlDataBufferFetcher; + @Mock + private SettingConfigService settingConfigService; + @InjectMocks ThemeEndpoint themeEndpoint; @@ -298,8 +303,25 @@ void updateWhenConfigMapNameMatch() { .exchange() .expectStatus().isOk(); } - } + @Test + void updateJsonConfigTest() { + Theme theme = new Theme(); + theme.setMetadata(new Metadata()); + theme.setSpec(new Theme.ThemeSpec()); + theme.getSpec().setConfigMapName("fake-config-map"); + + when(client.fetch(eq(Theme.class), eq("fake-theme"))).thenReturn(Mono.just(theme)); + when(settingConfigService.upsertConfig(eq("fake-config-map"), any())) + .thenReturn(Mono.empty()); + + webTestClient.put() + .uri("/themes/fake-theme/json-config") + .body(Mono.just(Map.of()), Map.class) + .exchange() + .expectStatus().is2xxSuccessful(); + } + } @Test void fetchActivatedTheme() { @@ -360,4 +382,24 @@ void fetchThemeConfig() { verify(client).fetch(eq(ConfigMap.class), eq("fake-config")); verify(client).fetch(eq(Theme.class), eq("fake")); } + + @Test + void fetchThemeJsonConfigTest() { + Theme theme = new Theme(); + theme.setMetadata(new Metadata()); + theme.getMetadata().setName("fake"); + theme.setSpec(new Theme.ThemeSpec()); + theme.getSpec().setConfigMapName("fake-config"); + + when(settingConfigService.fetchConfig(eq("fake-config"))).thenReturn(Mono.empty()); + + when(client.fetch(eq(Theme.class), eq("fake"))).thenReturn(Mono.just(theme)); + webTestClient.get() + .uri("/themes/fake/json-config") + .exchange() + .expectStatus().isOk(); + + verify(settingConfigService).fetchConfig(eq("fake-config")); + verify(client).fetch(eq(Theme.class), eq("fake")); + } } \ No newline at end of file