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());
}