Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
[JENKINS-34833] - Installation Wizard: Allow altering the list of sug…
…gested plugins from update sites (#2348)

* [JENKINS-34833] - Installation Wizard: Allow altering the list of succested plugins from update sites

In our company we provide a custom package of Jenkins, which provides a customized set of plugins (open-source and closed-source ones). These plugins come from the custom update site.

We would like to provide custom suggestions to users of our Jenkins packages when they install Jenkins. For such purpose we would like to make the plugin suggestions list overridable by UpdateSite extensions in Jenkins.

* [JENKINS-34833] - Fix the JS Unit test (code from @kzantow)

* [JENKINS-34833] - Unit tests for SetupWizard

* [JENKINS-34833] - Address comment regarding the error propagation for the unknown version

* [JENKINS-34833] - SetupWizard::getPlatformPluginList() is not a part of the public API

* [JENKINS-34833] - Address comments from @orrc
  • Loading branch information
oleg-nenashev committed May 15, 2016
1 parent 65f2a4a commit bb1f620
Show file tree
Hide file tree
Showing 6 changed files with 458 additions and 79 deletions.
185 changes: 184 additions & 1 deletion core/src/main/java/jenkins/install/SetupWizard.java
@@ -1,5 +1,6 @@
package jenkins.install;

import edu.umd.cs.findbugs.annotations.CheckForNull;
import java.io.IOException;
import java.util.Locale;
import java.util.UUID;
Expand All @@ -26,7 +27,9 @@

import hudson.BulkChange;
import hudson.FilePath;
import hudson.ProxyConfiguration;
import hudson.model.UpdateCenter;
import hudson.model.UpdateSite;
import hudson.model.User;
import hudson.security.FullControlOnceLoggedInAuthorizationStrategy;
import hudson.security.HudsonPrivateSecurityRealm;
Expand All @@ -35,8 +38,21 @@
import hudson.util.HttpResponses;
import hudson.util.PluginServletFilter;
import hudson.util.VersionNumber;
import java.io.File;
import java.net.HttpRetryException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.util.Iterator;
import jenkins.model.Jenkins;
import jenkins.security.s2m.AdminWhitelistRule;
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
import org.apache.commons.io.FileUtils;
import static org.apache.commons.io.FileUtils.readFileToString;
import org.apache.commons.io.IOUtils;
import static org.apache.commons.lang.StringUtils.defaultIfBlank;
import org.kohsuke.accmod.restrictions.DoNotUse;

/**
* A Jenkins instance used during first-run to provide a limited set of services while
Expand All @@ -51,7 +67,7 @@ public class SetupWizard {
*/
public static String initialSetupAdminUserName = "admin";

private final Logger LOGGER = Logger.getLogger(SetupWizard.class.getName());
private final static Logger LOGGER = Logger.getLogger(SetupWizard.class.getName());

private final Jenkins jenkins;

Expand Down Expand Up @@ -192,6 +208,168 @@ public void doCreateAdminUser(StaplerRequest req, StaplerResponse rsp) throws IO
}
}

/*package*/ static void completeUpgrade(Jenkins jenkins) throws IOException {
setCurrentLevel(Jenkins.getVersion());
//TODO: restore when https://github.com/jenkinsci/jenkins/pull/2281 gets merged
// jenkins.getInstallState().proceedToNextState();
}

/*package*/ static void setCurrentLevel(VersionNumber v) throws IOException {
FileUtils.writeStringToFile(getUpdateStateFile(), v.toString());
}

/**
* File that captures the state of upgrade.
*
* This file records the version number that the installation has upgraded to.
*/
/*package*/ static File getUpdateStateFile() {
return new File(Jenkins.getInstance().getRootDir(),"jenkins.install.UpgradeWizard.state");
}

/**
* What is the version the upgrade wizard has run the last time and upgraded to?.
* If {@link #getUpdateStateFile()} is missing, presumes the baseline is 1.0
* @return Current baseline. {@code null} if it cannot be retrieved.
*/
@Restricted(NoExternalUse.class)
@CheckForNull
public static VersionNumber getCurrentLevel() {
VersionNumber from = new VersionNumber("1.0");
File state = getUpdateStateFile();
if (state.exists()) {
try {
from = new VersionNumber(defaultIfBlank(readFileToString(state), "1.0").trim());
} catch (IOException ex) {
LOGGER.log(Level.SEVERE, "Cannot read the current version file", ex);
return null;
}
}
return from;
}

