44import com .amazonaws .auth .AWSStaticCredentialsProvider ;
55import com .amazonaws .auth .BasicAWSCredentials ;
66import com .amazonaws .client .builder .AwsClientBuilder ;
7- import lombok .Getter ;
8- import lombok .RequiredArgsConstructor ;
9- import lombok .experimental .FieldDefaults ;
10- import org .rnorth .ducttape .Preconditions ;
11- import org .testcontainers .containers .GenericContainer ;
12- import org .testcontainers .containers .wait .strategy .Wait ;
13- import org .testcontainers .utility .DockerImageName ;
14- import org .testcontainers .utility .TestcontainersConfiguration ;
15-
167import java .net .InetAddress ;
178import java .net .URI ;
189import java .net .URISyntaxException ;
2112import java .util .Arrays ;
2213import java .util .List ;
2314import java .util .stream .Collectors ;
15+ import lombok .Getter ;
16+ import lombok .RequiredArgsConstructor ;
17+ import lombok .experimental .FieldDefaults ;
18+ import lombok .extern .slf4j .Slf4j ;
19+ import org .rnorth .ducttape .Preconditions ;
20+ import org .testcontainers .containers .GenericContainer ;
21+ import org .testcontainers .containers .wait .strategy .Wait ;
22+ import org .testcontainers .utility .ComparableVersion ;
23+ import org .testcontainers .utility .DockerImageName ;
24+ import org .testcontainers .utility .TestcontainersConfiguration ;
2425
2526/**
2627 * <p>Container for Atlassian Labs Localstack, 'A fully functional local AWS cloud stack'.</p>
3031 * {@link LocalStackContainer#getDefaultCredentialsProvider()}
3132 * be used to obtain compatible endpoint configuration and credentials, respectively.</p>
3233 */
34+ @ Slf4j
3335public class LocalStackContainer extends GenericContainer <LocalStackContainer > {
3436
35- public static final String VERSION = "0.10.8" ;
37+ public static final String VERSION = "0.11.2" ;
38+ static final int PORT = 4566 ;
3639 private static final String HOSTNAME_EXTERNAL_ENV_VAR = "HOSTNAME_EXTERNAL" ;
37-
3840 private final List <Service > services = new ArrayList <>();
3941
42+ /**
43+ * Whether or to assume that all APIs run on different ports (when <code>true</code>) or are
44+ * exposed on a single port (<code>false</code>). From the Localstack README:
45+ *
46+ * <blockquote>Note: Starting with version 0.11.0, all APIs are exposed via a single edge
47+ * service [...] The API-specific endpoints below are still left for backward-compatibility but
48+ * may get removed in a future release - please reconfigure your client SDKs to start using the
49+ * single edge endpoint URL!</blockquote>
50+ * <p>
51+ * Testcontainers will use the tag of the docker image to infer whether or not the used version
52+ * of Localstack supports this feature.
53+ */
54+ private final boolean legacyMode ;
55+
4056 /**
4157 * @deprecated use {@link LocalStackContainer(DockerImageName)} instead
4258 */
@@ -53,13 +69,41 @@ public LocalStackContainer(String version) {
5369 this (DockerImageName .parse (TestcontainersConfiguration .getInstance ().getLocalStackImage () + ":" + version ));
5470 }
5571
72+ /**
73+ * @param dockerImageName image name to use for Localstack
74+ */
5675 public LocalStackContainer (final DockerImageName dockerImageName ) {
76+ this (dockerImageName , shouldRunInLegacyMode (dockerImageName .getVersionPart ()));
77+ }
78+
79+ /**
80+ * @param dockerImageName image name to use for Localstack
81+ * @param useLegacyMode if true, each AWS service is exposed on a different port
82+ */
83+ public LocalStackContainer (final DockerImageName dockerImageName , boolean useLegacyMode ) {
5784 super (dockerImageName );
85+ this .legacyMode = useLegacyMode ;
5886
5987 withFileSystemBind ("//var/run/docker.sock" , "/var/run/docker.sock" );
6088 waitingFor (Wait .forLogMessage (".*Ready\\ .\n " , 1 ));
6189 }
6290
91+ private static boolean shouldRunInLegacyMode (String version ) {
92+ if (version .equals ("latest" )) {
93+ return false ;
94+ }
95+
96+ ComparableVersion comparableVersion = new ComparableVersion (version );
97+ if (comparableVersion .isSemanticVersion ()) {
98+ boolean versionRequiresLegacyMode = comparableVersion .isLessThan ("0.11" );
99+ return versionRequiresLegacyMode ;
100+ }
101+
102+ log .warn ("Version {} is not a semantic version, LocalStack will run in legacy mode." , version );
103+ log .warn ("Consider using \" LocalStackContainer(DockerImageName dockerImageName, boolean legacyMode)\" constructor if you want to disable legacy mode." );
104+ return true ;
105+ }
106+
63107 @ Override
64108 protected void configure () {
65109 super .configure ();
@@ -81,9 +125,14 @@ protected void configure() {
81125 }
82126 logger ().info ("{} environment variable set to {} ({})" , HOSTNAME_EXTERNAL_ENV_VAR , getEnvMap ().get (HOSTNAME_EXTERNAL_ENV_VAR ), hostnameExternalReason );
83127
84- for (Service service : services ) {
85- addExposedPort (service .getPort ());
86- }
128+ exposePorts ();
129+ }
130+
131+ private void exposePorts () {
132+ services .stream ()
133+ .map (this ::getServicePort )
134+ .distinct ()
135+ .forEach (this ::addExposedPort );
87136 }
88137
89138 /**
@@ -154,12 +203,16 @@ public URI getEndpointOverride(Service service) {
154203 return new URI ("http://" +
155204 ipAddress +
156205 ":" +
157- getMappedPort (service . getPort ( )));
206+ getMappedPort (getServicePort ( service )));
158207 } catch (UnknownHostException | URISyntaxException e ) {
159208 throw new IllegalStateException ("Cannot obtain endpoint URL" , e );
160209 }
161210 }
162211
212+ private int getServicePort (Service service ) {
213+ return legacyMode ? service .port : PORT ;
214+ }
215+
163216 /**
164217 * Provides a {@link AWSCredentialsProvider} that is preconfigured to communicate with a given simulated service.
165218 * The credentials provider should be set in the AWS Java SDK when building a client, e.g.:
@@ -271,5 +324,13 @@ public enum Service {
271324 String localStackName ;
272325
273326 int port ;
327+
328+ @ Deprecated
329+ /*
330+ Since version 0.11, LocalStack exposes all services on a single (4566) port.
331+ */
332+ public int getPort () {
333+ return port ;
334+ }
274335 }
275336}
0 commit comments