mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-04 20:45:52 +01:00
implemented plugin installation
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
package sonia.scm.plugin;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
@@ -10,11 +11,23 @@ public class AvailablePluginDescriptor implements PluginDescriptor {
|
||||
private final PluginInformation information;
|
||||
private final PluginCondition condition;
|
||||
private final Set<String> dependencies;
|
||||
private final String url;
|
||||
private final String checksum;
|
||||
|
||||
public AvailablePluginDescriptor(PluginInformation information, PluginCondition condition, Set<String> dependencies) {
|
||||
public AvailablePluginDescriptor(PluginInformation information, PluginCondition condition, Set<String> dependencies, String url, String checksum) {
|
||||
this.information = information;
|
||||
this.condition = condition;
|
||||
this.dependencies = dependencies;
|
||||
this.url = url;
|
||||
this.checksum = checksum;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public Optional<String> getChecksum() {
|
||||
return Optional.ofNullable(checksum);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -37,14 +37,20 @@ package sonia.scm.plugin;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.inject.Singleton;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.NotFoundException;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
import javax.inject.Inject;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static sonia.scm.ContextEntry.ContextBuilder.entity;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
@@ -52,13 +58,17 @@ import java.util.stream.Collectors;
|
||||
@Singleton
|
||||
public class DefaultPluginManager implements PluginManager {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DefaultPluginManager.class);
|
||||
|
||||
private final PluginLoader loader;
|
||||
private final PluginCenter center;
|
||||
private final PluginInstaller installer;
|
||||
|
||||
@Inject
|
||||
public DefaultPluginManager(PluginLoader loader, PluginCenter center) {
|
||||
public DefaultPluginManager(PluginLoader loader, PluginCenter center, PluginInstaller installer) {
|
||||
this.loader = loader;
|
||||
this.center = center;
|
||||
this.installer = installer;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -98,6 +108,18 @@ public class DefaultPluginManager implements PluginManager {
|
||||
|
||||
@Override
|
||||
public void install(String name) {
|
||||
if (getInstalled(name).isPresent()){
|
||||
LOG.info("plugin {} is already installed, skipping installation", name);
|
||||
return;
|
||||
}
|
||||
AvailablePlugin plugin = getAvailable(name).orElseThrow(() -> NotFoundException.notFound(entity(AvailablePlugin.class, name)));
|
||||
Set<String> dependencies = plugin.getDescriptor().getDependencies();
|
||||
if (dependencies != null) {
|
||||
for (String dependency: dependencies){
|
||||
install(dependency);
|
||||
}
|
||||
}
|
||||
|
||||
installer.install(plugin);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package sonia.scm.plugin;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
@@ -77,6 +78,8 @@ public final class PluginCenterDto implements Serializable {
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
static class Link {
|
||||
private String href;
|
||||
}
|
||||
|
||||
@@ -17,8 +17,9 @@ public abstract class PluginCenterDtoMapper {
|
||||
Set<AvailablePlugin> map(PluginCenterDto pluginCenterDto) {
|
||||
Set<AvailablePlugin> plugins = new HashSet<>();
|
||||
for (PluginCenterDto.Plugin plugin : pluginCenterDto.getEmbedded().getPlugins()) {
|
||||
String url = plugin.getLinks().get("download").getHref();
|
||||
AvailablePluginDescriptor descriptor = new AvailablePluginDescriptor(
|
||||
map(plugin), map(plugin.getConditions()), plugin.getDependencies()
|
||||
map(plugin), map(plugin.getConditions()), plugin.getDependencies(), url, plugin.getSha256()
|
||||
);
|
||||
plugins.add(new AvailablePlugin(descriptor));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package sonia.scm.plugin;
|
||||
|
||||
public class PluginChecksumMismatchException extends PluginInstallException {
|
||||
public PluginChecksumMismatchException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package sonia.scm.plugin;
|
||||
|
||||
public class PluginDownloadException extends PluginInstallException {
|
||||
public PluginDownloadException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package sonia.scm.plugin;
|
||||
|
||||
public class PluginInstallException extends RuntimeException {
|
||||
|
||||
public PluginInstallException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public PluginInstallException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package sonia.scm.plugin;
|
||||
|
||||
import com.google.common.base.Throwables;
|
||||
import com.google.common.hash.HashFunction;
|
||||
import com.google.common.hash.Hashing;
|
||||
import com.google.common.io.ByteStreams;
|
||||
import com.google.common.io.Files;
|
||||
import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.net.ahc.AdvancedHttpClient;
|
||||
import sonia.scm.util.IOUtil;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Optional;
|
||||
|
||||
class PluginInstaller {
|
||||
|
||||
private final SCMContextProvider context;
|
||||
private final AdvancedHttpClient client;
|
||||
|
||||
@Inject
|
||||
public PluginInstaller(SCMContextProvider context, AdvancedHttpClient client) {
|
||||
this.context = context;
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
public void install(AvailablePlugin plugin) {
|
||||
File file = createFile(plugin);
|
||||
try (InputStream input = download(plugin); OutputStream output = new FileOutputStream(file)) {
|
||||
ByteStreams.copy(input, output);
|
||||
|
||||
verifyChecksum(plugin, file);
|
||||
} catch (IOException ex) {
|
||||
throw new PluginDownloadException("failed to install plugin", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void verifyChecksum(AvailablePlugin plugin, File file) throws IOException {
|
||||
Optional<String> checksum = plugin.getDescriptor().getChecksum();
|
||||
if (checksum.isPresent()) {
|
||||
String calculatedChecksum = Files.hash(file, Hashing.sha256()).toString();
|
||||
if (!checksum.get().equalsIgnoreCase(calculatedChecksum)) {
|
||||
throw new PluginChecksumMismatchException(
|
||||
String.format("downloaded plugin checksum %s does not match expected %s", calculatedChecksum, checksum.get())
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private InputStream download(AvailablePlugin plugin) throws IOException {
|
||||
return client.get(plugin.getDescriptor().getUrl()).request().contentAsStream();
|
||||
}
|
||||
|
||||
private File createFile(AvailablePlugin plugin) {
|
||||
File pluginDirectory = new File(context.getBaseDirectory(), "plugins");
|
||||
IOUtil.mkdirs(pluginDirectory);
|
||||
return new File(pluginDirectory, plugin.getDescriptor().getInformation().getName() + ".smp");
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,6 @@ import sonia.scm.plugin.AvailablePluginDescriptor;
|
||||
import sonia.scm.plugin.PluginCondition;
|
||||
import sonia.scm.plugin.PluginInformation;
|
||||
import sonia.scm.plugin.PluginManager;
|
||||
import sonia.scm.plugin.PluginState;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import javax.inject.Provider;
|
||||
@@ -148,7 +147,9 @@ class AvailablePluginResourceTest {
|
||||
}
|
||||
|
||||
private AvailablePlugin createPlugin(PluginInformation pluginInformation) {
|
||||
AvailablePluginDescriptor descriptor = new AvailablePluginDescriptor(pluginInformation, new PluginCondition(), Collections.emptySet());
|
||||
AvailablePluginDescriptor descriptor = new AvailablePluginDescriptor(
|
||||
pluginInformation, new PluginCondition(), Collections.emptySet(), "https://download.hitchhiker.com", null
|
||||
);
|
||||
return new AvailablePlugin(descriptor);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ package sonia.scm.plugin;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import org.checkerframework.checker.nullness.Opt;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Answers;
|
||||
@@ -10,16 +9,11 @@ import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class DefaultPluginManagerTest {
|
||||
@@ -30,6 +24,9 @@ class DefaultPluginManagerTest {
|
||||
@Mock
|
||||
private PluginCenter center;
|
||||
|
||||
@Mock
|
||||
private PluginInstaller installer;
|
||||
|
||||
@InjectMocks
|
||||
private DefaultPluginManager manager;
|
||||
|
||||
@@ -118,6 +115,44 @@ class DefaultPluginManagerTest {
|
||||
assertThat(available).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldInstallThePlugin() {
|
||||
AvailablePlugin git = createAvailable("scm-git-plugin");
|
||||
when(center.getAvailable()).thenReturn(ImmutableSet.of(git));
|
||||
|
||||
manager.install("scm-git-plugin");
|
||||
|
||||
verify(installer).install(git);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldInstallDependingPlugins() {
|
||||
AvailablePlugin review = createAvailable("scm-review-plugin");
|
||||
when(review.getDescriptor().getDependencies()).thenReturn(ImmutableSet.of("scm-mail-plugin"));
|
||||
AvailablePlugin mail = createAvailable("scm-mail-plugin");
|
||||
when(center.getAvailable()).thenReturn(ImmutableSet.of(review, mail));
|
||||
|
||||
manager.install("scm-review-plugin");
|
||||
|
||||
verify(installer).install(mail);
|
||||
verify(installer).install(review);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotInstallAlreadyInstalledDependencies() {
|
||||
AvailablePlugin review = createAvailable("scm-review-plugin");
|
||||
when(review.getDescriptor().getDependencies()).thenReturn(ImmutableSet.of("scm-mail-plugin"));
|
||||
AvailablePlugin mail = createAvailable("scm-mail-plugin");
|
||||
when(center.getAvailable()).thenReturn(ImmutableSet.of(review, mail));
|
||||
|
||||
InstalledPlugin installedMail = createInstalled("scm-mail-plugin");
|
||||
when(loader.getInstalledPlugins()).thenReturn(ImmutableList.of(installedMail));
|
||||
|
||||
manager.install("scm-review-plugin");
|
||||
|
||||
verify(installer).install(review);
|
||||
}
|
||||
|
||||
private AvailablePlugin createAvailable(String name) {
|
||||
PluginInformation information = new PluginInformation();
|
||||
information.setName(name);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package sonia.scm.plugin;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
@@ -42,13 +43,17 @@ class PluginCenterDtoMapperTest {
|
||||
"555000444",
|
||||
new Condition(Collections.singletonList("linux"), "amd64","2.0.0"),
|
||||
ImmutableSet.of("scm-review-plugin"),
|
||||
new HashMap<>());
|
||||
ImmutableMap.of("download", new Link("http://download.hitchhiker.com"))
|
||||
);
|
||||
|
||||
when(dto.getEmbedded().getPlugins()).thenReturn(Collections.singletonList(plugin));
|
||||
AvailablePluginDescriptor descriptor = mapper.map(dto).iterator().next().getDescriptor();
|
||||
PluginInformation information = descriptor.getInformation();
|
||||
PluginCondition condition = descriptor.getCondition();
|
||||
|
||||
assertThat(descriptor.getUrl()).isEqualTo("http://download.hitchhiker.com");
|
||||
assertThat(descriptor.getChecksum()).contains("555000444");
|
||||
|
||||
assertThat(information.getAuthor()).isEqualTo(plugin.getAuthor());
|
||||
assertThat(information.getCategory()).isEqualTo(plugin.getCategory());
|
||||
assertThat(information.getVersion()).isEqualTo(plugin.getVersion());
|
||||
@@ -72,7 +77,8 @@ class PluginCenterDtoMapperTest {
|
||||
"12345678aa",
|
||||
new Condition(Collections.singletonList("linux"), "amd64","2.0.0"),
|
||||
ImmutableSet.of("scm-review-plugin"),
|
||||
new HashMap<>());
|
||||
ImmutableMap.of("download", new Link("http://download.hitchhiker.com/review"))
|
||||
);
|
||||
|
||||
Plugin plugin2 = new Plugin(
|
||||
"scm-hitchhiker-plugin",
|
||||
@@ -85,7 +91,8 @@ class PluginCenterDtoMapperTest {
|
||||
"555000444",
|
||||
new Condition(Collections.singletonList("linux"), "amd64","2.0.0"),
|
||||
ImmutableSet.of("scm-review-plugin"),
|
||||
new HashMap<>());
|
||||
ImmutableMap.of("download", new Link("http://download.hitchhiker.com/hitchhiker"))
|
||||
);
|
||||
|
||||
when(dto.getEmbedded().getPlugins()).thenReturn(Arrays.asList(plugin1, plugin2));
|
||||
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
package sonia.scm.plugin;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junitpioneer.jupiter.TempDirectory;
|
||||
import org.mockito.Answers;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.net.ahc.AdvancedHttpClient;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.in;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith({MockitoExtension.class, TempDirectory.class})
|
||||
class PluginInstallerTest {
|
||||
|
||||
@Mock
|
||||
private SCMContextProvider context;
|
||||
|
||||
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
|
||||
private AdvancedHttpClient client;
|
||||
|
||||
@InjectMocks
|
||||
private PluginInstaller installer;
|
||||
|
||||
private Path directory;
|
||||
|
||||
@BeforeEach
|
||||
void setUpContext(@TempDirectory.TempDir Path directory) {
|
||||
this.directory = directory;
|
||||
when(context.getBaseDirectory()).thenReturn(directory.toFile());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDownloadPlugin() throws IOException {
|
||||
mockContent("42");
|
||||
|
||||
installer.install(createGitPlugin());
|
||||
|
||||
assertThat(directory.resolve("plugins").resolve("scm-git-plugin.smp")).hasContent("42");
|
||||
}
|
||||
|
||||
private void mockContent(String content) throws IOException {
|
||||
when(client.get("https://download.hitchhiker.com").request().contentAsStream())
|
||||
.thenReturn(new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)));
|
||||
}
|
||||
|
||||
private AvailablePlugin createGitPlugin() {
|
||||
return createPlugin(
|
||||
"scm-git-plugin",
|
||||
"https://download.hitchhiker.com",
|
||||
"73475cb40a568e8da8a045ced110137e159f890ac4da883b6b17dc651b3a8049" // 42
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowPluginDownloadException() throws IOException {
|
||||
when(client.get("https://download.hitchhiker.com").request()).thenThrow(new IOException("failed to download"));
|
||||
|
||||
assertThrows(PluginDownloadException.class, () -> installer.install(createGitPlugin()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowPluginChecksumMismatchException() throws IOException {
|
||||
mockContent("21");
|
||||
|
||||
assertThrows(PluginChecksumMismatchException.class, () -> installer.install(createGitPlugin()));
|
||||
}
|
||||
|
||||
|
||||
private AvailablePlugin createPlugin(String name, String url, String checksum) {
|
||||
PluginInformation information = new PluginInformation();
|
||||
information.setName(name);
|
||||
AvailablePluginDescriptor descriptor = new AvailablePluginDescriptor(
|
||||
information, null, Collections.emptySet(), url, checksum
|
||||
);
|
||||
return new AvailablePlugin(descriptor);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user