Browse Source

Implement simple JSON-based plugin registry and install command

tags/v1.5.0
James Moger 10 years ago
parent
commit
e5d0bacbf7

+ 1
- 0
releases.moxie View File

@@ -48,6 +48,7 @@ r22: {
- { name: 'git.sshBackend', defaultValue: 'NIO2' }
- { name: 'git.sshCommandStartThreads', defaultValue: '2' }
- { name: 'plugins.folder', defaultValue: '${baseFolder}/plugins' }
- { name: 'plugins.registry', defaultValue: 'http://gitblit.github.io/gitblit-registry/plugins.json' }
}

#

+ 12
- 8
src/main/distrib/data/gitblit.properties View File

@@ -548,6 +548,18 @@ tickets.redis.url =
# SINCE 1.4.0
tickets.perPage = 25
# The folder where plugins are loaded from.
#
# SINCE 1.5.0
# RESTART REQUIRED
# BASEFOLDER
plugins.folder = ${baseFolder}/plugins
# The registry of available plugins.
#
# SINCE 1.5.0
plugins.registry = http://gitblit.github.io/gitblit-registry/plugins.json
#
# Groovy Integration
#
@@ -1850,11 +1862,3 @@ server.requireClientCertificates = false
# SINCE 0.5.0
# RESTART REQUIRED
server.shutdownPort = 8081
# Base folder for plugins.
# This folder may contain Gitblit plugins
#
# SINCE 1.6.0
# RESTART REQUIRED
# BASEFOLDER
plugins.folder = ${baseFolder}/plugins

+ 36
- 0
src/main/java/com/gitblit/manager/GitblitManager.java View File

@@ -61,6 +61,8 @@ import com.gitblit.models.ForkModel;
import com.gitblit.models.GitClientApplication;
import com.gitblit.models.Mailing;
import com.gitblit.models.Metric;
import com.gitblit.models.PluginRegistry.PluginRegistration;
import com.gitblit.models.PluginRegistry.PluginRelease;
import com.gitblit.models.ProjectModel;
import com.gitblit.models.RegistrantAccessPermission;
import com.gitblit.models.RepositoryModel;
@@ -1180,6 +1182,10 @@ public class GitblitManager implements IGitblit {
return repositoryManager.isIdle(repository);
}

/*
* PLUGIN MANAGER
*/

