Skip to content

Commit ebb6d52

Browse files
authored
Fix APM configuration file delete (#91058) (#91250)
When we launch Elasticsearch with the APM monitoring agent, we create a temporary configuration file to securely pass the API key or secret. This temporary file is cleaned up on Elasticsearch Node creation. After we renamed the APM module, the delete logic didn't get updated, which means we never delete the file anymore. This commit: - fixes the APM module pattern match when we delete - adds additional delete safety net on failed node start - adds tests for ensuring the naming dependency isn't broken again.
1 parent 25b7586 commit ebb6d52

File tree

4 files changed

+120
-10
lines changed

4 files changed

+120
-10
lines changed

distribution/tools/server-cli/src/main/java/org/elasticsearch/server/cli/APMJvmOptions.java

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -166,13 +166,18 @@ static List<String> apmJvmOptions(Settings settings, @Nullable KeyStoreWrapper k
166166
final List<String> options = new ArrayList<>();
167167
// Use an agent argument to specify the config file instead of e.g. `-Delastic.apm.config_file=...`
168168
// because then the agent won't try to reload the file, and we can remove it after startup.
169-
options.add("-javaagent:" + agentJar + "=c=" + tmpProperties);
169+
options.add(agentCommandLineOption(agentJar, tmpProperties));
170170

171171
dynamicSettings.forEach((key, value) -> options.add("-Delastic.apm." + key + "=" + value));
172172

173173
return options;
174174
}
175175

176+
// package private for testing
177+
static String agentCommandLineOption(Path agentJar, Path tmpPropertiesFile) {
178+
return "-javaagent:" + agentJar + "=c=" + tmpPropertiesFile;
179+
}
180+
176181
private static void extractSecureSettings(KeyStoreWrapper keystore, Map<String, String> propertiesMap) {
177182
final Set<String> settingNames = keystore.getSettingNames();
178183
for (String key : List.of("api_key", "secret_token")) {
@@ -225,9 +230,18 @@ private static Map<String, String> extractApmSettings(Settings settings) throws
225230
return propertiesMap;
226231
}
227232

233+
// package private for testing
234+
static Path createTemporaryPropertiesFile(Path tmpdir) throws IOException {
235+
return Files.createTempFile(tmpdir, ".elstcapm.", ".tmp");
236+
}
237+
228238
/**
229239
* Writes a Java properties file with data from supplied map to a temporary config, and returns
230240
* the file that was created.
241+
* <p>
242+
* We expect that the deleteTemporaryApmConfig function in Node will delete this temporary
243+
* configuration file, however if we fail to launch the node (because of an error) we might leave the
244+
* file behind. Therefore, we register a CLI shutdown hook that will also attempt to delete the file.
231245
*
232246
* @param tmpdir the directory for the file
233247
* @param propertiesMap the data to write
@@ -238,10 +252,18 @@ private static Path writeApmProperties(Path tmpdir, Map<String, String> properti
238252
final Properties p = new Properties();
239253
p.putAll(propertiesMap);
240254

241-
final Path tmpFile = Files.createTempFile(tmpdir, ".elstcapm.", ".tmp");
255+
final Path tmpFile = createTemporaryPropertiesFile(tmpdir);
242256
try (OutputStream os = Files.newOutputStream(tmpFile)) {
243257
p.store(os, " Automatically generated by Elasticsearch, do not edit!");
244258
}
259+
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
260+
try {
261+
Files.deleteIfExists(tmpFile);
262+
} catch (IOException e) {
263+
// ignore
264+
}
265+
}, "elasticsearch[apmagent-cleanup]"));
266+
245267
return tmpFile;
246268
}
247269

@@ -253,7 +275,12 @@ private static Path writeApmProperties(Path tmpdir, Map<String, String> properti
253275
*/
254276
@Nullable
255277
private static Path findAgentJar() throws IOException, UserException {
256-
final Path apmModule = Path.of(System.getProperty("user.dir")).resolve("modules/apm");
278+
return findAgentJar(System.getProperty("user.dir"));
279+
}
280+
281+
// package private for testing
282+
static Path findAgentJar(String installDir) throws IOException, UserException {
283+
final Path apmModule = Path.of(installDir).resolve("modules").resolve("apm");
257284

258285
if (Files.notExists(apmModule)) {
259286
if (Build.CURRENT.isProductionRelease()) {
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
package org.elasticsearch.server.cli;
10+
11+
import org.elasticsearch.cli.UserException;
12+
import org.elasticsearch.monitor.jvm.JvmInfo;
13+
import org.elasticsearch.node.Node;
14+
import org.elasticsearch.test.ESTestCase;
15+
import org.junit.After;
16+
import org.junit.Before;
17+
18+
import java.io.IOException;
19+
import java.nio.file.Files;
20+
import java.nio.file.Path;
21+
22+
import static org.mockito.Mockito.doReturn;
23+
import static org.mockito.Mockito.mock;
24+
25+
@ESTestCase.WithoutSecurityManager
26+
public class APMJvmOptionsTests extends ESTestCase {
27+
28+
private Path installDir;
29+
private Path agentPath;
30+
31+
@Before
32+
public void setup() throws IOException, UserException {
33+
installDir = makeFakeAgentJar();
34+
agentPath = APMJvmOptions.findAgentJar(installDir.toAbsolutePath().toString());
35+
}
36+
37+
@After
38+
public void cleanup() throws IOException {
39+
Files.delete(agentPath);
40+
}
41+
42+
public void testFindJar() throws IOException {
43+
assertNotNull(agentPath);
44+
45+
Path anotherPath = Files.createDirectories(installDir.resolve("another"));
46+
Path apmPathDir = anotherPath.resolve("modules").resolve("apm");
47+
Files.createDirectories(apmPathDir);
48+
49+
assertTrue(
50+
expectThrows(UserException.class, () -> APMJvmOptions.findAgentJar(anotherPath.toAbsolutePath().toString())).getMessage()
51+
.contains("Installation is corrupt")
52+
);
53+
}
54+
55+
public void testFileDeleteWorks() throws IOException {
56+
var tempFile = APMJvmOptions.createTemporaryPropertiesFile(agentPath.getParent());
57+
var commandLineOption = APMJvmOptions.agentCommandLineOption(agentPath, tempFile);
58+
var jvmInfo = mock(JvmInfo.class);
59+
doReturn(new String[] { commandLineOption }).when(jvmInfo).getInputArguments();
60+
assertTrue(Files.exists(tempFile));
61+
Node.deleteTemporaryApmConfig(jvmInfo, (e, p) -> fail("Shouldn't hit an exception"));
62+
assertFalse(Files.exists(tempFile));
63+
}
64+
65+
private Path makeFakeAgentJar() throws IOException {
66+
Path tempFile = createTempFile();
67+
Path apmPathDir = tempFile.getParent().resolve("modules").resolve("apm");
68+
Files.createDirectories(apmPathDir);
69+
Path apmAgentFile = apmPathDir.resolve("elastic-apm-agent-0.0.0.jar");
70+
Files.move(tempFile, apmAgentFile);
71+
72+
return tempFile.getParent();
73+
}
74+
}

docs/changelog/91058.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
pr: 91058
2+
summary: Fix APM configuration file delete
3+
area: Infra/Core
4+
type: bug
5+
issues:
6+
- 89439

server/src/main/java/org/elasticsearch/node/Node.java

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@
226226
import java.util.Set;
227227
import java.util.concurrent.CountDownLatch;
228228
import java.util.concurrent.TimeUnit;
229+
import java.util.function.BiConsumer;
229230
import java.util.function.Function;
230231
import java.util.function.LongSupplier;
231232
import java.util.function.UnaryOperator;
@@ -401,7 +402,10 @@ protected Node(
401402
);
402403
}
403404

404-
deleteTemporaryApmConfig(jvmInfo);
405+
deleteTemporaryApmConfig(
406+
jvmInfo,
407+
(e, apmConfig) -> logger.error("failed to delete temporary APM config file [{}], reason: [{}]", apmConfig, e.getMessage())
408+
);
405409

406410
this.pluginsService = pluginServiceCtor.apply(tmpSettings);
407411
final Settings settings = mergePluginSettings(pluginsService.pluginMap(), tmpSettings);
@@ -1115,24 +1119,23 @@ protected Node(
11151119
* If the JVM was started with the Elastic APM agent and a config file argument was specified, then
11161120
* delete the config file. The agent only reads it once, when supplied in this fashion, and it
11171121
* may contain a secret token.
1122+
* <p>
1123+
* Public for testing only
11181124
*/
11191125
@SuppressForbidden(reason = "Cannot guarantee that the temp config path is relative to the environment")
1120-
private void deleteTemporaryApmConfig(JvmInfo jvmInfo) {
1126+
public static void deleteTemporaryApmConfig(JvmInfo jvmInfo, BiConsumer<Exception, Path> errorHandler) {
11211127
for (String inputArgument : jvmInfo.getInputArguments()) {
11221128
if (inputArgument.startsWith("-javaagent:")) {
11231129
final String agentArg = inputArgument.substring(11);
11241130
final String[] parts = agentArg.split("=", 2);
1125-
if (parts[0].matches("modules/x-pack-apm-integration/elastic-apm-agent-\\d+\\.\\d+\\.\\d+\\.jar")) {
1131+
if (parts[0].matches(".*modules/apm/elastic-apm-agent-\\d+\\.\\d+\\.\\d+\\.jar")) {
11261132
if (parts.length == 2 && parts[1].startsWith("c=")) {
11271133
final Path apmConfig = PathUtils.get(parts[1].substring(2));
11281134
if (apmConfig.getFileName().toString().matches("^\\.elstcapm\\..*\\.tmp")) {
11291135
try {
11301136
Files.deleteIfExists(apmConfig);
11311137
} catch (IOException e) {
1132-
logger.error(
1133-
"Failed to delete temporary APM config file [" + apmConfig + "], reason: [" + e.getMessage() + "]",
1134-
e
1135-
);
1138+
errorHandler.accept(e, apmConfig);
11361139
}
11371140
}
11381141
}

0 commit comments

Comments
 (0)