Skip to content

Commit 97f7211

Browse files
committed
Polish "Allow running under root on Linux when unshare is available"
1 parent 0fc492c commit 97f7211

File tree

3 files changed

+94
-86
lines changed

3 files changed

+94
-86
lines changed

README.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -150,17 +150,21 @@ Since `PostgreSQL 10.0`, there are additional artifacts with `alpine-lite` suffi
150150

151151
### Process [/tmp/embedded-pg/PG-XYZ/bin/initdb, ...] failed
152152

153-
Try to remove `/tmp/embedded-pg/PG-XYZ` directory containing temporary binaries of the embedded postgres database. That should solve the problem.
153+
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.
154+
155+
If the error is not present, try to clean up the `/tmp/embedded-pg/PG-XYZ` directory containing temporary binaries of the embedded database.
154156

155157
### Running tests on Windows does not work
156158

157-
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).
159+
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).
160+
161+
### Running tests in Docker does not work
158162

159-
### Running tests inside Docker does not work
163+
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.
160164

161-
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.
165+
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.
162166

163-
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.
167+
Below are some examples of how to prepare a docker image running with a non-root user:
164168

165169
<details>
166170
<summary>Standard Dockerfile</summary>

src/main/java/io/zonky/test/db/postgres/embedded/EmbeddedPostgres.java

Lines changed: 55 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,12 @@
1313
*/
1414
package io.zonky.test.db.postgres.embedded;
1515

16-
17-
import java.io.*;
16+
import java.io.ByteArrayInputStream;
17+
import java.io.Closeable;
18+
import java.io.File;
19+
import java.io.FileOutputStream;
20+
import java.io.IOException;
21+
import java.io.InputStream;
1822
import java.net.InetAddress;
1923
import java.net.InetSocketAddress;
2024
import java.net.ServerSocket;
@@ -66,7 +70,8 @@
6670
import org.slf4j.LoggerFactory;
6771
import org.tukaani.xz.XZInputStream;
6872

69-
import static io.zonky.test.db.postgres.util.LinuxUtils.isUnshareUseable;
73+
import io.zonky.test.db.postgres.util.LinuxUtils;
74+
7075
import static java.nio.file.StandardOpenOption.CREATE;
7176
import static java.nio.file.StandardOpenOption.WRITE;
7277
import static java.util.Collections.unmodifiableMap;
@@ -99,7 +104,6 @@ public class EmbeddedPostgres implements Closeable
99104
private volatile FileOutputStream lockStream;
100105
private volatile FileLock lock;
101106
private final boolean cleanDataDirectory;
102-
private static boolean useUnshare;
103107

104108
private final ProcessBuilder.Redirect errorRedirector;
105109
private final ProcessBuilder.Redirect outputRedirector;
@@ -129,8 +133,6 @@ public class EmbeddedPostgres implements Closeable
129133
this.pgStartupWait = pgStartupWait;
130134
Objects.requireNonNull(this.pgStartupWait, "Wait time cannot be null");
131135

132-
useUnshare = isUnshareUseable();
133-
134136
if (parentDirectory != null) {
135137
mkdirs(parentDirectory);
136138
cleanOldDataDirectories(parentDirectory);
@@ -239,12 +241,12 @@ private void initdb()
239241
{
240242
final StopWatch watch = new StopWatch();
241243
watch.start();
242-
List<String> command = new ArrayList<>();
243-
command.addAll(Arrays.asList(
244+
List<String> args = new ArrayList<>();
245+
args.addAll(Arrays.asList(
244246
"-A", "trust", "-U", PG_SUPERUSER,
245247
"-D", dataDirectory.getPath(), "-E", "UTF-8"));
246-
command.addAll(createLocaleOptions());
247-
system(pgBin("initdb"), command);
248+
args.addAll(createLocaleOptions());
249+
system(INIT_DB, args);
248250
LOG.info("{} initdb completed in {}", instanceId, watch);
249251
}
250252

@@ -257,21 +259,19 @@ private void startPostmaster() throws IOException
257259
}
258260

259261
final List<String> args = new ArrayList<>();
260-
args.addAll(pgBin("postgres"));
261-
args.addAll(Arrays.asList(
262-
"-D", dataDirectory.getPath()
263-
));
262+
args.addAll(Arrays.asList("-D", dataDirectory.getPath()));
264263
args.addAll(createInitOptions());
265264

266-
final ProcessBuilder builder = new ProcessBuilder(args);
265+
final ProcessBuilder builder = new ProcessBuilder();
266+
POSTGRES.applyTo(builder, args);
267267

268268
builder.redirectErrorStream(true);
269269
builder.redirectError(errorRedirector);
270270
builder.redirectOutput(outputRedirector);
271271
final Process postmaster = builder.start();
272272

273273
if (outputRedirector.type() == ProcessBuilder.Redirect.Type.PIPE) {
274-
ProcessOutputLogger.logOutput(LOG, postmaster, "postgres");
274+
ProcessOutputLogger.logOutput(LOG, postmaster, POSTGRES.processName());
275275
}
276276

277277
LOG.info("{} postmaster started as {} on port {}. Waiting up to {} for server startup to finish.", instanceId, postmaster.toString(), port, pgStartupWait);
@@ -416,7 +416,7 @@ private void pgCtl(File dir, String action)
416416
"-m", PG_STOP_MODE, "-t",
417417
PG_STOP_WAIT_S, "-w"
418418
));
419-
system(pgBin("pg_ctl"), args);
419+
system(PG_CTL, args);
420420
}
421421

