Skip to content

Commit

Permalink
[JENKINS-47393] Preparation for split of CommandLauncher to a plugin.
Browse files Browse the repository at this point in the history
  • Loading branch information
jglick committed Oct 11, 2017
1 parent 860d9c5 commit e376780
Show file tree
Hide file tree
Showing 74 changed files with 325 additions and 102 deletions.
52 changes: 52 additions & 0 deletions command-launcher-plugin/pom.xml
@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>plugin</artifactId>
<version>2.34</version>
<relativePath />
</parent>
<artifactId>command-launcher</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>hpi</packaging>
<properties>
<jenkins.version>2.85-SNAPSHOT</jenkins.version> <!-- TODO pending split -->
<java.level>8</java.level>
</properties>
<name>Command Launcher Plugin</name>
<description>Allows agents to be launched using a specified command.</description>
<url>https://wiki.jenkins-ci.org/display/JENKINS/Command+Launcher+Plugin</url>
<licenses>
<license>
<name>MIT License</name>
<url>http://opensource.org/licenses/MIT</url>
</license>
</licenses>
<!-- TODO after split
<scm>
<connection>scm:git:git://github.com/jenkinsci/${project.artifactId}-plugin.git</connection>
<developerConnection>scm:git:git@github.com:jenkinsci/${project.artifactId}-plugin.git</developerConnection>
<url>https://github.com/jenkinsci/${project.artifactId}-plugin</url>
</scm>
-->
<repositories>
<repository>
<id>repo.jenkins-ci.org</id>
<url>https://repo.jenkins-ci.org/public/</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>repo.jenkins-ci.org</id>
<url>https://repo.jenkins-ci.org/public/</url>
</pluginRepository>
</pluginRepositories>
<dependencies>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>script-security</artifactId>
<version>1.18.1</version>
</dependency>
</dependencies>
</project>
Expand Up @@ -28,11 +28,13 @@
import hudson.Util;
import hudson.model.TaskListener;
import hudson.util.FormValidation;
import java.io.IOException;
import org.jenkinsci.Symbol;
import org.jenkinsci.plugins.command_launcher.CommandLanguage;
import org.jenkinsci.plugins.command_launcher.Messages;
import org.jenkinsci.plugins.scriptsecurity.scripts.ApprovalContext;
import org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval;
import org.kohsuke.stapler.DataBoundConstructor;

import java.io.IOException;
import jenkins.model.Jenkins;
import org.kohsuke.stapler.QueryParameter;

/**
Expand All @@ -46,17 +48,19 @@ public class CommandConnector extends ComputerConnector {
@DataBoundConstructor
public CommandConnector(String command) {
this.command = command;
Jenkins.getInstance().checkPermission(Jenkins.RUN_SCRIPTS);
// TODO add withKey if we can determine the Cloud.name being configured
ScriptApproval.get().configuring(command, CommandLanguage.get(), ApprovalContext.create().withCurrentUser());
}

private Object readResolve() {
Jenkins.getInstance().checkPermission(Jenkins.RUN_SCRIPTS);
ScriptApproval.get().configuring(command, CommandLanguage.get(), ApprovalContext.create());
return this;
}

@Override
public CommandLauncher launch(String host, TaskListener listener) throws IOException, InterruptedException {
return new CommandLauncher(command,new EnvVars("SLAVE",host));
// no need to call ScriptApproval.using here; CommandLauncher.launch will do that
return new CommandLauncher(new EnvVars("SLAVE", host), command);
}

@Extension @Symbol("command")
Expand All @@ -67,13 +71,10 @@ public String getDisplayName() {
}

public FormValidation doCheckCommand(@QueryParameter String value) {
if (!Jenkins.getInstance().hasPermission(Jenkins.RUN_SCRIPTS)) {
return FormValidation.warning(Messages.CommandLauncher_cannot_be_configured_by_non_administrato());
}
if (Util.fixEmptyAndTrim(value) == null) {
return FormValidation.error(Messages.CommandLauncher_NoLaunchCommand());
} else {
return FormValidation.ok();
return ScriptApproval.get().checking(value, CommandLanguage.get());
}
}

Expand Down
Expand Up @@ -33,7 +33,6 @@
import jenkins.model.Jenkins;
import hudson.model.TaskListener;
import hudson.remoting.Channel;
import hudson.security.ACL;
import hudson.util.StreamCopyThread;
import hudson.util.FormValidation;
import hudson.util.ProcessTree;
Expand All @@ -45,6 +44,10 @@

import org.apache.commons.lang.StringUtils;
import org.jenkinsci.Symbol;
import org.jenkinsci.plugins.command_launcher.CommandLanguage;
import org.jenkinsci.plugins.scriptsecurity.scripts.ApprovalContext;
import org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval;
import org.jenkinsci.plugins.scriptsecurity.scripts.UnapprovedUsageException;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;

Expand All @@ -67,19 +70,30 @@ public class CommandLauncher extends ComputerLauncher {
*/
private final EnvVars env;

