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
John Niang 2022-07-26 11:52:13 +08:00 committed by GitHub
parent 71f9209006
commit 5eec9da2e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 74 additions and 40 deletions

View File

@ -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");
}
}

View File

@ -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();

View File

@ -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()))