Skip to content

Commit 2fc0ffc

Browse files
committed
Remove fixed ports to allow starting multiple containers and avoid conflicts with local Couchbase
1 parent 5ab5e5e commit 2fc0ffc

File tree

2 files changed

+300
-13
lines changed

2 files changed

+300
-13
lines changed
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
/*
2+
* Copyright (c) 2016 Couchbase, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.couchbase.client.core.config;
17+
18+
import com.couchbase.client.core.logging.CouchbaseLogger;
19+
import com.couchbase.client.core.logging.CouchbaseLoggerFactory;
20+
import com.couchbase.client.core.service.ServiceType;
21+
import com.couchbase.client.core.utils.NetworkAddress;
22+
import com.couchbase.client.deps.com.fasterxml.jackson.annotation.JsonCreator;
23+
import com.couchbase.client.deps.com.fasterxml.jackson.annotation.JsonIgnoreProperties;
24+
import com.couchbase.client.deps.com.fasterxml.jackson.annotation.JsonProperty;
25+
import org.testcontainers.couchbase.CouchbaseContainer;
26+
27+
import java.util.*;
28+
import java.util.stream.Collectors;
29+
30+
import static com.couchbase.client.core.logging.RedactableArgument.system;
31+
32+
/**
33+
* Patched to use non fixed container ports
34+
*/
35+
@JsonIgnoreProperties(ignoreUnknown = true)
36+
public class DefaultCouchbaseBucketConfig extends AbstractBucketConfig implements CouchbaseBucketConfig {
37+
38+
private static final CouchbaseLogger LOGGER = CouchbaseLoggerFactory.getInstance(CouchbaseBucketConfig.class);
39+
40+
public static final int PARTITION_NOT_EXISTENT = -2;
41+
42+
private final CouchbasePartitionInfo partitionInfo;
43+
private final List<NodeInfo> partitionHosts;
44+
private final Set<NetworkAddress> nodesWithPrimaryPartitions;
45+
46+
private final boolean tainted;
47+
private final long rev;
48+
private final boolean ephemeral;
49+
50+
/**
51+
* Creates a new {@link CouchbaseBucketConfig}.
52+
*
53+
* @param rev the revision of the config.
54+
* @param name the name of the bucket.
55+
* @param uri the URI for this bucket.
56+
* @param streamingUri the streaming URI for this bucket.
57+
* @param partitionInfo partition info for this bucket.
58+
* @param nodeInfos related node information.
59+
* @param portInfos port info for the nodes, including services.
60+
*/
61+
@JsonCreator
62+
public DefaultCouchbaseBucketConfig(
63+
@JsonProperty("rev") long rev,
64+
@JsonProperty("name") String name,
65+
@JsonProperty("uri") String uri,
66+
@JsonProperty("streamingUri") String streamingUri,
67+
@JsonProperty("vBucketServerMap") CouchbasePartitionInfo partitionInfo,
68+
@JsonProperty("nodes") List<NodeInfo> nodeInfos,
69+
@JsonProperty("nodesExt") List<PortInfo> portInfos,
70+
@JsonProperty("bucketCapabilities") List<BucketCapabilities> bucketCapabilities) {
71+
super(name, BucketNodeLocator.VBUCKET, uri, streamingUri, nodeInfos, getPortInfos(portInfos), bucketCapabilities);
72+
this.partitionInfo = partitionInfo;
73+
this.tainted = partitionInfo.tainted();
74+
List<NodeInfo> extendedNodeInfos = this.nodes(); // includes ports for SSL services
75+
this.partitionHosts = buildPartitionHosts(extendedNodeInfos, partitionInfo);
76+
this.nodesWithPrimaryPartitions = buildNodesWithPrimaryPartitions(nodeInfos, partitionInfo.partitions());
77+
this.rev = rev;
78+
79+
// Use bucket capabilities to identify if couchapi is missing (then its ephemeral). If its null then
80+
// we are running an old version of couchbase which doesn't have ephemeral buckets at all.
81+
this.ephemeral = bucketCapabilities != null && !bucketCapabilities.contains(BucketCapabilities.COUCHAPI);
82+
}
83+
84+
private static List<PortInfo> getPortInfos(List<PortInfo> portInfos) {
85+
return Optional.of(
86+
CouchbaseContainer.getContainers().stream()
87+
.map(CouchbaseContainer::getPortInfo)
88+
.collect(Collectors.toList()))
89+
.orElse(portInfos);
90+
}
91+
92+
/**
93+
* Pre-computes a set of nodes that have primary partitions active.
94+
*
95+
* @param nodeInfos the list of nodes.
96+
* @param partitions the partitions.
97+
* @return a set containing the addresses of nodes with primary partitions.
98+
*/
99+
private static Set<NetworkAddress> buildNodesWithPrimaryPartitions(final List<NodeInfo> nodeInfos,
100+
final List<Partition> partitions) {
101+
Set<NetworkAddress> nodes = new HashSet<NetworkAddress>(nodeInfos.size());
102+
for (Partition partition : partitions) {
103+
int index = partition.master();
104+
if (index >= 0) {
105+
nodes.add(nodeInfos.get(index).hostname());
106+
}
107+
}
108+
return nodes;
109+
}
110+
111+
/**
112+
* Helper method to reference the partition hosts from the raw node list.
113+
*
114+
* @param nodeInfos the node infos.
115+
* @param partitionInfo the partition info.
116+
* @return a ordered reference list for the partition hosts.
117+
*/
118+
private static List<NodeInfo> buildPartitionHosts(List<NodeInfo> nodeInfos, CouchbasePartitionInfo partitionInfo) {
119+
List<NodeInfo> partitionHosts = new ArrayList<NodeInfo>();
120+
for (String rawHost : partitionInfo.partitionHosts()) {
121+
NetworkAddress convertedHost;
122+
int directPort;
123+
try {
124+
String parts[] = rawHost.split(":");
125+
String host = "";
126+
String port = parts[parts.length - 1];
127+
if (parts.length > 2) {
128+
// Handle IPv6 syntax
129+
for (int i = 0; i < parts.length - 1; i++) {
130+
host += parts[i];
131+
if (parts[i].endsWith("]")) {
132+
break;
133+
} else {
134+
host += ":";
135+
}
136+
}
137+
} else {
138+
// Simple IPv4 Handling
139+
host = parts[0];
140+
}
141+
142+
convertedHost = NetworkAddress.create(host);
143+
try {
144+
int originalPort = Integer.parseInt(port);
145+
directPort = CouchbaseContainer.getContainers().stream()
146+
.filter(c -> c.getContainerIpAddress().equals(convertedHost.hostname()))
147+
.map(c -> c.getMappedPort(originalPort))
148+
.findFirst()
149+
.orElse(originalPort);
150+
} catch (NumberFormatException e) {
151+
LOGGER.warn("Could not parse port from the node address: {}, fallback to 0", system(rawHost));
152+
directPort = 0;
153+
}
154+
} catch (Exception e) {
155+
throw new ConfigurationException("Could not resolve " + rawHost + "on config building.", e);
156+
}
157+
for (NodeInfo nodeInfo : nodeInfos) {
158+
// Make sure we only take into account nodes which contain KV
159+
if (!nodeInfo.services().containsKey(ServiceType.BINARY)) {
160+
continue;
161+
}
162+
163+
if (nodeInfo.hostname().equals(convertedHost) &&
164+
(nodeInfo.services().get(ServiceType.BINARY) == directPort || directPort == 0)) {
165+
partitionHosts.add(nodeInfo);
166+
}
167+
}
168+
}
169+
if (partitionHosts.size() != partitionInfo.partitionHosts().length) {
170+
throw new ConfigurationException("Partition size is not equal after conversion, this is a bug.");
171+
}
172+
return partitionHosts;
173+
}
174+
175+
@Override
176+
public int numberOfReplicas() {
177+
return partitionInfo.numberOfReplicas();
178+
}
179+
180+
@Override
181+
public boolean tainted() {
182+
return tainted;
183+
}
184+
185+
@Override
186+
public boolean hasPrimaryPartitionsOnNode(final NetworkAddress hostname) {
187+
return nodesWithPrimaryPartitions.contains(hostname);
188+
}
189+
190+
@Override
191+
public short nodeIndexForMaster(int partition, boolean useFastForward) {
192+
if (useFastForward && !hasFastForwardMap()) {
193+
throw new IllegalStateException("Could not get index from FF-Map, none found in this config.");
194+
}
195+
196+
List<Partition> partitions = useFastForward ? partitionInfo.forwardPartitions() : partitionInfo.partitions();
197+
try {
198+
return partitions.get(partition).master();
199+
} catch (IndexOutOfBoundsException ex) {
200+
LOGGER.debug("Out of bounds on index for master " + partition + ".", ex);
201+
return PARTITION_NOT_EXISTENT;
202+
}
203+
}
204+
205+
@Override
206+
public short nodeIndexForReplica(int partition, int replica, boolean useFastForward) {
207+
if (useFastForward && !hasFastForwardMap()) {
208+
throw new IllegalStateException("Could not get index from FF-Map, none found in this config.");
209+
}
210+
211+
List<Partition> partitions = useFastForward ? partitionInfo.forwardPartitions() : partitionInfo.partitions();
212+
213+
try {
214+
return partitions.get(partition).replica(replica);
215+
} catch (IndexOutOfBoundsException ex) {
216+
LOGGER.debug("Out of bounds on index for replica " + partition + ".", ex);
217+
return PARTITION_NOT_EXISTENT;
218+
}
219+
}
220+
221+
@Override
222+
public int numberOfPartitions() {
223+
return partitionInfo.partitions().size();
224+
}
225+
226+
@Override
227+
public NodeInfo nodeAtIndex(int nodeIndex) {
228+
return partitionHosts.get(nodeIndex);
229+
}
230+
231+
@Override
232+
public long rev() {
233+
return rev;
234+
}
235+
236+
@Override
237+
public BucketType type() {
238+
return BucketType.COUCHBASE;
239+
}
240+
241+
@Override
242+
public boolean hasFastForwardMap() {
243+
return partitionInfo.hasFastForwardMap();
244+
}
245+
246+
@Override
247+
public boolean ephemeral() {
248+
return ephemeral;
249+
}
250+
251+
@Override
252+
public String toString() {
253+
return "DefaultCouchbaseBucketConfig{"
254+
+ "name='" + name() + '\''
255+
+ ", locator=" + locator()
256+
+ ", uri='" + uri() + '\''
257+
+ ", streamingUri='" + streamingUri() + '\''
258+
+ ", nodeInfo=" + nodes()
259+
+ ", partitionInfo=" + partitionInfo
260+
+ ", tainted=" + tainted
261+
+ ", rev=" + rev + '}';
262+
}
263+
}

