Adjust bootstrap sequence (#21543)
Added the ability for plugins to spawn a controller process at startup
This commit is contained in:
parent
36ac9fdfe1
commit
116593e5f5
|
@ -67,6 +67,7 @@ final class Bootstrap {
|
|||
private volatile Node node;
|
||||
private final CountDownLatch keepAliveLatch = new CountDownLatch(1);
|
||||
private final Thread keepAliveThread;
|
||||
private final Spawner spawner = new Spawner();
|
||||
|
||||
/** creates a new instance */
|
||||
Bootstrap() {
|
||||
|
@ -155,6 +156,23 @@ final class Bootstrap {
|
|||
|
||||
private void setup(boolean addShutdownHook, Environment environment) throws BootstrapException {
|
||||
Settings settings = environment.settings();
|
||||
|
||||
try {
|
||||
spawner.spawnNativePluginControllers(environment);
|
||||
Runtime.getRuntime().addShutdownHook(new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
spawner.close();
|
||||
} catch (IOException e) {
|
||||
throw new ElasticsearchException("Failed to destroy spawned controllers", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (IOException e) {
|
||||
throw new BootstrapException(e);
|
||||
}
|
||||
|
||||
initializeNatives(
|
||||
environment.tmpFile(),
|
||||
BootstrapSettings.MEMORY_LOCK_SETTING.get(settings),
|
||||
|
|
|
@ -0,0 +1,128 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package org.elasticsearch.bootstrap;
|
||||
|
||||
import org.apache.lucene.util.Constants;
|
||||
import org.apache.lucene.util.IOUtils;
|
||||
import org.elasticsearch.env.Environment;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.DirectoryStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Spawns native plugin controller processes if present. Will only work prior to seccomp being set up.
|
||||
*/
|
||||
final class Spawner implements Closeable {
|
||||
|
||||
private static final String PROGRAM_NAME = Constants.WINDOWS ? "controller.exe" : "controller";
|
||||
private static final String PLATFORM_NAME = makePlatformName(Constants.OS_NAME, Constants.OS_ARCH);
|
||||
private static final String TMP_ENVVAR = "TMPDIR";
|
||||
|
||||
/**
|
||||
* References to the processes that have been spawned, so that we can destroy them.
|
||||
*/
|
||||
private final List<Process> processes = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
try {
|
||||
IOUtils.close(() -> processes.stream().map(s -> (Closeable)s::destroy).iterator());
|
||||
} finally {
|
||||
processes.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For each plugin, attempt to spawn the controller daemon. Silently ignore any plugins
|
||||
* that don't include a controller for the correct platform.
|
||||
*/
|
||||
void spawnNativePluginControllers(Environment environment) throws IOException {
|
||||
if (Files.exists(environment.pluginsFile())) {
|
||||
try (DirectoryStream<Path> stream = Files.newDirectoryStream(environment.pluginsFile())) {
|
||||
for (Path plugin : stream) {
|
||||
Path spawnPath = makeSpawnPath(plugin);
|
||||
if (Files.isRegularFile(spawnPath)) {
|
||||
spawnNativePluginController(spawnPath, environment.tmpFile());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to spawn the controller daemon for a given plugin. The spawned process
|
||||
* will remain connected to this JVM via its stdin, stdout and stderr, but the
|
||||
* references to these streams are not available to code outside this package.
|
||||
*/
|
||||
private void spawnNativePluginController(Path spawnPath, Path tmpPath) throws IOException {
|
||||
ProcessBuilder pb = new ProcessBuilder(spawnPath.toString());
|
||||
|
||||
// The only environment variable passes on the path to the temporary directory
|
||||
pb.environment().clear();
|
||||
pb.environment().put(TMP_ENVVAR, tmpPath.toString());
|
||||
|
||||
// The output stream of the Process object corresponds to the daemon's stdin
|
||||
processes.add(pb.start());
|
||||
}
|
||||
|
||||
List<Process> getProcesses() {
|
||||
return Collections.unmodifiableList(processes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make the full path to the program to be spawned.
|
||||
*/
|
||||
static Path makeSpawnPath(Path plugin) {
|
||||
return plugin.resolve("platform").resolve(PLATFORM_NAME).resolve("bin").resolve(PROGRAM_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make the platform name in the format used in Kibana downloads, for example:
|
||||
* - darwin-x86_64
|
||||
* - linux-x86-64
|
||||
* - windows-x86_64
|
||||
* For *nix platforms this is more-or-less `uname -s`-`uname -m` converted to lower case.
|
||||
* However, for consistency between different operating systems on the same architecture
|
||||
* "amd64" is replaced with "x86_64" and "i386" with "x86".
|
||||
* For Windows it's "windows-" followed by either "x86" or "x86_64".
|
||||
*/
|
||||
static String makePlatformName(String osName, String osArch) {
|
||||
String os = osName.toLowerCase(Locale.ROOT);
|
||||
if (os.startsWith("windows")) {
|
||||
os = "windows";
|
||||
} else if (os.equals("mac os x")) {
|
||||
os = "darwin";
|
||||
}
|
||||
String cpu = osArch.toLowerCase(Locale.ROOT);
|
||||
if (cpu.equals("amd64")) {
|
||||
cpu = "x86_64";
|
||||
} else if (cpu.equals("i386")) {
|
||||
cpu = "x86";
|
||||
}
|
||||
return os + "-" + cpu;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package org.elasticsearch.bootstrap;
|
||||
|
||||
import org.apache.lucene.util.Constants;
|
||||
import org.elasticsearch.test.ESTestCase;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Doesn't actually test spawning a process, as seccomp is installed before tests run and forbids it.
|
||||
*/
|
||||
public class SpawnerTests extends ESTestCase {
|
||||
|
||||
public void testMakePlatformName() {
|
||||
String platformName = Spawner.makePlatformName(Constants.OS_NAME, Constants.OS_ARCH);
|
||||
|
||||
assertFalse(platformName, platformName.isEmpty());
|
||||
assertTrue(platformName, platformName.equals(platformName.toLowerCase(Locale.ROOT)));
|
||||
assertTrue(platformName, platformName.indexOf("-") > 0);
|
||||
assertTrue(platformName, platformName.indexOf("-") < platformName.length() - 1);
|
||||
assertFalse(platformName, platformName.contains(" "));
|
||||
}
|
||||
|
||||
public void testMakeSpecificPlatformNames() {
|
||||
assertEquals("darwin-x86_64", Spawner.makePlatformName("Mac OS X", "x86_64"));
|
||||
assertEquals("linux-x86_64", Spawner.makePlatformName("Linux", "amd64"));
|
||||
assertEquals("linux-x86", Spawner.makePlatformName("Linux", "i386"));
|
||||
assertEquals("windows-x86_64", Spawner.makePlatformName("Windows Server 2008 R2", "amd64"));
|
||||
assertEquals("windows-x86", Spawner.makePlatformName("Windows Server 2008", "x86"));
|
||||
assertEquals("windows-x86_64", Spawner.makePlatformName("Windows 8.1", "amd64"));
|
||||
assertEquals("sunos-x86_64", Spawner.makePlatformName("SunOS", "amd64"));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Tests that need to run without the Elasticsearch bootstrap, for
|
||||
* example to test process spawning.
|
||||
*/
|
||||
|
||||
apply plugin: 'elasticsearch.standalone-test'
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package org.elasticsearch.bootstrap;
|
||||
|
||||
import org.apache.lucene.util.Constants;
|
||||
import org.apache.lucene.util.LuceneTestCase;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.env.Environment;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.PosixFilePermission;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Create a simple "daemon controller", put it in the right place and check that it runs.
|
||||
*
|
||||
* Extends LuceneTestCase rather than ESTestCase as ESTestCase installs seccomp, and that
|
||||
* prevents the Spawner class doing its job. Also needs to run in a separate JVM to other
|
||||
* tests that extend ESTestCase for the same reason.
|
||||
*/
|
||||
public class SpawnerNoBootstrapTests extends LuceneTestCase {
|
||||
|
||||
private static final String CONTROLLER_SOURCE = "#!/bin/bash\n"
|
||||
+ "\n"
|
||||
+ "echo I am alive\n"
|
||||
+ "\n"
|
||||
+ "read SOMETHING\n";
|
||||
|
||||
public void testControllerSpawn() throws IOException, InterruptedException {
|
||||
// On Windows you cannot directly run a batch file - you have to run cmd.exe with the batch file
|
||||
// as an argument and that's out of the remit of the controller daemon process spawner. If
|
||||
// you need to build on Windows, just don't run this test. The process spawner itself will work
|
||||
// with native processes.
|
||||
assumeFalse("This test does not work on Windows", Constants.WINDOWS);
|
||||
|
||||
Path esHome = createTempDir().resolve("esHome");
|
||||
Settings.Builder settingsBuilder = Settings.builder();
|
||||
settingsBuilder.put(Environment.PATH_HOME_SETTING.getKey(), esHome.toString());
|
||||
Settings settings = settingsBuilder.build();
|
||||
|
||||
Environment environment = new Environment(settings);
|
||||
|
||||
// This plugin WILL have a controller daemon
|
||||
Path plugin = environment.pluginsFile().resolve("test_plugin");
|
||||
Files.createDirectories(plugin);
|
||||
Path controllerProgram = Spawner.makeSpawnPath(plugin);
|
||||
createControllerProgram(controllerProgram);
|
||||
|
||||
// This plugin will NOT have a controller daemon
|
||||
Path otherPlugin = environment.pluginsFile().resolve("other_plugin");
|
||||
Files.createDirectories(otherPlugin);
|
||||
|
||||
Spawner spawner = new Spawner();
|
||||
spawner.spawnNativePluginControllers(environment);
|
||||
|
||||
List<Process> processes = spawner.getProcesses();
|
||||
// 1 because there should only be a reference in the list for the plugin that had the controller daemon, not the other plugin
|
||||
assertEquals(1, processes.size());
|
||||
Process process = processes.get(0);
|
||||
try (BufferedReader stdoutReader = new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) {
|
||||
String line = stdoutReader.readLine();
|
||||
assertEquals("I am alive", line);
|
||||
spawner.close();
|
||||
// Fail if the process doesn't die within 1 second - usually it will be even quicker but it depends on OS scheduling
|
||||
assertTrue(process.waitFor(1, TimeUnit.SECONDS));
|
||||
}
|
||||
}
|
||||
|
||||
private void createControllerProgram(Path outputFile) throws IOException {
|
||||
Path outputDir = outputFile.getParent();
|
||||
Files.createDirectories(outputDir);
|
||||
Files.write(outputFile, CONTROLLER_SOURCE.getBytes(StandardCharsets.UTF_8));
|
||||
Set<PosixFilePermission> perms = new HashSet<>();
|
||||
perms.add(PosixFilePermission.OWNER_READ);
|
||||
perms.add(PosixFilePermission.OWNER_WRITE);
|
||||
perms.add(PosixFilePermission.OWNER_EXECUTE);
|
||||
perms.add(PosixFilePermission.GROUP_READ);
|
||||
perms.add(PosixFilePermission.GROUP_EXECUTE);
|
||||
perms.add(PosixFilePermission.OTHERS_READ);
|
||||
perms.add(PosixFilePermission.OTHERS_EXECUTE);
|
||||
Files.setPosixFilePermissions(outputFile, perms);
|
||||
}
|
||||
}
|
|
@ -56,6 +56,7 @@ List projects = [
|
|||
'plugins:store-smb',
|
||||
'qa:backwards-5.0',
|
||||
'qa:evil-tests',
|
||||
'qa:no-bootstrap-tests',
|
||||
'qa:rolling-upgrade',
|
||||
'qa:smoke-test-client',
|
||||
'qa:smoke-test-http',
|
||||
|
|
Loading…
Reference in New Issue