diff --git a/.github/workflows/tests@v1.yml b/.github/workflows/tests@v1.yml index 80e12c610a7..de9f873f582 100644 --- a/.github/workflows/tests@v1.yml +++ b/.github/workflows/tests@v1.yml @@ -94,6 +94,11 @@ jobs: steps: - name: Checkout source uses: actions/checkout@v2 + + - name: Setup Python 2 (setup it) + uses: actions/setup-python@v2 + with: + python-version: '2.x' - name: Setup Python 3 uses: actions/setup-python@v2 @@ -131,6 +136,11 @@ jobs: java-version: '8' distribution: 'adopt' + - name: Setup Python 2 (cas it) + uses: actions/setup-python@v2 + with: + python-version: '2.x' + - name: Setup Python 3 uses: actions/setup-python@v2 with: @@ -162,6 +172,13 @@ jobs: name: ccm-logs-cassandra-${{ matrix.cassandra-version }} path: /tmp/ccm*/ccm*/node*/logs/* + - name: Check python versions + run: | + python --version + echo "aaa" + python2 --version + python3 --version + scylla-integration-tests: name: Scylla ITs runs-on: ubuntu-latest @@ -183,6 +200,11 @@ jobs: java-version: '8' distribution: 'adopt' + - name: Setup Python 2 (scylla test) + uses: actions/setup-python@v2 + with: + python-version: '2.x' + - name: Setup Python 3 uses: actions/setup-python@v2 with: @@ -213,4 +235,11 @@ jobs: if: ${{ failure() }} with: name: ccm-logs-scylla-${{ matrix.scylla-version }} - path: /tmp/ccm*/ccm*/node*/logs/* \ No newline at end of file + path: /tmp/ccm*/ccm*/node*/logs/* + + - name: Check python versions + run: | + python --version + echo "aaa" + python2 --version + python3 --version \ No newline at end of file diff --git a/core/pom.xml b/core/pom.xml index c47e2bfcc95..853dc5c572d 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -115,6 +115,10 @@ com.fasterxml.jackson.core jackson-databind + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + org.reactivestreams reactive-streams @@ -192,6 +196,14 @@ wiremock test + + org.bouncycastle + bcprov-jdk18on + + + org.bouncycastle + bcpkix-jdk18on + diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/session/ProgrammaticArguments.java b/core/src/main/java/com/datastax/oss/driver/api/core/session/ProgrammaticArguments.java index b8b2bd8b723..168c4f8edff 100644 --- a/core/src/main/java/com/datastax/oss/driver/api/core/session/ProgrammaticArguments.java +++ b/core/src/main/java/com/datastax/oss/driver/api/core/session/ProgrammaticArguments.java @@ -64,6 +64,7 @@ public static Builder builder() { private final AuthProvider authProvider; private final SslEngineFactory sslEngineFactory; private final InetSocketAddress cloudProxyAddress; + private final String scyllaCloudNodeDomain; private final UUID startupClientId; private final String startupApplicationName; private final String startupApplicationVersion; @@ -82,6 +83,7 @@ private ProgrammaticArguments( @Nullable AuthProvider authProvider, @Nullable SslEngineFactory sslEngineFactory, @Nullable InetSocketAddress cloudProxyAddress, + @Nullable String scyllaCloudNodeDomain, @Nullable UUID startupClientId, @Nullable String startupApplicationName, @Nullable String startupApplicationVersion, @@ -99,6 +101,7 @@ private ProgrammaticArguments( this.authProvider = authProvider; this.sslEngineFactory = sslEngineFactory; this.cloudProxyAddress = cloudProxyAddress; + this.scyllaCloudNodeDomain = scyllaCloudNodeDomain; this.startupClientId = startupClientId; this.startupApplicationName = startupApplicationName; this.startupApplicationVersion = startupApplicationVersion; @@ -163,6 +166,11 @@ public InetSocketAddress getCloudProxyAddress() { return cloudProxyAddress; } + @Nullable + public String getScyllaCloudNodeDomain() { + return scyllaCloudNodeDomain; + } + @Nullable public UUID getStartupClientId() { return startupClientId; @@ -203,6 +211,7 @@ public static class Builder { private AuthProvider authProvider; private SslEngineFactory sslEngineFactory; private InetSocketAddress cloudProxyAddress; + private String scyllaCloudNodeDomain; private UUID startupClientId; private String startupApplicationName; private String startupApplicationVersion; @@ -366,6 +375,14 @@ public Builder withCloudProxyAddress(@Nullable InetSocketAddress cloudAddress) { return this; } + @NonNull + public Builder withScyllaCloudProxyAddress( + @Nullable InetSocketAddress cloudAddress, String scyllaCloudNodeDomain) { + this.cloudProxyAddress = cloudAddress; + this.scyllaCloudNodeDomain = scyllaCloudNodeDomain; + return this; + } + @NonNull public Builder withAuthProvider(@Nullable AuthProvider authProvider) { this.authProvider = authProvider; @@ -422,6 +439,7 @@ public ProgrammaticArguments build() { authProvider, sslEngineFactory, cloudProxyAddress, + scyllaCloudNodeDomain, startupClientId, startupApplicationName, startupApplicationVersion, diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/session/SessionBuilder.java b/core/src/main/java/com/datastax/oss/driver/api/core/session/SessionBuilder.java index fba54c29905..4a73de40c3f 100644 --- a/core/src/main/java/com/datastax/oss/driver/api/core/session/SessionBuilder.java +++ b/core/src/main/java/com/datastax/oss/driver/api/core/session/SessionBuilder.java @@ -44,15 +44,17 @@ import com.datastax.oss.driver.api.core.type.codec.registry.MutableCodecRegistry; import com.datastax.oss.driver.api.core.uuid.Uuids; import com.datastax.oss.driver.internal.core.ContactPoints; -import com.datastax.oss.driver.internal.core.config.cloud.CloudConfig; -import com.datastax.oss.driver.internal.core.config.cloud.CloudConfigFactory; +import com.datastax.oss.driver.internal.core.config.scyllacloud.ConfigurationBundle; +import com.datastax.oss.driver.internal.core.config.scyllacloud.ScyllaCloudConnectionConfig; import com.datastax.oss.driver.internal.core.config.typesafe.DefaultDriverConfigLoader; import com.datastax.oss.driver.internal.core.context.DefaultDriverContext; import com.datastax.oss.driver.internal.core.context.InternalDriverContext; import com.datastax.oss.driver.internal.core.metadata.DefaultEndPoint; +import com.datastax.oss.driver.internal.core.metadata.SniEndPoint; import com.datastax.oss.driver.internal.core.session.DefaultSession; import com.datastax.oss.driver.internal.core.util.concurrent.BlockingOperation; import com.datastax.oss.driver.internal.core.util.concurrent.CompletableFutures; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import java.io.InputStream; @@ -60,7 +62,6 @@ import java.net.MalformedURLException; import java.net.URL; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.Collection; import java.util.Collections; import java.util.HashSet; @@ -95,7 +96,7 @@ public abstract class SessionBuilder { protected DriverConfigLoader configLoader; protected Set programmaticContactPoints = new HashSet<>(); protected CqlIdentifier keyspace; - protected Callable cloudConfigInputStream; + protected Callable scyllaCloudConfigInputStream; protected ProgrammaticArguments.Builder programmaticArgumentsBuilder = ProgrammaticArguments.builder(); @@ -630,25 +631,11 @@ public SelfT withClassLoader(@Nullable ClassLoader classLoader) { return self; } - /** - * Configures this SessionBuilder for Cloud deployments by retrieving connection information from - * the provided {@link Path}. - * - *

To connect to a Cloud database, you must first download the secure database bundle from the - * DataStax Astra console that contains the connection information, then instruct the driver to - * read its contents using either this method or one if its variants. - * - *

For more information, please refer to the DataStax Astra documentation. - * - * @param cloudConfigPath Path to the secure connect bundle zip file. - * @see #withCloudSecureConnectBundle(URL) - * @see #withCloudSecureConnectBundle(InputStream) - */ @NonNull - public SelfT withCloudSecureConnectBundle(@NonNull Path cloudConfigPath) { + public SelfT withScyllaCloudSecureConnectBundle(@NonNull Path cloudConfigPath) { try { URL cloudConfigUrl = cloudConfigPath.toAbsolutePath().normalize().toUri().toURL(); - this.cloudConfigInputStream = cloudConfigUrl::openStream; + this.scyllaCloudConfigInputStream = cloudConfigUrl::openStream; } catch (MalformedURLException e) { throw new IllegalArgumentException("Incorrect format of cloudConfigPath", e); } @@ -667,69 +654,22 @@ public SelfT withCodecRegistry(@Nullable MutableCodecRegistry codecRegistry) { return self; } - /** - * Configures this SessionBuilder for Cloud deployments by retrieving connection information from - * the provided {@link URL}. - * - *

To connect to a Cloud database, you must first download the secure database bundle from the - * DataStax Astra console that contains the connection information, then instruct the driver to - * read its contents using either this method or one if its variants. - * - *

For more information, please refer to the DataStax Astra documentation. - * - * @param cloudConfigUrl URL to the secure connect bundle zip file. - * @see #withCloudSecureConnectBundle(Path) - * @see #withCloudSecureConnectBundle(InputStream) - */ @NonNull - public SelfT withCloudSecureConnectBundle(@NonNull URL cloudConfigUrl) { - this.cloudConfigInputStream = cloudConfigUrl::openStream; + public SelfT withScyllaCloudSecureConnectBundle(@NonNull URL cloudConfigUrl) { + this.scyllaCloudConfigInputStream = cloudConfigUrl::openStream; return self; } - /** - * Configures this SessionBuilder for Cloud deployments by retrieving connection information from - * the provided {@link InputStream}. - * - *

To connect to a Cloud database, you must first download the secure database bundle from the - * DataStax Astra console that contains the connection information, then instruct the driver to - * read its contents using either this method or one if its variants. - * - *

For more information, please refer to the DataStax Astra documentation. - * - *

Note that the provided stream will be consumed and closed when either {@link - * #build()} or {@link #buildAsync()} are called; attempting to reuse it afterwards will result in - * an error being thrown. - * - * @param cloudConfigInputStream A stream containing the secure connect bundle zip file. - * @see #withCloudSecureConnectBundle(Path) - * @see #withCloudSecureConnectBundle(URL) - */ @NonNull - public SelfT withCloudSecureConnectBundle(@NonNull InputStream cloudConfigInputStream) { - this.cloudConfigInputStream = () -> cloudConfigInputStream; + public SelfT withScyllaCloudSecureConnectBundle(@NonNull InputStream cloudConfigInputStream) { + this.scyllaCloudConfigInputStream = () -> cloudConfigInputStream; return self; } - /** - * Configures this SessionBuilder to use the provided Cloud proxy endpoint. - * - *

Normally, this method should not be called directly; the normal and easiest way to configure - * the driver for Cloud deployments is through a {@linkplain #withCloudSecureConnectBundle(URL) - * secure connect bundle}. - * - *

Setting this option to any non-null address will make the driver use a special topology - * monitor tailored for Cloud deployments. This topology monitor assumes that the target cluster - * should be contacted through the proxy specified here, using SNI routing. - * - *

For more information, please refer to the DataStax Astra documentation. - * - * @param cloudProxyAddress The address of the Cloud proxy to use. - * @see Server Name Indication - */ @NonNull - public SelfT withCloudProxyAddress(@Nullable InetSocketAddress cloudProxyAddress) { - this.programmaticArgumentsBuilder.withCloudProxyAddress(cloudProxyAddress); + public SelfT withScyllaCloudProxyAddress( + @Nullable InetSocketAddress cloudProxyAddress, String nodeDomain) { + this.programmaticArgumentsBuilder.withScyllaCloudProxyAddress(cloudProxyAddress, nodeDomain); return self; } @@ -857,16 +797,9 @@ protected final CompletionStage buildDefaultSessionAsync() { : defaultConfigLoader(programmaticArguments.getClassLoader()); DriverExecutionProfile defaultConfig = configLoader.getInitialConfig().getDefaultProfile(); - if (cloudConfigInputStream == null) { - String configUrlString = - defaultConfig.getString(DefaultDriverOption.CLOUD_SECURE_CONNECT_BUNDLE, null); - if (configUrlString != null) { - cloudConfigInputStream = () -> getURL(configUrlString).openStream(); - } - } List configContactPoints = defaultConfig.getStringList(DefaultDriverOption.CONTACT_POINTS, Collections.emptyList()); - if (cloudConfigInputStream != null) { + if (scyllaCloudConfigInputStream != null) { if (!programmaticContactPoints.isEmpty() || !configContactPoints.isEmpty()) { LOG.info( "Both a secure connect bundle and contact points were provided. These are mutually exclusive. The contact points from the secure bundle will have priority."); @@ -880,20 +813,27 @@ protected final CompletionStage buildDefaultSessionAsync() { LOG.info( "Both a secure connect bundle and SSL options were provided. They are mutually exclusive. The SSL options from the secure bundle will have priority."); } - CloudConfig cloudConfig = - new CloudConfigFactory().createCloudConfig(cloudConfigInputStream.call()); - addContactEndPoints(cloudConfig.getEndPoints()); + ScyllaCloudConnectionConfig cloudConfig = + ScyllaCloudConnectionConfig.fromInputStream(scyllaCloudConfigInputStream.call()); + InetSocketAddress proxyAddress = cloudConfig.getCurrentDatacenter().getServer(); + addContactEndPoints( + ImmutableList.of( + new SniEndPoint(proxyAddress, cloudConfig.getCurrentDatacenter().getNodeDomain()))); boolean localDataCenterDefined = anyProfileHasDatacenterDefined(configLoader.getInitialConfig()); if (programmaticLocalDatacenter || localDataCenterDefined) { LOG.info( - "Both a secure connect bundle and a local datacenter were provided. They are mutually exclusive. The local datacenter from the secure bundle will have priority."); + "Both a secure connect bundle and a local datacenter were provided. They are mutually exclusive. The currentContext datacenter name from the secure bundle will be ignored."); + } else { programmaticArgumentsBuilder.clearDatacenters(); + withLocalDatacenter(cloudConfig.getCurrentContext().getDatacenterName()); } - withLocalDatacenter(cloudConfig.getLocalDatacenter()); - withSslEngineFactory(cloudConfig.getSslEngineFactory()); - withCloudProxyAddress(cloudConfig.getProxyAddress()); + ConfigurationBundle bundle = cloudConfig.createBundle(); + withSslEngineFactory(bundle.getSSLEngineFactory()); + withScyllaCloudProxyAddress( + proxyAddress, cloudConfig.getCurrentDatacenter().getNodeDomain()); + programmaticArguments = programmaticArgumentsBuilder.build(); } @@ -912,7 +852,6 @@ protected final CompletionStage buildDefaultSessionAsync() { (InternalDriverContext) buildContext(configLoader, programmaticArguments), contactPoints, keyspace); - } catch (Throwable t) { // We construct the session synchronously (until the init() call), but async clients expect a // failed future if anything goes wrong. So wrap any error from that synchronous part. @@ -929,27 +868,6 @@ private boolean anyProfileHasDatacenterDefined(DriverConfig driverConfig) { return false; } - /** - * Returns URL based on the configUrl setting. If the configUrl has no protocol provided, the - * method will fallback to file:// protocol and return URL that has file protocol specified. - * - * @param configUrl url to config secure bundle - * @return URL with file protocol if there was not explicit protocol provided in the configUrl - * setting - */ - private URL getURL(String configUrl) throws MalformedURLException { - try { - return new URL(configUrl); - } catch (MalformedURLException e1) { - try { - return Paths.get(configUrl).toAbsolutePath().normalize().toUri().toURL(); - } catch (MalformedURLException e2) { - e2.addSuppressed(e1); - throw e2; - } - } - } - /** * This must return an instance of {@code InternalDriverContext} (it's not expressed * directly in the signature to avoid leaking that type through the protected API). diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/channel/DriverChannel.java b/core/src/main/java/com/datastax/oss/driver/internal/core/channel/DriverChannel.java index e1b1005cc14..7c69c987e0d 100644 --- a/core/src/main/java/com/datastax/oss/driver/internal/core/channel/DriverChannel.java +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/channel/DriverChannel.java @@ -69,7 +69,7 @@ public class DriverChannel { @SuppressWarnings("RedundantStringConstructorCall") static final Object FORCEFUL_CLOSE_MESSAGE = new String("FORCEFUL_CLOSE_MESSAGE"); - private final EndPoint endPoint; + private EndPoint endPoint; private final Channel channel; private final InFlightHandler inFlightHandler; private final WriteCoalescer writeCoalescer; @@ -326,4 +326,9 @@ public SetKeyspaceEvent(CqlIdentifier keyspaceName, Promise promise) { this.promise = promise; } } + + // Necessary for swapping ControlConnection endpoint when connecting with serverless clusters + public void setEndPoint(EndPoint endPoint) { + this.endPoint = endPoint; + } } diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/config/cloud/CloudConfig.java b/core/src/main/java/com/datastax/oss/driver/internal/core/config/cloud/CloudConfig.java deleted file mode 100644 index e2207e3db95..00000000000 --- a/core/src/main/java/com/datastax/oss/driver/internal/core/config/cloud/CloudConfig.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * 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 com.datastax.oss.driver.internal.core.config.cloud; - -import com.datastax.oss.driver.api.core.metadata.EndPoint; -import com.datastax.oss.driver.api.core.ssl.SslEngineFactory; -import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; -import edu.umd.cs.findbugs.annotations.NonNull; -import java.net.InetSocketAddress; -import java.util.List; -import net.jcip.annotations.ThreadSafe; - -@ThreadSafe -public class CloudConfig { - - private final InetSocketAddress proxyAddress; - private final List endPoints; - private final String localDatacenter; - private final SslEngineFactory sslEngineFactory; - - CloudConfig( - @NonNull InetSocketAddress proxyAddress, - @NonNull List endPoints, - @NonNull String localDatacenter, - @NonNull SslEngineFactory sslEngineFactory) { - this.proxyAddress = proxyAddress; - this.endPoints = ImmutableList.copyOf(endPoints); - this.localDatacenter = localDatacenter; - this.sslEngineFactory = sslEngineFactory; - } - - @NonNull - public InetSocketAddress getProxyAddress() { - return proxyAddress; - } - - @NonNull - public List getEndPoints() { - return endPoints; - } - - @NonNull - public String getLocalDatacenter() { - return localDatacenter; - } - - @NonNull - public SslEngineFactory getSslEngineFactory() { - return sslEngineFactory; - } -} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/config/cloud/CloudConfigFactory.java b/core/src/main/java/com/datastax/oss/driver/internal/core/config/cloud/CloudConfigFactory.java deleted file mode 100644 index f7386dcc390..00000000000 --- a/core/src/main/java/com/datastax/oss/driver/internal/core/config/cloud/CloudConfigFactory.java +++ /dev/null @@ -1,295 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * 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 com.datastax.oss.driver.internal.core.config.cloud; - -import com.datastax.oss.driver.api.core.metadata.EndPoint; -import com.datastax.oss.driver.internal.core.metadata.SniEndPoint; -import com.datastax.oss.driver.internal.core.ssl.SniSslEngineFactory; -import com.datastax.oss.driver.shaded.guava.common.io.ByteStreams; -import com.datastax.oss.driver.shaded.guava.common.net.HostAndPort; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import edu.umd.cs.findbugs.annotations.NonNull; -import java.io.BufferedReader; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.net.ConnectException; -import java.net.InetSocketAddress; -import java.net.MalformedURLException; -import java.net.URL; -import java.net.UnknownHostException; -import java.nio.charset.StandardCharsets; -import java.security.GeneralSecurityException; -import java.security.KeyStore; -import java.security.SecureRandom; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Objects; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; -import javax.net.ssl.HttpsURLConnection; -import javax.net.ssl.KeyManagerFactory; -import javax.net.ssl.SSLContext; -import javax.net.ssl.TrustManagerFactory; -import net.jcip.annotations.ThreadSafe; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -@ThreadSafe -public class CloudConfigFactory { - private static final Logger LOG = LoggerFactory.getLogger(CloudConfigFactory.class); - /** - * Creates a {@link CloudConfig} with information fetched from the specified Cloud configuration - * URL. - * - *

The target URL must point to a valid secure connect bundle archive in ZIP format. - * - * @param cloudConfigUrl the URL to fetch the Cloud configuration from; cannot be null. - * @throws IOException If the Cloud configuration cannot be read. - * @throws GeneralSecurityException If the Cloud SSL context cannot be created. - */ - @NonNull - public CloudConfig createCloudConfig(@NonNull URL cloudConfigUrl) - throws IOException, GeneralSecurityException { - Objects.requireNonNull(cloudConfigUrl, "cloudConfigUrl cannot be null"); - return createCloudConfig(cloudConfigUrl.openStream()); - } - - /** - * Creates a {@link CloudConfig} with information fetched from the specified {@link InputStream}. - * - *

The stream must contain a valid secure connect bundle archive in ZIP format. Note that the - * stream will be closed after a call to that method and cannot be used anymore. - * - * @param cloudConfig the stream to read the Cloud configuration from; cannot be null. - * @throws IOException If the Cloud configuration cannot be read. - * @throws GeneralSecurityException If the Cloud SSL context cannot be created. - */ - @NonNull - public CloudConfig createCloudConfig(@NonNull InputStream cloudConfig) - throws IOException, GeneralSecurityException { - Objects.requireNonNull(cloudConfig, "cloudConfig cannot be null"); - JsonNode configJson = null; - ByteArrayOutputStream keyStoreOutputStream = null; - ByteArrayOutputStream trustStoreOutputStream = null; - ObjectMapper mapper = new ObjectMapper().configure(JsonParser.Feature.AUTO_CLOSE_SOURCE, false); - try (ZipInputStream zipInputStream = new ZipInputStream(cloudConfig)) { - ZipEntry entry; - while ((entry = zipInputStream.getNextEntry()) != null) { - String fileName = entry.getName(); - switch (fileName) { - case "config.json": - configJson = mapper.readTree(zipInputStream); - break; - case "identity.jks": - keyStoreOutputStream = new ByteArrayOutputStream(); - ByteStreams.copy(zipInputStream, keyStoreOutputStream); - break; - case "trustStore.jks": - trustStoreOutputStream = new ByteArrayOutputStream(); - ByteStreams.copy(zipInputStream, trustStoreOutputStream); - break; - } - } - } - if (configJson == null) { - throw new IllegalStateException("Invalid bundle: missing file config.json"); - } - if (keyStoreOutputStream == null) { - throw new IllegalStateException("Invalid bundle: missing file identity.jks"); - } - if (trustStoreOutputStream == null) { - throw new IllegalStateException("Invalid bundle: missing file trustStore.jks"); - } - char[] keyStorePassword = getKeyStorePassword(configJson); - char[] trustStorePassword = getTrustStorePassword(configJson); - ByteArrayInputStream keyStoreInputStream = - new ByteArrayInputStream(keyStoreOutputStream.toByteArray()); - ByteArrayInputStream trustStoreInputStream = - new ByteArrayInputStream(trustStoreOutputStream.toByteArray()); - SSLContext sslContext = - createSslContext( - keyStoreInputStream, keyStorePassword, trustStoreInputStream, trustStorePassword); - URL metadataServiceUrl = getMetadataServiceUrl(configJson); - JsonNode proxyMetadataJson; - try (BufferedReader proxyMetadata = fetchProxyMetadata(metadataServiceUrl, sslContext)) { - proxyMetadataJson = mapper.readTree(proxyMetadata); - } - InetSocketAddress sniProxyAddress = getSniProxyAddress(proxyMetadataJson); - List endPoints = getEndPoints(proxyMetadataJson, sniProxyAddress); - String localDatacenter = getLocalDatacenter(proxyMetadataJson); - SniSslEngineFactory sslEngineFactory = new SniSslEngineFactory(sslContext); - validateIfBundleContainsUsernamePassword(configJson); - return new CloudConfig(sniProxyAddress, endPoints, localDatacenter, sslEngineFactory); - } - - @NonNull - protected char[] getKeyStorePassword(JsonNode configFile) { - if (configFile.has("keyStorePassword")) { - return configFile.get("keyStorePassword").asText().toCharArray(); - } else { - throw new IllegalStateException("Invalid config.json: missing field keyStorePassword"); - } - } - - @NonNull - protected char[] getTrustStorePassword(JsonNode configFile) { - if (configFile.has("trustStorePassword")) { - return configFile.get("trustStorePassword").asText().toCharArray(); - } else { - throw new IllegalStateException("Invalid config.json: missing field trustStorePassword"); - } - } - - @NonNull - protected URL getMetadataServiceUrl(JsonNode configFile) throws MalformedURLException { - if (configFile.has("host")) { - String metadataServiceHost = configFile.get("host").asText(); - if (configFile.has("port")) { - int metadataServicePort = configFile.get("port").asInt(); - return new URL("https", metadataServiceHost, metadataServicePort, "/metadata"); - } else { - throw new IllegalStateException("Invalid config.json: missing field port"); - } - } else { - throw new IllegalStateException("Invalid config.json: missing field host"); - } - } - - protected void validateIfBundleContainsUsernamePassword(JsonNode configFile) { - if (configFile.has("username") || configFile.has("password")) { - LOG.info( - "The bundle contains config.json with username and/or password. Providing it in the bundle is deprecated and ignored."); - } - } - - @NonNull - protected SSLContext createSslContext( - @NonNull ByteArrayInputStream keyStoreInputStream, - @NonNull char[] keyStorePassword, - @NonNull ByteArrayInputStream trustStoreInputStream, - @NonNull char[] trustStorePassword) - throws IOException, GeneralSecurityException { - KeyManagerFactory kmf = createKeyManagerFactory(keyStoreInputStream, keyStorePassword); - TrustManagerFactory tmf = createTrustManagerFactory(trustStoreInputStream, trustStorePassword); - SSLContext sslContext = SSLContext.getInstance("SSL"); - sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), new SecureRandom()); - return sslContext; - } - - @NonNull - protected KeyManagerFactory createKeyManagerFactory( - @NonNull InputStream keyStoreInputStream, @NonNull char[] keyStorePassword) - throws IOException, GeneralSecurityException { - KeyStore ks = KeyStore.getInstance("JKS"); - ks.load(keyStoreInputStream, keyStorePassword); - KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); - kmf.init(ks, keyStorePassword); - Arrays.fill(keyStorePassword, (char) 0); - return kmf; - } - - @NonNull - protected TrustManagerFactory createTrustManagerFactory( - @NonNull InputStream trustStoreInputStream, @NonNull char[] trustStorePassword) - throws IOException, GeneralSecurityException { - KeyStore ts = KeyStore.getInstance("JKS"); - ts.load(trustStoreInputStream, trustStorePassword); - TrustManagerFactory tmf = - TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - tmf.init(ts); - Arrays.fill(trustStorePassword, (char) 0); - return tmf; - } - - @NonNull - protected BufferedReader fetchProxyMetadata( - @NonNull URL metadataServiceUrl, @NonNull SSLContext sslContext) throws IOException { - try { - HttpsURLConnection connection = (HttpsURLConnection) metadataServiceUrl.openConnection(); - connection.setSSLSocketFactory(sslContext.getSocketFactory()); - connection.setRequestMethod("GET"); - connection.setRequestProperty("host", "localhost"); - return new BufferedReader( - new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8)); - } catch (ConnectException e) { - throw new IllegalStateException( - "Unable to connect to cloud metadata service. Please make sure your cluster is not parked or terminated", - e); - } catch (UnknownHostException e) { - throw new IllegalStateException( - "Unable to resolve host for cloud metadata service. Please make sure your cluster is not terminated", - e); - } - } - - @NonNull - protected String getLocalDatacenter(@NonNull JsonNode proxyMetadata) { - JsonNode contactInfo = getContactInfo(proxyMetadata); - if (contactInfo.has("local_dc")) { - return contactInfo.get("local_dc").asText(); - } else { - throw new IllegalStateException("Invalid proxy metadata: missing field local_dc"); - } - } - - @NonNull - protected InetSocketAddress getSniProxyAddress(@NonNull JsonNode proxyMetadata) { - JsonNode contactInfo = getContactInfo(proxyMetadata); - if (contactInfo.has("sni_proxy_address")) { - HostAndPort sniProxyHostAndPort = - HostAndPort.fromString(contactInfo.get("sni_proxy_address").asText()); - if (!sniProxyHostAndPort.hasPort()) { - throw new IllegalStateException( - "Invalid proxy metadata: missing port from field sni_proxy_address"); - } - return InetSocketAddress.createUnresolved( - sniProxyHostAndPort.getHost(), sniProxyHostAndPort.getPort()); - } else { - throw new IllegalStateException("Invalid proxy metadata: missing field sni_proxy_address"); - } - } - - @NonNull - protected List getEndPoints( - @NonNull JsonNode proxyMetadata, @NonNull InetSocketAddress sniProxyAddress) { - JsonNode contactInfo = getContactInfo(proxyMetadata); - if (contactInfo.has("contact_points")) { - List endPoints = new ArrayList<>(); - JsonNode hostIdsJson = contactInfo.get("contact_points"); - for (int i = 0; i < hostIdsJson.size(); i++) { - endPoints.add(new SniEndPoint(sniProxyAddress, hostIdsJson.get(i).asText())); - } - return endPoints; - } else { - throw new IllegalStateException("Invalid proxy metadata: missing field contact_points"); - } - } - - @NonNull - protected JsonNode getContactInfo(@NonNull JsonNode proxyMetadata) { - if (proxyMetadata.has("contact_info")) { - return proxyMetadata.get("contact_info"); - } else { - throw new IllegalStateException("Invalid proxy metadata: missing field contact_info"); - } - } -} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/config/scyllacloud/ConfigurationBundle.java b/core/src/main/java/com/datastax/oss/driver/internal/core/config/scyllacloud/ConfigurationBundle.java new file mode 100644 index 00000000000..485d0a5a2ea --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/config/scyllacloud/ConfigurationBundle.java @@ -0,0 +1,108 @@ +package com.datastax.oss.driver.internal.core.config.scyllacloud; + +import com.datastax.oss.driver.api.core.ssl.SslEngineFactory; +import com.datastax.oss.driver.internal.core.ssl.SniSslEngineFactory; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; + +public class ConfigurationBundle { + private final KeyStore identity; + private final KeyStore trustStore; + + public ConfigurationBundle(KeyStore identity, KeyStore trustStore) { + this.identity = identity; + this.trustStore = trustStore; + } + + public KeyStore getIdentity() { + return identity; + } + + public KeyStore getTrustStore() { + return trustStore; + } + + private void writeKeystore(String path, KeyStore ks, char[] password) + throws IOException, CertificateException, KeyStoreException, NoSuchAlgorithmException { + File file = new File(path); + OutputStream os = new FileOutputStream(file); + ks.store(os, password); + os.close(); + } + + public void writeIdentity(String path, char[] password) + throws CertificateException, IOException, KeyStoreException, NoSuchAlgorithmException { + writeKeystore(path, identity, password); + } + + public void writeTrustStore(String path, char[] password) + throws CertificateException, IOException, KeyStoreException, NoSuchAlgorithmException { + writeKeystore(path, trustStore, password); + } + + protected SSLContext getSSLContext() throws IOException, GeneralSecurityException { + KeyManagerFactory kmf = createKeyManagerFactory(identity); + TrustManagerFactory tmf = createTrustManagerFactory(trustStore); + SSLContext sslContext = SSLContext.getInstance("SSL"); + sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), new SecureRandom()); + return sslContext; + } + + protected SSLContext getInsecureSSLContext() throws IOException, GeneralSecurityException { + KeyManagerFactory kmf = createKeyManagerFactory(identity); + SSLContext sslContext = SSLContext.getInstance("SSL"); + TrustManager[] trustManager = + new TrustManager[] { + new X509TrustManager() { + @Override + public void checkClientTrusted(X509Certificate[] x509Certificates, String s) + throws CertificateException {} + + @Override + public void checkServerTrusted(X509Certificate[] x509Certificates, String s) + throws CertificateException {} + + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + } + }; + + sslContext.init(kmf.getKeyManagers(), trustManager, new SecureRandom()); + return sslContext; + } + + protected KeyManagerFactory createKeyManagerFactory(KeyStore ks) + throws IOException, GeneralSecurityException { + KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmf.init(ks, "cassandra".toCharArray()); + return kmf; + } + + protected TrustManagerFactory createTrustManagerFactory(KeyStore ts) + throws IOException, GeneralSecurityException { + TrustManagerFactory tmf = + TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(ts); + return tmf; + } + + public SslEngineFactory getSSLEngineFactory() throws GeneralSecurityException, IOException { + return new SniSslEngineFactory(getSSLContext()); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/config/scyllacloud/Parameters.java b/core/src/main/java/com/datastax/oss/driver/internal/core/config/scyllacloud/Parameters.java new file mode 100644 index 00000000000..72706c22e87 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/config/scyllacloud/Parameters.java @@ -0,0 +1,21 @@ +package com.datastax.oss.driver.internal.core.config.scyllacloud; + +import com.datastax.oss.driver.api.core.ConsistencyLevel; +import com.datastax.oss.driver.api.core.DefaultConsistencyLevel; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +@SuppressWarnings("unused") +public class Parameters { + private final ConsistencyLevel defaultConsistency; + private final ConsistencyLevel defaultSerialConsistency; + + @JsonCreator + public Parameters( + @JsonProperty(value = "defaultConsistency") DefaultConsistencyLevel defaultConsistency, + @JsonProperty(value = "defaultSerialConsistency") + DefaultConsistencyLevel defaultSerialConsistency) { + this.defaultConsistency = defaultConsistency; + this.defaultSerialConsistency = defaultSerialConsistency; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/config/scyllacloud/ScyllaCloudAuthInfo.java b/core/src/main/java/com/datastax/oss/driver/internal/core/config/scyllacloud/ScyllaCloudAuthInfo.java new file mode 100644 index 00000000000..94b266b0c73 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/config/scyllacloud/ScyllaCloudAuthInfo.java @@ -0,0 +1,80 @@ +package com.datastax.oss.driver.internal.core.config.scyllacloud; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.io.File; + +public class ScyllaCloudAuthInfo { + private final byte[] clientCertificateData; + private final String clientCertificatePath; + private final byte[] clientKeyData; + private final String clientKeyPath; + private final String username; + private final String password; + + @JsonCreator + public ScyllaCloudAuthInfo( + @JsonProperty(value = "clientCertificateData") byte[] clientCertificateData, + @JsonProperty(value = "clientCertificatePath") String clientCertificatePath, + @JsonProperty(value = "clientKeyData") byte[] clientKeyData, + @JsonProperty(value = "clientKeyPath") String clientKeyPath, + @JsonProperty(value = "username") String username, + @JsonProperty(value = "password") String password) { + this.clientCertificateData = clientCertificateData; + this.clientCertificatePath = clientCertificatePath; + this.clientKeyData = clientKeyData; + this.clientKeyPath = clientKeyPath; + this.username = username; + this.password = password; + } + + public void validate() { + if (clientCertificateData == null) { + if (clientCertificatePath == null) { + throw new IllegalArgumentException( + "Either clientCertificateData or clientCertificatePath has to be provided for authInfo."); + } + File file = new File(clientCertificatePath); + if (!file.canRead()) { + throw new IllegalArgumentException( + "Cannot read file at given clientCertificatePath (" + clientCertificatePath + ")."); + } + } + + if (clientKeyData == null) { + if (clientKeyPath == null) { + throw new IllegalArgumentException( + "Either clientKeyData or clientKeyPath has to be provided for authInfo."); + } + File file = new File(clientKeyPath); + if (!file.canRead()) { + throw new IllegalArgumentException( + "Cannot read file at given clientKeyPath (" + clientKeyPath + ")."); + } + } + } + + public byte[] getClientCertificateData() { + return clientCertificateData; + } + + public String getClientCertificatePath() { + return clientCertificatePath; + } + + public byte[] getClientKeyData() { + return clientKeyData; + } + + public String getClientKeyPath() { + return clientKeyPath; + } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/config/scyllacloud/ScyllaCloudConnectionConfig.java b/core/src/main/java/com/datastax/oss/driver/internal/core/config/scyllacloud/ScyllaCloudConnectionConfig.java new file mode 100644 index 00000000000..6d27088d4b7 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/config/scyllacloud/ScyllaCloudConnectionConfig.java @@ -0,0 +1,262 @@ +package com.datastax.oss.driver.internal.core.config.scyllacloud; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.StringReader; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.security.KeyPair; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.Security; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.spec.InvalidKeySpecException; +import java.util.Map; +import java.util.NoSuchElementException; +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; +import org.bouncycastle.openssl.PEMKeyPair; +import org.bouncycastle.openssl.PEMParser; +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; + +public class ScyllaCloudConnectionConfig { + private final String kind; + private final String apiVersion; + private final Map datacenters; + private final Map authInfos; + private final Map contexts; + private final String currentContext; + private final Parameters parameters; + + @JsonCreator + public ScyllaCloudConnectionConfig( + @JsonProperty(value = "kind") String kind, + @JsonProperty(value = "apiVersion") String apiVersion, + @JsonProperty(value = "datacenters", required = true) + Map datacenters, + @JsonProperty(value = "authInfos", required = true) + Map authInfos, + @JsonProperty(value = "contexts", required = true) Map contexts, + @JsonProperty(value = "currentContext", required = true) String currentContext, + @JsonProperty(value = "parameters") Parameters parameters) { + this.kind = kind; + this.apiVersion = apiVersion; + this.datacenters = datacenters; + this.authInfos = authInfos; + this.contexts = contexts; + this.currentContext = currentContext; + this.parameters = parameters; + } + + public static ScyllaCloudConnectionConfig fromInputStream(InputStream inputStream) + throws IOException { + ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); + ScyllaCloudConnectionConfig scyllaCloudConnectionConfig = + mapper.readValue(inputStream, ScyllaCloudConnectionConfig.class); + scyllaCloudConnectionConfig.validate(); + return scyllaCloudConnectionConfig; + } + + public void validate() { + if (this.datacenters == null) { + throw new IllegalArgumentException( + "Please provide datacenters (datacenters:) in the configuration yaml."); + } + for (ScyllaCloudDatacenter datacenter : datacenters.values()) { + datacenter.validate(); + } + + if (this.authInfos == null) { + throw new IllegalArgumentException( + "Please provide any authentication config (authInfos:) in the configuration yaml."); + } + for (ScyllaCloudAuthInfo authInfo : authInfos.values()) { + authInfo.validate(); + } + + if (this.contexts == null) { + throw new IllegalArgumentException( + "Please provide any configuration (contexts:) context in the configuration yaml."); + } + + if (this.currentContext == null) { + throw new IllegalArgumentException( + "Please set default context (currentContext:) in the configuration yaml."); + } + } + + public ConfigurationBundle createBundle() + throws KeyStoreException, CertificateException, IOException, NoSuchAlgorithmException, + InvalidKeySpecException { + this.validate(); + KeyStore identity = KeyStore.getInstance(KeyStore.getDefaultType()); + KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); + identity.load(null, null); + trustStore.load(null, null); + for (Map.Entry datacenterEntry : datacenters.entrySet()) { + ScyllaCloudDatacenter datacenter = datacenterEntry.getValue(); + InputStream certificateDataStream; + if (datacenter.getCertificateAuthorityData() != null) { + certificateDataStream = new ByteArrayInputStream(datacenter.getCertificateAuthorityData()); + } else if (datacenter.getCertificateAuthorityPath() != null) { + certificateDataStream = new FileInputStream(datacenter.getCertificateAuthorityPath()); + } else { + // impossible + throw new IllegalStateException( + "Neither CertificateAuthorityPath nor CertificateAuthorityData are set in this Datacenter object. " + + "Validation should have prevented this."); + } + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + Certificate cert = cf.generateCertificate(certificateDataStream); + trustStore.setCertificateEntry(datacenterEntry.getKey(), cert); + } + + for (Map.Entry authInfoEntry : authInfos.entrySet()) { + ScyllaCloudAuthInfo authInfo = authInfoEntry.getValue(); + InputStream certificateDataStream; + String keyString; + + if (authInfo.getClientCertificateData() != null) { + certificateDataStream = new ByteArrayInputStream(authInfo.getClientCertificateData()); + } else if (authInfo.getClientCertificatePath() != null) { + certificateDataStream = new FileInputStream(authInfo.getClientCertificatePath()); + } else { + // impossible + throw new RuntimeException( + "Neither CertificateAuthorityPath nor CertificateAuthorityData are set in this AuthInfo object. " + + "Validation should have prevented this."); + } + + if (authInfo.getClientKeyData() != null) { + keyString = new String(authInfo.getClientKeyData(), Charset.defaultCharset()); + } else if (authInfo.getClientKeyPath() != null) { + BufferedReader br = + Files.newBufferedReader( + Paths.get(authInfo.getClientKeyPath()), Charset.defaultCharset()); + StringBuilder sb = new StringBuilder(); + String line = br.readLine(); + while (line != null) { + sb.append(line); + line = br.readLine(); + } + keyString = sb.toString(); + } else { + // impossible + throw new RuntimeException( + "Neither ClientKeyData nor ClientKeyPath are set in this AuthInfo object. " + + "Validation should have prevented this."); + } + + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + Certificate cert = cf.generateCertificate(certificateDataStream); + + Certificate[] certArr = new Certificate[1]; + certArr[0] = cert; + + Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); + PEMParser pemParser = new PEMParser(new StringReader(keyString)); + JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC"); + Object object = pemParser.readObject(); + PrivateKey privateKey; + if (object instanceof PrivateKeyInfo) { + privateKey = converter.getPrivateKey((PrivateKeyInfo) object); + } else if (object instanceof PEMKeyPair) { + KeyPair kp = converter.getKeyPair((PEMKeyPair) object); + privateKey = kp.getPrivate(); + } else if (object == null) { + // Should not ever happen + throw new IllegalStateException( + "Error parsing authInfo " + + authInfoEntry.getKey() + + ". " + + "Somehow no objects are left in the stream. Is passed Client Key empty?"); + } else { + throw new InvalidKeySpecException( + "Error parsing authInfo " + + authInfoEntry.getKey() + + ". " + + "Make sure provided key signature is either 'RSA PRIVATE KEY' or 'PRIVATE KEY'"); + } + + identity.setKeyEntry(authInfoEntry.getKey(), privateKey, "cassandra".toCharArray(), certArr); + } + + return new ConfigurationBundle(identity, trustStore); + } + + public ScyllaCloudDatacenter getCurrentDatacenter() { + return getDatacenter(getCurrentContext().getDatacenterName()); + } + + public ScyllaCloudAuthInfo getCurrentAuthInfo() { + return getAuthInfo(getCurrentContext().getAuthInfoName()); + } + + public String getKind() { + return kind; + } + + public String getApiVersion() { + return apiVersion; + } + + public Map getDatacenters() { + return datacenters; + } + + public Map getAuthInfos() { + return authInfos; + } + + public Map getContexts() { + return contexts; + } + + public ScyllaCloudContext getContext(String ctx) { + if (!contexts.containsKey(ctx)) { + throw new NoSuchElementException( + String.format( + "There is no context named %s. Check your Scylla Cloud configuration file.", ctx)); + } + return contexts.get(ctx); + } + + public ScyllaCloudAuthInfo getAuthInfo(String authInfo) { + if (!authInfos.containsKey(authInfo)) { + throw new NoSuchElementException( + String.format( + "There is no authInfo named %s. Check your Scylla Cloud configuration file.", + authInfo)); + } + return authInfos.get(authInfo); + } + + public ScyllaCloudDatacenter getDatacenter(String datacenter) { + if (!datacenters.containsKey(datacenter)) { + throw new NoSuchElementException( + String.format( + "There is no datacenter named %s. Check your Scylla Cloud configuration file.", + datacenter)); + } + return datacenters.get(datacenter); + } + + public ScyllaCloudContext getCurrentContext() { + return getContext(currentContext); + } + + public Parameters getParameters() { + return parameters; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/config/scyllacloud/ScyllaCloudContext.java b/core/src/main/java/com/datastax/oss/driver/internal/core/config/scyllacloud/ScyllaCloudContext.java new file mode 100644 index 00000000000..2d94635cdc1 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/config/scyllacloud/ScyllaCloudContext.java @@ -0,0 +1,23 @@ +package com.datastax.oss.driver.internal.core.config.scyllacloud; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class ScyllaCloudContext { + private final String datacenterName; + private final String authInfoName; + + public ScyllaCloudContext( + @JsonProperty(value = "datacenterName", required = true) String datacenterName, + @JsonProperty(value = "authInfoName", required = true) String authInfoName) { + this.datacenterName = datacenterName; + this.authInfoName = authInfoName; + } + + public String getDatacenterName() { + return datacenterName; + } + + public String getAuthInfoName() { + return authInfoName; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/config/scyllacloud/ScyllaCloudDatacenter.java b/core/src/main/java/com/datastax/oss/driver/internal/core/config/scyllacloud/ScyllaCloudDatacenter.java new file mode 100644 index 00000000000..66c824edc71 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/config/scyllacloud/ScyllaCloudDatacenter.java @@ -0,0 +1,167 @@ +package com.datastax.oss.driver.internal.core.config.scyllacloud; + +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.driver.shaded.guava.common.net.HostAndPort; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.io.File; +import java.net.InetSocketAddress; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.List; + +public class ScyllaCloudDatacenter { + private final String certificateAuthorityPath; + private final byte[] certificateAuthorityData; + private final String server; + private final String tlsServerName; + private final String nodeDomain; + private final String proxyURL; + + private final boolean insecureSkipTlsVerify; + + // Full hostname has limit of 255 chars. + // Host UUID takes 32 chars for hex digits and 4 dashes. Additional 1 is for separator dot before + // nodeDomain + private static final int NODE_DOMAIN_MAX_LENGTH = 255 - 32 - 4 - 1; + + @JsonCreator + public ScyllaCloudDatacenter( + @JsonProperty(value = "certificateAuthorityPath") String certificateAuthorityPath, + @JsonProperty(value = "certificateAuthorityData") byte[] certificateAuthorityData, + @JsonProperty(value = "server") String server, + @JsonProperty(value = "tlsServerName") String tlsServerName, + @JsonProperty(value = "nodeDomain") String nodeDomain, + @JsonProperty(value = "proxyURL") String proxyURL, + @JsonProperty(value = "insecureSkipTlsVerify", defaultValue = "false") + boolean insecureSkipTlsVerify) { + this.certificateAuthorityPath = certificateAuthorityPath; + this.certificateAuthorityData = certificateAuthorityData; + this.server = server; + this.tlsServerName = tlsServerName; + this.nodeDomain = nodeDomain; + this.proxyURL = proxyURL; + this.insecureSkipTlsVerify = insecureSkipTlsVerify; + } + + public void validate() { + if (certificateAuthorityData == null) { + if (certificateAuthorityPath == null) { + throw new IllegalArgumentException( + "Either certificateAuthorityData or certificateAuthorityPath must be provided for datacenter description."); + } + File file = new File(certificateAuthorityPath); + if (!file.canRead()) { + throw new IllegalArgumentException( + "Cannot read file at given certificateAuthorityPath (" + + certificateAuthorityPath + + ")."); + } + } + validateServer(); + validateNodeDomain(); + } + + public String getCertificateAuthorityPath() { + return certificateAuthorityPath; + } + + public byte[] getCertificateAuthorityData() { + return certificateAuthorityData; + } + + public InetSocketAddress getServer() { + HostAndPort parsedServer = HostAndPort.fromString(server); + return InetSocketAddress.createUnresolved(parsedServer.getHost(), parsedServer.getPort()); + } + + public String getNodeDomain() { + return nodeDomain; + } + + public String getTlsServerName() { + return tlsServerName; + } + + public String getProxyURL() { + return proxyURL; + } + + public boolean isInsecureSkipTlsVerify() { + return insecureSkipTlsVerify; + } + + // Using parts relevant to hostnames as we're dealing with a part of hostname + // RFC-1123 Section 2.1 and RFC-952 1. + private void validateNodeDomain() { + if (nodeDomain == null || nodeDomain.length() == 0) { + throw new IllegalArgumentException( + "nodeDomain property is required in datacenter description."); + } else { + if (nodeDomain.length() > NODE_DOMAIN_MAX_LENGTH) { + // Should be shorter because it is not the whole hostname + throw new IllegalArgumentException( + "Subdomain name too long (max " + NODE_DOMAIN_MAX_LENGTH + "): " + nodeDomain); + } + if (nodeDomain.contains(" ")) { + throw new IllegalArgumentException( + "nodeDomain '" + nodeDomain + "' cannot contain spaces."); + } + if (nodeDomain.startsWith(".") || nodeDomain.endsWith(".")) { + throw new IllegalArgumentException( + "nodeDomain '" + nodeDomain + "' cannot start or end with a dot."); + } + if (nodeDomain.endsWith("-")) { + throw new IllegalArgumentException( + "nodeDomain '" + nodeDomain + "' cannot end with a minus sign."); + } + } + + List components = ImmutableList.copyOf(nodeDomain.split("\\.")); + for (String component : components) { + if (component.length() == 0) { + throw new IllegalArgumentException( + "nodeDomain '" + nodeDomain + "' cannot have empty components between dots."); + } + + for (int index = 0; index < component.length(); index++) { + if (!Character.isLetterOrDigit(component.charAt(index))) { + if (component.charAt(index) == '-') { + if (index == 0 || index == component.length() - 1) { + throw new IllegalArgumentException( + "nodeDomain '" + + nodeDomain + + "' components can have minus sign only as interior character: " + + component.charAt(index)); + } + } else { + throw new IllegalArgumentException( + "nodeDomain '" + + nodeDomain + + "' contains illegal character: " + + component.charAt(index)); + } + } + } + } + } + + private void validateServer() { + if (server == null) { + throw new IllegalArgumentException("server property is required in datacenter description."); + } else { + try { + // Property 'server' is not a true URL because it does not contain protocol prefix + // We're adding prefix just to satisfy that part of validation + URL url = new URL("http://" + server); + if (url.getPort() == -1) { + throw new IllegalArgumentException( + "server property '" + server + "' does not contain a port."); + } + } catch (MalformedURLException e) { + throw new IllegalArgumentException( + "server property '" + server + "' is not a valid URL", e); + } + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/context/DefaultDriverContext.java b/core/src/main/java/com/datastax/oss/driver/internal/core/context/DefaultDriverContext.java index 2dc0e45d7b8..b7991975190 100644 --- a/core/src/main/java/com/datastax/oss/driver/internal/core/context/DefaultDriverContext.java +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/context/DefaultDriverContext.java @@ -54,12 +54,12 @@ import com.datastax.oss.driver.internal.core.channel.DefaultWriteCoalescer; import com.datastax.oss.driver.internal.core.channel.WriteCoalescer; import com.datastax.oss.driver.internal.core.control.ControlConnection; -import com.datastax.oss.driver.internal.core.metadata.CloudTopologyMonitor; import com.datastax.oss.driver.internal.core.metadata.DefaultTopologyMonitor; import com.datastax.oss.driver.internal.core.metadata.LoadBalancingPolicyWrapper; import com.datastax.oss.driver.internal.core.metadata.MetadataManager; import com.datastax.oss.driver.internal.core.metadata.MultiplexingNodeStateListener; import com.datastax.oss.driver.internal.core.metadata.NoopNodeStateListener; +import com.datastax.oss.driver.internal.core.metadata.ScyllaCloudTopologyMonitor; import com.datastax.oss.driver.internal.core.metadata.TopologyMonitor; import com.datastax.oss.driver.internal.core.metadata.schema.MultiplexingSchemaChangeListener; import com.datastax.oss.driver.internal.core.metadata.schema.NoopSchemaChangeListener; @@ -236,6 +236,7 @@ public class DefaultDriverContext implements InternalDriverContext { private final Map nodeDistanceEvaluatorsFromBuilder; private final ClassLoader classLoader; private final InetSocketAddress cloudProxyAddress; + private final String scyllaCloudNodeDomain; private final LazyReference requestLogFormatterRef = new LazyReference<>("requestLogFormatter", this::buildRequestLogFormatter, cycleDetector); private final UUID startupClientId; @@ -291,6 +292,7 @@ public DefaultDriverContext( this.nodeDistanceEvaluatorsFromBuilder = programmaticArguments.getNodeDistanceEvaluators(); this.classLoader = programmaticArguments.getClassLoader(); this.cloudProxyAddress = programmaticArguments.getCloudProxyAddress(); + this.scyllaCloudNodeDomain = programmaticArguments.getScyllaCloudNodeDomain(); this.startupClientId = programmaticArguments.getStartupClientId(); this.startupApplicationName = programmaticArguments.getStartupApplicationName(); this.startupApplicationVersion = programmaticArguments.getStartupApplicationVersion(); @@ -489,10 +491,10 @@ protected ChannelFactory buildChannelFactory() { } protected TopologyMonitor buildTopologyMonitor() { - if (cloudProxyAddress == null) { - return new DefaultTopologyMonitor(this); + if (cloudProxyAddress != null) { + return new ScyllaCloudTopologyMonitor(this, cloudProxyAddress, scyllaCloudNodeDomain); } - return new CloudTopologyMonitor(this, cloudProxyAddress); + return new DefaultTopologyMonitor(this); } protected MetadataManager buildMetadataManager() { diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/CloudTopologyMonitor.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/CloudTopologyMonitor.java deleted file mode 100644 index 6df0bf3e055..00000000000 --- a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/CloudTopologyMonitor.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * 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 com.datastax.oss.driver.internal.core.metadata; - -import com.datastax.oss.driver.api.core.metadata.EndPoint; -import com.datastax.oss.driver.internal.core.adminrequest.AdminRow; -import com.datastax.oss.driver.internal.core.context.InternalDriverContext; -import edu.umd.cs.findbugs.annotations.NonNull; -import edu.umd.cs.findbugs.annotations.Nullable; -import java.net.InetSocketAddress; -import java.util.Objects; -import java.util.UUID; - -public class CloudTopologyMonitor extends DefaultTopologyMonitor { - - private final InetSocketAddress cloudProxyAddress; - - public CloudTopologyMonitor(InternalDriverContext context, InetSocketAddress cloudProxyAddress) { - super(context); - this.cloudProxyAddress = cloudProxyAddress; - } - - @NonNull - @Override - protected EndPoint buildNodeEndPoint( - @NonNull AdminRow row, - @Nullable InetSocketAddress broadcastRpcAddress, - @NonNull EndPoint localEndPoint) { - UUID hostId = Objects.requireNonNull(row.getUuid("host_id")); - return new SniEndPoint(cloudProxyAddress, hostId.toString()); - } -} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/DefaultTopologyMonitor.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/DefaultTopologyMonitor.java index da5fc2115eb..6716e1458b5 100644 --- a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/DefaultTopologyMonitor.java +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/DefaultTopologyMonitor.java @@ -68,11 +68,11 @@ public class DefaultTopologyMonitor implements TopologyMonitor { private static final int INFINITE_PAGE_SIZE = -1; private final String logPrefix; - private final InternalDriverContext context; - private final ControlConnection controlConnection; + protected final InternalDriverContext context; + protected final ControlConnection controlConnection; private final Duration timeout; - private final boolean reconnectOnInit; - private final CompletableFuture closeFuture; + protected final boolean reconnectOnInit; + protected final CompletableFuture closeFuture; @VisibleForTesting volatile boolean isSchemaV2; @VisibleForTesting volatile int port = -1; diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/ScyllaCloudTopologyMonitor.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/ScyllaCloudTopologyMonitor.java new file mode 100644 index 00000000000..b7e1ce690b2 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/ScyllaCloudTopologyMonitor.java @@ -0,0 +1,82 @@ +/* + * Copyright DataStax, Inc. + * + * 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 com.datastax.oss.driver.internal.core.metadata; + +import com.datastax.oss.driver.api.core.metadata.EndPoint; +import com.datastax.oss.driver.internal.core.adminrequest.AdminRow; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.util.concurrent.CompletableFutures; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableSet; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.net.InetSocketAddress; +import java.util.Collections; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.CompletionStage; + +public class ScyllaCloudTopologyMonitor extends DefaultTopologyMonitor { + + private final InetSocketAddress cloudProxyAddress; + private final String nodeDomain; + + public ScyllaCloudTopologyMonitor( + InternalDriverContext context, InetSocketAddress cloudProxyAddress, String nodeDomain) { + super(context); + this.cloudProxyAddress = cloudProxyAddress; + this.nodeDomain = nodeDomain; + } + + @NonNull + @Override + protected EndPoint buildNodeEndPoint( + @NonNull AdminRow row, + @Nullable InetSocketAddress broadcastRpcAddress, + @NonNull EndPoint localEndPoint) { + UUID hostId = Objects.requireNonNull(row.getUuid("host_id")); + return new SniEndPoint(cloudProxyAddress, hostId + "." + nodeDomain); + } + + // Perform usual init with extra steps. After establishing connection we need to replace + // endpoint that randomizes target node with concrete endpoint to the specific node. + @Override + public CompletionStage init() { + if (closeFuture.isDone()) { + return CompletableFutures.failedFuture(new IllegalStateException("closed")); + } + return controlConnection + .init(true, reconnectOnInit, true) + .thenCompose( + v -> { + return query( + controlConnection.channel(), + "SELECT host_id FROM system.local", + Collections.emptyMap()) + .toCompletableFuture(); + }) + .thenApply( + adminResult -> { + AdminRow localRow = adminResult.iterator().next(); + UUID hostId = localRow.getUuid("host_id"); + EndPoint newEndpoint = new SniEndPoint(cloudProxyAddress, hostId + "." + nodeDomain); + // Replace initial contact point with specified endpoint, so that reconnections won't + // choose different random node + context.getMetadataManager().addContactPoints(ImmutableSet.of(newEndpoint)); + controlConnection.channel().setEndPoint(newEndpoint); + return null; + }); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/config/cloud/CloudConfigFactoryTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/config/cloud/CloudConfigFactoryTest.java deleted file mode 100644 index 6f54413b601..00000000000 --- a/core/src/test/java/com/datastax/oss/driver/internal/core/config/cloud/CloudConfigFactoryTest.java +++ /dev/null @@ -1,233 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * 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 com.datastax.oss.driver.internal.core.config.cloud; - -import static com.datastax.oss.driver.Assertions.assertThat; -import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; -import static com.github.tomakehurst.wiremock.client.WireMock.any; -import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; -import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; -import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; -import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; -import static org.assertj.core.api.Assertions.catchThrowable; - -import com.datastax.oss.driver.internal.core.ssl.SniSslEngineFactory; -import com.fasterxml.jackson.core.JsonParseException; -import com.github.tomakehurst.wiremock.common.JettySettings; -import com.github.tomakehurst.wiremock.core.Options; -import com.github.tomakehurst.wiremock.http.AdminRequestHandler; -import com.github.tomakehurst.wiremock.http.HttpServer; -import com.github.tomakehurst.wiremock.http.HttpServerFactory; -import com.github.tomakehurst.wiremock.http.StubRequestHandler; -import com.github.tomakehurst.wiremock.jetty9.JettyHttpServer; -import com.github.tomakehurst.wiremock.junit.WireMockRule; -import com.google.common.base.Joiner; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.net.InetSocketAddress; -import java.net.URISyntaxException; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import org.eclipse.jetty.io.NetworkTrafficListener; -import org.eclipse.jetty.server.ConnectionFactory; -import org.eclipse.jetty.server.ServerConnector; -import org.eclipse.jetty.server.SslConnectionFactory; -import org.eclipse.jetty.util.ssl.SslContextFactory; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; - -@RunWith(MockitoJUnitRunner.class) -public class CloudConfigFactoryTest { - - private static final String BUNDLE_PATH = "/config/cloud/creds.zip"; - - @Rule - public WireMockRule wireMockRule = - new WireMockRule( - wireMockConfig() - .httpsPort(30443) - .dynamicPort() - .httpServerFactory(new HttpsServerFactory()) - .needClientAuth(true) - .keystorePath(path("/config/cloud/identity.jks").toString()) - .keystorePassword("fakePasswordForTests") - .trustStorePath(path("/config/cloud/trustStore.jks").toString()) - .trustStorePassword("fakePasswordForTests2")); - - public CloudConfigFactoryTest() throws URISyntaxException {} - - @Test - public void should_load_config_from_local_filesystem() throws Exception { - // given - URL configFile = getClass().getResource(BUNDLE_PATH); - mockProxyMetadataService(jsonMetadata()); - // when - CloudConfigFactory cloudConfigFactory = new CloudConfigFactory(); - CloudConfig cloudConfig = cloudConfigFactory.createCloudConfig(configFile); - // then - assertCloudConfig(cloudConfig); - } - - @Test - public void should_load_config_from_external_location() throws Exception { - // given - mockHttpSecureBundle(secureBundle()); - mockProxyMetadataService(jsonMetadata()); - // when - URL configFile = new URL("http", "localhost", wireMockRule.port(), BUNDLE_PATH); - CloudConfigFactory cloudConfigFactory = new CloudConfigFactory(); - CloudConfig cloudConfig = cloudConfigFactory.createCloudConfig(configFile); - // then - assertCloudConfig(cloudConfig); - } - - @Test - public void should_throw_when_bundle_not_found() throws Exception { - // given - stubFor(any(urlEqualTo(BUNDLE_PATH)).willReturn(aResponse().withStatus(404))); - // when - URL configFile = new URL("http", "localhost", wireMockRule.port(), BUNDLE_PATH); - CloudConfigFactory cloudConfigFactory = new CloudConfigFactory(); - Throwable t = catchThrowable(() -> cloudConfigFactory.createCloudConfig(configFile)); - assertThat(t) - .isInstanceOf(FileNotFoundException.class) - .hasMessageContaining(configFile.toExternalForm()); - } - - @Test - public void should_throw_when_bundle_not_readable() throws Exception { - // given - mockHttpSecureBundle("not a zip file".getBytes(StandardCharsets.UTF_8)); - // when - URL configFile = new URL("http", "localhost", wireMockRule.port(), BUNDLE_PATH); - CloudConfigFactory cloudConfigFactory = new CloudConfigFactory(); - Throwable t = catchThrowable(() -> cloudConfigFactory.createCloudConfig(configFile)); - assertThat(t) - .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("Invalid bundle: missing file config.json"); - } - - @Test - public void should_throw_when_metadata_not_found() throws Exception { - // given - mockHttpSecureBundle(secureBundle()); - stubFor(any(urlPathEqualTo("/metadata")).willReturn(aResponse().withStatus(404))); - // when - URL configFile = new URL("http", "localhost", wireMockRule.port(), BUNDLE_PATH); - CloudConfigFactory cloudConfigFactory = new CloudConfigFactory(); - Throwable t = catchThrowable(() -> cloudConfigFactory.createCloudConfig(configFile)); - assertThat(t).isInstanceOf(FileNotFoundException.class).hasMessageContaining("metadata"); - } - - @Test - public void should_throw_when_metadata_not_readable() throws Exception { - // given - mockHttpSecureBundle(secureBundle()); - mockProxyMetadataService("not a valid json payload"); - // when - URL configFile = new URL("http", "localhost", wireMockRule.port(), BUNDLE_PATH); - CloudConfigFactory cloudConfigFactory = new CloudConfigFactory(); - Throwable t = catchThrowable(() -> cloudConfigFactory.createCloudConfig(configFile)); - assertThat(t).isInstanceOf(JsonParseException.class).hasMessageContaining("Unrecognized token"); - } - - private void mockHttpSecureBundle(byte[] body) { - stubFor( - any(urlEqualTo(BUNDLE_PATH)) - .willReturn( - aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/octet-stream") - .withBody(body))); - } - - private void mockProxyMetadataService(String jsonMetadata) { - stubFor( - any(urlPathEqualTo("/metadata")) - .willReturn( - aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody(jsonMetadata))); - } - - private byte[] secureBundle() throws IOException, URISyntaxException { - return Files.readAllBytes(path(BUNDLE_PATH)); - } - - private String jsonMetadata() throws IOException, URISyntaxException { - return Joiner.on('\n') - .join(Files.readAllLines(path("/config/cloud/metadata.json"), StandardCharsets.UTF_8)); - } - - private Path path(String resource) throws URISyntaxException { - return Paths.get(getClass().getResource(resource).toURI()); - } - - private void assertCloudConfig(CloudConfig config) { - InetSocketAddress expectedProxyAddress = InetSocketAddress.createUnresolved("localhost", 30002); - assertThat(config.getLocalDatacenter()).isEqualTo("dc1"); - assertThat(config.getProxyAddress()).isEqualTo(expectedProxyAddress); - assertThat(config.getEndPoints()).extracting("proxyAddress").containsOnly(expectedProxyAddress); - assertThat(config.getEndPoints()) - .extracting("serverName") - .containsExactly( - "4ac06655-f861-49f9-881e-3fee22e69b94", - "2af7c253-3394-4a0d-bfac-f1ad81b5154d", - "b17b6e2a-3f48-4d6a-81c1-20a0a1f3192a"); - assertThat(config.getSslEngineFactory()).isNotNull().isInstanceOf(SniSslEngineFactory.class); - } - - static { - javax.net.ssl.HttpsURLConnection.setDefaultHostnameVerifier( - (hostname, sslSession) -> hostname.equals("localhost")); - } - - // see https://github.com/tomakehurst/wiremock/issues/874 - private static class HttpsServerFactory implements HttpServerFactory { - @Override - public HttpServer buildHttpServer( - Options options, - AdminRequestHandler adminRequestHandler, - StubRequestHandler stubRequestHandler) { - return new JettyHttpServer(options, adminRequestHandler, stubRequestHandler) { - @Override - protected ServerConnector createServerConnector( - String bindAddress, - JettySettings jettySettings, - int port, - NetworkTrafficListener listener, - ConnectionFactory... connectionFactories) { - if (port == options.httpsSettings().port()) { - SslConnectionFactory sslConnectionFactory = - (SslConnectionFactory) connectionFactories[0]; - SslContextFactory sslContextFactory = sslConnectionFactory.getSslContextFactory(); - sslContextFactory.setKeyStorePassword(options.httpsSettings().keyStorePassword()); - connectionFactories = - new ConnectionFactory[] {sslConnectionFactory, connectionFactories[1]}; - } - return super.createServerConnector( - bindAddress, jettySettings, port, listener, connectionFactories); - } - }; - } - } -} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/config/scyllacloud/CloudConfigYamlParsingTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/config/scyllacloud/CloudConfigYamlParsingTest.java new file mode 100644 index 00000000000..73818df524c --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/config/scyllacloud/CloudConfigYamlParsingTest.java @@ -0,0 +1,31 @@ +package com.datastax.oss.driver.internal.core.config.scyllacloud; + +import java.io.IOException; +import java.net.URL; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.spec.InvalidKeySpecException; +import org.junit.Test; + +public class CloudConfigYamlParsingTest { + @Test + public void read_simple_config_and_create_bundle() + throws CertificateException, KeyStoreException, IOException, NoSuchAlgorithmException, + InvalidKeySpecException { + final String CONFIG_PATH = "/config/scyllacloud/testConf.yaml"; + URL url = getClass().getResource(CONFIG_PATH); + ScyllaCloudConnectionConfig scyllaCloudConnectionConfig = + ScyllaCloudConnectionConfig.fromInputStream(url.openStream()); + scyllaCloudConnectionConfig.validate(); + scyllaCloudConnectionConfig.createBundle(); + } + + @Test(expected = IllegalArgumentException.class) + public void read_incomplete_config() throws IOException { + // This config does not contain certificates which are required + final String CONFIG_PATH = "/config/scyllacloud/incompleteConf.yaml"; + URL url = getClass().getResource(CONFIG_PATH); + ScyllaCloudConnectionConfig.fromInputStream(url.openStream()); + } +} diff --git a/core/src/test/resources/config/cloud/creds.zip b/core/src/test/resources/config/cloud/creds.zip deleted file mode 100644 index 3b5d1cb1cbd..00000000000 Binary files a/core/src/test/resources/config/cloud/creds.zip and /dev/null differ diff --git a/core/src/test/resources/config/cloud/identity.jks b/core/src/test/resources/config/cloud/identity.jks deleted file mode 100644 index bac5bbaa965..00000000000 Binary files a/core/src/test/resources/config/cloud/identity.jks and /dev/null differ diff --git a/core/src/test/resources/config/cloud/metadata.json b/core/src/test/resources/config/cloud/metadata.json deleted file mode 100644 index 35aa26f67f1..00000000000 --- a/core/src/test/resources/config/cloud/metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"region":"local","contact_info":{"type":"sni_proxy","local_dc":"dc1","contact_points":["4ac06655-f861-49f9-881e-3fee22e69b94","2af7c253-3394-4a0d-bfac-f1ad81b5154d","b17b6e2a-3f48-4d6a-81c1-20a0a1f3192a"],"sni_proxy_address":"localhost:30002"}} diff --git a/core/src/test/resources/config/cloud/trustStore.jks b/core/src/test/resources/config/cloud/trustStore.jks deleted file mode 100644 index 8ee03f97da0..00000000000 Binary files a/core/src/test/resources/config/cloud/trustStore.jks and /dev/null differ diff --git a/core/src/test/resources/config/scyllacloud/incompleteConf.yaml b/core/src/test/resources/config/scyllacloud/incompleteConf.yaml new file mode 100644 index 00000000000..b2fa018b1ef --- /dev/null +++ b/core/src/test/resources/config/scyllacloud/incompleteConf.yaml @@ -0,0 +1,21 @@ +datacenters: + eu-west-123: + server: redacted.com.anskjfbsdiubg:1234 + nodeDomain: redacted.com.anskjfbsdiubg + +authInfos: + default: + clientKeyData: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb0FJQkFBS0NBUUVBbTBnNWRiSGM4cEJEbFRTNDJqY3BmdlVyMTU3QnhsK2JIUFI5ZEZSWXNPTm5DR1NkCldGdWxqaHp1YkVxWE9lYmZhOTdpYmpIK0ZqbkZ4UEJmS0xzUi9nV2VORjNNVFE2MzZqa1YrVTNINDFEalRDT1AKdyt6ZTBDREM1S2huNEhZK0YxMFJDT1BVVWFNZ09nZi9IbXp5aTRiY0xxTUtJR2xEQXpSNHpQMHFnT1Jnemd0Sgo1bnV6SWtCakJtR29nQXdFNGhybnVxWVVjam5aYkZ0a0xZcnl5R2RmMjRSSHpSakw2ZnF5QnF1U2xBMHRqUFU0ClhyNlpQTStsUlhBSjZzNGNRUFZ3UzRHaTk1aWhwM1FmZ3piRlBFcHAzeFJ3Mkd1TXBXNFBlekcyc1dKdTdhb0YKQ294L2htVS9SU21lNllwVHF3Ym94WWF0MW9GT0pnTXFadFJub3dJREFRQUJBb0gvUUV1bmZ4UW1hRWxUc25RaQpGampBOWRDT1ZybGxncjRUZStuQUNHcmtUbG5hVDU5Wmh6eHJVR3lxVEN5V2NXTW5INE1xUG5aMHZyOHRKRjVqCkNIcHMrTUZhd0ZRV1E5SFVWU2ROOGV4QzE5eW1RT0I0cHFkdG1yLzE4cmZJU3lpcWpRSDhDN0pLTjROVDFMTjYKN3g0dFQ1aUhrc2Z2YVh3c1F5ZDIzV24yTDJlM1BuYVUwZkR6RzNYaGxLa3hOYXNpalF2dWNjNDhlYVJ4UlM5WgpFeHdLOC93SGh6WllBa1h4QndZaWNsaG91V1R5b2Jqd2UwVGJHQlJaRCtHWmwxdHU0TlRmb3E1cDR3V1VaUEJhCm1RUXVmbUtWMEZBUkRhYmNpa0lSRXIvelVWUjVzN3daaldzWSs0MUFGc3lUY2dCa2lack1ZVkdGbjMvaUl3R2kKWjRJUkFvR0JBUHFGZ1ZUZ2NCYnpQTjRLbmRMVWVmT2FnZkFSOEoxYjRVRUNpc21rOVhPTlJvRWJPSGpVa3JUMQpCK01lNmVyNXFjSEdSdjlHYVZwWWQ0Y1VyWlhiOGRsZUVSQ3ppYlY4OXpYVzBMZTk2Qk9ra09YK2VyNHh5US8xCnJUWnluZkZBUmRhK3B6VWdqZFFHRnJZREpsZmp2MXRjQU94WEFzWGF5UEZjbEp6SlBNanRBb0dCQUo2dGltRUUKeUlzcE1XOGw3WENNZ3I2aTdyN3M5M0dVT1dYb1RMSU91WENXMWd0bVdwdTRQY0d6MHdWUTNKR3pBNXdEMnJzRQpnbEZXQW9XeW53c1JZUWhiQWZRem5Ba1dqMk9RbC9BaW1oZzF3SkhqUXdzZ0VjbUJrK2tqbGlFTmFtQ1BHM0VJCkk3M2FneHNIN2xuQXhvZnBtSDdpS0Y1MjFva2pEb0NNVjdEUEFvR0FmUCs0TkYxNEVEdDFsMlM4c3NHSng5N0UKRHNFa3laOWFtVkZuWm8yRVd3K1dxanlteE1Oc0lCWlN4U1JibXY3UGtQd1oySzJOUzZMd29OblVjdjIzZ3JuSQowZ2lESWFja3doeFpNQlQxZ3plTmhQU2cwZDJOY1FVb1ZBNkVlQ2VWc1R5WHVZNXd0ZVlEMXZWallGOG16N0xzClV3Qm5SY21Ra2IyYitVNy9vVkVDZ1lCSStQQUpmQVNxRXRDY0Yrb2c5MDF0VkVyTlhQYlNzZUxQbmN3Zm1xdm4KUGtiRFNWZmtBdy9MaytJNHNKNHZGdzlTNFdibTJNVUJtTGRpT3VudlVoZTRtdm5FRHpQejdmOFZQN3JRQVdteQpObzRQeVY3Y3IrdmVLb3dXREhxUFNyY2dIcy8wNUZSampDajg5bUhEdnViT1BEd1lKZk9BdGRBbGt3eXBTMkZNCmV3S0JnRjFBZU1WRXpoTGkzV0RvTktieTE0RFFLVFh1akJjVFR3dkJZTk5OcXU2ZnVhYWJyU3I1azhBR3BaR00KdGgxTUZ0K2N1MnoyZUcvZ2c3enIzU002WVhqQldqNmJpa1NnRnA0TitjQ0JwZ1hYOExLVklua3huY0tEQk5OOQpqdXRPUlQrL1JVcDRDWEhvRWxhRWN1R3VCTVVDNnFEVjdZOWpOWGVDRmZIMHRqc1gKLS0tLS1FTkQgUlNBIFBSSVZBVEUgS0VZLS0tLS0K + username: cassandra + password: cassandra + +contexts: + default: + datacenterName: eu-west-123 + authInfoName: default + +currentContext: default + +parameters: + defaultConsistency: LOCAL_QUORUM + defaultSerialConsistency: LOCAL_SERIAL diff --git a/core/src/test/resources/config/scyllacloud/testConf.yaml b/core/src/test/resources/config/scyllacloud/testConf.yaml new file mode 100644 index 00000000000..785f79b94cf --- /dev/null +++ b/core/src/test/resources/config/scyllacloud/testConf.yaml @@ -0,0 +1,23 @@ +datacenters: + eu-west-123: + certificateAuthorityData: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUVGRENDQXZ5Z0F3SUJBZ0lFSmVQTDBqQU5CZ2txaGtpRzl3MEJBUXNGQURCTE1Rc3dDUVlEVlFRR0V3SlYNClV6RVJNQThHQTFVRUNoTUlSR0YwWVZOMFlYZ3hFREFPQmdOVkJBc1RCME5EVFc1dlpHVXhGekFWQmdOVkJBTVQNCkRrTmhjM05oYm1SeVlTQk9iMlJsTUI0WERUSXlNVEV3TnpFd016RTBOVm9YRFRJek1URXdOekV3TXpFME5Wb3cNClN6RUxNQWtHQTFVRUJoTUNWVk14RVRBUEJnTlZCQW9UQ0VSaGRHRlRkR0Y0TVJBd0RnWURWUVFMRXdkRFEwMXUNCmIyUmxNUmN3RlFZRFZRUURFdzVEWVhOellXNWtjbUVnVG05a1pUQ0NBU0l3RFFZSktvWklodmNOQVFFQkJRQUQNCmdnRVBBRENDQVFvQ2dnRUJBSnRJT1hXeDNQS1FRNVUwdU5vM0tYNzFLOWVld2NaZm14ejBmWFJVV0xEalp3aGsNCm5WaGJwWTRjN214S2x6bm0zMnZlNG00eC9oWTV4Y1R3WHlpN0VmNEZualJkekUwT3QrbzVGZmxOeCtOUTQwd2oNCmo4UHMzdEFnd3VTb1orQjJQaGRkRVFqajFGR2pJRG9IL3g1czhvdUczQzZqQ2lCcFF3TTBlTXo5S29Ea1lNNEwNClNlWjdzeUpBWXdaaHFJQU1CT0lhNTdxbUZISTUyV3hiWkMySzhzaG5YOXVFUjgwWXkrbjZzZ2Fya3BRTkxZejENCk9GNittVHpQcFVWd0Nlck9IRUQxY0V1Qm92ZVlvYWQwSDRNMnhUeEthZDhVY05ocmpLVnVEM3N4dHJGaWJ1MnENCkJRcU1mNFpsUDBVcG51bUtVNnNHNk1XR3JkYUJUaVlES21iVVo2TUNBd0VBQWFPQi96Q0IvRENCMmdZRFZSMFINCkJJSFNNSUhQZ2hsaGJua3VZMngxYzNSbGNpMXBaQzV6WTNsc2JHRXVZMjl0Z2pvMk5UWTVPVFJtTkMxa09HRTMNCkxUUmtOREl0WW1ObE9DMHdaVGxtWWpJNFl6Z3haalF1WTJ4MWMzUmxjaTFwWkM1elkzbHNiR0V1WTI5dGdqbzUNCk0ySmlZV0V6TXkxbU5tRTNMVFJtTkRJdFlXRmtaUzFqWkRJNU0yTmhOR0ZtT0RJdVkyeDFjM1JsY2kxcFpDNXoNClkzbHNiR0V1WTI5dGdqcG1Nakk0TkRSaFlTMDBPR0ZsTFRSbE5UY3RPR1UxWWkxbE1HSTVPRE5rT0RaallqSXUNClkyeDFjM1JsY2kxcFpDNXpZM2xzYkdFdVkyOXRNQjBHQTFVZERnUVdCQlNuMDFzZEM1QXYzVk1vTGtEeWViajMNCjJqalIwREFOQmdrcWhraUc5dzBCQVFzRkFBT0NBUUVBZlBRK1hEdysxMlI0cVZlcnQ1ekVCYVFhNFYyZW11SWMNCks0QkRyQ3lPdFh0eEV1eUhsb0NWS2FzR0FJWi95aTYvZ3p1NnVobDRYcjhrZ2tDaElDcDYwOWlNVFFtL0RnRm8NClQ4TFNlY0oxSjFYa0t5NXFUdXAyL2ROTW5CcXF6SGxMV1FXajNLTDQwMXgzbng4L2lPODJJTTNmZFV3Ri9LZ0QNClRpRVBtNFlydXdaTURqSVl0YVkzV290Y2x6WVcvb1Y1bmRXL1BwemxUNWdqNTFyK2t1bFMvcm5lYmM4cG1tbGwNCmpzMDZrMVEvN1Y3eTRMamNQWk80SG5HbEN1dHhDM1pKT2d2c0pwTTc3ZFdDbkhoNW9qdFJHRkdaUUd3dHBTMXENCjhndUhmMFdIUDdMTTY1dDVCKy8wOVIvdDFsRU1wajQrb3hiTWpkRUNKR0hMOWRQY1N2SlVIZz09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K + server: redacted.com.anskjfbsdiubg:1234 + nodeDomain: redacted.com.anskjfbsdiubg + +authInfos: + default: + clientCertificateData: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUVGRENDQXZ5Z0F3SUJBZ0lFSmVQTDBqQU5CZ2txaGtpRzl3MEJBUXNGQURCTE1Rc3dDUVlEVlFRR0V3SlYNClV6RVJNQThHQTFVRUNoTUlSR0YwWVZOMFlYZ3hFREFPQmdOVkJBc1RCME5EVFc1dlpHVXhGekFWQmdOVkJBTVQNCkRrTmhjM05oYm1SeVlTQk9iMlJsTUI0WERUSXlNVEV3TnpFd016RTBOVm9YRFRJek1URXdOekV3TXpFME5Wb3cNClN6RUxNQWtHQTFVRUJoTUNWVk14RVRBUEJnTlZCQW9UQ0VSaGRHRlRkR0Y0TVJBd0RnWURWUVFMRXdkRFEwMXUNCmIyUmxNUmN3RlFZRFZRUURFdzVEWVhOellXNWtjbUVnVG05a1pUQ0NBU0l3RFFZSktvWklodmNOQVFFQkJRQUQNCmdnRVBBRENDQVFvQ2dnRUJBSnRJT1hXeDNQS1FRNVUwdU5vM0tYNzFLOWVld2NaZm14ejBmWFJVV0xEalp3aGsNCm5WaGJwWTRjN214S2x6bm0zMnZlNG00eC9oWTV4Y1R3WHlpN0VmNEZualJkekUwT3QrbzVGZmxOeCtOUTQwd2oNCmo4UHMzdEFnd3VTb1orQjJQaGRkRVFqajFGR2pJRG9IL3g1czhvdUczQzZqQ2lCcFF3TTBlTXo5S29Ea1lNNEwNClNlWjdzeUpBWXdaaHFJQU1CT0lhNTdxbUZISTUyV3hiWkMySzhzaG5YOXVFUjgwWXkrbjZzZ2Fya3BRTkxZejENCk9GNittVHpQcFVWd0Nlck9IRUQxY0V1Qm92ZVlvYWQwSDRNMnhUeEthZDhVY05ocmpLVnVEM3N4dHJGaWJ1MnENCkJRcU1mNFpsUDBVcG51bUtVNnNHNk1XR3JkYUJUaVlES21iVVo2TUNBd0VBQWFPQi96Q0IvRENCMmdZRFZSMFINCkJJSFNNSUhQZ2hsaGJua3VZMngxYzNSbGNpMXBaQzV6WTNsc2JHRXVZMjl0Z2pvMk5UWTVPVFJtTkMxa09HRTMNCkxUUmtOREl0WW1ObE9DMHdaVGxtWWpJNFl6Z3haalF1WTJ4MWMzUmxjaTFwWkM1elkzbHNiR0V1WTI5dGdqbzUNCk0ySmlZV0V6TXkxbU5tRTNMVFJtTkRJdFlXRmtaUzFqWkRJNU0yTmhOR0ZtT0RJdVkyeDFjM1JsY2kxcFpDNXoNClkzbHNiR0V1WTI5dGdqcG1Nakk0TkRSaFlTMDBPR0ZsTFRSbE5UY3RPR1UxWWkxbE1HSTVPRE5rT0RaallqSXUNClkyeDFjM1JsY2kxcFpDNXpZM2xzYkdFdVkyOXRNQjBHQTFVZERnUVdCQlNuMDFzZEM1QXYzVk1vTGtEeWViajMNCjJqalIwREFOQmdrcWhraUc5dzBCQVFzRkFBT0NBUUVBZlBRK1hEdysxMlI0cVZlcnQ1ekVCYVFhNFYyZW11SWMNCks0QkRyQ3lPdFh0eEV1eUhsb0NWS2FzR0FJWi95aTYvZ3p1NnVobDRYcjhrZ2tDaElDcDYwOWlNVFFtL0RnRm8NClQ4TFNlY0oxSjFYa0t5NXFUdXAyL2ROTW5CcXF6SGxMV1FXajNLTDQwMXgzbng4L2lPODJJTTNmZFV3Ri9LZ0QNClRpRVBtNFlydXdaTURqSVl0YVkzV290Y2x6WVcvb1Y1bmRXL1BwemxUNWdqNTFyK2t1bFMvcm5lYmM4cG1tbGwNCmpzMDZrMVEvN1Y3eTRMamNQWk80SG5HbEN1dHhDM1pKT2d2c0pwTTc3ZFdDbkhoNW9qdFJHRkdaUUd3dHBTMXENCjhndUhmMFdIUDdMTTY1dDVCKy8wOVIvdDFsRU1wajQrb3hiTWpkRUNKR0hMOWRQY1N2SlVIZz09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K + clientKeyData: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb0FJQkFBS0NBUUVBbTBnNWRiSGM4cEJEbFRTNDJqY3BmdlVyMTU3QnhsK2JIUFI5ZEZSWXNPTm5DR1NkCldGdWxqaHp1YkVxWE9lYmZhOTdpYmpIK0ZqbkZ4UEJmS0xzUi9nV2VORjNNVFE2MzZqa1YrVTNINDFEalRDT1AKdyt6ZTBDREM1S2huNEhZK0YxMFJDT1BVVWFNZ09nZi9IbXp5aTRiY0xxTUtJR2xEQXpSNHpQMHFnT1Jnemd0Sgo1bnV6SWtCakJtR29nQXdFNGhybnVxWVVjam5aYkZ0a0xZcnl5R2RmMjRSSHpSakw2ZnF5QnF1U2xBMHRqUFU0ClhyNlpQTStsUlhBSjZzNGNRUFZ3UzRHaTk1aWhwM1FmZ3piRlBFcHAzeFJ3Mkd1TXBXNFBlekcyc1dKdTdhb0YKQ294L2htVS9SU21lNllwVHF3Ym94WWF0MW9GT0pnTXFadFJub3dJREFRQUJBb0gvUUV1bmZ4UW1hRWxUc25RaQpGampBOWRDT1ZybGxncjRUZStuQUNHcmtUbG5hVDU5Wmh6eHJVR3lxVEN5V2NXTW5INE1xUG5aMHZyOHRKRjVqCkNIcHMrTUZhd0ZRV1E5SFVWU2ROOGV4QzE5eW1RT0I0cHFkdG1yLzE4cmZJU3lpcWpRSDhDN0pLTjROVDFMTjYKN3g0dFQ1aUhrc2Z2YVh3c1F5ZDIzV24yTDJlM1BuYVUwZkR6RzNYaGxLa3hOYXNpalF2dWNjNDhlYVJ4UlM5WgpFeHdLOC93SGh6WllBa1h4QndZaWNsaG91V1R5b2Jqd2UwVGJHQlJaRCtHWmwxdHU0TlRmb3E1cDR3V1VaUEJhCm1RUXVmbUtWMEZBUkRhYmNpa0lSRXIvelVWUjVzN3daaldzWSs0MUFGc3lUY2dCa2lack1ZVkdGbjMvaUl3R2kKWjRJUkFvR0JBUHFGZ1ZUZ2NCYnpQTjRLbmRMVWVmT2FnZkFSOEoxYjRVRUNpc21rOVhPTlJvRWJPSGpVa3JUMQpCK01lNmVyNXFjSEdSdjlHYVZwWWQ0Y1VyWlhiOGRsZUVSQ3ppYlY4OXpYVzBMZTk2Qk9ra09YK2VyNHh5US8xCnJUWnluZkZBUmRhK3B6VWdqZFFHRnJZREpsZmp2MXRjQU94WEFzWGF5UEZjbEp6SlBNanRBb0dCQUo2dGltRUUKeUlzcE1XOGw3WENNZ3I2aTdyN3M5M0dVT1dYb1RMSU91WENXMWd0bVdwdTRQY0d6MHdWUTNKR3pBNXdEMnJzRQpnbEZXQW9XeW53c1JZUWhiQWZRem5Ba1dqMk9RbC9BaW1oZzF3SkhqUXdzZ0VjbUJrK2tqbGlFTmFtQ1BHM0VJCkk3M2FneHNIN2xuQXhvZnBtSDdpS0Y1MjFva2pEb0NNVjdEUEFvR0FmUCs0TkYxNEVEdDFsMlM4c3NHSng5N0UKRHNFa3laOWFtVkZuWm8yRVd3K1dxanlteE1Oc0lCWlN4U1JibXY3UGtQd1oySzJOUzZMd29OblVjdjIzZ3JuSQowZ2lESWFja3doeFpNQlQxZ3plTmhQU2cwZDJOY1FVb1ZBNkVlQ2VWc1R5WHVZNXd0ZVlEMXZWallGOG16N0xzClV3Qm5SY21Ra2IyYitVNy9vVkVDZ1lCSStQQUpmQVNxRXRDY0Yrb2c5MDF0VkVyTlhQYlNzZUxQbmN3Zm1xdm4KUGtiRFNWZmtBdy9MaytJNHNKNHZGdzlTNFdibTJNVUJtTGRpT3VudlVoZTRtdm5FRHpQejdmOFZQN3JRQVdteQpObzRQeVY3Y3IrdmVLb3dXREhxUFNyY2dIcy8wNUZSampDajg5bUhEdnViT1BEd1lKZk9BdGRBbGt3eXBTMkZNCmV3S0JnRjFBZU1WRXpoTGkzV0RvTktieTE0RFFLVFh1akJjVFR3dkJZTk5OcXU2ZnVhYWJyU3I1azhBR3BaR00KdGgxTUZ0K2N1MnoyZUcvZ2c3enIzU002WVhqQldqNmJpa1NnRnA0TitjQ0JwZ1hYOExLVklua3huY0tEQk5OOQpqdXRPUlQrL1JVcDRDWEhvRWxhRWN1R3VCTVVDNnFEVjdZOWpOWGVDRmZIMHRqc1gKLS0tLS1FTkQgUlNBIFBSSVZBVEUgS0VZLS0tLS0K + username: cassandra + password: cassandra + +contexts: + default: + datacenterName: eu-west-123 + authInfoName: default + +currentContext: default + +parameters: + defaultConsistency: LOCAL_QUORUM + defaultSerialConsistency: LOCAL_SERIAL diff --git a/examples/src/main/java/com/datastax/oss/driver/examples/astra/AstraReadCassandraVersion.java b/examples/src/main/java/com/datastax/oss/driver/examples/astra/AstraReadCassandraVersion.java deleted file mode 100644 index d434665552a..00000000000 --- a/examples/src/main/java/com/datastax/oss/driver/examples/astra/AstraReadCassandraVersion.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * 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 com.datastax.oss.driver.examples.astra; - -import com.datastax.oss.driver.api.core.CqlSession; -import com.datastax.oss.driver.api.core.cql.ResultSet; -import com.datastax.oss.driver.api.core.cql.Row; -import java.nio.file.Paths; - -/** - * Connects to a DataStax Astra cluster and extracts basic information from it. - * - *

Preconditions: - * - *

    - *
  • A DataStax Astra cluster is running and accessible. - *
  • A DataStax Astra secure connect bundle for the running cluster. - *
- * - *

Side effects: none. - * - * @see - * Creating an Astra Database (GCP) - * @see - * Providing access to Astra databases (GCP) - * @see - * Obtaining Astra secure connect bundle (GCP) - * @see Java driver online - * manual - */ -public class AstraReadCassandraVersion { - - public static void main(String[] args) { - - // The Session is what you use to execute queries. It is thread-safe and should be - // reused. - try (CqlSession session = - CqlSession.builder() - // Change the path here to the secure connect bundle location (see javadocs above) - .withCloudSecureConnectBundle(Paths.get("/path/to/secure-connect-database_name.zip")) - // Change the user_name and password here for the Astra instance - .withAuthCredentials("user_name", "fakePasswordForTests") - // Uncomment the next line to use a specific keyspace - // .withKeyspace("keyspace_name") - .build()) { - - // We use execute to send a query to Cassandra. This returns a ResultSet, which - // is essentially a collection of Row objects. - ResultSet rs = session.execute("select release_version from system.local"); - // Extract the first row (which is the only one in this case). - Row row = rs.one(); - - // Extract the value of the first (and only) column from the row. - assert row != null; - String releaseVersion = row.getString("release_version"); - System.out.printf("Cassandra version is: %s%n", releaseVersion); - } - // The try-with-resources block automatically close the session after we’re done with it. - // This step is important because it frees underlying resources (TCP connections, thread - // pools...). In a real application, you would typically do this at shutdown - // (for example, when undeploying your webapp). - } -} diff --git a/examples/src/main/java/com/datastax/oss/driver/examples/scyllacloud/ReadScyllaVersion.java b/examples/src/main/java/com/datastax/oss/driver/examples/scyllacloud/ReadScyllaVersion.java new file mode 100644 index 00000000000..a899edf7fb7 --- /dev/null +++ b/examples/src/main/java/com/datastax/oss/driver/examples/scyllacloud/ReadScyllaVersion.java @@ -0,0 +1,33 @@ +package com.datastax.oss.driver.examples.scyllacloud; + +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.DefaultProtocolVersion; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverConfigLoader; +import com.datastax.oss.driver.api.core.cql.ResultSet; +import com.datastax.oss.driver.api.core.cql.Row; +import java.io.File; + +public class ReadScyllaVersion { + + public static void main(String[] args) { + String configPath = "/path/to/scylla/cloud/conf/file"; + File configFile = new File(configPath); + DriverConfigLoader loader = + DriverConfigLoader.programmaticBuilder() + .withString(DefaultDriverOption.PROTOCOL_VERSION, DefaultProtocolVersion.V4.toString()) + .build(); + + try (CqlSession session = + CqlSession.builder() + .withConfigLoader(loader) + .withScyllaCloudSecureConnectBundle(configFile.toPath()) + .build()) { + ResultSet rs = session.execute("select release_version from system.local"); + Row row = rs.one(); + assert row != null; + String releaseVersion = row.getString("release_version"); + System.out.printf("Scylla version: %s%n", releaseVersion); + } + } +} diff --git a/integration-tests/src/test/java/com/datastax/oss/driver/api/core/cloud/CloudIT.java b/integration-tests/src/test/java/com/datastax/oss/driver/api/core/cloud/CloudIT.java deleted file mode 100644 index 7874c4719d8..00000000000 --- a/integration-tests/src/test/java/com/datastax/oss/driver/api/core/cloud/CloudIT.java +++ /dev/null @@ -1,498 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * 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 com.datastax.oss.driver.api.core.cloud; - -import static com.datastax.oss.driver.internal.core.util.LoggerTest.setupTestLogger; -import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; -import static com.github.tomakehurst.wiremock.client.WireMock.any; -import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; -import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; -import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.timeout; -import static org.mockito.Mockito.verify; - -import ch.qos.logback.classic.Level; -import ch.qos.logback.classic.spi.ILoggingEvent; -import com.datastax.oss.driver.api.core.AllNodesFailedException; -import com.datastax.oss.driver.api.core.CqlSession; -import com.datastax.oss.driver.api.core.auth.AuthenticationException; -import com.datastax.oss.driver.api.core.config.DefaultDriverOption; -import com.datastax.oss.driver.api.core.config.DriverConfigLoader; -import com.datastax.oss.driver.api.core.cql.ResultSet; -import com.datastax.oss.driver.api.core.session.SessionBuilder; -import com.datastax.oss.driver.api.testinfra.session.SessionUtils; -import com.datastax.oss.driver.categories.IsolatedTests; -import com.datastax.oss.driver.internal.core.config.cloud.CloudConfigFactory; -import com.datastax.oss.driver.internal.core.ssl.DefaultSslEngineFactory; -import com.datastax.oss.driver.internal.core.util.LoggerTest; -import com.github.tomakehurst.wiremock.junit.WireMockRule; -import java.io.IOException; -import java.io.InputStream; -import java.net.InetSocketAddress; -import java.net.URL; -import java.nio.file.Files; -import java.nio.file.Path; -import java.security.NoSuchAlgorithmException; -import java.util.Collections; -import java.util.List; -import javax.net.ssl.SSLContext; -import org.junit.ClassRule; -import org.junit.Ignore; -import org.junit.Rule; -import org.junit.Test; -import org.junit.experimental.categories.Category; - -@Category(IsolatedTests.class) -@Ignore("Disabled because it is causing trouble in Jenkins CI") -public class CloudIT { - - private static final String BUNDLE_URL_PATH = "/certs/bundles/creds.zip"; - - @ClassRule public static SniProxyRule proxyRule = new SniProxyRule(); - - // Used only to host the secure connect bundle, for tests that require external URLs - @Rule - public WireMockRule wireMockRule = - new WireMockRule(wireMockConfig().dynamicPort().dynamicHttpsPort()); - - @Test - public void should_connect_to_proxy_using_path() { - ResultSet set; - Path bundle = proxyRule.getProxy().getDefaultBundlePath(); - try (CqlSession session = - CqlSession.builder() - .withAuthCredentials("cassandra", "cassandra") - .withCloudSecureConnectBundle(bundle) - .build()) { - set = session.execute("select * from system.local"); - } - assertThat(set).isNotNull(); - } - - @Test - public void should_connect_and_log_info_that_config_json_with_username_password_was_provided() { - ResultSet set; - Path bundle = proxyRule.getProxy().getDefaultBundlePath(); - LoggerTest.LoggerSetup logger = setupTestLogger(CloudConfigFactory.class, Level.INFO); - - try (CqlSession session = - CqlSession.builder() - .withAuthCredentials("cassandra", "cassandra") - .withCloudSecureConnectBundle(bundle) - .build()) { - set = session.execute("select * from system.local"); - verify(logger.appender, timeout(500).atLeast(1)) - .doAppend(logger.loggingEventCaptor.capture()); - assertThat( - logger.loggingEventCaptor.getAllValues().stream() - .map(ILoggingEvent::getFormattedMessage)) - .contains( - "The bundle contains config.json with username and/or password. Providing it in the bundle is deprecated and ignored."); - } - assertThat(set).isNotNull(); - } - - @Test - public void - should_fail_with_auth_error_when_connecting_using_bundle_with_username_password_in_config_json() { - Path bundle = proxyRule.getProxy().getDefaultBundlePath(); - - // fails with auth error because username/password from config.json is ignored - AllNodesFailedException exception = null; - try { - CqlSession.builder().withCloudSecureConnectBundle(bundle).build(); - } catch (AllNodesFailedException ex) { - exception = ex; - } - assertThat(exception).isNotNull(); - List errors = exception.getAllErrors().values().iterator().next(); - Throwable firstError = errors.get(0); - assertThat(firstError).isInstanceOf(AuthenticationException.class); - } - - @Test - public void should_connect_to_proxy_without_credentials() { - ResultSet set; - Path bundle = proxyRule.getProxy().getBundleWithoutCredentialsPath(); - try (CqlSession session = - CqlSession.builder() - .withCloudSecureConnectBundle(bundle) - .withAuthCredentials("cassandra", "cassandra") - .build()) { - set = session.execute("select * from system.local"); - } - assertThat(set).isNotNull(); - } - - @Test - public void should_connect_to_proxy_using_non_normalized_path() { - Path bundle = proxyRule.getProxy().getBundlesRootPath().resolve("../bundles/creds-v1.zip"); - ResultSet set; - try (CqlSession session = - CqlSession.builder() - .withAuthCredentials("cassandra", "cassandra") - .withCloudSecureConnectBundle(bundle) - .build()) { - set = session.execute("select * from system.local"); - } - assertThat(set).isNotNull(); - } - - @Test - public void should_connect_to_proxy_using_input_stream() throws IOException { - InputStream bundle = Files.newInputStream(proxyRule.getProxy().getDefaultBundlePath()); - ResultSet set; - try (CqlSession session = - CqlSession.builder() - .withAuthCredentials("cassandra", "cassandra") - .withCloudSecureConnectBundle(bundle) - .build()) { - set = session.execute("select * from system.local"); - } - assertThat(set).isNotNull(); - } - - @Test - public void should_connect_to_proxy_using_URL() throws IOException { - // given - byte[] bundle = Files.readAllBytes(proxyRule.getProxy().getDefaultBundlePath()); - stubFor( - any(urlEqualTo(BUNDLE_URL_PATH)) - .willReturn( - aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/octet-stream") - .withBody(bundle))); - URL bundleUrl = - new URL(String.format("http://localhost:%d%s", wireMockRule.port(), BUNDLE_URL_PATH)); - - // when - ResultSet set; - try (CqlSession session = - CqlSession.builder() - .withAuthCredentials("cassandra", "cassandra") - .withCloudSecureConnectBundle(bundleUrl) - .build()) { - - // then - set = session.execute("select * from system.local"); - } - assertThat(set).isNotNull(); - } - - @Test - public void should_connect_to_proxy_using_absolute_path_provided_in_the_session_setting() { - // given - String bundle = proxyRule.getProxy().getDefaultBundlePath().toString(); - DriverConfigLoader loader = - DriverConfigLoader.programmaticBuilder() - .withString(DefaultDriverOption.CLOUD_SECURE_CONNECT_BUNDLE, bundle) - .build(); - // when - ResultSet set; - try (CqlSession session = - CqlSession.builder() - .withAuthCredentials("cassandra", "cassandra") - .withConfigLoader(loader) - .build()) { - - // then - set = session.execute("select * from system.local"); - } - assertThat(set).isNotNull(); - } - - @Test - public void should_connect_to_proxy_using_non_normalized_path_provided_in_the_session_setting() { - // given - String bundle = - proxyRule.getProxy().getBundlesRootPath().resolve("../bundles/creds-v1.zip").toString(); - DriverConfigLoader loader = - DriverConfigLoader.programmaticBuilder() - .withString(DefaultDriverOption.CLOUD_SECURE_CONNECT_BUNDLE, bundle) - .build(); - // when - ResultSet set; - try (CqlSession session = - CqlSession.builder() - .withAuthCredentials("cassandra", "cassandra") - .withConfigLoader(loader) - .build()) { - - // then - set = session.execute("select * from system.local"); - } - assertThat(set).isNotNull(); - } - - @Test - public void - should_connect_to_proxy_using_url_with_file_protocol_provided_in_the_session_setting() { - // given - String bundle = proxyRule.getProxy().getDefaultBundlePath().toString(); - DriverConfigLoader loader = - DriverConfigLoader.programmaticBuilder() - .withString(DefaultDriverOption.CLOUD_SECURE_CONNECT_BUNDLE, bundle) - .build(); - // when - ResultSet set; - try (CqlSession session = - CqlSession.builder() - .withAuthCredentials("cassandra", "cassandra") - .withConfigLoader(loader) - .build()) { - - // then - set = session.execute("select * from system.local"); - } - assertThat(set).isNotNull(); - } - - @Test - public void should_connect_to_proxy_using_url_with_http_protocol_provided_in_the_session_setting() - throws IOException { - // given - byte[] bundle = Files.readAllBytes(proxyRule.getProxy().getDefaultBundlePath()); - stubFor( - any(urlEqualTo(BUNDLE_URL_PATH)) - .willReturn( - aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/octet-stream") - .withBody(bundle))); - String bundleUrl = String.format("http://localhost:%d%s", wireMockRule.port(), BUNDLE_URL_PATH); - DriverConfigLoader loader = - DriverConfigLoader.programmaticBuilder() - .withString(DefaultDriverOption.CLOUD_SECURE_CONNECT_BUNDLE, bundleUrl) - .build(); - // when - ResultSet set; - try (CqlSession session = - CqlSession.builder() - .withAuthCredentials("cassandra", "cassandra") - .withConfigLoader(loader) - .build()) { - - // then - set = session.execute("select * from system.local"); - } - assertThat(set).isNotNull(); - } - - @Test - public void - should_connect_and_log_info_when_contact_points_and_secure_bundle_used_programmatic() { - // given - LoggerTest.LoggerSetup logger = setupTestLogger(SessionBuilder.class, Level.INFO); - - Path bundle = proxyRule.getProxy().getBundleWithoutCredentialsPath(); - - try (CqlSession session = - CqlSession.builder() - .withCloudSecureConnectBundle(bundle) - .addContactPoint(new InetSocketAddress("127.0.0.1", 9042)) - .withAuthCredentials("cassandra", "cassandra") - .build(); ) { - - // when - ResultSet set = session.execute("select * from system.local"); - // then - assertThat(set).isNotNull(); - verify(logger.appender, timeout(500).atLeast(1)) - .doAppend(logger.loggingEventCaptor.capture()); - assertThat( - logger.loggingEventCaptor.getAllValues().stream() - .map(ILoggingEvent::getFormattedMessage)) - .contains( - "Both a secure connect bundle and contact points were provided. These are mutually exclusive. The contact points from the secure bundle will have priority."); - - } finally { - logger.close(); - } - } - - @Test - public void should_connect_and_log_info_when_contact_points_and_secure_bundle_used_config() { - // given - LoggerTest.LoggerSetup logger = setupTestLogger(SessionBuilder.class, Level.INFO); - - DriverConfigLoader loader = - SessionUtils.configLoaderBuilder() - .withStringList( - DefaultDriverOption.CONTACT_POINTS, Collections.singletonList("localhost:9042")) - .build(); - - Path bundle = proxyRule.getProxy().getBundleWithoutCredentialsPath(); - - try (CqlSession session = - CqlSession.builder() - .withConfigLoader(loader) - .withCloudSecureConnectBundle(bundle) - .withAuthCredentials("cassandra", "cassandra") - .build(); ) { - - // when - ResultSet set = session.execute("select * from system.local"); - // then - assertThat(set).isNotNull(); - verify(logger.appender, timeout(500).atLeast(1)) - .doAppend(logger.loggingEventCaptor.capture()); - assertThat( - logger.loggingEventCaptor.getAllValues().stream() - .map(ILoggingEvent::getFormattedMessage)) - .contains( - "Both a secure connect bundle and contact points were provided. These are mutually exclusive. The contact points from the secure bundle will have priority."); - - } finally { - logger.close(); - } - } - - @Test - public void should_connect_and_log_info_when_ssl_context_and_secure_bundle_used_programmatic() - throws NoSuchAlgorithmException { - // given - LoggerTest.LoggerSetup logger = setupTestLogger(SessionBuilder.class, Level.INFO); - - Path bundle = proxyRule.getProxy().getBundleWithoutCredentialsPath(); - - try (CqlSession session = - CqlSession.builder() - .withCloudSecureConnectBundle(bundle) - .withAuthCredentials("cassandra", "cassandra") - .withSslContext(SSLContext.getInstance("SSL")) - .build()) { - // when - ResultSet set = session.execute("select * from system.local"); - // then - assertThat(set).isNotNull(); - verify(logger.appender, timeout(500).atLeast(1)) - .doAppend(logger.loggingEventCaptor.capture()); - assertThat( - logger.loggingEventCaptor.getAllValues().stream() - .map(ILoggingEvent::getFormattedMessage)) - .contains( - "Both a secure connect bundle and SSL options were provided. They are mutually exclusive. The SSL options from the secure bundle will have priority."); - } finally { - logger.close(); - } - } - - @Test - public void should_error_when_ssl_context_and_secure_bundle_used_config() - throws NoSuchAlgorithmException { - // given - LoggerTest.LoggerSetup logger = setupTestLogger(SessionBuilder.class, Level.INFO); - - DriverConfigLoader loader = - SessionUtils.configLoaderBuilder() - .withBoolean(DefaultDriverOption.RECONNECT_ON_INIT, true) - .withClass(DefaultDriverOption.SSL_ENGINE_FACTORY_CLASS, DefaultSslEngineFactory.class) - .build(); - - Path bundle = proxyRule.getProxy().getBundleWithoutCredentialsPath(); - - try (CqlSession session = - CqlSession.builder() - .withConfigLoader(loader) - .withCloudSecureConnectBundle(bundle) - .withAuthCredentials("cassandra", "cassandra") - .build()) { - // when - ResultSet set = session.execute("select * from system.local"); - // then - assertThat(set).isNotNull(); - verify(logger.appender, timeout(500).atLeast(1)) - .doAppend(logger.loggingEventCaptor.capture()); - assertThat( - logger.loggingEventCaptor.getAllValues().stream() - .map(ILoggingEvent::getFormattedMessage)) - .contains( - "Both a secure connect bundle and SSL options were provided. They are mutually exclusive. The SSL options from the secure bundle will have priority."); - } finally { - logger.close(); - } - } - - @Test - public void - should_connect_and_log_info_when_local_data_center_and_secure_bundle_used_programmatic() { - // given - LoggerTest.LoggerSetup logger = setupTestLogger(SessionBuilder.class, Level.INFO); - - DriverConfigLoader loader = - SessionUtils.configLoaderBuilder() - .withString(DefaultDriverOption.LOAD_BALANCING_LOCAL_DATACENTER, "dc-ignore") - .build(); - - Path bundle = proxyRule.getProxy().getBundleWithoutCredentialsPath(); - - try (CqlSession session = - CqlSession.builder() - .withCloudSecureConnectBundle(bundle) - .withConfigLoader(loader) - .withAuthCredentials("cassandra", "cassandra") - .build(); ) { - - // when - ResultSet set = session.execute("select * from system.local"); - // then - assertThat(set).isNotNull(); - verify(logger.appender, timeout(500).atLeast(1)) - .doAppend(logger.loggingEventCaptor.capture()); - assertThat( - logger.loggingEventCaptor.getAllValues().stream() - .map(ILoggingEvent::getFormattedMessage)) - .contains( - "Both a secure connect bundle and a local datacenter were provided. They are mutually exclusive. The local datacenter from the secure bundle will have priority."); - - } finally { - logger.close(); - } - } - - @Test - public void should_connect_and_log_info_when_local_data_center_and_secure_bundle_used_config() { - // given - LoggerTest.LoggerSetup logger = setupTestLogger(SessionBuilder.class, Level.INFO); - - Path bundle = proxyRule.getProxy().getBundleWithoutCredentialsPath(); - - try (CqlSession session = - CqlSession.builder() - .withCloudSecureConnectBundle(bundle) - .withLocalDatacenter("dc-ignored") - .withAuthCredentials("cassandra", "cassandra") - .build(); ) { - - // when - ResultSet set = session.execute("select * from system.local"); - // then - assertThat(set).isNotNull(); - verify(logger.appender, timeout(500).atLeast(1)) - .doAppend(logger.loggingEventCaptor.capture()); - assertThat( - logger.loggingEventCaptor.getAllValues().stream() - .map(ILoggingEvent::getFormattedMessage)) - .contains( - "Both a secure connect bundle and a local datacenter were provided. They are mutually exclusive. The local datacenter from the secure bundle will have priority."); - - } finally { - logger.close(); - } - } -} diff --git a/integration-tests/src/test/java/com/datastax/oss/driver/api/core/cloud/SniProxyRule.java b/integration-tests/src/test/java/com/datastax/oss/driver/api/core/cloud/SniProxyRule.java deleted file mode 100644 index 706f337d39c..00000000000 --- a/integration-tests/src/test/java/com/datastax/oss/driver/api/core/cloud/SniProxyRule.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * 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 com.datastax.oss.driver.api.core.cloud; - -import org.junit.rules.ExternalResource; - -public class SniProxyRule extends ExternalResource { - - private final SniProxyServer proxy; - - public SniProxyRule() { - proxy = new SniProxyServer(); - } - - @Override - protected void before() { - proxy.startProxy(); - } - - @Override - protected void after() { - proxy.stopProxy(); - } - - public SniProxyServer getProxy() { - return proxy; - } -} diff --git a/integration-tests/src/test/java/com/datastax/oss/driver/api/core/cloud/SniProxyServer.java b/integration-tests/src/test/java/com/datastax/oss/driver/api/core/cloud/SniProxyServer.java deleted file mode 100644 index af137f2bb70..00000000000 --- a/integration-tests/src/test/java/com/datastax/oss/driver/api/core/cloud/SniProxyServer.java +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * 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 com.datastax.oss.driver.api.core.cloud; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.concurrent.TimeUnit; -import org.apache.commons.exec.CommandLine; -import org.apache.commons.exec.DefaultExecutor; -import org.apache.commons.exec.ExecuteStreamHandler; -import org.apache.commons.exec.ExecuteWatchdog; -import org.apache.commons.exec.Executor; -import org.apache.commons.exec.LogOutputStream; -import org.apache.commons.exec.PumpStreamHandler; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class SniProxyServer { - - private static final Logger LOG = LoggerFactory.getLogger(SniProxyServer.class); - - private final Path proxyPath; - private final Path bundlesRootPath; - private final Path defaultBundlePath; - private final Path bundleWithoutCredentialsPath; - private final Path bundleWithoutClientCertificatesPath; - private final Path bundleWithInvalidCAPath; - private final Path bundleWithUnreachableMetadataServicePath; - - private volatile boolean running = false; - - public SniProxyServer() { - this(Paths.get(System.getProperty("proxy.path", "./"))); - } - - public SniProxyServer(Path proxyPath) { - this.proxyPath = proxyPath.normalize().toAbsolutePath(); - bundlesRootPath = proxyPath.resolve("certs/bundles/"); - defaultBundlePath = bundlesRootPath.resolve("creds-v1.zip"); - bundleWithoutCredentialsPath = bundlesRootPath.resolve("creds-v1-wo-creds.zip"); - bundleWithoutClientCertificatesPath = bundlesRootPath.resolve("creds-v1-wo-cert.zip"); - bundleWithInvalidCAPath = bundlesRootPath.resolve("creds-v1-invalid-ca.zip"); - bundleWithUnreachableMetadataServicePath = bundlesRootPath.resolve("creds-v1-unreachable.zip"); - } - - public void startProxy() { - CommandLine run = CommandLine.parse(proxyPath + "/run.sh"); - execute(run); - running = true; - } - - public void stopProxy() { - if (running) { - CommandLine findImageId = - CommandLine.parse("docker ps -a -q --filter ancestor=single_endpoint"); - String id = execute(findImageId); - CommandLine stop = CommandLine.parse("docker kill " + id); - execute(stop); - running = false; - } - } - - /** @return The root folder of the SNI proxy server docker image. */ - public Path getProxyPath() { - return proxyPath; - } - - /** - * @return The root folder where secure connect bundles exposed by this SNI proxy for testing - * purposes can be found. - */ - public Path getBundlesRootPath() { - return bundlesRootPath; - } - - /** - * @return The default secure connect bundle. It contains credentials and all certificates - * required to connect. - */ - public Path getDefaultBundlePath() { - return defaultBundlePath; - } - - /** @return A secure connect bundle without credentials in config.json. */ - public Path getBundleWithoutCredentialsPath() { - return bundleWithoutCredentialsPath; - } - - /** @return A secure connect bundle without client certificates (no identity.jks). */ - public Path getBundleWithoutClientCertificatesPath() { - return bundleWithoutClientCertificatesPath; - } - - /** @return A secure connect bundle with an invalid Certificate Authority. */ - public Path getBundleWithInvalidCAPath() { - return bundleWithInvalidCAPath; - } - - /** @return A secure connect bundle with an invalid address for the Proxy Metadata Service. */ - public Path getBundleWithUnreachableMetadataServicePath() { - return bundleWithUnreachableMetadataServicePath; - } - - private String execute(CommandLine cli) { - LOG.debug("Executing: " + cli); - ExecuteWatchdog watchDog = new ExecuteWatchdog(TimeUnit.MINUTES.toMillis(10)); - ByteArrayOutputStream outStream = new ByteArrayOutputStream(); - try (LogOutputStream errStream = - new LogOutputStream() { - @Override - protected void processLine(String line, int logLevel) { - LOG.error("sniendpointerr> {}", line); - } - }) { - Executor executor = new DefaultExecutor(); - ExecuteStreamHandler streamHandler = new PumpStreamHandler(outStream, errStream); - executor.setStreamHandler(streamHandler); - executor.setWatchdog(watchDog); - executor.setWorkingDirectory(proxyPath.toFile()); - int retValue = executor.execute(cli); - if (retValue != 0) { - LOG.error("Non-zero exit code ({}) returned from executing ccm command: {}", retValue, cli); - } - return outStream.toString(); - } catch (IOException ex) { - if (watchDog.killedProcess()) { - throw new RuntimeException("The command '" + cli + "' was killed after 10 minutes"); - } else { - throw new RuntimeException("The command '" + cli + "' failed to execute", ex); - } - } - } -} diff --git a/integration-tests/src/test/java/com/datastax/oss/driver/api/core/scyllacloud/ScyllaCloudMultiNodeIT.java b/integration-tests/src/test/java/com/datastax/oss/driver/api/core/scyllacloud/ScyllaCloudMultiNodeIT.java new file mode 100644 index 00000000000..1eb34bf9ebc --- /dev/null +++ b/integration-tests/src/test/java/com/datastax/oss/driver/api/core/scyllacloud/ScyllaCloudMultiNodeIT.java @@ -0,0 +1,88 @@ +package com.datastax.oss.driver.api.core.scyllacloud; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.DefaultProtocolVersion; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverConfigLoader; +import com.datastax.oss.driver.api.core.metadata.EndPoint; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata; +import com.datastax.oss.driver.api.core.metadata.schema.SchemaChangeListener; +import com.datastax.oss.driver.api.core.metadata.schema.SchemaChangeListenerBase; +import com.datastax.oss.driver.api.core.metadata.schema.TableMetadata; +import com.datastax.oss.driver.api.testinfra.CassandraSkip; +import com.datastax.oss.driver.api.testinfra.ccm.CustomCcmRule; +import com.datastax.oss.driver.categories.IsolatedTests; +import com.datastax.oss.driver.internal.core.config.scyllacloud.ScyllaCloudConnectionConfig; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.Map; +import java.util.UUID; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.mockito.Mockito; + +@Category(IsolatedTests.class) +@CassandraSkip +public class ScyllaCloudMultiNodeIT { + + private static final int NUMBER_OF_NODES = 3; + private static final int SNI_PORT = 0; // Let CCM pick + + @ClassRule + public static CustomCcmRule CCM_RULE = + CustomCcmRule.builder().withNodes(NUMBER_OF_NODES).withSniProxy(SNI_PORT).build(); + + @Test + public void connect_w_simple_operations_protocol_v4() { + String configPath = CCM_RULE.getCcmBridge().getScyllaCloudConfigPathString(); + File configFile = new File(configPath); + DriverConfigLoader loader = + DriverConfigLoader.programmaticBuilder() + .withString(DefaultDriverOption.PROTOCOL_VERSION, DefaultProtocolVersion.V4.toString()) + .build(); + SchemaChangeListener mockListener = Mockito.mock(SchemaChangeListenerBase.class); + try (CqlSession session = + CqlSession.builder() + .withConfigLoader(loader) + .withScyllaCloudSecureConnectBundle(configFile.toPath()) + // Currently ccm produces cloud config with eu-west-1 dc name but uses dc1 + .withLocalDatacenter("dc1") + .withSchemaChangeListener(mockListener) + .build()) { + + session.execute( + String.format( + "CREATE KEYSPACE %s " + + "WITH replication = {'class': 'SimpleStrategy', 'replication_factor': %d}", + "testks", NUMBER_OF_NODES)); + session.execute("CREATE TABLE testks.testtab (a int PRIMARY KEY, b int);"); + + verify(mockListener, times(1)).onTableCreated(any(TableMetadata.class)); + verify(mockListener, times(1)).onKeyspaceCreated(any(KeyspaceMetadata.class)); + + int sniPort = + ScyllaCloudConnectionConfig.fromInputStream(Files.newInputStream(configFile.toPath())) + .getCurrentDatacenter() + .getServer() + .getPort(); + Map map = session.getMetadata().getNodes(); + assertThat(map.size()).isEqualTo(NUMBER_OF_NODES); + String expectedEndpointPrefix = CCM_RULE.getCcmBridge().getIpPrefix() + "1:" + sniPort + ":"; + for (Map.Entry entry : map.entrySet()) { + EndPoint endPoint = entry.getValue().getEndPoint(); + assertThat(endPoint.toString()).startsWith(expectedEndpointPrefix); + assertThat(endPoint.toString()).contains(entry.getKey().toString()); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/pom.xml b/pom.xml index d137664037b..5602ea9d068 100644 --- a/pom.xml +++ b/pom.xml @@ -65,6 +65,7 @@ 2.13.2 2.13.2.2 1.9.12 + 1.72 1.1.7.3 1.7.1 @@ -342,6 +343,11 @@ jackson-databind ${jackson-databind.version} + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + ${jackson.version} + com.google.testing.compile compile-testing @@ -445,6 +451,16 @@ blockhound-junit-platform 1.0.4.RELEASE + + org.bouncycastle + bcprov-jdk18on + ${bouncycastle.version} + + + org.bouncycastle + bcpkix-jdk18on + ${bouncycastle.version} + diff --git a/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/ccm/CcmBridge.java b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/ccm/CcmBridge.java index 41801e52bcc..099e6ef3153 100644 --- a/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/ccm/CcmBridge.java +++ b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/ccm/CcmBridge.java @@ -23,6 +23,7 @@ import com.datastax.oss.driver.api.core.Version; import com.datastax.oss.driver.shaded.guava.common.base.Joiner; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; import com.datastax.oss.driver.shaded.guava.common.collect.Maps; import com.datastax.oss.driver.shaded.guava.common.io.Resources; @@ -179,6 +180,8 @@ public class CcmBridge implements AutoCloseable { private final List dseWorkloads; private final String jvmArgs; + private final int sniProxyPort; + private CcmBridge( Path configDirectory, int[] nodes, @@ -188,7 +191,8 @@ private CcmBridge( List dseConfigurationRawYaml, List createOptions, Collection jvmArgs, - List dseWorkloads) { + List dseWorkloads, + int sniProxyPort) { this.configDirectory = configDirectory; if (nodes.length == 1) { // Hack to ensure that the default DC is always called 'dc1': pass a list ('-nX:0') even if @@ -216,6 +220,7 @@ private CcmBridge( } this.jvmArgs = allJvmArgs.toString(); this.dseWorkloads = dseWorkloads; + this.sniProxyPort = sniProxyPort; } // Copied from Netty's PlatformDependent to avoid the dependency on Netty @@ -290,6 +295,14 @@ private String getCcmVersionString(Version version) { return version.toString(); } + public String getScyllaCloudConfigPathString() { + return configDirectory.toFile().getAbsolutePath() + "/" + CLUSTER_NAME + "/config_data.yaml"; + } + + public String getIpPrefix() { + return ipPrefix; + } + public void create() { if (created.compareAndSet(false, true)) { if (INSTALL_DIRECTORY != null) { @@ -359,7 +372,12 @@ public void reloadCore(int node, String keyspace, String table, boolean reindex) public void start() { if (started.compareAndSet(false, true)) { try { - execute("start", jvmArgs, "--wait-for-binary-proto"); + execute( + "start", + jvmArgs, + "--wait-for-binary-proto", + (sniProxyPort >= 0 ? "--sni-proxy" : ""), + (sniProxyPort > 0 ? "--sni-port=" + sniProxyPort : "")); } catch (RuntimeException re) { // if something went wrong starting CCM, see if we can also dump the error executeCheckLogError(); @@ -510,6 +528,8 @@ public static class Builder { private final Path configDirectory; + private int sniProxyPort = -1; + private Builder() { try { this.configDirectory = Files.createTempDirectory("ccm"); @@ -614,6 +634,14 @@ public Builder withDseWorkloads(String... workloads) { return this; } + /** Enable SNI proxy and use given port number. Port 0 means any port. */ + public Builder withSniProxy(int port) { + Preconditions.checkArgument(port >= 0); + Preconditions.checkArgument(port <= 65535); + this.sniProxyPort = port; + return this; + } + public CcmBridge build() { return new CcmBridge( configDirectory, @@ -624,7 +652,8 @@ public CcmBridge build() { dseRawYaml, createOptions, jvmArgs, - dseWorkloads); + dseWorkloads, + sniProxyPort); } } } diff --git a/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/ccm/CustomCcmRule.java b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/ccm/CustomCcmRule.java index 4ea1b3843f3..e6b0dd0a71b 100644 --- a/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/ccm/CustomCcmRule.java +++ b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/ccm/CustomCcmRule.java @@ -112,6 +112,11 @@ public Builder withSslAuth() { return this; } + public Builder withSniProxy(int port) { + bridgeBuilder.withSniProxy(port); + return this; + } + public CustomCcmRule build() { return new CustomCcmRule(bridgeBuilder.build()); }