Skip to content

Commit

Permalink
Make it possible to use Stack logging in Docker
Browse files Browse the repository at this point in the history
Backport of elastic#65778.

Closes elastic#62758.

Include the Stack log4j config in the Docker image, in order to
make it possible to write logs in a container environment in the
same way as for an archive or package deployment. This is useful
in situations where the user is bind-mounting the logs directory
and has their own arrangements for log shipping.

To use stack logging, set the environment variable `ES_LOG_STYLE`
to `file`. It can also be set to `console`, which is the same as
not specifying it at all.

The Docker logging config is now auto-generated at image build time,
by running the default config through a transformer program when
preparing the distribution in an image builder step.

Also, in the docker distribution `build.gradle`, I changed a helper
closure into a class with a static method in order to fix an
issue where the Docker image was always being rebuilt, even when
there were no changes.
  • Loading branch information
pugnascotia committed Dec 10, 2020
1 parent d82b892 commit 0906c33
Show file tree
Hide file tree
Showing 12 changed files with 424 additions and 196 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* 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.gradle.docker;

/**
* The methods in this class take a shell command and wrap it in retry logic, so that our
* Docker builds can be more robust in the face of transient errors e.g. network issues.
*/
public class ShellRetry {
static String loop(String name, String command) {
return loop(name, command, 4, "exit");
}

static String loop(String name, String command, int indentSize, String exitKeyword) {
String indent = " ".repeat(indentSize);

StringBuilder commandWithRetry = new StringBuilder("for iter in {1..10}; do \n");
commandWithRetry.append(indent).append(" ").append(command).append(" && \n");
commandWithRetry.append(indent).append(" exit_code=0 && break || \n");
commandWithRetry.append(indent);
commandWithRetry.append(" exit_code=$? && echo \"").append(name).append(" error: retry $iter in 10s\" && sleep 10; \n");
commandWithRetry.append(indent).append("done; \n");
commandWithRetry.append(indent).append(exitKeyword).append(" $exit_code");

// We need to escape all newlines so that the build process doesn't run all lines onto a single line
return commandWithRetry.toString().replaceAll(" *\n", " \\\\\n");
}
}
63 changes: 9 additions & 54 deletions distribution/docker/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import org.elasticsearch.gradle.ElasticsearchDistribution.Flavor
import org.elasticsearch.gradle.LoggedExec
import org.elasticsearch.gradle.VersionProperties
import org.elasticsearch.gradle.docker.DockerBuildTask
import org.elasticsearch.gradle.docker.ShellRetry
import org.elasticsearch.gradle.info.BuildParams
import org.elasticsearch.gradle.testfixtures.TestFixturesPlugin

Expand All @@ -21,13 +22,15 @@ configurations {
dockerSource
aarch64OssDockerSource
ossDockerSource
transformLog4jJar
}

dependencies {
aarch64DockerSource project(path: ":distribution:archives:linux-aarch64-tar", configuration:"default")
dockerSource project(path: ":distribution:archives:linux-tar", configuration:"default")
aarch64OssDockerSource project(path: ":distribution:archives:oss-linux-aarch64-tar", configuration:"default")
ossDockerSource project(path: ":distribution:archives:oss-linux-tar", configuration:"default")
transformLog4jJar project(path: ":distribution:docker:transform-log4j-config", configuration: "default")
}

ext.expansions = { Architecture architecture, boolean oss, DockerBase base, boolean local ->
Expand Down Expand Up @@ -67,26 +70,14 @@ ARG BASE_TAG=8.2
sourceElasticsearch = "COPY $elasticsearch /opt/elasticsearch.tar.gz"
} else {
sourceElasticsearch = """
RUN curl --retry 8 -S -L \\
RUN curl --retry 10 -S -L \\
--output /opt/elasticsearch.tar.gz \\
https://artifacts-no-kpi.elastic.co/downloads/elasticsearch/$elasticsearch
"""
}

def (major,minor) = VersionProperties.elasticsearch.split("\\.")

