Adjust bootstrap sequence (#21543)

Added the ability for plugins to spawn a controller process at startup
This commit is contained in:
David Roberts 2016-11-17 09:58:09 +00:00 committed by GitHub
parent 36ac9fdfe1
commit 116593e5f5
6 changed files with 332 additions and 0 deletions

View File

@ -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),

View File

@ -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;
}
}

View File

@ -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"));
}
}

View File

@ -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'

View File

@ -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);
}
}

View File

@ -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',