Provide system config endpoint (#3182)

#### What type of PR is this?

/kind feature
/area core
/milestone 2.2.x

#### What this PR does / why we need it:

Provide `globalconfig` actuator endpoint to let console and theme know how to do according various system configuration. The endpoint allows anonymous users to access, but other actuator endpoints can be accessed by admin users.

#### Which issue(s) this PR fixes:

Fixes https://github.com/halo-dev/halo/issues/3055

#### Special notes for your reviewer:

Try to request <http://localhost:8090/actuator/globalinfo> and see the result.

```json
{
   "externalUrl":"http://localhost:8090",
   "timeZone":"Asia/Shanghai",
   "locale":"en_US",
   "allowComments":true,
   "allowRegistration":false
}
```

You can request <http://localhost:8090/actuator/info> to see more detail as well.

```json
{
  "git": {
    "branch": "feat/system-info",
    "commit": {
      "id": "ca4e93d",
      "time": "2023-01-19T08:56:15Z"
    }
  },
  "build": {
    "artifact": "halo",
    "name": "halo",
    "time": "2023-01-29T15:04:42.151Z",
    "version": "2.2.0-SNAPSHOT",
    "group": "run.halo.app"
  },
  "java": {
    "version": "17.0.6",
    "vendor": {
      "name": "Amazon.com Inc.",
      "version": "Corretto-17.0.6.10.1"
    },
    "runtime": {
      "name": "OpenJDK Runtime Environment",
      "version": "17.0.6+10-LTS"
    },
    "jvm": {
      "name": "OpenJDK 64-Bit Server VM",
      "vendor": "Amazon.com Inc.",
      "version": "17.0.6+10-LTS"
    }
  },
  "os": {
    "name": "Windows 11",
    "version": "10.0",
    "arch": "amd64"
  }
}
```

#### Does this PR introduce a user-facing change?

```release-note
提供系统配置详情端口
```
pull/3185/head
John Niang 2023-01-30 15:20:11 +08:00 committed by GitHub
parent 3bd0a09764
commit 1810255aea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 157 additions and 3 deletions

View File

@ -1,6 +1,7 @@
plugins {
id 'org.springframework.boot' version '3.0.2'
id 'io.spring.dependency-management' version '1.1.0'
id "com.gorylenko.gradle-git-properties" version "2.3.2"
id "checkstyle"
id 'java'
}

View File

@ -1,8 +1,9 @@
package run.halo.app;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.integration.IntegrationAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.scheduling.annotation.EnableScheduling;
import run.halo.app.infra.properties.HaloProperties;
@ -22,7 +23,9 @@ import run.halo.app.infra.properties.HaloProperties;
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
new SpringApplicationBuilder(Application.class)
.applicationStartup(new BufferingApplicationStartup(1024))
.run(args);
}
}

View File

@ -0,0 +1,87 @@
package run.halo.app.actuator;
import java.net.URI;
import java.util.Locale;
import java.util.TimeZone;
import lombok.Data;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.web.annotation.WebEndpoint;
import org.springframework.stereotype.Component;
import run.halo.app.extension.ConfigMap;
import run.halo.app.infra.ExternalUrlSupplier;
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
import run.halo.app.infra.SystemSetting;
import run.halo.app.infra.SystemSetting.Comment;
import run.halo.app.infra.SystemSetting.User;
@WebEndpoint(id = "globalinfo")
@Component
public class GlobalInfoEndpoint {
private final ObjectProvider<SystemConfigurableEnvironmentFetcher> systemConfigFetcher;
private final ExternalUrlSupplier externalUrl;
public GlobalInfoEndpoint(
ObjectProvider<SystemConfigurableEnvironmentFetcher> systemConfigFetcher,
ExternalUrlSupplier externalUrl) {
this.systemConfigFetcher = systemConfigFetcher;
this.externalUrl = externalUrl;
}
@ReadOperation
public GlobalInfo globalInfo() {
final var info = new GlobalInfo();
info.setExternalUrl(externalUrl.get());
info.setLocale(Locale.getDefault());
info.setTimeZone(TimeZone.getDefault());
systemConfigFetcher.ifAvailable(fetcher -> fetcher.getConfigMapBlocking()
.ifPresent(configMap -> {
handleCommentSetting(info, configMap);
handleUserSetting(info, configMap);
}));
return info;
}
@Data
public static class GlobalInfo {
private URI externalUrl;
private TimeZone timeZone;
private Locale locale;
private boolean allowComments;
private boolean allowAnonymousComments;
private boolean allowRegistration;
}
private void handleCommentSetting(GlobalInfo info, ConfigMap configMap) {
var comment = SystemSetting.get(configMap, Comment.GROUP, Comment.class);
if (comment == null) {
info.setAllowComments(true);
info.setAllowAnonymousComments(true);
} else {
info.setAllowComments(comment.getEnable() != null && comment.getEnable());
info.setAllowAnonymousComments(
comment.getSystemUserOnly() == null || !comment.getSystemUserOnly());
}
}
private void handleUserSetting(GlobalInfo info, ConfigMap configMap) {
var user = SystemSetting.get(configMap, User.GROUP, User.class);
if (user == null) {
info.setAllowRegistration(false);
} else {
info.setAllowRegistration(
user.getAllowRegistration() != null && user.getAllowRegistration());
}
}
}

