-
Notifications
You must be signed in to change notification settings - Fork 711
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
Plugins installation #175
Changes from all commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
6905d28
Support plugin installation
ndeloof 7e7475a
rely on a shrink-wrap file for reproducibility
ndeloof 9b09997
test-case to cover plugin installation
ndeloof 586f19e
adjust to recent API changes
ndeloof 35d10b7
I hate you so much findbugs
ndeloof faeeefc
fail CasC on missing plugin
ndeloof 7d2529c
use api's ConfiguratorException to report plugin issues
ndeloof 983aeba
configure root element by Configurator order
ndeloof 3967b72
rely on DownloadService.signatureCheck
ndeloof 9c972a5
prefer plugins.txt for consistency with docker image
ndeloof 20cf9fa
Install plugin by baking minimal plugin metadata
ndeloof 5e01776
use dynamic installation
ndeloof c233106
Detect requirement to restart
ndeloof aa5d13c
let's make findbugs happy
ndeloof 17d5606
deploy with CorrelationId is Restricted
ndeloof 70a5183
export current pluginManager config to yaml
ndeloof 5572356
null check
ndeloof eddc6c6
Support 'artifact:version@site' notation for plugins not hosted by de…
ndeloof 9968213
Attempt to discover where a plugin has been installed from
ndeloof 7425a4d
site ID might not match any configured UpdateSite
ndeloof 5d62d4b
ignore plugins.txt
ndeloof 0b41e7c
force a restart on plugin upgrades
ndeloof 67203e2
Detect shrinkwrap file is outdate as it doesn't match plugins.yaml re…
ndeloof File filter
Filter by extension
Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
264 changes: 264 additions & 0 deletions
264
src/main/java/org/jenkinsci/plugins/casc/plugins/PluginManagerConfigurator.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
|
||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing newline |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing newline