src/main/java/org/testcontainers/couchbase/CouchbaseContainer.java

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
*/
1616
package org.testcontainers.couchbase;
1717

18+
import com.couchbase.client.core.config.DefaultPortInfo;
19+
import com.couchbase.client.core.config.PortInfo;
20+
import com.couchbase.client.core.service.ServiceType;
1821
import com.couchbase.client.core.utils.Base64;
1922
import com.couchbase.client.java.Bucket;
2023
import com.couchbase.client.java.CouchbaseCluster;
@@ -33,9 +36,7 @@
3336
import java.net.HttpURLConnection;
3437
import java.net.URL;
3538
import java.net.URLEncoder;
36-
import java.util.ArrayList;
37-
import java.util.Collections;
38-
import java.util.List;
39+
import java.util.*;
3940

4041
/**
4142
* Based on Laurent Doguin version
@@ -91,16 +92,23 @@ public class CouchbaseContainer<SELF extends CouchbaseContainer<SELF>> extends G
9192
@Getter(lazy = true)
9293
private final CouchbaseCluster couchbaseCluster = createCouchbaseCluster();
9394

95+
@Getter
96+
private static final Collection<CouchbaseContainer> containers = new HashSet<>();
97+
98+
@Getter(lazy = true)
99+
private final PortInfo portInfo = createPortInfo();
100+
94101
private List<BucketSettings> newBuckets = new ArrayList<>();
95102

96103
private String urlBase;
97104

98105
public CouchbaseContainer() {
99-
super("couchbase/server:latest");
106+
this("couchbase/server:latest");
100107
}
101108

102109
public CouchbaseContainer(String containerName) {
103110
super(containerName);
111+
containers.add(this);
104112
}
105113

106114
@Override
@@ -111,15 +119,7 @@ protected Integer getLivenessCheckPort() {
111119
@Override
112120
protected void configure() {
113121
// Configurable ports
114-
addExposedPorts(11210, 11207, 8091, 18091);
115-
116-
// Non configurable ports
117-
addFixedExposedPort(8092, 8092);
118-
addFixedExposedPort(8093, 8093);
119-
addFixedExposedPort(8094, 8094);
120-
addFixedExposedPort(8095, 8095);
121-
addFixedExposedPort(18092, 18092);
122-
addFixedExposedPort(18093, 18093);
122+
addExposedPorts(8091, 18091, 8092, 18092, 8093, 18093, 8094, 18094, 8095, 18095, 11207, 11210, 11211);
123123
setWaitStrategy(new HttpWaitStrategy().forPath("/ui/index.html#/"));
124124
}
125125

@@ -244,4 +244,28 @@ private DefaultCouchbaseEnvironment createCouchbaseEnvironment() {
244244
.bootstrapHttpSslPort(getMappedPort(18091))
245245
.build();
246246
}
247+
248+
private PortInfo createPortInfo() {
249+
DefaultPortInfo portInfo = new DefaultPortInfo(new HashMap<>(), null);
250+
try {
251+
portInfo.ports().put(ServiceType.VIEW, getMappedPort(8092));
252+
portInfo.sslPorts().put(ServiceType.VIEW, getMappedPort(18092));
253+
portInfo.ports().put(ServiceType.CONFIG, getMappedPort(8091));
254+
portInfo.sslPorts().put(ServiceType.CONFIG, getMappedPort(18091));
255+
portInfo.ports().put(ServiceType.BINARY, getMappedPort(11210));
256+
portInfo.sslPorts().put(ServiceType.BINARY, getMappedPort(11207));
257+
if (isQuery()) {
258+
portInfo.ports().put(ServiceType.QUERY, getMappedPort(8093));
259+
portInfo.sslPorts().put(ServiceType.QUERY, getMappedPort(18093));
260+
}
261+
if(isFts()) {
262+
portInfo.ports().put(ServiceType.SEARCH, getMappedPort(8094));
263+
portInfo.sslPorts().put(ServiceType.SEARCH, getMappedPort(18094));
264+
}
265+
266+
} catch (IllegalStateException e) {
267+
logger().warn("Container not started yet");
268+
}
269+
return portInfo;
270+
}
247271
}

0 commit comments

Comments
 (0)