Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Plugins installation #175

Merged
merged 23 commits into from May 30, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
6905d28
Support plugin installation
ndeloof Apr 17, 2018
7e7475a
rely on a shrink-wrap file for reproducibility
ndeloof Apr 17, 2018
9b09997
test-case to cover plugin installation
ndeloof Apr 23, 2018
586f19e
adjust to recent API changes
ndeloof Apr 23, 2018
35d10b7
I hate you so much findbugs
ndeloof Apr 23, 2018
faeeefc
fail CasC on missing plugin
ndeloof Apr 23, 2018
7d2529c
use api's ConfiguratorException to report plugin issues
ndeloof Apr 23, 2018
983aeba
configure root element by Configurator order
ndeloof Apr 24, 2018
3967b72
rely on DownloadService.signatureCheck
ndeloof Apr 24, 2018
9c972a5
prefer plugins.txt for consistency with docker image
ndeloof Apr 26, 2018
20cf9fa
Install plugin by baking minimal plugin metadata
ndeloof May 28, 2018
5e01776
use dynamic installation
ndeloof May 28, 2018
c233106
Detect requirement to restart
ndeloof May 28, 2018
aa5d13c
let's make findbugs happy
ndeloof May 28, 2018
17d5606
deploy with CorrelationId is Restricted
ndeloof May 28, 2018
70a5183
export current pluginManager config to yaml
ndeloof May 28, 2018
5572356
null check
ndeloof May 28, 2018
eddc6c6
Support 'artifact:version@site' notation for plugins not hosted by de…
ndeloof May 29, 2018
9968213
Attempt to discover where a plugin has been installed from
ndeloof May 29, 2018
7425a4d
site ID might not match any configured UpdateSite
ndeloof May 29, 2018
5d62d4b
ignore plugins.txt
ndeloof May 29, 2018
0b41e7c
force a restart on plugin upgrades
ndeloof May 29, 2018
67203e2
Detect shrinkwrap file is outdate as it doesn't match plugins.yaml re…
ndeloof May 29, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Expand Up @@ -4,4 +4,5 @@ work/
*.iml

# ignore jenkins.yaml from root folder (used by many for testing)
/jenkins.yaml
/jenkins.yaml
plugins.txt
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing newline