def retry_loop = { name, command, indentSize = 4, exitKeyword = 'exit' ->
String indent = ' ' * indentSize
String commandWithRetry = """for iter in {1..10}; do
${indent} ${command} &&
${indent} exit_code=0 && break ||
${indent} exit_code=\$? && echo "${name} error: retry \$iter in 10s" && sleep 10;
${indent}done;
${indent}${exitKeyword} \$exit_code"""

return commandWithRetry.replaceAll(" *\n", " \\\\\n")
}

return [
'base_image' : base.getImage(),
'bin_dir' : base == DockerBase.IRON_BANK ? 'scripts' : 'bin',
Expand All @@ -100,7 +91,7 @@ ${indent}${exitKeyword} \$exit_code"""
'docker_base' : base.name().toLowerCase(),
'version' : VersionProperties.elasticsearch,
'major_minor_version' : "${major}.${minor}",
'retry_loop' : retry_loop
'retry' : ShellRetry
]
}

Expand Down Expand Up @@ -196,6 +187,10 @@ void addCopyDockerContextTask(Architecture architecture, boolean oss, DockerBase

with dockerBuildContext(architecture, oss, base, true)

into(base == DockerBase.IRON_BANK ? 'scripts' : 'bin') {
from configurations.transformLog4jJar
}

if (architecture == Architecture.AARCH64) {
if (oss) {
from configurations.aarch64OssDockerSource
Expand Down Expand Up @@ -230,46 +225,6 @@ tasks.register("copyKeystore", Sync) {
}
}

tasks.register("checkSecurityAuditLayoutPatternIdentical") {
// the two log4j2.properties files containing security audit configuration for archive and docker builds respectively
def originalLog4j = project(":x-pack:plugin:core").file('src/main/config/log4j2.properties')
def dockerLog4j = project.file("src/docker/config/log4j2.properties")
inputs.files(originalLog4j, dockerLog4j)
def patternPropertyKey = "appender.audit_rolling.layout.pattern"
doLast {
def coreLog4jProperties = new Properties()
originalLog4j.withInputStream { input ->
coreLog4jProperties.load(input)
}

if (false == coreLog4jProperties.containsKey(patternPropertyKey)) {
throw new GradleException("The [${originalLog4j.getPath()}] file changed such that the layout pattern is not " +
"referred to by the property named [${patternPropertyKey}]. Please update the task [${name}] " +
"definition from project [${path}] to reflect the new name for the layout pattern property.")
}

def dockerLog4jProperties = new Properties()
dockerLog4j.withInputStream { input ->
dockerLog4jProperties.load(input)
}

if (false == dockerLog4jProperties.containsKey(patternPropertyKey)) {
throw new GradleException("The [${dockerLog4j.getPath()}] file changed such that the layout pattern is not " +
"referred to by the property named [${patternPropertyKey}]. Please update the task [${name}] " +
"definition from project [${path}] to reflect the new name for the layout pattern property.")
}

if (false == coreLog4jProperties.getProperty(patternPropertyKey).equals(dockerLog4jProperties.getProperty(patternPropertyKey))) {
throw new GradleException("The property value for the layout pattern [${patternPropertyKey}] is NOT identical " +
"between the [${originalLog4j.getPath()}] and the [${dockerLog4j.getPath()}] files.")
}
}
}

tasks.named("precommit").configure {
dependsOn 'checkSecurityAuditLayoutPatternIdentical'
}

elasticsearch_distributions {
Architecture.values().each { eachArchitecture ->
Flavor.values().each { distroFlavor ->
Expand Down
23 changes: 14 additions & 9 deletions distribution/docker/src/docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ FROM ${base_image} AS builder
<% if (docker_base == 'ubi') { %>
# Install required packages to extract the Elasticsearch distribution
RUN <%= retry_loop(package_manager, "${package_manager} install -y findutils tar gzip") %>
RUN <%= retry.loop(package_manager, "${package_manager} install -y findutils tar gzip") %>
<% } %>
<% if (docker_base == 'iron_bank') { %>
Expand Down Expand Up @@ -69,21 +69,26 @@ ${source_elasticsearch}
RUN tar -zxf /opt/elasticsearch.tar.gz --strip-components=1
# The distribution includes a `config` directory, no need to create it
COPY ${config_dir}/elasticsearch.yml ${config_dir}/log4j2.properties config/
COPY ${config_dir}/elasticsearch.yml config/
COPY ${bin_dir}/transform-log4j-config-${version}.jar /tmp/
# 1. Configure the distribution for Docker
# 2. Ensure directories are created. Most already are, but make sure
# 3. Apply correct permissions
# 4. Apply more correct permissions
# 5. The JDK's directories' permissions don't allow `java` to be executed under a different
# 4. Move the distribution's default logging config aside
# 5. Generate a docker logging config, to be used by default
# 6. Apply more correct permissions
# 7. The JDK's directories' permissions don't allow `java` to be executed under a different
# group to the default. Fix this.
# 6. Ensure that there are no files with setuid or setgid, in order to mitigate "stackclash" attacks.
# 7. Ensure all files are world-readable by default. It should be possible to
# 8. Ensure that there are no files with setuid or setgid, in order to mitigate "stackclash" attacks.
# 9. Ensure all files are world-readable by default. It should be possible to
# examine the contents of the image under any UID:GID
RUN sed -i -e 's/ES_DISTRIBUTION_TYPE=tar/ES_DISTRIBUTION_TYPE=docker/' /usr/share/elasticsearch/bin/elasticsearch-env && \\
RUN sed -i -e 's/ES_DISTRIBUTION_TYPE=tar/ES_DISTRIBUTION_TYPE=docker/' bin/elasticsearch-env && \\
mkdir -p config/jvm.options.d data logs plugins && \\
chmod 0775 config config/jvm.options.d data logs plugins && \\
chmod 0660 config/elasticsearch.yml config/log4j2.properties && \\
mv config/log4j2.properties config/log4j2.file.properties && \\
jdk/bin/java -jar /tmp/transform-log4j-config-${version}.jar config/log4j2.file.properties > config/log4j2.properties && \\
chmod 0660 config/elasticsearch.yml config/log4j2*.properties && \\
find ./jdk -type d -exec chmod 0755 {} + && \\
find . -xdev -perm -4000 -exec chmod ug-s {} + && \\
find . -type f -exec chmod o+r {} +
Expand All @@ -109,7 +114,7 @@ RUN ${package_manager} update --setopt=tsflags=nodocs -y && \\
<% } else { %>
RUN <%= retry_loop(
RUN <%= retry.loop(
package_manager,
"${package_manager} update --setopt=tsflags=nodocs -y && \n" +
" ${package_manager} install --setopt=tsflags=nodocs -y \n" +
Expand Down
17 changes: 17 additions & 0 deletions distribution/docker/src/docker/bin/docker-entrypoint.sh
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,21 @@ if [[ "$(id -u)" == "0" ]]; then
fi
fi

if [[ -n "$ES_LOG_STYLE" ]]; then
case "$ES_LOG_STYLE" in
console)
# This is the default. Nothing to do.
;;
file)
# Overwrite the default config with the stack config
mv /usr/share/elasticsearch/config/log4j2.file.properties /usr/share/elasticsearch/config/log4j2.properties
;;
*)
echo "ERROR: ES_LOG_STYLE set to [$ES_LOG_STYLE]. Expected [console] or [file]" >&2
exit 1 ;;
esac
fi

# Signal forwarding and child reaping is handled by `tini`, which is the
# actual entrypoint of the container
run_as_other_user_if_needed /usr/share/elasticsearch/bin/elasticsearch <<<"$KEYSTORE_PASSWORD"
131 changes: 0 additions & 131 deletions distribution/docker/src/docker/config/log4j2.properties

This file was deleted.

Loading

0 comments on commit 0906c33

Please sign in to comment.