@Override
public <T> List<T> getExtensions(Class<T> clazz) {
return pluginManager.getExtensions(clazz);
@@ -1195,6 +1201,36 @@ public class GitblitManager implements IGitblit {
return pluginManager.deletePlugin(wrapper);
}

@Override
public boolean refreshRegistry() {
return pluginManager.refreshRegistry();
}

@Override
public boolean installPlugin(String url) {
return pluginManager.installPlugin(url);
}

@Override
public boolean installPlugin(PluginRelease pv) {
return pluginManager.installPlugin(pv);
}

@Override
public List<PluginRegistration> getRegisteredPlugins() {
return pluginManager.getRegisteredPlugins();
}

@Override
public PluginRegistration lookupPlugin(String idOrName) {
return pluginManager.lookupPlugin(idOrName);
}

@Override
public PluginRelease lookupRelease(String idOrName, String version) {
return pluginManager.lookupRelease(idOrName, version);
}

@Override
public List<PluginWrapper> getPlugins() {
return pluginManager.getPlugins();

+ 46
- 2
src/main/java/com/gitblit/manager/IPluginManager.java View File

@@ -15,9 +15,14 @@
*/
package com.gitblit.manager;

import java.util.List;

import ro.fortsoft.pf4j.PluginManager;
import ro.fortsoft.pf4j.PluginWrapper;

import com.gitblit.models.PluginRegistry.PluginRegistration;
import com.gitblit.models.PluginRegistry.PluginRelease;

public interface IPluginManager extends IManager, PluginManager {

/**
@@ -27,12 +32,51 @@ public interface IPluginManager extends IManager, PluginManager {
* @return PluginWrapper that loaded the given class
*/
PluginWrapper whichPlugin(Class<?> clazz);
/**
* Delete the plugin represented by {@link PluginWrapper}.
*
*
* @param wrapper
* @return true if successful
*/
boolean deletePlugin(PluginWrapper wrapper);

/**
* Refresh the plugin registry.
*/
boolean refreshRegistry();

/**
* Install the plugin from the specified url.
*/
boolean installPlugin(String url);

/**
* Install the plugin.
*/
boolean installPlugin(PluginRelease pr);

/**
* The list of all registered plugins.
*
* @return a list of registered plugins
*/
List<PluginRegistration> getRegisteredPlugins();

/**
* Lookup a plugin registration from the plugin registries.
*
* @param idOrName
* @return a plugin registration or null
*/
PluginRegistration lookupPlugin(String idOrName);

/**
* Lookup a plugin release.
*
* @param idOrName
* @param version (use null for the current version)
* @return the identified plugin version or null
*/
PluginRelease lookupRelease(String idOrName, String version);
}

+ 245
- 5
src/main/java/com/gitblit/manager/PluginManager.java View File

@@ -15,31 +15,57 @@
*/
package com.gitblit.manager;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.Proxy;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ro.fortsoft.pf4j.DefaultPluginManager;
import ro.fortsoft.pf4j.PluginVersion;
import ro.fortsoft.pf4j.PluginWrapper;

import com.gitblit.Keys;
import com.gitblit.models.PluginRegistry;
import com.gitblit.models.PluginRegistry.PluginRegistration;
import com.gitblit.models.PluginRegistry.PluginRelease;
import com.gitblit.utils.Base64;
import com.gitblit.utils.FileUtils;
import com.gitblit.utils.JsonUtils;
import com.gitblit.utils.StringUtils;
import com.google.common.io.Files;
import com.google.common.io.InputSupplier;

/**
* The plugin manager maintains the lifecycle of plugins. It is exposed as
* Dagger bean. The extension consumers supposed to retrieve plugin manager
* from the Dagger DI and retrieve extensions provided by active plugins.
*
*
* @author David Ostrovsky
*
*
*/
public class PluginManager extends DefaultPluginManager implements IPluginManager {

private final Logger logger = LoggerFactory.getLogger(getClass());
private final IRuntimeManager runtimeManager;

// timeout defaults of Maven 3.0.4 in seconds
private int connectTimeout = 20;

private int readTimeout = 12800;

public PluginManager(IRuntimeManager runtimeManager) {
super(runtimeManager.getFileOrFolder(Keys.plugins.folder, "${baseFolder}/plugins"));
this.runtimeManager = runtimeManager;
@@ -60,13 +86,13 @@ public class PluginManager extends DefaultPluginManager implements IPluginManage
stopPlugins();
return null;
}
@Override
public boolean deletePlugin(PluginWrapper pw) {
File folder = runtimeManager.getFileOrFolder(Keys.plugins.folder, "${baseFolder}/plugins");
File pluginFolder = new File(folder, pw.getPluginPath());
File pluginZip = new File(folder, pw.getPluginPath() + ".zip");
if (pluginFolder.exists()) {
FileUtils.delete(pluginFolder);
}
@@ -75,4 +101,218 @@ public class PluginManager extends DefaultPluginManager implements IPluginManage
}
return true;
}

@Override
public boolean refreshRegistry() {
String dr = "http://gitblit.github.io/gitblit-registry/plugins.json";
String url = runtimeManager.getSettings().getString(Keys.plugins.registry, dr);
try {
return download(url);
} catch (Exception e) {
logger.error(String.format("Failed to retrieve plugins.json from %s", url), e);
}
return false;
}

protected List<PluginRegistry> getRegistries() {
List<PluginRegistry> list = new ArrayList<PluginRegistry>();
File folder = runtimeManager.getFileOrFolder(Keys.plugins.folder, "${baseFolder}/plugins");
FileFilter jsonFilter = new FileFilter() {
@Override
public boolean accept(File file) {
return !file.isDirectory() && file.getName().toLowerCase().endsWith(".json");
}
};

File [] files = folder.listFiles(jsonFilter);
if (files == null || files.length == 0) {
// automatically retrieve the registry if we don't have a local copy
refreshRegistry();
files = folder.listFiles(jsonFilter);
}

if (files == null || files.length == 0) {
return list;
}

for (File file : files) {
PluginRegistry registry = null;
try {
String json = FileUtils.readContent(file, "\n");
registry = JsonUtils.fromJsonString(json, PluginRegistry.class);
} catch (Exception e) {
logger.error("Failed to deserialize " + file, e);
}
if (registry != null) {
list.add(registry);
}
}
return list;
}

@Override
public List<PluginRegistration> getRegisteredPlugins() {
List<PluginRegistration> list = new ArrayList<PluginRegistration>();
Map<String, PluginRegistration> map = new TreeMap<String, PluginRegistration>();
for (PluginRegistry registry : getRegistries()) {
List<PluginRegistration> registrations = registry.registrations;
list.addAll(registrations);
for (PluginRegistration reg : registrations) {
reg.installedRelease = null;
map.put(reg.id, reg);
}
}
for (PluginWrapper pw : getPlugins()) {
String id = pw.getDescriptor().getPluginId();
PluginVersion pv = pw.getDescriptor().getVersion();
PluginRegistration reg = map.get(id);
if (reg != null) {
reg.installedRelease = pv.toString();
}
}
return list;
}

@Override
public PluginRegistration lookupPlugin(String idOrName) {
for (PluginRegistry registry : getRegistries()) {
PluginRegistration reg = registry.lookup(idOrName);
if (reg != null) {
return reg;
}
}
return null;
}

@Override
public PluginRelease lookupRelease(String idOrName, String version) {
for (PluginRegistry registry : getRegistries()) {
PluginRegistration reg = registry.lookup(idOrName);
if (reg != null) {
PluginRelease pv;
if (StringUtils.isEmpty(version)) {
pv = reg.getCurrentRelease();
} else {
pv = reg.getRelease(version);
}
if (pv != null) {
return pv;
}
}
}
return null;
}


/**
* Installs the plugin from the plugin version.
*
* @param pv
* @throws IOException
* @return true if successful
*/
@Override
public boolean installPlugin(PluginRelease pv) {
return installPlugin(pv.url);
}

/**
* Installs the plugin from the url.
*
* @param url
* @return true if successful
*/
@Override
public boolean installPlugin(String url) {
try {
if (!download(url)) {
return false;
}
// TODO stop, unload, load
} catch (IOException e) {
logger.error("Failed to install plugin from " + url, e);
}
return true;
}

/**
* Download a file to the plugins folder.
*
* @param url
* @return
* @throws IOException
*/
protected boolean download(String url) throws IOException {
File pFolder = runtimeManager.getFileOrFolder(Keys.plugins.folder, "${baseFolder}/plugins");
File tmpFile = new File(pFolder, StringUtils.getSHA1(url) + ".tmp");
if (tmpFile.exists()) {
tmpFile.delete();
}

URL u = new URL(url);
final URLConnection conn = getConnection(u);

// try to get the server-specified last-modified date of this artifact
long lastModified = conn.getHeaderFieldDate("Last-Modified", System.currentTimeMillis());

Files.copy(new InputSupplier<InputStream>() {
@Override
public InputStream getInput() throws IOException {
return new BufferedInputStream(conn.getInputStream());
}
}, tmpFile);

File destFile = new File(pFolder, StringUtils.getLastPathElement(u.getPath()));
if (destFile.exists()) {
destFile.delete();
}
tmpFile.renameTo(destFile);
destFile.setLastModified(lastModified);

return true;
}

protected URLConnection getConnection(URL url) throws IOException {
java.net.Proxy proxy = getProxy(url);
HttpURLConnection conn = (HttpURLConnection) url.openConnection(proxy);
if (java.net.Proxy.Type.DIRECT != proxy.type()) {
String auth = getProxyAuthorization(url);
conn.setRequestProperty("Proxy-Authorization", auth);
}

String username = null;
String password = null;
if (!StringUtils.isEmpty(username) && !StringUtils.isEmpty(password)) {
// set basic authentication header
String auth = Base64.encodeBytes((username + ":" + password).getBytes());
conn.setRequestProperty("Authorization", "Basic " + auth);
}

// configure timeouts
conn.setConnectTimeout(connectTimeout * 1000);
conn.setReadTimeout(readTimeout * 1000);

switch (conn.getResponseCode()) {
case HttpURLConnection.HTTP_MOVED_TEMP:
case HttpURLConnection.HTTP_MOVED_PERM:
// handle redirects by closing this connection and opening a new
// one to the new location of the requested resource
String newLocation = conn.getHeaderField("Location");
if (!StringUtils.isEmpty(newLocation)) {
logger.info("following redirect to {0}", newLocation);
conn.disconnect();
return getConnection(new URL(newLocation));
}
}

return conn;
}

protected Proxy getProxy(URL url) {
return java.net.Proxy.NO_PROXY;
}

protected String getProxyAuthorization(URL url) {
return "";
}
}

+ 143
- 0
src/main/java/com/gitblit/models/PluginRegistry.java View File

@@ -0,0 +1,143 @@
/*
* Copyright 2014 gitblit.com.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.gitblit.models;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import org.parboiled.common.StringUtils;
import ro.fortsoft.pf4j.PluginVersion;
/**
* Represents a list of plugin registrations.
*/
public class PluginRegistry implements Serializable {
private static final long serialVersionUID = 1L;
public final String name;
public final List<PluginRegistration> registrations;
public PluginRegistry(String name) {
this.name = name;
registrations = new ArrayList<PluginRegistration>();
}
public PluginRegistration lookup(String idOrName) {
for (PluginRegistration registration : registrations) {
if (registration.id.equalsIgnoreCase(idOrName)
|| registration.name.equalsIgnoreCase(idOrName)) {
return registration;
}
}
return null;
}
@Override
public String toString() {
return getClass().getSimpleName();
}
public static enum InstallState {
NOT_INSTALLED, INSTALLED, CAN_UPDATE, UNKNOWN
}
/**
* Represents a plugin registration.
*/
public static class PluginRegistration implements Serializable {
private static final long serialVersionUID = 1L;
public final String id;
public String name;
public String description;
public String provider;
public String projectUrl;
public String currentRelease;
public transient String installedRelease;
public List<PluginRelease> releases;
public PluginRegistration(String id) {
this.id = id;
this.releases = new ArrayList<PluginRelease>();
}
public PluginRelease getCurrentRelease() {
PluginRelease current = null;
if (!StringUtils.isEmpty(currentRelease)) {
current = getRelease(currentRelease);
}
if (current == null) {
Date date = new Date(0);
for (PluginRelease pv : releases) {
if (pv.date.after(date)) {
current = pv;
}
}
}
return current;
}
public PluginRelease getRelease(String version) {
for (PluginRelease pv : releases) {
if (pv.version.equalsIgnoreCase(version)) {
return pv;
}
}
return null;
}
public InstallState getInstallState() {
if (StringUtils.isEmpty(installedRelease)) {
return InstallState.NOT_INSTALLED;
}
PluginVersion ir = PluginVersion.createVersion(installedRelease);
PluginVersion cr = PluginVersion.createVersion(currentRelease);
switch (ir.compareTo(cr)) {
case -1:
return InstallState.UNKNOWN;
case 1:
return InstallState.CAN_UPDATE;
default:
return InstallState.INSTALLED;
}
}
@Override
public String toString() {
return id;
}
}
public static class PluginRelease {
public String version;
public Date date;
public String url;
}
}

+ 105
- 15
src/main/java/com/gitblit/transport/ssh/commands/PluginDispatcher.java View File

@@ -19,6 +19,7 @@ import java.util.ArrayList;
import java.util.List;

import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.Option;

import ro.fortsoft.pf4j.PluginDependency;
import ro.fortsoft.pf4j.PluginDescriptor;
@@ -26,6 +27,8 @@ import ro.fortsoft.pf4j.PluginState;
import ro.fortsoft.pf4j.PluginWrapper;

import com.gitblit.manager.IGitblit;
import com.gitblit.models.PluginRegistry.PluginRegistration;
import com.gitblit.models.PluginRegistry.PluginRelease;
import com.gitblit.models.UserModel;
import com.gitblit.utils.FlipTable;
import com.gitblit.utils.FlipTable.Borders;
@@ -46,7 +49,8 @@ public class PluginDispatcher extends DispatchCommand {
register(user, StopPlugin.class);
register(user, ShowPlugin.class);
register(user, RemovePlugin.class);
register(user, UploadPlugin.class);
register(user, InstallPlugin.class);
register(user, AvailablePlugins.class);
}

@CommandMetaData(name = "list", aliases = { "ls" }, description = "List the loaded plugins")
@@ -82,7 +86,7 @@ public class PluginDispatcher extends DispatchCommand {

stdout.println(FlipTable.of(headers, data, Borders.BODY_HCOLS));
}
@Override
protected void asTabbed(List<PluginWrapper> list) {
for (PluginWrapper pw : list) {
@@ -95,7 +99,7 @@ public class PluginDispatcher extends DispatchCommand {
}
}
}
@CommandMetaData(name = "start", description = "Start a plugin")
public static class StartPlugin extends SshCommand {

@@ -128,7 +132,7 @@ public class PluginDispatcher extends DispatchCommand {
}
}
}
protected void start(PluginWrapper pw) throws UnloggedFailure {
String id = pw.getDescriptor().getPluginId();
if (pw.getPluginState() == PluginState.STARTED) {
@@ -143,7 +147,7 @@ public class PluginDispatcher extends DispatchCommand {
}
}
}

