Skip to content

Commit e03ff5e

Browse files
authored
Merge pull request #33 from samsrabin/podman-casper
Add fallback for container build on Casper
2 parents 3ab6d06 + bba89e9 commit e03ff5e

2 files changed

Lines changed: 191 additions & 1 deletion

File tree

doc_builder/build_docs.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,36 @@ def run_build_command(build_command, version, options):
249249
start_container_software("docker desktop start")
250250

251251
print(" ".join(build_command))
252-
subprocess.check_call(build_command, env=env)
252+
try:
253+
subprocess.run(build_command, env=env, check=True, capture_output=True)
254+
except subprocess.CalledProcessError as err:
255+
stderr_text = err.stderr.decode("utf-8", errors="replace")
256+
if "failed to chown recursively host path" not in stderr_text:
257+
sys.stdout.write(err.stdout.decode("utf-8", errors="replace"))
258+
sys.stderr.write(err.stderr.decode("utf-8", errors="replace"))
259+
raise
260+
print("Container failed due to missing subuid/subgid mappings.")
261+
print("Retrying without :U mount flag and with --user 0:0...")
262+
build_command = _fix_command_for_missing_subids(build_command)
263+
print(" ".join(build_command))
264+
subprocess.check_call(build_command, env=env)
265+
266+
267+
def _fix_command_for_missing_subids(build_command):
268+
"""Modify a container build command to work without subuid/subgid mappings.
269+
270+
This removes the :U suffix from volume mounts and changes --user to 0:0
271+
(in rootless podman, UID 0 in the container maps to the host user).
272+
"""
273+
fixed = []
274+
for i, arg in enumerate(build_command):
275+
if arg.endswith(":U"):
276+
fixed.append(arg[:-2])
277+
elif i > 0 and build_command[i - 1] == "--user":
278+
fixed.append("0:0")
279+
else:
280+
fixed.append(arg)
281+
return fixed
253282

254283

