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;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.core.io.FileSystemResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.thymeleaf.util.StringUtils;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.Unstructured;
|
||||
import run.halo.app.infra.properties.HaloProperties;
|
||||
|
@ -32,6 +28,9 @@ import run.halo.app.infra.utils.YamlUnstructuredLoader;
|
|||
@Slf4j
|
||||
@Component
|
||||
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 ExtensionClient extensionClient;
|
||||
|
||||
|
@ -43,40 +42,46 @@ public class ExtensionResourceInitializer implements ApplicationListener<Applica
|
|||
|
||||
@Override
|
||||
public void onApplicationEvent(@NonNull ApplicationReadyEvent event) {
|
||||
Set<String> extensionLocations = haloProperties.getInitialExtensionLocations();
|
||||
if (!CollectionUtils.isEmpty(extensionLocations)) {
|
||||
var locations = new HashSet<String>();
|
||||
if (!haloProperties.isRequiredExtensionDisabled()) {
|
||||
locations.addAll(REQUIRED_EXTENSION_LOCATIONS);
|
||||
}
|
||||
if (haloProperties.getInitialExtensionLocations() != null) {
|
||||
locations.addAll(haloProperties.getInitialExtensionLocations());
|
||||
}
|
||||
|
||||
Resource[] resources = extensionLocations.stream()
|
||||
.map(this::listYamlFiles)
|
||||
.flatMap(List::stream)
|
||||
.toArray(Resource[]::new);
|
||||
if (CollectionUtils.isEmpty(locations)) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug("Initialization loaded [{}] resources to establish.", resources.length);
|
||||
var resources = locations.stream()
|
||||
.map(this::listResources)
|
||||
.flatMap(List::stream)
|
||||
.distinct()
|
||||
.toArray(Resource[]::new);
|
||||
|
||||
new YamlUnstructuredLoader(resources).load()
|
||||
.forEach(unstructured -> extensionClient.fetch(unstructured.groupVersionKind(),
|
||||
unstructured.getMetadata().getName())
|
||||
.ifPresentOrElse(persisted -> {
|
||||
unstructured.getMetadata()
|
||||
.setVersion(persisted.getMetadata().getVersion());
|
||||
extensionClient.update(unstructured);
|
||||
}, () -> extensionClient.create(unstructured)));
|
||||
log.info("Initializing [{}] extensions in locations: {}", resources.length, locations);
|
||||
|
||||
new YamlUnstructuredLoader(resources).load()
|
||||
.forEach(unstructured -> extensionClient.fetch(unstructured.groupVersionKind(),
|
||||
unstructured.getMetadata().getName())
|
||||
.ifPresentOrElse(persisted -> {
|
||||
unstructured.getMetadata()
|
||||
.setVersion(persisted.getMetadata().getVersion());
|
||||
// TODO Patch the unstructured instead of update it in the future
|
||||
extensionClient.update(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<>();
|
||||
|
||||
/**
|
||||
* 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 SecurityProperties security = new SecurityProperties();
|
||||
|
|
|
@ -10,10 +10,12 @@ import java.io.IOException;
|
|||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import org.json.JSONException;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
@ -22,6 +24,7 @@ import org.mockito.Mock;
|
|||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.skyscreamer.jsonassert.JSONAssert;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.util.FileSystemUtils;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.GroupVersionKind;
|
||||
import run.halo.app.extension.Unstructured;
|
||||
|
@ -46,13 +49,19 @@ class ExtensionResourceInitializerTest {
|
|||
|
||||
private ExtensionResourceInitializer extensionResourceInitializer;
|
||||
|
||||
List<Path> dirsToClean;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws IOException {
|
||||
extensionResourceInitializer =
|
||||
new ExtensionResourceInitializer(haloProperties, extensionClient);
|
||||
|
||||
dirsToClean = new ArrayList<>(2);
|
||||
|
||||
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"), """
|
||||
kind: FakeExtension
|
||||
apiVersion: v1
|
||||
|
@ -79,8 +88,9 @@ class ExtensionResourceInitializerTest {
|
|||
StandardCharsets.UTF_8);
|
||||
|
||||
// test file in directory
|
||||
Path filePath = Files.createTempDirectory("extension-resource-file-test")
|
||||
.resolve("good.yml");
|
||||
Path secondTempDir = Files.createTempDirectory("extension-resource-file-test");
|
||||
dirsToClean.add(secondTempDir);
|
||||
Path filePath = secondTempDir.resolve("good.yml");
|
||||
Files.writeString(filePath, """
|
||||
kind: FakeExtension
|
||||
apiVersion: v1
|
||||
|
@ -92,11 +102,23 @@ class ExtensionResourceInitializerTest {
|
|||
StandardCharsets.UTF_8);
|
||||
|
||||
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
|
||||
void onApplicationEvent() throws JSONException {
|
||||
when(haloProperties.isRequiredExtensionDisabled()).thenReturn(true);
|
||||
ArgumentCaptor<Unstructured> argumentCaptor = ArgumentCaptor.forClass(Unstructured.class);
|
||||
|
||||
when(extensionClient.fetch(any(GroupVersionKind.class), any()))
|
||||
|
|
Loading…
Reference in New Issue