Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Merge pull request #3250 from oleg-nenashev/feature/JENKINS-48766-min…
…imum-remoting-version

[JENKINS-48766] - Expose information about Remoting versions via API
  • Loading branch information
oleg-nenashev committed Jan 28, 2018
2 parents 73e2c9f + 07d8f4e commit dbc7512
Show file tree
Hide file tree
Showing 7 changed files with 124 additions and 20 deletions.
2 changes: 2 additions & 0 deletions core/src/main/java/hudson/TcpSlaveAgentListener.java
Expand Up @@ -34,6 +34,7 @@
import hudson.model.AperiodicWork;
import jenkins.model.Jenkins;
import jenkins.model.identity.InstanceIdentityProvider;
import jenkins.slaves.RemotingVersionInfo;
import jenkins.util.SystemProperties;
import hudson.slaves.OfflineCause;
import java.io.DataOutputStream;
Expand Down Expand Up @@ -300,6 +301,7 @@ private void respondHello(String header, Socket s) throws IOException {
o.write("Jenkins-Session: " + Jenkins.SESSION_HASH + "\r\n");
o.write("Client: " + s.getInetAddress().getHostAddress() + "\r\n");
o.write("Server: " + s.getLocalAddress().getHostAddress() + "\r\n");
o.write("Remoting-Minimum-Version: " + RemotingVersionInfo.getMinimumSupportedVersion() + "\r\n");
o.flush();
s.shutdownOutput();
} else {
Expand Down
8 changes: 8 additions & 0 deletions core/src/main/java/hudson/slaves/SlaveComputer.java
Expand Up @@ -47,13 +47,15 @@
import hudson.util.NullStream;
import hudson.util.RingBufferLogHandler;
import hudson.util.StreamTaskListener;
import hudson.util.VersionNumber;
import hudson.util.io.RewindableFileOutputStream;
import hudson.util.io.RewindableRotatingFileOutputStream;
import jenkins.model.Jenkins;
import jenkins.security.ChannelConfigurator;
import jenkins.security.MasterToSlaveCallable;
import jenkins.slaves.EncryptedSlaveAgentJnlpFile;
import jenkins.slaves.JnlpSlaveAgentProtocol;
import jenkins.slaves.RemotingVersionInfo;
import jenkins.slaves.systemInfo.SlaveSystemInfo;
import jenkins.util.SystemProperties;
import org.acegisecurity.context.SecurityContext;
Expand Down Expand Up @@ -545,6 +547,12 @@ public void onClosed(Channel c, IOException cause) {

String slaveVersion = channel.call(new SlaveVersion());
log.println("Remoting version: " + slaveVersion);
VersionNumber agentVersion = new VersionNumber(slaveVersion);
if (agentVersion.isOlderThan(RemotingVersionInfo.getMinimumSupportedVersion())) {
log.println(String.format("WARNING: Remoting version is older than a minimum required one (%s). " +
"Connection will not be rejected, but the compatibility is NOT guaranteed",
RemotingVersionInfo.getMinimumSupportedVersion()));
}

boolean _isUnix = channel.call(new DetectOS());
log.println(_isUnix? hudson.model.Messages.Slave_UnixSlave():hudson.model.Messages.Slave_WindowsSlave());
Expand Down
7 changes: 7 additions & 0 deletions core/src/main/java/jenkins/MasterToSlaveFileCallable.java
@@ -1,12 +1,19 @@
package jenkins;

import hudson.FilePath.FileCallable;
import hudson.remoting.VirtualChannel;
import jenkins.security.Roles;
import jenkins.slaves.RemotingVersionInfo;
import org.jenkinsci.remoting.RoleChecker;

import java.io.File;

/**
* {@link FileCallable}s that are meant to be only used on the master.
*
* Note that the logic within {@link #invoke(File, VirtualChannel)} should use API of a minimum supported Remoting version.
* See {@link RemotingVersionInfo#getMinimumSupportedVersion()}.
*
* @since 1.587 / 1.580.1
* @param <T> the return type; note that this must either be defined in your plugin or included in the stock JEP-200 whitelist
*/
Expand Down
Expand Up @@ -3,6 +3,7 @@
import hudson.remoting.Callable;
import hudson.remoting.Channel;
import hudson.remoting.ChannelClosedException;
import jenkins.slaves.RemotingVersionInfo;
import org.jenkinsci.remoting.RoleChecker;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
Expand All @@ -11,6 +12,9 @@
/**
* Convenient {@link Callable} meant to be run on agent.
*
* Note that the logic within {@link #call()} should use API of a minimum supported Remoting version.
* See {@link RemotingVersionInfo#getMinimumSupportedVersion()}.
*
* @author Kohsuke Kawaguchi
* @since 1.587 / 1.580.1
* @param <V> the return type; note that this must either be defined in your plugin or included in the stock JEP-200 whitelist
Expand Down
51 changes: 31 additions & 20 deletions core/src/main/java/jenkins/slaves/RemotingVersionInfo.java
Expand Up @@ -24,10 +24,7 @@
package jenkins.slaves;

import hudson.util.VersionNumber;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;

import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import java.io.IOException;
import java.io.InputStream;
Expand All @@ -39,17 +36,17 @@
/**
* Provides information about Remoting versions used within the core.
* @author Oleg Nenashev
* @since TODO
*/
@Restricted(NoExternalUse.class)
public class RemotingVersionInfo {

private static final Logger LOGGER = Logger.getLogger(RemotingVersionInfo.class.getName());
private static final String RESOURCE_NAME="remoting-info.properties";

@CheckForNull
@Nonnull
private static VersionNumber EMBEDDED_VERSION;

@CheckForNull
@Nonnull
private static VersionNumber MINIMUM_SUPPORTED_VERSION;

private RemotingVersionInfo() {}
Expand All @@ -64,39 +61,53 @@ private RemotingVersionInfo() {}
LOGGER.log(Level.WARNING, "Failed to load Remoting Info from " + RESOURCE_NAME, e);
}

EMBEDDED_VERSION = tryExtractVersion(props, "remoting.embedded.version");
MINIMUM_SUPPORTED_VERSION = tryExtractVersion(props, "remoting.minimum.supported.version");
EMBEDDED_VERSION = extractVersion(props, "remoting.embedded.version");
MINIMUM_SUPPORTED_VERSION = extractVersion(props, "remoting.minimum.supported.version");
}

@CheckForNull
private static VersionNumber tryExtractVersion(@Nonnull Properties props, @Nonnull String propertyName) {
@Nonnull
private static VersionNumber extractVersion(@Nonnull Properties props, @Nonnull String propertyName)
throws ExceptionInInitializerError {
String prop = props.getProperty(propertyName);
if (prop == null) {
LOGGER.log(Level.FINE, "Property {0} is not defined in {1}", new Object[] {propertyName, RESOURCE_NAME});
return null;
throw new ExceptionInInitializerError(String.format(
"Property %s is not defined in %s", propertyName, RESOURCE_NAME));
}

if(prop.contains("${")) { // Due to whatever reason, Maven does not nullify them
LOGGER.log(Level.WARNING, "Property {0} in {1} has unresolved variable(s). Raw value: {2}",
new Object[] {propertyName, RESOURCE_NAME, prop});
return null;
throw new ExceptionInInitializerError(String.format(
"Property %s in %s has unresolved variable(s). Raw value: %s",
propertyName, RESOURCE_NAME, prop));
}

try {
return new VersionNumber(prop);
} catch (RuntimeException ex) {
LOGGER.log(Level.WARNING, String.format("Failed to parse version for for property %s in %s. Raw Value: %s",
propertyName, RESOURCE_NAME, prop), ex);
return null;
throw new ExceptionInInitializerError(new IOException(
String.format("Failed to parse version for for property %s in %s. Raw Value: %s",
propertyName, RESOURCE_NAME, prop), ex));
}
}

@CheckForNull
/**
* Returns a version which is embedded into the Jenkins core.
* Note that this version <b>may</b> differ from one which is being really used in Jenkins.
* @return Remoting version
*/
@Nonnull
public static VersionNumber getEmbeddedVersion() {
return EMBEDDED_VERSION;
}

@CheckForNull
/**
* Gets Remoting version which is supported by the core.
* Jenkins core and plugins make invoke operations on agents (e.g. {@link jenkins.security.MasterToSlaveCallable})
* and use Remoting-internal API within them.
* In such case this API should be present on the remote side.
* This method defines a minimum expected version, so that all calls should use a compatible API.
* @return Minimal Remoting version for API calls.
*/
@Nonnull
public static VersionNumber getMinimumSupportedVersion() {
return MINIMUM_SUPPORTED_VERSION;
}
Expand Down
70 changes: 70 additions & 0 deletions test/src/test/java/jenkins/slaves/RemotingVersionInfoTest.java
@@ -0,0 +1,70 @@
/*
* The MIT License
*
* Copyright (c) 2018, CloudBees, 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 jenkins.slaves;

import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.For;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.JenkinsRule;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;

@For(RemotingVersionInfo.class)
public class RemotingVersionInfoTest {

@Rule
public JenkinsRule j = new JenkinsRule();

@Test
@Issue("JENKINS-48766")
public void warShouldIncludeRemotingManifestEntries() throws Exception {
ZipFile jenkinsWar = new ZipFile(new File(j.getWebAppRoot(), "../jenkins.war"));
ZipEntry entry = new JarEntry("META-INF/MANIFEST.MF");
try (InputStream inputStream = jenkinsWar.getInputStream(entry)) {
assertNotNull("Cannot open input stream for /META-INF/MANIFEST.MF", inputStream);
Manifest manifest = new Manifest(inputStream);

assertAttributeValue(manifest, "Remoting-Embedded-Version", RemotingVersionInfo.getEmbeddedVersion());
assertAttributeValue(manifest, "Remoting-Minimum-Supported-Version", RemotingVersionInfo.getMinimumSupportedVersion());
}
}

private void assertAttributeValue(Manifest manifest, String attributeName, Object expectedValue) throws AssertionError {
assertThat("Wrong value of manifest attribute " + attributeName,
manifest.getMainAttributes().getValue(attributeName),
equalTo(expectedValue.toString()));
}
}
2 changes: 2 additions & 0 deletions war/pom.xml
Expand Up @@ -199,6 +199,8 @@ THE SOFTWARE.
<Implementation-Version>${project.version}</Implementation-Version>
<Hudson-Version>1.395</Hudson-Version>
<Jenkins-Version>${project.version}</Jenkins-Version>
<Remoting-Embedded-Version>${remoting.version}</Remoting-Embedded-Version>
<Remoting-Minimum-Supported-Version>${remoting.minimum.supported.version}</Remoting-Minimum-Supported-Version>
</manifestEntries>
</archive>
<!--outputFileNameMapping>@{artifactId}@.@{extension}@</outputFileNameMapping-->
Expand Down

0 comments on commit dbc7512

Please sign in to comment.