mirror of
https://github.com/jenkinsci/jenkins.git
synced 2026-10-29 12:35:19 +00:00
Merge branch 'master' into JENKINS-34755
Conflicts: core/src/main/java/hudson/FilePath.java
This commit is contained in:
commit
fdabb52503
18 files changed
+530
-31
No files matched your search
+32
-5
@@ -60,18 +60,45 @@ Upcoming changes</a>
|
||||
Internal/build: <code>jenkins-ui</code> (NPM module) is private, used only internally.
|
||||
(<a href="https://issues.jenkins-ci.org/browse/JENKINS-34629">issue 34629</a>)
|
||||
<li class=rfe>
|
||||
Do not print stack trace if a plugin is missing its dependencies.
|
||||
Do not print stack trace during a plugin installation if it is missing its dependencies.
|
||||
(<a href="https://issues.jenkins-ci.org/browse/JENKINS-34683">issue 34683</a>)
|
||||
<li class=rfe>
|
||||
Allow specifying custom <code>AbortException</code>s.
|
||||
(<a href="https://github.com/jenkinsci/jenkins/pull/2288">pull 2288</a>)
|
||||
<li class=rfe>
|
||||
Internal: CLI command disconnect-node extracted from core to CLI.
|
||||
(<a href="https://issues.jenkins-ci.org/browse/JENKINS-34328">issue 34328</a>)
|
||||
Add a <code>hudson.model.UpdateCenter.defaultUpdateSiteId</code> system property,
|
||||
which allows specifying an alternate default Update Site ID.
|
||||
(<a href="https://issues.jenkins-ci.org/browse/JENKINS-34674">issue 34674</a>)
|
||||
<li class=rfe>
|
||||
Developer API: Switch Jenkins.getInstance() to @Nonnull
|
||||
(new <a href="https://wiki.jenkins-ci.org/display/JENKINS/Features+controlled+by+system+properties">system property</a>).
|
||||
Remove the historical initialization of CVS changelog parser for jobs without explicit SCM definition.
|
||||
<b>Warning!</b> This change may potentially cause a regression if a Jenkins plugin depends on this default behavior and injects changelogs without SCM.
|
||||
(<a href="https://issues.jenkins-ci.org/browse/JENKINS-4610">issue 4610</a>)
|
||||
<li class=bug>
|
||||
Prevent hanging of the Installation Wizard if the default Update Site ID cannot be resolved.
|
||||
In such case an error message will be displayed.
|
||||
(<a href="https://issues.jenkins-ci.org/browse/JENKINS-34675">issue 34675</a>)
|
||||
<li class=bug>
|
||||
Do not fail with error when enabling a plugin, which has been already enabled.
|
||||
It prevents errors in the new Installation Wizard, which installs plugins in parallel.
|
||||
(<a href="https://issues.jenkins-ci.org/browse/JENKINS-34710">issue 34710</a>)
|
||||
<li class=bug>
|
||||
Disable JSESSIONID in URLs when running in the JBoss web container.
|
||||
It prevents Error 404 due to invalid links starting from Jenkins 1.556.
|
||||
More info: <a href="https://issues.jboss.org/browse/WFLY-4782">WFLY-4782</a>
|
||||
(<a href="https://issues.jenkins-ci.org/browse/JENKINS-34675">issue 34675</a>)
|
||||
<li class=rfe>
|
||||
Internal: CLI commands <code>disconnect-node</code> and <code>reload-configuration</code> were extracted from the core to CLI.
|
||||
(<a href="https://issues.jenkins-ci.org/browse/JENKINS-34328">issue 34328</a> and
|
||||
<a href="https://issues.jenkins-ci.org/browse/JENKINS-31900">issue 31900</a>)
|
||||
<li class=rfe>
|
||||
Internal: Support latest source version to avoid compile time warnings with JDK7.
|
||||
(<a href="https://issues.jenkins-ci.org/browse/JENKINS-32978">issue 32978</a>)
|
||||
<li class=rfe>
|
||||
Developer API: Switch <code>Jenkins.getInstance()</code> to <code>@Nonnull</code>.
|
||||
(new <a href="https://wiki.jenkins-ci.org/display/JENKINS/Features+controlled+by+system+properties">system property</a>)
|
||||
(<a href="https://github.com/jenkinsci/jenkins/pull/2297">pull 2297</a>)
|
||||
<li class=bug>
|
||||
Tool installers now follow http redirects properly (<a href="https://issues.jenkins-ci.org/browse/JENKINS-23507">issue 23507</a>)
|
||||
|
||||
</ul>
|
||||
</div><!--=TRUNK-END=-->
|
||||
|
||||
+2
-2
@@ -195,7 +195,7 @@ THE SOFTWARE.
|
||||
<dependency>
|
||||
<groupId>org.jenkins-ci</groupId>
|
||||
<artifactId>annotation-indexer</artifactId>
|
||||
<version>1.7</version>
|
||||
<version>1.11</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jenkins-ci</groupId>
|
||||
@@ -539,7 +539,7 @@ THE SOFTWARE.
|
||||
<dependency>
|
||||
<groupId>net.java.sezpoz</groupId>
|
||||
<artifactId>sezpoz</artifactId>
|
||||
<version>1.9</version>
|
||||
<version>1.11</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.kohsuke.jinterop</groupId>
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
package hudson;
|
||||
|
||||
import jenkins.util.SystemProperties;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.jcraft.jzlib.GZIPInputStream;
|
||||
import com.jcraft.jzlib.GZIPOutputStream;
|
||||
import hudson.Launcher.LocalLauncher;
|
||||
@@ -72,6 +73,8 @@ import org.apache.tools.ant.Project;
|
||||
import org.apache.tools.ant.types.FileSet;
|
||||
import org.apache.tools.zip.ZipEntry;
|
||||
import org.apache.tools.zip.ZipFile;
|
||||
import org.kohsuke.accmod.Restricted;
|
||||
import org.kohsuke.accmod.restrictions.NoExternalUse;
|
||||
import org.kohsuke.stapler.Stapler;
|
||||
|
||||
import javax.annotation.CheckForNull;
|
||||
@@ -92,6 +95,7 @@ import java.io.RandomAccessFile;
|
||||
import java.io.Serializable;
|
||||
import java.io.Writer;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
@@ -189,6 +193,11 @@ import org.jenkinsci.remoting.RoleSensitive;
|
||||
* @see VirtualFile
|
||||
*/
|
||||
public final class FilePath implements Serializable {
|
||||
/**
|
||||
* Maximum http redirects we will follow. This defaults to the same number as Firefox/Chrome tolerates.
|
||||
*/
|
||||
private static final int MAX_REDIRECTS = 20;
|
||||
|
||||
/**
|
||||
* When this {@link FilePath} represents the remote path,
|
||||
* this field is always non-null on master (the field represents
|
||||
@@ -756,6 +765,10 @@ public final class FilePath implements Serializable {
|
||||
* @since 1.299
|
||||
*/
|
||||
public boolean installIfNecessaryFrom(@Nonnull URL archive, @CheckForNull TaskListener listener, @Nonnull String message) throws IOException, InterruptedException {
|
||||
return installIfNecessaryFrom(archive, listener, message, MAX_REDIRECTS);
|
||||
}
|
||||
|
||||
private boolean installIfNecessaryFrom(@Nonnull URL archive, @CheckForNull TaskListener listener, @Nonnull String message, int maxRedirects) throws InterruptedException, IOException {
|
||||
try {
|
||||
FilePath timestamp = this.child(".timestamp");
|
||||
long lastModified = timestamp.lastModified();
|
||||
@@ -778,14 +791,28 @@ public final class FilePath implements Serializable {
|
||||
}
|
||||
}
|
||||
|
||||
if (lastModified != 0 && con instanceof HttpURLConnection) {
|
||||
if (con instanceof HttpURLConnection) {
|
||||
HttpURLConnection httpCon = (HttpURLConnection) con;
|
||||
int responseCode = httpCon.getResponseCode();
|
||||
if (responseCode == HttpURLConnection.HTTP_NOT_MODIFIED) {
|
||||
return false;
|
||||
} else if (responseCode != HttpURLConnection.HTTP_OK) {
|
||||
listener.getLogger().println("Skipping installation of " + archive + " to " + remote + " due to server error: " + responseCode + " " + httpCon.getResponseMessage());
|
||||
return false;
|
||||
if (responseCode == HttpURLConnection.HTTP_MOVED_PERM
|
||||
|| responseCode == HttpURLConnection.HTTP_MOVED_TEMP) {
|
||||
// follows redirect
|
||||
if (maxRedirects > 0) {
|
||||
String location = httpCon.getHeaderField("Location");
|
||||
listener.getLogger().println("Following redirect " + archive.toExternalForm() + " -> " + location);
|
||||
return installIfNecessaryFrom(getUrlFactory().newURL(location), listener, message, maxRedirects - 1);
|
||||
} else {
|
||||
listener.getLogger().println("Skipping installation of " + archive + " to " + remote + " due to too many redirects.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (lastModified != 0) {
|
||||
if (responseCode == HttpURLConnection.HTTP_NOT_MODIFIED) {
|
||||
return false;
|
||||
} else if (responseCode != HttpURLConnection.HTTP_OK) {
|
||||
listener.getLogger().println("Skipping installation of " + archive + " to " + remote + " due to server error: " + responseCode + " " + httpCon.getResponseMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2521,6 +2548,31 @@ public final class FilePath implements Serializable {
|
||||
});
|
||||
}
|
||||
|
||||
private static final UrlFactory DEFAULT_URL_FACTORY = new UrlFactory();
|
||||
|
||||
@Restricted(NoExternalUse.class)
|
||||
static class UrlFactory {
|
||||
public URL newURL(String location) throws MalformedURLException {
|
||||
return new URL(location);
|
||||
}
|
||||
}
|
||||
|
||||
private UrlFactory urlFactory;
|
||||
|
||||
@VisibleForTesting
|
||||
@Restricted(NoExternalUse.class)
|
||||
void setUrlFactory(UrlFactory urlFactory) {
|
||||
this.urlFactory = urlFactory;
|
||||
}
|
||||
|
||||
private UrlFactory getUrlFactory() {
|
||||
if (urlFactory != null) {
|
||||
return urlFactory;
|
||||
} else {
|
||||
return DEFAULT_URL_FACTORY;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Short for {@code validateFileMask(path, value, true)}
|
||||
*/
|
||||
|
||||
@@ -559,9 +559,10 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas
|
||||
}
|
||||
|
||||
if (dependencyURL != null) {
|
||||
dependencySet.add(dependencyURL);
|
||||
// And transitive deps...
|
||||
addDependencies(dependencyURL, fromPath, dependencySet);
|
||||
// And then add the current plugin
|
||||
dependencySet.add(dependencyURL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -369,6 +369,7 @@ public abstract class Job<JobT extends Job<JobT, RunT>, RunT extends Run<JobT, R
|
||||
env.put("JENKINS_SERVER_COOKIE",SERVER_COOKIE.get());
|
||||
env.put("HUDSON_SERVER_COOKIE",SERVER_COOKIE.get()); // Legacy compatibility
|
||||
env.put("JOB_NAME",getFullName());
|
||||
env.put("JOB_BASE_NAME", getName());
|
||||
return env;
|
||||
}
|
||||
|
||||
|
||||
@@ -2394,7 +2394,7 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
|
||||
public String getEntryID(Run entry) {
|
||||
return "tag:" + "hudson.dev.java.net,"
|
||||
+ entry.getTimestamp().get(Calendar.YEAR) + ":"
|
||||
+ entry.getParent().getName()+':'+entry.getId();
|
||||
+ entry.getParent().getFullName()+':'+entry.getId();
|
||||
}
|
||||
|
||||
public String getEntryDescription(Run entry) {
|
||||
|
||||
@@ -198,6 +198,12 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
|
||||
* Connection status has not started yet.
|
||||
*/
|
||||
PRECHECK,
|
||||
/**
|
||||
* Connection status check has been skipped.
|
||||
* As example, it may happen if there is no connection check URL defined for the site.
|
||||
* @since TODO
|
||||
*/
|
||||
SKIPPED,
|
||||
/**
|
||||
* Connection status is being checked at this time.
|
||||
*/
|
||||
@@ -1379,6 +1385,10 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
|
||||
connectionStates.put(ConnectionStatus.INTERNET, ConnectionStatus.OK);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
LOGGER.log(WARNING, "Update site '{0}' does not declare the connection check URL. "
|
||||
+ "Skipping the network availability check.", site.getId());
|
||||
connectionStates.put(ConnectionStatus.INTERNET, ConnectionStatus.SKIPPED);
|
||||
}
|
||||
|
||||
connectionStates.put(ConnectionStatus.UPDATE_SITE, ConnectionStatus.CHECKING);
|
||||
|
||||
@@ -342,9 +342,11 @@ public class UpdateSite {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an "always up" server for Internet connectivity testing, or null if we are going to skip the test.
|
||||
* Gets a URL for the Internet connection check.
|
||||
* @return an "always up" server for Internet connectivity testing, or {@code null} if we are going to skip the test.
|
||||
*/
|
||||
@Exported
|
||||
@CheckForNull
|
||||
public String getConnectionCheckUrl() {
|
||||
Data dt = getData();
|
||||
if(dt==null) return "http://www.google.com/";
|
||||
|
||||
@@ -48,6 +48,9 @@ import static org.acegisecurity.ui.rememberme.TokenBasedRememberMeServices.ACEGI
|
||||
import org.acegisecurity.userdetails.UserDetailsService;
|
||||
import org.acegisecurity.userdetails.UserDetails;
|
||||
import org.acegisecurity.userdetails.UsernameNotFoundException;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.kohsuke.accmod.Restricted;
|
||||
import org.kohsuke.accmod.restrictions.DoNotUse;
|
||||
import org.kohsuke.stapler.HttpResponse;
|
||||
import org.kohsuke.stapler.Stapler;
|
||||
import org.kohsuke.stapler.StaplerRequest;
|
||||
@@ -62,6 +65,7 @@ import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpSession;
|
||||
import javax.servlet.http.Cookie;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Logger;
|
||||
@@ -484,6 +488,56 @@ public abstract class SecurityRealm extends AbstractDescribableImpl<SecurityReal
|
||||
*/
|
||||
public static final SecurityRealm NO_AUTHENTICATION = new None();
|
||||
|
||||
/**
|
||||
* Perform a calculation where we should go back after successful login
|
||||
*
|
||||
* @return Encoded URI where we should go back after successful login
|
||||
* or "/" if no way back or an issue occurred
|
||||
*
|
||||
* @since TODO
|
||||
*/
|
||||
@Restricted(DoNotUse.class)
|
||||
public static String getFrom() {
|
||||
String from = null, returnValue = null;
|
||||
final StaplerRequest request = Stapler.getCurrentRequest();
|
||||
|
||||
// Try to obtain a return point either from the Session
|
||||
// or from the QueryParameter in this order
|
||||
if (request != null
|
||||
&& request.getSession(false) != null) {
|
||||
from = (String) request.getSession().getAttribute("from");
|
||||
} else if (request != null) {
|
||||
from = request.getParameter("from");
|
||||
}
|
||||
|
||||
// If entry point was not found, try to deduce it from the request URI
|
||||
// except pages related to login process
|
||||
if (from == null
|
||||
&& request != null
|
||||
&& request.getRequestURI() != null
|
||||
&& !request.getRequestURI().equals("/loginError")
|
||||
&& !request.getRequestURI().equals("/login")) {
|
||||
|
||||
from = request.getRequestURI();
|
||||
}
|
||||
|
||||
// If deduced entry point isn't deduced yet or the content is a blank value
|
||||
// use the root web point "/" as a fallback
|
||||
if (StringUtils.isBlank(from)) {
|
||||
from = "/";
|
||||
}
|
||||
from.trim();
|
||||
|
||||
// Encode the return value
|
||||
try {
|
||||
returnValue = java.net.URLEncoder.encode(from, "UTF-8");
|
||||
} catch (UnsupportedEncodingException e) { }
|
||||
|
||||
// Return encoded value or at least "/" in the case exception occurred during encode()
|
||||
// or if the encoded content is blank value
|
||||
return StringUtils.isBlank(returnValue) ? "/" : returnValue;
|
||||
}
|
||||
|
||||
private static class None extends SecurityRealm {
|
||||
public SecurityComponents createSecurityComponents() {
|
||||
return new SecurityComponents(new AuthenticationManager() {
|
||||
|
||||
@@ -25,9 +25,5 @@ THE SOFTWARE.
|
||||
|
||||
<?jelly escape-by-default='true'?>
|
||||
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler">
|
||||
<j:invokeStatic var="from" className="java.net.URLEncoder" method="encode">
|
||||
<j:arg value="${if (request.session.attribute('from')!=null) request.session.getAttribute('from'); else if (request.getParameter('from')!=null) request.getParameter('from'); else if (request.requestURI=='/loginError' || request.requestURI=='/login') '/'; else request.requestURI;}"/>
|
||||
<j:arg value="UTF-8"/>
|
||||
</j:invokeStatic>
|
||||
<a href="${rootURL}/${app.securityRealm.loginUrl}?from=${from}"><b>${%login}</b></a>
|
||||
<a href="${rootURL}/${app.securityRealm.loginUrl}?from=${app.securityRealm.from}"><b>${%login}</b></a>
|
||||
</j:jelly>
|
||||
@@ -73,8 +73,10 @@
|
||||
H/15 * * * *
|
||||
# every ten minutes in the first half of every hour (three times, perhaps at :04, :14, :24)
|
||||
H(0-29)/10 * * * *
|
||||
# once every two hours every weekday (perhaps at 9:38 AM, 11:38 AM, 1:38 PM, 3:38 PM)
|
||||
H 9-16/2 * * 1-5
|
||||
# once every two hours at 45 minutes past the hour starting at 9:45 AM and finishing at 3:45 PM every weekday.
|
||||
45 9-16/2 * * 1-5
|
||||
# once in every two hours slot between 9 AM and 5 PM every weekday (perhaps at 10:38 AM, 12:38 PM, 2:38 PM, 4:38 PM)
|
||||
H H(9-16)/2 * * 1-5
|
||||
# once a day on the 1st and 15th of every month except December
|
||||
H H 1,15 1-11 *
|
||||
</pre>
|
||||
|
||||
@@ -3,7 +3,7 @@ package jenkins.model.CoreEnvironmentContributor;
|
||||
def l = namespace(lib.JenkinsTagLib)
|
||||
|
||||
// also advertises those contributed by Run.getCharacteristicEnvVars()
|
||||
["BUILD_NUMBER","BUILD_ID","BUILD_DISPLAY_NAME","JOB_NAME","BUILD_TAG","EXECUTOR_NUMBER","NODE_NAME","NODE_LABELS","WORKSPACE","JENKINS_HOME","JENKINS_URL","BUILD_URL","JOB_URL"].each { name ->
|
||||
["BUILD_NUMBER","BUILD_ID","BUILD_DISPLAY_NAME","JOB_NAME", "JOB_BASE_NAME","BUILD_TAG","EXECUTOR_NUMBER","NODE_NAME","NODE_LABELS","WORKSPACE","JENKINS_HOME","JENKINS_URL","BUILD_URL","JOB_URL"].each { name ->
|
||||
l.buildEnvVar(name:name) {
|
||||
raw(_("${name}.blurb"))
|
||||
}
|
||||
|
||||
+2
-1
@@ -1,7 +1,8 @@
|
||||
BUILD_NUMBER.blurb=The current build number, such as "153"
|
||||
BUILD_ID.blurb=The current build ID, identical to BUILD_NUMBER for builds created in 1.597+, but a YYYY-MM-DD_hh-mm-ss timestamp for older builds
|
||||
BUILD_DISPLAY_NAME.blurb=The display name of the current build, which is something like "#153" by default.
|
||||
JOB_NAME.blurb=Name of the project of this build, such as "foo" or "foo/bar". (To strip off folder paths from a Bourne shell script, try: <tt>$'{'JOB_NAME##*/}</tt>)
|
||||
JOB_NAME.blurb=Name of the project of this build, such as "foo" or "foo/bar".
|
||||
JOB_BASE_NAME.blurb=Short Name of the project of this build stripping off folder paths, such as "foo" for "bar/foo".
|
||||
BUILD_TAG.blurb=String of "jenkins-<i>$'{'JOB_NAME}</i>-<i>$'{'BUILD_NUMBER}</i>". Convenient to put into a resource file, a jar file, etc for easier identification.
|
||||
EXECUTOR_NUMBER.blurb=\
|
||||
The unique number that identifies the current executor \
|
||||
|
||||
@@ -38,9 +38,11 @@ import java.io.OutputStream;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.net.ConnectException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.net.URLStreamHandler;
|
||||
import java.net.URLStreamHandlerFactory;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
@@ -637,6 +639,28 @@ public class FilePathTest {
|
||||
assertTrue(log, log.contains("504 Gateway Timeout"));
|
||||
}
|
||||
|
||||
@Issue("JENKINS-23507")
|
||||
@Test public void installIfNecessaryFollowsRedirects() throws Exception{
|
||||
File tmp = temp.getRoot();
|
||||
final FilePath d = new FilePath(tmp);
|
||||
FilePath.UrlFactory urlFactory = mock(FilePath.UrlFactory.class);
|
||||
d.setUrlFactory(urlFactory);
|
||||
final HttpURLConnection con = mock(HttpURLConnection.class);
|
||||
final HttpURLConnection con2 = mock(HttpURLConnection.class);
|
||||
final URL url = someUrlToZipFile(con);
|
||||
when(con.getResponseCode()).thenReturn(HttpURLConnection.HTTP_MOVED_TEMP);
|
||||
URL url2 = someUrlToZipFile(con2);
|
||||
String someUrl = url2.toExternalForm();
|
||||
when(con.getHeaderField("Location")).thenReturn(someUrl);
|
||||
when(urlFactory.newURL(someUrl)).thenReturn(url2);
|
||||
when(con2.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK);
|
||||
when(con2.getInputStream()).thenReturn(someZippedContent());
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
String message = "going ahead";
|
||||
assertTrue(d.installIfNecessaryFrom(url, new StreamTaskListener(baos), message));
|
||||
}
|
||||
|
||||
private URL someUrlToZipFile(final URLConnection con) throws IOException {
|
||||
|
||||
final URLStreamHandler urlHandler = new URLStreamHandler() {
|
||||
|
||||
@@ -577,7 +577,7 @@ THE SOFTWARE.
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>animal-sniffer-maven-plugin</artifactId>
|
||||
<version>1.9</version>
|
||||
<version>1.15</version>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</pluginManagement>
|
||||
|
||||
@@ -0,0 +1,317 @@
|
||||
/*
|
||||
* The MIT License
|
||||
*
|
||||
* Copyright 2016 Red Hat, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
package hudson.cli;
|
||||
|
||||
import hudson.model.ExecutorTest;
|
||||
import hudson.model.FreeStyleProject;
|
||||
import hudson.model.Job;
|
||||
import hudson.model.Run;
|
||||
import hudson.model.labels.LabelAtom;
|
||||
import hudson.tasks.Shell;
|
||||
import jenkins.model.Jenkins;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.jvnet.hudson.test.JenkinsRule;
|
||||
|
||||
import static hudson.cli.CLICommandInvoker.Matcher.failedWith;
|
||||
import static hudson.cli.CLICommandInvoker.Matcher.hasNoStandardOutput;
|
||||
import static hudson.cli.CLICommandInvoker.Matcher.succeeded;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
|
||||
/**
|
||||
* @author pjanouse
|
||||
*/
|
||||
public class DeleteBuildsCommandTest {
|
||||
|
||||
private CLICommandInvoker command;
|
||||
|
||||
@Rule public final JenkinsRule j = new JenkinsRule();
|
||||
|
||||
@Before public void setUp() {
|
||||
command = new CLICommandInvoker(j, "delete-builds");
|
||||
}
|
||||
|
||||
@Test public void deleteBuildsShouldFailWithoutJobReadPermission() throws Exception {
|
||||
j.createFreeStyleProject("aProject").scheduleBuild2(0).get();
|
||||
|
||||
final CLICommandInvoker.Result result = command
|
||||
.authorizedTo(Jenkins.READ)
|
||||
.invokeWithArgs("aProject", "1");
|
||||
assertThat(result, failedWith(3));
|
||||
assertThat(result, hasNoStandardOutput());
|
||||
assertThat(result.stderr(), containsString("ERROR: No such job 'aProject'"));
|
||||
}
|
||||
|
||||
@Test public void deleteBuildsShouldFailWithoutRunDeletePermission() throws Exception {
|
||||
j.createFreeStyleProject("aProject").scheduleBuild2(0).get();
|
||||
|
||||
final CLICommandInvoker.Result result = command
|
||||
.authorizedTo(Jenkins.READ, Job.READ)
|
||||
.invokeWithArgs("aProject", "1");
|
||||
assertThat(result, failedWith(6));
|
||||
assertThat(result, hasNoStandardOutput());
|
||||
assertThat(result.stderr(), containsString("ERROR: user is missing the Run/Delete permission"));
|
||||
}
|
||||
|
||||
@Test public void deleteBuildsShouldFailIfJobDoesNotExist() throws Exception {
|
||||
final CLICommandInvoker.Result result = command
|
||||
.authorizedTo(Jenkins.READ, Job.READ, Run.DELETE)
|
||||
.invokeWithArgs("never_created", "1");
|
||||
assertThat(result, failedWith(3));
|
||||
assertThat(result, hasNoStandardOutput());
|
||||
assertThat(result.stderr(), containsString("ERROR: No such job 'never_created'"));
|
||||
}
|
||||
|
||||
@Test public void deleteBuildsShouldFailIfJobNameIsEmpty() throws Exception {
|
||||
j.createFreeStyleProject("aProject").scheduleBuild2(0).get();
|
||||
assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).getBuilds(), hasSize(1));
|
||||
|
||||
final CLICommandInvoker.Result result = command
|
||||
.authorizedTo(Jenkins.READ, Job.READ, Run.DELETE)
|
||||
.invokeWithArgs("", "1");
|
||||
assertThat(result, failedWith(3));
|
||||
assertThat(result, hasNoStandardOutput());
|
||||
assertThat(result.stderr(), containsString("ERROR: No such job ''; perhaps you meant 'aProject'?"));
|
||||
}
|
||||
|
||||
@Test public void deleteBuildsShouldSuccess() throws Exception {
|
||||
j.createFreeStyleProject("aProject").scheduleBuild2(0).get();
|
||||
assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).getBuilds(), hasSize(1));
|
||||
|
||||
final CLICommandInvoker.Result result = command
|
||||
.authorizedTo(Jenkins.READ, Job.READ, Run.DELETE)
|
||||
.invokeWithArgs("aProject", "1");
|
||||
assertThat(result, succeeded());
|
||||
assertThat(result.stdout(), containsString("Deleted 1 builds"));
|
||||
assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).getBuilds(), hasSize(0));
|
||||
}
|
||||
|
||||
@Test public void deleteBuildsShouldSuccessIfBuildDoesNotExist() throws Exception {
|
||||
j.createFreeStyleProject("aProject").scheduleBuild2(0).get();
|
||||
assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).getBuilds(), hasSize(1));
|
||||
|
||||
final CLICommandInvoker.Result result = command
|
||||
.authorizedTo(Jenkins.READ, Job.READ, Run.DELETE)
|
||||
.invokeWithArgs("aProject", "2");
|
||||
assertThat(result, succeeded());
|
||||
assertThat(result.stdout(), containsString("Deleted 0 builds"));
|
||||
}
|
||||
|
||||
@Test public void deleteBuildsShouldSuccessIfBuildNumberZeroSpecified() throws Exception {
|
||||
j.createFreeStyleProject("aProject").scheduleBuild2(0).get();
|
||||
assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).getBuilds(), hasSize(1));
|
||||
|
||||
final CLICommandInvoker.Result result = command
|
||||
.authorizedTo(Jenkins.READ, Job.READ, Run.DELETE)
|
||||
.invokeWithArgs("aProject", "0");
|
||||
assertThat(result, succeeded());
|
||||
assertThat(result.stdout(), containsString("Deleted 0 builds"));
|
||||
}
|
||||
|
||||
@Test public void deleteBuildsShouldSuccessEvenTheBuildIsRunning() throws Exception {
|
||||
FreeStyleProject project = j.createFreeStyleProject("aProject");
|
||||
ExecutorTest.startBlockingBuild(project);
|
||||
assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).getBuilds(), hasSize(1));
|
||||
|
||||
final CLICommandInvoker.Result result = command
|
||||
.authorizedTo(Jenkins.READ, Job.READ, Run.DELETE)
|
||||
.invokeWithArgs("aProject", "1");
|
||||
assertThat(result, succeeded());
|
||||
assertThat(result.stdout(), containsString("Deleted 1 builds"));
|
||||
assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).getBuilds(), hasSize(0));
|
||||
assertThat(project.isBuilding(), equalTo(false));
|
||||
}
|
||||
|
||||
@Test public void deleteBuildsShouldSuccessEvenTheBuildIsStuckInTheQueue() throws Exception {
|
||||
FreeStyleProject project = j.createFreeStyleProject("aProject");
|
||||
project.getBuildersList().add(new Shell("echo 1"));
|
||||
project.setAssignedLabel(new LabelAtom("never_created"));
|
||||
assertThat("Job wasn't scheduled properly", project.scheduleBuild(0), equalTo(true));
|
||||
Thread.sleep(1000);
|
||||
assertThat("Job wasn't scheduled properly - it isn't in the queue", project.isInQueue(), equalTo(true));
|
||||
assertThat("Job wasn't scheduled properly - it is running on non-exist node", project.isBuilding(), equalTo(false));
|
||||
|
||||
final CLICommandInvoker.Result result = command
|
||||
.authorizedTo(Jenkins.READ, Job.READ, Run.DELETE)
|
||||
.invokeWithArgs("aProject", "1");
|
||||
assertThat(result, succeeded());
|
||||
assertThat(result.stdout(), containsString("Deleted 0 builds"));
|
||||
assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).getBuilds(), hasSize(0));
|
||||
assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).isBuilding(), equalTo(false));
|
||||
assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).isInQueue(), equalTo(true));
|
||||
|
||||
Jenkins.getInstance().getQueue().cancel(project);
|
||||
assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).getBuilds(), hasSize(0));
|
||||
assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).isBuilding(), equalTo(false));
|
||||
assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).isInQueue(), equalTo(false));
|
||||
}
|
||||
|
||||
@Test public void deleteBuildsManyShouldSuccess() throws Exception {
|
||||
FreeStyleProject project = j.createFreeStyleProject("aProject");
|
||||
j.buildAndAssertSuccess(project);
|
||||
j.buildAndAssertSuccess(project);
|
||||
j.buildAndAssertSuccess(project);
|
||||
j.buildAndAssertSuccess(project);
|
||||
j.buildAndAssertSuccess(project);
|
||||
assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).getBuilds(), hasSize(5));
|
||||
|
||||
CLICommandInvoker.Result result = command
|
||||
.authorizedTo(Jenkins.READ, Job.READ, Run.DELETE)
|
||||
.invokeWithArgs("aProject", "1,2");
|
||||
assertThat(result, succeeded());
|
||||
assertThat(result.stdout(), containsString("Deleted 2 builds"));
|
||||
assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).getBuilds(), hasSize(3));
|
||||
|
||||
result = command
|
||||
.authorizedTo(Jenkins.READ, Job.READ, Run.DELETE)
|
||||
.invokeWithArgs("aProject", "3-5");
|
||||
assertThat(result, succeeded());
|
||||
assertThat(result.stdout(), containsString("Deleted 3 builds"));
|
||||
assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).getBuilds(), hasSize(0));
|
||||
}
|
||||
|
||||
@Test public void deleteBuildsManyShouldSuccessEvenABuildIsSpecifiedTwice() throws Exception {
|
||||
FreeStyleProject project = j.createFreeStyleProject("aProject");
|
||||
j.buildAndAssertSuccess(project);
|
||||
j.buildAndAssertSuccess(project);
|
||||
assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).getBuilds(), hasSize(2));
|
||||
|
||||
CLICommandInvoker.Result result = command
|
||||
.authorizedTo(Jenkins.READ, Job.READ, Run.DELETE)
|
||||
.invokeWithArgs("aProject", "1,1");
|
||||
assertThat(result, succeeded());
|
||||
assertThat(result.stdout(), containsString("Deleted 1 builds"));
|
||||
assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).getBuilds(), hasSize(1));
|
||||
|
||||
result = command
|
||||
.authorizedTo(Jenkins.READ, Job.READ, Run.DELETE)
|
||||
.invokeWithArgs("aProject", "1-1,1-2,2-2");
|
||||
assertThat(result, succeeded());
|
||||
assertThat(result.stdout(), containsString("Deleted 1 builds"));
|
||||
assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).getBuilds(), hasSize(0));
|
||||
}
|
||||
|
||||
@Test public void deleteBuildsManyShouldSuccessEvenLastBuildDoesNotExist() throws Exception {
|
||||
FreeStyleProject project = j.createFreeStyleProject("aProject");
|
||||
j.buildAndAssertSuccess(project);
|
||||
j.buildAndAssertSuccess(project);
|
||||
assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).getBuilds(), hasSize(2));
|
||||
|
||||
CLICommandInvoker.Result result = command
|
||||
.authorizedTo(Jenkins.READ, Job.READ, Run.DELETE)
|
||||
.invokeWithArgs("aProject", "1,3");
|
||||
assertThat(result, succeeded());
|
||||
assertThat(result.stdout(), containsString("Deleted 1 builds"));
|
||||
assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).getBuilds(), hasSize(1));
|
||||
|
||||
result = command
|
||||
.authorizedTo(Jenkins.READ, Job.READ, Run.DELETE)
|
||||
.invokeWithArgs("aProject", "2-3");
|
||||
assertThat(result, succeeded());
|
||||
assertThat(result.stdout(), containsString("Deleted 1 builds"));
|
||||
assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).getBuilds(), hasSize(0));
|
||||
}
|
||||
|
||||
@Test public void deleteBuildsManyShouldSuccessEvenMiddleBuildDoesNotExist() throws Exception {
|
||||
FreeStyleProject project = j.createFreeStyleProject("aProject");
|
||||
j.buildAndAssertSuccess(project);
|
||||
j.buildAndAssertSuccess(project);
|
||||
j.buildAndAssertSuccess(project);
|
||||
j.buildAndAssertSuccess(project);
|
||||
j.buildAndAssertSuccess(project);
|
||||
j.buildAndAssertSuccess(project);
|
||||
project.getBuildByNumber(2).delete();
|
||||
project.getBuildByNumber(5).delete();
|
||||
assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).getBuilds(), hasSize(4));
|
||||
|
||||
CLICommandInvoker.Result result = command
|
||||
.authorizedTo(Jenkins.READ, Job.READ, Run.DELETE)
|
||||
.invokeWithArgs("aProject", "1,2,3");
|
||||
assertThat(result, succeeded());
|
||||
assertThat(result.stdout(), containsString("Deleted 2 builds"));
|
||||
assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).getBuilds(), hasSize(2));
|
||||
|
||||
result = command
|
||||
.authorizedTo(Jenkins.READ, Job.READ, Run.DELETE)
|
||||
.invokeWithArgs("aProject", "4-6");
|
||||
assertThat(result, succeeded());
|
||||
assertThat(result.stdout(), containsString("Deleted 2 builds"));
|
||||
assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).getBuilds(), hasSize(0));
|
||||
}
|
||||
|
||||
@Test public void deleteBuildsManyShouldSuccessEvenFirstBuildDoesNotExist() throws Exception {
|
||||
FreeStyleProject project = j.createFreeStyleProject("aProject");
|
||||
j.buildAndAssertSuccess(project);
|
||||
j.buildAndAssertSuccess(project);
|
||||
j.buildAndAssertSuccess(project);
|
||||
project.getBuildByNumber(1).delete();
|
||||
assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).getBuilds(), hasSize(2));
|
||||
|
||||
CLICommandInvoker.Result result = command
|
||||
.authorizedTo(Jenkins.READ, Job.READ, Run.DELETE)
|
||||
.invokeWithArgs("aProject", "1,2");
|
||||
assertThat(result, succeeded());
|
||||
assertThat(result.stdout(), containsString("Deleted 1 builds"));
|
||||
assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).getBuilds(), hasSize(1));
|
||||
|
||||
result = command
|
||||
.authorizedTo(Jenkins.READ, Job.READ, Run.DELETE)
|
||||
.invokeWithArgs("aProject", "2-3");
|
||||
assertThat(result, succeeded());
|
||||
assertThat(result.stdout(), containsString("Deleted 1 builds"));
|
||||
assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).getBuilds(), hasSize(0));
|
||||
}
|
||||
|
||||
@Test public void deleteBuildsManyShouldSuccessEvenTheFirstAndLastBuildDoesNotExist() throws Exception {
|
||||
FreeStyleProject project = j.createFreeStyleProject("aProject");
|
||||
j.buildAndAssertSuccess(project);
|
||||
j.buildAndAssertSuccess(project);
|
||||
j.buildAndAssertSuccess(project);
|
||||
j.buildAndAssertSuccess(project);
|
||||
j.buildAndAssertSuccess(project);
|
||||
project.getBuildByNumber(1).delete();
|
||||
project.getBuildByNumber(3).delete();
|
||||
project.getBuildByNumber(5).delete();
|
||||
assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).getBuilds(), hasSize(2));
|
||||
|
||||
CLICommandInvoker.Result result = command
|
||||
.authorizedTo(Jenkins.READ, Job.READ, Run.DELETE)
|
||||
.invokeWithArgs("aProject", "1,2,3");
|
||||
assertThat(result, succeeded());
|
||||
assertThat(result.stdout(), containsString("Deleted 1 builds"));
|
||||
assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).getBuilds(), hasSize(1));
|
||||
|
||||
result = command
|
||||
.authorizedTo(Jenkins.READ, Job.READ, Run.DELETE)
|
||||
.invokeWithArgs("aProject", "3-5");
|
||||
assertThat(result, succeeded());
|
||||
assertThat(result.stdout(), containsString("Deleted 1 builds"));
|
||||
assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).getBuilds(), hasSize(0));
|
||||
}
|
||||
}
|
||||
@@ -144,14 +144,21 @@ public class ExecutorTest {
|
||||
assertThat(log, containsString("Disconnected by Johnny : Taking offline to break your buil"));
|
||||
}
|
||||
|
||||
private Future<FreeStyleBuild> startBlockingBuild(FreeStyleProject p) throws Exception {
|
||||
/**
|
||||
* Start a project with an infinite build step
|
||||
*
|
||||
* @param project {@link FreeStyleProject} to start
|
||||
* @return A {@link Future} object represents the started build
|
||||
* @throws Exception if somethink wrong happened
|
||||
*/
|
||||
public static Future<FreeStyleBuild> startBlockingBuild(FreeStyleProject project) throws Exception {
|
||||
final OneShotEvent e = new OneShotEvent();
|
||||
|
||||
p.getBuildersList().add(new BlockingBuilder(e));
|
||||
project.getBuildersList().add(new BlockingBuilder(e));
|
||||
|
||||
Future<FreeStyleBuild> r = p.scheduleBuild2(0);
|
||||
Future<FreeStyleBuild> r = project.scheduleBuild2(0);
|
||||
e.block(); // wait until we are safe to interrupt
|
||||
assertTrue(p.getLastBuild().isBuilding());
|
||||
assertTrue(project.getLastBuild().isBuilding());
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
@@ -200,12 +200,17 @@ exports.testConnectivity = function(siteId, handler) {
|
||||
handler(false, true, response.message);
|
||||
}
|
||||
|
||||
// Define statuses, which need additional check iteration via async job on the Jenkins master
|
||||
// Statuses like "OK" or "SKIPPED" are considered as fine.
|
||||
var uncheckedStatuses = ['PRECHECK', 'CHECKING', 'UNCHECKED'];
|
||||
if(uncheckedStatuses.indexOf(response.data.updatesite) >= 0 || uncheckedStatuses.indexOf(response.data.internet) >= 0) {
|
||||
setTimeout(testConnectivity, 100);
|
||||
}
|
||||
else {
|
||||
if(response.status !== 'ok' || response.data.updatesite !== 'OK' || response.data.internet !== 'OK') {
|
||||
// Update site should be always reachable, but we do not require the internet connection
|
||||
// if it's explicitly skipped by the update center
|
||||
if(response.status !== 'ok' || response.data.updatesite !== 'OK' ||
|
||||
(response.data.internet !== 'OK' && response.data.internet !== 'SKIPPED')) {
|
||||
// no connectivity, but not fatal
|
||||
handler(false, false);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user