View File

@ -51,7 +51,8 @@ public class WebServerSecurityConfig {
ObjectProvider<SecurityConfigurer> securityConfigurers,
ServerSecurityContextRepository securityContextRepository) {
http.securityMatcher(pathMatchers("/api/**", "/apis/**", "/login", "/logout"))
http.securityMatcher(
pathMatchers("/api/**", "/apis/**", "/login", "/logout", "/actuator/**"))
.authorizeExchange().anyExchange()
.access(new RequestInfoAuthorizationManager(roleService)).and()
.anonymous(spec -> {

View File

@ -3,6 +3,9 @@ package run.halo.app.infra;
import java.util.LinkedHashMap;
import java.util.Set;
import lombok.Data;
import org.springframework.boot.convert.ApplicationConversionService;
import run.halo.app.extension.ConfigMap;
import run.halo.app.infra.utils.JsonUtils;
/**
* TODO Optimization value acquisition.
@ -102,4 +105,16 @@ public class SystemSetting {
}
public static <T> T get(ConfigMap configMap, String key, Class<T> type) {
var data = configMap.getData();
var valueString = data.get(key);
if (valueString == null) {
return null;
}
var conversionService = ApplicationConversionService.getSharedInstance();
if (conversionService.canConvert(String.class, type)) {
return conversionService.convert(valueString, type);
}
return JsonUtils.jsonToObject(valueString, type);
}
}

View File

@ -50,3 +50,18 @@ logging:
max-file-size: 10MB
total-size-cap: 1GB
max-history: 0
management:
endpoints:
web:
exposure:
include: ["health", "info", "startup", "globalinfo"]
endpoint:
health:
probes:
enabled: true
info:
java:
enabled: true
os:
enabled: true

View File

@ -17,3 +17,5 @@ rules:
verbs: [ "*" ]
- nonResourceURLs: [ "/apis/api.halo.run/v1alpha1/trackers/*" ]
verbs: [ "create" ]
- nonResourceURLs: [ "/actuator/globalinfo", "/actuator/health", "/actuator/health/*"]
verbs: [ "get" ]

View File

@ -1,9 +1,14 @@
package run.halo.app.infra;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.HashMap;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import run.halo.app.extension.ConfigMap;
import run.halo.app.infra.SystemSetting.Comment;
import run.halo.app.infra.SystemSetting.ExtensionPointEnabled;
import run.halo.app.infra.utils.JsonUtils;
@ -27,4 +32,29 @@ class SystemSettingTest {
}
}
@Test
void shouldGetConfigFromJson() {
var configMap = new ConfigMap();
configMap.putDataItem("comment", """
{"enable": true}
""");
var comment = SystemSetting.get(configMap, Comment.GROUP, Comment.class);
assertTrue(comment.getEnable());
}
@Test
void shouldGetNullIfKeyNotExist() {
var configMap = new ConfigMap();
configMap.setData(new HashMap<>());
String fake = SystemSetting.get(configMap, "fake-key", String.class);
assertNull(fake);
}
@Test
void shouldGetConfigViaConversionService() {
var configMap = new ConfigMap();
configMap.putDataItem("int", "100");
var integer = SystemSetting.get(configMap, "int", Integer.class);
assertEquals(100, integer);
}
}