From 749c7563244e9de759458ba99e9aa31df85086c9 Mon Sep 17 00:00:00 2001 From: Richard North Date: Sun, 11 Mar 2018 12:28:18 +0000 Subject: [PATCH 01/34] Relocate MariaDB module into main testcontainers-java repository Also enable parallel running of Jdbc Driver tests for speed reasons --- modules/jdbc-test/build.gradle | 3 + .../testcontainers/jdbc/JDBCDriverTest.java | 37 ++++---- .../jdbc/JDBCDriverWithPoolTest.java | 3 +- .../junit/SimpleMariaDBTest.java | 87 +++++++++++++++++++ .../test/resources/somepath/init_mariadb.sql | 5 ++ .../somepath/mariadb_conf_override/my.cnf | 55 ++++++++++++ modules/mariadb/README.md | 38 ++++++++ modules/mariadb/build.gradle | 11 +++ .../containers/MariaDBContainer.java | 73 ++++++++++++++++ .../containers/MariaDBContainerProvider.java | 16 ++++ ...s.containers.JdbcDatabaseContainerProvider | 1 + .../resources/mariadb-default-conf/my.cnf | 48 ++++++++++ 12 files changed, 361 insertions(+), 16 deletions(-) create mode 100644 modules/jdbc-test/src/test/java/org/testcontainers/junit/SimpleMariaDBTest.java create mode 100644 modules/jdbc-test/src/test/resources/somepath/init_mariadb.sql create mode 100644 modules/jdbc-test/src/test/resources/somepath/mariadb_conf_override/my.cnf create mode 100644 modules/mariadb/README.md create mode 100644 modules/mariadb/build.gradle create mode 100644 modules/mariadb/src/main/java/org/testcontainers/containers/MariaDBContainer.java create mode 100644 modules/mariadb/src/main/java/org/testcontainers/containers/MariaDBContainerProvider.java create mode 100644 modules/mariadb/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider create mode 100644 modules/mariadb/src/main/resources/mariadb-default-conf/my.cnf diff --git a/modules/jdbc-test/build.gradle b/modules/jdbc-test/build.gradle index 262035ce027..d129d59a206 100644 --- a/modules/jdbc-test/build.gradle +++ b/modules/jdbc-test/build.gradle @@ -1,6 +1,7 @@ dependencies { compile project(':mysql') compile project(':postgresql') + compile project(':mariadb') testCompile 'com.google.guava:guava:18.0' testCompile 'org.postgresql:postgresql:42.0.0' @@ -10,4 +11,6 @@ dependencies { testCompile 'org.apache.tomcat:tomcat-jdbc:8.5.4' testCompile 'org.vibur:vibur-dbcp:9.0' testCompile 'commons-dbutils:commons-dbutils:1.6' + + testCompile 'com.googlecode.junit-toolbox:junit-toolbox:2.4' } diff --git a/modules/jdbc-test/src/test/java/org/testcontainers/jdbc/JDBCDriverTest.java b/modules/jdbc-test/src/test/java/org/testcontainers/jdbc/JDBCDriverTest.java index c87f7bce365..eee537a4821 100644 --- a/modules/jdbc-test/src/test/java/org/testcontainers/jdbc/JDBCDriverTest.java +++ b/modules/jdbc-test/src/test/java/org/testcontainers/jdbc/JDBCDriverTest.java @@ -1,5 +1,6 @@ package org.testcontainers.jdbc; +import com.googlecode.junittoolbox.ParallelParameterized; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import org.apache.commons.dbutils.QueryRunner; @@ -20,7 +21,7 @@ import static org.rnorth.visibleassertions.VisibleAssertions.assertEquals; import static org.rnorth.visibleassertions.VisibleAssertions.assertTrue; -@RunWith(Parameterized.class) +@RunWith(ParallelParameterized.class) public class JDBCDriverTest { @Parameter @@ -35,26 +36,32 @@ public class JDBCDriverTest { @Parameterized.Parameters(name = "{index} - {0}") public static Iterable data() { return asList( - new Object[][]{ - {"jdbc:tc:mysql:5.5.43://hostname/databasename", false, false, false}, - {"jdbc:tc:mysql://hostname/databasename?TC_INITSCRIPT=somepath/init_mysql.sql", true, false, false}, - {"jdbc:tc:mysql://hostname/databasename?TC_INITFUNCTION=org.testcontainers.jdbc.JDBCDriverTest::sampleInitFunction", true, false, false}, - {"jdbc:tc:mysql://hostname/databasename?useUnicode=yes&characterEncoding=utf8", false, true, false}, - {"jdbc:tc:mysql://hostname/databasename", false, false, false}, - {"jdbc:tc:mysql://hostname/databasename?useSSL=false", false, false, false}, - {"jdbc:tc:postgresql://hostname/databasename", false, false, false}, - {"jdbc:tc:mysql:5.6://hostname/databasename?TC_MY_CNF=somepath/mysql_conf_override", false, false, true}, - }); + new Object[][]{ + {"jdbc:tc:mysql:5.5.43://hostname/databasename", false, false, false}, + {"jdbc:tc:mysql://hostname/databasename?TC_INITSCRIPT=somepath/init_mysql.sql", true, false, false}, + {"jdbc:tc:mysql://hostname/databasename?TC_INITFUNCTION=org.testcontainers.jdbc.JDBCDriverTest::sampleInitFunction", true, false, false}, + {"jdbc:tc:mysql://hostname/databasename?useUnicode=yes&characterEncoding=utf8", false, true, false}, + {"jdbc:tc:mysql://hostname/databasename", false, false, false}, + {"jdbc:tc:mysql://hostname/databasename?useSSL=false", false, false, false}, + {"jdbc:tc:postgresql://hostname/databasename", false, false, false}, + {"jdbc:tc:mysql:5.6://hostname/databasename?TC_MY_CNF=somepath/mysql_conf_override", false, false, true}, + {"jdbc:tc:mariadb:10.1.16://hostname/databasename", false, false, false}, + {"jdbc:tc:mariadb://hostname/databasename", false, false, false}, + {"jdbc:tc:mariadb://hostname/databasename?useUnicode=yes&characterEncoding=utf8", false, true, false}, + {"jdbc:tc:mariadb://hostname/databasename?TC_INITSCRIPT=somepath/init_mariadb.sql", true, false, false}, + {"jdbc:tc:mariadb://hostname/databasename?TC_INITFUNCTION=org.testcontainers.jdbc.JDBCDriverTest::sampleInitFunction", true, false, false}, + {"jdbc:tc:mariadb:10.1.16://hostname/databasename?TC_MY_CNF=somepath/mariadb_conf_override", false, false, true} + }); } public static void sampleInitFunction(Connection connection) throws SQLException { connection.createStatement().execute("CREATE TABLE bar (\n" + - " foo VARCHAR(255)\n" + - ");"); + " foo VARCHAR(255)\n" + + ");"); connection.createStatement().execute("INSERT INTO bar (foo) VALUES ('hello world');"); connection.createStatement().execute("CREATE TABLE my_counter (\n" + - " n INT\n" + - ");"); + " n INT\n" + + ");"); } @AfterClass diff --git a/modules/jdbc-test/src/test/java/org/testcontainers/jdbc/JDBCDriverWithPoolTest.java b/modules/jdbc-test/src/test/java/org/testcontainers/jdbc/JDBCDriverWithPoolTest.java index 4c7e119996d..8f936863e7e 100644 --- a/modules/jdbc-test/src/test/java/org/testcontainers/jdbc/JDBCDriverWithPoolTest.java +++ b/modules/jdbc-test/src/test/java/org/testcontainers/jdbc/JDBCDriverWithPoolTest.java @@ -1,5 +1,6 @@ package org.testcontainers.jdbc; +import com.googlecode.junittoolbox.ParallelParameterized; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import org.apache.commons.dbutils.QueryRunner; @@ -24,7 +25,7 @@ /** * */ -@RunWith(Parameterized.class) +@RunWith(ParallelParameterized.class) public class JDBCDriverWithPoolTest { public static final String URL = "jdbc:tc:mysql://hostname/databasename?TC_INITFUNCTION=org.testcontainers.jdbc.JDBCDriverWithPoolTest::sampleInitFunction"; diff --git a/modules/jdbc-test/src/test/java/org/testcontainers/junit/SimpleMariaDBTest.java b/modules/jdbc-test/src/test/java/org/testcontainers/junit/SimpleMariaDBTest.java new file mode 100644 index 00000000000..1ad65e65465 --- /dev/null +++ b/modules/jdbc-test/src/test/java/org/testcontainers/junit/SimpleMariaDBTest.java @@ -0,0 +1,87 @@ +package org.testcontainers.junit; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import lombok.NonNull; + +import org.apache.commons.lang.SystemUtils; +import org.junit.Test; +import org.testcontainers.containers.MariaDBContainer; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import static org.rnorth.visibleassertions.VisibleAssertions.assertEquals; +import static org.rnorth.visibleassertions.VisibleAssertions.assertTrue; +import static org.junit.Assume.assumeFalse; + + +/** + * @author Miguel Gonzalez Sanchez + */ +public class SimpleMariaDBTest { + + @Test + public void testSimple() throws SQLException { + MariaDBContainer mariadb = new MariaDBContainer(); + mariadb.start(); + + try { + ResultSet resultSet = performQuery(mariadb, "SELECT 1"); + int resultSetInt = resultSet.getInt(1); + + assertEquals("A basic SELECT query succeeds", 1, resultSetInt); + } finally { + mariadb.stop(); + } + } + + @Test + public void testSpecificVersion() throws SQLException { + MariaDBContainer mariadbOldVersion = new MariaDBContainer("mariadb:5.5.51"); + mariadbOldVersion.start(); + + try { + ResultSet resultSet = performQuery(mariadbOldVersion, "SELECT VERSION()"); + String resultSetString = resultSet.getString(1); + + assertTrue("The database version can be set using a container rule parameter", resultSetString.startsWith("5.5.51")); + } finally { + mariadbOldVersion.stop(); + } + } + + @Test + public void testMariaDBWithCustomIniFile() throws SQLException { + assumeFalse(SystemUtils.IS_OS_WINDOWS); + MariaDBContainer mariadbCustomConfig = new MariaDBContainer("mariadb:10.1.16") + .withConfigurationOverride("somepath/mariadb_conf_override"); + mariadbCustomConfig.start(); + + try { + ResultSet resultSet = performQuery(mariadbCustomConfig, "SELECT @@GLOBAL.innodb_file_format"); + String result = resultSet.getString(1); + + assertEquals("The InnoDB file format has been set by the ini file content", "Barracuda", result); + } finally { + mariadbCustomConfig.stop(); + } + } + + @NonNull + protected ResultSet performQuery(MariaDBContainer containerRule, String sql) throws SQLException { + HikariConfig hikariConfig = new HikariConfig(); + hikariConfig.setJdbcUrl(containerRule.getJdbcUrl()); + hikariConfig.setUsername(containerRule.getUsername()); + hikariConfig.setPassword(containerRule.getPassword()); + + HikariDataSource ds = new HikariDataSource(hikariConfig); + Statement statement = ds.getConnection().createStatement(); + statement.execute(sql); + ResultSet resultSet = statement.getResultSet(); + + resultSet.next(); + return resultSet; + } +} diff --git a/modules/jdbc-test/src/test/resources/somepath/init_mariadb.sql b/modules/jdbc-test/src/test/resources/somepath/init_mariadb.sql new file mode 100644 index 00000000000..2b00ee968b0 --- /dev/null +++ b/modules/jdbc-test/src/test/resources/somepath/init_mariadb.sql @@ -0,0 +1,5 @@ +CREATE TABLE bar ( + foo VARCHAR(255) +); + +INSERT INTO bar (foo) VALUES ('hello world'); \ No newline at end of file diff --git a/modules/jdbc-test/src/test/resources/somepath/mariadb_conf_override/my.cnf b/modules/jdbc-test/src/test/resources/somepath/mariadb_conf_override/my.cnf new file mode 100644 index 00000000000..dcba17aff5c --- /dev/null +++ b/modules/jdbc-test/src/test/resources/somepath/mariadb_conf_override/my.cnf @@ -0,0 +1,55 @@ +[mysqld] +port = 3306 +#socket = /tmp/mysql.sock +skip-external-locking +key_buffer_size = 16K +max_allowed_packet = 1M +table_open_cache = 4 +sort_buffer_size = 64K +read_buffer_size = 256K +read_rnd_buffer_size = 256K +net_buffer_length = 2K +thread_stack = 128K +skip-host-cache +skip-name-resolve + + +innodb_file_format=Barracuda + + + + + +# Don't listen on a TCP/IP port at all. This can be a security enhancement, +# if all processes that need to connect to mysqld run on the same host. +# All interaction with mysqld must be made via Unix sockets or named pipes. +# Note that using this option without enabling named pipes on Windows +# (using the "enable-named-pipe" option) will render mysqld useless! +# +#skip-networking +#server-id = 1 + +# Uncomment the following if you want to log updates +#log-bin=mysql-bin + +# binary logging format - mixed recommended +#binlog_format=mixed + +# Causes updates to non-transactional engines using statement format to be +# written directly to binary log. Before using this option make sure that +# there are no dependencies between transactional and non-transactional +# tables such as in the statement INSERT INTO t_myisam SELECT * FROM +# t_innodb; otherwise, slaves may diverge from the master. +#binlog_direct_non_transactional_updates=TRUE + +# Uncomment the following if you are using InnoDB tables +innodb_data_file_path = ibdata1:10M:autoextend +# You can set .._buffer_pool_size up to 50 - 80 % +# of RAM but beware of setting memory usage too high +innodb_buffer_pool_size = 16M +#innodb_additional_mem_pool_size = 2M +# Set .._log_file_size to 25 % of buffer pool size +innodb_log_file_size = 5M +innodb_log_buffer_size = 8M +innodb_flush_log_at_trx_commit = 1 +innodb_lock_wait_timeout = 50 \ No newline at end of file diff --git a/modules/mariadb/README.md b/modules/mariadb/README.md new file mode 100644 index 00000000000..5f202a6112b --- /dev/null +++ b/modules/mariadb/README.md @@ -0,0 +1,38 @@ +# Testcontainers MariaDB Module + +Testcontainers module for the MariaDB database. + +## Usage example + +Running MariaDB as a stand-in for in a test: + +```java +public class SomeTest { + + @Rule + public MariaDBContainer mariaDB = new MariaDBContainer(); + + @Test + public void someTestMethod() { + String url = mariaDB.getJdbcUrl(); + + ... create a connection and run test as normal +``` + +## Dependency information + +### Maven + +``` + + org.testcontainers + mariadb + 1.4.3 + +``` + +### Gradle + +``` +compile group: 'org.testcontainers', name: 'mariadb', version: '1.4.3' +``` diff --git a/modules/mariadb/build.gradle b/modules/mariadb/build.gradle new file mode 100644 index 00000000000..ba6f4b97dcf --- /dev/null +++ b/modules/mariadb/build.gradle @@ -0,0 +1,11 @@ +description = "Testcontainers :: JDBC :: MariaDB" + +dependencies { + compile project(':jdbc') + + testCompile 'org.mariadb.jdbc:mariadb-java-client:1.5.9' + testCompile 'com.zaxxer:HikariCP-java6:2.3.8' + testCompile 'commons-dbutils:commons-dbutils:1.6' + testCompile 'org.apache.tomcat:tomcat-jdbc:8.5.4' + testCompile 'org.vibur:vibur-dbcp:9.0' +} diff --git a/modules/mariadb/src/main/java/org/testcontainers/containers/MariaDBContainer.java b/modules/mariadb/src/main/java/org/testcontainers/containers/MariaDBContainer.java new file mode 100644 index 00000000000..2bea9b8ad7b --- /dev/null +++ b/modules/mariadb/src/main/java/org/testcontainers/containers/MariaDBContainer.java @@ -0,0 +1,73 @@ +package org.testcontainers.containers; + +/** + * Container implementation for the MariaDB project. + * + * @author Miguel Gonzalez Sanchez + */ +public class MariaDBContainer> extends JdbcDatabaseContainer { + + public static final String NAME = "mariadb"; + public static final String IMAGE = "mariadb"; + private static final Integer MARIADB_PORT = 3306; + private static final String MARIADB_USER = "test"; + private static final String MARIADB_PASSWORD = "test"; + private static final String MARIADB_DATABASE = "test"; + private static final String MY_CNF_CONFIG_OVERRIDE_PARAM_NAME = "TC_MY_CNF"; + + public MariaDBContainer() { + super(IMAGE + ":latest"); + } + + public MariaDBContainer(String dockerImageName) { + super(dockerImageName); + } + + @Override + protected Integer getLivenessCheckPort() { + return getMappedPort(MARIADB_PORT); + } + + @Override + protected void configure() { + optionallyMapResourceParameterAsVolume(MY_CNF_CONFIG_OVERRIDE_PARAM_NAME, "/etc/mysql/conf.d", "mariadb-default-conf"); + + addExposedPort(MARIADB_PORT); + addEnv("MYSQL_DATABASE", MARIADB_DATABASE); + addEnv("MYSQL_USER", MARIADB_USER); + addEnv("MYSQL_PASSWORD", MARIADB_PASSWORD); + addEnv("MYSQL_ROOT_PASSWORD", MARIADB_PASSWORD); + setCommand("mysqld"); + setStartupAttempts(3); + } + + @Override + public String getDriverClassName() { + return "org.mariadb.jdbc.Driver"; + } + + @Override + public String getJdbcUrl() { + return "jdbc:mariadb://" + getContainerIpAddress() + ":" + getMappedPort(MARIADB_PORT) + "/test"; + } + + @Override + public String getUsername() { + return MARIADB_USER; + } + + @Override + public String getPassword() { + return MARIADB_PASSWORD; + } + + @Override + public String getTestQueryString() { + return "SELECT 1"; + } + + public SELF withConfigurationOverride(String s) { + parameters.put(MY_CNF_CONFIG_OVERRIDE_PARAM_NAME, s); + return self(); + } +} diff --git a/modules/mariadb/src/main/java/org/testcontainers/containers/MariaDBContainerProvider.java b/modules/mariadb/src/main/java/org/testcontainers/containers/MariaDBContainerProvider.java new file mode 100644 index 00000000000..fbe9d807af4 --- /dev/null +++ b/modules/mariadb/src/main/java/org/testcontainers/containers/MariaDBContainerProvider.java @@ -0,0 +1,16 @@ +package org.testcontainers.containers; + +/** + * Factory for MariaDB org.testcontainers.containers. + */ +public class MariaDBContainerProvider extends JdbcDatabaseContainerProvider { + @Override + public boolean supports(String databaseType) { + return databaseType.equals(MariaDBContainer.NAME); + } + + @Override + public JdbcDatabaseContainer newInstance(String tag) { + return new MariaDBContainer(MariaDBContainer.IMAGE + ":" + tag); + } +} diff --git a/modules/mariadb/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider b/modules/mariadb/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider new file mode 100644 index 00000000000..f5b51466d19 --- /dev/null +++ b/modules/mariadb/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider @@ -0,0 +1 @@ +org.testcontainers.containers.MariaDBContainerProvider \ No newline at end of file diff --git a/modules/mariadb/src/main/resources/mariadb-default-conf/my.cnf b/modules/mariadb/src/main/resources/mariadb-default-conf/my.cnf new file mode 100644 index 00000000000..87d4cfecd20 --- /dev/null +++ b/modules/mariadb/src/main/resources/mariadb-default-conf/my.cnf @@ -0,0 +1,48 @@ +[mysqld] +port = 3306 +#socket = /tmp/mysql.sock +skip-external-locking +key_buffer_size = 16K +max_allowed_packet = 1M +table_open_cache = 4 +sort_buffer_size = 64K +read_buffer_size = 256K +read_rnd_buffer_size = 256K +net_buffer_length = 2K +thread_stack = 512K +skip-host-cache +skip-name-resolve + +# Don't listen on a TCP/IP port at all. This can be a security enhancement, +# if all processes that need to connect to mysqld run on the same host. +# All interaction with mysqld must be made via Unix sockets or named pipes. +# Note that using this option without enabling named pipes on Windows +# (using the "enable-named-pipe" option) will render mysqld useless! +# +#skip-networking +#server-id = 1 + +# Uncomment the following if you want to log updates +#log-bin=mysql-bin + +# binary logging format - mixed recommended +#binlog_format=mixed + +# Causes updates to non-transactional engines using statement format to be +# written directly to binary log. Before using this option make sure that +# there are no dependencies between transactional and non-transactional +# tables such as in the statement INSERT INTO t_myisam SELECT * FROM +# t_innodb; otherwise, slaves may diverge from the master. +#binlog_direct_non_transactional_updates=TRUE + +# Uncomment the following if you are using InnoDB tables +innodb_data_file_path = ibdata1:10M:autoextend +# You can set .._buffer_pool_size up to 50 - 80 % +# of RAM but beware of setting memory usage too high +innodb_buffer_pool_size = 16M +#innodb_additional_mem_pool_size = 2M +# Set .._log_file_size to 25 % of buffer pool size +innodb_log_file_size = 5M +innodb_log_buffer_size = 8M +innodb_flush_log_at_trx_commit = 1 +innodb_lock_wait_timeout = 50 From 6bd213a102be15dc674c967743ef4e6bf5fe7f77 Mon Sep 17 00:00:00 2001 From: Richard North Date: Sun, 11 Mar 2018 12:28:18 +0000 Subject: [PATCH 02/34] Relocate Oracle XE module into main testcontainers-java repository Also enable use of sha256 hashes instead of image tags, for situations where image tags are not easily pinnable Extend pull/startup timeout for Oracle container --- .../images/RemoteDockerImage.java | 18 +++-- .../utility/DockerLoggerFactory.java | 12 ++- modules/jdbc-test/build.gradle | 9 +++ .../junit/SimpleOracleTest.java | 39 ++++++++++ .../containers/JdbcDatabaseContainer.java | 11 ++- modules/oracle-xe/README.md | 38 +++++++++ modules/oracle-xe/build.gradle | 17 ++++ .../containers/OracleContainer.java | 78 +++++++++++++++++++ .../containers/OracleContainerProvider.java | 16 ++++ ...s.containers.JdbcDatabaseContainerProvider | 1 + .../containers/OracleJDBCDriverTest.java | 48 ++++++++++++ 11 files changed, 275 insertions(+), 12 deletions(-) create mode 100644 modules/jdbc-test/src/test/java/org/testcontainers/junit/SimpleOracleTest.java create mode 100644 modules/oracle-xe/README.md create mode 100644 modules/oracle-xe/build.gradle create mode 100644 modules/oracle-xe/src/main/java/org/testcontainers/containers/OracleContainer.java create mode 100644 modules/oracle-xe/src/main/java/org/testcontainers/containers/OracleContainerProvider.java create mode 100644 modules/oracle-xe/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider create mode 100644 modules/oracle-xe/src/test/java/org/testcontainers/containers/OracleJDBCDriverTest.java diff --git a/core/src/main/java/org/testcontainers/images/RemoteDockerImage.java b/core/src/main/java/org/testcontainers/images/RemoteDockerImage.java index fb686c82e19..bea12e48d90 100644 --- a/core/src/main/java/org/testcontainers/images/RemoteDockerImage.java +++ b/core/src/main/java/org/testcontainers/images/RemoteDockerImage.java @@ -48,6 +48,7 @@ protected final String resolve() { profiler.start("Check local images"); int attempts = 0; + DockerClientException lastException = null; while (true) { // Does our cache already know the image? if (AVAILABLE_IMAGE_NAME_CACHE.contains(dockerImageName)) { @@ -82,19 +83,20 @@ protected final String resolve() { } if (attempts++ >= 3) { - logger.error("Retry limit reached while trying to pull image: " + dockerImageName + ". Please check output of `docker pull " + dockerImageName + "`"); - throw new ContainerFetchException("Retry limit reached while trying to pull image: " + dockerImageName); + logger.error("Retry limit reached while trying to pull image: {}" + dockerImageName + ". Please check output of `docker pull {}`", dockerImageName, dockerImageName); + throw new ContainerFetchException("Retry limit reached while trying to pull image: " + dockerImageName, lastException); } // The image is not available locally - pull it try { - dockerClient.pullImageCmd(dockerImageName).exec(new PullImageResultCallback()).awaitCompletion(); - } catch (InterruptedException e) { - throw new ContainerFetchException("Failed to fetch container image for " + dockerImageName, e); + final PullImageResultCallback callback = new PullImageResultCallback(); + dockerClient.pullImageCmd(dockerImageName).exec(callback); + callback.awaitSuccess(); + AVAILABLE_IMAGE_NAME_CACHE.add(dockerImageName); + break; + } catch (DockerClientException e) { + lastException = e; } - - // Do not break here, but step into the next iteration, where it will be verified with listImagesCmd(). - // see https://github.com/docker/docker/issues/10708 } return dockerImageName; diff --git a/core/src/main/java/org/testcontainers/utility/DockerLoggerFactory.java b/core/src/main/java/org/testcontainers/utility/DockerLoggerFactory.java index 6c013dcd692..69f7b998da2 100644 --- a/core/src/main/java/org/testcontainers/utility/DockerLoggerFactory.java +++ b/core/src/main/java/org/testcontainers/utility/DockerLoggerFactory.java @@ -9,10 +9,18 @@ public final class DockerLoggerFactory { public static Logger getLogger(String dockerImageName) { + + final String abbreviatedName; + if (dockerImageName.contains("@sha256")) { + abbreviatedName = dockerImageName.substring(0, dockerImageName.indexOf("@sha256") + 14) + "..."; + } else { + abbreviatedName = dockerImageName; + } + if ("UTF-8".equals(System.getProperty("file.encoding"))) { - return LoggerFactory.getLogger("\uD83D\uDC33 [" + dockerImageName + "]"); + return LoggerFactory.getLogger("\uD83D\uDC33 [" + abbreviatedName + "]"); } else { - return LoggerFactory.getLogger("docker[" + dockerImageName + "]"); + return LoggerFactory.getLogger("docker[" + abbreviatedName + "]"); } } } diff --git a/modules/jdbc-test/build.gradle b/modules/jdbc-test/build.gradle index d129d59a206..f9b877a1899 100644 --- a/modules/jdbc-test/build.gradle +++ b/modules/jdbc-test/build.gradle @@ -1,12 +1,21 @@ +repositories { + maven { + url "https://maven.atlassian.com/3rdparty/" + } +} + dependencies { compile project(':mysql') compile project(':postgresql') compile project(':mariadb') + compile project(':oracle-xe') testCompile 'com.google.guava:guava:18.0' testCompile 'org.postgresql:postgresql:42.0.0' testCompile 'mysql:mysql-connector-java:5.1.45' testCompile 'org.mariadb.jdbc:mariadb-java-client:1.4.6' + testCompile 'com.oracle:ojdbc6:12.1.0.1-atlassian-hosted' + testCompile 'com.zaxxer:HikariCP-java6:2.3.8' testCompile 'org.apache.tomcat:tomcat-jdbc:8.5.4' testCompile 'org.vibur:vibur-dbcp:9.0' diff --git a/modules/jdbc-test/src/test/java/org/testcontainers/junit/SimpleOracleTest.java b/modules/jdbc-test/src/test/java/org/testcontainers/junit/SimpleOracleTest.java new file mode 100644 index 00000000000..5c47d3e2f92 --- /dev/null +++ b/modules/jdbc-test/src/test/java/org/testcontainers/junit/SimpleOracleTest.java @@ -0,0 +1,39 @@ +package org.testcontainers.junit; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import org.junit.Rule; +import org.junit.Test; +import org.testcontainers.containers.OracleContainer; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import static org.rnorth.visibleassertions.VisibleAssertions.assertEquals; + +/** + * @author gusohal + */ +public class SimpleOracleTest { + + @Rule + public OracleContainer oracle = new OracleContainer(); + + @Test + public void testSimple() throws SQLException { + HikariConfig hikariConfig = new HikariConfig(); + hikariConfig.setJdbcUrl(oracle.getJdbcUrl()); + hikariConfig.setUsername(oracle.getUsername()); + hikariConfig.setPassword(oracle.getPassword()); + + HikariDataSource ds = new HikariDataSource(hikariConfig); + Statement statement = ds.getConnection().createStatement(); + statement.execute("SELECT 1 FROM dual"); + ResultSet resultSet = statement.getResultSet(); + + resultSet.next(); + int resultSetInt = resultSet.getInt(1); + assertEquals("A basic SELECT query succeeds", 1, resultSetInt); + } +} diff --git a/modules/jdbc/src/main/java/org/testcontainers/containers/JdbcDatabaseContainer.java b/modules/jdbc/src/main/java/org/testcontainers/containers/JdbcDatabaseContainer.java index 3ebaa87223e..1c7b7c71f05 100644 --- a/modules/jdbc/src/main/java/org/testcontainers/containers/JdbcDatabaseContainer.java +++ b/modules/jdbc/src/main/java/org/testcontainers/containers/JdbcDatabaseContainer.java @@ -1,6 +1,5 @@ package org.testcontainers.containers; -import java.util.concurrent.Future; import lombok.NonNull; import org.jetbrains.annotations.NotNull; import org.rnorth.ducttape.ratelimits.RateLimiter; @@ -15,6 +14,7 @@ import java.util.HashMap; import java.util.Map; import java.util.Properties; +import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; /** @@ -91,7 +91,7 @@ protected void waitUntilContainerStarted() { // Repeatedly try and open a connection to the DB and execute a test query logger().info("Waiting for database connection to become available at {} using query '{}'", getJdbcUrl(), getTestQueryString()); - Unreliables.retryUntilSuccess(120, TimeUnit.SECONDS, () -> { + Unreliables.retryUntilSuccess(getStartupTimeoutSeconds(), TimeUnit.SECONDS, () -> { if (!isRunning()) { throw new ContainerLaunchException("Container failed to start"); @@ -185,4 +185,11 @@ public void setParameters(Map parameters) { public void addParameter(String paramName, String value) { this.parameters.put(paramName, value); } + + /** + * @return startup time to allow, including image pull time, in seconds + */ + protected int getStartupTimeoutSeconds() { + return 120; + } } diff --git a/modules/oracle-xe/README.md b/modules/oracle-xe/README.md new file mode 100644 index 00000000000..52e7c302d34 --- /dev/null +++ b/modules/oracle-xe/README.md @@ -0,0 +1,38 @@ +# Testcontainers Oracle XE Module + +Testcontainers module for the Oracle XE database. + +## Usage example + +Running Oracle XE as a stand-in for in a test: + +```java +public class SomeTest { + + @Rule + public OracleContainer oracle = new OracleContainer(); + + @Test + public void someTestMethod() { + String url = oracle.getJdbcUrl(); + + ... create a connection and run test as normal +``` + +## Dependency information + +### Maven + +``` + + org.testcontainers + oracle-xe + 1.4.3 + +``` + +### Gradle + +``` +compile group: 'org.testcontainers', name: 'oracle-xe', version: '1.4.3' +``` diff --git a/modules/oracle-xe/build.gradle b/modules/oracle-xe/build.gradle new file mode 100644 index 00000000000..6b4ab1af763 --- /dev/null +++ b/modules/oracle-xe/build.gradle @@ -0,0 +1,17 @@ +description = "Testcontainers :: JDBC :: Oracle XE" + +repositories { + maven { + url "https://maven.atlassian.com/3rdparty/" + } +} + +dependencies { + compile project(':jdbc') + + testCompile 'com.oracle:ojdbc6:12.1.0.1-atlassian-hosted' + testCompile 'com.zaxxer:HikariCP-java6:2.3.8' + testCompile 'commons-dbutils:commons-dbutils:1.6' + testCompile 'org.apache.tomcat:tomcat-jdbc:8.5.4' + testCompile 'org.vibur:vibur-dbcp:9.0' +} diff --git a/modules/oracle-xe/src/main/java/org/testcontainers/containers/OracleContainer.java b/modules/oracle-xe/src/main/java/org/testcontainers/containers/OracleContainer.java new file mode 100644 index 00000000000..41dce1693a7 --- /dev/null +++ b/modules/oracle-xe/src/main/java/org/testcontainers/containers/OracleContainer.java @@ -0,0 +1,78 @@ +package org.testcontainers.containers; + +import org.testcontainers.utility.TestcontainersConfiguration; + +/** + * @author gusohal + */ +public class OracleContainer extends JdbcDatabaseContainer { + + public static final String NAME = "oracle"; + public static final String IMAGE = TestcontainersConfiguration.getInstance() + .getProperties().getProperty("oracle.container.image","wnameless/oracle-xe-11g"); + private static final int ORACLE_PORT = 1521; + private static final int APEX_HTTP_PORT = 8080; + + public OracleContainer() { + super(IMAGE + "@sha256:15ff9ef50b4f90613c9780b589c57d98a8a9d496decec854316d96396ec5c085"); + } + + public OracleContainer(String dockerImageName) { + super(dockerImageName); + } + + @Override + protected Integer getLivenessCheckPort() { + return getMappedPort(ORACLE_PORT); + } + + @Override + protected void configure() { + + addExposedPorts(ORACLE_PORT, APEX_HTTP_PORT); + } + + @Override + public String getDriverClassName() { + return "oracle.jdbc.OracleDriver"; + } + + @Override + public String getJdbcUrl() { + return "jdbc:oracle:thin:" + getUsername() + "/" + getPassword() + "@//" + getContainerIpAddress() + ":" + getOraclePort() + "/" + getSid(); + } + + @Override + public String getUsername() { + return "system"; + } + + @Override + public String getPassword() { + return "oracle"; + } + + @SuppressWarnings("SameReturnValue") + public String getSid() { + return "xe"; + } + + public Integer getOraclePort() { + return getMappedPort(ORACLE_PORT); + } + + @SuppressWarnings("unused") + public Integer getWebPort() { + return getMappedPort(APEX_HTTP_PORT); + } + + @Override + public String getTestQueryString() { + return "SELECT 1 FROM DUAL"; + } + + @Override + protected int getStartupTimeoutSeconds() { + return 240; + } +} diff --git a/modules/oracle-xe/src/main/java/org/testcontainers/containers/OracleContainerProvider.java b/modules/oracle-xe/src/main/java/org/testcontainers/containers/OracleContainerProvider.java new file mode 100644 index 00000000000..1aea9f7e443 --- /dev/null +++ b/modules/oracle-xe/src/main/java/org/testcontainers/containers/OracleContainerProvider.java @@ -0,0 +1,16 @@ +package org.testcontainers.containers; + +/** + * Factory for Oracle containers. + */ +public class OracleContainerProvider extends JdbcDatabaseContainerProvider { + @Override + public boolean supports(String databaseType) { + return databaseType.equals(OracleContainer.NAME); + } + + @Override + public JdbcDatabaseContainer newInstance(String tag) { + return new OracleContainer(OracleContainer.IMAGE + ":" + tag); + } +} diff --git a/modules/oracle-xe/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider b/modules/oracle-xe/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider new file mode 100644 index 00000000000..f031b7f955a --- /dev/null +++ b/modules/oracle-xe/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider @@ -0,0 +1 @@ +org.testcontainers.containers.OracleContainerProvider \ No newline at end of file diff --git a/modules/oracle-xe/src/test/java/org/testcontainers/containers/OracleJDBCDriverTest.java b/modules/oracle-xe/src/test/java/org/testcontainers/containers/OracleJDBCDriverTest.java new file mode 100644 index 00000000000..61a8d9b8d4b --- /dev/null +++ b/modules/oracle-xe/src/test/java/org/testcontainers/containers/OracleJDBCDriverTest.java @@ -0,0 +1,48 @@ +package org.testcontainers.containers; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import org.apache.commons.dbutils.QueryRunner; +import org.apache.commons.dbutils.ResultSetHandler; +import org.junit.Test; + +import java.sql.ResultSet; +import java.sql.SQLException; + +import static org.rnorth.visibleassertions.VisibleAssertions.assertEquals; + +/** + * @author gusohal + */ +public class OracleJDBCDriverTest { + + @Test + public void testOracleWithNoSpecifiedVersion() throws SQLException { + performSimpleTest("jdbc:tc:oracle://hostname/databasename"); + } + + + private void performSimpleTest(String jdbcUrl) throws SQLException { + HikariDataSource dataSource = getDataSource(jdbcUrl, 1); + new QueryRunner(dataSource).query("SELECT 1 FROM dual", new ResultSetHandler() { + @Override + public Object handle(ResultSet rs) throws SQLException { + rs.next(); + int resultSetInt = rs.getInt(1); + assertEquals("A basic SELECT query succeeds", 1, resultSetInt); + return true; + } + }); + dataSource.close(); + } + + private HikariDataSource getDataSource(String jdbcUrl, int poolSize) { + HikariConfig hikariConfig = new HikariConfig(); + hikariConfig.setJdbcUrl(jdbcUrl); + hikariConfig.setConnectionTestQuery("SELECT 1 FROM dual"); + hikariConfig.setMinimumIdle(1); + hikariConfig.setMaximumPoolSize(poolSize); + + return new HikariDataSource(hikariConfig); + } +} From ae5bdc133213fe54239f4c4e41bb1dd42868e7a3 Mon Sep 17 00:00:00 2001 From: Richard North Date: Sun, 11 Mar 2018 21:17:18 +0000 Subject: [PATCH 03/34] Relocate Localstack module into main testcontainers-java repository --- modules/localstack/README.md | 44 +++++ modules/localstack/build.gradle | 8 + .../localstack/LocalStackContainer.java | 166 ++++++++++++++++++ .../localstack/SimpleLocalstackS3Test.java | 52 ++++++ .../src/test/resources/logback-test.xml | 27 +++ 5 files changed, 297 insertions(+) create mode 100644 modules/localstack/README.md create mode 100644 modules/localstack/build.gradle create mode 100644 modules/localstack/src/main/java/org/testcontainers/containers/localstack/LocalStackContainer.java create mode 100644 modules/localstack/src/test/java/org/testcontainers/containers/localstack/SimpleLocalstackS3Test.java create mode 100644 modules/localstack/src/test/resources/logback-test.xml diff --git a/modules/localstack/README.md b/modules/localstack/README.md new file mode 100644 index 00000000000..188edfd2aa5 --- /dev/null +++ b/modules/localstack/README.md @@ -0,0 +1,44 @@ +# Testcontainers LocalStack AWS testing module + +Testcontainers module for the Atlassian's [LocalStack](https://github.com/localstack/localstack), 'a fully functional local AWS cloud stack'. + +## Usage example + +Running LocalStack as a stand-in for AWS S3 during a test: + +```java +public class SomeTest { + + @Rule + public LocalStackContainer localstack = new LocalStackContainer() + .withServices(S3); + + @Test + public void someTestMethod() { + AmazonS3 s3 = AmazonS3ClientBuilder + .standard() + .withEndpointConfiguration(localstack.getEndpointConfiguration(S3)) + .withCredentials(localstack.getDefaultCredentialsProvider()) + .build(); + + s3.createBucket("foo"); + s3.putObject("foo", "bar", "baz"); +``` + +## Dependency information + +### Maven + +``` + + org.testcontainers + localstack + 1.4.3 + +``` + +### Gradle + +``` +compile group: 'org.testcontainers', name: 'localstack', version: '1.4.3' +``` diff --git a/modules/localstack/build.gradle b/modules/localstack/build.gradle new file mode 100644 index 00000000000..94f0ee4070f --- /dev/null +++ b/modules/localstack/build.gradle @@ -0,0 +1,8 @@ +description = "Testcontainers :: Localstack" + +dependencies { + compile project(':testcontainers') + + compileOnly 'com.amazonaws:aws-java-sdk-s3:1.11.126' + testCompile 'com.amazonaws:aws-java-sdk-s3:1.11.126' +} diff --git a/modules/localstack/src/main/java/org/testcontainers/containers/localstack/LocalStackContainer.java b/modules/localstack/src/main/java/org/testcontainers/containers/localstack/LocalStackContainer.java new file mode 100644 index 00000000000..0938aba09a3 --- /dev/null +++ b/modules/localstack/src/main/java/org/testcontainers/containers/localstack/LocalStackContainer.java @@ -0,0 +1,166 @@ +package org.testcontainers.containers.localstack; + +import com.amazonaws.auth.AWSCredentialsProvider; +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.client.builder.AwsClientBuilder; +import org.jetbrains.annotations.Nullable; +import org.junit.rules.ExternalResource; +import org.rnorth.ducttape.Preconditions; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.LogMessageWaitStrategy; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Arrays; +import java.util.stream.Collectors; + +import static org.testcontainers.containers.BindMode.READ_WRITE; + +/** + *

Container for Atlassian Labs Localstack, 'A fully functional local AWS cloud stack'.

+ *

{@link LocalStackContainer#withServices(Service...)} should be used to select which services + * are to be launched. See {@link Service} for available choices. It is advised that + * {@link LocalStackContainer#getEndpointConfiguration(Service)} and + * {@link LocalStackContainer#getDefaultCredentialsProvider()} + * be used to obtain compatible endpoint configuration and credentials, respectively.

+ */ +public class LocalStackContainer extends ExternalResource { + + @Nullable private GenericContainer delegate; + private Service[] services; + + @Override + protected void before() throws Throwable { + + Preconditions.check("services list must not be empty", services != null && services.length > 0); + + final String servicesList = Arrays + .stream(services) + .map(Service::getLocalStackName) + .collect(Collectors.joining(",")); + + final Integer[] portsList = Arrays + .stream(services) + .map(Service::getPort) + .collect(Collectors.toSet()).toArray(new Integer[]{}); + + delegate = new GenericContainer("atlassianlabs/localstack:0.6.0") + .withExposedPorts(portsList) + .withFileSystemBind("/var/run/docker.sock", "/var/run/docker.sock", READ_WRITE) + .waitingFor(new LogMessageWaitStrategy().withRegEx(".*Ready\\.\n")) + .withEnv("SERVICES", servicesList); + + delegate.start(); + } + + @Override + protected void after() { + + Preconditions.check("delegate must have been created by before()", delegate != null); + + delegate.stop(); + } + + /** + * Declare a set of simulated AWS services that should be launched by this container. + * @param services one or more service names + * @return this container object + */ + public LocalStackContainer withServices(Service... services) { + this.services = services; + return this; + } + + /** + * Provides an endpoint configuration that is preconfigured to communicate with a given simulated service. + * The provided endpoint configuration should be set in the AWS Java SDK when building a client, e.g.: + *
AmazonS3 s3 = AmazonS3ClientBuilder
+            .standard()
+            .withEndpointConfiguration(localstack.getEndpointConfiguration(S3))
+            .withCredentials(localstack.getDefaultCredentialsProvider())
+            .build();
+     
+ * @param service the service that is to be accessed + * @return an {@link AwsClientBuilder.EndpointConfiguration} + */ + public AwsClientBuilder.EndpointConfiguration getEndpointConfiguration(Service service) { + + if (delegate == null) { + throw new IllegalStateException("LocalStack has not been started yet!"); + } + + final String address = delegate.getContainerIpAddress(); + String ipAddress = address; + try { + ipAddress = InetAddress.getByName(address).getHostAddress(); + } catch (UnknownHostException ignored) { + + } + ipAddress = ipAddress + ".xip.io"; + while (true) { + try { + //noinspection ResultOfMethodCallIgnored + InetAddress.getAllByName(ipAddress); + break; + } catch (UnknownHostException ignored) { + + } + } + + return new AwsClientBuilder.EndpointConfiguration( + "http://" + + ipAddress + + ":" + + delegate.getMappedPort(service.getPort()), "us-east-1"); + } + + /** + * Provides a {@link AWSCredentialsProvider} that is preconfigured to communicate with a given simulated service. + * The credentials provider should be set in the AWS Java SDK when building a client, e.g.: + *
AmazonS3 s3 = AmazonS3ClientBuilder
+            .standard()
+            .withEndpointConfiguration(localstack.getEndpointConfiguration(S3))
+            .withCredentials(localstack.getDefaultCredentialsProvider())
+            .build();
+     
+ * @return an {@link AWSCredentialsProvider} + */ + public AWSCredentialsProvider getDefaultCredentialsProvider() { + return new AWSStaticCredentialsProvider(new BasicAWSCredentials("accesskey", "secretkey")); + } + + public enum Service { + API_GATEWAY("apigateway", 4567), + KINESIS("kinesis", 4568), + DYNAMODB("dynamodb", 4569), + DYNAMODB_STREAMS("dynamodbstreams", 4570), + // TODO: Clarify usage for ELASTICSEARCH and ELASTICSEARCH_SERVICE +// ELASTICSEARCH("es", 4571), + S3("s3", 4572), + FIREHOSE("firehose", 4573), + LAMBDA("lambda", 4574), + SNS("sns", 4575), + SQS("sqs", 4576), + REDSHIFT("redshift", 4577), +// ELASTICSEARCH_SERVICE("", 4578), + SES("ses", 4579), + ROUTE53("route53", 4580), + CLOUDFORMATION("cloudformation", 4581), + CLOUDWATCH("cloudwatch", 4582); + + private final String localStackName; + private final int port; + + Service(String localstackName, int port) { + this.localStackName = localstackName; + this.port = port; + } + + public String getLocalStackName() { + return localStackName; + } + + public Integer getPort() { return port; } + } +} diff --git a/modules/localstack/src/test/java/org/testcontainers/containers/localstack/SimpleLocalstackS3Test.java b/modules/localstack/src/test/java/org/testcontainers/containers/localstack/SimpleLocalstackS3Test.java new file mode 100644 index 00000000000..f00423d31b7 --- /dev/null +++ b/modules/localstack/src/test/java/org/testcontainers/containers/localstack/SimpleLocalstackS3Test.java @@ -0,0 +1,52 @@ +package org.testcontainers.containers.localstack; + + +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import com.amazonaws.services.s3.model.Bucket; +import com.amazonaws.services.s3.model.ObjectListing; +import com.amazonaws.services.s3.model.S3Object; +import org.apache.commons.io.IOUtils; +import org.junit.Rule; +import org.junit.Test; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.List; + +import static org.rnorth.visibleassertions.VisibleAssertions.assertEquals; +import static org.testcontainers.containers.localstack.LocalStackContainer.Service.S3; + +public class SimpleLocalstackS3Test { + + @Rule + public LocalStackContainer localstack = new LocalStackContainer() + .withServices(S3); + + @Test + public void s3Test() throws IOException { + AmazonS3 s3 = AmazonS3ClientBuilder + .standard() + .withEndpointConfiguration(localstack.getEndpointConfiguration(S3)) + .withCredentials(localstack.getDefaultCredentialsProvider()) + .build(); + + s3.createBucket("foo"); + s3.putObject("foo", "bar", "baz"); + + final List buckets = s3.listBuckets(); + assertEquals("The created bucket is present", 1, buckets.size()); + final Bucket bucket = buckets.get(0); + + assertEquals("The created bucket has the right name", "foo", bucket.getName()); + assertEquals("The created bucket has the right name", "foo", bucket.getName()); + + final ObjectListing objectListing = s3.listObjects("foo"); + assertEquals("The created bucket has 1 item in it", 1, objectListing.getObjectSummaries().size()); + + final S3Object object = s3.getObject("foo", "bar"); + final String content = IOUtils.toString(object.getObjectContent(), Charset.forName("UTF-8")); + assertEquals("The object can be retrieved", "baz", content); + + } +} diff --git a/modules/localstack/src/test/resources/logback-test.xml b/modules/localstack/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..cc69fdf0ce0 --- /dev/null +++ b/modules/localstack/src/test/resources/logback-test.xml @@ -0,0 +1,27 @@ + + + + + + %d{HH:mm:ss.SSS} %-5level %logger - %msg%n + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 896339f84b6cd68e56200751541b9fb09c2c10e8 Mon Sep 17 00:00:00 2001 From: Richard North Date: Sun, 11 Mar 2018 21:28:29 +0000 Subject: [PATCH 04/34] Relocate MS SQL Server module into main testcontainers-java repository --- .github/CODEOWNERS | 13 ++++ modules/jdbc-test/build.gradle | 2 + .../junit/SimpleMSSQLServerTest.java | 62 +++++++++++++++++++ modules/mssqlserver/AUTHORS | 1 + modules/mssqlserver/LICENSE | 21 +++++++ modules/mssqlserver/README.md | 50 +++++++++++++++ modules/mssqlserver/build.gradle | 5 ++ .../containers/MSSQLServerContainer.java | 58 +++++++++++++++++ .../MSSQLServerContainerProvider.java | 16 +++++ ...s.containers.JdbcDatabaseContainerProvider | 1 + 10 files changed, 229 insertions(+) create mode 100644 .github/CODEOWNERS create mode 100644 modules/jdbc-test/src/test/java/org/testcontainers/junit/SimpleMSSQLServerTest.java create mode 100644 modules/mssqlserver/AUTHORS create mode 100644 modules/mssqlserver/LICENSE create mode 100644 modules/mssqlserver/README.md create mode 100644 modules/mssqlserver/build.gradle create mode 100644 modules/mssqlserver/src/main/java/org/testcontainers/containers/MSSQLServerContainer.java create mode 100644 modules/mssqlserver/src/main/java/org/testcontainers/containers/MSSQLServerContainerProvider.java create mode 100644 modules/mssqlserver/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000000..071bb63fa29 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,13 @@ +# Each line is a file pattern followed by one or more owners. + +# These owners will be the default reviewers for everything in +# the repo. + +* @rnorth @bsideup @kiview + +# The last matching pattern takes the most +# precedence. + +# Contributed modules can have different reviewers + +modules/mssqlserver/ @StefanHufschmidt @rnorth @bsideup @kiview diff --git a/modules/jdbc-test/build.gradle b/modules/jdbc-test/build.gradle index f9b877a1899..0223781eecb 100644 --- a/modules/jdbc-test/build.gradle +++ b/modules/jdbc-test/build.gradle @@ -9,12 +9,14 @@ dependencies { compile project(':postgresql') compile project(':mariadb') compile project(':oracle-xe') + compile project(':mssqlserver') testCompile 'com.google.guava:guava:18.0' testCompile 'org.postgresql:postgresql:42.0.0' testCompile 'mysql:mysql-connector-java:5.1.45' testCompile 'org.mariadb.jdbc:mariadb-java-client:1.4.6' testCompile 'com.oracle:ojdbc6:12.1.0.1-atlassian-hosted' + testCompile 'com.microsoft.sqlserver:mssql-jdbc:6.1.0.jre8' testCompile 'com.zaxxer:HikariCP-java6:2.3.8' testCompile 'org.apache.tomcat:tomcat-jdbc:8.5.4' diff --git a/modules/jdbc-test/src/test/java/org/testcontainers/junit/SimpleMSSQLServerTest.java b/modules/jdbc-test/src/test/java/org/testcontainers/junit/SimpleMSSQLServerTest.java new file mode 100644 index 00000000000..93904cfb94a --- /dev/null +++ b/modules/jdbc-test/src/test/java/org/testcontainers/junit/SimpleMSSQLServerTest.java @@ -0,0 +1,62 @@ +package org.testcontainers.junit; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import org.junit.Rule; +import org.junit.Test; +import org.testcontainers.containers.MSSQLServerContainer; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import static org.rnorth.visibleassertions.VisibleAssertions.assertEquals; + +/** + * @author Stefan Hufschmidt + */ +public class SimpleMSSQLServerTest { + + @Rule + public MSSQLServerContainer mssqlServer = new MSSQLServerContainer(); + + @Test + public void testSimple() throws SQLException { + HikariConfig hikariConfig = new HikariConfig(); + hikariConfig.setJdbcUrl(mssqlServer.getJdbcUrl()); + hikariConfig.setUsername(mssqlServer.getUsername()); + hikariConfig.setPassword(mssqlServer.getPassword()); + + HikariDataSource ds = new HikariDataSource(hikariConfig); + Statement statement = ds.getConnection().createStatement(); + statement.execute("SELECT 1"); + ResultSet resultSet = statement.getResultSet(); + + resultSet.next(); + int resultSetInt = resultSet.getInt(1); + assertEquals("A basic SELECT query succeeds", 1, resultSetInt); + } + + @Test + public void testSetupDatabase() throws SQLException { + HikariConfig hikariConfig = new HikariConfig(); + hikariConfig.setJdbcUrl(mssqlServer.getJdbcUrl()); + hikariConfig.setUsername(mssqlServer.getUsername()); + hikariConfig.setPassword(mssqlServer.getPassword()); + + HikariDataSource ds = new HikariDataSource(hikariConfig); + Statement statement = ds.getConnection().createStatement(); + statement.executeUpdate("CREATE DATABASE [test];"); + statement = ds.getConnection().createStatement(); + statement.executeUpdate("CREATE TABLE [test].[dbo].[Foo](ID INT PRIMARY KEY);"); + statement = ds.getConnection().createStatement(); + statement.executeUpdate("INSERT INTO [test].[dbo].[Foo] (ID) VALUES (3);"); + statement = ds.getConnection().createStatement(); + statement.execute("SELECT * FROM [test].[dbo].[Foo];"); + ResultSet resultSet = statement.getResultSet(); + + resultSet.next(); + int resultSetInt = resultSet.getInt("ID"); + assertEquals("A basic SELECT query succeeds", 3, resultSetInt); + } +} diff --git a/modules/mssqlserver/AUTHORS b/modules/mssqlserver/AUTHORS new file mode 100644 index 00000000000..bc3ac920da0 --- /dev/null +++ b/modules/mssqlserver/AUTHORS @@ -0,0 +1 @@ +Stefan Hufschmidt \ No newline at end of file diff --git a/modules/mssqlserver/LICENSE b/modules/mssqlserver/LICENSE new file mode 100644 index 00000000000..a3c58006a9c --- /dev/null +++ b/modules/mssqlserver/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2017 - 2019 G DATA Software AG and other authors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/modules/mssqlserver/README.md b/modules/mssqlserver/README.md new file mode 100644 index 00000000000..82854c27961 --- /dev/null +++ b/modules/mssqlserver/README.md @@ -0,0 +1,50 @@ +# TestContainers MS SQL Server Module + +Testcontainers module for the MS SQL Server database. + +See [testcontainers.org](https://www.testcontainers.org) for more information about Testcontainers. + +## Usage example + +Running MS SQL Server as a stand-in for in a test: + +```java +public class SomeTest { + + @Rule + public MSSQLServerContainer mssqlserver = new MSSQLServerContainer(); + + @Test + public void someTestMethod() { + String url = mssqlserver.getJdbcUrl(); + + ... create a connection and run test as normal +``` + +## Dependency information + +### Maven + +``` + + org.testcontainers + mssqlserver + 1.4.3 + +``` + +### Gradle + +``` +compile group: 'org.testcontainers', name: 'mssqlserver', version: '1.4.3' +``` + +## License + +See [LICENSE](LICENSE). + +## Copyright + +Copyright (c) 2017 - 2019 G DATA Software AG and other authors. + +See [AUTHORS](AUTHORS) for contributors. diff --git a/modules/mssqlserver/build.gradle b/modules/mssqlserver/build.gradle new file mode 100644 index 00000000000..62434722d63 --- /dev/null +++ b/modules/mssqlserver/build.gradle @@ -0,0 +1,5 @@ +description = "Testcontainers :: MS SQL Server" + +dependencies { + compile project(':jdbc') +} diff --git a/modules/mssqlserver/src/main/java/org/testcontainers/containers/MSSQLServerContainer.java b/modules/mssqlserver/src/main/java/org/testcontainers/containers/MSSQLServerContainer.java new file mode 100644 index 00000000000..c1c90f39d83 --- /dev/null +++ b/modules/mssqlserver/src/main/java/org/testcontainers/containers/MSSQLServerContainer.java @@ -0,0 +1,58 @@ +package org.testcontainers.containers; + +/** + * @author Stefan Hufschmidt + */ +public class MSSQLServerContainer> extends JdbcDatabaseContainer { + static final String NAME = "mssqlserver"; + static final String IMAGE = "microsoft/mssql-server-linux"; + public static final Integer MS_SQL_SERVER_PORT = 1433; + private String username = "SA"; + private String password = "A_Str0ng_Required_Password"; + + public MSSQLServerContainer() { + this(IMAGE + ":latest"); + } + + public MSSQLServerContainer(final String dockerImageName) { + super(dockerImageName); + } + + @Override + protected Integer getLivenessCheckPort() { + return getMappedPort(MS_SQL_SERVER_PORT); + } + + @Override + protected void configure() { + + addExposedPort(MS_SQL_SERVER_PORT); + addEnv("ACCEPT_EULA", "Y"); + addEnv("SA_PASSWORD", password); + } + + @Override + public String getDriverClassName() { + return "com.microsoft.sqlserver.jdbc.SQLServerDriver"; + } + + @Override + public String getJdbcUrl() { + return "jdbc:sqlserver://" + getContainerIpAddress() + ":" + getMappedPort(MS_SQL_SERVER_PORT); + } + + @Override + public String getUsername() { + return username; + } + + @Override + public String getPassword() { + return password; + } + + @Override + public String getTestQueryString() { + return "SELECT 1"; + } +} diff --git a/modules/mssqlserver/src/main/java/org/testcontainers/containers/MSSQLServerContainerProvider.java b/modules/mssqlserver/src/main/java/org/testcontainers/containers/MSSQLServerContainerProvider.java new file mode 100644 index 00000000000..f7768482a45 --- /dev/null +++ b/modules/mssqlserver/src/main/java/org/testcontainers/containers/MSSQLServerContainerProvider.java @@ -0,0 +1,16 @@ +package org.testcontainers.containers; + +/** + * Factory for MS SQL Server containers. + */ +public class MSSQLServerContainerProvider extends JdbcDatabaseContainerProvider { + @Override + public boolean supports(String databaseType) { + return databaseType.equals(MSSQLServerContainer.NAME); + } + + @Override + public JdbcDatabaseContainer newInstance(String tag) { + return new MSSQLServerContainer(MSSQLServerContainer.IMAGE + ":" + tag); + } +} diff --git a/modules/mssqlserver/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider b/modules/mssqlserver/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider new file mode 100644 index 00000000000..09b0a068a7f --- /dev/null +++ b/modules/mssqlserver/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider @@ -0,0 +1 @@ +org.testcontainers.containers.MSSQLServerContainerProvider \ No newline at end of file From a4037f88eacddf754ce2c3fcbd018011a5910fbe Mon Sep 17 00:00:00 2001 From: Richard North Date: Sun, 11 Mar 2018 21:20:52 +0000 Subject: [PATCH 05/34] Relocate Dynalite module into main testcontainers-java repository --- modules/dynalite/README.md | 45 +++++++++++ modules/dynalite/build.gradle | 8 ++ .../dynamodb/DynaliteContainer.java | 76 +++++++++++++++++++ .../dynamodb/DynaliteContainerTest.java | 52 +++++++++++++ .../src/test/resources/logback-test.xml | 24 ++++++ 5 files changed, 205 insertions(+) create mode 100644 modules/dynalite/README.md create mode 100644 modules/dynalite/build.gradle create mode 100644 modules/dynalite/src/main/java/org/testcontainers/dynamodb/DynaliteContainer.java create mode 100644 modules/dynalite/src/test/java/org/testcontainers/dynamodb/DynaliteContainerTest.java create mode 100644 modules/dynalite/src/test/resources/logback-test.xml diff --git a/modules/dynalite/README.md b/modules/dynalite/README.md new file mode 100644 index 00000000000..c1c7880f20c --- /dev/null +++ b/modules/dynalite/README.md @@ -0,0 +1,45 @@ +# Testcontainers Dynalite Module + +Testcontainers module for [Dynalite](https://github.com/mhart/dynalite). Dynalite is a clone of DynamoDB, enabling local testing. + +## Usage example + +Running Dynalite as a stand-in for DynamoDB in a test: + +```java +public class SomeTest { + + @Rule + public DynaliteContainer dynamoDB = new DynaliteContainer(); + + @Test + public void someTestMethod() { + // getClient() returns a preconfigured DynamoDB client that is connected to the + // dynalite container + final AmazonDynamoDB client = dynamoDB.getClient(); + + ... interact with client as if using DynamoDB normally +``` + +## Why Dynalite for DynamoDB testing? + +In part, because it's light and quick to run. Also, please see the [reasons given](https://github.com/mhart/dynalite#why-not-amazons-dynamodb-local) by the author of Dynalite and the [problems with Amazon's DynamoDB Local](https://github.com/mhart/dynalite#problems-with-amazons-dynamodb-local-updated-2016-04-19). + +## Dependency information + +### Maven + +``` + + org.testcontainers + dynalite + 1.4.3 + +``` + +### Gradle + +``` +compile group: 'org.testcontainers', name: 'dynalite', version: '1.4.3' +``` + diff --git a/modules/dynalite/build.gradle b/modules/dynalite/build.gradle new file mode 100644 index 00000000000..1250ba677b3 --- /dev/null +++ b/modules/dynalite/build.gradle @@ -0,0 +1,8 @@ +description = "Testcontainers :: Dynalite" + +dependencies { + compile project(':testcontainers') + + compileOnly 'com.amazonaws:aws-java-sdk-dynamodb:1.11.126' + testCompile 'com.amazonaws:aws-java-sdk-dynamodb:1.11.126' +} diff --git a/modules/dynalite/src/main/java/org/testcontainers/dynamodb/DynaliteContainer.java b/modules/dynalite/src/main/java/org/testcontainers/dynamodb/DynaliteContainer.java new file mode 100644 index 00000000000..0b7369cd53e --- /dev/null +++ b/modules/dynalite/src/main/java/org/testcontainers/dynamodb/DynaliteContainer.java @@ -0,0 +1,76 @@ +package org.testcontainers.dynamodb; + +import com.amazonaws.auth.AWSCredentialsProvider; +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.client.builder.AwsClientBuilder; +import com.amazonaws.services.dynamodbv2.AmazonDynamoDB; +import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder; +import org.junit.rules.ExternalResource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.output.Slf4jLogConsumer; + +/** + * Container for Dynalite, a DynamoDB clone. + */ +public class DynaliteContainer extends ExternalResource { + + private static final Logger LOGGER = LoggerFactory.getLogger(DynaliteContainer.class); + private final GenericContainer delegate; + + public DynaliteContainer() { + this("richnorth/dynalite:v1.2.1-1"); + } + + public DynaliteContainer(String imageName) { + this.delegate = new GenericContainer(imageName) + .withExposedPorts(4567) + .withLogConsumer(new Slf4jLogConsumer(LOGGER)); + } + + /** + * Gets a preconfigured {@link AmazonDynamoDB} client object for connecting to this + * container. + * + * @return preconfigured client + */ + public AmazonDynamoDB getClient() { + return AmazonDynamoDBClientBuilder.standard() + .withEndpointConfiguration(getEndpointConfiguration()) + .withCredentials(getCredentials()) + .build(); + } + + /** + * Gets {@link AwsClientBuilder.EndpointConfiguration} + * that may be used to connect to this container. + * + * @return endpoint configuration + */ + public AwsClientBuilder.EndpointConfiguration getEndpointConfiguration() { + return new AwsClientBuilder.EndpointConfiguration("http://" + + this.delegate.getContainerIpAddress() + ":" + + this.delegate.getMappedPort(4567), null); + } + + /** + * Gets an {@link AWSCredentialsProvider} that may be used to connect to this container. + * + * @return dummy AWS credentials + */ + public AWSCredentialsProvider getCredentials() { + return new AWSStaticCredentialsProvider(new BasicAWSCredentials("dummy", "dummy")); + } + + @Override + protected void before() throws Throwable { + delegate.start(); + } + + @Override + protected void after() { + delegate.stop(); + } +} diff --git a/modules/dynalite/src/test/java/org/testcontainers/dynamodb/DynaliteContainerTest.java b/modules/dynalite/src/test/java/org/testcontainers/dynamodb/DynaliteContainerTest.java new file mode 100644 index 00000000000..2d414aecb3b --- /dev/null +++ b/modules/dynalite/src/test/java/org/testcontainers/dynamodb/DynaliteContainerTest.java @@ -0,0 +1,52 @@ +package org.testcontainers.dynamodb; + +import com.amazonaws.services.dynamodbv2.AmazonDynamoDB; +import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder; +import com.amazonaws.services.dynamodbv2.model.*; +import org.junit.Rule; +import org.junit.Test; + +import static org.rnorth.visibleassertions.VisibleAssertions.assertEquals; +import static org.rnorth.visibleassertions.VisibleAssertions.assertNotNull; + +public class DynaliteContainerTest { + + @Rule + public DynaliteContainer dynamoDB = new DynaliteContainer(); + + @Test + public void simpleTestWithManualClientCreation() { + final AmazonDynamoDB client = AmazonDynamoDBClientBuilder.standard() + .withEndpointConfiguration(dynamoDB.getEndpointConfiguration()) + .withCredentials(dynamoDB.getCredentials()) + .build(); + + runTest(client); + } + + @Test + public void simpleTestWithProvidedClient() { + final AmazonDynamoDB client = dynamoDB.getClient(); + + runTest(client); + } + + private void runTest(AmazonDynamoDB client) { + CreateTableRequest request = new CreateTableRequest() + .withAttributeDefinitions(new AttributeDefinition( + "Name", ScalarAttributeType.S)) + .withKeySchema(new KeySchemaElement("Name", KeyType.HASH)) + .withProvisionedThroughput(new ProvisionedThroughput( + new Long(10), new Long(10))) + .withTableName("foo"); + + + client.createTable(request); + + final TableDescription tableDescription = client.describeTable("foo").getTable(); + + assertNotNull("the description is not null", tableDescription); + assertEquals("the table has the right name", "foo", tableDescription.getTableName()); + assertEquals("the name has the right primary key", "Name", tableDescription.getKeySchema().get(0).getAttributeName()); + } +} \ No newline at end of file diff --git a/modules/dynalite/src/test/resources/logback-test.xml b/modules/dynalite/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..5d27573a3cb --- /dev/null +++ b/modules/dynalite/src/test/resources/logback-test.xml @@ -0,0 +1,24 @@ + + + + + + %d{HH:mm:ss.SSS} %-5level %logger - %msg%n + + + + + + + + + + + + + + + + + \ No newline at end of file From b2351d6ea321ca1370428f1b9af948db9d164929 Mon Sep 17 00:00:00 2001 From: Richard North Date: Tue, 13 Mar 2018 19:28:18 +0000 Subject: [PATCH 06/34] Relocate Vault module into main testcontainers-java repository --- .github/CODEOWNERS | 1 + modules/vault/AUTHORS | 1 + modules/vault/LICENSE | 21 +++ modules/vault/README.md | 66 +++++++++ modules/vault/build.gradle | 7 + .../testcontainers/vault/VaultContainer.java | 133 ++++++++++++++++++ .../vault/VaultContainerTest.java | 79 +++++++++++ .../vault/src/test/resources/logback-test.xml | 24 ++++ 8 files changed, 332 insertions(+) create mode 100644 modules/vault/AUTHORS create mode 100644 modules/vault/LICENSE create mode 100644 modules/vault/README.md create mode 100644 modules/vault/build.gradle create mode 100644 modules/vault/src/main/java/org/testcontainers/vault/VaultContainer.java create mode 100644 modules/vault/src/test/java/org/testcontainers/vault/VaultContainerTest.java create mode 100644 modules/vault/src/test/resources/logback-test.xml diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 071bb63fa29..add6048a437 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -11,3 +11,4 @@ # Contributed modules can have different reviewers modules/mssqlserver/ @StefanHufschmidt @rnorth @bsideup @kiview +modules/vault/ @mikeoswald @rnorth @bsideup @kiview diff --git a/modules/vault/AUTHORS b/modules/vault/AUTHORS new file mode 100644 index 00000000000..528a7856aca --- /dev/null +++ b/modules/vault/AUTHORS @@ -0,0 +1 @@ +Michael Oswald \ No newline at end of file diff --git a/modules/vault/LICENSE b/modules/vault/LICENSE new file mode 100644 index 00000000000..957404f59b9 --- /dev/null +++ b/modules/vault/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2017 Capital One Services, LLC and other authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/modules/vault/README.md b/modules/vault/README.md new file mode 100644 index 00000000000..b5b33a17e08 --- /dev/null +++ b/modules/vault/README.md @@ -0,0 +1,66 @@ +# TestContainers Vault Module + +Testcontainers module for [Vault](https://github.com/hashicorp/vault). Vault is a tool for managing secrets. More information on Vault [here](https://www.vaultproject.io/). + +## Usage example + +Running Vault in your Junit tests is easily done with an @Rule or @ClassRule such as the following: + +```java +public class SomeTest { + + @ClassRule + public static VaultContainer vaultContainer = new VaultContainer<>() + .withVaultToken("my-root-token") + .withVaultPort(8200) + .withSecretInVault("secret/testing", "top_secret=password1","db_password=dbpassword1"); + + @Test + public void someTestMethod() { + //interact with Vault via the container's host, port and Vault token. + + //There are many integration clients for Vault so let's just define a general one here: + VaultClient client = new VaultClient( + vaultContainer.getContainerIpAddress(), + vaultContainer.getMappedPort(8200), + "my-root-token"); + + List secrets = client.readSecret("secret/testing"); + + } +``` + +## Why Vault in Junit tests? + +With the increasing popularity of Vault and secret management, applications are now needing to source secrets from Vault. +This can prove challenging in the development phase without a running Vault instance readily on hand. This library +aims to solve your apps integration testing with Vault. You can also use it to +test how your application behaves with Vault by writing different test scenarios in Junit. + +## Dependency information + +### Maven + +``` + + org.testcontainers + vault + 1.4.3 + +``` + +### Gradle + +``` +compile group: 'org.testcontainers', name: 'vault', version: '1.4.3' +``` + +## License + +See [LICENSE](LICENSE). + +## Copyright + +Copyright (c) 2017 Capital One Services, LLC and other authors. + +See [AUTHORS](AUTHORS) for contributors. diff --git a/modules/vault/build.gradle b/modules/vault/build.gradle new file mode 100644 index 00000000000..abff412b9a9 --- /dev/null +++ b/modules/vault/build.gradle @@ -0,0 +1,7 @@ +description = "Testcontainers :: Vault" + +dependencies { + compile project(':testcontainers') + + testCompile 'io.rest-assured:rest-assured:3.0.0' +} diff --git a/modules/vault/src/main/java/org/testcontainers/vault/VaultContainer.java b/modules/vault/src/main/java/org/testcontainers/vault/VaultContainer.java new file mode 100644 index 00000000000..3f0f7684e82 --- /dev/null +++ b/modules/vault/src/main/java/org/testcontainers/vault/VaultContainer.java @@ -0,0 +1,133 @@ +package org.testcontainers.vault; + +import com.github.dockerjava.api.command.InspectContainerResponse; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.traits.LinkableContainer; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static com.github.dockerjava.api.model.Capability.IPC_LOCK; + + +/** + * GenericContainer subclass for Vault specific configuration and features. The main feature is the + * withSecretInVault method, where users can specify which secrets to be pre-loaded into Vault for + * their specific test scenario. + * + * Other helpful features include the withVaultPort, and withVaultToken methods for convenience. + */ +public class VaultContainer> extends GenericContainer + implements LinkableContainer { + + private static final String VAULT_PORT = "8200"; + + private boolean vaultPortRequested = false; + + private Map> secretsMap = new HashMap<>(); + + public VaultContainer() { + this("vault:0.7.0"); + } + + public VaultContainer(String dockerImageName) { + super(dockerImageName); + } + + @Override + protected void configure() { + setStartupAttempts(3); + withCreateContainerCmdModifier(cmd -> cmd.withCapAdd(IPC_LOCK)); + if(!isVaultPortRequested()){ + withEnv("VAULT_ADDR", "http://0.0.0.0:" + VAULT_PORT); + } + } + + @Override + protected void containerIsStarted(InspectContainerResponse containerInfo) { + addSecrets(); + } + + private void addSecrets() { + if(!secretsMap.isEmpty()){ + try { + this.execInContainer(buildExecCommand(secretsMap)).getStdout().contains("Success"); + } + catch (IOException | InterruptedException e) { + logger().error("Failed to add these secrets {} into Vault via exec command. Exception message: {}", secretsMap, e.getMessage()); + } + } + } + + private String[] buildExecCommand(Map> map) { + StringBuilder stringBuilder = new StringBuilder(); + map.forEach((path, secrets) -> { + stringBuilder.append(" && vault write " + path); + secrets.forEach(item -> stringBuilder.append(" " + item)); + }); + return new String[] { "/bin/sh", "-c", stringBuilder.toString().substring(4)}; + } + + /** + * Sets the Vault root token for the container so application tests can source secrets using the token + * + * @param token the root token value to set for Vault. + * @return this + */ + public SELF withVaultToken(String token) { + withEnv("VAULT_DEV_ROOT_TOKEN_ID", token); + withEnv("VAULT_TOKEN", token); + return self(); + } + + /** + * Sets the Vault port in the container as well as the port bindings for the host to reach the container over HTTP. + * + * @param port the port number you want to have the Vault container listen on for tests. + * @return this + */ + public SELF withVaultPort(int port){ + setVaultPortRequested(true); + String vaultPort = String.valueOf(port); + withEnv("VAULT_ADDR", "http://0.0.0.0:" + VAULT_PORT); + setPortBindings(Arrays.asList(vaultPort + ":" + VAULT_PORT)); + return self(); + } + + /** + * Pre-loads secrets into Vault container. User may specify one or more secrets and all will be added to each path + * that is specified. Thus this can be called more than once for multiple paths to be added to Vault. + * + * The secrets are added to vault directly after the container is up via the + * {@link #addSecrets() addSecrets}, called from {@link #containerIsStarted(InspectContainerResponse) containerIsStarted} + * + * @param path specific Vault path to store specified secrets + * @param firstSecret first secret to add to specifed path + * @param remainingSecrets var args list of secrets to add to specified path + * @return this + */ + public SELF withSecretInVault(String path, String firstSecret, String... remainingSecrets) { + List list = new ArrayList<>(); + list.add(firstSecret); + for(String secret : remainingSecrets) { + list.add(secret); + } + if (secretsMap.containsKey(path)) { + list.addAll(list); + } + secretsMap.putIfAbsent(path,list); + return self(); + } + + private void setVaultPortRequested(boolean vaultPortRequested) { + this.vaultPortRequested = vaultPortRequested; + } + + private boolean isVaultPortRequested() { + return vaultPortRequested; + } +} \ No newline at end of file diff --git a/modules/vault/src/test/java/org/testcontainers/vault/VaultContainerTest.java b/modules/vault/src/test/java/org/testcontainers/vault/VaultContainerTest.java new file mode 100644 index 00000000000..5e1c55b33ac --- /dev/null +++ b/modules/vault/src/test/java/org/testcontainers/vault/VaultContainerTest.java @@ -0,0 +1,79 @@ +package org.testcontainers.vault; + +import org.junit.ClassRule; +import org.junit.Test; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.Wait; + +import java.io.IOException; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.junit.Assert.assertThat; + +/** + * This test shows the pattern to use the VaultContainer @ClassRule for a junit test. It also has tests that ensure + * the secrets were added correctly by reading from Vault with the CLI and over HTTP. + */ +public class VaultContainerTest { + + private static final int VAULT_PORT = 8201; //using non-default port to show other ports can be passed besides 8200 + + private static final String VAULT_TOKEN = "my-root-token"; + + @ClassRule + public static VaultContainer vaultContainer = new VaultContainer<>() + .withVaultToken(VAULT_TOKEN) + .withVaultPort(VAULT_PORT) + .withSecretInVault("secret/testing1", "top_secret=password123") + .withSecretInVault("secret/testing2", "secret_one=password1", + "secret_two=password2", "secret_three=password3", "secret_three=password3", + "secret_four=password4") + .waitingFor(Wait.forHttp("/v1/secret/testing1").forStatusCode(400)); + + @Test + public void readFirstSecretPathWithCli() throws IOException, InterruptedException { + GenericContainer.ExecResult result = vaultContainer.execInContainer("vault", + "read", "-field=top_secret", "secret/testing1"); + assertThat(result.getStdout(), containsString("password123")); + } + + @Test + public void readSecondSecretPathWithCli() throws IOException, InterruptedException { + GenericContainer.ExecResult result = vaultContainer.execInContainer("vault", + "read", "secret/testing2"); + String output = result.getStdout(); + assertThat(output, containsString("password1")); + assertThat(output, containsString("password2")); + assertThat(output, containsString("password3")); + assertThat(output, containsString("password4")); + } + + @Test + public void readFirstSecretPathOverHttpApi() throws InterruptedException { + given(). + header("X-Vault-Token", VAULT_TOKEN). + when(). + get("http://"+getHostAndPort()+"/v1/secret/testing1"). + then(). + assertThat().body("data.top_secret", equalTo("password123")); + } + + @Test + public void readSecondecretPathOverHttpApi() throws InterruptedException { + given(). + header("X-Vault-Token", VAULT_TOKEN). + when(). + get("http://"+getHostAndPort()+"/v1/secret/testing2"). + then(). + assertThat().body("data.secret_one", containsString("password1")). + assertThat().body("data.secret_two", containsString("password2")). + assertThat().body("data.secret_three", containsString("password3")). + assertThat().body("data.secret_four", containsString("password4")); + } + + private String getHostAndPort(){ + return vaultContainer.getContainerIpAddress()+":"+vaultContainer.getMappedPort(8200); + } +} diff --git a/modules/vault/src/test/resources/logback-test.xml b/modules/vault/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..5d27573a3cb --- /dev/null +++ b/modules/vault/src/test/resources/logback-test.xml @@ -0,0 +1,24 @@ + + + + + + %d{HH:mm:ss.SSS} %-5level %logger - %msg%n + + + + + + + + + + + + + + + + + \ No newline at end of file From 0dff7e58758c472d974f80ba784aa1a1a09bb34e Mon Sep 17 00:00:00 2001 From: Richard North Date: Mon, 12 Mar 2018 14:07:04 +0000 Subject: [PATCH 07/34] Initial docs update referring to new location of modules --- docs/usage/modules.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/usage/modules.md b/docs/usage/modules.md index e8b91487625..892a051acf0 100644 --- a/docs/usage/modules.md +++ b/docs/usage/modules.md @@ -1,10 +1,10 @@ # Contributed modules -The following modules have been largely developed outside of the Testcontainers core project and are maintained and released separately: +The following modules are available for specialised use cases: -* [MS SQL Server](https://github.com/testcontainers/testcontainers-java-module-mssqlserver) -* [Oracle XE](https://github.com/testcontainers/testcontainers-java-module-oracle-xe) -* [Hashicorp Vault](https://github.com/testcontainers/testcontainers-java-module-vault) -* [Dynalite](https://github.com/testcontainers/testcontainers-java-module-dynalite) (Amazon DynamoDB emulation) -* [Atlassian Localstack](https://github.com/testcontainers/testcontainers-java-module-localstack) (emulation of a broad set of various AWS services) -* [MariaDB](https://github.com/testcontainers/testcontainers-java-module-mariadb) \ No newline at end of file +* [MS SQL Server](../../modules/mssqlserver/README.md) +* [Oracle XE](../../modules/oracle-xe/README.md) +* [Dynalite](../../modules/dynalite/README.md) (Amazon DynamoDB emulation) +* [Atlassian Localstack](../../modules/localstack/README.md) (emulation of a broad set of various AWS services) +* [MariaDB](../../modules/mariadb/README.md) +* [Hashicorp Vault](../../modules/vault/README.md) From 9f1ca7b1e7743a039dbd2d5895905d88b653ee2a Mon Sep 17 00:00:00 2001 From: Richard North Date: Tue, 13 Mar 2018 20:02:34 +0000 Subject: [PATCH 08/34] Update copyright and thanks --- README.md | 6 +++++- docs/usage/modules.md | 4 ++-- modules/mssqlserver/README.md | 2 -- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 16bcd0120ec..92cc9972cd9 100644 --- a/README.md +++ b/README.md @@ -16,4 +16,8 @@ See [LICENSE](LICENSE). Copyright (c) 2015, 2016 Richard North and other authors. -See [AUTHORS](AUTHORS) for contributors. +MS SQL Server module is (c) 2017 - 2019 G DATA Software AG and other authors. + +Hashicorp Vault module is (c) 2017 Capital One Services, LLC and other authors. + +See [AUTHORS](AUTHORS) for all contributors. diff --git a/docs/usage/modules.md b/docs/usage/modules.md index 892a051acf0..788c5f561b2 100644 --- a/docs/usage/modules.md +++ b/docs/usage/modules.md @@ -2,9 +2,9 @@ The following modules are available for specialised use cases: -* [MS SQL Server](../../modules/mssqlserver/README.md) +* [MS SQL Server](../../modules/mssqlserver/README.md) - thank you to G DATA Software AG for contributing this module. * [Oracle XE](../../modules/oracle-xe/README.md) * [Dynalite](../../modules/dynalite/README.md) (Amazon DynamoDB emulation) * [Atlassian Localstack](../../modules/localstack/README.md) (emulation of a broad set of various AWS services) * [MariaDB](../../modules/mariadb/README.md) -* [Hashicorp Vault](../../modules/vault/README.md) +* [Hashicorp Vault](../../modules/vault/README.md) - thank you to Capital One Services, LLC for contributing this module. diff --git a/modules/mssqlserver/README.md b/modules/mssqlserver/README.md index 82854c27961..9d484043d6b 100644 --- a/modules/mssqlserver/README.md +++ b/modules/mssqlserver/README.md @@ -2,8 +2,6 @@ Testcontainers module for the MS SQL Server database. -See [testcontainers.org](https://www.testcontainers.org) for more information about Testcontainers. - ## Usage example Running MS SQL Server as a stand-in for in a test: From 8bf2d1c33056e20cf120c11610ecb69b0ac79ff7 Mon Sep 17 00:00:00 2001 From: Richard North Date: Tue, 13 Mar 2018 21:26:26 +0000 Subject: [PATCH 09/34] Change rate limiter usage to prevent excessive connection creation Intended to eliminate `ORA-12516, TNS:listener could not find available handler` error --- .../containers/JdbcDatabaseContainer.java | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/modules/jdbc/src/main/java/org/testcontainers/containers/JdbcDatabaseContainer.java b/modules/jdbc/src/main/java/org/testcontainers/containers/JdbcDatabaseContainer.java index 1c7b7c71f05..0d64444847a 100644 --- a/modules/jdbc/src/main/java/org/testcontainers/containers/JdbcDatabaseContainer.java +++ b/modules/jdbc/src/main/java/org/testcontainers/containers/JdbcDatabaseContainer.java @@ -29,9 +29,9 @@ public abstract class JdbcDatabaseContainer parameters = new HashMap<>(); private static final RateLimiter DB_CONNECT_RATE_LIMIT = RateLimiterBuilder.newBuilder() - .withRate(10, TimeUnit.SECONDS) - .withConstantThroughput() - .build(); + .withRate(10, TimeUnit.SECONDS) + .withConstantThroughput() + .build(); public JdbcDatabaseContainer(@NonNull final String dockerImageName) { super(dockerImageName); @@ -97,7 +97,7 @@ protected void waitUntilContainerStarted() { throw new ContainerLaunchException("Container failed to start"); } - try (Connection connection = DB_CONNECT_RATE_LIMIT.getWhenReady(() -> createConnection(""))) { + try (Connection connection = createConnection("")) { boolean success = connection.createStatement().execute(JdbcDatabaseContainer.this.getTestQueryString()); if (success) { @@ -133,9 +133,8 @@ public Driver getJdbcDriverInstance() { /** * Creates a connection to the underlying containerized database instance. * - * @param queryString - * query string parameters that should be appended to the JDBC connection URL. - * The '?' character must be included + * @param queryString query string parameters that should be appended to the JDBC connection URL. + * The '?' character must be included * @return a Connection * @throws SQLException if there is a repeated failure to create the connection */ @@ -148,7 +147,9 @@ public Connection createConnection(String queryString) throws SQLException { final Driver jdbcDriverInstance = getJdbcDriverInstance(); try { - return Unreliables.retryUntilSuccess(120, TimeUnit.SECONDS, () -> jdbcDriverInstance.connect(url, info)); + return Unreliables.retryUntilSuccess(120, TimeUnit.SECONDS, () -> + DB_CONNECT_RATE_LIMIT.getWhenReady(() -> + jdbcDriverInstance.connect(url, info))); } catch (Exception e) { throw new SQLException("Could not create new connection", e); } @@ -159,9 +160,8 @@ public Connection createConnection(String queryString) throws SQLException { * This should be overridden if the JDBC URL and query string concatenation or URL string * construction needs to be different to normal. * - * @param queryString - * query string parameters that should be appended to the JDBC connection URL. - * The '?' character must be included + * @param queryString query string parameters that should be appended to the JDBC connection URL. + * The '?' character must be included * @return a full JDBC URL including queryString */ protected String constructUrlForConnection(String queryString) { From b737b982ae07da67ad8aa4509ee8fe02c63288fc Mon Sep 17 00:00:00 2001 From: Richard North Date: Sun, 18 Mar 2018 19:59:15 +0000 Subject: [PATCH 10/34] Add tests for pulling various permutations of image registry/name/tag --- .../images/RemoteDockerImage.java | 56 +++++++++++++----- .../dockerclient/ImagePullTest.java | 58 +++++++++++++++++++ 2 files changed, 99 insertions(+), 15 deletions(-) create mode 100644 core/src/test/java/org/testcontainers/dockerclient/ImagePullTest.java diff --git a/core/src/main/java/org/testcontainers/images/RemoteDockerImage.java b/core/src/main/java/org/testcontainers/images/RemoteDockerImage.java index bea12e48d90..ff8491c0890 100644 --- a/core/src/main/java/org/testcontainers/images/RemoteDockerImage.java +++ b/core/src/main/java/org/testcontainers/images/RemoteDockerImage.java @@ -24,20 +24,39 @@ public class RemoteDockerImage extends LazyFuture { public static final Set AVAILABLE_IMAGE_NAME_CACHE = new HashSet<>(); private final String dockerImageName; + private final String tag; + private final String tagSeparator; public RemoteDockerImage(String dockerImageName) { DockerImageName.validate(dockerImageName); - this.dockerImageName = dockerImageName; + final String[] splitOnSha = dockerImageName.split("@sha256:"); + if (splitOnSha.length > 1) { + this.dockerImageName = splitOnSha[0]; + this.tag = "sha256:" + splitOnSha[1]; + this.tagSeparator = "@"; + } else { + final int splitOnColon = dockerImageName.lastIndexOf(":"); + this.dockerImageName = dockerImageName.substring(0, splitOnColon); + this.tag = dockerImageName.substring(splitOnColon + 1); + this.tagSeparator = ":"; + } } public RemoteDockerImage(@NonNull String repository, @NonNull String tag) { - this.dockerImageName = repository + ":" + tag; + this.dockerImageName = repository; + this.tag = tag; + + if (tag.startsWith("sha256:")) { + this.tagSeparator = "@"; + } else { + this.tagSeparator = ":"; + } } @Override protected final String resolve() { Profiler profiler = new Profiler("Rule creation - prefetch image"); - Logger logger = DockerLoggerFactory.getLogger(dockerImageName); + Logger logger = DockerLoggerFactory.getLogger(concatenatedName()); profiler.setLogger(logger); Profiler nested = profiler.startNested("Obtaining client"); @@ -51,8 +70,8 @@ protected final String resolve() { DockerClientException lastException = null; while (true) { // Does our cache already know the image? - if (AVAILABLE_IMAGE_NAME_CACHE.contains(dockerImageName)) { - logger.trace("{} is already in image name cache", dockerImageName); + if (AVAILABLE_IMAGE_NAME_CACHE.contains(concatenatedName())) { + logger.trace("{} is already in image name cache", concatenatedName()); break; } @@ -60,7 +79,7 @@ protected final String resolve() { ListImagesCmd listImagesCmd = dockerClient.listImagesCmd(); if (Boolean.parseBoolean(System.getProperty("useFilter"))) { - listImagesCmd = listImagesCmd.withImageNameFilter(dockerImageName); + listImagesCmd = listImagesCmd.withImageNameFilter(concatenatedName()); } List updatedImages = listImagesCmd.exec(); @@ -71,39 +90,46 @@ protected final String resolve() { } // And now? - if (AVAILABLE_IMAGE_NAME_CACHE.contains(dockerImageName)) { - logger.trace("{} is in image name cache following listing of images", dockerImageName); + if (AVAILABLE_IMAGE_NAME_CACHE.contains(concatenatedName())) { + logger.trace("{} is in image name cache following listing of images", concatenatedName()); break; } // Log only on first attempt if (attempts == 0) { - logger.info("Pulling docker image: {}. Please be patient; this may take some time but only needs to be done once.", dockerImageName); + logger.info("Pulling docker image: {}. Please be patient; this may take some time but only needs to be done once.", concatenatedName()); profiler.start("Pull image"); } if (attempts++ >= 3) { - logger.error("Retry limit reached while trying to pull image: {}" + dockerImageName + ". Please check output of `docker pull {}`", dockerImageName, dockerImageName); - throw new ContainerFetchException("Retry limit reached while trying to pull image: " + dockerImageName, lastException); + logger.error("Retry limit reached while trying to pull image: {}. Please check output of `docker pull {}`", concatenatedName(), concatenatedName()); + throw new ContainerFetchException("Retry limit reached while trying to pull image: " + concatenatedName(), lastException); } // The image is not available locally - pull it try { final PullImageResultCallback callback = new PullImageResultCallback(); - dockerClient.pullImageCmd(dockerImageName).exec(callback); + dockerClient + .pullImageCmd(dockerImageName) + .withTag(tag) + .exec(callback); callback.awaitSuccess(); - AVAILABLE_IMAGE_NAME_CACHE.add(dockerImageName); + AVAILABLE_IMAGE_NAME_CACHE.add(concatenatedName()); break; } catch (DockerClientException e) { lastException = e; } } - return dockerImageName; + return concatenatedName(); } catch (DockerClientException e) { - throw new ContainerFetchException("Failed to get Docker client for " + dockerImageName, e); + throw new ContainerFetchException("Failed to get Docker client for " + concatenatedName(), e); } finally { profiler.stop().log(); } } + + private String concatenatedName() { + return dockerImageName + tagSeparator + tag; + } } diff --git a/core/src/test/java/org/testcontainers/dockerclient/ImagePullTest.java b/core/src/test/java/org/testcontainers/dockerclient/ImagePullTest.java new file mode 100644 index 00000000000..69556e461df --- /dev/null +++ b/core/src/test/java/org/testcontainers/dockerclient/ImagePullTest.java @@ -0,0 +1,58 @@ +package org.testcontainers.dockerclient; + +import org.junit.Test; +import org.testcontainers.containers.GenericContainer; + +public class ImagePullTest { + + @Test + public void pullOfficialLatestImageTest() { + doPullStartAndStop("alpine:latest"); + } + + @Test + public void pullOfficialImageByTagTest() { + doPullStartAndStop("alpine:3.6"); + } + + @Test + public void pullOfficialImageByShaTest() { + doPullStartAndStop("alpine@sha256:8fd4b76819e1e5baac82bd0a3d03abfe3906e034cc5ee32100d12aaaf3956dc7"); + } + + @Test + public void pullLatestImageTest() { + doPullStartAndStop("gliderlabs/alpine:latest"); + } + + @Test + public void pullImageByTagTest() { + doPullStartAndStop("gliderlabs/alpine:3.5"); + } + + @Test + public void pullImageByShaTest() { + doPullStartAndStop("gliderlabs/alpine@sha256:a19aa4a17a525c97e5a90a0c53a9f3329d2dc61b0a14df5447757a865671c085"); + } + + @Test + public void pullLatestImageFromPublicRegistryTest() { + doPullStartAndStop("quay.io/coreos/etcd:latest"); + } + + @Test + public void pullImageByTagFromPublicRegistryTest() { + doPullStartAndStop("quay.io/coreos/etcd:v3.1"); + } + + @Test + public void pullImageByShaFromPublicRegistryTest() { + doPullStartAndStop("quay.io/coreos/etcd@sha256:39a30367cd1f3186d540a063ea0257353c8f81b0d3c920c87c7e0f602bb6197c"); + } + + private void doPullStartAndStop(String s) { + final GenericContainer container = new GenericContainer<>(s); + container.start(); + container.stop(); + } +} From 24cc3989d0098758cd6f947ec48db7f3fa4b9da2 Mon Sep 17 00:00:00 2001 From: Richard North Date: Sun, 18 Mar 2018 21:21:12 +0000 Subject: [PATCH 11/34] Extract logic for processing docker image names Add further tests for docker image name processing --- .../images/RemoteDockerImage.java | 58 ++----- .../dockerfile/traits/FromStatementTrait.java | 2 +- .../utility/DockerImageName.java | 163 +++++++++++++++++- .../utility/DockerImageNameTest.java | 43 ++++- 4 files changed, 208 insertions(+), 58 deletions(-) diff --git a/core/src/main/java/org/testcontainers/images/RemoteDockerImage.java b/core/src/main/java/org/testcontainers/images/RemoteDockerImage.java index ff8491c0890..8342ae10a6d 100644 --- a/core/src/main/java/org/testcontainers/images/RemoteDockerImage.java +++ b/core/src/main/java/org/testcontainers/images/RemoteDockerImage.java @@ -23,40 +23,20 @@ public class RemoteDockerImage extends LazyFuture { public static final Set AVAILABLE_IMAGE_NAME_CACHE = new HashSet<>(); - private final String dockerImageName; - private final String tag; - private final String tagSeparator; + private DockerImageName imageName; public RemoteDockerImage(String dockerImageName) { - DockerImageName.validate(dockerImageName); - final String[] splitOnSha = dockerImageName.split("@sha256:"); - if (splitOnSha.length > 1) { - this.dockerImageName = splitOnSha[0]; - this.tag = "sha256:" + splitOnSha[1]; - this.tagSeparator = "@"; - } else { - final int splitOnColon = dockerImageName.lastIndexOf(":"); - this.dockerImageName = dockerImageName.substring(0, splitOnColon); - this.tag = dockerImageName.substring(splitOnColon + 1); - this.tagSeparator = ":"; - } + imageName = new DockerImageName(dockerImageName); } public RemoteDockerImage(@NonNull String repository, @NonNull String tag) { - this.dockerImageName = repository; - this.tag = tag; - - if (tag.startsWith("sha256:")) { - this.tagSeparator = "@"; - } else { - this.tagSeparator = ":"; - } + imageName = new DockerImageName(repository, tag); } @Override protected final String resolve() { Profiler profiler = new Profiler("Rule creation - prefetch image"); - Logger logger = DockerLoggerFactory.getLogger(concatenatedName()); + Logger logger = DockerLoggerFactory.getLogger(imageName.toString()); profiler.setLogger(logger); Profiler nested = profiler.startNested("Obtaining client"); @@ -70,8 +50,8 @@ protected final String resolve() { DockerClientException lastException = null; while (true) { // Does our cache already know the image? - if (AVAILABLE_IMAGE_NAME_CACHE.contains(concatenatedName())) { - logger.trace("{} is already in image name cache", concatenatedName()); + if (AVAILABLE_IMAGE_NAME_CACHE.contains(imageName.toString())) { + logger.trace("{} is already in image name cache", imageName.toString()); break; } @@ -79,7 +59,7 @@ protected final String resolve() { ListImagesCmd listImagesCmd = dockerClient.listImagesCmd(); if (Boolean.parseBoolean(System.getProperty("useFilter"))) { - listImagesCmd = listImagesCmd.withImageNameFilter(concatenatedName()); + listImagesCmd = listImagesCmd.withImageNameFilter(imageName.toString()); } List updatedImages = listImagesCmd.exec(); @@ -90,46 +70,42 @@ protected final String resolve() { } // And now? - if (AVAILABLE_IMAGE_NAME_CACHE.contains(concatenatedName())) { - logger.trace("{} is in image name cache following listing of images", concatenatedName()); + if (AVAILABLE_IMAGE_NAME_CACHE.contains(imageName.toString())) { + logger.trace("{} is in image name cache following listing of images", imageName.toString()); break; } // Log only on first attempt if (attempts == 0) { - logger.info("Pulling docker image: {}. Please be patient; this may take some time but only needs to be done once.", concatenatedName()); + logger.info("Pulling docker image: {}. Please be patient; this may take some time but only needs to be done once.", imageName.toString()); profiler.start("Pull image"); } if (attempts++ >= 3) { - logger.error("Retry limit reached while trying to pull image: {}. Please check output of `docker pull {}`", concatenatedName(), concatenatedName()); - throw new ContainerFetchException("Retry limit reached while trying to pull image: " + concatenatedName(), lastException); + logger.error("Retry limit reached while trying to pull image: {}. Please check output of `docker pull {}`", imageName.toString(), imageName.toString()); + throw new ContainerFetchException("Retry limit reached while trying to pull image: " + imageName.toString(), lastException); } // The image is not available locally - pull it try { final PullImageResultCallback callback = new PullImageResultCallback(); dockerClient - .pullImageCmd(dockerImageName) - .withTag(tag) + .pullImageCmd(imageName.getUnversionedPart()) + .withTag(imageName.getVersionPart()) .exec(callback); callback.awaitSuccess(); - AVAILABLE_IMAGE_NAME_CACHE.add(concatenatedName()); + AVAILABLE_IMAGE_NAME_CACHE.add(imageName.toString()); break; } catch (DockerClientException e) { lastException = e; } } - return concatenatedName(); + return imageName.toString(); } catch (DockerClientException e) { - throw new ContainerFetchException("Failed to get Docker client for " + concatenatedName(), e); + throw new ContainerFetchException("Failed to get Docker client for " + imageName.toString(), e); } finally { profiler.stop().log(); } } - - private String concatenatedName() { - return dockerImageName + tagSeparator + tag; - } } diff --git a/core/src/main/java/org/testcontainers/images/builder/dockerfile/traits/FromStatementTrait.java b/core/src/main/java/org/testcontainers/images/builder/dockerfile/traits/FromStatementTrait.java index c148bff106b..2a87cb08223 100644 --- a/core/src/main/java/org/testcontainers/images/builder/dockerfile/traits/FromStatementTrait.java +++ b/core/src/main/java/org/testcontainers/images/builder/dockerfile/traits/FromStatementTrait.java @@ -6,7 +6,7 @@ public interface FromStatementTrait & DockerfileBuilderTrait> { default SELF from(String dockerImageName) { - DockerImageName.validate(dockerImageName); + new DockerImageName(dockerImageName).assertValid(); return ((SELF) this).withStatement(new SingleArgumentStatement("FROM", dockerImageName)); } diff --git a/core/src/main/java/org/testcontainers/utility/DockerImageName.java b/core/src/main/java/org/testcontainers/utility/DockerImageName.java index 42345223aff..f1551eb5fc5 100644 --- a/core/src/main/java/org/testcontainers/utility/DockerImageName.java +++ b/core/src/main/java/org/testcontainers/utility/DockerImageName.java @@ -1,20 +1,165 @@ package org.testcontainers.utility; + +import com.google.common.net.HostAndPort; + public final class DockerImageName { - public static void validate(String dockerImageName) throws IllegalArgumentException { - int repoSeparatorIndex = dockerImageName.indexOf('/'); - int tagSeparatorIndex; - if (repoSeparatorIndex == -1) { - tagSeparatorIndex = dockerImageName.indexOf(':'); + /* Regex patterns used for validation */ + private static final String ALPHA_NUMERIC = "[a-z0-9]+"; + private static final String SEPARATOR = "([\\.]{1}|_{1,2}|-+)"; + private static final String REPO_NAME_PART = ALPHA_NUMERIC + "(" + SEPARATOR + ALPHA_NUMERIC + ")*"; + private static final String REPO_NAME = REPO_NAME_PART + "(/" + REPO_NAME_PART + ")*"; + + private final String rawName; + private final String registry; + private final String repo; + private final Versioning versioning; + + public DockerImageName(String name) { + this.rawName = name; + final int slashIndex = name.indexOf('/'); + + String remoteName; + if (slashIndex == -1 || + (!name.substring(0, slashIndex).contains(".") && + !name.substring(0, slashIndex).contains(":") && + !name.substring(0, slashIndex).equals("localhost"))) { + registry = null; + remoteName = name; + } else { + registry = name.substring(0, slashIndex); + remoteName = name.substring(slashIndex + 1); + } + + if (remoteName.contains("@sha256:")) { + repo = remoteName.split("@sha256:")[0]; + versioning = new Sha256Versioning(remoteName.split("@sha256:")[1]); + } else if (remoteName.contains(":")) { + repo = remoteName.split(":")[0]; + versioning = new TagVersioning(remoteName.split(":")[1]); + } else { + repo = remoteName; + versioning = null; + } + } + + public DockerImageName(String name, String tag) { + this.rawName = name; + final int slashIndex = name.indexOf('/'); + + String remoteName; + if (slashIndex == -1 || + (!name.substring(0, slashIndex).contains(".") && + !name.substring(0, slashIndex).contains(":") && + !name.substring(0, slashIndex).equals("localhost"))) { + registry = null; + remoteName = name; } else { - tagSeparatorIndex = dockerImageName.indexOf(':', repoSeparatorIndex); + registry = name.substring(0, slashIndex - 1); + remoteName = name.substring(slashIndex + 1); + } + + if (tag.startsWith("sha256:")) { + repo = remoteName; + versioning = new Sha256Versioning(tag); + } else { + repo = remoteName; + versioning = new TagVersioning(tag); + } + } + + /** + * @return the unversioned (non 'tag') part of this name + */ + public String getUnversionedPart() { + if (registry != null) { + return registry + "/" + repo; + } else { + return repo; + } + } + + /** + * @return the versioned part of this name (tag or sha256) + */ + public String getVersionPart() { + return versioning.toString(); + } + + @Override + public String toString() { + return getUnversionedPart() + versioning.getSeparator() + versioning.toString(); + } + + /** + * Is the image name valid? + * + * @throws IllegalArgumentException if not valid + */ + public void assertValid() { + HostAndPort.fromString(registry); + if (!repo.matches(REPO_NAME)) { + throw new IllegalArgumentException(repo + " is not a valid Docker image name (in " + rawName + ")"); } - if (tagSeparatorIndex == -1) { + if (versioning == null) { throw new IllegalArgumentException("No image tag was specified in docker image name " + - "(" + dockerImageName + "). Please provide a tag; this may be 'latest' or a specific version"); + "(" + rawName + "). Please provide a tag; this may be 'latest' or a specific version"); + } + if (!versioning.isValid()) { + throw new IllegalArgumentException(versioning + " is not a valid image versioning identifier (in " + rawName + ")"); } } - private DockerImageName() {} + private interface Versioning { + boolean isValid(); + + String getSeparator(); + } + + private static class TagVersioning implements Versioning { + private final String tag; + + TagVersioning(String tag) { + this.tag = tag; + } + + @Override + public boolean isValid() { + return tag.matches("[\\w][\\w\\.\\-]{0,127}"); + } + + @Override + public String getSeparator() { + return ":"; + } + + @Override + public String toString() { + return tag; + } + } + + private class Sha256Versioning implements Versioning { + private final String hash; + + Sha256Versioning(String hash) { + this.hash = hash; + } + + @Override + public boolean isValid() { + return hash.matches("[0-9a-fA-F]{32,}"); + } + + @Override + public String getSeparator() { + return "@"; + } + + @Override + public String toString() { + return "sha256:" + hash; + } + } } diff --git a/core/src/test/java/org/testcontainers/utility/DockerImageNameTest.java b/core/src/test/java/org/testcontainers/utility/DockerImageNameTest.java index aea13f69068..3fedbc19df3 100644 --- a/core/src/test/java/org/testcontainers/utility/DockerImageNameTest.java +++ b/core/src/test/java/org/testcontainers/utility/DockerImageNameTest.java @@ -2,17 +2,46 @@ import org.junit.Test; +import static org.junit.Assert.fail; + public class DockerImageNameTest { @Test public void validNames() { - DockerImageName.validate("myname:latest"); // no repo - DockerImageName.validate("repo/my-name:1.0"); // no repo - DockerImageName.validate("repo.foo.com:1234/my-name:1.0"); // no repo + testValid("myname:latest"); + testValid("myname:latest"); + testValid("repo/my-name:1.0"); + testValid("repo.foo.com:1234/my-name:1.0"); + testValid("repo.foo.com/my-name:1.0"); + testValid("repo.foo.com:1234/repo_here/my-name:1.0"); + testValid("repo.foo.com:1234/repo-here/my-name@sha256:1234abcd1234abcd1234abcd1234abcd"); + testValid("repo.foo.com:1234/my-name@sha256:1234abcd1234abcd1234abcd1234abcd"); + testValid("1.2.3.4/my-name:1.0"); + testValid("1.2.3.4:1234/my-name:1.0"); + testValid("1.2.3.4/repo-here/my-name:1.0"); + testValid("1.2.3.4:1234/repo-here/my-name:1.0"); + } + + @Test + public void invalidNames() { + testInvalid("myname"); + testInvalid(":latest"); + testInvalid("/myname:latest"); + testInvalid("/myname@sha256:latest"); + testInvalid("/myname@sha256:gggggggggggggggggggggggggggggggg"); + testInvalid("repo:notaport/myname:latest"); } - @Test(expected = IllegalArgumentException.class) - public void missingTag() { - DockerImageName.validate("myname"); + private void testValid(String s) { + new DockerImageName(s).assertValid(); + } + + private void testInvalid(String myname) { + try { + new DockerImageName(myname).assertValid(); + fail(); + } catch (IllegalArgumentException expected) { + + } } -} \ No newline at end of file +} From c298d84cc3b6f90ec123e3537886909e331b0427 Mon Sep 17 00:00:00 2001 From: Richard North Date: Sun, 18 Mar 2018 21:29:48 +0000 Subject: [PATCH 12/34] Extract constants --- .../java/org/testcontainers/utility/DockerImageName.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/org/testcontainers/utility/DockerImageName.java b/core/src/main/java/org/testcontainers/utility/DockerImageName.java index f1551eb5fc5..372e5eabfbc 100644 --- a/core/src/main/java/org/testcontainers/utility/DockerImageName.java +++ b/core/src/main/java/org/testcontainers/utility/DockerImageName.java @@ -118,6 +118,7 @@ private interface Versioning { } private static class TagVersioning implements Versioning { + public static final String TAG_REGEX = "[\\w][\\w\\.\\-]{0,127}"; private final String tag; TagVersioning(String tag) { @@ -126,7 +127,7 @@ private static class TagVersioning implements Versioning { @Override public boolean isValid() { - return tag.matches("[\\w][\\w\\.\\-]{0,127}"); + return tag.matches(TAG_REGEX); } @Override @@ -141,6 +142,7 @@ public String toString() { } private class Sha256Versioning implements Versioning { + public static final String HASH_REGEX = "[0-9a-fA-F]{32,}"; private final String hash; Sha256Versioning(String hash) { @@ -149,7 +151,7 @@ private class Sha256Versioning implements Versioning { @Override public boolean isValid() { - return hash.matches("[0-9a-fA-F]{32,}"); + return hash.matches(HASH_REGEX); } @Override From ca080d0fadd936f60906599b45ceb103ad7eea96 Mon Sep 17 00:00:00 2001 From: Richard North Date: Sun, 18 Mar 2018 21:41:00 +0000 Subject: [PATCH 13/34] squashme --- .../java/org/testcontainers/utility/DockerImageName.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/org/testcontainers/utility/DockerImageName.java b/core/src/main/java/org/testcontainers/utility/DockerImageName.java index 372e5eabfbc..529afc50e33 100644 --- a/core/src/main/java/org/testcontainers/utility/DockerImageName.java +++ b/core/src/main/java/org/testcontainers/utility/DockerImageName.java @@ -25,7 +25,7 @@ public DockerImageName(String name) { (!name.substring(0, slashIndex).contains(".") && !name.substring(0, slashIndex).contains(":") && !name.substring(0, slashIndex).equals("localhost"))) { - registry = null; + registry = ""; remoteName = name; } else { registry = name.substring(0, slashIndex); @@ -53,7 +53,7 @@ public DockerImageName(String name, String tag) { (!name.substring(0, slashIndex).contains(".") && !name.substring(0, slashIndex).contains(":") && !name.substring(0, slashIndex).equals("localhost"))) { - registry = null; + registry = ""; remoteName = name; } else { registry = name.substring(0, slashIndex - 1); @@ -73,7 +73,7 @@ public DockerImageName(String name, String tag) { * @return the unversioned (non 'tag') part of this name */ public String getUnversionedPart() { - if (registry != null) { + if (!"".equals(registry)) { return registry + "/" + repo; } else { return repo; From a479bd35caba2bf3d3fe5e32eacdd1762fad2abe Mon Sep 17 00:00:00 2001 From: Richard North Date: Sun, 18 Mar 2018 21:47:47 +0000 Subject: [PATCH 14/34] Include pointers to latest versions available through Maven Central --- modules/dynalite/README.md | 6 ++++-- modules/localstack/README.md | 7 ++++--- modules/mariadb/README.md | 6 ++++-- modules/mssqlserver/README.md | 6 ++++-- modules/oracle-xe/README.md | 6 ++++-- modules/vault/README.md | 6 ++++-- 6 files changed, 24 insertions(+), 13 deletions(-) diff --git a/modules/dynalite/README.md b/modules/dynalite/README.md index c1c7880f20c..6fd01895eb5 100644 --- a/modules/dynalite/README.md +++ b/modules/dynalite/README.md @@ -27,19 +27,21 @@ In part, because it's light and quick to run. Also, please see the [reasons give ## Dependency information +Replace `VERSION` with the [latest version available on Maven Central](https://search.maven.org/#search%7Cga%7C1%7Cg%3A%22org.testcontainers%22). + ### Maven ``` org.testcontainers dynalite - 1.4.3 + VERSION ``` ### Gradle ``` -compile group: 'org.testcontainers', name: 'dynalite', version: '1.4.3' +compile group: 'org.testcontainers', name: 'dynalite', version: 'VERSION' ``` diff --git a/modules/localstack/README.md b/modules/localstack/README.md index 188edfd2aa5..520cb0c147c 100644 --- a/modules/localstack/README.md +++ b/modules/localstack/README.md @@ -27,18 +27,19 @@ public class SomeTest { ## Dependency information -### Maven +Replace `VERSION` with the [latest version available on Maven Central](https://search.maven.org/#search%7Cga%7C1%7Cg%3A%22org.testcontainers%22). +### Maven ``` org.testcontainers localstack - 1.4.3 + VERSION ``` ### Gradle ``` -compile group: 'org.testcontainers', name: 'localstack', version: '1.4.3' +compile group: 'org.testcontainers', name: 'localstack', version: 'VERSION' ``` diff --git a/modules/mariadb/README.md b/modules/mariadb/README.md index 5f202a6112b..20649b03582 100644 --- a/modules/mariadb/README.md +++ b/modules/mariadb/README.md @@ -21,18 +21,20 @@ public class SomeTest { ## Dependency information +Replace `VERSION` with the [latest version available on Maven Central](https://search.maven.org/#search%7Cga%7C1%7Cg%3A%22org.testcontainers%22). + ### Maven ``` org.testcontainers mariadb - 1.4.3 + VERSION ``` ### Gradle ``` -compile group: 'org.testcontainers', name: 'mariadb', version: '1.4.3' +compile group: 'org.testcontainers', name: 'mariadb', version: 'VERSION' ``` diff --git a/modules/mssqlserver/README.md b/modules/mssqlserver/README.md index 9d484043d6b..323bc3fd8e8 100644 --- a/modules/mssqlserver/README.md +++ b/modules/mssqlserver/README.md @@ -21,20 +21,22 @@ public class SomeTest { ## Dependency information +Replace `VERSION` with the [latest version available on Maven Central](https://search.maven.org/#search%7Cga%7C1%7Cg%3A%22org.testcontainers%22). + ### Maven ``` org.testcontainers mssqlserver - 1.4.3 + VERSION ``` ### Gradle ``` -compile group: 'org.testcontainers', name: 'mssqlserver', version: '1.4.3' +compile group: 'org.testcontainers', name: 'mssqlserver', version: 'VERSION' ``` ## License diff --git a/modules/oracle-xe/README.md b/modules/oracle-xe/README.md index 52e7c302d34..38def171fab 100644 --- a/modules/oracle-xe/README.md +++ b/modules/oracle-xe/README.md @@ -21,18 +21,20 @@ public class SomeTest { ## Dependency information +Replace `VERSION` with the [latest version available on Maven Central](https://search.maven.org/#search%7Cga%7C1%7Cg%3A%22org.testcontainers%22). + ### Maven ``` org.testcontainers oracle-xe - 1.4.3 + VERSION ``` ### Gradle ``` -compile group: 'org.testcontainers', name: 'oracle-xe', version: '1.4.3' +compile group: 'org.testcontainers', name: 'oracle-xe', version: 'VERSION' ``` diff --git a/modules/vault/README.md b/modules/vault/README.md index b5b33a17e08..9c2eb6bed7c 100644 --- a/modules/vault/README.md +++ b/modules/vault/README.md @@ -39,20 +39,22 @@ test how your application behaves with Vault by writing different test scenarios ## Dependency information +Replace `VERSION` with the [latest version available on Maven Central](https://search.maven.org/#search%7Cga%7C1%7Cg%3A%22org.testcontainers%22). + ### Maven ``` org.testcontainers vault - 1.4.3 + VERSION ``` ### Gradle ``` -compile group: 'org.testcontainers', name: 'vault', version: '1.4.3' +compile group: 'org.testcontainers', name: 'vault', version: 'VERSION' ``` ## License From ca4f318fd18f224662a00d00de29fd4ba60c7a67 Mon Sep 17 00:00:00 2001 From: Richard North Date: Sun, 18 Mar 2018 22:00:38 +0000 Subject: [PATCH 15/34] Tidy --- .../images/RemoteDockerImage.java | 34 ++++++++++--------- .../utility/DockerImageName.java | 12 +++++-- 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/core/src/main/java/org/testcontainers/images/RemoteDockerImage.java b/core/src/main/java/org/testcontainers/images/RemoteDockerImage.java index 8342ae10a6d..df8d2daa0de 100644 --- a/core/src/main/java/org/testcontainers/images/RemoteDockerImage.java +++ b/core/src/main/java/org/testcontainers/images/RemoteDockerImage.java @@ -14,14 +14,15 @@ import org.testcontainers.utility.DockerLoggerFactory; import org.testcontainers.utility.LazyFuture; -import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Objects; import java.util.Set; +import java.util.stream.Stream; public class RemoteDockerImage extends LazyFuture { - public static final Set AVAILABLE_IMAGE_NAME_CACHE = new HashSet<>(); + public static final Set AVAILABLE_IMAGE_NAME_CACHE = new HashSet<>(); private DockerImageName imageName; @@ -50,8 +51,8 @@ protected final String resolve() { DockerClientException lastException = null; while (true) { // Does our cache already know the image? - if (AVAILABLE_IMAGE_NAME_CACHE.contains(imageName.toString())) { - logger.trace("{} is already in image name cache", imageName.toString()); + if (AVAILABLE_IMAGE_NAME_CACHE.contains(imageName)) { + logger.trace("{} is already in image name cache", imageName); break; } @@ -63,27 +64,28 @@ protected final String resolve() { } List updatedImages = listImagesCmd.exec(); - for (Image image : updatedImages) { - if (image.getRepoTags() != null) { - Collections.addAll(AVAILABLE_IMAGE_NAME_CACHE, image.getRepoTags()); - } - } + updatedImages.stream() + .map(Image::getRepoTags) + .filter(Objects::nonNull) + .flatMap(Stream::of) + .map(DockerImageName::new) + .forEach(AVAILABLE_IMAGE_NAME_CACHE::add); // And now? - if (AVAILABLE_IMAGE_NAME_CACHE.contains(imageName.toString())) { - logger.trace("{} is in image name cache following listing of images", imageName.toString()); + if (AVAILABLE_IMAGE_NAME_CACHE.contains(imageName)) { + logger.trace("{} is in image name cache following listing of images", imageName); break; } // Log only on first attempt if (attempts == 0) { - logger.info("Pulling docker image: {}. Please be patient; this may take some time but only needs to be done once.", imageName.toString()); + logger.info("Pulling docker image: {}. Please be patient; this may take some time but only needs to be done once.", imageName); profiler.start("Pull image"); } if (attempts++ >= 3) { - logger.error("Retry limit reached while trying to pull image: {}. Please check output of `docker pull {}`", imageName.toString(), imageName.toString()); - throw new ContainerFetchException("Retry limit reached while trying to pull image: " + imageName.toString(), lastException); + logger.error("Retry limit reached while trying to pull image: {}. Please check output of `docker pull {}`", imageName, imageName); + throw new ContainerFetchException("Retry limit reached while trying to pull image: " + imageName, lastException); } // The image is not available locally - pull it @@ -94,7 +96,7 @@ protected final String resolve() { .withTag(imageName.getVersionPart()) .exec(callback); callback.awaitSuccess(); - AVAILABLE_IMAGE_NAME_CACHE.add(imageName.toString()); + AVAILABLE_IMAGE_NAME_CACHE.add(imageName); break; } catch (DockerClientException e) { lastException = e; @@ -103,7 +105,7 @@ protected final String resolve() { return imageName.toString(); } catch (DockerClientException e) { - throw new ContainerFetchException("Failed to get Docker client for " + imageName.toString(), e); + throw new ContainerFetchException("Failed to get Docker client for " + imageName, e); } finally { profiler.stop().log(); } diff --git a/core/src/main/java/org/testcontainers/utility/DockerImageName.java b/core/src/main/java/org/testcontainers/utility/DockerImageName.java index 529afc50e33..212b81a20eb 100644 --- a/core/src/main/java/org/testcontainers/utility/DockerImageName.java +++ b/core/src/main/java/org/testcontainers/utility/DockerImageName.java @@ -2,14 +2,19 @@ import com.google.common.net.HostAndPort; +import lombok.Data; +import lombok.EqualsAndHashCode; +import java.util.regex.Pattern; + +@EqualsAndHashCode public final class DockerImageName { /* Regex patterns used for validation */ private static final String ALPHA_NUMERIC = "[a-z0-9]+"; private static final String SEPARATOR = "([\\.]{1}|_{1,2}|-+)"; private static final String REPO_NAME_PART = ALPHA_NUMERIC + "(" + SEPARATOR + ALPHA_NUMERIC + ")*"; - private static final String REPO_NAME = REPO_NAME_PART + "(/" + REPO_NAME_PART + ")*"; + private static final Pattern REPO_NAME = Pattern.compile(REPO_NAME_PART + "(/" + REPO_NAME_PART + ")*"); private final String rawName; private final String registry; @@ -99,7 +104,7 @@ public String toString() { */ public void assertValid() { HostAndPort.fromString(registry); - if (!repo.matches(REPO_NAME)) { + if (!REPO_NAME.matcher(repo).matches()) { throw new IllegalArgumentException(repo + " is not a valid Docker image name (in " + rawName + ")"); } if (versioning == null) { @@ -113,10 +118,10 @@ public void assertValid() { private interface Versioning { boolean isValid(); - String getSeparator(); } + @Data private static class TagVersioning implements Versioning { public static final String TAG_REGEX = "[\\w][\\w\\.\\-]{0,127}"; private final String tag; @@ -141,6 +146,7 @@ public String toString() { } } + @Data private class Sha256Versioning implements Versioning { public static final String HASH_REGEX = "[0-9a-fA-F]{32,}"; private final String hash; From 78129d6c27d14ff369a5f7f2adf3c6197cb211f7 Mon Sep 17 00:00:00 2001 From: Richard North Date: Mon, 19 Mar 2018 19:08:27 +0000 Subject: [PATCH 16/34] Introduce assertion for license acceptance, e.g. for the SQL Server EULA --- .../utility/LicenceAcceptance.java | 36 +++++++++++++++++++ .../container-license-acceptance.txt | 1 + .../containers/MSSQLServerContainer.java | 3 ++ 3 files changed, 40 insertions(+) create mode 100644 core/src/main/java/org/testcontainers/utility/LicenceAcceptance.java create mode 100644 modules/jdbc-test/src/test/resources/container-license-acceptance.txt diff --git a/core/src/main/java/org/testcontainers/utility/LicenceAcceptance.java b/core/src/main/java/org/testcontainers/utility/LicenceAcceptance.java new file mode 100644 index 00000000000..4027954171e --- /dev/null +++ b/core/src/main/java/org/testcontainers/utility/LicenceAcceptance.java @@ -0,0 +1,36 @@ +package org.testcontainers.utility; + +import com.google.common.base.Charsets; +import com.google.common.io.Resources; +import lombok.experimental.UtilityClass; + +import java.net.URL; +import java.util.List; + +/** + * Utility class to ensure that licenses have been accepted by the developer. + */ +@UtilityClass +public class LicenceAcceptance { + + private static final String ACCEPTANCE_FILE_NAME = "container-license-acceptance.txt"; + + public static void assertLicenseAccepted(final String imageName) { + try { + final URL url = Resources.getResource(ACCEPTANCE_FILE_NAME); + final List acceptedLicences = Resources.readLines(url, Charsets.UTF_8); + + if (acceptedLicences.stream().anyMatch(imageName::equals)) { + return; + } + } catch (Exception ignored) { + // suppressed + } + + throw new IllegalStateException("The image " + imageName + " requires you to accept a license agreement. " + + "Please place a file at the root of the classpath named " + ACCEPTANCE_FILE_NAME + ", e.g. at " + + "src/test/resources/" + ACCEPTANCE_FILE_NAME + ". This file should contain the line:\n " + + imageName); + + } +} diff --git a/modules/jdbc-test/src/test/resources/container-license-acceptance.txt b/modules/jdbc-test/src/test/resources/container-license-acceptance.txt new file mode 100644 index 00000000000..5a65838e7e7 --- /dev/null +++ b/modules/jdbc-test/src/test/resources/container-license-acceptance.txt @@ -0,0 +1 @@ +microsoft/mssql-server-linux diff --git a/modules/mssqlserver/src/main/java/org/testcontainers/containers/MSSQLServerContainer.java b/modules/mssqlserver/src/main/java/org/testcontainers/containers/MSSQLServerContainer.java index c1c90f39d83..1ad78a0255b 100644 --- a/modules/mssqlserver/src/main/java/org/testcontainers/containers/MSSQLServerContainer.java +++ b/modules/mssqlserver/src/main/java/org/testcontainers/containers/MSSQLServerContainer.java @@ -1,5 +1,7 @@ package org.testcontainers.containers; +import org.testcontainers.utility.LicenceAcceptance; + /** * @author Stefan Hufschmidt */ @@ -12,6 +14,7 @@ public class MSSQLServerContainer> exten public MSSQLServerContainer() { this(IMAGE + ":latest"); + LicenceAcceptance.assertLicenseAccepted(IMAGE); } public MSSQLServerContainer(final String dockerImageName) { From 6014278e321d9009ec297205786cbba5039498f4 Mon Sep 17 00:00:00 2001 From: Richard North Date: Mon, 19 Mar 2018 19:08:50 +0000 Subject: [PATCH 17/34] Use latest localstack image --- .../containers/localstack/LocalStackContainer.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/localstack/src/main/java/org/testcontainers/containers/localstack/LocalStackContainer.java b/modules/localstack/src/main/java/org/testcontainers/containers/localstack/LocalStackContainer.java index 0938aba09a3..18738de6ca6 100644 --- a/modules/localstack/src/main/java/org/testcontainers/containers/localstack/LocalStackContainer.java +++ b/modules/localstack/src/main/java/org/testcontainers/containers/localstack/LocalStackContainer.java @@ -45,9 +45,9 @@ protected void before() throws Throwable { .map(Service::getPort) .collect(Collectors.toSet()).toArray(new Integer[]{}); - delegate = new GenericContainer("atlassianlabs/localstack:0.6.0") + delegate = new GenericContainer("localstack/localstack:0.8.5") .withExposedPorts(portsList) - .withFileSystemBind("/var/run/docker.sock", "/var/run/docker.sock", READ_WRITE) + .withFileSystemBind("//var/run/docker.sock", "/var/run/docker.sock", READ_WRITE) .waitingFor(new LogMessageWaitStrategy().withRegEx(".*Ready\\.\n")) .withEnv("SERVICES", servicesList); From 5e94cd8b1c7902da8a2841057d63cdf6db739e7a Mon Sep 17 00:00:00 2001 From: Richard North Date: Mon, 19 Mar 2018 19:15:16 +0000 Subject: [PATCH 18/34] Cleanup following code review --- .../images/RemoteDockerImage.java | 3 +- .../dockerclient/ImagePullTest.java | 66 +++++++------------ 2 files changed, 25 insertions(+), 44 deletions(-) diff --git a/core/src/main/java/org/testcontainers/images/RemoteDockerImage.java b/core/src/main/java/org/testcontainers/images/RemoteDockerImage.java index df8d2daa0de..7e75b9af23e 100644 --- a/core/src/main/java/org/testcontainers/images/RemoteDockerImage.java +++ b/core/src/main/java/org/testcontainers/images/RemoteDockerImage.java @@ -18,6 +18,7 @@ import java.util.List; import java.util.Objects; import java.util.Set; +import java.util.stream.Collectors; import java.util.stream.Stream; public class RemoteDockerImage extends LazyFuture { @@ -69,7 +70,7 @@ protected final String resolve() { .filter(Objects::nonNull) .flatMap(Stream::of) .map(DockerImageName::new) - .forEach(AVAILABLE_IMAGE_NAME_CACHE::add); + .collect(Collectors.toCollection(() -> AVAILABLE_IMAGE_NAME_CACHE)); // And now? if (AVAILABLE_IMAGE_NAME_CACHE.contains(imageName)) { diff --git a/core/src/test/java/org/testcontainers/dockerclient/ImagePullTest.java b/core/src/test/java/org/testcontainers/dockerclient/ImagePullTest.java index 69556e461df..414de67d3c3 100644 --- a/core/src/test/java/org/testcontainers/dockerclient/ImagePullTest.java +++ b/core/src/test/java/org/testcontainers/dockerclient/ImagePullTest.java @@ -1,58 +1,38 @@ package org.testcontainers.dockerclient; import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; import org.testcontainers.containers.GenericContainer; +@RunWith(Parameterized.class) public class ImagePullTest { - @Test - public void pullOfficialLatestImageTest() { - doPullStartAndStop("alpine:latest"); - } + private String image; - @Test - public void pullOfficialImageByTagTest() { - doPullStartAndStop("alpine:3.6"); + @Parameterized.Parameters(name = "{0}") + public static String[] parameters() { + return new String[] { + "alpine:latest", + "alpine:3.6", + "alpine@sha256:8fd4b76819e1e5baac82bd0a3d03abfe3906e034cc5ee32100d12aaaf3956dc7", + "gliderlabs/alpine:latest", + "gliderlabs/alpine:3.5", + "gliderlabs/alpine@sha256:a19aa4a17a525c97e5a90a0c53a9f3329d2dc61b0a14df5447757a865671c085", + "quay.io/coreos/etcd:latest", + "quay.io/coreos/etcd:v3.1", + "quay.io/coreos/etcd@sha256:39a30367cd1f3186d540a063ea0257353c8f81b0d3c920c87c7e0f602bb6197c" + }; } - @Test - public void pullOfficialImageByShaTest() { - doPullStartAndStop("alpine@sha256:8fd4b76819e1e5baac82bd0a3d03abfe3906e034cc5ee32100d12aaaf3956dc7"); + public ImagePullTest(String image) { + this.image = image; } @Test - public void pullLatestImageTest() { - doPullStartAndStop("gliderlabs/alpine:latest"); - } - - @Test - public void pullImageByTagTest() { - doPullStartAndStop("gliderlabs/alpine:3.5"); - } - - @Test - public void pullImageByShaTest() { - doPullStartAndStop("gliderlabs/alpine@sha256:a19aa4a17a525c97e5a90a0c53a9f3329d2dc61b0a14df5447757a865671c085"); - } - - @Test - public void pullLatestImageFromPublicRegistryTest() { - doPullStartAndStop("quay.io/coreos/etcd:latest"); - } - - @Test - public void pullImageByTagFromPublicRegistryTest() { - doPullStartAndStop("quay.io/coreos/etcd:v3.1"); - } - - @Test - public void pullImageByShaFromPublicRegistryTest() { - doPullStartAndStop("quay.io/coreos/etcd@sha256:39a30367cd1f3186d540a063ea0257353c8f81b0d3c920c87c7e0f602bb6197c"); - } - - private void doPullStartAndStop(String s) { - final GenericContainer container = new GenericContainer<>(s); - container.start(); - container.stop(); + public void test() { + try (final GenericContainer __ = new GenericContainer<>(image)) { + // do nothing other than start and stop + } } } From 70eb9d68de03a934d21c9821a4ce48b82589a547 Mon Sep 17 00:00:00 2001 From: Richard North Date: Mon, 19 Mar 2018 19:22:51 +0000 Subject: [PATCH 19/34] Add toString for better error messages --- .../java/org/testcontainers/images/RemoteDockerImage.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/core/src/main/java/org/testcontainers/images/RemoteDockerImage.java b/core/src/main/java/org/testcontainers/images/RemoteDockerImage.java index 7e75b9af23e..510f0a1c830 100644 --- a/core/src/main/java/org/testcontainers/images/RemoteDockerImage.java +++ b/core/src/main/java/org/testcontainers/images/RemoteDockerImage.java @@ -111,4 +111,11 @@ protected final String resolve() { profiler.stop().log(); } } + + @Override + public String toString() { + return "RemoteDockerImage{" + + "imageName=" + imageName + + '}'; + } } From 32c541b6dbaa12408cf7ba5aafbe278ef92bfb8e Mon Sep 17 00:00:00 2001 From: Richard North Date: Mon, 19 Mar 2018 19:25:29 +0000 Subject: [PATCH 20/34] Improve error message --- .../java/org/testcontainers/containers/GenericContainer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/org/testcontainers/containers/GenericContainer.java b/core/src/main/java/org/testcontainers/containers/GenericContainer.java index 859b452ea71..a00fe33d8ae 100644 --- a/core/src/main/java/org/testcontainers/containers/GenericContainer.java +++ b/core/src/main/java/org/testcontainers/containers/GenericContainer.java @@ -868,7 +868,7 @@ public String getDockerImageName() { try { return image.get(); } catch (Exception e) { - throw new ContainerFetchException("Can't get Docker image name from " + image, e); + throw new ContainerFetchException("Can't get Docker image: " + image, e); } } From 0495e736e5c51e5ffc937ea456b9198d6d73196a Mon Sep 17 00:00:00 2001 From: Richard North Date: Mon, 19 Mar 2018 19:32:26 +0000 Subject: [PATCH 21/34] Add test for LicenseAcceptance utility class and rename --- ...ceAcceptance.java => LicenseAcceptance.java} | 2 +- .../utility/LicenseAcceptanceTest.java | 17 +++++++++++++++++ .../resources/container-license-acceptance.txt | 3 +++ .../containers/MSSQLServerContainer.java | 4 ++-- 4 files changed, 23 insertions(+), 3 deletions(-) rename core/src/main/java/org/testcontainers/utility/{LicenceAcceptance.java => LicenseAcceptance.java} (97%) create mode 100644 core/src/test/java/org/testcontainers/utility/LicenseAcceptanceTest.java create mode 100644 core/src/test/resources/container-license-acceptance.txt diff --git a/core/src/main/java/org/testcontainers/utility/LicenceAcceptance.java b/core/src/main/java/org/testcontainers/utility/LicenseAcceptance.java similarity index 97% rename from core/src/main/java/org/testcontainers/utility/LicenceAcceptance.java rename to core/src/main/java/org/testcontainers/utility/LicenseAcceptance.java index 4027954171e..0982da50081 100644 --- a/core/src/main/java/org/testcontainers/utility/LicenceAcceptance.java +++ b/core/src/main/java/org/testcontainers/utility/LicenseAcceptance.java @@ -11,7 +11,7 @@ * Utility class to ensure that licenses have been accepted by the developer. */ @UtilityClass -public class LicenceAcceptance { +public class LicenseAcceptance { private static final String ACCEPTANCE_FILE_NAME = "container-license-acceptance.txt"; diff --git a/core/src/test/java/org/testcontainers/utility/LicenseAcceptanceTest.java b/core/src/test/java/org/testcontainers/utility/LicenseAcceptanceTest.java new file mode 100644 index 00000000000..63da017c88e --- /dev/null +++ b/core/src/test/java/org/testcontainers/utility/LicenseAcceptanceTest.java @@ -0,0 +1,17 @@ +package org.testcontainers.utility; + +import org.junit.Test; + +public class LicenseAcceptanceTest { + + @Test + public void testForExistingNames() { + LicenseAcceptance.assertLicenseAccepted("a"); + LicenseAcceptance.assertLicenseAccepted("b"); + } + + @Test(expected = IllegalStateException.class) + public void testForMissingNames() { + LicenseAcceptance.assertLicenseAccepted("c"); + } +} diff --git a/core/src/test/resources/container-license-acceptance.txt b/core/src/test/resources/container-license-acceptance.txt new file mode 100644 index 00000000000..bfdaa0f1c34 --- /dev/null +++ b/core/src/test/resources/container-license-acceptance.txt @@ -0,0 +1,3 @@ +a +b + diff --git a/modules/mssqlserver/src/main/java/org/testcontainers/containers/MSSQLServerContainer.java b/modules/mssqlserver/src/main/java/org/testcontainers/containers/MSSQLServerContainer.java index 1ad78a0255b..00922de4448 100644 --- a/modules/mssqlserver/src/main/java/org/testcontainers/containers/MSSQLServerContainer.java +++ b/modules/mssqlserver/src/main/java/org/testcontainers/containers/MSSQLServerContainer.java @@ -1,6 +1,6 @@ package org.testcontainers.containers; -import org.testcontainers.utility.LicenceAcceptance; +import org.testcontainers.utility.LicenseAcceptance; /** * @author Stefan Hufschmidt @@ -14,7 +14,7 @@ public class MSSQLServerContainer> exten public MSSQLServerContainer() { this(IMAGE + ":latest"); - LicenceAcceptance.assertLicenseAccepted(IMAGE); + LicenseAcceptance.assertLicenseAccepted(IMAGE); } public MSSQLServerContainer(final String dockerImageName) { From a4cf0a54fa2f0ff04061324aa04c9f1218cc5a84 Mon Sep 17 00:00:00 2001 From: Richard North Date: Tue, 20 Mar 2018 08:23:20 +0100 Subject: [PATCH 22/34] Add getConnectTimeoutSeconds --- .../testcontainers/containers/JdbcDatabaseContainer.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/modules/jdbc/src/main/java/org/testcontainers/containers/JdbcDatabaseContainer.java b/modules/jdbc/src/main/java/org/testcontainers/containers/JdbcDatabaseContainer.java index 0d64444847a..140aaa7dde3 100644 --- a/modules/jdbc/src/main/java/org/testcontainers/containers/JdbcDatabaseContainer.java +++ b/modules/jdbc/src/main/java/org/testcontainers/containers/JdbcDatabaseContainer.java @@ -147,7 +147,7 @@ public Connection createConnection(String queryString) throws SQLException { final Driver jdbcDriverInstance = getJdbcDriverInstance(); try { - return Unreliables.retryUntilSuccess(120, TimeUnit.SECONDS, () -> + return Unreliables.retryUntilSuccess(getConnectTimeoutSeconds(), TimeUnit.SECONDS, () -> DB_CONNECT_RATE_LIMIT.getWhenReady(() -> jdbcDriverInstance.connect(url, info))); } catch (Exception e) { @@ -192,4 +192,11 @@ public void addParameter(String paramName, String value) { protected int getStartupTimeoutSeconds() { return 120; } + + /** + * @return time to allow for the database to start and establish an initial connection, in seconds + */ + protected int getConnectTimeoutSeconds() { + return 120; + } } From 44b81f4c4d9aabd227ef1e1f8d9f1bc0b1f317be Mon Sep 17 00:00:00 2001 From: Richard North Date: Tue, 20 Mar 2018 08:40:40 +0100 Subject: [PATCH 23/34] Migrate dynalite image to quay.io --- .../java/org/testcontainers/dynamodb/DynaliteContainer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/dynalite/src/main/java/org/testcontainers/dynamodb/DynaliteContainer.java b/modules/dynalite/src/main/java/org/testcontainers/dynamodb/DynaliteContainer.java index 0b7369cd53e..31a3a7914e8 100644 --- a/modules/dynalite/src/main/java/org/testcontainers/dynamodb/DynaliteContainer.java +++ b/modules/dynalite/src/main/java/org/testcontainers/dynamodb/DynaliteContainer.java @@ -21,7 +21,7 @@ public class DynaliteContainer extends ExternalResource { private final GenericContainer delegate; public DynaliteContainer() { - this("richnorth/dynalite:v1.2.1-1"); + this("quay.io/testcontainers/dynalite:v1.2.1-1"); } public DynaliteContainer(String imageName) { From 701f7d354e6d480b3a48afad40c8bf2e889b8bec Mon Sep 17 00:00:00 2001 From: Richard North Date: Wed, 21 Mar 2018 08:28:23 +0100 Subject: [PATCH 24/34] Update following review --- .../org/testcontainers/images/RemoteDockerImage.java | 9 ++------- .../org/testcontainers/utility/LicenseAcceptance.java | 2 +- .../org/testcontainers/dockerclient/ImagePullTest.java | 3 ++- modules/mssqlserver/README.md | 2 ++ .../testcontainers/containers/MSSQLServerContainer.java | 5 +++-- 5 files changed, 10 insertions(+), 11 deletions(-) diff --git a/core/src/main/java/org/testcontainers/images/RemoteDockerImage.java b/core/src/main/java/org/testcontainers/images/RemoteDockerImage.java index 510f0a1c830..59f1c2539f5 100644 --- a/core/src/main/java/org/testcontainers/images/RemoteDockerImage.java +++ b/core/src/main/java/org/testcontainers/images/RemoteDockerImage.java @@ -6,6 +6,7 @@ import com.github.dockerjava.api.model.Image; import com.github.dockerjava.core.command.PullImageResultCallback; import lombok.NonNull; +import lombok.ToString; import org.slf4j.Logger; import org.slf4j.profiler.Profiler; import org.testcontainers.DockerClientFactory; @@ -21,6 +22,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +@ToString public class RemoteDockerImage extends LazyFuture { public static final Set AVAILABLE_IMAGE_NAME_CACHE = new HashSet<>(); @@ -111,11 +113,4 @@ protected final String resolve() { profiler.stop().log(); } } - - @Override - public String toString() { - return "RemoteDockerImage{" + - "imageName=" + imageName + - '}'; - } } diff --git a/core/src/main/java/org/testcontainers/utility/LicenseAcceptance.java b/core/src/main/java/org/testcontainers/utility/LicenseAcceptance.java index 0982da50081..55037d8978c 100644 --- a/core/src/main/java/org/testcontainers/utility/LicenseAcceptance.java +++ b/core/src/main/java/org/testcontainers/utility/LicenseAcceptance.java @@ -20,7 +20,7 @@ public static void assertLicenseAccepted(final String imageName) { final URL url = Resources.getResource(ACCEPTANCE_FILE_NAME); final List acceptedLicences = Resources.readLines(url, Charsets.UTF_8); - if (acceptedLicences.stream().anyMatch(imageName::equals)) { + if (acceptedLicences.stream().map(String::trim).anyMatch(imageName::equals)) { return; } } catch (Exception ignored) { diff --git a/core/src/test/java/org/testcontainers/dockerclient/ImagePullTest.java b/core/src/test/java/org/testcontainers/dockerclient/ImagePullTest.java index 414de67d3c3..e29f4f38e8b 100644 --- a/core/src/test/java/org/testcontainers/dockerclient/ImagePullTest.java +++ b/core/src/test/java/org/testcontainers/dockerclient/ImagePullTest.java @@ -31,7 +31,8 @@ public ImagePullTest(String image) { @Test public void test() { - try (final GenericContainer __ = new GenericContainer<>(image)) { + try (final GenericContainer container = new GenericContainer<>(image)) { + container.start(); // do nothing other than start and stop } } diff --git a/modules/mssqlserver/README.md b/modules/mssqlserver/README.md index 323bc3fd8e8..b0bf21fe19b 100644 --- a/modules/mssqlserver/README.md +++ b/modules/mssqlserver/README.md @@ -19,6 +19,8 @@ public class SomeTest { ... create a connection and run test as normal ``` +> *Note:* Due to licencing restrictions you are required to accept an EULA for this container image. To indicate that you accept the MS SQL Server image EULA, Please place a file at the root of the classpath named `container-license-acceptance.txt`, e.g. at `src/test/resources/container-license-acceptance.txt`. This file should contain the line: `microsoft/mssql-server-linux:latest` + ## Dependency information Replace `VERSION` with the [latest version available on Maven Central](https://search.maven.org/#search%7Cga%7C1%7Cg%3A%22org.testcontainers%22). diff --git a/modules/mssqlserver/src/main/java/org/testcontainers/containers/MSSQLServerContainer.java b/modules/mssqlserver/src/main/java/org/testcontainers/containers/MSSQLServerContainer.java index 00922de4448..a44662a7f36 100644 --- a/modules/mssqlserver/src/main/java/org/testcontainers/containers/MSSQLServerContainer.java +++ b/modules/mssqlserver/src/main/java/org/testcontainers/containers/MSSQLServerContainer.java @@ -14,7 +14,6 @@ public class MSSQLServerContainer> exten public MSSQLServerContainer() { this(IMAGE + ":latest"); - LicenseAcceptance.assertLicenseAccepted(IMAGE); } public MSSQLServerContainer(final String dockerImageName) { @@ -28,9 +27,11 @@ protected Integer getLivenessCheckPort() { @Override protected void configure() { - addExposedPort(MS_SQL_SERVER_PORT); + + LicenseAcceptance.assertLicenseAccepted(this.getDockerImageName()); addEnv("ACCEPT_EULA", "Y"); + addEnv("SA_PASSWORD", password); } From 2cc4633e384d9cd056ee260d15f3c50531ffdc93 Mon Sep 17 00:00:00 2001 From: Richard North Date: Wed, 21 Mar 2018 08:40:05 +0100 Subject: [PATCH 25/34] Update following review --- .../src/test/resources/container-license-acceptance.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/jdbc-test/src/test/resources/container-license-acceptance.txt b/modules/jdbc-test/src/test/resources/container-license-acceptance.txt index 5a65838e7e7..b0c43ef2bff 100644 --- a/modules/jdbc-test/src/test/resources/container-license-acceptance.txt +++ b/modules/jdbc-test/src/test/resources/container-license-acceptance.txt @@ -1 +1 @@ -microsoft/mssql-server-linux +microsoft/mssql-server-linux:latest From c5df2f7f25ec6572869b3f5c10bd108b75be40b2 Mon Sep 17 00:00:00 2001 From: Richard North Date: Wed, 21 Mar 2018 22:11:30 +0100 Subject: [PATCH 26/34] Declare time zone --- circle.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/circle.yml b/circle.yml index 43aff9d8f3d..17885776817 100644 --- a/circle.yml +++ b/circle.yml @@ -5,6 +5,8 @@ jobs: steps: - checkout - run: ./gradlew testcontainers:check + environment: + TZ: "/usr/share/zoneinfo/ETC/UTC" - run: name: Save test results command: | @@ -17,6 +19,8 @@ jobs: steps: - checkout - run: ./gradlew check -x testcontainers:check -x selenium:check + environment: + TZ: "/usr/share/zoneinfo/ETC/UTC" - run: name: Save test results command: | @@ -29,6 +33,8 @@ jobs: steps: - checkout - run: ./gradlew selenium:check + environment: + TZ: "/usr/share/zoneinfo/ETC/UTC" - run: name: Save test results command: | From 73676155c1fb228845d48e611ce1f2e264768dcd Mon Sep 17 00:00:00 2001 From: Richard North Date: Wed, 21 Mar 2018 22:15:28 +0100 Subject: [PATCH 27/34] Add parallel run for jdbc-test --- circle.yml | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/circle.yml b/circle.yml index 17885776817..366862322fc 100644 --- a/circle.yml +++ b/circle.yml @@ -15,10 +15,24 @@ jobs: when: always - store_test_results: path: ~/junit - modules: + modules-no-jdbc-test-no-selenium: steps: - checkout - - run: ./gradlew check -x testcontainers:check -x selenium:check + - run: ./gradlew check -x testcontainers:check -x selenium:check -x modules:jdbc-test + environment: + TZ: "/usr/share/zoneinfo/ETC/UTC" + - run: + name: Save test results + command: | + mkdir -p ~/junit/ + find . -type f -regex ".*/build/test-results/.*xml" -exec cp {} ~/junit/ \; + when: always + - store_test_results: + path: ~/junit + modules-jdbc-test: + steps: + - checkout + - run: ./gradlew jdbc-test:check environment: TZ: "/usr/share/zoneinfo/ETC/UTC" - run: @@ -49,5 +63,6 @@ workflows: test_all: jobs: - core - - modules + - modules-no-jdbc-test-no-selenium + - modules-jdbc-test - selenium From e03ce094548c12dfb53ffab7484b7bc4de1c42ff Mon Sep 17 00:00:00 2001 From: Richard North Date: Wed, 21 Mar 2018 22:17:52 +0100 Subject: [PATCH 28/34] Fix circle.yml --- circle.yml | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/circle.yml b/circle.yml index 366862322fc..e90d4365a9f 100644 --- a/circle.yml +++ b/circle.yml @@ -4,9 +4,10 @@ jobs: core: steps: - checkout - - run: ./gradlew testcontainers:check - environment: - TZ: "/usr/share/zoneinfo/ETC/UTC" + - run: + command: ./gradlew testcontainers:check + environment: + TZ: "/usr/share/zoneinfo/ETC/UTC" - run: name: Save test results command: | @@ -18,9 +19,10 @@ jobs: modules-no-jdbc-test-no-selenium: steps: - checkout - - run: ./gradlew check -x testcontainers:check -x selenium:check -x modules:jdbc-test - environment: - TZ: "/usr/share/zoneinfo/ETC/UTC" + - run: + command: ./gradlew check -x testcontainers:check -x selenium:check -x modules:jdbc-test + environment: + TZ: "/usr/share/zoneinfo/ETC/UTC" - run: name: Save test results command: | @@ -32,9 +34,10 @@ jobs: modules-jdbc-test: steps: - checkout - - run: ./gradlew jdbc-test:check - environment: - TZ: "/usr/share/zoneinfo/ETC/UTC" + - run: + command: ./gradlew jdbc-test:check + environment: + TZ: "/usr/share/zoneinfo/ETC/UTC" - run: name: Save test results command: | @@ -46,9 +49,10 @@ jobs: selenium: steps: - checkout - - run: ./gradlew selenium:check - environment: - TZ: "/usr/share/zoneinfo/ETC/UTC" + - run: + command: ./gradlew selenium:check + environment: + TZ: "/usr/share/zoneinfo/ETC/UTC" - run: name: Save test results command: | From bc5ebbf72e03a7f7c778da0e2bfb4637e57ed915 Mon Sep 17 00:00:00 2001 From: Richard North Date: Wed, 21 Mar 2018 22:20:09 +0100 Subject: [PATCH 29/34] Fix circle.yml --- circle.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/circle.yml b/circle.yml index e90d4365a9f..94a8acdd9d5 100644 --- a/circle.yml +++ b/circle.yml @@ -20,7 +20,7 @@ jobs: steps: - checkout - run: - command: ./gradlew check -x testcontainers:check -x selenium:check -x modules:jdbc-test + command: ./gradlew check -x testcontainers:check -x selenium:check -x jdbc-test:check environment: TZ: "/usr/share/zoneinfo/ETC/UTC" - run: From b0e4477787cec00a7c2d0ba90e5ffea9fd1db252 Mon Sep 17 00:00:00 2001 From: Richard North Date: Wed, 21 Mar 2018 22:33:35 +0100 Subject: [PATCH 30/34] Fix pull tests --- .../java/org/testcontainers/dockerclient/ImagePullTest.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/src/test/java/org/testcontainers/dockerclient/ImagePullTest.java b/core/src/test/java/org/testcontainers/dockerclient/ImagePullTest.java index e29f4f38e8b..9f2714e530e 100644 --- a/core/src/test/java/org/testcontainers/dockerclient/ImagePullTest.java +++ b/core/src/test/java/org/testcontainers/dockerclient/ImagePullTest.java @@ -4,6 +4,7 @@ import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy; @RunWith(Parameterized.class) public class ImagePullTest { @@ -31,7 +32,8 @@ public ImagePullTest(String image) { @Test public void test() { - try (final GenericContainer container = new GenericContainer<>(image)) { + try (final GenericContainer container = new GenericContainer<>(image).withStartupCheckStrategy( + new OneShotStartupCheckStrategy())) { container.start(); // do nothing other than start and stop } From 781656b8b8edda207ae54447abcf3b8f19c5aea4 Mon Sep 17 00:00:00 2001 From: Richard North Date: Thu, 22 Mar 2018 08:08:11 +0100 Subject: [PATCH 31/34] Use ryuk instead of etcd for pull tests --- .../java/org/testcontainers/dockerclient/ImagePullTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/src/test/java/org/testcontainers/dockerclient/ImagePullTest.java b/core/src/test/java/org/testcontainers/dockerclient/ImagePullTest.java index 9f2714e530e..b9be2f322e4 100644 --- a/core/src/test/java/org/testcontainers/dockerclient/ImagePullTest.java +++ b/core/src/test/java/org/testcontainers/dockerclient/ImagePullTest.java @@ -20,9 +20,9 @@ public static String[] parameters() { "gliderlabs/alpine:latest", "gliderlabs/alpine:3.5", "gliderlabs/alpine@sha256:a19aa4a17a525c97e5a90a0c53a9f3329d2dc61b0a14df5447757a865671c085", - "quay.io/coreos/etcd:latest", - "quay.io/coreos/etcd:v3.1", - "quay.io/coreos/etcd@sha256:39a30367cd1f3186d540a063ea0257353c8f81b0d3c920c87c7e0f602bb6197c" + "quay.io/testcontainers/ryuk:latest", + "quay.io/testcontainers/ryuk:0.2.2", + "quay.io/testcontainers/ryuk@sha256:4b606e54c4bba1af4fd814019d342e4664d51e28d3ba2d18d24406edbefd66da" }; } From 30797757d5f9f5a11c2b99dc2590f9e3afcb5507 Mon Sep 17 00:00:00 2001 From: Richard North Date: Thu, 22 Mar 2018 08:12:51 +0100 Subject: [PATCH 32/34] Add startup command to pull tests --- .../java/org/testcontainers/dockerclient/ImagePullTest.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/src/test/java/org/testcontainers/dockerclient/ImagePullTest.java b/core/src/test/java/org/testcontainers/dockerclient/ImagePullTest.java index b9be2f322e4..f06ff40fc40 100644 --- a/core/src/test/java/org/testcontainers/dockerclient/ImagePullTest.java +++ b/core/src/test/java/org/testcontainers/dockerclient/ImagePullTest.java @@ -32,8 +32,9 @@ public ImagePullTest(String image) { @Test public void test() { - try (final GenericContainer container = new GenericContainer<>(image).withStartupCheckStrategy( - new OneShotStartupCheckStrategy())) { + try (final GenericContainer container = new GenericContainer<>(image) + .withCommand("/bin/sh", "-c", "sleep 0") + .withStartupCheckStrategy(new OneShotStartupCheckStrategy())) { container.start(); // do nothing other than start and stop } From 74a753f02ddabda22e62c714736a83c964e80f85 Mon Sep 17 00:00:00 2001 From: Richard North Date: Mon, 26 Mar 2018 20:43:38 +0100 Subject: [PATCH 33/34] Remove unnecessary env vars --- circle.yml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/circle.yml b/circle.yml index 94a8acdd9d5..5c4460f6445 100644 --- a/circle.yml +++ b/circle.yml @@ -6,8 +6,6 @@ jobs: - checkout - run: command: ./gradlew testcontainers:check - environment: - TZ: "/usr/share/zoneinfo/ETC/UTC" - run: name: Save test results command: | @@ -21,8 +19,6 @@ jobs: - checkout - run: command: ./gradlew check -x testcontainers:check -x selenium:check -x jdbc-test:check - environment: - TZ: "/usr/share/zoneinfo/ETC/UTC" - run: name: Save test results command: | @@ -37,6 +33,7 @@ jobs: - run: command: ./gradlew jdbc-test:check environment: + # Oracle JDBC drivers require a timezone to be set TZ: "/usr/share/zoneinfo/ETC/UTC" - run: name: Save test results @@ -51,8 +48,6 @@ jobs: - checkout - run: command: ./gradlew selenium:check - environment: - TZ: "/usr/share/zoneinfo/ETC/UTC" - run: name: Save test results command: | From d69726d4a966d00b373309454d0d42ce55c7aa9b Mon Sep 17 00:00:00 2001 From: Richard North Date: Mon, 26 Mar 2018 21:59:21 +0100 Subject: [PATCH 34/34] Reinstate env for tests that include Oracle --- circle.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/circle.yml b/circle.yml index 5c4460f6445..a793bf8cb6a 100644 --- a/circle.yml +++ b/circle.yml @@ -19,6 +19,9 @@ jobs: - checkout - run: command: ./gradlew check -x testcontainers:check -x selenium:check -x jdbc-test:check + environment: + # Oracle JDBC drivers require a timezone to be set + TZ: "/usr/share/zoneinfo/ETC/UTC" - run: name: Save test results command: |