diff --git a/src/main/java/run/halo/app/core/extension/endpoint/PluginEndpoint.java b/src/main/java/run/halo/app/core/extension/endpoint/PluginEndpoint.java index 82532f93f..164792e6a 100644 --- a/src/main/java/run/halo/app/core/extension/endpoint/PluginEndpoint.java +++ b/src/main/java/run/halo/app/core/extension/endpoint/PluginEndpoint.java @@ -104,6 +104,23 @@ public class PluginEndpoint implements CustomEndpoint { .content(contentBuilder().mediaType(MediaType.MULTIPART_FORM_DATA_VALUE) .schema(schemaBuilder().implementation(InstallRequest.class)))) ) + .PUT("plugins/{name}/config", this::updatePluginConfig, + builder -> builder.operationId("updatePluginConfig") + .description("Update the configMap 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(ConfigMap.class)))) + .response(responseBuilder() + .implementation(ConfigMap.class)) + ) .PUT("plugins/{name}/reset-config", this::resetSettingConfig, builder -> builder.operationId("ResetPluginConfig") .description("Reset the configMap of plugin setting.") @@ -124,9 +141,88 @@ public class PluginEndpoint implements CustomEndpoint { .response(responseBuilder().implementation(generateGenericClass(Plugin.class))); buildParametersFromType(builder, ListRequest.class); }) + .GET("plugins/{name}/setting", this::fetchPluginSetting, + builder -> builder.operationId("fetchPluginSetting") + .description("Fetch setting of plugin.") + .tag(tag) + .parameter(parameterBuilder() + .name("name") + .in(ParameterIn.PATH) + .required(true) + .implementation(String.class) + ) + .response(responseBuilder() + .implementation(Setting.class)) + ) + .GET("plugins/{name}/config", this::fetchPluginConfig, + builder -> builder.operationId("fetchPluginConfig") + .description("Fetch configMap of plugin by configured configMapName.") + .tag(tag) + .parameter(parameterBuilder() + .name("name") + .in(ParameterIn.PATH) + .required(true) + .implementation(String.class) + ) + .response(responseBuilder() + .implementation(ConfigMap.class)) + ) .build(); } + private Mono fetchPluginConfig(ServerRequest request) { + final var name = request.pathVariable("name"); + return client.fetch(Plugin.class, name) + .mapNotNull(plugin -> plugin.getSpec().getConfigMapName()) + .flatMap(configMapName -> client.fetch(ConfigMap.class, configMapName)) + .flatMap(configMap -> ServerResponse.ok().bodyValue(configMap)); + } + + private Mono fetchPluginSetting(ServerRequest request) { + final var name = request.pathVariable("name"); + return client.fetch(Plugin.class, name) + .mapNotNull(plugin -> plugin.getSpec().getSettingName()) + .flatMap(settingName -> client.fetch(Setting.class, settingName)) + .flatMap(setting -> ServerResponse.ok().bodyValue(setting)); + } + + private Mono updatePluginConfig(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(ConfigMap.class) + .doOnNext(configMapToUpdate -> { + var configMapNameToUpdate = configMapToUpdate.getMetadata().getName(); + if (!configMapName.equals(configMapNameToUpdate)) { + throw new ServerWebInputException( + "The name from the request body does not match the plugin " + + "configMapName name."); + } + }) + .flatMap(configMapToUpdate -> client.fetch(ConfigMap.class, configMapName) + .map(persisted -> { + configMapToUpdate.getMetadata() + .setVersion(persisted.getMetadata().getVersion()); + return configMapToUpdate; + }) + .switchIfEmpty(client.create(configMapToUpdate)) + ) + .flatMap(client::update) + .retryWhen(Retry.backoff(5, Duration.ofMillis(300)) + .filter(OptimisticLockingFailureException.class::isInstance) + ); + }) + .flatMap(configMap -> ServerResponse.ok().bodyValue(configMap)); + } + private Mono resetSettingConfig(ServerRequest request) { String name = request.pathVariable("name"); return client.fetch(Plugin.class, name) diff --git a/src/main/resources/extensions/role-template-plugin.yaml b/src/main/resources/extensions/role-template-plugin.yaml index 63593a629..6e433d8df 100644 --- a/src/main/resources/extensions/role-template-plugin.yaml +++ b/src/main/resources/extensions/role-template-plugin.yaml @@ -16,7 +16,7 @@ rules: resources: [ "plugins" ] verbs: [ "create", "patch", "update", "delete", "deletecollection" ] - apiGroups: [ "api.console.halo.run" ] - resources: [ "plugins/upgrade", "plugins/resetconfig" ] + resources: [ "plugins/upgrade", "plugins/resetconfig", "plugins/config" ] verbs: [ "*" ] - nonResourceURLs: [ "/apis/api.console.halo.run/v1alpha1/plugins/*" ] verbs: [ "create" ] @@ -37,5 +37,5 @@ rules: resources: [ "plugins" ] verbs: [ "get", "list" ] - apiGroups: [ "api.console.halo.run" ] - resources: [ "plugins" ] + resources: [ "plugins", "plugins/setting", "plugins/config" ] verbs: [ "get", "list" ] diff --git a/src/test/java/run/halo/app/core/extension/endpoint/PluginEndpointTest.java b/src/test/java/run/halo/app/core/extension/endpoint/PluginEndpointTest.java index c6aed940d..102213a57 100644 --- a/src/test/java/run/halo/app/core/extension/endpoint/PluginEndpointTest.java +++ b/src/test/java/run/halo/app/core/extension/endpoint/PluginEndpointTest.java @@ -4,6 +4,7 @@ import static java.util.Objects.requireNonNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.same; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.never; @@ -38,6 +39,8 @@ import org.springframework.http.client.MultipartBodyBuilder; import org.springframework.test.web.reactive.server.WebTestClient; import reactor.core.publisher.Mono; import run.halo.app.core.extension.Plugin; +import run.halo.app.core.extension.Setting; +import run.halo.app.extension.ConfigMap; import run.halo.app.extension.ListResult; import run.halo.app.extension.Metadata; import run.halo.app.extension.ReactiveExtensionClient; @@ -306,6 +309,115 @@ class PluginEndpointTest { } + @Nested + class UpdatePluginConfigTest { + WebTestClient webClient; + + @BeforeEach + void setUp() { + webClient = WebTestClient.bindToRouterFunction(endpoint.endpoint()) + .build(); + } + + @Test + void updateWhenConfigMapNameIsNull() { + Plugin plugin = createPlugin("fake-plugin"); + plugin.getSpec().setConfigMapName(null); + + when(client.fetch(eq(Plugin.class), eq("fake-plugin"))).thenReturn(Mono.just(plugin)); + webClient.put() + .uri("/plugins/fake-plugin/config") + .exchange() + .expectStatus().isBadRequest(); + } + + @Test + void updateWhenConfigMapNameNotMatch() { + Plugin plugin = createPlugin("fake-plugin"); + plugin.getSpec().setConfigMapName("fake-config-map"); + + when(client.fetch(eq(Plugin.class), eq("fake-plugin"))).thenReturn(Mono.just(plugin)); + webClient.put() + .uri("/plugins/fake-plugin/config") + .body(Mono.fromSupplier(() -> { + ConfigMap configMap = new ConfigMap(); + configMap.setMetadata(new Metadata()); + configMap.getMetadata().setName("not-match"); + return configMap; + }), ConfigMap.class) + .exchange() + .expectStatus().isBadRequest(); + } + + @Test + void updateWhenConfigMapNameMatch() { + 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(client.fetch(eq(ConfigMap.class), eq("fake-config-map"))).thenReturn(Mono.empty()); + when(client.create(any(ConfigMap.class))).thenReturn(Mono.empty()); + + webClient.put() + .uri("/plugins/fake-plugin/config") + .body(Mono.fromSupplier(() -> { + ConfigMap configMap = new ConfigMap(); + configMap.setMetadata(new Metadata()); + configMap.getMetadata().setName("fake-config-map"); + return configMap; + }), ConfigMap.class) + .exchange() + .expectStatus().isOk(); + } + } + + @Nested + class PluginConfigAndSettingFetchTest { + WebTestClient webClient; + + @BeforeEach + void setUp() { + webClient = WebTestClient.bindToRouterFunction(endpoint.endpoint()) + .build(); + } + + @Test + void fetchSetting() { + Plugin plugin = createPlugin("fake"); + plugin.getSpec().setSettingName("fake-setting"); + + when(client.fetch(eq(Setting.class), eq("fake-setting"))) + .thenReturn(Mono.just(new Setting())); + + when(client.fetch(eq(Plugin.class), eq("fake"))).thenReturn(Mono.just(plugin)); + webClient.get() + .uri("/plugins/fake/setting") + .exchange() + .expectStatus().isOk(); + + verify(client).fetch(eq(Setting.class), eq("fake-setting")); + verify(client).fetch(eq(Plugin.class), eq("fake")); + } + + @Test + void fetchConfig() { + Plugin plugin = createPlugin("fake"); + plugin.getSpec().setConfigMapName("fake-config"); + + when(client.fetch(eq(ConfigMap.class), eq("fake-config"))) + .thenReturn(Mono.just(new ConfigMap())); + + when(client.fetch(eq(Plugin.class), eq("fake"))).thenReturn(Mono.just(plugin)); + webClient.get() + .uri("/plugins/fake/config") + .exchange() + .expectStatus().isOk(); + + verify(client).fetch(eq(ConfigMap.class), eq("fake-config")); + verify(client).fetch(eq(Plugin.class), eq("fake")); + } + } + Plugin createPlugin(String name) { return createPlugin(name, "fake display name", "fake description", null); }