Recently, I experienced an odd issue starting an Ammonite REPL from a Ubuntu Docker container using a non-root user.
It took me some time to identify and solve the issue and this post will describe the issue and my process to find a solution.
But if you're just interested in a quick solution, here you go:
TLDR - The fix
To fix the issue you must give a name to your newly created non-root user.
For example, if your Dockerfile
has something like:
# adds a new non-root user
RUN useradd 10500 -m
you can change to:
# adds a named non-root user
RUN useradd namedUser -u 10500 -m
This simple change will fix the issue with java.lang.IllegalArgumentException: requirement failed: ? is not an absolute path
exception.
It seems that Ammonite, and sbt as well, are relying on the OS username for some work.
Another alternative is to switch to an Alpine linux image.
Reproducing the issue
The idea here is simple we need to spin up a ubuntu container with JDK 8, download and install Ammonite, and finally set up a user without sudo privileges to be used on the container.
To make the error easily reproducible I'll share with you a Dockerfile
very similar to the one as was used in the occasion, and the necessary commands.
Dockerfile for ammonite console
FROM rightmesh/ubuntu-openjdk:18.04
# installs curl and Ammonite
RUN apt-get update && \
apt-get install -y curl && \
sh -c '(echo "#!/usr/bin/env sh" && \
curl -L https://github.com/lihaoyi/Ammonite/releases/download/2.1.1/2.12-2.1.1) > /usr/local/bin/amm && \
chmod +x /usr/local/bin/amm'
# adds a non-root user
RUN useradd 10500 -m
# start the container with the non-root user
USER 10500
Steps to reproduce the issue
First, we need to build the image using: docker build -t amm-console .
Then we can reproduce the issue by running:
docker run -it --rm amm-console amm
You should get the following exception as result:
Exception in thread "main" java.lang.ExceptionInInitializerError
at ammonite.main.Cli$Config$.apply$default$4(Cli.scala:22)
at ammonite.Main$.main0(Main.scala:294)
at ammonite.Main$.main(Main.scala:275)
at ammonite.Main.main(Main.scala)
Caused by: java.lang.IllegalArgumentException: requirement failed: ? is not an absolute path
at scala.Predef$.require(Predef.scala:281)
at os.Path.<init>(Path.scala:429)
at os.Path$.apply(Path.scala:388)
at os.package$.<init>(package.scala:19)
at os.package$.<clinit>(package.scala)
... 4 more
Identifying the issue
Strategy 1 - Stack trace analysis
After investing some time, with no success, googling for a solution. I decide by investigating the exception to try to find a clue about what's happening.
Caused by: java.lang.IllegalArgumentException: requirement failed: ? is not an absolute path
This error message isn't helpful here, in my opinion, but suggests that something is wrong in a path. Even though we are not explicitly specifying any path.
So I move up on the stack trace until I find this:
at ammonite.main.Cli$Config$.apply$default$4(Cli.scala:22)
So I decided that was time to look on the Ammonite Cli.scala
code, more specifically at line 22, and this is how the Cli.:
object Cli{
//.... more code here
case class Config(predefCode: String = "",
defaultPredef: Boolean = true,
homePredef: Boolean = true,
wd: os.Path = os.pwd, //this is the line 22
//.... more code here
I don't know about you, but at least for me, this was not answering any question so far. And the frustration has started to build on.
Strategy 2 - bypass the problem changing from Ammonite to sbt
Ok, I realize that I didn't have enough information to solve the Ammonite issue by myself, so I started to ask myself:
Do I need to solve the Ammonite issue or do I need to have a Scala REPL working?
Thanks to that question, I decided by replacing Ammonite
by sbt
since I could use sbt console
and have access to a REPL, and that would solve my problem.
So I changed the Dockerfile to this:
FROM rightmesh/ubuntu-openjdk:18.04
RUN apt-get update && \
apt-get install -y curl && \
curl -L -o sbt-1.3.8.deb http://dl.bintray.com/sbt/debian/sbt-1.3.8.deb && \
dpkg -i sbt-1.3.8.deb && \
apt-get install sbt
RUN useradd 10500 -m
USER 10500
I built the image again using: docker build -t amm-console .
And try to run sbt
executing:
docker run -it --rm amm-console sbt
So... it failed again. The bypass strategy didn't work it out, but now I had a different exception with a more helpful error message, as you can see here:
java.io.IOException: failed to create lock file /?/.sbt/boot/sbt.boot.lock
at xsbt.boot.Locks$.liftedTree1$1(Locks.scala:35)
//... more error lines
Caused by: java.io.IOException: No such file or directory
at java.io.UnixFileSystem.createFileExclusively(Native Method)
at java.io.File.createNewFile(File.java:1012)
at xsbt.boot.Locks$.liftedTree1$1(Locks.scala:34)
... 12 more
The breakthrough
Comparing both error messages from Ammonite
and sbt
we can isolate a common element, the mysterious ?
.
# -- Ammonite Error
requirement failed: ? is not an absolute path
# -- sbt Error
failed to create lock file /?/.sbt/boot/sbt.boot.lock
Somehow this question mark is being part of my user folder path, and this only happens for my newly created nonroot user 10500
.
However, when I run eithersbt
or Ammonite
docker containers using the root user
this error never shows up.
So, it must be something related to this newly created user and his home path.
I decided to run the whoami
command for both users root
and 10500
and compare the results. And so I did:
# -- user 10500
docker run -it --rm amm-console whoami
--> whoami: cannot find name for user ID 10500
# -- root user
docker run -it --rm amm-console whoami
--> root
As you can see the whoami
for user 10500 complains about the fact that this user doesn't have a name.
At least for me, there is no strong evidence that this could be linked to the Ammonite
and sbt
error, but I decided to give it a try, and to give a name to my user 10500
.
So, I changed again my Dockerfile
to be like this:
FROM rightmesh/ubuntu-openjdk:18.04
RUN apt-get update && \
apt-get install -y curl && \
curl -L -o sbt-1.3.8.deb http://dl.bintray.com/sbt/debian/sbt-1.3.8.deb && \
dpkg -i sbt-1.3.8.deb && \
apt-get install sbt
RUN useradd namedUser -u 10500 -m
USER 10500
Once more I build the image using: docker build -t amm-console .
And one more time I ran sbt
with:
docker run -it --rm amm-console sbt
And it finally worked 🎉
docker run -it --rm amm-console sbt
[info] [launcher] getting org.scala-sbt sbt 1.3.8 (this may take some time)...
Changing back to Ammonite
Great! Now that it works I can finally move back to Ammonite
and check if it will work with my new namedUser
.
FROM rightmesh/ubuntu-openjdk:18.04
RUN apt-get update && \
apt-get install -y curl && \
sh -c '(echo "#!/usr/bin/env sh" && \
curl -L https://github.com/lihaoyi/Ammonite/releases/download/2.1.1/2.12-2.1.1) > /usr/local/bin/amm && \
chmod +x /usr/local/bin/amm'
RUN useradd namedUser -u 10500 -m
USER 10500
So I need to build the image again using: docker build -t amm-console .
An try to start Ammonite REPL
with:
docker run -it --rm amm-console amm
And...
docker run -it --rm amm-console-fix amm
Loading...
Compiling (synthetic)/ammonite/predef/DefaultPredef.sc
Welcome to the Ammonite Repl 2.1.1 (Scala 2.12.11 Java 1.8.0_181)
@
it finally worked 🎉🎉🎉 and I could move my task to done ✅