diff --git a/distribution/docker/build.gradle b/distribution/docker/build.gradle index 8e33d66326d..c9ecceac110 100644 --- a/distribution/docker/build.gradle +++ b/distribution/docker/build.gradle @@ -28,22 +28,32 @@ dependencies { } ext.expansions = { architecture, oss, local -> + String base_image = null + String tini_arch = null + String classifier = null switch (architecture) { case "aarch64": + base_image = "arm64v8/centos:7" + tini_arch = "arm64" + classifier = "linux-aarch64" + break; case "x64": + base_image = "amd64/centos:7" + tini_arch = "amd64" + classifier = "linux-x86_64" break; default: throw new IllegalArgumentException("unrecongized architecture [" + architecture + "], must be one of (aarch64|x64)") } - final String classifier = "aarch64".equals(architecture) ? "linux-aarch64" : "linux-x86_64" final String elasticsearch = oss ? "elasticsearch-oss-${VersionProperties.elasticsearch}-${classifier}.tar.gz" : "elasticsearch-${VersionProperties.elasticsearch}-${classifier}.tar.gz" return [ - 'base_image' : "aarch64".equals(architecture) ? "arm64v8/centos:7" : "centos:7", + 'base_image' : base_image, 'build_date' : BuildParams.buildDate, 'elasticsearch' : elasticsearch, 'git_revision' : BuildParams.gitRevision, 'license' : oss ? 'Apache-2.0' : 'Elastic-License', 'source_elasticsearch': local ? "COPY $elasticsearch /opt/" : "RUN cd /opt && curl --retry 8 -s -L -O https://artifacts.elastic.co/downloads/elasticsearch/${elasticsearch} && cd -", + 'tini_arch' : tini_arch, 'version' : VersionProperties.elasticsearch ] } @@ -227,6 +237,7 @@ subprojects { Project subProject -> def tarFile = "${parent.projectDir}/build/elasticsearch${"aarch64".equals(architecture) ? '-aarch64' : ''}${oss ? '-oss' : ''}_test.${VersionProperties.elasticsearch}.docker.tar" final Task exportDockerImageTask = task(exportTaskName, type: LoggedExec) { + inputs.file("${parent.projectDir}/build/markers/${buildTaskName}.marker") executable 'docker' outputs.file(tarFile) args "save", diff --git a/distribution/docker/src/docker/Dockerfile b/distribution/docker/src/docker/Dockerfile index 1858597e230..e8babab244a 100644 --- a/distribution/docker/src/docker/Dockerfile +++ b/distribution/docker/src/docker/Dockerfile @@ -14,10 +14,22 @@ FROM ${base_image} AS builder RUN for iter in {1..10}; do yum update --setopt=tsflags=nodocs -y && \ - yum install --setopt=tsflags=nodocs -y gzip shadow-utils tar && \ + yum install --setopt=tsflags=nodocs -y wget gzip shadow-utils tar && \ yum clean all && exit_code=0 && break || exit_code=\$? && echo "yum error: retry \$iter in 10s" && sleep 10; done; \ (exit \$exit_code) +# `tini` is a tiny but valid init for containers. This is used to cleanly +# control how ES and any child processes are shut down. +# +# The tini GitHub page gives instructions for verifying the binary using +# gpg, but the keyservers are slow to return the key and this can fail the +# build. Instead, we check the binary against a checksum that they provide. +RUN wget --no-cookies --quiet https://github.com/krallin/tini/releases/download/v0.19.0/tini-${tini_arch} \ + && wget --no-cookies --quiet https://github.com/krallin/tini/releases/download/v0.19.0/tini-${tini_arch}.sha256sum \ + && sha256sum -c tini-${tini_arch}.sha256sum \ + && mv tini-${tini_arch} /tini \ + && chmod +x /tini + ENV PATH /usr/share/elasticsearch/bin:\$PATH RUN groupadd -g 1000 elasticsearch && \ @@ -45,6 +57,8 @@ FROM ${base_image} ENV ELASTIC_CONTAINER true +COPY --from=builder /tini /tini + RUN for iter in {1..10}; do yum update --setopt=tsflags=nodocs -y && \ yum install --setopt=tsflags=nodocs -y nc shadow-utils zip unzip && \ yum clean all && exit_code=0 && break || exit_code=\$? && echo "yum error: retry \$iter in 10s" && sleep 10; done; \ @@ -65,17 +79,14 @@ RUN ln -sf /etc/pki/ca-trust/extracted/java/cacerts /usr/share/elasticsearch/jdk ENV PATH /usr/share/elasticsearch/bin:\$PATH -COPY --chown=1000:0 bin/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh +COPY bin/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh + +RUN chmod g=u /etc/passwd && \ + chmod 0775 /usr/local/bin/docker-entrypoint.sh # Ensure that there are no files with setuid or setgid, in order to mitigate "stackclash" attacks. RUN find / -xdev -perm -4000 -exec chmod ug-s {} + -# Openshift overrides USER and uses ones with randomly uid>1024 and gid=0 -# Allow ENTRYPOINT (and ES) to run even with a different user -RUN chgrp 0 /usr/local/bin/docker-entrypoint.sh && \ - chmod g=u /etc/passwd && \ - chmod 0775 /usr/local/bin/docker-entrypoint.sh - EXPOSE 9200 9300 LABEL org.label-schema.build-date="${build_date}" \ @@ -98,7 +109,7 @@ LABEL org.label-schema.build-date="${build_date}" \ org.opencontainers.image.vendor="Elastic" \ org.opencontainers.image.version="${version}" -ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"] +ENTRYPOINT ["/tini", "--", "/usr/local/bin/docker-entrypoint.sh"] # Dummy overridable parameter parsed by entrypoint CMD ["eswrapper"] diff --git a/qa/os/src/test/java/org/elasticsearch/packaging/test/DockerTests.java b/qa/os/src/test/java/org/elasticsearch/packaging/test/DockerTests.java index 9a16800dc84..33e1f8851c7 100644 --- a/qa/os/src/test/java/org/elasticsearch/packaging/test/DockerTests.java +++ b/qa/os/src/test/java/org/elasticsearch/packaging/test/DockerTests.java @@ -68,6 +68,7 @@ import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.Matchers.startsWith; import static org.junit.Assume.assumeFalse; import static org.junit.Assume.assumeTrue; @@ -139,7 +140,7 @@ public class DockerTests extends PackagingTestCase { /** * Checks that there are Amazon trusted certificates in the cacaerts keystore. */ - public void test043AmazonCaCertsAreInTheKeystore() { + public void test041AmazonCaCertsAreInTheKeystore() { final boolean matches = Arrays.stream( sh.run("jdk/bin/keytool -cacerts -storepass changeit -list | grep trustedCertEntry").stdout.split("\n") ).anyMatch(line -> line.contains("amazonrootca")); @@ -251,8 +252,8 @@ public class DockerTests extends PackagingTestCase { 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", + "Failed to check whether Elasticsearch had started. This could be because " + + "authentication isn't working properly. Check the container logs", e ); } @@ -335,8 +336,7 @@ public class DockerTests extends PackagingTestCase { Files.write(tempDir.resolve(passwordFilename), "hunter2\n".getBytes(StandardCharsets.UTF_8)); - Map envVars = new HashMap<>(); - envVars.put("ELASTIC_PASSWORD_FILE", "/run/secrets/" + passwordFilename); + Map envVars = singletonMap("ELASTIC_PASSWORD_FILE", "/run/secrets/" + passwordFilename); // Set invalid file permissions Files.setPosixFilePermissions(tempDir.resolve(passwordFilename), p660); @@ -484,7 +484,6 @@ public class DockerTests extends PackagingTestCase { /** * Check that the Docker image has the expected "Label Schema" labels. - * * @see Label Schema website */ public void test110OrgLabelSchemaLabels() throws Exception { @@ -526,7 +525,6 @@ public class DockerTests extends PackagingTestCase { /** * Check that the Docker image has the expected "Open Containers Annotations" labels. - * * @see Open Containers Annotations */ public void test110OrgOpencontainersLabels() throws Exception { @@ -577,10 +575,10 @@ public class DockerTests extends PackagingTestCase { } /** - * Check that the Java process running inside the container has the expect PID, UID and username. + * Check that the Java process running inside the container has the expected UID, GID and username. */ - public void test130JavaHasCorrectPidAndOwnership() { - final List processes = Arrays.stream(sh.run("ps -o pid,uid,user -C java").stdout.split("\n")) + public void test130JavaHasCorrectOwnership() { + final List processes = Arrays.stream(sh.run("ps -o uid,gid,user -C java").stdout.split("\n")) .skip(1) .collect(Collectors.toList()); @@ -589,11 +587,34 @@ public class DockerTests extends PackagingTestCase { final String[] fields = processes.get(0).trim().split("\\s+"); assertThat(fields, arrayWithSize(3)); - assertThat("Incorrect PID", fields[0], equalTo("1")); - assertThat("Incorrect UID", fields[1], equalTo("1000")); + assertThat("Incorrect UID", fields[0], equalTo("1000")); + assertThat("Incorrect GID", fields[1], equalTo("0")); assertThat("Incorrect username", fields[2], equalTo("elasticsearch")); } + /** + * Check that the init process running inside the container has the expected PID, UID, GID and user. + * The PID is particularly important because PID 1 handles signal forwarding and child reaping. + */ + public void test131InitProcessHasCorrectPID() { + final List processes = Arrays.stream(sh.run("ps -o pid,uid,gid,command -p 1").stdout.split("\n")) + .skip(1) + .collect(Collectors.toList()); + + assertThat("Expected a single process", processes, hasSize(1)); + + final String[] fields = processes.get(0).trim().split("\\s+", 4); + + assertThat(fields, arrayWithSize(4)); + assertThat("Incorrect PID", fields[0], equalTo("1")); + assertThat("Incorrect UID", fields[1], equalTo("0")); + assertThat("Incorrect GID", fields[2], equalTo("0")); + assertThat("Incorrect init command", fields[3], startsWith("/tini")); + } + + /** + * Check that Elasticsearch reports per-node cgroup information. + */ public void test140CgroupOsStatsAreAvailable() throws Exception { waitForElasticsearch(installation);