/**
* Returns the initial plugin list in JSON format
*/
@Restricted(DoNotUse.class) // WebOnly
public HttpResponse doPlatformPluginList() throws IOException {
jenkins.install.SetupWizard setupWizard = Jenkins.getInstance().getSetupWizard();
if (setupWizard != null) {
if (InstallState.UPGRADE.equals(Jenkins.getInstance().getInstallState())) {
JSONArray initialPluginData = getPlatformPluginUpdates();
if(initialPluginData != null) {
return HttpResponses.okJSON(initialPluginData);
}
} else {
JSONArray initialPluginData = getPlatformPluginList();
if(initialPluginData != null) {
return HttpResponses.okJSON(initialPluginData);
}
}
}
return HttpResponses.okJSON();
}

/**
* Provides the list of platform plugin updates from the last time
* the upgrade was run.
* @return {@code null} if the version range cannot be retrieved.
*/
@CheckForNull
/*package*/ JSONArray getPlatformPluginUpdates() {
final VersionNumber version = getCurrentLevel();
if (version == null) {
return null;
}
return getPlatformPluginsForUpdate(version, Jenkins.getVersion());
}

/**
* Gets the suggested plugin list from the update sites, falling back to a local version
* @return JSON array with the categorized plugon list
*/
@CheckForNull
/*package*/ JSONArray getPlatformPluginList() {
Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
JSONArray initialPluginList = null;
updateSiteList: for (UpdateSite updateSite : Jenkins.getInstance().getUpdateCenter().getSiteList()) {
String updateCenterJsonUrl = updateSite.getUrl();
String suggestedPluginUrl = updateCenterJsonUrl.replace("/update-center.json", "/platform-plugins.json");
try {
URLConnection connection = ProxyConfiguration.open(new URL(suggestedPluginUrl));

try {
if(connection instanceof HttpURLConnection) {
int responseCode = ((HttpURLConnection)connection).getResponseCode();
if(HttpURLConnection.HTTP_OK != responseCode) {
throw new HttpRetryException("Invalid response code (" + responseCode + ") from URL: " + suggestedPluginUrl, responseCode);
}
}

String initialPluginJson = IOUtils.toString(connection.getInputStream(), "utf-8");
initialPluginList = JSONArray.fromObject(initialPluginJson);
break updateSiteList;
} catch(Exception e) {
// not found or otherwise unavailable
LOGGER.log(Level.FINE, e.getMessage(), e);
continue updateSiteList;
}
} catch(Exception e) {
LOGGER.log(Level.FINE, e.getMessage(), e);
}
}
if (initialPluginList == null) {
// fall back to local file
try {
ClassLoader cl = getClass().getClassLoader();
URL localPluginData = cl.getResource("jenkins/install/platform-plugins.json");
String initialPluginJson = IOUtils.toString(localPluginData.openStream(), "utf-8");
initialPluginList = JSONArray.fromObject(initialPluginJson);
} catch (Exception e) {
LOGGER.log(Level.SEVERE, e.getMessage(), e);
}
}
return initialPluginList;
}

/**
* Get the platform plugins added in the version range
*/
/*package*/ JSONArray getPlatformPluginsForUpdate(VersionNumber from, VersionNumber to) {
JSONArray pluginCategories = JSONArray.fromObject(getPlatformPluginList().toString());
for (Iterator<?> categoryIterator = pluginCategories.iterator(); categoryIterator.hasNext();) {
Object category = categoryIterator.next();
if (category instanceof JSONObject) {
JSONObject cat = (JSONObject)category;
JSONArray plugins = cat.getJSONArray("plugins");

nextPlugin: for (Iterator<?> pluginIterator = plugins.iterator(); pluginIterator.hasNext();) {
Object pluginData = pluginIterator.next();
if (pluginData instanceof JSONObject) {
JSONObject plugin = (JSONObject)pluginData;
if (plugin.has("added")) {
String sinceVersion = plugin.getString("added");
if (sinceVersion != null) {
VersionNumber v = new VersionNumber(sinceVersion);
if(v.compareTo(to) <= 0 && v.compareTo(from) > 0) {
plugin.put("suggested", false);
continue nextPlugin;
}
}
}
}

pluginIterator.remove();
}

if (plugins.isEmpty()) {
categoryIterator.remove();
}
}
}
return pluginCategories;
}

