Skip to content

Commit 157b80b

Browse files
Provide a mechanism to modify config files in a running test cluster (#117859) (#117932)
Co-authored-by: Elastic Machine <[email protected]>
1 parent 51f1bc0 commit 157b80b

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;
@@ -115,6 +117,9 @@ public static class Node {
115117
private Version currentVersion;
116118
private Process process = null;
117119
private DistributionDescriptor distributionDescriptor;
120+
private Set<String> extraConfigListeners = new HashSet<>();
121+
private Set<String> keystoreFileListeners = new HashSet<>();
122+
private Set<Resource> roleFileListeners = new HashSet<>();
118123

119124
public Node(Path baseWorkingDir, DistributionResolver distributionResolver, LocalNodeSpec spec) {
120125
this(baseWorkingDir, distributionResolver, spec, null, false);
@@ -436,6 +441,10 @@ private void writeConfiguration() {
436441

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

@@ -485,29 +502,39 @@ private void addKeystoreSettings() {
485502

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

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

630+
private void writeRolesFile() {
631+
Path destination = workingDir.resolve("config").resolve("roles.yml");
632+
spec.getRolesFiles().forEach(rolesFile -> {
633+
try (
634+
Writer writer = Files.newBufferedWriter(destination, StandardOpenOption.APPEND);
635+
Reader reader = new BufferedReader(new InputStreamReader(rolesFile.asStream()))
636+
) {
637+
reader.transferTo(writer);
638+
} catch (IOException e) {
639+
throw new UncheckedIOException("Failed to append roles file " + rolesFile + " to " + destination, e);
640+
}
641+
});
642+
}
643+
599644
private void installPlugins() {
600645
if (spec.getPlugins().isEmpty() == false) {
601646
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)