mirror of https://github.com/halo-dev/halo
Initialize required extensions when system starts up (#2274)
#### What type of PR is this? /kind improvement /area core /milestone 2.0 #### What this PR does / why we need it: This PR makes required extensions got initialized when system starts up. Of course, we can stop the initialization by setting property `halo.required-extension-disabled=true`. Secondly, we are using [PathMatchingResourcePatternResolver](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/core/io/support/PathMatchingResourcePatternResolver.html) support more functional Extension locations, please see the doc for more. #### Does this PR introduce a user-facing change? ```release-note None ```pull/2278/head
parent
71f9209006
commit
5eec9da2e6
|
@ -1,21 +1,17 @@
|
||||||
package run.halo.app.infra;
|
package run.halo.app.infra;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.util.HashSet;
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Stream;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||||
import org.springframework.context.ApplicationListener;
|
import org.springframework.context.ApplicationListener;
|
||||||
import org.springframework.core.io.FileSystemResource;
|
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
|
||||||
import org.springframework.lang.NonNull;
|
import org.springframework.lang.NonNull;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.util.CollectionUtils;
|
import org.springframework.util.CollectionUtils;
|
||||||
import org.thymeleaf.util.StringUtils;
|
|
||||||
import run.halo.app.extension.ExtensionClient;
|
import run.halo.app.extension.ExtensionClient;
|
||||||
import run.halo.app.extension.Unstructured;
|
import run.halo.app.extension.Unstructured;
|
||||||
import run.halo.app.infra.properties.HaloProperties;
|
import run.halo.app.infra.properties.HaloProperties;
|
||||||
|
@ -32,6 +28,9 @@ import run.halo.app.infra.utils.YamlUnstructuredLoader;
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
public class ExtensionResourceInitializer implements ApplicationListener<ApplicationReadyEvent> {
|
public class ExtensionResourceInitializer implements ApplicationListener<ApplicationReadyEvent> {
|
||||||
|
|
||||||
|
public static final Set<String> REQUIRED_EXTENSION_LOCATIONS =
|
||||||
|
Set.of("classpath:/extensions/*.yaml", "classpath:/extensions/*.yml");
|
||||||
private final HaloProperties haloProperties;
|
private final HaloProperties haloProperties;
|
||||||
private final ExtensionClient extensionClient;
|
private final ExtensionClient extensionClient;
|
||||||
|
|
||||||
|
@ -43,15 +42,25 @@ public class ExtensionResourceInitializer implements ApplicationListener<Applica
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onApplicationEvent(@NonNull ApplicationReadyEvent event) {
|
public void onApplicationEvent(@NonNull ApplicationReadyEvent event) {
|
||||||
Set<String> extensionLocations = haloProperties.getInitialExtensionLocations();
|
var locations = new HashSet<String>();
|
||||||
if (!CollectionUtils.isEmpty(extensionLocations)) {
|
if (!haloProperties.isRequiredExtensionDisabled()) {
|
||||||
|
locations.addAll(REQUIRED_EXTENSION_LOCATIONS);
|
||||||
|
}
|
||||||
|
if (haloProperties.getInitialExtensionLocations() != null) {
|
||||||
|
locations.addAll(haloProperties.getInitialExtensionLocations());
|
||||||
|
}
|
||||||
|
|
||||||
Resource[] resources = extensionLocations.stream()
|
if (CollectionUtils.isEmpty(locations)) {
|
||||||
.map(this::listYamlFiles)
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var resources = locations.stream()
|
||||||
|
.map(this::listResources)
|
||||||
.flatMap(List::stream)
|
.flatMap(List::stream)
|
||||||
|
.distinct()
|
||||||
.toArray(Resource[]::new);
|
.toArray(Resource[]::new);
|
||||||
|
|
||||||
log.debug("Initialization loaded [{}] resources to establish.", resources.length);
|
log.info("Initializing [{}] extensions in locations: {}", resources.length, locations);
|
||||||
|
|
||||||
new YamlUnstructuredLoader(resources).load()
|
new YamlUnstructuredLoader(resources).load()
|
||||||
.forEach(unstructured -> extensionClient.fetch(unstructured.groupVersionKind(),
|
.forEach(unstructured -> extensionClient.fetch(unstructured.groupVersionKind(),
|
||||||
|
@ -59,24 +68,20 @@ public class ExtensionResourceInitializer implements ApplicationListener<Applica
|
||||||
.ifPresentOrElse(persisted -> {
|
.ifPresentOrElse(persisted -> {
|
||||||
unstructured.getMetadata()
|
unstructured.getMetadata()
|
||||||
.setVersion(persisted.getMetadata().getVersion());
|
.setVersion(persisted.getMetadata().getVersion());
|
||||||
|
// TODO Patch the unstructured instead of update it in the future
|
||||||
extensionClient.update(unstructured);
|
extensionClient.update(unstructured);
|
||||||
}, () -> extensionClient.create(unstructured)));
|
}, () -> extensionClient.create(unstructured)));
|
||||||
|
|
||||||
|
log.info("Initialized [{}] extensions in locations: {}", resources.length, locations);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Resource> listResources(String location) {
|
||||||
|
var resolver = new PathMatchingResourcePatternResolver();
|
||||||
|
try {
|
||||||
|
return List.of(resolver.getResources(location));
|
||||||
|
} catch (IOException ie) {
|
||||||
|
throw new IllegalArgumentException("Invalid extension location: " + location, ie);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<FileSystemResource> listYamlFiles(String location) {
|
|
||||||
try (Stream<Path> walk = Files.walk(Paths.get(location))) {
|
|
||||||
return walk.filter(this::isYamlFile)
|
|
||||||
.map(path -> new FileSystemResource(path.toFile()))
|
|
||||||
.toList();
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new IllegalArgumentException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isYamlFile(Path pathname) {
|
|
||||||
Path fileName = pathname.getFileName();
|
|
||||||
return StringUtils.endsWith(fileName, ".yaml")
|
|
||||||
|| StringUtils.endsWith(fileName, ".yml");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,13 @@ public class HaloProperties {
|
||||||
|
|
||||||
private Set<String> initialExtensionLocations = new HashSet<>();
|
private Set<String> initialExtensionLocations = new HashSet<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This property could stop initializing required Extensions defined in classpath.
|
||||||
|
* See {@link run.halo.app.infra.ExtensionResourceInitializer#REQUIRED_EXTENSION_LOCATIONS}
|
||||||
|
* for more.
|
||||||
|
*/
|
||||||
|
private boolean requiredExtensionDisabled;
|
||||||
|
|
||||||
private final ExtensionProperties extension = new ExtensionProperties();
|
private final ExtensionProperties extension = new ExtensionProperties();
|
||||||
|
|
||||||
private final SecurityProperties security = new SecurityProperties();
|
private final SecurityProperties security = new SecurityProperties();
|
||||||
|
|
|
@ -10,10 +10,12 @@ import java.io.IOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
@ -22,6 +24,7 @@ import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
import org.skyscreamer.jsonassert.JSONAssert;
|
import org.skyscreamer.jsonassert.JSONAssert;
|
||||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||||
|
import org.springframework.util.FileSystemUtils;
|
||||||
import run.halo.app.extension.ExtensionClient;
|
import run.halo.app.extension.ExtensionClient;
|
||||||
import run.halo.app.extension.GroupVersionKind;
|
import run.halo.app.extension.GroupVersionKind;
|
||||||
import run.halo.app.extension.Unstructured;
|
import run.halo.app.extension.Unstructured;
|
||||||
|
@ -46,13 +49,19 @@ class ExtensionResourceInitializerTest {
|
||||||
|
|
||||||
private ExtensionResourceInitializer extensionResourceInitializer;
|
private ExtensionResourceInitializer extensionResourceInitializer;
|
||||||
|
|
||||||
|
List<Path> dirsToClean;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() throws IOException {
|
void setUp() throws IOException {
|
||||||
extensionResourceInitializer =
|
extensionResourceInitializer =
|
||||||
new ExtensionResourceInitializer(haloProperties, extensionClient);
|
new ExtensionResourceInitializer(haloProperties, extensionClient);
|
||||||
|
|
||||||
|
dirsToClean = new ArrayList<>(2);
|
||||||
|
|
||||||
Path tempDirectory = Files.createTempDirectory("extension-resource-initializer-test");
|
Path tempDirectory = Files.createTempDirectory("extension-resource-initializer-test");
|
||||||
Path multiDirectory = Files.createDirectories(tempDirectory.resolve("a/b/c"));
|
dirsToClean.add(tempDirectory);
|
||||||
|
Path multiDirectory =
|
||||||
|
Files.createDirectories(tempDirectory.resolve("a").resolve("b").resolve("c"));
|
||||||
Files.writeString(tempDirectory.resolve("hello.yml"), """
|
Files.writeString(tempDirectory.resolve("hello.yml"), """
|
||||||
kind: FakeExtension
|
kind: FakeExtension
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
|
@ -79,8 +88,9 @@ class ExtensionResourceInitializerTest {
|
||||||
StandardCharsets.UTF_8);
|
StandardCharsets.UTF_8);
|
||||||
|
|
||||||
// test file in directory
|
// test file in directory
|
||||||
Path filePath = Files.createTempDirectory("extension-resource-file-test")
|
Path secondTempDir = Files.createTempDirectory("extension-resource-file-test");
|
||||||
.resolve("good.yml");
|
dirsToClean.add(secondTempDir);
|
||||||
|
Path filePath = secondTempDir.resolve("good.yml");
|
||||||
Files.writeString(filePath, """
|
Files.writeString(filePath, """
|
||||||
kind: FakeExtension
|
kind: FakeExtension
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
|
@ -92,11 +102,23 @@ class ExtensionResourceInitializerTest {
|
||||||
StandardCharsets.UTF_8);
|
StandardCharsets.UTF_8);
|
||||||
|
|
||||||
when(haloProperties.getInitialExtensionLocations())
|
when(haloProperties.getInitialExtensionLocations())
|
||||||
.thenReturn(Set.of(tempDirectory.toString(), filePath.toString()));
|
.thenReturn(Set.of("file:" + tempDirectory + "/**/*.yaml",
|
||||||
|
"file:" + tempDirectory + "/**/*.yml",
|
||||||
|
"file:" + filePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void cleanUp() throws IOException {
|
||||||
|
if (dirsToClean != null) {
|
||||||
|
for (var dir : dirsToClean) {
|
||||||
|
FileSystemUtils.deleteRecursively(dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void onApplicationEvent() throws JSONException {
|
void onApplicationEvent() throws JSONException {
|
||||||
|
when(haloProperties.isRequiredExtensionDisabled()).thenReturn(true);
|
||||||
ArgumentCaptor<Unstructured> argumentCaptor = ArgumentCaptor.forClass(Unstructured.class);
|
ArgumentCaptor<Unstructured> argumentCaptor = ArgumentCaptor.forClass(Unstructured.class);
|
||||||
|
|
||||||
when(extensionClient.fetch(any(GroupVersionKind.class), any()))
|
when(extensionClient.fetch(any(GroupVersionKind.class), any()))
|
||||||
|
|
Loading…
Reference in New Issue