Skip to content

Commit 29dafe6

Browse files
donoghucmergify[bot]
authored andcommitted
Rewrite Env2yaml in java instead of Go (#18423)
* WIP: Rewrite Env2yaml in java instead of Go Managing a Go toolchain for persisting ENV vars in logstash container artifacts has become cumbersome. We already manage a java runtime so this commit presents a path forward to use that instead of Go. The Go binary is faster than java (in my testing Go would complete in around less than 200ms while java takes over 300ms). Given the container startup time is on the order of magnitute of seconds this change should be inperceptable to consumers. The benefit from consolidating in Java is worth the slightly lower performance. * Use TreeMap in java to try to replicate lexicographical order * Explicit imports and TreeMap everywher * Go removals and ironbank workflow update * More non-code removals * Update based on codereview Use snakeyaml-engine and some java flags for faster execution * Build env2yaml in stage Build env2yaml in a separate build stage for container artifacts. Include its dependencies and manage separately from logstash. Continue to use the java runtime in the final container to run the program, but manage the classpath separately. Note this did not use gradle for dependency management because installing that as a depdendcy was not worth it compared with downloading a jar directly. * Use gradle to manage snakeyaml-engine dependency Use gradle (and a dedicated gradle base image) for building env2yaml * Refactor to build env2yaml with gradle rather than in docker build Dont rely on compiling at docker build time, rather do it when logstash compilation is done. * Dont try to use snakeyaml from jruby The complexity around trying to copy over the jar shipped with jruby is not worth how easy it is to just manage it with gradle. This helps with keeping env2yaml contained. * Add license for snakeyaml-engine Licence from https://bitbucket.org/snakeyaml/snakeyaml-engine/src/master/LICENSE.txt * Cleanup and bugfix * Stop skipping empty env vars I mistakenly thought I had observed this behavior in the go version. * Remove quotes from interpolated values Even though we set `.setDefaultScalarStyle(ScalarStyle.PLAIN)` snakeyaml-engine ends up quoting `${}` values. This commit removes them as this was not the behavior with the go version. (cherry picked from commit b15c6c5) # Conflicts: # docker/data/logstash/env2yaml/env2yaml.go # docker/templates/Dockerfile.erb # rakelib/artifacts.rake
1 parent 7946f7c commit 29dafe6

File tree

18 files changed

+509
-64
lines changed

18 files changed

+509
-64
lines changed

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ out
1616
local
1717
test/setup/elasticsearch/elasticsearch-*
1818
vendor
19-
!docker/ironbank/go/src/env2yaml/vendor
2019
.sass-cache
2120
/data
2221
.buildpath

build.gradle

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,13 @@ tasks.register("bootstrap") {
297297
}
298298

299299

300+
301+
task dockerBootstrap {
302+
description = "Docker bootstrap ensures env2yaml java is compiled and staged for inclusion in tarballs"
303+
dependsOn bootstrap
304+
dependsOn ':docker:data:logstash:env2yaml:compileJava'
305+
}
306+
300307
tasks.register("installDefaultGems") {
301308
dependsOn bootstrap
302309
doLast {

docker/Makefile

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ build-from-local-wolfi-artifacts: dockerfile
6060

6161
COPY_FILES := $(ARTIFACTS_DIR)/docker/config/pipelines.yml $(ARTIFACTS_DIR)/docker/config/logstash-oss.yml $(ARTIFACTS_DIR)/docker/config/logstash-full.yml
6262
COPY_FILES += $(ARTIFACTS_DIR)/docker/config/log4j2.file.properties $(ARTIFACTS_DIR)/docker/config/log4j2.properties
63-
COPY_FILES += $(ARTIFACTS_DIR)/docker/env2yaml/env2yaml.go $(ARTIFACTS_DIR)/docker/env2yaml/go.mod $(ARTIFACTS_DIR)/docker/env2yaml/go.sum
63+
COPY_FILES += $(ARTIFACTS_DIR)/docker/env2yaml/env2yaml
6464
COPY_FILES += $(ARTIFACTS_DIR)/docker/pipeline/default.conf $(ARTIFACTS_DIR)/docker/bin/docker-entrypoint
6565

6666
$(ARTIFACTS_DIR)/docker/config/pipelines.yml: data/logstash/config/pipelines.yml
@@ -70,9 +70,7 @@ $(ARTIFACTS_DIR)/docker/config/log4j2.file.properties: data/logstash/config/log4
7070
$(ARTIFACTS_DIR)/docker/config/log4j2.properties: data/logstash/config/log4j2.properties
7171
$(ARTIFACTS_DIR)/docker/pipeline/default.conf: data/logstash/pipeline/default.conf
7272
$(ARTIFACTS_DIR)/docker/bin/docker-entrypoint: data/logstash/bin/docker-entrypoint
73-
$(ARTIFACTS_DIR)/docker/env2yaml/env2yaml.go: data/logstash/env2yaml/env2yaml.go
74-
$(ARTIFACTS_DIR)/docker/env2yaml/go.mod: data/logstash/env2yaml/go.mod
75-
$(ARTIFACTS_DIR)/docker/env2yaml/go.sum: data/logstash/env2yaml/go.sum
73+
$(ARTIFACTS_DIR)/docker/env2yaml/env2yaml: data/logstash/env2yaml/env2yaml
7674

7775
$(ARTIFACTS_DIR)/docker/%:
7876
cp -f $< $@
@@ -83,22 +81,22 @@ docker_paths:
8381
mkdir -p $(ARTIFACTS_DIR)/docker/config
8482
mkdir -p $(ARTIFACTS_DIR)/docker/env2yaml
8583
mkdir -p $(ARTIFACTS_DIR)/docker/pipeline
84+
cp -r data/logstash/env2yaml/classes $(ARTIFACTS_DIR)/docker/env2yaml/
85+
cp -r data/logstash/env2yaml/lib $(ARTIFACTS_DIR)/docker/env2yaml/
8686

8787
COPY_IRONBANK_FILES := $(ARTIFACTS_DIR)/ironbank/scripts/config/pipelines.yml $(ARTIFACTS_DIR)/ironbank/scripts/config/logstash.yml
8888
COPY_IRONBANK_FILES += $(ARTIFACTS_DIR)/ironbank/scripts/config/log4j2.file.properties $(ARTIFACTS_DIR)/ironbank/scripts/config/log4j2.properties
89-
COPY_IRONBANK_FILES += $(ARTIFACTS_DIR)/ironbank/scripts/pipeline/default.conf $(ARTIFACTS_DIR)/ironbank/scripts/bin/docker-entrypoint $(ARTIFACTS_DIR)/ironbank/scripts/go/src/env2yaml/env2yaml.go
90-
COPY_IRONBANK_FILES += $(ARTIFACTS_DIR)/ironbank/scripts/go/src/env2yaml/go.mod $(ARTIFACTS_DIR)/ironbank/scripts/go/src/env2yaml/go.sum $(ARTIFACTS_DIR)/ironbank/scripts/go/src/env2yaml/vendor/modules.txt $(ARTIFACTS_DIR)/ironbank/LICENSE $(ARTIFACTS_DIR)/ironbank/README.md
89+
COPY_IRONBANK_FILES += $(ARTIFACTS_DIR)/ironbank/scripts/pipeline/default.conf $(ARTIFACTS_DIR)/ironbank/scripts/bin/docker-entrypoint
90+
COPY_IRONBANK_FILES += $(ARTIFACTS_DIR)/ironbank/scripts/env2yaml/env2yaml
91+
COPY_IRONBANK_FILES += $(ARTIFACTS_DIR)/ironbank/LICENSE $(ARTIFACTS_DIR)/ironbank/README.md
9192

9293
$(ARTIFACTS_DIR)/ironbank/scripts/config/pipelines.yml: data/logstash/config/pipelines.yml
9394
$(ARTIFACTS_DIR)/ironbank/scripts/config/logstash.yml: data/logstash/config/logstash-full.yml
9495
$(ARTIFACTS_DIR)/ironbank/scripts/config/log4j2.file.properties: data/logstash/config/log4j2.file.properties
9596
$(ARTIFACTS_DIR)/ironbank/scripts/config/log4j2.properties: data/logstash/config/log4j2.properties
9697
$(ARTIFACTS_DIR)/ironbank/scripts/pipeline/default.conf: data/logstash/pipeline/default.conf
9798
$(ARTIFACTS_DIR)/ironbank/scripts/bin/docker-entrypoint: data/logstash/bin/docker-entrypoint
98-
$(ARTIFACTS_DIR)/ironbank/scripts/go/src/env2yaml/env2yaml.go: data/logstash/env2yaml/env2yaml.go
99-
$(ARTIFACTS_DIR)/ironbank/scripts/go/src/env2yaml/go.mod: ironbank/go/src/env2yaml/go.mod
100-
$(ARTIFACTS_DIR)/ironbank/scripts/go/src/env2yaml/go.sum: ironbank/go/src/env2yaml/go.sum
101-
$(ARTIFACTS_DIR)/ironbank/scripts/go/src/env2yaml/vendor/modules.txt: ironbank/go/src/env2yaml/vendor/modules.txt
99+
$(ARTIFACTS_DIR)/ironbank/scripts/env2yaml/env2yaml: data/logstash/env2yaml/env2yaml
102100
$(ARTIFACTS_DIR)/ironbank/LICENSE: ironbank/LICENSE
103101
$(ARTIFACTS_DIR)/ironbank/README.md: ironbank/README.md
104102

@@ -110,8 +108,10 @@ ironbank_docker_paths:
110108
mkdir -p $(ARTIFACTS_DIR)/ironbank/scripts
111109
mkdir -p $(ARTIFACTS_DIR)/ironbank/scripts/bin
112110
mkdir -p $(ARTIFACTS_DIR)/ironbank/scripts/config
113-
mkdir -p $(ARTIFACTS_DIR)/ironbank/scripts/go/src/env2yaml/vendor
111+
mkdir -p $(ARTIFACTS_DIR)/ironbank/scripts/env2yaml
114112
mkdir -p $(ARTIFACTS_DIR)/ironbank/scripts/pipeline
113+
cp -r data/logstash/env2yaml/classes $(ARTIFACTS_DIR)/ironbank/scripts/env2yaml/
114+
cp -r data/logstash/env2yaml/lib $(ARTIFACTS_DIR)/ironbank/scripts/env2yaml/
115115

116116
public-dockerfiles: public-dockerfiles_oss public-dockerfiles_full public-dockerfiles_wolfi public-dockerfiles_ironbank
117117

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
plugins {
2+
id 'java'
3+
}
4+
5+
compileJava {
6+
destinationDirectory = file("${projectDir}/classes")
7+
}
8+
9+
repositories {
10+
mavenCentral()
11+
}
12+
13+
dependencies {
14+
implementation 'org.snakeyaml:snakeyaml-engine:2.9'
15+
}
16+
17+
tasks.register('copyRuntimeLibs', Copy) {
18+
from configurations.runtimeClasspath
19+
into "${projectDir}/lib"
20+
}
21+
22+
compileJava.finalizedBy copyRuntimeLibs
23+
jar.enabled = false
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#!/bin/bash
2+
3+
# Execute the env2yaml java program. Ensure the snakeyaml-engine jar is in the classpath.
4+
5+
exec /usr/share/logstash/jdk/bin/java \
6+
-XX:+UseSerialGC \
7+
-Xms32m \
8+
-Xmx32m \
9+
-cp "/usr/share/logstash/env2yaml/classes:/usr/share/logstash/env2yaml/lib/*" \
10+
org.logstash.env2yaml.Env2Yaml "$@"
11+
12+

docker/data/logstash/env2yaml/go.mod

Lines changed: 0 additions & 5 deletions
This file was deleted.

docker/data/logstash/env2yaml/go.sum

Lines changed: 0 additions & 3 deletions
This file was deleted.
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
package org.logstash.env2yaml;
2+
3+
import java.io.InputStream;
4+
import java.nio.charset.StandardCharsets;
5+
import java.nio.file.Files;
6+
import java.nio.file.Path;
7+
import java.nio.file.Paths;
8+
import java.nio.file.StandardOpenOption;
9+
import java.nio.file.attribute.PosixFilePermission;
10+
import java.time.LocalDateTime;
11+
import java.time.format.DateTimeFormatter;
12+
import java.util.Map;
13+
import java.util.Set;
14+
import java.util.TreeMap;
15+
import org.snakeyaml.engine.v2.api.Dump;
16+
import org.snakeyaml.engine.v2.api.DumpSettings;
17+
import org.snakeyaml.engine.v2.api.Load;
18+
import org.snakeyaml.engine.v2.api.LoadSettings;
19+
import org.snakeyaml.engine.v2.common.FlowStyle;
20+
import org.snakeyaml.engine.v2.common.ScalarStyle;
21+
22+
/**
23+
* Environment variable to YAML configuration merger
24+
*
25+
* Takes environment variables and merges them into logstash.yml
26+
* Example: docker run -e pipeline.workers=6
27+
* or: docker run -e PIPELINE_WORKERS=6
28+
* Result: pipeline.workers: 6 in logstash.yml
29+
*/
30+
public class Env2Yaml {
31+
private static final DateTimeFormatter TIMESTAMP_FORMAT = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
32+
private static class SettingValidator {
33+
private final Map<String, String> normalizedToCanonical;
34+
35+
public SettingValidator() {
36+
this.normalizedToCanonical = buildSettingMap();
37+
}
38+
39+
private Map<String, String> buildSettingMap() {
40+
Map<String, String> map = new TreeMap<>();
41+
String[] allowedConfigs = {
42+
"api.enabled", "api.http.host", "api.http.port", "api.environment",
43+
"node.name", "path.data", "pipeline.id", "pipeline.workers",
44+
"pipeline.output.workers", "pipeline.batch.size", "pipeline.batch.delay",
45+
"pipeline.unsafe_shutdown", "pipeline.ecs_compatibility", "pipeline.ordered",
46+
"pipeline.plugin_classloaders", "pipeline.separate_logs", "path.config",
47+
"config.string", "config.test_and_exit", "config.reload.automatic",
48+
"config.reload.interval", "config.debug", "config.support_escapes",
49+
"config.field_reference.escape_style", "queue.type", "path.queue",
50+
"queue.page_capacity", "queue.max_events", "queue.max_bytes",
51+
"queue.checkpoint.acks", "queue.checkpoint.writes", "queue.checkpoint.interval",
52+
"queue.compression", "queue.drain", "dead_letter_queue.enable",
53+
"dead_letter_queue.max_bytes", "dead_letter_queue.flush_interval",
54+
"dead_letter_queue.storage_policy", "dead_letter_queue.retain.age",
55+
"path.dead_letter_queue", "log.level", "log.format",
56+
"log.format.json.fix_duplicate_message_fields", "metric.collect",
57+
"path.logs", "path.plugins", "api.auth.type", "api.auth.basic.username",
58+
"api.auth.basic.password", "api.auth.basic.password_policy.mode",
59+
"api.auth.basic.password_policy.length.minimum", "api.auth.basic.password_policy.include.upper",
60+
"api.auth.basic.password_policy.include.lower", "api.auth.basic.password_policy.include.digit",
61+
"api.auth.basic.password_policy.include.symbol", "allow_superuser",
62+
"monitoring.cluster_uuid", "xpack.monitoring.allow_legacy_collection",
63+
"xpack.monitoring.enabled", "xpack.monitoring.collection.interval",
64+
"xpack.monitoring.elasticsearch.hosts", "xpack.monitoring.elasticsearch.username",
65+
"xpack.monitoring.elasticsearch.password", "xpack.monitoring.elasticsearch.proxy",
66+
"xpack.monitoring.elasticsearch.api_key", "xpack.monitoring.elasticsearch.cloud_auth",
67+
"xpack.monitoring.elasticsearch.cloud_id", "xpack.monitoring.elasticsearch.sniffing",
68+
"xpack.monitoring.elasticsearch.ssl.certificate_authority", "xpack.monitoring.elasticsearch.ssl.ca_trusted_fingerprint",
69+
"xpack.monitoring.elasticsearch.ssl.verification_mode", "xpack.monitoring.elasticsearch.ssl.truststore.path",
70+
"xpack.monitoring.elasticsearch.ssl.truststore.password", "xpack.monitoring.elasticsearch.ssl.keystore.path",
71+
"xpack.monitoring.elasticsearch.ssl.keystore.password", "xpack.monitoring.elasticsearch.ssl.certificate",
72+
"xpack.monitoring.elasticsearch.ssl.key", "xpack.monitoring.elasticsearch.ssl.cipher_suites",
73+
"xpack.management.enabled", "xpack.management.logstash.poll_interval",
74+
"xpack.management.pipeline.id", "xpack.management.elasticsearch.hosts",
75+
"xpack.management.elasticsearch.username", "xpack.management.elasticsearch.password",
76+
"xpack.management.elasticsearch.proxy", "xpack.management.elasticsearch.api_key",
77+
"xpack.management.elasticsearch.cloud_auth", "xpack.management.elasticsearch.cloud_id",
78+
"xpack.management.elasticsearch.sniffing", "xpack.management.elasticsearch.ssl.certificate_authority",
79+
"xpack.management.elasticsearch.ssl.ca_trusted_fingerprint", "xpack.management.elasticsearch.ssl.verification_mode",
80+
"xpack.management.elasticsearch.ssl.truststore.path", "xpack.management.elasticsearch.ssl.truststore.password",
81+
"xpack.management.elasticsearch.ssl.keystore.path", "xpack.management.elasticsearch.ssl.keystore.password",
82+
"xpack.management.elasticsearch.ssl.certificate", "xpack.management.elasticsearch.ssl.key",
83+
"xpack.management.elasticsearch.ssl.cipher_suites", "xpack.geoip.download.endpoint",
84+
"xpack.geoip.downloader.enabled"
85+
};
86+
87+
for (String configName : allowedConfigs) {
88+
String normalizedKey = normalizeKey(configName);
89+
map.put(normalizedKey, configName);
90+
}
91+
return map;
92+
}
93+
94+
public String findCanonicalSetting(String envVarName) {
95+
String normalized = normalizeKey(envVarName);
96+
return normalizedToCanonical.get(normalized);
97+
}
98+
}
99+
100+
private static String normalizeKey(String key) {
101+
return key.toLowerCase()
102+
.replace(".", "")
103+
.replace("_", "");
104+
}
105+
106+
public static void main(String[] args) {
107+
if (args.length != 1) {
108+
System.err.println("usage: env2yaml FILENAME");
109+
System.exit(1);
110+
}
111+
112+
try {
113+
String configPath = args[0];
114+
new Env2Yaml().processConfigFile(configPath);
115+
} catch (Exception e) {
116+
System.err.println("error: " + e.getMessage());
117+
System.exit(1);
118+
}
119+
}
120+
121+
private void processConfigFile(String configPath) throws Exception {
122+
Path fileLocation = Paths.get(configPath);
123+
Map<String, Object> configData = loadExistingConfig(fileLocation);
124+
125+
SettingValidator validator = new SettingValidator();
126+
boolean addedNewConfigs = incorporateEnvironmentVars(configData, validator);
127+
128+
if (addedNewConfigs) {
129+
saveUpdatedConfig(fileLocation, configData);
130+
}
131+
}
132+
133+
@SuppressWarnings("unchecked")
134+
private Map<String, Object> loadExistingConfig(Path fileLocation) throws Exception {
135+
if (!Files.exists(fileLocation)) {
136+
return new TreeMap<>();
137+
}
138+
LoadSettings loadSettings = LoadSettings.builder().build();
139+
Load loader = new Load(loadSettings);
140+
try (InputStream fileInput = Files.newInputStream(fileLocation)) {
141+
Object parsedData = loader.loadFromInputStream(fileInput);
142+
if (parsedData instanceof Map) {
143+
// Convert to TreeMap to ensure alphabetical ordering like Go version
144+
return new TreeMap<>((Map<String, Object>) parsedData);
145+
}
146+
return new TreeMap<>();
147+
}
148+
}
149+
150+
private boolean incorporateEnvironmentVars(Map<String, Object> configData, SettingValidator validator) {
151+
boolean addedNewConfigs = false;
152+
153+
for (Map.Entry<String, String> envEntry : System.getenv().entrySet()) {
154+
String envVarName = envEntry.getKey();
155+
String envValue = envEntry.getValue();
156+
String canonicalSetting = validator.findCanonicalSetting(envVarName);
157+
158+
if (canonicalSetting != null) {
159+
addedNewConfigs = true;
160+
System.err.println(LocalDateTime.now().format(TIMESTAMP_FORMAT) + " Setting '" + canonicalSetting + "' from environment.");
161+
configData.put(canonicalSetting, "${" + envVarName + "}");
162+
}
163+
}
164+
165+
return addedNewConfigs;
166+
}
167+
168+
private void saveUpdatedConfig(Path fileLocation, Map<String, Object> configData) throws Exception {
169+
// Configure YAML output to match Go version formatting (block style)
170+
DumpSettings dumpSettings = DumpSettings.builder()
171+
.setDefaultFlowStyle(FlowStyle.BLOCK)
172+
.setDefaultScalarStyle(ScalarStyle.PLAIN)
173+
.setIndent(2)
174+
.build();
175+
Dump dumper = new Dump(dumpSettings);
176+
String yamlOutput = dumper.dumpToString(configData);
177+
// Remove quotes (single or double) around ${VAR} to match Go behavior
178+
// https://github.com/snakeyaml/snakeyaml-engine/blob/2070eb4e3d23bb1d81097875526a071003067877/src/main/java/org/snakeyaml/engine/v2/emitter/Emitter.java#L969-L996
179+
yamlOutput = yamlOutput.replaceAll("(['\"])(\\$\\{[^}]+\\})\\1", "$2");
180+
Set<PosixFilePermission> existingPermissions = getFilePermissions(fileLocation);
181+
182+
Files.write(fileLocation, yamlOutput.getBytes(StandardCharsets.UTF_8), StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING);
183+
184+
applyFilePermissions(fileLocation, existingPermissions);
185+
}
186+
187+
private Set<PosixFilePermission> getFilePermissions(Path fileLocation) {
188+
try {
189+
return Files.getPosixFilePermissions(fileLocation);
190+
} catch (UnsupportedOperationException e) {
191+
return null;
192+
} catch (Exception e) {
193+
return null;
194+
}
195+
}
196+
197+
private void applyFilePermissions(Path fileLocation, Set<PosixFilePermission> existingPermissions) {
198+
if (existingPermissions != null) {
199+
try {
200+
Files.setPosixFilePermissions(fileLocation, existingPermissions);
201+
} catch (Exception e) {
202+
// Ignore failures
203+
}
204+
}
205+
}
206+
}

docker/ironbank/go/src/env2yaml/go.mod

Lines changed: 0 additions & 5 deletions
This file was deleted.

docker/ironbank/go/src/env2yaml/go.sum

Lines changed: 0 additions & 3 deletions
This file was deleted.

0 commit comments

Comments
 (0)