48 changes: 31 additions & 17 deletions src/main/java/org/jenkinsci/plugins/casc/ConfigurationAsCode.java
Expand Up @@ -50,6 +50,7 @@
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -276,9 +277,7 @@ public static boolean isSupportedURI(String configurationParameter) {
private void configureWith(List<YamlSource> configs) throws ConfiguratorException {
final Node merged = YamlUtils.merge(configs);
final Mapping map = loadAs(merged);
for (Map.Entry<String, CNode> entry : map.entrySet()) {
configureWith(entry);
}
configureWith(map.entrySet());
}

private Mapping loadAs(Node node) {
Expand Down Expand Up @@ -322,24 +321,39 @@ private static Stream<? extends Map.Entry<String, Object>> entries(Reader config
* Configuration with help of {@link RootElementConfigurator}s.
* Corresponding configurator is searched by entry key, passing entry value as object with all required properties.
*
* @param entry key-value pair, where key should match to root configurator and value have all required properties
* @param entries key-value pairs, where key should match to root configurator and value have all required properties
* @throws ConfiguratorException configuration error
*/
public static void configureWith(Map.Entry<String, CNode> entry) throws ConfiguratorException {

RootElementConfigurator configurator = Configurator.lookupRootElement(entry.getKey());
if (configurator == null) {
throw new ConfiguratorException(format("no configurator for root element <%s>", entry.getKey()));
public static void configureWith(Set<Map.Entry<String, CNode>> entries) throws ConfiguratorException {

// Run configurators by order, consuming entries until all have found a matching configurator
// configurators order is important so that org.jenkinsci.plugins.casc.plugins.PluginManagerConfigurator run
// before any other, and can install plugins required by other configuration to successfully parse yaml data
for (RootElementConfigurator configurator : RootElementConfigurator.all()) {
final Iterator<Map.Entry<String, CNode>> it = entries.iterator();
while (it.hasNext()) {
Map.Entry<String, CNode> entry = it.next();
if (! entry.getKey().equalsIgnoreCase(configurator.getName())) {
continue;
}
try {
configurator.configure(entry.getValue());
it.remove();
break;
} catch (ConfiguratorException e) {
throw new ConfiguratorException(
configurator,
format("error configuring <%s> with <%s> configurator", entry.getKey(), configurator.getName()), e
);
}
}
}
try {
configurator.configure(entry.getValue());
} catch (ConfiguratorException e) {
throw new ConfiguratorException(
configurator,
format("error configuring <%s> with <%s> configurator", entry.getKey(), configurator.getName()),
e
);

if (!entries.isEmpty()) {
final Map.Entry<String, CNode> next = entries.iterator().next();
throw new ConfiguratorException(format("No configurator for root element <%s>", next.getKey()));
}

}


Expand Down
14 changes: 14 additions & 0 deletions src/main/java/org/jenkinsci/plugins/casc/model/Mapping.java
@@ -1,5 +1,7 @@
package org.jenkinsci.plugins.casc.model;

import org.jenkinsci.plugins.casc.ConfiguratorException;

import java.util.HashMap;

/**
Expand Down Expand Up @@ -31,4 +33,16 @@ public void put(String key, Number value) {
public void put(String key, Boolean value) {
super.put(key, new Scalar(String.valueOf(value)));
}

public void putIfNotNull(String key, CNode node) {
if (node != null) super.put(key, node);
}

public void putIfNotEmpry(String key, Sequence seq) {
if (!seq.isEmpty()) super.put(key, seq);
}

public String getScalarValue(String key) throws ConfiguratorException {
return get(key).asScalar().getValue();
}
}
@@ -0,0 +1,264 @@
package org.jenkinsci.plugins.casc.plugins;

import hudson.Extension;
import hudson.Plugin;
import hudson.PluginManager;
import hudson.PluginWrapper;
import hudson.ProxyConfiguration;
import hudson.lifecycle.RestartNotSupportedException;
import hudson.model.DownloadService;
import hudson.model.UpdateCenter;
import hudson.model.UpdateSite;
import jenkins.model.Jenkins;
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
import org.apache.commons.io.FileUtils;
import org.jenkinsci.plugins.casc.Attribute;
import org.jenkinsci.plugins.casc.BaseConfigurator;
import org.jenkinsci.plugins.casc.Configurator;
import org.jenkinsci.plugins.casc.ConfiguratorException;
import org.jenkinsci.plugins.casc.MultivaluedAttribute;
import org.jenkinsci.plugins.casc.RootElementConfigurator;
import org.jenkinsci.plugins.casc.model.CNode;
import org.jenkinsci.plugins.casc.model.Mapping;
import org.jenkinsci.plugins.casc.model.Sequence;

import javax.annotation.CheckForNull;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.jar.JarFile;
import java.util.logging.Logger;
import java.util.stream.Collectors;

import static java.nio.charset.StandardCharsets.UTF_8;

/**
* @author <a href="mailto:nicolas.deloof@gmail.com">Nicolas De Loof</a>
*/
@Extension(ordinal = 999)
public class PluginManagerConfigurator extends BaseConfigurator<PluginManager> implements RootElementConfigurator<PluginManager> {

private final static Logger logger = Logger.getLogger(PluginManagerConfigurator.class.getName());

@Override
public Class<PluginManager> getTarget() {
return PluginManager.class;
}

@Override
public PluginManager getTargetComponent() {
return Jenkins.getInstance().getPluginManager();
}

@Override
public PluginManager configure(CNode config) throws ConfiguratorException {
Mapping map = config.asMapping();
final Jenkins jenkins = Jenkins.getInstance();

final CNode proxy = map.get("proxy");
if (proxy != null) {
Configurator<ProxyConfiguration> pc = Configurator.lookup(ProxyConfiguration.class);
if (pc == null) throw new ConfiguratorException("ProxyConfiguration not well registered");
ProxyConfiguration pcc = pc.configure(proxy);
jenkins.proxy = pcc;
}

final CNode sites = map.get("sites");
final UpdateCenter updateCenter = jenkins.getUpdateCenter();
if (sites != null) {
Configurator<UpdateSite> usc = Configurator.lookup(UpdateSite.class);
List<UpdateSite> updateSites = new ArrayList<>();
for (CNode data : sites.asSequence()) {
UpdateSite in = usc.configure(data);
if (in.isDue()) {
in.updateDirectly(DownloadService.signatureCheck);
}
updateSites.add(in);
}

try {
updateCenter.getSites().replaceBy(updateSites);
} catch (IOException e) {
throw new ConfiguratorException("failed to reconfigure updateCenter.sites", e);
}
}


Queue<PluginToInstall> plugins = new LinkedList<>();
final CNode required = map.get("required");
if (required != null) {
for (Map.Entry<String, CNode> entry : required.asMapping().entrySet()) {
plugins.add(new PluginToInstall(entry.getKey(), entry.getValue().asScalar().getValue()));
}
}

File shrinkwrap = new File("./plugins.txt");
Map<String, PluginToInstall> shrinkwrapped = new HashMap<>();
if (shrinkwrap.exists()) {
try {
final List<String> lines = FileUtils.readLines(shrinkwrap, UTF_8);
for (String line : lines) {
int i = line.indexOf(':');
final String shortname = line.substring(0, i);
shrinkwrapped.put(shortname, new PluginToInstall(shortname, line.substring(i+1)));
}
} catch (IOException e) {
throw new ConfiguratorException("failed to load plugins.txt shrinkwrap file", e);
}

// Check if required plugin list has been updated, in which case the shrinkwrap file is obsolete
boolean outdated = false;
for (PluginToInstall plugin : plugins) {
final PluginToInstall other = shrinkwrapped.get(plugin.shortname);
if (other == null || !other.equals(plugin)) {
// plugins was added or version updates, so shrinkwrap isn't relevant anymore
outdated = true;
break;
}
}
if (!outdated) plugins.addAll(shrinkwrapped.values());
}


final PluginManager pluginManager = getTargetComponent();
if (!plugins.isEmpty()) {

boolean requireRestart = false;
Set<String> installed = new HashSet<>();

// Install a plugin from the plugins list.
// For each installed plugin, get the dependency list and update the plugins list accordingly
install:
while (!plugins.isEmpty()) {
PluginToInstall p = plugins.remove();
if (installed.contains(p.shortname)) continue;

final Plugin plugin = jenkins.getPlugin(p.shortname);
if (plugin == null || !plugin.getWrapper().getVersion().equals(p.version)) { // Need to install

// if plugin is being _upgraded_, not just installed, we NEED to restart
requireRestart |= (plugin != null);

// FIXME update sites don't give us metadata about hosted plugins but "latest"
// So we need to assume the URL layout to bake download metadata
JSONObject json = new JSONObject();
json.accumulate("name", p.shortname);
json.accumulate("version", p.version);
json.accumulate("url", "download/plugins/" + p.shortname + "/" + p.version + "/" + p.shortname + ".hpi");
json.accumulate("dependencies", new JSONArray());

boolean downloaded = false;
UpdateSite updateSite = updateCenter.getSite(p.site);
if (updateSite == null)
throw new ConfiguratorException("Can't install " + p + ": no update site " + p.site);
final UpdateSite.Plugin installable = updateSite.new Plugin(updateSite.getId(), json);
try {
final UpdateCenter.UpdateCenterJob job = installable.deploy(true).get();
if (job.getError() != null) {
if (job.getError() instanceof UpdateCenter.DownloadJob.SuccessButRequiresRestart) {
requireRestart = true;
} else {
throw job.getError();
}
}
installed.add(p.shortname);

final File jpi = new File(pluginManager.rootDir, p.shortname + ".jpi");
try (JarFile jar = new JarFile(jpi)) {
String dependencySpec = jar.getManifest().getMainAttributes().getValue("Plugin-Dependencies");
if (dependencySpec != null) {
plugins.addAll(Arrays.stream(dependencySpec.split(","))
.filter(t -> !t.endsWith(";resolution:=optional"))
.map(t -> t.substring(0, t.indexOf(':')))
.map(a -> new PluginToInstall(a, "latest"))
.collect(Collectors.toList()));
}
}
downloaded = true;
break install;
} catch (InterruptedException | ExecutionException ex) {
logger.info("Failed to download plugin " + p.shortname + ':' + p.version + "from update site " + updateSite.getId());
} catch (Throwable ex) {
throw new ConfiguratorException("Failed to download plugin " + p.shortname + ':' + p.version, ex);
}

if (!downloaded) {
throw new ConfiguratorException("Failed to install plugin " + p.shortname + ':' + p.version);
}
}
}

try (PrintWriter w = new PrintWriter(shrinkwrap, UTF_8.name())) {
for (PluginWrapper pw : pluginManager.getPlugins()) {
if (pw.getShortName().equals("configuration-as-code")) continue;
String from = UpdateCenter.PREDEFINED_UPDATE_SITE_ID;
for (UpdateSite site : jenkins.getUpdateCenter().getSites()) {
if (site.getPlugin(pw.getShortName()) != null) {
from = site.getId();
break;
}
}
w.println(pw.getShortName() + ':' + pw.getVersionNumber().toString() + '@' + from);
}
} catch (IOException e) {
throw new ConfiguratorException("failed to write plugins.txt shrinkwrap file", e);
}

if (requireRestart) {
try {
jenkins.restart();
} catch (RestartNotSupportedException e) {
throw new ConfiguratorException("Can't restart master after plugins installation", e);
}
}
}

try {
jenkins.save();
} catch (IOException e) {
throw new ConfiguratorException("failed to save Jenkins configuration", e);
}
return pluginManager;
}

@Override
public String getName() {
return "plugins";
}

@Override
public Set<Attribute> describe() {
Set<Attribute> attr = new HashSet<>();
attr.add(new Attribute<PluginManager, ProxyConfiguration>("proxy", ProxyConfiguration.class));
attr.add(new MultivaluedAttribute<PluginManager, UpdateSite>("sites", UpdateSite.class));
attr.add(new MultivaluedAttribute<PluginManager, Plugins>("required", Plugins.class));
return attr;
}

@CheckForNull
@Override
public CNode describe(PluginManager instance) throws Exception {
final Mapping mapping = new Mapping();
final Configurator cp = Configurator.lookupOrFail(ProxyConfiguration.class);
mapping.putIfNotNull("proxy", cp.describe(Jenkins.getInstance().proxy));
Sequence seq = new Sequence();
final Configurator cs = Configurator.lookupOrFail(UpdateSite.class);
for (UpdateSite site : Jenkins.getInstance().getUpdateCenter().getSiteList()) {
seq.add(cs.describe(site));
}
mapping.putIfNotEmpry("sites", seq);
return mapping;
}

}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing newline