/**
* Gets the file used to store the initial admin password
*/
Expand All @@ -211,6 +389,11 @@ public HttpResponse doCompleteInstall() throws IOException, ServletException {
static void completeSetup(Jenkins jenkins) {
jenkins.setInstallState(InstallState.INITIAL_SETUP_COMPLETED);
InstallUtil.saveLastExecVersion();
try {
completeUpgrade(jenkins);
} catch(IOException ex) {
LOGGER.log(Level.SEVERE, "Cannot update the upgrade status", ex);
}
// Also, clean up the setup wizard if it's completed
jenkins.setSetupWizard(null);
}
Expand Down
@@ -1,68 +1,33 @@
//
// TODO: Get all of this information from the Update Center via a REST API.
//

//
// TODO: Decide on what the real "recommended" plugin set is. This is just a 1st stab.
// Also remember, the user ultimately has full control as they can easily customize
// away from these.
//
exports.recommendedPlugins = [
"ant",
"antisamy-markup-formatter",
"build-timeout",
"cloudbees-folder",
"credentials-binding",
"email-ext",
"git",
"gradle",
"ldap",
"mailer",
"matrix-auth",
"pam-auth",
"pipeline-stage-view",
"ssh-slaves",
"subversion",
"timestamper",
"workflow-aggregator",
"github-organization-folder",
"ws-cleanup"
];

//
// A Categorized list of the plugins offered for install in the wizard.
// This is a community curated list.
//
exports.availablePlugins = [
[
{
"category":"Organization and Administration",
"plugins": [
{ "name": "dashboard-view" },
{ "name": "cloudbees-folder" },
{ "name": "antisamy-markup-formatter" }
{ "name": "cloudbees-folder", "suggested": true },
{ "name": "antisamy-markup-formatter", "suggested": true }
]
},
{
"category":"Build Features",
"description":"Add general purpose features to your jobs",
"plugins": [
{ "name": "build-name-setter" },
{ "name": "build-timeout" },
{ "name": "build-timeout", "suggested": true },
{ "name": "config-file-provider" },
{ "name": "credentials-binding" },
{ "name": "credentials-binding", "suggested": true },
{ "name": "embeddable-build-status" },
{ "name": "rebuild" },
{ "name": "ssh-agent" },
{ "name": "throttle-concurrents" },
{ "name": "timestamper" },
{ "name": "ws-cleanup" }
{ "name": "timestamper", "suggested": true },
{ "name": "ws-cleanup", "suggested": true }
]
},
{
"category":"Build Tools",
"plugins": [
{ "name": "ant" },
{ "name": "gradle" },
{ "name": "ant", "suggested": true },
{ "name": "gradle", "suggested": true },
{ "name": "msbuild" },
{ "name": "nodejs" }
]
Expand All @@ -81,9 +46,9 @@ exports.availablePlugins = [
{
"category":"Pipelines and Continuous Delivery",
"plugins": [
{ "name": "workflow-aggregator" },
{ "name": "github-organization-folder" },
{ "name": "pipeline-stage-view" },
{ "name": "workflow-aggregator", "suggested": true, "added": "2.0" },
{ "name": "github-organization-folder", "suggested": true, "added": "2.0" },
{ "name": "pipeline-stage-view", "suggested": true, "added": "2.0" },
{ "name": "build-pipeline-plugin" },
{ "name": "conditional-buildstep" },
{ "name": "jenkins-multijob-plugin" },
Expand All @@ -97,13 +62,13 @@ exports.availablePlugins = [
{ "name": "bitbucket" },
{ "name": "clearcase" },
{ "name": "cvs" },
{ "name": "git" },
{ "name": "git", "suggested": true },
{ "name": "git-parameter" },
{ "name": "github" },
{ "name": "gitlab-plugin" },
{ "name": "p4" },
{ "name": "repo" },
{ "name": "subversion" },
{ "name": "subversion", "suggested": true },
{ "name": "teamconcert" },
{ "name": "tfs" }
]
Expand All @@ -112,29 +77,28 @@ exports.availablePlugins = [
"category":"Distributed Builds",
"plugins": [
{ "name": "matrix-project" },
{ "name": "ssh-slaves" },
{ "name": "ssh-slaves", "suggested": true },
{ "name": "windows-slaves" }
]
},
{
"category":"User Management and Security",
"plugins": [
{ "name": "matrix-auth" },
{ "name": "pam-auth" },
{ "name": "ldap" },
{ "name": "matrix-auth", "suggested": true },
{ "name": "pam-auth", "suggested": true },
{ "name": "ldap", "suggested": true },
{ "name": "role-strategy" },
{ "name": "active-directory" }
]
},
{
"category":"Notifications and Publishing",
"plugins": [
{ "name": "email-ext" },
{ "name": "email-ext", "suggested": true },
{ "name": "emailext-template" },
{ "name": "mailer" },
{ "name": "mailer", "suggested": true },
{ "name": "publish-over-ssh" },
// { "name": "slack" }, // JENKINS-33571, https://github.com/jenkinsci/slack-plugin/issues/191
{ "name": "ssh" }
]
}
];
]

0 comments on commit bb1f620

Please sign in to comment.