From bb0b5b26e20e5316b9ec16fa297e9137e34d21a9 Mon Sep 17 00:00:00 2001 From: guqing <38999863+guqing@users.noreply.github.com> Date: Wed, 20 Jul 2022 14:35:24 +0800 Subject: [PATCH] feat: add plugin initial resource cleanup when plugin stop (#2262) * refactor: the way of plugin extension update * feat: add plugin initial resource cleanup when plugin stop * refactor: remove role delete watcher --- .../run/halo/app/core/extension/Plugin.java | 10 -- .../halo/app/plugin/HaloPluginManager.java | 8 +- .../app/plugin/PluginApplicationContext.java | 90 ++++++++++++++++++ .../plugin/PluginBeforeStopSyncListener.java | 44 +++++++++ .../app/plugin/PluginStartedListener.java | 91 ++++++++++++++++++- .../run/halo/app/plugin/YamlPluginFinder.java | 80 +--------------- .../event/HaloPluginBeforeStopEvent.java | 21 +++++ .../reconciler/RoleReconcilerTest.java | 1 - .../app/plugin/PluginStartedListenerTest.java | 51 +++++++++++ .../halo/app/plugin/YamlPluginFinderTest.java | 26 ++---- 10 files changed, 315 insertions(+), 107 deletions(-) create mode 100644 src/main/java/run/halo/app/plugin/PluginBeforeStopSyncListener.java create mode 100644 src/main/java/run/halo/app/plugin/event/HaloPluginBeforeStopEvent.java create mode 100644 src/test/java/run/halo/app/plugin/PluginStartedListenerTest.java diff --git a/src/main/java/run/halo/app/core/extension/Plugin.java b/src/main/java/run/halo/app/core/extension/Plugin.java index f829c62b5..8b6593c6a 100644 --- a/src/main/java/run/halo/app/core/extension/Plugin.java +++ b/src/main/java/run/halo/app/core/extension/Plugin.java @@ -6,7 +6,6 @@ import java.time.Instant; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Objects; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.Getter; @@ -78,18 +77,9 @@ public class Plugin extends AbstractExtension { private Boolean enabled = false; - private List extensionLocations; - private String settingName; private String configMapName; - - @NonNull - @JsonIgnore - public List extensionLocationsNonNull() { - this.extensionLocations = Objects.requireNonNullElseGet(extensionLocations, List::of); - return this.extensionLocations; - } } @Getter diff --git a/src/main/java/run/halo/app/plugin/HaloPluginManager.java b/src/main/java/run/halo/app/plugin/HaloPluginManager.java index f99868db7..e86fc03de 100644 --- a/src/main/java/run/halo/app/plugin/HaloPluginManager.java +++ b/src/main/java/run/halo/app/plugin/HaloPluginManager.java @@ -25,6 +25,7 @@ import org.springframework.beans.factory.InitializingBean; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.lang.NonNull; +import run.halo.app.plugin.event.HaloPluginBeforeStopEvent; import run.halo.app.plugin.event.HaloPluginLoadedEvent; import run.halo.app.plugin.event.HaloPluginStartedEvent; import run.halo.app.plugin.event.HaloPluginStateChangedEvent; @@ -127,7 +128,6 @@ public class HaloPluginManager extends DefaultPluginManager @Override protected PluginState stopPlugin(String pluginId, boolean stopDependents) { checkPluginId(pluginId); - PluginWrapper pluginWrapper = getPlugin(pluginId); PluginDescriptor pluginDescriptor = pluginWrapper.getDescriptor(); PluginState pluginState = pluginWrapper.getPluginState(); @@ -142,6 +142,8 @@ public class HaloPluginManager extends DefaultPluginManager return pluginState; } + rootApplicationContext.publishEvent(new HaloPluginBeforeStopEvent(this, pluginWrapper)); + if (stopDependents) { List dependents = dependencyResolver.getDependents(pluginId); while (!dependents.isEmpty()) { @@ -171,7 +173,7 @@ public class HaloPluginManager extends DefaultPluginManager @Override public PluginState stopPlugin(String pluginId) { - return stopPlugin(pluginId, true); + return this.stopPlugin(pluginId, true); } @Override @@ -294,6 +296,8 @@ public class HaloPluginManager extends DefaultPluginManager PluginState pluginState = pluginWrapper.getPluginState(); if (PluginState.STARTED == pluginState) { try { + rootApplicationContext.publishEvent( + new HaloPluginBeforeStopEvent(this, pluginWrapper)); log.info("Stop plugin '{}'", getPluginLabel(pluginWrapper.getDescriptor())); pluginWrapper.getPlugin().stop(); pluginWrapper.setPluginState(PluginState.STOPPED); diff --git a/src/main/java/run/halo/app/plugin/PluginApplicationContext.java b/src/main/java/run/halo/app/plugin/PluginApplicationContext.java index 33ae252de..610e03292 100644 --- a/src/main/java/run/halo/app/plugin/PluginApplicationContext.java +++ b/src/main/java/run/halo/app/plugin/PluginApplicationContext.java @@ -1,6 +1,12 @@ package run.halo.app.plugin; +import java.util.List; +import java.util.concurrent.locks.StampedLock; import org.springframework.context.support.GenericApplicationContext; +import org.springframework.util.Assert; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import run.halo.app.extension.GroupVersionKind; /** * The generic IOC container for plugins. @@ -12,6 +18,8 @@ import org.springframework.context.support.GenericApplicationContext; */ public class PluginApplicationContext extends GenericApplicationContext { + private final GvkExtensionMapping gvkExtensionMapping = new GvkExtensionMapping(); + private String pluginId; public String getPluginId() { @@ -21,4 +29,86 @@ public class PluginApplicationContext extends GenericApplicationContext { public void setPluginId(String pluginId) { this.pluginId = pluginId; } + + /** + * Gets the gvk-extension mapping. + * It is thread safe + * + * @param gvk the group-kind-version + * @param extensionName extension resources name + */ + public void addExtensionMapping(GroupVersionKind gvk, String extensionName) { + gvkExtensionMapping.addExtensionMapping(gvk, extensionName); + } + + /** + * Gets the extension names by gvk. + * It is thread safe + * + * @param gvk the group-kind-version + * @return a immutable list of extension names + */ + public List getExtensionNames(GroupVersionKind gvk) { + return List.copyOf(gvkExtensionMapping.getExtensionNames(gvk)); + } + + public MultiValueMap extensionNamesMapping() { + return gvkExtensionMapping.extensionNamesMapping(); + } + + static class GvkExtensionMapping { + private final StampedLock sl = new StampedLock(); + private final MultiValueMap extensionNamesMapping = + new LinkedMultiValueMap<>(); + + public void addAllExtensionMapping(GroupVersionKind gvk, List extensionNames) { + long stamp = sl.writeLock(); + try { + extensionNamesMapping.addAll(gvk, extensionNames); + } finally { + sl.unlockWrite(stamp); + } + } + + public void addExtensionMapping(GroupVersionKind gvk, String extensionName) { + long stamp = sl.writeLock(); + try { + extensionNamesMapping.add(gvk, extensionName); + } finally { + sl.unlockWrite(stamp); + } + } + + public List getExtensionNames(GroupVersionKind gvk) { + Assert.notNull(gvk, "The gvk must not be null"); + long stamp = sl.tryOptimisticRead(); + List values = extensionNamesMapping.get(gvk); + if (!sl.validate(stamp)) { + // Check if another write lock occurs after the optimistic read lock + // If so, escalate lock to a pessimistic lock + stamp = sl.readLock(); + try { + return extensionNamesMapping.get(gvk); + } finally { + sl.unlockRead(stamp); + } + } + return values; + } + + public MultiValueMap extensionNamesMapping() { + return new LinkedMultiValueMap<>(extensionNamesMapping); + } + + public void clear() { + extensionNamesMapping.clear(); + } + } + + @Override + protected void onClose() { + // For subclasses: do nothing by default. + super.onClose(); + gvkExtensionMapping.clear(); + } } diff --git a/src/main/java/run/halo/app/plugin/PluginBeforeStopSyncListener.java b/src/main/java/run/halo/app/plugin/PluginBeforeStopSyncListener.java new file mode 100644 index 000000000..aa1c2a19a --- /dev/null +++ b/src/main/java/run/halo/app/plugin/PluginBeforeStopSyncListener.java @@ -0,0 +1,44 @@ +package run.halo.app.plugin; + +import org.pf4j.PluginWrapper; +import org.springframework.context.ApplicationListener; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; +import org.springframework.util.MultiValueMap; +import run.halo.app.extension.ExtensionClient; +import run.halo.app.extension.GroupVersionKind; +import run.halo.app.plugin.event.HaloPluginBeforeStopEvent; + +/** + * Synchronization listener executed by the plugin before it is stopped. + * + * @author guqing + * @since 2.0.0 + */ +@Component +public class PluginBeforeStopSyncListener + implements ApplicationListener { + + private final ExtensionClient client; + + public PluginBeforeStopSyncListener(ExtensionClient client) { + this.client = client; + } + + @Override + public void onApplicationEvent(@NonNull HaloPluginBeforeStopEvent event) { + PluginWrapper pluginWrapper = event.getPlugin(); + PluginApplicationContext pluginContext = ExtensionContextRegistry.getInstance() + .getByPluginId(pluginWrapper.getPluginId()); + + cleanUpPluginExtensionResources(pluginContext); + } + + private void cleanUpPluginExtensionResources(PluginApplicationContext context) { + MultiValueMap gvkExtensionNames = + context.extensionNamesMapping(); + gvkExtensionNames.forEach((gvk, extensionNames) -> + extensionNames.forEach(extensionName -> client.fetch(gvk, extensionName) + .ifPresent(client::delete))); + } +} diff --git a/src/main/java/run/halo/app/plugin/PluginStartedListener.java b/src/main/java/run/halo/app/plugin/PluginStartedListener.java index 0c4f5985d..608b20b4f 100644 --- a/src/main/java/run/halo/app/plugin/PluginStartedListener.java +++ b/src/main/java/run/halo/app/plugin/PluginStartedListener.java @@ -1,9 +1,24 @@ package run.halo.app.plugin; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.jar.JarFile; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; +import lombok.extern.slf4j.Slf4j; +import org.pf4j.DevelopmentPluginClasspath; +import org.pf4j.PluginRuntimeException; import org.pf4j.PluginWrapper; +import org.pf4j.RuntimeMode; import org.springframework.context.ApplicationListener; import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.Resource; @@ -37,13 +52,22 @@ public class PluginStartedListener implements ApplicationListener new YamlUnstructuredLoader(resource).load()) .flatMap(List::stream) .forEach(unstructured -> { MetadataOperator metadata = unstructured.getMetadata(); + // collector plugin initialize extension resources + pluginApplicationContext.addExtensionMapping(unstructured.groupVersionKind(), + metadata.getName()); Map labels = metadata.getLabels(); if (labels == null) { labels = new HashMap<>(); @@ -57,4 +81,69 @@ public class PluginStartedListener implements ApplicationListener extensionClient.create(unstructured)); }); } + + Set lookupExtensions(Path pluginPath, RuntimeMode runtimeMode) { + if (RuntimeMode.DEVELOPMENT.equals(runtimeMode)) { + return PluginExtensionLoaderUtils.lookupFromClasses(pluginPath); + } else { + return PluginExtensionLoaderUtils.lookupFromJar(pluginPath); + } + } + + @Slf4j + static class PluginExtensionLoaderUtils { + static final String EXTENSION_LOCATION = "extensions"; + static final DevelopmentPluginClasspath PLUGIN_CLASSPATH = new DevelopmentPluginClasspath(); + + static Set lookupFromClasses(Path pluginPath) { + Set result = new HashSet<>(); + for (String directory : PLUGIN_CLASSPATH.getClassesDirectories()) { + File file = pluginPath.resolve(directory).resolve(EXTENSION_LOCATION).toFile(); + if (file.exists() && file.isDirectory()) { + result.addAll(walkExtensionFiles(file.toPath())); + } + } + return result; + } + + private static Set walkExtensionFiles(Path location) { + try (Stream stream = Files.walk(location)) { + return stream.map(Path::normalize) + .filter(Files::isRegularFile) + .filter(path -> isYamlFile(path.getFileName().toString())) + .map(path -> location.getParent().relativize(path).toString()) + .collect(Collectors.toSet()); + } catch (IOException e) { + log.debug("Failed to walk extension files from [{}]", location); + return Collections.emptySet(); + } + } + + static boolean isYamlFile(String path) { + return path.endsWith(".yaml") || path.endsWith(".yml"); + } + + /** + *

Lists the path of the unstructured yaml configuration file from the plugin jar.

+ * + * @param pluginJarPath plugin jar path + * @return Unstructured file paths relative to plugin classpath + * @throws PluginRuntimeException If loading the file fails + */ + static Set lookupFromJar(Path pluginJarPath) { + try (JarFile jarFile = new JarFile(pluginJarPath.toFile())) { + return jarFile.stream() + .filter(jarEntry -> { + String name = jarEntry.getName(); + return name.startsWith(EXTENSION_LOCATION) + && !jarEntry.isDirectory() + && isYamlFile(name); + }) + .map(ZipEntry::getName) + .collect(Collectors.toSet()); + } catch (IOException e) { + throw new PluginRuntimeException(e); + } + } + } } diff --git a/src/main/java/run/halo/app/plugin/YamlPluginFinder.java b/src/main/java/run/halo/app/plugin/YamlPluginFinder.java index c8f965abd..f4cf7ef06 100644 --- a/src/main/java/run/halo/app/plugin/YamlPluginFinder.java +++ b/src/main/java/run/halo/app/plugin/YamlPluginFinder.java @@ -4,20 +4,14 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.Collections; import java.util.List; -import java.util.Set; -import java.util.jar.JarFile; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import java.util.zip.ZipEntry; import lombok.extern.slf4j.Slf4j; +import org.pf4j.DevelopmentPluginClasspath; import org.pf4j.PluginRuntimeException; import org.pf4j.PluginState; import org.pf4j.util.FileUtils; import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; -import org.springframework.lang.Nullable; import run.halo.app.core.extension.Plugin; import run.halo.app.extension.Unstructured; import run.halo.app.infra.utils.PathUtils; @@ -56,8 +50,8 @@ import run.halo.app.infra.utils.YamlUnstructuredLoader; */ @Slf4j public class YamlPluginFinder { + static final DevelopmentPluginClasspath PLUGIN_CLASSPATH = new DevelopmentPluginClasspath(); public static final String DEFAULT_PROPERTIES_FILE_NAME = "plugin.yaml"; - private static final String DEFAULT_RESOURCE_LOCATION = "extensions/"; private final String propertiesFileName; public YamlPluginFinder() { @@ -75,12 +69,6 @@ public class YamlPluginFinder { pluginStatus.setPhase(PluginState.RESOLVED); plugin.setStatus(pluginStatus); } - // read unstructured files - if (FileUtils.isJarFile(pluginPath)) { - plugin.getSpec().setExtensionLocations(getUnstructuredFilePathFromJar(pluginPath)); - } else { - plugin.getSpec().setExtensionLocations(getUnstructuredFileFromClasspath(pluginPath)); - } return plugin; } @@ -113,7 +101,7 @@ public class YamlPluginFinder { protected Path getManifestPath(Path pluginPath, String propertiesFileName) { if (Files.isDirectory(pluginPath)) { - for (String location : getSearchLocations()) { + for (String location : PLUGIN_CLASSPATH.getClassesDirectories()) { String s = PathUtils.combinePath(pluginPath.toString(), location, propertiesFileName); Path path = Paths.get(s); @@ -133,66 +121,4 @@ public class YamlPluginFinder { } } } - - /** - *

Lists the path of the unstructured yaml configuration file from the plugin jar.

- * - * @param jarPath plugin jar path - * @return Unstructured file paths relative to plugin classpath - * @throws PluginRuntimeException If loading the file fails - */ - protected List getUnstructuredFilePathFromJar(Path jarPath) { - try (JarFile jarFile = new JarFile(jarPath.toFile())) { - return jarFile.stream() - .filter(jarEntry -> { - String name = jarEntry.getName(); - return name.startsWith(DEFAULT_RESOURCE_LOCATION) - && !jarEntry.isDirectory() - && isYamlFile(name); - }) - .map(ZipEntry::getName) - .toList(); - } catch (IOException e) { - throw new PluginRuntimeException(e); - } - } - - private List getUnstructuredFileFromClasspath(Path pluginPath) { - final Path unstructuredLocation = decisionUnstructuredLocation(pluginPath); - if (unstructuredLocation == null) { - return Collections.emptyList(); - } - try (Stream stream = Files.walk(unstructuredLocation)) { - return stream.map(Path::normalize) - .filter(Files::isRegularFile) - .filter(path -> isYamlFile(path.getFileName().toString())) - .map(path -> unstructuredLocation.getParent().relativize(path).toString()) - .collect(Collectors.toList()); - } catch (IOException e) { - return Collections.emptyList(); - } - } - - @Nullable - private Path decisionUnstructuredLocation(Path pluginPath) { - for (String searchLocation : getSearchLocations()) { - String unstructuredLocationString = PathUtils.combinePath(pluginPath.toString(), - searchLocation, DEFAULT_RESOURCE_LOCATION); - Path path = Paths.get(unstructuredLocationString); - boolean exists = Files.exists(path); - if (exists) { - return path; - } - } - return null; - } - - private boolean isYamlFile(String path) { - return path.endsWith(".yaml") || path.endsWith(".yml"); - } - - private Set getSearchLocations() { - // TODO 优化路径获取 - return Set.of("build/resources/main/", "target/classes/"); - } } diff --git a/src/main/java/run/halo/app/plugin/event/HaloPluginBeforeStopEvent.java b/src/main/java/run/halo/app/plugin/event/HaloPluginBeforeStopEvent.java new file mode 100644 index 000000000..71904d023 --- /dev/null +++ b/src/main/java/run/halo/app/plugin/event/HaloPluginBeforeStopEvent.java @@ -0,0 +1,21 @@ +package run.halo.app.plugin.event; + +import org.pf4j.PluginWrapper; +import org.springframework.context.ApplicationEvent; + +/** + * @author guqing + * @since 2.0.0 + */ +public class HaloPluginBeforeStopEvent extends ApplicationEvent { + private final PluginWrapper plugin; + + public HaloPluginBeforeStopEvent(Object source, PluginWrapper plugin) { + super(source); + this.plugin = plugin; + } + + public PluginWrapper getPlugin() { + return plugin; + } +} diff --git a/src/test/java/run/halo/app/core/extension/reconciler/RoleReconcilerTest.java b/src/test/java/run/halo/app/core/extension/reconciler/RoleReconcilerTest.java index 9d53c643f..0bb96c037 100644 --- a/src/test/java/run/halo/app/core/extension/reconciler/RoleReconcilerTest.java +++ b/src/test/java/run/halo/app/core/extension/reconciler/RoleReconcilerTest.java @@ -89,7 +89,6 @@ class RoleReconcilerTest { .get(Role.ROLE_DEPENDENCY_RULES), false); } - @Test void reconcileUiPermission() { Role roleManage = TestRole.getRoleManage(); diff --git a/src/test/java/run/halo/app/plugin/PluginStartedListenerTest.java b/src/test/java/run/halo/app/plugin/PluginStartedListenerTest.java new file mode 100644 index 000000000..c22ded3db --- /dev/null +++ b/src/test/java/run/halo/app/plugin/PluginStartedListenerTest.java @@ -0,0 +1,51 @@ +package run.halo.app.plugin; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Set; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.util.ResourceUtils; + +/** + * Tests for {@link PluginStartedListener}. + * + * @author guqing + * @since 2.0.0 + */ +class PluginStartedListenerTest { + + @Nested + class PluginExtensionLoaderUtilsTest { + + @Test + void lookupFromClasses() throws IOException { + Path tempPluginPath = Files.createTempDirectory("halo-test-plugin"); + + Path directories = + Files.createDirectories(tempPluginPath.resolve("build/resources/main")); + Path extensions = Files.createDirectory(directories.resolve("extensions")); + Files.createFile(extensions.resolve("roles.yaml")); + + Set extensionResources = + PluginStartedListener.PluginExtensionLoaderUtils.lookupFromClasses(tempPluginPath); + assertThat(extensionResources).containsAll(Set.of("extensions/roles.yaml")); + } + + @Test + void lookupFromJar() throws FileNotFoundException { + File file = + ResourceUtils.getFile("classpath:plugin/test-unstructured-resource-loader.jar"); + Set unstructuredFilePathFromJar = + PluginStartedListener.PluginExtensionLoaderUtils.lookupFromJar(file.toPath()); + assertThat(unstructuredFilePathFromJar).hasSize(3); + assertThat(unstructuredFilePathFromJar).containsAll(Set.of("extensions/roles.yaml", + "extensions/reverseProxy.yaml", "extensions/test.yml")); + } + } +} \ No newline at end of file diff --git a/src/test/java/run/halo/app/plugin/YamlPluginFinderTest.java b/src/test/java/run/halo/app/plugin/YamlPluginFinderTest.java index f57498666..672144bfb 100644 --- a/src/test/java/run/halo/app/plugin/YamlPluginFinderTest.java +++ b/src/test/java/run/halo/app/plugin/YamlPluginFinderTest.java @@ -10,7 +10,6 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.List; import org.json.JSONException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -48,8 +47,6 @@ class YamlPluginFinderTest { Path directories = Files.createDirectories(tempDirectory.resolve("build/resources/main")); FileCopyUtils.copy(testFile, directories.resolve("plugin.yaml").toFile()); - Path extensions = Files.createDirectory(directories.resolve("extensions")); - Files.createFile(extensions.resolve("roles.yaml")); Plugin plugin = pluginFinder.find(tempDirectory); assertThat(plugin).isNotNull(); @@ -66,11 +63,19 @@ class YamlPluginFinderTest { """, JsonUtils.objectToJson(plugin.getStatus()), true); - assertThat(plugin.getSpec().getExtensionLocations()).contains("extensions/roles.yaml"); } @Test - void unstructuredToPluginTest() throws JsonProcessingException, JSONException { + void findFromJar() throws FileNotFoundException { + File file = + ResourceUtils.getFile("classpath:plugin/test-unstructured-resource-loader.jar"); + Plugin plugin = pluginFinder.find(file.toPath()); + assertThat(plugin).isNotNull(); + assertThat(plugin.getMetadata().getName()).isEqualTo("io.github.guqing.apples"); + } + + @Test + void unstructuredToPluginTest() throws JSONException { Plugin plugin = pluginFinder.unstructuredToPlugin(new FileSystemResource(testFile)); assertThat(plugin).isNotNull(); JSONAssert.assertEquals(""" @@ -94,7 +99,6 @@ class YamlPluginFinderTest { "requires": ">=2.0.0", "pluginClass": null, "enabled": false, - "extensionLocations": null, settingName: null, configMapName: null }, @@ -169,14 +173,4 @@ class YamlPluginFinderTest { assertThat(plugin.getSpec()).isNotNull(); JSONAssert.assertEquals(pluginJson, JsonUtils.objectToJson(plugin), false); } - - @Test - void getUnstructuredFilePathFromJar() throws FileNotFoundException { - File file = ResourceUtils.getFile("classpath:plugin/test-unstructured-resource-loader.jar"); - List unstructuredFilePathFromJar = - pluginFinder.getUnstructuredFilePathFromJar(file.toPath()); - assertThat(unstructuredFilePathFromJar).hasSize(3); - assertThat(unstructuredFilePathFromJar).contains("extensions/roles.yaml", - "extensions/reverseProxy.yaml", "extensions/test.yml"); - } }