Skip to content

Commit 6aeca51

Browse files
committed
Add interactive CLI mode #178
1 parent fc33230 commit 6aeca51

File tree

7 files changed

+243
-118
lines changed

7 files changed

+243
-118
lines changed

DOCKER.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ Optionally, if you need special Jenkins configuration, you can mount JCasC YAML
5858
[Configuration-as-Code documentation](https://github.com/jenkinsci/configuration-as-code-plugin)
5959
for available options and the [JCasC demo](demo/casc/README.md) for an example.
6060

61+
To get an interactive Jenkins CLI shell in the container, pass
62+
`-i -e FORCE_JENKINS_CLI=true` to `docker run` as extra parameters.
63+
6164
## Debug
6265
In case you want to debug Jenkinsfile Runner, you need to use the "Vanilla" Docker image built following the steps mentioned in the section above.
6366

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,9 +118,10 @@ The executable of Jenkinsfile Runner allows its invocation with these cli option
118118
Requires Jenkins 2.119 or above
119119
-a (--arg) : Parameters to be passed to workflow job. Use
120120
multiple -a switches for multiple params
121+
--cli : Launch interactive CLI. (default: false)
121122
-u (--keep-undefined-parameters) : Keep undefined parameters if set, defaults
122123
to false.
123-
-f (--file) FILE : Path to Jenkinsfile (or directory containing a
124+
-f (--file) FILE : Path to Jenkinsfile (or directory containing a
124125
Jenkinsfile) to run, default to ./Jenkinsfile.
125126
-ns (--no-sandbox) : Disable workflow job execution within sandbox
126127
environment
@@ -182,6 +183,8 @@ Advanced options:
182183

183184
* The `-ns` and `-a` options can be specified and passed to the image in the same way as the command line execution.
184185

186+
* You may pass `--cli` to obtain an interactive Jenkins CLI session.
187+
185188
## Docker build
186189

187190
docker build -t jenkins/jenkinsfile-runner .

bootstrap/src/main/java/io/jenkins/jenkinsfile/runner/bootstrap/Bootstrap.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ public class Bootstrap {
7070
"Note that the folder specified via --runHome will not be disposed after the run.")
7171
public File runHome;
7272

73-
73+
7474
private static final String DEFAULT_JOBNAME = "job";
7575

7676
/**
@@ -107,6 +107,8 @@ public class Bootstrap {
107107
@Option(name = "-u", aliases = { "--keep-undefined-parameters"}, usage = "Keep undefined parameters if set")
108108
public boolean keepUndefinedParameters = false;
109109

110+
@Option(name = "--cli", usage = "Launch interactive CLI.", forbids = { "-v", "--runWorkspace", "-a", "-ns" })
111+
public boolean cliOnly;
110112

111113
public static void main(String[] args) throws Throwable {
112114
// break for attaching profiler
@@ -141,6 +143,10 @@ private void postConstruct(CmdLineParser parser) throws IOException {
141143
System.exit(0);
142144
}
143145

146+
if (System.getenv("FORCE_JENKINS_CLI") != null) {
147+
this.cliOnly = true;
148+
}
149+
144150
if (this.version != null && !isVersionSupported()) {
145151
System.err.printf("Jenkins version [%s] not suported by this jenkinsfile-runner version (requires %s). \n",
146152
this.version,
@@ -159,7 +165,7 @@ private void postConstruct(CmdLineParser parser) throws IOException {
159165
}
160166

161167
if (this.jenkinsfile == null) this.jenkinsfile = new File("Jenkinsfile");
162-
if (!this.jenkinsfile.exists()) {
168+
if (!this.cliOnly && !this.jenkinsfile.exists()) {
163169
System.err.println("no Jenkinsfile in current directory.");
164170
System.exit(-1);
165171
}

setup/src/main/java/io/jenkins/jenkinsfile/runner/App.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,15 @@
99
public class App implements IApp {
1010
@Override
1111
public int run(Bootstrap bootstrap) throws Throwable {
12-
JenkinsfileRunnerLauncher launcher = new JenkinsfileRunnerLauncher(bootstrap);
13-
12+
JenkinsLauncher launcher = createLauncherFor(bootstrap);
1413
return launcher.launch();
1514
}
15+
16+
private JenkinsLauncher createLauncherFor(Bootstrap bootstrap) {
17+
if(bootstrap.cliOnly) {
18+
return new CLILauncher(bootstrap);
19+
} else {
20+
return new JenkinsfileRunnerLauncher(bootstrap);
21+
}
22+
}
1623
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package io.jenkins.jenkinsfile.runner;
2+
3+
import hudson.cli.CLICommand;
4+
import hudson.security.ACL;
5+
import io.jenkins.jenkinsfile.runner.bootstrap.Bootstrap;
6+
7+
import java.io.BufferedReader;
8+
import java.io.InputStreamReader;
9+
import java.util.Arrays;
10+
import java.util.Collections;
11+
import java.util.List;
12+
import java.util.Locale;
13+
14+
/**
15+
* Sets up a Jenkins environment that provides an interactive CLI.
16+
*/
17+
public class CLILauncher extends JenkinsLauncher {
18+
public CLILauncher(Bootstrap bootstrap) {
19+
super(bootstrap);
20+
}
21+
22+
@Override
23+
protected int doLaunch() throws Exception {
24+
// so that the CLI has all the access to the system
25+
ACL.impersonate(ACL.SYSTEM);
26+
BufferedReader commandIn = new BufferedReader(new InputStreamReader(System.in));
27+
String line;
28+
System.out.printf("Connected to Jenkins!%nType 'help' for a list of available commands, or 'exit' to quit.%n");
29+
System.out.print(" > ");
30+
while ((line = commandIn.readLine()) != null) {
31+
if(line.equalsIgnoreCase("exit")) {
32+
break;
33+
} else if(line.isEmpty()) {
34+
continue;
35+
}
36+
tryRunCommand(line);
37+
System.out.print(" > ");
38+
}
39+
System.out.println("bye");
40+
return 0;
41+
}
42+
43+
private void tryRunCommand(String commandLine) {
44+
try {
45+
String[] parts = commandLine.split(" ", 2);
46+
CLICommand command = CLICommand.clone(parts[0]);
47+
if(command == null) {
48+
System.err.println("No such command, try 'help'.");
49+
return;
50+
}
51+
command.main(findArgsFromLineParts(parts), Locale.getDefault(), System.in, System.out, System.err);
52+
} catch (Exception e) {
53+
e.printStackTrace();
54+
System.err.println("An unexpected error occurred executing this command.");
55+
}
56+
}
57+
58+
private List<String> findArgsFromLineParts(String[] commandLineParts) {
59+
if(commandLineParts.length > 1) {
60+
return Arrays.asList(commandLineParts[1].split(" "));
61+
} else {
62+
return Collections.emptyList();
63+
}
64+
}
65+
66+
@Override
67+
protected String getThreadName() {
68+
return "Jenkins CLI";
69+
}
70+
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package io.jenkins.jenkinsfile.runner;
2+
3+
import hudson.ClassicPluginStrategy;
4+
import io.jenkins.jenkinsfile.runner.bootstrap.Bootstrap;
5+
import io.jenkins.jenkinsfile.runner.util.HudsonHomeLoader;
6+
import jenkins.slaves.DeprecatedAgentProtocolMonitor;
7+
import org.eclipse.jetty.security.HashLoginService;
8+
import org.eclipse.jetty.security.LoginService;
9+
import org.eclipse.jetty.server.Server;
10+
import org.eclipse.jetty.util.thread.QueuedThreadPool;
11+
import org.eclipse.jetty.webapp.Configuration;
12+
import org.eclipse.jetty.webapp.WebAppContext;
13+
import org.eclipse.jetty.webapp.WebXmlConfiguration;
14+
15+
import javax.servlet.ServletContext;
16+
import java.util.HashSet;
17+
import java.util.Set;
18+
import java.util.logging.Level;
19+
import java.util.logging.Logger;
20+
21+
/**
22+
* Shared behaviour for different modes of launching an embedded Jenkins in the context of jenkinsfile-runner.
23+
*/
24+
public abstract class JenkinsLauncher extends JenkinsEmbedder {
25+
protected final Bootstrap bootstrap;
26+
/**
27+
* Keep the reference around to prevent them from getting GCed.
28+
*/
29+
private final Set<Object> noGc = new HashSet<>();
30+
31+
public JenkinsLauncher(Bootstrap bootstrap) {
32+
this.bootstrap = bootstrap;
33+
if(bootstrap.runHome != null) {
34+
if(!bootstrap.runHome.isDirectory()) {
35+
throw new IllegalArgumentException("--runHome is not a directory: " + bootstrap.runHome.getAbsolutePath());
36+
}
37+
if(bootstrap.runHome.list().length > 0) {
38+
throw new IllegalArgumentException("--runHome directory is not empty: " + bootstrap.runHome.getAbsolutePath());
39+
}
40+
//Override homeLoader to use existing directory instead of creating temporary one
41+
this.homeLoader = new HudsonHomeLoader.UseExisting(bootstrap.runHome);
42+
}
43+
}
44+
45+
/**
46+
* Sets up Jetty without any actual TCP port serving HTTP.
47+
*/
48+
@Override
49+
protected ServletContext createWebServer() throws Exception {
50+
QueuedThreadPool queuedThreadPool = new QueuedThreadPool(10);
51+
server = new Server(queuedThreadPool);
52+
53+
WebAppContext context = new WebAppContext(bootstrap.warDir.getPath(), contextPath);
54+
context.setClassLoader(getClass().getClassLoader());
55+
context.setConfigurations(new Configuration[]{new WebXmlConfiguration()});
56+
context.addBean(new NoListenerConfiguration(context));
57+
server.setHandler(context);
58+
context.getSecurityHandler().setLoginService(configureUserRealm());
59+
context.setResourceBase(bootstrap.warDir.getPath());
60+
61+
server.start();
62+
63+
localPort = -1;
64+
65+
setPluginManager(new PluginManagerImpl(context.getServletContext(), bootstrap.pluginsDir));
66+
67+
return context.getServletContext();
68+
}
69+
70+
public int launch() throws Throwable {
71+
Thread currentThread = Thread.currentThread();
72+
String originalThreadName = currentThread.getName();
73+
currentThread.setName(getThreadName());
74+
before();
75+
try {
76+
return doLaunch();
77+
} finally {
78+
after();
79+
currentThread.setName(originalThreadName);
80+
}
81+
}
82+
83+
/**
84+
* @return the thread name to use for executing the action
85+
*/
86+
protected abstract String getThreadName();
87+
88+
/**
89+
* Actually launches the Jenkins instance, without any time out or output message.
90+
* @return the return code for the process
91+
*/
92+
protected abstract int doLaunch() throws Exception;
93+
94+
@Override
95+
public void recipe() {
96+
// Not action needed so far
97+
}
98+
99+
/**
100+
* Supply a dummy {@link LoginService} that allows nobody.
101+
*/
102+
@Override
103+
protected LoginService configureUserRealm() {
104+
return new HashLoginService();
105+
}
106+
107+
@Override
108+
public void before() throws Throwable {
109+
setLogLevels();
110+
super.before();
111+
}
112+
113+
/**
114+
* We don't want to clutter console with log messages, so kill of any unimportant ones.
115+
*/
116+
private void setLogLevels() {
117+
Logger.getLogger("").setLevel(Level.WARNING);
118+
// Prevent warnings for plugins with old plugin POM (JENKINS-54425)
119+
Logger.getLogger(ClassicPluginStrategy.class.getName()).setLevel(Level.SEVERE);
120+
Logger l = Logger.getLogger(DeprecatedAgentProtocolMonitor.class.getName());
121+
l.setLevel(Level.OFF);
122+
noGc.add(l); // the configuration will be lost if Logger gets GCed.
123+
}
124+
125+
/**
126+
* Skips the clean up.
127+
*
128+
* This was initially motivated by SLF4J leaving gnarly messages.
129+
* The whole JVM is going to die anyway, so we don't really care about cleaning up anything nicely.
130+
*/
131+
@Override
132+
public void after() throws Exception {
133+
jenkins = null;
134+
super.after();
135+
}
136+
}

0 commit comments

Comments
 (0)