Support `_FILE` suffixed env vars in Docker entrypoint (#49182)

Backport of #47573.

Closes #43603. Allow environment variables to be passed to ES in a Docker
container via a file, by setting an environment variable with the `_FILE`
suffix that points to the file with the intended value of the env var.
This commit is contained in:
Rory Hunter 2019-11-18 08:22:35 +00:00 committed by GitHub
parent b29f4dd9c2
commit e84e21174b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 421 additions and 149 deletions

View File

@ -38,6 +38,40 @@ if [[ "$1" != "eswrapper" ]]; then
fi fi
fi fi
# Allow environment variables to be set by creating a file with the
# contents, and setting an environment variable with the suffix _FILE to
# point to it. This can be used to provide secrets to a container, without
# the values being specified explicitly when running the container.
for VAR_NAME_FILE in $(env | cut -f1 -d= | grep '_FILE$'); do
if [[ -n "$VAR_NAME_FILE" ]]; then
VAR_NAME="${VAR_NAME_FILE%_FILE}"
if env | grep "^${VAR_NAME}="; then
echo "ERROR: Both $VAR_NAME_FILE and $VAR_NAME are set. These are mutually exclusive." >&2
exit 1
fi
if [[ ! -e "${!VAR_NAME_FILE}" ]]; then
echo "ERROR: File ${!VAR_NAME_FILE} from $VAR_NAME_FILE does not exist" >&2
exit 1
fi
FILE_PERMS="$(stat -c '%a' ${!VAR_NAME_FILE})"
if [[ "$FILE_PERMS" != "400" && "$FILE_PERMS" != 600 ]]; then
echo "ERROR: File ${!VAR_NAME_FILE} from $VAR_NAME_FILE must have file permissions 400 or 600, but actually has: $FILE_PERMS" >&2
exit 1
fi
echo "Setting $VAR_NAME from $VAR_NAME_FILE at ${!VAR_NAME_FILE}" >&2
export "$VAR_NAME"="$(cat ${!VAR_NAME_FILE})"
unset VAR_NAME
# Unset the suffixed environment variable
unset "$VAR_NAME_FILE"
fi
done
# Parse Docker env vars to customize Elasticsearch # Parse Docker env vars to customize Elasticsearch
# #
# e.g. Setting the env var cluster.name=testcluster # e.g. Setting the env var cluster.name=testcluster

View File

@ -314,6 +314,19 @@ You can set individual {es} configuration parameters using Docker environment va
The <<docker-compose-file, sample compose file>> and the The <<docker-compose-file, sample compose file>> and the
<<docker-cli-run-dev-mode, single-node example>> use this method. <<docker-cli-run-dev-mode, single-node example>> use this method.
To use the contents of a file to set an environment variable, suffix the environment
variable name with `_FILE`. This is useful for passing secrets such as passwords to {es}
without specifying them directly.
For example, to set the {es} bootstrap password from a file, you can bind mount the
file and set the `ELASTIC_PASSWORD_FILE` environment variable to the mount location.
If you mount the password file to `/run/secrets/password.txt`, specify:
[source,sh]
--------------------------------------------
-e ELASTIC_PASSWORD_FILE=/run/secrets/bootstrapPassword.txt
--------------------------------------------
You can also override the default command for the image to pass {es} configuration You can also override the default command for the image to pass {es} configuration
parameters as command line options. For example: parameters as command line options. For example:

View File

@ -25,38 +25,45 @@ import org.elasticsearch.packaging.util.Docker.DockerShell;
import org.elasticsearch.packaging.util.Installation; import org.elasticsearch.packaging.util.Installation;
import org.elasticsearch.packaging.util.ServerUtils; import org.elasticsearch.packaging.util.ServerUtils;
import org.elasticsearch.packaging.util.Shell.Result; import org.elasticsearch.packaging.util.Shell.Result;
import org.junit.After;
import org.junit.AfterClass; import org.junit.AfterClass;
import org.junit.Before; import org.junit.Before;
import org.junit.BeforeClass; import org.junit.BeforeClass;
import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import static java.nio.file.attribute.PosixFilePermissions.fromString; import static java.nio.file.attribute.PosixFilePermissions.fromString;
import static java.util.Collections.singletonMap;
import static org.elasticsearch.packaging.util.Docker.assertPermissionsAndOwnership; import static org.elasticsearch.packaging.util.Docker.assertPermissionsAndOwnership;
import static org.elasticsearch.packaging.util.Docker.copyFromContainer; import static org.elasticsearch.packaging.util.Docker.copyFromContainer;
import static org.elasticsearch.packaging.util.Docker.ensureImageIsLoaded; import static org.elasticsearch.packaging.util.Docker.ensureImageIsLoaded;
import static org.elasticsearch.packaging.util.Docker.existsInContainer; import static org.elasticsearch.packaging.util.Docker.existsInContainer;
import static org.elasticsearch.packaging.util.Docker.removeContainer; import static org.elasticsearch.packaging.util.Docker.removeContainer;
import static org.elasticsearch.packaging.util.Docker.runContainer; import static org.elasticsearch.packaging.util.Docker.runContainer;
import static org.elasticsearch.packaging.util.Docker.runContainerExpectingFailure;
import static org.elasticsearch.packaging.util.Docker.verifyContainerInstallation; import static org.elasticsearch.packaging.util.Docker.verifyContainerInstallation;
import static org.elasticsearch.packaging.util.Docker.waitForElasticsearch;
import static org.elasticsearch.packaging.util.Docker.waitForPathToExist; import static org.elasticsearch.packaging.util.Docker.waitForPathToExist;
import static org.elasticsearch.packaging.util.FileMatcher.p600;
import static org.elasticsearch.packaging.util.FileMatcher.p660; import static org.elasticsearch.packaging.util.FileMatcher.p660;
import static org.elasticsearch.packaging.util.FileUtils.append; import static org.elasticsearch.packaging.util.FileUtils.append;
import static org.elasticsearch.packaging.util.FileUtils.getTempDir; import static org.elasticsearch.packaging.util.FileUtils.getTempDir;
import static org.elasticsearch.packaging.util.FileUtils.mkdir;
import static org.elasticsearch.packaging.util.FileUtils.rm; import static org.elasticsearch.packaging.util.FileUtils.rm;
import static org.elasticsearch.packaging.util.ServerUtils.makeRequest; import static org.elasticsearch.packaging.util.ServerUtils.makeRequest;
import static org.elasticsearch.packaging.util.ServerUtils.waitForElasticsearch;
import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.emptyString; import static org.hamcrest.Matchers.emptyString;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assume.assumeTrue; import static org.junit.Assume.assumeTrue;
public class DockerTests extends PackagingTestCase { public class DockerTests extends PackagingTestCase {
protected DockerShell sh; protected DockerShell sh;
private Path tempDir;
@BeforeClass @BeforeClass
public static void filterDistros() { public static void filterDistros() {
@ -72,9 +79,15 @@ public class DockerTests extends PackagingTestCase {
} }
@Before @Before
public void setupTest() throws Exception { public void setupTest() throws IOException {
sh = new DockerShell(); sh = new DockerShell();
installation = runContainer(distribution()); installation = runContainer(distribution());
tempDir = Files.createTempDirectory(getTempDir(), DockerTests.class.getSimpleName());
}
@After
public void teardownTest() {
rm(tempDir);
} }
/** /**
@ -144,40 +157,152 @@ public class DockerTests extends PackagingTestCase {
* Check that the default config can be overridden using a bind mount, and that env vars are respected * Check that the default config can be overridden using a bind mount, and that env vars are respected
*/ */
public void test70BindMountCustomPathConfAndJvmOptions() throws Exception { public void test70BindMountCustomPathConfAndJvmOptions() throws Exception {
final Path tempConf = getTempDir().resolve("esconf-alternate"); copyFromContainer(installation.config("elasticsearch.yml"), tempDir.resolve("elasticsearch.yml"));
copyFromContainer(installation.config("log4j2.properties"), tempDir.resolve("log4j2.properties"));
try {
mkdir(tempConf);
copyFromContainer(installation.config("elasticsearch.yml"), tempConf.resolve("elasticsearch.yml"));
copyFromContainer(installation.config("log4j2.properties"), tempConf.resolve("log4j2.properties"));
// we have to disable Log4j from using JMX lest it will hit a security // we have to disable Log4j from using JMX lest it will hit a security
// manager exception before we have configured logging; this will fail // manager exception before we have configured logging; this will fail
// startup since we detect usages of logging before it is configured // startup since we detect usages of logging before it is configured
final String jvmOptions = final String jvmOptions = "-Xms512m\n-Xmx512m\n-Dlog4j2.disable.jmx=true\n";
"-Xms512m\n" + append(tempDir.resolve("jvm.options"), jvmOptions);
"-Xmx512m\n" +
"-Dlog4j2.disable.jmx=true\n";
append(tempConf.resolve("jvm.options"), jvmOptions);
// Make the temp directory and contents accessible when bind-mounted // Make the temp directory and contents accessible when bind-mounted
Files.setPosixFilePermissions(tempConf, fromString("rwxrwxrwx")); Files.setPosixFilePermissions(tempDir, fromString("rwxrwxrwx"));
final Map<String, String> envVars = new HashMap<>();
envVars.put("ES_JAVA_OPTS", "-XX:-UseCompressedOops");
// Restart the container // Restart the container
removeContainer(); final Map<Path, Path> volumes = singletonMap(tempDir, Paths.get("/usr/share/elasticsearch/config"));
runContainer(distribution(), tempConf, envVars); final Map<String, String> envVars = singletonMap("ES_JAVA_OPTS", "-XX:-UseCompressedOops");
runContainer(distribution(), volumes, envVars);
waitForElasticsearch(installation); waitForElasticsearch(installation);
final String nodesResponse = makeRequest(Request.Get("http://localhost:9200/_nodes")); final String nodesResponse = makeRequest(Request.Get("http://localhost:9200/_nodes"));
assertThat(nodesResponse, containsString("\"heap_init_in_bytes\":536870912")); assertThat(nodesResponse, containsString("\"heap_init_in_bytes\":536870912"));
assertThat(nodesResponse, containsString("\"using_compressed_ordinary_object_pointers\":\"false\"")); assertThat(nodesResponse, containsString("\"using_compressed_ordinary_object_pointers\":\"false\""));
} finally {
rm(tempConf);
} }
/**
* Check that environment variables can be populated by setting variables with the suffix "_FILE",
* which point to files that hold the required values.
*/
public void test80SetEnvironmentVariablesUsingFiles() throws Exception {
final String optionsFilename = "esJavaOpts.txt";
// ES_JAVA_OPTS_FILE
append(tempDir.resolve(optionsFilename), "-XX:-UseCompressedOops\n");
Map<String, String> envVars = singletonMap("ES_JAVA_OPTS_FILE", "/run/secrets/" + optionsFilename);
// File permissions need to be secured in order for the ES wrapper to accept
// them for populating env var values
Files.setPosixFilePermissions(tempDir.resolve(optionsFilename), p600);
final Map<Path, Path> volumes = singletonMap(tempDir, Paths.get("/run/secrets"));
// Restart the container
runContainer(distribution(), volumes, envVars);
waitForElasticsearch(installation);
final String nodesResponse = makeRequest(Request.Get("http://localhost:9200/_nodes"));
assertThat(nodesResponse, containsString("\"using_compressed_ordinary_object_pointers\":\"false\""));
}
/**
* Check that the elastic user's password can be configured via a file and the ELASTIC_PASSWORD_FILE environment variable.
*/
public void test81ConfigurePasswordThroughEnvironmentVariableFile() throws Exception {
// Test relies on configuring security
assumeTrue(distribution.isDefault());
final String xpackPassword = "hunter2";
final String passwordFilename = "password.txt";
// ELASTIC_PASSWORD_FILE
append(tempDir.resolve(passwordFilename), xpackPassword + "\n");
// Enable security so that we can test that the password has been used
Map<String, String> envVars = new HashMap<>();
envVars.put("ELASTIC_PASSWORD_FILE", "/run/secrets/" + passwordFilename);
envVars.put("xpack.security.enabled", "true");
// File permissions need to be secured in order for the ES wrapper to accept
// them for populating env var values
Files.setPosixFilePermissions(tempDir.resolve(passwordFilename), p600);
final Map<Path, Path> volumes = singletonMap(tempDir, Paths.get("/run/secrets"));
// Restart the container
runContainer(distribution(), volumes, envVars);
// If we configured security correctly, then this call will only work if we specify the correct credentials.
try {
waitForElasticsearch("green", null, installation, "elastic", "hunter2");
} catch (Exception e) {
throw new AssertionError(
"Failed to check whether Elasticsearch had started. This could be because "
+ "authentication isn't working properly. Check the container logs",
e
);
}
// Also check that an unauthenticated call fails
final int statusCode = Request.Get("http://localhost:9200/_nodes").execute().returnResponse().getStatusLine().getStatusCode();
assertThat("Expected server to require authentication", statusCode, equalTo(401));
}
/**
* Check that environment variables cannot be used with _FILE environment variables.
*/
public void test81CannotUseEnvVarsAndFiles() throws Exception {
final String optionsFilename = "esJavaOpts.txt";
// ES_JAVA_OPTS_FILE
append(tempDir.resolve(optionsFilename), "-XX:-UseCompressedOops\n");
Map<String, String> envVars = new HashMap<>();
envVars.put("ES_JAVA_OPTS", "-XX:+UseCompressedOops");
envVars.put("ES_JAVA_OPTS_FILE", "/run/secrets/" + optionsFilename);
// File permissions need to be secured in order for the ES wrapper to accept
// them for populating env var values
Files.setPosixFilePermissions(tempDir.resolve(optionsFilename), p600);
final Map<Path, Path> volumes = singletonMap(tempDir, Paths.get("/run/secrets"));
final Result dockerLogs = runContainerExpectingFailure(distribution, volumes, envVars);
assertThat(
dockerLogs.stderr,
containsString("ERROR: Both ES_JAVA_OPTS_FILE and ES_JAVA_OPTS are set. These are mutually exclusive.")
);
}
/**
* Check that when populating environment variables by setting variables with the suffix "_FILE",
* the files' permissions are checked.
*/
public void test82EnvironmentVariablesUsingFilesHaveCorrectPermissions() throws Exception {
final String optionsFilename = "esJavaOpts.txt";
// ES_JAVA_OPTS_FILE
append(tempDir.resolve(optionsFilename), "-XX:-UseCompressedOops\n");
Map<String, String> envVars = singletonMap("ES_JAVA_OPTS_FILE", "/run/secrets/" + optionsFilename);
// Set invalid file permissions
Files.setPosixFilePermissions(tempDir.resolve(optionsFilename), p660);
final Map<Path, Path> volumes = singletonMap(tempDir, Paths.get("/run/secrets"));
// Restart the container
final Result dockerLogs = runContainerExpectingFailure(distribution(), volumes, envVars);
assertThat(
dockerLogs.stderr,
containsString("ERROR: File /run/secrets/" + optionsFilename + " from ES_JAVA_OPTS_FILE must have file permissions 400 or 600")
);
} }
/** /**
@ -221,7 +346,6 @@ public class DockerTests extends PackagingTestCase {
final Installation.Executables bin = installation.executables(); final Installation.Executables bin = installation.executables();
final Result result = sh.run(bin.elasticsearchNode + " -h"); final Result result = sh.run(bin.elasticsearchNode + " -h");
assertThat(result.stdout, assertThat(result.stdout, containsString("A CLI tool to do unsafe cluster and index manipulations on current node"));
containsString("A CLI tool to do unsafe cluster and index manipulations on current node"));
} }
} }

View File

@ -25,7 +25,6 @@ import org.apache.commons.logging.LogFactory;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermission; import java.nio.file.attribute.PosixFilePermission;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
@ -79,8 +78,8 @@ public class Docker {
* Runs an Elasticsearch Docker container. * Runs an Elasticsearch Docker container.
* @param distribution details about the docker image being tested. * @param distribution details about the docker image being tested.
*/ */
public static Installation runContainer(Distribution distribution) throws Exception { public static Installation runContainer(Distribution distribution) {
return runContainer(distribution, null, Collections.emptyMap()); return runContainer(distribution, null, null);
} }
/** /**
@ -88,23 +87,51 @@ public class Docker {
* through a bind mount, and passing additional environment variables. * through a bind mount, and passing additional environment variables.
* *
* @param distribution details about the docker image being tested. * @param distribution details about the docker image being tested.
* @param configPath the path to the config to bind mount, or null * @param volumes a map that declares any volume mappings to apply, or null
* @param envVars environment variables to set when running the container * @param envVars environment variables to set when running the container, or null
*/ */
public static Installation runContainer(Distribution distribution, Path configPath, Map<String,String> envVars) throws Exception { public static Installation runContainer(Distribution distribution, Map<Path, Path> volumes, Map<String, String> envVars) {
executeDockerRun(distribution, volumes, envVars);
waitForElasticsearchToStart();
return Installation.ofContainer();
}
/**
* Similar to {@link #runContainer(Distribution, Map, Map)} in that it runs an Elasticsearch Docker
* container, expect that the container expecting it to exit e.g. due to configuration problem.
*
* @param distribution details about the docker image being tested.
* @param volumes a map that declares any volume mappings to apply, or null
* @param envVars environment variables to set when running the container, or null
* @return the docker logs of the container
*/
public static Shell.Result runContainerExpectingFailure(
Distribution distribution,
Map<Path, Path> volumes,
Map<String, String> envVars
) {
executeDockerRun(distribution, volumes, envVars);
waitForElasticsearchToExit();
return sh.run("docker logs " + containerId);
}
private static void executeDockerRun(Distribution distribution, Map<Path, Path> volumes, Map<String, String> envVars) {
removeContainer(); removeContainer();
final List<String> args = new ArrayList<>(); final List<String> args = new ArrayList<>();
args.add("docker run"); args.add("docker run");
// Remove the container once it exits
args.add("--rm");
// Run the container in the background // Run the container in the background
args.add("--detach"); args.add("--detach");
if (envVars != null) {
envVars.forEach((key, value) -> args.add("--env " + key + "=\"" + value + "\"")); envVars.forEach((key, value) -> args.add("--env " + key + "=\"" + value + "\""));
}
// The container won't run without configuring discovery // The container won't run without configuring discovery
args.add("--env discovery.type=single-node"); args.add("--env discovery.type=single-node");
@ -113,33 +140,30 @@ public class Docker {
args.add("--publish 9200:9200"); args.add("--publish 9200:9200");
args.add("--publish 9300:9300"); args.add("--publish 9300:9300");
if (configPath != null) { // Bind-mount any volumes
// Bind-mount the config dir, if specified if (volumes != null) {
args.add("--volume \"" + configPath + ":/usr/share/elasticsearch/config\""); volumes.forEach((localPath, containerPath) -> args.add("--volume \"" + localPath + ":" + containerPath + "\""));
} }
args.add(distribution.flavor.name + ":test"); args.add(distribution.flavor.name + ":test");
final String command = String.join(" ", args); final String command = String.join(" ", args);
logger.debug("Running command: " + command); logger.info("Running command: " + command);
containerId = sh.run(command).stdout.trim(); containerId = sh.run(command).stdout.trim();
waitForElasticsearchToStart();
return Installation.ofContainer();
} }
/** /**
* Waits for the Elasticsearch process to start executing in the container. * Waits for the Elasticsearch process to start executing in the container.
* This is called every time a container is started. * This is called every time a container is started.
*/ */
private static void waitForElasticsearchToStart() throws InterruptedException { private static void waitForElasticsearchToStart() {
boolean isElasticsearchRunning = false; boolean isElasticsearchRunning = false;
int attempt = 0; int attempt = 0;
String psOutput; String psOutput = null;
do { do {
try {
// Give the container a chance to crash out // Give the container a chance to crash out
Thread.sleep(1000); Thread.sleep(1000);
@ -149,12 +173,48 @@ public class Docker {
isElasticsearchRunning = true; isElasticsearchRunning = true;
break; break;
} }
} catch (Exception e) {
logger.warn("Caught exception while waiting for ES to start", e);
}
} while (attempt++ < 5); } while (attempt++ < 5);
if (!isElasticsearchRunning) { if (isElasticsearchRunning == false) {
final String dockerLogs = sh.run("docker logs " + containerId).stdout; final Shell.Result dockerLogs = sh.run("docker logs " + containerId);
fail("Elasticsearch container did start successfully.\n\n" + psOutput + "\n\n" + dockerLogs); fail(
"Elasticsearch container did not start successfully.\n\nps output:\n"
+ psOutput
+ "\n\nStdout:\n"
+ dockerLogs.stdout
+ "\n\nStderr:\n"
+ dockerLogs.stderr
);
}
}
/**
* Waits for the Elasticsearch container to exit.
*/
private static void waitForElasticsearchToExit() {
boolean isElasticsearchRunning = true;
int attempt = 0;
do {
try {
// Give the container a chance to exit out
Thread.sleep(1000);
if (sh.run("docker ps --quiet --no-trunc").stdout.contains(containerId) == false) {
isElasticsearchRunning = false;
break;
}
} catch (Exception e) {
logger.warn("Caught exception while waiting for ES to exit", e);
}
} while (attempt++ < 5);
if (isElasticsearchRunning) {
final Shell.Result dockerLogs = sh.run("docker logs " + containerId);
fail("Elasticsearch container did exit.\n\nStdout:\n" + dockerLogs.stdout + "\n\nStderr:\n" + dockerLogs.stderr);
} }
} }
@ -170,10 +230,12 @@ public class Docker {
final Shell.Result result = sh.runIgnoreExitCode(command); final Shell.Result result = sh.runIgnoreExitCode(command);
if (result.isSuccess() == false) { if (result.isSuccess() == false) {
boolean isErrorAcceptable = result.stderr.contains("removal of container " + containerId + " is already in progress")
|| result.stderr.contains("Error: No such container: " + containerId);
// I'm not sure why we're already removing this container, but that's OK. // I'm not sure why we're already removing this container, but that's OK.
if (result.stderr.contains("removal of container " + " is already in progress") == false) { if (isErrorAcceptable == false) {
throw new RuntimeException( throw new RuntimeException("Command was not successful: [" + command + "] result: " + result.toString());
"Command was not successful: [" + command + "] result: " + result.toString());
} }
} }
} finally { } finally {
@ -204,11 +266,7 @@ public class Docker {
protected String[] getScriptCommand(String script) { protected String[] getScriptCommand(String script) {
assert containerId != null; assert containerId != null;
return super.getScriptCommand("docker exec " + return super.getScriptCommand("docker exec " + "--user elasticsearch:root " + "--tty " + containerId + " " + script);
"--user elasticsearch:root " +
"--tty " +
containerId + " " +
script);
} }
} }
@ -278,41 +336,30 @@ public class Docker {
final String homeDir = passwdResult.stdout.trim().split(":")[5]; final String homeDir = passwdResult.stdout.trim().split(":")[5];
assertThat(homeDir, equalTo("/usr/share/elasticsearch")); assertThat(homeDir, equalTo("/usr/share/elasticsearch"));
Stream.of( Stream.of(es.home, es.data, es.logs, es.config).forEach(dir -> assertPermissionsAndOwnership(dir, p775));
es.home,
es.data,
es.logs,
es.config
).forEach(dir -> assertPermissionsAndOwnership(dir, p775));
Stream.of( Stream.of(es.plugins, es.modules).forEach(dir -> assertPermissionsAndOwnership(dir, p755));
es.plugins,
es.modules
).forEach(dir -> assertPermissionsAndOwnership(dir, p755));
// FIXME these files should all have the same permissions // FIXME these files should all have the same permissions
Stream.of( Stream
.of(
"elasticsearch.keystore", "elasticsearch.keystore",
// "elasticsearch.yml", // "elasticsearch.yml",
"jvm.options" "jvm.options"
// "log4j2.properties" // "log4j2.properties"
).forEach(configFile -> assertPermissionsAndOwnership(es.config(configFile), p660)); )
.forEach(configFile -> assertPermissionsAndOwnership(es.config(configFile), p660));
Stream.of( Stream
"elasticsearch.yml", .of("elasticsearch.yml", "log4j2.properties")
"log4j2.properties" .forEach(configFile -> assertPermissionsAndOwnership(es.config(configFile), p644));
).forEach(configFile -> assertPermissionsAndOwnership(es.config(configFile), p644));
assertThat( assertThat(dockerShell.run(es.bin("elasticsearch-keystore") + " list").stdout, containsString("keystore.seed"));
dockerShell.run(es.bin("elasticsearch-keystore") + " list").stdout,
containsString("keystore.seed"));
Stream.of( Stream.of(es.bin, es.lib).forEach(dir -> assertPermissionsAndOwnership(dir, p755));
es.bin,
es.lib
).forEach(dir -> assertPermissionsAndOwnership(dir, p755));
Stream.of( Stream
.of(
"elasticsearch", "elasticsearch",
"elasticsearch-cli", "elasticsearch-cli",
"elasticsearch-env", "elasticsearch-env",
@ -321,17 +368,15 @@ public class Docker {
"elasticsearch-node", "elasticsearch-node",
"elasticsearch-plugin", "elasticsearch-plugin",
"elasticsearch-shard" "elasticsearch-shard"
).forEach(executable -> assertPermissionsAndOwnership(es.bin(executable), p755)); )
.forEach(executable -> assertPermissionsAndOwnership(es.bin(executable), p755));
Stream.of( Stream.of("LICENSE.txt", "NOTICE.txt", "README.textile").forEach(doc -> assertPermissionsAndOwnership(es.home.resolve(doc), p644));
"LICENSE.txt",
"NOTICE.txt",
"README.textile"
).forEach(doc -> assertPermissionsAndOwnership(es.home.resolve(doc), p644));
} }
private static void verifyDefaultInstallation(Installation es) { private static void verifyDefaultInstallation(Installation es) {
Stream.of( Stream
.of(
"elasticsearch-certgen", "elasticsearch-certgen",
"elasticsearch-certutil", "elasticsearch-certutil",
"elasticsearch-croneval", "elasticsearch-croneval",
@ -343,17 +388,38 @@ public class Docker {
"x-pack-env", "x-pack-env",
"x-pack-security-env", "x-pack-security-env",
"x-pack-watcher-env" "x-pack-watcher-env"
).forEach(executable -> assertPermissionsAndOwnership(es.bin(executable), p755)); )
.forEach(executable -> assertPermissionsAndOwnership(es.bin(executable), p755));
// at this time we only install the current version of archive distributions, but if that changes we'll need to pass // at this time we only install the current version of archive distributions, but if that changes we'll need to pass
// the version through here // the version through here
assertPermissionsAndOwnership(es.bin("elasticsearch-sql-cli-" + getCurrentVersion() + ".jar"), p755); assertPermissionsAndOwnership(es.bin("elasticsearch-sql-cli-" + getCurrentVersion() + ".jar"), p755);
Stream.of( Stream
"role_mapping.yml", .of("role_mapping.yml", "roles.yml", "users", "users_roles")
"roles.yml", .forEach(configFile -> assertPermissionsAndOwnership(es.config(configFile), p660));
"users", }
"users_roles"
).forEach(configFile -> assertPermissionsAndOwnership(es.config(configFile), p660)); public static void waitForElasticsearch(Installation installation) throws Exception {
withLogging(() -> ServerUtils.waitForElasticsearch(installation));
}
public static void waitForElasticsearch(String status, String index, Installation installation, String username, String password)
throws Exception {
withLogging(() -> ServerUtils.waitForElasticsearch(status, index, installation, username, password));
}
private static void withLogging(ThrowingRunnable r) throws Exception {
try {
r.run();
} catch (Exception e) {
final Shell.Result logs = sh.run("docker logs " + containerId);
logger.warn("Elasticsearch container failed to start.\n\nStdout:\n" + logs.stdout + "\n\nStderr:\n" + logs.stderr);
throw e;
}
}
private interface ThrowingRunnable {
void run() throws Exception;
} }
} }

View File

@ -50,6 +50,7 @@ public class FileMatcher extends TypeSafeMatcher<Path> {
public static final Set<PosixFilePermission> p750 = fromString("rwxr-x---"); public static final Set<PosixFilePermission> p750 = fromString("rwxr-x---");
public static final Set<PosixFilePermission> p660 = fromString("rw-rw----"); public static final Set<PosixFilePermission> p660 = fromString("rw-rw----");
public static final Set<PosixFilePermission> p644 = fromString("rw-r--r--"); public static final Set<PosixFilePermission> p644 = fromString("rw-r--r--");
public static final Set<PosixFilePermission> p600 = fromString("rw-------");
private final Fileness fileness; private final Fileness fileness;
private final String owner; private final String owner;

View File

@ -19,7 +19,9 @@
package org.elasticsearch.packaging.util; package org.elasticsearch.packaging.util;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse; import org.apache.http.HttpResponse;
import org.apache.http.client.fluent.Executor;
import org.apache.http.client.fluent.Request; import org.apache.http.client.fluent.Request;
import org.apache.http.entity.ContentType; import org.apache.http.entity.ContentType;
import org.apache.http.util.EntityUtils; import org.apache.http.util.EntityUtils;
@ -35,7 +37,7 @@ import static org.hamcrest.Matchers.containsString;
public class ServerUtils { public class ServerUtils {
protected static final Logger logger = LogManager.getLogger(ServerUtils.class); private static final Logger logger = LogManager.getLogger(ServerUtils.class);
// generous timeout as nested virtualization can be quite slow ... // generous timeout as nested virtualization can be quite slow ...
private static final long waitTime = TimeUnit.MINUTES.toMillis(3); private static final long waitTime = TimeUnit.MINUTES.toMillis(3);
@ -43,10 +45,36 @@ public class ServerUtils {
private static final long requestInterval = TimeUnit.SECONDS.toMillis(5); private static final long requestInterval = TimeUnit.SECONDS.toMillis(5);
public static void waitForElasticsearch(Installation installation) throws IOException { public static void waitForElasticsearch(Installation installation) throws IOException {
waitForElasticsearch("green", null, installation); waitForElasticsearch("green", null, installation, null, null);
} }
public static void waitForElasticsearch(String status, String index, Installation installation) throws IOException { /**
* Executes the supplied request, optionally applying HTTP basic auth if the
* username and pasword field are supplied.
* @param request the request to execute
* @param username the username to supply, or null
* @param password the password to supply, or null
* @return the response from the server
* @throws IOException if an error occurs
*/
private static HttpResponse execute(Request request, String username, String password) throws IOException {
final Executor executor = Executor.newInstance();
if (username != null && password != null) {
executor.auth(username, password);
executor.authPreemptive(new HttpHost("localhost", 9200));
}
return executor.execute(request).returnResponse();
}
public static void waitForElasticsearch(
String status,
String index,
Installation installation,
String username,
String password
) throws IOException {
Objects.requireNonNull(status); Objects.requireNonNull(status);
@ -56,15 +84,19 @@ public class ServerUtils {
long timeElapsed = 0; long timeElapsed = 0;
boolean started = false; boolean started = false;
Throwable thrownException = null; Throwable thrownException = null;
while (started == false && timeElapsed < waitTime) { while (started == false && timeElapsed < waitTime) {
if (System.currentTimeMillis() - lastRequest > requestInterval) { if (System.currentTimeMillis() - lastRequest > requestInterval) {
try { try {
final HttpResponse response = Request.Get("http://localhost:9200/_cluster/health") final HttpResponse response = execute(
Request
.Get("http://localhost:9200/_cluster/health")
.connectTimeout((int) timeoutLength) .connectTimeout((int) timeoutLength)
.socketTimeout((int) timeoutLength) .socketTimeout((int) timeoutLength),
.execute() username,
.returnResponse(); password
);
if (response.getStatusLine().getStatusCode() >= 300) { if (response.getStatusLine().getStatusCode() >= 300) {
final String statusLine = response.getStatusLine().toString(); final String statusLine = response.getStatusLine().toString();
@ -101,10 +133,9 @@ public class ServerUtils {
url = "http://localhost:9200/_cluster/health?wait_for_status=" + status + "&timeout=60s&pretty"; url = "http://localhost:9200/_cluster/health?wait_for_status=" + status + "&timeout=60s&pretty";
} else { } else {
url = "http://localhost:9200/_cluster/health/" + index + "?wait_for_status=" + status + "&timeout=60s&pretty"; url = "http://localhost:9200/_cluster/health/" + index + "?wait_for_status=" + status + "&timeout=60s&pretty";
} }
final String body = makeRequest(Request.Get(url)); final String body = makeRequest(Request.Get(url), username, password);
assertThat("cluster health response must contain desired status", body, containsString(status)); assertThat("cluster health response must contain desired status", body, containsString(status));
} }
@ -124,7 +155,11 @@ public class ServerUtils {
} }
public static String makeRequest(Request request) throws IOException { public static String makeRequest(Request request) throws IOException {
final HttpResponse response = request.execute().returnResponse(); return makeRequest(request, null, null);
}
public static String makeRequest(Request request, String username, String password) throws IOException {
final HttpResponse response = execute(request, username, password);
final String body = EntityUtils.toString(response.getEntity()); final String body = EntityUtils.toString(response.getEntity());
if (response.getStatusLine().getStatusCode() >= 300) { if (response.getStatusLine().getStatusCode() >= 300) {
@ -132,6 +167,5 @@ public class ServerUtils {
} }
return body; return body;
} }
} }