From bc5514cad37b8d05d7c09208e5db818e30d5ac2b Mon Sep 17 00:00:00 2001 From: alexsa Date: Mon, 10 Feb 2025 11:21:03 +0100 Subject: [PATCH 1/9] Add SubnetAddressTranslator --- core/pom.xml | 4 + .../SubnetAddressTranslator.java | 210 ++++++++++++++++++ core/src/main/resources/reference.conf | 3 +- .../SubnetAddressTranslatorTest.java | 130 +++++++++++ pom.xml | 6 + 5 files changed, 352 insertions(+), 1 deletion(-) create mode 100644 core/src/main/java/com/datastax/oss/driver/internal/core/addresstranslation/SubnetAddressTranslator.java create mode 100644 core/src/test/java/com/datastax/oss/driver/internal/core/addresstranslation/SubnetAddressTranslatorTest.java diff --git a/core/pom.xml b/core/pom.xml index b8d7d5c2d3b..0e9200576b5 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -116,6 +116,10 @@ org.reactivestreams reactive-streams + + commons-net + commons-net + com.github.stephenc.jcip jcip-annotations diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/addresstranslation/SubnetAddressTranslator.java b/core/src/main/java/com/datastax/oss/driver/internal/core/addresstranslation/SubnetAddressTranslator.java new file mode 100644 index 00000000000..ce05c9b69cb --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/addresstranslation/SubnetAddressTranslator.java @@ -0,0 +1,210 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.addresstranslation; + +import com.datastax.oss.driver.api.core.addresstranslation.AddressTranslator; +import com.datastax.oss.driver.api.core.config.DriverOption; +import com.datastax.oss.driver.api.core.context.DriverContext; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.net.InetSocketAddress; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; +import org.apache.commons.net.util.SubnetUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This translator returns the proxy address of the private subnet containing the Cassandra node IP, + * or default address if no matching subnets, or passes through the original node address if no + * default configured. + * + *