255284
def setup_for_container():
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
#!/usr/bin/env python3
2+
3+
"""Unit test driver for _fix_command_for_missing_subids and the retry logic in run_build_command"""
4+
5+
import subprocess
6+
import unittest
7+
from argparse import Namespace
8+
from unittest.mock import patch
9+
10+
# pylint: disable=import-error
11+
from doc_builder.build_docs import _fix_command_for_missing_subids, run_build_command
12+
from doc_builder.build_commands import DEFAULT_IMAGE
13+
14+
# Allow names that pylint doesn't like, because otherwise I find it hard
15+
# to make readable unit test names
16+
# pylint: disable=invalid-name
17+
18+
# Build commands to be tested
19+
_BASE_VOLUME_STR = "/path/to/repo:/home/user/mounted_home:U"
20+
_FIXED_VOLUME_STR = "/path/to/repo:/home/user/mounted_home"
21+
_BASE_USER_STR = "37083:1000"
22+
_FIXED_USER_STR = "0:0"
23+
_BASE_COMMAND = [
24+
"podman",
25+
"run",
26+
"--name",
27+
"foo",
28+
"--user",
29+
_BASE_USER_STR,
30+
"-v",
31+
_BASE_VOLUME_STR,
32+
"--workdir",
33+
"/home/user/mounted_home/doc",
34+
"-t",
35+
"--rm",
36+
DEFAULT_IMAGE,
37+
"make",
38+
"html",
39+
]
40+
_FIXED_COMMAND = []
41+
for x in _BASE_COMMAND:
42+
if x == _BASE_VOLUME_STR:
43+
_FIXED_COMMAND.append(_FIXED_VOLUME_STR)
44+
elif x == _BASE_USER_STR:
45+
_FIXED_COMMAND.append(_FIXED_USER_STR)
46+
else:
47+
_FIXED_COMMAND.append(x)
48+
49+
50+
class TestFixCommandForMissingSubids(unittest.TestCase):
51+
"""Test the _fix_command_for_missing_subids function"""
52+
53+
def setUp(self):
54+
self.result = _fix_command_for_missing_subids(list(_BASE_COMMAND))
55+
56+
def test_removes_U_suffix_from_mount(self):
57+
"""The :U suffix should be removed from volume mount arguments"""
58+
self.assertIn("/path/to/repo:/home/user/mounted_home", self.result)
59+
self.assertNotIn("/path/to/repo:/home/user/mounted_home:U", self.result)
60+
61+
def test_changes_user_to_root(self):
62+
"""--user should be changed to 0:0"""
63+
user_idx = self.result.index("--user")
64+
self.assertEqual(self.result[user_idx + 1], _FIXED_USER_STR)
65+
66+
def test_preserves_other_args(self):
67+
"""Arguments that aren't --user or :U mounts should be unchanged"""
68+
self.assertEqual(_FIXED_COMMAND, self.result)
69+
70+
def test_no_U_suffix_only_changes_user(self):
71+
"""A command without :U mount should only change --user"""
72+
command = [
73+
"podman",
74+
"run",
75+
"--user",
76+
_BASE_USER_STR,
77+
"--mount",
78+
"type=bind,source=/path,target=/home/user/mounted_home",
79+
"image:latest",
80+
"make",
81+
"html",
82+
]
83+
result = _fix_command_for_missing_subids(command)
84+
self.assertEqual(result[result.index("--user") + 1], _FIXED_USER_STR)
85+
self.assertIn("type=bind,source=/path,target=/home/user/mounted_home", result)
86+
87+
88+
def _make_options():
89+
"""Create a minimal options Namespace for run_build_command"""
90+
return Namespace(
91+
version_display_name=None,
92+
static_path="_static",
93+
templates_path="_templates",
94+
versions=False,
95+
build_in_container=True,
96+
)
97+
98+
99+
@patch("doc_builder.build_docs.start_container_software")
100+
class TestRunBuildCommandRetry(unittest.TestCase):
101+
"""Test that run_build_command retries with fixed args on chown failure"""
102+
103+
@patch("doc_builder.build_docs.subprocess.check_call")
104+
@patch("doc_builder.build_docs.subprocess.run")
105+
def test_retries_on_chown_error(self, mock_run, mock_check_call, _mock_start):
106+
"""On chown failure, should retry with :U removed and --user 0:0"""
107+
msg = b"Error: failed to chown recursively host path: lchown /some/path: invalid argument"
108+
mock_run.side_effect = subprocess.CalledProcessError(
109+
returncode=126,
110+
cmd=_BASE_COMMAND,
111+
output=b"",
112+
stderr=msg,
113+
)
114+
mock_check_call.return_value = 0
115+
116+
run_build_command(
117+
build_command=list(_BASE_COMMAND),
118+
version="None",
119+
options=_make_options(),
120+
)
121+
122+
mock_check_call.assert_called_once()
123+
retry_cmd = mock_check_call.call_args[0][0]
124+
user_idx = retry_cmd.index("--user")
125+
self.assertEqual(retry_cmd[user_idx + 1], _FIXED_USER_STR)
126+
self.assertNotIn("/path/to/repo:/home/user/mounted_home:U", retry_cmd)
127+
self.assertIn("/path/to/repo:/home/user/mounted_home", retry_cmd)
128+
129+
@patch("doc_builder.build_docs.subprocess.run")
130+
def test_raises_on_other_error(self, mock_run, _mock_start):
131+
"""On non-chown failure, should re-raise the error"""
132+
mock_run.side_effect = subprocess.CalledProcessError(
133+
returncode=1,
134+
cmd=_BASE_COMMAND,
135+
output=b"",
136+
stderr=b"Error: something else went wrong",
137+
)
138+
139+
with self.assertRaises(subprocess.CalledProcessError):
140+
run_build_command(
141+
build_command=list(_BASE_COMMAND),
142+
version="None",
143+
options=_make_options(),
144+
)
145+
146+
@patch("doc_builder.build_docs.subprocess.run")
147+
def test_succeeds_without_retry(self, mock_run, _mock_start):
148+
"""On success, should not retry"""
149+
mock_run.return_value = None
150+
151+
run_build_command(
152+
build_command=list(_BASE_COMMAND),
153+
version="None",
154+
options=_make_options(),
155+
)
156+
157+
mock_run.assert_called_once()
158+
159+
160+
if __name__ == "__main__":
161+
unittest.main()

0 commit comments

Comments
 (0)