From 484d72d7bde1d67996a2b71e910b880541d0f919 Mon Sep 17 00:00:00 2001 From: Nils Bandener Date: Tue, 26 Jul 2022 16:17:53 +0200 Subject: [PATCH 01/16] New test framework Signed-Off-By: Nils Bandener --- build.gradle | 43 +- .../security/support/PemKeyReader.java | 20 +- .../org/opensearch/node/PluginAwareNode.java | 49 ++ .../test/AbstractIntegrationTest.java | 36 ++ .../test/GenericIntegrationTest.java | 79 +++ .../test/PrivilegesEvaluatorTest.java | 71 ++ .../opensearch/test/SecurityRolesTests.java | 77 +++ .../opensearch/test/framework/TestIndex.java | 87 +++ .../test/framework/TestSecurityConfig.java | 609 +++++++++++++++++ .../framework/certificate/Certificates.java | 165 +++++ .../certificate/TestCertificates.java | 63 ++ .../cluster/ClusterConfiguration.java | 131 ++++ .../test/framework/cluster/ClusterInfo.java | 45 ++ .../cluster/ContextHeaderDecoratorClient.java | 60 ++ .../test/framework/cluster/LocalCluster.java | 373 +++++++++++ .../cluster/LocalOpenSearchCluster.java | 610 ++++++++++++++++++ ...inimumSecuritySettingsSupplierFactory.java | 80 +++ .../framework/cluster/NestedValueMap.java | 518 +++++++++++++++ .../cluster/NodeSettingsSupplier.java | 34 + .../cluster/OpenSearchClientProvider.java | 133 ++++ .../test/framework/cluster/PortAllocator.java | 157 +++++ .../test/framework/cluster/SocketUtils.java | 319 +++++++++ .../framework/cluster/TestRestClient.java | 366 +++++++++++ 23 files changed, 4116 insertions(+), 9 deletions(-) create mode 100644 src/newTest/java/org/opensearch/node/PluginAwareNode.java create mode 100644 src/newTest/java/org/opensearch/test/AbstractIntegrationTest.java create mode 100644 src/newTest/java/org/opensearch/test/GenericIntegrationTest.java create mode 100644 src/newTest/java/org/opensearch/test/PrivilegesEvaluatorTest.java create mode 100644 src/newTest/java/org/opensearch/test/SecurityRolesTests.java create mode 100644 src/newTest/java/org/opensearch/test/framework/TestIndex.java create mode 100644 src/newTest/java/org/opensearch/test/framework/TestSecurityConfig.java create mode 100644 src/newTest/java/org/opensearch/test/framework/certificate/Certificates.java create mode 100644 src/newTest/java/org/opensearch/test/framework/certificate/TestCertificates.java create mode 100644 src/newTest/java/org/opensearch/test/framework/cluster/ClusterConfiguration.java create mode 100644 src/newTest/java/org/opensearch/test/framework/cluster/ClusterInfo.java create mode 100644 src/newTest/java/org/opensearch/test/framework/cluster/ContextHeaderDecoratorClient.java create mode 100644 src/newTest/java/org/opensearch/test/framework/cluster/LocalCluster.java create mode 100644 src/newTest/java/org/opensearch/test/framework/cluster/LocalOpenSearchCluster.java create mode 100644 src/newTest/java/org/opensearch/test/framework/cluster/MinimumSecuritySettingsSupplierFactory.java create mode 100644 src/newTest/java/org/opensearch/test/framework/cluster/NestedValueMap.java create mode 100644 src/newTest/java/org/opensearch/test/framework/cluster/NodeSettingsSupplier.java create mode 100644 src/newTest/java/org/opensearch/test/framework/cluster/OpenSearchClientProvider.java create mode 100644 src/newTest/java/org/opensearch/test/framework/cluster/PortAllocator.java create mode 100644 src/newTest/java/org/opensearch/test/framework/cluster/SocketUtils.java create mode 100644 src/newTest/java/org/opensearch/test/framework/cluster/TestRestClient.java diff --git a/build.gradle b/build.gradle index 35c25c020c..36ad5bd553 100644 --- a/build.gradle +++ b/build.gradle @@ -224,10 +224,51 @@ configurations.all { force "io.netty:netty-common:${versions.netty}" force "io.netty:netty-handler:${versions.netty}" force "io.netty:netty-transport:${versions.netty}" - force "io.netty:netty-transport-native-unix-common:${versions.netty}" + } +} + +//create source set 'newTest' +//add classes from the main source set to the compilation and runtime classpaths of the newTest +sourceSets { + newTest { + compileClasspath += sourceSets.main.output + runtimeClasspath += sourceSets.main.output } } +//add new task that runs new tests +task newTest(type: Test) { + description = 'Run new tests.' + group = 'verification' + testClassesDirs = sourceSets.newTest.output.classesDirs + classpath = sourceSets.newTest.runtimeClasspath + + //run the newTest task after the test task + shouldRunAfter test +} + +//run the newTest task before the check task +check.dependsOn newTest + +configurations { + all { + resolutionStrategy { + force 'commons-codec:commons-codec:1.14' + force 'org.apache.santuario:xmlsec:2.2.3' + force 'org.cryptacular:cryptacular:1.2.4' + force 'net.minidev:json-smart:2.4.7' + force 'commons-cli:commons-cli:1.3.1' + force 'org.apache.httpcomponents:httpcore:4.4.12' + force "org.apache.commons:commons-lang3:3.4" + force "org.springframework:spring-core:5.3.20" + force "com.google.guava:guava:30.0-jre" + } + } + + //use testImplementation dependencies in the newTestImplementation configuration + newTestImplementation.extendsFrom testImplementation +} + dependencies { implementation 'jakarta.annotation:jakarta.annotation-api:1.3.5' implementation "org.opensearch.plugin:transport-netty4-client:${opensearch_version}" diff --git a/src/main/java/org/opensearch/security/support/PemKeyReader.java b/src/main/java/org/opensearch/security/support/PemKeyReader.java index 53eeb21736..66d1af8799 100644 --- a/src/main/java/org/opensearch/security/support/PemKeyReader.java +++ b/src/main/java/org/opensearch/security/support/PemKeyReader.java @@ -274,15 +274,19 @@ public static X509Certificate[] loadCertificatesFromFile(String file) throws Exc return null; } - CertificateFactory fact = CertificateFactory.getInstance("X.509"); try(FileInputStream is = new FileInputStream(file)) { - Collection certs = fact.generateCertificates(is); - X509Certificate[] x509Certs = new X509Certificate[certs.size()]; - int i=0; - for(Certificate cert: certs) { - x509Certs[i++] = (X509Certificate) cert; - } - return x509Certs; + return loadCertificatesFromStream(is); + } + + } + + public static X509Certificate[] loadCertificatesFromFile(File file) throws Exception { + if(file == null) { + return null; + } + + try(FileInputStream is = new FileInputStream(file)) { + return loadCertificatesFromStream(is); } } diff --git a/src/newTest/java/org/opensearch/node/PluginAwareNode.java b/src/newTest/java/org/opensearch/node/PluginAwareNode.java new file mode 100644 index 0000000000..39c93e6b8e --- /dev/null +++ b/src/newTest/java/org/opensearch/node/PluginAwareNode.java @@ -0,0 +1,49 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.node; + +import java.util.Arrays; +import java.util.Collections; + +import org.opensearch.common.settings.Settings; +import org.opensearch.plugins.Plugin; + +public class PluginAwareNode extends Node { + + private final boolean clusterManagerEligible; + + @SafeVarargs + public PluginAwareNode(boolean clusterManagerEligible, final Settings preparedSettings, final Class... plugins) { + super(InternalSettingsPreparer.prepareEnvironment(preparedSettings, Collections.emptyMap(), null, () -> System.getenv("HOSTNAME")), Arrays.asList(plugins), true); + this.clusterManagerEligible = clusterManagerEligible; + } + + + public boolean isClusterManagerEligible() { + return clusterManagerEligible; + } +} diff --git a/src/newTest/java/org/opensearch/test/AbstractIntegrationTest.java b/src/newTest/java/org/opensearch/test/AbstractIntegrationTest.java new file mode 100644 index 0000000000..3f13fe261a --- /dev/null +++ b/src/newTest/java/org/opensearch/test/AbstractIntegrationTest.java @@ -0,0 +1,36 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.test; + +import org.junit.runner.RunWith; +import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.TestSecurityConfig.Role; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; + +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class AbstractIntegrationTest { + + /** + * Auth domain with HTTPS Basic and the internal user backend + */ + protected final static TestSecurityConfig.AuthcDomain AUTHC_HTTPBASIC_INTERNAL = new TestSecurityConfig.AuthcDomain("basic", 0) + .httpAuthenticator("basic").backend("internal"); + + /** + * Admin user with full access to all indices + */ + protected final static TestSecurityConfig.User USER_ADMIN = new TestSecurityConfig.User("admin") + .roles(new Role("allaccess").indexPermissions("*").on("*").clusterPermissions("*")); + +} diff --git a/src/newTest/java/org/opensearch/test/GenericIntegrationTest.java b/src/newTest/java/org/opensearch/test/GenericIntegrationTest.java new file mode 100644 index 0000000000..cfddf84ad5 --- /dev/null +++ b/src/newTest/java/org/opensearch/test/GenericIntegrationTest.java @@ -0,0 +1,79 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.test; + +import org.apache.http.HttpStatus; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Test; +import org.opensearch.test.framework.TestIndex; +import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.TestSecurityConfig.Role; +import org.opensearch.test.framework.cluster.ClusterConfiguration; +import org.opensearch.test.framework.cluster.LocalCluster; +import org.opensearch.test.framework.cluster.TestRestClient; +import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse; + +import com.fasterxml.jackson.core.JsonPointer; + +/** + * WIP + * Generic test class that demonstrates how to use the test framework to + * set up a test cluster with users, roles, indices and data, and how to + * implement tests. One main goal here is to make tests self-contained. + */ +public class GenericIntegrationTest extends AbstractIntegrationTest { + + // define indices used in this test + private final static TestIndex INDEX_A = TestIndex.name("index-a").build(); + private final static TestIndex INDEX_B = TestIndex.name("index-b").build(); + + private final static TestSecurityConfig.User INDEX_A_USER = new TestSecurityConfig.User("index_a_user") + .roles(new Role("index_a_role").indexPermissions("*").on(INDEX_A).clusterPermissions("*")); + + + // build our test cluster as a ClassRule + @ClassRule + public static LocalCluster cluster = new LocalCluster.Builder().clusterConfiguration(ClusterConfiguration.THREE_MASTERS) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .users(USER_ADMIN, INDEX_A_USER) + .indices(INDEX_A, INDEX_B).build(); + + @Test + public void testAdminUserHasAccessToAllIndices() throws Exception { + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + HttpResponse response = client.get("*/_search?pretty"); + Assert.assertEquals(response.getStatusCode(), HttpStatus.SC_OK); + } + } + + @Test + public void testIndexAUserHasOnlyAccessToIndexA() throws Exception { + try (TestRestClient client = cluster.getRestClient(INDEX_A_USER)) { + HttpResponse response = client.get("index-a/_search?pretty"); + Assert.assertEquals(response.getStatusCode(), HttpStatus.SC_OK); + + // demo: work with JSON response body and check values + JsonPointer jsonPointer = JsonPointer.compile("/_source/hits/value"); + int hits = response.toJsonNode().at(jsonPointer).asInt(); + + response = client.get("index-b/_search?pretty"); + Assert.assertEquals(response.getStatusCode(), HttpStatus.SC_FORBIDDEN); + } + } + + @AfterClass + public static void close() { + cluster.close(); + } +} diff --git a/src/newTest/java/org/opensearch/test/PrivilegesEvaluatorTest.java b/src/newTest/java/org/opensearch/test/PrivilegesEvaluatorTest.java new file mode 100644 index 0000000000..44307c6a02 --- /dev/null +++ b/src/newTest/java/org/opensearch/test/PrivilegesEvaluatorTest.java @@ -0,0 +1,71 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.test; + +import org.apache.http.HttpStatus; +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Test; +import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.TestSecurityConfig.Role; +import org.opensearch.test.framework.cluster.ClusterConfiguration; +import org.opensearch.test.framework.cluster.LocalCluster; +import org.opensearch.test.framework.cluster.TestRestClient; +import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse; + +/** + * This is a port for the test + * org.opensearch.security.privileges.PrivilegesEvaluatorTest to the new test + * framework for direct comparison + * + */ +public class PrivilegesEvaluatorTest extends AbstractIntegrationTest { + + protected final static TestSecurityConfig.User NEGATIVE_LOOKAHEAD = new TestSecurityConfig.User( + "negative_lookahead_user") + .roles(new Role("negative_lookahead_role").indexPermissions("read").on("/^(?!t.*).*/") + .clusterPermissions("cluster_composite_ops")); + + protected final static TestSecurityConfig.User NEGATED_REGEX = new TestSecurityConfig.User("negated_regex_user") + .roles(new Role("negated_regex_role").indexPermissions("read").on("/^[a-z].*/") + .clusterPermissions("cluster_composite_ops")); + + @ClassRule + public static LocalCluster cluster = new LocalCluster.Builder() + .clusterConfiguration(ClusterConfiguration.THREE_MASTERS).authc(AUTHC_HTTPBASIC_INTERNAL) + .users(NEGATIVE_LOOKAHEAD, NEGATED_REGEX).build(); + + @Test + public void testNegativeLookaheadPattern() throws Exception { + + try (TestRestClient client = cluster.getRestClient(NEGATIVE_LOOKAHEAD)) { + HttpResponse response = client.get("*/_search"); + Assert.assertEquals(response.getStatusCode(), HttpStatus.SC_FORBIDDEN); + + response = client.get("r*/_search"); + Assert.assertEquals(response.getStatusCode(), HttpStatus.SC_OK); + } + } + + @Test + public void testRegexPattern() throws Exception { + + try (TestRestClient client = cluster.getRestClient(NEGATED_REGEX)) { + HttpResponse response = client.get("*/_search"); + Assert.assertEquals(response.getStatusCode(), HttpStatus.SC_FORBIDDEN); + + response = client.get("r*/_search"); + Assert.assertEquals(response.getStatusCode(), HttpStatus.SC_OK); + } + + } +} diff --git a/src/newTest/java/org/opensearch/test/SecurityRolesTests.java b/src/newTest/java/org/opensearch/test/SecurityRolesTests.java new file mode 100644 index 0000000000..b5de319912 --- /dev/null +++ b/src/newTest/java/org/opensearch/test/SecurityRolesTests.java @@ -0,0 +1,77 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.test; + +import org.apache.http.HttpStatus; +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Test; +import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.TestSecurityConfig.Role; +import org.opensearch.test.framework.cluster.ClusterConfiguration; +import org.opensearch.test.framework.cluster.LocalCluster; +import org.opensearch.test.framework.cluster.TestRestClient; +import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse; + +import com.fasterxml.jackson.core.JsonPointer; + +public class SecurityRolesTests extends AbstractIntegrationTest { + + protected final static TestSecurityConfig.User USER_SR = new TestSecurityConfig.User("sr_user").roles( + new Role("abc_ber").indexPermissions("*").on("*").clusterPermissions("*"), + new Role("def_efg").indexPermissions("*").on("*").clusterPermissions("*")); + + @ClassRule + public static LocalCluster cluster = new LocalCluster.Builder() + .clusterConfiguration(ClusterConfiguration.THREE_MASTERS).anonymousAuth(true) + .authc(AUTHC_HTTPBASIC_INTERNAL).users(USER_SR).build(); + + @Test + public void testSecurityRolesAnon() throws Exception { + + try (TestRestClient client = cluster.getRestClient(USER_SR)) { + HttpResponse response = client.getAuthInfo(); + Assert.assertEquals(response.getStatusCode(), HttpStatus.SC_OK); + + // Check username + JsonPointer jsonPointer = JsonPointer.compile("/user_name"); + String username = response.toJsonNode().at(jsonPointer).asText(); + Assert.assertEquals("sr_user", username); + + // Check security roles + jsonPointer = JsonPointer.compile("/roles/0"); + String securityRole = response.toJsonNode().at(jsonPointer).asText(); + Assert.assertEquals("user_sr_user__abc_ber", securityRole); + + jsonPointer = JsonPointer.compile("/roles/1"); + securityRole = response.toJsonNode().at(jsonPointer).asText(); + Assert.assertEquals("user_sr_user__def_efg", securityRole); + + } + } + +} diff --git a/src/newTest/java/org/opensearch/test/framework/TestIndex.java b/src/newTest/java/org/opensearch/test/framework/TestIndex.java new file mode 100644 index 0000000000..035418c2b4 --- /dev/null +++ b/src/newTest/java/org/opensearch/test/framework/TestIndex.java @@ -0,0 +1,87 @@ +/* + * Copyright 2021-2022 floragunn GmbH + * + * Licensed 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. + * + */ + +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.test.framework; + +import org.opensearch.action.admin.indices.create.CreateIndexRequest; +import org.opensearch.action.admin.indices.exists.indices.IndicesExistsRequest; +import org.opensearch.client.Client; +import org.opensearch.common.settings.Settings; + +public class TestIndex { + + private final String name; + private final Settings settings; + + public TestIndex(String name, Settings settings) { + this.name = name; + this.settings = settings; + + } + + public void create(Client client) { + if (!client.admin().indices().exists(new IndicesExistsRequest(name)).actionGet().isExists()) { + client.admin().indices().create(new CreateIndexRequest(name).settings(settings)).actionGet(); + } + } + + public String getName() { + return name; + } + + + public static Builder name(String name) { + return new Builder().name(name); + } + + public static class Builder { + private String name; + private Settings.Builder settings = Settings.builder(); + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder setting(String name, int value) { + settings.put(name, value); + return this; + } + + public Builder shards(int value) { + settings.put("index.number_of_shards", 5); + return this; + } + + public TestIndex build() { + return new TestIndex(name, settings.build()); + } + + } + +} diff --git a/src/newTest/java/org/opensearch/test/framework/TestSecurityConfig.java b/src/newTest/java/org/opensearch/test/framework/TestSecurityConfig.java new file mode 100644 index 0000000000..88e86b51b3 --- /dev/null +++ b/src/newTest/java/org/opensearch/test/framework/TestSecurityConfig.java @@ -0,0 +1,609 @@ +/* + * Copyright 2021 floragunn GmbH + * + * Licensed 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. + * + */ + +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.test.framework; + +import java.nio.ByteBuffer; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.bouncycastle.crypto.generators.OpenBSDBCrypt; +import org.opensearch.action.admin.indices.create.CreateIndexRequest; +import org.opensearch.action.index.IndexRequest; +import org.opensearch.action.support.WriteRequest.RefreshPolicy; +import org.opensearch.client.Client; +import org.opensearch.common.Strings; +import org.opensearch.common.bytes.BytesReference; +import org.opensearch.common.xcontent.XContentBuilder; +import org.opensearch.common.xcontent.XContentFactory; +import org.opensearch.security.action.configupdate.ConfigUpdateAction; +import org.opensearch.security.action.configupdate.ConfigUpdateRequest; +import org.opensearch.security.action.configupdate.ConfigUpdateResponse; +import org.opensearch.security.securityconf.impl.CType; +import org.opensearch.test.framework.cluster.NestedValueMap; +import org.opensearch.test.framework.cluster.NestedValueMap.Path; +import org.opensearch.test.framework.cluster.OpenSearchClientProvider.UserCredentialsHolder; + +public class TestSecurityConfig { + + private static final Logger log = LogManager.getLogger(TestSecurityConfig.class); + + private NestedValueMap overrideSecurityConfigSettings; + private NestedValueMap overrideUserSettings; + private NestedValueMap overrideRoleSettings; + private NestedValueMap overrideRoleMappingSettings; + private String indexName = ".opendistro_security"; + private Map> variableSuppliers = new HashMap<>(); + + public TestSecurityConfig() { + + } + + public TestSecurityConfig configIndexName(String configIndexName) { + this.indexName = configIndexName; + return this; + } + + public TestSecurityConfig var(String name, Supplier variableSupplier) { + this.variableSuppliers.put(name, variableSupplier); + return this; + } + + public TestSecurityConfig securityConfigSettings(String keyPath, Object value, Object... more) { + if (overrideSecurityConfigSettings == null) { + overrideSecurityConfigSettings = new NestedValueMap(); + } + + overrideSecurityConfigSettings.put(NestedValueMap.Path.parse(keyPath), value); + + for (int i = 0; i < more.length - 1; i += 2) { + overrideSecurityConfigSettings.put(NestedValueMap.Path.parse(String.valueOf(more[i])), more[i + 1]); + } + + return this; + } + + public TestSecurityConfig xff(String proxies) { + if (overrideSecurityConfigSettings == null) { + overrideSecurityConfigSettings = new NestedValueMap(); + } + + overrideSecurityConfigSettings.put(new NestedValueMap.Path("config", "dynamic", "http", "xff"), + NestedValueMap.of("enabled", true, "internalProxies", proxies)); + + return this; + } + + public TestSecurityConfig anonymousAuth(boolean anonymousAuthEnabled) { + if (overrideSecurityConfigSettings == null) { + overrideSecurityConfigSettings = new NestedValueMap(); + } + + overrideSecurityConfigSettings.put(new NestedValueMap.Path("config", "dynamic", "http"), + NestedValueMap.of("anonymous_auth_enabled", anonymousAuthEnabled)); + + return this; + } + + public TestSecurityConfig authc(AuthcDomain authcDomain) { + if (overrideSecurityConfigSettings == null) { + overrideSecurityConfigSettings = new NestedValueMap(); + } + + overrideSecurityConfigSettings.put(new NestedValueMap.Path("config", "dynamic", "authc"), authcDomain.toMap()); + + return this; + } + + public TestSecurityConfig user(User user) { + if (user.roleNames != null) { + return this.user(user.name, user.password, user.attributes, user.roleNames); + } else { + return this.user(user.name, user.password, user.attributes, user.roles); + } + } + + public TestSecurityConfig user(String name, String password, String... sgRoles) { + return user(name, password, null, sgRoles); + } + + public TestSecurityConfig user(String name, String password, Map attributes, String... securityRoles) { + if (overrideUserSettings == null) { + overrideUserSettings = new NestedValueMap(); + } + + overrideUserSettings.put(new NestedValueMap.Path(name, "hash"), hash(password.toCharArray())); + + if (securityRoles != null && securityRoles.length > 0) { + overrideUserSettings.put(new NestedValueMap.Path(name, "opensearch_security_roles"), securityRoles); + } + + if (attributes != null && attributes.size() != 0) { + for (Map.Entry attr : attributes.entrySet()) { + overrideUserSettings.put(new NestedValueMap.Path(name, "attributes", attr.getKey()), attr.getValue()); + } + } + + return this; + } + + public TestSecurityConfig user(String name, String password, Role... sgRoles) { + return user(name, password, null, sgRoles); + } + + public TestSecurityConfig user(String name, String password, Map attributes, Role... sgRoles) { + if (overrideUserSettings == null) { + overrideUserSettings = new NestedValueMap(); + } + + overrideUserSettings.put(new NestedValueMap.Path(name, "hash"), hash(password.toCharArray())); + + if (sgRoles != null && sgRoles.length > 0) { + String roleNamePrefix = "user_" + name + "__"; + + overrideUserSettings.put(new NestedValueMap.Path(name, "opendistro_security_roles"), + Arrays.asList(sgRoles).stream().map((r) -> roleNamePrefix + r.name).collect(Collectors.toList())); + roles(roleNamePrefix, sgRoles); + } + + if (attributes != null && attributes.size() != 0) { + for (Map.Entry attr : attributes.entrySet()) { + overrideUserSettings.put(new NestedValueMap.Path(name, "attributes", attr.getKey()), attr.getValue()); + } + } + + return this; + } + + public TestSecurityConfig roles(Role... roles) { + return roles("", roles); + } + + public TestSecurityConfig roles(String roleNamePrefix, Role... roles) { + if (overrideRoleSettings == null) { + overrideRoleSettings = new NestedValueMap(); + } + + for (Role role : roles) { + + String name = roleNamePrefix + role.name; + + if (role.clusterPermissions.size() > 0) { + overrideRoleSettings.put(new NestedValueMap.Path(name, "cluster_permissions"), role.clusterPermissions); + } + + if (role.indexPermissions.size() > 0) { + overrideRoleSettings.put(new NestedValueMap.Path(name, "index_permissions"), + role.indexPermissions.stream().map((p) -> p.toJsonMap()).collect(Collectors.toList())); + } + } + + return this; + } + + public TestSecurityConfig roleMapping(RoleMapping... roleMappings) { + if (overrideRoleMappingSettings == null) { + overrideRoleMappingSettings = new NestedValueMap(); + } + + for (RoleMapping roleMapping : roleMappings) { + + String name = roleMapping.name; + + if (roleMapping.backendRoles.size() > 0) { + overrideRoleMappingSettings.put(new NestedValueMap.Path(name, "backend_roles"), + roleMapping.backendRoles); + } + + if (roleMapping.users.size() > 0) { + overrideRoleMappingSettings.put(new NestedValueMap.Path(name, "users"), roleMapping.users); + } + } + + return this; + } + + public TestSecurityConfig roleToRoleMapping(Role role, String... backendRoles) { + return this.roleMapping(new RoleMapping(role.name).backendRoles(backendRoles)); + } + + public static class User implements UserCredentialsHolder { + private String name; + private String password; + private Role[] roles; + private String[] roleNames; + private Map attributes = new HashMap<>(); + + public User(String name) { + this.name = name; + this.password = "secret"; + } + + public User password(String password) { + this.password = password; + return this; + } + + public User roles(Role... roles) { + this.roles = roles; + return this; + } + + public User roles(String... roles) { + this.roleNames = roles; + return this; + } + + public User attr(String key, Object value) { + this.attributes.put(key, value); + return this; + } + + public String getName() { + return name; + } + + public String getPassword() { + return password; + } + + public Set getRoleNames() { + Set result = new HashSet(); + + if (roleNames != null) { + result.addAll(Arrays.asList(roleNames)); + } + + if (roles != null) { + result.addAll(Arrays.asList(roles).stream().map(Role::getName).collect(Collectors.toSet())); + } + + return result; + } + + } + + public static class Role { + public static Role ALL_ACCESS = new Role("all_access").clusterPermissions("*").indexPermissions("*").on("*"); + + private String name; + private List clusterPermissions = new ArrayList<>(); + + private List indexPermissions = new ArrayList<>(); + + public Role(String name) { + this.name = name; + } + + public Role clusterPermissions(String... clusterPermissions) { + this.clusterPermissions.addAll(Arrays.asList(clusterPermissions)); + return this; + } + + public IndexPermission indexPermissions(String... indexPermissions) { + return new IndexPermission(this, indexPermissions); + } + + public String getName() { + return name; + } + } + + public static class RoleMapping { + private String name; + private List backendRoles = new ArrayList<>(); + private List users = new ArrayList<>(); + + public RoleMapping(String name) { + this.name = name; + } + + public RoleMapping backendRoles(String... backendRoles) { + this.backendRoles.addAll(Arrays.asList(backendRoles)); + return this; + } + + public RoleMapping users(String... users) { + this.users.addAll(Arrays.asList(users)); + return this; + } + + } + + public static class IndexPermission { + private List allowedActions; + private List indexPatterns; + private Role role; + private String dlsQuery; + private List fls; + private List maskedFields; + + IndexPermission(Role role, String... allowedActions) { + this.allowedActions = Arrays.asList(allowedActions); + this.role = role; + } + + public IndexPermission dls(String dlsQuery) { + this.dlsQuery = dlsQuery; + return this; + } + + public IndexPermission fls(String... fls) { + this.fls = Arrays.asList(fls); + return this; + } + + public IndexPermission maskedFields(String... maskedFields) { + this.maskedFields = Arrays.asList(maskedFields); + return this; + } + + public Role on(String... indexPatterns) { + this.indexPatterns = Arrays.asList(indexPatterns); + this.role.indexPermissions.add(this); + return this.role; + } + + public Role on(TestIndex... testindices) { + this.indexPatterns = Arrays.asList(testindices).stream().map(TestIndex::getName).collect(Collectors.toList()); + this.role.indexPermissions.add(this); + return this.role; + } + + public NestedValueMap toJsonMap() { + NestedValueMap result = new NestedValueMap(); + + result.put("index_patterns", indexPatterns); + result.put("allowed_actions", allowedActions); + + if (dlsQuery != null) { + result.put("dls", dlsQuery); + } + + if (fls != null) { + result.put("fls", fls); + } + + if (maskedFields != null) { + result.put("masked_fields", maskedFields); + } + + return result; + } + + } + + public static class AuthcDomain { + + private final String id; + private boolean enabled = true; + private boolean transportEnabled = true; + private int order; + private List skipUsers = new ArrayList<>(); + private HttpAuthenticator httpAuthenticator; + private AuthenticationBackend authenticationBackend; + + public AuthcDomain(String id, int order) { + this.id = id; + this.order = order; + } + + public AuthcDomain httpAuthenticator(String type) { + this.httpAuthenticator = new HttpAuthenticator(type); + return this; + } + + public AuthcDomain challengingAuthenticator(String type) { + this.httpAuthenticator = new HttpAuthenticator(type).challenge(true); + return this; + } + + public AuthcDomain httpAuthenticator(HttpAuthenticator httpAuthenticator) { + this.httpAuthenticator = httpAuthenticator; + return this; + } + + public AuthcDomain backend(String type) { + this.authenticationBackend = new AuthenticationBackend(type); + return this; + } + + public AuthcDomain backend(AuthenticationBackend authenticationBackend) { + this.authenticationBackend = authenticationBackend; + return this; + } + + public AuthcDomain skipUsers(String... users) { + this.skipUsers.addAll(Arrays.asList(users)); + return this; + } + + NestedValueMap toMap() { + NestedValueMap result = new NestedValueMap(); + result.put(new NestedValueMap.Path(id, "http_enabled"), enabled); + result.put(new NestedValueMap.Path(id, "transport_enabled"), transportEnabled); + result.put(new NestedValueMap.Path(id, "order"), order); + + if (httpAuthenticator != null) { + result.put(new NestedValueMap.Path(id, "http_authenticator"), httpAuthenticator.toMap()); + } + + if (authenticationBackend != null) { + result.put(new NestedValueMap.Path(id, "authentication_backend"), authenticationBackend.toMap()); + } + + + if (skipUsers != null && skipUsers.size() > 0) { + result.put(new NestedValueMap.Path(id, "skip_users"), skipUsers); + } + + return result; + } + + public static class HttpAuthenticator { + private final String type; + private boolean challenge; + private NestedValueMap config = new NestedValueMap(); + + public HttpAuthenticator(String type) { + this.type = type; + } + + public HttpAuthenticator challenge(boolean challenge) { + this.challenge = challenge; + return this; + } + + public HttpAuthenticator config(Map config) { + this.config.putAllFromAnyMap(config); + return this; + } + + public HttpAuthenticator config(String key, Object value) { + this.config.put(Path.parse(key), value); + return this; + } + + NestedValueMap toMap() { + NestedValueMap result = new NestedValueMap(); + result.put("type", type); + result.put("challenge", challenge); + result.put("config", config); + return result; + } + } + + public static class AuthenticationBackend { + private final String type; + private NestedValueMap config = new NestedValueMap(); + + public AuthenticationBackend(String type) { + this.type = type; + } + + public AuthenticationBackend config(Map config) { + this.config.putAllFromAnyMap(config); + return this; + } + + public AuthenticationBackend config(String key, Object value) { + this.config.put(Path.parse(key), value); + return this; + } + + NestedValueMap toMap() { + NestedValueMap result = new NestedValueMap(); + result.put("type", type); + result.put("config", config); + return result; + } + } + } + + public TestSecurityConfig clone() { + TestSecurityConfig result = new TestSecurityConfig(); + result.indexName = indexName; + result.overrideRoleSettings = overrideRoleSettings != null ? overrideRoleSettings.clone() : null; + result.overrideSecurityConfigSettings = overrideSecurityConfigSettings != null ? overrideSecurityConfigSettings.clone() : null; + result.overrideUserSettings = overrideUserSettings != null ? overrideUserSettings.clone() : null; + + return result; + } + + public void initIndex(Client client) { + Map settings = new HashMap<>(); + if (indexName.startsWith(".")) { + settings.put("index.hidden", true); + } + client.admin().indices().create(new CreateIndexRequest(indexName).settings(settings)).actionGet(); + + writeConfigToIndex(client, CType.CONFIG, overrideSecurityConfigSettings); + writeConfigToIndex(client, CType.ROLES, overrideRoleSettings); + writeConfigToIndex(client, CType.INTERNALUSERS, overrideUserSettings); + writeConfigToIndex(client, CType.ROLESMAPPING, overrideRoleMappingSettings); + writeConfigToIndex(client, CType.ACTIONGROUPS); + writeConfigToIndex(client, CType.TENANTS); + + ConfigUpdateResponse configUpdateResponse = client.execute(ConfigUpdateAction.INSTANCE, + new ConfigUpdateRequest(CType.lcStringValues().toArray(new String[0]))).actionGet(); + + if (configUpdateResponse.hasFailures()) { + throw new RuntimeException("ConfigUpdateResponse produced failures: " + configUpdateResponse.failures()); + } + } + + + private static String hash(final char[] clearTextPassword) { + final byte[] salt = new byte[16]; + new SecureRandom().nextBytes(salt); + final String hash = OpenBSDBCrypt.generate((Objects.requireNonNull(clearTextPassword)), salt, 12); + Arrays.fill(salt, (byte) 0); + Arrays.fill(clearTextPassword, '\0'); + return hash; + } + + + private void writeConfigToIndex(Client client, CType configType) { + writeConfigToIndex(client, configType, NestedValueMap.createNonCloningMap()); + } + + private void writeConfigToIndex(Client client, CType configType, NestedValueMap overrides) { + try { + + NestedValueMap config = NestedValueMap.of(new NestedValueMap.Path("_meta", "type"), configType.toLCString(), + new NestedValueMap.Path("_meta", "config_version"), 2); + + if (overrides != null) { + config.overrideLeafs(overrides); + } + + XContentBuilder builder = XContentFactory.jsonBuilder().map(config); + String json = Strings.toString(builder); + + log.info("Writing " + configType + ":\n" + json); + + client.index(new IndexRequest(indexName).id(configType.toLCString()) + .setRefreshPolicy(RefreshPolicy.IMMEDIATE).source(configType.toLCString(), + BytesReference.fromByteBuffer(ByteBuffer.wrap(json.getBytes("utf-8"))))) + .actionGet(); + } catch (Exception e) { + throw new RuntimeException("Error while initializing config for " + indexName, e); + } + } +} diff --git a/src/newTest/java/org/opensearch/test/framework/certificate/Certificates.java b/src/newTest/java/org/opensearch/test/framework/certificate/Certificates.java new file mode 100644 index 0000000000..c24eebd1b7 --- /dev/null +++ b/src/newTest/java/org/opensearch/test/framework/certificate/Certificates.java @@ -0,0 +1,165 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.test.framework.certificate; + +/** + * Contains static certificates for the test cluster. + * Note: This is WIP and will be replaced by classes + * that can generate certificates on the fly. This + * class will be removed after that. + */ +public class Certificates { + + final static String ROOT_CA_CERTIFICATE = "-----BEGIN CERTIFICATE-----\n" + + "MIID/jCCAuagAwIBAgIBATANBgkqhkiG9w0BAQsFADCBjzETMBEGCgmSJomT8ixk\n" + + "ARkWA2NvbTEXMBUGCgmSJomT8ixkARkWB2V4YW1wbGUxGTAXBgNVBAoMEEV4YW1w\n" + + "bGUgQ29tIEluYy4xITAfBgNVBAsMGEV4YW1wbGUgQ29tIEluYy4gUm9vdCBDQTEh\n" + + "MB8GA1UEAwwYRXhhbXBsZSBDb20gSW5jLiBSb290IENBMB4XDTE4MDQyMjAzNDM0\n" + + "NloXDTI4MDQxOTAzNDM0NlowgY8xEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJ\n" + + "kiaJk/IsZAEZFgdleGFtcGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSEw\n" + + "HwYDVQQLDBhFeGFtcGxlIENvbSBJbmMuIFJvb3QgQ0ExITAfBgNVBAMMGEV4YW1w\n" + + "bGUgQ29tIEluYy4gUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC\n" + + "ggEBAK/u+GARP5innhpXK0c0q7s1Su1VTEaIgmZr8VWI6S8amf5cU3ktV7WT9SuV\n" + + "TsAm2i2A5P+Ctw7iZkfnHWlsC3HhPUcd6mvzGZ4moxnamM7r+a9otRp3owYoGStX\n" + + "ylVTQusAjbq9do8CMV4hcBTepCd+0w0v4h6UlXU8xjhj1xeUIz4DKbRgf36q0rv4\n" + + "VIX46X72rMJSETKOSxuwLkov1ZOVbfSlPaygXIxqsHVlj1iMkYRbQmaTib6XWHKf\n" + + "MibDaqDejOhukkCjzpptGZOPFQ8002UtTTNv1TiaKxkjMQJNwz6jfZ53ws3fh1I0\n" + + "RWT6WfM4oeFRFnyFRmc4uYTUgAkCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAf\n" + + "BgNVHSMEGDAWgBSSNQzgDx4rRfZNOfN7X6LmEpdAczAdBgNVHQ4EFgQUkjUM4A8e\n" + + "K0X2TTnze1+i5hKXQHMwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4IB\n" + + "AQBoQHvwsR34hGO2m8qVR9nQ5Klo5HYPyd6ySKNcT36OZ4AQfaCGsk+SecTi35QF\n" + + "RHL3g2qffED4tKR0RBNGQSgiLavmHGCh3YpDupKq2xhhEeS9oBmQzxanFwWFod4T\n" + + "nnsG2cCejyR9WXoRzHisw0KJWeuNlwjUdJY0xnn16srm1zL/M/f0PvCyh9HU1mF1\n" + + "ivnOSqbDD2Z7JSGyckgKad1Omsg/rr5XYtCeyJeXUPcmpeX6erWJJNTUh6yWC/hY\n" + + "G/dFC4xrJhfXwz6Z0ytUygJO32bJG4Np2iGAwvvgI9EfxzEv/KP+FGrJOvQJAq4/\n" + + "BU36ZAa80W/8TBnqZTkNnqZV\n" + + "-----END CERTIFICATE-----\n" + + ""; + + final static String NODE_CERTIFICATE = "-----BEGIN CERTIFICATE-----\n" + + "MIIEyTCCA7GgAwIBAgIGAWLrc1O2MA0GCSqGSIb3DQEBCwUAMIGPMRMwEQYKCZIm\n" + + "iZPyLGQBGRYDY29tMRcwFQYKCZImiZPyLGQBGRYHZXhhbXBsZTEZMBcGA1UECgwQ\n" + + "RXhhbXBsZSBDb20gSW5jLjEhMB8GA1UECwwYRXhhbXBsZSBDb20gSW5jLiBSb290\n" + + "IENBMSEwHwYDVQQDDBhFeGFtcGxlIENvbSBJbmMuIFJvb3QgQ0EwHhcNMTgwNDIy\n" + + "MDM0MzQ3WhcNMjgwNDE5MDM0MzQ3WjBeMRIwEAYKCZImiZPyLGQBGRYCZGUxDTAL\n" + + "BgNVBAcMBHRlc3QxDTALBgNVBAoMBG5vZGUxDTALBgNVBAsMBG5vZGUxGzAZBgNV\n" + + "BAMMEm5vZGUtMC5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC\n" + + "AQoCggEBAJa+f476vLB+AwK53biYByUwN+40D8jMIovGXm6wgT8+9Sbs899dDXgt\n" + + "9CE1Beo65oP1+JUz4c7UHMrCY3ePiDt4cidHVzEQ2g0YoVrQWv0RedS/yx/DKhs8\n" + + "Pw1O715oftP53p/2ijD5DifFv1eKfkhFH+lwny/vMSNxellpl6NxJTiJVnQ9HYOL\n" + + "gf2t971ITJHnAuuxUF48HcuNovW4rhtkXef8kaAN7cE3LU+A9T474ULNCKkEFPIl\n" + + "ZAKN3iJNFdVsxrTU+CUBHzk73Do1cCkEvJZ0ZFjp0Z3y8wLY/gqWGfGVyA9l2CUq\n" + + "eIZNf55PNPtGzOrvvONiui48vBKH1LsCAwEAAaOCAVkwggFVMIG8BgNVHSMEgbQw\n" + + "gbGAFJI1DOAPHitF9k0583tfouYSl0BzoYGVpIGSMIGPMRMwEQYKCZImiZPyLGQB\n" + + "GRYDY29tMRcwFQYKCZImiZPyLGQBGRYHZXhhbXBsZTEZMBcGA1UECgwQRXhhbXBs\n" + + "ZSBDb20gSW5jLjEhMB8GA1UECwwYRXhhbXBsZSBDb20gSW5jLiBSb290IENBMSEw\n" + + "HwYDVQQDDBhFeGFtcGxlIENvbSBJbmMuIFJvb3QgQ0GCAQEwHQYDVR0OBBYEFKyv\n" + + "78ZmFjVKM9g7pMConYH7FVBHMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgXg\n" + + "MCAGA1UdJQEB/wQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjA1BgNVHREELjAsiAUq\n" + + "AwQFBYISbm9kZS0wLmV4YW1wbGUuY29tgglsb2NhbGhvc3SHBH8AAAEwDQYJKoZI\n" + + "hvcNAQELBQADggEBAIOKuyXsFfGv1hI/Lkpd/73QNqjqJdxQclX57GOMWNbOM5H0\n" + + "5/9AOIZ5JQsWULNKN77aHjLRr4owq2jGbpc/Z6kAd+eiatkcpnbtbGrhKpOtoEZy\n" + + "8KuslwkeixpzLDNISSbkeLpXz4xJI1ETMN/VG8ZZP1bjzlHziHHDu0JNZ6TnNzKr\n" + + "XzCGMCohFfem8vnKNnKUneMQMvXd3rzUaAgvtf7Hc2LTBlf4fZzZF1EkwdSXhaMA\n" + + "1lkfHiqOBxtgeDLxCHESZ2fqgVqsWX+t3qHQfivcPW6txtDyrFPRdJOGhiMGzT/t\n" + + "e/9kkAtQRgpTb3skYdIOOUOV0WGQ60kJlFhAzIs=\n" + + "-----END CERTIFICATE-----\n" + + ""; + + final static String NODE_KEY = "-----BEGIN PRIVATE KEY-----\n" + + "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCWvn+O+rywfgMC\n" + + "ud24mAclMDfuNA/IzCKLxl5usIE/PvUm7PPfXQ14LfQhNQXqOuaD9fiVM+HO1BzK\n" + + "wmN3j4g7eHInR1cxENoNGKFa0Fr9EXnUv8sfwyobPD8NTu9eaH7T+d6f9oow+Q4n\n" + + "xb9Xin5IRR/pcJ8v7zEjcXpZaZejcSU4iVZ0PR2Di4H9rfe9SEyR5wLrsVBePB3L\n" + + "jaL1uK4bZF3n/JGgDe3BNy1PgPU+O+FCzQipBBTyJWQCjd4iTRXVbMa01PglAR85\n" + + "O9w6NXApBLyWdGRY6dGd8vMC2P4KlhnxlcgPZdglKniGTX+eTzT7Rszq77zjYrou\n" + + "PLwSh9S7AgMBAAECggEABwiohxFoEIwws8XcdKqTWsbfNTw0qFfuHLuK2Htf7IWR\n" + + "htlzn66F3F+4jnwc5IsPCoVFriCXnsEC/usHHSMTZkL+gJqxlNaGdin6DXS/aiOQ\n" + + "nb69SaQfqNmsz4ApZyxVDqsQGkK0vAhDAtQVU45gyhp/nLLmmqP8lPzMirOEodmp\n" + + "U9bA8t/ttrzng7SVAER42f6IVpW0iTKTLyFii0WZbq+ObViyqib9hVFrI6NJuQS+\n" + + "IelcZB0KsSi6rqIjXg1XXyMiIUcSlhq+GfEa18AYgmsbPwMbExate7/8Ci7ZtCbh\n" + + "lx9bves2+eeqq5EMm3sMHyhdcg61yzd5UYXeZhwJkQKBgQDS9YqrAtztvLY2gMgv\n" + + "d+wOjb9awWxYbQTBjx33kf66W+pJ+2j8bI/XX2CpZ98w/oq8VhMqbr9j5b8MfsrF\n" + + "EoQvedA4joUo8sXd4j1mR2qKF4/KLmkgy6YYusNP2UrVSw7sh77bzce+YaVVoO/e\n" + + "0wIVTHuD/QZ6fG6MasOqcbl6hwKBgQC27cQruaHFEXR/16LrMVAX+HyEEv44KOCZ\n" + + "ij5OE4P7F0twb+okngG26+OJV3BtqXf0ULlXJ+YGwXCRf6zUZkld3NMy3bbKPgH6\n" + + "H/nf3BxqS2tudj7+DV52jKtisBghdvtlKs56oc9AAuwOs37DvhptBKUPdzDDqfys\n" + + "Qchv5JQdLQKBgERev+pcqy2Bk6xmYHrB6wdseS/4sByYeIoi0BuEfYH4eB4yFPx6\n" + + "UsQCbVl6CKPgWyZe3ydJbU37D8gE78KfFagtWoZ56j4zMF2RDUUwsB7BNCDamce/\n" + + "OL2bCeG/Erm98cBG3lxufOX+z47I8fTNfkdY2k8UmhzoZwurLm73HJ3RAoGBAKsp\n" + + "6yamuXF2FbYRhUXgjHsBbTD/vJO72/yO2CGiLRpi/5mjfkjo99269trp0C8sJSub\n" + + "5PBiSuADXFsoRgUv+HI1UAEGaCTwxFTQWrRWdtgW3d0sE2EQDVWL5kmfT9TwSeat\n" + + "mSoyAYR5t3tCBNkPJhbgA7pm4mASzHQ50VyxWs25AoGBAKPFx9X2oKhYQa+mW541\n" + + "bbqRuGFMoXIIcr/aeM3LayfLETi48o5NDr2NDP11j4yYuz26YLH0Dj8aKpWuehuH\n" + + "uB27n6j6qu0SVhQi6mMJBe1JrKbzhqMKQjYOoy8VsC2gdj5pCUP/kLQPW7zm9diX\n" + + "CiKTtKgPIeYdigor7V3AHcVT\n" + + "-----END PRIVATE KEY-----\n" + + ""; + + final static String ADMIN_CERTIFICATE = "-----BEGIN CERTIFICATE-----\n" + + "MIIEdzCCA1+gAwIBAgIGAWLrc1O4MA0GCSqGSIb3DQEBCwUAMIGPMRMwEQYKCZIm\n" + + "iZPyLGQBGRYDY29tMRcwFQYKCZImiZPyLGQBGRYHZXhhbXBsZTEZMBcGA1UECgwQ\n" + + "RXhhbXBsZSBDb20gSW5jLjEhMB8GA1UECwwYRXhhbXBsZSBDb20gSW5jLiBSb290\n" + + "IENBMSEwHwYDVQQDDBhFeGFtcGxlIENvbSBJbmMuIFJvb3QgQ0EwHhcNMTgwNDIy\n" + + "MDM0MzQ3WhcNMjgwNDE5MDM0MzQ3WjBNMQswCQYDVQQGEwJkZTENMAsGA1UEBwwE\n" + + "dGVzdDEPMA0GA1UECgwGY2xpZW50MQ8wDQYDVQQLDAZjbGllbnQxDTALBgNVBAMM\n" + + "BGtpcmswggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDCwgBOoO88uMM8\n" + + "dREJsk58Yt4Jn0zwQ2wUThbvy3ICDiEWhiAhUbg6dTggpS5vWWJto9bvaaqgMVoh\n" + + "ElfYHdTDncX3UQNBEP8tqzHON6BFEFSGgJRGLd6f5dri6rK32nCotYS61CFXBFxf\n" + + "WumXjSukjyrcTsdkR3C5QDo2oN7F883MOQqRENPzAtZi9s3jNX48u+/e3yvJzXsB\n" + + "GS9Qmsye6C71enbIujM4CVwDT/7a5jHuaUp6OuNCFbdRPnu/wLYwOS2/yOtzAqk7\n" + + "/PFnPCe7YOa10ShnV/jx2sAHhp7ZQBJgFkkgnIERz9Ws74Au+EbptWnsWuB+LqRL\n" + + "x5G02IzpAgMBAAGjggEYMIIBFDCBvAYDVR0jBIG0MIGxgBSSNQzgDx4rRfZNOfN7\n" + + "X6LmEpdAc6GBlaSBkjCBjzETMBEGCgmSJomT8ixkARkWA2NvbTEXMBUGCgmSJomT\n" + + "8ixkARkWB2V4YW1wbGUxGTAXBgNVBAoMEEV4YW1wbGUgQ29tIEluYy4xITAfBgNV\n" + + "BAsMGEV4YW1wbGUgQ29tIEluYy4gUm9vdCBDQTEhMB8GA1UEAwwYRXhhbXBsZSBD\n" + + "b20gSW5jLiBSb290IENBggEBMB0GA1UdDgQWBBRsdhuHn3MGDvZxOe22+1wliCJB\n" + + "mDAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIF4DAWBgNVHSUBAf8EDDAKBggr\n" + + "BgEFBQcDAjANBgkqhkiG9w0BAQsFAAOCAQEAkPrUTKKn+/6g0CjhTPBFeX8mKXhG\n" + + "zw5z9Oq+xnwefZwxV82E/tgFsPcwXcJIBg0f43BaVSygPiV7bXqWhxASwn73i24z\n" + + "lveIR4+z56bKIhP6c3twb8WWR9yDcLu2Iroin7dYEm3dfVUrhz/A90WHr6ddwmLL\n" + + "3gcFF2kBu3S3xqM5OmN/tqRXFmo+EvwrdJRiTh4Fsf0tX1ZT07rrGvBFYktK7Kma\n" + + "lqDl4UDCF1UWkiiFubc0Xw+DR6vNAa99E0oaphzvCmITU1wITNnYZTKzVzQ7vUCq\n" + + "kLmXOFLTcxTQpptxSo5xDD3aTpzWGCvjExCKpXQtsITUOYtZc02AGjjPOQ==\n" + + "-----END CERTIFICATE-----\n" + + ""; + + final static String ADMIN_KEY = "-----BEGIN PRIVATE KEY-----\n" + + "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDCwgBOoO88uMM8\n" + + "dREJsk58Yt4Jn0zwQ2wUThbvy3ICDiEWhiAhUbg6dTggpS5vWWJto9bvaaqgMVoh\n" + + "ElfYHdTDncX3UQNBEP8tqzHON6BFEFSGgJRGLd6f5dri6rK32nCotYS61CFXBFxf\n" + + "WumXjSukjyrcTsdkR3C5QDo2oN7F883MOQqRENPzAtZi9s3jNX48u+/e3yvJzXsB\n" + + "GS9Qmsye6C71enbIujM4CVwDT/7a5jHuaUp6OuNCFbdRPnu/wLYwOS2/yOtzAqk7\n" + + "/PFnPCe7YOa10ShnV/jx2sAHhp7ZQBJgFkkgnIERz9Ws74Au+EbptWnsWuB+LqRL\n" + + "x5G02IzpAgMBAAECggEAEzwnMkeBbqqDgyRqFbO/PgMNvD7i0b/28V0dCtCPEVY6\n" + + "klzrg3RCERP5V9AN8VVkppYjPkCzZ2A4b0JpMUu7ncOmr7HCnoSCj2IfEyePSVg+\n" + + "4OHbbcBOAoDTHiI2myM/M9++8izNS34qGV4t6pfjaDyeQQ/5cBVWNBWnKjS34S5H\n" + + "rJWpAcDgxYk5/ah2Xs2aULZlXDMxbSikjrv+n4JIYTKFQo8ydzL8HQDBRmXAFLjC\n" + + "gNOSHf+5u1JdpY3uPIxK1ugVf8zPZ4/OEB23j56uu7c8+sZ+kZwfRWAQmMhFVG/y\n" + + "OXxoT5mOruBsAw29m2Ijtxg252/YzSTxiDqFziB/eQKBgQDjeVAdi55GW/bvhuqn\n" + + "xME/An8E3hI/FyaaITrMQJUBjiCUaStTEqUgQ6A7ZfY/VX6qafOX7sli1svihrXC\n" + + "uelmKrdve/CFEEqzX9JWWRiPiQ0VZD+EQRsJvX85Tw2UGvVUh6dO3UGPS0BhplMD\n" + + "jeVpyXgZ7Gy5we+DWjfwhYrCmwKBgQDbLmQhRy+IdVljObZmv3QtJ0cyxxZETWzU\n" + + "MKmgBFvcRw+KvNwO+Iy0CHEbDu06Uj63kzI2bK3QdINaSrjgr8iftXIQpBmcgMF+\n" + + "a1l5HtHlCp6RWd55nWQOEvn36IGN3cAaQkXuh4UYM7QfEJaAbzJhyJ+wXA3jWqUd\n" + + "8bDTIAZ0ywKBgFuZ44gyTAc7S2JDa0Up90O/ZpT4NFLRqMrSbNIJg7d/m2EIRNkM\n" + + "HhCzCthAg/wXGo3XYq+hCdnSc4ICCzmiEfoBY6LyPvXmjJ5VDOeWs0xBvVIK74T7\n" + + "jr7KX2wdiHNGs9pZUidw89CXVhK8nptEzcheyA1wZowbK68yamph7HHXAoGBAK3x\n" + + "7D9Iyl1mnDEWPT7f1Gh9UpDm1TIRrDvd/tBihTCVKK13YsFy2d+LD5Bk0TpGyUVR\n" + + "STlOGMdloFUJFh4jA3pUOpkgUr8Uo/sbYN+x6Ov3+I3sH5aupRhSURVA7YhUIz/z\n" + + "tqIt5R+m8Nzygi6dkQNvf+Qruk3jw0S3ahizwsvvAoGAL7do6dTLp832wFVxkEf4\n" + + "gg1M6DswfkgML5V/7GQ3MkIX/Hrmiu+qSuHhDGrp9inZdCDDYg5+uy1+2+RBMRZ3\n" + + "vDUUacvc4Fep05zp7NcjgU5y+/HWpuKVvLIlZAO1MBY4Xinqqii6RdxukIhxw7eT\n" + + "C6TPL5KAcV1R/XAihDhI18Y=\n" + + "-----END PRIVATE KEY-----\n" + + ""; +} diff --git a/src/newTest/java/org/opensearch/test/framework/certificate/TestCertificates.java b/src/newTest/java/org/opensearch/test/framework/certificate/TestCertificates.java new file mode 100644 index 0000000000..9ca0aa54e8 --- /dev/null +++ b/src/newTest/java/org/opensearch/test/framework/certificate/TestCertificates.java @@ -0,0 +1,63 @@ +/* + * Copyright 2021 floragunn GmbH + * + * Licensed 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.opensearch.test.framework.certificate; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +/** + * Provides TLS certificates required in test cases. + * WIP At the moment the certificates are hard coded. + * This will be replaced by classes + * that can generate certificates on the fly. + */ +public class TestCertificates { + + public File getRootCertificate() throws IOException { + return createTempFile("root", ".cert", Certificates.ROOT_CA_CERTIFICATE); + } + + public File getNodeCertificate(int node) throws IOException { + return createTempFile("node-" + node, ".cert", Certificates.NODE_CERTIFICATE); + } + + public File getNodeKey(int node) throws IOException { + return createTempFile("node-" + node, ".key", Certificates.NODE_KEY); + } + + public File getAdminCertificate() throws IOException { + return createTempFile("admin", ".cert", Certificates.ADMIN_CERTIFICATE); + } + + public File getAdminKey() throws IOException { + return createTempFile("admin", ".key", Certificates.ADMIN_KEY); + } + + public String[] getAdminDNs() throws IOException { + return new String[] {"CN=kirk,OU=client,O=client,L=test,C=de"}; + } + + private File createTempFile(String name, String suffix, String contents) throws IOException { + Path path = Files.createTempFile(name, suffix); + Files.writeString(path, contents); + return path.toFile(); + + } +} diff --git a/src/newTest/java/org/opensearch/test/framework/cluster/ClusterConfiguration.java b/src/newTest/java/org/opensearch/test/framework/cluster/ClusterConfiguration.java new file mode 100644 index 0000000000..08e8eb31c1 --- /dev/null +++ b/src/newTest/java/org/opensearch/test/framework/cluster/ClusterConfiguration.java @@ -0,0 +1,131 @@ +/* + * Copyright 2015-2017 floragunn GmbH + * + * Licensed 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. + * + */ + +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.test.framework.cluster; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Collectors; + +import org.opensearch.index.reindex.ReindexPlugin; +import org.opensearch.join.ParentJoinPlugin; +import org.opensearch.percolator.PercolatorPlugin; +import org.opensearch.plugins.Plugin; +import org.opensearch.search.aggregations.matrix.MatrixAggregationPlugin; +import org.opensearch.security.OpenSearchSecurityPlugin; +import org.opensearch.transport.Netty4Plugin; + +import com.google.common.collect.Lists; + +public enum ClusterConfiguration { + //first one needs to be a master + //HUGE(new NodeSettings(true, false, false), new NodeSettings(true, false, false), new NodeSettings(true, false, false), new NodeSettings(false, true,false), new NodeSettings(false, true, false)), + + //3 nodes (1m, 2d) + DEFAULT(new NodeSettings(true, false), new NodeSettings(false, true), new NodeSettings(false, true)), + + //1 node (1md) + SINGLENODE(new NodeSettings(true, true)), + + //4 node (1m, 2d, 1c) + CLIENTNODE(new NodeSettings(true, false), new NodeSettings(false, true), new NodeSettings(false, true), new NodeSettings(false, false)), + + THREE_MASTERS(new NodeSettings(true, false), new NodeSettings(true, false), new NodeSettings(true, false), new NodeSettings(false, true), new NodeSettings(false, true)); + + private List nodeSettings = new LinkedList<>(); + + private ClusterConfiguration(NodeSettings... settings) { + nodeSettings.addAll(Arrays.asList(settings)); + } + + public List getNodeSettings() { + return Collections.unmodifiableList(nodeSettings); + } + + public List getMasterNodeSettings() { + return Collections.unmodifiableList(nodeSettings.stream().filter(a -> a.masterNode).collect(Collectors.toList())); + } + + public List getNonMasterNodeSettings() { + return Collections.unmodifiableList(nodeSettings.stream().filter(a -> !a.masterNode).collect(Collectors.toList())); + } + + public int getNodes() { + return nodeSettings.size(); + } + + public int getMasterNodes() { + return (int) nodeSettings.stream().filter(a -> a.masterNode).count(); + } + + public int getDataNodes() { + return (int) nodeSettings.stream().filter(a -> a.dataNode).count(); + } + + public int getClientNodes() { + return (int) nodeSettings.stream().filter(a -> !a.masterNode && !a.dataNode).count(); + } + + public static class NodeSettings { + public boolean masterNode; + public boolean dataNode; + public List> plugins = Lists.newArrayList(Netty4Plugin.class, OpenSearchSecurityPlugin.class, MatrixAggregationPlugin.class, + ParentJoinPlugin.class, PercolatorPlugin.class, ReindexPlugin.class); + + public NodeSettings(boolean masterNode, boolean dataNode) { + super(); + this.masterNode = masterNode; + this.dataNode = dataNode; + } + + public NodeSettings(boolean masterNode, boolean dataNode, List> additionalPlugins) { + this(masterNode, dataNode); + + this.plugins.addAll(additionalPlugins); + } + + @SuppressWarnings("unchecked") + public Class[] getPlugins() { + return plugins.toArray(new Class[0]); + } + + @SuppressWarnings("unchecked") + public Class[] getPlugins(List> additionalPlugins) { + List> plugins = new ArrayList<>(this.plugins); + + if (additionalPlugins != null) { + plugins.addAll(additionalPlugins); + } + + return plugins.toArray(new Class[0]); + } + } +} diff --git a/src/newTest/java/org/opensearch/test/framework/cluster/ClusterInfo.java b/src/newTest/java/org/opensearch/test/framework/cluster/ClusterInfo.java new file mode 100644 index 0000000000..e82ee82391 --- /dev/null +++ b/src/newTest/java/org/opensearch/test/framework/cluster/ClusterInfo.java @@ -0,0 +1,45 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.test.framework.cluster; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.opensearch.common.transport.TransportAddress; + +// TODO: change to getter/setter +public class ClusterInfo { + public int numNodes; + public String httpHost = null; + public int httpPort = -1; + public Set httpAdresses = new HashSet(); + public String nodeHost; + public int nodePort; + public String clustername; + public List tcpMasterPortsOnly; +} diff --git a/src/newTest/java/org/opensearch/test/framework/cluster/ContextHeaderDecoratorClient.java b/src/newTest/java/org/opensearch/test/framework/cluster/ContextHeaderDecoratorClient.java new file mode 100644 index 0000000000..76805d1ea6 --- /dev/null +++ b/src/newTest/java/org/opensearch/test/framework/cluster/ContextHeaderDecoratorClient.java @@ -0,0 +1,60 @@ +package org.opensearch.test.framework.cluster; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.opensearch.action.ActionListener; +import org.opensearch.action.ActionRequest; +import org.opensearch.action.ActionResponse; +import org.opensearch.action.ActionType; +import org.opensearch.action.support.ContextPreservingActionListener; +import org.opensearch.client.Client; +import org.opensearch.client.FilterClient; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.common.util.concurrent.ThreadContext.StoredContext; + +public class ContextHeaderDecoratorClient extends FilterClient { + + private Map headers; + + public ContextHeaderDecoratorClient(Client in, Map headers) { + super(in); + this.headers = headers != null ? headers : Collections.emptyMap(); + } + + public ContextHeaderDecoratorClient(Client in, String... headers) { + this(in, arrayToMap(headers)); + } + + @Override + protected void doExecute(ActionType action, Request request, + ActionListener listener) { + + ThreadContext threadContext = threadPool().getThreadContext(); + ContextPreservingActionListener wrappedListener = new ContextPreservingActionListener<>(threadContext.newRestorableContext(true), listener); + + try (StoredContext ctx = threadContext.stashContext()) { + threadContext.putHeader(this.headers); + super.doExecute(action, request, wrappedListener); + } + } + + private static Map arrayToMap(String[] headers) { + if (headers == null) { + return null; + } + + if (headers.length % 2 != 0) { + throw new IllegalArgumentException("The headers array must consist of key-value pairs"); + } + + Map result = new HashMap<>(headers.length / 2); + + for (int i = 0; i < headers.length; i += 2) { + result.put(headers[i], headers[i + 1]); + } + + return result; + } +} diff --git a/src/newTest/java/org/opensearch/test/framework/cluster/LocalCluster.java b/src/newTest/java/org/opensearch/test/framework/cluster/LocalCluster.java new file mode 100644 index 0000000000..3fddec2f15 --- /dev/null +++ b/src/newTest/java/org/opensearch/test/framework/cluster/LocalCluster.java @@ -0,0 +1,373 @@ +/* + * Copyright 2015-2021 floragunn GmbH + * + * Licensed 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. + * + */ + +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.test.framework.cluster; + +import java.io.File; +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.junit.rules.ExternalResource; +import org.opensearch.client.Client; +import org.opensearch.common.settings.Settings; +import org.opensearch.node.PluginAwareNode; +import org.opensearch.plugins.Plugin; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.test.framework.TestIndex; +import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.TestSecurityConfig.Role; +import org.opensearch.test.framework.TestSecurityConfig.RoleMapping; +import org.opensearch.test.framework.certificate.TestCertificates; + + +public class LocalCluster extends ExternalResource implements AutoCloseable, OpenSearchClientProvider { + + private static final Logger log = LogManager.getLogger(LocalCluster.class); + + static { + System.setProperty("security.default_init.dir", new File("./securityconfig").getAbsolutePath()); + } + + protected static final AtomicLong num = new AtomicLong(); + + private final List> plugins; + private final ClusterConfiguration clusterConfiguration; + private final TestSecurityConfig testSecurityConfig; + private Settings nodeOverride; + private final String clusterName; + private final MinimumSecuritySettingsSupplierFactory minimumOpenSearchSettingsSupplierFactory; + private final TestCertificates testCertificates; + private final List clusterDependencies; + private final Map remotes; + private volatile LocalOpenSearchCluster localOpenSearchCluster; + private final List testIndices; + + private LocalCluster(String clusterName, TestSecurityConfig testSgConfig, Settings nodeOverride, + ClusterConfiguration clusterConfiguration, List> plugins, TestCertificates testCertificates, + List clusterDependencies, Map remotes, List testIndices) { + this.plugins = plugins; + this.testCertificates = testCertificates; + this.clusterConfiguration = clusterConfiguration; + this.testSecurityConfig = testSgConfig; + this.nodeOverride = nodeOverride; + this.clusterName = clusterName; + this.minimumOpenSearchSettingsSupplierFactory = new MinimumSecuritySettingsSupplierFactory(testCertificates); + this.remotes = remotes; + this.clusterDependencies = clusterDependencies; + this.testIndices = testIndices; + } + + @Override + public void before() throws Throwable { + if (localOpenSearchCluster == null) { + for (LocalCluster dependency : clusterDependencies) { + if (!dependency.isStarted()) { + dependency.before(); + } + } + + for (Map.Entry entry : remotes.entrySet()) { + @SuppressWarnings("resource") + InetSocketAddress transportAddress = entry.getValue().localOpenSearchCluster.masterNode().getTransportAddress(); + nodeOverride = Settings.builder().put(nodeOverride) + .putList("cluster.remote." + entry.getKey() + ".seeds", transportAddress.getHostString() + ":" + transportAddress.getPort()) + .build(); + } + + start(); + } + } + + @Override + protected void after() { + if (localOpenSearchCluster != null && localOpenSearchCluster.isStarted()) { + try { + Thread.sleep(1234); + localOpenSearchCluster.destroy(); + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + localOpenSearchCluster = null; + } + } + } + + @Override + public void close() { + if (localOpenSearchCluster != null && localOpenSearchCluster.isStarted()) { + try { + Thread.sleep(100); + localOpenSearchCluster.destroy(); + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + localOpenSearchCluster = null; + } + } + } + + @Override + public String getClusterName() { + return clusterName; + } + + @Override + public InetSocketAddress getHttpAddress() { + return localOpenSearchCluster.clientNode().getHttpAddress(); + } + + @Override + public InetSocketAddress getTransportAddress() { + return localOpenSearchCluster.clientNode().getTransportAddress(); + } + + public Client getInternalNodeClient() { + return localOpenSearchCluster.clientNode().getInternalNodeClient(); + } + + public PluginAwareNode node() { + return this.localOpenSearchCluster.masterNode().esNode(); + } + + public List nodes() { + return this.localOpenSearchCluster.getAllNodes(); + } + + public LocalOpenSearchCluster.Node getNodeByName(String name) { + return this.localOpenSearchCluster.getNodeByName(name); + } + + public LocalOpenSearchCluster.Node getRandomClientNode() { + return this.localOpenSearchCluster.randomClientNode(); + } + + public boolean isStarted() { + return localOpenSearchCluster != null; + } + + public Random getRandom() { + return localOpenSearchCluster.getRandom(); + } + + private void start() { + try { + localOpenSearchCluster = new LocalOpenSearchCluster(clusterName, clusterConfiguration, + minimumOpenSearchSettingsSupplierFactory.minimumOpenSearchSettings(nodeOverride), plugins, testCertificates); + + localOpenSearchCluster.start(); + + + if (testSecurityConfig != null) { + initSecurityIndex(testSecurityConfig); + } + + try (Client client = getInternalNodeClient()) { + for (TestIndex index : this.testIndices) { + index.create(client); + } + } + + } catch (Exception e) { + log.error("Local ES cluster start failed", e); + throw new RuntimeException(e); + } + } + + private void initSecurityIndex(TestSecurityConfig testSecurityConfig) { + log.info("Initializing OpenSearch Security index"); + Client client = new ContextHeaderDecoratorClient(this.getInternalNodeClient(), Map.of(ConfigConstants.OPENDISTRO_SECURITY_CONF_REQUEST_HEADER , "true")); + testSecurityConfig.initIndex(client); + } + + public static class Builder { + + private final Settings.Builder nodeOverrideSettingsBuilder = Settings.builder(); + private final List> plugins = new ArrayList<>(); + private Map remoteClusters = new HashMap<>(); + private List clusterDependencies = new ArrayList<>(); + private List testIndices = new ArrayList<>(); + private ClusterConfiguration clusterConfiguration = ClusterConfiguration.DEFAULT; + private TestSecurityConfig testSecurityConfig = new TestSecurityConfig(); + private String clusterName = "local_cluster"; + private TestCertificates testCertificates; + + public Builder() { + this.testCertificates = new TestCertificates(); + } + + public Builder dependsOn(Object object) { + // We just want to make sure that the object is already done + if (object == null) { + throw new IllegalStateException("Dependency not fulfilled"); + } + return this; + } + + public Builder clusterConfiguration(ClusterConfiguration clusterConfiguration) { + this.clusterConfiguration = clusterConfiguration; + return this; + } + + public Builder singleNode() { + this.clusterConfiguration = ClusterConfiguration.SINGLENODE; + return this; + } + + public Builder sgConfig(TestSecurityConfig testSgConfig) { + this.testSecurityConfig = testSgConfig; + return this; + } + + public Builder nodeSettings(Object... settings) { + for (int i = 0; i < settings.length - 1; i += 2) { + String key = String.valueOf(settings[i]); + Object value = settings[i + 1]; + + if (value instanceof List) { + List values = ((List) value).stream().map(String::valueOf).collect(Collectors.toList()); + nodeOverrideSettingsBuilder.putList(key, values); + } else { + nodeOverrideSettingsBuilder.put(key, String.valueOf(value)); + } + } + + return this; + } + + public Builder plugin(Class plugin) { + this.plugins.add(plugin); + + return this; + } + + public Builder remote(String name, LocalCluster anotherCluster) { + remoteClusters.put(name, anotherCluster); + + clusterDependencies.add(anotherCluster); + + return this; + } + + public Builder indices(TestIndex... indices) { + this.testIndices.addAll(Arrays.asList(indices)); + return this; + } + + public Builder users(TestSecurityConfig.User... users) { + for (TestSecurityConfig.User user : users) { + testSecurityConfig.user(user); + } + return this; + } + + public Builder user(TestSecurityConfig.User user) { + testSecurityConfig.user(user); + return this; + } + + public Builder user(String name, String password, String... sgRoles) { + testSecurityConfig.user(name, password, sgRoles); + return this; + } + + public Builder user(String name, String password, Role... sgRoles) { + testSecurityConfig.user(name, password, sgRoles); + return this; + } + + public Builder roles(Role... roles) { + testSecurityConfig.roles(roles); + return this; + } + + public Builder roleMapping(RoleMapping... mappings) { + testSecurityConfig.roleMapping(mappings); + return this; + } + + public Builder roleToRoleMapping(Role role, String... backendRoles) { + testSecurityConfig.roleToRoleMapping(role, backendRoles); + return this; + } + + public Builder authc(TestSecurityConfig.AuthcDomain authc) { + testSecurityConfig.authc(authc); + return this; + } + + public Builder var(String name, Supplier value) { + testSecurityConfig.var(name, value); + return this; + } + + public Builder clusterName(String clusterName) { + this.clusterName = clusterName; + return this; + } + + public Builder configIndexName(String configIndexName) { + testSecurityConfig.configIndexName(configIndexName); + return this; + } + + public Builder anonymousAuth(boolean anonAuthEnabled) { + testSecurityConfig.anonymousAuth(anonAuthEnabled); + return this; + } + + public LocalCluster build() { + try { + + clusterName += "_" + num.incrementAndGet(); + + return new LocalCluster(clusterName, testSecurityConfig, nodeOverrideSettingsBuilder.build(), clusterConfiguration, plugins, + testCertificates, clusterDependencies, remoteClusters, testIndices); + } catch (Exception e) { + log.error("Failed to build LocalCluster", e); + throw new RuntimeException(e); + } + } + + } + + @Override + public TestCertificates getTestCertificates() { + return testCertificates; + } + +} diff --git a/src/newTest/java/org/opensearch/test/framework/cluster/LocalOpenSearchCluster.java b/src/newTest/java/org/opensearch/test/framework/cluster/LocalOpenSearchCluster.java new file mode 100644 index 0000000000..4ae2c8b956 --- /dev/null +++ b/src/newTest/java/org/opensearch/test/framework/cluster/LocalOpenSearchCluster.java @@ -0,0 +1,610 @@ +/* + * Copyright 2015-2021 floragunn GmbH + * + * Licensed 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. + * + */ + +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.test.framework.cluster; + +import static org.junit.Assert.assertEquals; + +import java.io.File; +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Random; +import java.util.SortedSet; +import java.util.concurrent.CompletableFuture; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import org.apache.commons.io.FileUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.opensearch.OpenSearchTimeoutException; +import org.opensearch.action.admin.cluster.health.ClusterHealthResponse; +import org.opensearch.action.admin.cluster.node.info.NodeInfo; +import org.opensearch.action.admin.cluster.node.info.NodesInfoRequest; +import org.opensearch.action.admin.cluster.node.info.NodesInfoResponse; +import org.opensearch.action.admin.indices.template.put.PutIndexTemplateRequest; +import org.opensearch.action.support.master.AcknowledgedResponse; +import org.opensearch.client.AdminClient; +import org.opensearch.client.Client; +import org.opensearch.cluster.health.ClusterHealthStatus; +import org.opensearch.cluster.node.DiscoveryNodeRole; +import org.opensearch.common.Strings; +import org.opensearch.common.settings.Settings; +import org.opensearch.common.transport.TransportAddress; +import org.opensearch.common.unit.TimeValue; +import org.opensearch.common.xcontent.XContentType; +import org.opensearch.http.BindHttpException; +import org.opensearch.http.HttpInfo; +import org.opensearch.node.PluginAwareNode; +import org.opensearch.plugins.Plugin; +import org.opensearch.test.framework.certificate.TestCertificates; +import org.opensearch.test.framework.cluster.ClusterConfiguration.NodeSettings; +import org.opensearch.transport.BindTransportException; +import org.opensearch.transport.TransportInfo; + +import com.google.common.net.InetAddresses; + + +public class LocalOpenSearchCluster { + + static { + System.setProperty("opensearch.enforce.bootstrap.checks", "true"); + } + + private static final Logger log = LogManager.getLogger(LocalOpenSearchCluster.class); + + private final String clusterName; + private final ClusterConfiguration clusterConfiguration; + private final NodeSettingsSupplier nodeSettingsSupplier; + private final List> additionalPlugins; + private final List allNodes = new ArrayList<>(); + private final List masterNodes = new ArrayList<>(); + private final List dataNodes = new ArrayList<>(); + private final List clientNodes = new ArrayList<>(); + private final TestCertificates testCertificates; + + private File clusterHomeDir; + private List seedHosts; + private List initialMasterHosts; + private int retry = 0; + private boolean started; + private Random random = new Random(); + + public LocalOpenSearchCluster(String clusterName, ClusterConfiguration clusterConfiguration, NodeSettingsSupplier nodeSettingsSupplier, + List> additionalPlugins, TestCertificates testCertificates) { + this.clusterName = clusterName; + this.clusterConfiguration = clusterConfiguration; + this.nodeSettingsSupplier = nodeSettingsSupplier; + this.additionalPlugins = additionalPlugins; + this.testCertificates = testCertificates; + try { + this.clusterHomeDir = Files.createTempDirectory("local_cluster_" + clusterName).toFile(); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + public void start() throws Exception { + log.info("Starting {}", clusterName); + + int forkNumber = getUnitTestForkNumber(); + int masterNodeCount = clusterConfiguration.getMasterNodes(); + int nonMasterNodeCount = clusterConfiguration.getDataNodes() + clusterConfiguration.getClientNodes(); + + SortedSet masterNodeTransportPorts = PortAllocator.TCP.allocate(clusterName, Math.max(masterNodeCount, 4), 5000 + forkNumber * 1000 + 300); + SortedSet masterNodeHttpPorts = PortAllocator.TCP.allocate(clusterName, masterNodeCount, 5000 + forkNumber * 1000 + 200); + + this.seedHosts = toHostList(masterNodeTransportPorts); + this.initialMasterHosts = toHostList(masterNodeTransportPorts.stream().limit(masterNodeCount).collect(Collectors.toSet())); + + started = true; + + CompletableFuture masterNodeFuture = startNodes(clusterConfiguration.getMasterNodeSettings(), masterNodeTransportPorts, + masterNodeHttpPorts); + + SortedSet nonMasterNodeTransportPorts = PortAllocator.TCP.allocate(clusterName, nonMasterNodeCount, 5000 + forkNumber * 1000 + 310); + SortedSet nonMasterNodeHttpPorts = PortAllocator.TCP.allocate(clusterName, nonMasterNodeCount, 5000 + forkNumber * 1000 + 210); + + CompletableFuture nonMasterNodeFuture = startNodes(clusterConfiguration.getNonMasterNodeSettings(), nonMasterNodeTransportPorts, + nonMasterNodeHttpPorts); + + CompletableFuture.allOf(masterNodeFuture, nonMasterNodeFuture).join(); + + if (isNodeFailedWithPortCollision()) { + log.info("Detected port collision for master node. Retrying."); + + retry(); + return; + } + + log.info("Startup finished. Waiting for GREEN"); + + waitForCluster(ClusterHealthStatus.GREEN, TimeValue.timeValueSeconds(10), allNodes.size()); + putDefaultTemplate(); + + log.info("Started: {}", this); + + } + + public String getClusterName() { + return clusterName; + } + + public boolean isStarted() { + return started; + } + + public void stop() { + + for (Node node : clientNodes) { + node.stop(); + } + + for (Node node : dataNodes) { + node.stop(); + } + + for (Node node : masterNodes) { + node.stop(); + } + } + + public void destroy() { + stop(); + clientNodes.clear(); + dataNodes.clear(); + masterNodes.clear(); + + try { + FileUtils.deleteDirectory(clusterHomeDir); + } catch (IOException e) { + log.warn("Error while deleting " + clusterHomeDir, e); + } + } + + public Node clientNode() { + return findRunningNode(clientNodes, dataNodes, masterNodes); + } + + public Node randomClientNode() { + return randomRunningNode(clientNodes, dataNodes, masterNodes); + } + + public Node masterNode() { + return findRunningNode(masterNodes); + } + + public List getAllNodes() { + return Collections.unmodifiableList(allNodes); + } + + public Node getNodeByName(String name) { + return allNodes.stream().filter(node -> node.getNodeName().equals(name)).findAny().orElseThrow(() -> new RuntimeException( + "No such node with name: " + name + "; available: " + allNodes.stream().map(Node::getNodeName).collect(Collectors.toList()))); + } + + private boolean isNodeFailedWithPortCollision() { + return allNodes.stream().anyMatch(Node::isPortCollision); + } + + private void retry() throws Exception { + retry++; + + if (retry > 10) { + throw new RuntimeException("Detected port collisions for master node. Giving up."); + } + + stop(); + + this.allNodes.clear(); + this.masterNodes.clear(); + this.dataNodes.clear(); + this.clientNodes.clear(); + this.seedHosts = null; + this.initialMasterHosts = null; + this.clusterHomeDir = Files.createTempDirectory("local_cluster_" + clusterName + "_retry_" + retry).toFile(); + + start(); + } + + @SafeVarargs + private final Node findRunningNode(List nodes, List... moreNodes) { + for (Node node : nodes) { + if (node.isRunning()) { + return node; + } + } + + if (moreNodes != null && moreNodes.length > 0) { + for (List nodesList : moreNodes) { + for (Node node : nodesList) { + if (node.isRunning()) { + return node; + } + } + } + } + + return null; + } + + @SafeVarargs + private final Node randomRunningNode(List nodes, List... moreNodes) { + ArrayList runningNodes = new ArrayList<>(); + + for (Node node : nodes) { + if (node.isRunning()) { + runningNodes.add(node); + } + } + + if (moreNodes != null && moreNodes.length > 0) { + for (List nodesList : moreNodes) { + for (Node node : nodesList) { + if (node.isRunning()) { + runningNodes.add(node); + } + } + } + } + + if (runningNodes.size() == 0) { + return null; + } + + int index = this.random.nextInt(runningNodes.size()); + + return runningNodes.get(index); + } + + private void putDefaultTemplate() { + String defaultTemplate = "{\n" + " \"index_patterns\": [\"*\"],\n" + " \"order\": -1,\n" + " \"settings\": {\n" + + " \"number_of_shards\": \"5\",\n" + " \"number_of_replicas\": \"1\"\n" + " }\n" // + + " }"; + + AcknowledgedResponse templateAck = clientNode().getInternalNodeClient().admin().indices() + .putTemplate(new PutIndexTemplateRequest("default").source(defaultTemplate, XContentType.JSON)).actionGet(); + + if (!templateAck.isAcknowledged()) { + throw new RuntimeException("Default template could not be created"); + } + } + + private CompletableFuture startNodes(List nodeSettingList, SortedSet transportPorts, SortedSet httpPorts) { + Iterator transportPortIterator = transportPorts.iterator(); + Iterator httpPortIterator = httpPorts.iterator(); + List> futures = new ArrayList<>(); + + for (NodeSettings nodeSettings : nodeSettingList) { + Node node = new Node(nodeSettings, transportPortIterator.next(), httpPortIterator.next()); + futures.add(node.start()); + } + + return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); + } + + public ClusterInfo waitForCluster(ClusterHealthStatus status, TimeValue timeout, int expectedNodeCount) throws IOException { + ClusterInfo clusterInfo = new ClusterInfo(); + Client client = clientNode().getInternalNodeClient(); + + try { + log.debug("waiting for cluster state {} and {} nodes", status.name(), expectedNodeCount); + AdminClient adminClient = client.admin(); + + final ClusterHealthResponse healthResponse = adminClient.cluster().prepareHealth().setWaitForStatus(status).setTimeout(timeout) + .setMasterNodeTimeout(timeout).setWaitForNodes("" + expectedNodeCount).execute().actionGet(); + + if (log.isDebugEnabled()) { + log.debug("Current ClusterState:\n{}", Strings.toString(healthResponse)); + } + + if (healthResponse.isTimedOut()) { + throw new IOException( + "cluster state is " + healthResponse.getStatus().name() + " with " + healthResponse.getNumberOfNodes() + " nodes"); + } else { + log.debug("... cluster state ok {} with {} nodes", healthResponse.getStatus().name(), healthResponse.getNumberOfNodes()); + } + + assertEquals(expectedNodeCount, healthResponse.getNumberOfNodes()); + + final NodesInfoResponse nodesInfoResponse = adminClient.cluster().nodesInfo(new NodesInfoRequest()).actionGet(); + + final List nodes = nodesInfoResponse.getNodes(); + + final List masterNodes = nodes.stream() + .filter(nodeInfo -> nodeInfo.getNode().getRoles().contains(DiscoveryNodeRole.CLUSTER_MANAGER_ROLE)).collect(Collectors.toList()); + + final List dataNodes = nodes.stream().filter(nodeInfo -> nodeInfo.getNode().getRoles().contains(DiscoveryNodeRole.DATA_ROLE) + && !nodeInfo.getNode().getRoles().contains(DiscoveryNodeRole.CLUSTER_MANAGER_ROLE)).collect(Collectors.toList()); + + final List clientNodes = nodes.stream() + .filter(nodeInfo -> !nodeInfo.getNode().getRoles().contains(DiscoveryNodeRole.CLUSTER_MANAGER_ROLE) + && !nodeInfo.getNode().getRoles().contains(DiscoveryNodeRole.DATA_ROLE)) + .collect(Collectors.toList()); + + for (NodeInfo nodeInfo : masterNodes) { + final TransportAddress is = nodeInfo.getInfo(TransportInfo.class).getAddress().publishAddress(); + clusterInfo.nodePort = is.getPort(); + clusterInfo.nodeHost = is.getAddress(); + } + + Predicate hasHttpAddressPredicate = nodeInfo -> nodeInfo.getInfo(HttpInfo.class) != null + && nodeInfo.getInfo(HttpInfo.class).address() != null; + + if (!clientNodes.isEmpty()) { + NodeInfo nodeInfo = clientNodes.get(0); + if (hasHttpAddressPredicate.test(nodeInfo)) { + final TransportAddress his = nodeInfo.getInfo(HttpInfo.class).address().publishAddress(); + clusterInfo.httpPort = his.getPort(); + clusterInfo.httpHost = his.getAddress(); + } else { + throw new RuntimeException("no http host/port for client node"); + } + } else if (!dataNodes.isEmpty()) { + for (NodeInfo nodeInfo : dataNodes) { + if (hasHttpAddressPredicate.test(nodeInfo)) { + final TransportAddress his = nodeInfo.getInfo(HttpInfo.class).address().publishAddress(); + clusterInfo.httpPort = his.getPort(); + clusterInfo.httpHost = his.getAddress(); + break; + } + } + } else { + for (NodeInfo nodeInfo : nodes) { + if (hasHttpAddressPredicate.test(nodeInfo)) { + final TransportAddress his = nodeInfo.getInfo(HttpInfo.class).address().publishAddress(); + clusterInfo.httpPort = his.getPort(); + clusterInfo.httpHost = his.getAddress(); + break; + } + } + } + + for (NodeInfo nodeInfo : nodes) { + clusterInfo.httpAdresses.add(nodeInfo.getInfo(HttpInfo.class).address().publishAddress()); + } + } catch (final OpenSearchTimeoutException e) { + throw new IOException("timeout, cluster does not respond to health request, cowardly refusing to continue with operations"); + } + return clusterInfo; + } + + @Override + public String toString() { + return "\nES Cluster " + clusterName + "\nmaster nodes: " + masterNodes + "\n data nodes: " + dataNodes + "\nclient nodes: " + clientNodes + + "\n"; + } + + private static List toHostList(Collection ports) { + return ports.stream().map(port -> "127.0.0.1:" + port).collect(Collectors.toList()); + } + + private String createNextNodeName(NodeSettings nodeSettings) { + List nodes; + String nodeType; + + if (nodeSettings.masterNode) { + nodes = this.masterNodes; + nodeType = "master"; + } else if (nodeSettings.dataNode) { + nodes = this.dataNodes; + nodeType = "data"; + } else { + nodes = this.clientNodes; + nodeType = "client"; + } + + return nodeType + "_" + nodes.size(); + } + + public class Node implements OpenSearchClientProvider { + private final String nodeName; + private final NodeSettings nodeSettings; + private final File nodeHomeDir; + private final File dataDir; + private final File logsDir; + private final int transportPort; + private final int httpPort; + private final InetSocketAddress httpAddress; + private final InetSocketAddress transportAddress; + private PluginAwareNode node; + private boolean running = false; + private boolean portCollision = false; + + Node(NodeSettings nodeSettings, int transportPort, int httpPort) { + this.nodeName = createNextNodeName(nodeSettings); + this.nodeSettings = nodeSettings; + this.nodeHomeDir = new File(clusterHomeDir, nodeName); + this.dataDir = new File(this.nodeHomeDir, "data"); + this.logsDir = new File(this.nodeHomeDir, "logs"); + this.transportPort = transportPort; + this.httpPort = httpPort; + InetAddress hostAddress = InetAddresses.forString("127.0.0.1"); + this.httpAddress = new InetSocketAddress(hostAddress, httpPort); + this.transportAddress = new InetSocketAddress(hostAddress, transportPort); + + if (nodeSettings.masterNode) { + masterNodes.add(this); + } else if (nodeSettings.dataNode) { + dataNodes.add(this); + } else { + clientNodes.add(this); + } + allNodes.add(this); + } + + CompletableFuture start() { + CompletableFuture completableFuture = new CompletableFuture<>(); + + this.node = new PluginAwareNode(nodeSettings.masterNode, getEsSettings(), nodeSettings.getPlugins(additionalPlugins)); + + new Thread(new Runnable() { + + @Override + public void run() { + try { + node.start(); + running = true; + completableFuture.complete("initialized"); + } catch (BindTransportException | BindHttpException e) { + log.warn("Port collision detected for {}", this, e); + portCollision = true; + try { + node.close(); + } catch (IOException e1) { + log.error(e1); + } + + node = null; + PortAllocator.TCP.reserve(transportPort, httpPort); + + completableFuture.complete("retry"); + + } catch (Throwable e) { + log.error("Unable to start {}", this, e); + node = null; + completableFuture.completeExceptionally(e); + } + } + }).start(); + + return completableFuture; + } + + public Client getInternalNodeClient() { + return node.client(); + } + + public PluginAwareNode esNode() { + return node; + } + + public boolean isRunning() { + return running; + } + + public X getInjectable(Class clazz) { + return node.injector().getInstance(clazz); + } + + public void stop() { + try { + log.info("Stopping {}", this); + + running = false; + + if (node != null) { + node.close(); + node = null; + Thread.sleep(10); + } + + } catch (Throwable e) { + log.warn("Error while stopping " + this, e); + } + } + + @Override + public String toString() { + String state = running ? "RUNNING" : node != null ? "INITIALIZING" : "STOPPED"; + + return nodeName + " " + state + " [" + transportPort + ", " + httpPort + "]"; + } + + public boolean isPortCollision() { + return portCollision; + } + + public String getNodeName() { + return nodeName; + } + + @Override + public InetSocketAddress getHttpAddress() { + return httpAddress; + } + + @Override + public InetSocketAddress getTransportAddress() { + return transportAddress; + } + + private Settings getEsSettings() { + Settings settings = getMinimalEsSettings(); + + if (nodeSettingsSupplier != null) { + // TODO node number + return Settings.builder().put(settings).put(nodeSettingsSupplier.get(0)).build(); + } + + return settings; + } + + private Settings getMinimalEsSettings() { + return Settings.builder().put("node.name", nodeName).put("node.data", nodeSettings.dataNode).put("node.master", nodeSettings.masterNode) + .put("cluster.name", clusterName).put("path.home", nodeHomeDir.toPath()).put("path.data", dataDir.toPath()) + .put("path.logs", logsDir.toPath()).putList("cluster.initial_master_nodes", initialMasterHosts) + .put("discovery.initial_state_timeout", "8s").putList("discovery.seed_hosts", seedHosts).put("transport.tcp.port", transportPort) + .put("http.port", httpPort).put("cluster.routing.allocation.disk.threshold_enabled", false) + .put("discovery.probe.connect_timeout", "10s").put("discovery.probe.handshake_timeout", "10s").put("http.cors.enabled", true) + .build(); + } + + @Override + public String getClusterName() { + return clusterName; + } + + @Override + public TestCertificates getTestCertificates() { + return testCertificates; + } + } + + private static int getUnitTestForkNumber() { + String forkno = System.getProperty("forkno"); + + if (forkno != null && forkno.length() > 0) { + return Integer.parseInt(forkno.split("_")[1]); + } else { + return 42; + } + } + + public Random getRandom() { + return random; + } + +} diff --git a/src/newTest/java/org/opensearch/test/framework/cluster/MinimumSecuritySettingsSupplierFactory.java b/src/newTest/java/org/opensearch/test/framework/cluster/MinimumSecuritySettingsSupplierFactory.java new file mode 100644 index 0000000000..37728c14cc --- /dev/null +++ b/src/newTest/java/org/opensearch/test/framework/cluster/MinimumSecuritySettingsSupplierFactory.java @@ -0,0 +1,80 @@ +/* + * Copyright 2021 floragunn GmbH + * + * Licensed 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. + * + */ + +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.test.framework.cluster; + +import java.io.IOException; + +import org.opensearch.common.settings.Settings; +import org.opensearch.test.framework.certificate.TestCertificates; + +public class MinimumSecuritySettingsSupplierFactory { + + private TestCertificates testCertificates; + + public MinimumSecuritySettingsSupplierFactory(TestCertificates testCertificates) { + if (testCertificates == null) { + throw new IllegalArgumentException("certificates must not be null"); + } + this.testCertificates = testCertificates; + + } + + public NodeSettingsSupplier minimumOpenSearchSettings(Settings other) { + return i -> minimumOpenSearchSettingsBuilder(i, false).put(other).build(); + } + + public NodeSettingsSupplier minimumOpenSearchSettingsSslOnly(Settings other) { + return i -> minimumOpenSearchSettingsBuilder(i, true).put(other).build(); + } + + private Settings.Builder minimumOpenSearchSettingsBuilder(int node, boolean sslOnly) { + + Settings.Builder builder = Settings.builder(); + + // TODO: At the moment the test node certificates have an OID set, so we do not need to + // specify any node_dns here. Once we make generating and specifying + try { + builder.put("plugins.security.ssl.transport.pemtrustedcas_filepath", testCertificates.getRootCertificate().getAbsolutePath()); + builder.put("plugins.security.ssl.transport.pemcert_filepath", testCertificates.getNodeCertificate(node).getAbsolutePath()); + builder.put("plugins.security.ssl.transport.pemkey_filepath", testCertificates.getNodeKey(node).getAbsolutePath()); + + builder.put("plugins.security.ssl.http.enabled", true); + builder.put("plugins.security.ssl.http.pemtrustedcas_filepath", testCertificates.getRootCertificate().getAbsolutePath()); + builder.put("plugins.security.ssl.http.pemcert_filepath", testCertificates.getNodeCertificate(node).getAbsolutePath()); + builder.put("plugins.security.ssl.http.pemkey_filepath", testCertificates.getNodeKey(node).getAbsolutePath()); + + builder.putList("plugins.security.authcz.admin_dn", testCertificates.getAdminDNs()); + } catch (IOException e) { + throw new IllegalArgumentException("Invalid test certificates provided on local cluster start"); + } + + return builder; + + } +} diff --git a/src/newTest/java/org/opensearch/test/framework/cluster/NestedValueMap.java b/src/newTest/java/org/opensearch/test/framework/cluster/NestedValueMap.java new file mode 100644 index 0000000000..0ed52e203f --- /dev/null +++ b/src/newTest/java/org/opensearch/test/framework/cluster/NestedValueMap.java @@ -0,0 +1,518 @@ +/* + * Copyright 2021-2022 floragunn GmbH + * + * Licensed 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. + * + */ + +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.test.framework.cluster; + +import java.io.File; +import java.lang.reflect.Array; +import java.net.URI; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import com.google.common.collect.MapMaker; + +public class NestedValueMap extends HashMap { + + private static final long serialVersionUID = 2953312818482932741L; + + private Map originalToCloneMap; + private final boolean cloneWhilePut; + private boolean writable = true; + + public NestedValueMap() { + originalToCloneMap = new MapMaker().weakKeys().makeMap(); + cloneWhilePut = true; + } + + public NestedValueMap(int initialCapacity) { + super(initialCapacity); + originalToCloneMap = new MapMaker().weakKeys().makeMap(); + cloneWhilePut = true; + } + + NestedValueMap(Map originalToCloneMap, boolean cloneWhilePut) { + this.originalToCloneMap = originalToCloneMap; + this.cloneWhilePut = cloneWhilePut; + } + + NestedValueMap(int initialCapacity, Map originalToCloneMap, boolean cloneWhilePut) { + super(initialCapacity); + this.originalToCloneMap = originalToCloneMap; + this.cloneWhilePut = cloneWhilePut; + } + + @Override + public NestedValueMap clone() { + NestedValueMap result = new NestedValueMap(Math.max(this.size(), 10), + this.originalToCloneMap != null ? new MapMaker().weakKeys().makeMap() : null, this.cloneWhilePut); + + result.putAll(this); + + return result; + } + + public NestedValueMap without(String... keys) { + NestedValueMap result = new NestedValueMap(Math.max(this.size(), 10), + this.originalToCloneMap != null ? new MapMaker().weakKeys().makeMap() : null, this.cloneWhilePut); + + Set withoutKeySet = new HashSet<>(Arrays.asList(keys)); + + for (Map.Entry entry : this.entrySet()) { + if (!withoutKeySet.contains(entry.getKey())) { + result.put(entry.getKey(), entry.getValue()); + } + } + + return result; + } + + public static NestedValueMap copy(Map data) { + NestedValueMap result = new NestedValueMap(data.size()); + + result.putAllFromAnyMap(data); + + return result; + } + + public static NestedValueMap copy(Object data) { + if (data instanceof Map) { + return copy((Map) data); + } else { + NestedValueMap result = new NestedValueMap(); + result.put("_value", data); + return result; + } + } + + public static NestedValueMap createNonCloningMap() { + return new NestedValueMap(null, false); + } + + public static NestedValueMap createUnmodifieableMap(Map data) { + NestedValueMap result = new NestedValueMap(data.size()); + + result.putAllFromAnyMap(data); + result.seal(); + + return result; + } + +// public static NestedValueMap fromJsonString(String jsonString) throws IOException, DocumentParseException, UnexpectedDocumentStructureException { +// return NestedValueMap.copy(DocReader.json().readObject(jsonString)); +// } +// +// public static NestedValueMap fromYaml(String yamlString) throws IOException, DocumentParseException { +// return NestedValueMap.copy(DocReader.yaml().read(yamlString)); +// } +// +// public static NestedValueMap fromYaml(InputStream inputSteam) throws DocumentParseException, IOException { +// return NestedValueMap.copy(DocReader.yaml().read(inputSteam)); +// } + + public static NestedValueMap of(String key1, Object value1) { + NestedValueMap result = new NestedValueMap(1); + result.put(key1, value1); + return result; + } + + public static NestedValueMap of(String key1, Object value1, String key2, Object value2) { + NestedValueMap result = new NestedValueMap(2); + result.put(key1, value1); + result.put(key2, value2); + return result; + } + + public static NestedValueMap of(String key1, Object value1, String key2, Object value2, String key3, Object value3) { + NestedValueMap result = new NestedValueMap(3); + result.put(key1, value1); + result.put(key2, value2); + result.put(key3, value3); + + return result; + } + + public static NestedValueMap of(String key1, Object value1, String key2, Object value2, String key3, Object value3, Object... furtherEntries) { + NestedValueMap result = new NestedValueMap(3 + furtherEntries.length); + result.put(key1, value1); + result.put(key2, value2); + result.put(key3, value3); + + for (int i = 0; i < furtherEntries.length - 1; i += 2) { + result.put(String.valueOf(furtherEntries[i]), furtherEntries[i + 1]); + } + + return result; + } + + public static NestedValueMap of(Path key1, Object value1) { + NestedValueMap result = new NestedValueMap(1); + result.put(key1, value1); + return result; + } + + public static NestedValueMap of(Path key1, Object value1, Path key2, Object value2) { + NestedValueMap result = new NestedValueMap(2); + result.put(key1, value1); + result.put(key2, value2); + return result; + } + + public static NestedValueMap of(Path key1, Object value1, Path key2, Object value2, Path key3, Object value3) { + NestedValueMap result = new NestedValueMap(3); + result.put(key1, value1); + result.put(key2, value2); + result.put(key3, value3); + + return result; + } + + public static NestedValueMap of(Path key1, Object value1, Path key2, Object value2, Path key3, Object value3, Object... furtherEntries) { + NestedValueMap result = new NestedValueMap(3 + furtherEntries.length); + result.put(key1, value1); + result.put(key2, value2); + result.put(key3, value3); + + for (int i = 0; i < furtherEntries.length - 1; i += 2) { + result.put(Path.parse(String.valueOf(furtherEntries[i])), furtherEntries[i + 1]); + } + + return result; + } + + public Object put(String key, Map data) { + checkWritable(); + + Object result = this.get(key); + NestedValueMap subMap = this.getOrCreateSubMapAt(key, data.size()); + + subMap.putAllFromAnyMap(data); + return result; + } + + public void putAll(Map map) { + checkWritable(); + + for (Map.Entry entry : map.entrySet()) { + String key = String.valueOf(entry.getKey()); + put(key, entry.getValue()); + } + } + + public void putAllFromAnyMap(Map map) { + checkWritable(); + + for (Map.Entry entry : map.entrySet()) { + String key = String.valueOf(entry.getKey()); + put(key, entry.getValue()); + } + } + + public void overrideLeafs(NestedValueMap map) { + checkWritable(); + + for (Map.Entry entry : map.entrySet()) { + String key = String.valueOf(entry.getKey()); + + if (entry.getValue() instanceof NestedValueMap) { + NestedValueMap subMap = (NestedValueMap) entry.getValue(); + + getOrCreateSubMapAt(key, subMap.size()).overrideLeafs(subMap); + } else { + put(key, entry.getValue()); + } + } + } + + public Object put(String key, Object object) { + checkWritable(); + + if (object instanceof Map) { + return put(key, (Map) object); + } + + return super.put(key, deepCloneObject(object)); + } + + public void put(Path path, Object object) { + checkWritable(); + + if (path.isEmpty()) { + if (object instanceof Map) { + putAllFromAnyMap((Map) object); + } else { + throw new IllegalArgumentException("put([], " + object + "): If an empty path is given, the object must be of type map"); + } + + } else { + NestedValueMap subMap = getOrCreateSubMapAtPath(path.withoutLast()); + subMap.put(path.getLast(), object); + } + } + + public Object get(Path path) { + if (path.isEmpty()) { + return this; + } else if (path.length() == 1) { + return this.get(path.getFirst()); + } else { + Object subObject = this.get(path.getFirst()); + + if (subObject instanceof NestedValueMap) { + return ((NestedValueMap) subObject).get(path.withoutFirst()); + } else { + return null; + } + } + } + + public void seal() { + if (!this.writable) { + return; + } + + this.writable = false; + this.originalToCloneMap = null; + + for (Object value : this.values()) { + if (value instanceof NestedValueMap) { + NestedValueMap subMap = (NestedValueMap) value; + subMap.seal(); + } else if (value instanceof Iterable) { + for (Object subValue : ((Iterable) value)) { + if (subValue instanceof NestedValueMap) { + NestedValueMap subMap = (NestedValueMap) subValue; + subMap.seal(); + } + } + } + } + } + +// public String toJsonString() { +// return DocWriter.json().writeAsString(this); +// } +// +// public String toYamlString() { +// return DocWriter.yaml().writeAsString(this); +// } + + private Object deepCloneObject(Object object) { + if (!cloneWhilePut || object == null || isImmutable(object)) { + return object; + } + + Object clone = this.originalToCloneMap.get(object); + + if (clone != null) { + return clone; + } + + if (object instanceof Set) { + Set set = (Set) object; + Set copy = new HashSet<>(set.size()); + this.originalToCloneMap.put(object, copy); + + for (Object element : set) { + copy.add(deepCloneObject(element)); + } + + return copy; + } else if (object instanceof Map) { + Map map = (Map) object; + NestedValueMap copy = new NestedValueMap(map.size(), this.originalToCloneMap, this.cloneWhilePut); + this.originalToCloneMap.put(object, copy); + + for (Map.Entry entry : map.entrySet()) { + copy.put((String) deepCloneObject(String.valueOf(entry.getKey())), deepCloneObject(entry.getValue())); + } + + return copy; + } else if (object instanceof Collection) { + Collection collection = (Collection) object; + ArrayList copy = new ArrayList<>(collection.size()); + this.originalToCloneMap.put(object, copy); + + for (Object element : collection) { + copy.add(deepCloneObject(element)); + } + + return copy; + } else if (object.getClass().isArray()) { + int length = Array.getLength(object); + Object copy = Array.newInstance(object.getClass().getComponentType(), length); + this.originalToCloneMap.put(object, copy); + + for (int i = 0; i < length; i++) { + Array.set(copy, i, deepCloneObject(Array.get(object, i))); + } + + return copy; + } else { + // Hope the best + + return object; + } + } + + private boolean isImmutable(Object object) { + return object instanceof String || object instanceof Number || object instanceof Boolean || object instanceof Void || object instanceof Class + || object instanceof Character || object instanceof Enum || object instanceof File || object instanceof UUID || object instanceof URL + || object instanceof URI; + } + + private NestedValueMap getOrCreateSubMapAt(String key, int capacity) { + Object value = this.get(key); + + if (value instanceof NestedValueMap) { + return (NestedValueMap) value; + } else { + if (value instanceof Map) { + capacity = Math.max(capacity, ((Map) value).size()); + } + + NestedValueMap mapValue = new NestedValueMap(capacity, this.originalToCloneMap, this.cloneWhilePut); + + if (value instanceof Map) { + mapValue.putAllFromAnyMap((Map) value); + } + + super.put(key, mapValue); + return mapValue; + } + + } + + private NestedValueMap getOrCreateSubMapAtPath(Path path) { + if (path.isEmpty()) { + return this; + } + + String pathElement = path.getFirst(); + Path remainingPath = path.withoutFirst(); + + Object value = this.get(pathElement); + + if (value instanceof NestedValueMap) { + NestedValueMap mapValue = (NestedValueMap) value; + if (remainingPath.isEmpty()) { + return mapValue; + } else { + return mapValue.getOrCreateSubMapAtPath(remainingPath); + } + } else { + NestedValueMap mapValue = new NestedValueMap(this.originalToCloneMap, this.cloneWhilePut); + super.put(pathElement, mapValue); + + if (remainingPath.isEmpty()) { + return mapValue; + } else { + return mapValue.getOrCreateSubMapAtPath(remainingPath); + } + } + } + + private void checkWritable() { + if (!writable) { + throw new UnsupportedOperationException("Map is not writable"); + } + } + + public static class Path { + private String[] elements; + private int start; + private int end; + + public Path(String... elements) { + this.elements = elements; + this.start = 0; + this.end = elements.length; + } + + private Path(String[] elements, int start, int end) { + this.elements = elements; + this.start = start; + this.end = end; + } + + public String getFirst() { + if (this.start >= this.end) { + return null; + } + + return this.elements[start]; + } + + public String getLast() { + if (this.start >= this.end) { + return null; + } + + return this.elements[end - 1]; + } + + public Path withoutFirst() { + if (this.start >= this.end - 1) { + return new Path(null, 0, 0); + } + + return new Path(elements, start + 1, end); + } + + public Path withoutLast() { + if (this.start >= this.end - 1) { + return new Path(null, 0, 0); + } + + return new Path(elements, start, end - 1); + } + + public int length() { + return this.end - this.start; + } + + public boolean isEmpty() { + return this.start == this.end; + } + + public static Path parse(String path) { + if (path.length() == 0) { + return new Path(new String [0]); + } else { + return new Path(path.split("\\.")); + } + } + } + +} diff --git a/src/newTest/java/org/opensearch/test/framework/cluster/NodeSettingsSupplier.java b/src/newTest/java/org/opensearch/test/framework/cluster/NodeSettingsSupplier.java new file mode 100644 index 0000000000..828eb0f05a --- /dev/null +++ b/src/newTest/java/org/opensearch/test/framework/cluster/NodeSettingsSupplier.java @@ -0,0 +1,34 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.test.framework.cluster; + +import org.opensearch.common.settings.Settings; + +@FunctionalInterface +public interface NodeSettingsSupplier { + Settings get(int i); +} diff --git a/src/newTest/java/org/opensearch/test/framework/cluster/OpenSearchClientProvider.java b/src/newTest/java/org/opensearch/test/framework/cluster/OpenSearchClientProvider.java new file mode 100644 index 0000000000..c98d052c57 --- /dev/null +++ b/src/newTest/java/org/opensearch/test/framework/cluster/OpenSearchClientProvider.java @@ -0,0 +1,133 @@ +/* + * Copyright 2020 floragunn GmbH + * + * Licensed 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. + * + */ + +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.test.framework.cluster; + +import java.net.InetSocketAddress; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.security.KeyStore; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; + +import org.apache.http.Header; +import org.apache.http.message.BasicHeader; +import org.opensearch.security.support.PemKeyReader; +import org.opensearch.test.framework.certificate.TestCertificates; + + +public interface OpenSearchClientProvider { + + String getClusterName(); + + TestCertificates getTestCertificates(); + + InetSocketAddress getHttpAddress(); + + InetSocketAddress getTransportAddress(); + + default URI getHttpAddressAsURI() { + InetSocketAddress address = getHttpAddress(); + return URI.create("https://" + address.getHostString() + ":" + address.getPort()); + } + + default TestRestClient getRestClient(UserCredentialsHolder user, Header... headers) { + return getRestClient(user.getName(), user.getPassword(), headers); + } + + default TestRestClient getRestClient(String user, String password, String tenant) { + return getRestClient(user, password, new BasicHeader("sgtenant", tenant)); + } + + default TestRestClient getRestClient(String user, String password, Header... headers) { + BasicHeader basicAuthHeader = getBasicAuthHeader(user, password); + if (headers != null && headers.length > 0) { + List
concatenatedHeaders = Stream.concat(Stream.of(basicAuthHeader), Stream.of(headers)).collect(Collectors.toList()); + return getRestClient(concatenatedHeaders); + } + return getRestClient(basicAuthHeader); + } + + default TestRestClient getRestClient(Header... headers) { + return getRestClient(Arrays.asList(headers)); + } + + default TestRestClient getRestClient(List
headers) { + return createGenericClientRestClient(headers); + } + + default TestRestClient createGenericClientRestClient(List
headers) { + return new TestRestClient(getHttpAddress(), headers, getSSLContext()); + } + + default BasicHeader getBasicAuthHeader(String user, String password) { + return new BasicHeader("Authorization", + "Basic " + Base64.getEncoder().encodeToString((user + ":" + Objects.requireNonNull(password)).getBytes(StandardCharsets.UTF_8))); + } + + private SSLContext getSSLContext() { + X509Certificate[] trustCertificates; + + try { + trustCertificates = PemKeyReader.loadCertificatesFromFile(getTestCertificates().getRootCertificate() ); + + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); + + ks.load(null); + + for (int i = 0; i < trustCertificates.length; i++) { + ks.setCertificateEntry("caCert-" + i, trustCertificates[i]); + } + + tmf.init(ks); + + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, tmf.getTrustManagers(), null); + return sslContext; + + } catch (Exception e) { + throw new RuntimeException("Error loading root CA ", e); + } + } + + public interface UserCredentialsHolder { + String getName(); + String getPassword(); + } + +} diff --git a/src/newTest/java/org/opensearch/test/framework/cluster/PortAllocator.java b/src/newTest/java/org/opensearch/test/framework/cluster/PortAllocator.java new file mode 100644 index 0000000000..04aba5211d --- /dev/null +++ b/src/newTest/java/org/opensearch/test/framework/cluster/PortAllocator.java @@ -0,0 +1,157 @@ +/* + * Copyright 2021 floragunn GmbH + * + * Licensed 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. + * + */ + +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.test.framework.cluster; + +import java.time.Duration; +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; +import java.util.SortedSet; +import java.util.TreeSet; + +import org.opensearch.test.framework.cluster.SocketUtils.SocketType; + + +public class PortAllocator { + + public static final PortAllocator TCP = new PortAllocator(SocketType.TCP, Duration.ofSeconds(100)); + public static final PortAllocator UDP = new PortAllocator(SocketType.UDP, Duration.ofSeconds(100)); + + private final SocketType socketType; + private final Duration timeoutDuration; + private final Map allocatedPorts = new HashMap<>(); + + PortAllocator(SocketType socketType, Duration timeoutDuration) { + this.socketType = socketType; + this.timeoutDuration = timeoutDuration; + } + + public SortedSet allocate(String clientName, int numRequested, int minPort) { + + int startPort = minPort; + + while (!isAvailable(startPort)) { + startPort += 10; + } + + SortedSet foundPorts = new TreeSet<>(); + + for (int currentPort = startPort; foundPorts.size() < numRequested && currentPort < SocketUtils.PORT_RANGE_MAX + && (currentPort - startPort) < 10000; currentPort++) { + if (allocate(clientName, currentPort)) { + foundPorts.add(currentPort); + } + } + + if (foundPorts.size() < numRequested) { + throw new IllegalStateException("Could not find " + numRequested + " free ports starting at " + minPort + " for " + clientName); + } + + return foundPorts; + } + + public int allocateSingle(String clientName, int minPort) { + + int startPort = minPort; + + for (int currentPort = startPort; currentPort < SocketUtils.PORT_RANGE_MAX && (currentPort - startPort) < 10000; currentPort++) { + if (allocate(clientName, currentPort)) { + return currentPort; + } + } + + throw new IllegalStateException("Could not find free port starting at " + minPort + " for " + clientName); + + } + + public void reserve(int... ports) { + + for (int port : ports) { + allocate("reserved", port); + } + } + + private boolean isInUse(int port) { + boolean result = !this.socketType.isPortAvailable(port); + + if (result) { + synchronized (this) { + allocatedPorts.put(port, new AllocatedPort("external")); + } + } + + return result; + } + + private boolean isAvailable(int port) { + return !isAllocated(port) && !isInUse(port); + } + + private synchronized boolean isAllocated(int port) { + AllocatedPort allocatedPort = this.allocatedPorts.get(port); + + return allocatedPort != null && !allocatedPort.isTimedOut(); + } + + private synchronized boolean allocate(String clientName, int port) { + + AllocatedPort allocatedPort = allocatedPorts.get(port); + + if (allocatedPort != null && allocatedPort.isTimedOut()) { + allocatedPort = null; + allocatedPorts.remove(port); + } + + if (allocatedPort == null && !isInUse(port)) { + allocatedPorts.put(port, new AllocatedPort(clientName)); + return true; + } else { + return false; + } + } + + private class AllocatedPort { + final String client; + final Instant allocatedAt; + + AllocatedPort(String client) { + this.client = client; + this.allocatedAt = Instant.now(); + } + + boolean isTimedOut() { + return allocatedAt.plus(timeoutDuration).isBefore(Instant.now()); + } + + @Override + public String toString() { + return "AllocatedPort [client=" + client + ", allocatedAt=" + allocatedAt + "]"; + } + } +} diff --git a/src/newTest/java/org/opensearch/test/framework/cluster/SocketUtils.java b/src/newTest/java/org/opensearch/test/framework/cluster/SocketUtils.java new file mode 100644 index 0000000000..36718b6a43 --- /dev/null +++ b/src/newTest/java/org/opensearch/test/framework/cluster/SocketUtils.java @@ -0,0 +1,319 @@ +/* + * Copyright 2002-2017 the original author or authors. + * + * Licensed 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. + */ + +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.test.framework.cluster; + +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.util.Random; +import java.util.SortedSet; +import java.util.TreeSet; + +import javax.net.ServerSocketFactory; + +/** + * Simple utility methods for working with network sockets — for example, + * for finding available ports on {@code localhost}. + * + *

Within this class, a TCP port refers to a port for a {@link ServerSocket}; + * whereas, a UDP port refers to a port for a {@link DatagramSocket}. + * + * @author Sam Brannen + * @author Ben Hale + * @author Arjen Poutsma + * @author Gunnar Hillert + * @author Gary Russell + * @since 4.0 + */ +public class SocketUtils { + + /** + * The default minimum value for port ranges used when finding an available + * socket port. + */ + public static final int PORT_RANGE_MIN = 1024; + + /** + * The default maximum value for port ranges used when finding an available + * socket port. + */ + public static final int PORT_RANGE_MAX = 65535; + + + private static final Random random = new Random(System.currentTimeMillis()); + + + /** + * Although {@code SocketUtils} consists solely of static utility methods, + * this constructor is intentionally {@code public}. + *

Rationale

+ *

Static methods from this class may be invoked from within XML + * configuration files using the Spring Expression Language (SpEL) and the + * following syntax. + *

<bean id="bean1" ... p:port="#{T(org.springframework.util.SocketUtils).findAvailableTcpPort(12000)}" />
+ * If this constructor were {@code private}, you would be required to supply + * the fully qualified class name to SpEL's {@code T()} function for each usage. + * Thus, the fact that this constructor is {@code public} allows you to reduce + * boilerplate configuration with SpEL as can be seen in the following example. + *
<bean id="socketUtils" class="org.springframework.util.SocketUtils" />
+     * <bean id="bean1" ... p:port="#{socketUtils.findAvailableTcpPort(12000)}" />
+     * <bean id="bean2" ... p:port="#{socketUtils.findAvailableTcpPort(30000)}" />
+ */ + public SocketUtils() { + /* no-op */ + } + + + /** + * Find an available TCP port randomly selected from the range + * [{@value #PORT_RANGE_MIN}, {@value #PORT_RANGE_MAX}]. + * @return an available TCP port number + * @throws IllegalStateException if no available port could be found + */ + public static int findAvailableTcpPort() { + return findAvailableTcpPort(PORT_RANGE_MIN); + } + + /** + * Find an available TCP port randomly selected from the range + * [{@code minPort}, {@value #PORT_RANGE_MAX}]. + * @param minPort the minimum port number + * @return an available TCP port number + * @throws IllegalStateException if no available port could be found + */ + public static int findAvailableTcpPort(int minPort) { + return findAvailableTcpPort(minPort, PORT_RANGE_MAX); + } + + /** + * Find an available TCP port randomly selected from the range + * [{@code minPort}, {@code maxPort}]. + * @param minPort the minimum port number + * @param maxPort the maximum port number + * @return an available TCP port number + * @throws IllegalStateException if no available port could be found + */ + public static int findAvailableTcpPort(int minPort, int maxPort) { + return SocketType.TCP.findAvailablePort(minPort, maxPort); + } + + /** + * Find the requested number of available TCP ports, each randomly selected + * from the range [{@value #PORT_RANGE_MIN}, {@value #PORT_RANGE_MAX}]. + * @param numRequested the number of available ports to find + * @return a sorted set of available TCP port numbers + * @throws IllegalStateException if the requested number of available ports could not be found + */ + public static SortedSet findAvailableTcpPorts(int numRequested) { + return findAvailableTcpPorts(numRequested, PORT_RANGE_MIN, PORT_RANGE_MAX); + } + + /** + * Find the requested number of available TCP ports, each randomly selected + * from the range [{@code minPort}, {@code maxPort}]. + * @param numRequested the number of available ports to find + * @param minPort the minimum port number + * @param maxPort the maximum port number + * @return a sorted set of available TCP port numbers + * @throws IllegalStateException if the requested number of available ports could not be found + */ + public static SortedSet findAvailableTcpPorts(int numRequested, int minPort, int maxPort) { + return SocketType.TCP.findAvailablePorts(numRequested, minPort, maxPort); + } + + /** + * Find an available UDP port randomly selected from the range + * [{@value #PORT_RANGE_MIN}, {@value #PORT_RANGE_MAX}]. + * @return an available UDP port number + * @throws IllegalStateException if no available port could be found + */ + public static int findAvailableUdpPort() { + return findAvailableUdpPort(PORT_RANGE_MIN); + } + + /** + * Find an available UDP port randomly selected from the range + * [{@code minPort}, {@value #PORT_RANGE_MAX}]. + * @param minPort the minimum port number + * @return an available UDP port number + * @throws IllegalStateException if no available port could be found + */ + public static int findAvailableUdpPort(int minPort) { + return findAvailableUdpPort(minPort, PORT_RANGE_MAX); + } + + /** + * Find an available UDP port randomly selected from the range + * [{@code minPort}, {@code maxPort}]. + * @param minPort the minimum port number + * @param maxPort the maximum port number + * @return an available UDP port number + * @throws IllegalStateException if no available port could be found + */ + public static int findAvailableUdpPort(int minPort, int maxPort) { + return SocketType.UDP.findAvailablePort(minPort, maxPort); + } + + /** + * Find the requested number of available UDP ports, each randomly selected + * from the range [{@value #PORT_RANGE_MIN}, {@value #PORT_RANGE_MAX}]. + * @param numRequested the number of available ports to find + * @return a sorted set of available UDP port numbers + * @throws IllegalStateException if the requested number of available ports could not be found + */ + public static SortedSet findAvailableUdpPorts(int numRequested) { + return findAvailableUdpPorts(numRequested, PORT_RANGE_MIN, PORT_RANGE_MAX); + } + + /** + * Find the requested number of available UDP ports, each randomly selected + * from the range [{@code minPort}, {@code maxPort}]. + * @param numRequested the number of available ports to find + * @param minPort the minimum port number + * @param maxPort the maximum port number + * @return a sorted set of available UDP port numbers + * @throws IllegalStateException if the requested number of available ports could not be found + */ + public static SortedSet findAvailableUdpPorts(int numRequested, int minPort, int maxPort) { + return SocketType.UDP.findAvailablePorts(numRequested, minPort, maxPort); + } + + + public enum SocketType { + + TCP { + @Override + protected boolean isPortAvailable(int port) { + try { + ServerSocket serverSocket = ServerSocketFactory.getDefault().createServerSocket( + port, 1, InetAddress.getByName("localhost")); + serverSocket.close(); + return true; + } + catch (Exception ex) { + return false; + } + } + }, + + UDP { + @Override + protected boolean isPortAvailable(int port) { + try { + DatagramSocket socket = new DatagramSocket(port, InetAddress.getByName("localhost")); + socket.close(); + return true; + } + catch (Exception ex) { + return false; + } + } + }; + + /** + * Determine if the specified port for this {@code SocketType} is + * currently available on {@code localhost}. + */ + protected abstract boolean isPortAvailable(int port); + + /** + * Find a pseudo-random port number within the range + * [{@code minPort}, {@code maxPort}]. + * @param minPort the minimum port number + * @param maxPort the maximum port number + * @return a random port number within the specified range + */ + private int findRandomPort(int minPort, int maxPort) { + int portRange = maxPort - minPort; + return minPort + random.nextInt(portRange + 1); + } + + /** + * Find an available port for this {@code SocketType}, randomly selected + * from the range [{@code minPort}, {@code maxPort}]. + * @param minPort the minimum port number + * @param maxPort the maximum port number + * @return an available port number for this socket type + * @throws IllegalStateException if no available port could be found + */ + int findAvailablePort(int minPort, int maxPort) { + //Assert.assertTrue(minPort > 0, "'minPort' must be greater than 0"); + //Assert.isTrue(maxPort >= minPort, "'maxPort' must be greater than or equal to 'minPort'"); + //Assert.isTrue(maxPort <= PORT_RANGE_MAX, "'maxPort' must be less than or equal to " + PORT_RANGE_MAX); + + int portRange = maxPort - minPort; + int candidatePort; + int searchCounter = 0; + do { + if (searchCounter > portRange) { + throw new IllegalStateException(String.format( + "Could not find an available %s port in the range [%d, %d] after %d attempts", + name(), minPort, maxPort, searchCounter)); + } + candidatePort = findRandomPort(minPort, maxPort); + searchCounter++; + } + while (!isPortAvailable(candidatePort)); + + return candidatePort; + } + + /** + * Find the requested number of available ports for this {@code SocketType}, + * each randomly selected from the range [{@code minPort}, {@code maxPort}]. + * @param numRequested the number of available ports to find + * @param minPort the minimum port number + * @param maxPort the maximum port number + * @return a sorted set of available port numbers for this socket type + * @throws IllegalStateException if the requested number of available ports could not be found + */ + SortedSet findAvailablePorts(int numRequested, int minPort, int maxPort) { + //Assert.isTrue(minPort > 0, "'minPort' must be greater than 0"); + //Assert.isTrue(maxPort > minPort, "'maxPort' must be greater than 'minPort'"); + //Assert.isTrue(maxPort <= PORT_RANGE_MAX, "'maxPort' must be less than or equal to " + PORT_RANGE_MAX); + //Assert.isTrue(numRequested > 0, "'numRequested' must be greater than 0"); + //Assert.isTrue((maxPort - minPort) >= numRequested, + // "'numRequested' must not be greater than 'maxPort' - 'minPort'"); + + SortedSet availablePorts = new TreeSet<>(); + int attemptCount = 0; + while ((++attemptCount <= numRequested + 100) && availablePorts.size() < numRequested) { + availablePorts.add(findAvailablePort(minPort, maxPort)); + } + + if (availablePorts.size() != numRequested) { + throw new IllegalStateException(String.format( + "Could not find %d available %s ports in the range [%d, %d]", + numRequested, name(), minPort, maxPort)); + } + + return availablePorts; + } + } + +} \ No newline at end of file diff --git a/src/newTest/java/org/opensearch/test/framework/cluster/TestRestClient.java b/src/newTest/java/org/opensearch/test/framework/cluster/TestRestClient.java new file mode 100644 index 0000000000..1c207baaa5 --- /dev/null +++ b/src/newTest/java/org/opensearch/test/framework/cluster/TestRestClient.java @@ -0,0 +1,366 @@ +/* + * Copyright 2021 floragunn GmbH + * + * Licensed 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. + * + */ + +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.test.framework.cluster; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import javax.net.ssl.SSLContext; + +import org.apache.commons.io.IOUtils; +import org.apache.http.Header; +import org.apache.http.HttpEntity; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpHead; +import org.apache.http.client.methods.HttpOptions; +import org.apache.http.client.methods.HttpPatch; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.config.SocketConfig; +import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.message.BasicHeader; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.opensearch.common.Strings; +import org.opensearch.common.xcontent.ToXContentObject; +import org.opensearch.security.DefaultObjectMapper; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.collect.Lists; + +public class TestRestClient implements AutoCloseable { + + private static final Logger log = LogManager.getLogger(TestRestClient.class); + + private boolean enableHTTPClientSSL = true; + private boolean enableHTTPClientSSLv3Only = false; + private boolean sendHTTPClientCertificate = false; + private InetSocketAddress nodeHttpAddress; + private RequestConfig requestConfig; + private List
headers = new ArrayList<>(); + private Header CONTENT_TYPE_JSON = new BasicHeader("Content-Type", "application/json"); + private boolean trackResources = false; + private SSLContext sslContext; + private Set puttedResourcesSet = new HashSet<>(); + private List puttedResourcesList = new ArrayList<>(); + + public TestRestClient(InetSocketAddress nodeHttpAddress, List
headers, SSLContext sslContext) { + this.nodeHttpAddress = nodeHttpAddress; + this.headers.addAll(headers); + this.sslContext = sslContext; + } + + public HttpResponse get(String path, Header... headers) throws Exception { + return executeRequest(new HttpGet(getHttpServerUri() + "/" + path), headers); + } + + public HttpResponse getAuthInfo( Header... headers) throws Exception { + return executeRequest(new HttpGet(getHttpServerUri() + "/_opendistro/_security/authinfo?pretty"), headers); + } + + public HttpResponse head(String path, Header... headers) throws Exception { + return executeRequest(new HttpHead(getHttpServerUri() + "/" + path), headers); + } + + public HttpResponse options(String path, Header... headers) throws Exception { + return executeRequest(new HttpOptions(getHttpServerUri() + "/" + path), headers); + } + + public HttpResponse putJson(String path, String body, Header... headers) throws Exception { + HttpPut uriRequest = new HttpPut(getHttpServerUri() + "/" + path); + uriRequest.setEntity(new StringEntity(body)); + + HttpResponse response = executeRequest(uriRequest, mergeHeaders(CONTENT_TYPE_JSON, headers)); + + if (response.getStatusCode() < 400 && trackResources && !puttedResourcesSet.contains(path)) { + puttedResourcesSet.add(path); + puttedResourcesList.add(path); + } + + return response; + } + + public HttpResponse putJson(String path, ToXContentObject body) throws Exception { + return putJson(path, Strings.toString(body)); + } + + public HttpResponse put(String path) throws Exception { + HttpPut uriRequest = new HttpPut(getHttpServerUri() + "/" + path); + HttpResponse response = executeRequest(uriRequest); + + if (response.getStatusCode() < 400 && trackResources && !puttedResourcesSet.contains(path)) { + puttedResourcesSet.add(path); + puttedResourcesList.add(path); + } + + return response; + } + + public HttpResponse delete(String path, Header... headers) throws Exception { + return executeRequest(new HttpDelete(getHttpServerUri() + "/" + path), headers); + } + + public HttpResponse postJson(String path, String body, Header... headers) throws Exception { + HttpPost uriRequest = new HttpPost(getHttpServerUri() + "/" + path); + uriRequest.setEntity(new StringEntity(body)); + return executeRequest(uriRequest, mergeHeaders(CONTENT_TYPE_JSON, headers)); + } + + public HttpResponse postJson(String path, ToXContentObject body) throws Exception { + return postJson(path, Strings.toString(body)); + } + + public HttpResponse post(String path) throws Exception { + HttpPost uriRequest = new HttpPost(getHttpServerUri() + "/" + path); + return executeRequest(uriRequest); + } + + public HttpResponse patch(String path, String body) throws Exception { + HttpPatch uriRequest = new HttpPatch(getHttpServerUri() + "/" + path); + uriRequest.setEntity(new StringEntity(body)); + return executeRequest(uriRequest, CONTENT_TYPE_JSON); + } + + public HttpResponse executeRequest(HttpUriRequest uriRequest, Header... requestSpecificHeaders) throws Exception { + + CloseableHttpClient httpClient = null; + try { + + httpClient = getHTTPClient(); + + if (requestSpecificHeaders != null && requestSpecificHeaders.length > 0) { + for (int i = 0; i < requestSpecificHeaders.length; i++) { + Header h = requestSpecificHeaders[i]; + uriRequest.addHeader(h); + } + } + + for (Header header : headers) { + uriRequest.addHeader(header); + } + + HttpResponse res = new HttpResponse(httpClient.execute(uriRequest)); + log.debug(res.getBody()); + return res; + } finally { + + if (httpClient != null) { + httpClient.close(); + } + } + } + + public TestRestClient trackResources() { + trackResources = true; + return this; + } + + private void cleanupResources() { + if (puttedResourcesList.size() > 0) { + log.info("Cleaning up " + puttedResourcesList); + + for (String resource : Lists.reverse(puttedResourcesList)) { + try { + delete(resource); + } catch (Exception e) { + log.error("Error cleaning up created resources " + resource, e); + } + } + } + } + + protected final String getHttpServerUri() { + return "http" + (enableHTTPClientSSL ? "s" : "") + "://" + nodeHttpAddress.getHostString() + ":" + nodeHttpAddress.getPort(); + } + + protected final CloseableHttpClient getHTTPClient() throws Exception { + + final HttpClientBuilder hcb = HttpClients.custom(); + + String[] protocols = null; + + if (enableHTTPClientSSLv3Only) { + protocols = new String[] { "SSLv3" }; + } else { + protocols = new String[] { "TLSv1", "TLSv1.1", "TLSv1.2" }; + } + + final SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(this.sslContext, protocols, null, + NoopHostnameVerifier.INSTANCE); + + hcb.setSSLSocketFactory(sslsf); + + hcb.setDefaultSocketConfig(SocketConfig.custom().setSoTimeout(60 * 1000).build()); + + if (requestConfig != null) { + hcb.setDefaultRequestConfig(requestConfig); + } + + return hcb.build(); + } + + private Header[] mergeHeaders(Header header, Header... headers) { + + if (headers == null || headers.length == 0) { + return new Header[] { header }; + } else { + Header[] result = new Header[headers.length + 1]; + result[0] = header; + System.arraycopy(headers, 0, result, 1, headers.length); + return result; + } + } + + public static class HttpResponse { + private final CloseableHttpResponse inner; + private final String body; + private final Header[] header; + private final int statusCode; + private final String statusReason; + + public HttpResponse(CloseableHttpResponse inner) throws IllegalStateException, IOException { + super(); + this.inner = inner; + final HttpEntity entity = inner.getEntity(); + if (entity == null) { //head request does not have a entity + this.body = ""; + } else { + this.body = IOUtils.toString(entity.getContent(), StandardCharsets.UTF_8); + } + this.header = inner.getAllHeaders(); + this.statusCode = inner.getStatusLine().getStatusCode(); + this.statusReason = inner.getStatusLine().getReasonPhrase(); + inner.close(); + } + + public String getContentType() { + Header h = getInner().getFirstHeader("content-type"); + if (h != null) { + return h.getValue(); + } + return null; + } + + public boolean isJsonContentType() { + String ct = getContentType(); + if (ct == null) { + return false; + } + return ct.contains("application/json"); + } + + public CloseableHttpResponse getInner() { + return inner; + } + + public String getBody() { + return body; + } + + public Header[] getHeader() { + return header; + } + + public int getStatusCode() { + return statusCode; + } + + public String getStatusReason() { + return statusReason; + } + + public List
getHeaders() { + return header == null ? Collections.emptyList() : Arrays.asList(header); + } + + public JsonNode toJsonNode() throws JsonProcessingException, IOException { + return DefaultObjectMapper.objectMapper.readTree(getBody()); + } + + @Override + public String toString() { + return "HttpResponse [inner=" + inner + ", body=" + body + ", header=" + Arrays.toString(header) + ", statusCode=" + statusCode + + ", statusReason=" + statusReason + "]"; + } + + } + + @Override + public String toString() { + return "TestRestClient [server=" + getHttpServerUri() + ", node=" + nodeHttpAddress + "]"; + } + + public RequestConfig getRequestConfig() { + return requestConfig; + } + + public void setRequestConfig(RequestConfig requestConfig) { + this.requestConfig = requestConfig; + } + + public void setLocalAddress(InetAddress inetAddress) { + if (requestConfig == null) { + requestConfig = RequestConfig.custom().setLocalAddress(inetAddress).build(); + } else { + requestConfig = RequestConfig.copy(requestConfig).setLocalAddress(inetAddress).build(); + } + } + + @Override + public void close() throws IOException { + cleanupResources(); + } + + public boolean isSendHTTPClientCertificate() { + return sendHTTPClientCertificate; + } + + public void setSendHTTPClientCertificate(boolean sendHTTPClientCertificate) { + this.sendHTTPClientCertificate = sendHTTPClientCertificate; + } + +} From 5a68fb20b9d543115baec92c621da989e0075da9 Mon Sep 17 00:00:00 2001 From: jochenkressin Date: Mon, 1 Aug 2022 18:43:46 +0200 Subject: [PATCH 02/16] removed abstract base class, prefer composition over inheritance --- .../test/AbstractIntegrationTest.java | 36 ------------------- .../test/GenericIntegrationTest.java | 11 ++++-- .../test/PrivilegesEvaluatorTest.java | 10 ++++-- .../opensearch/test/SecurityRolesTests.java | 8 ++++- .../test/framework/TestSecurityConfig.java | 10 +++++- 5 files changed, 33 insertions(+), 42 deletions(-) delete mode 100644 src/newTest/java/org/opensearch/test/AbstractIntegrationTest.java diff --git a/src/newTest/java/org/opensearch/test/AbstractIntegrationTest.java b/src/newTest/java/org/opensearch/test/AbstractIntegrationTest.java deleted file mode 100644 index 3f13fe261a..0000000000 --- a/src/newTest/java/org/opensearch/test/AbstractIntegrationTest.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -package org.opensearch.test; - -import org.junit.runner.RunWith; -import org.opensearch.test.framework.TestSecurityConfig; -import org.opensearch.test.framework.TestSecurityConfig.Role; - -import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; - -@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) -@ThreadLeakScope(ThreadLeakScope.Scope.NONE) -public class AbstractIntegrationTest { - - /** - * Auth domain with HTTPS Basic and the internal user backend - */ - protected final static TestSecurityConfig.AuthcDomain AUTHC_HTTPBASIC_INTERNAL = new TestSecurityConfig.AuthcDomain("basic", 0) - .httpAuthenticator("basic").backend("internal"); - - /** - * Admin user with full access to all indices - */ - protected final static TestSecurityConfig.User USER_ADMIN = new TestSecurityConfig.User("admin") - .roles(new Role("allaccess").indexPermissions("*").on("*").clusterPermissions("*")); - -} diff --git a/src/newTest/java/org/opensearch/test/GenericIntegrationTest.java b/src/newTest/java/org/opensearch/test/GenericIntegrationTest.java index cfddf84ad5..56b28ba20c 100644 --- a/src/newTest/java/org/opensearch/test/GenericIntegrationTest.java +++ b/src/newTest/java/org/opensearch/test/GenericIntegrationTest.java @@ -16,6 +16,7 @@ import org.junit.Assert; import org.junit.ClassRule; import org.junit.Test; +import org.junit.runner.RunWith; import org.opensearch.test.framework.TestIndex; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.TestSecurityConfig.Role; @@ -24,15 +25,21 @@ import org.opensearch.test.framework.cluster.TestRestClient; import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse; +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; import com.fasterxml.jackson.core.JsonPointer; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.*; +import static org.opensearch.test.framework.TestSecurityConfig.User.*; + /** * WIP * Generic test class that demonstrates how to use the test framework to * set up a test cluster with users, roles, indices and data, and how to * implement tests. One main goal here is to make tests self-contained. */ -public class GenericIntegrationTest extends AbstractIntegrationTest { +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class GenericIntegrationTest { // define indices used in this test private final static TestIndex INDEX_A = TestIndex.name("index-a").build(); @@ -46,7 +53,7 @@ public class GenericIntegrationTest extends AbstractIntegrationTest { @ClassRule public static LocalCluster cluster = new LocalCluster.Builder().clusterConfiguration(ClusterConfiguration.THREE_MASTERS) .authc(AUTHC_HTTPBASIC_INTERNAL) - .users(USER_ADMIN, INDEX_A_USER) + .users(INDEX_A_USER) .indices(INDEX_A, INDEX_B).build(); @Test diff --git a/src/newTest/java/org/opensearch/test/PrivilegesEvaluatorTest.java b/src/newTest/java/org/opensearch/test/PrivilegesEvaluatorTest.java index 44307c6a02..34bc546fba 100644 --- a/src/newTest/java/org/opensearch/test/PrivilegesEvaluatorTest.java +++ b/src/newTest/java/org/opensearch/test/PrivilegesEvaluatorTest.java @@ -11,10 +11,13 @@ package org.opensearch.test; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; + import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.ClassRule; import org.junit.Test; +import org.junit.runner.RunWith; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.TestSecurityConfig.Role; import org.opensearch.test.framework.cluster.ClusterConfiguration; @@ -22,13 +25,16 @@ import org.opensearch.test.framework.cluster.TestRestClient; import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse; +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; + /** * This is a port for the test * org.opensearch.security.privileges.PrivilegesEvaluatorTest to the new test * framework for direct comparison - * */ -public class PrivilegesEvaluatorTest extends AbstractIntegrationTest { +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class PrivilegesEvaluatorTest { protected final static TestSecurityConfig.User NEGATIVE_LOOKAHEAD = new TestSecurityConfig.User( "negative_lookahead_user") diff --git a/src/newTest/java/org/opensearch/test/SecurityRolesTests.java b/src/newTest/java/org/opensearch/test/SecurityRolesTests.java index b5de319912..7b82fb41c7 100644 --- a/src/newTest/java/org/opensearch/test/SecurityRolesTests.java +++ b/src/newTest/java/org/opensearch/test/SecurityRolesTests.java @@ -26,10 +26,13 @@ package org.opensearch.test; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; + import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.ClassRule; import org.junit.Test; +import org.junit.runner.RunWith; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.TestSecurityConfig.Role; import org.opensearch.test.framework.cluster.ClusterConfiguration; @@ -37,9 +40,12 @@ import org.opensearch.test.framework.cluster.TestRestClient; import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse; +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; import com.fasterxml.jackson.core.JsonPointer; -public class SecurityRolesTests extends AbstractIntegrationTest { +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class SecurityRolesTests { protected final static TestSecurityConfig.User USER_SR = new TestSecurityConfig.User("sr_user").roles( new Role("abc_ber").indexPermissions("*").on("*").clusterPermissions("*"), diff --git a/src/newTest/java/org/opensearch/test/framework/TestSecurityConfig.java b/src/newTest/java/org/opensearch/test/framework/TestSecurityConfig.java index 88e86b51b3..b64fea1090 100644 --- a/src/newTest/java/org/opensearch/test/framework/TestSecurityConfig.java +++ b/src/newTest/java/org/opensearch/test/framework/TestSecurityConfig.java @@ -56,6 +56,7 @@ import org.opensearch.security.action.configupdate.ConfigUpdateRequest; import org.opensearch.security.action.configupdate.ConfigUpdateResponse; import org.opensearch.security.securityconf.impl.CType; +import org.opensearch.test.framework.TestSecurityConfig.Role; import org.opensearch.test.framework.cluster.NestedValueMap; import org.opensearch.test.framework.cluster.NestedValueMap.Path; import org.opensearch.test.framework.cluster.OpenSearchClientProvider.UserCredentialsHolder; @@ -244,6 +245,10 @@ public TestSecurityConfig roleToRoleMapping(Role role, String... backendRoles) { } public static class User implements UserCredentialsHolder { + + public final static TestSecurityConfig.User USER_ADMIN = new TestSecurityConfig.User("admin") + .roles(new Role("allaccess").indexPermissions("*").on("*").clusterPermissions("*")); + private String name; private String password; private Role[] roles; @@ -411,6 +416,9 @@ public NestedValueMap toJsonMap() { public static class AuthcDomain { + public final static AuthcDomain AUTHC_HTTPBASIC_INTERNAL = new TestSecurityConfig.AuthcDomain("basic", 0) + .httpAuthenticator("basic").backend("internal"); + private final String id; private boolean enabled = true; private boolean transportEnabled = true; @@ -418,7 +426,7 @@ public static class AuthcDomain { private List skipUsers = new ArrayList<>(); private HttpAuthenticator httpAuthenticator; private AuthenticationBackend authenticationBackend; - + public AuthcDomain(String id, int order) { this.id = id; this.order = order; From d6771c4a33b9cdcc789159fa2c415bab354e0016 Mon Sep 17 00:00:00 2001 From: jochenkressin Date: Mon, 1 Aug 2022 20:03:31 +0200 Subject: [PATCH 03/16] moved Jackson dependencies from test classes to TestRestClient --- .../test/GenericIntegrationTest.java | 11 +++---- .../opensearch/test/SecurityRolesTests.java | 12 +++---- .../framework/cluster/TestRestClient.java | 32 ++++++++++++++++++- 3 files changed, 40 insertions(+), 15 deletions(-) diff --git a/src/newTest/java/org/opensearch/test/GenericIntegrationTest.java b/src/newTest/java/org/opensearch/test/GenericIntegrationTest.java index 56b28ba20c..6be35ff423 100644 --- a/src/newTest/java/org/opensearch/test/GenericIntegrationTest.java +++ b/src/newTest/java/org/opensearch/test/GenericIntegrationTest.java @@ -11,6 +11,9 @@ package org.opensearch.test; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; +import static org.opensearch.test.framework.TestSecurityConfig.User.USER_ADMIN; + import org.apache.http.HttpStatus; import org.junit.AfterClass; import org.junit.Assert; @@ -26,10 +29,6 @@ import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse; import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; -import com.fasterxml.jackson.core.JsonPointer; - -import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.*; -import static org.opensearch.test.framework.TestSecurityConfig.User.*; /** * WIP @@ -71,8 +70,8 @@ public void testIndexAUserHasOnlyAccessToIndexA() throws Exception { Assert.assertEquals(response.getStatusCode(), HttpStatus.SC_OK); // demo: work with JSON response body and check values - JsonPointer jsonPointer = JsonPointer.compile("/_source/hits/value"); - int hits = response.toJsonNode().at(jsonPointer).asInt(); + int hits = response.getIntFromJsonBody("/_source/hits/value"); + Assert.assertEquals(0, hits); response = client.get("index-b/_search?pretty"); Assert.assertEquals(response.getStatusCode(), HttpStatus.SC_FORBIDDEN); diff --git a/src/newTest/java/org/opensearch/test/SecurityRolesTests.java b/src/newTest/java/org/opensearch/test/SecurityRolesTests.java index 7b82fb41c7..37b4459e5c 100644 --- a/src/newTest/java/org/opensearch/test/SecurityRolesTests.java +++ b/src/newTest/java/org/opensearch/test/SecurityRolesTests.java @@ -41,7 +41,6 @@ import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse; import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; -import com.fasterxml.jackson.core.JsonPointer; @RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) @ThreadLeakScope(ThreadLeakScope.Scope.NONE) @@ -63,18 +62,15 @@ public void testSecurityRolesAnon() throws Exception { HttpResponse response = client.getAuthInfo(); Assert.assertEquals(response.getStatusCode(), HttpStatus.SC_OK); - // Check username - JsonPointer jsonPointer = JsonPointer.compile("/user_name"); - String username = response.toJsonNode().at(jsonPointer).asText(); + // Check username + String username = response.getTextFromJsonBody("/user_name"); Assert.assertEquals("sr_user", username); // Check security roles - jsonPointer = JsonPointer.compile("/roles/0"); - String securityRole = response.toJsonNode().at(jsonPointer).asText(); + String securityRole = response.getTextFromJsonBody("/roles/0"); Assert.assertEquals("user_sr_user__abc_ber", securityRole); - jsonPointer = JsonPointer.compile("/roles/1"); - securityRole = response.toJsonNode().at(jsonPointer).asText(); + securityRole = response.getTextFromJsonBody("/roles/0"); Assert.assertEquals("user_sr_user__def_efg", securityRole); } diff --git a/src/newTest/java/org/opensearch/test/framework/cluster/TestRestClient.java b/src/newTest/java/org/opensearch/test/framework/cluster/TestRestClient.java index 1c207baaa5..207b257f42 100644 --- a/src/newTest/java/org/opensearch/test/framework/cluster/TestRestClient.java +++ b/src/newTest/java/org/opensearch/test/framework/cluster/TestRestClient.java @@ -317,10 +317,40 @@ public List
getHeaders() { return header == null ? Collections.emptyList() : Arrays.asList(header); } - public JsonNode toJsonNode() throws JsonProcessingException, IOException { + public String getTextFromJsonBody(String jsonPointer) { + return getJsonNodeAt(jsonPointer).asText(); + } + + public int getIntFromJsonBody(String jsonPointer) { + return getJsonNodeAt(jsonPointer).asInt(); + } + + public Boolean getBooleanFromJsonBody(String jsonPointer) { + return getJsonNodeAt(jsonPointer).asBoolean(); + } + + public Double getDoubleFromJsonBody(String jsonPointer) { + return getJsonNodeAt(jsonPointer).asDouble(); + } + + public Long getLongFromJsonBody(String jsonPointer) { + return getJsonNodeAt(jsonPointer).asLong(); + } + + private JsonNode getJsonNodeAt(String jsonPointer) { + try { + return toJsonNode().at(jsonPointer); + } catch (IOException e) { + throw new IllegalArgumentException("Cound not convert response body to JSON node ",e); + } + } + + private JsonNode toJsonNode() throws JsonProcessingException, IOException { return DefaultObjectMapper.objectMapper.readTree(getBody()); } + + @Override public String toString() { return "HttpResponse [inner=" + inner + ", body=" + body + ", header=" + Arrays.toString(header) + ", statusCode=" + statusCode From 2debb05fde9c141898783beda1eca5a5b556545b Mon Sep 17 00:00:00 2001 From: jochenkressin Date: Mon, 1 Aug 2022 20:15:32 +0200 Subject: [PATCH 04/16] removed unused code, replaced some old ES references --- .../test/framework/TestSecurityConfig.java | 8 +- .../test/framework/cluster/ClusterInfo.java | 45 ---------- .../test/framework/cluster/LocalCluster.java | 5 -- .../cluster/LocalOpenSearchCluster.java | 89 ++----------------- .../framework/cluster/TestRestClient.java | 7 -- 5 files changed, 8 insertions(+), 146 deletions(-) delete mode 100644 src/newTest/java/org/opensearch/test/framework/cluster/ClusterInfo.java diff --git a/src/newTest/java/org/opensearch/test/framework/TestSecurityConfig.java b/src/newTest/java/org/opensearch/test/framework/TestSecurityConfig.java index b64fea1090..38523bb13f 100644 --- a/src/newTest/java/org/opensearch/test/framework/TestSecurityConfig.java +++ b/src/newTest/java/org/opensearch/test/framework/TestSecurityConfig.java @@ -70,8 +70,7 @@ public class TestSecurityConfig { private NestedValueMap overrideRoleSettings; private NestedValueMap overrideRoleMappingSettings; private String indexName = ".opendistro_security"; - private Map> variableSuppliers = new HashMap<>(); - + public TestSecurityConfig() { } @@ -81,11 +80,6 @@ public TestSecurityConfig configIndexName(String configIndexName) { return this; } - public TestSecurityConfig var(String name, Supplier variableSupplier) { - this.variableSuppliers.put(name, variableSupplier); - return this; - } - public TestSecurityConfig securityConfigSettings(String keyPath, Object value, Object... more) { if (overrideSecurityConfigSettings == null) { overrideSecurityConfigSettings = new NestedValueMap(); diff --git a/src/newTest/java/org/opensearch/test/framework/cluster/ClusterInfo.java b/src/newTest/java/org/opensearch/test/framework/cluster/ClusterInfo.java deleted file mode 100644 index e82ee82391..0000000000 --- a/src/newTest/java/org/opensearch/test/framework/cluster/ClusterInfo.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2015-2018 _floragunn_ GmbH - * Licensed 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. - */ - -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -package org.opensearch.test.framework.cluster; - -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import org.opensearch.common.transport.TransportAddress; - -// TODO: change to getter/setter -public class ClusterInfo { - public int numNodes; - public String httpHost = null; - public int httpPort = -1; - public Set httpAdresses = new HashSet(); - public String nodeHost; - public int nodePort; - public String clustername; - public List tcpMasterPortsOnly; -} diff --git a/src/newTest/java/org/opensearch/test/framework/cluster/LocalCluster.java b/src/newTest/java/org/opensearch/test/framework/cluster/LocalCluster.java index 3fddec2f15..2ed877c021 100644 --- a/src/newTest/java/org/opensearch/test/framework/cluster/LocalCluster.java +++ b/src/newTest/java/org/opensearch/test/framework/cluster/LocalCluster.java @@ -330,11 +330,6 @@ public Builder authc(TestSecurityConfig.AuthcDomain authc) { return this; } - public Builder var(String name, Supplier value) { - testSecurityConfig.var(name, value); - return this; - } - public Builder clusterName(String clusterName) { this.clusterName = clusterName; return this; diff --git a/src/newTest/java/org/opensearch/test/framework/cluster/LocalOpenSearchCluster.java b/src/newTest/java/org/opensearch/test/framework/cluster/LocalOpenSearchCluster.java index 4ae2c8b956..6c92b12789 100644 --- a/src/newTest/java/org/opensearch/test/framework/cluster/LocalOpenSearchCluster.java +++ b/src/newTest/java/org/opensearch/test/framework/cluster/LocalOpenSearchCluster.java @@ -152,7 +152,6 @@ public void start() throws Exception { log.info("Startup finished. Waiting for GREEN"); waitForCluster(ClusterHealthStatus.GREEN, TimeValue.timeValueSeconds(10), allNodes.size()); - putDefaultTemplate(); log.info("Started: {}", this); @@ -289,19 +288,6 @@ private final Node randomRunningNode(List nodes, List... moreNodes) return runningNodes.get(index); } - private void putDefaultTemplate() { - String defaultTemplate = "{\n" + " \"index_patterns\": [\"*\"],\n" + " \"order\": -1,\n" + " \"settings\": {\n" - + " \"number_of_shards\": \"5\",\n" + " \"number_of_replicas\": \"1\"\n" + " }\n" // - + " }"; - - AcknowledgedResponse templateAck = clientNode().getInternalNodeClient().admin().indices() - .putTemplate(new PutIndexTemplateRequest("default").source(defaultTemplate, XContentType.JSON)).actionGet(); - - if (!templateAck.isAcknowledged()) { - throw new RuntimeException("Default template could not be created"); - } - } - private CompletableFuture startNodes(List nodeSettingList, SortedSet transportPorts, SortedSet httpPorts) { Iterator transportPortIterator = transportPorts.iterator(); Iterator httpPortIterator = httpPorts.iterator(); @@ -315,11 +301,10 @@ private CompletableFuture startNodes(List nodeSettingList, S return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); } - public ClusterInfo waitForCluster(ClusterHealthStatus status, TimeValue timeout, int expectedNodeCount) throws IOException { - ClusterInfo clusterInfo = new ClusterInfo(); + public void waitForCluster(ClusterHealthStatus status, TimeValue timeout, int expectedNodeCount) throws IOException { Client client = clientNode().getInternalNodeClient(); - try { + log.debug("waiting for cluster state {} and {} nodes", status.name(), expectedNodeCount); AdminClient adminClient = client.admin(); @@ -338,67 +323,7 @@ public ClusterInfo waitForCluster(ClusterHealthStatus status, TimeValue timeout, } assertEquals(expectedNodeCount, healthResponse.getNumberOfNodes()); - - final NodesInfoResponse nodesInfoResponse = adminClient.cluster().nodesInfo(new NodesInfoRequest()).actionGet(); - - final List nodes = nodesInfoResponse.getNodes(); - - final List masterNodes = nodes.stream() - .filter(nodeInfo -> nodeInfo.getNode().getRoles().contains(DiscoveryNodeRole.CLUSTER_MANAGER_ROLE)).collect(Collectors.toList()); - - final List dataNodes = nodes.stream().filter(nodeInfo -> nodeInfo.getNode().getRoles().contains(DiscoveryNodeRole.DATA_ROLE) - && !nodeInfo.getNode().getRoles().contains(DiscoveryNodeRole.CLUSTER_MANAGER_ROLE)).collect(Collectors.toList()); - - final List clientNodes = nodes.stream() - .filter(nodeInfo -> !nodeInfo.getNode().getRoles().contains(DiscoveryNodeRole.CLUSTER_MANAGER_ROLE) - && !nodeInfo.getNode().getRoles().contains(DiscoveryNodeRole.DATA_ROLE)) - .collect(Collectors.toList()); - - for (NodeInfo nodeInfo : masterNodes) { - final TransportAddress is = nodeInfo.getInfo(TransportInfo.class).getAddress().publishAddress(); - clusterInfo.nodePort = is.getPort(); - clusterInfo.nodeHost = is.getAddress(); - } - - Predicate hasHttpAddressPredicate = nodeInfo -> nodeInfo.getInfo(HttpInfo.class) != null - && nodeInfo.getInfo(HttpInfo.class).address() != null; - - if (!clientNodes.isEmpty()) { - NodeInfo nodeInfo = clientNodes.get(0); - if (hasHttpAddressPredicate.test(nodeInfo)) { - final TransportAddress his = nodeInfo.getInfo(HttpInfo.class).address().publishAddress(); - clusterInfo.httpPort = his.getPort(); - clusterInfo.httpHost = his.getAddress(); - } else { - throw new RuntimeException("no http host/port for client node"); - } - } else if (!dataNodes.isEmpty()) { - for (NodeInfo nodeInfo : dataNodes) { - if (hasHttpAddressPredicate.test(nodeInfo)) { - final TransportAddress his = nodeInfo.getInfo(HttpInfo.class).address().publishAddress(); - clusterInfo.httpPort = his.getPort(); - clusterInfo.httpHost = his.getAddress(); - break; - } - } - } else { - for (NodeInfo nodeInfo : nodes) { - if (hasHttpAddressPredicate.test(nodeInfo)) { - final TransportAddress his = nodeInfo.getInfo(HttpInfo.class).address().publishAddress(); - clusterInfo.httpPort = his.getPort(); - clusterInfo.httpHost = his.getAddress(); - break; - } - } - } - - for (NodeInfo nodeInfo : nodes) { - clusterInfo.httpAdresses.add(nodeInfo.getInfo(HttpInfo.class).address().publishAddress()); - } - } catch (final OpenSearchTimeoutException e) { - throw new IOException("timeout, cluster does not respond to health request, cowardly refusing to continue with operations"); - } - return clusterInfo; + } @Override @@ -468,7 +393,7 @@ public class Node implements OpenSearchClientProvider { CompletableFuture start() { CompletableFuture completableFuture = new CompletableFuture<>(); - this.node = new PluginAwareNode(nodeSettings.masterNode, getEsSettings(), nodeSettings.getPlugins(additionalPlugins)); + this.node = new PluginAwareNode(nodeSettings.masterNode, getOpenSearchSettings(), nodeSettings.getPlugins(additionalPlugins)); new Thread(new Runnable() { @@ -561,8 +486,8 @@ public InetSocketAddress getTransportAddress() { return transportAddress; } - private Settings getEsSettings() { - Settings settings = getMinimalEsSettings(); + private Settings getOpenSearchSettings() { + Settings settings = getMinimalOpenSearchSettings(); if (nodeSettingsSupplier != null) { // TODO node number @@ -572,7 +497,7 @@ private Settings getEsSettings() { return settings; } - private Settings getMinimalEsSettings() { + private Settings getMinimalOpenSearchSettings() { return Settings.builder().put("node.name", nodeName).put("node.data", nodeSettings.dataNode).put("node.master", nodeSettings.masterNode) .put("cluster.name", clusterName).put("path.home", nodeHomeDir.toPath()).put("path.data", dataDir.toPath()) .put("path.logs", logsDir.toPath()).putList("cluster.initial_master_nodes", initialMasterHosts) diff --git a/src/newTest/java/org/opensearch/test/framework/cluster/TestRestClient.java b/src/newTest/java/org/opensearch/test/framework/cluster/TestRestClient.java index 207b257f42..8f486916e2 100644 --- a/src/newTest/java/org/opensearch/test/framework/cluster/TestRestClient.java +++ b/src/newTest/java/org/opensearch/test/framework/cluster/TestRestClient.java @@ -77,7 +77,6 @@ public class TestRestClient implements AutoCloseable { private static final Logger log = LogManager.getLogger(TestRestClient.class); private boolean enableHTTPClientSSL = true; - private boolean enableHTTPClientSSLv3Only = false; private boolean sendHTTPClientCertificate = false; private InetSocketAddress nodeHttpAddress; private RequestConfig requestConfig; @@ -223,12 +222,6 @@ protected final CloseableHttpClient getHTTPClient() throws Exception { String[] protocols = null; - if (enableHTTPClientSSLv3Only) { - protocols = new String[] { "SSLv3" }; - } else { - protocols = new String[] { "TLSv1", "TLSv1.1", "TLSv1.2" }; - } - final SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(this.sslContext, protocols, null, NoopHostnameVerifier.INSTANCE); From 799ae320731b29b84fc449c45455ab33a1ca3843 Mon Sep 17 00:00:00 2001 From: jochenkressin Date: Mon, 1 Aug 2022 21:55:52 +0200 Subject: [PATCH 05/16] replaced JUnit asserts with Hamcrest asserts --- .../test/GenericIntegrationTest.java | 25 ++++++------------- .../test/PrivilegesEvaluatorTest.java | 18 +++++-------- .../opensearch/test/SecurityRolesTests.java | 19 +++++++------- 3 files changed, 22 insertions(+), 40 deletions(-) diff --git a/src/newTest/java/org/opensearch/test/GenericIntegrationTest.java b/src/newTest/java/org/opensearch/test/GenericIntegrationTest.java index 6be35ff423..77bfc04516 100644 --- a/src/newTest/java/org/opensearch/test/GenericIntegrationTest.java +++ b/src/newTest/java/org/opensearch/test/GenericIntegrationTest.java @@ -11,12 +11,12 @@ package org.opensearch.test; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; import static org.opensearch.test.framework.TestSecurityConfig.User.USER_ADMIN; import org.apache.http.HttpStatus; -import org.junit.AfterClass; -import org.junit.Assert; import org.junit.ClassRule; import org.junit.Test; import org.junit.runner.RunWith; @@ -26,7 +26,6 @@ import org.opensearch.test.framework.cluster.ClusterConfiguration; import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; -import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse; import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; @@ -58,28 +57,18 @@ public class GenericIntegrationTest { @Test public void testAdminUserHasAccessToAllIndices() throws Exception { try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { - HttpResponse response = client.get("*/_search?pretty"); - Assert.assertEquals(response.getStatusCode(), HttpStatus.SC_OK); + assertThat(client.get("*/_search?pretty").getStatusCode(), equalTo(HttpStatus.SC_OK)); } } @Test public void testIndexAUserHasOnlyAccessToIndexA() throws Exception { - try (TestRestClient client = cluster.getRestClient(INDEX_A_USER)) { - HttpResponse response = client.get("index-a/_search?pretty"); - Assert.assertEquals(response.getStatusCode(), HttpStatus.SC_OK); - + try (TestRestClient client = cluster.getRestClient(INDEX_A_USER)) { + assertThat(client.get("index-a/_search?pretty").getStatusCode(), equalTo(HttpStatus.SC_OK)); // demo: work with JSON response body and check values - int hits = response.getIntFromJsonBody("/_source/hits/value"); - Assert.assertEquals(0, hits); - - response = client.get("index-b/_search?pretty"); - Assert.assertEquals(response.getStatusCode(), HttpStatus.SC_FORBIDDEN); + assertThat(client.get("index-a/_search?pretty").getIntFromJsonBody("/_source/hits/value"), equalTo(0)); + assertThat(client.get("index-b/_search?pretty"), equalTo(HttpStatus.SC_FORBIDDEN)); } } - @AfterClass - public static void close() { - cluster.close(); - } } diff --git a/src/newTest/java/org/opensearch/test/PrivilegesEvaluatorTest.java b/src/newTest/java/org/opensearch/test/PrivilegesEvaluatorTest.java index 34bc546fba..d21be816d8 100644 --- a/src/newTest/java/org/opensearch/test/PrivilegesEvaluatorTest.java +++ b/src/newTest/java/org/opensearch/test/PrivilegesEvaluatorTest.java @@ -11,10 +11,11 @@ package org.opensearch.test; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; import org.apache.http.HttpStatus; -import org.junit.Assert; import org.junit.ClassRule; import org.junit.Test; import org.junit.runner.RunWith; @@ -23,7 +24,6 @@ import org.opensearch.test.framework.cluster.ClusterConfiguration; import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; -import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse; import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; @@ -54,11 +54,8 @@ public class PrivilegesEvaluatorTest { public void testNegativeLookaheadPattern() throws Exception { try (TestRestClient client = cluster.getRestClient(NEGATIVE_LOOKAHEAD)) { - HttpResponse response = client.get("*/_search"); - Assert.assertEquals(response.getStatusCode(), HttpStatus.SC_FORBIDDEN); - - response = client.get("r*/_search"); - Assert.assertEquals(response.getStatusCode(), HttpStatus.SC_OK); + assertThat(client.get("*/_search").getStatusCode(), equalTo(HttpStatus.SC_FORBIDDEN)); + assertThat(client.get("r*/_search").getStatusCode(), equalTo(HttpStatus.SC_OK)); } } @@ -66,11 +63,8 @@ public void testNegativeLookaheadPattern() throws Exception { public void testRegexPattern() throws Exception { try (TestRestClient client = cluster.getRestClient(NEGATED_REGEX)) { - HttpResponse response = client.get("*/_search"); - Assert.assertEquals(response.getStatusCode(), HttpStatus.SC_FORBIDDEN); - - response = client.get("r*/_search"); - Assert.assertEquals(response.getStatusCode(), HttpStatus.SC_OK); + assertThat(client.get("*/_search").getStatusCode(), equalTo(HttpStatus.SC_FORBIDDEN)); + assertThat(client.get("r*/_search").getStatusCode(), equalTo(HttpStatus.SC_OK)); } } diff --git a/src/newTest/java/org/opensearch/test/SecurityRolesTests.java b/src/newTest/java/org/opensearch/test/SecurityRolesTests.java index 37b4459e5c..e29c281655 100644 --- a/src/newTest/java/org/opensearch/test/SecurityRolesTests.java +++ b/src/newTest/java/org/opensearch/test/SecurityRolesTests.java @@ -28,6 +28,9 @@ import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.ClassRule; @@ -56,22 +59,18 @@ public class SecurityRolesTests { .authc(AUTHC_HTTPBASIC_INTERNAL).users(USER_SR).build(); @Test - public void testSecurityRolesAnon() throws Exception { + public void testSecurityRoles() throws Exception { try (TestRestClient client = cluster.getRestClient(USER_SR)) { HttpResponse response = client.getAuthInfo(); - Assert.assertEquals(response.getStatusCode(), HttpStatus.SC_OK); + assertThat(response.getStatusCode(), equalTo(HttpStatus.SC_OK)); // Check username - String username = response.getTextFromJsonBody("/user_name"); - Assert.assertEquals("sr_user", username); - - // Check security roles - String securityRole = response.getTextFromJsonBody("/roles/0"); - Assert.assertEquals("user_sr_user__abc_ber", securityRole); + assertThat(response.getTextFromJsonBody("/user_name"), equalTo("sr_user")); - securityRole = response.getTextFromJsonBody("/roles/0"); - Assert.assertEquals("user_sr_user__def_efg", securityRole); + // Check security roles + assertThat(response.getTextFromJsonBody("/roles/0"), equalTo("user_sr_user__abc_ber")); + assertThat(response.getTextFromJsonBody("/roles/1"), equalTo("user_sr_user__def_efg")); } } From 99fefce25e215e04a4997d0204e915adc74cdff6 Mon Sep 17 00:00:00 2001 From: jochenkressin Date: Mon, 1 Aug 2022 22:08:40 +0200 Subject: [PATCH 06/16] renamed ClusterConfiguration to ClusterManager --- .../org/opensearch/test/GenericIntegrationTest.java | 4 ++-- .../org/opensearch/test/PrivilegesEvaluatorTest.java | 4 ++-- .../java/org/opensearch/test/SecurityRolesTests.java | 4 ++-- .../{ClusterConfiguration.java => ClusterManager.java} | 4 ++-- .../test/framework/cluster/LocalCluster.java | 10 +++++----- .../test/framework/cluster/LocalOpenSearchCluster.java | 6 +++--- 6 files changed, 16 insertions(+), 16 deletions(-) rename src/newTest/java/org/opensearch/test/framework/cluster/{ClusterConfiguration.java => ClusterManager.java} (98%) diff --git a/src/newTest/java/org/opensearch/test/GenericIntegrationTest.java b/src/newTest/java/org/opensearch/test/GenericIntegrationTest.java index 77bfc04516..c01ec32fa8 100644 --- a/src/newTest/java/org/opensearch/test/GenericIntegrationTest.java +++ b/src/newTest/java/org/opensearch/test/GenericIntegrationTest.java @@ -23,7 +23,7 @@ import org.opensearch.test.framework.TestIndex; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.TestSecurityConfig.Role; -import org.opensearch.test.framework.cluster.ClusterConfiguration; +import org.opensearch.test.framework.cluster.ClusterManager; import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; @@ -49,7 +49,7 @@ public class GenericIntegrationTest { // build our test cluster as a ClassRule @ClassRule - public static LocalCluster cluster = new LocalCluster.Builder().clusterConfiguration(ClusterConfiguration.THREE_MASTERS) + public static LocalCluster cluster = new LocalCluster.Builder().clusterConfiguration(ClusterManager.THREE_MASTERS) .authc(AUTHC_HTTPBASIC_INTERNAL) .users(INDEX_A_USER) .indices(INDEX_A, INDEX_B).build(); diff --git a/src/newTest/java/org/opensearch/test/PrivilegesEvaluatorTest.java b/src/newTest/java/org/opensearch/test/PrivilegesEvaluatorTest.java index d21be816d8..cbea84ff2f 100644 --- a/src/newTest/java/org/opensearch/test/PrivilegesEvaluatorTest.java +++ b/src/newTest/java/org/opensearch/test/PrivilegesEvaluatorTest.java @@ -21,7 +21,7 @@ import org.junit.runner.RunWith; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.TestSecurityConfig.Role; -import org.opensearch.test.framework.cluster.ClusterConfiguration; +import org.opensearch.test.framework.cluster.ClusterManager; import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; @@ -47,7 +47,7 @@ public class PrivilegesEvaluatorTest { @ClassRule public static LocalCluster cluster = new LocalCluster.Builder() - .clusterConfiguration(ClusterConfiguration.THREE_MASTERS).authc(AUTHC_HTTPBASIC_INTERNAL) + .clusterConfiguration(ClusterManager.THREE_MASTERS).authc(AUTHC_HTTPBASIC_INTERNAL) .users(NEGATIVE_LOOKAHEAD, NEGATED_REGEX).build(); @Test diff --git a/src/newTest/java/org/opensearch/test/SecurityRolesTests.java b/src/newTest/java/org/opensearch/test/SecurityRolesTests.java index e29c281655..956b3eb17f 100644 --- a/src/newTest/java/org/opensearch/test/SecurityRolesTests.java +++ b/src/newTest/java/org/opensearch/test/SecurityRolesTests.java @@ -38,7 +38,7 @@ import org.junit.runner.RunWith; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.TestSecurityConfig.Role; -import org.opensearch.test.framework.cluster.ClusterConfiguration; +import org.opensearch.test.framework.cluster.ClusterManager; import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse; @@ -55,7 +55,7 @@ public class SecurityRolesTests { @ClassRule public static LocalCluster cluster = new LocalCluster.Builder() - .clusterConfiguration(ClusterConfiguration.THREE_MASTERS).anonymousAuth(true) + .clusterConfiguration(ClusterManager.THREE_MASTERS).anonymousAuth(true) .authc(AUTHC_HTTPBASIC_INTERNAL).users(USER_SR).build(); @Test diff --git a/src/newTest/java/org/opensearch/test/framework/cluster/ClusterConfiguration.java b/src/newTest/java/org/opensearch/test/framework/cluster/ClusterManager.java similarity index 98% rename from src/newTest/java/org/opensearch/test/framework/cluster/ClusterConfiguration.java rename to src/newTest/java/org/opensearch/test/framework/cluster/ClusterManager.java index 08e8eb31c1..5d6deb7f3f 100644 --- a/src/newTest/java/org/opensearch/test/framework/cluster/ClusterConfiguration.java +++ b/src/newTest/java/org/opensearch/test/framework/cluster/ClusterManager.java @@ -45,7 +45,7 @@ import com.google.common.collect.Lists; -public enum ClusterConfiguration { +public enum ClusterManager { //first one needs to be a master //HUGE(new NodeSettings(true, false, false), new NodeSettings(true, false, false), new NodeSettings(true, false, false), new NodeSettings(false, true,false), new NodeSettings(false, true, false)), @@ -62,7 +62,7 @@ public enum ClusterConfiguration { private List nodeSettings = new LinkedList<>(); - private ClusterConfiguration(NodeSettings... settings) { + private ClusterManager(NodeSettings... settings) { nodeSettings.addAll(Arrays.asList(settings)); } diff --git a/src/newTest/java/org/opensearch/test/framework/cluster/LocalCluster.java b/src/newTest/java/org/opensearch/test/framework/cluster/LocalCluster.java index 2ed877c021..ee01344bfb 100644 --- a/src/newTest/java/org/opensearch/test/framework/cluster/LocalCluster.java +++ b/src/newTest/java/org/opensearch/test/framework/cluster/LocalCluster.java @@ -66,7 +66,7 @@ public class LocalCluster extends ExternalResource implements AutoCloseable, Ope protected static final AtomicLong num = new AtomicLong(); private final List> plugins; - private final ClusterConfiguration clusterConfiguration; + private final ClusterManager clusterConfiguration; private final TestSecurityConfig testSecurityConfig; private Settings nodeOverride; private final String clusterName; @@ -78,7 +78,7 @@ public class LocalCluster extends ExternalResource implements AutoCloseable, Ope private final List testIndices; private LocalCluster(String clusterName, TestSecurityConfig testSgConfig, Settings nodeOverride, - ClusterConfiguration clusterConfiguration, List> plugins, TestCertificates testCertificates, + ClusterManager clusterConfiguration, List> plugins, TestCertificates testCertificates, List clusterDependencies, Map remotes, List testIndices) { this.plugins = plugins; this.testCertificates = testCertificates; @@ -221,7 +221,7 @@ public static class Builder { private Map remoteClusters = new HashMap<>(); private List clusterDependencies = new ArrayList<>(); private List testIndices = new ArrayList<>(); - private ClusterConfiguration clusterConfiguration = ClusterConfiguration.DEFAULT; + private ClusterManager clusterConfiguration = ClusterManager.DEFAULT; private TestSecurityConfig testSecurityConfig = new TestSecurityConfig(); private String clusterName = "local_cluster"; private TestCertificates testCertificates; @@ -238,13 +238,13 @@ public Builder dependsOn(Object object) { return this; } - public Builder clusterConfiguration(ClusterConfiguration clusterConfiguration) { + public Builder clusterConfiguration(ClusterManager clusterConfiguration) { this.clusterConfiguration = clusterConfiguration; return this; } public Builder singleNode() { - this.clusterConfiguration = ClusterConfiguration.SINGLENODE; + this.clusterConfiguration = ClusterManager.SINGLENODE; return this; } diff --git a/src/newTest/java/org/opensearch/test/framework/cluster/LocalOpenSearchCluster.java b/src/newTest/java/org/opensearch/test/framework/cluster/LocalOpenSearchCluster.java index 6c92b12789..147837a2cf 100644 --- a/src/newTest/java/org/opensearch/test/framework/cluster/LocalOpenSearchCluster.java +++ b/src/newTest/java/org/opensearch/test/framework/cluster/LocalOpenSearchCluster.java @@ -70,7 +70,7 @@ import org.opensearch.node.PluginAwareNode; import org.opensearch.plugins.Plugin; import org.opensearch.test.framework.certificate.TestCertificates; -import org.opensearch.test.framework.cluster.ClusterConfiguration.NodeSettings; +import org.opensearch.test.framework.cluster.ClusterManager.NodeSettings; import org.opensearch.transport.BindTransportException; import org.opensearch.transport.TransportInfo; @@ -86,7 +86,7 @@ public class LocalOpenSearchCluster { private static final Logger log = LogManager.getLogger(LocalOpenSearchCluster.class); private final String clusterName; - private final ClusterConfiguration clusterConfiguration; + private final ClusterManager clusterConfiguration; private final NodeSettingsSupplier nodeSettingsSupplier; private final List> additionalPlugins; private final List allNodes = new ArrayList<>(); @@ -102,7 +102,7 @@ public class LocalOpenSearchCluster { private boolean started; private Random random = new Random(); - public LocalOpenSearchCluster(String clusterName, ClusterConfiguration clusterConfiguration, NodeSettingsSupplier nodeSettingsSupplier, + public LocalOpenSearchCluster(String clusterName, ClusterManager clusterConfiguration, NodeSettingsSupplier nodeSettingsSupplier, List> additionalPlugins, TestCertificates testCertificates) { this.clusterName = clusterName; this.clusterConfiguration = clusterConfiguration; From 89c94e508859c3a8b1d03430355a8b23f8ac25de Mon Sep 17 00:00:00 2001 From: jochenkressin Date: Tue, 2 Aug 2022 15:54:39 +0200 Subject: [PATCH 07/16] throw exception if index already exists --- .../java/org/opensearch/test/GenericIntegrationTest.java | 2 +- .../java/org/opensearch/test/framework/TestIndex.java | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/newTest/java/org/opensearch/test/GenericIntegrationTest.java b/src/newTest/java/org/opensearch/test/GenericIntegrationTest.java index c01ec32fa8..17c78239ea 100644 --- a/src/newTest/java/org/opensearch/test/GenericIntegrationTest.java +++ b/src/newTest/java/org/opensearch/test/GenericIntegrationTest.java @@ -51,7 +51,7 @@ public class GenericIntegrationTest { @ClassRule public static LocalCluster cluster = new LocalCluster.Builder().clusterConfiguration(ClusterManager.THREE_MASTERS) .authc(AUTHC_HTTPBASIC_INTERNAL) - .users(INDEX_A_USER) + .users(USER_ADMIN, INDEX_A_USER) .indices(INDEX_A, INDEX_B).build(); @Test diff --git a/src/newTest/java/org/opensearch/test/framework/TestIndex.java b/src/newTest/java/org/opensearch/test/framework/TestIndex.java index 035418c2b4..1dbf3f3ca9 100644 --- a/src/newTest/java/org/opensearch/test/framework/TestIndex.java +++ b/src/newTest/java/org/opensearch/test/framework/TestIndex.java @@ -44,11 +44,9 @@ public TestIndex(String name, Settings settings) { } - public void create(Client client) { - if (!client.admin().indices().exists(new IndicesExistsRequest(name)).actionGet().isExists()) { - client.admin().indices().create(new CreateIndexRequest(name).settings(settings)).actionGet(); - } - } + public void create(Client client) { + client.admin().indices().create(new CreateIndexRequest(name).settings(settings)).actionGet(); + } public String getName() { return name; From b91747a43970b4648787d4196d9c640fe009f66f Mon Sep 17 00:00:00 2001 From: jochenkressin Date: Tue, 2 Aug 2022 16:16:25 +0200 Subject: [PATCH 08/16] fixed incorrect test --- .../java/org/opensearch/test/GenericIntegrationTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/newTest/java/org/opensearch/test/GenericIntegrationTest.java b/src/newTest/java/org/opensearch/test/GenericIntegrationTest.java index 17c78239ea..98fd63b084 100644 --- a/src/newTest/java/org/opensearch/test/GenericIntegrationTest.java +++ b/src/newTest/java/org/opensearch/test/GenericIntegrationTest.java @@ -67,7 +67,7 @@ public void testIndexAUserHasOnlyAccessToIndexA() throws Exception { assertThat(client.get("index-a/_search?pretty").getStatusCode(), equalTo(HttpStatus.SC_OK)); // demo: work with JSON response body and check values assertThat(client.get("index-a/_search?pretty").getIntFromJsonBody("/_source/hits/value"), equalTo(0)); - assertThat(client.get("index-b/_search?pretty"), equalTo(HttpStatus.SC_FORBIDDEN)); + assertThat(client.get("index-b/_search?pretty").getStatusCode(), equalTo(HttpStatus.SC_FORBIDDEN)); } } From e57ca874ff192507c6c82238fb09ff3de2e11bf1 Mon Sep 17 00:00:00 2001 From: jochenkressin Date: Tue, 2 Aug 2022 16:28:47 +0200 Subject: [PATCH 09/16] fixed code hygiene issues --- .../test/GenericIntegrationTest.java | 12 +++---- .../test/PrivilegesEvaluatorTest.java | 10 +++--- .../opensearch/test/SecurityRolesTests.java | 12 +++---- .../opensearch/test/framework/TestIndex.java | 1 - .../test/framework/TestSecurityConfig.java | 3 +- .../framework/cluster/ClusterManager.java | 5 ++- .../test/framework/cluster/LocalCluster.java | 2 +- .../cluster/LocalOpenSearchCluster.java | 18 ++--------- .../cluster/OpenSearchClientProvider.java | 2 +- .../test/framework/cluster/SocketUtils.java | 2 +- .../framework/cluster/TestRestClient.java | 31 +++++-------------- 11 files changed, 33 insertions(+), 65 deletions(-) diff --git a/src/newTest/java/org/opensearch/test/GenericIntegrationTest.java b/src/newTest/java/org/opensearch/test/GenericIntegrationTest.java index 98fd63b084..b993a7a317 100644 --- a/src/newTest/java/org/opensearch/test/GenericIntegrationTest.java +++ b/src/newTest/java/org/opensearch/test/GenericIntegrationTest.java @@ -11,15 +11,12 @@ package org.opensearch.test; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.equalTo; -import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; -import static org.opensearch.test.framework.TestSecurityConfig.User.USER_ADMIN; - +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; import org.apache.http.HttpStatus; import org.junit.ClassRule; import org.junit.Test; import org.junit.runner.RunWith; + import org.opensearch.test.framework.TestIndex; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.TestSecurityConfig.Role; @@ -27,7 +24,10 @@ import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; -import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; +import static org.opensearch.test.framework.TestSecurityConfig.User.USER_ADMIN; /** * WIP diff --git a/src/newTest/java/org/opensearch/test/PrivilegesEvaluatorTest.java b/src/newTest/java/org/opensearch/test/PrivilegesEvaluatorTest.java index cbea84ff2f..b3521af808 100644 --- a/src/newTest/java/org/opensearch/test/PrivilegesEvaluatorTest.java +++ b/src/newTest/java/org/opensearch/test/PrivilegesEvaluatorTest.java @@ -11,21 +11,21 @@ package org.opensearch.test; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.equalTo; -import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; - +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; import org.apache.http.HttpStatus; import org.junit.ClassRule; import org.junit.Test; import org.junit.runner.RunWith; + import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.TestSecurityConfig.Role; import org.opensearch.test.framework.cluster.ClusterManager; import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; -import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; /** * This is a port for the test diff --git a/src/newTest/java/org/opensearch/test/SecurityRolesTests.java b/src/newTest/java/org/opensearch/test/SecurityRolesTests.java index 956b3eb17f..416b6580bd 100644 --- a/src/newTest/java/org/opensearch/test/SecurityRolesTests.java +++ b/src/newTest/java/org/opensearch/test/SecurityRolesTests.java @@ -26,16 +26,12 @@ package org.opensearch.test; -import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.*; - +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; import org.apache.http.HttpStatus; -import org.junit.Assert; import org.junit.ClassRule; import org.junit.Test; import org.junit.runner.RunWith; + import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.TestSecurityConfig.Role; import org.opensearch.test.framework.cluster.ClusterManager; @@ -43,7 +39,9 @@ import org.opensearch.test.framework.cluster.TestRestClient; import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse; -import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; @RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) @ThreadLeakScope(ThreadLeakScope.Scope.NONE) diff --git a/src/newTest/java/org/opensearch/test/framework/TestIndex.java b/src/newTest/java/org/opensearch/test/framework/TestIndex.java index 1dbf3f3ca9..97ace5e4b8 100644 --- a/src/newTest/java/org/opensearch/test/framework/TestIndex.java +++ b/src/newTest/java/org/opensearch/test/framework/TestIndex.java @@ -29,7 +29,6 @@ package org.opensearch.test.framework; import org.opensearch.action.admin.indices.create.CreateIndexRequest; -import org.opensearch.action.admin.indices.exists.indices.IndicesExistsRequest; import org.opensearch.client.Client; import org.opensearch.common.settings.Settings; diff --git a/src/newTest/java/org/opensearch/test/framework/TestSecurityConfig.java b/src/newTest/java/org/opensearch/test/framework/TestSecurityConfig.java index 38523bb13f..ef8afc33c3 100644 --- a/src/newTest/java/org/opensearch/test/framework/TestSecurityConfig.java +++ b/src/newTest/java/org/opensearch/test/framework/TestSecurityConfig.java @@ -38,12 +38,12 @@ import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.function.Supplier; import java.util.stream.Collectors; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.bouncycastle.crypto.generators.OpenBSDBCrypt; + import org.opensearch.action.admin.indices.create.CreateIndexRequest; import org.opensearch.action.index.IndexRequest; import org.opensearch.action.support.WriteRequest.RefreshPolicy; @@ -56,7 +56,6 @@ import org.opensearch.security.action.configupdate.ConfigUpdateRequest; import org.opensearch.security.action.configupdate.ConfigUpdateResponse; import org.opensearch.security.securityconf.impl.CType; -import org.opensearch.test.framework.TestSecurityConfig.Role; import org.opensearch.test.framework.cluster.NestedValueMap; import org.opensearch.test.framework.cluster.NestedValueMap.Path; import org.opensearch.test.framework.cluster.OpenSearchClientProvider.UserCredentialsHolder; diff --git a/src/newTest/java/org/opensearch/test/framework/cluster/ClusterManager.java b/src/newTest/java/org/opensearch/test/framework/cluster/ClusterManager.java index 5d6deb7f3f..dfb4e95937 100644 --- a/src/newTest/java/org/opensearch/test/framework/cluster/ClusterManager.java +++ b/src/newTest/java/org/opensearch/test/framework/cluster/ClusterManager.java @@ -35,6 +35,8 @@ import java.util.List; import java.util.stream.Collectors; +import com.google.common.collect.Lists; + import org.opensearch.index.reindex.ReindexPlugin; import org.opensearch.join.ParentJoinPlugin; import org.opensearch.percolator.PercolatorPlugin; @@ -43,8 +45,6 @@ import org.opensearch.security.OpenSearchSecurityPlugin; import org.opensearch.transport.Netty4Plugin; -import com.google.common.collect.Lists; - public enum ClusterManager { //first one needs to be a master //HUGE(new NodeSettings(true, false, false), new NodeSettings(true, false, false), new NodeSettings(true, false, false), new NodeSettings(false, true,false), new NodeSettings(false, true, false)), @@ -108,7 +108,6 @@ public NodeSettings(boolean masterNode, boolean dataNode) { public NodeSettings(boolean masterNode, boolean dataNode, List> additionalPlugins) { this(masterNode, dataNode); - this.plugins.addAll(additionalPlugins); } diff --git a/src/newTest/java/org/opensearch/test/framework/cluster/LocalCluster.java b/src/newTest/java/org/opensearch/test/framework/cluster/LocalCluster.java index ee01344bfb..590f0ae280 100644 --- a/src/newTest/java/org/opensearch/test/framework/cluster/LocalCluster.java +++ b/src/newTest/java/org/opensearch/test/framework/cluster/LocalCluster.java @@ -37,12 +37,12 @@ import java.util.Map; import java.util.Random; import java.util.concurrent.atomic.AtomicLong; -import java.util.function.Supplier; import java.util.stream.Collectors; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.junit.rules.ExternalResource; + import org.opensearch.client.Client; import org.opensearch.common.settings.Settings; import org.opensearch.node.PluginAwareNode; diff --git a/src/newTest/java/org/opensearch/test/framework/cluster/LocalOpenSearchCluster.java b/src/newTest/java/org/opensearch/test/framework/cluster/LocalOpenSearchCluster.java index 147837a2cf..3c42a85835 100644 --- a/src/newTest/java/org/opensearch/test/framework/cluster/LocalOpenSearchCluster.java +++ b/src/newTest/java/org/opensearch/test/framework/cluster/LocalOpenSearchCluster.java @@ -28,8 +28,6 @@ package org.opensearch.test.framework.cluster; -import static org.junit.Assert.assertEquals; - import java.io.File; import java.io.IOException; import java.net.InetAddress; @@ -43,38 +41,28 @@ import java.util.Random; import java.util.SortedSet; import java.util.concurrent.CompletableFuture; -import java.util.function.Predicate; import java.util.stream.Collectors; +import com.google.common.net.InetAddresses; import org.apache.commons.io.FileUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.opensearch.OpenSearchTimeoutException; + import org.opensearch.action.admin.cluster.health.ClusterHealthResponse; -import org.opensearch.action.admin.cluster.node.info.NodeInfo; -import org.opensearch.action.admin.cluster.node.info.NodesInfoRequest; -import org.opensearch.action.admin.cluster.node.info.NodesInfoResponse; -import org.opensearch.action.admin.indices.template.put.PutIndexTemplateRequest; -import org.opensearch.action.support.master.AcknowledgedResponse; import org.opensearch.client.AdminClient; import org.opensearch.client.Client; import org.opensearch.cluster.health.ClusterHealthStatus; -import org.opensearch.cluster.node.DiscoveryNodeRole; import org.opensearch.common.Strings; import org.opensearch.common.settings.Settings; -import org.opensearch.common.transport.TransportAddress; import org.opensearch.common.unit.TimeValue; -import org.opensearch.common.xcontent.XContentType; import org.opensearch.http.BindHttpException; -import org.opensearch.http.HttpInfo; import org.opensearch.node.PluginAwareNode; import org.opensearch.plugins.Plugin; import org.opensearch.test.framework.certificate.TestCertificates; import org.opensearch.test.framework.cluster.ClusterManager.NodeSettings; import org.opensearch.transport.BindTransportException; -import org.opensearch.transport.TransportInfo; -import com.google.common.net.InetAddresses; +import static org.junit.Assert.assertEquals; public class LocalOpenSearchCluster { diff --git a/src/newTest/java/org/opensearch/test/framework/cluster/OpenSearchClientProvider.java b/src/newTest/java/org/opensearch/test/framework/cluster/OpenSearchClientProvider.java index c98d052c57..02817a1dfa 100644 --- a/src/newTest/java/org/opensearch/test/framework/cluster/OpenSearchClientProvider.java +++ b/src/newTest/java/org/opensearch/test/framework/cluster/OpenSearchClientProvider.java @@ -35,7 +35,6 @@ import java.security.cert.X509Certificate; import java.util.Arrays; import java.util.Base64; -import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; @@ -46,6 +45,7 @@ import org.apache.http.Header; import org.apache.http.message.BasicHeader; + import org.opensearch.security.support.PemKeyReader; import org.opensearch.test.framework.certificate.TestCertificates; diff --git a/src/newTest/java/org/opensearch/test/framework/cluster/SocketUtils.java b/src/newTest/java/org/opensearch/test/framework/cluster/SocketUtils.java index 36718b6a43..35ef48f022 100644 --- a/src/newTest/java/org/opensearch/test/framework/cluster/SocketUtils.java +++ b/src/newTest/java/org/opensearch/test/framework/cluster/SocketUtils.java @@ -316,4 +316,4 @@ SortedSet findAvailablePorts(int numRequested, int minPort, int maxPort } } -} \ No newline at end of file +} diff --git a/src/newTest/java/org/opensearch/test/framework/cluster/TestRestClient.java b/src/newTest/java/org/opensearch/test/framework/cluster/TestRestClient.java index 8f486916e2..dbe13d5cdf 100644 --- a/src/newTest/java/org/opensearch/test/framework/cluster/TestRestClient.java +++ b/src/newTest/java/org/opensearch/test/framework/cluster/TestRestClient.java @@ -41,6 +41,8 @@ import javax.net.ssl.SSLContext; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; import org.apache.commons.io.IOUtils; import org.apache.http.Header; import org.apache.http.HttpEntity; @@ -64,14 +66,11 @@ import org.apache.http.message.BasicHeader; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; + import org.opensearch.common.Strings; import org.opensearch.common.xcontent.ToXContentObject; import org.opensearch.security.DefaultObjectMapper; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.google.common.collect.Lists; - public class TestRestClient implements AutoCloseable { private static final Logger log = LogManager.getLogger(TestRestClient.class); @@ -198,20 +197,6 @@ public TestRestClient trackResources() { return this; } - private void cleanupResources() { - if (puttedResourcesList.size() > 0) { - log.info("Cleaning up " + puttedResourcesList); - - for (String resource : Lists.reverse(puttedResourcesList)) { - try { - delete(resource); - } catch (Exception e) { - log.error("Error cleaning up created resources " + resource, e); - } - } - } - } - protected final String getHttpServerUri() { return "http" + (enableHTTPClientSSL ? "s" : "") + "://" + nodeHttpAddress.getHostString() + ":" + nodeHttpAddress.getPort(); } @@ -373,11 +358,6 @@ public void setLocalAddress(InetAddress inetAddress) { } } - @Override - public void close() throws IOException { - cleanupResources(); - } - public boolean isSendHTTPClientCertificate() { return sendHTTPClientCertificate; } @@ -386,4 +366,9 @@ public void setSendHTTPClientCertificate(boolean sendHTTPClientCertificate) { this.sendHTTPClientCertificate = sendHTTPClientCertificate; } + @Override + public void close() throws Exception { + // TODO: Is there anything to clean up here? + } + } From cf77fa31a21a0925261c4fc5ba5c7b2eda7f7f79 Mon Sep 17 00:00:00 2001 From: kt-eliatra Date: Mon, 8 Aug 2022 09:35:09 +0200 Subject: [PATCH 10/16] remove duplicate configuration declaration, fix newTest** configuration setup, declare dependencies required to run new tests --- build.gradle | 72 +++++++++++++++++++++++++++------------------------- 1 file changed, 37 insertions(+), 35 deletions(-) diff --git a/build.gradle b/build.gradle index 36ad5bd553..ec5d906487 100644 --- a/build.gradle +++ b/build.gradle @@ -210,29 +210,40 @@ bundlePlugin { } } -configurations.all { - resolutionStrategy { - force 'commons-codec:commons-codec:1.14' - force 'org.slf4j:slf4j-api:1.7.30' - force 'org.scala-lang:scala-library:2.13.8' - force 'commons-io:commons-io:2.11.0' - force "com.fasterxml.jackson:jackson-bom:${versions.jackson}" - force "com.fasterxml.jackson.core:jackson-core:${versions.jackson}" - force "com.fasterxml.jackson.datatype:jackson-datatype-jdk8:${versions.jackson}" - force "com.fasterxml.jackson.core:jackson-databind:${versions.jackson_databind}" - force "io.netty:netty-buffer:${versions.netty}" - force "io.netty:netty-common:${versions.netty}" - force "io.netty:netty-handler:${versions.netty}" - force "io.netty:netty-transport:${versions.netty}" +configurations { + all { + resolutionStrategy { + force 'commons-codec:commons-codec:1.14' + force 'org.slf4j:slf4j-api:1.7.30' + force 'org.scala-lang:scala-library:2.13.8' + force 'commons-io:commons-io:2.11.0' + force "com.fasterxml.jackson:jackson-bom:${versions.jackson}" + force "com.fasterxml.jackson.core:jackson-core:${versions.jackson}" + force "com.fasterxml.jackson.datatype:jackson-datatype-jdk8:${versions.jackson}" + force "com.fasterxml.jackson.core:jackson-databind:${versions.jackson_databind}" + force "io.netty:netty-buffer:${versions.netty}" + force "io.netty:netty-common:${versions.netty}" + force "io.netty:netty-handler:${versions.netty}" + force "io.netty:netty-transport:${versions.netty}" } + } + + newTestImplementation.extendsFrom implementation + newTestRuntimeOnly.extendsFrom runtimeOnly } //create source set 'newTest' //add classes from the main source set to the compilation and runtime classpaths of the newTest sourceSets { newTest { - compileClasspath += sourceSets.main.output - runtimeClasspath += sourceSets.main.output + java { + compileClasspath += sourceSets.main.output + runtimeClasspath += sourceSets.main.output + srcDir file ('src/newTest/java') + } + resources { + srcDir file('src/newTest/resources') + } } } @@ -250,25 +261,6 @@ task newTest(type: Test) { //run the newTest task before the check task check.dependsOn newTest -configurations { - all { - resolutionStrategy { - force 'commons-codec:commons-codec:1.14' - force 'org.apache.santuario:xmlsec:2.2.3' - force 'org.cryptacular:cryptacular:1.2.4' - force 'net.minidev:json-smart:2.4.7' - force 'commons-cli:commons-cli:1.3.1' - force 'org.apache.httpcomponents:httpcore:4.4.12' - force "org.apache.commons:commons-lang3:3.4" - force "org.springframework:spring-core:5.3.20" - force "com.google.guava:guava:30.0-jre" - } - } - - //use testImplementation dependencies in the newTestImplementation configuration - newTestImplementation.extendsFrom testImplementation -} - dependencies { implementation 'jakarta.annotation:jakarta.annotation-api:1.3.5' implementation "org.opensearch.plugin:transport-netty4-client:${opensearch_version}" @@ -391,6 +383,16 @@ dependencies { implementation "com.fasterxml.jackson.core:jackson-databind:${versions.jackson_databind}" compileOnly "org.opensearch:opensearch:${opensearch_version}" + + //new test framework: + newTestImplementation('com.carrotsearch.randomizedtesting:randomizedtesting-runner:2.7.1') { + exclude(group: 'junit', module: 'junit') + } + newTestImplementation 'junit:junit:4.13.2' + newTestImplementation "org.opensearch.plugin:reindex-client:${opensearch_version}" + newTestImplementation "org.opensearch.plugin:percolator-client:${opensearch_version}" + newTestImplementation 'commons-io:commons-io:2.7' + newTestImplementation 'org.apache.logging.log4j:log4j-core:2.17.1' } jar { From b23b5363377ac8362ebc913d95516ce562dce192 Mon Sep 17 00:00:00 2001 From: Lukasz Soszynski Date: Mon, 8 Aug 2022 16:13:28 +0200 Subject: [PATCH 11/16] TestRestClient uses unchecked exceptions --- .../cluster/RestClientException.java | 7 +++ .../framework/cluster/TestRestClient.java | 57 ++++++++++--------- 2 files changed, 38 insertions(+), 26 deletions(-) create mode 100644 src/newTest/java/org/opensearch/test/framework/cluster/RestClientException.java diff --git a/src/newTest/java/org/opensearch/test/framework/cluster/RestClientException.java b/src/newTest/java/org/opensearch/test/framework/cluster/RestClientException.java new file mode 100644 index 0000000000..78cc158863 --- /dev/null +++ b/src/newTest/java/org/opensearch/test/framework/cluster/RestClientException.java @@ -0,0 +1,7 @@ +package org.opensearch.test.framework.cluster; + +class RestClientException extends RuntimeException { + public RestClientException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/newTest/java/org/opensearch/test/framework/cluster/TestRestClient.java b/src/newTest/java/org/opensearch/test/framework/cluster/TestRestClient.java index dbe13d5cdf..69e5746a5c 100644 --- a/src/newTest/java/org/opensearch/test/framework/cluster/TestRestClient.java +++ b/src/newTest/java/org/opensearch/test/framework/cluster/TestRestClient.java @@ -29,6 +29,7 @@ package org.opensearch.test.framework.cluster; import java.io.IOException; +import java.io.UnsupportedEncodingException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; @@ -46,6 +47,7 @@ import org.apache.commons.io.IOUtils; import org.apache.http.Header; import org.apache.http.HttpEntity; +import org.apache.http.client.ClientProtocolException; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpDelete; @@ -92,25 +94,25 @@ public TestRestClient(InetSocketAddress nodeHttpAddress, List
headers, S this.sslContext = sslContext; } - public HttpResponse get(String path, Header... headers) throws Exception { + public HttpResponse get(String path, Header... headers) { return executeRequest(new HttpGet(getHttpServerUri() + "/" + path), headers); } - public HttpResponse getAuthInfo( Header... headers) throws Exception { + public HttpResponse getAuthInfo( Header... headers) { return executeRequest(new HttpGet(getHttpServerUri() + "/_opendistro/_security/authinfo?pretty"), headers); } - public HttpResponse head(String path, Header... headers) throws Exception { + public HttpResponse head(String path, Header... headers) { return executeRequest(new HttpHead(getHttpServerUri() + "/" + path), headers); } - public HttpResponse options(String path, Header... headers) throws Exception { + public HttpResponse options(String path, Header... headers) { return executeRequest(new HttpOptions(getHttpServerUri() + "/" + path), headers); } - public HttpResponse putJson(String path, String body, Header... headers) throws Exception { + public HttpResponse putJson(String path, String body, Header... headers) { HttpPut uriRequest = new HttpPut(getHttpServerUri() + "/" + path); - uriRequest.setEntity(new StringEntity(body)); + uriRequest.setEntity(toStringEntity(body)); HttpResponse response = executeRequest(uriRequest, mergeHeaders(CONTENT_TYPE_JSON, headers)); @@ -122,11 +124,19 @@ public HttpResponse putJson(String path, String body, Header... headers) throws return response; } - public HttpResponse putJson(String path, ToXContentObject body) throws Exception { + private StringEntity toStringEntity(String body) { + try { + return new StringEntity(body); + } catch (UnsupportedEncodingException e) { + throw new RestClientException("Cannot create string entity", e); + } + } + + public HttpResponse putJson(String path, ToXContentObject body) { return putJson(path, Strings.toString(body)); } - public HttpResponse put(String path) throws Exception { + public HttpResponse put(String path) { HttpPut uriRequest = new HttpPut(getHttpServerUri() + "/" + path); HttpResponse response = executeRequest(uriRequest); @@ -138,37 +148,35 @@ public HttpResponse put(String path) throws Exception { return response; } - public HttpResponse delete(String path, Header... headers) throws Exception { + public HttpResponse delete(String path, Header... headers) { return executeRequest(new HttpDelete(getHttpServerUri() + "/" + path), headers); } - public HttpResponse postJson(String path, String body, Header... headers) throws Exception { + public HttpResponse postJson(String path, String body, Header... headers) { HttpPost uriRequest = new HttpPost(getHttpServerUri() + "/" + path); - uriRequest.setEntity(new StringEntity(body)); + uriRequest.setEntity(toStringEntity(body)); return executeRequest(uriRequest, mergeHeaders(CONTENT_TYPE_JSON, headers)); } - public HttpResponse postJson(String path, ToXContentObject body) throws Exception { + public HttpResponse postJson(String path, ToXContentObject body) { return postJson(path, Strings.toString(body)); } - public HttpResponse post(String path) throws Exception { + public HttpResponse post(String path) { HttpPost uriRequest = new HttpPost(getHttpServerUri() + "/" + path); return executeRequest(uriRequest); } - public HttpResponse patch(String path, String body) throws Exception { + public HttpResponse patch(String path, String body) { HttpPatch uriRequest = new HttpPatch(getHttpServerUri() + "/" + path); - uriRequest.setEntity(new StringEntity(body)); + uriRequest.setEntity(toStringEntity(body)); return executeRequest(uriRequest, CONTENT_TYPE_JSON); } - public HttpResponse executeRequest(HttpUriRequest uriRequest, Header... requestSpecificHeaders) throws Exception { + public HttpResponse executeRequest(HttpUriRequest uriRequest, Header... requestSpecificHeaders) { - CloseableHttpClient httpClient = null; - try { + try(CloseableHttpClient httpClient = getHTTPClient()) { - httpClient = getHTTPClient(); if (requestSpecificHeaders != null && requestSpecificHeaders.length > 0) { for (int i = 0; i < requestSpecificHeaders.length; i++) { @@ -184,11 +192,8 @@ public HttpResponse executeRequest(HttpUriRequest uriRequest, Header... requestS HttpResponse res = new HttpResponse(httpClient.execute(uriRequest)); log.debug(res.getBody()); return res; - } finally { - - if (httpClient != null) { - httpClient.close(); - } + } catch (IOException e) { + throw new RestClientException("Error occured during HTTP request execution", e); } } @@ -201,7 +206,7 @@ protected final String getHttpServerUri() { return "http" + (enableHTTPClientSSL ? "s" : "") + "://" + nodeHttpAddress.getHostString() + ":" + nodeHttpAddress.getPort(); } - protected final CloseableHttpClient getHTTPClient() throws Exception { + protected final CloseableHttpClient getHTTPClient() { final HttpClientBuilder hcb = HttpClients.custom(); @@ -367,7 +372,7 @@ public void setSendHTTPClientCertificate(boolean sendHTTPClientCertificate) { } @Override - public void close() throws Exception { + public void close() { // TODO: Is there anything to clean up here? } From f81910e268804135b82746eab0f780d623ed465d Mon Sep 17 00:00:00 2001 From: Lukasz Soszynski Date: Mon, 8 Aug 2022 16:15:56 +0200 Subject: [PATCH 12/16] List of plugins in ClusterManager is immutable. --- .../framework/cluster/ClusterManager.java | 50 ++++++++++--------- .../cluster/LocalOpenSearchCluster.java | 4 +- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/src/newTest/java/org/opensearch/test/framework/cluster/ClusterManager.java b/src/newTest/java/org/opensearch/test/framework/cluster/ClusterManager.java index dfb4e95937..ce9903b59b 100644 --- a/src/newTest/java/org/opensearch/test/framework/cluster/ClusterManager.java +++ b/src/newTest/java/org/opensearch/test/framework/cluster/ClusterManager.java @@ -28,15 +28,14 @@ package org.opensearch.test.framework.cluster; -import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.LinkedList; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; -import com.google.common.collect.Lists; - import org.opensearch.index.reindex.ReindexPlugin; import org.opensearch.join.ParentJoinPlugin; import org.opensearch.percolator.PercolatorPlugin; @@ -45,6 +44,8 @@ import org.opensearch.security.OpenSearchSecurityPlugin; import org.opensearch.transport.Netty4Plugin; +import static java.util.Collections.unmodifiableList; + public enum ClusterManager { //first one needs to be a master //HUGE(new NodeSettings(true, false, false), new NodeSettings(true, false, false), new NodeSettings(true, false, false), new NodeSettings(false, true,false), new NodeSettings(false, true, false)), @@ -67,15 +68,15 @@ private ClusterManager(NodeSettings... settings) { } public List getNodeSettings() { - return Collections.unmodifiableList(nodeSettings); + return unmodifiableList(nodeSettings); } public List getMasterNodeSettings() { - return Collections.unmodifiableList(nodeSettings.stream().filter(a -> a.masterNode).collect(Collectors.toList())); + return unmodifiableList(nodeSettings.stream().filter(a -> a.masterNode).collect(Collectors.toList())); } public List getNonMasterNodeSettings() { - return Collections.unmodifiableList(nodeSettings.stream().filter(a -> !a.masterNode).collect(Collectors.toList())); + return unmodifiableList(nodeSettings.stream().filter(a -> !a.masterNode).collect(Collectors.toList())); } public int getNodes() { @@ -95,36 +96,39 @@ public int getClientNodes() { } public static class NodeSettings { - public boolean masterNode; - public boolean dataNode; - public List> plugins = Lists.newArrayList(Netty4Plugin.class, OpenSearchSecurityPlugin.class, MatrixAggregationPlugin.class, - ParentJoinPlugin.class, PercolatorPlugin.class, ReindexPlugin.class); + + private final static List> DEFAULT_PLUGINS = List.of(Netty4Plugin.class, OpenSearchSecurityPlugin.class, + MatrixAggregationPlugin.class, ParentJoinPlugin.class, PercolatorPlugin.class, ReindexPlugin.class); + public final boolean masterNode; + public final boolean dataNode; + public final List> plugins; public NodeSettings(boolean masterNode, boolean dataNode) { + this(masterNode, dataNode, Collections.emptyList()); + } + + public NodeSettings(boolean masterNode, boolean dataNode, List> additionalPlugins) { super(); this.masterNode = masterNode; this.dataNode = dataNode; + this.plugins = mergePlugins(additionalPlugins, DEFAULT_PLUGINS); } - public NodeSettings(boolean masterNode, boolean dataNode, List> additionalPlugins) { - this(masterNode, dataNode); - this.plugins.addAll(additionalPlugins); + private List> mergePlugins(Collection>...plugins) { + List> mergedPlugins = Arrays.stream(plugins) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .collect(Collectors.toList()); + return unmodifiableList(mergedPlugins); } @SuppressWarnings("unchecked") public Class[] getPlugins() { return plugins.toArray(new Class[0]); } - - @SuppressWarnings("unchecked") - public Class[] getPlugins(List> additionalPlugins) { - List> plugins = new ArrayList<>(this.plugins); - - if (additionalPlugins != null) { - plugins.addAll(additionalPlugins); - } - - return plugins.toArray(new Class[0]); + + public Class[] pluginsWithAddition(List> additionalPlugins) { + return mergePlugins(plugins, additionalPlugins).toArray(Class[]::new); } } } diff --git a/src/newTest/java/org/opensearch/test/framework/cluster/LocalOpenSearchCluster.java b/src/newTest/java/org/opensearch/test/framework/cluster/LocalOpenSearchCluster.java index 3c42a85835..9ab3a0533f 100644 --- a/src/newTest/java/org/opensearch/test/framework/cluster/LocalOpenSearchCluster.java +++ b/src/newTest/java/org/opensearch/test/framework/cluster/LocalOpenSearchCluster.java @@ -380,8 +380,8 @@ public class Node implements OpenSearchClientProvider { CompletableFuture start() { CompletableFuture completableFuture = new CompletableFuture<>(); - - this.node = new PluginAwareNode(nodeSettings.masterNode, getOpenSearchSettings(), nodeSettings.getPlugins(additionalPlugins)); + Class[] mergedPlugins = nodeSettings.pluginsWithAddition(additionalPlugins); + this.node = new PluginAwareNode(nodeSettings.masterNode, getOpenSearchSettings(), mergedPlugins); new Thread(new Runnable() { From f29359034f8d2495d04823bc33ac68ac51358f3c Mon Sep 17 00:00:00 2001 From: kt-eliatra Date: Mon, 8 Aug 2022 16:50:44 +0200 Subject: [PATCH 13/16] add readme - configuration of new tests --- NEW_TEST_FRAMEWORK.md | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 NEW_TEST_FRAMEWORK.md diff --git a/NEW_TEST_FRAMEWORK.md b/NEW_TEST_FRAMEWORK.md new file mode 100644 index 0000000000..c1c5fed525 --- /dev/null +++ b/NEW_TEST_FRAMEWORK.md @@ -0,0 +1,39 @@ +# New test framework + +Decision was made that new tests should be fully independent of existing [(old)](src/test) tests. It means that the new tests must: +* not use any of classes or resources located in [src/test/...](src/test), +* not use any of already declared test dependencies, +* be located in separate folder. + +Thanks to that we can keep the possibility to execute the old tests, incrementally add new tests, and replace the old tests with new ones in the future. + + +## Project structure + +Tests are located in two directories: +* the source code of the existing tests is located in the [src/test/java](src/test/java) directory, +* the [src/newTest/java](src/newTest/java) directory contains the source code of tests implemented using the new test framework. + +A new source set called `newTest` has been defined in [build.gradle](build.gradle), and configured: +* the production classes from the main source set are added to the compile time classpath, +* the production classes from the main source set are added to the runtime classpath, +* the source directory of new tests is set to `src/newTest/java`, +* the resource directory of new tests is set to `src/newTest/resources`. + +## Dependencies + +The Java Plugin automatically creates configurations for each of defined source sets. +Names of configurations are prefixed by the name of the source set, e.g. **newTest**RuntimeOnly, **newTest**CompileOnly. +`newTestImplementation` and `newTestRuntimeOnly` have been configured to extend from `implementation` and `runtimeOnly` respectively. +As a result all the declared dependencies of the production code also become dependencies of the new tests. + +## Running tests + +There is a new task called `newTest` that runs new tests. It has been configured to run after the `test` task, e.g. when +``` +./gradlew clean build +``` +command is executed. In order to run just the new test, use command: +``` +./gradle clean newTest +``` From 4e25b4fd63ec0f15fba324bc61ce87c269f6ffc7 Mon Sep 17 00:00:00 2001 From: kt-eliatra Date: Mon, 8 Aug 2022 17:25:21 +0200 Subject: [PATCH 14/16] add hamcrest dependency --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index ec5d906487..b419520861 100644 --- a/build.gradle +++ b/build.gradle @@ -393,6 +393,7 @@ dependencies { newTestImplementation "org.opensearch.plugin:percolator-client:${opensearch_version}" newTestImplementation 'commons-io:commons-io:2.7' newTestImplementation 'org.apache.logging.log4j:log4j-core:2.17.1' + newTestImplementation 'org.hamcrest:hamcrest:2.1' } jar { From 9d69ade495f87958240831fef54eabf354633af3 Mon Sep 17 00:00:00 2001 From: kt-eliatra Date: Mon, 8 Aug 2022 17:27:16 +0200 Subject: [PATCH 15/16] remove unused import --- .../org/opensearch/test/framework/cluster/TestRestClient.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/newTest/java/org/opensearch/test/framework/cluster/TestRestClient.java b/src/newTest/java/org/opensearch/test/framework/cluster/TestRestClient.java index 69e5746a5c..7871747a08 100644 --- a/src/newTest/java/org/opensearch/test/framework/cluster/TestRestClient.java +++ b/src/newTest/java/org/opensearch/test/framework/cluster/TestRestClient.java @@ -47,7 +47,6 @@ import org.apache.commons.io.IOUtils; import org.apache.http.Header; import org.apache.http.HttpEntity; -import org.apache.http.client.ClientProtocolException; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpDelete; From 165fbf00c91f4b77cf6f99a14ce0aaea2e5da3e5 Mon Sep 17 00:00:00 2001 From: kt-eliatra Date: Mon, 8 Aug 2022 17:44:04 +0200 Subject: [PATCH 16/16] disable forbiddenApisNewTest --- build.gradle | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/build.gradle b/build.gradle index b419520861..3d509d4519 100644 --- a/build.gradle +++ b/build.gradle @@ -84,6 +84,12 @@ forbiddenPatterns.enabled = false testingConventions.enabled = false // Conflicts between runtime kafka-clients:3.0.1 & testRuntime kafka-clients:3.0.1:test jarHell.enabled = false +tasks.whenTaskAdded {task -> + if(task.name.contains("forbiddenApisNewTest")) { + task.enabled = false + } +} + test { include '**/*.class'