@CommandMetaData(name = "stop", description = "Stop a plugin")
public static class StopPlugin extends SshCommand {
@@ -177,7 +181,7 @@ public class PluginDispatcher extends DispatchCommand {
}
}
}
protected void stop(PluginWrapper pw) throws UnloggedFailure {
String id = pw.getDescriptor().getPluginId();
if (pw.getPluginState() == PluginState.STOPPED) {
@@ -192,7 +196,7 @@ public class PluginDispatcher extends DispatchCommand {
}
}
}
@CommandMetaData(name = "show", description = "Show the details of a plugin")
public static class ShowPlugin extends SshCommand {

@@ -230,7 +234,7 @@ public class PluginDispatcher extends DispatchCommand {
String ext = exts.get(i);
data[0] = new Object[] { ext.toString(), ext.toString() };
}
extensions = FlipTable.of(headers, data, Borders.COLS);
extensions = FlipTable.of(headers, data, Borders.COLS);
}

// DEPENDENCIES
@@ -246,9 +250,9 @@ public class PluginDispatcher extends DispatchCommand {
PluginDependency dep = deps.get(i);
data[0] = new Object[] { dep.getPluginId(), dep.getPluginVersion() };
}
dependencies = FlipTable.of(headers, data, Borders.COLS);
dependencies = FlipTable.of(headers, data, Borders.COLS);
}
String[] headers = { d.getPluginId() };
Object[][] data = new Object[5][];
data[0] = new Object[] { fields };
@@ -256,10 +260,10 @@ public class PluginDispatcher extends DispatchCommand {
data[2] = new Object[] { extensions };
data[3] = new Object[] { "DEPENDENCIES" };
data[4] = new Object[] { dependencies };
stdout.println(FlipTable.of(headers, data));
stdout.println(FlipTable.of(headers, data));
}
}
@CommandMetaData(name = "remove", aliases= { "rm", "del" }, description = "Remove a plugin", hidden = true)
public static class RemovePlugin extends SshCommand {

@@ -282,12 +286,98 @@ public class PluginDispatcher extends DispatchCommand {
}
}
}
@CommandMetaData(name = "receive", aliases= { "upload" }, description = "Upload a plugin to the server", hidden = true)
public static class UploadPlugin extends SshCommand {

@CommandMetaData(name = "install", description = "Download and installs a plugin", hidden = true)
public static class InstallPlugin extends SshCommand {

@Argument(index = 0, required = true, metaVar = "<URL>|<ID>|<NAME>", usage = "the id, name, or the url of the plugin to download and install")
protected String urlOrIdOrName;

@Option(name = "--version", usage = "The specific version to install")
private String version;

@Override
public void run() throws UnloggedFailure {
IGitblit gitblit = getContext().getGitblit();
try {
String ulc = urlOrIdOrName.toLowerCase();
if (ulc.startsWith("http://") || ulc.startsWith("https://")) {
if (gitblit.installPlugin(urlOrIdOrName)) {
stdout.println(String.format("Installed %s", urlOrIdOrName));
} else {
new UnloggedFailure(1, String.format("Failed to install %s", urlOrIdOrName));
}
} else {
PluginRelease pv = gitblit.lookupRelease(urlOrIdOrName, version);
if (pv == null) {
throw new UnloggedFailure(1, String.format("Plugin \"%s\" is not in the registry!", urlOrIdOrName));
}
if (gitblit.installPlugin(pv)) {
stdout.println(String.format("Installed %s", urlOrIdOrName));
} else {
throw new UnloggedFailure(1, String.format("Failed to install %s", urlOrIdOrName));
}
}
} catch (Exception e) {
log.error("Failed to install " + urlOrIdOrName, e);
throw new UnloggedFailure(1, String.format("Failed to install %s", urlOrIdOrName), e);
}
}
}

@CommandMetaData(name = "available", description = "List the available plugins")
public static class AvailablePlugins extends ListFilterCommand<PluginRegistration> {

@Option(name = "--refresh", aliases = { "-r" }, usage = "refresh the plugin registry")
protected boolean refresh;

@Override
protected List<PluginRegistration> getItems() throws UnloggedFailure {
IGitblit gitblit = getContext().getGitblit();
if (refresh) {
gitblit.refreshRegistry();
}
List<PluginRegistration> list = gitblit.getRegisteredPlugins();
return list;
}

@Override
protected boolean matches(String filter, PluginRegistration t) {
return t.id.matches(filter) || t.name.matches(filter);
}

@Override
protected void asTable(List<PluginRegistration> list) {
String[] headers;
if (verbose) {
String [] h = { "Name", "Description", "Installed", "Release", "State", "Id", "Provider" };
headers = h;
} else {
String [] h = { "Name", "Description", "Installed", "Release", "State" };
headers = h;
}
Object[][] data = new Object[list.size()][];
for (int i = 0; i < list.size(); i++) {
PluginRegistration p = list.get(i);
if (verbose) {
data[i] = new Object[] {p.name, p.description, p.installedRelease, p.currentRelease, p.getInstallState(), p.id, p.provider};
} else {
data[i] = new Object[] {p.name, p.description, p.installedRelease, p.currentRelease, p.getInstallState()};
}
}

stdout.println(FlipTable.of(headers, data, Borders.BODY_HCOLS));
}

@Override
protected void asTabbed(List<PluginRegistration> list) {
for (PluginRegistration p : list) {
if (verbose) {
outTabbed(p.name, p.description, p.currentRelease, p.getInstallState(), p.id, p.provider);
} else {
outTabbed(p.name, p.description, p.currentRelease, p.getInstallState());
}
}
}
}
}

Loading…
Cancel
Save