Skip to content

Commit 52a9400

Browse files
committed
load-task: allow specifying caches on the command line
When debugging a task it can be useful to re-use e.g. the checkout cache between load-task invocations, to not have to clone from scratch each time.
1 parent ee9db51 commit 52a9400

File tree

4 files changed

+78
-15
lines changed

4 files changed

+78
-15
lines changed

src/taskgraph/docker.py

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -188,11 +188,11 @@ def build_image(
188188

189189
output_dir = temp_dir / "out"
190190
output_dir.mkdir()
191-
volumes = {
191+
volumes = [
192192
# TODO write artifacts to tmpdir
193-
str(output_dir): "/workspace/out",
194-
str(image_context): "/workspace/context.tar.gz",
195-
}
193+
(str(output_dir), "/workspace/out"),
194+
(str(image_context), "/workspace/context.tar.gz"),
195+
]
196196

197197
assert label in image_tasks
198198
task = image_tasks[label]
@@ -211,7 +211,7 @@ def build_image(
211211
parent = task.dependencies["parent"][len("docker-image-") :]
212212
parent_tar = temp_dir / "parent.tar"
213213
build_image(graph_config, parent, save_image=str(parent_tar))
214-
volumes[str(parent_tar)] = "/workspace/parent.tar"
214+
volumes.append((str(parent_tar), "/workspace/parent.tar"))
215215

216216
task_def["payload"]["env"]["CHOWN_OUTPUT"] = f"{os.getuid()}:{os.getgid()}"
217217
load_task(
@@ -425,7 +425,7 @@ def load_task(
425425
user: Optional[str] = None,
426426
custom_image: Optional[str] = None,
427427
interactive: Optional[bool] = False,
428-
volumes: Optional[dict[str, str]] = None,
428+
volumes: Optional[list[tuple[str, str]]] = None,
429429
) -> int:
430430
"""Load and run a task interactively in a Docker container.
431431
@@ -523,9 +523,18 @@ def load_task(
523523
env.update(task_def["payload"].get("env", {})) # type: ignore
524524

525525
# run-task expects the worker to mount a volume for each path defined in
526-
# TASKCLUSTER_CACHES, delete them to avoid needing to do the same.
526+
# TASKCLUSTER_CACHES; delete them to avoid needing to do the same, unless
527+
# they're passed in as volumes.
527528
if "TASKCLUSTER_CACHES" in env:
528-
del env["TASKCLUSTER_CACHES"]
529+
if volumes:
530+
caches = env["TASKCLUSTER_CACHES"].split(";")
531+
caches = [cache for cache in caches if any(path == cache for _, path in volumes)]
532+
else:
533+
caches = []
534+
if caches:
535+
env["TASKCLUSTER_CACHES"] = ";".join(caches)
536+
else:
537+
del env["TASKCLUSTER_CACHES"]
529538

530539
envfile = None
531540
initfile = None
@@ -570,7 +579,7 @@ def load_task(
570579
command.extend(["-v", f"{initfile.name}:/builds/worker/.bashrc"])
571580

572581
if volumes:
573-
for k, v in volumes.items():
582+
for k, v in volumes:
574583
command.extend(["-v", f"{k}:{v}"])
575584

576585
command.append(image_tag)

src/taskgraph/main.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -792,6 +792,14 @@ def image_digest(args):
792792
default="taskcluster",
793793
help="Relative path to the root of the Taskgraph definition.",
794794
)
795+
@argument(
796+
"--volume",
797+
"-v",
798+
metavar="HOST_DIR:CONTAINER_DIR",
799+
default=[],
800+
action="append",
801+
help="Mount local path into the container.",
802+
)
795803
def load_task(args):
796804
from taskgraph.config import load_graph_config # noqa: PLC0415
797805
from taskgraph.docker import load_task # noqa: PLC0415
@@ -806,6 +814,15 @@ def load_task(args):
806814
except ValueError:
807815
args["task"] = data # assume it is a taskId
808816

817+
volumes = []
818+
for vol in args["volume"]:
819+
if ":" not in vol:
820+
raise ValueError("Invalid volume specification '{vol}', expected HOST_DIR:CONTAINER_DIR")
821+
k, v = vol.split(":", 1)
822+
if not k or not v:
823+
raise ValueError("Invalid volume specification '{vol}', expected HOST_DIR:CONTAINER_DIR")
824+
volumes.append((k, v))
825+
809826
root = args["root"]
810827
graph_config = load_graph_config(root)
811828
return load_task(
@@ -815,6 +832,7 @@ def load_task(args):
815832
remove=args["remove"],
816833
user=args["user"],
817834
custom_image=args["image"],
835+
volumes=volumes,
818836
)
819837

820838

test/test_docker.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ def test_load_task(run_load_task):
137137
},
138138
}
139139
# Test with custom volumes
140-
volumes = {"/host/path": "/container/path", "/another/host": "/another/container"}
140+
volumes = [("/host/path", "/container/path"), ("/another/host", "/another/container")]
141141
ret, mocks = run_load_task(task, volumes=volumes)
142142
assert ret == 0
143143

@@ -216,11 +216,11 @@ def test_load_task_env_init_and_remove(mocker, run_load_task):
216216
"--",
217217
"echo foo",
218218
],
219-
"env": {"FOO": "BAR", "BAZ": "1", "TASKCLUSTER_CACHES": "path"},
219+
"env": {"FOO": "BAR", "BAZ": "1", "TASKCLUSTER_CACHES": "/path;/cache"},
220220
"image": {"taskId": image_task_id, "type": "task-image"},
221221
},
222222
}
223-
ret, mocks = run_load_task(task, remove=True)
223+
ret, mocks = run_load_task(task, remove=True, volumes=[("/host/path", "/cache")])
224224
assert ret == 0
225225

226226
# NamedTemporaryFile was called twice (once for env, once for init)
@@ -231,7 +231,7 @@ def test_load_task_env_init_and_remove(mocker, run_load_task):
231231
env_lines = written_env_content[0].split("\n")
232232

233233
# Verify written env is expected
234-
assert "TASKCLUSTER_CACHES=path" not in env_lines
234+
assert "TASKCLUSTER_CACHES=/cache" in env_lines
235235
assert "FOO=BAR" in env_lines
236236
assert "BAZ=1" in env_lines
237237

test/test_main.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -417,7 +417,7 @@ def fake_actions_load(_graph_config):
417417

418418

419419
@pytest.fixture
420-
def run_load_task(mocker, monkeypatch):
420+
def run_load_task(mocker, monkeypatch, run_taskgraph):
421421
def inner(args, stdin_data=None):
422422
# Mock the docker module functions
423423
m_validate_docker = mocker.patch("taskgraph.main.validate_docker")
@@ -445,7 +445,7 @@ def inner(args, stdin_data=None):
445445
}
446446

447447
# Run the command
448-
result = taskgraph_main(args)
448+
result = run_taskgraph(args)
449449

450450
return result, mocks
451451

@@ -466,6 +466,7 @@ def test_load_task_command(run_load_task):
466466
remove=True,
467467
user=None,
468468
custom_image=None,
469+
volumes=[],
469470
)
470471

471472
# Test with interactive flag
@@ -479,9 +480,42 @@ def test_load_task_command(run_load_task):
479480
remove=True,
480481
user=None,
481482
custom_image=None,
483+
volumes=[],
482484
)
483485

484486

487+
def test_load_task_command_with_volume(run_load_task):
488+
# Test with correct volume specification
489+
result, mocks = run_load_task(["load-task", "task-id-123", "-v", "/host/path:/builds/worker/checkouts"])
490+
491+
assert result == 0
492+
mocks["validate_docker"].assert_called_once()
493+
mocks["load_graph_config"].assert_called_once_with("taskcluster")
494+
mocks["docker_load_task"].assert_called_once_with(
495+
mocks["graph_config"],
496+
"task-id-123",
497+
interactive=False,
498+
remove=True,
499+
user=None,
500+
custom_image=None,
501+
volumes=[("/host/path", "/builds/worker/checkouts")],
502+
)
503+
504+
# Test with no colon
505+
result, mocks = run_load_task(["load-task", "task-id-123", "-v", "/builds/worker/checkouts"])
506+
assert result == 1
507+
mocks["validate_docker"].assert_called_once()
508+
mocks["load_graph_config"].assert_not_called()
509+
mocks["docker_load_task"].assert_not_called()
510+
511+
# Test with missing container path
512+
result, mocks = run_load_task(["load-task", "task-id-123", "-v", "/host/path:"])
513+
assert result == 1
514+
mocks["validate_docker"].assert_called_once()
515+
mocks["load_graph_config"].assert_not_called()
516+
mocks["docker_load_task"].assert_not_called()
517+
518+
485519
def test_load_task_command_with_stdin(run_load_task):
486520
# Test with JSON task definition from stdin
487521
task_def = {
@@ -503,6 +537,7 @@ def test_load_task_command_with_stdin(run_load_task):
503537
remove=True,
504538
user=None,
505539
custom_image=None,
540+
volumes=[],
506541
)
507542

508543

@@ -520,6 +555,7 @@ def test_load_task_command_with_task_id(run_load_task):
520555
remove=True,
521556
user=None,
522557
custom_image=None,
558+
volumes=[],
523559
)
524560

525561

0 commit comments

Comments
 (0)