/** Constructor for use from UI. Conditionally approves the script. */
@DataBoundConstructor
public CommandLauncher(String command) {
this(command, null);
agentCommand = command;
env = null;
// TODO add withKey if we can determine the Slave.nodeName being configured
ScriptApproval.get().configuring(command, CommandLanguage.get(), ApprovalContext.create().withCurrentUser());
}

/** Constructor for programmatic use. Always approves the script. */
public CommandLauncher(String command, EnvVars env) {
this.agentCommand = command;
this.env = env;
Jenkins.getInstance().checkPermission(Jenkins.RUN_SCRIPTS);
ScriptApproval.get().preapprove(command, CommandLanguage.get());
}

/** Constructor for use from {@link CommandConnector}. Never approves the script. */
CommandLauncher(EnvVars env, String command) {
this.agentCommand = command;
this.env = env;
}

private Object readResolve() {
Jenkins.getInstance().checkPermission(Jenkins.RUN_SCRIPTS);
ScriptApproval.get().configuring(agentCommand, CommandLanguage.get(), ApprovalContext.create());
return this;
}

Expand All @@ -104,14 +118,15 @@ public void launch(SlaveComputer computer, final TaskListener listener) {
throw new AbortException("Cannot launch commands on deleted nodes");
}

listener.getLogger().println(hudson.model.Messages.Slave_Launching(getTimestamp()));
if(getCommand().trim().length()==0) {
listener.getLogger().println(Messages.CommandLauncher_NoLaunchCommand());
listener.getLogger().println(org.jenkinsci.plugins.command_launcher.Messages.Slave_Launching(getTimestamp()));
String command = ScriptApproval.get().using(getCommand(), CommandLanguage.get());
if (command.trim().length() == 0) {
listener.getLogger().println(org.jenkinsci.plugins.command_launcher.Messages.CommandLauncher_NoLaunchCommand());
return;
}
listener.getLogger().println("$ " + getCommand());
listener.getLogger().println("$ " + command);

ProcessBuilder pb = new ProcessBuilder(Util.tokenize(getCommand()));
ProcessBuilder pb = new ProcessBuilder(Util.tokenize(command));
final EnvVars cookie = _cookie = EnvVars.createCookie();
pb.environment().putAll(cookie);
pb.environment().put("WORKSPACE", StringUtils.defaultString(computer.getAbsoluteRemoteFs(), node.getRemoteFS())); //path for local slave log
Expand Down Expand Up @@ -153,6 +168,8 @@ public void onClosed(Channel channel, IOException cause) {
LOGGER.info("agent launched for " + computer.getDisplayName());
} catch (InterruptedException e) {
Functions.printStackTrace(e, listener.error(Messages.ComputerLauncher_abortedLaunch()));
} catch (UnapprovedUsageException e) {
listener.error(e.getMessage());
} catch (RuntimeException e) {
Functions.printStackTrace(e, listener.error(Messages.ComputerLauncher_unexpectedError()));
} catch (Error e) {
Expand All @@ -167,7 +184,7 @@ public void onClosed(Channel channel, IOException cause) {
msg = " : " + msg;
// FIXME TODO i18n what is this!?
}
msg = hudson.model.Messages.Slave_UnableToLaunch(computer.getDisplayName(), msg);
msg = org.jenkinsci.plugins.command_launcher.Messages.Slave_UnableToLaunch(computer.getDisplayName(), msg);
LOGGER.log(Level.SEVERE, msg, e);
Functions.printStackTrace(e, listener.error(msg));

Expand Down Expand Up @@ -196,17 +213,14 @@ private static void reportProcessTerminated(Process proc, TaskListener listener)
@Extension @Symbol("command")
public static class DescriptorImpl extends Descriptor<ComputerLauncher> {
public String getDisplayName() {
return Messages.CommandLauncher_displayName();
return org.jenkinsci.plugins.command_launcher.Messages.CommandLauncher_displayName();
}

public FormValidation doCheckCommand(@QueryParameter String value) {
if (!Jenkins.getInstance().hasPermission(Jenkins.RUN_SCRIPTS)) {
return FormValidation.warning(Messages.CommandLauncher_cannot_be_configured_by_non_administrato());
}
if(Util.fixEmptyAndTrim(value)==null)
return FormValidation.error(Messages.CommandLauncher_NoLaunchCommand());
return FormValidation.error(org.jenkinsci.plugins.command_launcher.Messages.CommandLauncher_NoLaunchCommand());
else
return FormValidation.ok();
return ScriptApproval.get().checking(value, CommandLanguage.get());
}
}
}
@@ -0,0 +1,55 @@
/*
* The MIT License
*
* Copyright 2017 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 org.jenkinsci.plugins.command_launcher;

import hudson.Extension;
import hudson.ExtensionList;
import hudson.Util;
import org.jenkinsci.plugins.scriptsecurity.scripts.Language;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;

/**
* Language for launched processes, as per {@link Util#tokenize(String)} and {@link ProcessBuilder}.
*/
@Restricted(NoExternalUse.class) // TODO move to script-security
@Extension
public class CommandLanguage extends Language {

public static Language get() {
return ExtensionList.lookup(Language.class).get(CommandLanguage.class);
}

@Override
public String getName() {
return "command";
}

@Override
public String getDisplayName() {
return "External Commands";
}

}
4 changes: 4 additions & 0 deletions command-launcher-plugin/src/main/resources/index.jelly
@@ -0,0 +1,4 @@
<?jelly escape-by-default='true'?>
<div>
Allows agents to be launched using a specified command.
</div>
@@ -0,0 +1,26 @@
# The MIT License
#
# Copyright 2017 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.

Slave.Launching={0} Launching agent
CommandLauncher.NoLaunchCommand=No launch command specified
CommandLauncher.displayName=Launch agent via execution of command on the master
Slave.UnableToLaunch=Unable to launch the agent for {0}{1}
@@ -0,0 +1,6 @@
CommandLauncher.NoLaunchCommand=\
\u041d\u0435 \u0435 \u0443\u043a\u0430\u0437\u0430\u043d\u0430 \u043a\u043e\u043c\u0430\u043d\u0434\u0430 \u0437\u0430 \u0441\u0442\u0430\u0440\u0442\u0438\u0440\u0430\u043d\u0435.
CommandLauncher.displayName=\
\u0421\u0442\u0430\u0440\u0442\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0430\u0433\u0435\u043d\u0442 \u0447\u0440\u0435\u0437 \u0441\u0442\u0430\u0440\u0442\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043a\u043e\u043c\u0430\u043d\u0434\u0430 \u043d\u0430 \u043e\u0441\u043d\u043e\u0432\u043d\u0438\u044f \u043a\u043e\u043c\u043f\u044e\u0442\u044a\u0440
Slave.UnableToLaunch=\
\u0410\u0433\u0435\u043d\u0442\u044a\u0442 \u0437\u0430 {0}{1} \u043d\u0435 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0441\u0435 \u0441\u0442\u0430\u0440\u0442\u0438\u0440\u0430
@@ -0,0 +1 @@
CommandLauncher.NoLaunchCommand=Ingen opstartskommando givet
@@ -0,0 +1,4 @@
Slave.Launching=Starte Agenten
CommandLauncher.NoLaunchCommand=Kein Startkommando angegeben.
CommandLauncher.displayName=Agent durch Ausf\u00FChrung eines Befehls auf dem Master-Knoten starten
Slave.UnableToLaunch=Kann Agent \u201E{0}\u201C nicht starten{1}
@@ -0,0 +1,3 @@
Slave.Launching={0} Arrancando el agente
CommandLauncher.NoLaunchCommand=No se ha especificado ningún comando
Slave.UnableToLaunch=Imposible ejecutar el agente en {0}{1}
@@ -0,0 +1 @@
CommandLauncher.NoLaunchCommand=Aucune commande de lancement sp\u00e9cifi\u00e9e
@@ -0,0 +1 @@
CommandLauncher.NoLaunchCommand=\u8d77\u52d5\u30b3\u30de\u30f3\u30c9\u304c\u6307\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002
@@ -0,0 +1,2 @@
Slave.Launching={0} Paleid\u017eiamas agentas
Slave.UnableToLaunch=Nepavyksta paleisti agento skirto {0}{1}
@@ -0,0 +1 @@
CommandLauncher.NoLaunchCommand=Sem nenhum comando de lan\u00e7amento especificado
@@ -0,0 +1,4 @@
Slave.Launching={0} \u043F\u043E\u043A\u0440\u0435\u0442\u0430\u045A\u0435 \u0430\u0433\u0435\u043D\u0442\u043E\u043C
CommandLauncher.NoLaunchCommand=\u041D\u0438\u0458\u0435 \u043D\u0430\u0432\u0435\u0434\u0435\u043D\u043E \u043A\u043E\u043C\u0430\u043D\u0434\u0430 \u0437\u0430 \u043F\u043E\u043A\u0440\u0435\u0442\u0430\u045A\u0435.
CommandLauncher.displayName=\u041F\u043E\u043A\u0440\u0435\u043D\u0438 \u0430\u0433\u0435\u043D\u0442\u0430 \u0438\u0437\u0432\u0440\u0448\u0430\u0432\u0430\u045A\u0443 \u043A\u043E\u043C\u0430\u043D\u0434\u0435 \u043D\u0430 \u0433\u043B\u0430\u0432\u043D\u043E\u0458 \u043C\u0430\u0448\u0438\u043D\u0438
Slave.UnableToLaunch=\u0410\u0433\u0435\u043D\u0430\u0442 \u0437\u0430 {0}{1} \u043D\u0435 \u043C\u043E\u0436\u0435 \u0434\u0430 \u0441\u0435 \u043F\u043E\u043A\u0440\u0435\u043D\u0435
@@ -0,0 +1 @@
CommandLauncher.NoLaunchCommand=\u6c92\u6709\u6307\u5b9a\u555f\u52d5\u6307\u4ee4
Expand Up @@ -28,7 +28,7 @@
/**
* @author Kohsuke Kawaguchi
*/
public class ComputerConnectorTest extends HudsonTestCase {
public class CommandConnectorTest extends HudsonTestCase {
public void testConfigRoundtrip() throws Exception {
CommandConnector cc = new CommandConnector("abc def");
assertEqualDataBoundBeans(cc,configRoundtrip(cc));
Expand Down

0 comments on commit e376780

Please sign in to comment.