diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 90ba9f60..e19593aa 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -6,7 +6,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - java: [8, 11, 13] + java: [8, 11, 13, 14] steps: - name: Checkout project uses: actions/checkout@v1 diff --git a/README.md b/README.md index e55337da..bb1a2193 100644 --- a/README.md +++ b/README.md @@ -150,17 +150,21 @@ Since `PostgreSQL 10.0`, there are additional artifacts with `alpine-lite` suffi ### Process [/tmp/embedded-pg/PG-XYZ/bin/initdb, ...] failed -Try to remove `/tmp/embedded-pg/PG-XYZ` directory containing temporary binaries of the embedded postgres database. That should solve the problem. +Check the console output for an `initdb: cannot be run as root` message. If the error is present, try to upgrade to a newer version of the library (1.2.8+), or ensure the build process to be running as a non-root user. + +If the error is not present, try to clean up the `/tmp/embedded-pg/PG-XYZ` directory containing temporary binaries of the embedded database. ### Running tests on Windows does not work -You probably need to install the [Microsoft Visual C++ 2013 Redistributable Package](https://support.microsoft.com/en-us/help/3179560/update-for-visual-c-2013-and-visual-c-redistributable-package). The version 2013 is important, installation of other versions will not help. More detailed is the problem discussed [here](https://github.com/opentable/otj-pg-embedded/issues/65). +You probably need to install [Microsoft Visual C++ 2013 Redistributable Package](https://support.microsoft.com/en-us/help/3179560/update-for-visual-c-2013-and-visual-c-redistributable-package). The version 2013 is important, installation of other versions will not help. More detailed is the problem discussed [here](https://github.com/opentable/otj-pg-embedded/issues/65). + +### Running tests in Docker does not work -### Running tests inside Docker does not work +Running builds inside a Docker container is fully supported, including Alpine Linux. However, PostgreSQL has a restriction the database process must run under a non-root user. Otherwise, the database does not start and fails with an error. -Running build inside Docker is fully supported, including Alpine Linux. But you must keep in mind that the **PostgreSQL database must be run under a non-root user**. Otherwise, the database does not start and fails with an error. +So be sure to use a docker image that uses a non-root user. Or, since version `1.2.8` you can run the docker container with `--privileged` option, which allows taking advantage of `unshare` command to run the database process in a separate namespace. -So be sure to use a docker image that uses a non-root user, or you can use any of the following Dockerfiles to prepare your own image. +Below are some examples of how to prepare a docker image running with a non-root user:
Standard Dockerfile diff --git a/pom.xml b/pom.xml index 16b287c4..7fef61a6 100644 --- a/pom.xml +++ b/pom.xml @@ -109,12 +109,12 @@ org.apache.commons commons-lang3 - 3.6 + 3.10 org.apache.commons commons-compress - 1.19 + 1.20 org.tukaani @@ -124,29 +124,29 @@ commons-io commons-io - 2.6 + 2.7 commons-codec commons-codec - 1.11 + 1.14 org.flywaydb flyway-core - 6.0.8 + 6.5.1 true org.liquibase liquibase-core - 3.6.3 + 4.0.0 true org.postgresql postgresql - 42.2.5 + 42.2.14 junit @@ -158,7 +158,7 @@ org.junit.jupiter junit-jupiter-api - 5.3.2 + 5.6.2 provided true @@ -166,13 +166,13 @@ org.slf4j slf4j-simple - 1.7.25 + 1.7.30 test org.mockito mockito-core - 2.13.0 + 3.4.0 test @@ -181,7 +181,7 @@ maven-pmd-plugin - 3.8 + 3.13.0 verify @@ -194,13 +194,13 @@ net.sourceforge.pmd pmd-core - 5.6.1 + 6.25.0 compile net.sourceforge.pmd pmd-java - 5.6.1 + 6.25.0 compile @@ -216,7 +216,7 @@ org.apache.maven.plugins maven-source-plugin - 3.0.1 + 3.2.1 attach-sources @@ -229,7 +229,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 2.10.4 + 3.2.0 attach-javadocs @@ -263,7 +263,7 @@ org.apache.maven.plugins maven-gpg-plugin - 1.5 + 1.6 sign-artifacts diff --git a/src/main/java/io/zonky/test/db/postgres/embedded/EmbeddedPostgres.java b/src/main/java/io/zonky/test/db/postgres/embedded/EmbeddedPostgres.java index fef7a9f0..cd9aef92 100644 --- a/src/main/java/io/zonky/test/db/postgres/embedded/EmbeddedPostgres.java +++ b/src/main/java/io/zonky/test/db/postgres/embedded/EmbeddedPostgres.java @@ -13,7 +13,6 @@ */ package io.zonky.test.db.postgres.embedded; - import java.io.ByteArrayInputStream; import java.io.Closeable; import java.io.File; @@ -55,7 +54,6 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; -import java.util.stream.Collectors; import javax.sql.DataSource; @@ -72,6 +70,8 @@ import org.slf4j.LoggerFactory; import org.tukaani.xz.XZInputStream; +import io.zonky.test.db.postgres.util.LinuxUtils; + import static java.nio.file.StandardOpenOption.CREATE; import static java.nio.file.StandardOpenOption.WRITE; import static java.util.Collections.unmodifiableMap; @@ -241,12 +241,12 @@ private void initdb() { final StopWatch watch = new StopWatch(); watch.start(); - List command = new ArrayList<>(); - command.addAll(Arrays.asList( - pgBin("initdb"), "-A", "trust", "-U", PG_SUPERUSER, + List args = new ArrayList<>(); + args.addAll(Arrays.asList( + "-A", "trust", "-U", PG_SUPERUSER, "-D", dataDirectory.getPath(), "-E", "UTF-8")); - command.addAll(createLocaleOptions()); - system(command.toArray(new String[command.size()])); + args.addAll(createLocaleOptions()); + system(INIT_DB, args); LOG.info("{} initdb completed in {}", instanceId, watch); } @@ -259,15 +259,11 @@ private void startPostmaster() throws IOException } final List args = new ArrayList<>(); - args.addAll(Arrays.asList( - pgBin("pg_ctl"), - "-D", dataDirectory.getPath(), - "-o", createInitOptions().stream().collect(Collectors.joining(" ")), - "-w", - "start" - )); + args.addAll(Arrays.asList("-D", dataDirectory.getPath())); + args.addAll(createInitOptions()); - final ProcessBuilder builder = new ProcessBuilder(args); + final ProcessBuilder builder = new ProcessBuilder(); + POSTGRES.applyTo(builder, args); builder.redirectErrorStream(true); builder.redirectError(errorRedirector); @@ -275,7 +271,7 @@ private void startPostmaster() throws IOException final Process postmaster = builder.start(); if (outputRedirector.type() == ProcessBuilder.Redirect.Type.PIPE) { - ProcessOutputLogger.logOutput(LOG, postmaster, "pg_ctl"); + ProcessOutputLogger.logOutput(LOG, postmaster, POSTGRES.processName()); } LOG.info("{} postmaster started as {} on port {}. Waiting up to {} for server startup to finish.", instanceId, postmaster.toString(), port, pgStartupWait); @@ -414,7 +410,13 @@ public void close() throws IOException private void pgCtl(File dir, String action) { - system(pgBin("pg_ctl"), "-D", dir.getPath(), action, "-m", PG_STOP_MODE, "-t", PG_STOP_WAIT_S, "-w"); + final List args = new ArrayList<>(); + args.addAll(Arrays.asList( + "-D", dir.getPath(), action, + "-m", PG_STOP_MODE, "-t", + PG_STOP_WAIT_S, "-w" + )); + system(PG_CTL, args); } private void cleanOldDataDirectories(File parentDirectory) @@ -461,12 +463,6 @@ private void cleanOldDataDirectories(File parentDirectory) } } - private String pgBin(String binaryName) - { - final String extension = SystemUtils.IS_OS_WINDOWS ? ".exe" : ""; - return new File(pgDir, "bin/" + binaryName + extension).getPath(); - } - private static File getWorkingDirectory() { final File tempWorkingDirectory = new File(System.getProperty("java.io.tmpdir"), "embedded-pg"); @@ -614,21 +610,23 @@ public int hashCode() { } } - private void system(String... command) + private void system(Command command, List args) { try { - final ProcessBuilder builder = new ProcessBuilder(command); + final ProcessBuilder builder = new ProcessBuilder(); + + command.applyTo(builder, args); builder.redirectErrorStream(true); builder.redirectError(errorRedirector); builder.redirectOutput(outputRedirector); + final Process process = builder.start(); if (outputRedirector.type() == ProcessBuilder.Redirect.Type.PIPE) { - String processName = command[0].replaceAll("^.*[\\\\/](\\w+)(\\.exe)?$", "$1"); - ProcessOutputLogger.logOutput(LOG, process, processName); + ProcessOutputLogger.logOutput(LOG, process, command.processName()); } if (0 != process.waitFor()) { - throw new IllegalStateException(String.format("Process %s failed", Arrays.asList(command))); + throw new IllegalStateException(String.format("Process %s failed", builder.command())); } } catch (final RuntimeException e) { // NOPMD throw e; @@ -841,4 +839,35 @@ public String toString() { return "EmbeddedPG-" + instanceId; } + + private final Command INIT_DB = new Command("initdb"); + private final Command POSTGRES = new Command("postgres"); + private final Command PG_CTL = new Command("pg_ctl"); + + private class Command { + + private final String commandName; + + private Command(String commandName) { + this.commandName = commandName; + } + + public String processName() { + return commandName; + } + + public void applyTo(ProcessBuilder builder, List arguments) { + List command = new ArrayList<>(); + + if (LinuxUtils.isUnshareAvailable()) { + command.addAll(Arrays.asList("unshare", "-U")); + } + + String extension = SystemUtils.IS_OS_WINDOWS ? ".exe" : ""; + command.add(new File(pgDir, "bin/" + commandName + extension).getPath()); + command.addAll(arguments); + + builder.command(command); + } + } } diff --git a/src/main/java/io/zonky/test/db/postgres/util/LinuxUtils.java b/src/main/java/io/zonky/test/db/postgres/util/LinuxUtils.java index 70088693..d7260a0d 100644 --- a/src/main/java/io/zonky/test/db/postgres/util/LinuxUtils.java +++ b/src/main/java/io/zonky/test/db/postgres/util/LinuxUtils.java @@ -22,6 +22,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.lang.reflect.Method; import java.nio.file.Files; import java.nio.file.Path; @@ -33,6 +34,7 @@ public final class LinuxUtils { private static final Logger logger = LoggerFactory.getLogger(LinuxUtils.class); private static final String DISTRIBUTION_NAME = resolveDistributionName(); + private static final boolean UNSHARE_AVAILABLE = unshareAvailable(); private LinuxUtils() {} @@ -40,6 +42,10 @@ public static String getDistributionName() { return DISTRIBUTION_NAME; } + public static boolean isUnshareAvailable() { + return UNSHARE_AVAILABLE; + } + private static String resolveDistributionName() { if (!SystemUtils.IS_OS_LINUX) { return null; @@ -85,4 +91,37 @@ private static String resolveDistributionName() { return null; } } + + private static boolean unshareAvailable() { + if (!SystemUtils.IS_OS_LINUX) { + return false; + } + + try { + Class clazz = Class.forName("com.sun.security.auth.module.UnixSystem"); + Object instance = clazz.getDeclaredConstructor().newInstance(); + Method method = clazz.getDeclaredMethod("getUid"); + int uid = ((Number) method.invoke(instance)).intValue(); + + if (uid != 0) { + return false; + } + + ProcessBuilder builder = new ProcessBuilder(); + builder.command("unshare", "-U", "id", "-u"); + + Process process = builder.start(); + process.waitFor(); + + try (BufferedReader outputReader = new BufferedReader(new InputStreamReader(process.getInputStream(), UTF_8))) { + if (process.exitValue() == 0 && !"0".equals(outputReader.readLine())) { + return true; + } + } + + return false; + } catch (Exception e) { + return false; + } + } }