Update opensearch to 2.9.0 in matrix integration tests (#16294)

* Update opensearch to 2.9.0 in matrix integration tests

* Update datanode to 2.9.0 opensearch

* workaround for opensearch number_of_replicas in single-node mode

* trigger the fix of replica numbers in opensearch only for 2.9.0 and higher releases

* better hostname handling for DatanodeClusterIT

* log all request/responses for DatanodeClusterIT

* add response logging to DatanodeClusterIT

* print also logs from node B in DatanodeClusterIT in case of error

* fixed DatanodeClusterIT, cached datanode images, synchronized maven packaging

* better handling of DatanodeClusterIT waiting for cluster status

* code cleanup

* log last recorded exception in case of failed waiting for datanode opensearch

* log state machine transitions

* increase waiting timeout for DatanodeClusterIT

* print shard info in case of datanode cluster failure

* safer shard info debug in DatanodeOpensearchWait

* disable scluster.routing.allocation.disk.threshold for datanode integration tests (causes problems in github actions)

---------

Co-authored-by: Dennis Oelkers <dennis@graylog.com>
This commit is contained in:
Tomas Dvorak
2023-09-08 14:12:58 +02:00
committed by GitHub
parent cda0d12d90
commit aa1d69505f
19 changed files with 425 additions and 83 deletions

View File

@@ -0,0 +1,172 @@
/*
* Copyright (C) 2020 Graylog, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the Server Side Public License, version 1,
* as published by MongoDB, Inc.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* Server Side Public License for more details.
*
* You should have received a copy of the Server Side Public License
* along with this program. If not, see
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
package org.graylog.datanode;
import com.github.rholder.retry.Attempt;
import com.github.rholder.retry.RetryException;
import com.github.rholder.retry.RetryListener;
import com.github.rholder.retry.Retryer;
import com.github.rholder.retry.RetryerBuilder;
import com.github.rholder.retry.StopStrategies;
import com.github.rholder.retry.WaitStrategies;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import io.restassured.response.ExtractableResponse;
import io.restassured.response.Response;
import io.restassured.response.ValidatableResponse;
import io.restassured.specification.RequestSpecification;
import org.apache.http.NoHttpResponseException;
import org.graylog.datanode.integration.DatanodeSecuritySetupIT;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.SSLHandshakeException;
import java.net.SocketException;
import java.security.KeyStore;
import java.util.Locale;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
public class DatanodeOpensearchWait {
private static final Logger LOG = LoggerFactory.getLogger(DatanodeSecuritySetupIT.class);
private static final int ATTEMPTS_COUNT = 160;
private final int opensearchPort;
private KeyStore truststore;
private String username;
private String password;
private String lastRecordedResponse;
private DatanodeOpensearchWait(int opensearchPort) {
this.opensearchPort = opensearchPort;
}
public static DatanodeOpensearchWait onPort(int opensearchPort) {
return new DatanodeOpensearchWait(opensearchPort);
}
public DatanodeOpensearchWait withTruststore(KeyStore truststore) {
this.truststore = truststore;
return this;
}
public DatanodeOpensearchWait withBasicAuth(String username, String password) {
this.username = username;
this.password = password;
return this;
}
public ValidatableResponse waitForGreenStatus() throws ExecutionException, RetryException {
return waitForOpensearch(
input -> !input.extract().body().path("status").equals("green"));
}
public ValidatableResponse waitForNodesCount(int countOfNodes) throws ExecutionException, RetryException {
return waitForOpensearch(
input -> !input.extract().body().path("discovered_cluster_manager").equals(true),
input -> !input.extract().body().path("status").equals("green"),
input -> !input.extract().body().path("number_of_nodes").equals(countOfNodes)
);
}
@SafeVarargs
private ValidatableResponse waitForOpensearch(Predicate<ValidatableResponse>... predicates) throws ExecutionException, RetryException {
//noinspection UnstableApiUsage
final RetryerBuilder<ValidatableResponse> builder = RetryerBuilder.<ValidatableResponse>newBuilder()
.withWaitStrategy(WaitStrategies.fixedWait(1, TimeUnit.SECONDS))
.withStopStrategy(StopStrategies.stopAfterAttempt(ATTEMPTS_COUNT))
.retryIfException(input -> input instanceof NoHttpResponseException)
.retryIfException(input -> input instanceof SocketException)
.retryIfException(input -> input instanceof SSLHandshakeException) // may happen before SSL is configured properly
.retryIfResult(input -> !input.extract().contentType().startsWith("application/json"))
.withRetryListener(new RetryListener() {
@Override
public <V> void onRetry(Attempt<V> attempt) {
if (attempt.hasResult() && attempt.getResult() instanceof ValidatableResponse response) {
lastRecordedResponse = serializeResponse(response.extract());
} else if (attempt.getExceptionCause() != null) {
lastRecordedResponse = attempt.getExceptionCause().getMessage();
}
}
});
for (Predicate<ValidatableResponse> predicate : predicates) {
builder.retryIfResult(predicate::test);
}
final Retryer<ValidatableResponse> retryer = builder
.build();
try {
return retryer.call(() -> {
final RequestSpecification req = RestAssured.given()
.accept(ContentType.JSON);
Optional.ofNullable(truststore).ifPresent(ts -> req.trustStore(truststore));
if (username != null && password != null) {
req.auth().basic(username, password);
}
return req.get(formatClusterHeathUrl())
.then();
});
} catch (ExecutionException | RetryException e) {
if (lastRecordedResponse != null) {
LOG.warn("Last recorded opensearch response, waiting for {}: {}", formatClusterHeathUrl(), lastRecordedResponse);
}
printShardsInfo();
throw e;
}
}
private void printShardsInfo() {
try {
final RequestSpecification req = RestAssured.given()
.accept(ContentType.JSON);
Optional.ofNullable(truststore).ifPresent(ts -> req.trustStore(truststore));
if (username != null && password != null) {
req.auth().basic(username, password);
}
final String shardsResponse = req.get("https://localhost:" + opensearchPort + "/_cat/shards?v=true&h=index,shard,prirep,state,node,unassigned.reason&s=state")
.then().extract().body().asString();
LOG.warn("Cluster shards status: {}", shardsResponse);
} catch (Exception e) {
LOG.warn("Failed to obtain shard info", e);
}
}
@NotNull
private String formatClusterHeathUrl() {
return "https://localhost:" + opensearchPort + "/_cluster/health";
}
private String serializeResponse(ExtractableResponse<Response> extract) {
final String body = extract.body().asString();
final String contentType = extract.contentType();
final String headers = extract.headers().toString();
return String.format(Locale.ROOT, "\nContent-type: %s\nheaders: %s\nbody: %s", contentType, headers, body);
}
}

View File

@@ -17,14 +17,8 @@
package org.graylog.datanode.integration;
import com.github.rholder.retry.RetryException;
import com.github.rholder.retry.Retryer;
import com.github.rholder.retry.RetryerBuilder;
import com.github.rholder.retry.StopStrategies;
import com.github.rholder.retry.WaitStrategies;
import io.restassured.RestAssured;
import io.restassured.response.ValidatableResponse;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.http.NoHttpResponseException;
import org.graylog.datanode.DatanodeOpensearchWait;
import org.graylog.datanode.configuration.variants.KeystoreInformation;
import org.graylog.datanode.testinfra.DatanodeContainerizedBackend;
import org.junit.jupiter.api.AfterEach;
@@ -38,12 +32,11 @@ import org.testcontainers.containers.Network;
import javax.validation.constraints.NotNull;
import java.io.IOException;
import java.net.SocketException;
import java.nio.file.Path;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import static org.graylog.datanode.testinfra.DatanodeContainerizedBackend.IMAGE_WORKING_DIR;
@@ -57,9 +50,11 @@ public class DatanodeClusterIT {
static Path tempDir;
private String hostnameNodeA;
private KeyStore trustStore;
private String usernameNodeA;
private String passwordNodeA;
private String initialAdminUsername;
private String initialAdminPassword;
private KeystoreInformation ca;
private Network network;
private GenericContainer<?> mongodbContainer;
@BeforeEach
void setUp() throws GeneralSecurityException, IOException {
@@ -70,45 +65,49 @@ public class DatanodeClusterIT {
trustStore = DatanodeSecurityTestUtils.buildTruststore(ca);
hostnameNodeA = "graylog-datanode-host-" + RandomStringUtils.random(8, "0123456789abcdef");
final KeystoreInformation transportNodeA = DatanodeSecurityTestUtils.generateTransportCert(tempDir, ca, "nodeA");
final KeystoreInformation httpNodeA = DatanodeSecurityTestUtils.generateHttpCert(tempDir, ca, "nodeA");
usernameNodeA = RandomStringUtils.randomAlphabetic(10);
passwordNodeA = RandomStringUtils.randomAlphabetic(10);
final KeystoreInformation transportNodeA = DatanodeSecurityTestUtils.generateTransportCert(tempDir, ca, hostnameNodeA);
final KeystoreInformation httpNodeA = DatanodeSecurityTestUtils.generateHttpCert(tempDir, ca, hostnameNodeA);
initialAdminUsername = RandomStringUtils.randomAlphabetic(10);
initialAdminPassword = RandomStringUtils.randomAlphabetic(10);
this.network = Network.newNetwork();
this.mongodbContainer = DatanodeContainerizedBackend.createMongodbContainer(network);
this.mongodbContainer.start();
final Network network = Network.newNetwork();
final GenericContainer<?> mongodb = DatanodeContainerizedBackend.createMongodbContainer(network);
nodeA = createDatanodeContainer(
network,
mongodb,
mongodbContainer,
hostnameNodeA,
transportNodeA,
httpNodeA,
usernameNodeA,
passwordNodeA
).start();
initialAdminUsername,
initialAdminPassword
);
final String hostnameNodeB = "graylog-datanode-host-" + RandomStringUtils.random(8, "0123456789abcdef");
final KeystoreInformation transportNodeB = DatanodeSecurityTestUtils.generateTransportCert(tempDir, ca, "nodeB");
final KeystoreInformation httpNodeB = DatanodeSecurityTestUtils.generateHttpCert(tempDir, ca, "nodeB");
final String usernameNodeB = RandomStringUtils.randomAlphabetic(10);
final String passwordNodeB = RandomStringUtils.randomAlphabetic(10);
final KeystoreInformation transportNodeB = DatanodeSecurityTestUtils.generateTransportCert(tempDir, ca, hostnameNodeB);
final KeystoreInformation httpNodeB = DatanodeSecurityTestUtils.generateHttpCert(tempDir, ca, hostnameNodeB);
nodeB = createDatanodeContainer(
network,
mongodb,
mongodbContainer,
hostnameNodeB,
transportNodeB,
httpNodeB,
usernameNodeB,
passwordNodeB
).start();
initialAdminUsername,
initialAdminPassword
);
Stream.of(nodeA, nodeB).parallel().forEach(DatanodeContainerizedBackend::start);
}
@AfterEach
void tearDown() {
nodeB.stop();
nodeA.stop();
mongodbContainer.stop();
network.close();
}
@Test
@@ -120,18 +119,16 @@ public class DatanodeClusterIT {
void testAddingNodeToExistingCluster() throws ExecutionException, RetryException {
final String hostnameNodeC = "graylog-datanode-host-" + RandomStringUtils.random(8, "0123456789abcdef");
final KeystoreInformation transportNodeC = DatanodeSecurityTestUtils.generateTransportCert(tempDir, ca, "nodeB");
final KeystoreInformation httpNodeC = DatanodeSecurityTestUtils.generateHttpCert(tempDir, ca, "nodeB");
final String usernameNodeC = RandomStringUtils.randomAlphabetic(10);
final String passwordNodeC = RandomStringUtils.randomAlphabetic(10);
final KeystoreInformation transportNodeC = DatanodeSecurityTestUtils.generateTransportCert(tempDir, ca, hostnameNodeC);
final KeystoreInformation httpNodeC = DatanodeSecurityTestUtils.generateHttpCert(tempDir, ca, hostnameNodeC);
final DatanodeContainerizedBackend nodeC = createDatanodeContainer(
nodeA.getNetwork(), nodeA.getMongodbContainer(),
hostnameNodeC,
transportNodeC,
httpNodeC,
usernameNodeC,
passwordNodeC
initialAdminUsername,
initialAdminPassword
);
nodeC.start();
@@ -179,25 +176,13 @@ public class DatanodeClusterIT {
private void waitForNodesCount(int countOfNodes) throws ExecutionException, RetryException {
try {
final Retryer<ValidatableResponse> retryer = RetryerBuilder.<ValidatableResponse>newBuilder()
.withWaitStrategy(WaitStrategies.fixedWait(1, TimeUnit.SECONDS))
.withStopStrategy(StopStrategies.stopAfterAttempt(120))
.retryIfException(input -> input instanceof NoHttpResponseException)
.retryIfException(input -> input instanceof SocketException)
.retryIfResult(input -> !input.extract().body().path("number_of_nodes").equals(countOfNodes))
.retryIfResult(input -> !input.extract().body().path("status").equals("green"))
.retryIfResult(input -> !input.extract().body().path("discovered_cluster_manager").equals(true))
.build();
retryer.call(() -> {
Integer mappedPort = nodeA.getOpensearchRestPort();
return RestAssured.given()
.trustStore(trustStore)
.auth().basic(usernameNodeA, passwordNodeA)
.get("https://localhost:" + mappedPort + "/_cluster/health")
.then();
});
DatanodeOpensearchWait.onPort(nodeA.getOpensearchRestPort())
.withTruststore(trustStore)
.withBasicAuth(initialAdminUsername, initialAdminPassword)
.waitForNodesCount(countOfNodes);
} catch (Exception retryException) {
LOG.error("DataNode Container logs follow:\n" + nodeA.getLogs());
LOG.error("DataNode Container logs from nodeA follow:\n" + nodeA.getLogs());
LOG.error("DataNode Container logs from node B follow:\n" + nodeB.getLogs());
throw retryException;
}
}

View File

@@ -37,6 +37,7 @@ import org.junit.jupiter.api.io.TempDir;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.SSLHandshakeException;
import java.io.IOException;
import java.net.SocketException;
import java.nio.file.Path;
@@ -137,6 +138,8 @@ public class DatanodeSecuritySetupIT {
.withStopStrategy(StopStrategies.stopAfterAttempt(120))
.retryIfException(input -> input instanceof NoHttpResponseException)
.retryIfException(input -> input instanceof SocketException)
.retryIfException(input -> input instanceof SSLHandshakeException) // may happen before SSL is configured properly
.retryIfResult(input -> !input.extract().contentType().startsWith("application/json"))
.retryIfResult(input -> !input.extract().body().path("opensearch.node.state").equals("AVAILABLE"))
.build();

View File

@@ -28,7 +28,7 @@ class ProcessWatchdogTracerTest {
void testLifecycle() {
final TestableProcess process = new TestableProcess();
final ProcessWatchdog watchdog = new ProcessWatchdog(process, 3);
process.setStateMachineTracer(watchdog);
process.addStateMachineTracer(watchdog);
process.restart();
// both process and watchdog are running now. Let's stop the process and see if the watchdog will restart it

View File

@@ -57,7 +57,7 @@ public class TestableProcess implements ManagableProcess<String> {
}
@Override
public void setStateMachineTracer(StateMachineTracer stateMachineTracer) {
public void addStateMachineTracer(StateMachineTracer stateMachineTracer) {
stateMachine.setTrace(stateMachineTracer);
}

View File

@@ -16,11 +16,14 @@
*/
package org.graylog.datanode.testinfra;
import com.google.common.base.Suppliers;
import org.graylog.datanode.OpensearchDistribution;
import org.graylog.testing.completebackend.DefaultMavenProjectDirProvider;
import org.graylog.testing.completebackend.DefaultPluginJarsProvider;
import org.graylog.testing.containermatrix.MongodbServer;
import org.graylog.testing.graylognode.MavenPackager;
import org.graylog.testing.graylognode.NodeContainerConfig;
import org.graylog.testing.mongodb.MongoDBContainer;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.Network;
import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy;
@@ -32,6 +35,7 @@ import java.nio.file.Path;
import java.time.Duration;
import java.util.Collections;
import java.util.Properties;
import java.util.function.Supplier;
public class DatanodeContainerizedBackend {
@@ -39,9 +43,13 @@ public class DatanodeContainerizedBackend {
public static final int DATANODE_OPENSEARCH_PORT = 9200;
public static final String IMAGE_WORKING_DIR = "/usr/share/graylog/datanode";
private final Network network;
private boolean shouldCloseNetwork = false;
private final GenericContainer<?> mongodbContainer;
private boolean shouldCloseMongodb = false;
private final GenericContainer<?> datanodeContainer;
private static final Supplier<ImageFromDockerfile> imageSupplier = Suppliers.memoize(DatanodeContainerizedBackend::createImage);
public DatanodeContainerizedBackend() {
this(new DatanodeDockerHooksAdapter());
}
@@ -51,11 +59,17 @@ public class DatanodeContainerizedBackend {
}
public DatanodeContainerizedBackend(final String nodeName, DatanodeDockerHooks hooks) {
this.network = Network.newNetwork();
this.mongodbContainer = createMongodbContainer(this.network);
// we have created these resources, we have to close them.
this.shouldCloseNetwork = true;
this.shouldCloseMongodb = true;
this.datanodeContainer = createDatanodeContainer(
nodeName,
hooks, createDockerImageFile(getOpensearchVersion()),
hooks,
getDatanodeVersion());
}
@@ -65,15 +79,13 @@ public class DatanodeContainerizedBackend {
this.datanodeContainer = createDatanodeContainer(
nodeName,
hooks,
createDockerImageFile(getOpensearchVersion()),
getDatanodeVersion());
}
private GenericContainer<?> createDatanodeContainer(String nodeName, DatanodeDockerHooks customizer, ImageFromDockerfile image, String datanodeVersion) {
private GenericContainer<?> createDatanodeContainer(String nodeName, DatanodeDockerHooks customizer, String datanodeVersion) {
MavenPackager.packageJarIfNecessary(createConfig());
GenericContainer<?> container = new GenericContainer<>(image)
GenericContainer<?> container = new GenericContainer<>(imageSupplier.get())
.withExposedPorts(DATANODE_REST_PORT, DATANODE_OPENSEARCH_PORT)
.withNetwork(network)
@@ -98,6 +110,11 @@ public class DatanodeContainerizedBackend {
.withEnv("GRAYLOG_DATANODE_NODE_ID_FILE", "./node-id")
.withEnv("GRAYLOG_DATANODE_HTTP_BIND_ADDRESS", "0.0.0.0:" + DATANODE_REST_PORT)
// disable disk threshold in tests, it causes problems in github builds where we don't have
// enough free space
.withEnv("opensearch.cluster.routing.allocation.disk.threshold_enabled", "false")
.withNetworkAliases(nodeName)
.dependsOn(mongodbContainer)
.waitingFor(new LogMessageWaitStrategy()
@@ -113,8 +130,8 @@ public class DatanodeContainerizedBackend {
return new NodeContainerConfig(this.network, this.mongodbContainer.getHost(), null, null, new int[]{}, new DefaultPluginJarsProvider(),new DefaultMavenProjectDirProvider(), Collections.emptyList());
}
private static ImageFromDockerfile createDockerImageFile(String opensearchVersion) {
final String opensearchTarArchive = "opensearch-" + opensearchVersion + "-linux-" + OpensearchDistribution.archCode(System.getProperty("os.arch")) + ".tar.gz";
private static ImageFromDockerfile createImage() {
final String opensearchTarArchive = "opensearch-" + getOpensearchVersion() + "-linux-" + OpensearchDistribution.archCode(System.getProperty("os.arch")) + ".tar.gz";
final Path downloadedOpensearch = Path.of("target", "downloads", opensearchTarArchive);
if(!Files.exists(downloadedOpensearch)) {
@@ -145,23 +162,23 @@ public class DatanodeContainerizedBackend {
}
public static GenericContainer<?> createMongodbContainer(Network network) {
return new GenericContainer<>("mongo:5.0")
.withNetwork(network)
.withNetworkAliases("mongodb")
.withExposedPorts(27017);
return MongoDBContainer.create(MongodbServer.MONGO5, network);
}
public DatanodeContainerizedBackend start() {
mongodbContainer.start();
datanodeContainer.start();
return this;
}
public void stop() {
datanodeContainer.stop();
mongodbContainer.stop();
network.close();
if (shouldCloseMongodb) {
mongodbContainer.stop();
}
if (shouldCloseNetwork) {
network.close();
}
}
public Network getNetwork() {
@@ -195,10 +212,10 @@ public class DatanodeContainerizedBackend {
}
}
private String getOpensearchVersion() {
private static String getOpensearchVersion() {
try {
final Properties props = new Properties();
props.load(getClass().getResourceAsStream("/opensearch.properties"));
props.load(DatanodeContainerizedBackend.class.getResourceAsStream("/opensearch.properties"));
return props.getProperty("opensearchVersion");
} catch (IOException e) {
throw new RuntimeException(e);