Skip to content

Commit ef36df6

Browse files
authored
Respect environment variables in jvm.options (#16834)
JvmOptionsParser adds support for ${VAR:default} syntax when parsing jvm.options - allow dynamic resolution of environment variables in the jvm.options file - enables fallback to default value when the environment variable is not set
1 parent de6a6c5 commit ef36df6

File tree

3 files changed

+97
-5
lines changed

3 files changed

+97
-5
lines changed

config/jvm.options

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
## basic
3131

3232
# set the I/O temp directory
33-
#-Djava.io.tmpdir=$HOME
33+
#-Djava.io.tmpdir=${HOME}
3434

3535
# set to headless, just in case
3636
-Djava.awt.headless=true

tools/jvm-options-parser/src/main/java/org/logstash/launchers/JvmOptionsParser.java

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232
import java.util.Arrays;
3333
import java.util.Collection;
3434
import java.util.Collections;
35-
import java.util.HashSet;
3635
import java.util.LinkedHashSet;
3736
import java.util.List;
3837
import java.util.Locale;
@@ -283,7 +282,6 @@ public List<String> getJvmOptions() {
283282
}
284283

285284
private static final Pattern OPTION_DEFINITION = Pattern.compile("((?<start>\\d+)(?<range>-)?(?<end>\\d+)?:)?(?<option>-.*)$");
286-
287285
/**
288286
*
289287
* If the version syntax specified on a line matches the specified JVM options, the JVM option callback will be invoked with the JVM
@@ -372,13 +370,16 @@ private static Optional<String> jvmOptionFromLine(final int javaMajorVersion, fi
372370
// Skip comments and blank lines
373371
return Optional.empty();
374372
}
375-
final Matcher matcher = OPTION_DEFINITION.matcher(line);
373+
374+
String subbedLine = resolveEnvVar(line, System.getenv());
375+
376+
final Matcher matcher = OPTION_DEFINITION.matcher(subbedLine);
376377
if (matcher.matches()) {
377378
final String start = matcher.group("start");
378379
final String end = matcher.group("end");
379380
if (start == null) {
380381
// no range present, unconditionally apply the JVM option
381-
return Optional.of(line);
382+
return Optional.of(subbedLine);
382383
} else {
383384
final int lower = Integer.parseInt(start);
384385
final int upper;
@@ -404,6 +405,23 @@ private static Optional<String> jvmOptionFromLine(final int javaMajorVersion, fi
404405
return Optional.empty();
405406
}
406407

408+
private static final Pattern ENV_VAR_PATTERN = Pattern.compile("\\$\\{([a-zA-Z_.][a-zA-Z0-9_.]*)(?::([^}]*))?\\}");
409+
410+
static String resolveEnvVar(String line, Map<String,String> env) {
411+
Matcher matcher = ENV_VAR_PATTERN.matcher(line);
412+
StringBuilder sb = new StringBuilder();
413+
414+
while (matcher.find()) {
415+
String varName = matcher.group(1);
416+
String defaultValue = Optional.ofNullable(matcher.group(2)).orElse("");
417+
String replacement = env.getOrDefault(varName, defaultValue);
418+
matcher.appendReplacement(sb, Matcher.quoteReplacement(replacement));
419+
}
420+
matcher.appendTail(sb);
421+
422+
return sb.toString();
423+
}
424+
407425
private static final Pattern JAVA_VERSION = Pattern.compile("^(?:1\\.)?(?<javaMajorVersion>\\d+)(?:\\.\\d+)?$");
408426

409427
private static int javaMajorVersion() {

tools/jvm-options-parser/src/test/java/org/logstash/launchers/JvmOptionsParserTest.java

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,4 +198,78 @@ private BufferedReader asReader(String s) {
198198
return new BufferedReader(new StringReader(s));
199199
}
200200

201+
202+
@Test
203+
public void testSingleEnvSub() throws IOException {
204+
String result = JvmOptionsParser.resolveEnvVar("-XX:HeapDumpPath=${LOGSTASH_HOME}/heapdump.hprof",
205+
Map.of("LOGSTASH_HOME", "/path/to/ls_home"));
206+
assertEquals("-XX:HeapDumpPath=/path/to/ls_home/heapdump.hprof", result);
207+
}
208+
209+
@Test
210+
public void testMultipleEnvSub() throws IOException {
211+
String result = JvmOptionsParser.resolveEnvVar("-XX:HeapDumpPath=${LOGSTASH_HOME}/${DATA}/heapdump.hprof",
212+
Map.of("LOGSTASH_HOME", "/path/to/ls_home", "DATA", "data"));
213+
assertEquals("-XX:HeapDumpPath=/path/to/ls_home/data/heapdump.hprof", result);
214+
}
215+
216+
@Test
217+
public void testPeriodEnvSub() throws IOException {
218+
String result = JvmOptionsParser.resolveEnvVar("-XX:HeapDumpPath=${.HOME}/heapdump.hprof",
219+
Map.of(".HOME", "/path/to/.home"));
220+
assertEquals("-XX:HeapDumpPath=/path/to/.home/heapdump.hprof", result);
221+
}
222+
223+
@Test
224+
public void testEmptyEnvSub() throws IOException {
225+
String result = JvmOptionsParser.resolveEnvVar("-XX:HeapDumpPath=${NOT_VALID}/heapdump.hprof", Map.of());
226+
assertEquals("-XX:HeapDumpPath=/heapdump.hprof", result);
227+
}
228+
229+
@Test
230+
public void testNoSub() throws IOException {
231+
String result = JvmOptionsParser.resolveEnvVar(" ", Map.of());
232+
assertEquals(" ", result);
233+
}
234+
235+
@Test
236+
public void testEnvSubWithDefault() throws IOException {
237+
String result = JvmOptionsParser.resolveEnvVar("-XX:HeapDumpPath=${LOGSTASH_HOME:/usr/share/logstash}/${DATA:data}/heapdump.hprof",
238+
Map.of());
239+
assertEquals("-XX:HeapDumpPath=/usr/share/logstash/data/heapdump.hprof", result);
240+
}
241+
242+
@Test
243+
public void testEnvSubWithDefaultSpecialChar() throws IOException {
244+
String result = JvmOptionsParser.resolveEnvVar("-XX:HeapDumpPath=${LOGSTASH_HOME:/usr/share/logstash}/${DATA:{$crazy!enough?'bless'@[you]}/heapdump.hprof",
245+
Map.of());
246+
assertEquals("-XX:HeapDumpPath=/usr/share/logstash/{$crazy!enough?'bless'@[you]/heapdump.hprof", result);
247+
}
248+
249+
@Test
250+
public void testEnvSubWithDefaultOverwritten() throws IOException {
251+
String result = JvmOptionsParser.resolveEnvVar("-XX:HeapDumpPath=${LOGSTASH_HOME:/usr/share/logstash}/${DATA:data}/heapdump.hprof",
252+
Map.of("DATA", "data2"));
253+
assertEquals("-XX:HeapDumpPath=/usr/share/logstash/data2/heapdump.hprof", result);
254+
}
255+
256+
@Test
257+
public void testEnvSubInFile() throws IOException {
258+
File optionsFile = writeIntoTempOptionsFile(
259+
writer -> writer.println("-Xlog:gc*,gc+age=trace,safepoint:file=${UNKNOWN}:"));
260+
261+
JvmOptionsParser.handleJvmOptions(new String[] {"/path/to/ls_home", optionsFile.toString()}, "-Dcli.opts=something");
262+
263+
final String output = outputStreamCaptor.toString();
264+
assertTrue("env variable should be substituted ", output.contains("-Xlog:gc*,gc+age=trace,safepoint:file=:"));
265+
}
266+
267+
@Test
268+
public void testCommentedEnvSub() throws IOException {
269+
final BufferedReader options = asReader("# -Xlog:gc*,gc+age=trace,safepoint:file=${UNKNOWN}:");
270+
final JvmOptionsParser.ParseResult res = JvmOptionsParser.parse(11, options);
271+
272+
assertTrue("no invalid lines can be present", res.getInvalidLines().isEmpty());
273+
assertFalse(String.join(System.lineSeparator(), res.getJvmOptions()).contains("-Xlog:gc*,gc+age=trace,safepoint"));
274+
}
201275
}

0 commit comments

Comments
 (0)