Skip to content

Commit 295565f

Browse files
committed
feat(load-task): support using custom images
1 parent 3f87f71 commit 295565f

File tree

3 files changed

+140
-15
lines changed

3 files changed

+140
-15
lines changed

src/taskgraph/docker.py

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import tarfile
1010
import tempfile
1111
from io import BytesIO
12+
from pathlib import Path
1213
from textwrap import dedent
1314
from typing import Any, Dict, Generator, List, Mapping, Optional, Union
1415

@@ -17,6 +18,7 @@
1718
except ImportError as e:
1819
zstd = e
1920

21+
from taskgraph.config import GraphConfig
2022
from taskgraph.util import docker, json
2123
from taskgraph.util.taskcluster import (
2224
find_task_id,
@@ -353,7 +355,47 @@ def _index(l: List, s: str) -> Optional[int]:
353355
pass
354356

355357

356-
def load_task(task_id: str, remove: bool = True, user: Optional[str] = None) -> int:
358+
def _resolve_image(image: Union[str, Dict[str, str]], graph_config: GraphConfig) -> str:
359+
image_task_id = None
360+
361+
# Standard case, image comes from the task definition.
362+
if isinstance(image, dict):
363+
assert "type" in image
364+
365+
if image["type"] == "task-image":
366+
image_task_id = image["taskId"]
367+
elif image["type"] == "indexed-image":
368+
image_task_id = find_task_id(image["namespace"])
369+
else:
370+
raise Exception(f"Tasks with {image['type']} images are not supported!")
371+
else:
372+
# Check if image refers to an in-tree image under taskcluster/docker,
373+
# if so build it.
374+
image_dir = docker.image_path(image, graph_config)
375+
if Path(image_dir).is_dir():
376+
tag = f"taskcluster/{image}:latest"
377+
build_image(image, tag, graph_config, os.environ)
378+
return tag
379+
380+
# Check if we're referencing a task or index.
381+
if image.startswith("task-id="):
382+
image_task_id = image.split("=", 1)[1]
383+
elif image.startswith("index="):
384+
index = image.split("=", 1)[1]
385+
image_task_id = find_task_id(index)
386+
else:
387+
raise Exception(f"Unable to resolve image '{image}'!")
388+
389+
return load_image_by_task_id(image_task_id)
390+
391+
392+
def load_task(
393+
graph_config: GraphConfig,
394+
task_id: str,
395+
remove: bool = True,
396+
user: Optional[str] = None,
397+
custom_image: Optional[str] = None,
398+
) -> int:
357399
"""Load and run a task interactively in a Docker container.
358400
359401
Downloads the Docker image from a task's artifacts and runs it in an
@@ -362,9 +404,11 @@ def load_task(task_id: str, remove: bool = True, user: Optional[str] = None) ->
362404
the 'exec-task' function provided in the shell.
363405
364406
Args:
407+
graph_config: The graph configuration object.
365408
task_id: The ID of the task to load.
366409
remove: Whether to remove the container after exit (default True).
367410
user: The user to switch to in the container (default 'worker').
411+
custom_image: A custom image to use instead of the task's image.
368412
369413
Returns:
370414
int: The exit code from the Docker container.
@@ -386,6 +430,13 @@ def load_task(task_id: str, remove: bool = True, user: Optional[str] = None) ->
386430
print("Only tasks using `run-task` are supported!")
387431
return 1
388432

433+
try:
434+
image = custom_image or image
435+
image_tag = _resolve_image(image, graph_config)
436+
except Exception as e:
437+
print(e)
438+
return 1
439+
389440
# Remove the payload section of the task's command. This way run-task will
390441
# set up the task (clone repos, download fetches, etc) but won't actually
391442
# start the core of the task. Instead we'll drop the user into an interactive
@@ -414,16 +465,6 @@ def load_task(task_id: str, remove: bool = True, user: Optional[str] = None) ->
414465
else:
415466
task_cwd = "$TASK_WORKDIR"
416467

417-
if image["type"] == "task-image":
418-
image_task_id = image["taskId"]
419-
elif image["type"] == "indexed-image":
420-
image_task_id = find_task_id(image["namespace"])
421-
else:
422-
print(f"Tasks with {image['type']} images are not supported!")
423-
return 1
424-
425-
image_tag = load_image_by_task_id(image_task_id)
426-
427468
# Set some env vars the worker would normally set.
428469
env = {
429470
"RUN_ID": "0",

src/taskgraph/main.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -569,7 +569,7 @@ def show_taskgraph(options):
569569
"--root",
570570
"-r",
571571
default="taskcluster",
572-
help="relative path for the root of the taskgraph definition",
572+
help="Relative path to the root of the Taskgraph definition.",
573573
)
574574
@argument(
575575
"-t", "--tag", help="tag that the image should be built as.", metavar="name:tag"
@@ -683,11 +683,34 @@ def image_digest(args):
683683
help="Keep the docker container after exiting.",
684684
)
685685
@argument("--user", default=None, help="Container user to start shell with.")
686+
@argument(
687+
"--image",
688+
default=None,
689+
help="Use a custom image instead of the task's image. Can be the name of "
690+
"an image under `taskcluster/docker`, a reference to a local docker image, "
691+
"`task-id=<task id>`, or `index=<index path>`.",
692+
)
693+
@argument(
694+
"--root",
695+
"-r",
696+
default="taskcluster",
697+
help="Relative path to the root of the Taskgraph definition.",
698+
)
686699
def load_task(args):
700+
from taskgraph.config import load_graph_config # noqa: PLC0415
687701
from taskgraph.docker import load_task # noqa: PLC0415
688702

689703
validate_docker()
690-
return load_task(args["task_id"], remove=args["remove"], user=args["user"])
704+
705+
root = args["root"]
706+
graph_config = load_graph_config(root)
707+
return load_task(
708+
graph_config,
709+
args["task_id"],
710+
remove=args["remove"],
711+
user=args["user"],
712+
custom_image=args["image"],
713+
)
691714

692715

693716
@command("decision", help="Run the decision task")

test/test_docker.py

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,11 +113,22 @@ def mock_run(*popenargs, check=False, **kwargs):
113113
def run_load_task(mocker):
114114
task_id = "abc"
115115

116-
def inner(task, remove=False):
116+
def inner(task, remove=False, custom_image=None):
117117
proc = mocker.MagicMock()
118118
proc.returncode = 0
119119

120+
graph_config = GraphConfig(
121+
{
122+
"trust-domain": "test-domain",
123+
"docker-image-kind": "docker-image",
124+
},
125+
"test/data/taskcluster",
126+
)
127+
120128
mocks = {
129+
"build_image": mocker.patch.object(
130+
docker, "build_image", return_value=None
131+
),
121132
"get_task_definition": mocker.patch.object(
122133
docker, "get_task_definition", return_value=task
123134
),
@@ -129,7 +140,9 @@ def inner(task, remove=False):
129140
),
130141
}
131142

132-
ret = docker.load_task(task_id, remove=remove)
143+
ret = docker.load_task(
144+
graph_config, task_id, remove=remove, custom_image=custom_image
145+
)
133146
return ret, mocks
134147

135148
return inner
@@ -335,3 +348,51 @@ def test_load_task_with_unsupported_image_type(capsys, run_load_task):
335348

336349
out, _ = capsys.readouterr()
337350
assert "Tasks with unsupported-type images are not supported!" in out
351+
352+
353+
@pytest.fixture
354+
def task():
355+
return {
356+
"payload": {
357+
"command": [
358+
"/usr/bin/run-task",
359+
"--task-cwd=/builds/worker",
360+
"--",
361+
"echo",
362+
"test",
363+
],
364+
"image": {"type": "task-image", "taskId": "abc"},
365+
},
366+
}
367+
368+
369+
def test_load_task_with_custom_image_in_tree(run_load_task, task):
370+
image = "hello-world"
371+
ret, mocks = run_load_task(task, custom_image=image)
372+
assert ret == 0
373+
374+
mocks["build_image"].assert_called_once()
375+
args = mocks["subprocess_run"].call_args[0][0]
376+
tag = args[args.index("-it") + 1]
377+
assert tag == f"taskcluster/{image}:latest"
378+
379+
380+
def test_load_task_with_custom_image_task_id(run_load_task, task):
381+
image = "task-id=abc"
382+
ret, mocks = run_load_task(task, custom_image=image)
383+
assert ret == 0
384+
mocks["load_image_by_task_id"].assert_called_once_with("abc")
385+
386+
387+
def test_load_task_with_custom_image_index(mocker, run_load_task, task):
388+
image = "index=abc"
389+
mocker.patch.object(docker, "find_task_id", return_value="abc")
390+
ret, mocks = run_load_task(task, custom_image=image)
391+
assert ret == 0
392+
mocks["load_image_by_task_id"].assert_called_once_with("abc")
393+
394+
395+
def test_load_task_with_custom_image_unsupported(mocker, run_load_task, task):
396+
image = "unsupported"
397+
ret, mocks = run_load_task(task, custom_image=image)
398+
assert ret == 1

0 commit comments

Comments
 (0)