422422
private void cleanOldDataDirectories(File parentDirectory)
@@ -463,19 +463,6 @@ private void cleanOldDataDirectories(File parentDirectory)
463463
}
464464
}
465465

466-
private List<String> pgBin(String binaryName)
467-
{
468-
final List<String> args = new ArrayList<>();
469-
if (useUnshare) {
470-
args.addAll(Arrays.asList(
471-
"unshare", "-U"
472-
));
473-
}
474-
final String extension = SystemUtils.IS_OS_WINDOWS ? ".exe" : "";
475-
args.add(new File(pgDir, "bin/" + binaryName + extension).getPath());
476-
return args;
477-
}
478-
479466
private static File getWorkingDirectory()
480467
{
481468
final File tempWorkingDirectory = new File(System.getProperty("java.io.tmpdir"), "embedded-pg");
@@ -623,24 +610,23 @@ public int hashCode() {
623610
}
624611
}
625612

626-
private void system(List<String> bin, List<String> args)
613+
private void system(Command command, List<String> args)
627614
{
628-
final List<String> command = new ArrayList<>();
629-
command.addAll(bin);
630-
command.addAll(args);
631615
try {
632-
final ProcessBuilder builder = new ProcessBuilder(command);
616+
final ProcessBuilder builder = new ProcessBuilder();
617+
618+
command.applyTo(builder, args);
633619
builder.redirectErrorStream(true);
634620
builder.redirectError(errorRedirector);
635621
builder.redirectOutput(outputRedirector);
622+
636623
final Process process = builder.start();
637624

638625
if (outputRedirector.type() == ProcessBuilder.Redirect.Type.PIPE) {
639-
String processName = bin.get(bin.size() - 1).replaceAll("^.*[\\\\/](\\w+)(\\.exe)?$", "$1");
640-
ProcessOutputLogger.logOutput(LOG, process, processName);
626+
ProcessOutputLogger.logOutput(LOG, process, command.processName());
641627
}
642628
if (0 != process.waitFor()) {
643-
throw new IllegalStateException(String.format("Process %s failed", Arrays.asList(command)));
629+
throw new IllegalStateException(String.format("Process %s failed", builder.command()));
644630
}
645631
} catch (final RuntimeException e) { // NOPMD
646632
throw e;
@@ -853,4 +839,35 @@ public String toString()
853839
{
854840
return "EmbeddedPG-" + instanceId;
855841
}
842+
843+
private final Command INIT_DB = new Command("initdb");
844+
private final Command POSTGRES = new Command("postgres");
845+
private final Command PG_CTL = new Command("pg_ctl");
846+
847+
private class Command {
848+
849+
private final String commandName;
850+
851+
private Command(String commandName) {
852+
this.commandName = commandName;
853+
}
854+
855+
public String processName() {
856+
return commandName;
857+
}
858+
859+
public void applyTo(ProcessBuilder builder, List<String> arguments) {
860+
List<String> command = new ArrayList<>();
861+
862+
if (LinuxUtils.isUnshareAvailable()) {
863+
command.addAll(Arrays.asList("unshare", "-U"));
864+
}
865+
866+
String extension = SystemUtils.IS_OS_WINDOWS ? ".exe" : "";
867+
command.add(new File(pgDir, "bin/" + commandName + extension).getPath());
868+
command.addAll(arguments);
869+
870+
builder.command(command);
871+
}
872+
}
856873
}

src/main/java/io/zonky/test/db/postgres/util/LinuxUtils.java

Lines changed: 30 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,9 @@
2222
import java.io.IOException;
2323
import java.io.InputStream;
2424
import java.io.InputStreamReader;
25-
import java.lang.reflect.InvocationTargetException;
2625
import java.lang.reflect.Method;
2726
import java.nio.file.Files;
2827
import java.nio.file.Path;
29-
import java.util.ArrayList;
30-
import java.util.Arrays;
31-
import java.util.List;
3228

3329
import static java.nio.charset.StandardCharsets.UTF_8;
3430
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
@@ -38,16 +34,17 @@ public final class LinuxUtils {
3834
private static final Logger logger = LoggerFactory.getLogger(LinuxUtils.class);
3935

4036
private static final String DISTRIBUTION_NAME = resolveDistributionName();
41-
42-
private static final boolean UNSHARE_USEABLE = unshareUseable();
37+
private static final boolean UNSHARE_AVAILABLE = unshareAvailable();
4338

4439
private LinuxUtils() {}
4540

4641
public static String getDistributionName() {
4742
return DISTRIBUTION_NAME;
4843
}
4944

50-
public static boolean isUnshareUseable() { return UNSHARE_USEABLE; }
45+
public static boolean isUnshareAvailable() {
46+
return UNSHARE_AVAILABLE;
47+
}
5148

5249
private static String resolveDistributionName() {
5350
if (!SystemUtils.IS_OS_LINUX) {
@@ -95,46 +92,36 @@ private static String resolveDistributionName() {
9592
}
9693
}
9794

98-
private static boolean unshareUseable() {
99-
if (SystemUtils.IS_OS_LINUX) {
100-
int uid;
101-
try {
102-
Class<?> c = Class.forName("com.sun.security.auth.module.UnixSystem");
103-
Object o = c.getDeclaredConstructor().newInstance();
104-
Method method = c.getDeclaredMethod("getUid");
105-
uid = ((Number) method.invoke(o)).intValue();
106-
} catch (ClassNotFoundException | IllegalAccessException | InstantiationException |
107-
NoSuchMethodException | InvocationTargetException e) {
95+
private static boolean unshareAvailable() {
96+
if (!SystemUtils.IS_OS_LINUX) {
97+
return false;
98+
}
99+
100+
try {
101+
Class<?> clazz = Class.forName("com.sun.security.auth.module.UnixSystem");
102+
Object instance = clazz.getDeclaredConstructor().newInstance();
103+
Method method = clazz.getDeclaredMethod("getUid");
104+
int uid = ((Number) method.invoke(instance)).intValue();
105+
106+
if (uid != 0) {
108107
return false;
109108
}
110-
if (uid == 0) {
111-
final List<String> command = new ArrayList<>();
112-
command.addAll(Arrays.asList(
113-
"unshare", "-U",
114-
"id", "-u"
115-
));
116-
final ProcessBuilder builder = new ProcessBuilder(command);
117-
final Process process;
118-
try {
119-
process = builder.start();
120-
} catch (IOException e) {
121-
return false;
122-
}
123-
BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()));
124-
try {
125-
process.waitFor();
126-
} catch (InterruptedException e) {
127-
return false;
128-
}
129-
try {
130-
if (process.exitValue() == 0 && br.readLine() != "0") {
131-
return true;
132-
}
133-
} catch (IOException e) {
134-
return false;
109+
110+
ProcessBuilder builder = new ProcessBuilder();
111+
builder.command("unshare", "-U", "id", "-u");
112+
113+
Process process = builder.start();
114+
process.waitFor();
115+
116+
try (BufferedReader outputReader = new BufferedReader(new InputStreamReader(process.getInputStream(), UTF_8))) {
117+
if (process.exitValue() == 0 && !"0".equals(outputReader.readLine())) {
118+
return true;
135119
}
136120
}
121+
122+
return false;
123+
} catch (Exception e) {
124+
return false;
137125
}
138-
return false;
139126
}
140127
}

0 commit comments

Comments
 (0)