Skip to content

Commit 05d9932

Browse files
authored
Set PIP_USER in base image (#437)
Ensure that pip installs to ~/.local by default, instead of /opt/conda. Other minor changes: * Do not pin pip version and always upgrade to latest, as recommended by pip maintainers. f there are any breaking changes in the future, they should be caught by the integration tests. * Install appmode from PyPI * Cleanup custom logo setup * pytest: Make --variant a required parameter * Simplify aiidalab_exec fixture usage * Move test_pip_check to common tests
1 parent c1aeaec commit 05d9932

File tree

9 files changed

+107
-60
lines changed

9 files changed

+107
-60
lines changed

docker-bake.hcl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ target "base-with-services" {
8383
"PGSQL_VERSION" = "${PGSQL_VERSION}"
8484
}
8585
}
86+
# PYTHON_MINOR_VERSION is a Python version string
87+
# without the patch version (e.g. "3.9")
88+
# Used to construct paths to Python site-packages folder.
8689
target "lab" {
8790
inherits = ["lab-meta"]
8891
context = "stack/lab"
@@ -93,6 +96,7 @@ target "lab" {
9396
args = {
9497
"AIIDALAB_VERSION" = "${AIIDALAB_VERSION}"
9598
"AIIDALAB_HOME_VERSION" = "${AIIDALAB_HOME_VERSION}"
99+
"PYTHON_MINOR_VERSION" = join(".", slice(split(".", "${PYTHON_VERSION}"), 0, 2))
96100
}
97101
}
98102
target "full-stack" {

stack/base/Dockerfile

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,7 @@ ARG AIIDA_VERSION
2020
# We pin aiida-core to the exact installed version,
2121
# to prevent accidental upgrade or downgrade, that might
2222
# induce DB migration or break shared dependencies of AiiDAlab Apps.
23-
RUN echo "pip==23.3.1" > /opt/requirements.txt && \
24-
echo "aiida-core==${AIIDA_VERSION}" >> /opt/requirements.txt
23+
RUN echo "aiida-core==${AIIDA_VERSION}" > /opt/requirements.txt
2524

2625
# Install the shared requirements.
2726
RUN mamba install --yes \
@@ -32,11 +31,15 @@ RUN mamba install --yes \
3231
fix-permissions "${CONDA_DIR}" && \
3332
fix-permissions "/home/${NB_USER}"
3433

35-
# Pin shared requirements in the base environemnt.
34+
# Pin shared requirements in the conda base environment.
3635
RUN cat /opt/requirements.txt | xargs -I{} conda config --system --add pinned_packages {}
3736

37+
# Upgrade pip to latest
38+
RUN pip install --upgrade --no-cache-dir pip
3839
# Configure pip to use requirements file as constraints file.
39-
ENV PIP_CONSTRAINT=/opt/requirements.txt
40+
ENV PIP_CONSTRAINT /opt/requirements.txt
41+
# Ensure that pip installs packages to ~/.local by default
42+
ENV PIP_USER 1
4043

4144
# Enable verdi autocompletion.
4245
RUN mkdir -p "${CONDA_DIR}/etc/conda/activate.d" && \

stack/lab/Dockerfile

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -41,20 +41,20 @@ ARG AIIDALAB_HOME_VERSION
4141
RUN git clone https://github.com/aiidalab/aiidalab-home && \
4242
cd aiidalab-home && \
4343
git checkout v"${AIIDALAB_HOME_VERSION}" && \
44-
pip install --quiet --no-cache-dir "./" && \
44+
pip install --no-user --quiet --no-cache-dir "./" && \
4545
fix-permissions "./" && \
4646
fix-permissions "${CONDA_DIR}" && \
4747
fix-permissions "/home/${NB_USER}"
4848

49-
# Install and enable appmode.
50-
RUN git clone https://github.com/oschuett/appmode.git && \
51-
cd appmode && \
52-
git checkout v0.8.0
53-
COPY gears.svg ./appmode/appmode/static/gears.svg
54-
RUN pip install ./appmode --no-cache-dir && \
55-
jupyter nbextension enable --py --sys-prefix appmode && \
49+
# Install and enable appmode, turning Jupyter notebooks to Apps
50+
RUN pip install appmode==0.8.0 --no-cache-dir --no-user
51+
# Enable appmode extension
52+
RUN jupyter nbextension enable --py --sys-prefix appmode && \
5653
jupyter serverextension enable --py --sys-prefix appmode
5754

55+
# Swap appmode icon for AiiDAlab gears icon, shown during app load
56+
COPY --chown=${NB_UID}:${NB_GID} gears.svg ${CONDA_DIR}/share/jupyter/nbextensions/appmode/gears.svg
57+
5858
# Copy start-up scripts for AiiDAlab.
5959
COPY before-notebook.d/* /usr/local/bin/before-notebook.d/
6060

@@ -107,8 +107,5 @@ ENV NOTEBOOK_ARGS \
107107
"--TerminalManager.cull_interval=60"
108108

109109
# Set up the logo of notebook interface
110-
COPY --chown=${NB_UID}:${NB_GID} aiidalab-wide-logo.png /tmp/notebook-logo.png
111-
112-
# The directory location of logo.png is in the `${CONDA_DIR}/lib/python3.9/site-packages/notebook/static/base/images/logo.png`,
113-
# but the python version may change in the future, thus we use the wildcard to match the python version.
114-
RUN mv /tmp/notebook-logo.png ${CONDA_DIR}/lib/python3*/site-packages/notebook/static/base/images/logo.png
110+
ARG PYTHON_MINOR_VERSION
111+
COPY --chown=${NB_UID}:${NB_GID} aiidalab-wide-logo.png ${CONDA_DIR}/lib/python${PYTHON_MINOR_VERSION}/site-packages/notebook/static/base/images/logo.png

tests/conftest.py

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
from requests.exceptions import ConnectionError
88

9+
VARIANTS = ("base", "lab", "base-with-services", "full-stack")
10+
911

1012
def is_responsive(url):
1113
try:
@@ -16,12 +18,20 @@ def is_responsive(url):
1618
return False
1719

1820

21+
def variant_checker(value):
22+
msg = f"Invalid image variant '{value}', must be one of: {VARIANTS}"
23+
if value not in VARIANTS:
24+
raise pytest.UsageError(msg)
25+
return value
26+
27+
1928
def pytest_addoption(parser):
2029
parser.addoption(
2130
"--variant",
2231
action="store",
23-
default="base",
32+
required=True,
2433
help="Variant (image name) of the docker-compose file to use.",
34+
type=variant_checker,
2535
)
2636

2737

@@ -54,14 +64,30 @@ def execute(command, user=None, **kwargs):
5464
command = f"exec -T --user={user} aiidalab {command}"
5565
else:
5666
command = f"exec -T aiidalab {command}"
57-
return docker_compose.execute(command, **kwargs)
67+
out = docker_compose.execute(command, **kwargs)
68+
return out.decode()
5869

5970
return execute
6071

6172

6273
@pytest.fixture
6374
def nb_user(aiidalab_exec):
64-
return aiidalab_exec("bash -c 'echo \"${NB_USER}\"'").decode().strip()
75+
return aiidalab_exec("bash -c 'echo \"${NB_USER}\"'").strip()
76+
77+
78+
@pytest.fixture
79+
def pip_install(aiidalab_exec, nb_user):
80+
"""Temporarily install package via pip"""
81+
package = None
82+
83+
def _pip_install(pkg, **args):
84+
nonlocal package
85+
package = pkg
86+
return aiidalab_exec(f"pip install {pkg}", **args)
87+
88+
yield _pip_install
89+
if package:
90+
aiidalab_exec(f"pip uninstall --yes {package}")
6591

6692

6793
@pytest.fixture(scope="session")

tests/test-base-with-services.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,8 @@
55

66

77
def test_correct_pgsql_version_installed(aiidalab_exec, pgsql_version):
8-
info = json.loads(
9-
aiidalab_exec(
10-
"mamba list -n aiida-core-services --json --full-name postgresql"
11-
).decode()
12-
)[0]
8+
cmd = "mamba list -n aiida-core-services --json --full-name postgresql"
9+
info = json.loads(aiidalab_exec(cmd))[0]
1310
assert info["name"] == "postgresql"
1411
assert parse(info["version"]).major == parse(pgsql_version).major
1512

@@ -18,5 +15,4 @@ def test_rabbitmq_can_start(aiidalab_exec):
1815
"""Test the rabbitmq-server can start, the output should be empty if
1916
the command is successful."""
2017
output = aiidalab_exec("mamba run -n aiida-core-services rabbitmq-server -detached")
21-
22-
assert output == b""
18+
assert output == ""

tests/test-base.py

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,50 +5,61 @@
55
from packaging.version import parse
66

77

8-
@pytest.mark.parametrize("incompatible_version", ["2.3.0"])
9-
def test_prevent_pip_install_of_incompatible_aiida_version(
10-
aiidalab_exec, nb_user, aiida_version, incompatible_version
8+
@pytest.mark.parametrize("pkg_manager", ["pip", "mamba"])
9+
def test_prevent_installation_of_aiida(
10+
aiidalab_exec, nb_user, aiida_version, pkg_manager
1111
):
12-
package_manager = "pip"
12+
"""aiida-core is pinned to the exact version in the container,
13+
test that both pip and mamba refuse to install a different version"""
14+
15+
incompatible_version = "2.3.0"
1316
assert parse(aiida_version) != parse(incompatible_version)
17+
1418
# Expected to succeed, although should be a no-op.
15-
aiidalab_exec(
16-
f"{package_manager} install aiida-core=={aiida_version}", user=nb_user
17-
)
19+
aiidalab_exec(f"{pkg_manager} install aiida-core=={aiida_version}", user=nb_user)
1820
with pytest.raises(Exception):
1921
aiidalab_exec(
20-
f"{package_manager} install aiida-core=={incompatible_version}",
22+
f"{pkg_manager} install aiida-core=={incompatible_version}",
2123
user=nb_user,
2224
)
2325

2426

2527
def test_correct_python_version_installed(aiidalab_exec, python_version):
26-
info = json.loads(aiidalab_exec("mamba list --json --full-name python").decode())[0]
28+
info = json.loads(aiidalab_exec("mamba list --json --full-name python"))[0]
2729
assert info["name"] == "python"
2830
assert parse(info["version"]) == parse(python_version)
2931

3032

3133
def test_create_conda_environment(aiidalab_exec, nb_user):
32-
output = aiidalab_exec("conda create -y -n tmp", user=nb_user).decode().strip()
34+
output = aiidalab_exec("conda create -y -n tmp", user=nb_user).strip()
3335
assert "conda activate tmp" in output
3436
# New conda environments should be created in ~/.conda/envs/
35-
output = aiidalab_exec("conda env list", user=nb_user).decode().strip()
37+
output = aiidalab_exec("conda env list", user=nb_user).strip()
3638
assert f"/home/{nb_user}/.conda/envs/tmp" in output
3739

3840

39-
def test_pip_check(aiidalab_exec):
40-
aiidalab_exec("pip check")
41-
42-
4341
def test_correct_aiida_version_installed(aiidalab_exec, aiida_version):
44-
info = json.loads(
45-
aiidalab_exec("mamba list --json --full-name aiida-core").decode()
46-
)[0]
42+
cmd = "mamba list --json --full-name aiida-core"
43+
info = json.loads(aiidalab_exec(cmd))[0]
4744
assert info["name"] == "aiida-core"
4845
assert parse(info["version"]) == parse(aiida_version)
4946

5047

5148
def test_path_local_pip(aiidalab_exec, nb_user):
5249
"""test that the pip local bin path ~/.local/bin is added to PATH"""
53-
output = aiidalab_exec("bash -c 'echo \"${PATH}\"'", user=nb_user).decode()
50+
output = aiidalab_exec("bash -c 'echo \"${PATH}\"'", user=nb_user)
5451
assert f"/home/{nb_user}/.local/bin" in output
52+
53+
54+
def test_pip_user_install(aiidalab_exec, pip_install, nb_user):
55+
"""Test that pip installs packages to ~/.local/ by default"""
56+
import email
57+
58+
# We use 'tuna' as an example of python-only package without dependencies
59+
pkg = "tuna"
60+
pip_install(pkg)
61+
output = aiidalab_exec(f"pip show {pkg}")
62+
63+
# `pip show` output is in the RFC-compliant email header format
64+
msg = email.message_from_string(output)
65+
assert msg.get("Location").startswith(f"/home/{nb_user}/.local/")

tests/test-common.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,18 @@ def test_notebook_service_available(notebook_service):
99

1010

1111
def test_verdi_status(aiidalab_exec, nb_user):
12-
output = aiidalab_exec("verdi status", user=nb_user).decode().strip()
12+
output = aiidalab_exec("verdi status", user=nb_user).strip()
1313
assert "Connected to RabbitMQ" in output
1414
assert "Daemon is running" in output
1515

1616

1717
def test_ssh_agent_is_running(aiidalab_exec, nb_user):
18-
output = aiidalab_exec("ps aux | grep ssh-agent", user=nb_user).decode().strip()
18+
output = aiidalab_exec("ps aux | grep ssh-agent", user=nb_user).strip()
1919
assert "ssh-agent" in output
2020

2121
# also check only one ssh-agent process is running
2222
assert len(output.splitlines()) == 1
23+
24+
25+
def test_pip_check(aiidalab_exec):
26+
aiidalab_exec("pip check")

tests/test-full-stack.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,10 @@
44
@pytest.fixture(scope="function")
55
def generate_aiidalab_install_output(aiidalab_exec, nb_user):
66
def _generate_aiidalab_install_output(package_name):
7-
output = (
8-
aiidalab_exec(f"aiidalab install --yes --pre {package_name}", user=nb_user)
9-
.decode()
10-
.strip()
11-
)
7+
cmd = f"aiidalab install --yes --pre {package_name}"
8+
output = aiidalab_exec(cmd, user=nb_user).strip()
129

13-
output += aiidalab_exec(f"pip check", user=nb_user).decode().strip()
10+
output += aiidalab_exec("pip check", user=nb_user).strip()
1411

1512
# Uninstall the package to make sure the test is repeatable
1613
app_name = package_name.split("@")[0]

tests/test-lab.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,30 @@
44

55

66
def test_correct_aiidalab_version_installed(aiidalab_exec, aiidalab_version):
7-
info = json.loads(aiidalab_exec("mamba list --json --full-name aiidalab").decode())[
8-
0
9-
]
7+
cmd = "mamba list --json --full-name aiidalab"
8+
info = json.loads(aiidalab_exec(cmd))[0]
109
assert info["name"] == "aiidalab"
1110
assert parse(info["version"]) == parse(aiidalab_version)
1211

1312

1413
def test_correct_aiidalab_home_version_installed(aiidalab_exec, aiidalab_home_version):
15-
info = json.loads(
16-
aiidalab_exec("mamba list --json --full-name aiidalab-home").decode()
17-
)[0]
14+
cmd = "mamba list --json --full-name aiidalab-home"
15+
info = json.loads(aiidalab_exec(cmd))[0]
1816
assert info["name"] == "aiidalab-home"
1917
assert parse(info["version"]) == parse(aiidalab_home_version)
2018

2119

20+
def test_appmode_installed(aiidalab_exec):
21+
"""Test that appmode pip package is installed in correct location"""
22+
import email
23+
24+
output = aiidalab_exec("pip show appmode")
25+
26+
# `pip show` output is in the RFC-compliant email header format
27+
msg = email.message_from_string(output)
28+
assert msg.get("Location").startswith("/opt/conda/lib/python")
29+
30+
2231
@pytest.mark.parametrize("incompatible_version", ["22.7.1"])
2332
def test_prevent_pip_install_of_incompatible_aiidalab_version(
2433
aiidalab_exec,

0 commit comments

Comments
 (0)