The translator can be used for scenarios when all nodes are behind some kind of proxy, and + * that proxy is different for nodes located in different subnets (eg. when Cassandra is deployed in + * multiple datacenters/regions). One can use this, for example, for Cassandra on Kubernetes with + * different Cassandra datacenters deployed to different Kubernetes clusters. + */ +public class SubnetAddressTranslator implements AddressTranslator { + + private static final Logger LOG = LoggerFactory.getLogger(SubnetAddressTranslator.class); + + /** + * A comma separated list with Cassandra node subnets (CIDR notations) to target addresses in a + * format `::`, for example: + * `100.64.0.0/15:cassandra.datacenter1.com:9042,100.66.0.0/15:cassandra.datacenter1.com:9042`. If + * configured without port, the default 9042 will be used. + */ + public static final String ADDRESS_TRANSLATOR_SUBNET_ADDRESSES = + "advanced.address-translator.subnet-addresses"; + /** + * A default address to fallback to if Cassandra node IP isn't contained in any of the configured + * subnets. If configured without port, the default 9042 will be used. + */ + public static final String ADDRESS_TRANSLATOR_DEFAULT_ADDRESS = + "advanced.address-translator.default-address"; + + public static DriverOption ADDRESS_TRANSLATOR_SUBNET_ADDRESSES_OPTION = + new DriverOption() { + @NonNull + @Override + public String getPath() { + return ADDRESS_TRANSLATOR_SUBNET_ADDRESSES; + } + }; + public static DriverOption ADDRESS_TRANSLATOR_DEFAULT_ADDRESS_OPTION = + new DriverOption() { + @NonNull + @Override + public String getPath() { + return ADDRESS_TRANSLATOR_DEFAULT_ADDRESS; + } + }; + + private static final String DELIMITER = ":"; + private static final int DEFAULT_PORT = 9042; + + private final List subnetAddresses; + private final Optional defaultAddress; + private final String logPrefix; + + public SubnetAddressTranslator(@NonNull DriverContext context) { + logPrefix = context.getSessionName(); + this.subnetAddresses = + context.getConfig().getDefaultProfile() + .getStringList(ADDRESS_TRANSLATOR_SUBNET_ADDRESSES_OPTION).stream() + .map(SubnetAddress::fromString) + .collect(Collectors.toList()); + this.defaultAddress = + Optional.ofNullable( + context + .getConfig() + .getDefaultProfile() + .getString(ADDRESS_TRANSLATOR_DEFAULT_ADDRESS_OPTION, null)) + .map(SubnetAddressTranslator::parseAddress); + SubnetAddressTranslator.validateSubnetsAreNotOverlapping(this.subnetAddresses); + } + + @NonNull + @Override + public InetSocketAddress translate(@NonNull InetSocketAddress address) { + InetSocketAddress translatedAddress = null; + for (SubnetAddress subnetAddress : subnetAddresses) { + if (subnetAddress.contains(address)) { + translatedAddress = subnetAddress.address; + } + } + if (translatedAddress == null && defaultAddress.isPresent()) { + translatedAddress = defaultAddress.get(); + } + if (translatedAddress == null) { + translatedAddress = address; + } + LOG.debug("[{}] Resolved {} to {}", logPrefix, address, translatedAddress); + return translatedAddress; + } + + @Override + public void close() {} + + private static InetSocketAddress parseAddress(String address) { + String[] addressTuple = address.split(DELIMITER); + if (addressTuple.length == 2) { + return new InetSocketAddress(addressTuple[0], Integer.parseInt(addressTuple[1])); + } + if (addressTuple.length == 1) { + return new InetSocketAddress(addressTuple[0], DEFAULT_PORT); + } + throw new IllegalArgumentException("Invalid default address: " + address); + } + + private static void validateSubnetsAreNotOverlapping(List subnetAddresses) { + for (int i = 0; i < subnetAddresses.size() - 1; i++) { + for (int j = i + 1; j < subnetAddresses.size(); j++) { + SubnetAddress subnetAddress1 = subnetAddresses.get(i); + SubnetAddress subnetAddress2 = subnetAddresses.get(j); + if (subnetAddress1.isOverlapping(subnetAddress2)) { + throw new IllegalArgumentException( + String.format( + "Configured subnets are overlapping: %s, %s", subnetAddress1, subnetAddress2)); + } + } + } + } + + private static class SubnetAddress { + private final SubnetUtils subnet; + private final InetSocketAddress address; + + private SubnetAddress(String subnet, String hostName) { + this(subnet, hostName, DEFAULT_PORT); + } + + private SubnetAddress(String subnet, String hostName, int port) { + this.subnet = new SubnetUtils(subnet); + this.address = new InetSocketAddress(hostName, port); + } + + private boolean isOverlapping(SubnetAddress other) { + SubnetUtils.SubnetInfo thisSubnet = this.subnet.getInfo(); + SubnetUtils.SubnetInfo otherSubnet = other.subnet.getInfo(); + return thisSubnet.isInRange(otherSubnet.getLowAddress()) + || thisSubnet.isInRange(otherSubnet.getHighAddress()) + || otherSubnet.isInRange(thisSubnet.getLowAddress()) + || otherSubnet.isInRange(thisSubnet.getHighAddress()); + } + + private boolean contains(InetSocketAddress address) { + return subnet.getInfo().isInRange(address.getAddress().getHostAddress()); + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + if (other == null || getClass() != other.getClass()) return false; + SubnetAddress that = (SubnetAddress) other; + return Objects.equals(subnet, that.subnet) && Objects.equals(address, that.address); + } + + @Override + public int hashCode() { + return Objects.hash(subnet, address); + } + + @Override + public String toString() { + return String.join( + DELIMITER, + subnet.getInfo().getCidrSignature(), + address.getHostName(), + String.valueOf(address.getPort())); + } + + private static SubnetAddress fromString(String subnetAddress) { + String[] subnetAddressTuple = subnetAddress.split(DELIMITER); + if (subnetAddressTuple.length == 3) { + return new SubnetAddress( + subnetAddressTuple[0], subnetAddressTuple[1], Integer.parseInt(subnetAddressTuple[2])); + } else if (subnetAddressTuple.length == 2) { + return new SubnetAddress(subnetAddressTuple[0], subnetAddressTuple[1]); + } + throw new IllegalArgumentException( + "Subnet must be in format `::`, for example: `100.64.0.0/15:cassandra.datacenter1.com:9042`"); + } + } +} diff --git a/core/src/main/resources/reference.conf b/core/src/main/resources/reference.conf index f09ffd18a10..1dd9f3a977e 100644 --- a/core/src/main/resources/reference.conf +++ b/core/src/main/resources/reference.conf @@ -1026,8 +1026,9 @@ datastax-java-driver { # the package com.datastax.oss.driver.internal.core.addresstranslation. # # The driver provides the following implementations out of the box: - # - PassThroughAddressTranslator: returns all addresses unchanged + # - PassThroughAddressTranslator: returns all addresses unchanged. # - FixedHostNameAddressTranslator: translates all addresses to a specific hostname. + # - SubnetAddressTranslator: translates addresses to hostname based on the subnet matches. # - Ec2MultiRegionAddressTranslator: suitable for an Amazon multi-region EC2 deployment where # clients are also deployed in EC2. It optimizes network costs by favoring private IPs over # public ones whenever possible. diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/addresstranslation/SubnetAddressTranslatorTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/addresstranslation/SubnetAddressTranslatorTest.java new file mode 100644 index 00000000000..a0acb45914f --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/addresstranslation/SubnetAddressTranslatorTest.java @@ -0,0 +1,130 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.addresstranslation; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.internal.core.context.DefaultDriverContext; +import com.datastax.oss.driver.internal.core.context.MockedDriverContextFactory; +import java.net.InetSocketAddress; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import org.junit.Test; + +public class SubnetAddressTranslatorTest { + + @Test + public void should_translate_to_correct_subnet_address() { + List subnetAddresses = + listOf( + "100.64.0.0/15:cassandra.datacenter1.com:19042", + "100.66.0.0/15:cassandra.datacenter2.com:19042"); + DefaultDriverContext context = context(subnetAddresses); + SubnetAddressTranslator translator = new SubnetAddressTranslator(context); + InetSocketAddress address = new InetSocketAddress("100.64.0.1", 9042); + assertThat(translator.translate(address)) + .isEqualTo(new InetSocketAddress("cassandra.datacenter1.com", 19042)); + } + + @Test + public void should_translate_to_default_address() { + List subnetAddresses = + listOf( + "100.64.0.0/15:cassandra.datacenter1.com:19042", + "100.66.0.0/15:cassandra.datacenter2.com:19042"); + DefaultDriverContext context = context(subnetAddresses); + when(context + .getConfig() + .getDefaultProfile() + .getString(SubnetAddressTranslator.ADDRESS_TRANSLATOR_DEFAULT_ADDRESS_OPTION, null)) + .thenReturn("cassandra.com:19042"); + SubnetAddressTranslator translator = new SubnetAddressTranslator(context); + InetSocketAddress address = new InetSocketAddress("100.68.0.1", 9042); + assertThat(translator.translate(address)) + .isEqualTo(new InetSocketAddress("cassandra.com", 19042)); + } + + @Test + public void should_pass_through_not_matched_address() { + List subnetAddresses = + listOf( + "100.64.0.0/15:cassandra.datacenter1.com:19042", + "100.66.0.0/15:cassandra.datacenter2.com:19042"); + DefaultDriverContext context = context(subnetAddresses); + SubnetAddressTranslator translator = new SubnetAddressTranslator(context); + InetSocketAddress address = new InetSocketAddress("100.68.0.1", 9042); + assertThat(translator.translate(address)).isEqualTo(address); + } + + @Test + public void should_fail_on_intersecting_subnets() { + List subnetAddresses = + listOf( + "100.64.0.0/15:cassandra.datacenter1.com:19042", + "100.65.0.0/15:cassandra.datacenter2.com:19042"); + DefaultDriverContext context = context(subnetAddresses); + assertThatIllegalArgumentException() + .isThrownBy(() -> new SubnetAddressTranslator(context)) + .withMessage( + String.format( + "Configured subnets are overlapping: %s, %s", + subnetAddresses.get(0), subnetAddresses.get(1))); + } + + @Test + public void should_parse_subnet_with_default_port() { + List subnetAddresses = listOf("100.64.0.0/15:cassandra.datacenter1.com"); + DefaultDriverContext context = context(subnetAddresses); + SubnetAddressTranslator translator = new SubnetAddressTranslator(context); + InetSocketAddress address = new InetSocketAddress("100.64.0.1", 19042); + assertThat(translator.translate(address)) + .isEqualTo(new InetSocketAddress("cassandra.datacenter1.com", 9042)); + } + + @Test + public void should_parse_default_address_with_default_port() { + List subnetAddresses = listOf("100.64.0.0/15:cassandra.datacenter1.com"); + DefaultDriverContext context = context(subnetAddresses); + when(context + .getConfig() + .getDefaultProfile() + .getString(SubnetAddressTranslator.ADDRESS_TRANSLATOR_DEFAULT_ADDRESS_OPTION, null)) + .thenReturn("cassandra.com"); + SubnetAddressTranslator translator = new SubnetAddressTranslator(context); + InetSocketAddress address = new InetSocketAddress("100.68.0.1", 19042); + assertThat(translator.translate(address)) + .isEqualTo(new InetSocketAddress("cassandra.com", 9042)); + } + + private static DefaultDriverContext context(List subnetAddresses) { + DriverExecutionProfile profile = mock(DriverExecutionProfile.class); + when(profile.getStringList(SubnetAddressTranslator.ADDRESS_TRANSLATOR_SUBNET_ADDRESSES_OPTION)) + .thenReturn(subnetAddresses); + return MockedDriverContextFactory.defaultDriverContext(Optional.of(profile)); + } + + private static List listOf(T... items) { + return Arrays.stream(items).collect(Collectors.toList()); + } +} diff --git a/pom.xml b/pom.xml index 3fd2d1347c2..4853217d67c 100644 --- a/pom.xml +++ b/pom.xml @@ -56,6 +56,7 @@ 1.4.1 2.1.12 + 3.11.1 4.1.18 4.1.94.Final 1.2.1 @@ -157,6 +158,11 @@ HdrHistogram ${hdrhistogram.version} + + commons-net + commons-net + ${commons-net.version} + com.esri.geometry esri-geometry-api From d4fae06de0261cc3141d9182d02103edf89ec591 Mon Sep 17 00:00:00 2001 From: alexsa Date: Wed, 12 Feb 2025 13:26:13 +0100 Subject: [PATCH 2/9] Switch lib commons-net to ipaddress and make it optional --- core/pom.xml | 5 +- .../SubnetAddressTranslator.java | 118 ++++++++------- .../SubnetAddressTranslatorTest.java | 143 ++++++++++++------ .../internal/core/config/MockOptions.java | 1 + .../typesafe/TypesafeDriverConfigTest.java | 14 +- pom.xml | 8 +- 6 files changed, 181 insertions(+), 108 deletions(-) diff --git a/core/pom.xml b/core/pom.xml index 0e9200576b5..2167b1cabc0 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -117,8 +117,9 @@ reactive-streams - commons-net - commons-net + com.github.seancfoley + ipaddress + true com.github.stephenc.jcip diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/addresstranslation/SubnetAddressTranslator.java b/core/src/main/java/com/datastax/oss/driver/internal/core/addresstranslation/SubnetAddressTranslator.java index ce05c9b69cb..cfb12f934df 100644 --- a/core/src/main/java/com/datastax/oss/driver/internal/core/addresstranslation/SubnetAddressTranslator.java +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/addresstranslation/SubnetAddressTranslator.java @@ -20,13 +20,15 @@ import com.datastax.oss.driver.api.core.addresstranslation.AddressTranslator; import com.datastax.oss.driver.api.core.config.DriverOption; import com.datastax.oss.driver.api.core.context.DriverContext; +import com.google.common.base.Splitter; import edu.umd.cs.findbugs.annotations.NonNull; +import inet.ipaddr.IPAddress; +import inet.ipaddr.IPAddressString; import java.net.InetSocketAddress; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; -import org.apache.commons.net.util.SubnetUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -45,16 +47,29 @@ public class SubnetAddressTranslator implements AddressTranslator { private static final Logger LOG = LoggerFactory.getLogger(SubnetAddressTranslator.class); /** - * A comma separated list with Cassandra node subnets (CIDR notations) to target addresses in a - * format `::`, for example: - * `100.64.0.0/15:cassandra.datacenter1.com:9042,100.66.0.0/15:cassandra.datacenter1.com:9042`. If - * configured without port, the default 9042 will be used. + * A map of Cassandra node subnets (CIDR notations) to target addresses, for example (note quoted + * keys): + * + *

+   * advanced.address-translator.subnet-addresses {
+   *   "100.64.0.0/15" = "cassandra.datacenter1.com:9042"
+   *   "100.66.0.0/15" = "cassandra.datacenter2.com"
+   *   # IPv6 example:
+   *   # "::ffff:6440:0/111" = "cassandra.datacenter1.com:9042"
+   *   # "::ffff:6442:0/111" = "cassandra.datacenter2.com"
+   * }
+   * 
+ * + * If configured without port, the default 9042 will be used. Also supports IPv6 subnets. Note: + * subnets must be represented as prefix blocks, see {@link inet.ipaddr.Address#isPrefixBlock()}. */ public static final String ADDRESS_TRANSLATOR_SUBNET_ADDRESSES = "advanced.address-translator.subnet-addresses"; + /** * A default address to fallback to if Cassandra node IP isn't contained in any of the configured - * subnets. If configured without port, the default 9042 will be used. + * subnets. If configured without port, the default 9042 will be used. Also supports IPv6 + * addresses. */ public static final String ADDRESS_TRANSLATOR_DEFAULT_ADDRESS = "advanced.address-translator.default-address"; @@ -67,6 +82,7 @@ public String getPath() { return ADDRESS_TRANSLATOR_SUBNET_ADDRESSES; } }; + public static DriverOption ADDRESS_TRANSLATOR_DEFAULT_ADDRESS_OPTION = new DriverOption() { @NonNull @@ -87,8 +103,16 @@ public SubnetAddressTranslator(@NonNull DriverContext context) { logPrefix = context.getSessionName(); this.subnetAddresses = context.getConfig().getDefaultProfile() - .getStringList(ADDRESS_TRANSLATOR_SUBNET_ADDRESSES_OPTION).stream() - .map(SubnetAddress::fromString) + .getStringMap(ADDRESS_TRANSLATOR_SUBNET_ADDRESSES_OPTION).entrySet().stream() + .map( + e -> { + // Quoted and/or containing forward slashes map keys in reference.conf are read to + // strings with additional quotes, eg. 100.64.0.0/15 -> '100.64.0."0/15"' or + // "100.64.0.0/15" -> '"100.64.0.0/15"' + String subnet = e.getKey().replaceAll("\"", ""); + String address = e.getValue(); + return new SubnetAddress(subnet, address); + }) .collect(Collectors.toList()); this.defaultAddress = Optional.ofNullable( @@ -123,12 +147,12 @@ public InetSocketAddress translate(@NonNull InetSocketAddress address) { public void close() {} private static InetSocketAddress parseAddress(String address) { - String[] addressTuple = address.split(DELIMITER); - if (addressTuple.length == 2) { - return new InetSocketAddress(addressTuple[0], Integer.parseInt(addressTuple[1])); + List addressTuple = Splitter.onPattern(DELIMITER).splitToList(address); + if (addressTuple.size() == 2) { + return new InetSocketAddress(addressTuple.get(0), Integer.parseInt(addressTuple.get(1))); } - if (addressTuple.length == 1) { - return new InetSocketAddress(addressTuple[0], DEFAULT_PORT); + if (addressTuple.size() == 1) { + return new InetSocketAddress(addressTuple.get(0), DEFAULT_PORT); } throw new IllegalArgumentException("Invalid default address: " + address); } @@ -141,42 +165,55 @@ private static void validateSubnetsAreNotOverlapping(List subnetA if (subnetAddress1.isOverlapping(subnetAddress2)) { throw new IllegalArgumentException( String.format( - "Configured subnets are overlapping: %s, %s", subnetAddress1, subnetAddress2)); + "Configured subnets are overlapping: %s, %s", + subnetAddress1.subnet, subnetAddress2.subnet)); } } } } private static class SubnetAddress { - private final SubnetUtils subnet; + private final IPAddress subnet; private final InetSocketAddress address; - private SubnetAddress(String subnet, String hostName) { - this(subnet, hostName, DEFAULT_PORT); - } - - private SubnetAddress(String subnet, String hostName, int port) { - this.subnet = new SubnetUtils(subnet); - this.address = new InetSocketAddress(hostName, port); + private SubnetAddress(String subnet, String address) { + IPAddress subnetIpAddress = new IPAddressString(subnet).getAddress(); + if (subnetIpAddress == null) { + throw new IllegalArgumentException("Invalid subnet: " + subnet); + } + if (!subnetIpAddress.isPrefixBlock()) { + throw new IllegalArgumentException( + String.format( + "Subnet %s must be represented as a network prefix block %s", + subnetIpAddress, subnetIpAddress.toPrefixBlock())); + } + this.subnet = subnetIpAddress; + this.address = parseAddress(address); } private boolean isOverlapping(SubnetAddress other) { - SubnetUtils.SubnetInfo thisSubnet = this.subnet.getInfo(); - SubnetUtils.SubnetInfo otherSubnet = other.subnet.getInfo(); - return thisSubnet.isInRange(otherSubnet.getLowAddress()) - || thisSubnet.isInRange(otherSubnet.getHighAddress()) - || otherSubnet.isInRange(thisSubnet.getLowAddress()) - || otherSubnet.isInRange(thisSubnet.getHighAddress()); + IPAddress thisSubnet = this.subnet; + IPAddress otherSubnet = other.subnet; + return thisSubnet.contains(otherSubnet.getLower()) + || thisSubnet.contains(otherSubnet.getUpper()) + || otherSubnet.contains(thisSubnet.getLower()) + || otherSubnet.contains(thisSubnet.getUpper()); } private boolean contains(InetSocketAddress address) { - return subnet.getInfo().isInRange(address.getAddress().getHostAddress()); + IPAddress ipAddress = new IPAddressString(address.getAddress().getHostAddress()).getAddress(); + if (subnet.isIPv4() && ipAddress.isIPv4Convertible()) { + ipAddress = ipAddress.toIPv4(); + } else if (subnet.isIPv6() && ipAddress.isIPv6Convertible()) { + ipAddress = ipAddress.toIPv6(); + } + return subnet.contains(ipAddress); } @Override public boolean equals(Object other) { if (this == other) return true; - if (other == null || getClass() != other.getClass()) return false; + if (!(other instanceof SubnetAddress)) return false; SubnetAddress that = (SubnetAddress) other; return Objects.equals(subnet, that.subnet) && Objects.equals(address, that.address); } @@ -185,26 +222,5 @@ public boolean equals(Object other) { public int hashCode() { return Objects.hash(subnet, address); } - - @Override - public String toString() { - return String.join( - DELIMITER, - subnet.getInfo().getCidrSignature(), - address.getHostName(), - String.valueOf(address.getPort())); - } - - private static SubnetAddress fromString(String subnetAddress) { - String[] subnetAddressTuple = subnetAddress.split(DELIMITER); - if (subnetAddressTuple.length == 3) { - return new SubnetAddress( - subnetAddressTuple[0], subnetAddressTuple[1], Integer.parseInt(subnetAddressTuple[2])); - } else if (subnetAddressTuple.length == 2) { - return new SubnetAddress(subnetAddressTuple[0], subnetAddressTuple[1]); - } - throw new IllegalArgumentException( - "Subnet must be in format `::`, for example: `100.64.0.0/15:cassandra.datacenter1.com:9042`"); - } } } diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/addresstranslation/SubnetAddressTranslatorTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/addresstranslation/SubnetAddressTranslatorTest.java index a0acb45914f..9710c9fe648 100644 --- a/core/src/test/java/com/datastax/oss/driver/internal/core/addresstranslation/SubnetAddressTranslatorTest.java +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/addresstranslation/SubnetAddressTranslatorTest.java @@ -25,21 +25,20 @@ import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; import com.datastax.oss.driver.internal.core.context.DefaultDriverContext; import com.datastax.oss.driver.internal.core.context.MockedDriverContextFactory; +import com.google.common.collect.ImmutableMap; import java.net.InetSocketAddress; -import java.util.Arrays; -import java.util.List; +import java.util.Map; import java.util.Optional; -import java.util.stream.Collectors; import org.junit.Test; public class SubnetAddressTranslatorTest { @Test - public void should_translate_to_correct_subnet_address() { - List subnetAddresses = - listOf( - "100.64.0.0/15:cassandra.datacenter1.com:19042", - "100.66.0.0/15:cassandra.datacenter2.com:19042"); + public void should_translate_to_correct_subnet_address_ipv4() { + Map subnetAddresses = + ImmutableMap.of( + "\"100.64.0.0/15\"", "cassandra.datacenter1.com:19042", + "100.66.0.\"0/15\"", "cassandra.datacenter2.com:19042"); DefaultDriverContext context = context(subnetAddresses); SubnetAddressTranslator translator = new SubnetAddressTranslator(context); InetSocketAddress address = new InetSocketAddress("100.64.0.1", 9042); @@ -47,13 +46,45 @@ public void should_translate_to_correct_subnet_address() { .isEqualTo(new InetSocketAddress("cassandra.datacenter1.com", 19042)); } + // TODO (jahstreet): Add IPv6 test, check how to parse it with and without ports. @Test - public void should_translate_to_default_address() { - List subnetAddresses = - listOf( - "100.64.0.0/15:cassandra.datacenter1.com:19042", - "100.66.0.0/15:cassandra.datacenter2.com:19042"); + public void should_translate_to_correct_subnet_address_with_default_port_ipv4() { + Map subnetAddresses = + ImmutableMap.of("\"100.64.0.0/15\"", "cassandra.datacenter1.com"); + DefaultDriverContext context = context(subnetAddresses); + SubnetAddressTranslator translator = new SubnetAddressTranslator(context); + InetSocketAddress address = new InetSocketAddress("100.64.0.1", 19042); + assertThat(translator.translate(address)) + .isEqualTo(new InetSocketAddress("cassandra.datacenter1.com", 9042)); + } + + @Test + public void should_translate_to_correct_subnet_address_ipv6() { + Map subnetAddresses = + ImmutableMap.of( + "\"::ffff:6440:0/111\"", "cassandra.datacenter1.com:19042", + "\"::ffff:6442:0/111\"", "cassandra.datacenter2.com:19042"); DefaultDriverContext context = context(subnetAddresses); + SubnetAddressTranslator translator = new SubnetAddressTranslator(context); + InetSocketAddress address = new InetSocketAddress("::ffff:6440:1", 9042); + assertThat(translator.translate(address)) + .isEqualTo(new InetSocketAddress("cassandra.datacenter1.com", 19042)); + } + + @Test + public void should_translate_to_correct_subnet_address_with_default_port_ipv6() { + Map subnetAddresses = + ImmutableMap.of("\"::ffff:6440:0/111\"", "cassandra.datacenter1.com"); + DefaultDriverContext context = context(subnetAddresses); + SubnetAddressTranslator translator = new SubnetAddressTranslator(context); + InetSocketAddress address = new InetSocketAddress("100.64.0.1", 19042); + assertThat(translator.translate(address)) + .isEqualTo(new InetSocketAddress("cassandra.datacenter1.com", 9042)); + } + + @Test + public void should_translate_to_default_address() { + DefaultDriverContext context = context(ImmutableMap.of()); when(context .getConfig() .getDefaultProfile() @@ -65,66 +96,78 @@ public void should_translate_to_default_address() { .isEqualTo(new InetSocketAddress("cassandra.com", 19042)); } + @Test + public void should_translate_to_default_address_with_default_port() { + DefaultDriverContext context = context(ImmutableMap.of()); + when(context + .getConfig() + .getDefaultProfile() + .getString(SubnetAddressTranslator.ADDRESS_TRANSLATOR_DEFAULT_ADDRESS_OPTION, null)) + .thenReturn("cassandra.com"); + SubnetAddressTranslator translator = new SubnetAddressTranslator(context); + InetSocketAddress address = new InetSocketAddress("100.68.0.1", 19042); + assertThat(translator.translate(address)) + .isEqualTo(new InetSocketAddress("cassandra.com", 9042)); + } + @Test public void should_pass_through_not_matched_address() { - List subnetAddresses = - listOf( - "100.64.0.0/15:cassandra.datacenter1.com:19042", - "100.66.0.0/15:cassandra.datacenter2.com:19042"); - DefaultDriverContext context = context(subnetAddresses); + DefaultDriverContext context = context(ImmutableMap.of()); SubnetAddressTranslator translator = new SubnetAddressTranslator(context); InetSocketAddress address = new InetSocketAddress("100.68.0.1", 9042); assertThat(translator.translate(address)).isEqualTo(address); } @Test - public void should_fail_on_intersecting_subnets() { - List subnetAddresses = - listOf( - "100.64.0.0/15:cassandra.datacenter1.com:19042", - "100.65.0.0/15:cassandra.datacenter2.com:19042"); + public void should_fail_on_intersecting_subnets_ipv4() { + Map subnetAddresses = + ImmutableMap.of( + "\"100.64.0.0/15\"", "cassandra.datacenter1.com:19042", + "100.65.0.\"0/16\"", "cassandra.datacenter2.com:19042"); DefaultDriverContext context = context(subnetAddresses); assertThatIllegalArgumentException() .isThrownBy(() -> new SubnetAddressTranslator(context)) - .withMessage( - String.format( - "Configured subnets are overlapping: %s, %s", - subnetAddresses.get(0), subnetAddresses.get(1))); + .withMessage("Configured subnets are overlapping: 100.64.0.0/15, 100.65.0.0/16"); } @Test - public void should_parse_subnet_with_default_port() { - List subnetAddresses = listOf("100.64.0.0/15:cassandra.datacenter1.com"); + public void should_fail_on_intersecting_subnets_ipv6() { + Map subnetAddresses = + ImmutableMap.of( + "\"::ffff:6440:0/111\"", "cassandra.datacenter1.com:19042", + "\"::ffff:6441:0/112\"", "cassandra.datacenter2.com:19042"); DefaultDriverContext context = context(subnetAddresses); - SubnetAddressTranslator translator = new SubnetAddressTranslator(context); - InetSocketAddress address = new InetSocketAddress("100.64.0.1", 19042); - assertThat(translator.translate(address)) - .isEqualTo(new InetSocketAddress("cassandra.datacenter1.com", 9042)); + assertThatIllegalArgumentException() + .isThrownBy(() -> new SubnetAddressTranslator(context)) + .withMessage("Configured subnets are overlapping: ::ffff:6440:0/111, ::ffff:6441:0/112"); } @Test - public void should_parse_default_address_with_default_port() { - List subnetAddresses = listOf("100.64.0.0/15:cassandra.datacenter1.com"); + public void should_fail_on_not_prefix_block_subnet_ipv4() { + Map subnetAddresses = + ImmutableMap.of("\"100.65.0.0/15\"", "cassandra.datacenter1.com:19042"); DefaultDriverContext context = context(subnetAddresses); - when(context - .getConfig() - .getDefaultProfile() - .getString(SubnetAddressTranslator.ADDRESS_TRANSLATOR_DEFAULT_ADDRESS_OPTION, null)) - .thenReturn("cassandra.com"); - SubnetAddressTranslator translator = new SubnetAddressTranslator(context); - InetSocketAddress address = new InetSocketAddress("100.68.0.1", 19042); - assertThat(translator.translate(address)) - .isEqualTo(new InetSocketAddress("cassandra.com", 9042)); + assertThatIllegalArgumentException() + .isThrownBy(() -> new SubnetAddressTranslator(context)) + .withMessage( + "Subnet 100.65.0.0/15 must be represented as a network prefix block 100.64.0.0/15"); + } + + @Test + public void should_fail_on_not_prefix_block_subnet_ipv6() { + Map subnetAddresses = + ImmutableMap.of("\"::ffff:6441:0/111\"", "cassandra.datacenter1.com:19042"); + DefaultDriverContext context = context(subnetAddresses); + assertThatIllegalArgumentException() + .isThrownBy(() -> new SubnetAddressTranslator(context)) + .withMessage( + "Subnet ::ffff:6441:0/111 must be represented as a network prefix block ::ffff:6440:0/111"); } - private static DefaultDriverContext context(List subnetAddresses) { + private static DefaultDriverContext context(Map subnetAddresses) { DriverExecutionProfile profile = mock(DriverExecutionProfile.class); - when(profile.getStringList(SubnetAddressTranslator.ADDRESS_TRANSLATOR_SUBNET_ADDRESSES_OPTION)) + when(profile.getStringMap(SubnetAddressTranslator.ADDRESS_TRANSLATOR_SUBNET_ADDRESSES_OPTION)) .thenReturn(subnetAddresses); return MockedDriverContextFactory.defaultDriverContext(Optional.of(profile)); } - - private static List listOf(T... items) { - return Arrays.stream(items).collect(Collectors.toList()); - } } diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/config/MockOptions.java b/core/src/test/java/com/datastax/oss/driver/internal/core/config/MockOptions.java index 25c1e8b26fd..cee57abbfdf 100644 --- a/core/src/test/java/com/datastax/oss/driver/internal/core/config/MockOptions.java +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/config/MockOptions.java @@ -24,6 +24,7 @@ public enum MockOptions implements DriverOption { INT1("int1"), INT2("int2"), AUTH_PROVIDER("auth_provider"), + SUBNET_ADDRESSES("subnet_addresses"), ; private final String path; diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/config/typesafe/TypesafeDriverConfigTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/config/typesafe/TypesafeDriverConfigTest.java index 16ccb73da9f..4a78c3ccb03 100644 --- a/core/src/test/java/com/datastax/oss/driver/internal/core/config/typesafe/TypesafeDriverConfigTest.java +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/config/typesafe/TypesafeDriverConfigTest.java @@ -101,7 +101,6 @@ public void should_fetch_string_map() { parse( "int1 = 42 \n auth_provider { auth_thing_one= one \n auth_thing_two = two \n auth_thing_three = three}"); DriverExecutionProfile base = config.getDefaultProfile(); - base.getStringMap(MockOptions.AUTH_PROVIDER); Map map = base.getStringMap(MockOptions.AUTH_PROVIDER); assertThat(map.entrySet().size()).isEqualTo(3); assertThat(map.get("auth_thing_one")).isEqualTo("one"); @@ -109,6 +108,19 @@ public void should_fetch_string_map() { assertThat(map.get("auth_thing_three")).isEqualTo("three"); } + @Test + public void should_fetch_string_map_with_forward_slash_in_keys() { + TypesafeDriverConfig config = + parse( + "subnet_addresses { 100.64.0.0/15 = \"cassandra.datacenter1.com:9042\" \n \"100.66.0.0/15\" = \"cassandra.datacenter2.com\" \n \"::ffff:6440:0/111\" = \"cassandra.datacenter3.com:19042\" }"); + DriverExecutionProfile base = config.getDefaultProfile(); + Map map = base.getStringMap(MockOptions.SUBNET_ADDRESSES); + assertThat(map.entrySet().size()).isEqualTo(3); + assertThat(map.get("100.64.0.\"0/15\"")).isEqualTo("cassandra.datacenter1.com:9042"); + assertThat(map.get("\"100.66.0.0/15\"")).isEqualTo("cassandra.datacenter2.com"); + assertThat(map.get("\"::ffff:6440:0/111\"")).isEqualTo("cassandra.datacenter3.com:19042"); + } + @Test public void should_create_derived_profile_with_string_map() { TypesafeDriverConfig config = parse("int1 = 42"); diff --git a/pom.xml b/pom.xml index 4853217d67c..1b8cf3a5a74 100644 --- a/pom.xml +++ b/pom.xml @@ -56,7 +56,6 @@ 1.4.1 2.1.12 - 3.11.1 4.1.18 4.1.94.Final 1.2.1 @@ -75,6 +74,7 @@ 1.1.10.1 1.7.1 + 5.5.1 3.19.0 1.3 @@ -159,9 +159,9 @@ ${hdrhistogram.version}
- commons-net - commons-net - ${commons-net.version} + com.github.seancfoley + ipaddress + ${ipaddress.version} com.esri.geometry From b6bc879a2b10ec829dada7f9584a8a20e08864d4 Mon Sep 17 00:00:00 2001 From: alexsa Date: Wed, 12 Feb 2025 13:26:21 +0100 Subject: [PATCH 3/9] Update docs --- core/src/main/resources/reference.conf | 12 +++++++ manual/core/address_resolution/README.md | 46 ++++++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/core/src/main/resources/reference.conf b/core/src/main/resources/reference.conf index 1dd9f3a977e..7bebd2063e9 100644 --- a/core/src/main/resources/reference.conf +++ b/core/src/main/resources/reference.conf @@ -1036,8 +1036,20 @@ datastax-java-driver { # You can also specify a custom class that implements AddressTranslator and has a public # constructor with a DriverContext argument. class = PassThroughAddressTranslator + # # This property has to be set only in case you use FixedHostNameAddressTranslator. # advertised-hostname = mycustomhostname + # + # Theses properties have to be set only in case you use SubnetAddressTranslator. + # subnet-addresses { + # "100.64.0.0/15" = "cassandra.datacenter1.com:9042" + # "100.66.0.0/15" = "cassandra.datacenter2.com" # port defaults to 9042 if not specified + # # IPv6 example: + # # "::ffff:6440:0/111" = "cassandra.datacenter1.com:9042" + # # "::ffff:6442:0/111" = "cassandra.datacenter2.com" # port defaults to 9042 if not specified + # } + # Optional. When configured, addresses not matching the configured subnets are translated to it. Port defaults to 9042 if not specified. + # default-address = "cassandra.datacenter1.com:9042" } # Whether to resolve the addresses passed to `basic.contact-points`. diff --git a/manual/core/address_resolution/README.md b/manual/core/address_resolution/README.md index 84efb4a796c..335b4cb0d77 100644 --- a/manual/core/address_resolution/README.md +++ b/manual/core/address_resolution/README.md @@ -118,6 +118,52 @@ datastax-java-driver.advanced.address-translator.class = com.mycompany.MyAddress Note: the contact points provided while creating the `CqlSession` are not translated, only addresses retrieved from or sent by Cassandra nodes are. +### Fixed proxy hostname + +If your client applications access Cassandra through some kind of proxy (eg. with AWS PrivateLink when all Cassandra +nodes are exposed via one hostname pointing to AWS Endpoint), you can configure driver with +`FixedHostNameAddressTranslator` to always translate all node addresses to that same proxy hostname, no matter what IP +address a node has but still using its native transport port. + +To use it, specify the following in the [configuration](../configuration): + +``` +datastax-java-driver.advanced.address-translator.class = FixedHostNameAddressTranslator +advertised-hostname = proxyhostname +``` + +### Fixed proxy hostname per subnet + +When running Cassandra in a private network and accessing it from outside of that private network via some kind of +proxy, we have an option to use `FixedHostNameAddressTranslator`. But for multi-datacenter Cassandra deployments, we +want to have more control over routing queries to a specific datacenter (eg. for optimizing latencies), which requires +setting up a separate proxy per datacenter. + +Normally, each Cassandra datacenter nodes are deployed to a separate subnet to support internode communications in the +cluster and avoid IP addresses collisions. So when Cassandra broadcasts its nodes IP addresses, we can determine which +datacenter that node belongs to by checking its IP address against the given datacenter subnet. + +For such scenarios you can use `SubnetAddressTranslator` to translate node IPs to the datacenter proxy address +associated with it. + +To use it, specify the following in the [configuration](../configuration): +``` +datastax-java-driver.advanced.address-translator { + class = SubnetAddressTranslator + subnet-addresses { + "100.64.0.0/15" = "cassandra.datacenter1.com:9042" + "100.66.0.0/15" = "cassandra.datacenter2.com" # port defaults to 9042 if not specified + # IPv6 example: + # "::ffff:6440:0/111" = "cassandra.datacenter1.com:9042" + # "::ffff:6442:0/111" = "cassandra.datacenter2.com" # port defaults to 9042 if not specified + } + # Optional. When configured, addresses not matching the configured subnets are translated to it. Port defaults to 9042 if not specified. + default-address = "cassandra.datacenter1.com:9042" +} +``` + +Such setup is common for running Cassandra on Kubernetes with [k8ssandra](https://docs.k8ssandra.io/). + ### EC2 multi-region If you deploy both Cassandra and client applications on Amazon EC2, and your cluster spans multiple regions, you'll have From 05f38bfc062124b9d00cee60bc602dc083f96f21 Mon Sep 17 00:00:00 2001 From: alexsa Date: Thu, 13 Feb 2025 11:53:15 +0100 Subject: [PATCH 4/9] Make ports required and move ContactPoints#extract to AddressUtils --- .../driver/internal/core/ContactPoints.java | 61 +++-------- .../SubnetAddressTranslator.java | 103 +++++++++--------- .../internal/core/util/AddressUtils.java | 59 ++++++++++ core/src/main/resources/reference.conf | 10 +- .../internal/core/ContactPointsTest.java | 4 +- .../SubnetAddressTranslatorTest.java | 68 +++++------- manual/core/address_resolution/README.md | 4 + 7 files changed, 166 insertions(+), 143 deletions(-) create mode 100644 core/src/main/java/com/datastax/oss/driver/internal/core/util/AddressUtils.java diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/ContactPoints.java b/core/src/main/java/com/datastax/oss/driver/internal/core/ContactPoints.java index 1ed2a1cebf3..d88b09e592d 100644 --- a/core/src/main/java/com/datastax/oss/driver/internal/core/ContactPoints.java +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/ContactPoints.java @@ -19,14 +19,11 @@ import com.datastax.oss.driver.api.core.metadata.EndPoint; import com.datastax.oss.driver.internal.core.metadata.DefaultEndPoint; +import com.datastax.oss.driver.internal.core.util.AddressUtils; import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableSet; import com.datastax.oss.driver.shaded.guava.common.collect.Sets; -import java.net.InetAddress; import java.net.InetSocketAddress; -import java.net.UnknownHostException; -import java.util.Arrays; import java.util.Collections; -import java.util.HashSet; import java.util.List; import java.util.Set; import org.slf4j.Logger; @@ -41,7 +38,22 @@ public static Set merge( Set result = Sets.newHashSet(programmaticContactPoints); for (String spec : configContactPoints) { - for (InetSocketAddress address : extract(spec, resolve)) { + + Set addresses = Collections.emptySet(); + try { + addresses = AddressUtils.extract(spec, resolve); + } catch (RuntimeException e) { + LOG.warn("Ignoring invalid contact point {} ({})", spec, e.getMessage()); + } + + if (addresses.size() > 1) { + LOG.info( + "Contact point {} resolves to multiple addresses, will use them all ({})", + spec, + addresses); + } + + for (InetSocketAddress address : addresses) { DefaultEndPoint endPoint = new DefaultEndPoint(address); boolean wasNew = result.add(endPoint); if (!wasNew) { @@ -51,43 +63,4 @@ public static Set merge( } return ImmutableSet.copyOf(result); } - - private static Set extract(String spec, boolean resolve) { - int separator = spec.lastIndexOf(':'); - if (separator < 0) { - LOG.warn("Ignoring invalid contact point {} (expecting host:port)", spec); - return Collections.emptySet(); - } - - String host = spec.substring(0, separator); - String portSpec = spec.substring(separator + 1); - int port; - try { - port = Integer.parseInt(portSpec); - } catch (NumberFormatException e) { - LOG.warn("Ignoring invalid contact point {} (expecting a number, got {})", spec, portSpec); - return Collections.emptySet(); - } - if (!resolve) { - return ImmutableSet.of(InetSocketAddress.createUnresolved(host, port)); - } else { - try { - InetAddress[] inetAddresses = InetAddress.getAllByName(host); - if (inetAddresses.length > 1) { - LOG.info( - "Contact point {} resolves to multiple addresses, will use them all ({})", - spec, - Arrays.deepToString(inetAddresses)); - } - Set result = new HashSet<>(); - for (InetAddress inetAddress : inetAddresses) { - result.add(new InetSocketAddress(inetAddress, port)); - } - return result; - } catch (UnknownHostException e) { - LOG.warn("Ignoring invalid contact point {} (unknown host {})", spec, host); - return Collections.emptySet(); - } - } - } } diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/addresstranslation/SubnetAddressTranslator.java b/core/src/main/java/com/datastax/oss/driver/internal/core/addresstranslation/SubnetAddressTranslator.java index cfb12f934df..0e1d9a4d9e8 100644 --- a/core/src/main/java/com/datastax/oss/driver/internal/core/addresstranslation/SubnetAddressTranslator.java +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/addresstranslation/SubnetAddressTranslator.java @@ -17,16 +17,18 @@ */ package com.datastax.oss.driver.internal.core.addresstranslation; +import static com.datastax.oss.driver.api.core.config.DefaultDriverOption.RESOLVE_CONTACT_POINTS; + import com.datastax.oss.driver.api.core.addresstranslation.AddressTranslator; import com.datastax.oss.driver.api.core.config.DriverOption; import com.datastax.oss.driver.api.core.context.DriverContext; -import com.google.common.base.Splitter; +import com.datastax.oss.driver.internal.core.util.AddressUtils; import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; import inet.ipaddr.IPAddress; import inet.ipaddr.IPAddressString; import java.net.InetSocketAddress; import java.util.List; -import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; import org.slf4j.Logger; @@ -43,7 +45,6 @@ * different Cassandra datacenters deployed to different Kubernetes clusters. */ public class SubnetAddressTranslator implements AddressTranslator { - private static final Logger LOG = LoggerFactory.getLogger(SubnetAddressTranslator.class); /** @@ -53,23 +54,22 @@ public class SubnetAddressTranslator implements AddressTranslator { *
    * advanced.address-translator.subnet-addresses {
    *   "100.64.0.0/15" = "cassandra.datacenter1.com:9042"
-   *   "100.66.0.0/15" = "cassandra.datacenter2.com"
+   *   "100.66.0.0/15" = "cassandra.datacenter2.com:9042"
    *   # IPv6 example:
    *   # "::ffff:6440:0/111" = "cassandra.datacenter1.com:9042"
-   *   # "::ffff:6442:0/111" = "cassandra.datacenter2.com"
+   *   # "::ffff:6442:0/111" = "cassandra.datacenter2.com:9042"
    * }
    * 
* - * If configured without port, the default 9042 will be used. Also supports IPv6 subnets. Note: - * subnets must be represented as prefix blocks, see {@link inet.ipaddr.Address#isPrefixBlock()}. + * Note: subnets must be represented as prefix blocks, see {@link + * inet.ipaddr.Address#isPrefixBlock()}. */ public static final String ADDRESS_TRANSLATOR_SUBNET_ADDRESSES = "advanced.address-translator.subnet-addresses"; /** * A default address to fallback to if Cassandra node IP isn't contained in any of the configured - * subnets. If configured without port, the default 9042 will be used. Also supports IPv6 - * addresses. + * subnets. */ public static final String ADDRESS_TRANSLATOR_DEFAULT_ADDRESS = "advanced.address-translator.default-address"; @@ -92,11 +92,9 @@ public String getPath() { } }; - private static final String DELIMITER = ":"; - private static final int DEFAULT_PORT = 9042; - private final List subnetAddresses; - private final Optional defaultAddress; + private final Optional defaultAddress; + private final boolean resolveAddresses; private final String logPrefix; public SubnetAddressTranslator(@NonNull DriverContext context) { @@ -109,19 +107,22 @@ public SubnetAddressTranslator(@NonNull DriverContext context) { // Quoted and/or containing forward slashes map keys in reference.conf are read to // strings with additional quotes, eg. 100.64.0.0/15 -> '100.64.0."0/15"' or // "100.64.0.0/15" -> '"100.64.0.0/15"' - String subnet = e.getKey().replaceAll("\"", ""); + String subnetCIDR = e.getKey().replaceAll("\"", ""); String address = e.getValue(); - return new SubnetAddress(subnet, address); + return new SubnetAddress(subnetCIDR, address); }) .collect(Collectors.toList()); this.defaultAddress = Optional.ofNullable( - context - .getConfig() - .getDefaultProfile() - .getString(ADDRESS_TRANSLATOR_DEFAULT_ADDRESS_OPTION, null)) - .map(SubnetAddressTranslator::parseAddress); - SubnetAddressTranslator.validateSubnetsAreNotOverlapping(this.subnetAddresses); + context + .getConfig() + .getDefaultProfile() + .getString(ADDRESS_TRANSLATOR_DEFAULT_ADDRESS_OPTION, null)); + this.resolveAddresses = + context.getConfig().getDefaultProfile().getBoolean(RESOLVE_CONTACT_POINTS, true); + + validateSubnetsAreNotOverlapping(this.subnetAddresses); + this.defaultAddress.ifPresent(SubnetAddressTranslator::validateAddress); } @NonNull @@ -130,11 +131,11 @@ public InetSocketAddress translate(@NonNull InetSocketAddress address) { InetSocketAddress translatedAddress = null; for (SubnetAddress subnetAddress : subnetAddresses) { if (subnetAddress.contains(address)) { - translatedAddress = subnetAddress.address; + translatedAddress = parseAddress(subnetAddress.address, resolveAddresses); } } if (translatedAddress == null && defaultAddress.isPresent()) { - translatedAddress = defaultAddress.get(); + translatedAddress = parseAddress(defaultAddress.get(), resolveAddresses); } if (translatedAddress == null) { translatedAddress = address; @@ -146,15 +147,17 @@ public InetSocketAddress translate(@NonNull InetSocketAddress address) { @Override public void close() {} - private static InetSocketAddress parseAddress(String address) { - List addressTuple = Splitter.onPattern(DELIMITER).splitToList(address); - if (addressTuple.size() == 2) { - return new InetSocketAddress(addressTuple.get(0), Integer.parseInt(addressTuple.get(1))); - } - if (addressTuple.size() == 1) { - return new InetSocketAddress(addressTuple.get(0), DEFAULT_PORT); + @Nullable + private static InetSocketAddress parseAddress(String address, boolean resolve) { + return AddressUtils.extract(address, resolve).iterator().next(); + } + + private static void validateAddress(String address) { + try { + parseAddress(address, false); + } catch (RuntimeException e) { + throw new IllegalArgumentException("Invalid address: " + address, e); } - throw new IllegalArgumentException("Invalid default address: " + address); } private static void validateSubnetsAreNotOverlapping(List subnetAddresses) { @@ -174,21 +177,28 @@ private static void validateSubnetsAreNotOverlapping(List subnetA private static class SubnetAddress { private final IPAddress subnet; - private final InetSocketAddress address; + private final String address; + + private SubnetAddress(String subnetCIDR, String address) { + this.subnet = parseSubnet(subnetCIDR); + this.address = address; - private SubnetAddress(String subnet, String address) { - IPAddress subnetIpAddress = new IPAddressString(subnet).getAddress(); - if (subnetIpAddress == null) { - throw new IllegalArgumentException("Invalid subnet: " + subnet); + validateAddress(this.address); + } + + private static IPAddress parseSubnet(String subnetCIDR) { + IPAddress subnet = new IPAddressString(subnetCIDR).getAddress(); + if (subnet == null) { + throw new IllegalArgumentException("Invalid subnet: " + subnetCIDR); } - if (!subnetIpAddress.isPrefixBlock()) { + if (!subnet.isPrefixBlock()) { + throw new IllegalArgumentException( String.format( "Subnet %s must be represented as a network prefix block %s", - subnetIpAddress, subnetIpAddress.toPrefixBlock())); + subnet, subnet.toPrefixBlock())); } - this.subnet = subnetIpAddress; - this.address = parseAddress(address); + return subnet; } private boolean isOverlapping(SubnetAddress other) { @@ -209,18 +219,5 @@ private boolean contains(InetSocketAddress address) { } return subnet.contains(ipAddress); } - - @Override - public boolean equals(Object other) { - if (this == other) return true; - if (!(other instanceof SubnetAddress)) return false; - SubnetAddress that = (SubnetAddress) other; - return Objects.equals(subnet, that.subnet) && Objects.equals(address, that.address); - } - - @Override - public int hashCode() { - return Objects.hash(subnet, address); - } } } diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/util/AddressUtils.java b/core/src/main/java/com/datastax/oss/driver/internal/core/util/AddressUtils.java new file mode 100644 index 00000000000..c087db29081 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/util/AddressUtils.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.util; + +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableSet; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.util.HashSet; +import java.util.Set; + +public class AddressUtils { + + public static Set extract(String address, boolean resolve) { + int separator = address.lastIndexOf(':'); + if (separator < 0) { + throw new IllegalArgumentException("expecting format host:port"); + } + + String host = address.substring(0, separator); + String portString = address.substring(separator + 1); + int port; + try { + port = Integer.parseInt(portString); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("expecting port to be a number, got " + portString, e); + } + if (!resolve) { + return ImmutableSet.of(InetSocketAddress.createUnresolved(host, port)); + } else { + InetAddress[] inetAddresses; + try { + inetAddresses = InetAddress.getAllByName(host); + } catch (UnknownHostException e) { + throw new RuntimeException("unknown host " + host, e); + } + Set result = new HashSet<>(); + for (InetAddress inetAddress : inetAddresses) { + result.add(new InetSocketAddress(inetAddress, port)); + } + return result; + } + } +} diff --git a/core/src/main/resources/reference.conf b/core/src/main/resources/reference.conf index 7bebd2063e9..4f8d652125f 100644 --- a/core/src/main/resources/reference.conf +++ b/core/src/main/resources/reference.conf @@ -1040,16 +1040,18 @@ datastax-java-driver { # This property has to be set only in case you use FixedHostNameAddressTranslator. # advertised-hostname = mycustomhostname # - # Theses properties have to be set only in case you use SubnetAddressTranslator. + # These properties are only applicable in case you use SubnetAddressTranslator. # subnet-addresses { # "100.64.0.0/15" = "cassandra.datacenter1.com:9042" - # "100.66.0.0/15" = "cassandra.datacenter2.com" # port defaults to 9042 if not specified + # "100.66.0.0/15" = "cassandra.datacenter2.com:9042" # # IPv6 example: # # "::ffff:6440:0/111" = "cassandra.datacenter1.com:9042" - # # "::ffff:6442:0/111" = "cassandra.datacenter2.com" # port defaults to 9042 if not specified + # # "::ffff:6442:0/111" = "cassandra.datacenter2.com:9042" # } - # Optional. When configured, addresses not matching the configured subnets are translated to it. Port defaults to 9042 if not specified. + # Optional. When configured, addresses not matching the configured subnets are translated this address. # default-address = "cassandra.datacenter1.com:9042" + # Note: `advanced.resolve-contact-points` (see below) is used to determine whether to resolve subnet + # and default address on translation. } # Whether to resolve the addresses passed to `basic.contact-points`. diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/ContactPointsTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/ContactPointsTest.java index 9e0d8737619..72b875b8602 100644 --- a/core/src/test/java/com/datastax/oss/driver/internal/core/ContactPointsTest.java +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/ContactPointsTest.java @@ -121,7 +121,7 @@ public void should_ignore_malformed_host_and_port_and_warn() { ContactPoints.merge(Collections.emptySet(), ImmutableList.of("foobar"), true); assertThat(endPoints).isEmpty(); - assertLog(Level.WARN, "Ignoring invalid contact point foobar (expecting host:port)"); + assertLog(Level.WARN, "Ignoring invalid contact point foobar (expecting format host:port)"); } @Test @@ -132,7 +132,7 @@ public void should_ignore_malformed_port_and_warn() { assertThat(endPoints).isEmpty(); assertLog( Level.WARN, - "Ignoring invalid contact point 127.0.0.1:foobar (expecting a number, got foobar)"); + "Ignoring invalid contact point 127.0.0.1:foobar (expecting port to be a number, got foobar)"); } @Test diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/addresstranslation/SubnetAddressTranslatorTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/addresstranslation/SubnetAddressTranslatorTest.java index 9710c9fe648..99ba9e6f2cb 100644 --- a/core/src/test/java/com/datastax/oss/driver/internal/core/addresstranslation/SubnetAddressTranslatorTest.java +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/addresstranslation/SubnetAddressTranslatorTest.java @@ -17,6 +17,7 @@ */ package com.datastax.oss.driver.internal.core.addresstranslation; +import static com.datastax.oss.driver.api.core.config.DefaultDriverOption.RESOLVE_CONTACT_POINTS; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.mockito.Mockito.mock; @@ -43,19 +44,7 @@ public void should_translate_to_correct_subnet_address_ipv4() { SubnetAddressTranslator translator = new SubnetAddressTranslator(context); InetSocketAddress address = new InetSocketAddress("100.64.0.1", 9042); assertThat(translator.translate(address)) - .isEqualTo(new InetSocketAddress("cassandra.datacenter1.com", 19042)); - } - - // TODO (jahstreet): Add IPv6 test, check how to parse it with and without ports. - @Test - public void should_translate_to_correct_subnet_address_with_default_port_ipv4() { - Map subnetAddresses = - ImmutableMap.of("\"100.64.0.0/15\"", "cassandra.datacenter1.com"); - DefaultDriverContext context = context(subnetAddresses); - SubnetAddressTranslator translator = new SubnetAddressTranslator(context); - InetSocketAddress address = new InetSocketAddress("100.64.0.1", 19042); - assertThat(translator.translate(address)) - .isEqualTo(new InetSocketAddress("cassandra.datacenter1.com", 9042)); + .isEqualTo(InetSocketAddress.createUnresolved("cassandra.datacenter1.com", 19042)); } @Test @@ -68,18 +57,7 @@ public void should_translate_to_correct_subnet_address_ipv6() { SubnetAddressTranslator translator = new SubnetAddressTranslator(context); InetSocketAddress address = new InetSocketAddress("::ffff:6440:1", 9042); assertThat(translator.translate(address)) - .isEqualTo(new InetSocketAddress("cassandra.datacenter1.com", 19042)); - } - - @Test - public void should_translate_to_correct_subnet_address_with_default_port_ipv6() { - Map subnetAddresses = - ImmutableMap.of("\"::ffff:6440:0/111\"", "cassandra.datacenter1.com"); - DefaultDriverContext context = context(subnetAddresses); - SubnetAddressTranslator translator = new SubnetAddressTranslator(context); - InetSocketAddress address = new InetSocketAddress("100.64.0.1", 19042); - assertThat(translator.translate(address)) - .isEqualTo(new InetSocketAddress("cassandra.datacenter1.com", 9042)); + .isEqualTo(InetSocketAddress.createUnresolved("cassandra.datacenter1.com", 19042)); } @Test @@ -93,21 +71,7 @@ public void should_translate_to_default_address() { SubnetAddressTranslator translator = new SubnetAddressTranslator(context); InetSocketAddress address = new InetSocketAddress("100.68.0.1", 9042); assertThat(translator.translate(address)) - .isEqualTo(new InetSocketAddress("cassandra.com", 19042)); - } - - @Test - public void should_translate_to_default_address_with_default_port() { - DefaultDriverContext context = context(ImmutableMap.of()); - when(context - .getConfig() - .getDefaultProfile() - .getString(SubnetAddressTranslator.ADDRESS_TRANSLATOR_DEFAULT_ADDRESS_OPTION, null)) - .thenReturn("cassandra.com"); - SubnetAddressTranslator translator = new SubnetAddressTranslator(context); - InetSocketAddress address = new InetSocketAddress("100.68.0.1", 19042); - assertThat(translator.translate(address)) - .isEqualTo(new InetSocketAddress("cassandra.com", 9042)); + .isEqualTo(InetSocketAddress.createUnresolved("cassandra.com", 19042)); } @Test @@ -164,10 +128,34 @@ public void should_fail_on_not_prefix_block_subnet_ipv6() { "Subnet ::ffff:6441:0/111 must be represented as a network prefix block ::ffff:6440:0/111"); } + @Test + public void should_fail_on_subnet_address_without_port() { + Map subnetAddresses = + ImmutableMap.of("\"100.64.0.0/15\"", "cassandra.datacenter1.com"); + DefaultDriverContext context = context(subnetAddresses); + assertThatIllegalArgumentException() + .isThrownBy(() -> new SubnetAddressTranslator(context)) + .withMessage("Invalid address: cassandra.datacenter1.com"); + } + + @Test + public void should_fail_on_default_address_without_port() { + DefaultDriverContext context = context(ImmutableMap.of()); + when(context + .getConfig() + .getDefaultProfile() + .getString(SubnetAddressTranslator.ADDRESS_TRANSLATOR_DEFAULT_ADDRESS_OPTION, null)) + .thenReturn("cassandra.com"); + assertThatIllegalArgumentException() + .isThrownBy(() -> new SubnetAddressTranslator(context)) + .withMessage("Invalid address: cassandra.com"); + } + private static DefaultDriverContext context(Map subnetAddresses) { DriverExecutionProfile profile = mock(DriverExecutionProfile.class); when(profile.getStringMap(SubnetAddressTranslator.ADDRESS_TRANSLATOR_SUBNET_ADDRESSES_OPTION)) .thenReturn(subnetAddresses); + when(profile.getBoolean(RESOLVE_CONTACT_POINTS, true)).thenReturn(false); return MockedDriverContextFactory.defaultDriverContext(Optional.of(profile)); } } diff --git a/manual/core/address_resolution/README.md b/manual/core/address_resolution/README.md index 335b4cb0d77..72dfd8a9e46 100644 --- a/manual/core/address_resolution/README.md +++ b/manual/core/address_resolution/README.md @@ -164,6 +164,10 @@ datastax-java-driver.advanced.address-translator { Such setup is common for running Cassandra on Kubernetes with [k8ssandra](https://docs.k8ssandra.io/). +Note: this address translator has optional dependency +on [IPAddress](https://mvnrepository.com/artifact/com.github.seancfoley/ipaddress) which must be explicitly set in your +project to use this address translator. + ### EC2 multi-region If you deploy both Cassandra and client applications on Amazon EC2, and your cluster spans multiple regions, you'll have From eb4b576a6ee8ca821bb33174fbbc4a75c4ed34aa Mon Sep 17 00:00:00 2001 From: alexsa Date: Thu, 13 Feb 2025 12:04:13 +0100 Subject: [PATCH 5/9] Nits and docs improvements --- .../addresstranslation/SubnetAddressTranslator.java | 1 - core/src/main/resources/reference.conf | 4 ++-- manual/core/address_resolution/README.md | 12 +++++++----- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/addresstranslation/SubnetAddressTranslator.java b/core/src/main/java/com/datastax/oss/driver/internal/core/addresstranslation/SubnetAddressTranslator.java index 0e1d9a4d9e8..2a7725fd1ff 100644 --- a/core/src/main/java/com/datastax/oss/driver/internal/core/addresstranslation/SubnetAddressTranslator.java +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/addresstranslation/SubnetAddressTranslator.java @@ -192,7 +192,6 @@ private static IPAddress parseSubnet(String subnetCIDR) { throw new IllegalArgumentException("Invalid subnet: " + subnetCIDR); } if (!subnet.isPrefixBlock()) { - throw new IllegalArgumentException( String.format( "Subnet %s must be represented as a network prefix block %s", diff --git a/core/src/main/resources/reference.conf b/core/src/main/resources/reference.conf index 4f8d652125f..90596531d54 100644 --- a/core/src/main/resources/reference.conf +++ b/core/src/main/resources/reference.conf @@ -1028,7 +1028,7 @@ datastax-java-driver { # The driver provides the following implementations out of the box: # - PassThroughAddressTranslator: returns all addresses unchanged. # - FixedHostNameAddressTranslator: translates all addresses to a specific hostname. - # - SubnetAddressTranslator: translates addresses to hostname based on the subnet matches. + # - SubnetAddressTranslator: translates addresses to hostname based on the subnet match. # - Ec2MultiRegionAddressTranslator: suitable for an Amazon multi-region EC2 deployment where # clients are also deployed in EC2. It optimizes network costs by favoring private IPs over # public ones whenever possible. @@ -1048,7 +1048,7 @@ datastax-java-driver { # # "::ffff:6440:0/111" = "cassandra.datacenter1.com:9042" # # "::ffff:6442:0/111" = "cassandra.datacenter2.com:9042" # } - # Optional. When configured, addresses not matching the configured subnets are translated this address. + # Optional. When configured, addresses not matching the configured subnets are translated to this address. # default-address = "cassandra.datacenter1.com:9042" # Note: `advanced.resolve-contact-points` (see below) is used to determine whether to resolve subnet # and default address on translation. diff --git a/manual/core/address_resolution/README.md b/manual/core/address_resolution/README.md index 72dfd8a9e46..8aac59f61ce 100644 --- a/manual/core/address_resolution/README.md +++ b/manual/core/address_resolution/README.md @@ -139,8 +139,8 @@ proxy, we have an option to use `FixedHostNameAddressTranslator`. But for multi- want to have more control over routing queries to a specific datacenter (eg. for optimizing latencies), which requires setting up a separate proxy per datacenter. -Normally, each Cassandra datacenter nodes are deployed to a separate subnet to support internode communications in the -cluster and avoid IP addresses collisions. So when Cassandra broadcasts its nodes IP addresses, we can determine which +Normally, each Cassandra datacenter nodes are deployed to a different subnet to support internode communications in the +cluster and avoid IP address collisions. So when Cassandra broadcasts its nodes IP addresses, we can determine which datacenter that node belongs to by checking its IP address against the given datacenter subnet. For such scenarios you can use `SubnetAddressTranslator` to translate node IPs to the datacenter proxy address @@ -152,13 +152,15 @@ datastax-java-driver.advanced.address-translator { class = SubnetAddressTranslator subnet-addresses { "100.64.0.0/15" = "cassandra.datacenter1.com:9042" - "100.66.0.0/15" = "cassandra.datacenter2.com" # port defaults to 9042 if not specified + "100.66.0.0/15" = "cassandra.datacenter2.com:9042" # IPv6 example: # "::ffff:6440:0/111" = "cassandra.datacenter1.com:9042" - # "::ffff:6442:0/111" = "cassandra.datacenter2.com" # port defaults to 9042 if not specified + # "::ffff:6442:0/111" = "cassandra.datacenter2.com:9042" } - # Optional. When configured, addresses not matching the configured subnets are translated to it. Port defaults to 9042 if not specified. + # Optional. When configured, addresses not matching the configured subnets are translated to this address. default-address = "cassandra.datacenter1.com:9042" + # Note: `advanced.resolve-contact-points` is used to determine whether to resolve subnet and default address + # on translation. } ``` From addc6be2af9ea76a7645b1b9b481126e95aa9e56 Mon Sep 17 00:00:00 2001 From: alexsa Date: Fri, 14 Feb 2025 09:58:34 +0100 Subject: [PATCH 6/9] Resolve addresses on init and use separate property to toggle address resolution --- .../SubnetAddressTranslator.java | 65 +++++++++++-------- core/src/main/resources/reference.conf | 5 +- .../SubnetAddressTranslatorTest.java | 6 +- manual/core/address_resolution/README.md | 5 +- 4 files changed, 47 insertions(+), 34 deletions(-) diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/addresstranslation/SubnetAddressTranslator.java b/core/src/main/java/com/datastax/oss/driver/internal/core/addresstranslation/SubnetAddressTranslator.java index 2a7725fd1ff..4dab495e035 100644 --- a/core/src/main/java/com/datastax/oss/driver/internal/core/addresstranslation/SubnetAddressTranslator.java +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/addresstranslation/SubnetAddressTranslator.java @@ -17,8 +17,6 @@ */ package com.datastax.oss.driver.internal.core.addresstranslation; -import static com.datastax.oss.driver.api.core.config.DefaultDriverOption.RESOLVE_CONTACT_POINTS; - import com.datastax.oss.driver.api.core.addresstranslation.AddressTranslator; import com.datastax.oss.driver.api.core.config.DriverOption; import com.datastax.oss.driver.api.core.context.DriverContext; @@ -74,6 +72,13 @@ public class SubnetAddressTranslator implements AddressTranslator { public static final String ADDRESS_TRANSLATOR_DEFAULT_ADDRESS = "advanced.address-translator.default-address"; + /** + * Whether to resolve the addresses on initialization (if true) or on each node (re-)connection + * (if false). Defaults to false. + */ + public static final String ADDRESS_TRANSLATOR_RESOLVE_ADDRESSES = + "advanced.address-translator.resolve-addresses"; + public static DriverOption ADDRESS_TRANSLATOR_SUBNET_ADDRESSES_OPTION = new DriverOption() { @NonNull @@ -92,13 +97,26 @@ public String getPath() { } }; + public static DriverOption ADDRESS_TRANSLATOR_RESOLVE_ADDRESSES_OPTION = + new DriverOption() { + @NonNull + @Override + public String getPath() { + return ADDRESS_TRANSLATOR_RESOLVE_ADDRESSES; + } + }; + private final List subnetAddresses; - private final Optional defaultAddress; - private final boolean resolveAddresses; + private final Optional defaultAddress; private final String logPrefix; public SubnetAddressTranslator(@NonNull DriverContext context) { logPrefix = context.getSessionName(); + boolean resolveAddresses = + context + .getConfig() + .getDefaultProfile() + .getBoolean(ADDRESS_TRANSLATOR_RESOLVE_ADDRESSES_OPTION, false); this.subnetAddresses = context.getConfig().getDefaultProfile() .getStringMap(ADDRESS_TRANSLATOR_SUBNET_ADDRESSES_OPTION).entrySet().stream() @@ -109,20 +127,18 @@ public SubnetAddressTranslator(@NonNull DriverContext context) { // "100.64.0.0/15" -> '"100.64.0.0/15"' String subnetCIDR = e.getKey().replaceAll("\"", ""); String address = e.getValue(); - return new SubnetAddress(subnetCIDR, address); + return new SubnetAddress(subnetCIDR, parseAddress(address, resolveAddresses)); }) .collect(Collectors.toList()); this.defaultAddress = Optional.ofNullable( - context - .getConfig() - .getDefaultProfile() - .getString(ADDRESS_TRANSLATOR_DEFAULT_ADDRESS_OPTION, null)); - this.resolveAddresses = - context.getConfig().getDefaultProfile().getBoolean(RESOLVE_CONTACT_POINTS, true); + context + .getConfig() + .getDefaultProfile() + .getString(ADDRESS_TRANSLATOR_DEFAULT_ADDRESS_OPTION, null)) + .map(address -> parseAddress(address, resolveAddresses)); validateSubnetsAreNotOverlapping(this.subnetAddresses); - this.defaultAddress.ifPresent(SubnetAddressTranslator::validateAddress); } @NonNull @@ -131,16 +147,16 @@ public InetSocketAddress translate(@NonNull InetSocketAddress address) { InetSocketAddress translatedAddress = null; for (SubnetAddress subnetAddress : subnetAddresses) { if (subnetAddress.contains(address)) { - translatedAddress = parseAddress(subnetAddress.address, resolveAddresses); + translatedAddress = subnetAddress.address; } } if (translatedAddress == null && defaultAddress.isPresent()) { - translatedAddress = parseAddress(defaultAddress.get(), resolveAddresses); + translatedAddress = defaultAddress.get(); } if (translatedAddress == null) { translatedAddress = address; } - LOG.debug("[{}] Resolved {} to {}", logPrefix, address, translatedAddress); + LOG.debug("[{}] Translated {} to {}", logPrefix, address, translatedAddress); return translatedAddress; } @@ -148,15 +164,14 @@ public InetSocketAddress translate(@NonNull InetSocketAddress address) { public void close() {} @Nullable - private static InetSocketAddress parseAddress(String address, boolean resolve) { - return AddressUtils.extract(address, resolve).iterator().next(); - } - - private static void validateAddress(String address) { + private InetSocketAddress parseAddress(String address, boolean resolve) { try { - parseAddress(address, false); + InetSocketAddress parsedAddress = AddressUtils.extract(address, resolve).iterator().next(); + LOG.debug("[{}] Parsed {} to {}", logPrefix, address, parsedAddress); + return parsedAddress; } catch (RuntimeException e) { - throw new IllegalArgumentException("Invalid address: " + address, e); + throw new IllegalArgumentException( + String.format("Invalid address %s (%s)", address, e.getMessage())); } } @@ -177,13 +192,11 @@ private static void validateSubnetsAreNotOverlapping(List subnetA private static class SubnetAddress { private final IPAddress subnet; - private final String address; + private final InetSocketAddress address; - private SubnetAddress(String subnetCIDR, String address) { + private SubnetAddress(String subnetCIDR, InetSocketAddress address) { this.subnet = parseSubnet(subnetCIDR); this.address = address; - - validateAddress(this.address); } private static IPAddress parseSubnet(String subnetCIDR) { diff --git a/core/src/main/resources/reference.conf b/core/src/main/resources/reference.conf index 90596531d54..3c6851a48ee 100644 --- a/core/src/main/resources/reference.conf +++ b/core/src/main/resources/reference.conf @@ -1050,8 +1050,9 @@ datastax-java-driver { # } # Optional. When configured, addresses not matching the configured subnets are translated to this address. # default-address = "cassandra.datacenter1.com:9042" - # Note: `advanced.resolve-contact-points` (see below) is used to determine whether to resolve subnet - # and default address on translation. + # Whether to resolve the addresses once on initialization (if true) or on each node (re-)connection (if false). + # If not configured, defaults to false. + # resolve-addresses = false } # Whether to resolve the addresses passed to `basic.contact-points`. diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/addresstranslation/SubnetAddressTranslatorTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/addresstranslation/SubnetAddressTranslatorTest.java index 99ba9e6f2cb..173a57c87a7 100644 --- a/core/src/test/java/com/datastax/oss/driver/internal/core/addresstranslation/SubnetAddressTranslatorTest.java +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/addresstranslation/SubnetAddressTranslatorTest.java @@ -17,7 +17,6 @@ */ package com.datastax.oss.driver.internal.core.addresstranslation; -import static com.datastax.oss.driver.api.core.config.DefaultDriverOption.RESOLVE_CONTACT_POINTS; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.mockito.Mockito.mock; @@ -135,7 +134,7 @@ public void should_fail_on_subnet_address_without_port() { DefaultDriverContext context = context(subnetAddresses); assertThatIllegalArgumentException() .isThrownBy(() -> new SubnetAddressTranslator(context)) - .withMessage("Invalid address: cassandra.datacenter1.com"); + .withMessage("Invalid address cassandra.datacenter1.com (expecting format host:port)"); } @Test @@ -148,14 +147,13 @@ public void should_fail_on_default_address_without_port() { .thenReturn("cassandra.com"); assertThatIllegalArgumentException() .isThrownBy(() -> new SubnetAddressTranslator(context)) - .withMessage("Invalid address: cassandra.com"); + .withMessage("Invalid address cassandra.com (expecting format host:port)"); } private static DefaultDriverContext context(Map subnetAddresses) { DriverExecutionProfile profile = mock(DriverExecutionProfile.class); when(profile.getStringMap(SubnetAddressTranslator.ADDRESS_TRANSLATOR_SUBNET_ADDRESSES_OPTION)) .thenReturn(subnetAddresses); - when(profile.getBoolean(RESOLVE_CONTACT_POINTS, true)).thenReturn(false); return MockedDriverContextFactory.defaultDriverContext(Optional.of(profile)); } } diff --git a/manual/core/address_resolution/README.md b/manual/core/address_resolution/README.md index 8aac59f61ce..867b1d02735 100644 --- a/manual/core/address_resolution/README.md +++ b/manual/core/address_resolution/README.md @@ -159,8 +159,9 @@ datastax-java-driver.advanced.address-translator { } # Optional. When configured, addresses not matching the configured subnets are translated to this address. default-address = "cassandra.datacenter1.com:9042" - # Note: `advanced.resolve-contact-points` is used to determine whether to resolve subnet and default address - # on translation. + # Whether to resolve the addresses once on initialization (if true) or on each node (re-)connection (if false). + # If not configured, defaults to false. + resolve-addresses = false } ``` From 7f7fdf5a50c5dd3cb3509c26918c231367dc556c Mon Sep 17 00:00:00 2001 From: alexsa Date: Mon, 17 Feb 2025 09:39:15 +0100 Subject: [PATCH 7/9] Suppress warnings --- .../core/addresstranslation/SubnetAddressTranslator.java | 1 + .../core/addresstranslation/SubnetAddressTranslatorTest.java | 1 + 2 files changed, 2 insertions(+) diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/addresstranslation/SubnetAddressTranslator.java b/core/src/main/java/com/datastax/oss/driver/internal/core/addresstranslation/SubnetAddressTranslator.java index 4dab495e035..007f23dc15d 100644 --- a/core/src/main/java/com/datastax/oss/driver/internal/core/addresstranslation/SubnetAddressTranslator.java +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/addresstranslation/SubnetAddressTranslator.java @@ -107,6 +107,7 @@ public String getPath() { }; private final List subnetAddresses; + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") private final Optional defaultAddress; private final String logPrefix; diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/addresstranslation/SubnetAddressTranslatorTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/addresstranslation/SubnetAddressTranslatorTest.java index 173a57c87a7..1a8da505ffe 100644 --- a/core/src/test/java/com/datastax/oss/driver/internal/core/addresstranslation/SubnetAddressTranslatorTest.java +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/addresstranslation/SubnetAddressTranslatorTest.java @@ -31,6 +31,7 @@ import java.util.Optional; import org.junit.Test; +@SuppressWarnings("resource") public class SubnetAddressTranslatorTest { @Test From 844287efc6991c8a9736bcaa2e0307939ab4e873 Mon Sep 17 00:00:00 2001 From: alexsa Date: Tue, 18 Feb 2025 12:20:41 +0100 Subject: [PATCH 8/9] Improve exception handling --- .../com/datastax/oss/driver/internal/core/ContactPoints.java | 2 +- .../core/addresstranslation/SubnetAddressTranslator.java | 4 +++- .../datastax/oss/driver/internal/core/util/AddressUtils.java | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/ContactPoints.java b/core/src/main/java/com/datastax/oss/driver/internal/core/ContactPoints.java index d88b09e592d..bb65661b72f 100644 --- a/core/src/main/java/com/datastax/oss/driver/internal/core/ContactPoints.java +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/ContactPoints.java @@ -43,7 +43,7 @@ public static Set merge( try { addresses = AddressUtils.extract(spec, resolve); } catch (RuntimeException e) { - LOG.warn("Ignoring invalid contact point {} ({})", spec, e.getMessage()); + LOG.warn("Ignoring invalid contact point {} ({})", spec, e.getMessage(), e); } if (addresses.size() > 1) { diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/addresstranslation/SubnetAddressTranslator.java b/core/src/main/java/com/datastax/oss/driver/internal/core/addresstranslation/SubnetAddressTranslator.java index 007f23dc15d..287b5285cb0 100644 --- a/core/src/main/java/com/datastax/oss/driver/internal/core/addresstranslation/SubnetAddressTranslator.java +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/addresstranslation/SubnetAddressTranslator.java @@ -107,8 +107,10 @@ public String getPath() { }; private final List subnetAddresses; + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") private final Optional defaultAddress; + private final String logPrefix; public SubnetAddressTranslator(@NonNull DriverContext context) { @@ -172,7 +174,7 @@ private InetSocketAddress parseAddress(String address, boolean resolve) { return parsedAddress; } catch (RuntimeException e) { throw new IllegalArgumentException( - String.format("Invalid address %s (%s)", address, e.getMessage())); + String.format("Invalid address %s (%s)", address, e.getMessage()), e); } } diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/util/AddressUtils.java b/core/src/main/java/com/datastax/oss/driver/internal/core/util/AddressUtils.java index c087db29081..8905edb9192 100644 --- a/core/src/main/java/com/datastax/oss/driver/internal/core/util/AddressUtils.java +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/util/AddressUtils.java @@ -47,7 +47,7 @@ public static Set extract(String address, boolean resolve) { try { inetAddresses = InetAddress.getAllByName(host); } catch (UnknownHostException e) { - throw new RuntimeException("unknown host " + host, e); + throw new RuntimeException(e); } Set result = new HashSet<>(); for (InetAddress inetAddress : inetAddresses) { From 85d59316572c0d455b3ee7c47a98ab3c78664723 Mon Sep 17 00:00:00 2001 From: alexsa Date: Sun, 23 Mar 2025 12:26:25 +0100 Subject: [PATCH 9/9] Move address translator driver options to DefaultDriverOption --- .../api/core/config/DefaultDriverOption.java | 43 ++++++++++- .../FixedHostNameAddressTranslator.java | 20 +---- .../SubnetAddressTranslator.java | 74 ++----------------- .../FixedHostNameAddressTranslatorTest.java | 5 +- .../SubnetAddressTranslatorTest.java | 9 ++- 5 files changed, 60 insertions(+), 91 deletions(-) diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/config/DefaultDriverOption.java b/core/src/main/java/com/datastax/oss/driver/api/core/config/DefaultDriverOption.java index 6ffd51d86ef..4e45bf7b117 100644 --- a/core/src/main/java/com/datastax/oss/driver/api/core/config/DefaultDriverOption.java +++ b/core/src/main/java/com/datastax/oss/driver/api/core/config/DefaultDriverOption.java @@ -994,7 +994,48 @@ public enum DefaultDriverOption implements DriverOption { * *

Value-type: boolean */ - SSL_ALLOW_DNS_REVERSE_LOOKUP_SAN("advanced.ssl-engine-factory.allow-dns-reverse-lookup-san"); + SSL_ALLOW_DNS_REVERSE_LOOKUP_SAN("advanced.ssl-engine-factory.allow-dns-reverse-lookup-san"), + /** + * An address to always translate all node addresses to that same proxy hostname no matter what IP + * address a node has, but still using its native transport port. + * + *

Value-Type: {@link String} + */ + ADDRESS_TRANSLATOR_ADVERTISED_HOSTNAME("advanced.address-translator.advertised-hostname"), + /** + * A map of Cassandra node subnets (CIDR notations) to target addresses, for example (note quoted + * keys): + * + *

+   * advanced.address-translator.subnet-addresses {
+   *   "100.64.0.0/15" = "cassandra.datacenter1.com:9042"
+   *   "100.66.0.0/15" = "cassandra.datacenter2.com:9042"
+   *   # IPv6 example:
+   *   # "::ffff:6440:0/111" = "cassandra.datacenter1.com:9042"
+   *   # "::ffff:6442:0/111" = "cassandra.datacenter2.com:9042"
+   * }
+   * 
+ * + * Note: subnets must be represented as prefix blocks, see {@link + * inet.ipaddr.Address#isPrefixBlock()}. + * + *

Value type: {@link java.util.Map Map}<{@link String},{@link String}> + */ + ADDRESS_TRANSLATOR_SUBNET_ADDRESSES("advanced.address-translator.subnet-addresses"), + /** + * A default address to fallback to if Cassandra node IP isn't contained in any of the configured + * subnets. + * + *

Value-Type: {@link String} + */ + ADDRESS_TRANSLATOR_DEFAULT_ADDRESS("advanced.address-translator.default-address"), + /** + * Whether to resolve the addresses on initialization (if true) or on each node (re-)connection + * (if false). Defaults to false. + * + *

Value-Type: boolean + */ + ADDRESS_TRANSLATOR_RESOLVE_ADDRESSES("advanced.address-translator.resolve-addresses"); private final String path; diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/addresstranslation/FixedHostNameAddressTranslator.java b/core/src/main/java/com/datastax/oss/driver/internal/core/addresstranslation/FixedHostNameAddressTranslator.java index 4fb9782f566..5cc6c2518fb 100644 --- a/core/src/main/java/com/datastax/oss/driver/internal/core/addresstranslation/FixedHostNameAddressTranslator.java +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/addresstranslation/FixedHostNameAddressTranslator.java @@ -17,8 +17,9 @@ */ package com.datastax.oss.driver.internal.core.addresstranslation; +import static com.datastax.oss.driver.api.core.config.DefaultDriverOption.ADDRESS_TRANSLATOR_ADVERTISED_HOSTNAME; + import com.datastax.oss.driver.api.core.addresstranslation.AddressTranslator; -import com.datastax.oss.driver.api.core.config.DriverOption; import com.datastax.oss.driver.api.core.context.DriverContext; import edu.umd.cs.findbugs.annotations.NonNull; import java.net.InetSocketAddress; @@ -37,28 +38,13 @@ public class FixedHostNameAddressTranslator implements AddressTranslator { private static final Logger LOG = LoggerFactory.getLogger(FixedHostNameAddressTranslator.class); - public static final String ADDRESS_TRANSLATOR_ADVERTISED_HOSTNAME = - "advanced.address-translator.advertised-hostname"; - - public static DriverOption ADDRESS_TRANSLATOR_ADVERTISED_HOSTNAME_OPTION = - new DriverOption() { - @NonNull - @Override - public String getPath() { - return ADDRESS_TRANSLATOR_ADVERTISED_HOSTNAME; - } - }; - private final String advertisedHostname; private final String logPrefix; public FixedHostNameAddressTranslator(@NonNull DriverContext context) { logPrefix = context.getSessionName(); advertisedHostname = - context - .getConfig() - .getDefaultProfile() - .getString(ADDRESS_TRANSLATOR_ADVERTISED_HOSTNAME_OPTION); + context.getConfig().getDefaultProfile().getString(ADDRESS_TRANSLATOR_ADVERTISED_HOSTNAME); } @NonNull diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/addresstranslation/SubnetAddressTranslator.java b/core/src/main/java/com/datastax/oss/driver/internal/core/addresstranslation/SubnetAddressTranslator.java index 287b5285cb0..2a2798a7117 100644 --- a/core/src/main/java/com/datastax/oss/driver/internal/core/addresstranslation/SubnetAddressTranslator.java +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/addresstranslation/SubnetAddressTranslator.java @@ -17,8 +17,11 @@ */ package com.datastax.oss.driver.internal.core.addresstranslation; +import static com.datastax.oss.driver.api.core.config.DefaultDriverOption.ADDRESS_TRANSLATOR_DEFAULT_ADDRESS; +import static com.datastax.oss.driver.api.core.config.DefaultDriverOption.ADDRESS_TRANSLATOR_RESOLVE_ADDRESSES; +import static com.datastax.oss.driver.api.core.config.DefaultDriverOption.ADDRESS_TRANSLATOR_SUBNET_ADDRESSES; + import com.datastax.oss.driver.api.core.addresstranslation.AddressTranslator; -import com.datastax.oss.driver.api.core.config.DriverOption; import com.datastax.oss.driver.api.core.context.DriverContext; import com.datastax.oss.driver.internal.core.util.AddressUtils; import edu.umd.cs.findbugs.annotations.NonNull; @@ -45,67 +48,6 @@ public class SubnetAddressTranslator implements AddressTranslator { private static final Logger LOG = LoggerFactory.getLogger(SubnetAddressTranslator.class); - /** - * A map of Cassandra node subnets (CIDR notations) to target addresses, for example (note quoted - * keys): - * - *

-   * advanced.address-translator.subnet-addresses {
-   *   "100.64.0.0/15" = "cassandra.datacenter1.com:9042"
-   *   "100.66.0.0/15" = "cassandra.datacenter2.com:9042"
-   *   # IPv6 example:
-   *   # "::ffff:6440:0/111" = "cassandra.datacenter1.com:9042"
-   *   # "::ffff:6442:0/111" = "cassandra.datacenter2.com:9042"
-   * }
-   * 
- * - * Note: subnets must be represented as prefix blocks, see {@link - * inet.ipaddr.Address#isPrefixBlock()}. - */ - public static final String ADDRESS_TRANSLATOR_SUBNET_ADDRESSES = - "advanced.address-translator.subnet-addresses"; - - /** - * A default address to fallback to if Cassandra node IP isn't contained in any of the configured - * subnets. - */ - public static final String ADDRESS_TRANSLATOR_DEFAULT_ADDRESS = - "advanced.address-translator.default-address"; - - /** - * Whether to resolve the addresses on initialization (if true) or on each node (re-)connection - * (if false). Defaults to false. - */ - public static final String ADDRESS_TRANSLATOR_RESOLVE_ADDRESSES = - "advanced.address-translator.resolve-addresses"; - - public static DriverOption ADDRESS_TRANSLATOR_SUBNET_ADDRESSES_OPTION = - new DriverOption() { - @NonNull - @Override - public String getPath() { - return ADDRESS_TRANSLATOR_SUBNET_ADDRESSES; - } - }; - - public static DriverOption ADDRESS_TRANSLATOR_DEFAULT_ADDRESS_OPTION = - new DriverOption() { - @NonNull - @Override - public String getPath() { - return ADDRESS_TRANSLATOR_DEFAULT_ADDRESS; - } - }; - - public static DriverOption ADDRESS_TRANSLATOR_RESOLVE_ADDRESSES_OPTION = - new DriverOption() { - @NonNull - @Override - public String getPath() { - return ADDRESS_TRANSLATOR_RESOLVE_ADDRESSES; - } - }; - private final List subnetAddresses; @SuppressWarnings("OptionalUsedAsFieldOrParameterType") @@ -119,10 +61,10 @@ public SubnetAddressTranslator(@NonNull DriverContext context) { context .getConfig() .getDefaultProfile() - .getBoolean(ADDRESS_TRANSLATOR_RESOLVE_ADDRESSES_OPTION, false); + .getBoolean(ADDRESS_TRANSLATOR_RESOLVE_ADDRESSES, false); this.subnetAddresses = - context.getConfig().getDefaultProfile() - .getStringMap(ADDRESS_TRANSLATOR_SUBNET_ADDRESSES_OPTION).entrySet().stream() + context.getConfig().getDefaultProfile().getStringMap(ADDRESS_TRANSLATOR_SUBNET_ADDRESSES) + .entrySet().stream() .map( e -> { // Quoted and/or containing forward slashes map keys in reference.conf are read to @@ -138,7 +80,7 @@ public SubnetAddressTranslator(@NonNull DriverContext context) { context .getConfig() .getDefaultProfile() - .getString(ADDRESS_TRANSLATOR_DEFAULT_ADDRESS_OPTION, null)) + .getString(ADDRESS_TRANSLATOR_DEFAULT_ADDRESS, null)) .map(address -> parseAddress(address, resolveAddresses)); validateSubnetsAreNotOverlapping(this.subnetAddresses); diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/addresstranslation/FixedHostNameAddressTranslatorTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/addresstranslation/FixedHostNameAddressTranslatorTest.java index c5e864b4bae..92800998056 100644 --- a/core/src/test/java/com/datastax/oss/driver/internal/core/addresstranslation/FixedHostNameAddressTranslatorTest.java +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/addresstranslation/FixedHostNameAddressTranslatorTest.java @@ -17,6 +17,7 @@ */ package com.datastax.oss.driver.internal.core.addresstranslation; +import static com.datastax.oss.driver.api.core.config.DefaultDriverOption.ADDRESS_TRANSLATOR_ADVERTISED_HOSTNAME; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -33,9 +34,7 @@ public class FixedHostNameAddressTranslatorTest { @Test public void should_translate_address() { DriverExecutionProfile defaultProfile = mock(DriverExecutionProfile.class); - when(defaultProfile.getString( - FixedHostNameAddressTranslator.ADDRESS_TRANSLATOR_ADVERTISED_HOSTNAME_OPTION)) - .thenReturn("myaddress"); + when(defaultProfile.getString(ADDRESS_TRANSLATOR_ADVERTISED_HOSTNAME)).thenReturn("myaddress"); DefaultDriverContext defaultDriverContext = MockedDriverContextFactory.defaultDriverContext(Optional.of(defaultProfile)); diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/addresstranslation/SubnetAddressTranslatorTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/addresstranslation/SubnetAddressTranslatorTest.java index 1a8da505ffe..b8c2f6422cf 100644 --- a/core/src/test/java/com/datastax/oss/driver/internal/core/addresstranslation/SubnetAddressTranslatorTest.java +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/addresstranslation/SubnetAddressTranslatorTest.java @@ -17,6 +17,8 @@ */ package com.datastax.oss.driver.internal.core.addresstranslation; +import static com.datastax.oss.driver.api.core.config.DefaultDriverOption.ADDRESS_TRANSLATOR_DEFAULT_ADDRESS; +import static com.datastax.oss.driver.api.core.config.DefaultDriverOption.ADDRESS_TRANSLATOR_SUBNET_ADDRESSES; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.mockito.Mockito.mock; @@ -66,7 +68,7 @@ public void should_translate_to_default_address() { when(context .getConfig() .getDefaultProfile() - .getString(SubnetAddressTranslator.ADDRESS_TRANSLATOR_DEFAULT_ADDRESS_OPTION, null)) + .getString(ADDRESS_TRANSLATOR_DEFAULT_ADDRESS, null)) .thenReturn("cassandra.com:19042"); SubnetAddressTranslator translator = new SubnetAddressTranslator(context); InetSocketAddress address = new InetSocketAddress("100.68.0.1", 9042); @@ -144,7 +146,7 @@ public void should_fail_on_default_address_without_port() { when(context .getConfig() .getDefaultProfile() - .getString(SubnetAddressTranslator.ADDRESS_TRANSLATOR_DEFAULT_ADDRESS_OPTION, null)) + .getString(ADDRESS_TRANSLATOR_DEFAULT_ADDRESS, null)) .thenReturn("cassandra.com"); assertThatIllegalArgumentException() .isThrownBy(() -> new SubnetAddressTranslator(context)) @@ -153,8 +155,7 @@ public void should_fail_on_default_address_without_port() { private static DefaultDriverContext context(Map subnetAddresses) { DriverExecutionProfile profile = mock(DriverExecutionProfile.class); - when(profile.getStringMap(SubnetAddressTranslator.ADDRESS_TRANSLATOR_SUBNET_ADDRESSES_OPTION)) - .thenReturn(subnetAddresses); + when(profile.getStringMap(ADDRESS_TRANSLATOR_SUBNET_ADDRESSES)).thenReturn(subnetAddresses); return MockedDriverContextFactory.defaultDriverContext(Optional.of(profile)); } }