Skip to content

Commit dcc7698

Browse files
authored
load-task: allow specifying caches on the command line (#858)
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 4718a91 commit dcc7698

File tree

4 files changed

+91
-15
lines changed

4 files changed

+91
-15
lines changed

src/taskgraph/docker.py

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -189,11 +189,11 @@ def build_image(
189189

190190
output_dir = temp_dir / "out"
191191
output_dir.mkdir()
192-
volumes = {
192+
volumes = [
193193
# TODO write artifacts to tmpdir
194-
str(output_dir): "/workspace/out",
195-
str(image_context): "/workspace/context.tar.gz",
196-
}
194+
(str(output_dir), "/workspace/out"),
195+
(str(image_context), "/workspace/context.tar.gz"),
196+
]
197197

198198
assert label in image_tasks
199199
task = image_tasks[label]
@@ -212,7 +212,7 @@ def build_image(
212212
parent = task.dependencies["parent"][len("docker-image-") :]
213213
parent_tar = temp_dir / "parent.tar"
214214
build_image(graph_config, parent, save_image=str(parent_tar))
215-
volumes[str(parent_tar)] = "/workspace/parent.tar"
215+
volumes.append((str(parent_tar), "/workspace/parent.tar"))
216216

217217
task_def["payload"]["env"]["CHOWN_OUTPUT"] = f"{os.getuid()}:{os.getgid()}"
218218
load_task(
@@ -431,7 +431,7 @@ def load_task(
431431
user: Optional[str] = None,
432432
custom_image: Optional[str] = None,
433433
interactive: Optional[bool] = False,
434-
volumes: Optional[dict[str, str]] = None,
434+
volumes: Optional[list[tuple[str, str]]] = None,
435435
) -> int:
436436
"""Load and run a task interactively in a Docker container.
437437
@@ -530,9 +530,20 @@ def load_task(
530530
env.update(task_def["payload"].get("env", {})) # type: ignore
531531

532532
# run-task expects the worker to mount a volume for each path defined in
533-
# TASKCLUSTER_CACHES, delete them to avoid needing to do the same.
533+
# TASKCLUSTER_CACHES; delete them to avoid needing to do the same, unless
534+
# they're passed in as volumes.
534535
if "TASKCLUSTER_CACHES" in env:
535-
del env["TASKCLUSTER_CACHES"]
536+
if volumes:
537+
caches = env["TASKCLUSTER_CACHES"].split(";")
538+
caches = [
539+
cache for cache in caches if any(path == cache for _, path in volumes)
540+
]
541+
else:
542+
caches = []
543+
if caches:
544+
env["TASKCLUSTER_CACHES"] = ";".join(caches)
545+
else:
546+
del env["TASKCLUSTER_CACHES"]
536547

537548
envfile = None
538549
initfile = None
@@ -577,7 +588,7 @@ def load_task(
577588
command.extend(["-v", f"{initfile.name}:/builds/worker/.bashrc"])
578589

579590
if volumes:
580-
for k, v in volumes.items():
591+
for k, v in volumes:
581592
command.extend(["-v", f"{k}:{v}"])
582593

583594
command.append(image_tag)

src/taskgraph/main.py

Lines changed: 22 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,19 @@ 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(
821+
"Invalid volume specification '{vol}', expected HOST_DIR:CONTAINER_DIR"
822+
)
823+
k, v = vol.split(":", 1)
824+
if not k or not v:
825+
raise ValueError(
826+
"Invalid volume specification '{vol}', expected HOST_DIR:CONTAINER_DIR"
827+
)
828+
volumes.append((k, v))
829+
809830
root = args["root"]
810831
graph_config = load_graph_config(root)
811832
return load_task(
@@ -815,6 +836,7 @@ def load_task(args):
815836
remove=args["remove"],
816837
user=args["user"],
817838
custom_image=args["image"],
839+
volumes=volumes,
818840
)
819841

820842

test/test_docker.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,10 @@ 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 = [
141+
("/host/path", "/container/path"),
142+
("/another/host", "/another/container"),
143+
]
141144
ret, mocks = run_load_task(task, volumes=volumes)
142145
assert ret == 0
143146

@@ -216,11 +219,11 @@ def test_load_task_env_init_and_remove(mocker, run_load_task):
216219
"--",
217220
"echo foo",
218221
],
219-
"env": {"FOO": "BAR", "BAZ": "1", "TASKCLUSTER_CACHES": "path"},
222+
"env": {"FOO": "BAR", "BAZ": "1", "TASKCLUSTER_CACHES": "/path;/cache"},
220223
"image": {"taskId": image_task_id, "type": "task-image"},
221224
},
222225
}
223-
ret, mocks = run_load_task(task, remove=True)
226+
ret, mocks = run_load_task(task, remove=True, volumes=[("/host/path", "/cache")])
224227
assert ret == 0
225228

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

233236
# Verify written env is expected
234-
assert "TASKCLUSTER_CACHES=path" not in env_lines
237+
assert "TASKCLUSTER_CACHES=/cache" in env_lines
235238
assert "FOO=BAR" in env_lines
236239
assert "BAZ=1" in env_lines
237240

test/test_main.py

Lines changed: 42 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,46 @@ 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(
490+
["load-task", "task-id-123", "-v", "/host/path:/builds/worker/checkouts"]
491+
)
492+
493+
assert result == 0
494+
mocks["validate_docker"].assert_called_once()
495+
mocks["load_graph_config"].assert_called_once_with("taskcluster")
496+
mocks["docker_load_task"].assert_called_once_with(
497+
mocks["graph_config"],
498+
"task-id-123",
499+
interactive=False,
500+
remove=True,
501+
user=None,
502+
custom_image=None,
503+
volumes=[("/host/path", "/builds/worker/checkouts")],
504+
)
505+
506+
# Test with no colon
507+
result, mocks = run_load_task(
508+
["load-task", "task-id-123", "-v", "/builds/worker/checkouts"]
509+
)
510+
assert result == 1
511+
mocks["validate_docker"].assert_called_once()
512+
mocks["load_graph_config"].assert_not_called()
513+
mocks["docker_load_task"].assert_not_called()
514+
515+
# Test with missing container path
516+
result, mocks = run_load_task(["load-task", "task-id-123", "-v", "/host/path:"])
517+
assert result == 1
518+
mocks["validate_docker"].assert_called_once()
519+
mocks["load_graph_config"].assert_not_called()
520+
mocks["docker_load_task"].assert_not_called()
521+
522+
485523
def test_load_task_command_with_stdin(run_load_task):
486524
# Test with JSON task definition from stdin
487525
task_def = {
@@ -503,6 +541,7 @@ def test_load_task_command_with_stdin(run_load_task):
503541
remove=True,
504542
user=None,
505543
custom_image=None,
544+
volumes=[],
506545
)
507546

508547

@@ -520,6 +559,7 @@ def test_load_task_command_with_task_id(run_load_task):
520559
remove=True,
521560
user=None,
522561
custom_image=None,
562+
volumes=[],
523563
)
524564

525565

0 commit comments

Comments
 (0)