From abef2ccd407dd173375b0daf04146c94cf10449a Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Sat, 9 Nov 2024 16:46:02 -0500 Subject: [PATCH] Introducing `ControllerToAgentCallable` and `ControllerToAgentFileCallable` (#9921) Co-authored-by: Vincent Latombe --- core/src/main/java/hudson/FilePath.java | 27 ++--------- core/src/main/java/hudson/Launcher.java | 47 +++--------------- .../jenkins/MasterToSlaveFileCallable.java | 22 +++------ .../agents/ControllerToAgentCallable.java | 48 +++++++++++++++++++ .../agents/ControllerToAgentFileCallable.java | 43 +++++++++++++++++ .../security/MasterToSlaveCallable.java | 20 +++----- .../jenkins/slaves/RemotingVersionInfo.java | 3 +- 7 files changed, 114 insertions(+), 96 deletions(-) create mode 100644 core/src/main/java/jenkins/agents/ControllerToAgentCallable.java create mode 100644 core/src/main/java/jenkins/agents/ControllerToAgentFileCallable.java diff --git a/core/src/main/java/hudson/FilePath.java b/core/src/main/java/hudson/FilePath.java index f5288b26d9d1..e48d7b45108f 100644 --- a/core/src/main/java/hudson/FilePath.java +++ b/core/src/main/java/hudson/FilePath.java @@ -124,7 +124,7 @@ import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; import jenkins.MasterToSlaveFileCallable; -import jenkins.SlaveToMasterFileCallable; +import jenkins.agents.ControllerToAgentFileCallable; import jenkins.model.Jenkins; import jenkins.security.MasterToSlaveCallable; import jenkins.util.ContextResettingExecutorService; @@ -520,21 +520,7 @@ public int archive(final ArchiverFactory factory, OutputStream os, final DirScan return act(new Archive(factory, out, scanner, verificationRoot, openOptions)); } - private static class Archive extends MasterToSlaveFileCallable { - private final ArchiverFactory factory; - private final OutputStream out; - private final DirScanner scanner; - private final String verificationRoot; - private OpenOption[] openOptions; - - Archive(ArchiverFactory factory, OutputStream out, DirScanner scanner, String verificationRoot, OpenOption... openOptions) { - this.factory = factory; - this.out = out; - this.scanner = scanner; - this.verificationRoot = verificationRoot; - this.openOptions = openOptions; - } - + private record Archive(ArchiverFactory factory, OutputStream out, DirScanner scanner, String verificationRoot, OpenOption... openOptions) implements ControllerToAgentFileCallable { @Override public Integer invoke(File f, VirtualChannel channel) throws IOException { try (Archiver a = factory.create(out)) { @@ -542,8 +528,6 @@ public Integer invoke(File f, VirtualChannel channel) throws IOException { return a.countEntries(); } } - - private static final long serialVersionUID = 1L; } public int archive(final ArchiverFactory factory, OutputStream os, final FileFilter filter) throws IOException, InterruptedException { @@ -1185,12 +1169,7 @@ public void copyFrom(org.apache.commons.fileupload.FileItem file) throws IOExcep /** * Code that gets executed on the machine where the {@link FilePath} is local. * Used to act on {@link FilePath}. - * Warning: implementations must be serializable, so prefer a static nested class to an inner class. - * - *

- * Subtypes would likely want to extend from either {@link MasterToSlaveCallable} - * or {@link SlaveToMasterFileCallable}. - * + * A typical implementation would be a {@code record} implementing {@link ControllerToAgentFileCallable}. * @see FilePath#act(FileCallable) */ public interface FileCallable extends Serializable, RoleSensitive { diff --git a/core/src/main/java/hudson/Launcher.java b/core/src/main/java/hudson/Launcher.java index e6ba431691fb..195671b1b42e 100644 --- a/core/src/main/java/hudson/Launcher.java +++ b/core/src/main/java/hudson/Launcher.java @@ -55,6 +55,7 @@ import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; +import jenkins.agents.ControllerToAgentCallable; import jenkins.model.Jenkins; import jenkins.security.MasterToSlaveCallable; import jenkins.tasks.filters.EnvVarsFilterLocalRule; @@ -1114,8 +1115,7 @@ public Proc launch(ProcStarter ps) throws IOException { final String workDir = psPwd == null ? null : psPwd.getRemote(); try { - RemoteLaunchCallable remote = new RemoteLaunchCallable(ps.commands, ps.masks, ps.envs, in, ps.reverseStdin, out, ps.reverseStdout, err, ps.reverseStderr, ps.quiet, workDir, listener, ps.stdoutListener); - remote.setEnvVarsFilterRuleWrapper(envVarsFilterRuleWrapper); + RemoteLaunchCallable remote = new RemoteLaunchCallable(ps.commands, ps.masks, ps.envs, in, ps.reverseStdin, out, ps.reverseStdout, err, ps.reverseStderr, ps.quiet, workDir, listener, ps.stdoutListener, envVarsFilterRuleWrapper); // reset the rules to prevent build step without rules configuration to re-use those envVarsFilterRuleWrapper = null; return new ProcImpl(getChannel().call(remote)); @@ -1334,46 +1334,13 @@ public interface RemoteProcess { IOTriplet getIOtriplet(); } - private static class RemoteLaunchCallable extends MasterToSlaveCallable { - private final @NonNull List cmd; - private final @CheckForNull boolean[] masks; - private final @CheckForNull String[] env; - private final @CheckForNull InputStream in; - private final @CheckForNull OutputStream out; - private final @CheckForNull OutputStream err; - private final @CheckForNull String workDir; - private final @NonNull TaskListener listener; - private final @CheckForNull TaskListener stdoutListener; - private final boolean reverseStdin, reverseStdout, reverseStderr; - private final boolean quiet; - - private EnvVarsFilterRuleWrapper envVarsFilterRuleWrapper; - - RemoteLaunchCallable(@NonNull List cmd, @CheckForNull boolean[] masks, @CheckForNull String[] env, + private record RemoteLaunchCallable(@NonNull List cmd, @CheckForNull boolean[] masks, @CheckForNull String[] env, @CheckForNull InputStream in, boolean reverseStdin, @CheckForNull OutputStream out, boolean reverseStdout, @CheckForNull OutputStream err, boolean reverseStderr, - boolean quiet, @CheckForNull String workDir, @NonNull TaskListener listener, @CheckForNull TaskListener stdoutListener) { - this.cmd = new ArrayList<>(cmd); - this.masks = masks; - this.env = env; - this.in = in; - this.out = out; - this.err = err; - this.workDir = workDir; - this.listener = listener; - this.stdoutListener = stdoutListener; - this.reverseStdin = reverseStdin; - this.reverseStdout = reverseStdout; - this.reverseStderr = reverseStderr; - this.quiet = quiet; - } - - @Restricted(NoExternalUse.class) - public void setEnvVarsFilterRuleWrapper(EnvVarsFilterRuleWrapper envVarsFilterRuleWrapper) { - this.envVarsFilterRuleWrapper = envVarsFilterRuleWrapper; - } - + boolean quiet, @CheckForNull String workDir, + @NonNull TaskListener listener, @CheckForNull TaskListener stdoutListener, + @CheckForNull EnvVarsFilterRuleWrapper envVarsFilterRuleWrapper) implements ControllerToAgentCallable { @Override public RemoteProcess call() throws IOException { final Channel channel = getOpenChannelOrFail(); @@ -1433,8 +1400,6 @@ public IOTriplet getIOtriplet() { } }); } - - private static final long serialVersionUID = 1L; } private static class RemoteChannelLaunchCallable extends MasterToSlaveCallable { diff --git a/core/src/main/java/jenkins/MasterToSlaveFileCallable.java b/core/src/main/java/jenkins/MasterToSlaveFileCallable.java index 34afd3c11ae5..6296c6d65b28 100644 --- a/core/src/main/java/jenkins/MasterToSlaveFileCallable.java +++ b/core/src/main/java/jenkins/MasterToSlaveFileCallable.java @@ -1,26 +1,16 @@ package jenkins; import hudson.FilePath.FileCallable; -import hudson.remoting.VirtualChannel; -import java.io.File; -import jenkins.security.Roles; -import jenkins.slaves.RemotingVersionInfo; -import org.jenkinsci.remoting.RoleChecker; +import jenkins.agents.ControllerToAgentFileCallable; /** - * {@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()}. - * + * {@link FileCallable}s that could run on an agent. + * For new code, implement {@link ControllerToAgentFileCallable} + * which has the advantage that it can be used on {@code record}s. * @since 1.587 / 1.580.1 - * @param the return type; note that this must either be defined in your plugin or included in the stock JEP-200 whitelist + * @param the return type */ -public abstract class MasterToSlaveFileCallable implements FileCallable { - @Override - public void checkRoles(RoleChecker checker) throws SecurityException { - checker.check(this, Roles.SLAVE); - } +public abstract class MasterToSlaveFileCallable implements ControllerToAgentFileCallable { private static final long serialVersionUID = 1L; } diff --git a/core/src/main/java/jenkins/agents/ControllerToAgentCallable.java b/core/src/main/java/jenkins/agents/ControllerToAgentCallable.java new file mode 100644 index 000000000000..b724da77f6a2 --- /dev/null +++ b/core/src/main/java/jenkins/agents/ControllerToAgentCallable.java @@ -0,0 +1,48 @@ +/* + * The MIT License + * + * Copyright 2024 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.agents; + +import hudson.remoting.Callable; +import jenkins.security.Roles; +import jenkins.slaves.RemotingVersionInfo; +import org.jenkinsci.remoting.RoleChecker; + +/** + * {@link Callable} meant to be serialized then run on an agent. + * A typical implementation will be a {@link Record} + * since instance state merely transfers a set of parameters to an agent JVM. + *

Note that the logic within {@link #call} may not use Remoting APIs + * newer than {@link RemotingVersionInfo#getMinimumSupportedVersion}. + * (Core and plugin APIs will be identical to those run inside the controller.) + * @param the return type; note that this must either be defined in your plugin or included in the stock JEP-200 whitelist + * @since TODO + */ +public interface ControllerToAgentCallable extends Callable { + + @Override + default void checkRoles(RoleChecker checker) throws SecurityException { + checker.check(this, Roles.SLAVE); + } +} diff --git a/core/src/main/java/jenkins/agents/ControllerToAgentFileCallable.java b/core/src/main/java/jenkins/agents/ControllerToAgentFileCallable.java new file mode 100644 index 000000000000..22904e0c2e43 --- /dev/null +++ b/core/src/main/java/jenkins/agents/ControllerToAgentFileCallable.java @@ -0,0 +1,43 @@ +/* + * The MIT License + * + * Copyright 2024 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.agents; + +import hudson.FilePath; +import jenkins.security.Roles; +import org.jenkinsci.remoting.RoleChecker; + +/** + * {@link FilePath.FileCallable} meant to be serialized then run on an agent. + * Like {@link ControllerToAgentCallable} this will typically be a {@link Record}. + * @param the return type; note that this must either be defined in your plugin or included in the stock JEP-200 whitelist + * @since TODO + */ +public interface ControllerToAgentFileCallable extends FilePath.FileCallable { + + @Override + default void checkRoles(RoleChecker checker) throws SecurityException { + checker.check(this, Roles.SLAVE); + } +} diff --git a/core/src/main/java/jenkins/security/MasterToSlaveCallable.java b/core/src/main/java/jenkins/security/MasterToSlaveCallable.java index db0406bcf548..7d3830bb9562 100644 --- a/core/src/main/java/jenkins/security/MasterToSlaveCallable.java +++ b/core/src/main/java/jenkins/security/MasterToSlaveCallable.java @@ -1,25 +1,17 @@ package jenkins.security; import hudson.remoting.Callable; -import jenkins.slaves.RemotingVersionInfo; -import org.jenkinsci.remoting.RoleChecker; +import jenkins.agents.ControllerToAgentCallable; /** - * 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()}. - * + * {@link Callable} meant to be run on agent. + * For new code, implement {@link ControllerToAgentCallable} + * which has the advantage that it can be used on {@code record}s. * @author Kohsuke Kawaguchi * @since 1.587 / 1.580.1 - * @param the return type; note that this must either be defined in your plugin or included in the stock JEP-200 whitelist + * @param the return type */ -public abstract class MasterToSlaveCallable implements Callable { +public abstract class MasterToSlaveCallable implements ControllerToAgentCallable { private static final long serialVersionUID = 1L; - - @Override - public void checkRoles(RoleChecker checker) throws SecurityException { - checker.check(this, Roles.SLAVE); - } } diff --git a/core/src/main/java/jenkins/slaves/RemotingVersionInfo.java b/core/src/main/java/jenkins/slaves/RemotingVersionInfo.java index fb17b7243c9f..d595a21d22df 100644 --- a/core/src/main/java/jenkins/slaves/RemotingVersionInfo.java +++ b/core/src/main/java/jenkins/slaves/RemotingVersionInfo.java @@ -31,6 +31,7 @@ import java.util.Properties; import java.util.logging.Level; import java.util.logging.Logger; +import jenkins.agents.ControllerToAgentCallable; /** * Provides information about Remoting versions used within the core. @@ -100,7 +101,7 @@ public static VersionNumber getEmbeddedVersion() { /** * Gets Remoting version which is supported by the core. - * Jenkins core and plugins make invoke operations on agents (e.g. {@link jenkins.security.MasterToSlaveCallable}) + * Jenkins core and plugins make invoke operations on agents (e.g. {@link ControllerToAgentCallable}) * 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.