Skip to content

Commit 76673b4

Browse files
authored
Provide a mechanism to modify config files in a running test cluster (#117859) (#117931)
1 parent 417f3bb commit 76673b4

File tree

3 files changed

+128
-30
lines changed

3 files changed

+128
-30
lines changed

test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/AbstractLocalClusterFactory.java

Lines changed: 74 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
import org.elasticsearch.test.cluster.util.ProcessUtils;
2727
import org.elasticsearch.test.cluster.util.Retry;
2828
import org.elasticsearch.test.cluster.util.Version;
29+
import org.elasticsearch.test.cluster.util.resource.MutableResource;
30+
import org.elasticsearch.test.cluster.util.resource.Resource;
2931

3032
import java.io.BufferedInputStream;
3133
import java.io.BufferedReader;
@@ -114,6 +116,9 @@ public static class Node {
114116
private Version currentVersion;
115117
private Process process = null;
116118
private DistributionDescriptor distributionDescriptor;
119+
private Set<String> extraConfigListeners = new HashSet<>();
120+
private Set<String> keystoreFileListeners = new HashSet<>();
121+
private Set<Resource> roleFileListeners = new HashSet<>();
117122

118123
public Node(Path baseWorkingDir, DistributionResolver distributionResolver, LocalNodeSpec spec) {
119124
this(baseWorkingDir, distributionResolver, spec, null, false);
@@ -435,6 +440,10 @@ private void writeConfiguration() {
435440

436441
private void copyExtraConfigFiles() {
437442
spec.getExtraConfigFiles().forEach((fileName, resource) -> {
443+
if (fileName.equals("roles.yml")) {
444+
throw new IllegalArgumentException("Security roles should be configured via 'rolesFile()' method.");
445+
}
446+
438447
final Path target = configDir.resolve(fileName);
439448
final Path directory = target.getParent();
440449
if (Files.exists(directory) == false) {
@@ -445,6 +454,14 @@ private void copyExtraConfigFiles() {
445454
}
446455
}
447456
resource.writeTo(target);
457+
458+
// Register and update listener for this config file
459+
if (resource instanceof MutableResource && extraConfigListeners.add(fileName)) {
460+
((MutableResource) resource).addUpdateListener(updated -> {
461+
LOGGER.info("Updating config file '{}'", fileName);
462+
updated.writeTo(target);
463+
});
464+
}
448465
});
449466
}
450467

@@ -483,29 +500,39 @@ private void addKeystoreSettings() {
483500

484501
private void addKeystoreFiles() {
485502
spec.getKeystoreFiles().forEach((key, file) -> {
486-
try {
487-
Path path = Files.createTempFile(tempDir, key, null);
488-
file.writeTo(path);
489-
490-
ProcessUtils.exec(
491-
spec.getKeystorePassword(),
492-
workingDir,
493-
OS.conditional(
494-
c -> c.onWindows(() -> distributionDir.resolve("bin").resolve("elasticsearch-keystore.bat"))
495-
.onUnix(() -> distributionDir.resolve("bin").resolve("elasticsearch-keystore"))
496-
),
497-
getEnvironmentVariables(),
498-
false,
499-
"add-file",
500-
key,
501-
path.toString()
502-
).waitFor();
503-
} catch (InterruptedException | IOException e) {
504-
throw new RuntimeException(e);
503+
addKeystoreFile(key, file);
504+
if (file instanceof MutableResource && keystoreFileListeners.add(key)) {
505+
((MutableResource) file).addUpdateListener(updated -> {
506+
LOGGER.info("Updating keystore file '{}'", key);
507+
addKeystoreFile(key, updated);
508+
});
505509
}
506510
});
507511
}
508512

513+
private void addKeystoreFile(String key, Resource file) {
514+
try {
515+
Path path = Files.createTempFile(tempDir, key, null);
516+
file.writeTo(path);
517+
518+
ProcessUtils.exec(
519+
spec.getKeystorePassword(),
520+
workingDir,
521+
OS.conditional(
522+
c -> c.onWindows(() -> distributionDir.resolve("bin").resolve("elasticsearch-keystore.bat"))
523+
.onUnix(() -> distributionDir.resolve("bin").resolve("elasticsearch-keystore"))
524+
),
525+
getEnvironmentVariables(),
526+
false,
527+
"add-file",
528+
key,
529+
path.toString()
530+
).waitFor();
531+
} catch (InterruptedException | IOException e) {
532+
throw new RuntimeException(e);
533+
}
534+
}
535+
509536
private void writeSecureSecretsFile() {
510537
if (spec.getKeystoreFiles().isEmpty() == false) {
511538
throw new IllegalStateException(
@@ -533,16 +560,20 @@ private void configureSecurity() {
533560
if (spec.isSecurityEnabled()) {
534561
if (spec.getUsers().isEmpty() == false) {
535562
LOGGER.info("Setting up roles.yml for node '{}'", name);
536-
537-
Path destination = workingDir.resolve("config").resolve("roles.yml");
538-
spec.getRolesFiles().forEach(rolesFile -> {
539-
try (
540-
Writer writer = Files.newBufferedWriter(destination, StandardOpenOption.APPEND);
541-
Reader reader = new BufferedReader(new InputStreamReader(rolesFile.asStream()))
542-
) {
543-
reader.transferTo(writer);
544-
} catch (IOException e) {
545-
throw new UncheckedIOException("Failed to append roles file " + rolesFile + " to " + destination, e);
563+
writeRolesFile();
564+
spec.getRolesFiles().forEach(resource -> {
565+
if (resource instanceof MutableResource && roleFileListeners.add(resource)) {
566+
((MutableResource) resource).addUpdateListener(updated -> {
567+
LOGGER.info("Updating roles.yml for node '{}'", name);
568+
Path rolesFile = workingDir.resolve("config").resolve("roles.yml");
569+
try {
570+
Files.delete(rolesFile);
571+
Files.copy(distributionDir.resolve("config").resolve("roles.yml"), rolesFile);
572+
writeRolesFile();
573+
} catch (IOException e) {
574+
throw new UncheckedIOException(e);
575+
}
576+
});
546577
}
547578
});
548579
}
@@ -594,6 +625,20 @@ private void configureSecurity() {
594625
}
595626
}
596627

628+
private void writeRolesFile() {
629+
Path destination = workingDir.resolve("config").resolve("roles.yml");
630+
spec.getRolesFiles().forEach(rolesFile -> {
631+
try (
632+
Writer writer = Files.newBufferedWriter(destination, StandardOpenOption.APPEND);
633+
Reader reader = new BufferedReader(new InputStreamReader(rolesFile.asStream()))
634+
) {
635+
reader.transferTo(writer);
636+
} catch (IOException e) {
637+
throw new UncheckedIOException("Failed to append roles file " + rolesFile + " to " + destination, e);
638+
}
639+
});
640+
}
641+
597642
private void installPlugins() {
598643
if (spec.getPlugins().isEmpty() == false) {
599644
Pattern pattern = Pattern.compile("(.+)(?:-\\d+\\.\\d+\\.\\d+(-SNAPSHOT)?\\.zip)");
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
package org.elasticsearch.test.cluster.util.resource;
11+
12+
import java.io.InputStream;
13+
import java.util.ArrayList;
14+
import java.util.List;
15+
import java.util.function.Consumer;
16+
17+
/**
18+
* A mutable version of {@link Resource}. Anywhere a {@link Resource} is accepted in the test clusters API a {@link MutableResource} can
19+
* be supplied instead. Unless otherwise specified, when the {@link #update(Resource)} method is called, the backing configuration will
20+
* be updated in-place.
21+
*/
22+
public class MutableResource implements Resource {
23+
private final List<Consumer<? super Resource>> listeners = new ArrayList<>();
24+
private Resource delegate;
25+
26+
private MutableResource(Resource delegate) {
27+
this.delegate = delegate;
28+
}
29+
30+
@Override
31+
public InputStream asStream() {
32+
return delegate.asStream();
33+
}
34+
35+
public static MutableResource from(Resource delegate) {
36+
return new MutableResource(delegate);
37+
}
38+
39+
public void update(Resource delegate) {
40+
this.delegate = delegate;
41+
this.listeners.forEach(listener -> listener.accept(this));
42+
}
43+
44+
/**
45+
* Registers a listener that will be notified when any updates are made to this resource. This listener will receive a reference to
46+
* the resource with the updated value.
47+
*
48+
* @param listener action to be called on update
49+
*/
50+
public synchronized void addUpdateListener(Consumer<? super Resource> listener) {
51+
listeners.add(listener);
52+
}
53+
}

x-pack/plugin/eql/qa/security/src/javaRestTest/java/org/elasticsearch/xpack/eql/EqlSecurityTestCluster.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public static ElasticsearchCluster getCluster() {
1919
.setting("xpack.license.self_generated.type", "basic")
2020
.setting("xpack.monitoring.collection.enabled", "true")
2121
.setting("xpack.security.enabled", "true")
22-
.configFile("roles.yml", Resource.fromClasspath("roles.yml"))
22+
.rolesFile(Resource.fromClasspath("roles.yml"))
2323
.user("test-admin", "x-pack-test-password", "test-admin", false)
2424
.user("user1", "x-pack-test-password", "user1", false)
2525
.user("user2", "x-pack-test-password", "user2", false)

0 commit comments

Comments
 (0)